Files
junhong_cmp_fiber/internal/service/enterprise/service.go
break b3499adfca 固化七月迭代审计治理进展以隔离线上热修
Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。

Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。

Confidence: medium

Scope-risk: broad

Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。

Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。

Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
2026-08-03 09:47:22 +08:00

589 lines
21 KiB
Go

package enterprise
import (
"context"
stderrors "errors"
accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Service struct {
db *gorm.DB
enterpriseStore *postgres.EnterpriseStore
shopStore *postgres.ShopStore
accountStore *postgres.AccountStore
accessAudit accessauditapp.Writer
}
// New 创建企业生命周期服务。
func New(
db *gorm.DB,
enterpriseStore *postgres.EnterpriseStore,
shopStore *postgres.ShopStore,
accountStore *postgres.AccountStore,
accessAudit accessauditapp.Writer,
) *Service {
return &Service{
db: db,
enterpriseStore: enterpriseStore,
shopStore: shopStore,
accountStore: accountStore,
accessAudit: accessAudit,
}
}
func (s *Service) Create(ctx context.Context, req *dto.CreateEnterpriseReq) (*dto.CreateEnterpriseResp, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
if s.db == nil || s.accessAudit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "企业审计接缝未配置")
}
enterprise := &model.Enterprise{
EnterpriseName: req.EnterpriseName, EnterpriseCode: req.EnterpriseCode, OwnerShopID: req.OwnerShopID,
LegalPerson: req.LegalPerson, ContactName: req.ContactName, ContactPhone: req.ContactPhone,
BusinessLicense: req.BusinessLicense, Province: req.Province, City: req.City,
District: req.District, Address: req.Address, Status: constants.StatusEnabled,
}
enterprise.Creator = currentUserID
enterprise.Updater = currentUserID
if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeEnterprise {
err := errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err)
return nil, err
}
if middleware.GetUserTypeFromContext(ctx) == constants.UserTypeAgent && req.OwnerShopID == nil {
err := errors.New(errors.CodeForbidden, "代理账号不能创建平台主管企业")
s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err)
return nil, err
}
if req.EnterpriseCode != "" {
existing, _ := s.enterpriseStore.GetByCode(ctx, req.EnterpriseCode)
if existing != nil {
err := errors.New(errors.CodeEnterpriseCodeExists, "企业编号已存在")
s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err)
return nil, err
}
}
existingAccount, _ := s.accountStore.GetByPhone(ctx, req.LoginPhone)
if existingAccount != nil {
err := errors.New(errors.CodePhoneExists, "手机号已被使用")
s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err)
return nil, err
}
var ownerShop *model.Shop
if req.OwnerShopID != nil {
if err := middleware.CanManageShop(ctx, *req.OwnerShopID); err != nil {
err = errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err)
return nil, err
}
var err error
ownerShop, err = s.shopStore.GetByID(ctx, *req.OwnerShopID)
if err != nil {
err = errors.New(errors.CodeShopNotFound, "归属店铺不存在或无效")
s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, nil, nil, err)
return nil, err
}
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
appErr := errors.Wrap(errors.CodeInternalError, err, "密码加密失败")
s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", enterprise, ownerShop, nil, appErr)
return nil, appErr
}
var account *model.Account
err = s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.WithContext(ctx).Create(enterprise).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "创建企业失败")
}
account = &model.Account{
Username: req.EnterpriseName,
Phone: req.LoginPhone,
Password: string(hashedPassword),
UserType: constants.UserTypeEnterprise,
EnterpriseID: &enterprise.ID,
Status: constants.StatusEnabled,
}
account.Creator = currentUserID
account.Updater = currentUserID
if err := tx.WithContext(ctx).Create(account).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "创建企业账号失败")
}
return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionEnterpriseCreated, Summary: "创建企业", OperatorID: currentUserID,
Enterprise: enterprise, Shop: ownerShop,
Accounts: []accessauditapp.AccountChange{{
Account: account, Role: constants.AuditResourceRoleEnterpriseAccount,
AfterData: map[string]any{"status": account.Status, "credentials_configured": true},
}},
AfterData: enterpriseProfileData(enterprise),
})
})
if err != nil {
failureEnterprise := *enterprise
failureEnterprise.ID = 0
s.recordFailure(ctx, constants.AuditActionEnterpriseCreated, "创建企业失败", &failureEnterprise, ownerShop, nil, err)
return nil, err
}
ownerShopName := ""
if ownerShop != nil {
ownerShopName = ownerShop.ShopName
}
return &dto.CreateEnterpriseResp{
Enterprise: dto.EnterpriseItem{
ID: enterprise.ID,
EnterpriseName: enterprise.EnterpriseName,
EnterpriseCode: enterprise.EnterpriseCode,
OwnerShopID: enterprise.OwnerShopID,
OwnerShopName: ownerShopName,
LegalPerson: enterprise.LegalPerson,
ContactName: enterprise.ContactName,
ContactPhone: enterprise.ContactPhone,
LoginPhone: req.LoginPhone,
BusinessLicense: enterprise.BusinessLicense,
Province: enterprise.Province,
City: enterprise.City,
District: enterprise.District,
Address: enterprise.Address,
Status: enterprise.Status,
StatusName: constants.GetStatusName(enterprise.Status),
CreatedAt: enterprise.CreatedAt.Format("2006-01-02 15:04:05"),
},
AccountID: account.ID,
}, nil
}
// Update 更新企业信息
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateEnterpriseRequest) (*model.Enterprise, error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
if s.db == nil || s.accessAudit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "企业审计接缝未配置")
}
if err := middleware.CanManageEnterprise(ctx, id, s.enterpriseStore); err != nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
var enterprise *model.Enterprise
var before *model.Enterprise
var ownerShop *model.Shop
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
locked, err := lockEnterprise(ctx, tx, id)
if err != nil {
return err
}
beforeValue := *locked
before = &beforeValue
enterprise = locked
ownerShop = loadEnterpriseOwnerShop(tx, locked.OwnerShopID)
if req.EnterpriseCode != nil && *req.EnterpriseCode != locked.EnterpriseCode {
var count int64
if err := tx.Model(&model.Enterprise{}).Where("enterprise_code = ? AND id <> ?", *req.EnterpriseCode, id).Count(&count).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "检查企业编号失败")
}
if count > 0 {
return errors.New(errors.CodeEnterpriseCodeExists, "企业编号已存在")
}
locked.EnterpriseCode = *req.EnterpriseCode
}
applyEnterpriseUpdate(locked, req)
locked.Updater = currentUserID
if err := tx.Save(locked).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新企业失败")
}
if !enterpriseProfileChanged(before, locked) {
return nil
}
if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionEnterpriseUpdated, Summary: "更新企业基础资料", OperatorID: currentUserID,
Enterprise: locked, Shop: ownerShop,
BeforeData: enterpriseProfileData(before), AfterData: enterpriseProfileData(locked),
}); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "写入企业更新审计失败")
}
return nil
})
if err != nil {
if before != nil {
s.recordFailure(ctx, constants.AuditActionEnterpriseUpdated, "更新企业基础资料失败", before, ownerShop, nil, err)
}
return nil, err
}
return enterprise, nil
}
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
if s.db == nil || s.accessAudit == nil {
return errors.New(errors.CodeInvalidStatus, "企业审计接缝未配置")
}
if err := middleware.CanManageEnterprise(ctx, id, s.enterpriseStore); err != nil {
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
var before *model.Enterprise
var ownerShop *model.Shop
var accounts []*model.Account
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
enterprise, err := lockEnterprise(ctx, tx, id)
if err != nil {
return err
}
beforeValue := *enterprise
before = &beforeValue
ownerShop = loadEnterpriseOwnerShop(tx, enterprise.OwnerShopID)
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("enterprise_id = ?", id).Find(&accounts).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询企业账号失败")
}
enterprise.Status = status
enterprise.Updater = currentUserID
if err := tx.Save(enterprise).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新企业状态失败")
}
if err := tx.Model(&model.Account{}).
Where("enterprise_id = ?", id).
Updates(map[string]interface{}{
"status": status,
"updater": currentUserID,
}).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "同步更新企业账号状态失败")
}
if before.Status == status {
return nil
}
accountChanges := make([]accessauditapp.AccountChange, 0, len(accounts))
for _, account := range accounts {
accountChanges = append(accountChanges, accessauditapp.AccountChange{
Account: account, Role: constants.AuditResourceRoleEnterpriseAccount,
BeforeData: map[string]any{"status": account.Status}, AfterData: map[string]any{"status": status},
})
}
if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionEnterpriseStatusUpdated, Summary: "更新企业状态", OperatorID: currentUserID,
Enterprise: enterprise, Shop: ownerShop, Accounts: accountChanges,
BeforeData: map[string]any{"status": before.Status}, AfterData: map[string]any{"status": status},
}); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "写入企业状态审计失败")
}
return nil
})
if err != nil {
if before != nil {
s.recordFailure(ctx, constants.AuditActionEnterpriseStatusUpdated, "更新企业状态失败", before, ownerShop, accounts, err)
}
return err
}
return nil
}
func (s *Service) UpdatePassword(ctx context.Context, id uint, password string) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
if s.db == nil || s.accessAudit == nil {
return errors.New(errors.CodeInvalidStatus, "企业审计接缝未配置")
}
if err := middleware.CanManageEnterprise(ctx, id, s.enterpriseStore); err != nil {
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
enterprise, err := s.enterpriseStore.GetByID(ctx, id)
if err != nil {
return errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
}
var ownerShop *model.Shop
if enterprise.OwnerShopID != nil {
ownerShop, _ = s.shopStore.GetByID(ctx, *enterprise.OwnerShopID)
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
appErr := errors.Wrap(errors.CodeInternalError, err, "密码加密失败")
s.recordFailure(ctx, constants.AuditActionEnterprisePasswordUpdated, "更新企业账号密码失败", enterprise, ownerShop, nil, appErr)
return appErr
}
var accounts []*model.Account
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
enterprise, err = lockEnterprise(ctx, tx, id)
if err != nil {
return err
}
ownerShop = loadEnterpriseOwnerShop(tx, enterprise.OwnerShopID)
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("enterprise_id = ?", id).Find(&accounts).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询企业账号失败")
}
if err := tx.Model(&model.Account{}).Where("enterprise_id = ?", id).Updates(map[string]interface{}{
"password": string(hashedPassword), "updater": currentUserID,
}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新企业账号密码失败")
}
accountChanges := make([]accessauditapp.AccountChange, 0, len(accounts))
for _, account := range accounts {
accountChanges = append(accountChanges, accessauditapp.AccountChange{
Account: account, Role: constants.AuditResourceRoleEnterpriseAccount,
BeforeData: map[string]any{"credentials_configured": account.Password != ""},
AfterData: map[string]any{"credentials_configured": true, "state": "changed"},
})
}
if err := s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionEnterprisePasswordUpdated, Summary: "更新企业账号密码", OperatorID: currentUserID,
Enterprise: enterprise, Shop: ownerShop, Accounts: accountChanges,
BeforeData: map[string]any{"credentials_configured": len(accounts) > 0},
AfterData: map[string]any{"credentials_configured": len(accounts) > 0, "state": "changed"},
}); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "写入企业改密审计失败")
}
return nil
})
if err != nil {
if enterprise != nil {
s.recordFailure(ctx, constants.AuditActionEnterprisePasswordUpdated, "更新企业账号密码失败", enterprise, ownerShop, accounts, err)
}
return err
}
return nil
}
func applyEnterpriseUpdate(enterprise *model.Enterprise, req *dto.UpdateEnterpriseRequest) {
if req.EnterpriseName != nil {
enterprise.EnterpriseName = *req.EnterpriseName
}
if req.LegalPerson != nil {
enterprise.LegalPerson = *req.LegalPerson
}
if req.ContactName != nil {
enterprise.ContactName = *req.ContactName
}
if req.ContactPhone != nil {
enterprise.ContactPhone = *req.ContactPhone
}
if req.BusinessLicense != nil {
enterprise.BusinessLicense = *req.BusinessLicense
}
if req.Province != nil {
enterprise.Province = *req.Province
}
if req.City != nil {
enterprise.City = *req.City
}
if req.District != nil {
enterprise.District = *req.District
}
if req.Address != nil {
enterprise.Address = *req.Address
}
}
func enterpriseProfileData(enterprise *model.Enterprise) map[string]any {
return map[string]any{
"enterprise_name": enterprise.EnterpriseName, "enterprise_code": enterprise.EnterpriseCode,
"owner_shop_id": enterprise.OwnerShopID, "legal_person": enterprise.LegalPerson,
"contact_name": enterprise.ContactName, "contact_phone": enterprise.ContactPhone,
"business_license": enterprise.BusinessLicense, "province": enterprise.Province,
"city": enterprise.City, "district": enterprise.District, "address": enterprise.Address,
"status": enterprise.Status,
}
}
func enterpriseProfileChanged(before, after *model.Enterprise) bool {
return before.EnterpriseName != after.EnterpriseName || before.EnterpriseCode != after.EnterpriseCode ||
before.LegalPerson != after.LegalPerson || before.ContactName != after.ContactName ||
before.ContactPhone != after.ContactPhone || before.BusinessLicense != after.BusinessLicense ||
before.Province != after.Province || before.City != after.City || before.District != after.District ||
before.Address != after.Address
}
func lockEnterprise(ctx context.Context, tx *gorm.DB, id uint) (*model.Enterprise, error) {
var enterprise model.Enterprise
query := middleware.ApplyOwnerShopFilter(ctx, tx.WithContext(ctx).Where("id = ?", id))
if err := query.Clauses(clause.Locking{Strength: "UPDATE"}).First(&enterprise).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询企业失败")
}
return &enterprise, nil
}
func loadEnterpriseOwnerShop(tx *gorm.DB, ownerShopID *uint) *model.Shop {
if ownerShopID == nil {
return nil
}
var shop model.Shop
if err := tx.Unscoped().First(&shop, *ownerShopID).Error; err != nil {
return nil
}
return &shop
}
func (s *Service) recordFailure(
ctx context.Context,
actionCode, summary string,
enterprise *model.Enterprise,
ownerShop *model.Shop,
accounts []*model.Account,
originalErr error,
) {
accountChanges := make([]accessauditapp.AccountChange, 0, len(accounts))
for _, account := range accounts {
accountChanges = append(accountChanges, accessauditapp.AccountChange{
Account: account, Role: constants.AuditResourceRoleEnterpriseAccount,
})
}
accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{
ActionCode: actionCode, Summary: summary, Result: enterpriseAuditFailureResult(originalErr),
OperatorID: middleware.GetUserIDFromContext(ctx), Enterprise: enterprise, Shop: ownerShop,
Accounts: accountChanges, SubjectVisibility: constants.AuditSubjectInternalOnly,
}, originalErr)
}
func enterpriseAuditFailureResult(err error) string {
var appErr *errors.AppError
if stderrors.As(err, &appErr) {
switch appErr.Code {
case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeEnterpriseNotFound,
errors.CodeEnterpriseCodeExists, errors.CodePhoneExists, errors.CodeShopNotFound:
return constants.AuditResultDenied
}
}
return constants.AuditResultFailed
}
func (s *Service) GetByID(ctx context.Context, id uint) (*model.Enterprise, error) {
enterprise, err := s.enterpriseStore.GetByID(ctx, id)
if err != nil {
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
}
return enterprise, nil
}
func (s *Service) List(ctx context.Context, req *dto.EnterpriseListReq) (*dto.EnterprisePageResult, error) {
opts := &store.QueryOptions{
Page: req.Page,
PageSize: req.PageSize,
}
if opts.Page == 0 {
opts.Page = 1
}
if opts.PageSize == 0 {
opts.PageSize = constants.DefaultPageSize
}
filters := make(map[string]interface{})
if req.EnterpriseName != "" {
filters["enterprise_name"] = req.EnterpriseName
}
if req.ContactPhone != "" {
filters["contact_phone"] = req.ContactPhone
}
if req.OwnerShopID != nil {
filters["owner_shop_id"] = *req.OwnerShopID
}
if req.Status != nil {
filters["status"] = *req.Status
}
enterprises, total, err := s.enterpriseStore.List(ctx, opts, filters)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询企业列表失败")
}
enterpriseIDs := make([]uint, 0, len(enterprises))
shopIDs := make([]uint, 0)
for _, e := range enterprises {
enterpriseIDs = append(enterpriseIDs, e.ID)
if e.OwnerShopID != nil {
shopIDs = append(shopIDs, *e.OwnerShopID)
}
}
accountMap := make(map[uint]string)
if len(enterpriseIDs) > 0 {
var accounts []model.Account
s.db.WithContext(ctx).Where("enterprise_id IN ?", enterpriseIDs).Find(&accounts)
for _, acc := range accounts {
if acc.EnterpriseID != nil {
accountMap[*acc.EnterpriseID] = acc.Phone
}
}
}
shopMap := make(map[uint]string)
if len(shopIDs) > 0 {
var shops []model.Shop
// 使用 Unscoped() 包含已删除的店铺,确保能显示店铺名称
s.db.WithContext(ctx).Unscoped().Where("id IN ?", shopIDs).Find(&shops)
for _, shop := range shops {
shopMap[shop.ID] = shop.ShopName
}
}
items := make([]dto.EnterpriseItem, 0, len(enterprises))
for _, e := range enterprises {
ownerShopName := ""
if e.OwnerShopID != nil {
ownerShopName = shopMap[*e.OwnerShopID]
}
items = append(items, dto.EnterpriseItem{
ID: e.ID,
EnterpriseName: e.EnterpriseName,
EnterpriseCode: e.EnterpriseCode,
OwnerShopID: e.OwnerShopID,
OwnerShopName: ownerShopName,
LegalPerson: e.LegalPerson,
ContactName: e.ContactName,
ContactPhone: e.ContactPhone,
LoginPhone: accountMap[e.ID],
BusinessLicense: e.BusinessLicense,
Province: e.Province,
City: e.City,
District: e.District,
Address: e.Address,
Status: e.Status,
StatusName: constants.GetStatusName(e.Status),
CreatedAt: e.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
return &dto.EnterprisePageResult{
Items: items,
Total: total,
Page: opts.Page,
Size: opts.PageSize,
}, nil
}