Files
affiliate_dash/backend/internal/service/tenant.go
T

214 lines
6.2 KiB
Go

package service
import (
"errors"
"strconv"
"strings"
"affiliate_dash/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// TenantService 负责商户成员关系与请求租户解析。
type TenantService struct {
db *gorm.DB
}
func NewTenantService(db *gorm.DB) *TenantService {
return &TenantService{db: db}
}
func (s *TenantService) ResolveMember(userID uint, merchantRef string) (*model.MerchantMember, error) {
if userID == 0 {
return nil, errors.New("无效的用户身份")
}
tx := s.db.Preload("Merchant").
Where("merchant_members.user_id = ? AND merchant_members.status = ?", userID, 1).
Joins("JOIN merchants ON merchants.id = merchant_members.merchant_id AND merchants.status = ?", model.MerchantStatusActive)
if merchantRef != "" {
if id, err := strconv.ParseUint(merchantRef, 10, 64); err == nil {
tx = tx.Where("merchant_members.merchant_id = ?", uint(id))
} else {
tx = tx.Where("merchants.code = ?", merchantRef)
}
}
var member model.MerchantMember
err := tx.Order("merchant_members.is_default DESC, merchant_members.id ASC").First(&member).Error
if err == nil {
return &member, nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("当前账号无权访问该商户")
}
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 {
return err
}
return s.EnsureMember(merchant.ID, userID, role, status, role == model.MemberRoleOwner)
}
func (s *TenantService) EnsureMember(merchantID, userID uint, role string, status int, isDefault bool) error {
if merchantID == 0 || userID == 0 {
return errors.New("商户和用户不能为空")
}
if !isValidMemberRole(role) {
return errors.New("无效的商户成员角色")
}
member := model.MerchantMember{
MerchantID: merchantID,
UserID: userID,
Role: role,
Status: status,
IsDefault: isDefault,
}
return s.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "merchant_id"}, {Name: "user_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"role": role,
"status": status,
"is_default": isDefault,
}),
}).Create(&member).Error
}
func isValidMemberRole(role string) bool {
switch role {
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
}
func MemberCanManageFinance(memberRole string) bool {
return memberRole == model.MemberRoleOwner || memberRole == model.MemberRoleFinance
}
func ParseScopes(scopes string) map[string]struct{} {
out := make(map[string]struct{})
for _, scope := range strings.FieldsFunc(scopes, func(r rune) bool {
return r == ',' || r == ' ' || r == '\n' || r == '\t'
}) {
if scope != "" {
out[scope] = struct{}{}
}
}
return out
}
func HasAnyScope(scopes string, wanted ...string) bool {
set := ParseScopes(scopes)
if _, ok := set["*"]; ok {
return true
}
for _, scope := range wanted {
if _, ok := set[scope]; ok {
return true
}
}
return false
}
func NormalizeMerchantFeatures(features string) string {
set := ParseScopes(features)
if len(set) == 0 {
set = ParseScopes(model.DefaultMerchantFeatures)
}
valid := map[string]struct{}{
model.MerchantFeatureProducts: {},
model.MerchantFeatureOrders: {},
model.MerchantFeatureWallet: {},
model.MerchantFeatureAPI: {},
model.MerchantFeatureCallbacks: {},
}
ordered := []string{
model.MerchantFeatureProducts,
model.MerchantFeatureOrders,
model.MerchantFeatureWallet,
model.MerchantFeatureAPI,
model.MerchantFeatureCallbacks,
}
out := make([]string, 0, len(ordered))
for _, feature := range ordered {
if _, ok := set[feature]; ok {
if _, allowed := valid[feature]; allowed {
out = append(out, feature)
}
}
}
if len(out) == 0 {
return model.DefaultMerchantFeatures
}
return strings.Join(out, ",")
}
func MerchantHasFeature(features string, wanted ...string) bool {
set := ParseScopes(features)
for _, feature := range wanted {
if _, ok := set[feature]; ok {
return true
}
}
return false
}