固化七月迭代审计治理进展以隔离线上热修

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 参数类型错误未通过。
This commit is contained in:
2026-08-03 09:47:22 +08:00
parent cf2ff0ac1c
commit b3499adfca
114 changed files with 16961 additions and 2782 deletions

View File

@@ -3,11 +3,13 @@ package shop
import (
"context"
stderrors "errors"
"strings"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
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/pkg/constants"
@@ -17,12 +19,13 @@ import (
// CreateService 收口平台与代理创建店铺的完整事务。
type CreateService struct {
db *gorm.DB
db *gorm.DB
audit accessauditapp.Writer
}
// NewCreateService 创建店铺创建事务脚本。
func NewCreateService(db *gorm.DB) *CreateService {
return &CreateService{db: db}
func NewCreateService(db *gorm.DB, audit accessauditapp.Writer) *CreateService {
return &CreateService{db: db, audit: audit}
}
// Create 按操作者类型执行平台显式归属或代理安全继承。
@@ -37,26 +40,26 @@ func (s *CreateService) Create(ctx context.Context, request *dto.CreateShopReque
case constants.UserTypeSuperAdmin, constants.UserTypePlatform:
case constants.UserTypeAgent:
if request.BusinessOwnerAccountIDSet {
return nil, errors.New(errors.CodeForbidden, "无权限设置店铺业务员")
return s.fail(ctx, request, errors.New(errors.CodeForbidden, "无权限设置店铺业务员"))
}
if request.ParentID == nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
return s.fail(ctx, request, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在"))
}
if err := middleware.CanManageShop(ctx, *request.ParentID); err != nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
return s.fail(ctx, request, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在"))
}
resolver = resolveInheritedBusinessOwner
default:
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
return s.fail(ctx, request, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在"))
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.InitPassword), bcrypt.DefaultCost)
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
return s.fail(ctx, request, errors.Wrap(errors.CodeInternalError, err, "密码哈希失败"))
}
var response *dto.ShopResponse
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
created, createErr := createShop(tx, request, operatorID, string(hashedPassword), resolver)
created, createErr := createShop(ctx, tx, request, operatorID, string(hashedPassword), resolver, s.audit)
if createErr != nil {
return createErr
}
@@ -64,14 +67,14 @@ func (s *CreateService) Create(ctx context.Context, request *dto.CreateShopReque
return nil
})
if err != nil {
return nil, err
return s.fail(ctx, request, err)
}
return response, nil
}
type businessOwnerResolver func(*gorm.DB, *dto.CreateShopRequest, *model.Shop) (*uint, error)
func createShop(tx *gorm.DB, request *dto.CreateShopRequest, operatorID uint, hashedPassword string, resolveOwner businessOwnerResolver) (*dto.ShopResponse, error) {
func createShop(ctx context.Context, tx *gorm.DB, request *dto.CreateShopRequest, operatorID uint, hashedPassword string, resolveOwner businessOwnerResolver, audit accessauditapp.Writer) (*dto.ShopResponse, error) {
if exists, err := recordExists(tx, &model.Shop{}, "shop_code = ?", request.ShopCode); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "校验店铺编号失败")
} else if exists {
@@ -157,9 +160,77 @@ func createShop(tx *gorm.DB, request *dto.CreateShopRequest, operatorID uint, ha
if err := fillBusinessOwnerResponse(tx, shop, response); err != nil {
return nil, err
}
if audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "店铺创建审计接缝未配置")
}
if err := audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionShopCreated, Summary: "创建店铺",
OperatorID: operatorID, Shop: shop, ParentShop: parent,
AfterData: shopCreationData(shop),
}); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "写入店铺创建审计失败")
}
if ownerID != nil {
if err := audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionShopBusinessOwnerUpdated, Summary: "设置店铺业务员归属",
OperatorID: operatorID, Shop: shop, ParentShop: parent,
Accounts: businessOwnerAuditAccounts(tx, nil, ownerID),
AfterData: map[string]any{"business_owner_account_id": ownerID},
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "店铺业务员归属已设置",
}); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "写入店铺业务员审计失败")
}
}
return response, nil
}
func (s *CreateService) fail(ctx context.Context, request *dto.CreateShopRequest, originalErr error) (*dto.ShopResponse, error) {
shop := &model.Shop{ShopName: request.ShopName, ShopCode: request.ShopCode, ParentID: request.ParentID}
var parent *model.Shop
if request.ParentID != nil {
parent = &model.Shop{}
parent.ID = *request.ParentID
}
accessauditapp.RecordFailure(ctx, s.db, s.audit, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionShopCreated, Summary: "创建店铺失败", Result: shopAuditFailureResult(originalErr),
OperatorID: middleware.GetUserIDFromContext(ctx), Shop: shop, ParentShop: parent,
}, originalErr)
return nil, originalErr
}
func shopCreationData(shop *model.Shop) map[string]any {
data := shopProfileData(shop)
data["shop_code"] = shop.ShopCode
data["parent_id"] = shop.ParentID
data["level"] = shop.Level
return data
}
func shopProfileData(shop *model.Shop) map[string]any {
return map[string]any{
"shop_name": shop.ShopName, "contact_name": shop.ContactName, "contact_phone": shop.ContactPhone,
"province": shop.Province, "city": shop.City, "district": shop.District, "address": shop.Address,
}
}
func shopProfileChanged(before, after *model.Shop) bool {
return before.ShopName != after.ShopName || before.ContactName != after.ContactName ||
before.ContactPhone != after.ContactPhone || before.Province != after.Province || before.City != after.City ||
before.District != after.District || before.Address != after.Address
}
func shopAuditFailureResult(err error) string {
var appErr *errors.AppError
if stderrors.As(err, &appErr) {
switch appErr.Code {
case errors.CodeForbidden, errors.CodeInvalidParam, errors.CodeNotFound, errors.CodeInvalidParentID,
errors.CodeShopLevelExceeded, errors.CodeShopCodeExists, errors.CodeUsernameExists, errors.CodePhoneExists:
return constants.AuditResultDenied
}
}
return constants.AuditResultFailed
}
func resolveParent(tx *gorm.DB, parentID *uint) (*model.Shop, int, error) {
if parentID == nil {
return nil, 1, nil

View File

@@ -6,6 +6,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
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/pkg/constants"
@@ -15,12 +16,13 @@ import (
// UpdateService 收口店铺资料与业务员归属的简单写事务脚本。
type UpdateService struct {
db *gorm.DB
db *gorm.DB
audit accessauditapp.Writer
}
// NewUpdateService 创建店铺更新事务脚本。
func NewUpdateService(db *gorm.DB) *UpdateService {
return &UpdateService{db: db}
func NewUpdateService(db *gorm.DB, audit accessauditapp.Writer) *UpdateService {
return &UpdateService{db: db, audit: audit}
}
// Update 更新单个店铺;业务员归属变化不会传播到其他店铺。
@@ -50,11 +52,16 @@ func (s *UpdateService) Update(ctx context.Context, shopID uint, request *dto.Up
return nil, errors.New(errors.CodeUnauthorized)
}
var response *dto.ShopResponse
var beforeShop *model.Shop
var parentShop *model.Shop
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var shop model.Shop
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&shop, shopID).Error; err != nil {
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
before := shop
beforeShop = &before
parentShop = loadAuditParentShop(tx, shop.ParentID)
if request.BusinessOwnerAccountIDSet {
ownerID, err := validateUpdatedBusinessOwner(tx, request.BusinessOwnerAccountID)
if err != nil {
@@ -88,14 +95,160 @@ func (s *UpdateService) Update(ctx context.Context, shopID uint, request *dto.Up
if err := fillBusinessOwnerResponse(tx, &shop, response); err != nil {
return err
}
if shopProfileChanged(&before, &shop) {
if s.audit == nil {
return errors.New(errors.CodeInvalidStatus, "店铺更新审计接缝未配置")
}
if err := s.audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionShopUpdated, Summary: "更新店铺基础资料",
OperatorID: operatorID, Shop: &shop, ParentShop: parentShop,
BeforeData: shopProfileData(&before), AfterData: shopProfileData(&shop),
}); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "写入店铺更新审计失败")
}
}
if err := s.writeStateAudits(ctx, tx, &before, &shop, parentShop, operatorID); err != nil {
return err
}
return nil
})
if err != nil {
if beforeShop != nil && requestedShopProfileChanged(beforeShop, request) {
accessauditapp.RecordFailure(ctx, s.db, s.audit, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionShopUpdated, Summary: "更新店铺基础资料失败", Result: shopAuditFailureResult(err),
OperatorID: operatorID, Shop: beforeShop, ParentShop: parentShop,
}, err)
}
s.recordStateFailures(ctx, beforeShop, parentShop, request, operatorID, err)
return nil, err
}
return response, nil
}
func (s *UpdateService) writeStateAudits(ctx context.Context, tx *gorm.DB, before, after, parent *model.Shop, operatorID uint) error {
if s.audit == nil && (before.Status != after.Status || !sameOptionalUint(before.BusinessOwnerAccountID, after.BusinessOwnerAccountID) || before.ClientLoginDisabled != after.ClientLoginDisabled) {
return errors.New(errors.CodeInvalidStatus, "店铺状态审计接缝未配置")
}
if before.Status != after.Status {
action, summary, subject := constants.AuditActionShopDisabled, "禁用店铺", "店铺已禁用"
if after.Status == constants.StatusEnabled {
action, summary, subject = constants.AuditActionShopEnabled, "启用店铺", "店铺已启用"
}
if err := s.audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: action, Summary: summary, OperatorID: operatorID, Shop: after, ParentShop: parent,
BeforeData: map[string]any{"status": before.Status}, AfterData: map[string]any{"status": after.Status},
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: subject,
}); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "写入店铺状态审计失败")
}
}
if !sameOptionalUint(before.BusinessOwnerAccountID, after.BusinessOwnerAccountID) {
accounts := businessOwnerAuditAccounts(tx, before.BusinessOwnerAccountID, after.BusinessOwnerAccountID)
if err := s.audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionShopBusinessOwnerUpdated, Summary: "更新店铺业务员归属",
OperatorID: operatorID, Shop: after, ParentShop: parent, Accounts: accounts,
BeforeData: map[string]any{"business_owner_account_id": before.BusinessOwnerAccountID},
AfterData: map[string]any{"business_owner_account_id": after.BusinessOwnerAccountID},
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "店铺业务员归属已更新",
}); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "写入店铺业务员审计失败")
}
}
if before.ClientLoginDisabled != after.ClientLoginDisabled {
subject := "店铺 C 端登录限制已解除"
if after.ClientLoginDisabled {
subject = "店铺 C 端登录已限制"
}
if err := s.audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
ActionCode: constants.AuditActionShopClientLoginLimitUpdated, Summary: "更新店铺 C 端登录限制",
OperatorID: operatorID, Shop: after, ParentShop: parent,
BeforeData: map[string]any{"client_login_disabled": before.ClientLoginDisabled},
AfterData: map[string]any{"client_login_disabled": after.ClientLoginDisabled},
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: subject,
}); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "写入店铺登录限制审计失败")
}
}
return nil
}
func (s *UpdateService) recordStateFailures(ctx context.Context, shop, parent *model.Shop, request *dto.UpdateShopRequest, operatorID uint, originalErr error) {
if shop == nil {
return
}
record := func(action, summary string) {
accessauditapp.RecordFailure(ctx, s.db, s.audit, accessauditapp.ChangeAudit{
ActionCode: action, Summary: summary, Result: shopAuditFailureResult(originalErr),
OperatorID: operatorID, Shop: shop, ParentShop: parent, SubjectVisibility: constants.AuditSubjectInternalOnly,
}, originalErr)
}
if shop.Status != request.Status {
action := constants.AuditActionShopDisabled
if request.Status == constants.StatusEnabled {
action = constants.AuditActionShopEnabled
}
record(action, "更新店铺状态失败")
}
if request.BusinessOwnerAccountIDSet && !sameOptionalUint(shop.BusinessOwnerAccountID, request.BusinessOwnerAccountID) {
record(constants.AuditActionShopBusinessOwnerUpdated, "更新店铺业务员归属失败")
}
if request.ClientLoginDisabled != nil && shop.ClientLoginDisabled != *request.ClientLoginDisabled {
record(constants.AuditActionShopClientLoginLimitUpdated, "更新店铺 C 端登录限制失败")
}
}
func businessOwnerAuditAccounts(tx *gorm.DB, beforeID, afterID *uint) []accessauditapp.AccountChange {
changes := make([]accessauditapp.AccountChange, 0, 2)
if account := loadAuditAccount(tx, beforeID); account != nil {
changes = append(changes, accessauditapp.AccountChange{
Account: account, Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleShopPreviousBusinessOwner,
BeforeData: map[string]any{"assigned": true}, AfterData: map[string]any{"assigned": false},
})
}
if account := loadAuditAccount(tx, afterID); account != nil {
changes = append(changes, accessauditapp.AccountChange{
Account: account, Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleShopBusinessOwner,
BeforeData: map[string]any{"assigned": false}, AfterData: map[string]any{"assigned": true},
})
}
return changes
}
func loadAuditAccount(tx *gorm.DB, accountID *uint) *model.Account {
if accountID == nil {
return nil
}
var account model.Account
if err := tx.Unscoped().First(&account, *accountID).Error; err != nil {
return nil
}
return &account
}
func sameOptionalUint(left, right *uint) bool {
if left == nil || right == nil {
return left == nil && right == nil
}
return *left == *right
}
func requestedShopProfileChanged(shop *model.Shop, request *dto.UpdateShopRequest) bool {
return shop.ShopName != request.ShopName || shop.ContactName != request.ContactName ||
shop.ContactPhone != request.ContactPhone || shop.Province != request.Province || shop.City != request.City ||
shop.District != request.District || shop.Address != request.Address
}
func loadAuditParentShop(tx *gorm.DB, parentID *uint) *model.Shop {
if parentID == nil {
return nil
}
var parent model.Shop
if err := tx.Unscoped().First(&parent, *parentID).Error; err != nil {
return nil
}
return &parent
}
func validateUpdatedBusinessOwner(tx *gorm.DB, requestedID *uint) (*uint, error) {
if requestedID == nil {
return nil, nil