feat(导出时间筛选): AUG26-014 统一时间筛选与临期导出,归档并同步主 Spec 与证据链
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 13m40s

统一时间筛选:新增共享严格解析器 pkg/utils/time_range.go,只接受带显式时区的 RFC3339 秒级时间(拒绝小数秒、无时区、date-only、空格分隔、±hhmm、未补零、非法日期与越界偏移),闭区间含两端、归一为 UTC 瞬时,创建期与执行期共用同一份实现。

端点改造(13 个入口):IoT 卡导入任务、设备导入任务、导出任务列表、订单列表参数名不变仅收紧解析;换货、分配记录、代理充值、临期列表改名 start_time/end_time(旧参数名显式拒绝);提现记录两处删除解析失败静默跳过,非法参数一律 1001;授权记录由起始闭结束开改为闭区间含两端;临期列表改按当前生效主套餐最终到期时刻比较,保留剩余天数上下限与既有粗放窗口。

临期导出新建:新场景 expiring_asset 与受控入口 POST /api/admin/expiring-assets/export,复用列表候选预筛与最终到期推算,一行一资产、加油包不单独成行,列序与 111 §18.1 逐列一致,店铺/业务员/用户组按执行时当前归属补充且不超出创建时冻结范围。

佣金明细导出新增按创建时间闭区间筛选(原佣金与回溯两条分支各自创建时间列),记录粒度、列定义与余额口径不变。

冻结与遗留任务:创建期把筛选与时间边界规范化为 UTC RFC3339 秒级串写入既有 query_json,无新列无迁移;执行期只按冻结值严格解析,非法冻结值在任何分片与文件动作前落任务失败并写安全摘要,不放行全量;重试沿用原快照。达量预警导出执行期同样纳入严格解析(其入口契约、列定义与触发快照口径不变)。

归档 add-export-time-filter-standards 并新建主 Spec openspec/specs/export-time-filter/spec.md,同步 requirement-evidence.json 与入口能力矩阵,README 导出场景清单更新为 11 个场景。

验证:junhong_cmp_test + 本地隔离 Redis(DB7,测试部署共享队列 DB6 未被占用)实跑 85 PASS / 0 FAIL(接受/拒绝集合、区间与顺序语义、列表与导出同筛选行集一致、代理 HTTP 全链路与范围冻结、遗留旧格式任务安全失败、列与余额口径回归、表头逐字),门禁 gofmt/go build/gendocs 两次一致/openspec validate/doctor/context-health 全绿;无 Schema 变更、无迁移、无运行时开关。
This commit is contained in:
2026-09-17 18:37:02 +08:00
parent e8ab1f471e
commit 62419d4b17
67 changed files with 2405 additions and 398 deletions

View File

@@ -4471,5 +4471,17 @@
"asset-auto-renewal::成功后的可靠复机"
],
"classification": "behavior"
},
{
"entry_type": "http",
"entry": "POST /api/admin/expiring-assets/export",
"capability": "export-time-filter",
"requirements": [
"export-time-filter::统一时间筛选参数与解析契约",
"export-time-filter::受影响端点与旧格式替换",
"export-time-filter::临期导出",
"export-time-filter::三类异步导出的创建期冻结与不扩大范围"
],
"classification": "behavior"
}
]

View File

