feat: support merchant team roles and permissions
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
-- 商户自定义员工角色。role code 仅在所属商户内唯一,权限以逗号分隔保存。
|
||||
CREATE TABLE IF NOT EXISTS merchant_roles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
merchant_id BIGINT NOT NULL,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
permissions TEXT NOT NULL DEFAULT '',
|
||||
status BIGINT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_merchant_role_code
|
||||
ON merchant_roles (merchant_id, code);
|
||||
CREATE INDEX IF NOT EXISTS idx_merchant_roles_merchant_id
|
||||
ON merchant_roles (merchant_id);
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 为已有商户补齐可编辑的默认团队角色。负责人仍是系统保留角色,不落库。
|
||||
INSERT INTO merchant_roles (merchant_id, code, name, permissions, status)
|
||||
SELECT merchants.id, roles.code, roles.name, roles.permissions, 1
|
||||
FROM merchants
|
||||
CROSS JOIN (
|
||||
VALUES
|
||||
('operator', '运营', 'api:manage,callbacks:manage,orders:manage,products:manage,wallet:view'),
|
||||
('finance', '财务', 'recharge:manage,wallet:ledger,wallet:view'),
|
||||
('support', '客服', 'orders:manage')
|
||||
) AS roles(code, name, permissions)
|
||||
ON CONFLICT (merchant_id, code) DO NOTHING;
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -38,11 +39,21 @@ func (h *MerchantHandler) Current(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{
|
||||
"merchant": merchant,
|
||||
"role": middleware.GetMerchantRole(c),
|
||||
"merchant": merchant,
|
||||
"role": middleware.GetMerchantRole(c),
|
||||
"permissions": mapKeys(middleware.GetMerchantPermissions(c)),
|
||||
})
|
||||
}
|
||||
|
||||
func mapKeys(values map[string]struct{}) []string {
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (h *MerchantHandler) ListProducts(c *gin.Context) {
|
||||
page, size := pageParams(c)
|
||||
list, total, err := h.merchantSvc.ListMerchantProducts(middleware.GetMerchantID(c), page, size, false)
|
||||
@@ -398,7 +409,10 @@ func (h *MerchantHandler) ListMembers(c *gin.Context) {
|
||||
}
|
||||
|
||||
type addMemberReq struct {
|
||||
UserID uint `json:"user_id" binding:"required"`
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Nickname string `json:"nickname"`
|
||||
Role string `json:"role" binding:"required"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
@@ -406,11 +420,14 @@ type addMemberReq struct {
|
||||
func (h *MerchantHandler) AddCurrentMerchantMember(c *gin.Context) {
|
||||
var req addMemberReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误:user_id 与 role 必填")
|
||||
response.BadRequest(c, "参数错误:角色必填")
|
||||
return
|
||||
}
|
||||
member, err := h.merchantSvc.AddMember(middleware.GetMerchantID(c), service.AddMemberInput{
|
||||
UserID: req.UserID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Nickname: req.Nickname,
|
||||
Role: req.Role,
|
||||
IsDefault: req.IsDefault,
|
||||
}, middleware.GetUserID(c))
|
||||
@@ -421,6 +438,54 @@ func (h *MerchantHandler) AddCurrentMerchantMember(c *gin.Context) {
|
||||
response.OK(c, member)
|
||||
}
|
||||
|
||||
func (h *MerchantHandler) ListRoles(c *gin.Context) {
|
||||
roles, err := h.merchantSvc.ListRoles(middleware.GetMerchantID(c))
|
||||
if err != nil {
|
||||
response.ServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, roles)
|
||||
}
|
||||
|
||||
type createMerchantRoleReq struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
func (h *MerchantHandler) CreateRole(c *gin.Context) {
|
||||
var req createMerchantRoleReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误:角色编码和名称必填")
|
||||
return
|
||||
}
|
||||
role, err := h.merchantSvc.CreateRole(middleware.GetMerchantID(c), service.MerchantRoleInput{
|
||||
Code: req.Code, Name: req.Name, Permissions: req.Permissions,
|
||||
}, middleware.GetUserID(c))
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, role)
|
||||
}
|
||||
|
||||
func (h *MerchantHandler) UpdateRole(c *gin.Context) {
|
||||
roleID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
var req createMerchantRoleReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误:角色名称必填")
|
||||
return
|
||||
}
|
||||
role, err := h.merchantSvc.UpdateRole(middleware.GetMerchantID(c), uint(roleID), service.MerchantRoleInput{
|
||||
Name: req.Name, Permissions: req.Permissions,
|
||||
}, middleware.GetUserID(c))
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, role)
|
||||
}
|
||||
|
||||
func (h *MerchantHandler) ListPlatformMerchants(c *gin.Context) {
|
||||
page, size := pageParams(c)
|
||||
list, total, err := h.merchantSvc.ListMerchants(page, size)
|
||||
@@ -595,6 +660,9 @@ func (h *MerchantHandler) AddPlatformMerchantMember(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
member, err := h.merchantSvc.AddMember(uint(id), service.AddMemberInput{
|
||||
UserID: req.UserID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Nickname: req.Nickname,
|
||||
Role: req.Role,
|
||||
IsDefault: req.IsDefault,
|
||||
}, middleware.GetUserID(c))
|
||||
|
||||
@@ -19,10 +19,50 @@ func NewDashboardHandler(svc *service.FulfillmentService) *DashboardHandler {
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) Dashboard(c *gin.Context) {
|
||||
stats, err := h.svc.Dashboard(middleware.GetMerchantID(c), middleware.GetRole(c) == model.RoleAdmin)
|
||||
isAdmin := middleware.GetRole(c) == model.RoleAdmin
|
||||
stats, err := h.svc.Dashboard(middleware.GetMerchantID(c), isAdmin)
|
||||
if err != nil {
|
||||
response.ServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
if !isAdmin {
|
||||
permissions := middleware.GetMerchantPermissions(c)
|
||||
if _, ok := permissions[model.PermissionProductsManage]; !ok {
|
||||
stats.CatalogProductCount = 0
|
||||
stats.ProductCount = 0
|
||||
stats.ActiveProductCount = 0
|
||||
}
|
||||
if _, ok := permissions[model.PermissionMembersManage]; !ok {
|
||||
stats.UserCount = 0
|
||||
}
|
||||
if _, ok := permissions[model.PermissionOrdersManage]; !ok {
|
||||
stats.OrderCount = 0
|
||||
stats.TodayOrderCount = 0
|
||||
stats.PaidOrderCount = 0
|
||||
stats.DeliveringOrderCount = 0
|
||||
stats.DeliveredOrderCount = 0
|
||||
stats.ShipFailedOrderCount = 0
|
||||
stats.CancelledOrderCount = 0
|
||||
}
|
||||
if _, ok := permissions[model.PermissionWalletView]; !ok {
|
||||
stats.TotalSales = 0
|
||||
stats.TodaySales = 0
|
||||
stats.TotalFees = 0
|
||||
stats.TodayFees = 0
|
||||
}
|
||||
if _, ok := permissions[model.PermissionWalletView]; !ok {
|
||||
stats.WalletAvailableBalance = 0
|
||||
stats.WalletFrozenBalance = 0
|
||||
}
|
||||
if _, ok := permissions[model.PermissionAPIManage]; !ok {
|
||||
stats.APIClientCount = 0
|
||||
stats.ActiveAPIClientCount = 0
|
||||
}
|
||||
if _, ok := permissions[model.PermissionCallbacksManage]; !ok {
|
||||
stats.CallbackSubscriptionCount = 0
|
||||
stats.PendingCallbackCount = 0
|
||||
stats.FailedCallbackCount = 0
|
||||
}
|
||||
}
|
||||
response.OK(c, stats)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
"affiliate_dash/internal/pkg/jwt"
|
||||
"affiliate_dash/internal/pkg/response"
|
||||
"affiliate_dash/internal/service"
|
||||
@@ -11,10 +12,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
CtxUserID = "user_id"
|
||||
CtxUsername = "username"
|
||||
CtxRole = "role"
|
||||
CtxMerchantRole = "merchant_role"
|
||||
CtxUserID = "user_id"
|
||||
CtxUsername = "username"
|
||||
CtxRole = "role"
|
||||
CtxMerchantRole = "merchant_role"
|
||||
CtxMerchantPermissions = "merchant_permissions"
|
||||
)
|
||||
|
||||
func Auth(jm *jwt.Manager) gin.HandlerFunc {
|
||||
@@ -77,7 +79,22 @@ func GetRole(c *gin.Context) string {
|
||||
// 未指定时选择该账号的默认商户,确保旧后台继续落到“自营商户”。
|
||||
func Tenant(tenantSvc *service.TenantService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
member, err := tenantSvc.ResolveMember(GetUserID(c), c.GetHeader("X-Merchant-ID"))
|
||||
merchantRef := c.GetHeader("X-Merchant-ID")
|
||||
if GetRole(c) == model.RoleAdmin && merchantRef != "" {
|
||||
merchant, err := tenantSvc.ResolveMerchantForAdmin(merchantRef)
|
||||
if err != nil {
|
||||
response.Forbidden(c, err.Error())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(CtxMerchantID, merchant.ID)
|
||||
c.Set(CtxMerchantRole, model.MemberRoleOwner)
|
||||
permissions, _ := tenantSvc.MerchantRolePermissions(merchant.ID, model.MemberRoleOwner)
|
||||
c.Set(CtxMerchantPermissions, permissions)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
member, err := tenantSvc.ResolveMember(GetUserID(c), merchantRef)
|
||||
if err != nil {
|
||||
response.Forbidden(c, err.Error())
|
||||
c.Abort()
|
||||
@@ -85,6 +102,13 @@ func Tenant(tenantSvc *service.TenantService) gin.HandlerFunc {
|
||||
}
|
||||
c.Set(CtxMerchantID, member.MerchantID)
|
||||
c.Set(CtxMerchantRole, member.Role)
|
||||
permissions, err := tenantSvc.MerchantRolePermissions(member.MerchantID, member.Role)
|
||||
if err != nil {
|
||||
response.Forbidden(c, err.Error())
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Set(CtxMerchantPermissions, permissions)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -106,6 +130,28 @@ func RequireMerchantRole(roles ...string) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// RequireMerchantPermissions 按当前商户的角色权限校验。保留角色通过兼容权限集计算,
|
||||
// 自定义角色从 merchant_roles 读取,所有列出的权限均需具备。
|
||||
func RequireMerchantPermissions(tenantSvc *service.TenantService, permissions ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
granted := GetMerchantPermissions(c)
|
||||
for _, permission := range permissions {
|
||||
if _, ok := granted[permission]; !ok {
|
||||
response.Forbidden(c, "商户权限不足")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func GetMerchantPermissions(c *gin.Context) map[string]struct{} {
|
||||
value, _ := c.Get(CtxMerchantPermissions)
|
||||
permissions, _ := value.(map[string]struct{})
|
||||
return permissions
|
||||
}
|
||||
|
||||
func GetMerchantRole(c *gin.Context) string {
|
||||
value, _ := c.Get(CtxMerchantRole)
|
||||
role, _ := value.(string)
|
||||
|
||||
@@ -25,6 +25,15 @@ const (
|
||||
MemberRoleFinance = "finance"
|
||||
MemberRoleViewer = "viewer"
|
||||
|
||||
PermissionMembersManage = "members:manage"
|
||||
PermissionProductsManage = "products:manage"
|
||||
PermissionOrdersManage = "orders:manage"
|
||||
PermissionWalletView = "wallet:view"
|
||||
PermissionWalletLedger = "wallet:ledger"
|
||||
PermissionRechargeManage = "recharge:manage"
|
||||
PermissionAPIManage = "api:manage"
|
||||
PermissionCallbacksManage = "callbacks:manage"
|
||||
|
||||
FeeTypeRate = "rate" // 手续费按百分比
|
||||
FeeTypeFixed = "fixed" // 手续费按固定金额
|
||||
|
||||
@@ -104,6 +113,18 @@ type MerchantMember struct {
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// MerchantRole 是商户内可配置的员工角色;负责人仍是系统保留角色。
|
||||
type MerchantRole struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
MerchantID uint `gorm:"not null;uniqueIndex:idx_merchant_role_code;index" json:"merchant_id"`
|
||||
Code string `gorm:"size:64;not null;uniqueIndex:idx_merchant_role_code" json:"code"`
|
||||
Name string `gorm:"size:64;not null" json:"name"`
|
||||
Permissions string `gorm:"type:text;not null;default:''" json:"permissions"`
|
||||
Status int `gorm:"not null;default:1" json:"status"`
|
||||
}
|
||||
|
||||
// APIClient 为一个商户的外部系统集成凭证。SecretCiphertext 仅保存加密后的密钥。
|
||||
type APIClient struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
|
||||
@@ -152,33 +152,36 @@ func Setup(h *Handlers) *gin.Engine {
|
||||
merchant := auth.Group("/merchant")
|
||||
{
|
||||
merchant.GET("", h.Merchant.Current)
|
||||
merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), h.Merchant.ListProducts)
|
||||
merchant.PATCH("/products/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.UpdateProductStatus)
|
||||
merchant.GET("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), h.Merchant.ListOrders)
|
||||
merchant.POST("/orders/manual", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateManualOrder)
|
||||
merchant.POST("/orders/test", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateTestOrder)
|
||||
merchant.GET("/orders/:order_no/delivery-link", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.GetDeliveryLink)
|
||||
merchant.POST("/orders/:order_no/delivery-link/revoke", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.RevokeDeliveryLink)
|
||||
merchant.POST("/orders/:order_no/delivery-link/restore", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.RestoreDeliveryLink)
|
||||
merchant.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator, model.MemberRoleFinance), h.Merchant.GetWallet)
|
||||
merchant.GET("/wallet/ledger", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Merchant.ListWalletLedger)
|
||||
merchant.POST("/recharge/applications", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Recharge.CreateApplication)
|
||||
merchant.GET("/recharge/applications", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator, model.MemberRoleFinance), h.Recharge.ListApplications)
|
||||
merchant.GET("/recharge/alert-config", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Recharge.GetAlertConfig)
|
||||
merchant.PUT("/recharge/alert-config", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Recharge.SaveAlertConfig)
|
||||
merchant.GET("/alert-channels", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Recharge.ListAlertChannels)
|
||||
merchant.POST("/alert-channels", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Recharge.CreateAlertChannel)
|
||||
merchant.PUT("/alert-channels/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Recharge.UpdateAlertChannel)
|
||||
merchant.DELETE("/alert-channels/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Recharge.DeleteAlertChannel)
|
||||
merchant.POST("/alert-channels/:id/test", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Recharge.TestAlertChannel)
|
||||
merchant.GET("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), h.Merchant.ListAPIClients)
|
||||
merchant.POST("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateAPIClient)
|
||||
merchant.PATCH("/api-clients/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.UpdateAPIClientStatus)
|
||||
merchant.DELETE("/api-clients/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.DeleteAPIClient)
|
||||
merchant.GET("/callbacks", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureCallbacks), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.ListCallbacks)
|
||||
merchant.POST("/callbacks", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureCallbacks), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateCallback)
|
||||
merchant.GET("/members", middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.ListMembers)
|
||||
merchant.POST("/members", middleware.RequireMerchantRole(model.MemberRoleOwner), h.Merchant.AddCurrentMerchantMember)
|
||||
merchant.GET("/products", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.ListProducts)
|
||||
merchant.PATCH("/products/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureProducts), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionProductsManage), h.Merchant.UpdateProductStatus)
|
||||
merchant.GET("/orders", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureOrders), middleware.RequireMerchantPermissions(h.Tenant, model.PermissionOrdersManage), h.Merchant.ListOrders)
|
||||
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.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.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)
|
||||
}
|
||||
|
||||
// 用户 / 发货记录 / 平台商户(仅管理员)
|
||||
|
||||
@@ -99,6 +99,9 @@ func (s *MerchantService) CreateMerchant(in CreateMerchantInput, actorUserID uin
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := createDefaultMerchantRoles(tx, merchant.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copyDefaultProducts(tx, merchant.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,39 +3,201 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AddMemberInput struct {
|
||||
UserID uint
|
||||
Username string
|
||||
Password string
|
||||
Nickname string
|
||||
Role string
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
type MerchantRoleInput struct {
|
||||
Code string
|
||||
Name string
|
||||
Permissions []string
|
||||
}
|
||||
|
||||
var merchantRoleCodePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,63}$`)
|
||||
|
||||
func merchantPermissionsText(permissions []string) (string, error) {
|
||||
allowed := map[string]struct{}{
|
||||
model.PermissionProductsManage: {}, model.PermissionOrdersManage: {}, model.PermissionWalletView: {},
|
||||
model.PermissionWalletLedger: {}, model.PermissionRechargeManage: {}, model.PermissionAPIManage: {},
|
||||
model.PermissionCallbacksManage: {}, model.PermissionMembersManage: {},
|
||||
}
|
||||
set := make(map[string]struct{}, len(permissions))
|
||||
for _, permission := range permissions {
|
||||
permission = strings.TrimSpace(permission)
|
||||
if _, ok := allowed[permission]; !ok {
|
||||
return "", errors.New("包含无效的角色权限")
|
||||
}
|
||||
set[permission] = struct{}{}
|
||||
}
|
||||
items := make([]string, 0, len(set))
|
||||
for permission := range set {
|
||||
items = append(items, permission)
|
||||
}
|
||||
sort.Strings(items)
|
||||
return strings.Join(items, ","), nil
|
||||
}
|
||||
|
||||
func (s *MerchantService) ListRoles(merchantID uint) ([]model.MerchantRole, error) {
|
||||
roles := builtinMerchantRoles(merchantID)
|
||||
var custom []model.MerchantRole
|
||||
if err := s.db.Where("merchant_id = ?", merchantID).Order("id ASC").Find(&custom).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(roles, custom...), nil
|
||||
}
|
||||
|
||||
func builtinMerchantRoles(merchantID uint) []model.MerchantRole {
|
||||
permissions, _ := (&TenantService{}).MerchantRolePermissions(merchantID, model.MemberRoleOwner)
|
||||
items := make([]string, 0, len(permissions))
|
||||
for permission := range permissions {
|
||||
items = append(items, permission)
|
||||
}
|
||||
sort.Strings(items)
|
||||
return []model.MerchantRole{{MerchantID: merchantID, Code: model.MemberRoleOwner, Name: "负责人", Permissions: strings.Join(items, ","), Status: 1}}
|
||||
}
|
||||
|
||||
func createDefaultMerchantRoles(tx *gorm.DB, merchantID uint) error {
|
||||
roles := []model.MerchantRole{
|
||||
{MerchantID: merchantID, Code: model.MemberRoleOperator, Name: "运营", Permissions: strings.Join([]string{model.PermissionAPIManage, model.PermissionCallbacksManage, model.PermissionOrdersManage, model.PermissionProductsManage, model.PermissionWalletView}, ","), Status: 1},
|
||||
{MerchantID: merchantID, Code: model.MemberRoleFinance, Name: "财务", Permissions: strings.Join([]string{model.PermissionRechargeManage, model.PermissionWalletLedger, model.PermissionWalletView}, ","), Status: 1},
|
||||
{MerchantID: merchantID, Code: "support", Name: "客服", Permissions: model.PermissionOrdersManage, Status: 1},
|
||||
}
|
||||
return tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "merchant_id"}, {Name: "code"}}, DoNothing: true}).Create(&roles).Error
|
||||
}
|
||||
|
||||
func (s *MerchantService) CreateRole(merchantID uint, in MerchantRoleInput, actorUserID uint) (*model.MerchantRole, error) {
|
||||
in.Code = strings.ToLower(strings.TrimSpace(in.Code))
|
||||
in.Name = strings.TrimSpace(in.Name)
|
||||
if !merchantRoleCodePattern.MatchString(in.Code) {
|
||||
return nil, errors.New("角色编码需为 2-64 位小写字母、数字或连字符")
|
||||
}
|
||||
if isReservedMerchantRoleCode(in.Code) {
|
||||
return nil, errors.New("角色编码不能使用系统保留角色")
|
||||
}
|
||||
if in.Name == "" {
|
||||
return nil, errors.New("角色名称不能为空")
|
||||
}
|
||||
permissions, err := merchantPermissionsText(in.Permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role := &model.MerchantRole{MerchantID: merchantID, Code: in.Code, Name: in.Name, Permissions: permissions, Status: 1}
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var merchant model.Merchant
|
||||
if err := tx.Where("id = ? AND status = ?", merchantID, model.MerchantStatusActive).First(&merchant).Error; err != nil {
|
||||
return errors.New("商户不存在或已禁用")
|
||||
}
|
||||
if err := tx.Create(role).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.role.create", "merchant_role", fmt.Sprint(role.ID), map[string]string{"code": role.Code})
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate key") {
|
||||
return nil, errors.New("角色编码已存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func isReservedMerchantRoleCode(code string) bool {
|
||||
switch code {
|
||||
case model.MemberRoleOwner, model.MemberRoleViewer, model.MemberRoleOperator, model.MemberRoleFinance, "support":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MerchantService) UpdateRole(merchantID, roleID uint, in MerchantRoleInput, actorUserID uint) (*model.MerchantRole, error) {
|
||||
in.Name = strings.TrimSpace(in.Name)
|
||||
if in.Name == "" {
|
||||
return nil, errors.New("角色名称不能为空")
|
||||
}
|
||||
permissions, err := merchantPermissionsText(in.Permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var role model.MerchantRole
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("id = ? AND merchant_id = ?", roleID, merchantID).First(&role).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("角色不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&role).Updates(map[string]interface{}{"name": in.Name, "permissions": permissions}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.role.update", "merchant_role", fmt.Sprint(roleID), map[string]string{"code": role.Code})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func (s *MerchantService) AddMember(merchantID uint, in AddMemberInput, actorUserID uint) (*model.MerchantMember, error) {
|
||||
if !isValidMemberRole(in.Role) {
|
||||
return nil, errors.New("无效的商户成员角色")
|
||||
in.Role = strings.TrimSpace(in.Role)
|
||||
if in.Role == "" {
|
||||
return nil, errors.New("商户成员角色不能为空")
|
||||
}
|
||||
member := &model.MerchantMember{
|
||||
MerchantID: merchantID,
|
||||
UserID: in.UserID,
|
||||
Role: in.Role,
|
||||
Status: 1,
|
||||
IsDefault: in.IsDefault,
|
||||
if in.UserID == 0 && strings.TrimSpace(in.Username) == "" {
|
||||
return nil, errors.New("请选择已有账号或填写新员工账号")
|
||||
}
|
||||
member := &model.MerchantMember{MerchantID: merchantID, Role: in.Role, Status: 1, IsDefault: in.IsDefault}
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var merchant model.Merchant
|
||||
if err := tx.Where("id = ? AND status = ?", merchantID, model.MerchantStatusActive).First(&merchant).Error; err != nil {
|
||||
return errors.New("商户不存在或已禁用")
|
||||
}
|
||||
var user model.User
|
||||
if err := tx.Where("id = ? AND status = ?", in.UserID, 1).First(&user).Error; err != nil {
|
||||
return errors.New("用户不存在或已禁用")
|
||||
createdUser := false
|
||||
if in.UserID != 0 {
|
||||
if err := tx.Where("id = ? AND status = ?", in.UserID, 1).First(&user).Error; err != nil {
|
||||
return errors.New("用户不存在或已禁用")
|
||||
}
|
||||
} else {
|
||||
username := strings.TrimSpace(in.Username)
|
||||
if len(username) < 3 {
|
||||
return errors.New("员工用户名至少 3 位")
|
||||
}
|
||||
if len(in.Password) < 6 {
|
||||
return errors.New("员工密码至少 6 位")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user = model.User{Username: username, PasswordHash: string(hash), Nickname: fallbackName(in.Nickname, username), Role: model.RoleMerchant, Status: 1}
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return errors.New("员工用户名已存在")
|
||||
}
|
||||
createdUser = true
|
||||
}
|
||||
if _, err := (&TenantService{db: tx}).MerchantRolePermissions(merchantID, in.Role); err != nil {
|
||||
return err
|
||||
}
|
||||
member.UserID = user.ID
|
||||
// 新建员工仅属于当前商户,登录时应直接进入该商户。
|
||||
member.IsDefault = in.IsDefault || createdUser
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "merchant_id"}, {Name: "user_id"}},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
@@ -46,12 +208,12 @@ func (s *MerchantService) AddMember(merchantID uint, in AddMemberInput, actorUse
|
||||
}).Create(member).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.member.upsert", "merchant_member", fmt.Sprintf("%d:%d", merchantID, in.UserID), map[string]string{"role": in.Role})
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.member.upsert", "merchant_member", fmt.Sprintf("%d:%d", merchantID, user.ID), map[string]string{"role": in.Role})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.Where("merchant_id = ? AND user_id = ?", merchantID, in.UserID).First(member).Error; err != nil {
|
||||
if err := s.db.Preload("User").Where("merchant_id = ? AND user_id = ?", merchantID, member.UserID).First(member).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return member, nil
|
||||
|
||||
@@ -35,3 +35,97 @@ func TestCreateAPIClientEnforcesPerMerchantLimit(t *testing.T) {
|
||||
t.Fatalf("expected per-merchant limit error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerchantCustomRoleIsScopedAndCanCreateEmployee(t *testing.T) {
|
||||
db := newServiceTestDB(t)
|
||||
merchantA := model.Merchant{Code: "team-merchant-a", Name: "团队商户 A", Status: model.MerchantStatusActive}
|
||||
merchantB := model.Merchant{Code: "team-merchant-b", Name: "团队商户 B", Status: model.MerchantStatusActive}
|
||||
actor := model.User{Username: "team-owner", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
|
||||
if err := db.Create(&merchantA).Error; err != nil {
|
||||
t.Fatalf("create merchant A: %v", err)
|
||||
}
|
||||
if err := db.Create(&merchantB).Error; err != nil {
|
||||
t.Fatalf("create merchant B: %v", err)
|
||||
}
|
||||
if err := db.Create(&actor).Error; err != nil {
|
||||
t.Fatalf("create actor: %v", err)
|
||||
}
|
||||
|
||||
svc := NewMerchantService(db, nil, NewTenantService(db))
|
||||
role, err := svc.CreateRole(merchantA.ID, MerchantRoleInput{
|
||||
Code: "after-sales",
|
||||
Name: "客服",
|
||||
Permissions: []string{model.PermissionOrdersManage},
|
||||
}, actor.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("create custom role: %v", err)
|
||||
}
|
||||
|
||||
member, err := svc.AddMember(merchantA.ID, AddMemberInput{
|
||||
Username: "merchant-a-support",
|
||||
Password: "password123",
|
||||
Nickname: "客服小李",
|
||||
Role: role.Code,
|
||||
}, actor.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("create employee: %v", err)
|
||||
}
|
||||
if member.User == nil || member.User.Username != "merchant-a-support" || member.Role != "after-sales" {
|
||||
t.Fatalf("unexpected member: %+v", member)
|
||||
}
|
||||
if !member.IsDefault {
|
||||
t.Fatal("new employee should default to the merchant that created it")
|
||||
}
|
||||
|
||||
if _, err := svc.AddMember(merchantB.ID, AddMemberInput{
|
||||
Username: "merchant-b-support",
|
||||
Password: "password123",
|
||||
Role: role.Code,
|
||||
}, actor.ID); err == nil {
|
||||
t.Fatal("expected merchant A role to be rejected for merchant B")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultMerchantRolesCanBeEdited(t *testing.T) {
|
||||
db := newServiceTestDB(t)
|
||||
merchant := model.Merchant{Code: "default-roles", Name: "默认角色商户", Status: model.MerchantStatusActive}
|
||||
actor := model.User{Username: "default-role-owner", PasswordHash: "hash", Role: model.RoleMerchant, Status: 1}
|
||||
if err := db.Create(&merchant).Error; err != nil {
|
||||
t.Fatalf("create merchant: %v", err)
|
||||
}
|
||||
if err := db.Create(&actor).Error; err != nil {
|
||||
t.Fatalf("create actor: %v", err)
|
||||
}
|
||||
if err := createDefaultMerchantRoles(db, merchant.ID); err != nil {
|
||||
t.Fatalf("create default roles: %v", err)
|
||||
}
|
||||
|
||||
svc := NewMerchantService(db, nil, NewTenantService(db))
|
||||
roles, err := svc.ListRoles(merchant.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("list roles: %v", err)
|
||||
}
|
||||
var finance model.MerchantRole
|
||||
var hasSupport bool
|
||||
for _, role := range roles {
|
||||
if role.Code == model.MemberRoleFinance {
|
||||
finance = role
|
||||
}
|
||||
if role.Code == "support" && role.Name == "客服" {
|
||||
hasSupport = true
|
||||
}
|
||||
}
|
||||
if finance.ID == 0 || !hasSupport {
|
||||
t.Fatalf("expected editable finance and support roles, got %+v", roles)
|
||||
}
|
||||
updated, err := svc.UpdateRole(merchant.ID, finance.ID, MerchantRoleInput{
|
||||
Name: "财务主管",
|
||||
Permissions: []string{model.PermissionWalletView},
|
||||
}, actor.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("update finance role: %v", err)
|
||||
}
|
||||
if updated.Name != "财务主管" || updated.Permissions != model.PermissionWalletView {
|
||||
t.Fatalf("unexpected updated role: %+v", updated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,27 @@ func (s *TenantService) ResolveMember(userID uint, merchantRef string) (*model.M
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ResolveMerchantForAdmin 允许平台管理员显式进入任一启用商户的后台管理上下文。
|
||||
func (s *TenantService) ResolveMerchantForAdmin(merchantRef string) (*model.Merchant, error) {
|
||||
if strings.TrimSpace(merchantRef) == "" {
|
||||
return nil, errors.New("请选择商户")
|
||||
}
|
||||
tx := s.db.Where("status = ?", model.MerchantStatusActive)
|
||||
if id, err := strconv.ParseUint(merchantRef, 10, 64); err == nil {
|
||||
tx = tx.Where("id = ?", uint(id))
|
||||
} else {
|
||||
tx = tx.Where("code = ?", merchantRef)
|
||||
}
|
||||
var merchant model.Merchant
|
||||
if err := tx.First(&merchant).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("商户不存在或已禁用")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &merchant, nil
|
||||
}
|
||||
|
||||
func (s *TenantService) EnsureSelfMember(userID uint, role string, status int) error {
|
||||
var merchant model.Merchant
|
||||
if err := s.db.Where("code = ?", model.MerchantCodeSelfOperated).First(&merchant).Error; err != nil {
|
||||
@@ -79,13 +100,42 @@ func (s *TenantService) EnsureMember(merchantID, userID uint, role string, statu
|
||||
|
||||
func isValidMemberRole(role string) bool {
|
||||
switch role {
|
||||
case model.MemberRoleOwner, model.MemberRoleOperator, model.MemberRoleFinance, model.MemberRoleViewer:
|
||||
case model.MemberRoleOwner, model.MemberRoleViewer:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MerchantRolePermissions 返回保留角色或商户自定义角色的权限集合。
|
||||
// 负责人始终拥有该商户的全部后台权限,避免被自定义角色配置锁死。
|
||||
func (s *TenantService) MerchantRolePermissions(merchantID uint, role string) (map[string]struct{}, error) {
|
||||
permissions := builtinMerchantRolePermissions(role)
|
||||
if permissions != nil {
|
||||
return permissions, nil
|
||||
}
|
||||
var merchantRole model.MerchantRole
|
||||
if err := s.db.Where("merchant_id = ? AND code = ? AND status = ?", merchantID, role, 1).First(&merchantRole).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("商户角色不存在或已禁用")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ParseScopes(merchantRole.Permissions), nil
|
||||
}
|
||||
|
||||
func builtinMerchantRolePermissions(role string) map[string]struct{} {
|
||||
all := func(values ...string) map[string]struct{} { return ParseScopes(strings.Join(values, ",")) }
|
||||
switch role {
|
||||
case model.MemberRoleOwner:
|
||||
return all(model.PermissionMembersManage, model.PermissionProductsManage, model.PermissionOrdersManage, model.PermissionWalletView, model.PermissionWalletLedger, model.PermissionRechargeManage, model.PermissionAPIManage, model.PermissionCallbacksManage)
|
||||
case model.MemberRoleViewer:
|
||||
return map[string]struct{}{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func MemberCanManage(memberRole string) bool {
|
||||
return memberRole == model.MemberRoleOwner || memberRole == model.MemberRoleOperator
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user