Files
affiliate_dash/backend/internal/router/router.go
T
yml2213 e293cebaf5 feat(rock): 发货记录表格新增时间与备注列并支持上报失效联动
- 做了什么:
  1. 数据库新增 013_rock_tokens_remark.sql 迁移,rock_tokens 表补充 remark 字段并在 ListTokens 中支持备注模糊检索;
  2. 后端扩展 UpdateTokenRemark、MarkTokenAbnormal 接口,同时优化 ResetMismatchToken 支持 mismatch 与 abnormal 状态的解冻重置;
  3. 前端 RockTokenTable 新增「流转时间」列(入库/绑错/发货/绑定/提货)与「备注」列(TokenRemarkCell 浮窗快捷预设选择与自定义输入);
  4. 快捷选择「已上报」或点击「上报失效」自动将链接标记为 abnormal 作废,从可用库存池中剔除,防止被新订单领走;
  5. 优化操作列与顶部筛选栏:mismatch 增加「上报失效」与警示重置;abnormal 状态明确显示已失效并提供强制恢复;筛选栏新增「已上报失效」Tab。

- 为什么:
  解决绑定错误上报 0868 上游后无法记录备注、且原「解冻重置」容易误将已作废卡密放回可用库存池导致后续订单报错的问题。

- 验证方式:
  1. 后端单元测试 TestRockService_TokenRemarkAndAbnormal 与 TestRock 测试集通过;
  2. make fmt、go vet 通过;
  3. 前端 oxlint 检查通过 (0 errors, 0 warnings),npm run build 生产构建通过。
2026-10-08 20:13:25 +08:00

344 lines
20 KiB
Go

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
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"},
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)
// 乐玩开放平台 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)
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/purchase", h.Rock.PurchaseTokens)
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/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.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
}