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 UpdateMemberInput struct { Role string Status int 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) { in.Role = strings.TrimSpace(in.Role) if in.Role == "" { return nil, errors.New("商户成员角色不能为空") } 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 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{}{ "role": in.Role, "status": 1, "is_default": in.IsDefault, }), }).Create(member).Error; err != nil { return err } 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.Preload("User").Where("merchant_id = ? AND user_id = ?", merchantID, member.UserID).First(member).Error; err != nil { return nil, err } return member, nil } func (s *MerchantService) ListMembers(merchantID uint) ([]model.MerchantMember, error) { var members []model.MerchantMember err := s.db.Preload("User").Where("merchant_id = ?", merchantID).Order("id ASC").Find(&members).Error return members, err } // UpdateMember updates a member's relationship with one merchant without changing the login account. func (s *MerchantService) UpdateMember(merchantID, memberID uint, in UpdateMemberInput, actorUserID uint) (*model.MerchantMember, error) { in.Role = strings.TrimSpace(in.Role) if in.Role == "" { return nil, errors.New("商户成员角色不能为空") } if in.Status != 0 && in.Status != 1 { return nil, errors.New("成员状态无效") } var member model.MerchantMember err := s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("id = ? AND merchant_id = ?", memberID, merchantID).First(&member).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("成员不存在") } return err } if member.Role == model.MemberRoleOwner { return errors.New("负责人不能在成员管理中修改") } if member.UserID == actorUserID { return errors.New("不能修改当前登录账号") } if _, err := (&TenantService{db: tx}).MerchantRolePermissions(merchantID, in.Role); err != nil { return err } updates := map[string]interface{}{"role": in.Role, "status": in.Status, "is_default": in.IsDefault} if err := tx.Model(&member).Updates(updates).Error; err != nil { return err } return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.member.update", "merchant_member", fmt.Sprint(memberID), map[string]string{"role": in.Role, "status": fmt.Sprint(in.Status)}) }) if err != nil { return nil, err } if err := s.db.Preload("User").First(&member, member.ID).Error; err != nil { return nil, err } return &member, nil } // RemoveMember removes only the selected merchant relationship and retains the account for audit and other merchants. func (s *MerchantService) RemoveMember(merchantID, memberID, actorUserID uint) error { return s.db.Transaction(func(tx *gorm.DB) error { var member model.MerchantMember if err := tx.Where("id = ? AND merchant_id = ?", memberID, merchantID).First(&member).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("成员不存在") } return err } if member.Role == model.MemberRoleOwner { return errors.New("负责人不能移除") } if member.UserID == actorUserID { return errors.New("不能移除当前登录账号") } if err := tx.Delete(&member).Error; err != nil { return err } return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.member.remove", "merchant_member", fmt.Sprint(memberID), nil) }) }