feat(导出时间筛选): AUG26-014 统一时间筛选与临期导出,归档并同步主 Spec 与证据链
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 13m40s
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:
@@ -1,21 +0,0 @@
|
||||
## Decisions
|
||||
|
||||
- 共享导出筛选解析器返回 UTC 边界和冻结筛选快照,查询不在导出 Worker 中重新解释日期。
|
||||
- 导出任务保存授权范围快照而非执行时重新计算;列表/导出复用同一 Query 条件构造。
|
||||
|
||||
## 参数、查询与导出契约
|
||||
|
||||
### 统一时间解析
|
||||
|
||||
- 新增共享解析器,输入可选 `start_time`、`end_time` 字符串,必须以 RFC3339 秒级且带显式时区解析为瞬时 UTC 值;拒绝无时区、毫秒精度、非法日期和 `start_time > end_time`。两端均存在时 Query 使用 `time >= start_time AND time <= end_time`;单端只应用对应边界。
|
||||
- IoT/设备任务、换货、分配、订单、代理充值、佣金、提现统一绑定该参数并按各自创建/申请时间过滤;授权记录按授权发生时间;临期列表按当前有效主套餐最终到期时间。DTO、OpenAPI 和导出筛选名均固定为 `start_time`/`end_time`,不再接受模块私有日期字段作为新契约。
|
||||
|
||||
### 导出任务快照
|
||||
|
||||
- 创建临期、佣金明细、达量预警导出时,先复用页面 Query 构造器解析全部筛选和时间边界,再保存规范化过滤器、操作者 ID、创建时可见店铺/资产范围、时区、创建时间和口径版本。Worker 只读取该快照,不重新从请求、当前角色或当前页面解析筛选。
|
||||
- 临期导出以资产为粒度,选择当前有效主套餐最终到期时间和剩余天数;加油包不单独生成行。佣金明细导出的粒度与列定义由 `add-commission-clawback-records` 确定(佣金记录粒度,原佣金与回溯记录各一行,入账后/回溯后余额可为负);本 Change 只负责统一 `start_time`/`end_time` 参数、闭区间语义与筛选、权限快照冻结,不改变既有导出粒度与列定义。预警导出以预警记录为粒度,套餐/流量/阈值/到期字段读触发快照,店铺/业务员/用户组可按执行时当前归属补全,但必须同时落在创建时冻结范围。
|
||||
- 导出完成记录结果文件、行数、完成时间和失败安全摘要;任何权限变化、筛选条件变化或后台归属变化不得扩大已创建任务的数据集。失败重试继续使用原快照,不创建第二份不同口径文件。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
为任务快照新增成对迁移;验证时区边界、空边界、非法/超长区间、权限变化及 up/down/up。
|
||||
@@ -1,24 +0,0 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-014`。
|
||||
|
||||
## Why
|
||||
|
||||
后台导出和列表可能使用不同时间口径或在异步执行时漂移权限范围。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 统一日期/时间解析、闭区间(`start_time <= t <= end_time`,参数为带时区 RFC3339 秒级时间,任一端可省略)和最大范围校验。
|
||||
- 导出冻结列表筛选、时区和数据范围。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `export-time-filter`: 导出时间筛选标准。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响后台导出任务、查询 DTO、权限快照和 OpenAPI。
|
||||
@@ -1,17 +0,0 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 统一时间筛选参数与字段
|
||||
系统 SHALL 对 IoT/设备任务、换货、分配、订单、代理充值、佣金、提现及其导出统一使用可选 `start_time`、`end_time` 参数。参数必须为带时区的 RFC3339 秒级时间,区间为**闭区间**;任一端可缺省。上述业务按创建时间或申请时间筛选;授权记录按授权发生时间筛选;临期列表按当前生效主套餐最终到期时间筛选。格式非法或开始时间晚于结束时间时拒绝请求。
|
||||
|
||||
#### Scenario: 两端均传入
|
||||
- **WHEN** 请求携带合法的 `start_time` 与 `end_time`
|
||||
- **THEN** 系统仅返回权威时间大于等于开始时间且小于等于结束时间的记录
|
||||
|
||||
### Requirement: 三类异步导出及冻结口径
|
||||
临期列表、佣金明细和套餐流量达量预警 SHALL 复用既有异步导出任务,并在创建时冻结全部页面筛选条件、操作者和可见店铺范围。临期导出一行对应一项资产,仅取当前生效主套餐最终到期时间和剩余天数,加油包不得单独成行。预警导出一行对应一条预警记录,套餐、用量、总量、阈值和到期时间使用触发快照,店铺、业务员和用户组在执行时按当前归属补充。佣金明细导出的记录粒度、列定义与余额口径不属于本需求:本需求只要求其冻结创建时筛选条件、操作者与可见店铺范围,且 MUST NOT 改变既有粒度与列定义。
|
||||
|
||||
异步执行不得重新解释时间、扩大创建时店铺范围或遗漏页面筛选;文件结果只含创建时有权读取的事实。
|
||||
|
||||
#### Scenario: 预警归属在导出前变更
|
||||
- **WHEN** 预警记录创建后资产所属店铺或业务员变更,再执行已创建导出任务
|
||||
- **THEN** 套餐及流量字段仍使用触发快照,店铺、业务员和用户组使用执行时当前归属,且不得超出任务创建时冻结的可见店铺范围
|
||||
@@ -1,12 +0,0 @@
|
||||
## Purpose
|
||||
|
||||
为 2026 年 8 月迭代提供独立、可验证的 统一时间筛选与导出快照 行为契约,避免与既有模块的兼容行为混淆。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 统一时间筛选与导出快照
|
||||
系统 SHALL 对受影响列表使用可单端省略的 `start_time` 和 `end_time` RFC3339 秒级闭区间,并按规定业务时间筛选。异步导出必须冻结创建时筛选条件、操作者和可见店铺范围,且按各业务规定使用触发快照或执行时归属。
|
||||
|
||||
#### Scenario: 规则命中
|
||||
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||
@@ -1,8 +0,0 @@
|
||||
## 1. 统一筛选
|
||||
- [ ] 1.1 清点本期后台导出/列表入口及现有时间字段和权限 Query。
|
||||
- [ ] 1.2 实现上海时区日期解析、闭区间(`start_time <= t <= end_time`,参数为带时区 RFC3339 秒级时间,任一端可省略)、最大范围校验和筛选快照。
|
||||
- [ ] 1.3 改造导出任务以冻结范围并复用列表 Query;更新 DTO/OpenAPI。仅覆盖统一 `start_time`/`end_time` 参数、闭区间语义与筛选、权限与筛选快照冻结;不改变任何导出的记录粒度、列定义与余额口径。
|
||||
|
||||
## 2. 验证
|
||||
- [ ] 2.1 验证日期边界、时间格式、空范围、超限、权限变更和导出/列表一致性,并核对佣金明细导出任务的记录粒度、列定义与余额口径未被本 Change 改变。
|
||||
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-export-time-filter-standards --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,98 @@
|
||||
## Context
|
||||
|
||||
现状约束(全部为可复现事实,证据行号取本次修订时的现读行号):
|
||||
|
||||
- **宽松解析有两个来源,必须区分**。其一,`cmd/api/main.go:204` 在启动时调用 `registerTimeParserCompat()`(`:222`,实现 `parseFlexibleTime` `:242`)用 `fiber.SetParserDecoder` 全局注册 `time.Time` 转换器,接受 RFC3339、`2006-01-02T15:04:05`、`2006-01-02 15:04:05`、`2006-01-02`(后三者按 `time.Local`,容器 `TZ=Asia/Shanghai`,见 `Dockerfile.api:51`、`Dockerfile.worker:46`)。其二,`internal/exporter/filter_helpers.go:238` 的 `filterTime` 与 `:259` 的 `filterEndDate` 在导出执行期独立做多格式解析,**解析失败静默返回 false(等于不加条件)**。
|
||||
- **导出三段式与既有冻结点**:创建期冻结 `query_json`(`req.Query` 原样 `sonic.Marshal`,`internal/service/export_task/service.go:76-83`)、可见店铺范围 `scope_shop_ids` 与操作者四元组(`:99-126`);dispatch 期冻结 `resolved_headers` 与分片计划(`internal/task/export_dispatch.go:122-123,198-213`)。Worker 全程 `GetByIDForWorker`(无数据权限),不读请求上下文。
|
||||
- **唯一合规先例**:达量预警导出在创建期把时间规范化为带时区 RFC3339 字符串(`internal/handler/admin/package_traffic_alert.go:174-178`),执行期按 `triggered_at` 闭区间筛选(`internal/exporter/package_traffic_alert_scene.go:170-175`),并有场景级角色二次门禁(`:322-328`)。
|
||||
- **临期导出不存在**:`pkg/constants/constants.go:385-402` 仅 10 个导出场景常量,`internal/exporter/registry.go:22-34` 仅 10 个数据源,导出路由仅 `internal/routes/export_task.go` 与 `internal/routes/package_traffic_alert.go:76`。
|
||||
- **临期列表不是 SQL 区间筛选**:`internal/query/packageexpiry/list.go` 先用 `expires_at < 今天+16 天` 粗筛候选(`:127-129`、`:155-157`),再在内存按 `EstimatedFinalExpiresAt` 过滤(`:205-208`);推算口径为 `master_usage_id IS NULL` 的主套餐队列(`internal/query/packageexpiry/query.go:52-56,154-166`),不含加油包。既有 `days_min`/`days_max` 与 `expires_from`/`expires_to` 均按上海自然日(`list.go:315-339`)。
|
||||
- **既有严格 RFC3339 先例**:审计列表用 `string` 参数 + `auditTimeRange`/`optionalAuditTime` 严格 `time.RFC3339`(`internal/handler/admin/audit.go:342-368`),但要求 `from < to`,**不采用其顺序语义**。
|
||||
- **错误响应无字段级结构**:`pkg/errors` 仅 `{code,data,msg,timestamp}` 与可选的 `Data`;字段中文名来自 DTO 的 `description` tag(`internal/handler/validation/validation.go:14`)。
|
||||
- **硬门禁**:`scripts/context-health.sh:53-73` 强制新旧能力 Requirement 与 `docs/verification/context-reset/` 两份 JSON 双向一致(路由索引 ↔ 入口矩阵),`:18-20` 禁止仓库内出现任何 `*_test.go`。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 受影响端点的时间参数唯一化(`start_time`/`end_time`)、格式唯一化(带显式时区 RFC3339 秒级)、语义唯一化(UTC 瞬时闭区间),且在发送端即可预测接受与拒绝。
|
||||
- 导出执行期不再解释任何非冻结来源;冻结值非法时以失败收场而不是放行全量。
|
||||
- 临期导出从无到有,与列表同筛选同口径。
|
||||
|
||||
**Non-Goals(设计层边界):**
|
||||
|
||||
- 不改进任何导出的记录粒度、列定义、余额口径(佣金明细负数与入账后余额、达量预警触发快照列组、换货迁移状态列)。
|
||||
- 不重构未列入本次的端点与其宽松解析;不删除全局兼容解析器。
|
||||
- 不引入跨度上限、不引入运行时开关、不引入结构化字段级错误载荷。
|
||||
- 不修改数据库 Schema(见 Decisions 的承载结论)。
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1 共享严格解析器 + 受影响 DTO 的时间字段一律改为字符串
|
||||
|
||||
新增一个共享解析器,输入 `start`/`end` 字符串,输出 `(*time.Time, *time.Time, error)`,语义为「解析为 UTC 瞬时 + 闭区间 + 仅一端/两端缺省 + 开始晚于结束即拒绝」,实现固定为精确 layout `2006-01-02T15:04:05Z07:00` 解析,并在解析前拒绝含 `.` 的输入(Go 的 `time.Parse` 即使 layout 不含小数也会接受小数秒,必须显式拒绝;`±hhmm` 由该 layout 天然拒绝)。落点选 `pkg/utils`(与既有 `pkg/utils/period.go` 同级):创建期的 handler 与执行期的 `internal/exporter` 都要用同一份实现,只有低层公共包能同时被两者依赖。
|
||||
|
||||
受影响端点的 DTO 时间字段 MUST 由 `*time.Time` 或裸字符串统一改为 `string`,由 handler 调用共享解析器。原因:只要字段仍是 `*time.Time`,全局兼容转换器就会生效,严格契约在 DTO 层即失效;且字符串化后才能给出指明字段的中文错误消息(`*time.Time` 只能得到通用「请求参数解析失败」)。
|
||||
|
||||
备选与取舍:备选一是删除 `registerTimeParserCompat` 让全局 `*time.Time` 退回 RFC3339。缺点是爆炸半径覆盖未列入本次的端点(资产钱包流水、客户钱包流水、设备资产列表、手机号关联列表、代理充值订单表单等),违反「不主动重构需求未触碰的旧模块」,且这些端点拿不到字段级提示。备选二是保留 `*time.Time` 并另加校验标签,无法表达「必须带时区且不允许小数秒」。均不采用。全局兼容解析器**保持不动**,并在已知差异中登记。
|
||||
|
||||
### D2 创建期规范化并冻结,执行期只做严格解析
|
||||
|
||||
创建导出任务时按场景校验并规范化时间边界为 UTC RFC3339 秒级字符串,与筛选、操作者、可见店铺范围一并冻结(沿用既有列,见 D5)。执行期复用同一共享解析器解析冻结值;解析失败(含变更前遗留任务的 date-only/无时区冻结值)→ 任务落失败并写安全失败摘要,绝不静默忽略。
|
||||
|
||||
备选:执行期保持 `filterTime` 的宽松多格式以兼容遗留任务。缺点是把「列表与导出口径漂移」这个原始问题固化在执行期,且非法冻结值会静默放行全量,与「不得扩大数据集」直接冲突。不采用。
|
||||
|
||||
### D3 临期导出新建:独立数据源 + 受控端点,复用列表查询与列定义
|
||||
|
||||
新增唯一导出场景与受控端点 `POST /api/admin/expiring-assets/export`,请求体复用列表的筛选集合(时间区间、`days_min`/`days_max`、套餐、资产类型、关键字、店铺)。数据源 MUST 复用 `internal/query/packageexpiry` 的候选预筛与最终到期推算,不得另写一套到期口径;列按 `111.md` §18.1,粒度一行一资产(加油包不单独成行)。店铺/业务员/用户组按执行时当前归属补充,与达量预警同口径(依据既有 Spec),但订阅范围仍受创建时冻结的可见店铺范围约束。
|
||||
|
||||
备选:走通用 `POST /api/admin/export-tasks` 传自由 `query` map。缺点是把「复用页面全部筛选」降级为前端约定,正是本 Change 要消除的问题。不采用。
|
||||
|
||||
实现取舍:为与达量预警导出保持同一店铺/业务员/用户组口径,从达量预警场景中抽出了共享的归属列查询(店铺当前业务员 + 业务员业务用户组实时推导,`internal/exporter/ownership_columns.go`),两场景共用同一实现,行为等价(达量预警的列与口径逐字不变)。
|
||||
|
||||
### D4 佣金明细导出补筛选,不改口径
|
||||
|
||||
`commission_record` 场景新增 `start_time`/`end_time`,按两条分支各自的创建时间列闭区间筛选。该场景的存储层已预留同列的时间分支(`internal/store/postgres/commission_record_store.go:176-181`)但当前无人赋值,接线即可。记录粒度(原佣金与回溯各一行)、列定义、负数与入账后余额口径 MUST NOT 改变。
|
||||
|
||||
### D5 无 Schema 变更、无迁移
|
||||
|
||||
| 需求要素 | 承载 |
|
||||
| --- | --- |
|
||||
| 筛选条件 + 时间边界 | `tb_export_task.query_json`(jsonb,`migrations/000135_create_export_task_tables.up.sql:26`),写入规范化后的 UTC RFC3339 秒级字符串 |
|
||||
| 操作者 | `creator_user_id`/`creator_user_type`/`creator_shop_id`/`creator_enterprise_id` 与 `creator`/`updater`(`:9-10,29-32`) |
|
||||
| 可见店铺范围 | `scope_shop_ids`(jsonb,`:27`) |
|
||||
| 结果文件 / 行数 / 失败安全摘要 | `file_key`/`file_size`/`total_rows`/`error_message`(`:34-36,18`),失败摘要沿用 `error_message` 通道(`internal/service/export_task/service.go:360-374`) |
|
||||
| 表头冻结 | `query_json.resolved_headers`(`internal/store/postgres/export_task_store.go:148-172`) |
|
||||
|
||||
因此 **无成对迁移、无 up/down/up**。取消原设计中「时区」与「口径版本」快照字段:前者无消费者(比较用 UTC 瞬时,仅临期剩余天数沿用既有上海固定偏移常量),后者既无 Requirement 也无读取方。
|
||||
|
||||
### D6 未列入端点与其导出同改或同不改
|
||||
|
||||
判定规则:一个页面与其导出场景的时间筛选键必须一致,要么同时纳入本次改造,要么同时不纳入。据此,换货与代理充值纳入时其导出场景键同步改名;设备资产列表、代理主钱包流水、套餐、退款、IoT 卡资产等未纳入者与其导出场景一律保持原参数与宽松解析,作为已知差异登记。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [前端未同迭代改造 → 受影响端点立即 400] → 契约以「无兼容期、无开关」为前提,交付物必须包含 DTO 与 OpenAPI 变更清单及前端同步清单,并在上线前置中写明前端同步;后端不做双读。
|
||||
- [临期列表的 15 天候选窗口被误读为新参数的上限] → 明确该粗筛是既有语义(`internal/query/packageexpiry/list.go:127-129`),新参数不得附加跨度上限;验收以「同筛选下列表与导出行集一致」覆盖。
|
||||
- [date-only 改瞬时刻后,前端若继续按自然日传值会改变结果边界] → 契约要求显式偏移;验收覆盖「授权记录闭区间含两端」与「临期列表按最终到期时刻筛选」,并要求前端以 00:00:00/23:59:59 的带偏移写法表达整日区间。
|
||||
- [遗留 pending 任务冻结值为旧格式 → 任务失败] → 这是刻意的:失败可重试重读,静默放行会扩大数据集;验收要求构造遗留任务验证「落失败且不产出全量文件」。
|
||||
- [全局兼容解析器仍在,未列入端点继续接受宽松格式 → 同参数名在不同端点行为不同] → 以 D6 的成对规则限定爆炸半径,并在设计文档「已知差异」中显式登记,避免被误当作遗漏。
|
||||
- [列定义与表头在 dispatch 期冻结,历史任务沿用旧表头] → 临期导出为新建,无历史任务;佣金明细与达量预警的列定义本次不得改动,验收以表头输出逐字不变作回归。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
- 无数据库迁移、无数据回填、无运行时开关;冻结字符串写入既有 jsonb 列。
|
||||
- 发布顺序:前端与后端同迭代上线;后端上线顺序无特殊要求(受影响端点仅参数契约变化,不涉及数据形态)。
|
||||
- 回滚策略:回滚二进制即恢复旧参数接受范围;已创建任务的冻结值仍为合法 RFC3339 字符串,新旧版本均可解析,不需要数据修复。
|
||||
- 上线后门禁:`gofmt`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`(连续两次结果一致)、`openspec validate --all --strict`、`openspec doctor --json`、`./scripts/context-health.sh`。
|
||||
|
||||
## 已知差异登记(本次不修)
|
||||
|
||||
1. 未列入本次的端点(资产钱包流水、客户钱包流水、设备资产列表及其导出、代理主钱包流水及其导出、手机号—资产关联列表、审计类列表、员工代收款账单、轮询告警历史、企业卡与企业设备授权列表、IoT 卡资产列表、套餐导出、退款导出)仍接受宽松时间格式;全局兼容解析器(`cmd/api/main.go:222`)继续为其服务。
|
||||
2. 佣金统计类接口(`GET /api/admin/shops/{shop_id}/commission-stats`、`GET /api/admin/shops/{shop_id}/commission-daily-stats`)把裸字符串直接传入数据库做时间比较,格式非法时依赖数据库隐式转换(可能报错)。属独立问题,本次不修。
|
||||
3. `tb_export_task` 的创建与完成时间列为裸 `timestamp`(`migrations/000135_create_export_task_tables.up.sql:5-7,38-39`),与业务表的带时区时间列不同;本次只读筛选与写入冻结字符串,不受影响。
|
||||
4. 死代码登记(本次不删):`dto.CommissionRecordListRequest`(`internal/model/dto/commission.go:25`,无调用方)、`dto.RechargeListRequest`(`internal/model/dto/recharge.go:60-62`,无调用方)、`internal/exporter/scope.go`(单行空文件)。
|
||||
5. 达量预警列表与导出(`GET /api/admin/package-traffic-alerts`、`POST /api/admin/package-traffic-alerts/export`)本期只收紧执行期、不改入口契约:`dto.ListPackageTrafficAlertRequest` / `dto.ExportPackageTrafficAlertRequest` 的时间字段仍为 `*time.Time`,列表与导出入口仍由 `cmd/api/main.go` 的全局兼容解析器接受 date-only、空格分隔与无时区等宽松格式,创建期由 `handler/admin/package_traffic_alert.go` 的 `exportFilters` 归一为 UTC RFC3339 秒级串后冻结;**执行期改为只按冻结值严格解析**(`internal/exporter/time_filters.go` 已将本场景纳入 `timeFilterScenes`,`internal/exporter/package_traffic_alert_scene.go` 改用共享 `strictTimeRange`),冻结值非法时任务落失败而不静默放行全量。列定义、记录粒度、触发快照口径与前端契约不变。
|
||||
6. 无时区列的时间锚定(本次不改):`tb_export_task`、`tb_commission_record`、`tb_commission_clawback_record`、`tb_commission_withdrawal_request`、`tb_agent_recharge_record`、`tb_enterprise_card_authorization`、`tb_iot_card_import_task`、`tb_device_import_task` 的时间列为 PostgreSQL `timestamp without time zone`,其中保存的是应用本地时区(部署容器 `TZ=Asia/Shanghai`)的挂钟时间。本次统一按「归一后的 UTC 瞬时」绑定时间参数;列表与导出两条路径落到同一列上的有效边界一致(写库为同一 UTC 挂钟值,驱动与显式文本转换均丢弃时区指示),不存在列表/导出口径差异。「用户本地整日区间」与「UTC 挂钟边界」之间的差值属既有事实,本次不改,实测证据见 [`docs/verification/add-export-time-filter-standards-verification.md`](../../../../docs/verification/add-export-time-filter-standards-verification.md) 第 7 节。
|
||||
7. 临期导出的 `Count`/`Fetch` 复用列表全量推算(本次取舍):最终到期时间不是 SQL 列,而是候选预筛后在内存按主套餐队列推算的结果,因此分片执行时每个分片都重新执行「候选预筛 + 批量推算」而不下推 `LIMIT`/`COUNT` 到 SQL。受既有 16 天粗筛窗口约束,结果集规模有限;为满足「不得另写第二套到期口径」与「列表与导出同筛选同口径」,刻意复用同一实现,不做第二套 SQL 下推。
|
||||
8. 代理/企业账号对 `/api/admin` 后半段路由不可达(**既有缺陷,早于本 Change 存在,已由维护者修复,本次不改动**):现象——代理与企业账号访问 `GET /api/admin/expiring-assets` 与本次新增的 `POST /api/admin/expiring-assets/export` 得到 403 / code 1005「无权限操作该资源或资源不存在」,超级管理员与平台账号正常。真实根因——`internal/routes/package_traffic_alert.go` 与 `internal/routes/asset_auto_renewal.go` 在**管理端根组**上用 `router.Group("", gate)` 注册「仅超管/平台」的组级门禁;Fiber 的 `Group(prefix, handlers...)` 会把该处理器落为 `/api/admin` 前缀的 USE 处理器并按注册顺序前置执行,因此**在其之后注册的路由**(`orders`、`exchanges`、`/assets/*`、`/expiring-assets`(含本次新增的导出)、`agent-recharges` 等)以及**不匹配任何路由的 `/api/admin/*` 路径**(404 被覆盖为 1005)都会被拦截。现状与边界——该缺陷早于本 Change 存在,且**已由维护者提交 `398a5e4`(`fix(路由): 修正套餐真流量预警与资产自动续费的超管/平台 gate 作用域`)修复**(把 gate 改为挂在功能前缀上,例如 `/package-traffic-alert-rules`、`/asset-auto-renewal-config`);该修复**不属于本 Change 的改动**(`git diff HEAD` 中不含这两个路由文件)。本 Change 不再对它做任何改动,只在此登记事实与本 Change 的验证影响:代理视角的 HTTP 创建/拒绝路径在 HEAD `398a5e4` 的门禁修复后已可复现验证(见 [`docs/verification/add-export-time-filter-standards-verification.md`](../../../../docs/verification/add-export-time-filter-standards-verification.md) 第 6 节对应小节)。
|
||||
@@ -0,0 +1,46 @@
|
||||
## Why
|
||||
|
||||
迭代编号:`AUG26-014`。后台列表与导出当前各自使用不同的时间参数名与时间格式(`created_at_start`/`created_at_end`、`start_date`/`end_date`、date-only、无时区串、裸字符串直传),异步导出还会在执行期重新解释筛选并沿用宽松解析,导致同一筛选在列表与导出之间口径漂移、非法参数被静默忽略后放行全量数据,且执行期可能扩大已创建任务的数据集。
|
||||
|
||||
需求依据:`docs/product/2026-08-迭代-PRD-讨论稿.md` §2.16(时间筛选与导出快照两段)为权威口径;`111.md` §18 提供三类导出的列定义,§24 的「无法支持/不支持」清单与 PRD 相反,以 PRD 为准。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING** 统一时间筛选参数:PRD §2.16 点名的列表与导出固定使用 `start_time`/`end_time`,取值 MUST 为带显式时区的 RFC3339 秒级时间,闭区间(含两端),任一端可缺省;解析结果归一为 UTC 瞬时比较。
|
||||
- **BREAKING** 替换旧参数与旧格式:`created_at_start`/`created_at_end`、`start_date`/`end_date`、date-only(`2006-01-02`)、无时区串(`2006-01-02 15:04:05`)在受影响端点被拒绝;不保留静默兼容、不新增运行时开关;前端必须同迭代上线。
|
||||
- **BREAKING** 受影响端点的时间参数类型一律由 `*time.Time` 或裸字符串改为字符串,不得残留经全局宽松解析的类型,否则严格契约失效。
|
||||
- 修正授权记录筛选语义:由「起始闭、结束开且结束日加一天」改为闭区间含两端。
|
||||
- 修正提现记录筛选语义:非法时间参数一律拒绝,不再静默忽略筛选条件后返回全量。
|
||||
- 临期导出新建:既有代码中不存在该导出场景,本次新建数据源、场景注册与受控端点;列定义取 `111.md` §18.1,粒度为一行一项资产。
|
||||
- 佣金明细导出新增按创建时间的闭区间筛选,创建期校验并冻结;不改其记录粒度、列定义与余额口径。
|
||||
- 导出创建期统一规范化并冻结筛选与时间边界(UTC RFC3339 秒级字符串);执行期不再做多格式宽松解析,只按冻结值严格解析,解析失败必须落失败任务并写安全摘要。
|
||||
- 不实现跨度上限。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `export-time-filter`: 统一时间筛选参数与解析契约、受影响端点与旧格式替换清单,以及临期、佣金明细与达量预警三类异步导出的创建期冻结与「不得扩大数据集」不变式;临期导出为本次新建场景。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无(未修改任何既有能力的行为契约)。
|
||||
|
||||
delta 的操作头口径(`## MODIFIED Requirements`):
|
||||
|
||||
- `export-time-filter` 是**本次新增的能力**,但其主 Spec(`openspec/specs/export-time-filter/spec.md`)由本变更落盘:`scripts/context-health.sh` 要求主 Spec 与 `docs/verification/context-reset/` 两份证据 JSON 双向一致,主 Spec 必须在本次交付。
|
||||
- 因此 delta 采用 `## MODIFIED Requirements`:主 Spec 已存在同名 Requirement 时,`## ADDED` 会让未来的 `openspec archive` 报 `already exists` 并中止(`Aborted. No files were changed.`,退出码 1),而 `## MODIFIED` 可正常归档;`MODIFIED` 块必须携带主 Spec 中同名 Requirement 的全部 Scenario,本变更的主 Spec 正由该 delta 生成,两边 Scenario 集逐字一致。
|
||||
- 主 Spec 与 delta 的 Requirement 名称与正文逐字一致,归档时不会出现「current spec contains scenario(s) not present in the modified block」。
|
||||
|
||||
清点依据(复核既有能力是否需要 MODIFIED):
|
||||
|
||||
- `openspec/specs/` 全量检索 `start_time`、`end_time`、`RFC3339`、`筛选`:无任何 Requirement 固定时间参数名或时间格式;唯一命中是 `personal-customer` 的响应字段描述(返回体中的开始时间),不是筛选参数契约。
|
||||
- `openspec/specs/export-task/spec.md` 只描述导出任务终态(待处理/处理中/已完成/已失败/已取消),不含筛选、时间快照或范围冻结语义。
|
||||
- `openspec/specs/package-traffic-alert/spec.md` 的「预警查询与导出」已要求导出复用异步任务、创建时冻结操作者/筛选/时间范围/可见资产范围,并按触发时间筛选,与本 Change 的新契约一致而非冲突;新能力的 Requirement 因此不得复述该条的粒度与触发快照口径,只指向既有 Spec。
|
||||
|
||||
## Impact
|
||||
|
||||
- 后台列表端点与异步导出场景;请求 DTO 与 OpenAPI 参数类型(`*time.Time`/裸字符串 → 字符串)。
|
||||
- 前端必须同迭代改造受影响端点的参数名与传值格式,无兼容期。
|
||||
- 新增能力 `export-time-filter` 的主 Spec 与证据链同步落盘(`openspec/specs/export-time-filter/spec.md`、`docs/verification/context-reset/` 两份 JSON)。
|
||||
- 无 Schema 变更、无迁移、无运行时开关;不触碰未列入本次的端点与导出场景。
|
||||
@@ -0,0 +1,182 @@
|
||||
## Purpose
|
||||
|
||||
为 2026 年 8 月迭代提供统一的时间筛选参数与解析契约,规定受影响端点与旧格式的替换关系,并规定临期、佣金明细与达量预警三类异步导出在创建时冻结筛选与数据范围、执行期不得重新解释或扩大数据范围的行为。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 统一时间筛选参数与解析契约
|
||||
|
||||
系统 SHALL 在被本需求覆盖的列表与导出入口使用可选的 `start_time` 与 `end_time` 参数,取值 MUST 为带显式时区的 RFC3339 秒级时间,解析结果 MUST 归一为 UTC 瞬时;筛选区间 MUST 为闭区间(含两端)。参数未传与传入空串 MUST 产生相同语义。
|
||||
|
||||
接受集合 MUST 仅包含带 `Z` 或 `±hh:mm` 偏移的秒级时刻,例如 `2026-09-01T00:00:00Z` 与 `2026-09-01T08:00:00+08:00`。系统 MUST 拒绝小数秒(含 `.000` 形式)、无时区时间(形如 `2026-09-01T00:00:00`)、date-only(`2026-09-01`)、空格分隔时间(`2026-09-01 00:00:00`)、`±hhmm` 偏移、未补零的日期或时间、非法日历日期(如 `2026-02-30T00:00:00+08:00`)以及任何其他非 RFC3339 输入。系统 MUST NOT 实施跨度上限。
|
||||
|
||||
系统 SHALL 在开始时间晚于结束时间时拒绝请求。格式非法与顺序错误 MUST 使用既有参数非法错误码并返回固定中文消息,消息 MUST 指明字段;MUST NOT 引入结构化字段级错误载荷。系统 MUST NOT 保留旧格式的静默兼容,MUST NOT 提供运行时开关。
|
||||
|
||||
#### Scenario: 两端均传入
|
||||
|
||||
- **WHEN** 请求携带合法的 `start_time` 与 `end_time`
|
||||
- **THEN** 系统仅返回业务时间大于等于开始时间且小于等于结束时间的记录
|
||||
|
||||
#### Scenario: 仅传一端
|
||||
|
||||
- **WHEN** 请求只携带 `start_time`,或只携带 `end_time`
|
||||
- **THEN** 系统只应用该端边界,另一端不限
|
||||
|
||||
#### Scenario: 两端缺省或为空串
|
||||
|
||||
- **WHEN** 请求未携带任何时间参数,或两个参数均为空串
|
||||
- **THEN** 系统不附加时间条件,保持既有列表范围
|
||||
|
||||
#### Scenario: 开始等于结束
|
||||
|
||||
- **WHEN** `start_time` 与 `end_time` 表示同一时刻,且存在业务时间恰好等于该时刻的记录
|
||||
- **THEN** 系统返回这些记录
|
||||
|
||||
#### Scenario: 接受带显式时区的秒级时间
|
||||
|
||||
- **WHEN** 请求携带带 `Z` 或 `±hh:mm` 偏移的秒级时间
|
||||
- **THEN** 系统按该偏移解析为同一瞬时并执行闭区间筛选,结果与同一时刻的另一种合法偏移写法一致
|
||||
|
||||
#### Scenario: 拒绝旧格式与非法格式
|
||||
|
||||
- **WHEN** 请求携带小数秒、无时区时间、date-only、空格分隔时间、`±hhmm` 偏移或非法日期
|
||||
- **THEN** 系统以参数非法错误码拒绝请求,且不返回任何记录
|
||||
|
||||
#### Scenario: 开始晚于结束
|
||||
|
||||
- **WHEN** `start_time` 晚于 `end_time`
|
||||
- **THEN** 系统以参数非法错误码拒绝请求,且不返回任何记录
|
||||
|
||||
### Requirement: 受影响端点与旧格式替换
|
||||
|
||||
系统 SHALL 按下表对受影响端点使用业务时间字段筛选,并按「目标形态」列改造。下表中标注「必改」的端点 MUST 在本次变更后使用统一参数与解析契约;标注「已符合」的端点 MUST 保持既有行为。
|
||||
|
||||
| 端点 | 改造前参数与格式 | 筛选依据业务时间 | 目标形态 | 归类 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET /api/admin/iot-cards/import-tasks` | `start_time`/`end_time`(宽松:兼容 RFC3339、无时区、date-only) | 导入任务创建时间 | 参数名不变,解析收紧 | 必改 |
|
||||
| `GET /api/admin/devices/import/tasks` | `start_time`/`end_time`(宽松) | 导入任务创建时间 | 参数名不变,解析收紧 | 必改 |
|
||||
| `GET /api/admin/export-tasks` | `start_time`/`end_time`(宽松) | 导出任务创建时间 | 参数名不变,解析收紧 | 必改 |
|
||||
| `GET /api/admin/orders` | `start_time`/`end_time`(宽松) | 订单创建时间 | 参数名不变,解析收紧 | 必改 |
|
||||
| `GET /api/admin/exchanges` | `created_at_start`/`created_at_end`(宽松) | 换货单创建时间 | 改名为 `start_time`/`end_time` 并收紧 | 必改 |
|
||||
| `GET /api/admin/asset-allocation-records` | `created_at_start`/`created_at_end`(宽松) | 分配记录创建时间 | 改名为 `start_time`/`end_time` 并收紧 | 必改 |
|
||||
| `GET /api/admin/agent-recharges` | `start_date`/`end_date`(无格式约束字符串,按日补齐当日 00:00:00 与 23:59:59) | 代理充值记录创建时间 | 改名为 `start_time`/`end_time` 并改用严格解析 | 必改 |
|
||||
| `GET /api/admin/shops/{shop_id}/commission-records` | 无时间参数 | 佣金明细创建时间 | 新增 `start_time`/`end_time` | 必改 |
|
||||
| `GET /api/admin/commission/withdrawal-requests` | `start_time`/`end_time`(无时区 `2006-01-02 15:04:05`,解析失败静默忽略该条件) | 提现申请创建时间 | 解析严格化,非法格式一律拒绝 | 必改 |
|
||||
| `GET /api/admin/shops/{shop_id}/withdrawal-requests` | `start_time`/`end_time`(同上,解析失败静默忽略) | 提现申请创建时间 | 解析严格化,非法格式一律拒绝 | 必改 |
|
||||
| `GET /api/admin/authorizations` | `start_time`/`end_time`(date-only `2006-01-02`) | 授权发生时间 | 解析严格化;区间由「起始闭、结束开且结束日加一天」改为闭区间含两端 | 必改 |
|
||||
| `GET /api/admin/expiring-assets` | `expires_from`/`expires_to`(date-only,按上海自然日比较) | 当前生效主套餐最终到期时刻 | 改名为 `start_time`/`end_time`,按时刻闭区间比较;保留既有的剩余天数上下限筛选 | 必改 |
|
||||
| 换货导出(导出场景 `exchange`) | `created_at_start`/`created_at_end`(宽松) | 换货单创建时间 | 与换货列表同步改名并收紧关键字的筛选键 | 必改 |
|
||||
| 代理充值导出(导出场景 `agent_recharge`) | `start_date`/`end_date`(宽松) | 代理充值记录创建时间 | 与代理充值列表同步改名并收紧筛选键 | 必改 |
|
||||
| 订单导出(导出场景 `order`) | `start_time`/`end_time`(宽松) | 订单创建时间 | 参数名不变,解析收紧 | 必改 |
|
||||
| 佣金明细导出(导出场景 `commission_record`) | 无时间筛选 | 佣金明细创建时间 | 新增 `start_time`/`end_time` 筛选 | 必改 |
|
||||
| 临期导出 | 场景不存在 | 当前生效主套餐最终到期时刻 | 本次新建,见「临期导出」需求 | 必改 |
|
||||
| `GET /api/admin/package-traffic-alerts` | `start_time`/`end_time`(RFC3339 秒级) | 预警触发时间 | 保持 | 已符合 |
|
||||
| `POST /api/admin/package-traffic-alerts/export` | `start_time`/`end_time`(RFC3339 秒级,创建时冻结) | 预警触发时间 | 保持 | 已符合 |
|
||||
|
||||
被替换的旧参数与旧格式 MUST 在受影响端点被拒绝:`created_at_start`/`created_at_end`、`start_date`/`end_date`、date-only、无时区串、空格分隔时间、小数秒与 `±hhmm` 偏移。未列入上表的端点(资产钱包流水、客户钱包流水、设备资产列表及其导出、代理主钱包流水及其导出、手机号—资产关联列表、佣金统计类接口、审计类列表、员工代收款账单、轮询告警历史、企业卡与企业设备授权列表、IoT 卡资产列表、套餐导出、退款导出)本期 MUST NOT 新增时间参数、MUST NOT 改变参数名与格式。
|
||||
|
||||
#### Scenario: 换货列表按新参数筛选
|
||||
|
||||
- **WHEN** 请求以 `start_time` 与 `end_time` 查询换货列表
|
||||
- **THEN** 系统按换货单创建时间闭区间筛选;携带 `created_at_start` 或 `created_at_end` 时该条件 MUST NOT 生效
|
||||
|
||||
#### Scenario: 代理充值列表按新参数筛选
|
||||
|
||||
- **WHEN** 请求以带时区的 `start_time` 与 `end_time` 查询代理充值记录
|
||||
- **THEN** 系统按充值记录创建时间闭区间筛选,结果不再依赖按日补齐的当日首末秒
|
||||
|
||||
#### Scenario: 授权记录闭区间含两端
|
||||
|
||||
- **WHEN** 授权记录的授权发生时间恰好等于 `start_time` 或恰好等于 `end_time`
|
||||
- **THEN** 系统返回该记录
|
||||
|
||||
#### Scenario: 提现记录非法参数不再返回全量
|
||||
|
||||
- **WHEN** 提现记录列表携带非法时间参数
|
||||
- **THEN** 系统以参数非法错误码拒绝请求,MUST NOT 忽略该参数后返回全量记录
|
||||
|
||||
#### Scenario: 临期列表按最终到期时刻筛选
|
||||
|
||||
- **WHEN** 请求以 `start_time` 与 `end_time` 查询临期资产列表
|
||||
- **THEN** 系统按当前生效主套餐最终到期时刻执行闭区间筛选,并继续应用既有的剩余天数上下限筛选
|
||||
|
||||
#### Scenario: 未列入端点保持既有行为
|
||||
|
||||
- **WHEN** 调用未列入上表的端点并携带其既有时间参数(含宽松格式)
|
||||
- **THEN** 系统保持该端点既有参数名、既有格式接受范围与既有筛选结果
|
||||
|
||||
### Requirement: 临期导出
|
||||
|
||||
系统 SHALL 为临期列表提供异步导出,并在创建时冻结该页面的全部筛选条件、操作者与可见店铺范围。本次为该导出**新建**数据源与受控入口,既有代码中不存在该导出场景,因此 MUST NOT 复用不存在的场景。
|
||||
|
||||
临期导出的粒度 MUST 为一行对应一项资产,取该资产当前生效主套餐的最终到期时间与剩余天数;加油包 MUST NOT 单独成行。导出的筛选集合 MUST 与临期列表一致(时间区间、剩余天数上下限、套餐、资产类型、关键字、店铺),并 MUST 复用与列表相同的最终到期推算口径。导出列 MUST 为:店铺、业务员、用户组、资产类型、设备类型、设备型号、资产标识、当前套餐、到期时间、剩余天数(依据 `111.md` §18.1)。
|
||||
|
||||
临期导出 MAY 对店铺、业务员与用户组按导出执行时的当前归属补充,但结果 MUST NOT 超出任务创建时冻结的可见店铺范围;该归属补充口径与达量预警导出一致(依据 `openspec/specs/package-traffic-alert/spec.md`)。
|
||||
|
||||
#### Scenario: 导出与列表同筛选同口径
|
||||
|
||||
- **WHEN** 以同一筛选条件分别调用临期列表与创建临期导出
|
||||
- **THEN** 导出的行集合与列表结果一致,且每行的到期时间与剩余天数取列表同一最终到期推算口径
|
||||
|
||||
#### Scenario: 加油包不单独成行
|
||||
|
||||
- **WHEN** 某资产的当前生效主套餐关联加油包
|
||||
- **THEN** 该资产在导出中只出现一行,取主套餐最终到期时间与剩余天数,加油包不产生额外行
|
||||
|
||||
#### Scenario: 列定义与 111 §18.1 一致
|
||||
|
||||
- **WHEN** 查看临期导出文件表头
|
||||
- **THEN** 表头为店铺、业务员、用户组、资产类型、设备类型、设备型号、资产标识、当前套餐、到期时间、剩余天数
|
||||
|
||||
#### Scenario: 导出前归属变更
|
||||
|
||||
- **WHEN** 临期导出任务创建后,资产所属店铺或业务员发生变更,再执行该任务
|
||||
- **THEN** 店铺、业务员与用户组按执行时当前归属补充,且结果不超出创建时冻结的可见店铺范围
|
||||
|
||||
### Requirement: 三类异步导出的创建期冻结与不扩大范围
|
||||
|
||||
临期、佣金明细与达量预警三类导出 SHALL 在创建时冻结全部筛选条件、时间边界、操作者与可见店铺范围;三类导出的时间边界 MUST 被规范化为 UTC RFC3339 秒级字符串后冻结。执行期 MUST NOT 做多格式宽松解析,只按冻结值严格解析,MUST NOT 重新读取请求、当前角色或当前页面筛选。创建期 MUST 校验时间边界,非法值 MUST 在创建时被拒绝。
|
||||
|
||||
冻结值解析失败时(含变更前创建的遗留任务),任务 MUST 落为失败并写入安全失败摘要,MUST NOT 静默忽略该条件后放行全量数据。失败重试 MUST 沿用原快照,MUST NOT 产生第二份不同口径的文件。导出完成时 MUST 记录结果文件、行数与安全失败摘要。
|
||||
|
||||
数据范围 SHALL 沿用既有创建期冻结与导出侧范围过滤:代理空可见范围 MUST 在创建时被拒绝,执行期空范围 MUST 返回空结果;MUST NOT 使用请求上下文版过滤。权限、筛选条件或后台归属的任何变化 MUST NOT 扩大已创建任务的数据集。
|
||||
|
||||
佣金明细导出的记录粒度、列定义与余额口径不属于本需求:本需求只要求其新增按创建时间的闭区间筛选并冻结创建时筛选条件、操作者与可见店铺范围;原佣金与回溯记录各占一行、负数金额与入账后余额不可裁剪的既有行为 MUST NOT 被改变。达量预警导出的粒度与触发快照口径同样不属于本需求,见既有 Spec。
|
||||
|
||||
#### Scenario: 导出完成记录产物与摘要
|
||||
|
||||
- **WHEN** 导出任务成功完成
|
||||
- **THEN** 任务记录结果文件、行数与完成时间;失败时记录安全失败摘要,不泄露内部细节
|
||||
|
||||
#### Scenario: 遗留任务冻结值非法
|
||||
|
||||
- **WHEN** 执行一个冻结了旧格式时间值的遗留导出任务
|
||||
- **THEN** 任务落为失败并写入安全失败摘要,MUST NOT 按无时间条件执行并产出全量文件
|
||||
|
||||
#### Scenario: 失败重试沿用原快照
|
||||
|
||||
- **WHEN** 导出任务首次执行失败后重试
|
||||
- **THEN** 重试使用同一份冻结筛选与时间边界,不产生第二份口径不同的文件
|
||||
|
||||
#### Scenario: 执行期权限变化不扩大范围
|
||||
|
||||
- **WHEN** 任务创建后创建者的数据范围、角色或筛选条件发生变化,再执行该任务
|
||||
- **THEN** 导出结果仍不超过创建时冻结的可见范围与筛选条件
|
||||
|
||||
#### Scenario: 代理空可见范围
|
||||
|
||||
- **WHEN** 代理账号没有任何可见店铺范围并创建导出任务
|
||||
- **THEN** 系统在创建时以无权限拒绝,不创建任务
|
||||
|
||||
#### Scenario: 佣金明细导出含回溯记录
|
||||
|
||||
- **WHEN** 导出包含回溯记录的佣金明细
|
||||
- **THEN** 原佣金与回溯记录各占一行,回溯行金额为负数、入账后余额原样导出,列定义保持既有不变
|
||||
|
||||
## 可达操作索引
|
||||
|
||||
本节只用于入口导航,不是行为 Requirement;业务义务以上述 Requirements 为准。
|
||||
|
||||
### 临期导出
|
||||
|
||||
`POST /api/admin/expiring-assets/export`(创建临期资产导出任务)。
|
||||
@@ -0,0 +1,69 @@
|
||||
## 1. T1 共享严格解析器
|
||||
|
||||
- [x] 1.1 在 `pkg/utils` 新增共享时间区间解析器:输入 `start`/`end` 字符串,输出归一为 UTC 瞬时的起止边界与错误;实现固定为精确 layout `2006-01-02T15:04:05Z07:00`,并在解析前拒绝含 `.` 的输入(Go 的 `time.Parse` 会接受小数秒,必须显式拒绝)。
|
||||
- [x] 1.2 写死接受集合:`Z` 与 `±hh:mm` 偏移的秒级时间。写死拒绝集合:小数秒(含 `.000`)、无时区 `2026-09-01T00:00:00`、date-only `2026-09-01`、空格分隔 `2026-09-01 00:00:00`、`±hhmm` 偏移、未补零日期或时间、非法日历日期、其他非 RFC3339 输入。
|
||||
- [x] 1.3 写死区间与缺省语义:闭区间含两端;开始等于结束合法;仅传一端只应用该端;两端缺省或均为空串不加时间条件;开始晚于结束一律拒绝。
|
||||
- [x] 1.4 写死错误契约:沿用既有参数非法错误码与固定中文消息(消息指明字段),不新增结构化字段级错误载荷;不实现跨度上限。
|
||||
- [x] 1.5 将受影响端点的 DTO 时间字段由 `*time.Time` 或裸字符串统一改为 `string`,并在 handler 调用共享解析器;确认受影响端点不再残留经全局宽松解析的类型。
|
||||
|
||||
## 2. T2 列表端点改造
|
||||
|
||||
- [x] 2.1 解析收紧(参数名不变):IoT 卡导入任务列表、设备导入任务列表、导出任务列表、订单列表。
|
||||
- [x] 2.2 参数改名并收紧:换货列表、分配记录列表(`created_at_start`/`created_at_end` → `start_time`/`end_time`)。
|
||||
- [x] 2.3 参数改名并改类型:代理充值列表(`start_date`/`end_date` 无约束字符串 → `start_time`/`end_time` 严格解析),去掉按日补齐当日首末秒的字符串拼接。
|
||||
- [x] 2.4 新增筛选:佣金明细列表新增 `start_time`/`end_time`,按佣金明细创建时间闭区间筛选。
|
||||
- [x] 2.5 类型与格式改造:提现记录两处列表(审批列表与店铺提现记录)改为严格解析,**删除解析失败静默跳过**,非法参数一律返回参数非法错误码。
|
||||
- [x] 2.6 类型与格式改造:授权记录列表改为严格解析,并将区间由「起始闭、结束开且结束日加一天」改为闭区间含两端。
|
||||
- [x] 2.7 临期列表:`expires_from`/`expires_to` 改名 `start_time`/`end_time`,改按当前生效主套餐最终到期**时刻**闭区间比较;保留既有剩余天数上下限筛选与既有最终到期推算口径。
|
||||
- [x] 2.8 未列入本次的端点保持原参数名与宽松解析(不新增参数、不改格式),按 design 已知差异登记核对一遍,避免顺手改动。
|
||||
- [x] 2.9 导出侧筛选键与解析同步(同一页面与其导出必须同口径):换货导出与代理充值导出随列表同步改名为 `start_time`/`end_time`;订单导出沿用同名但收紧解析;受影响导出场景一律改用共享严格解析器,废弃多格式宽松解析与「按日补齐当日末秒」。
|
||||
- [x] 2.10 共享文件唯一负责人:可执行路由注册、导出场景常量、导出场景注册表与支持场景判定、`docs/verification/context-reset/` 两份证据 JSON 由本任务负责落盘;T3 需改动这些文件时先经 `hub` 与本任务协调,禁止并行直接改写。
|
||||
|
||||
## 3. T3 临期导出新建
|
||||
|
||||
- [x] 3.1 新增临期导出数据源与唯一导出场景标识(既有代码中不存在该场景,不复用不存在的实现)。
|
||||
- [x] 3.2 场景注册与支持判定:导出场景常量、数据源、默认注册表、支持场景白名单、创建导出任务 DTO 的场景联合类型白名单同步新增该场景。
|
||||
- [x] 3.3 新增受控端点 `POST /api/admin/expiring-assets/export`,经既有 `registerPackageExpiryRoutes` 注册。本次未新增 Handler 类型:`AssetHandler` 已由 `pkg/openapi/handlers.go` 的文档工厂统一装配,`cmd/api/docs.go` 与 `cmd/gendocs/main.go` 均经该工厂取用,**确认无需改动**;新路由的 Input/Output 元数据已装配并出现在重新生成的 `docs/admin-openapi.yaml` 中。
|
||||
- [x] 3.4 粒度与列定义:一行对应一项资产,取当前生效主套餐最终到期时间与剩余天数,加油包不单独成行;列为店铺、业务员、用户组、资产类型、设备类型、设备型号、资产标识、当前套餐、到期时间、剩余天数(依据 111 §18.1)。
|
||||
- [x] 3.5 筛选集合与列表一致(时间区间、剩余天数上下限、套餐、资产类型、关键字、店铺),并复用列表同一候选预筛与最终到期推算实现,不得另写第二套到期口径。
|
||||
- [x] 3.6 店铺、业务员、用户组按执行时当前归属补充(与达量预警同一口径),且结果不超出任务创建时冻结的可见店铺范围。
|
||||
- [x] 3.7 与 T2 协调后落盘共享文件(路由、场景常量、注册表、证据 JSON)。
|
||||
|
||||
## 4. T4 佣金明细导出补筛选
|
||||
|
||||
- [x] 4.1 佣金明细导出场景新增按创建时间的 `start_time`/`end_time` 闭区间筛选,覆盖原佣金与回溯两条分支各自的创建时间列。
|
||||
- [x] 4.2 创建期校验时间边界并冻结为 UTC RFC3339 秒级字符串。
|
||||
- [x] 4.3 回归确认记录粒度(原佣金与回溯各一行)、列定义与余额口径逐字不变:负数金额不裁剪、入账后余额原样导出。
|
||||
|
||||
## 5. T5 冻结规范化与遗留任务失败语义
|
||||
|
||||
- [x] 5.1 创建导出任务时按场景校验并规范化时间边界为 UTC RFC3339 秒级字符串,与筛选、操作者、可见店铺范围一并冻结(沿用既有列,见 design「无 Schema 变更」承载表)。纳入该规范化与校验的场景集合为 `exchange`、`agent_recharge`、`order`、`commission_record`、`expiring_asset` 与 `package_traffic_alert`;达量预警的冻结值本已是 UTC RFC3339 秒级串,规范化对其为幂等。
|
||||
- [x] 5.2 执行期不再做多格式宽松解析,只按冻结值严格解析;解析失败(含变更前遗留任务的旧格式冻结值)任务落失败并写入安全失败摘要,禁止静默忽略该条件后放行全量。达量预警导出(场景 `package_traffic_alert`)同属该执行期规则:其列表与创建入口的宽松接受面与 `*time.Time` DTO 类型不变(仍由全局兼容解析器归一为 UTC RFC3339 秒级串后冻结),执行期改用同一份共享严格解析器,冻结值非法时任务落失败。
|
||||
- [x] 5.3 失败重试沿用原快照,不产生第二份不同口径文件。
|
||||
- [x] 5.4 数据范围沿用既有创建期冻结与导出侧范围过滤:代理空可见范围创建时拒绝、执行期空范围返回空结果;不得使用请求上下文版过滤;权限、筛选或归属变化不扩大已创建任务的数据集。
|
||||
|
||||
## 6. T6 契约与文档
|
||||
|
||||
- [x] 6.1 生成 DTO 与 OpenAPI 变更清单(受影响端点、参数名、类型由 `*time.Time`/裸字符串改为字符串、错误语义),供前端同步。
|
||||
- [x] 6.2 生成前端同步清单:改参数名的端点(换货、分配记录、代理充值、临期列表)、改传值格式的端点(IoT 卡导入任务、设备导入任务、导出任务列表、订单、授权记录、提现记录两处、临期列表及其导出、换货导出、代理充值导出)、新增参数的端点(佣金明细列表、佣金明细导出、临期导出)。
|
||||
- [x] 6.3 更新 `docs/verification/context-reset/requirement-evidence.json` 与 `entry-capability-requirement-matrix.json`:新增本能力的 Requirement 证据行与新增路由 `POST /api/admin/expiring-assets/export` 的入口行,保持与 Specs 双向一致。
|
||||
- [x] 6.4 更新 `README.md` 的导出场景清单与相关系数说明(现仅列 `device`/`iot_card`)。
|
||||
- [x] 6.5 核对 Handler 占位装配与文档生成入口:本次未新增 Handler 类型,`AssetHandler` 已由 `pkg/openapi/handlers.go` 的文档工厂统一装配,`cmd/api/docs.go` 与 `cmd/gendocs/main.go` **确认无需改动**;已重新生成 `docs/admin-openapi.yaml`,新路由 `POST /api/admin/expiring-assets/export` 与 13 个受影响端点的时间参数均已更新。
|
||||
- [x] 6.6 记录「确认无 Schema 变更」的证据:核对该变更未新增/修改任何迁移文件,冻结字段承载于既有 `tb_export_task` 列(`query_json`、`scope_shop_ids`、操作者列、`file_key`/`file_size`/`total_rows`/`error_message`),因此迁移 up/down/up 为不适用而非跳过。
|
||||
|
||||
## 7. 验证
|
||||
|
||||
环境口径(ENG-TEST-001):维护者指定的测试 PostgreSQL `junhong_cmp_test`、Redis DB 6、`Iteration/8-11` 测试部署与 `cmp-test` 日志主机为唯一验证面;fixture 仅创建与清理本 Change 自己的记录,不重置整个测试数据库;自动化测试按项目决策为 N/A,且仓库内不得新增 `*_test.go`。
|
||||
|
||||
- [x] 7.1 接受集合与拒绝集合逐条实跑:`Z`、`±hh:mm`、小数秒(含 `.000`)、无时区、date-only、空格分隔、`±hhmm`、未补零、非法日期,断言各自接受或返回参数非法错误码。
|
||||
- [x] 7.2 区间边界:仅传开始、仅传结束、两端均传、两端缺省、开始等于结束(存在恰好等于该时刻的记录并返回)。
|
||||
- [x] 7.3 顺序校验:开始晚于结束一律拒绝。
|
||||
- [x] 7.4 未改造端点保持既有宽松行为与既有结果不变。
|
||||
- [x] 7.5 提现记录非法参数不再返回全量;授权记录闭区间含两端。
|
||||
- [x] 7.6 列表与导出在同一筛选下行集一致:临期列表与其导出、佣金明细列表与其导出、达量预警列表与其导出。
|
||||
- [x] 7.7 临期导出表头与 111 §18.1 逐列一致,且加油包不单独成行。
|
||||
- [x] 7.8 权限与归属:创建导出任务后变更店铺归属、业务员或用户组,再执行任务,行集不超出创建时冻结范围;代理空范围创建被拒绝;越权与不存在统一不可见。(代理视角经 HTTP 的创建/拒绝路径在 HEAD `398a5e4` 的门禁修复后已可复现验证,见验证文档第 6 节;范围冻结语义另由等价冻结任务实测覆盖)
|
||||
- [x] 7.9 遗留任务安全:构造一条冻结了旧格式时间值的待处理导出任务(fixture 仅属于本 Change),执行后落失败并写入安全失败摘要,且不产出全量文件;执行后清理该 fixture。
|
||||
- [x] 7.10 回归:佣金明细导出与达量预警导出的列定义、负数金额与余额口径逐字不变;导出表头输出与变更前一致。
|
||||
- [x] 7.11 确认无迁移已记录:该变更未修改任何迁移文件,`up/down/up` 不适用;引用 T6 的证据记录。
|
||||
- [x] 7.12 门禁命令:`gofmt -w`(变更文件)、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`(连续两次生成结果一致)、`openspec validate add-export-time-filter-standards --strict`、`openspec validate --all`、`openspec doctor --json`、`./scripts/context-health.sh`。
|
||||
Reference in New Issue
Block a user