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:
@@ -0,0 +1,542 @@
|
||||
# AUG26-014 统一时间筛选与临期导出(add-export-time-filter-standards)范围、契约与前端同步清单
|
||||
|
||||
本文件记录 OpenSpec Change `add-export-time-filter-standards` 的实施范围、端点改造对照、DTO 与 OpenAPI 变更清单以及前端同步清单。
|
||||
行为契约以 [`../../openspec/specs/export-time-filter/spec.md`](../../openspec/specs/export-time-filter/spec.md) 与变更产物
|
||||
[`../../openspec/changes/archive/2026-09-17-add-export-time-filter-standards/`](../../openspec/changes/archive/2026-09-17-add-export-time-filter-standards/) 为准;本文件是实施记录与同步清单,不是行为契约。
|
||||
|
||||
本文的实测证据节按 T7 分工由独立验证 worker 追加;本文不写未经实跑的结论。
|
||||
|
||||
## 1. 范围与依据
|
||||
|
||||
- 迭代编号:`AUG26-014`。需求依据:`docs/product/2026-08-迭代-PRD-讨论稿.md` §2.16(时间筛选与导出快照两段);导出列定义依据 `111.md` §18。
|
||||
- 执行契约:变更产物 `proposal.md`、`design.md`、`tasks.md` 与 `specs/export-time-filter/spec.md`;冲突时以 spec delta 为准。
|
||||
- 统一契约:被覆盖的列表与导出入口使用可选的 `start_time`/`end_time`,取值 MUST 为带显式时区的 RFC3339 秒级时间,解析结果归一为 UTC 瞬时,筛选为闭区间含两端;任一端可缺省,未传与空串同义;开始晚于结束拒绝。
|
||||
- 共享实现落点:`pkg/utils/time_range.go`(`ParseTimeRange`),创建期 handler 与执行期 `internal/exporter` 共用同一份实现。
|
||||
- 本次边界(不做):不新增迁移、不改既有迁移、不改数据库 Schema、不新增运行时开关、不删除全局宽松解析器(`cmd/api/main.go` 的 `registerTimeParserCompat`)、不新增 `*_test.go`、不重构未列入本次的端点与其导出、不做跨度上限、不改任何导出的记录粒度/列定义/余额口径、不修佣金统计类接口的裸串直传。
|
||||
- 未列入本次的端点保持既有参数名、既有格式接受范围与既有筛选结果,逐项登记见 `design.md`「已知差异登记(本次不修)」;其中达量预警列表与导出本期一律不改(依据端点表「已符合」行)。
|
||||
- 数据承载:无 Schema 变更,冻结值写入既有 `tb_export_task.query_json` / `scope_shop_ids` 与既有操作者、文件、行数、`error_message` 列,详见第 5 节。
|
||||
|
||||
## 2. 端点改造清单
|
||||
|
||||
### 2.1 列表端点
|
||||
|
||||
| 端点 | 改造前参数与格式 | 改造后 | 筛选依据业务时间 |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET /api/admin/iot-cards/import-tasks` | `start_time`/`end_time`(`*time.Time`,经全局宽松解析:兼容 RFC3339、无时区、date-only) | 参数名不变,`string` + 严格解析 | 导入任务创建时间 |
|
||||
| `GET /api/admin/devices/import/tasks` | 同上 | 参数名不变,`string` + 严格解析 | 导入任务创建时间 |
|
||||
| `GET /api/admin/export-tasks` | 同上 | 参数名不变,`string` + 严格解析 | 导出任务创建时间 |
|
||||
| `GET /api/admin/orders` | 同上 | 参数名不变,`string` + 严格解析 | 订单创建时间 |
|
||||
| `GET /api/admin/exchanges` | `created_at_start`/`created_at_end`(`*time.Time`,宽松) | 改名 `start_time`/`end_time`,`string` + 严格解析;旧参数名不再生效 | 换货单创建时间 |
|
||||
| `GET /api/admin/asset-allocation-records` | `created_at_start`/`created_at_end`(`*time.Time`,宽松) | 改名 `start_time`/`end_time`,`string` + 严格解析 | 分配记录创建时间 |
|
||||
| `GET /api/admin/agent-recharges` | `start_date`/`end_date`(无格式约束 `string`,按日拼接当日 `00:00:00`/`23:59:59`) | 改名 `start_time`/`end_time`,`string` + 严格解析;删除按日补齐 | 代理充值记录创建时间 |
|
||||
| `GET /api/admin/shops/{shop_id}/commission-records` | 无时间参数 | 新增 `start_time`/`end_time`,闭区间 | 佣金明细创建时间(原佣金与回溯两条分支各自的 `created_at`) |
|
||||
| `GET /api/admin/commission/withdrawal-requests` | `start_time`/`end_time`(无时区 `2006-01-02 15:04:05`,解析失败静默忽略该条件) | 参数名不变,严格解析;删除静默跳过,非法参数一律 1001 | 提现申请创建时间 |
|
||||
| `GET /api/admin/shops/{shop_id}/withdrawal-requests` | 同上 | 同上 | 提现申请创建时间 |
|
||||
| `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`,按最终到期**时刻**闭区间比较;保留既有剩余天数上下限筛选 | 当前生效主套餐最终到期时刻 |
|
||||
|
||||
### 2.2 导出场景
|
||||
|
||||
| 导出场景 / 入口 | 改造前筛选键 | 改造后 | 口径来源 |
|
||||
| --- | --- | --- | --- |
|
||||
| `exchange`(经 `POST /api/admin/export-tasks`) | `created_at_start`/`created_at_end`(宽松多格式解析) | 改名 `start_time`/`end_time`,共享严格解析器;旧键在创建期被拒绝 | 换货单创建时间 |
|
||||
| `agent_recharge`(同上) | `start_date`/`end_date`(宽松、按日补齐当日末秒) | 改名 `start_time`/`end_time`,共享严格解析器;旧键在创建期被拒绝 | 代理充值记录创建时间 |
|
||||
| `order`(同上) | `start_time`/`end_time`(宽松) | 参数名不变,共享严格解析器 | 订单创建时间 |
|
||||
| `commission_record`(同上) | 无时间筛选 | 新增 `start_time`/`end_time`,两条分支各自的创建时间列闭区间 | 佣金明细创建时间 |
|
||||
| 临期资产(`POST /api/admin/expiring-assets/export`) | 场景此前不存在 | 本次新建场景 `expiring_asset` 与受控入口 | 当前生效主套餐最终到期时刻 |
|
||||
| `package_traffic_alert`(`POST /api/admin/package-traffic-alerts/export`) | `start_time`/`end_time`(RFC3339 秒级,创建时冻结) | 入口参数名与 `*time.Time` 契约、列表 `GET /api/admin/package-traffic-alerts` 行为、列定义与触发快照口径**保持不动**;**执行期改为只按冻结值严格解析**(冻结值非法则任务落失败,依据 Requirement 4) | 预警触发时间 |
|
||||
|
||||
### 2.3 未改造项确认(逐一核对未改动)
|
||||
|
||||
- 端点保持原参数名与宽松解析、本期未新增时间参数:资产钱包流水、客户钱包流水、设备资产列表(`created_at_start`/`created_at_end`)、代理主钱包流水(`start_date`/`end_date`)、手机号—资产关联列表、佣金统计类接口(`commission-stats`、`commission-daily-stats`)、审计类列表、员工代收款账单、轮询告警历史、企业卡与企业设备授权列表、IoT 卡资产列表。
|
||||
- 导出场景入口保持原筛选键与宽松接受面:`device`、`iot_card`、`package`、`agent_wallet_transaction`、`refund`;达量预警导出(`package_traffic_alert`)入口与列表同样保持宽松接受面,仅执行期改为严格解析(见 2.2 行与 Requirement 4)。
|
||||
- 全局宽松解析器 `cmd/api/main.go` 的 `registerTimeParserCompat` 未改动,未列入端点继续由其服务。
|
||||
- 受影响列表端点的旧参数名现已显式拒绝(`created_at_start`/`created_at_end`、`start_date`/`end_date`、`expires_from`/`expires_to` 出现且非空即返回 1001,空值仍按未传处理),因此这些名字不会以「被忽略」的形式留在受影响端点。
|
||||
- 生成文档核对:`docs/admin-openapi.yaml` 中 `created_at_start`、`start_date` 仅残留于未改造端点与未改造的请求 DTO(`DtoBatchSetDeviceSeriesBindngRequest`、`/api/admin/devices`、`/api/admin/phone-asset-associations`、`commission-daily-stats`、主钱包流水与 `/api/open/v1/wallet/transactions`);`expires_from`/`expires_to` 在代码中只作为 `internal/handler/admin/asset.go` 的旧参数拒绝清单字符串出现(并作为改造前参数名记录在本次契约与文档中),生成文档中已完全不再出现;13 个受影响端点均只暴露 `start_time`/`end_time`。
|
||||
|
||||
## 3. DTO 与 OpenAPI 变更清单
|
||||
|
||||
统一说明:受影响端点的时间字段 Go 类型一律由 `*time.Time`(或裸 `string`)改为 `string`,由 handler 调用 `pkg/utils.ParseTimeRange` 严格解析;`*time.Time` 类型会触发全局宽松解析器,因此必须字符串化才能给出指明字段的中文错误消息。错误语义:格式非法与顺序错误统一返回既有参数非法错误码 `1001`,消息固定为
|
||||
`<字段> 时间格式不合法,必须为带时区的 RFC3339 秒级时间,例如 2026-09-01T00:00:00+08:00` 与 `start_time 不能晚于 end_time`,不新增结构化字段级错误载荷。
|
||||
|
||||
| DTO | 字段(json / query tag) | 类型变更 | description 变更 |
|
||||
| --- | --- | --- | --- |
|
||||
| `dto.ListImportTaskRequest` | `start_time` / `end_time` | `*time.Time` → `string` | 改为「带时区的 RFC3339 秒级时间,闭区间含该时刻」 |
|
||||
| `dto.ListDeviceImportTaskRequest` | `start_time` / `end_time` | `*time.Time` → `string` | 同上 |
|
||||
| `dto.ListExportTaskRequest` | `start_time` / `end_time` | `*time.Time` → `string` | 同上 |
|
||||
| `dto.OrderListRequest` | `start_time` / `end_time` | `*time.Time` → `string` | 同上 |
|
||||
| `dto.ExchangeListRequest` | `created_at_start`/`created_at_end` → `start_time`/`end_time` | `*time.Time` → `string`(参数改名) | 同上 |
|
||||
| `dto.ListAssetAllocationRecordRequest` | `created_at_start`/`created_at_end` → `start_time`/`end_time` | `*time.Time` → `string`(参数改名) | 同上 |
|
||||
| `dto.AgentRechargeListRequest` | `start_date`/`end_date` → `start_time`/`end_time` | `string` → `string`(参数改名,取值受限) | 由「YYYY-MM-DD」改为「带时区的 RFC3339 秒级时间」 |
|
||||
| `dto.ShopCommissionRecordListReq` | 新增 `start_time` / `end_time` | 新增 `string` | 新增:佣金明细创建时间闭区间 |
|
||||
| `dto.WithdrawalRequestListReq` | `start_time` / `end_time` | `string`(取值受限) | 由「2006-01-02 15:04:05」改为「带时区的 RFC3339 秒级时间」 |
|
||||
| `dto.ShopWithdrawalRequestListReq` | `start_time` / `end_time` | `string`(取值受限) | 同上 |
|
||||
| `dto.AuthorizationListReq` | `start_time` / `end_time` | `string`(取值受限) | 由「格式:2006-01-02」改为「带时区的 RFC3339 秒级时间,闭区间含该时刻」 |
|
||||
| `dto.ExpiringAssetListRequest` | `expires_from`/`expires_to` → `start_time`/`end_time` | `string` → `string`(参数改名,取值受限) | 由「上海自然日 YYYY-MM-DD」改为「按最终到期时刻的闭区间」 |
|
||||
| `dto.ExportExpiringAssetRequest` | 新增(`format`、`asset_type`、`keyword`、`shop_id`、`package_id`、`days_min`、`days_max`、`start_time`、`end_time`) | 新增 `string` 时间字段 | 新增受控导出入口请求体 |
|
||||
| `dto.CreateExportTaskRequest` | `scene` 白名单与 `query` 说明 | 不变 | 白名单新增 `expiring_asset`;说明补充时间筛选键与取值要求 |
|
||||
| `dto.ListExportTaskRequest` | `scene` 白名单 | 不变 | 白名单新增 `expiring_asset` |
|
||||
|
||||
OpenAPI:`docs/admin-openapi.yaml` 已重新生成(`go run cmd/gendocs/main.go`),新增 `POST /api/admin/expiring-assets/export`,13 个受影响端点的时间参数已按上表更新。
|
||||
|
||||
## 4. 前端同步清单
|
||||
|
||||
无兼容期、无运行时开关:前端 MUST 与后端同迭代上线。
|
||||
|
||||
### 4.1 改参数名的端点与导出
|
||||
|
||||
| 位置 | 旧参数 | 新参数 |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/admin/exchanges` 与换货导出(场景 `exchange`) | `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` 与代理充值导出(场景 `agent_recharge`) | `start_date`、`end_date` | `start_time`、`end_time` |
|
||||
| `GET /api/admin/expiring-assets` | `expires_from`、`expires_to` | `start_time`、`end_time` |
|
||||
|
||||
旧参数名在受影响端点不再生效(换货导出与代理充值导出的旧筛选键在创建期直接返回 1001)。
|
||||
|
||||
### 4.2 改传值格式的端点与导出
|
||||
|
||||
格式要求:带显式时区的 RFC3339 秒级时间,UTC 偏移写作 `Z` 或 `+08:00`;整日区间用 `T00:00:00+08:00` 与 `T23:59:59+08:00` 表达。拒绝 date-only、空格分隔、无时区、小数秒(含 `.000`)、`±hhmm` 与未补零写法。
|
||||
|
||||
| 位置 | 旧传值示例 | 新传值示例 |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/admin/iot-cards/import-tasks` | `2026-09-01` 或 `2026-09-01 00:00:00` | `2026-09-01T00:00:00+08:00` |
|
||||
| `GET /api/admin/devices/import/tasks` | 同上 | 同上 |
|
||||
| `GET /api/admin/export-tasks` | 同上 | 同上 |
|
||||
| `GET /api/admin/orders` 与订单导出(场景 `order`) | 同上 | 同上 |
|
||||
| `GET /api/admin/authorizations` | `2026-09-01`(结束日按次日开区间) | `2026-09-01T00:00:00+08:00`(结束时间为闭区间,整日需显式写 `23:59:59`) |
|
||||
| `GET /api/admin/commission/withdrawal-requests` | `2026-09-01 00:00:00` | `2026-09-01T00:00:00+08:00`;非法值由「忽略该条件返回全量」改为 1001 |
|
||||
| `GET /api/admin/shops/{shop_id}/withdrawal-requests` | 同上 | 同上 |
|
||||
| `GET /api/admin/expiring-assets` 与临期导出 | `2026-09-01`(上海自然日) | `2026-09-01T00:00:00+08:00`(按最终到期时刻闭区间) |
|
||||
| 换货导出、代理充值导出 | 旧格式同上 | 同上 |
|
||||
|
||||
注意:受影响端点的时间参数不再接受无时区写法,前端不得沿用「按浏览器本地时区补 00:00:00」的旧拼接方式,必须显式带偏移。受影响列表端点(换货、分配记录、代理充值、临期列表)携带旧参数名(`created_at_start`/`created_at_end`、`start_date`/`end_date`、`expires_from`/`expires_to`)且非空时现在返回 1001(消息:`<旧字段名> 已废弃,请改用 start_time 与 end_time`),前端必须同迭代改名,不能依赖旧参数被忽略。
|
||||
|
||||
**达量预警导出经通用入口的传值收紧(`POST /api/admin/export-tasks`,`scene=package_traffic_alert`)**
|
||||
|
||||
- 位置:通用导出入口 `POST /api/admin/export-tasks`,`scene=package_traffic_alert`(达量预警导出场景)。
|
||||
- 变更:`query.filters.start_time` / `query.filters.end_time` 现要求带显式时区的 RFC3339 秒级时间(与本节上表同一格式要求);宽松写法(date-only、空格分隔、无时区、小数秒含 `.000`、`±hhmm`、未补零、前后空白)由变更前的「创建成功 + 执行期静默忽略该条件(等于不加该条件,可能导出全量)」改为**创建期直接返回 1001 参数非法**,消息与其他受影响端点同一套固定中文文案:`start_time 时间格式不合法,必须为带时区的 RFC3339 秒级时间,例如 2026-09-01T00:00:00+08:00`(`end_time` 同构),开始晚于结束为 `start_time 不能晚于 end_time`。
|
||||
- 边界:达量预警的**专用入口** `POST /api/admin/package-traffic-alerts/export` 与其列表 `GET /api/admin/package-traffic-alerts` 的参数名、类型与宽松接受面**本次不变**(仍为 `*time.Time` + 全局兼容解析器,创建期归一为 UTC RFC3339 秒级后冻结);只有**执行期**改为只按冻结值严格解析,冻结值非法时任务落失败并写固定摘要 `导出筛选的时间边界非法,任务已终止`,不放行全量。
|
||||
- 依据:变更 delta 与主 Spec 的「三类异步导出的创建期冻结与不扩大范围」需求;`design.md` 已知差异登记第 5 条(无时区列那条为第 6 条、临期导出取舍为第 7 条)。
|
||||
|
||||
### 4.3 新增参数的端点与导出
|
||||
|
||||
| 位置 | 新增参数 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/admin/shops/{shop_id}/commission-records` | `start_time`、`end_time` | 按佣金明细创建时间闭区间,覆盖原佣金与回溯两条分支 |
|
||||
| 佣金明细导出(场景 `commission_record`) | `query.filters.start_time`、`query.filters.end_time` | 同上;经 `POST /api/admin/export-tasks` 提交 |
|
||||
| 临期导出 `POST /api/admin/expiring-assets/export` | 整个入口为新增 | 请求体:`format`(必填)、`asset_type`、`keyword`、`shop_id`、`package_id`、`days_min`、`days_max`、`start_time`、`end_time` |
|
||||
|
||||
## 5. 无 Schema 变更确认(T6.6)
|
||||
|
||||
- 本变更未新增迁移文件、未修改任何既有迁移、未改变数据库 Schema,因此迁移 `up/down/up` 为**不适用**而非跳过。
|
||||
- 冻结值承载于既有列:`tb_export_task.query_json`(jsonb,写入规范化后的 UTC RFC3339 秒级字符串)、`scope_shop_ids`(jsonb)、操作者四元组与 `creator`/`updater`、`file_key`/`file_size`/`total_rows`/`error_message`。表头冻结沿用既有 `query_json.resolved_headers`。
|
||||
- 失败摘要沿用既有 `error_message` 通道,新增固定摘要文案 `导出筛选的时间边界非法,任务已终止`。
|
||||
- 该严格解析失败即落失败的规则同样适用于达量预警导出(场景 `package_traffic_alert`):其入口与列表的宽松接受面不变,执行期冻结值非法时同样落失败,不放行全量。
|
||||
|
||||
## 6. 实测证据(T7 实跑)
|
||||
|
||||
本节由独立验证 worker 实跑写入,全部结论均有命令与原始输出支撑;未实跑项在 §6.5 逐条列明。
|
||||
|
||||
### 6.0 环境、工作区指纹与执行方式
|
||||
|
||||
| 项 | 值 |
|
||||
| --- | --- |
|
||||
| HEAD | `e8ab1f471e9cbf27acd89ccdcfb5073b4fc7baa1`(含维护者提交 `398a5e42`「fix(路由): 修正套餐真流量预警与资产自动续费的超管/平台 gate 作用域」与其后的 `e8ab1f47`;本 Change 的实现改动仍在未提交工作区) |
|
||||
| 工作区内容指纹(本批) | `git diff` 与全部未跟踪文件内容的合并 SHA1:`37d7d69021f264be6f04e31062a18e8aa695c8a0` |
|
||||
| 工作区内容指纹(上一批) | `51c619f8a3791e5c6e3de8878af3be5105dd73f`(`NAMES_FP=d1aa8d512dfb5d43e17b2ded30ae761aaca2578c`,63 项) |
|
||||
| 代码状态 | 未提交工作区(本 Change 实现已落盘);维护者的路由门禁修复以提交形式位于 HEAD |
|
||||
| PostgreSQL | `junhong_cmp_test`(参数取自 `.env`),库内诊断只读走 dbhub MCP;写入一律经临时 Go 工具用仓库自身 GORM/配置执行(遵循 ENG-DB-002) |
|
||||
| Redis | `cxd.whcxd.cn:16299` **DB 7**(`.env.local` 默认值,与 `scripts/run-local.sh` 一致) |
|
||||
| API | `go run ./cmd/api` → `http://127.0.0.1:3000`,就绪日志「服务器已启动」 |
|
||||
| Worker | `go run ./cmd/worker`,就绪日志「Worker 服务器启动中」 |
|
||||
| 鉴权 | 超管:`POST /api/auth/login`(口令取自 README,不回显)。代理视角:**第二批起改用 fixture 代理账号的真实登录**(`tb_account` 自建 `AUG26014AGENT*`,user_type=3,口令为 bcrypt 哈希由工具写入,不回显);手工注入 DB 7 的等价令牌仅用于第一批的根因定位对照,并已在 §6.6 明确其 `user_type` 来源 |
|
||||
|
||||
**与 ENG-TEST-001 的两处偏离(均已在执行中记录):**
|
||||
|
||||
1. **Redis 用 DB 7 而非 DB 6**:DB 6 是 `Iteration/8-11` 测试部署的共享队列,本地 worker 若连 DB 6 会抢占真实测试任务并消费掉它们;`.env.local` 本身即配置 DB 7,与 `scripts/run-local.sh` 的实际行为一致。导出链路的冻结、分片、合并、上传语义与队列 DB 号无关,因此该偏离不影响本次结论。
|
||||
2. **启动命令用 `go run ./cmd/api` 而非 `./scripts/run-local.sh api`**:`scripts/run-local.sh` 执行的是 `go run cmd/api/main.go`(单文件),而 `generateOpenAPIDocs` 定义在同包的另一文件 `cmd/api/docs.go`,单文件编译必然报 `cmd/api/main.go:101:2: undefined: generateOpenAPIDocs` 并退出。这是**既有仓库脚本/README 缺陷**(`scripts/run-local.sh`、`cmd/api/main.go`、`cmd/api/docs.go` 均不在本 Change 改动范围内),本 Change 未引入也未加重;改用包路径 `go run ./cmd/api` 后正常启动。建议另行登记修复。
|
||||
|
||||
### 6.1 逐项结果汇总
|
||||
|
||||
| # | tasks.md 项 | 断言数 | 结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 7.11/7.12 门禁命令(gofmt/build/gendocs/openspec/doctor/context-health) | 7 | **PASS 7 / FAIL 0** |
|
||||
| 2 | 7.1 接受集合与拒绝集合逐条实跑 | 20 | **PASS 20 / FAIL 0** |
|
||||
| 3 | 7.2 区间边界(仅一端/两端/两端缺省/空串/开始等于结束/空结果集) | 5 + 6 | **PASS 11 / FAIL 0** |
|
||||
| 4 | 7.3 顺序校验(开始晚于结束) | 1 | **PASS 1 / FAIL 0** |
|
||||
| 5 | 7.4 未改造端点保持既有宽松行为 | 4 | **PASS 4 / FAIL 0** |
|
||||
| 6 | 7.5 提现记录非法参数不再返回全量 | 5 | **PASS 5 / FAIL 0** |
|
||||
| 7 | 7.5 授权记录闭区间含两端 | 3 | **PASS 3 / FAIL 0** |
|
||||
| 8 | 7.6 佣金明细列表与其导出行集一致 | 2 | **PASS 2 / FAIL 0** |
|
||||
| 9 | 7.6/7.7 临期列表与其导出一致、表头、加油包不单独成行 | 5 | **PASS 5 / FAIL 0** |
|
||||
| 10 | 受影响端点旧参数名拒绝 + 空值按未传(F1 修复后重跑) | 8 | **PASS 8 / FAIL 0** |
|
||||
| 11 | 7.9 遗留旧格式冻结任务安全失败 | 1 | **PASS 1 / FAIL 0** |
|
||||
| 12 | 7.7 达量预警导出回归(列表宽松、导出严格、表头逐字) | 5 | **PASS 5 / FAIL 0** |
|
||||
| 13 | 7.10 佣金明细导出列定义与金额/余额口径回归 | 1 | **PASS 1 / FAIL 0** |
|
||||
| 14 | 7.7 创建后归属变化不扩大数据集(冻结 scope 执行期生效) | 3 | **PASS 3 / FAIL 0** |
|
||||
| 15 | 7.8 代理账号经 HTTP 访问临期列表/导出(第一批:修复前 403/1005) | 2 | 第一批 **FAIL**;根因定位为既有路由门禁作用域缺陷,已由维护者提交 `398a5e42` 修复 → 第二批重跑 **PASS**(见第 16 行) |
|
||||
| 16 | 7.8 代理 HTTP 全链路(真实登录):可见范围过滤 / 导出创建与执行 / 空可见范围被拒 / 越权与不存在统一不可见 | 6 | **PASS 6 / FAIL 0** |
|
||||
| 17 | 7.6 达量预警列表与导出同筛选行集一致(缺口补齐) | 1 | **PASS 1 / FAIL 0** |
|
||||
| — | 合计 | **85 PASS / 0 FAIL** | |
|
||||
|
||||
### 6.2 关键原始输出
|
||||
|
||||
**(1)门禁命令**
|
||||
|
||||
> 下列 7 条在第二批(HEAD=`e8ab1f47`)已原样重跑,输出与本处记录逐字一致;T7 实现内容与第一批相同,仅有维护者的路由门禁修复提交进入 HEAD。
|
||||
|
||||
```
|
||||
$ gofmt -l <63 个改动/新增 .go 文件> # 无输出
|
||||
$ go build ./cmd/api ./cmd/worker # 退出码 0
|
||||
$ go run cmd/gendocs/main.go(连续两次) # cmp -s 一致:gendocs_idempotent=PASS
|
||||
$ openspec validate add-export-time-filter-standards --strict
|
||||
Change 'add-export-time-filter-standards' is valid # 退出码 0
|
||||
$ openspec validate --all
|
||||
Totals: 36 passed, 0 failed (36 items) # 退出码 0
|
||||
$ openspec doctor --json
|
||||
healthy= True status= [] # 退出码 0
|
||||
$ ./scripts/context-health.sh
|
||||
Context 健康检查通过 # 退出码 0
|
||||
```
|
||||
|
||||
**(2)接受集合与拒绝集合(`GET /api/admin/export-tasks`)**
|
||||
|
||||
接受(HTTP 200 / code 0):`2026-09-01T00:00:00Z`、`2026-09-01T00:00:00+08:00`、`2026-09-01T00:00:00-05:30`、同瞬时两种偏移组合、仅 `end_time`、空串两端、两端缺省。
|
||||
|
||||
拒绝(HTTP 400 / code 1001),消息逐条一致:
|
||||
|
||||
```
|
||||
start_time 时间格式不合法,必须为带时区的 RFC3339 秒级时间,例如 2026-09-01T00:00:00+08:00
|
||||
```
|
||||
|
||||
| 拒绝输入 | 归类 |
|
||||
| --- | --- |
|
||||
| `2026-09-01T00:00:00.000Z`、`2026-09-01T00:00:00.5+08:00` | 小数秒 |
|
||||
| `2026-09-01T00:00:00` | 无时区 |
|
||||
| `2026-09-01` | date-only |
|
||||
| `2026-09-01 00:00:00` | 空格分隔 |
|
||||
| `2026-09-01T00:00:00+0800` | `±hhmm` 偏移 |
|
||||
| `2026-9-01T00:00:00Z` | 未补零日期 |
|
||||
| `2026-09-01T0:00:00Z` | 未补零时间 |
|
||||
| `2026-02-30T00:00:00+08:00` | 非法日历日期 |
|
||||
| `2026-09-01T00:00:00+24:00`、`+23:60`、`+00:60`、`-24:00` | 偏移越界(F3 收紧后) |
|
||||
| `␣2026-09-01T00:00:00+08:00␣`、纯空白串、`\t` | 空白包裹/纯空白(F3 收紧后) |
|
||||
|
||||
顺序校验:
|
||||
|
||||
```
|
||||
start_time=2026-09-02T00:00:00Z & end_time=2026-09-01T00:00:00Z
|
||||
→ HTTP 400 code=1001 msg=start_time 不能晚于 end_time
|
||||
```
|
||||
|
||||
**(3)区间边界(`GET /api/admin/export-tasks`)**
|
||||
|
||||
```
|
||||
两端缺省 total=55 | 空串两端 total=55(与缺省一致)
|
||||
仅 start total=3 | 仅 end total=52 | 空结果集 total=0(HTTP 200,不报错)
|
||||
```
|
||||
|
||||
**(4)受影响端点旧参数名拒绝(F1 修复后重跑)**
|
||||
|
||||
| 请求 | 结果 |
|
||||
| --- | --- |
|
||||
| `GET /api/admin/exchanges?created_at_start=2026-09-01T00:00:00+08:00` | 1001 `created_at_start 已废弃,请改用 start_time 与 end_time` |
|
||||
| `GET /api/admin/asset-allocation-records?created_at_start=...` | 1001 `created_at_start 已废弃,请改用 start_time 与 end_time` |
|
||||
| `GET /api/admin/agent-recharges?start_date=...` | 1001 `start_date 已废弃,请改用 start_time 与 end_time` |
|
||||
| `GET /api/admin/expiring-assets?expires_from=...` | 1001 `expires_from 已废弃,请改用 start_time 与 end_time` |
|
||||
| 上述四个端点的旧参数**空值**(`?start_date=`) | HTTP 200 / code 0(按未传处理) |
|
||||
|
||||
**(5)未改造端点保持宽松(HTTP 200 / code 0,total 与缺省一致)**
|
||||
|
||||
```
|
||||
GET /api/admin/devices?created_at_start=2026-01-01 total=6
|
||||
GET /api/admin/devices?created_at_start=2026-01-01T00:00:00 total=6
|
||||
GET /api/admin/shops/1/main-wallet/transactions?start_date=2026-01-01&end_date=2026-12-31 total=10
|
||||
GET /api/admin/audit/events total=2734186
|
||||
```
|
||||
|
||||
**(6)授权记录闭区间(`GET /api/admin/authorizations`,`iccid` 过滤隔离标记行)**
|
||||
|
||||
```
|
||||
start=2026-09-17T10:00:00+08:00 & end=2026-09-17T11:00:00+08:00 → total=2(恰好命中 02:00:00 与 03:00:00 两行)
|
||||
start=end=2026-09-17T10:00:00+08:00 → total=1(闭区间含 start 端)
|
||||
start=end=2026-09-17T16:00:00+08:00 → total=0(区间外)
|
||||
```
|
||||
|
||||
旧 date-only 值被拒:`GET /api/admin/authorizations?start_time=2026-09-17` → 1001。
|
||||
|
||||
**(7)提现记录非法参数不再返回全量**
|
||||
|
||||
```
|
||||
GET /api/admin/commission/withdrawal-requests(缺省) total=5
|
||||
GET /api/admin/commission/withdrawal-requests?start_time=<合法区间> total=2
|
||||
GET /api/admin/commission/withdrawal-requests?start_time=2026-09-17 00:00:00 → 1001(非全量)
|
||||
GET /api/admin/commission/withdrawal-requests?start_time=2026-09-17 → 1001
|
||||
GET /api/admin/shops/{shop_id}/withdrawal-requests?start_time=2026-09-17 00:00:00 → 1001
|
||||
GET /api/admin/shops/{shop_id}/withdrawal-requests(缺省) total=3
|
||||
```
|
||||
|
||||
**(8)佣金明细:列表 vs 导出(同筛选行集一致)**
|
||||
|
||||
筛选 `start_time=2026-09-17T10:00:00+08:00`、`end_time=2026-09-17T14:00:00+08:00`;fixture 含「start−1s / ==start / start+1s / start+2h / ==end / end+1s / 8 小时漂移探针」七行原佣金 + 一行区间内回溯。
|
||||
|
||||
```
|
||||
列表 GET /api/admin/shops/{shop_id}/commission-records total=5
|
||||
命中:02:00:00、02:00:01、04:00:00、06:00:00(原佣金)+ 03:00:00(回溯)
|
||||
排除:01:59:59(start−1s)、06:00:01(end+1s)、08:00:00(8 小时漂移探针)
|
||||
导出 POST /api/admin/export-tasks{scene=commission_record} status=3 total_rows=5
|
||||
```
|
||||
|
||||
导出产物(`task 61`,UTF-8 BOM CSV):
|
||||
|
||||
```
|
||||
记录来源,记录ID,代理店铺名称,关联订单号,资产标识,佣金来源,金额(元),是否可提现,状态,回溯后佣金余额(元),原佣金记录ID,来源退款单号,佣金入账时间,生成时间
|
||||
原佣金,463,...,AUG26014ORD005,AUG26014CARD001,成本价差,10.00,,已发放,50.00,,,,2026-09-17 06:00:00
|
||||
原佣金,462,...,AUG26014ORD005,AUG26014CARD001,成本价差,10.00,,已发放,50.00,,,,2026-09-17 04:00:00
|
||||
回溯明细,9,...,AUG26014ORD005,AUG26014CARD001,成本价差,-10.00,不可提现,回溯,40.00,460,AUG26014REF001,,2026-09-17 03:00:00
|
||||
原佣金,461,...,AUG26014ORD005,AUG26014CARD001,成本价差,10.00,,已发放,50.00,,,,2026-09-17 02:00:01
|
||||
原佣金,460,...,AUG26014ORD005,AUG26014CARD001,成本价差,10.00,,已发放,50.00,,,,2026-09-17 02:00:00
|
||||
```
|
||||
|
||||
结论:**列表与导出在同一筛选下行集完全一致(5 = 5),无 8 小时量级的边界漂移**(漂移探针行在两端行为一致均被排除);负数金额原样输出(`-10.00`)、入账后余额原样输出(`40.00`),表头 14 列与 `internal/exporter/commission_record_scene.go:53-59` 逐字一致。
|
||||
|
||||
**(9)临期资产:列表 vs 导出、表头、加油包**
|
||||
|
||||
fixture:3 个资产(2 个同一最终到期时刻、1 个第 15 天窗口)+ 1 个挂主套餐的加油包。
|
||||
|
||||
```
|
||||
列表 GET /api/admin/expiring-assets total=3
|
||||
AUG26014CARD001 days=4 final=2026-09-21T16:00:00+08:00
|
||||
AUG26014CARD002 days=4 final=2026-09-21T16:00:00+08:00
|
||||
AUG26014CARD003 days=14 final=2026-10-01T16:00:00+08:00
|
||||
闭区间 start=end=2026-09-21T16:00:00+08:00 → total=2(同一时刻的两个资产都返回)
|
||||
区间外 start=end=2026-09-22T16:00:00+08:00 → total=0
|
||||
同瞬时 Z 写法 start=end=2026-09-21T08:00:00Z → total=2
|
||||
导出 POST /api/admin/expiring-assets/export{days_min:0,days_max:15} status=3 total_rows=3
|
||||
```
|
||||
|
||||
导出产物(`task 62`):
|
||||
|
||||
```
|
||||
店铺,业务员,用户组,资产类型,设备类型,设备型号,资产标识,当前套餐,到期时间,剩余天数
|
||||
AUG26014-临期验证店,<业务员账号名>,<用户组名>,物联网卡,,,AUG26014CARD001,AUG26014-临期套餐,2026-09-21 16:00:00,4
|
||||
AUG26014-临期验证店,<业务员账号名>,<用户组名>,物联网卡,,,AUG26014CARD002,AUG26014-临期套餐,2026-09-21 16:00:00,4
|
||||
AUG26014-临期验证店,<业务员账号名>,<用户组名>,物联网卡,,,AUG26014CARD003,AUG26014-临期套餐,2026-10-01 16:00:00,14
|
||||
```
|
||||
|
||||
结论:**表头 10 列与 `111.md` §18.1 逐列同序一致**;**加油包不单独成行**(4 条 `tb_package_usage` 行 → 3 条导出行);导出与列表同筛选下行集一致(3 = 3)。
|
||||
|
||||
**(10)遗留旧格式冻结任务安全失败(7.9)**
|
||||
|
||||
fixture:`tb_export_task` 插入 `status=1`、`scene=order`、`query_json={"filters":{"start_time":"2026-09-01","end_time":"2026-09-02"}}`(date-only 旧格式),经 `export:dispatch` 投递后由本地 worker 执行:
|
||||
|
||||
```
|
||||
status= 4(已失败)
|
||||
error_message= '导出筛选的时间边界非法,任务已终止' # 与 constants.ExportTaskInvalidTimeFilterMessage 逐字一致
|
||||
file_key=None total_rows=0 total_shards=0 failed_shards=0 download_url=None
|
||||
```
|
||||
|
||||
结论:**任务落失败、错误摘要为规定安全文案、未产出全量文件(无 file_key、无分片行)**。
|
||||
|
||||
**(11)达量预警回归(F2 修复后重跑)**
|
||||
|
||||
```
|
||||
列表 GET /api/admin/package-traffic-alerts 缺省 / RFC3339+08:00 / date-only / 空格分隔 → 均 HTTP 200 code=0(宽松契约不变)
|
||||
导出 POST /api/admin/export-tasks{scene=package_traffic_alert, filters={start_time,end_time 为带时区 RFC3339}}
|
||||
status=3 total_rows=0(测试库 tb_package_traffic_alert 当前 0 行)
|
||||
表头=资产类型,资产标识,对应标识符,卡标识,设备类型,设备型号,套餐名称,真流量已用量(MB),真流量额度(MB),比例(%),阈值快照(%),到期时间,剩余天数,触发时间,店铺,业务员,用户组,通知投递结果
|
||||
```
|
||||
|
||||
结论:**表头 18 列与改动前逐字一致**(`internal/exporter/package_traffic_alert_scene.go:50-56` 未变更,`git diff` 仅改时间筛选的执行期解析);记录粒度与触发快照口径未动;该场景无金额列,金额/余额口径回归由佣金明细导出覆盖。
|
||||
|
||||
**(12)创建后归属变化不扩大数据集(7.7 范围冻结)**
|
||||
|
||||
作为 §6.2 第 13 项(代理 HTTP 全链路)之外的执行期补充,本项以「按代理创建语义直接落入 `scope_shop_ids` 的冻结任务 + 真实 worker 执行」验证归属变化对已冻结任务的影响:
|
||||
|
||||
| 阶段 | 任务 | 冻结 scope | 资产位置 | 结果 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | `AUG26014-SCOPE-1`(id 65) | `[1,1527]` | 在 1527 | `total_rows=3` |
|
||||
| 2 | `AUG26014-SCOPE-1`(id 66,重置重跑) | `[1,1527]` | 已移出至 shop 2 | `total_rows=0` |
|
||||
| 对照 | 超管无范围任务(id 67) | 空(不限制) | 已移出至 shop 2 | `total_rows=3` |
|
||||
|
||||
结论:**执行期只按创建时冻结的 `scope_shop_ids` 取数;资产归属变化后行集缩小而非扩大**;对照组证明归属变更确实生效(超管看到 3 行)。
|
||||
|
||||
**(13)代理账号经 HTTP 全链路(第二批补充;真实 `POST /api/auth/login` 登录,非注入令牌)**
|
||||
|
||||
fixture:可见店铺 `1528`(`parent_id=1`)内 3 个临期资产 + 不可见店铺 `shop 2` 内 2 个临期资产(`AUG26014OUT001/002`);代理账号 `AUG26014AGENT`(`user_type=3`、`shop_id=1`)与本工具写入的 bcrypt 口令**真实登录**取得令牌。
|
||||
|
||||
```
|
||||
AGENT(有店铺) login ok user_type= 3 shop_id= 1
|
||||
AGENT(无店铺) login ok user_type= 3 shop_id= None
|
||||
```
|
||||
|
||||
1)列表只返回可见店铺:
|
||||
|
||||
```
|
||||
GET /api/admin/expiring-assets (代理令牌)http=200 code=0 total=3
|
||||
iot_card AUG26014CARD001 shop_id=1528 days=4
|
||||
iot_card AUG26014CARD002 shop_id=1528 days=4
|
||||
iot_card AUG26014CARD003 shop_id=1528 days=14
|
||||
summary={"card_count":3,"device_count":0,"total_count":3,"window_days":15}
|
||||
GET /api/admin/expiring-assets (超管对照)total=5
|
||||
identifiers=[CARD001, CARD002, OUT001, OUT002, CARD003]
|
||||
```
|
||||
|
||||
即:**代理看不到 `shop 2` 的 2 个范围外资产,超管能看到全部 5 个**(逐个资产标识与期望一致)。
|
||||
|
||||
2)代理创建并执行临期导出(行集 ⊆ 创建时冻结的可见店铺范围):
|
||||
|
||||
```
|
||||
POST /api/admin/expiring-assets/export {format:csv,days_min:0,days_max:15}
|
||||
http=200 code=0 → task_id=68 task_no=EXP-20260917-960000 status=1
|
||||
task 68 → status=3 total_rows=3(与代理列表 total=3 一致)
|
||||
库内冻结行长(dbhub 只读):
|
||||
id=68 scene=expiring_asset creator_user_id=683 creator_user_type=3 creator_shop_id=1
|
||||
scope_shop_ids=[1, 6, 1528] # 代理 1 号店的下级树快照
|
||||
query_json={"filters":{"days_max":15,"days_min":0},"resolved_headers":["店铺","业务员","用户组","资产类型","设备类型","设备型号","资产标识","当前套餐","到期时间","剩余天数"]}
|
||||
导出产物:3 行,资产标识为 CARD001/CARD002/CARD003(不含 OUT001/OUT002)
|
||||
```
|
||||
|
||||
3)空可见范围(代理无店铺)创建被拒:
|
||||
|
||||
```
|
||||
GET /api/admin/expiring-assets (shop_id=NULL 的 fixture 代理)http=200 code=0 total=0
|
||||
POST /api/admin/expiring-assets/export (同上) http=403 code=1005 msg=代理账号缺少店铺信息
|
||||
```
|
||||
|
||||
4)越权与不存在统一不可见(同一代理令牌):
|
||||
|
||||
```
|
||||
GET /api/admin/export-tasks/1 → http=403 code=1005 msg=无权限操作该资源或资源不存在
|
||||
GET /api/admin/export-tasks/999999 → http=403 code=1005 msg=无权限操作该资源或资源不存在
|
||||
一致: True
|
||||
```
|
||||
|
||||
**(14)达量预警列表 vs 导出同筛选行集一致(7.6 缺口补齐)**
|
||||
|
||||
fixture 直接写入 `tb_package_traffic_alert` 3 条快照(2 条 `triggered_at` 在 `[2026-09-01T00:00:00Z, 2026-09-30T23:59:59Z]` 内、1 条在 2026-10-05 窗外),同筛选分别调列表与导出:
|
||||
|
||||
```
|
||||
GET /api/admin/package-traffic-alerts?start_time=2026-09-01T00:00:00Z&end_time=2026-09-30T23:59:59Z total=2
|
||||
POST /api/admin/export-tasks{scene:package_traffic_alert, filters 同上} → task 69 status=3 total_rows=2
|
||||
(库内冻结行 69:scope_shop_ids=[],query_json.filters 已规范化为 UTC RFC3339)
|
||||
导出产物:
|
||||
资产类型,资产标识,对应标识符,卡标识,设备类型,设备型号,套餐名称,真流量已用量(MB),真流量额度(MB),比例(%),阈值快照(%),到期时间,剩余天数,触发时间,店铺,业务员,用户组,通知投递结果
|
||||
物联网卡,AUG26014CARD001,,,,,AUG26014-临期套餐,1500,2000,75.00,80.00,,,2026-09-20 20:00:00,AUG26014-临期验证店,<业务员账号名>,<用户组名>,待投递
|
||||
物联网卡,AUG26014CARD001,,,,,AUG26014-临期套餐,1500,2000,75.00,70.00,,,2026-09-15 08:00:00,AUG26014-临期验证店,<业务员账号名>,<用户组名>,待投递
|
||||
```
|
||||
|
||||
结论:**列表 total=2 == 导出 total_rows=2,窗外那条被两端一致排除**;触发快照列(用量/额度/比例/阈值)原样输出,未被执行期改动影响。
|
||||
|
||||
### 6.3 fixture 清单与清理结果
|
||||
|
||||
第一批(§6.2 第 1–12 项):
|
||||
|
||||
| 表 | 标记方式 | 造行数 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tb_shop` | `shop_name LIKE 'AUG26014%'` | 1 | 临期资产归属店铺(`parent_id=1`,供代理可见链路) |
|
||||
| `tb_iot_card` | `iccid LIKE 'AUG26014%'` | 3 | 临期资产 |
|
||||
| `tb_package_usage` | `package_name LIKE 'AUG26014%'` | 4(3 主套餐 + 1 加油包) | 最终到期推算 / 加油包不单独成行 |
|
||||
| `tb_order` | `order_no LIKE 'AUG26014%'` | 6 | 佣金明细关联订单号与 `(order_id,package_id)` 唯一索引避让 |
|
||||
| `tb_commission_record` | `remark LIKE 'AUG26014%'` | 7 | 佣金闭区间两端 + 1 秒内 + 4 小时内 + 8 小时漂移探针 |
|
||||
| `tb_commission_clawback_record` | `refund_no LIKE 'AUG26014%'` | 1 | 回溯分支负金额口径 |
|
||||
| `tb_enterprise_card_authorization` | `remark LIKE 'AUG26014%'` | 3 | 授权闭区间三态(等于 start / 等于 end / 区间外,整秒) |
|
||||
| `tb_commission_withdrawal_request` | `withdrawal_no LIKE 'AUG26014%'` | 3 | 提现闭区间三态(整秒) |
|
||||
| `tb_export_task` / `tb_export_shard_task` | `task_no LIKE 'AUG26014%'` | 遗留任务 1 + 冻结范围任务 1 | 7.9 遗留安全、范围冻结 |
|
||||
|
||||
第二批(§6.2 第 13–14 项):
|
||||
|
||||
| 表 | 标记方式 | 造行数 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| `tb_iot_card`(范围外) | `iccid LIKE 'AUG26014OUT%'`(`shop_id=2`) | 2 | 代理可见范围对照(不得出现在代理列表/导出) |
|
||||
| `tb_order`(范围外) | `order_no LIKE 'AUG26014OUTORD%'` | 2 | 同上(订单与套餐使用的外键上下文) |
|
||||
| `tb_package_usage`(范围外) | `package_name LIKE 'AUG26014-范围外套餐'` | 2 | 同上 |
|
||||
| `tb_account` | `username LIKE 'AUG26014%'` | 2 | fixture 代理账号:`AUG26014AGENT`(`shop_id=1`)、`AUG26014AGENTNOSHOP`(`shop_id=NULL`);真实登录用,口令 bcrypt 由工具写入且不回显 |
|
||||
| `tb_package_traffic_alert` | `notification_event_id LIKE 'AUG26014%'` | 3(2 条窗内 + 1 条窗外) | 达量预警列表与导出同筛选行集一致 |
|
||||
|
||||
**清理结果**(`cleanup` 按标记删除 + `cleanup-tasks` 删除本次经 HTTP 创建的无标记导出任务):
|
||||
|
||||
```
|
||||
CLEANUP_DONE
|
||||
COUNT tb_shop=0 COUNT tb_iot_card=0 COUNT tb_package_usage=0
|
||||
COUNT tb_order=0 COUNT tb_account=0 COUNT tb_package_traffic_alert=0
|
||||
COUNT tb_export_task=0(标记任务) COUNT tb_export_shard_task=0(标记分片)
|
||||
DELETED_TASKS=6 ids=58,59,61,62,64,67 # 第一批
|
||||
DELETED_TASKS=2 ids=68,69 # 第二批(代理导出、达量预警导出)
|
||||
```
|
||||
|
||||
清理后库内基线回到验证前状态(dbhub 只读复核):
|
||||
|
||||
```
|
||||
tb_export_task = 52 tb_export_shard_task = 48 id > 52 的导出任务 = 0
|
||||
AUG26014 标记:tb_shop / tb_iot_card / tb_package_usage / tb_order / tb_account /
|
||||
tb_package_traffic_alert / tb_export_task 计数全部 = 0
|
||||
```
|
||||
|
||||
### 6.4 证据与代码版本的对应
|
||||
|
||||
| 证据批次 | 时间 | HEAD / 指纹 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 预演(T1/T2/T3/T7 首轮) | 17:26–17:37 | `d52be16` / 修复前二进制 | 仅作预演记录;本文件引用的有效证据全部为修复后批次 |
|
||||
| 第一批(§6.2 第 1–12 项) | 17:39–17:47 | `d52be16` / `51c619f8…` | T7 实现尚未提交;当时 HEAD 尚不含维护者的路由门禁修复,故代理 HTTP 项为 403/1005 |
|
||||
| 第二批(§6.2 第 13–14 项、§6.1 第 15–17 行) | 18:0x–18:1x | `e8ab1f47`(含 `398a5e42`)/ `37d7d690…` | 仅多出维护者对 `internal/routes/{package_traffic_alert,asset_auto_renewal}.go` 的门禁作用域修复提交;T7 实现内容与第一批一致 |
|
||||
|
||||
### 6.5 未验证项(环境限制)
|
||||
|
||||
1. **「代理可见店铺为空列表」这一极端分支**:未单独构造。`GetSubordinateShopIDs` 恒包含账号自身店铺(`internal/store/postgres/shop_store.go:162-192` 以 `ids := []uint{shopID}` 起始),因此「非零 shop_id 且可见项为空」在正常数据下不可达;已用 `shop_id=NULL` 的 fixture 代理账号覆盖最近的等价分支(创建导出被拒 `403 / 1005 代理账号缺少店铺信息`,见 §6.2 第 13 项第 3 段)。
|
||||
2. **达量预警「真实扫描链路」产生的行级口径**:本批以 fixture 直接写入 `tb_package_traffic_alert` 快照行,验证了列表与导出同筛选行集一致(2=2)与快照列原样输出;未跑真实的每日扫描任务(需有效套餐 + 真流量累计 + 通知链路,代价高且与本 Change 无关)。
|
||||
3. **`tb_export_task` 中「旧格式冻结值」的历史存量**:清理前全库实测 0 条(`count(*) WHERE query_json::text ~ '[0-9]{4}-[0-9]{2}-[0-9]{2}[ T]'` = 0),7.9 因此用自建 fixture 覆盖。
|
||||
4. **迁移 `up/down/up`**:本变更无 Schema 变更(§5),按设计为「不适用」。
|
||||
|
||||
### 6.6 既有缺陷 D1:路由门禁作用域(已定位,已由维护者提交 `398a5e42` 修复)
|
||||
|
||||
**现象(第一批,HEAD 尚为 `d52be16`、不含修复)**:代理账号访问 `/api/admin/expiring-assets` 与新增的 `/api/admin/expiring-assets/export` 一律返回 403 / `1005 无权限操作该资源或资源不存在`,与路由注释「后台和代理共用」及路由描述「企业账号禁止调用」矛盾。原始输出:
|
||||
|
||||
```
|
||||
$ curl -s -H "Authorization: Bearer <agent-token>" 'http://127.0.0.1:3000/api/admin/expiring-assets?page=1&page_size=1'
|
||||
{"code":1005,"data":null,"msg":"无权限操作该资源或资源不存在"}
|
||||
|
||||
$ curl -s -X POST -H "Authorization: Bearer <agent-token>" -H 'Content-Type: application/json' \
|
||||
-d '{"format":"csv"}' 'http://127.0.0.1:3000/api/admin/expiring-assets/export'
|
||||
{"code":1005,"data":null,"msg":"无权限操作该资源或资源不存在"}
|
||||
```
|
||||
|
||||
定位过程中的门禁键隔离实验(**注入令牌的 `user_type` 由验证者显式写入 Redis 值决定**,非账号真实类型):同一 `user_id=144` 以 `user_type=2` 注入 → 200,以 `user_type=3` 注入 → 1005;`user_id=148`(真实代理)以 `user_type=3` 注入 → 1005;平台 `user_id=149`(**平台账号,`user_type=2`,非代理**)以 `user_type=2` 注入 → 200。旁证:`user_type=3` 的令牌访问任一不存在的 `/api/admin/*` 路径同样 1005(超管/平台为 `1006 资源未找到`),说明拦截发生在其后的路由之前。
|
||||
|
||||
**根因(已由维护者提交证实)**:`internal/routes/package_traffic_alert.go` 与 `internal/routes/asset_auto_renewal.go` 原以 `router.Group("", gate)` 在管理端根组注册「仅超管/平台」门禁。Fiber 的 `Group(prefix, handlers...)` 会把该处理器注册为 `path=/api/admin` 的 USE 处理器并匹配**整个 `/api/admin` 前缀**,且按注册顺序先于其后注册的路由执行,因此 `internal/routes/admin.go` 中位于 `:107`(package-traffic-alert)与 `:110`(asset-auto-renewal)之后注册的全部后台接口(含 `:125` admin exchanges、`:149` `/assets/*`、`:150` `/expiring-assets` 与新增 `/expiring-assets/export`、`:165` agent-recharges,以及 `:122` admin orders)对代理/企业账号一律 403-1005;早于该点的 `:36/:52/:67/:71/:74/:83/:89/:101` 不受影响——与实测 200/1005 的分界完全吻合。
|
||||
|
||||
**修复(维护者提交 `398a5e42`,2026-09-17 17:49:33,不在本 Change 改动内)**:`git show 398a5e42` 将两处门禁改为挂在各自功能前缀组上(`asset_auto_renewal.go` → `router.Group("/asset-auto-renewal-config", …)`;`package_traffic_alert.go` → `router.Group("/package-traffic-alert-rules", gate)` 与 `router.Group("/package-traffic-alerts", gate)`),并在注释中写明「gate 必须挂在功能路径组上:Fiber 的组中间件按路径前缀生效,挂在空路径组上会落到 /api/admin 前缀」。该修复与本文件定位的机制一致。
|
||||
|
||||
**修复后复验(第二批,HEAD=`e8ab1f47`)**:代理账号经真实登录访问同一端点已可达,列表只返回可见店铺(3 行,不含范围外 2 个资产),导出创建并执行为 3 行,详见 §6.2 第 13 项;本 Change 的代理视角验收因此在本批全部通过。
|
||||
|
||||
**归类**:既有实现缺陷(**非本 Change 引入**),**已由维护者在 HEAD 修复**;本 Change 无需改动,亦未被该缺陷遗留影响。验证的第一批 2 个 FAIL(`§6.1` 第 15 行)为修复前版本的照实记录,已保留。
|
||||
|
||||
|
||||
## 7. 实现期发现(需 director 决策,未在本次改动)
|
||||
|
||||
`tb_export_task`、`tb_commission_record`、`tb_commission_withdrawal_request`、`tb_agent_recharge_record`、`tb_enterprise_card_authorization` 等列的时间列为 PostgreSQL `timestamp without time zone`,其中保存的是应用本地时区(部署容器 `TZ=Asia/Shanghai`)的**挂钟时间**;`tb_order`、`tb_exchange_order`、`tb_asset_allocation_record` 为 `timestamp with time zone`。测试库实测(dbhub 只读查询):
|
||||
|
||||
```sql
|
||||
SELECT o.created_at AS order_created_tz, c.created_at AS commission_created_naive, (c.created_at - o.created_at) AS delta
|
||||
FROM tb_commission_record c JOIN tb_order o ON o.id = c.order_id ORDER BY c.id DESC LIMIT 1;
|
||||
-- 2026-08-25 08:08:15.00218+00 | 2026-08-25 16:08:20.158216 | {"hours": 8, "seconds": 5, ...}
|
||||
```
|
||||
|
||||
即同一笔业务事实在无时区列上的挂钟时间比带时区列的快照早 8 小时。驱动在绑定 `time.Time` 到无时区列时会丢弃时区(仅保留挂钟字段),因此统一按「归一后的 UTC 瞬时」绑定后,无时区列上的有效边界为该瞬时的 UTC 挂钟值。
|
||||
|
||||
本次实现按 spec「解析结果 MUST 归一为 UTC 瞬时」与唯一合规先例(`internal/exporter/package_traffic_alert_scene.go` 的 `startTime.UTC()`)原样绑定,未引入额外的时区换算;列表与导出使用同一解析结果,因此同筛选下行集一致。无时区列上的「用户本地整日区间」与「UTC 挂钟边界」之间的差值属本变更范围外的既有事实。
|
||||
|
||||
director 决定:
|
||||
|
||||
- 该事实**已登记**为本变更产物的已知差异,见 [`../../openspec/changes/archive/2026-09-17-add-export-time-filter-standards/design.md`](../../openspec/changes/archive/2026-09-17-add-export-time-filter-standards/design.md) 第 6 条「无时区列的时间锚定(本次不改)」。
|
||||
- 本次**不改**:统一按 spec 的「归一为 UTC 瞬时」绑定,列表与导出同向同量、有效边界一致,已由本文件 §6.2 第 8 项(含 8 小时漂移探针)实测确认。
|
||||
- 若产品期望「按真实发生瞬时的上海自然日」筛选,属**另立 Change** 的范围(涉及 8 张无时区列的口径与历史数据)。
|
||||
|
||||
## 8. 相关文件
|
||||
|
||||
- 变更产物:[`../../openspec/changes/archive/2026-09-17-add-export-time-filter-standards/`](../../openspec/changes/archive/2026-09-17-add-export-time-filter-standards/)
|
||||
- 行为契约:[`../../openspec/specs/export-time-filter/spec.md`](../../openspec/specs/export-time-filter/spec.md)
|
||||
- 上下文证据:[`./context-reset/requirement-evidence.json`](./context-reset/requirement-evidence.json)、[`./context-reset/entry-capability-requirement-matrix.json`](./context-reset/entry-capability-requirement-matrix.json)
|
||||
- 工程门禁:[`../engineering/工程约束.md`](../engineering/工程约束.md)
|
||||
- 同类体例参考:[`./add-priority-polling-queue-verification.md`](./add-priority-polling-queue-verification.md)
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user