feat(运营报表): AUG26-015 设备激活与套餐续费日报快照、查询趋势与受控导出
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m33s
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
This commit is contained in:
@@ -208,7 +208,11 @@
|
||||
|
||||
## 2.17 已确认的报表基线
|
||||
|
||||
激活报表的采购数量以成功导入系统的设备数量计算,不另建采购或入库台账。功能上线后每日生成稳定日报快照;上线前日期不提供报表或明确显示无快照数据,不回填历史。累计激活设备严格采用任一当前关联卡已实名的口径;每日快照中的累计在网设备为已实名且存在有效主套餐的设备,活跃设备为该套餐周期内任一卡真流量大于零的设备,用量为设备当前套餐周期内全部关联卡真流量之和。报表可选择设备名称、型号、制造商、用户组、代理、店铺、业务员中的一个分组维度;未选择时汇总为一行。套餐续费按资产去重,统计期内有主套餐到期的资产计一次到期,至少成功续购一次主套餐计一次续费,续费率不超过 100%。日报快照冻结当天店铺、业务员及用户组归属。后端提供日/月趋势汇总数据和异步导出,图表渲染由前端负责。
|
||||
激活报表的采购数量以成功导入系统的设备数量计算,不另建采购或入库台账。
|
||||
|
||||
> **AUG26-015 实施口径标注(2026-09-17)**:上式「以成功导入系统的设备数量计算」按**系统内实际存在的设备**实施——采购数量 = 截至快照日(上海自然日)系统内未删除的设备数,与 `111.md` §22.4.1「系统录入的设备数量」同读法,且不新建采购或入库台账。不采用「设备导入任务成功行数」(`tb_device_import_task` 中 `operation_type='import'` 且已完成任务的 `success_count` 之和):生产库实测该值为 **474**,而系统内未删除设备为 **18,970**,差额来自老系统迁移脚本直接写入设备、绕过导入任务;按字面口径激活率约 **1,399%**,指标不可用。该口径收敛在一个口径函数内,切换成本为一行。本条其余文字与其余基线确认内容不变。
|
||||
|
||||
功能上线后每日生成稳定日报快照;上线前日期不提供报表或明确显示无快照数据,不回填历史。累计激活设备严格采用任一当前关联卡已实名的口径;每日快照中的累计在网设备为已实名且存在有效主套餐的设备,活跃设备为该套餐周期内任一卡真流量大于零的设备,用量为设备当前套餐周期内全部关联卡真流量之和。报表可选择设备名称、型号、制造商、用户组、代理、店铺、业务员中的一个分组维度;未选择时汇总为一行。套餐续费按资产去重,统计期内有主套餐到期的资产计一次到期,至少成功续购一次主套餐计一次续费,续费率不超过 100%。日报快照冻结当天店铺、业务员及用户组归属。后端提供日/月趋势汇总数据和异步导出,图表渲染由前端负责。
|
||||
|
||||
## 2.18 已确认的店铺批量换绑 Excel 基线
|
||||
|
||||
|
||||
949
docs/verification/add-operations-reports-verification.md
Normal file
949
docs/verification/add-operations-reports-verification.md
Normal file
File diff suppressed because one or more lines are too long
@@ -4483,5 +4483,82 @@
|
||||
"export-time-filter::三类异步导出的创建期冻结与不扩大范围"
|
||||
],
|
||||
"classification": "behavior"
|
||||
},
|
||||
{
|
||||
"entry_type": "http",
|
||||
"entry": "GET /api/admin/operations-reports/activation-summary",
|
||||
"capability": "operations-report",
|
||||
"requirements": [
|
||||
"operations-report::激活情况指标口径",
|
||||
"operations-report::报表维度分组与归属冻结",
|
||||
"operations-report::报表查询与趋势",
|
||||
"operations-report::报表权限、数据范围与导出冻结"
|
||||
],
|
||||
"classification": "behavior"
|
||||
},
|
||||
{
|
||||
"entry_type": "http",
|
||||
"entry": "GET /api/admin/operations-reports/activation-trend",
|
||||
"capability": "operations-report",
|
||||
"requirements": [
|
||||
"operations-report::激活情况指标口径",
|
||||
"operations-report::报表维度分组与归属冻结",
|
||||
"operations-report::报表查询与趋势",
|
||||
"operations-report::报表权限、数据范围与导出冻结"
|
||||
],
|
||||
"classification": "behavior"
|
||||
},
|
||||
{
|
||||
"entry_type": "http",
|
||||
"entry": "POST /api/admin/operations-reports/activation-summary/export",
|
||||
"capability": "operations-report",
|
||||
"requirements": [
|
||||
"operations-report::激活情况指标口径",
|
||||
"operations-report::报表权限、数据范围与导出冻结"
|
||||
],
|
||||
"classification": "behavior"
|
||||
},
|
||||
{
|
||||
"entry_type": "http",
|
||||
"entry": "GET /api/admin/operations-reports/package-renewal-summary",
|
||||
"capability": "operations-report",
|
||||
"requirements": [
|
||||
"operations-report::套餐续费指标口径",
|
||||
"operations-report::报表维度分组与归属冻结",
|
||||
"operations-report::报表查询与趋势",
|
||||
"operations-report::报表权限、数据范围与导出冻结"
|
||||
],
|
||||
"classification": "behavior"
|
||||
},
|
||||
{
|
||||
"entry_type": "http",
|
||||
"entry": "GET /api/admin/operations-reports/package-renewal-trend",
|
||||
"capability": "operations-report",
|
||||
"requirements": [
|
||||
"operations-report::套餐续费指标口径",
|
||||
"operations-report::报表维度分组与归属冻结",
|
||||
"operations-report::报表查询与趋势",
|
||||
"operations-report::报表权限、数据范围与导出冻结"
|
||||
],
|
||||
"classification": "behavior"
|
||||
},
|
||||
{
|
||||
"entry_type": "http",
|
||||
"entry": "POST /api/admin/operations-reports/package-renewal-summary/export",
|
||||
"capability": "operations-report",
|
||||
"requirements": [
|
||||
"operations-report::套餐续费指标口径",
|
||||
"operations-report::报表权限、数据范围与导出冻结"
|
||||
],
|
||||
"classification": "behavior"
|
||||
},
|
||||
{
|
||||
"entry_type": "async",
|
||||
"entry": "constants.TaskTypeOperationsReportSnapshot",
|
||||
"capability": "operations-report",
|
||||
"requirements": [
|
||||
"operations-report::每日报表快照与不回填历史"
|
||||
],
|
||||
"classification": "behavior"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5019,5 +5019,220 @@
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "operations-report",
|
||||
"requirement": "每日报表快照与不回填历史",
|
||||
"spec": "openspec/specs/operations-report/spec.md",
|
||||
"entries": [
|
||||
"constants.TaskTypeOperationsReportSnapshot"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"internal/task/operations_report_snapshot.go",
|
||||
"pkg/queue/handler.go",
|
||||
"cmd/worker/main.go"
|
||||
],
|
||||
"application_service_query": [
|
||||
"internal/application/operationsreport/generate.go"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/domain/operationsreport/metrics.go"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"migrations/000231_create_operations_report_snapshot.up.sql",
|
||||
"migrations/000231_create_operations_report_snapshot.down.sql",
|
||||
"internal/infrastructure/operationsreport/snapshot_store.go"
|
||||
],
|
||||
"verification": {
|
||||
"command": "rg -n \"TaskTypeOperationsReportSnapshot\" pkg/constants/constants.go pkg/queue/handler.go cmd/worker/main.go internal/task/operations_report_snapshot.go",
|
||||
"literal_output": [
|
||||
"pkg/constants/constants.go:100:\tTaskTypeOperationsReportSnapshot = \"operations:report:snapshot\" // 每日运营报表日报快照生成",
|
||||
"pkg/constants/constants.go:323:\tcase TaskTypeOperationsReportSnapshot:",
|
||||
"pkg/queue/handler.go:404:\th.mux.HandleFunc(constants.TaskTypeOperationsReportSnapshot, handler.Handle)",
|
||||
"pkg/queue/handler.go:405:\th.logger.Info(\"注册每日运营报表日报快照生成任务处理器\", zap.String(\"task_type\", constants.TaskTypeOperationsReportSnapshot))",
|
||||
"cmd/worker/main.go:1018:\t\tconstants.TaskTypeOperationsReportSnapshot,",
|
||||
"cmd/worker/main.go:1023:\t\tasynq.Queue(constants.QueueForTaskType(constants.TaskTypeOperationsReportSnapshot)),"
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "operations-report",
|
||||
"requirement": "激活情况指标口径",
|
||||
"spec": "openspec/specs/operations-report/spec.md",
|
||||
"entries": [
|
||||
"/api/admin/operations-reports/activation-summary",
|
||||
"/api/admin/operations-reports/activation-summary/export"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"internal/handler/admin/operations_report.go",
|
||||
"internal/exporter/operations_report_scene.go"
|
||||
],
|
||||
"application_service_query": [
|
||||
"internal/application/operationsreport/generate.go",
|
||||
"internal/query/operationsreport/aggregate.go"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/domain/operationsreport/metrics.go",
|
||||
"internal/domain/operationsreport/purchase.go"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"internal/infrastructure/operationsreport/source.go",
|
||||
"migrations/000231_create_operations_report_snapshot.up.sql"
|
||||
],
|
||||
"verification": {
|
||||
"command": "rg -n \"MBPerGB|func Ratio|func CardAverageGB|func ForecastCardAverageGB|func PurchaseCountAsOf|ValidMainPackageStatuses\" internal/domain/operationsreport/metrics.go internal/domain/operationsreport/purchase.go",
|
||||
"literal_output": [
|
||||
"internal/domain/operationsreport/metrics.go:15:const MBPerGB = 1024",
|
||||
"internal/domain/operationsreport/metrics.go:100:func Ratio(numerator, denominator int64) (float64, bool) {",
|
||||
"internal/domain/operationsreport/metrics.go:109:func CardAverageGB(totalRealTrafficMB float64, denominator int64) (float64, bool) {",
|
||||
"internal/domain/operationsreport/metrics.go:113:\taverage := totalRealTrafficMB / MBPerGB / float64(denominator)",
|
||||
"internal/domain/operationsreport/metrics.go:120:func ForecastCardAverageGB(totalRealTrafficMB float64, denominator int64, endDate time.Time) (float64, bool) {",
|
||||
"internal/domain/operationsreport/purchase.go:26:func PurchaseCountAsOf(ctx context.Context, source PurchaseCountSource, snapshotDate time.Time) (int64, error) {",
|
||||
"internal/domain/operationsreport/purchase.go:34:var ValidMainPackageStatuses = []int{"
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "operations-report",
|
||||
"requirement": "套餐续费指标口径",
|
||||
"spec": "openspec/specs/operations-report/spec.md",
|
||||
"entries": [
|
||||
"/api/admin/operations-reports/package-renewal-summary",
|
||||
"/api/admin/operations-reports/package-renewal-summary/export"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"internal/handler/admin/operations_report.go",
|
||||
"internal/exporter/operations_report_scene.go"
|
||||
],
|
||||
"application_service_query": [
|
||||
"internal/application/operationsreport/generate.go",
|
||||
"internal/query/operationsreport/aggregate.go"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/domain/operationsreport/renewal.go",
|
||||
"internal/domain/operationsreport/metrics.go"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"internal/infrastructure/operationsreport/source.go",
|
||||
"migrations/000231_create_operations_report_snapshot.up.sql"
|
||||
],
|
||||
"verification": {
|
||||
"command": "rg -n \"func IsRenewed|RenewalRate|RenewalDueAssetCount|RenewalRenewedAssetCount|due_assets\" internal/domain/operationsreport/renewal.go internal/domain/operationsreport/metrics.go internal/application/operationsreport/generate.go internal/query/operationsreport/aggregate.go",
|
||||
"literal_output": [
|
||||
"internal/domain/operationsreport/renewal.go:30:func IsRenewed(expired ExpiredMainUsage, candidates []MainUsageCandidate) bool {",
|
||||
"internal/domain/operationsreport/metrics.go:148:func RenewalRate(renewed, due int64) (float64, bool) {",
|
||||
"internal/application/operationsreport/generate.go:140:\t\t\tzap.Int64(\"renewal_due_asset_count\", snapshot.RenewalDueAssetCount),",
|
||||
"internal/application/operationsreport/generate.go:236:\tsnapshot.RenewalDueAssetCount = int64(len(dueAssets))",
|
||||
"internal/query/operationsreport/aggregate.go:304:\t\t\"COUNT(DISTINCT asset_type || ':' || asset_id) AS due_assets\","
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "operations-report",
|
||||
"requirement": "报表维度分组与归属冻结",
|
||||
"spec": "openspec/specs/operations-report/spec.md",
|
||||
"entries": [
|
||||
"/api/admin/operations-reports/activation-summary",
|
||||
"/api/admin/operations-reports/activation-trend",
|
||||
"/api/admin/operations-reports/package-renewal-summary",
|
||||
"/api/admin/operations-reports/package-renewal-trend"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"internal/handler/admin/operations_report.go"
|
||||
],
|
||||
"application_service_query": [
|
||||
"internal/query/operationsreport/aggregate.go"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/domain/operationsreport/dimension.go"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"internal/infrastructure/operationsreport/source.go",
|
||||
"migrations/000231_create_operations_report_snapshot.up.sql"
|
||||
],
|
||||
"verification": {
|
||||
"command": "rg -n \"DimensionAll =|PlaceholderUnset =|RootShopName|AgentAccountID\" internal/domain/operationsreport/dimension.go internal/infrastructure/operationsreport/source.go",
|
||||
"literal_output": [
|
||||
"internal/domain/operationsreport/dimension.go:27:const DimensionAll = \"全部\"",
|
||||
"internal/domain/operationsreport/dimension.go:50:const PlaceholderUnset = \"未设置\"",
|
||||
"internal/infrastructure/operationsreport/source.go:344:\tRootShopName string",
|
||||
"internal/infrastructure/operationsreport/source.go:345:\tAgentAccountID *uint",
|
||||
"internal/infrastructure/operationsreport/source.go:365:\t\tattribute.RootShopName = o.shops[rootID].ShopName",
|
||||
"internal/infrastructure/operationsreport/source.go:369:\t\t\tattribute.AgentAccountID = &agentID"
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "operations-report",
|
||||
"requirement": "报表查询与趋势",
|
||||
"spec": "openspec/specs/operations-report/spec.md",
|
||||
"entries": [
|
||||
"/api/admin/operations-reports/activation-summary",
|
||||
"/api/admin/operations-reports/activation-trend",
|
||||
"/api/admin/operations-reports/package-renewal-summary",
|
||||
"/api/admin/operations-reports/package-renewal-trend"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"internal/handler/admin/operations_report.go"
|
||||
],
|
||||
"application_service_query": [
|
||||
"internal/query/operationsreport/query.go",
|
||||
"internal/query/operationsreport/periods.go"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/domain/operationsreport/metrics.go"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"internal/model/dto/operations_report_dto.go"
|
||||
],
|
||||
"verification": {
|
||||
"command": "rg -n \"utils.ParseTimeRange|granularity 只能为 day 或 month|has_snapshot|snapshot_dates\" internal/query/operationsreport/query.go internal/model/dto/operations_report_dto.go",
|
||||
"literal_output": [
|
||||
"internal/query/operationsreport/query.go:43:\tstart, end, err := utils.ParseTimeRange(request.StartTime, request.EndTime)",
|
||||
"internal/query/operationsreport/query.go:134:\t\treturn nil, errors.New(errors.CodeInvalidParam, \"granularity 只能为 day 或 month\")",
|
||||
"internal/model/dto/operations_report_dto.go:54:\tHasSnapshot bool `json:\"has_snapshot\" description:\"结束日是否存在快照;为 false 时累计类与派生指标为空且分组行为空集\"`",
|
||||
"internal/model/dto/operations_report_dto.go:55:\tSnapshotDates []string `json:\"snapshot_dates\" description:\"区间内实际命中的快照日期(yyyy-MM-dd)\"`"
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"capability": "operations-report",
|
||||
"requirement": "报表权限、数据范围与导出冻结",
|
||||
"spec": "openspec/specs/operations-report/spec.md",
|
||||
"entries": [
|
||||
"/api/admin/operations-reports/activation-summary/export",
|
||||
"/api/admin/operations-reports/package-renewal-summary/export"
|
||||
],
|
||||
"handler_consumer_job": [
|
||||
"internal/routes/operations_report.go",
|
||||
"internal/handler/admin/operations_report.go"
|
||||
],
|
||||
"application_service_query": [
|
||||
"internal/service/export_task/service.go"
|
||||
],
|
||||
"domain_state_amount": [
|
||||
"internal/exporter/time_filters.go"
|
||||
],
|
||||
"store_migration_config": [
|
||||
"internal/exporter/registry.go",
|
||||
"internal/exporter/operations_report_scene.go"
|
||||
],
|
||||
"verification": {
|
||||
"command": "rg -n \"ensureOperationsReportExportAllowed|frozenQueryContext|PlatformManagementForbiddenMessage\" internal/exporter/operations_report_scene.go internal/routes/operations_report.go internal/query/operationsreport/query.go",
|
||||
"literal_output": [
|
||||
"internal/exporter/operations_report_scene.go:72:\tif err := ensureOperationsReportExportAllowed(params); err != nil {",
|
||||
"internal/exporter/operations_report_scene.go:79:\tresponse, err := s.query.ActivationSummary(frozenQueryContext(ctx, params), request)",
|
||||
"internal/exporter/operations_report_scene.go:289:func frozenQueryContext(ctx context.Context, params ExportParams) context.Context {",
|
||||
"internal/exporter/operations_report_scene.go:298:func ensureOperationsReportExportAllowed(params ExportParams) error {",
|
||||
"internal/exporter/operations_report_scene.go:302:\treturn errors.New(errors.CodeForbidden, constants.PlatformManagementForbiddenMessage)",
|
||||
"internal/routes/operations_report.go:24:\t\t\treturn errors.New(errors.CodeForbidden, constants.PlatformManagementForbiddenMessage)"
|
||||
],
|
||||
"exit_status": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user