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:
184
openspec/specs/export-time-filter/spec.md
Normal file
184
openspec/specs/export-time-filter/spec.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# export-time-filter Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
为 2026 年 8 月迭代提供统一的时间筛选参数与解析契约,规定受影响端点与旧格式的替换关系,并规定临期、佣金明细与达量预警三类异步导出在创建时冻结筛选与数据范围、执行期不得重新解释或扩大数据范围的行为。
|
||||
|
||||
## 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`(创建临期资产导出任务)。
|
||||
Reference in New Issue
Block a user