package router import ( "fmt" "strings" "time" "affiliate_dash/internal/handler" "affiliate_dash/internal/middleware" "affiliate_dash/internal/model" "affiliate_dash/internal/pkg/applog" "affiliate_dash/internal/pkg/jwt" "affiliate_dash/internal/pkg/openlog" "affiliate_dash/internal/service" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type Handlers struct { Auth *handler.AuthHandler DCAuth *handler.DCAuthHandler Dashboard *handler.DashboardHandler Delivery *handler.DeliveryHandler User *handler.UserHandler Open *handler.OpenV1Handler SourceOpen *handler.OpenHandler Merchant *handler.MerchantHandler Recharge *handler.RechargeHandler JWT *jwt.Manager Tenant *service.TenantService OpenDB *gorm.DB SecretCodec *service.SecretCodec OpenAPIKey string OpenAPISecret string OpenSignSkew int64 OpenAPIDebug bool UploadDir string // CORSAllowedOrigins 跨域来源白名单;为空时使用本地开发默认来源。 CORSAllowedOrigins []string LewanWebhook *handler.LewanWebhookHandler PlatformLewan *handler.PlatformLewanHandler Rock *handler.RockHandler } // defaultCORSOrigins 未配置 CORS_ALLOWED_ORIGINS 时的本地开发默认来源。 var defaultCORSOrigins = []string{ "http://localhost:15173", "http://127.0.0.1:15173", "http://localhost:18080", "http://127.0.0.1:18080", } // corsOrigins 返回生效的 CORS 来源白名单,并拒绝通配符 "*" // (与 AllowCredentials 组合会让浏览器把凭证请求放行给任意站点)。 func corsOrigins(h *Handlers) []string { var origins []string for _, o := range h.CORSAllowedOrigins { o = strings.TrimSpace(o) if o == "" || o == "*" { continue } origins = append(origins, o) } if len(origins) == 0 { return defaultCORSOrigins } return origins } // isPollingEndpoint 判断是否属于高频轮询读接口 func isPollingEndpoint(method, path string) bool { if method != "GET" && method != "HEAD" { return false } if path == "/health" { return true } // 用户侧发货页轮询:绑定结果、订单详情、图片代理 if strings.HasPrefix(path, "/api/delivery/v1/orders/") { return true } if strings.HasPrefix(path, "/api/delivery/v1/image-proxy") { return true } // 商户开放平台轮询:订单详情、发货状态、钱包余额 if strings.HasPrefix(path, "/api/client/v1/orders/") && !strings.HasSuffix(path, "/cancel") { return true } if path == "/api/client/v1/wallet" { return true } // 源头侧查询订单 if strings.HasPrefix(path, "/api/open/v1/orders/") { return true } return false } // requestLogger 结构化请求日志中间件: // 1. 无 ANSI 转义乱码 // 2. 自动补充/透传 X-Request-ID // 3. 高频轮询请求(GET 且 200 成功)降为 DEBUG 级别,避免生产日志刷屏 // 4. 写操作与状态变更(POST/PUT/DELETE)按 INFO 输出 // 5. 错误请求(>=400)输出为 WARN/ERROR func requestLogger() gin.HandlerFunc { return func(c *gin.Context) { reqID := c.GetHeader("X-Request-ID") if reqID == "" { reqID = openlog.GetReqID(c) } if reqID == "" || reqID == "-" { reqID = openlog.EnsureReqID(c) } else { c.Set(openlog.CtxReqID, reqID) } c.Header("X-Request-ID", reqID) start := time.Now() c.Next() latency := time.Since(start) status := c.Writer.Status() method := c.Request.Method path := c.Request.URL.Path ip := c.ClientIP() var extra string if mID := middleware.GetMerchantID(c); mID > 0 { extra = fmt.Sprintf(" merchant_id=%d", mID) } if errStr := c.Errors.String(); errStr != "" { extra += fmt.Sprintf(" err=%q", errStr) } if status >= 500 { applog.Errorf("[HTTP] %d %s %s %v ip=%s req_id=%s%s", status, method, path, latency, ip, reqID, extra) } else if status >= 400 { applog.Warnf("[HTTP] %d %s %s %v ip=%s req_id=%s%s", status, method, path, latency, ip, reqID, extra) } else if isPollingEndpoint(method, path) && latency < 3*time.Second { applog.Debugf("[HTTP] %d %s %s %v ip=%s req_id=%s%s", status, method, path, latency, ip, reqID, extra) } else { applog.Infof("[HTTP] %d %s %s %v ip=%s req_id=%s%s", status, method, path, latency, ip, reqID, extra) } } } func Setup(h *Handlers) *gin.Engine { r := gin.New() r.Use(requestLogger(), gin.Recovery()) r.Use(cors.New(cors.Config{ AllowOrigins: corsOrigins(h), AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-Merchant-ID", "X-App-Key", "X-Api-Key", "X-Timestamp", "X-Nonce", "X-Sign", "X-Request-ID", "X-Poll-Token"}, ExposeHeaders: []string{"Content-Length", "X-Request-ID"}, AllowCredentials: true, })) r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) // 上传的凭证等静态文件(公开访问,文件名不可枚举) if h.UploadDir != "" { r.Static("/uploads", h.UploadDir) } api := r.Group("/api") { api.POST("/auth/login", h.Auth.Login) api.POST("/auth/login/sms-code", h.Auth.SendLoginSMSCode) api.POST("/auth/login/prepare", h.Auth.PrepareLogin) api.POST("/auth/login/complete", h.Auth.CompleteLogin) // DCAuth 2.0.0 客户端协议端点(客户端硬编码 /api/dcauth/authorize)。 if h.DCAuth != nil { api.POST("/dcauth/authorize", h.DCAuth.Authorize) api.GET("/dcauth/challenge/:challengeId/status", h.DCAuth.Status) } // 乐玩开放平台 Webhook 回调 if h.LewanWebhook != nil { api.POST("/upstream/lewan/webhook", h.LewanWebhook.Handle) } // 用户侧公开发货页接口:商户把发货链接给用户,用户只填写游戏 UID。 delivery := api.Group("/delivery/v1") { delivery.GET("/orders/:order_no", h.Delivery.GetOrder) delivery.POST("/orders/:order_no/bind", h.Delivery.Bind) delivery.GET("/orders/:order_no/bind-result", h.Delivery.GetBindResult) delivery.POST("/orders/:order_no/submit", h.Delivery.Submit) delivery.GET("/image-proxy", h.Delivery.ImageProxy) delivery.HEAD("/image-proxy", h.Delivery.ImageProxy) } // 源头侧接口:上游发货平台查询订单并回传发货结果。 sourceOpen := api.Group("/open/v1") sourceOpen.Use(middleware.SourceOpenAuth(middleware.SourceOpenAuthConfig{ DB: h.OpenDB, APIKey: h.OpenAPIKey, APISecret: h.OpenAPISecret, SkewSeconds: h.OpenSignSkew, Debug: h.OpenAPIDebug, })) { sourceOpen.GET("/orders/:order_no", h.SourceOpen.QueryOrder) sourceOpen.POST("/orders/ship-notify", h.SourceOpen.ShipNotify) } // 商户侧接口:每个商户独立 API 客户端 + HMAC 签名。 clientOpen := api.Group("/client/v1") clientOpen.Use(middleware.OpenAuth(middleware.OpenAuthConfig{ DB: h.OpenDB, Codec: h.SecretCodec, SkewSeconds: h.OpenSignSkew, Debug: h.OpenAPIDebug, })) { clientOpen.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireAPIScope("products:read"), h.Open.ListProducts) clientOpen.POST("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:write"), h.Open.CreateOrder) clientOpen.GET("/orders/:order_no", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:read", "shipping:read"), h.Open.QueryOrder) clientOpen.POST("/orders/:order_no/cancel", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:write"), h.Open.CancelOrder) clientOpen.GET("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:read", "shipping:read"), h.Open.GetDeliveryLink) clientOpen.GET("/orders/:order_no/delivery", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:read", "shipping:read"), h.Open.GetDeliveryOrder) clientOpen.POST("/orders/:order_no/delivery/bind", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:write"), h.Open.BindDeliveryOrder) clientOpen.GET("/orders/:order_no/delivery/bind-result", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:read", "shipping:read"), h.Open.GetDeliveryBindResult) clientOpen.POST("/orders/:order_no/delivery/submit", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireAPIScope("orders:write"), h.Open.SubmitDeliveryOrder) clientOpen.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireAPIScope("wallet:read"), h.Open.GetWallet) } // 平台管理端:只依赖 JWT 与角色,刻意不挂 Tenant。 // 平台接口本身跨商户,一旦让它们解析 X-Merchant-ID,管理员哪怕只是残留一个已禁用 // 商户的上下文,Tenant 就会对全部请求返回 403,整个管理端都会被锁死。 platform := api.Group("") platform.Use(middleware.Auth(h.JWT)) platform.Use(middleware.RequireRole(model.RoleAdmin)) { platform.GET("/platform/admins", h.User.ListPlatformAdmins) platform.GET("/platform/merchant-accounts", h.User.ListMerchantAccountGroups) platform.DELETE("/platform/merchant-accounts/:id", h.User.DeleteMerchantAccount) platform.POST("/platform/admins", h.User.CreatePlatformAdmin) platform.PATCH("/platform/admins/:id", h.User.UpdatePlatformAdmin) platform.DELETE("/platform/admins/:id", h.User.DeletePlatformAdmin) // 登录安全:管理端绑定手机号与重置 DCAuth 设备。 platform.POST("/admin/users/:id/phone", h.User.BindPlatformUserPhone) platform.POST("/admin/users/:id/device/reset", h.User.ResetPlatformUserDevice) platform.GET("/platform/merchants", h.Merchant.ListPlatformMerchants) // 不分页的精简商户列表,供筛选下拉使用(分页接口的 size 上限是 100) platform.GET("/platform/merchant-options", h.Merchant.ListMerchantOptions) // 跨商户订单视图:商户后台的 /api/merchant/orders 强制单商户隔离, // 超管需要一条平台视角入口来查看全部下游订单。 platform.GET("/platform/orders", h.Merchant.ListPlatformOrders) platform.POST("/platform/merchants", h.Merchant.CreateMerchant) platform.PATCH("/platform/merchants/:id", h.Merchant.UpdateMerchantSettings) platform.GET("/platform/merchants/:id/members", h.Merchant.ListPlatformMerchantMembers) platform.POST("/platform/merchants/:id/members", h.Merchant.AddPlatformMerchantMember) platform.PATCH("/platform/merchants/:id/members/:mid", h.Merchant.UpdatePlatformMerchantMember) platform.DELETE("/platform/merchants/:id/members/:mid", h.Merchant.DeletePlatformMerchantMember) platform.GET("/platform/merchants/:id/roles", h.Merchant.ListPlatformMerchantRoles) platform.GET("/platform/product-catalog", h.Merchant.ListProductCatalog) platform.GET("/platform/merchants/:id/products", h.Merchant.ListMerchantProductsByAdmin) platform.PATCH("/platform/merchants/:id/products/:pid", h.Merchant.AdminUpdateMerchantProduct) platform.POST("/platform/merchants/:id/products/assign", h.Merchant.AssignProducts) platform.GET("/platform/merchants/:id/wallet", h.Merchant.AdminGetWallet) platform.GET("/platform/merchants/:id/account", h.Merchant.AdminGetMerchantAccount) platform.PATCH("/platform/merchants/:id/account", h.Merchant.AdminUpdateMerchantAccount) platform.GET("/platform/merchants/:id/wallet/ledger", h.Merchant.AdminListWalletLedger) platform.POST("/platform/merchants/:id/wallet/adjust", h.Merchant.AdminAdjustWallet) platform.GET("/platform/recharge/applications", h.Recharge.AdminListApplications) platform.POST("/platform/recharge/applications/:id/review", h.Recharge.AdminReviewApplication) // 乐玩平台商品拉取、同步与履约路由配置 if h.PlatformLewan != nil { platform.GET("/platform/lewan/products", h.PlatformLewan.ListLewanProducts) platform.POST("/platform/lewan/sync-catalog", h.PlatformLewan.SyncLewanProduct) platform.POST("/platform/lewan/batch-sync-catalog", h.PlatformLewan.BatchSyncLewanProducts) platform.PUT("/platform/products/:id/routing", h.PlatformLewan.UpdateProductRouting) } // Rock平台 (0666.pro) 自动发货与库存风控管理 if h.Rock != nil { platform.GET("/platform/rock/config", h.Rock.GetConfig) platform.PUT("/platform/rock/config", h.Rock.UpdateConfig) platform.POST("/platform/rock/test-conn", h.Rock.TestConnection) platform.GET("/platform/rock/stats", h.Rock.GetStats) platform.POST("/platform/rock/dashboard/refresh", h.Rock.RefreshDashboard) platform.POST("/platform/rock/purchase", h.Rock.PurchaseTokens) platform.GET("/platform/rock/purchase-logs", h.Rock.ListPurchaseLogs) platform.GET("/platform/rock/purchase-logs/:id", h.Rock.GetPurchaseLog) platform.POST("/platform/rock/upstream-packages/import-all", h.Rock.ImportAllUpstreamPackages) platform.GET("/platform/rock/tokens", h.Rock.ListTokens) platform.POST("/platform/rock/tokens/:id/expected-uid", h.Rock.CorrectTokenUID) platform.POST("/platform/rock/tokens/:id/reset", h.Rock.ResetToken) platform.POST("/platform/rock/tokens/:id/release", h.Rock.ReleaseToken) platform.POST("/platform/rock/tokens/:id/refresh-binding", h.Rock.RefreshBinding) platform.GET("/platform/rock/tokens/:id/package-link", h.Rock.TokenPackageLink) platform.GET("/platform/rock/token-batches/preview", h.Rock.PreviewTokenDelete) platform.POST("/platform/rock/token-batches/batch-delete", h.Rock.BatchDeleteTokens) platform.POST("/platform/rock/token-batches/delete-all", h.Rock.DeleteAllTokens) platform.GET("/platform/rock/token-batches/deleted", h.Rock.ListDeletedTokens) platform.POST("/platform/rock/token-batches/restore", h.Rock.RestoreTokens) platform.POST("/platform/rock/tokens/:id/remark", h.Rock.UpdateRemark) platform.POST("/platform/rock/tokens/:id/abnormal", h.Rock.MarkAbnormal) } } auth := api.Group("") auth.Use(middleware.Auth(h.JWT)) auth.Use(middleware.Tenant(h.Tenant)) { auth.GET("/auth/profile", h.Auth.Profile) auth.PUT("/auth/account", h.Auth.UpdateCurrentAccount) auth.PUT("/auth/password", h.Auth.ChangePassword) auth.GET("/dashboard", h.Dashboard.Dashboard) // 商户后台:商户就是平台下游客户,成员只代表该商户内部员工。 merchant := auth.Group("/merchant") { merchant.GET("", h.Merchant.Current) merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.ListProducts) merchant.PATCH("/products/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.UpdateProduct) merchant.PATCH("/products/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.UpdateProductStatus) merchant.DELETE("/products/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.DeleteProduct) merchant.GET("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListOrders) merchant.GET("/orders/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListManualOrderProducts) merchant.POST("/orders/manual", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CreateManualOrder) merchant.POST("/orders/test", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CreateTestOrder) merchant.GET("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.GetDeliveryLink) merchant.POST("/orders/:order_no/delivery-link/revoke", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.RevokeDeliveryLink) merchant.POST("/orders/:order_no/delivery-link/restore", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.RestoreDeliveryLink) merchant.POST("/orders/:order_no/cancel", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.CancelOrder) merchant.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionWalletView), h.Merchant.GetWallet) merchant.GET("/wallet/ledger", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionWalletLedger), h.Merchant.ListWalletLedger) merchant.POST("/recharge/applications", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.CreateApplication) merchant.GET("/recharge/applications", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionWalletView), h.Recharge.ListApplications) merchant.GET("/recharge/alert-config", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.GetAlertConfig) merchant.PUT("/recharge/alert-config", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.SaveAlertConfig) merchant.GET("/alert-channels", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.ListAlertChannels) merchant.POST("/alert-channels", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.CreateAlertChannel) merchant.PUT("/alert-channels/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.UpdateAlertChannel) merchant.DELETE("/alert-channels/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.DeleteAlertChannel) merchant.POST("/alert-channels/:id/test", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionRechargeManage), h.Recharge.TestAlertChannel) merchant.GET("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionAPIManage), h.Merchant.ListAPIClients) merchant.POST("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionAPIManage), h.Merchant.CreateAPIClient) merchant.PATCH("/api-clients/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionAPIManage), h.Merchant.UpdateAPIClientStatus) merchant.DELETE("/api-clients/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionAPIManage), h.Merchant.DeleteAPIClient) merchant.GET("/callbacks", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureCallbacks), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionCallbacksManage), h.Merchant.ListCallbacks) merchant.POST("/callbacks", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureCallbacks), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionCallbacksManage), h.Merchant.CreateCallback) merchant.GET("/members", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.ListMembers) merchant.POST("/members", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.AddCurrentMerchantMember) merchant.PATCH("/members/:id", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.UpdateCurrentMerchantMember) merchant.DELETE("/members/:id", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.RemoveCurrentMerchantMember) merchant.POST("/members/:id/phone", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.BindCurrentMerchantMemberPhone) merchant.POST("/members/:id/device/reset", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.ResetCurrentMerchantMemberDevice) merchant.GET("/roles", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.ListRoles) merchant.POST("/roles", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.CreateRole) merchant.PATCH("/roles/:id", middleware.RequireMerchantPermissions(h.Tenant, model.PermissionMembersManage), h.Merchant.UpdateRole) } auth.POST("/upload", h.Recharge.Upload) } } return r }