@@ -4830,5 +4830,194 @@
],
"exit_status": 0
}
},
{
"capability": "export-time-filter",
"requirement": "统一时间筛选参数与解析契约",
"spec": "openspec/specs/export-time-filter/spec.md",
"entries": [
"/api/admin/iot-cards/import-tasks",
"/api/admin/devices/import/tasks",
"/api/admin/export-tasks",
"/api/admin/orders",
"/api/admin/exchanges",
"/api/admin/asset-allocation-records",
"/api/admin/agent-recharges",
"/api/admin/shops/{shop_id}/commission-records",
"/api/admin/commission/withdrawal-requests",
"/api/admin/shops/{shop_id}/withdrawal-requests",
"/api/admin/authorizations",
"/api/admin/expiring-assets"
],
"handler_consumer_job": [
"internal/handler/admin/exchange.go",
"internal/handler/admin/asset.go",
"internal/handler/admin/authorization.go",
"internal/model/dto/export_task_dto.go"
],
"application_service_query": [
"internal/query/packageexpiry/list.go",
"internal/query/exchange/list.go"
],
"domain_state_amount": [
"pkg/utils/time_range.go"
],
"store_migration_config": [
"internal/store/postgres/commission_record_store.go"
],
"verification": {
"command": "rg -n 'timeFilterLayout|timeFilterPattern|func ParseTimeRange|FormatTimeFilterValue|TimeFilterFormatError|timeFilterFormatMessageSuffix|timeFilterOrderMessage' pkg/utils/time_range.go",
"literal_output": [
"pkg/utils/time_range.go:21:const timeFilterLayout = \"2006-01-02T15:04:05Z07:00\"",
"pkg/utils/time_range.go:24:const timeFilterFormatMessageSuffix = \" 时间格式不合法,必须为带时区的 RFC3339 秒级时间,例如 2026-09-01T00:00:00+08:00\"",
"pkg/utils/time_range.go:27:const timeFilterOrderMessage = \"start_time 不能晚于 end_time\"",
"pkg/utils/time_range.go:34:func ParseTimeRange(start, end string) (*time.Time, *time.Time, error) {",
"pkg/utils/time_range.go:50:func FormatTimeFilterValue(value time.Time) string {",
"pkg/utils/time_range.go:55:func TimeFilterFormatError(field string) error {",
"pkg/utils/time_range.go:63:var timeFilterPattern = regexp.MustCompile(`^\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$`)"
],
"exit_status": 0
}
},
{
"capability": "export-time-filter",
"requirement": "受影响端点与旧格式替换",
"spec": "openspec/specs/export-time-filter/spec.md",
"entries": [
"/api/admin/exchanges",
"/api/admin/asset-allocation-records",
"/api/admin/agent-recharges",
"/api/admin/shops/{shop_id}/commission-records",
"/api/admin/commission/withdrawal-requests",
"/api/admin/shops/{shop_id}/withdrawal-requests",
"/api/admin/authorizations",
"/api/admin/expiring-assets"
],
"handler_consumer_job": [
"internal/handler/admin/exchange.go",
"internal/handler/admin/agent_recharge.go",
"internal/handler/admin/authorization.go",
"internal/handler/admin/asset.go"
],
"application_service_query": [
"internal/query/exchange/list.go",
"internal/service/agent_recharge/service.go",
"internal/service/commission_withdrawal/service.go",
"internal/service/shop_commission/service.go",
"internal/query/packageexpiry/list.go"
],
"domain_state_amount": [
"internal/exporter/time_filters.go"
],
"store_migration_config": [
"internal/store/postgres/enterprise_card_authorization_store.go",
"internal/store/postgres/asset_allocation_record_store.go"
],
"verification": {
"command": "rg -n -e 'start_time' -e 'strictTimeRange' -e 'authorized_at <= ?' -e '闭区间' internal/model/dto/agent_recharge_dto.go internal/model/dto/exchange_dto.go internal/service/commission_withdrawal/service.go internal/service/shop_commission/service.go internal/exporter/exchange_scene.go internal/exporter/agent_recharge_scene.go internal/exporter/order_scene.go internal/query/packageexpiry/list.go internal/store/postgres/enterprise_card_authorization_store.go internal/store/postgres/asset_allocation_record_store.go",
"literal_output": [
"internal/exporter/exchange_scene.go:124:\tstart, end, err := strictTimeRange(params.Filters)",
"internal/exporter/agent_recharge_scene.go:139:\tstart, end, err := strictTimeRange(params.Filters)",
"internal/model/dto/agent_recharge_dto.go:140:\tStartTime string `json:\"start_time\" query:\"start_time\" description:\"创建时间起始(带时区的 RFC3339 秒级时间,闭区间含该时刻,如 2026-09-01T00:00:00+08:00\"`",
"internal/query/packageexpiry/list.go:72:\t\tStartTime: startTime,",
"internal/query/packageexpiry/list.go:241:\t\t// 按当前生效主套餐的最终到期时刻做闭区间比较,含两端。",
"internal/model/dto/exchange_dto.go:24:\tStartTime string `json:\"start_time\" query:\"start_time\" description:\"创建时间起始(带时区的 RFC3339 秒级时间,闭区间含该时刻,如 2026-09-01T00:00:00+08:00\"`",
"internal/exporter/order_scene.go:151:\tstart, end, err := strictTimeRange(params.Filters)",
"internal/store/postgres/enterprise_card_authorization_store.go:347:\t\tbaseQuery += \" AND a.authorized_at <= ?\"",
"internal/store/postgres/asset_allocation_record_store.go:81:\tif createdAtStart, ok := filters[\"start_time\"].(time.Time); ok {",
"internal/store/postgres/asset_allocation_record_store.go:84:\tif createdAtEnd, ok := filters[\"end_time\"].(time.Time); ok {",
"internal/service/commission_withdrawal/service.go:68:\t\tStartTime: startTime,",
"internal/service/shop_commission/service.go:103:\t\tStartTime: startTime,"
],
"exit_status": 0
}
},
{
"capability": "export-time-filter",
"requirement": "临期导出",
"spec": "openspec/specs/export-time-filter/spec.md",
"entries": [
"/api/admin/expiring-assets",
"/api/admin/expiring-assets/export",
"/api/admin/export-tasks"
],
"handler_consumer_job": [
"internal/handler/admin/asset.go",
"internal/routes/package_expiry.go"
],
"application_service_query": [
"internal/query/packageexpiry/list.go",
"internal/exporter/expiring_asset_scene.go"
],
"domain_state_amount": [
"internal/exporter/ownership_columns.go"
],
"store_migration_config": [
"internal/exporter/registry.go"
],
"verification": {
"command": "rg -n -e 'ExportTaskSceneExpiringAsset' -e 'ListAllWithScope' -e 'expiring-assets/export' -e 'rejectLegacyTimeParams' pkg/constants/constants.go internal/exporter/expiring_asset_scene.go internal/query/packageexpiry/list.go internal/routes/package_expiry.go internal/exporter/registry.go internal/handler/admin/asset.go internal/handler/admin/time_params.go",
"literal_output": [
"internal/handler/admin/time_params.go:9:// rejectLegacyTimeParams 拒绝受影响端点上已废弃的旧时间参数名。",
"internal/handler/admin/time_params.go:12:func rejectLegacyTimeParams(c *fiber.Ctx, keys ...string) error {",
"internal/exporter/registry.go:41:\t\tNewExpiringAssetDataSource(db),",
"internal/exporter/registry.go:80:\t\tconstants.ExportTaskSceneExpiringAsset:",
"internal/handler/admin/asset.go:161:\tif err := rejectLegacyTimeParams(c, \"expires_from\", \"expires_to\"); err != nil {",
"internal/handler/admin/asset.go:186:// POST /api/admin/expiring-assets/export",
"internal/handler/admin/asset.go:204:\t\tScene: constants.ExportTaskSceneExpiringAsset,",
"internal/routes/package_expiry.go:22:\tRegister(router, doc, basePath, \"POST\", \"/expiring-assets/export\", handler.ExportExpiring, RouteSpec{",
"internal/query/packageexpiry/list.go:92:// ListAllWithScope 按调用方给定的数据范围与筛选返回全部临期资产。",
"internal/query/packageexpiry/list.go:94:func (q *Query) ListAllWithScope(ctx context.Context, scope ScopeApplier, filter ListFilter) ([]dto.ExpiringAssetItem, error) {",
"internal/exporter/expiring_asset_scene.go:25:// NewExpiringAssetDataSource 创建临期资产导出数据源。",
"internal/exporter/expiring_asset_scene.go:26:func NewExpiringAssetDataSource(db *gorm.DB) *ExpiringAssetDataSource {",
"internal/exporter/expiring_asset_scene.go:32:\treturn constants.ExportTaskSceneExpiringAsset",
"internal/exporter/expiring_asset_scene.go:47:\t\t\"店铺\", \"业务员\", \"用户组\", \"资产类型\", \"设备类型\", \"设备型号\", \"资产标识\", \"当前套餐\", \"到期时间\", \"剩余天数\",",
"internal/exporter/expiring_asset_scene.go:144:\treturn s.expiryQ.ListAllWithScope(ctx, scope, filter)",
"pkg/constants/constants.go:402:\t// ExportTaskSceneExpiringAsset 表示临期资产导出场景,一行对应一项资产,取当前生效主套餐最终到期时间。",
"pkg/constants/constants.go:403:\tExportTaskSceneExpiringAsset = \"expiring_asset\""
],
"exit_status": 0
}
},
{
"capability": "export-time-filter",
"requirement": "三类异步导出的创建期冻结与不扩大范围",
"spec": "openspec/specs/export-time-filter/spec.md",
"entries": [
"/api/admin/export-tasks",
"/api/admin/expiring-assets/export",
"/api/admin/package-traffic-alerts/export"
],
"handler_consumer_job": [
"internal/handler/admin/export_task.go",
"internal/handler/admin/package_traffic_alert.go"
],
"application_service_query": [
"internal/service/export_task/service.go",
"internal/exporter/time_filters.go"
],
"domain_state_amount": [
"internal/task/export_dispatch.go"
],
"store_migration_config": [
"internal/store/postgres/export_task_store.go"
],
"verification": {
"command": "rg -n -e 'NormalizeTaskTimeFilters' -e 'ValidateTaskTimeFilters' -e 'ExportTaskInvalidTimeFilterMessage' -e '当前账号无可导出的数据范围' -e 'strictTimeRange' internal/service/export_task/service.go internal/exporter/time_filters.go internal/task/export_dispatch.go pkg/constants/constants.go internal/exporter/package_traffic_alert_scene.go",
"literal_output": [
"internal/exporter/package_traffic_alert_scene.go:179:\tstartTime, endTime, err := strictTimeRange(params.Filters)",
"internal/task/export_dispatch.go:123:\tif err := exporter.ValidateTaskTimeFilters(exportTask.Scene, params.Filters); err != nil {",
"internal/task/export_dispatch.go:125:\t\t_ = h.taskStore.MarkFailed(ctx, exportTask.ID, updater, constants.ExportTaskInvalidTimeFilterMessage)",
"internal/exporter/time_filters.go:37:// NormalizeTaskTimeFilters 在创建导出任务时按场景校验并规范化时间边界。",
"internal/exporter/time_filters.go:40:func NormalizeTaskTimeFilters(scene string, query map[string]interface{}) error {",
"internal/exporter/time_filters.go:64:// ValidateTaskTimeFilters 校验导出任务筛选快照中的时间边界,创建期与执行期共用。",
"internal/exporter/time_filters.go:67:func ValidateTaskTimeFilters(scene string, filters map[string]any) error {",
"pkg/constants/constants.go:406:// ExportTaskInvalidTimeFilterMessage 是导出任务冻结的时间边界非法时写入 error_message 的安全失败摘要。",
"pkg/constants/constants.go:408:const ExportTaskInvalidTimeFilterMessage = \"导出筛选的时间边界非法,任务已终止\"",
"internal/service/export_task/service.go:78:\tif err := exporter.NormalizeTaskTimeFilters(req.Scene, req.Query); err != nil {",
"internal/service/export_task/service.go:115:\t\t\treturn nil, errors.New(errors.CodeForbidden, \"当前账号无可导出的数据范围\")"
],
"exit_status": 0
}
}
]