Files
junhong_cmp_fiber/internal/task/operations_report_snapshot.go
break 5e78809b93
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m33s
feat(运营报表): AUG26-015 设备激活与套餐续费日报快照、查询趋势与受控导出
- 新增成对迁移 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
2026-09-18 09:42:28 +08:00

89 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}