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

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

@@ -7,6 +7,7 @@ import (
"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/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
@@ -18,10 +19,12 @@ import (
)
type Service struct {
db *gorm.DB
packageStore *postgres.PackageStore
packageSeriesStore *postgres.PackageSeriesStore
packageAllocationStore *postgres.ShopPackageAllocationStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
auditWriter *audit.Writer
}
func New(
@@ -38,7 +41,7 @@ func New(
}
}
func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*dto.PackageResponse, error) {
func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (_ *dto.PackageResponse, retErr error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
@@ -114,6 +117,13 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
Status: constants.StatusEnabled,
ShelfStatus: 2,
}
defer func() {
if retErr != nil {
failedPackage := *pkg
failedPackage.ID = 0
s.recordPackageFailure(ctx, constants.AuditActionPackageCreated, "创建套餐商品失败 "+pkg.PackageCode, &failedPackage, nil, retErr)
}
}()
if req.SeriesID != nil {
pkg.SeriesID = *req.SeriesID
}
@@ -140,8 +150,13 @@ func (s *Service) Create(ctx context.Context, req *dto.CreatePackageRequest) (*d
pkg.VirtualRatio = calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB)
pkg.Creator = currentUserID
if err := s.packageStore.Create(ctx, pkg); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "创建套餐失败")
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := postgres.NewPackageStore(tx).Create(ctx, pkg); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "创建套餐失败")
}
return s.appendPackageAudit(ctx, tx, constants.AuditActionPackageCreated, "创建套餐商品 "+pkg.PackageCode, pkg, nil, packageData(pkg))
}); err != nil {
return nil, err
}
resp := s.toResponse(ctx, pkg)
@@ -157,7 +172,6 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageResponse, error
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
resp := s.toResponse(ctx, pkg)
// 查询系列名称
if pkg.SeriesID > 0 {
@@ -191,7 +205,7 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageResponse, error
return resp, nil
}
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageRequest) (*dto.PackageResponse, error) {
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageRequest) (_ *dto.PackageResponse, retErr error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
@@ -204,6 +218,12 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
before := *pkg
defer func() {
if retErr != nil {
s.recordPackageFailure(ctx, constants.AuditActionPackageUpdated, "更新套餐商品失败 "+before.PackageCode, &before, packageData(&before), retErr)
}
}()
var seriesName *string
if req.SeriesID != nil && *req.SeriesID > 0 {
@@ -305,8 +325,13 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
pkg.VirtualRatio = calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB)
pkg.Updater = currentUserID
if err := s.packageStore.Update(ctx, pkg); err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "更新套餐失败")
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := postgres.NewPackageStore(tx).Update(ctx, pkg); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新套餐失败")
}
return s.appendPackageAudit(ctx, tx, constants.AuditActionPackageUpdated, "更新套餐商品 "+pkg.PackageCode, pkg, packageData(&before), packageData(pkg))
}); err != nil {
return nil, err
}
resp := s.toResponse(ctx, pkg)
@@ -314,20 +339,26 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
return resp, nil
}
func (s *Service) Delete(ctx context.Context, id uint) error {
_, err := s.packageStore.GetByID(ctx, id)
func (s *Service) Delete(ctx context.Context, id uint) (retErr error) {
pkg, err := s.packageStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
defer func() {
if retErr != nil {
s.recordPackageFailure(ctx, constants.AuditActionPackageDeleted, "删除套餐商品失败 "+pkg.PackageCode, pkg, packageData(pkg), retErr)
}
}()
if err := s.packageStore.Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除套餐失败")
}
return nil
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := postgres.NewPackageStore(tx).Delete(ctx, id); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "删除套餐失败")
}
return s.appendPackageAudit(ctx, tx, constants.AuditActionPackageDeleted, "删除套餐商品 "+pkg.PackageCode, pkg, packageData(pkg), map[string]any{"deleted": true})
})
}
func (s *Service) List(ctx context.Context, req *dto.PackageListRequest) ([]*dto.PackageResponse, int64, error) {
@@ -444,7 +475,7 @@ func (s *Service) batchGetSeriesAllocationsForShop(ctx context.Context, shopID u
return result
}
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) (retErr error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
@@ -457,6 +488,12 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
before := *pkg
defer func() {
if retErr != nil {
s.recordPackageFailure(ctx, constants.AuditActionPackageStatusUpdated, "更新套餐商品状态失败 "+before.PackageCode, &before, map[string]any{"status": before.Status, "shelf_status": before.ShelfStatus}, retErr)
}
}()
pkg.Status = status
pkg.Updater = currentUserID
@@ -465,14 +502,16 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
pkg.ShelfStatus = 2
}
if err := s.packageStore.Update(ctx, pkg); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新套餐状态失败")
}
return nil
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := postgres.NewPackageStore(tx).Update(ctx, pkg); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新套餐状态失败")
}
return s.appendPackageAudit(ctx, tx, constants.AuditActionPackageStatusUpdated, "更新套餐商品状态 "+pkg.PackageCode, pkg,
map[string]any{"status": before.Status, "shelf_status": before.ShelfStatus}, map[string]any{"status": pkg.Status, "shelf_status": pkg.ShelfStatus})
})
}
func (s *Service) UpdateShelfStatus(ctx context.Context, id uint, shelfStatus int) error {
func (s *Service) UpdateShelfStatus(ctx context.Context, id uint, shelfStatus int) (retErr error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
@@ -493,6 +532,12 @@ func (s *Service) UpdateShelfStatus(ctx context.Context, id uint, shelfStatus in
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
before := *pkg
defer func() {
if retErr != nil {
s.recordPackageFailure(ctx, constants.AuditActionPackageShelfStatusUpdated, "更新套餐上架状态失败 "+before.PackageCode, &before, map[string]any{"shelf_status": before.ShelfStatus}, retErr)
}
}()
if shelfStatus == constants.ShelfStatusOn && pkg.Status == constants.StatusDisabled {
return errors.New(errors.CodeInvalidStatus, "禁用的套餐不能上架,请先启用")
@@ -501,15 +546,17 @@ func (s *Service) UpdateShelfStatus(ctx context.Context, id uint, shelfStatus in
pkg.ShelfStatus = shelfStatus
pkg.Updater = currentUserID
if err := s.packageStore.Update(ctx, pkg); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新套餐上架状态失败")
}
return nil
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := postgres.NewPackageStore(tx).Update(ctx, pkg); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新套餐上架状态失败")
}
return s.appendPackageAudit(ctx, tx, constants.AuditActionPackageShelfStatusUpdated, "更新套餐上架状态 "+pkg.PackageCode, pkg,
map[string]any{"shelf_status": before.ShelfStatus}, map[string]any{"shelf_status": pkg.ShelfStatus})
})
}
// UpdateRetailPrice 代理修改自己店铺的套餐零售价
func (s *Service) UpdateRetailPrice(ctx context.Context, packageID uint, retailPrice int64) error {
func (s *Service) UpdateRetailPrice(ctx context.Context, packageID uint, retailPrice int64) (retErr error) {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
@@ -540,6 +587,14 @@ func (s *Service) UpdateRetailPrice(ctx context.Context, packageID uint, retailP
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
beforePrice, beforeStatus := allocation.RetailPrice, allocation.RetailPriceConfigStatus
beforeAllocation := *allocation
defer func() {
if retErr != nil {
s.recordAllocationFailure(ctx, constants.AuditActionPackageRetailPriceUpdated, "更新店铺套餐零售价失败 "+pkg.PackageCode, &beforeAllocation, pkg,
map[string]any{"retail_price": beforePrice, "retail_price_config_status": beforeStatus}, retErr)
}
}()
if pkg.IsGift {
return errors.New(errors.CodeForbidden, "赠送套餐不允许代理修改零售价")
}
@@ -550,16 +605,19 @@ func (s *Service) UpdateRetailPrice(ctx context.Context, packageID uint, retailP
if retailPrice < allocation.CostPrice {
return errors.New(errors.CodeInvalidParam, "零售价不能低于成本价")
}
if err := s.packageAllocationStore.UpdateRetailPrice(ctx, allocation.ID, storedRetailPrice, priceConfigStatus, currentUserID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新零售价失败")
}
return nil
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := postgres.NewShopPackageAllocationStore(tx).UpdateRetailPrice(ctx, allocation.ID, storedRetailPrice, priceConfigStatus, currentUserID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新零售价失败")
}
allocation.RetailPrice, allocation.RetailPriceConfigStatus = storedRetailPrice, priceConfigStatus
return s.appendAllocationAudit(ctx, tx, constants.AuditActionPackageRetailPriceUpdated, "更新店铺套餐零售价 "+pkg.PackageCode, allocation, pkg,
map[string]any{"retail_price": beforePrice, "retail_price_config_status": beforeStatus},
map[string]any{"retail_price": storedRetailPrice, "retail_price_config_status": priceConfigStatus})
})
}
// updateAgentShelfStatus 代理上下架路径:更新分配记录的 shelf_status
func (s *Service) updateAgentShelfStatus(ctx context.Context, packageID uint, shelfStatus int, updaterID uint) error {
func (s *Service) updateAgentShelfStatus(ctx context.Context, packageID uint, shelfStatus int, updaterID uint) (retErr error) {
shopID := middleware.GetShopIDFromContext(ctx)
if shopID == 0 {
return errors.New(errors.CodeUnauthorized, "当前用户不属于任何店铺")
@@ -573,26 +631,34 @@ func (s *Service) updateAgentShelfStatus(ctx context.Context, packageID uint, sh
}
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
}
beforeShelfStatus := allocation.ShelfStatus
beforeAllocation := *allocation
pkg, err := s.packageStore.GetByID(ctx, packageID)
if err != nil {
return errors.New(errors.CodeNotFound, "套餐不存在")
}
defer func() {
if retErr != nil {
s.recordAllocationFailure(ctx, constants.AuditActionShopPackageShelfStatusUpdated, "更新店铺套餐上架状态失败 "+pkg.PackageCode, &beforeAllocation, pkg,
map[string]any{"shelf_status": beforeShelfStatus}, retErr)
}
}()
// 上架时检查套餐全局禁用状态
if shelfStatus == constants.ShelfStatusOn {
pkg, err := s.packageStore.GetByID(ctx, packageID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
if pkg.Status == constants.StatusDisabled {
return errors.New(errors.CodeInvalidStatus, "套餐已禁用,无法上架")
}
}
if err := s.packageAllocationStore.UpdateShelfStatus(ctx, allocation.ID, shelfStatus, updaterID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新上下架状态失败")
}
return nil
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := postgres.NewShopPackageAllocationStore(tx).UpdateShelfStatus(ctx, allocation.ID, shelfStatus, updaterID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新上下架状态失败")
}
allocation.ShelfStatus = shelfStatus
return s.appendAllocationAudit(ctx, tx, constants.AuditActionShopPackageShelfStatusUpdated, "更新店铺套餐上架状态 "+pkg.PackageCode, allocation, pkg,
map[string]any{"shelf_status": beforeShelfStatus}, map[string]any{"shelf_status": shelfStatus})
})
}
func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.PackageResponse {
@@ -607,29 +673,29 @@ func (s *Service) toResponse(ctx context.Context, pkg *model.Package) *dto.Packa
}
resp := &dto.PackageResponse{
ID: pkg.ID,
PackageCode: pkg.PackageCode,
PackageName: pkg.PackageName,
SeriesID: seriesID,
PackageType: pkg.PackageType,
IsGift: pkg.IsGift,
DurationMonths: pkg.DurationMonths,
RealDataMB: pkg.RealDataMB,
VirtualDataMB: pkg.VirtualDataMB,
EnableVirtualData: pkg.EnableVirtualData,
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
CostPrice: pkg.CostPrice,
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
PriceConfigStatus: pkg.PriceConfigStatus,
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
CalendarType: pkg.CalendarType,
DurationDays: durationDays,
DataResetCycle: pkg.DataResetCycle,
ExpiryBase: pkg.ExpiryBase,
Status: pkg.Status,
ShelfStatus: pkg.ShelfStatus,
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
ID: pkg.ID,
PackageCode: pkg.PackageCode,
PackageName: pkg.PackageName,
SeriesID: seriesID,
PackageType: pkg.PackageType,
IsGift: pkg.IsGift,
DurationMonths: pkg.DurationMonths,
RealDataMB: pkg.RealDataMB,
VirtualDataMB: pkg.VirtualDataMB,
EnableVirtualData: pkg.EnableVirtualData,
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
CostPrice: pkg.CostPrice,
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
PriceConfigStatus: pkg.PriceConfigStatus,
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
CalendarType: pkg.CalendarType,
DurationDays: durationDays,
DataResetCycle: pkg.DataResetCycle,
ExpiryBase: pkg.ExpiryBase,
Status: pkg.Status,
ShelfStatus: pkg.ShelfStatus,
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
}
initPackageExpiryBaseFields(resp, pkg)
@@ -686,29 +752,29 @@ func (s *Service) toResponseWithAllocation(_ context.Context, pkg *model.Package
}
resp := &dto.PackageResponse{
ID: pkg.ID,
PackageCode: pkg.PackageCode,
PackageName: pkg.PackageName,
SeriesID: seriesID,
PackageType: pkg.PackageType,
IsGift: pkg.IsGift,
DurationMonths: pkg.DurationMonths,
RealDataMB: pkg.RealDataMB,
VirtualDataMB: pkg.VirtualDataMB,
EnableVirtualData: pkg.EnableVirtualData,
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
CostPrice: pkg.CostPrice,
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
PriceConfigStatus: pkg.PriceConfigStatus,
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
CalendarType: pkg.CalendarType,
DurationDays: durationDays,
DataResetCycle: pkg.DataResetCycle,
ExpiryBase: pkg.ExpiryBase,
Status: pkg.Status,
ShelfStatus: pkg.ShelfStatus,
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
ID: pkg.ID,
PackageCode: pkg.PackageCode,
PackageName: pkg.PackageName,
SeriesID: seriesID,
PackageType: pkg.PackageType,
IsGift: pkg.IsGift,
DurationMonths: pkg.DurationMonths,
RealDataMB: pkg.RealDataMB,
VirtualDataMB: pkg.VirtualDataMB,
EnableVirtualData: pkg.EnableVirtualData,
VirtualRatio: calculateVirtualRatio(pkg.EnableVirtualData, pkg.RealDataMB, pkg.VirtualDataMB),
CostPrice: pkg.CostPrice,
SuggestedRetailPrice: packageprice.PackageRawSuggestedRetailPrice(pkg),
PriceConfigStatus: pkg.PriceConfigStatus,
PriceConfigStatusName: packagePriceConfigStatusName(pkg.PriceConfigStatus),
CalendarType: pkg.CalendarType,
DurationDays: durationDays,
DataResetCycle: pkg.DataResetCycle,
ExpiryBase: pkg.ExpiryBase,
Status: pkg.Status,
ShelfStatus: pkg.ShelfStatus,
CreatedAt: pkg.CreatedAt.Format(time.RFC3339),
UpdatedAt: pkg.UpdatedAt.Format(time.RFC3339),
}
initPackageExpiryBaseFields(resp, pkg)