暂存
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m32s

This commit is contained in:
2026-08-06 09:35:00 +08:00
parent 8659dfc658
commit 88cc5e96ec
75 changed files with 6520 additions and 513 deletions

View File

@@ -0,0 +1,61 @@
package task
import (
"context"
"fmt"
"time"
"github.com/bytedance/sonic"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/application/auditarchive"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// AuditDailyArchivePayload 是人工补档时可选的任务载荷。
type AuditDailyArchivePayload struct {
ArchiveDate string `json:"archive_date"`
}
// AuditDailyArchiveHandler 处理统一审计每日冷归档任务。
type AuditDailyArchiveHandler struct {
service *auditarchive.Service
logger *zap.Logger
}
// NewAuditDailyArchiveHandler 创建统一审计每日冷归档任务处理器。
func NewAuditDailyArchiveHandler(service *auditarchive.Service, logger *zap.Logger) *AuditDailyArchiveHandler {
return &AuditDailyArchiveHandler{service: service, logger: logger}
}
// Handle 执行前一完整自然日归档,或按任务载荷补档指定自然日。
func (h *AuditDailyArchiveHandler) Handle(ctx context.Context, task *asynq.Task) error {
if h.service == nil {
return fmt.Errorf("统一审计归档服务未配置")
}
var err error
if len(task.Payload()) == 0 {
err = h.service.ArchivePreviousDay(ctx)
} else {
var payload AuditDailyArchivePayload
if unmarshalErr := sonic.Unmarshal(task.Payload(), &payload); unmarshalErr != nil {
return fmt.Errorf("解析统一审计归档任务载荷失败: %w", unmarshalErr)
}
location, locationErr := time.LoadLocation(constants.AuditArchiveTimezone)
if locationErr != nil {
return fmt.Errorf("加载统一审计归档时区失败: %w", locationErr)
}
archiveDate, parseErr := time.ParseInLocation(time.DateOnly, payload.ArchiveDate, location)
if parseErr != nil {
return fmt.Errorf("解析统一审计归档日期失败: %w", parseErr)
}
err = h.service.ArchiveDate(ctx, archiveDate)
}
if err != nil {
h.logger.Error("统一审计每日冷归档失败", zap.Error(err))
return err
}
h.logger.Info("统一审计每日冷归档完成")
return nil
}

View File

@@ -0,0 +1,64 @@
package task
import (
"context"
"fmt"
"time"
"github.com/bytedance/sonic"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/application/auditarchive"
)
// AuditMonthlyRetentionPayload 是人工补跑月度清理时可选的任务载荷。
type AuditMonthlyRetentionPayload struct {
ArchiveMonth string `json:"archive_month"`
}
// AuditMonthlyRetentionHandler 处理归档完整性门禁与上月在线日志物理清理。
type AuditMonthlyRetentionHandler struct {
service *auditarchive.Service
logger *zap.Logger
}
// NewAuditMonthlyRetentionHandler 创建月度日志留存清理处理器。
func NewAuditMonthlyRetentionHandler(service *auditarchive.Service, logger *zap.Logger) *AuditMonthlyRetentionHandler {
return &AuditMonthlyRetentionHandler{service: service, logger: logger}
}
// Handle 校验整月归档后按固定顺序分批物理删除 PostgreSQL 在线日志。
func (h *AuditMonthlyRetentionHandler) Handle(ctx context.Context, task *asynq.Task) error {
if h.service == nil {
return fmt.Errorf("月度日志留存清理服务未配置")
}
startedAt := time.Now()
var result auditarchive.RetentionResult
var err error
if len(task.Payload()) == 0 {
result, err = h.service.CleanupPreviousMonth(ctx)
} else {
var payload AuditMonthlyRetentionPayload
if unmarshalErr := sonic.Unmarshal(task.Payload(), &payload); unmarshalErr != nil {
return fmt.Errorf("解析月度日志留存清理任务载荷失败: %w", unmarshalErr)
}
month, parseErr := parseArchiveMonth(payload.ArchiveMonth)
if parseErr != nil {
return parseErr
}
result, err = h.service.CleanupMonth(ctx, month)
}
fields := []zap.Field{
zap.String("archive_month", result.Month), zap.Int64("audit_event_count", result.EventCount),
zap.Int64("event_resource_count", result.ResourceCount), zap.Int64("integration_log_count", result.IntegrationCount),
zap.Duration("duration", time.Since(startedAt)), zap.Int("manifest_count", len(result.ManifestKeys)),
}
if err != nil {
fields = append(fields, zap.String("severity", "critical"), zap.Error(err))
h.logger.Error("月度日志留存清理失败PostgreSQL 整月清理已阻断或等待断点续跑", fields...)
return err
}
h.logger.Info("月度日志留存清理完成", fields...)
return nil
}

View File

@@ -27,7 +27,10 @@ import (
// AutoPurchasePayload 充值后自动购包任务载荷
type AutoPurchasePayload struct {
RechargeOrderID uint `json:"recharge_order_id"`
RechargeOrderID uint `json:"recharge_order_id"`
RequestID string `json:"request_id,omitempty"`
CorrelationID string `json:"correlation_id,omitempty"`
ParentEventID string `json:"parent_event_id,omitempty"`
}
// AutoPurchaseHandler 充值后自动购包任务处理器
@@ -123,10 +126,14 @@ 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
}
correlationID := payload.CorrelationID
if correlationID == "" {
correlationID = rechargeOrder.RechargeOrderNo
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeAutoPurchaseAfterRecharge,
ActorName: "充值后自动购包任务", Source: constants.AuditSourceWorker,
CorrelationID: rechargeOrder.RechargeOrderNo,
RequestID: payload.RequestID, CorrelationID: correlationID, ParentEventID: payload.ParentEventID,
})
if rechargeOrder.AutoPurchaseStatus == constants.AutoPurchaseStatusSuccess {
@@ -301,7 +308,11 @@ func (h *AutoPurchaseHandler) ProcessTask(ctx context.Context, task *asynq.Task)
// 事务提交成功后触发佣金计算(不在事务内,防止任务提交后事务回滚的数据一致性问题)
if h.asynqClient != nil && createdOrderID > 0 {
payloadBytes, marshalErr := sonic.Marshal(map[string]any{"order_id": createdOrderID})
linkage := auditcontext.From(ctx)
payloadBytes, marshalErr := sonic.Marshal(CommissionCalculationPayload{
OrderID: createdOrderID, RequestID: linkage.RequestID,
CorrelationID: linkage.CorrelationID, ParentEventID: linkage.ParentEventID,
})
if marshalErr != nil {
h.logger.Warn("佣金任务载荷序列化失败",
zap.Uint("order_id", createdOrderID),

View File

@@ -8,6 +8,8 @@ import (
"go.uber.org/zap"
cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
@@ -32,6 +34,11 @@ func (h *CardObservationSeriesHandler) Handle(ctx context.Context, task *asynq.T
h.logger.Error("解析卡观测序列任务载荷失败", zap.Error(err))
return errors.Wrap(errors.CodeInvalidParam, err, "卡观测序列任务载荷无法解析")
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeCardObservationSeries,
ActorName: "卡观测序列任务", Source: constants.AuditSourceWorker,
RequestID: payload.RequestID, CorrelationID: payload.CorrelationID, ParentEventID: payload.ParentEventID,
})
if err := h.service.Execute(ctx, payload); err != nil {
h.logger.Warn("卡观测序列当前尝试失败",
zap.String("series_id", payload.SeriesID), zap.Int("attempt", payload.Attempt), zap.Error(err))

View File

@@ -18,7 +18,10 @@ const (
)
type CommissionCalculationPayload struct {
OrderID uint `json:"order_id"`
OrderID uint `json:"order_id"`
RequestID string `json:"request_id,omitempty"`
CorrelationID string `json:"correlation_id,omitempty"`
ParentEventID string `json:"parent_event_id,omitempty"`
}
type CommissionCalculationHandler struct {
@@ -48,9 +51,15 @@ func (h *CommissionCalculationHandler) HandleCommissionCalculation(ctx context.C
)
return asynq.SkipRetry
}
correlationID := payload.CorrelationID
if correlationID == "" {
correlationID = task.ResultWriter().TaskID()
}
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: constants.AuditActorIDCommissionCalculationWorker,
ActorName: "订单佣金计算任务", Source: constants.AuditSourceWorker,
RequestID: payload.RequestID, CorrelationID: correlationID,
ParentEventID: payload.ParentEventID,
})
if err := h.service.CalculateCommission(ctx, payload.OrderID); err != nil {

View File

@@ -0,0 +1,113 @@
package task
import (
"context"
"fmt"
"time"
"github.com/bytedance/sonic"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/application/auditarchive"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// IntegrationDailyArchivePayload 是人工补档时可选的任务载荷。
type IntegrationDailyArchivePayload struct {
ArchiveDate string `json:"archive_date"`
}
// IntegrationMonthlyFinalizePayload 是人工月度复核时可选的任务载荷。
type IntegrationMonthlyFinalizePayload struct {
ArchiveMonth string `json:"archive_month"`
}
// IntegrationArchiveHandler 处理 Integration Log 每日归档与月度最终复核。
type IntegrationArchiveHandler struct {
service *auditarchive.Service
logger *zap.Logger
}
// NewIntegrationArchiveHandler 创建 Integration Log 归档任务处理器。
func NewIntegrationArchiveHandler(service *auditarchive.Service, logger *zap.Logger) *IntegrationArchiveHandler {
return &IntegrationArchiveHandler{service: service, logger: logger}
}
// HandleDaily 执行前一完整自然日归档,或按任务载荷补档指定自然日。
func (h *IntegrationArchiveHandler) HandleDaily(ctx context.Context, task *asynq.Task) error {
if h.service == nil {
return fmt.Errorf("Integration Log 归档服务未配置")
}
var err error
if len(task.Payload()) == 0 {
err = h.service.ArchivePreviousIntegrationDay(ctx)
} else {
var payload IntegrationDailyArchivePayload
if unmarshalErr := sonic.Unmarshal(task.Payload(), &payload); unmarshalErr != nil {
return fmt.Errorf("解析 Integration Log 每日归档任务载荷失败: %w", unmarshalErr)
}
date, parseErr := parseArchiveDate(payload.ArchiveDate)
if parseErr != nil {
return parseErr
}
err = h.service.ArchiveIntegrationDate(ctx, date)
}
if err != nil {
h.logger.Error("Integration Log 每日冷归档失败", zap.Error(err))
return err
}
h.logger.Info("Integration Log 每日冷归档完成")
return nil
}
// HandleMonthlyFinalize 执行上一个完整自然月复核,或按任务载荷复核指定月份。
func (h *IntegrationArchiveHandler) HandleMonthlyFinalize(ctx context.Context, task *asynq.Task) error {
if h.service == nil {
return fmt.Errorf("Integration Log 归档服务未配置")
}
var err error
if len(task.Payload()) == 0 {
err = h.service.FinalizePreviousIntegrationMonth(ctx)
} else {
var payload IntegrationMonthlyFinalizePayload
if unmarshalErr := sonic.Unmarshal(task.Payload(), &payload); unmarshalErr != nil {
return fmt.Errorf("解析 Integration Log 月度复核任务载荷失败: %w", unmarshalErr)
}
month, parseErr := parseArchiveMonth(payload.ArchiveMonth)
if parseErr != nil {
return parseErr
}
err = h.service.FinalizeIntegrationMonth(ctx, month)
}
if err != nil {
h.logger.Error("Integration Log 月度最终版本复核失败,后续清理必须阻止", zap.Error(err))
return err
}
h.logger.Info("Integration Log 月度最终版本复核完成")
return nil
}
func parseArchiveDate(value string) (time.Time, error) {
location, err := time.LoadLocation(constants.AuditArchiveTimezone)
if err != nil {
return time.Time{}, fmt.Errorf("加载 Integration Log 归档时区失败: %w", err)
}
date, err := time.ParseInLocation(time.DateOnly, value, location)
if err != nil {
return time.Time{}, fmt.Errorf("解析 Integration Log 归档日期失败: %w", err)
}
return date, nil
}
func parseArchiveMonth(value string) (time.Time, error) {
location, err := time.LoadLocation(constants.AuditArchiveTimezone)
if err != nil {
return time.Time{}, fmt.Errorf("加载 Integration Log 归档时区失败: %w", err)
}
month, err := time.ParseInLocation("2006-01", value, location)
if err != nil {
return time.Time{}, fmt.Errorf("解析 Integration Log 归档月份失败: %w", err)
}
return month, nil
}

View File

@@ -23,10 +23,11 @@ func NewNotificationCleanupHandler(service *notificationinfra.CleanupService, lo
}
// Handle 执行有界、可重入的通知分批清理。
func (h *NotificationCleanupHandler) Handle(ctx context.Context, _ *asynq.Task) error {
func (h *NotificationCleanupHandler) Handle(ctx context.Context, task *asynq.Task) error {
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeNotificationCleanup,
ActorName: "站内通知清理任务", Source: constants.AuditSourceWorker,
CorrelationID: task.ResultWriter().TaskID(),
})
h.logger.Info("开始执行站内通知保留清理")
if err := h.service.Run(ctx); err != nil {

View File

@@ -77,6 +77,7 @@ func (h *PollingCarddataHandler) Handle(ctx context.Context, task *asynq.Task) e
if logErr := completeGatewayAttempt(ctx, h.integration, attempt, true, attemptStartedAt); logErr != nil {
return h.failAndRequeue(ctx, cardID, startedAt, "完成 Gateway 流量 Integration Log 失败", logErr)
}
ctx = withPollingWorkerAuditContext(ctx, constants.TaskTypePollingCarddata, "卡流量轮询任务", attempt.IntegrationID)
if h.observation == nil || h.carrier == nil {
return h.failAndRequeue(ctx, cardID, startedAt, "卡流量观测能力未配置", nil)
}

View File

@@ -76,6 +76,7 @@ func (h *PollingCardStatusHandler) Handle(ctx context.Context, task *asynq.Task)
if logErr := completeGatewayAttempt(ctx, h.integration, attempt, true, attemptStartedAt); logErr != nil {
return h.failAndRequeue(ctx, cardID, startedAt, "完成 Gateway 网络 Integration Log 失败", logErr)
}
ctx = withPollingWorkerAuditContext(ctx, constants.TaskTypePollingCardStatus, "卡网络状态轮询任务", attempt.IntegrationID)
if h.observation == nil {
return h.failAndRequeue(ctx, cardID, startedAt, "卡网络观测能力未配置", nil)
}

View File

@@ -44,6 +44,7 @@ func (h *PollingPackageHandler) Handle(ctx context.Context, t *asynq.Task) error
if !ok {
return nil
}
ctx = withPollingWorkerAuditContext(ctx, constants.TaskTypePollingPackage, "套餐状态轮询任务", t.ResultWriter().TaskID())
if !h.base.acquireConcurrency(ctx, constants.TaskTypePollingPackage) {
h.base.logger.Debug("并发已满,重新入队", zap.Uint("card_id", cardID))

View File

@@ -52,6 +52,7 @@ func (h *PollingProtectHandler) Handle(ctx context.Context, t *asynq.Task) error
if !ok {
return nil
}
ctx = withPollingWorkerAuditContext(ctx, constants.TaskTypePollingProtect, "保护期一致性轮询任务", t.ResultWriter().TaskID())
if !h.base.acquireConcurrency(ctx, constants.TaskTypePollingProtect) {
h.base.logger.Debug("并发已满,重新入队", zap.Uint("card_id", cardID))

View File

@@ -68,6 +68,7 @@ func (h *PollingRealnameHandler) Handle(ctx context.Context, task *asynq.Task) e
if logErr := completeGatewayAttempt(ctx, h.integration, attempt, true, attemptStartedAt); logErr != nil {
return h.failAndRequeue(ctx, cardID, startedAt, "完成 Gateway 实名 Integration Log 失败", logErr)
}
ctx = withPollingWorkerAuditContext(ctx, constants.TaskTypePollingRealname, "实名状态轮询任务", attempt.IntegrationID)
if h.observation == nil {
return h.failAndRequeue(ctx, cardID, startedAt, "卡实名观测能力未配置", nil)
}

View File

@@ -1,14 +1,29 @@
package task
import (
"context"
"strconv"
"time"
"github.com/bytedance/sonic"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// withPollingWorkerAuditContext 为实际改变业务事实的轮询任务补充真实系统操作者与链路。
func withPollingWorkerAuditContext(ctx context.Context, taskType, taskName, correlationID string) context.Context {
if correlationID == "" {
correlationID = taskType
}
return auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: taskType,
ActorName: taskName, Source: constants.AuditSourceWorker, CorrelationID: correlationID,
})
}
// shortTaskType 从完整任务类型中提取简短名称(如 polling:carddata → carddata
func shortTaskType(fullTaskType string) string {
for i := len(fullTaskType) - 1; i >= 0; i-- {