收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -0,0 +1,93 @@
package shop_series_grant
import (
"context"
"strconv"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
type allocationAuditChange struct {
before map[string]any
after map[string]any
}
func (s *Service) appendGrantAudit(
ctx context.Context,
tx *gorm.DB,
actionCode, summary string,
allocation *model.ShopSeriesAllocation,
series *model.PackageSeries,
shop *model.Shop,
beforeData, afterData map[string]any,
packageAllocations []*model.ShopPackageAllocation,
priceHistories []*model.ShopPackageAllocationPriceHistory,
packageChanges map[uint]allocationAuditChange,
) error {
if s.auditWriter == nil {
return errors.New(errors.CodeInvalidStatus, "店铺系列授权统一审计接缝未配置")
}
resources := []audit.ResourceInput{
audit.ShopSeriesAllocationResource(allocation, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleShopSeriesAllocation, beforeData, afterData),
audit.PackageSeriesResource(series, constants.AuditResourceRelationReference, constants.AuditResourceRolePackageSeries, nil, nil),
audit.ShopResource(shop, constants.AuditResourceRelationReference, constants.AuditResourceRolePackageConfigShop),
}
for _, packageAllocation := range packageAllocations {
change, ok := packageChanges[packageAllocation.ID]
if !ok {
change.after = map[string]any{
"cost_price": packageAllocation.CostPrice, "retail_price": packageAllocation.RetailPrice,
"expiry_base_override": packageAllocation.ExpiryBaseOverride, "status": packageAllocation.Status,
}
}
resources = append(resources, audit.ShopPackageAllocationResource(
packageAllocation, constants.AuditResourceRelationAffected, constants.AuditResourceRoleShopPackageAllocation, change.before, change.after,
))
var pkg model.Package
if err := tx.WithContext(ctx).Unscoped().Where("id = ?", packageAllocation.PackageID).First(&pkg).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询系列授权套餐审计快照失败")
}
resources = append(resources, audit.PackageResource(&pkg, constants.AuditResourceRelationReference, constants.AuditResourceRolePackageTarget, nil, nil))
}
for _, history := range priceHistories {
resources = append(resources, audit.ShopPackagePriceHistoryResource(history))
}
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopeShop,
ScopeID: strconv.FormatUint(uint64(shop.ID), 10), Result: constants.AuditResultSuccess,
Resources: resources,
})
}
func (s *Service) recordGrantFailure(ctx context.Context, actionCode, summary string, allocation *model.ShopSeriesAllocation, series *model.PackageSeries, shop *model.Shop, beforeData map[string]any, businessErr error) {
if allocation == nil || series == nil || shop == nil {
return
}
s.auditWriter.RecordFailure(ctx, s.db, audit.AppendInput{
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopeShop,
ScopeID: strconv.FormatUint(uint64(shop.ID), 10), Resources: []audit.ResourceInput{
audit.ShopSeriesAllocationResource(allocation, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleShopSeriesAllocation, beforeData, nil),
audit.PackageSeriesResource(series, constants.AuditResourceRelationReference, constants.AuditResourceRolePackageSeries, nil, nil),
audit.ShopResource(shop, constants.AuditResourceRelationReference, constants.AuditResourceRolePackageConfigShop),
},
}, businessErr)
}
func grantData(allocation *model.ShopSeriesAllocation) map[string]any {
if allocation == nil {
return nil
}
return map[string]any{
"one_time_commission_amount": allocation.OneTimeCommissionAmount,
"commission_tiers": allocation.CommissionTiersJSON,
"enable_force_recharge": allocation.EnableForceRecharge,
"force_recharge_amount": allocation.ForceRechargeAmount,
"force_recharge_trigger_type": allocation.ForceRechargeTriggerType,
"status": allocation.Status,
}
}

View File

