refactor(account): 统一账号管理API、完善权限检查和操作审计
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m17s

- 合并 customer_account 和 shop_account 路由到统一的 account 接口
- 新增统一认证接口 (auth handler)
- 实现越权防护中间件和权限检查工具函数
- 新增操作审计日志模型和服务
- 更新数据库迁移 (版本 39: account_operation_log 表)
- 补充集成测试覆盖权限检查和审计日志场景
This commit is contained in:
2026-02-02 17:23:20 +08:00
parent 5851cc6403
commit 80f560df33
58 changed files with 10743 additions and 4915 deletions

View File

@@ -22,54 +22,86 @@ type Service struct {
accountStore *postgres.AccountStore
roleStore *postgres.RoleStore
accountRoleStore *postgres.AccountRoleStore
shopStore middleware.ShopStoreInterface
enterpriseStore middleware.EnterpriseStoreInterface
auditService AuditServiceInterface
}
type AuditServiceInterface interface {
LogOperation(ctx context.Context, log *model.AccountOperationLog)
}
// New 创建账号服务
func New(accountStore *postgres.AccountStore, roleStore *postgres.RoleStore, accountRoleStore *postgres.AccountRoleStore) *Service {
func New(
accountStore *postgres.AccountStore,
roleStore *postgres.RoleStore,
accountRoleStore *postgres.AccountRoleStore,
shopStore middleware.ShopStoreInterface,
enterpriseStore middleware.EnterpriseStoreInterface,
auditService AuditServiceInterface,
) *Service {
return &Service{
accountStore: accountStore,
roleStore: roleStore,
accountRoleStore: accountRoleStore,
shopStore: shopStore,
enterpriseStore: enterpriseStore,
auditService: auditService,
}
}
// Create 创建账号
func (s *Service) Create(ctx context.Context, req *dto.CreateAccountRequest) (*model.Account, error) {
// 获取当前用户 ID
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 验证代理账号必须提供 shop_id
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeEnterprise {
return nil, errors.New(errors.CodeForbidden, "企业账号不允许创建账号")
}
if userType == constants.UserTypeAgent && req.UserType == constants.UserTypePlatform {
return nil, errors.New(errors.CodeForbidden, "无权限创建平台账号")
}
if req.UserType == constants.UserTypeAgent && req.ShopID == nil {
return nil, errors.New(errors.CodeInvalidParam, "代理账号必须提供店铺ID")
}
// 验证企业账号必须提供 enterprise_id
if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID == nil {
return nil, errors.New(errors.CodeInvalidParam, "企业账号必须提供企业ID")
}
// 检查用户名唯一性
if req.UserType == constants.UserTypeAgent && req.ShopID != nil {
if err := middleware.CanManageShop(ctx, *req.ShopID, s.shopStore); err != nil {
return nil, err
}
}
if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID != nil {
if err := middleware.CanManageEnterprise(ctx, *req.EnterpriseID, s.enterpriseStore, s.shopStore); err != nil {
return nil, err
}
}
existing, err := s.accountStore.GetByUsername(ctx, req.Username)
if err == nil && existing != nil {
return nil, errors.New(errors.CodeUsernameExists, "用户名已存在")
}
// 检查手机号唯一性
existing, err = s.accountStore.GetByPhone(ctx, req.Phone)
if err == nil && existing != nil {
return nil, errors.New(errors.CodePhoneExists, "手机号已存在")
}
// bcrypt 哈希密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
}
// 创建账号
account := &model.Account{
Username: req.Username,
Phone: req.Phone,
@@ -84,8 +116,40 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateAccountRequest) (*m
return nil, errors.Wrap(errors.CodeInternalError, err, "创建账号失败")
}
// TODO: 清除店铺的下级 ID 缓存(需要在 Service 层处理)
// 由于账号层级关系改为通过 Shop 表维护,这里的缓存清理逻辑已废弃
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
operatorName := ""
if currentAccount != nil {
operatorName = currentAccount.Username
}
afterData := model.JSONB{
"id": account.ID,
"username": account.Username,
"phone": account.Phone,
"user_type": account.UserType,
"shop_id": account.ShopID,
"enterprise_id": account.EnterpriseID,
"status": account.Status,
}
requestID := middleware.GetRequestIDFromContext(ctx)
ipAddress := middleware.GetIPFromContext(ctx)
userAgent := middleware.GetUserAgentFromContext(ctx)
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: currentUserID,
OperatorType: userType,
OperatorName: operatorName,
TargetAccountID: &account.ID,
TargetUsername: &account.Username,
TargetUserType: &account.UserType,
OperationType: "create",
OperationDesc: fmt.Sprintf("创建账号: %s", account.Username),
AfterData: afterData,
RequestID: requestID,
IPAddress: ipAddress,
UserAgent: userAgent,
})
return account, nil
}
@@ -104,24 +168,37 @@ func (s *Service) Get(ctx context.Context, id uint) (*model.Account, error) {
// Update 更新账号
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateAccountRequest) (*model.Account, error) {
// 获取当前用户 ID
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
// 获取现有账号
account, err := s.accountStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
}
// 更新字段
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeAgent {
if account.ShopID == nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该账号")
}
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
}
beforeData := model.JSONB{
"username": account.Username,
"phone": account.Phone,
"status": account.Status,
}
if req.Username != nil {
// 检查新用户名唯一性
existing, err := s.accountStore.GetByUsername(ctx, *req.Username)
if err == nil && existing != nil && existing.ID != id {
return nil, errors.New(errors.CodeUsernameExists, "用户名已存在")
@@ -130,7 +207,6 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateAccountReq
}
if req.Phone != nil {
// 检查新手机号唯一性
existing, err := s.accountStore.GetByPhone(ctx, *req.Phone)
if err == nil && existing != nil && existing.ID != id {
return nil, errors.New(errors.CodePhoneExists, "手机号已存在")
@@ -156,26 +232,102 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateAccountReq
return nil, errors.Wrap(errors.CodeInternalError, err, "更新账号失败")
}
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
operatorName := ""
if currentAccount != nil {
operatorName = currentAccount.Username
}
afterData := model.JSONB{
"username": account.Username,
"phone": account.Phone,
"status": account.Status,
}
requestID := middleware.GetRequestIDFromContext(ctx)
ipAddress := middleware.GetIPFromContext(ctx)
userAgent := middleware.GetUserAgentFromContext(ctx)
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: currentUserID,
OperatorType: userType,
OperatorName: operatorName,
TargetAccountID: &account.ID,
TargetUsername: &account.Username,
TargetUserType: &account.UserType,
OperationType: "update",
OperationDesc: fmt.Sprintf("更新账号: %s", account.Username),
BeforeData: beforeData,
AfterData: afterData,
RequestID: requestID,
IPAddress: ipAddress,
UserAgent: userAgent,
})
return account, nil
}
// Delete 软删除账号
func (s *Service) Delete(ctx context.Context, id uint) error {
// 检查账号存在
_, err := s.accountStore.GetByID(ctx, id)
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
account, err := s.accountStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeAccountNotFound, "账号不存在")
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
}
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeAgent {
if account.ShopID == nil {
return errors.New(errors.CodeForbidden, "无权限操作该账号")
}
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
}
beforeData := model.JSONB{
"id": account.ID,
"username": account.Username,
"phone": account.Phone,
"status": account.Status,
}
if err := s.accountStore.Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除账号失败")
}
// 账号删除后不需要清理缓存
// 数据权限过滤现在基于店铺层级,店铺相关的缓存清理由 ShopService 负责
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
operatorName := ""
if currentAccount != nil {
operatorName = currentAccount.Username
}
requestID := middleware.GetRequestIDFromContext(ctx)
ipAddress := middleware.GetIPFromContext(ctx)
userAgent := middleware.GetUserAgentFromContext(ctx)
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: currentUserID,
OperatorType: userType,
OperatorName: operatorName,
TargetAccountID: &account.ID,
TargetUsername: &account.Username,
TargetUserType: &account.UserType,
OperationType: "delete",
OperationDesc: fmt.Sprintf("删除账号: %s", account.Username),
BeforeData: beforeData,
RequestID: requestID,
IPAddress: ipAddress,
UserAgent: userAgent,
})
return nil
}
@@ -221,12 +373,22 @@ func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uin
account, err := s.accountStore.GetByID(ctx, accountID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
}
// 超级管理员禁止分配角色
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeAgent {
if account.ShopID == nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该账号")
}
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
}
if account.UserType == constants.UserTypeSuperAdmin {
return nil, errors.New(errors.CodeInvalidParam, "超级管理员不允许分配角色")
}
@@ -295,6 +457,35 @@ func (s *Service) AssignRoles(ctx context.Context, accountID uint, roleIDs []uin
ars = append(ars, ar)
}
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
operatorName := ""
if currentAccount != nil {
operatorName = currentAccount.Username
}
afterData := model.JSONB{
"role_ids": roleIDs,
}
requestID := middleware.GetRequestIDFromContext(ctx)
ipAddress := middleware.GetIPFromContext(ctx)
userAgent := middleware.GetUserAgentFromContext(ctx)
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: currentUserID,
OperatorType: userType,
OperatorName: operatorName,
TargetAccountID: &account.ID,
TargetUsername: &account.Username,
TargetUserType: &account.UserType,
OperationType: "assign_roles",
OperationDesc: fmt.Sprintf("为账号 %s 分配角色", account.Username),
AfterData: afterData,
RequestID: requestID,
IPAddress: ipAddress,
UserAgent: userAgent,
})
return ars, nil
}
@@ -325,20 +516,63 @@ func (s *Service) GetRoles(ctx context.Context, accountID uint) ([]*model.Role,
// RemoveRole 移除账号的角色
func (s *Service) RemoveRole(ctx context.Context, accountID, roleID uint) error {
// 检查账号存在
_, err := s.accountStore.GetByID(ctx, accountID)
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
account, err := s.accountStore.GetByID(ctx, accountID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeAccountNotFound, "账号不存在")
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
}
// 删除关联
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeAgent {
if account.ShopID == nil {
return errors.New(errors.CodeForbidden, "无权限操作该账号")
}
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
}
if err := s.accountRoleStore.Delete(ctx, accountID, roleID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除账号-角色关联失败")
}
currentAccount, _ := s.accountStore.GetByID(ctx, currentUserID)
operatorName := ""
if currentAccount != nil {
operatorName = currentAccount.Username
}
afterData := model.JSONB{
"removed_role_id": roleID,
}
requestID := middleware.GetRequestIDFromContext(ctx)
ipAddress := middleware.GetIPFromContext(ctx)
userAgent := middleware.GetUserAgentFromContext(ctx)
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: currentUserID,
OperatorType: userType,
OperatorName: operatorName,
TargetAccountID: &account.ID,
TargetUsername: &account.Username,
TargetUserType: &account.UserType,
OperationType: "remove_role",
OperationDesc: fmt.Sprintf("移除账号 %s 的角色", account.Username),
AfterData: afterData,
RequestID: requestID,
IPAddress: ipAddress,
UserAgent: userAgent,
})
return nil
}