408 lines
18 KiB
Go
408 lines
18 KiB
Go
package router
|
||
|
||
import (
|
||
"context"
|
||
|
||
"soul-api/internal/config"
|
||
"soul-api/internal/database"
|
||
"soul-api/internal/handler"
|
||
"soul-api/internal/middleware"
|
||
"soul-api/internal/redis"
|
||
|
||
"github.com/gin-contrib/cors"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// Setup 创建并配置 Gin 引擎,路径与 app/api 一致
|
||
func Setup(cfg *config.Config) *gin.Engine {
|
||
gin.SetMode(cfg.Mode)
|
||
r := gin.New()
|
||
r.Use(gin.Recovery())
|
||
r.Use(gin.Logger())
|
||
_ = r.SetTrustedProxies(cfg.TrustedProxies)
|
||
|
||
r.Use(middleware.Secure())
|
||
r.Use(cors.New(cors.Config{
|
||
AllowOrigins: cfg.CORSOrigins,
|
||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||
AllowCredentials: true,
|
||
MaxAge: 86400,
|
||
}))
|
||
rateLimiter := middleware.NewRateLimiter(100, 200)
|
||
r.Use(rateLimiter.Middleware())
|
||
|
||
uploadDir := cfg.UploadDir
|
||
if uploadDir == "" {
|
||
uploadDir = "./uploads"
|
||
}
|
||
r.Static("/uploads", uploadDir)
|
||
|
||
api := r.Group("/api")
|
||
{
|
||
// ----- 管理端 -----
|
||
api.GET("/admin", handler.AdminCheck)
|
||
api.POST("/admin", handler.AdminLogin)
|
||
api.POST("/admin/logout", handler.AdminLogout)
|
||
|
||
admin := api.Group("/admin")
|
||
admin.Use(middleware.AdminAuth())
|
||
{
|
||
admin.GET("/chapters", handler.AdminChaptersList)
|
||
admin.POST("/chapters", handler.AdminChaptersAction)
|
||
admin.PUT("/chapters", handler.AdminChaptersAction)
|
||
admin.DELETE("/chapters", handler.AdminChaptersAction)
|
||
admin.GET("/content", handler.AdminContent)
|
||
admin.POST("/content", handler.AdminContent)
|
||
admin.PUT("/content", handler.AdminContent)
|
||
admin.DELETE("/content", handler.AdminContent)
|
||
admin.GET("/dashboard/stats", handler.AdminDashboardStats)
|
||
admin.GET("/dashboard/recent-orders", handler.AdminDashboardRecentOrders)
|
||
admin.GET("/dashboard/new-users", handler.AdminDashboardNewUsers)
|
||
admin.GET("/dashboard/overview", handler.AdminDashboardOverview)
|
||
admin.GET("/dashboard/merchant-balance", handler.AdminDashboardMerchantBalance)
|
||
admin.GET("/distribution/overview", handler.AdminDistributionOverview)
|
||
admin.GET("/payment", handler.AdminPayment)
|
||
admin.POST("/payment", handler.AdminPayment)
|
||
admin.PUT("/payment", handler.AdminPayment)
|
||
admin.DELETE("/payment", handler.AdminPayment)
|
||
admin.GET("/referral", handler.AdminReferral)
|
||
admin.POST("/referral", handler.AdminReferral)
|
||
admin.PUT("/referral", handler.AdminReferral)
|
||
admin.DELETE("/referral", handler.AdminReferral)
|
||
admin.GET("/withdrawals", handler.AdminWithdrawalsList)
|
||
admin.PUT("/withdrawals", handler.AdminWithdrawalsAction)
|
||
admin.POST("/withdrawals/sync", handler.AdminWithdrawalsSync)
|
||
admin.GET("/withdraw-test", handler.AdminWithdrawTest)
|
||
admin.POST("/withdraw-test", handler.AdminWithdrawTest)
|
||
admin.GET("/settings", handler.AdminSettingsGet)
|
||
admin.POST("/settings", handler.AdminSettingsPost)
|
||
admin.GET("/linked-miniprograms", handler.AdminLinkedMpList)
|
||
admin.POST("/linked-miniprograms", handler.AdminLinkedMpCreate)
|
||
admin.PUT("/linked-miniprograms", handler.AdminLinkedMpUpdate)
|
||
admin.DELETE("/linked-miniprograms/:id", handler.AdminLinkedMpDelete)
|
||
admin.GET("/referral-settings", handler.AdminReferralSettingsGet)
|
||
admin.POST("/referral-settings", handler.AdminReferralSettingsPost)
|
||
// 存客宝开放 API 辅助接口:设备列表(供链接人与事选择设备)
|
||
admin.GET("/ckb/devices", handler.AdminCKBDevices)
|
||
admin.GET("/author-settings", handler.AdminAuthorSettingsGet)
|
||
admin.POST("/author-settings", handler.AdminAuthorSettingsPost)
|
||
admin.GET("/shensheshou/query", handler.AdminShensheShouQuery)
|
||
admin.POST("/shensheshou/enrich", handler.AdminShensheShouEnrich)
|
||
admin.POST("/shensheshou/ingest", handler.AdminShensheShouIngest)
|
||
admin.PUT("/orders/refund", handler.AdminOrderRefund)
|
||
admin.GET("/users/:id/balance", handler.AdminUserBalanceGet)
|
||
admin.POST("/users/:id/balance/adjust", handler.AdminUserBalanceAdjust)
|
||
admin.GET("/users", handler.AdminUsersList)
|
||
admin.POST("/users", handler.AdminUsersAction)
|
||
admin.PUT("/users", handler.AdminUsersAction)
|
||
admin.DELETE("/users", handler.AdminUsersAction)
|
||
admin.GET("/orders", handler.OrdersList)
|
||
admin.GET("/gift-pay-requests", handler.AdminGiftPayRequestsList)
|
||
admin.GET("/user/track", handler.UserTrackGet)
|
||
admin.GET("/track/stats", handler.AdminTrackStats)
|
||
}
|
||
|
||
// ----- 鉴权 -----
|
||
api.POST("/auth/login", handler.AuthLogin)
|
||
api.POST("/auth/reset-password", handler.AuthResetPassword)
|
||
|
||
// ----- 书籍/章节(只读,写操作由 /api/db/book 管理端路由承担) -----
|
||
api.GET("/book/all-chapters", handler.BookAllChapters)
|
||
api.GET("/book/parts", handler.BookParts)
|
||
api.GET("/book/chapters-by-part", handler.BookChaptersByPart)
|
||
api.GET("/book/chapter/:id", handler.BookChapterByID)
|
||
api.GET("/book/chapter/by-mid/:mid", handler.BookChapterByMID)
|
||
api.GET("/book/chapters", handler.BookChapters)
|
||
// POST/PUT/DELETE /api/book/chapters 已移除:写操作须由管理端 /api/db/book(AdminAuth)完成
|
||
api.GET("/book/hot", handler.BookHot)
|
||
api.GET("/book/recommended", handler.BookRecommended)
|
||
api.GET("/book/latest-chapters", handler.BookLatestChapters)
|
||
api.GET("/book/search", handler.BookSearch)
|
||
api.GET("/book/stats", handler.BookStats)
|
||
api.GET("/book/sync", handler.BookSync)
|
||
api.POST("/book/sync", handler.BookSync)
|
||
|
||
// ----- CKB -----
|
||
api.POST("/ckb/join", handler.CKBJoin)
|
||
api.POST("/ckb/match", handler.CKBMatch)
|
||
api.GET("/ckb/sync", handler.CKBSync)
|
||
api.POST("/ckb/sync", handler.CKBSync)
|
||
|
||
// ----- 配置 -----
|
||
api.GET("/config", handler.GetConfig)
|
||
// 小程序用:GET /api/db/config 返回 freeChapters、prices(不鉴权,先于 db 组匹配)
|
||
api.GET("/db/config", handler.GetPublicDBConfig)
|
||
|
||
// ----- 内容 -----
|
||
api.GET("/content", handler.ContentGet)
|
||
|
||
// ----- 定时任务(须携带 X-Cron-Secret 请求头,与 .env CRON_SECRET 一致) -----
|
||
cron := api.Group("/cron")
|
||
cron.Use(middleware.CronAuth())
|
||
{
|
||
cron.GET("/sync-orders", handler.CronSyncOrders)
|
||
cron.POST("/sync-orders", handler.CronSyncOrders)
|
||
cron.GET("/unbind-expired", handler.CronUnbindExpired)
|
||
cron.POST("/unbind-expired", handler.CronUnbindExpired)
|
||
}
|
||
|
||
// ----- 数据库(管理端) -----
|
||
db := api.Group("/db")
|
||
db.Use(middleware.AdminAuth())
|
||
{
|
||
db.GET("/book", handler.DBBookAction)
|
||
db.POST("/book", handler.DBBookAction)
|
||
db.PUT("/book", handler.DBBookAction)
|
||
db.DELETE("/book", handler.DBBookDelete)
|
||
db.GET("/chapters", handler.DBChapters)
|
||
db.POST("/chapters", handler.DBChapters)
|
||
db.GET("/config/full", handler.DBConfigGet) // 管理端拉全量配置;GET /api/db/config 已用于公开接口 GetPublicDBConfig
|
||
db.POST("/config", handler.DBConfigPost)
|
||
db.DELETE("/config", handler.DBConfigDelete)
|
||
db.GET("/distribution", handler.DBDistribution)
|
||
db.GET("/init", handler.DBInitGet)
|
||
db.POST("/init", handler.DBInit)
|
||
db.GET("/migrate", handler.DBMigrateGet)
|
||
db.POST("/migrate", handler.DBMigratePost)
|
||
db.GET("/users", handler.DBUsersList)
|
||
db.POST("/users", handler.DBUsersAction)
|
||
db.PUT("/users", handler.DBUsersAction)
|
||
db.DELETE("/users", handler.DBUsersDelete)
|
||
db.GET("/users/referrals", handler.DBUsersReferrals)
|
||
db.GET("/users/rfm", handler.DBUsersRFM)
|
||
db.GET("/users/journey-stats", handler.DBUsersJourneyStats)
|
||
db.GET("/vip-roles", handler.DBVipRolesList)
|
||
db.POST("/vip-roles", handler.DBVipRolesAction)
|
||
db.PUT("/vip-roles", handler.DBVipRolesAction)
|
||
db.DELETE("/vip-roles", handler.DBVipRolesAction)
|
||
db.GET("/vip-members", handler.DBVipMembersList)
|
||
db.GET("/match-records", handler.DBMatchRecordsList)
|
||
db.GET("/match-pool-counts", handler.DBMatchPoolCounts)
|
||
db.GET("/mentors", handler.DBMentorsList)
|
||
db.POST("/mentors", handler.DBMentorsAction)
|
||
db.PUT("/mentors", handler.DBMentorsAction)
|
||
db.DELETE("/mentors", handler.DBMentorsAction)
|
||
db.GET("/mentor-consultations", handler.DBMentorConsultationsList)
|
||
db.GET("/persons", handler.DBPersonList)
|
||
db.GET("/person", handler.DBPersonDetail)
|
||
db.POST("/persons", handler.DBPersonSave)
|
||
db.DELETE("/persons", handler.DBPersonDelete)
|
||
db.GET("/link-tags", handler.DBLinkTagList)
|
||
db.POST("/link-tags", handler.DBLinkTagSave)
|
||
db.DELETE("/link-tags", handler.DBLinkTagDelete)
|
||
db.GET("/ckb-leads", handler.DBCKBLeadList)
|
||
db.GET("/ckb-plan-stats", handler.CKBPlanStats)
|
||
db.GET("/user-rules", handler.DBUserRulesList)
|
||
db.POST("/user-rules", handler.DBUserRulesAction)
|
||
db.PUT("/user-rules", handler.DBUserRulesAction)
|
||
db.DELETE("/user-rules", handler.DBUserRulesAction)
|
||
}
|
||
|
||
// ----- 分销 -----
|
||
api.GET("/distribution", handler.DistributionGet)
|
||
api.POST("/distribution", handler.DistributionGet)
|
||
api.PUT("/distribution", handler.DistributionGet)
|
||
api.GET("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||
api.POST("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||
api.DELETE("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||
api.GET("/distribution/messages", handler.DistributionMessages)
|
||
api.POST("/distribution/messages", handler.DistributionMessages)
|
||
|
||
// ----- 文档生成 -----
|
||
api.POST("/documentation/generate", handler.DocGenerate)
|
||
|
||
// ----- 找伙伴 -----
|
||
api.GET("/match/config", handler.MatchConfigGet)
|
||
api.POST("/match/config", handler.MatchConfigPost)
|
||
api.POST("/match/users", handler.MatchUsers)
|
||
|
||
// ----- 菜单 -----
|
||
api.GET("/menu", handler.MenuGet)
|
||
|
||
// /api/orders 已移入 admin 组(需鉴权),见下方
|
||
|
||
// ----- 支付 -----
|
||
api.POST("/payment/alipay/notify", handler.PaymentAlipayNotify)
|
||
api.POST("/payment/callback", handler.PaymentCallback)
|
||
api.POST("/payment/create-order", handler.PaymentCreateOrder)
|
||
api.GET("/payment/methods", handler.PaymentMethods)
|
||
api.GET("/payment/query", handler.PaymentQuery)
|
||
api.GET("/payment/status/:orderSn", handler.PaymentStatusOrderSn)
|
||
api.POST("/payment/verify", handler.PaymentVerify)
|
||
api.POST("/payment/wechat/notify", handler.PaymentWechatNotify)
|
||
api.GET("/payment/wechat/transfer/notify", handler.PaymentWechatTransferNotify)
|
||
api.POST("/payment/wechat/transfer/notify", handler.PaymentWechatTransferNotify)
|
||
|
||
// ----- 推荐 -----
|
||
api.POST("/referral/bind", handler.ReferralBind)
|
||
api.GET("/referral/data", handler.ReferralData)
|
||
api.POST("/referral/visit", handler.ReferralVisit)
|
||
|
||
// ----- 搜索 -----
|
||
api.GET("/search", handler.SearchGet)
|
||
|
||
// ----- 同步 -----
|
||
api.GET("/sync", handler.SyncGet)
|
||
api.POST("/sync", handler.SyncPost)
|
||
api.PUT("/sync", handler.SyncPut)
|
||
|
||
// ----- 上传 -----
|
||
api.POST("/upload", handler.UploadPost)
|
||
api.DELETE("/upload", handler.UploadDelete)
|
||
|
||
// ----- 用户 -----
|
||
api.GET("/user/addresses", handler.UserAddressesGet)
|
||
api.POST("/user/addresses", handler.UserAddressesPost)
|
||
api.GET("/user/addresses/:id", handler.UserAddressesByID)
|
||
api.PUT("/user/addresses/:id", handler.UserAddressesByID)
|
||
api.DELETE("/user/addresses/:id", handler.UserAddressesByID)
|
||
api.GET("/user/check-purchased", handler.UserCheckPurchased)
|
||
api.GET("/user/profile", handler.UserProfileGet)
|
||
api.POST("/user/profile", handler.UserProfilePost)
|
||
api.GET("/user/purchase-status", handler.UserPurchaseStatus)
|
||
api.GET("/user/reading-progress", handler.UserReadingProgressGet)
|
||
api.POST("/user/reading-progress", handler.UserReadingProgressPost)
|
||
api.GET("/user/track", handler.UserTrackGet)
|
||
api.POST("/user/track", handler.UserTrackPost)
|
||
api.POST("/user/update", handler.UserUpdate)
|
||
|
||
// ----- 微信登录 -----
|
||
api.POST("/wechat/login", handler.WechatLogin)
|
||
api.POST("/wechat/phone-login", handler.WechatPhoneLogin)
|
||
|
||
// ----- 小程序组(所有小程序端接口统一在 /api/miniprogram 下) -----
|
||
miniprogram := api.Group("/miniprogram")
|
||
{
|
||
miniprogram.GET("/config", handler.GetPublicDBConfig)
|
||
miniprogram.POST("/login", handler.MiniprogramLogin)
|
||
miniprogram.POST("/phone-login", handler.WechatPhoneLogin)
|
||
miniprogram.POST("/dev/login-as", handler.MiniprogramDevLoginAs) // 开发专用:按 userId 切换账号
|
||
miniprogram.POST("/phone", handler.MiniprogramPhone)
|
||
miniprogram.GET("/pay", handler.MiniprogramPay)
|
||
miniprogram.POST("/pay", handler.MiniprogramPay)
|
||
miniprogram.POST("/pay/notify", handler.MiniprogramPayNotify) // 微信支付回调,URL 需在商户平台配置
|
||
miniprogram.POST("/qrcode", handler.MiniprogramQrcode)
|
||
miniprogram.GET("/qrcode/image", handler.MiniprogramQrcodeImage)
|
||
miniprogram.GET("/book/all-chapters", handler.BookAllChapters)
|
||
miniprogram.GET("/book/parts", handler.BookParts)
|
||
miniprogram.GET("/book/chapters-by-part", handler.BookChaptersByPart)
|
||
miniprogram.GET("/book/chapter/:id", handler.BookChapterByID)
|
||
miniprogram.GET("/book/chapter/by-mid/:mid", handler.BookChapterByMID)
|
||
miniprogram.GET("/book/hot", handler.BookHot)
|
||
miniprogram.GET("/book/recommended", handler.BookRecommended)
|
||
miniprogram.GET("/book/latest-chapters", handler.BookLatestChapters)
|
||
miniprogram.GET("/book/search", handler.BookSearch)
|
||
miniprogram.GET("/book/stats", handler.BookStats)
|
||
miniprogram.POST("/referral/visit", handler.ReferralVisit)
|
||
miniprogram.POST("/referral/bind", handler.ReferralBind)
|
||
miniprogram.GET("/referral/data", handler.ReferralData)
|
||
miniprogram.GET("/earnings", handler.MyEarnings)
|
||
miniprogram.GET("/match/config", handler.MatchConfigGet)
|
||
miniprogram.POST("/match/users", handler.MatchUsers)
|
||
miniprogram.POST("/ckb/join", handler.CKBJoin)
|
||
miniprogram.POST("/ckb/match", handler.CKBMatch)
|
||
miniprogram.POST("/ckb/lead", handler.CKBLead)
|
||
miniprogram.POST("/ckb/index-lead", handler.CKBIndexLead)
|
||
miniprogram.POST("/upload", handler.UploadPost)
|
||
miniprogram.DELETE("/upload", handler.UploadDelete)
|
||
miniprogram.GET("/user/addresses", handler.UserAddressesGet)
|
||
miniprogram.POST("/user/addresses", handler.UserAddressesPost)
|
||
miniprogram.GET("/user/addresses/:id", handler.UserAddressesByID)
|
||
miniprogram.PUT("/user/addresses/:id", handler.UserAddressesByID)
|
||
miniprogram.DELETE("/user/addresses/:id", handler.UserAddressesByID)
|
||
miniprogram.GET("/user/check-purchased", handler.UserCheckPurchased)
|
||
miniprogram.GET("/user/dashboard-stats", handler.UserDashboardStats)
|
||
miniprogram.GET("/user/profile", handler.UserProfileGet)
|
||
miniprogram.POST("/user/profile", handler.UserProfilePost)
|
||
miniprogram.GET("/user/purchase-status", handler.UserPurchaseStatus)
|
||
miniprogram.GET("/user/reading-progress", handler.UserReadingProgressGet)
|
||
miniprogram.POST("/user/reading-progress", handler.UserReadingProgressPost)
|
||
miniprogram.POST("/user/update", handler.UserUpdate)
|
||
miniprogram.POST("/withdraw", handler.WithdrawPost)
|
||
miniprogram.GET("/withdraw/records", handler.WithdrawRecords)
|
||
miniprogram.GET("/withdraw/pending-confirm", handler.WithdrawPendingConfirm)
|
||
miniprogram.POST("/withdraw/confirm-received", handler.WithdrawConfirmReceived)
|
||
miniprogram.GET("/withdraw/confirm-info", handler.WithdrawConfirmInfo)
|
||
// VIP 接口(小程序专用,按使用方区分路径)
|
||
miniprogram.GET("/vip/status", handler.VipStatus)
|
||
miniprogram.GET("/vip/profile", handler.VipProfileGet)
|
||
miniprogram.POST("/vip/profile", handler.VipProfilePost)
|
||
miniprogram.GET("/vip/members", handler.VipMembers)
|
||
// 用户列表/单个(首页超级个体、会员详情回退)
|
||
miniprogram.GET("/users", handler.MiniprogramUsers)
|
||
miniprogram.GET("/orders", handler.MiniprogramOrders)
|
||
// 导师(stitch_soul)
|
||
miniprogram.GET("/mentors", handler.MiniprogramMentorsList)
|
||
miniprogram.GET("/mentors/:id", handler.MiniprogramMentorsDetail)
|
||
miniprogram.POST("/mentors/:id/book", handler.MiniprogramMentorsBook)
|
||
miniprogram.GET("/about/author", handler.MiniprogramAboutAuthor)
|
||
// 埋点
|
||
miniprogram.POST("/track", handler.MiniprogramTrackPost)
|
||
// 规则引擎(用户旅程引导)
|
||
miniprogram.GET("/user-rules", handler.MiniprogramUserRulesGet)
|
||
// 余额
|
||
miniprogram.GET("/balance", handler.BalanceGet)
|
||
miniprogram.GET("/balance/transactions", handler.BalanceTransactionsGet)
|
||
miniprogram.POST("/balance/recharge", handler.BalanceRechargePost)
|
||
miniprogram.POST("/balance/recharge/confirm", handler.BalanceRechargeConfirmPost)
|
||
miniprogram.POST("/balance/refund", handler.BalanceRefundPost)
|
||
miniprogram.POST("/balance/consume", handler.BalanceConsumePost)
|
||
miniprogram.GET("/gift/link", handler.GiftLinkGet)
|
||
// 代付(美团式:代付页面)
|
||
miniprogram.POST("/gift-pay/create", handler.GiftPayCreate)
|
||
miniprogram.GET("/gift-pay/detail", handler.GiftPayDetail)
|
||
miniprogram.POST("/gift-pay/pay", handler.GiftPayPay)
|
||
miniprogram.POST("/gift-pay/cancel", handler.GiftPayCancel)
|
||
miniprogram.GET("/gift-pay/my-requests", handler.GiftPayMyRequests)
|
||
miniprogram.GET("/gift-pay/my-payments", handler.GiftPayMyPayments)
|
||
}
|
||
|
||
// ----- 提现 -----
|
||
api.POST("/withdraw", handler.WithdrawPost)
|
||
api.GET("/withdraw/records", handler.WithdrawRecords)
|
||
api.GET("/withdraw/pending-confirm", handler.WithdrawPendingConfirm)
|
||
// 提现测试(固定用户 1 元,无需 admin 鉴权,仅用于脚本/本地调试)
|
||
api.GET("/withdraw-test", handler.AdminWithdrawTest)
|
||
api.POST("/withdraw-test", handler.AdminWithdrawTest)
|
||
|
||
// ----- 提现 V3(独立实现,依文档 提现功能完整技术文档.md) -----
|
||
api.POST("/v3/withdraw/initiate", handler.WithdrawV3Initiate)
|
||
api.POST("/v3/withdraw/notify", handler.WithdrawV3Notify)
|
||
api.POST("/v3/withdraw/query", handler.WithdrawV3Query)
|
||
}
|
||
|
||
// 根路径不返回任何页面(仅 204)
|
||
r.GET("/", func(c *gin.Context) {
|
||
c.Status(204)
|
||
})
|
||
|
||
// 健康检查:返回状态、版本号、数据库与 Redis 连接状态
|
||
r.GET("/health", func(c *gin.Context) {
|
||
dbStatus := "ok"
|
||
if sqlDB, err := database.DB().DB(); err != nil {
|
||
dbStatus = "error"
|
||
} else if err := sqlDB.Ping(); err != nil {
|
||
dbStatus = "disconnected"
|
||
}
|
||
|
||
redisStatus := "disabled"
|
||
if redis.Client() != nil {
|
||
if err := redis.Client().Ping(context.Background()).Err(); err != nil {
|
||
redisStatus = "disconnected"
|
||
} else {
|
||
redisStatus = "ok"
|
||
}
|
||
}
|
||
|
||
c.JSON(200, gin.H{
|
||
"status": "ok",
|
||
"version": cfg.Version,
|
||
"database": dbStatus,
|
||
"redis": redisStatus,
|
||
})
|
||
})
|
||
|
||
return r
|
||
}
|