@@ -6,6 +6,7 @@ import (
"context"
"time"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
@@ -29,6 +30,7 @@ type Service struct {
packageStore *postgres.PackageStore
packageSeriesStore *postgres.PackageSeriesStore
logger *zap.Logger
auditWriter *audit.Writer
}
// initialGrantPackage 保存通过创建前校验的套餐及其请求价格。
@@ -47,6 +49,7 @@ func New(
packageStore *postgres.PackageStore,
packageSeriesStore *postgres.PackageSeriesStore,
logger *zap.Logger,
auditWriter *audit.Writer,
) *Service {
return &Service{
db: db,
@@ -57,6 +60,7 @@ func New(
packageStore: packageStore,
packageSeriesStore: packageSeriesStore,
logger: logger,
auditWriter: auditWriter,
}
}
@@ -299,7 +303,7 @@ func (s *Service) buildGrantResponse(ctx context.Context, allocation *model.Shop
// Create 创建系列授权
// POST /api/admin/shop-series-grants
func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequest) (*dto.ShopSeriesGrantResponse, error) {
func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequest) (_ *dto.ShopSeriesGrantResponse, retErr error) {
expiryBaseOverride, err := packagepkg.ValidateExpiryBaseOverride(req.ExpiryBaseOverride, req.ExpiryBaseOverrideSet)
if err != nil {
return nil, err
@@ -324,6 +328,17 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
if err != nil {
return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
allocation := &model.ShopSeriesAllocation{
ShopID: req.ShopID, SeriesID: req.SeriesID, Status: constants.StatusEnabled, CommissionTiersJSON: "[]",
}
businessCommitted := false
defer func() {
if retErr != nil && !businessCommitted {
failedAllocation := *allocation
failedAllocation.ID = 0
s.recordGrantFailure(ctx, constants.AuditActionShopSeriesGrantCreated, "创建店铺套餐系列授权失败", &failedAllocation, series, targetShop, nil, retErr)
}
}()
// 2. 检查重复授权
exists, err := s.shopSeriesAllocationStore.ExistsByShopAndSeries(ctx, req.ShopID, req.SeriesID)
@@ -354,13 +369,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
}
// 4. 参数验证:仅启用一次性佣金的系列才需要配置佣金金额
allocation := &model.ShopSeriesAllocation{
ShopID: req.ShopID,
SeriesID: req.SeriesID,
AllocatorShopID: allocatorShopID,
Status: constants.StatusEnabled,
CommissionTiersJSON: "[]",
}
allocation.AllocatorShopID = allocatorShopID
allocation.Creator = operatorID
allocation.Updater = operatorID
@@ -423,6 +432,8 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
}
// 6. 事务中创建 ShopSeriesAllocation + N 条 ShopPackageAllocation
createdPackageAllocations := make([]*model.ShopPackageAllocation, 0, len(req.Packages))
createdPriceHistories := make([]*model.ShopPackageAllocationPriceHistory, 0, len(req.Packages))
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var lockedTargetShop model.Shop
if lockErr := tx.WithContext(ctx).
@@ -476,23 +487,28 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
if err := txPkgStore.Create(ctx, pkgAlloc); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建套餐分配失败")
}
if err := txHistoryStore.Create(ctx, &model.ShopPackageAllocationPriceHistory{
history := &model.ShopPackageAllocationPriceHistory{
AllocationID: pkgAlloc.ID,
OldCostPrice: 0,
NewCostPrice: *item.CostPrice,
ChangeReason: "初始授权",
ChangedBy: operatorID,
EffectiveFrom: time.Now(),
}); err != nil {
}
if err := txHistoryStore.Create(ctx, history); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建套餐价格历史失败")
}
createdPackageAllocations = append(createdPackageAllocations, pkgAlloc)
createdPriceHistories = append(createdPriceHistories, history)
}
return nil
return s.appendGrantAudit(ctx, tx, constants.AuditActionShopSeriesGrantCreated, "创建店铺套餐系列授权", allocation, series, targetShop,
nil, grantData(allocation), createdPackageAllocations, createdPriceHistories, nil)
})
if err != nil {
return nil, err
}
businessCommitted = true
// 事务提交后构建完整响应(此时 packages 已可查询到)
return s.buildGrantResponse(ctx, allocation, series, config)
@@ -638,7 +654,7 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesGrantListRequest)
// Update 更新系列授权
// PUT /api/admin/shop-series-grants/:id
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeriesGrantRequest) (*dto.ShopSeriesGrantResponse, error) {
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeriesGrantRequest) (_ *dto.ShopSeriesGrantResponse, retErr error) {
operatorID := middleware.GetUserIDFromContext(ctx)
operatorShopID := middleware.GetShopIDFromContext(ctx)
@@ -649,16 +665,27 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询授权记录失败")
}
before := *allocation
series, seriesErr := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
if seriesErr != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, seriesErr, "查询套餐系列失败")
}
shop, shopErr := s.shopStore.GetByID(ctx, allocation.ShopID)
if shopErr != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, shopErr, "查询授权店铺失败")
}
businessCommitted := false
defer func() {
if retErr != nil && !businessCommitted {
s.recordGrantFailure(ctx, constants.AuditActionShopSeriesGrantUpdated, "更新店铺套餐系列授权失败", &before, series, shop, grantData(&before), retErr)
}
}()
// 代理只能修改自己分配出去的授权
if operatorShopID > 0 && allocation.AllocatorShopID != operatorShopID {
return nil, errors.New(errors.CodeForbidden, "无权限操作该授权记录")
}
series, err := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
}
config, err := series.GetOneTimeCommissionConfig()
if err != nil || config == nil {
return nil, errors.New(errors.CodeInternalError, "获取系列佣金配置失败")
@@ -713,16 +740,23 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
}
allocation.Updater = operatorID
if err := s.shopSeriesAllocationStore.Update(ctx, allocation); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "更新授权记录失败")
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := postgres.NewShopSeriesAllocationStore(tx).Update(ctx, allocation); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新授权记录失败")
}
return s.appendGrantAudit(ctx, tx, constants.AuditActionShopSeriesGrantUpdated, "更新店铺套餐系列授权", allocation, series, shop,
grantData(&before), grantData(allocation), nil, nil, nil)
}); err != nil {
return nil, err
}
businessCommitted = true
return s.buildGrantResponse(ctx, allocation, series, config)
}
// ManagePackages 管理授权套餐(新增/更新/删除)
// PUT /api/admin/shop-series-grants/:id/packages
func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGrantPackagesRequest) (*dto.ShopSeriesGrantResponse, error) {
func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGrantPackagesRequest) (_ *dto.ShopSeriesGrantResponse, retErr error) {
expiryBaseOverride, err := packagepkg.ValidateExpiryBaseOverride(req.ExpiryBaseOverride, req.ExpiryBaseOverrideSet)
if err != nil {
return nil, err
@@ -737,12 +771,29 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询授权记录失败")
}
series, seriesErr := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
if seriesErr != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, seriesErr, "查询套餐系列失败")
}
shop, shopErr := s.shopStore.GetByID(ctx, allocation.ShopID)
if shopErr != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, shopErr, "查询授权店铺失败")
}
businessCommitted := false
defer func() {
if retErr != nil && !businessCommitted {
s.recordGrantFailure(ctx, constants.AuditActionShopSeriesGrantPackagesManaged, "管理店铺系列套餐授权失败", allocation, series, shop, nil, retErr)
}
}()
// 代理只能操作自己分配的授权
if operatorShopID > 0 && allocation.AllocatorShopID != operatorShopID {
return nil, errors.New(errors.CodeForbidden, "无权限操作该授权记录")
}
affectedAllocations := make([]*model.ShopPackageAllocation, 0, len(req.Packages))
priceHistories := make([]*model.ShopPackageAllocationPriceHistory, 0, len(req.Packages))
packageChanges := make(map[uint]allocationAuditChange, len(req.Packages))
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txPkgStore := postgres.NewShopPackageAllocationStore(tx)
txHistoryStore := postgres.NewShopPackageAllocationPriceHistoryStore(tx)
@@ -758,6 +809,11 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
if deleteErr := txPkgStore.Delete(ctx, existing.ID); deleteErr != nil {
return errors.Wrap(errors.CodeDatabaseError, deleteErr, "删除套餐分配失败")
}
affectedAllocations = append(affectedAllocations, existing)
packageChanges[existing.ID] = allocationAuditChange{
before: map[string]any{"cost_price": existing.CostPrice, "status": existing.Status},
after: map[string]any{"deleted": true},
}
continue
}
@@ -789,16 +845,22 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
return errors.Wrap(errors.CodeDatabaseError, updateErr, "更新套餐分配失败")
}
if oldPrice != costPrice {
if historyErr := txHistoryStore.Create(ctx, &model.ShopPackageAllocationPriceHistory{
history := &model.ShopPackageAllocationPriceHistory{
AllocationID: existing.ID,
OldCostPrice: oldPrice,
NewCostPrice: costPrice,
ChangeReason: "手动调价",
ChangedBy: operatorID,
EffectiveFrom: time.Now(),
}); historyErr != nil {
}
if historyErr := txHistoryStore.Create(ctx, history); historyErr != nil {
return errors.Wrap(errors.CodeDatabaseError, historyErr, "创建套餐价格历史失败")
}
priceHistories = append(priceHistories, history)
affectedAllocations = append(affectedAllocations, existing)
packageChanges[existing.ID] = allocationAuditChange{
before: map[string]any{"cost_price": oldPrice}, after: map[string]any{"cost_price": costPrice},
}
}
} else {
pkg, pkgErr := s.packageStore.GetByID(ctx, item.PackageID)
@@ -831,23 +893,35 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
if createErr := txPkgStore.Create(ctx, pkgAlloc); createErr != nil {
return errors.Wrap(errors.CodeDatabaseError, createErr, "创建套餐分配失败")
}
if historyErr := txHistoryStore.Create(ctx, &model.ShopPackageAllocationPriceHistory{
history := &model.ShopPackageAllocationPriceHistory{
AllocationID: pkgAlloc.ID,
OldCostPrice: 0,
NewCostPrice: costPrice,
ChangeReason: "新增授权",
ChangedBy: operatorID,
EffectiveFrom: time.Now(),
}); historyErr != nil {
}
if historyErr := txHistoryStore.Create(ctx, history); historyErr != nil {
return errors.Wrap(errors.CodeDatabaseError, historyErr, "创建套餐价格历史失败")
}
affectedAllocations = append(affectedAllocations, pkgAlloc)
priceHistories = append(priceHistories, history)
packageChanges[pkgAlloc.ID] = allocationAuditChange{after: map[string]any{
"cost_price": pkgAlloc.CostPrice, "retail_price": pkgAlloc.RetailPrice,
"expiry_base_override": pkgAlloc.ExpiryBaseOverride, "status": pkgAlloc.Status,
}}
}
}
return nil
if len(affectedAllocations) == 0 {
return nil
}
return s.appendGrantAudit(ctx, tx, constants.AuditActionShopSeriesGrantPackagesManaged, "管理店铺系列套餐授权", allocation, series, shop,
nil, nil, affectedAllocations, priceHistories, packageChanges)
})
if err != nil {
return nil, err
}
businessCommitted = true
// 重新查询最新状态
return s.Get(ctx, id)
@@ -855,7 +929,7 @@ func (s *Service) ManagePackages(ctx context.Context, id uint, req *dto.ManageGr
// Delete 删除系列授权(软删除)
// DELETE /api/admin/shop-series-grants/:id
func (s *Service) Delete(ctx context.Context, id uint) error {
func (s *Service) Delete(ctx context.Context, id uint) (retErr error) {
operatorShopID := middleware.GetShopIDFromContext(ctx)
allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, id)
@@ -865,6 +939,19 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询授权记录失败")
}
series, seriesErr := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
if seriesErr != nil {
return errors.Wrap(errors.CodeDatabaseError, seriesErr, "查询套餐系列失败")
}
shop, shopErr := s.shopStore.GetByID(ctx, allocation.ShopID)
if shopErr != nil {
return errors.Wrap(errors.CodeDatabaseError, shopErr, "查询授权店铺失败")
}
defer func() {
if retErr != nil {
s.recordGrantFailure(ctx, constants.AuditActionShopSeriesGrantDeleted, "删除店铺套餐系列授权失败", allocation, series, shop, grantData(allocation), retErr)
}
}()
// 代理只能删除自己分配的授权
if operatorShopID > 0 && allocation.AllocatorShopID != operatorShopID {
@@ -886,7 +973,12 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
txPkgStore := postgres.NewShopPackageAllocationStore(tx)
pkgAllocations, _ := txPkgStore.GetBySeriesAllocationID(ctx, id)
packageChanges := make(map[uint]allocationAuditChange, len(pkgAllocations))
for _, pa := range pkgAllocations {
packageChanges[pa.ID] = allocationAuditChange{
before: map[string]any{"cost_price": pa.CostPrice, "retail_price": pa.RetailPrice, "status": pa.Status},
after: map[string]any{"deleted": true},
}
if delErr := txPkgStore.Delete(ctx, pa.ID); delErr != nil {
return errors.Wrap(errors.CodeDatabaseError, delErr, "删除套餐分配失败")
}
@@ -895,6 +987,7 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
if delErr := txSeriesStore.Delete(ctx, id); delErr != nil {
return errors.Wrap(errors.CodeDatabaseError, delErr, "删除系列授权失败")
}
return nil
return s.appendGrantAudit(ctx, tx, constants.AuditActionShopSeriesGrantDeleted, "删除店铺套餐系列授权", allocation, series, shop,
grantData(allocation), map[string]any{"deleted": true}, pkgAllocations, nil, packageChanges)
})
}