All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m33s
- 新增成对迁移 000231 与三张快照表:日级头行、设备粒度激活行、到期事件粒度续费行,以快照日期为唯一键 - 新增每日 03:30(Asia/Shanghai)日报快照生成任务与幂等整日替换,失败重试沿用同一目标日 - 新增六条受控入口:两张报表的汇总、日/月趋势与受控导出,配套查询层只读快照事实 - 新增 operations_activation 与 operations_renewal 两个导出场景,创建期冻结筛选与可见店铺范围、派发期冻结表头、执行期只按冻结值复核资格 - 采购数量口径按系统内未删除设备数实施并在 PRD 标注,附实测差额依据 - 同步证据链 requirement-evidence.json 与入口能力矩阵、ARCHITECTURE 与验证记录 - 归档 change add-operations-reports 并新建主 Spec openspec/specs/operations-report/spec.md
89 lines
3.8 KiB
Go
89 lines
3.8 KiB
Go
package task
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"github.com/bytedance/sonic"
|
||
"github.com/hibiken/asynq"
|
||
"go.uber.org/zap"
|
||
|
||
operationsreportapp "github.com/break/junhong_cmp_fiber/internal/application/operationsreport"
|
||
domainreport "github.com/break/junhong_cmp_fiber/internal/domain/operationsreport"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
)
|
||
|
||
// OperationsReportSnapshotPayload 是每日运营报表快照任务的载荷。
|
||
//
|
||
// 载荷只含单一目标上海自然日。仓库内没有该任务的入队点(不提供补跑接口),
|
||
// 带载荷的调用只能来自运维侧 asynq 控制台或临时程序,此时目标日由载荷固定。
|
||
// 定时调度**不带载荷**(asynq 的 Scheduler 只能注册静态 Task,无法按次生成 payload),
|
||
// 此时目标日 = 处理时刻所在上海自然日的前一天。
|
||
//
|
||
// 「重试沿用同一目标日期」的依据不是「沿用同一份载荷」(空载荷会在重试时刻重新推导),
|
||
// 而是「cron 时点 + 有界重试窗口」:默认重试延迟为 n^4+15+rand(0..29)*(n+1) 秒
|
||
// (asynq v0.25.1 server.go 的 DefaultRetryDelayFunc,本仓库 pkg/queue/server.go 显式采用),
|
||
// MaxRetry(3) 的三次重试合计 ≤ 236 秒,叠加 Timeout(30m) 仍远小于一个上海自然日;
|
||
// 服务端重试路径不经过唯一锁,因此 23 小时去重窗口不会吞掉失败重试。
|
||
// 调整 cron 时点、MaxRetry 或 Timeout 必须重新评估该前提。
|
||
type OperationsReportSnapshotPayload struct {
|
||
SnapshotDate string `json:"snapshot_date"`
|
||
}
|
||
|
||
// OperationsReportSnapshotHandler 每日运营报表快照任务处理器。
|
||
// 处理器是薄壳:解析目标日期后调用生成用例,口径与幂等全部在用例内闭合。
|
||
type OperationsReportSnapshotHandler struct {
|
||
generator *operationsreportapp.Generator
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// NewOperationsReportSnapshotHandler 创建每日运营报表快照任务处理器。
|
||
func NewOperationsReportSnapshotHandler(generator *operationsreportapp.Generator, logger *zap.Logger) *OperationsReportSnapshotHandler {
|
||
return &OperationsReportSnapshotHandler{generator: generator, logger: logger}
|
||
}
|
||
|
||
// Handle 生成当日(或载荷指定的)运营报表日报快照。
|
||
func (h *OperationsReportSnapshotHandler) Handle(ctx context.Context, task *asynq.Task) error {
|
||
if h == nil || h.generator == nil {
|
||
return errors.New(errors.CodeInternalError, "运营报表快照生成用例未配置")
|
||
}
|
||
snapshotDate, err := ResolveOperationsReportSnapshotDate(task, time.Now())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := h.generator.Generate(ctx, snapshotDate); err != nil {
|
||
if h.logger != nil {
|
||
h.logger.Error("运营报表日报快照生成失败",
|
||
zap.String("snapshot_date", domainreport.FormatSnapshotDay(snapshotDate)),
|
||
zap.Error(err))
|
||
}
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ResolveOperationsReportSnapshotDate 解析本次生成的目标上海自然日。
|
||
//
|
||
// 载荷为空(定时调度)时取 now 所在上海自然日的前一自然日;
|
||
// 载荷给出目标日期时必须为 yyyy-MM-dd,非法载荷直接失败,不静默退化为「当天」。
|
||
func ResolveOperationsReportSnapshotDate(task *asynq.Task, now time.Time) (time.Time, error) {
|
||
if task != nil {
|
||
payload := task.Payload()
|
||
if len(payload) > 0 {
|
||
var parsed OperationsReportSnapshotPayload
|
||
if err := sonic.Unmarshal(payload, &parsed); err != nil {
|
||
return time.Time{}, errors.Wrap(errors.CodeInvalidParam, err, "运营报表快照任务载荷格式不正确")
|
||
}
|
||
if parsed.SnapshotDate != "" {
|
||
day, err := domainreport.ParseSnapshotDay(parsed.SnapshotDate)
|
||
if err != nil {
|
||
return time.Time{}, errors.New(errors.CodeInvalidParam,
|
||
"运营报表快照任务的目标日期必须为 yyyy-MM-dd 格式的上海自然日")
|
||
}
|
||
return day, nil
|
||
}
|
||
}
|
||
}
|
||
return domainreport.PreviousDay(now), nil
|
||
}
|