收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
@@ -14,10 +14,14 @@ import (
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
"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/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/asynctask"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
@@ -41,11 +45,16 @@ type AssetPackageBatchOrderHandler struct {
|
||||
orderCreator AssetPackageBatchOrderCreator
|
||||
storageService *storage.Service
|
||||
logger *zap.Logger
|
||||
auditWriter *audit.Writer
|
||||
}
|
||||
|
||||
// NewAssetPackageBatchOrderHandler 创建资产套餐批量订购任务处理器。
|
||||
func NewAssetPackageBatchOrderHandler(taskStore *postgres.AssetPackageBatchOrderTaskStore, shopStore *postgres.ShopStore, orderCreator AssetPackageBatchOrderCreator, storageService *storage.Service, logger *zap.Logger) *AssetPackageBatchOrderHandler {
|
||||
return &AssetPackageBatchOrderHandler{taskStore: taskStore, shopStore: shopStore, orderCreator: orderCreator, storageService: storageService, logger: logger}
|
||||
func NewAssetPackageBatchOrderHandler(taskStore *postgres.AssetPackageBatchOrderTaskStore, shopStore *postgres.ShopStore, orderCreator AssetPackageBatchOrderCreator, storageService *storage.Service, logger *zap.Logger, auditWriters ...*audit.Writer) *AssetPackageBatchOrderHandler {
|
||||
handler := &AssetPackageBatchOrderHandler{taskStore: taskStore, shopStore: shopStore, orderCreator: orderCreator, storageService: storageService, logger: logger}
|
||||
if len(auditWriters) > 0 {
|
||||
handler.auditWriter = auditWriters[0]
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// Handle 处理资产套餐批量订购任务。
|
||||
@@ -60,6 +69,12 @@ func (h *AssetPackageBatchOrderHandler) Handle(ctx context.Context, taskMessage
|
||||
h.logger.Error("查询资产套餐批量订购任务失败", zap.Uint("task_id", payload.TaskID), zap.Error(err))
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
rootEventID := audit.TaskEventID(constants.AuditResourceAssetPackageBatchOrderTask, taskRecord.ID, "completed")
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeAssetPackageBatchOrder,
|
||||
ActorName: "资产套餐批量订购任务", Source: constants.AuditSourceWorker,
|
||||
CorrelationID: taskRecord.TaskNo, ParentEventID: rootEventID,
|
||||
})
|
||||
claimed, err := h.taskStore.Claim(ctx, taskRecord.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -71,17 +86,68 @@ func (h *AssetPackageBatchOrderHandler) Handle(ctx context.Context, taskMessage
|
||||
rows, err := h.downloadAndParse(ctx, taskRecord.StorageKey)
|
||||
if err != nil {
|
||||
h.logger.Error("下载或解析批量订购CSV失败", zap.Uint("task_id", taskRecord.ID), zap.Error(err))
|
||||
_ = h.taskStore.MarkFailed(ctx, taskRecord.ID, err.Error())
|
||||
if finishErr := h.finishBatchOrderTask(ctx, taskRecord, nil, 0, 1, asynctask.StatusFailed, err.Error()); finishErr != nil {
|
||||
h.resetBatchOrderTaskForRetry(ctx, taskRecord.ID)
|
||||
return finishErr
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
items, successCount, failCount := h.processRows(ctx, taskRecord, rows)
|
||||
if err := h.taskStore.Complete(ctx, taskRecord.ID, items, successCount, failCount); err != nil {
|
||||
if err := h.finishBatchOrderTask(ctx, taskRecord, items, successCount, failCount, asynctask.StatusCompleted, ""); err != nil {
|
||||
h.resetBatchOrderTaskForRetry(ctx, taskRecord.ID)
|
||||
return err
|
||||
}
|
||||
h.logger.Info("资产套餐批量订购任务完成", zap.Uint("task_id", taskRecord.ID), zap.Int("success", successCount), zap.Int("fail", failCount))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AssetPackageBatchOrderHandler) resetBatchOrderTaskForRetry(ctx context.Context, taskID uint) {
|
||||
_ = h.taskStore.DB().WithContext(ctx).Model(&model.AssetPackageBatchOrderTask{}).
|
||||
Where("id = ? AND status = ?", taskID, asynctask.StatusProcessing).
|
||||
Updates(map[string]any{"status": asynctask.StatusPending, "started_at": nil}).Error
|
||||
}
|
||||
|
||||
func (h *AssetPackageBatchOrderHandler) finishBatchOrderTask(ctx context.Context, taskRecord *model.AssetPackageBatchOrderTask, items model.AssetPackageBatchOrderResultItems, successCount, failCount, status int, errorMessage string) error {
|
||||
if h.auditWriter == nil {
|
||||
return apperrors.New(apperrors.CodeInvalidStatus, "资产套餐批量订购统一审计接缝未配置")
|
||||
}
|
||||
return h.taskStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
txStore := h.taskStore.WithTx(tx)
|
||||
if status == asynctask.StatusFailed {
|
||||
if err := txStore.MarkFailed(ctx, taskRecord.ID, errorMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := txStore.Complete(ctx, taskRecord.ID, items, successCount, failCount); err != nil {
|
||||
return err
|
||||
}
|
||||
rootID := audit.TaskEventID(constants.AuditResourceAssetPackageBatchOrderTask, taskRecord.ID, "completed")
|
||||
var childCount int64
|
||||
if err := tx.WithContext(ctx).Model(&model.AuditEvent{}).
|
||||
Where("parent_event_id = ? AND action_code = ?", rootID, constants.AuditActionOrderCreated).
|
||||
Count(&childCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := batchAuditResult(int(childCount), failCount)
|
||||
return h.auditWriter.WriteTask(ctx, tx, audit.TaskInput{
|
||||
EventID: rootID, ActionCode: constants.AuditActionAssetPackageBatchOrderTaskCompleted,
|
||||
Summary: "完成资产套餐批量订购任务", TaskID: taskRecord.ID, TaskNo: taskRecord.TaskNo,
|
||||
Result: result, CorrelationID: taskRecord.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceAssetPackageBatchOrderTask, taskRecord.ID, "created"),
|
||||
BatchTotal: len(items), SuccessCount: int(childCount), FailCount: failCount,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": taskRecord.ID, "task_no": taskRecord.TaskNo, "file_name": taskRecord.FileName,
|
||||
"package_id": taskRecord.PackageID, "package_code": taskRecord.PackageCode,
|
||||
"package_name": taskRecord.PackageName, "payment_method": taskRecord.PaymentMethod,
|
||||
},
|
||||
BeforeData: map[string]any{"status": asynctask.StatusProcessing},
|
||||
AfterData: map[string]any{
|
||||
"status": status, "total_count": len(items), "success_count": successCount, "fail_count": failCount,
|
||||
},
|
||||
Metadata: map[string]any{"task_success_count": successCount, "task_fail_count": failCount},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type assetPackageBatchOrderRow struct {
|
||||
Line int
|
||||
Identifier string
|
||||
@@ -150,6 +216,12 @@ func (h *AssetPackageBatchOrderHandler) processRows(ctx context.Context, taskRec
|
||||
Username: taskRecord.CreatorName, ShopID: taskRecord.CreatorShopID,
|
||||
SubordinateShopIDs: subordinateShopIDs,
|
||||
})
|
||||
workerCtx = auditcontext.With(workerCtx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeAssetPackageBatchOrder,
|
||||
ActorName: "资产套餐批量订购任务", Source: constants.AuditSourceWorker,
|
||||
CorrelationID: taskRecord.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceAssetPackageBatchOrderTask, taskRecord.ID, "completed"),
|
||||
})
|
||||
buyerType, buyerID := "", uint(0)
|
||||
if taskRecord.CreatorUserType == constants.UserTypeAgent {
|
||||
buyerType, buyerID = model.BuyerTypeAgent, taskRecord.CreatorShopID
|
||||
|
||||
@@ -15,10 +15,12 @@ import (
|
||||
|
||||
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/packageprice"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
@@ -44,6 +46,7 @@ type AutoPurchaseHandler struct {
|
||||
asynqClient *asynq.Client // 用于事务提交成功后触发佣金计算任务
|
||||
logger *zap.Logger
|
||||
observationSeriesEvents cardObservationApp.SeriesEventWriter
|
||||
auditWriter *audit.Writer
|
||||
}
|
||||
|
||||
// NewAutoPurchaseHandler 创建充值后自动购包处理器
|
||||
@@ -59,6 +62,7 @@ func NewAutoPurchaseHandler(
|
||||
asynqClient *asynq.Client,
|
||||
logger *zap.Logger,
|
||||
observationSeriesEvents cardObservationApp.SeriesEventWriter,
|
||||
auditWriter *audit.Writer,
|
||||
) *AutoPurchaseHandler {
|
||||
if orderStore == nil {
|
||||
orderStore = postgres.NewOrderStore(db, redisClient)
|
||||
@@ -94,6 +98,7 @@ func NewAutoPurchaseHandler(
|
||||
asynqClient: asynqClient,
|
||||
logger: logger,
|
||||
observationSeriesEvents: observationSeriesEvents,
|
||||
auditWriter: auditWriter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +123,11 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
h.logger.Error("查询充值订单失败", zap.Uint("recharge_order_id", payload.RechargeOrderID), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeAutoPurchaseAfterRecharge,
|
||||
ActorName: "充值后自动购包任务", Source: constants.AuditSourceWorker,
|
||||
CorrelationID: rechargeOrder.RechargeOrderNo,
|
||||
})
|
||||
|
||||
if rechargeOrder.AutoPurchaseStatus == constants.AutoPurchaseStatusSuccess {
|
||||
return nil
|
||||
@@ -276,8 +286,10 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
Update("auto_purchase_status", constants.AutoPurchaseStatusSuccess).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
if h.auditWriter == nil {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "自动购包统一审计接缝未配置")
|
||||
}
|
||||
return h.appendAutoPurchaseAudit(ctx, tx, rechargeOrder, order, payment, wallet, walletTx, packages)
|
||||
}); err != nil {
|
||||
h.logger.Error("自动购包任务执行失败",
|
||||
zap.Uint("recharge_record_id", rechargeOrder.ID),
|
||||
@@ -311,6 +323,61 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AutoPurchaseHandler) appendAutoPurchaseAudit(ctx context.Context, tx *gorm.DB, recharge *model.RechargeOrder, order *model.Order, payment *model.Payment, wallet *model.AssetWallet, walletTx *model.AssetWalletTransaction, packages []*model.Package) error {
|
||||
rechargeID := strconv.FormatUint(uint64(recharge.ID), 10)
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceRechargeOrder, ID: &rechargeID, Key: recharge.RechargeOrderNo, DisplayName: recharge.RechargeOrderNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleRechargeTarget,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": recharge.ID, "recharge_order_no": recharge.RechargeOrderNo, "user_id": recharge.UserID,
|
||||
"asset_wallet_id": recharge.AssetWalletID, "resource_type": recharge.ResourceType,
|
||||
"resource_id": recharge.ResourceID, "amount": recharge.Amount, "status": recharge.Status,
|
||||
},
|
||||
BeforeData: map[string]any{"auto_purchase_status": recharge.AutoPurchaseStatus},
|
||||
AfterData: map[string]any{"auto_purchase_status": constants.AutoPurchaseStatusSuccess},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "充值后自动购包已完成",
|
||||
}}
|
||||
orderResource := audit.OrderResource(order, constants.AuditResourceRelationAffected, constants.AuditResourceRoleRechargeAutoPurchaseOrder)
|
||||
orderResource.SubjectVisibility = constants.AuditSubjectResult
|
||||
orderResource.SubjectSummary = "充值后自动购包已完成"
|
||||
resources = append(resources, orderResource)
|
||||
walletID := strconv.FormatUint(uint64(wallet.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetWallet, ID: &walletID, Key: walletID, DisplayName: "资产钱包 " + walletID,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleRechargeWallet,
|
||||
IdentitySnapshot: map[string]any{"id": wallet.ID, "resource_type": wallet.ResourceType, "resource_id": wallet.ResourceID, "currency": wallet.Currency},
|
||||
BeforeData: map[string]any{"balance": walletTx.BalanceBefore}, AfterData: map[string]any{"balance": walletTx.BalanceAfter},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "充值后自动购包已完成",
|
||||
})
|
||||
walletTxID := strconv.FormatUint(uint64(walletTx.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetWalletTransaction, ID: &walletTxID, Key: walletTxID, DisplayName: "资产钱包流水 " + walletTxID,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleRechargeWalletTransaction,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": walletTx.ID, "asset_wallet_id": walletTx.AssetWalletID, "resource_type": walletTx.ResourceType,
|
||||
"resource_id": walletTx.ResourceID, "transaction_type": walletTx.TransactionType,
|
||||
"reference_type": walletTx.ReferenceType, "reference_no": walletTx.ReferenceNo, "status": walletTx.Status,
|
||||
},
|
||||
AfterData: map[string]any{"amount": walletTx.Amount, "balance_before": walletTx.BalanceBefore, "balance_after": walletTx.BalanceAfter},
|
||||
})
|
||||
resources = append(resources, audit.PaymentResource(payment, constants.AuditResourceRelationReference, constants.AuditResourceRoleOrderPayment, nil, nil))
|
||||
for _, pkg := range packages {
|
||||
resources = append(resources, audit.PackageResource(pkg, constants.AuditResourceRelationReference, constants.AuditResourceRoleOrderPackage, nil, nil))
|
||||
}
|
||||
var usages []model.PackageUsage
|
||||
if err := tx.WithContext(ctx).Where("order_id = ?", order.ID).Order("id ASC").Find(&usages).Error; err != nil {
|
||||
return pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "查询自动购包套餐权益审计快照失败")
|
||||
}
|
||||
for i := range usages {
|
||||
resources = append(resources, audit.PackageUsageResource(&usages[i], constants.AuditResourceRelationAffected, constants.AuditResourceRolePackageUsageTarget, nil, nil))
|
||||
}
|
||||
return h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: constants.AuditActionAssetRechargeAutoPurchased, Summary: "充值后自动购包已完成",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
|
||||
CorrelationID: recharge.RechargeOrderNo, Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func orderObservationResource(order *model.Order) (string, uint) {
|
||||
if order != nil && order.DeviceID != nil {
|
||||
return constants.CardObservationResourceTypeDevice, *order.DeviceID
|
||||
@@ -348,10 +415,39 @@ func (h *AutoPurchaseHandler) markAutoPurchaseFailedIfFinalRetry(ctx context.Con
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.WithContext(ctx).
|
||||
Model(&model.RechargeOrder{}).
|
||||
Where("id = ?", rechargeOrderID).
|
||||
Update("auto_purchase_status", constants.AutoPurchaseStatusFailed).Error; err != nil {
|
||||
if err := h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var recharge model.RechargeOrder
|
||||
if err := tx.WithContext(ctx).First(&recharge, rechargeOrderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.WithContext(ctx).Model(&model.RechargeOrder{}).
|
||||
Where("id = ? AND auto_purchase_status <> ?", rechargeOrderID, constants.AutoPurchaseStatusFailed).
|
||||
Update("auto_purchase_status", constants.AutoPurchaseStatusFailed)
|
||||
if result.Error != nil || result.RowsAffected == 0 {
|
||||
return result.Error
|
||||
}
|
||||
if h.auditWriter == nil {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "自动购包统一审计接缝未配置")
|
||||
}
|
||||
rechargeID := strconv.FormatUint(uint64(recharge.ID), 10)
|
||||
return h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: constants.AuditActionAssetRechargeAutoPurchased, Summary: "充值后自动购包失败",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultFailed,
|
||||
CorrelationID: recharge.RechargeOrderNo,
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceRechargeOrder, ID: &rechargeID, Key: recharge.RechargeOrderNo, DisplayName: recharge.RechargeOrderNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleRechargeTarget,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": recharge.ID, "recharge_order_no": recharge.RechargeOrderNo, "user_id": recharge.UserID,
|
||||
"asset_wallet_id": recharge.AssetWalletID, "resource_type": recharge.ResourceType,
|
||||
"resource_id": recharge.ResourceID, "amount": recharge.Amount, "status": recharge.Status,
|
||||
},
|
||||
BeforeData: map[string]any{"auto_purchase_status": recharge.AutoPurchaseStatus},
|
||||
AfterData: map[string]any{"auto_purchase_status": constants.AutoPurchaseStatusFailed},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "充值后自动购包失败",
|
||||
}},
|
||||
})
|
||||
}); err != nil {
|
||||
h.logger.Error("更新自动购包失败状态失败",
|
||||
zap.Uint("recharge_record_id", rechargeOrderID),
|
||||
zap.Error(err),
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/commission_calculation"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -46,6 +48,10 @@ func (h *CommissionCalculationHandler) HandleCommissionCalculation(ctx context.C
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.AuditActorIDCommissionCalculationWorker,
|
||||
ActorName: "订单佣金计算任务", Source: constants.AuditSourceWorker,
|
||||
})
|
||||
|
||||
if err := h.service.CalculateCommission(ctx, payload.OrderID); err != nil {
|
||||
h.logger.Error("佣金计算失败",
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
|
||||
"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/store/postgres"
|
||||
@@ -26,17 +27,24 @@ type deviceBatchAllocationRow struct {
|
||||
|
||||
func (h *DeviceImportHandler) handleDeviceBatchAllocation(ctx context.Context, task *model.DeviceImportTask) error {
|
||||
if h.allocationExecutor == nil || (task.OperationType != constants.DeviceImportOperationRecall && (task.TargetID == nil || *task.TargetID == 0)) {
|
||||
_ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "设备CSV批量执行器或目标未配置")
|
||||
if err := h.finishDeviceImportTask(ctx, task, 0, 0, 1, model.ImportTaskStatusFailed, "设备CSV批量执行器或目标未配置"); err != nil {
|
||||
return err
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
rows, err := h.downloadDeviceBatchAllocationCSV(ctx, task)
|
||||
if err != nil {
|
||||
_ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, err.Error())
|
||||
if finishErr := h.finishDeviceImportTask(ctx, task, 0, 0, 1, model.ImportTaskStatusFailed, err.Error()); finishErr != nil {
|
||||
return finishErr
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
task.TotalCount = len(rows)
|
||||
shopScope, err := h.resolveDeviceBatchShopScope(ctx, task)
|
||||
if err != nil {
|
||||
_ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, err.Error())
|
||||
if finishErr := h.finishDeviceImportTask(ctx, task, 0, 0, 1, model.ImportTaskStatusFailed, err.Error()); finishErr != nil {
|
||||
return finishErr
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
workerCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
|
||||
@@ -47,19 +55,20 @@ func (h *DeviceImportHandler) handleDeviceBatchAllocation(ctx context.Context, t
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeDeviceImport,
|
||||
ActorName: "设备CSV批量操作任务", Source: constants.AuditSourceWorker,
|
||||
CorrelationID: task.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceDeviceImportTask, task.ID, "completed"),
|
||||
})
|
||||
result, err := h.executeDeviceBatchAllocation(workerCtx, task, rows)
|
||||
if err != nil {
|
||||
_ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, err.Error())
|
||||
if finishErr := h.finishDeviceImportTask(ctx, task, 0, 0, 1, model.ImportTaskStatusFailed, err.Error()); finishErr != nil {
|
||||
return finishErr
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
_ = h.importTaskStore.UpdateResult(ctx, task.ID, len(rows), result.successCount, result.skipCount, result.failCount, 0, result.skippedItems, result.failedItems, nil)
|
||||
status, errorMessage := model.ImportTaskStatusCompleted, ""
|
||||
if result.successCount == 0 && result.failCount > 0 {
|
||||
_ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "所有设备操作均失败")
|
||||
} else {
|
||||
_ = h.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusCompleted, "")
|
||||
status, errorMessage = model.ImportTaskStatusFailed, "所有设备操作均失败"
|
||||
}
|
||||
return nil
|
||||
return h.finishDeviceImportTask(ctx, task, result.successCount, result.skipCount, result.failCount, status, errorMessage, result.skippedItems, result.failedItems)
|
||||
}
|
||||
|
||||
func (h *DeviceImportHandler) downloadDeviceBatchAllocationCSV(ctx context.Context, task *model.DeviceImportTask) ([]deviceBatchAllocationRow, error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,10 +15,13 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"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/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
)
|
||||
@@ -45,6 +49,7 @@ type DeviceImportHandler struct {
|
||||
assetWalletStore *postgres.AssetWalletStore
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
storageService *storage.Service
|
||||
auditWriter *audit.Writer
|
||||
logger *zap.Logger
|
||||
allocationExecutor DeviceBatchAllocationExecutor
|
||||
}
|
||||
@@ -59,6 +64,7 @@ func NewDeviceImportHandler(
|
||||
assetWalletStore *postgres.AssetWalletStore,
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore,
|
||||
storageSvc *storage.Service,
|
||||
auditWriter *audit.Writer,
|
||||
logger *zap.Logger,
|
||||
allocationExecutor DeviceBatchAllocationExecutor,
|
||||
) *DeviceImportHandler {
|
||||
@@ -72,6 +78,7 @@ func NewDeviceImportHandler(
|
||||
assetWalletStore: assetWalletStore,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
storageService: storageSvc,
|
||||
auditWriter: auditWriter,
|
||||
logger: logger,
|
||||
allocationExecutor: allocationExecutor,
|
||||
}
|
||||
@@ -95,6 +102,12 @@ func (h *DeviceImportHandler) HandleDeviceImport(ctx context.Context, task *asyn
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeDeviceImport,
|
||||
ActorName: "设备导入任务", Source: constants.AuditSourceWorker,
|
||||
CorrelationID: importTask.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceDeviceImportTask, importTask.ID, "completed"),
|
||||
})
|
||||
|
||||
switch importTask.Status {
|
||||
case model.ImportTaskStatusPending:
|
||||
@@ -132,7 +145,9 @@ func (h *DeviceImportHandler) HandleDeviceImport(ctx context.Context, task *asyn
|
||||
zap.Uint("task_id", importTask.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusFailed, err.Error())
|
||||
if finishErr := h.finishDeviceImportTask(ctx, importTask, 0, 0, 1, model.ImportTaskStatusFailed, err.Error()); finishErr != nil {
|
||||
return finishErr
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
@@ -147,12 +162,13 @@ func (h *DeviceImportHandler) HandleDeviceImport(ctx context.Context, task *asyn
|
||||
result.failCount++
|
||||
}
|
||||
|
||||
h.importTaskStore.UpdateResult(ctx, importTask.ID, parseResult.TotalCount, result.successCount, result.skipCount, result.failCount, 0, result.skippedItems, result.failedItems, nil)
|
||||
|
||||
importTask.TotalCount = parseResult.TotalCount
|
||||
status, errorMessage := model.ImportTaskStatusCompleted, ""
|
||||
if result.failCount > 0 && result.successCount == 0 {
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusFailed, "所有导入均失败")
|
||||
} else {
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusCompleted, "")
|
||||
status, errorMessage = model.ImportTaskStatusFailed, "所有导入均失败"
|
||||
}
|
||||
if err := h.finishDeviceImportTask(ctx, importTask, result.successCount, result.skipCount, result.failCount, status, errorMessage, result.skippedItems, result.failedItems); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("设备导入任务完成",
|
||||
@@ -393,7 +409,7 @@ func (h *DeviceImportHandler) processBatch(ctx context.Context, task *model.Devi
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return h.appendDeviceCreateAudit(ctx, tx, task, device)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -423,4 +439,137 @@ func (h *DeviceImportHandler) processBatch(ctx context.Context, task *model.Devi
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DeviceImportHandler) appendDeviceCreateAudit(ctx context.Context, tx *gorm.DB, task *model.DeviceImportTask, device *model.Device) error {
|
||||
if h.auditWriter == nil || task == nil || device == nil || device.ID == 0 {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "设备导入统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &resourceID,
|
||||
Key: audit.DeviceResourceKey(device), DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget,
|
||||
IdentitySnapshot: audit.DeviceIdentitySnapshot(device), AfterData: map[string]any{"created": true},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备已导入",
|
||||
}}
|
||||
var bindings []*model.DeviceSimBinding
|
||||
if err := tx.WithContext(ctx).Where("device_id = ? AND bind_status = ?", device.ID, constants.BindStatusBound).Order("slot_position ASC").Find(&bindings).Error; err != nil {
|
||||
return pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "查询设备导入卡槽关系失败")
|
||||
}
|
||||
cardIDs := make([]uint, 0, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
cardIDs = append(cardIDs, binding.IotCardID)
|
||||
}
|
||||
cardByID := make(map[uint]*model.IotCard, len(cardIDs))
|
||||
if len(cardIDs) > 0 {
|
||||
var cards []*model.IotCard
|
||||
if err := tx.WithContext(ctx).Where("id IN ?", cardIDs).Find(&cards).Error; err != nil {
|
||||
return pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "查询设备导入绑定卡失败")
|
||||
}
|
||||
for _, card := range cards {
|
||||
cardByID[card.ID] = card
|
||||
}
|
||||
}
|
||||
for index, binding := range bindings {
|
||||
card := cardByID[binding.IotCardID]
|
||||
if card != nil {
|
||||
cardID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceIotCard, ID: &cardID,
|
||||
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleDeviceBindingTargetCard,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(card),
|
||||
AfterData: map[string]any{"device_id": device.ID, "slot_position": binding.SlotPosition},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备导入并绑定 IoT 卡", SortOrder: index*2 + 1,
|
||||
})
|
||||
}
|
||||
bindingID := strconv.FormatUint(uint64(binding.ID), 10)
|
||||
identity := map[string]any{
|
||||
"id": binding.ID, "device_id": binding.DeviceID, "device_virtual_no": device.VirtualNo,
|
||||
"slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID, "is_current": binding.IsCurrent,
|
||||
}
|
||||
if card != nil {
|
||||
identity["iccid"] = card.ICCID
|
||||
identity["virtual_no"] = card.VirtualNo
|
||||
}
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID,
|
||||
Key: bindingID, DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleDeviceCreatedBinding,
|
||||
IdentitySnapshot: identity,
|
||||
AfterData: map[string]any{
|
||||
"slot_position": binding.SlotPosition, "bind_status": constants.BindStatusBound, "is_current": binding.IsCurrent,
|
||||
},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index*2 + 2,
|
||||
})
|
||||
}
|
||||
return h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
EventID: audit.TaskEventID(constants.AuditResourceDeviceImportTask, device.ID, "item"),
|
||||
ActionCode: constants.AuditActionDeviceCreated, Summary: "导入创建设备",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
|
||||
Metadata: map[string]any{"import_task_id": task.ID, "import_task_no": task.TaskNo},
|
||||
Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceImportHandler) finishDeviceImportTask(ctx context.Context, task *model.DeviceImportTask, successCount, skipCount, failCount, status int, errorMessage string, items ...model.ImportResultItems) error {
|
||||
if h.auditWriter == nil {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "设备导入任务统一审计接缝未配置")
|
||||
}
|
||||
return h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
now := time.Now()
|
||||
updates := map[string]any{
|
||||
"status": status, "total_count": task.TotalCount, "success_count": successCount,
|
||||
"skip_count": skipCount, "fail_count": failCount, "error_message": errorMessage,
|
||||
"completed_at": now, "updated_at": now,
|
||||
}
|
||||
if len(items) > 0 {
|
||||
updates["skipped_items"] = items[0]
|
||||
}
|
||||
if len(items) > 1 {
|
||||
updates["failed_items"] = items[1]
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.DeviceImportTask{}).Where("id = ?", task.ID).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
rootID := audit.TaskEventID(constants.AuditResourceDeviceImportTask, task.ID, "completed")
|
||||
var childCount int64
|
||||
if err := tx.WithContext(ctx).Model(&model.AuditEvent{}).
|
||||
Where("correlation_id = ? AND action_code = ? AND result = ?", task.TaskNo, deviceImportItemAction(task.OperationType), constants.AuditResultSuccess).
|
||||
Count(&childCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return h.auditWriter.WriteTask(ctx, tx, audit.TaskInput{
|
||||
EventID: rootID, ActionCode: constants.AuditActionDeviceImportTaskCompleted,
|
||||
Summary: "完成设备导入任务", TaskID: task.ID, TaskNo: task.TaskNo,
|
||||
Result: batchAuditResult(int(childCount), failCount), CorrelationID: task.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceDeviceImportTask, task.ID, "created"),
|
||||
BatchTotal: task.TotalCount, SuccessCount: int(childCount), FailCount: failCount,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": task.ID, "task_no": task.TaskNo, "file_name": task.FileName,
|
||||
"operation_type": task.OperationType, "target_id": task.TargetID,
|
||||
"batch_no": task.BatchNo, "realname_policy": task.RealnamePolicy,
|
||||
},
|
||||
BeforeData: map[string]any{"status": model.ImportTaskStatusProcessing},
|
||||
AfterData: map[string]any{
|
||||
"status": status, "total_count": task.TotalCount, "success_count": successCount,
|
||||
"skip_count": skipCount, "fail_count": failCount,
|
||||
},
|
||||
Metadata: map[string]any{"skip_count": skipCount},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func deviceImportItemAction(operationType string) string {
|
||||
switch operationType {
|
||||
case constants.DeviceImportOperationAssignShop:
|
||||
return constants.AuditActionDeviceAllocated
|
||||
case constants.DeviceImportOperationAssignSeries:
|
||||
return constants.AuditActionDeviceSeriesBound
|
||||
case constants.DeviceImportOperationRecall:
|
||||
return constants.AuditActionDeviceRecalled
|
||||
default:
|
||||
return constants.AuditActionDeviceCreated
|
||||
}
|
||||
}
|
||||
|
||||
var ErrMissingDeviceNoColumn = stderrors.New("CSV 缺少 virtual_no 列")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,9 +15,12 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"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/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/validator"
|
||||
@@ -41,14 +45,15 @@ type PollingCallback interface {
|
||||
}
|
||||
|
||||
type IotCardImportHandler struct {
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
importTaskStore *postgres.IotCardImportTaskStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
importTaskStore *postgres.IotCardImportTaskStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
assetWalletStore *postgres.AssetWalletStore
|
||||
storageService *storage.Service
|
||||
pollingCallback PollingCallback
|
||||
logger *zap.Logger
|
||||
storageService *storage.Service
|
||||
pollingCallback PollingCallback
|
||||
auditWriter *audit.Writer
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewIotCardImportHandler(
|
||||
@@ -59,6 +64,7 @@ func NewIotCardImportHandler(
|
||||
assetWalletStore *postgres.AssetWalletStore,
|
||||
storageSvc *storage.Service,
|
||||
pollingCallback PollingCallback,
|
||||
auditWriter *audit.Writer,
|
||||
logger *zap.Logger,
|
||||
) *IotCardImportHandler {
|
||||
return &IotCardImportHandler{
|
||||
@@ -69,6 +75,7 @@ func NewIotCardImportHandler(
|
||||
assetWalletStore: assetWalletStore,
|
||||
storageService: storageSvc,
|
||||
pollingCallback: pollingCallback,
|
||||
auditWriter: auditWriter,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -91,6 +98,12 @@ func (h *IotCardImportHandler) HandleIotCardImport(ctx context.Context, task *as
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeIotCardImport,
|
||||
ActorName: "IoT 卡导入任务", Source: constants.AuditSourceWorker,
|
||||
CorrelationID: importTask.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceIotCardImportTask, importTask.ID, "completed"),
|
||||
})
|
||||
|
||||
switch importTask.Status {
|
||||
case model.ImportTaskStatusPending:
|
||||
@@ -125,7 +138,9 @@ func (h *IotCardImportHandler) HandleIotCardImport(ctx context.Context, task *as
|
||||
zap.Uint("task_id", importTask.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusFailed, err.Error())
|
||||
if finishErr := h.finishImportTask(ctx, importTask, 0, 0, 1, model.ImportTaskStatusFailed, err.Error()); finishErr != nil {
|
||||
return finishErr
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
@@ -138,12 +153,12 @@ func (h *IotCardImportHandler) HandleIotCardImport(ctx context.Context, task *as
|
||||
result.failedItems = append(parseFailures, result.failedItems...)
|
||||
result.failCount += len(parseFailures)
|
||||
|
||||
h.importTaskStore.UpdateResult(ctx, importTask.ID, result.successCount, result.skipCount, result.failCount, result.skippedItems, result.failedItems)
|
||||
|
||||
status, errorMessage := model.ImportTaskStatusCompleted, ""
|
||||
if result.failCount > 0 && result.successCount == 0 {
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusFailed, "所有导入均失败")
|
||||
} else {
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusCompleted, "")
|
||||
status, errorMessage = model.ImportTaskStatusFailed, "所有导入均失败"
|
||||
}
|
||||
if err := h.finishImportTask(ctx, importTask, result.successCount, result.skipCount, result.failCount, status, errorMessage, result.skippedItems, result.failedItems); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("IoT 卡导入任务完成",
|
||||
@@ -434,7 +449,10 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot
|
||||
})
|
||||
}
|
||||
}
|
||||
return tx.CreateInBatches(&identifiers, 500).Error
|
||||
if err := tx.CreateInBatches(&identifiers, 500).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return h.appendCardCreateAudits(ctx, tx, iotCards)
|
||||
})
|
||||
|
||||
if txErr != nil {
|
||||
@@ -463,6 +481,91 @@ func (h *IotCardImportHandler) processBatch(ctx context.Context, task *model.Iot
|
||||
}
|
||||
}
|
||||
|
||||
func (h *IotCardImportHandler) appendCardCreateAudits(ctx context.Context, tx *gorm.DB, cards []*model.IotCard) error {
|
||||
if h.auditWriter == nil {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "IoT 卡导入统一审计接缝未配置")
|
||||
}
|
||||
for _, card := range cards {
|
||||
if card == nil || card.ID == 0 {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "IoT 卡导入审计资源不完整")
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
if err := h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
EventID: audit.TaskEventID(constants.AuditResourceIotCardImportTask, card.ID, "item"),
|
||||
ActionCode: constants.AuditActionIotCardCreated, Summary: "导入创建 IoT 卡",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceIotCard, ID: &resourceID,
|
||||
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), AfterData: map[string]any{"created": true},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "IoT 卡已导入",
|
||||
}},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *IotCardImportHandler) finishImportTask(ctx context.Context, task *model.IotCardImportTask, successCount, skipCount, failCount, status int, errorMessage string, items ...model.ImportResultItems) error {
|
||||
if h.auditWriter == nil {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "IoT 卡导入任务统一审计接缝未配置")
|
||||
}
|
||||
return h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
now := time.Now()
|
||||
updates := map[string]any{
|
||||
"status": status, "success_count": successCount, "skip_count": skipCount, "fail_count": failCount,
|
||||
"error_message": errorMessage, "completed_at": now, "updated_at": now,
|
||||
}
|
||||
if len(items) > 0 {
|
||||
updates["skipped_items"] = items[0]
|
||||
}
|
||||
if len(items) > 1 {
|
||||
updates["failed_items"] = items[1]
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.IotCardImportTask{}).Where("id = ?", task.ID).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
rootID := audit.TaskEventID(constants.AuditResourceIotCardImportTask, task.ID, "completed")
|
||||
var childCount int64
|
||||
if err := tx.WithContext(ctx).Model(&model.AuditEvent{}).
|
||||
Where("parent_event_id = ? AND action_code = ?", rootID, constants.AuditActionIotCardCreated).
|
||||
Count(&childCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := batchAuditResult(int(childCount), failCount)
|
||||
return h.auditWriter.WriteTask(ctx, tx, audit.TaskInput{
|
||||
EventID: rootID, ActionCode: constants.AuditActionIotCardImportTaskCompleted,
|
||||
Summary: "完成 IoT 卡导入任务", TaskID: task.ID, TaskNo: task.TaskNo,
|
||||
Result: result, CorrelationID: task.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceIotCardImportTask, task.ID, "created"),
|
||||
BatchTotal: task.TotalCount, SuccessCount: int(childCount), FailCount: failCount,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": task.ID, "task_no": task.TaskNo, "file_name": task.FileName,
|
||||
"carrier_id": task.CarrierID, "carrier_name": task.CarrierName, "batch_no": task.BatchNo,
|
||||
"card_category": task.CardCategory, "realname_policy": task.RealnamePolicy,
|
||||
},
|
||||
BeforeData: map[string]any{"status": model.ImportTaskStatusProcessing},
|
||||
AfterData: map[string]any{
|
||||
"status": status, "total_count": task.TotalCount, "success_count": successCount,
|
||||
"skip_count": skipCount, "fail_count": failCount,
|
||||
},
|
||||
Metadata: map[string]any{"skip_count": skipCount},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func batchAuditResult(successCount, failCount int) string {
|
||||
if successCount > 0 && failCount > 0 {
|
||||
return constants.AuditResultPartial
|
||||
}
|
||||
if successCount == 0 && failCount > 0 {
|
||||
return constants.AuditResultFailed
|
||||
}
|
||||
return constants.AuditResultSuccess
|
||||
}
|
||||
|
||||
// batchCreateWallets 批量为 IoT 卡创建资产钱包
|
||||
func (h *IotCardImportHandler) batchCreateWallets(ctx context.Context, cards []*model.IotCard) {
|
||||
if h.assetWalletStore == nil {
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
notificationinfra "github.com/break/junhong_cmp_fiber/internal/infrastructure/notification"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// NotificationCleanupHandler 处理低峰通知保留清理任务。
|
||||
@@ -22,6 +24,10 @@ func NewNotificationCleanupHandler(service *notificationinfra.CleanupService, lo
|
||||
|
||||
// Handle 执行有界、可重入的通知分批清理。
|
||||
func (h *NotificationCleanupHandler) Handle(ctx context.Context, _ *asynq.Task) error {
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeNotificationCleanup,
|
||||
ActorName: "站内通知清理任务", Source: constants.AuditSourceWorker,
|
||||
})
|
||||
h.logger.Info("开始执行站内通知保留清理")
|
||||
if err := h.service.Run(ctx); err != nil {
|
||||
h.logger.Error("站内通知保留清理失败", zap.String("failure_category", "database"), zap.Error(err))
|
||||
|
||||
@@ -3,6 +3,8 @@ package task
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -29,6 +31,10 @@ func NewOrderExpireHandler(orderExpirer OrderExpirer, logger *zap.Logger) *Order
|
||||
// HandleOrderExpire 处理订单超时取消任务
|
||||
// 由 Asynq Scheduler 每分钟触发,扫描并取消所有已超时的待支付订单
|
||||
func (h *OrderExpireHandler) HandleOrderExpire(ctx context.Context, _ *asynq.Task) error {
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorScheduledJob, ActorID: constants.AuditActorIDOrderExpireScheduler,
|
||||
ActorName: "订单过期关闭计划任务", Source: constants.AuditSourceScheduler,
|
||||
})
|
||||
cancelled, err := h.orderExpirer.CancelExpiredOrders(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("订单超时自动取消失败", zap.Error(err))
|
||||
|
||||
@@ -2,7 +2,9 @@ package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -10,10 +12,15 @@ import (
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
"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/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
||||
)
|
||||
|
||||
@@ -29,6 +36,7 @@ type OrderPackageInvalidateHandler struct {
|
||||
packageUsageStore *postgres.PackageUsageStore
|
||||
storageService *storage.Service
|
||||
logger *zap.Logger
|
||||
auditWriter *audit.Writer
|
||||
}
|
||||
|
||||
// NewOrderPackageInvalidateHandler 创建处理器实例
|
||||
@@ -38,14 +46,19 @@ func NewOrderPackageInvalidateHandler(
|
||||
packageUsageStore *postgres.PackageUsageStore,
|
||||
storageSvc *storage.Service,
|
||||
logger *zap.Logger,
|
||||
auditWriters ...*audit.Writer,
|
||||
) *OrderPackageInvalidateHandler {
|
||||
return &OrderPackageInvalidateHandler{
|
||||
handler := &OrderPackageInvalidateHandler{
|
||||
taskStore: taskStore,
|
||||
orderStore: orderStore,
|
||||
packageUsageStore: packageUsageStore,
|
||||
storageService: storageSvc,
|
||||
logger: logger,
|
||||
}
|
||||
if len(auditWriters) > 0 {
|
||||
handler.auditWriter = auditWriters[0]
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// Handle 处理批量失效订单套餐任务
|
||||
@@ -68,8 +81,17 @@ func (h *OrderPackageInvalidateHandler) Handle(ctx context.Context, t *asynq.Tas
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
if importTask.Status != model.ImportTaskStatusPending {
|
||||
rootEventID := audit.TaskEventID(constants.AuditResourceOrderPackageInvalidateTask, importTask.ID, "completed")
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeOrderPackageInvalidate,
|
||||
ActorName: "订单套餐批量失效任务", Source: constants.AuditSourceWorker,
|
||||
CorrelationID: importTask.TaskNo, ParentEventID: rootEventID,
|
||||
})
|
||||
claimed, err := h.taskStore.Claim(ctx, importTask.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !claimed {
|
||||
h.logger.Info("批量失效任务已处理,跳过",
|
||||
zap.Uint("task_id", payload.TaskID),
|
||||
zap.Int("status", importTask.Status),
|
||||
@@ -77,8 +99,6 @@ func (h *OrderPackageInvalidateHandler) Handle(ctx context.Context, t *asynq.Tas
|
||||
return nil
|
||||
}
|
||||
|
||||
h.taskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusProcessing, "")
|
||||
|
||||
h.logger.Info("开始处理批量失效订单套餐任务",
|
||||
zap.Uint("task_id", importTask.ID),
|
||||
zap.String("task_no", importTask.TaskNo),
|
||||
@@ -90,21 +110,24 @@ func (h *OrderPackageInvalidateHandler) Handle(ctx context.Context, t *asynq.Tas
|
||||
zap.Uint("task_id", importTask.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
h.taskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusFailed, err.Error())
|
||||
if finishErr := h.finishInvalidateTask(ctx, importTask, 0, 0, 1, model.ImportTaskStatusFailed, err.Error(), nil); finishErr != nil {
|
||||
h.resetInvalidateTaskForRetry(ctx, importTask.ID)
|
||||
return finishErr
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
successCount, failedItems := h.processRows(ctx, orderNos)
|
||||
successCount, failedItems := h.processRows(ctx, importTask.ID, orderNos)
|
||||
failCount := len(failedItems)
|
||||
totalCount := len(orderNos)
|
||||
|
||||
h.taskStore.UpdateResult(ctx, importTask.ID, totalCount, successCount, failCount,
|
||||
model.ImportResultItems(toImportResultItems(failedItems)))
|
||||
|
||||
status, errorMessage := model.ImportTaskStatusCompleted, ""
|
||||
if failCount > 0 && successCount == 0 {
|
||||
h.taskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusFailed, "所有行均处理失败")
|
||||
} else {
|
||||
h.taskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusCompleted, "")
|
||||
status, errorMessage = model.ImportTaskStatusFailed, "所有行均处理失败"
|
||||
}
|
||||
if err := h.finishInvalidateTask(ctx, importTask, totalCount, successCount, failCount, status, errorMessage, model.ImportResultItems(toImportResultItems(failedItems))); err != nil {
|
||||
h.resetInvalidateTaskForRetry(ctx, importTask.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("批量失效订单套餐任务完成",
|
||||
@@ -117,6 +140,12 @@ func (h *OrderPackageInvalidateHandler) Handle(ctx context.Context, t *asynq.Tas
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *OrderPackageInvalidateHandler) resetInvalidateTaskForRetry(ctx context.Context, taskID uint) {
|
||||
_ = h.taskStore.DB().WithContext(ctx).Model(&model.OrderPackageInvalidateTask{}).
|
||||
Where("id = ? AND status = ?", taskID, model.ImportTaskStatusProcessing).
|
||||
Updates(map[string]any{"status": model.ImportTaskStatusPending, "started_at": nil}).Error
|
||||
}
|
||||
|
||||
// invalidateRow 单行处理结果
|
||||
type invalidateRow struct {
|
||||
line int
|
||||
@@ -125,13 +154,13 @@ type invalidateRow struct {
|
||||
}
|
||||
|
||||
// processRows 逐行处理订单号,返回成功数和失败列表
|
||||
func (h *OrderPackageInvalidateHandler) processRows(ctx context.Context, rows []string) (int, []invalidateRow) {
|
||||
func (h *OrderPackageInvalidateHandler) processRows(ctx context.Context, taskID uint, rows []string) (int, []invalidateRow) {
|
||||
successCount := 0
|
||||
var failed []invalidateRow
|
||||
|
||||
for i, orderNo := range rows {
|
||||
line := i + 2 // 第1行为表头,数据从第2行开始
|
||||
if err := h.processOneOrder(ctx, orderNo); err != nil {
|
||||
if err := h.processOneOrder(ctx, taskID, orderNo); err != nil {
|
||||
failed = append(failed, invalidateRow{line: line, orderNo: orderNo, reason: err.Error()})
|
||||
} else {
|
||||
successCount++
|
||||
@@ -142,34 +171,117 @@ func (h *OrderPackageInvalidateHandler) processRows(ctx context.Context, rows []
|
||||
}
|
||||
|
||||
// processOneOrder 处理单个订单号:查订单 → 查套餐 → 批量更新状态=4
|
||||
func (h *OrderPackageInvalidateHandler) processOneOrder(ctx context.Context, orderNo string) error {
|
||||
func (h *OrderPackageInvalidateHandler) processOneOrder(ctx context.Context, taskID uint, orderNo string) error {
|
||||
order, err := h.orderStore.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil {
|
||||
h.appendInvalidateFailure(ctx, taskID, &model.Order{OrderNo: orderNo}, "订单不存在")
|
||||
return errOrderNotFound(orderNo)
|
||||
}
|
||||
|
||||
usages, err := h.packageUsageStore.ListActiveByOrderID(ctx, order.ID)
|
||||
queryFailed := false
|
||||
err = h.taskStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
usages, queryErr := postgres.NewPackageUsageStore(tx, nil).ListActiveByOrderID(ctx, order.ID)
|
||||
if queryErr != nil {
|
||||
queryFailed = true
|
||||
return queryErr
|
||||
}
|
||||
if len(usages) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]uint, 0, len(usages))
|
||||
resources := []audit.ResourceInput{audit.OrderResource(order, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleOrderTarget)}
|
||||
for _, usage := range usages {
|
||||
ids = append(ids, usage.ID)
|
||||
resources = append(resources, audit.PackageUsageResource(usage, constants.AuditResourceRelationAffected, constants.AuditResourceRolePackageUsageTarget,
|
||||
map[string]any{"status": usage.Status}, map[string]any{"status": constants.PackageUsageStatusInvalidated}))
|
||||
}
|
||||
if err := postgres.NewPackageUsageStore(tx, nil).BatchUpdateStatus(ctx, ids, constants.PackageUsageStatusInvalidated); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
EventID: audit.TaskEventID(constants.AuditResourceOrderPackageInvalidateTask, taskID, fmt.Sprintf("item:%d", order.ID)),
|
||||
ActionCode: constants.AuditActionOrderPackageInvalidateItem, Summary: "失效订单套餐权益",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess, Resources: resources,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return errQueryFailed(orderNo)
|
||||
}
|
||||
|
||||
if len(usages) == 0 {
|
||||
// 套餐全部已是终态,视为成功
|
||||
return nil
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(usages))
|
||||
for _, u := range usages {
|
||||
ids = append(ids, u.ID)
|
||||
}
|
||||
|
||||
if err := h.packageUsageStore.BatchUpdateStatus(ctx, ids, constants.PackageUsageStatusInvalidated); err != nil {
|
||||
summary := "更新套餐状态失败"
|
||||
if queryFailed {
|
||||
summary = "查询套餐失败"
|
||||
}
|
||||
h.appendInvalidateFailure(ctx, taskID, order, summary)
|
||||
if queryFailed {
|
||||
return errQueryFailed(orderNo)
|
||||
}
|
||||
return errUpdateFailed(orderNo)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *OrderPackageInvalidateHandler) appendInvalidateFailure(ctx context.Context, taskID uint, order *model.Order, summary string) {
|
||||
if h.auditWriter == nil || order == nil || order.OrderNo == "" {
|
||||
return
|
||||
}
|
||||
err := h.taskStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
keyHash := sha256.Sum256([]byte(order.OrderNo))
|
||||
return h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
EventID: fmt.Sprintf("task:order_invalidate:%d:failed:%x", taskID, keyHash[:6]),
|
||||
ActionCode: constants.AuditActionOrderPackageInvalidateItem, Summary: summary,
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultFailed,
|
||||
ErrorCode: fmt.Sprintf("%d", pkgerrors.CodeDatabaseError), ErrorSummary: summary,
|
||||
Resources: []audit.ResourceInput{audit.OrderResource(order, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleOrderTarget)},
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
auditfailure.RecordSecondaryWriteFailure(constants.AuditActionOrderPackageInvalidateItem, order.OrderNo, "", auditcontext.From(ctx).CorrelationID, fmt.Sprintf("%d", pkgerrors.CodeDatabaseError), err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *OrderPackageInvalidateHandler) finishInvalidateTask(ctx context.Context, task *model.OrderPackageInvalidateTask, totalCount, successCount, failCount, status int, errorMessage string, failedItems model.ImportResultItems) error {
|
||||
if h.auditWriter == nil {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "订单套餐失效任务统一审计接缝未配置")
|
||||
}
|
||||
return h.taskStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
txStore := h.taskStore.WithTx(tx)
|
||||
if err := txStore.UpdateResult(ctx, task.ID, totalCount, successCount, failCount, failedItems); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := txStore.UpdateStatus(ctx, task.ID, status, errorMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
rootID := audit.TaskEventID(constants.AuditResourceOrderPackageInvalidateTask, task.ID, "completed")
|
||||
var actualSuccess, actualFail int64
|
||||
if err := tx.WithContext(ctx).Model(&model.AuditEvent{}).
|
||||
Where("parent_event_id = ? AND action_code = ? AND result = ?", rootID, constants.AuditActionOrderPackageInvalidateItem, constants.AuditResultSuccess).
|
||||
Count(&actualSuccess).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.AuditEvent{}).
|
||||
Where("parent_event_id = ? AND action_code = ? AND result = ?", rootID, constants.AuditActionOrderPackageInvalidateItem, constants.AuditResultFailed).
|
||||
Count(&actualFail).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
auditFailCount := int(actualFail)
|
||||
if auditFailCount == 0 && failCount > 0 {
|
||||
auditFailCount = failCount
|
||||
}
|
||||
return h.auditWriter.WriteTask(ctx, tx, audit.TaskInput{
|
||||
EventID: rootID, ActionCode: constants.AuditActionOrderPackageInvalidateTaskCompleted,
|
||||
Summary: "完成订单套餐批量失效任务", TaskID: task.ID, TaskNo: task.TaskNo,
|
||||
Result: batchAuditResult(int(actualSuccess), auditFailCount), CorrelationID: task.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceOrderPackageInvalidateTask, task.ID, "created"),
|
||||
BatchTotal: int(actualSuccess) + auditFailCount, SuccessCount: int(actualSuccess), FailCount: auditFailCount,
|
||||
IdentitySnapshot: map[string]any{"id": task.ID, "task_no": task.TaskNo, "file_name": task.FileName},
|
||||
BeforeData: map[string]any{"status": model.ImportTaskStatusProcessing},
|
||||
AfterData: map[string]any{
|
||||
"status": status, "total_count": totalCount, "success_count": successCount, "fail_count": failCount,
|
||||
},
|
||||
Metadata: map[string]any{"task_success_count": successCount, "task_fail_count": failCount},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// downloadAndParseCSV 从对象存储下载 CSV 并解析 order_no 列
|
||||
func (h *OrderPackageInvalidateHandler) downloadAndParseCSV(ctx context.Context, task *model.OrderPackageInvalidateTask) ([]string, error) {
|
||||
if h.storageService == nil {
|
||||
|
||||
@@ -2,7 +2,6 @@ package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
@@ -11,11 +10,9 @@ import (
|
||||
|
||||
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
iot_card_svc "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
|
||||
"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"
|
||||
)
|
||||
|
||||
// PollingProtectHandler 保护期一致性检查任务处理器
|
||||
@@ -23,13 +20,10 @@ import (
|
||||
// 保护期结束:调 EvaluateAndAct 重新评估正常停复机条件
|
||||
// 两种路径不可混淆:保护期内=强制修正;保护期结束=重新评估
|
||||
type PollingProtectHandler struct {
|
||||
db *gorm.DB
|
||||
observationSeriesEvents cardObservationApp.SeriesEventWriter
|
||||
base *PollingBase
|
||||
gateway *gateway.Client
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
stopResumeSvc iot_card_svc.StopResumeServiceInterface
|
||||
base *PollingBase
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
stopResumeSvc iot_card_svc.StopResumeServiceInterface
|
||||
}
|
||||
|
||||
// NewPollingProtectHandler 创建保护期一致性检查任务处理器
|
||||
@@ -43,13 +37,10 @@ func NewPollingProtectHandler(
|
||||
stopResumeSvc iot_card_svc.StopResumeServiceInterface,
|
||||
) *PollingProtectHandler {
|
||||
return &PollingProtectHandler{
|
||||
db: db,
|
||||
observationSeriesEvents: observationSeriesEvents,
|
||||
base: base,
|
||||
gateway: gw,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
stopResumeSvc: stopResumeSvc,
|
||||
base: base,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceSimBindingStore: deviceSimBindingStore,
|
||||
stopResumeSvc: stopResumeSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,28 +96,15 @@ func (h *PollingProtectHandler) Handle(ctx context.Context, t *asynq.Task) error
|
||||
// 保护期内:停机保护期发现开机卡 → 强制停机(绕过 EvaluateAndAct)
|
||||
h.base.logger.Info("保护期一致性:停机保护期内发现开机卡,强制停机",
|
||||
zap.Uint("card_id", card.ID), zap.Uint("device_id", deviceID))
|
||||
if h.gateway == nil {
|
||||
if h.stopResumeSvc == nil {
|
||||
break
|
||||
}
|
||||
if err := h.gateway.StopCard(ctx, &gateway.CardOperationReq{CardNo: card.ICCID}); err != nil {
|
||||
if err := h.stopResumeSvc.ForceStopCard(ctx, card, constants.StopReasonProtectPeriod); err != nil {
|
||||
h.base.logger.Error("保护期强制停机失败",
|
||||
zap.Uint("card_id", card.ID), zap.Error(err))
|
||||
h.base.updateStats(ctx, constants.TaskTypePollingProtect, false, time.Since(startTime))
|
||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)
|
||||
}
|
||||
if updateErr := h.updateCardAndAppendNetworkSeries(ctx, cardID, map[string]any{
|
||||
"network_status": constants.NetworkStatusOffline,
|
||||
"stopped_at": time.Now(),
|
||||
"stop_reason": constants.StopReasonProtectPeriod,
|
||||
}, constants.CardObservationSceneBusinessStop, "offline", "stop", stopProtectGeneration); updateErr != nil {
|
||||
h.base.logger.Warn("保护期停机 DB 更新失败", zap.Uint("card_id", cardID), zap.Error(updateErr))
|
||||
h.base.invalidateCardCache(ctx, cardID)
|
||||
h.base.updateStats(ctx, constants.TaskTypePollingProtect, false, time.Since(startTime))
|
||||
if requeueErr := h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect); requeueErr != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, requeueErr, "保护期停机事务失败且重入队失败")
|
||||
}
|
||||
return updateErr
|
||||
}
|
||||
h.base.updateCardCache(ctx, cardID, map[string]any{"network_status": constants.NetworkStatusOffline})
|
||||
actionTaken = "forced_stop"
|
||||
|
||||
@@ -134,28 +112,15 @@ func (h *PollingProtectHandler) Handle(ctx context.Context, t *asynq.Task) error
|
||||
// 保护期内:复机保护期发现停机卡 → 强制复机(绕过 EvaluateAndAct)
|
||||
h.base.logger.Info("保护期一致性:复机保护期内发现停机卡,强制复机",
|
||||
zap.Uint("card_id", card.ID), zap.Uint("device_id", deviceID))
|
||||
if h.gateway == nil {
|
||||
if h.stopResumeSvc == nil {
|
||||
break
|
||||
}
|
||||
if err := h.gateway.StartCard(ctx, &gateway.CardOperationReq{CardNo: card.ICCID}); err != nil {
|
||||
if err := h.stopResumeSvc.ForceStartCard(ctx, card); err != nil {
|
||||
h.base.logger.Error("保护期强制复机失败",
|
||||
zap.Uint("card_id", card.ID), zap.Error(err))
|
||||
h.base.updateStats(ctx, constants.TaskTypePollingProtect, false, time.Since(startTime))
|
||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)
|
||||
}
|
||||
if updateErr := h.updateCardAndAppendNetworkSeries(ctx, cardID, map[string]any{
|
||||
"network_status": constants.NetworkStatusOnline,
|
||||
"resumed_at": time.Now(),
|
||||
"stop_reason": "",
|
||||
}, constants.CardObservationSceneBusinessResume, "online", "start", startProtectGeneration); updateErr != nil {
|
||||
h.base.logger.Warn("保护期复机 DB 更新失败", zap.Uint("card_id", cardID), zap.Error(updateErr))
|
||||
h.base.invalidateCardCache(ctx, cardID)
|
||||
h.base.updateStats(ctx, constants.TaskTypePollingProtect, false, time.Since(startTime))
|
||||
if requeueErr := h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect); requeueErr != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, requeueErr, "保护期复机事务失败且重入队失败")
|
||||
}
|
||||
return updateErr
|
||||
}
|
||||
h.base.updateCardCache(ctx, cardID, map[string]any{"network_status": constants.NetworkStatusOnline})
|
||||
actionTaken = "forced_resume"
|
||||
|
||||
@@ -189,35 +154,3 @@ func (h *PollingProtectHandler) Handle(ctx context.Context, t *asynq.Task) error
|
||||
h.base.updateStats(ctx, constants.TaskTypePollingProtect, true, time.Since(startTime))
|
||||
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingProtect)
|
||||
}
|
||||
|
||||
// updateCardAndAppendNetworkSeries 在同一事务中更新卡状态并写入网络观测请求。
|
||||
func (h *PollingProtectHandler) updateCardAndAppendNetworkSeries(
|
||||
ctx context.Context,
|
||||
cardID uint,
|
||||
fields map[string]any,
|
||||
scene string,
|
||||
expected string,
|
||||
operation string,
|
||||
protectGeneration string,
|
||||
) error {
|
||||
if h.db == nil || h.observationSeriesEvents == nil {
|
||||
return errors.New(errors.CodeInternalError, "保护期停复机观测 Outbox 能力未配置")
|
||||
}
|
||||
if protectGeneration == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "保护期停复机事件缺少保护期标识")
|
||||
}
|
||||
requestID := "card-observation:polling-protect:" + operation + ":" +
|
||||
strconv.FormatUint(uint64(cardID), 10) + ":" + protectGeneration
|
||||
return h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).Updates(fields).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新保护期停复机卡状态失败")
|
||||
}
|
||||
return h.observationSeriesEvents.AppendSeriesRequested(ctx, tx, cardObservationApp.SeriesRequestedEvent{
|
||||
EventID: requestID, Scene: scene,
|
||||
ResourceType: constants.CardObservationResourceTypeCard, ResourceID: cardID,
|
||||
SyncTypes: []string{constants.CardObservationSyncTypeNetwork}, ExpectedValue: expected,
|
||||
Source: constants.CardObservationSourceBusinessEvent, OccurredAt: time.Now().UTC(),
|
||||
RequestID: requestID, CorrelationID: requestID,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user