feat: support merchant team roles and permissions
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user