feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-18
|
||||
@@ -0,0 +1,186 @@
|
||||
## Context
|
||||
|
||||
本变更涉及 12 个既有能力的行为契约补齐,全部落在既有模块边界内,不引入新模块、新角色或新第三方依赖。影响面与约束:
|
||||
|
||||
- **代码事实**(详见各 change 归档与主 Spec):员工账单列表请求当前只有 `source_type/source_no/status/debtor_account_id/customer_id/created_from/created_to`(`internal/model/dto/employee_collection_dto.go:57-67`);商户池响应当前无成员计数、当前命中成员与 `updated_at`(`internal/model/dto/payment_merchant_dto.go:82-97`);运营弹窗配置无类型字段(`internal/model/dto/h5_popup_dto.go:8-31`);退款创建请求的 `refund_reason` 为 `omitempty` 且无备注字段(`internal/model/dto/refund_dto.go:4-30`),退款审批材料只提交套餐使用记录 ID(`internal/application/wecom/scene.go:325-338`、`internal/application/refundapproval/creation.go:548-566`);优先轮询事实表无资产、设备号、代理、下一执行时间、出队时间列(`migrations/000228_add_polling_priority_queue.up.sql`);通道阈值锁表无命中用量与阈值列(`migrations/000227_add_carrier_traffic_threshold.up.sql`);佣金明细导出表头 14 列(`internal/exporter/commission_record_scene.go:44-49`);报表导出表头首列为分组列(`internal/exporter/operations_report_scene.go:89-91`);验证码校验无失败计数(`internal/service/verification/service.go:130-160`)。
|
||||
- **文档冲突**:主 Spec「商户池唯一性与轮询配置」的文字(全部成员达标即失败)与代码在 `每轮累计` 周期下的行为(自动开启新一轮)不一致;`111.md` §6.4(单成员池固定使用)与讨论稿 §2.6 第 6 条(全达标即失败)在单成员场景下互相张力。本变更按 111.md 收口并显式写入 Spec。
|
||||
- **契约冲突**:`openspec/specs/export-time-filter/spec.md` 的受影响端点表把「员工代收款账单」等端点列为「本期 MUST NOT 新增时间参数、MUST NOT 改变参数名与格式」,而 PRD §2.16 要求所有需要时间筛选的页面统一使用 `start_time`/`end_time` 与带时区 RFC3339 秒级闭区间。本变更新增时间筛选的三处端点(员工账单列表、优先轮询项查询、通道阈值命中查询)与该表述冲突,因此新增一份 `export-time-filter` delta 把这些端点改为已纳入契约,并明确员工账单既有「产生时间」参数同批替换;全变更 MUST NOT 复制第二套时间解析。
|
||||
- **工程约束**:新 Schema 变化必须成对新增迁移,不修改既有迁移;自动化测试当前为 N/A,验证在维护者指定的测试环境(`junhong_cmp_test` 与测试 Redis,ENG-TEST-001)运行,配合 smoke 与 OpenAPI 生成物;前端不在本仓库,页面渲染类需求只落到后端契约。
|
||||
- **上线前置**:企业微信审批模板控件映射由维护者在生产按既有场景接口配置,变更不得写入模板。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- 把 12 组字段级与接口级缺口固化为 Spec 级可验证行为,并让代码与 Spec 文字在商户池轮询语义上重新一致。
|
||||
- 新增字段一律以「只读投影优先」实现:能用既有事实派生就不新增写入路径。
|
||||
- 新增迁移只做加列、重建受新增状态影响的约束与回填默认值,保持旧数据可读、旧行为可回滚。
|
||||
- 新增时间筛选全部复用既有共享严格解析器(`pkg/utils.ParseTimeRange`)与闭区间语义,并消除既有契约中与本变更冲突的「不得新增时间参数」表述。
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- 不新增能力目录、不新增命名角色(含「运营管理员」「财务」),不改既有权限模型与数据范围下推方式。
|
||||
- 不实现优先轮询 P0/P1/P2 分级与优先级配置、不恢复「普通轮询异常补偿」场景、不做换货佣金回溯、不实现自动续费「有余额不停机」、不实现按导入批次配置绑定要求。
|
||||
- 不新增人类可读的账单编号列(账单编号即账单主键标识;若业务需要可读编号,属独立需求)。
|
||||
- 不引入前端页面、图表渲染、模板下载或二维码渲染。
|
||||
- 不为任何新增字段引入运行时开关或对外可配项。
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1 员工账单:筛选与投影走读侧扩展,不改写入路径
|
||||
|
||||
列表新增筛选(账单编号、核销通过时间、员工姓名或账号与客户名称模糊)与详情新增投影(操作日志、来源订单快照)全部在 `internal/query/employeecollection` 内实现:模糊匹配用 `ILIKE` 子串并走既有索引前缀;核销通过时间取该账单经「状态为已通过(`status = 1`)的分摊」关联到的申请表审批终态时间(`tb_employee_collection_application.decided_at`),以相关子查询表达——分摊表没有通过时间列,且其 `released_at` 在既有实现中同时承担「审批通过转已核销」与「驳回或释放预占」两种语义,MUST NOT 作为核销通过时间使用;操作日志复用既有审计事实投影(按 `employee_collection_bill` 资源读取既有审计查询),来源订单快照按来源类型分别读取订单/充值记录既有列。
|
||||
「账单编号」即账单主键标识:列表、详情、关闭与筛选一律以该标识定位,本变更 MUST NOT 新增编号列;若业务需要人类可读编号,登记为后续独立候选(需另定编号规则与历史回填,`111.md` 未定义格式)。
|
||||
理由:这些是读侧缺口,新增写入会引入重复事实。
|
||||
备选:在账单表冗余姓名与客户名称列 —— 数据会随账号改名漂移,放弃;新增人类可读账单编号列 —— 需另定编号规则与历史回填且超出本变更范围,放弃。
|
||||
|
||||
### D2 未核销金额与剩余可核销金额并列返回
|
||||
|
||||
`未核销金额 = 应收 − 已核销`,`剩余可核销金额 = 应收 − 已核销 − 审批中预占`,两者同时返回且已关闭账单均为零。
|
||||
理由:原文的口径是前者,服务端校验用的是后者;并列返回消除歧义,且不改变既有预占逻辑。
|
||||
备选:只改前者语义 —— 会破坏并发核销防超额校验,放弃。
|
||||
|
||||
### D3 商户池「全部成员达标」按统计周期分别定义
|
||||
|
||||
`每轮累计`:视为当轮结束,以保存成功时间为新起点开启新一轮并从零累计;`自然日累计`/`自然月累计`:拒绝创建并提示当前周期暂无可用商户。
|
||||
统计分子/分母:金额与笔数累计只统计 `tb_payment_merchant_routing_success` 中支付所属池与当前统计世代匹配的记录,自然日/自然月方式再加支付时间不早于当期窗口起点;首次成功按支付 ID 唯一一次性计入创建时就冻结的商户与统计世代。
|
||||
并发不变量:`每轮累计` 开启新一轮 MUST 以条件更新递增统计世代(`WHERE id = ? AND routing_epoch = ?`),仅在当前世代未被并发改变时成功;更新冲突 MUST 按并发冲突拒绝创建本次支付,MUST NOT 在旧世代上重复累计或静默沿用旧世代。
|
||||
理由:这正是「每轮累计在商户重新被轮到时清零」的语义,也是 `round` 与自然周期的唯一可区分点;Spec 原文字面把所有周期一律写成失败,属文档过宽。
|
||||
备选:让 `round` 也返回失败 —— 会使 `round` 与自然周期不可区分,且轮次永远无法推进,放弃。
|
||||
|
||||
### D4 单成员池固定使用该成员
|
||||
|
||||
池内只有一个启用成员时,该成员达到阈值后仍被选中,且不因「全部成员达标」拒绝创建。
|
||||
理由:`111.md` §6.4 明确该行为;上线迁移按讨论稿 §2.6 第 7 条会为每种支付方式创建「包含一个商户的启用商户池」,若达标即拒付,单商户池在自然日/自然月周期内将当天无法再收款,业务不可接受。
|
||||
备选:保持自然周期拒付 —— 需要运营把统计周期改为 `round` 才能用,隐性依赖过强,放弃。
|
||||
影响:该规则优先于 D3 的自然周期拒绝语义,Spec 中已写明优先级。
|
||||
|
||||
### D5 时间轮询起点由服务端在保存成功时重算
|
||||
|
||||
修改周期数值/单位、起始时间或成员顺序并保存成功时,服务端 MUST 以保存成功时间覆盖起点,忽略请求携带的旧起点;创建时按请求起点,请求缺省时取保存时间。
|
||||
理由:原文要求「以保存成功时间为新起点」,把重算放在服务端可避免前端时钟与漏传导致的时段漂移。
|
||||
备选:继续信任请求值 —— 前端漏传即产生错误轮转,放弃。
|
||||
|
||||
### D6 商户池列表的「当前命中成员」只读计算
|
||||
|
||||
复用 `chooseMerchant`/`chooseTimedMerchant` 的判定抽成只读函数:金额/笔数取顺序中第一个未达标成员,`round` 全达标取首位,时间方式按当前时段;MUST NOT 推进世代或创建支付单。既有实现依赖事务并会推进世代,不可直接复用为读侧,因此只读函数独立抽取并在列表内按页批量计算。
|
||||
理由:避免为列表展示引入第二套判定,也避免查询产生副作用。
|
||||
备选:列表不展示当前成员 —— 与 111.md §6.2 冲突,放弃。
|
||||
|
||||
### D7 H5 运营弹窗类型决定优先级类别
|
||||
|
||||
新增必填 `popup_type`(`promotion`/`announcement`),排序先按类别(风险换卡 > 推广 > 公告)再按显式优先级,同优先级取最近更新时间最新;既有配置迁移为 `announcement`。
|
||||
类别顺序 MUST 表达为显式的类别排序表达式,MUST NOT 依赖类型取值的字典序:`announcement` 的字典序小于 `promotion`,直接按列排序会把公告排在推广之前,与 `111.md` §13.3 相反。类别序稳定后无需新增排序冗余列,既有匹配索引继续服务过滤与时间窗口筛选(本表为后台维护的小型配置表)。
|
||||
理由:原文要求类型与分级;把类别作为第一排序键后,显式优先级仍可表达同类内次序,既有配置的相对顺序不变。
|
||||
备选:仅加类型字段不参与排序 —— 无法满足 §13.3 的优先级要求,放弃。
|
||||
|
||||
### D8 退款原因必填与申请人备注
|
||||
|
||||
`refund_reason` 在创建与重提时必填(去空白后非空),校验放在应用层的创建与重提用例,保证内部调用同样受约束。
|
||||
校验位置边界:MUST NOT 把该校验放进审批尝试构造与补发历史审批的路径(`refundapproval` 的 `Execute` 与 `TriggerHistorical` 会为历史与补发申请建立首条尝试);历史申请与补发历史审批不受本约束、不追溯。
|
||||
理由:原文与讨论稿 §2.3 第 15 条均要求必填,仅靠 DTO 校验会漏掉内部路径;但把校验下沉到尝试构造会阻断历史申请的补发审批。
|
||||
|
||||
### D9 退款渠道标识落库而非读取时推导
|
||||
|
||||
退款单新增 `source_payment_no` 与 `original_channel_trade_no`(冻结原成功支付事实)与 `offline_settlement_no`(财务补录,写审计)。列表/详情/导出直接返回冻结值。
|
||||
申请人备注与渠道标识一样逐次冻结,但备注落在**审批尝试表**(每次提交或重提一条,历史尝试不被改写),MUST NOT 复用或改写退款主表既有 `remark`(该列语义为审批备注,已被既有实现占用,本变更不改其语义与展示口径)。
|
||||
理由:原文要求退款单可追溯原支付与退款流水;读取时跨三表推导无法表达冻结语义,也不满足对账需要的稳定性。
|
||||
备选:只在响应中联表返回 —— 支付单后续被改挂或数据缺失时结果漂移,放弃。
|
||||
|
||||
### D10 可选退款方式走独立只读接口并复用同一判定
|
||||
|
||||
新增按订单查询可选方式与原因的接口,内部调用与创建/提交/执行相同的判定函数;判定事实缺失返回不可用原因而非报错。
|
||||
「同一判定实现」落点:方式集合、可用性与不可用原因来自既有的方式判定实现(创建与重提已在调用);原路可退的凭证判定来自既有的凭证判定单一来源(企业微信通过后执行前的预检已在调用)。只读接口 MUST 复用这两个实现,MUST NOT 引入第二套判定或独立开关。提交环节只消费已冻结材料,不新增判定。
|
||||
接口落点:`GET /api/admin/refunds/order-options`;字面量路径 MUST 注册在同组参数路由(`/:id`)之前;权限沿用退款组门禁(超级管理员/平台/代理,企业账号拒绝)并按订单数据范围下推,越权与订单不存在不可区分。
|
||||
理由:原文 §12.6 第 1 条要求页面可按订单类型展示;复用同一判定避免出现两套口径。
|
||||
备选:由前端硬编码矩阵 —— 与渠道凭证状态相关,前端无法判断,放弃。
|
||||
|
||||
### D11 退款审批材料带出用量与资产维度
|
||||
|
||||
在退款审批材料中增加资产类型、设备类型、设备型号、套餐已用量/总量与原支付渠道交易流水号,取值复用「退款展示当前退款套餐用量」的既有解析规则,并冻结进审批尝试快照;控件缺失时明确失败而非静默丢弃。
|
||||
取值来源:套餐已用量与总量复用既有展示解析规则与真流量口径;资产类型沿用退款请求的下单快照推导;设备类型与设备型号取退款请求关联设备(资产为设备时),无关联设备时以空值提交;原支付渠道交易流水号取创建申请时冻结的原成功支付事实。
|
||||
控件强制范围:仅对本变更新增字段强制映射——场景白名单为新增字段标记为必须映射,场景映射校验在缺失映射时明确失败,表单构建在快照缺值时明确失败并提示缺失控件;既有未映射的可选控件保持既有静默跳过行为,MUST NOT 推广为全场景强制映射。
|
||||
理由:审批人需要在企业微信侧看到用量数字,静默丢弃会造成审批材料与本地不一致;但把全部白名单字段改为强制映射会误伤既有场景按模板能力裁剪的可选字段。
|
||||
备选:仅保留套餐使用记录 ID —— 正是本次要修的缺口,放弃。
|
||||
|
||||
### D12 优先轮询补齐字段、出口与详情投影
|
||||
|
||||
- 事实表加列:资产类型/资产 ID、设备号快照、代理归属、任务类型(既有)、尝试上限(常量回填)、执行开始/结束时间、出队时间、下次执行时间(可空)、集成交互日志标识、有效期起算时间。
|
||||
- 迁移边界:状态 CHECK 必须随新增的「已关闭」「已过期」两个终态重建(既有约束只允许待执行、执行中、已完成、失败出队四个取值);活动项部分唯一索引谓词保持 `status IN ('pending','processing')` 不变,新终态不占键位;为下次计划执行时间建部分索引以支撑到期扫描与筛选。
|
||||
- 有效期起算:新增有效期起算时间列,up 脚本把既有活动项回填为迁移时刻,使既有未完成项不会在上线后被首轮扫描批量过期;新增行取入队时间。有效期长度与尝试上限均为代码常量,不新增可维护配置项。
|
||||
- 到期处理承载:新增独立周期任务类型承载有效期到期扫描与出队,按既有周期任务注册形态注册到任务处理器与 Worker 调度;MUST NOT 依赖查询或读侧隐式触发。
|
||||
- 读侧兼容:既有行的下次执行时间为空时,读侧 MUST NOT 据此判定已过期。
|
||||
- 新增人工关闭(原因必填,写审计)与有效期(固定常量,超期转已过期终态)两个出口;失败与已过期项支持人工重触发,复用人工入队入口并新建活动项(尝试次数归零)。
|
||||
- 读侧补齐字段与「异常与重试」筛选视图(复用同一事实表,不建独立异常表);优先轮询状态投影落在既有资产解析端点(`GET /api/admin/assets/resolve/:identifier`)的卡与设备两个分支,MUST NOT 为投影新增独立端点(新增 `asset-device` 要求)。
|
||||
理由:§23.4/§23.6/§23.7/§23.9/§23.10 的字段与出口是运营排障的前提;有效期与人工关闭使失败项可被清理。本次显式取代先前 change「MUST NOT 引入有效期字段」的裁剪。
|
||||
备选:维持原样只做文档登记 —— 用户明确要求补齐,放弃。
|
||||
|
||||
### D13 通道阈值命中事实留痕
|
||||
|
||||
在既有锁表加列或新建命中事实表记录:累计流量读数、阈值与单位、周期起点、判定时间、触发来源;周期恢复记录解锁时间与复机结果。
|
||||
查询落点与权限:命中记录查询使用不与通道详情参数路由冲突的顶层路径,自带超级管理员与平台用户门禁并按卡数据范围下推;越权与不存在不可区分,不返回凭证或上游响应原文。
|
||||
留痕失败边界:留痕与停机事实在同一事务内写入;留痕写入失败 MUST NOT 放行或延迟停机,停机事实仍为主结果。
|
||||
历史行兼容:判定时间回填既有锁行的创建时间,命中数值与阈值可空(历史读数不可重建)。
|
||||
理由:运维需要数字证据解释停机;把留痕放在同一事务内保证与停机事实一致。
|
||||
备选:仅写运行日志 —— 日志非查询事实且随留存清理消失,放弃。
|
||||
偏好:加列到既有锁表,锁表本身即「一次命中一条」,避免新表与锁表 1:1 冗余。
|
||||
|
||||
### D14 手机号关联数量与验证码失败限制
|
||||
|
||||
关联查询按当页手机号集合一次聚合有效关系数并返回最近解绑人(后台账号名称快照,按既有上限判定的同一计数口径);验证码校验在 Redis 以手机号维度原子累加失败计数并在达到阈值后短时锁定,成功后清零。
|
||||
验证码失败限制细节:计数在手机号维度原子累加(并发不丢次数);锁定检查在验证码比对之前,锁定期内即使提交正确验证码也不通过;窗口内校验成功清零;阈值与窗口为固定常量,不新增对外可配置项;错误码使用既有请求过多错误码,提示不泄露验证码正确性、剩余次数或内部键名。计数写入失败时放行并记录,MUST NOT 因限流计数故障阻断注册、绑定、换绑、换证与登录的成功路径;校验失败不消费验证码、发送侧频率限制与验证码有效期语义保持不变。
|
||||
归属:该要求挂在 `phone-asset-association`(来源为 `111.md` §15 手机号绑定章节),作用域覆盖注册、绑定、换绑与换证的全部短信验证码校验流程。
|
||||
理由:数量口径必须与「最多十项」判定一致,复用同一计数函数可避免漂移;失败限制是安全基线,原子计数避免并发绕过。
|
||||
|
||||
### D15 报表导出与佣金导出列
|
||||
|
||||
报表导出首列固定为「序号」(分组行按展示顺序连续递增,合计行序号列留空且不改合计行其他列的既有表示);佣金明细导出按下述列序补齐店铺、业务员、用户组、资产类型、设备类型、设备型号、资产标识、订单号、下单时间、佣金金额、佣金来源、佣金状态、关联佣金明细、入账后金额、创建时间。归属列按导出执行时当前归属补充,缺失列以空值导出而不阻断;回溯记录金额保持负数、可提现标识保持不可提现。
|
||||
理由:原文列出这两类导出的完整列清单;导出必须与页面口径一致且不因个别行缺值整批失败。
|
||||
|
||||
### D16 不新增「运营管理员」命名角色
|
||||
|
||||
本次只登记口径,不引入角色:运营类动作继续由超级管理员与平台用户在其数据范围内承担,店铺分类仍由业务用户组推导。
|
||||
理由:新增角色会牵动权限体系与全部路由鉴权,远超原文「角色与权限设计」表格的表达意图,且与既有「本期 MUST NOT 引入财务角色」的裁剪方向一致。
|
||||
备选:新增角色并接入 `RequirePermission` —— 本轮不引入鉴权体系改造,放弃。
|
||||
|
||||
### D17 新增时间筛选统一纳入既有契约
|
||||
|
||||
三处新增时间筛选(员工账单「核销通过时间」、优先轮询项查询、通道阈值命中查询)MUST 复用既有共享严格解析器(`pkg/utils.ParseTimeRange`)与同一闭区间语义:带显式时区的 RFC3339 秒级、归一为 UTC 瞬时、含两端、开始晚于结束拒绝、未传与空串等价;MUST NOT 复制既有账单日期解析或客户端可选时间解析等第二套实现。
|
||||
参数命名:主时间字段使用统一参数名 `start_time`/`end_time`;同一端点存在第二个时间字段时使用「业务语义 + 时间后缀」的固定命名——员工账单核销通过时间为 `decided_start_time`/`decided_end_time`,与主字段同一解析器、同一闭区间。
|
||||
员工账单既有「产生时间」参数(日期形态)同批替换为统一参数名并改用严格解析:旧参数名与旧格式 MUST 被拒绝,MUST NOT 保留兼容或别名。
|
||||
契约侧同步:新增 `export-time-filter` delta,把员工账单列表、优先轮询项查询、通道阈值命中查询从「本期不得新增时间参数」的排除列表改为受影响端点表内条目,并在表中写明各自筛选的时间字段(员工账单产生时间与核销通过时间、优先轮询入队时间、阈值命中判定时间)。
|
||||
理由:PRD §2.16 已确立统一时间筛选基线;同一仓库并存两套时间解析会让拒绝集与边界语义漂移,也使新增端点的行为无法被单一契约验证。
|
||||
备选:新增端点为回避契约而沿用各区自行解析 —— 与 PRD §2.16 及既有主 Spec 的严格解析方向相悖,放弃。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [新增模糊筛选拖慢员工账单列表] → 使用子串匹配并限制输入长度上限,筛选列复用既有索引;如后续数据量放大再引入检索列。
|
||||
- [商户池单成员固定使用可能与运营预期(周期内限流)冲突] → Spec 已写明优先级;运营如需周期内限流,应改为多成员池。
|
||||
- [退款原因必填会阻断历史习惯的内部调用] → 有意的破坏性收敛;上线说明与接口文档同步更新,错误文案明确指向缺失字段。
|
||||
- [优先轮询新增有效期与人工关闭引入新状态] → 新状态只影响读侧投影与出队,不改变调度通道、并发上限与持锁拒绝;对既有活动项按无有效期处理,避免上线瞬间批量过期。
|
||||
- [命中留痕加列使锁表变宽] → 只加定长数值与时间列,无新索引主键变更;锁定写入仍在停机同一事务内。
|
||||
- [企微模板控件未配置会导致退款/注册审批提交失败] → 明确失败并提示缺失控件,避免静默丢弃造成数据不一致;上线前置项写入任务。
|
||||
- [既有运营弹窗配置迁移为「通用公告」会改变相对排序] → 迁移只赋予类型,显式优先级与同类别内顺序保持;跨类别次序变化即 111.md §13.3 期望结果。
|
||||
- [报表序号列改变既有导出列序] → 属期望列定义变化;导出为异步任务,列冻结在执行期,旧任务不受影响。
|
||||
- [新列默认值与回滚] → 新列一律可空或带默认值;读侧对空值按「无」处理;既有优先轮询行的有效期起算时间非空但下次计划执行时间为空时,读侧 MUST NOT 据此判定已过期;回滚到上一版本时新列为空不影响既有读写。
|
||||
- [时间参数替换影响既有调用方] → 员工账单列表「产生时间」参数改名属有意的破坏性收敛,与第 16 项对换货、分配记录、代理充值的处理同类;旧参数名与旧格式一律拒绝并返回参数非法错误,不再静默忽略。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. **成对迁移(新文件,不改既有)**:六对,编号与内容以本文件与 `proposal.md`、`tasks.md` 第 1 节逐字一致:
|
||||
1. `000232` H5 弹窗配置加类型列(枚举两值、非空、默认通用公告)并回填既有行。
|
||||
2. `000233` 退款主表加来源支付单号、原支付渠道交易流水号、线下退款处理流水号与登记时间、登记人;退款审批尝试表加申请人备注列。
|
||||
3. `000234` 优先轮询事实表加资产类型与资产 ID、设备号快照、代理归属、尝试上限(默认 3)、执行开始与结束时间、下次计划执行时间、出队时间、集成交互日志标识、有效期起算时间;重建状态 CHECK 容纳「已关闭」「已过期」;活动项部分唯一索引谓词不变;为下次计划执行时间建部分索引;有效期起算时间回填为迁移时刻。
|
||||
4. `000235` 通道阈值锁表加命中累计流量、阈值数值、阈值单位、判定时间、触发来源、解锁时间、复机结果;判定时间回填创建时间、命中数值留空;加按通道、卡与判定时间的查询索引。
|
||||
5. `000236` 手机号关联加最近解绑人名称快照列(注释写明审计侧仍只写脱敏值)。
|
||||
6. `000237` 提现审批尝试表加资料资格版本标识、校验时间、是否通过、未通过稳定原因列。
|
||||
2. **无需 DDL**:员工账单(主键即账单编号、通过时间取自申请表)、商户池列表投影(池表已有更新时间列)、佣金明细导出与报表序号列(导出执行期 join 补齐)。
|
||||
3. **回填策略**:新列一律可空或带默认值;既有优先项有效期自迁移时刻起算,不回填过期;既有运营弹窗回填通用公告;退款原因必填只对上线后新提交与重提生效,历史申请与补发历史审批不追溯。
|
||||
4. **上线顺序**:先迁移 → 再发布 API/Worker 二进制(手工发布,见生产运行说明)→ 再由维护者配置企微模板控件映射(仅本次新增字段强制映射)→ 最后开放新字段使用。
|
||||
5. **回滚**:代码回滚到上一版本时新增列仍为空,读侧保持兼容(空值按空处理);企微控件配置回滚需同步移除新增控件映射;时间参数替换的回滚需同步前端(旧参数名不再被接受)。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 企业微信退款审批与新增代理审批模板的控件新增项,具体控件类型与必填性待维护者按模板能力确认后配置,不影响本变更的 Spec 与任务拆分。
|
||||
- 线下退款处理流水号是否需要按渠道做格式校验,待财务确认口径;当前按自由文本登记并写审计。
|
||||
- 账单若需要人类可读编号(而非主键标识),需另行提出需求并定义编号规则与历史回填方案。
|
||||
@@ -0,0 +1,55 @@
|
||||
## Why
|
||||
|
||||
2026 年 8 月迭代的 20 个需求域已完成开发并归档,但按 `111.md` 原始需求逐条比对(约 950 个判定点)后,仍有一批「原文明确要求、实现未做或只做一半、且讨论稿与 change design 均无裁剪记录」的字段级与接口级缺口。它们集中在**导出列缺失、企业微信审批材料不完整、必填校验缺失、列表与详情投影不全、优先轮询留痕不足、阈值命中无数字证据**六类,直接影响财务对账、审批判断依据、安全基线与事后追溯,因此需要一次收口变更把它们补齐并固化为可验证契约。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **员工账单**:列表补齐「账单编号」精确筛选、「核销通过时间」范围筛选,以及按员工姓名或账号、客户名称的模糊查询;明确区分「未核销金额」与「剩余可核销金额」两个口径;账单详情内嵌操作日志与来源订单快照。
|
||||
- **商户池**:列表返回启用/总商户数量、当前命中商户与最近配置更新时间;按统计周期区分「全部成员达标」语义,单成员池保持固定使用该成员;时间轮询在周期、起始时间或成员顺序变更后由服务端以保存成功时间重算起点。
|
||||
- **H5 运营弹窗**:新增必填「弹窗类型(套餐政策推广/通用公告)」配置字段,并据此确定默认优先级分级。
|
||||
- **退款**:退款原因必填;申请新增申请人备注;详情返回原收款商户、原支付渠道交易流水号与商户退款能力校验结果;新增按订单类型返回可选退款方式的查询接口;退款单落「来源支付单号」与「原支付渠道交易流水号」,并支持登记线下退款处理流水号/凭证编号;企业微信退款审批材料带出当前退款套餐已用量与总量、资产类型与设备类型(含设备型号)以及原支付渠道交易流水号。
|
||||
- **优先轮询**:补齐队列持久化与读侧字段(资产标识、设备号、代理归属、尝试次数与上限、最近与下一次执行时间、执行耗时、接口请求/响应摘要引用);支持人工关闭出队、有效期到期出队与失败后人工重触发;资产解析端点的卡与设备两个分支返回优先轮询状态;异常与重试记录按既有列表筛选可查。
|
||||
- **运营商通道阈值**:阈值命中事实持久化触发时累计流量、阈值数值、单位、判定时间与操作来源,并提供可查询投影。
|
||||
- **代理**:企业微信新增代理审批材料补齐「业务员」字段;店铺详情返回下级代理数量;提现申请记录当次生效资料资格版本与校验结果。
|
||||
- **手机号—资产关联**:后台查看返回手机号当前有效关联数量与最近解绑人;新增短信验证码校验失败次数限制(作用域覆盖注册、绑定、换绑与换证的全部短信验证码校验流程)。
|
||||
- **报表**:激活情况、套餐续费两张报表的导出补齐「序号」列(首列固定为序号,分组行按展示顺序连续递增,合计行序号列留空且不改合计行其他列的表示)。
|
||||
- **时间筛选**:本变更新增的三处时间范围筛选(员工账单核销通过时间、优先轮询项查询、通道阈值命中查询)统一纳入既有时间筛选契约——统一参数名、共享严格解析器、带时区 RFC3339 秒级闭区间;员工账单列表既有「产生时间」参数同批替换为统一参数名,不保留旧参数名与第二套解析。
|
||||
- **端点与证据链**:新增按订单查询可选退款方式、线下退款处理流水号补录、优先轮询人工关闭、优先轮询人工重触发、通道阈值命中查询五个受权限控制的入口(方法、路径与权限主体见 `tasks.md` 第 12 节),并同步主 Spec 可达操作索引与两份证据链 JSON。
|
||||
- **口径登记(无行为变化)**:不新增「运营管理员」命名角色,其职责由超级管理员与平台用户在其数据范围内承担,店铺分类口径仍由业务用户组推导。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
无新增能力:本次全部落在既有能力的行为契约上。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `employee-collection-bill`: 新增账单列表查询筛选与账单详情投影要求(含未核销金额口径)。
|
||||
- `merchant-payment-routing`: 新增商户池列表投影要求;修改金额/笔数全达标与单成员池语义、时间轮询起点重算。
|
||||
- `h5-popup-notification`: 修改运营弹窗匹配要求,加入弹窗类型与默认优先级分级。
|
||||
- `order-refund-exchange`: 修改退款方式矩阵(新增可选方式查询);新增退款原因必填与申请备注、退款展示与渠道标识字段、企业微信退款审批材料字段。
|
||||
- `phone-asset-association`: 修改后台查看要求,加入手机号维度有效关联数量与最近解绑人;新增短信验证码校验失败次数限制要求(覆盖注册、绑定、换绑与换证的全部短信验证码校验流程)。
|
||||
- `export-time-filter`: 修改受影响端点与旧格式替换要求,把员工账单列表、优先轮询项查询与通道阈值命中查询纳入统一时间筛选契约,并规定同一端点第二个时间字段的命名、解析与闭区间语义。
|
||||
- `priority-polling-queue`: 修改优先项尝试与出队要求(人工关闭、有效期到期、人工重触发);修改可追溯要求(补齐字段与异常重试查询)。
|
||||
- `asset-device`: 新增资产解析端点(卡与设备两个分支)的优先轮询状态投影要求。
|
||||
- `carrier-channel-traffic-threshold`: 新增阈值命中事实留痕要求。
|
||||
- `agent-distribution-withdrawal`: 修改注册审批材料(业务员字段)与提现资料校验结果留痕;新增店铺下级代理数量投影。
|
||||
- `agent-funds-commission`: 修改佣金明细导出列要求。
|
||||
- `operations-report`: 修改报表导出要求,加入序号列。
|
||||
|
||||
## Impact
|
||||
|
||||
- **受影响代码**:`internal/query`(员工账单、退款、手机号关联、代理店铺、优先轮询、阈值命中)、`internal/application`(employeecollection、merchantpayment、h5popup、refundapproval、refund、prioritypolling、carrierthreshold、distributionwithdrawal)、`internal/service`(verification、refund、polling 优先队列、phone_asset_association)、`internal/exporter`(agentrecharge 佣金明细、operationsreport、operationsreport/refund 场景)、`internal/routes` 与 `internal/handler/admin`。复用既有 Handler,不修改文档生成装配。
|
||||
- **受影响迁移**:新增六对成对迁移(新顺序号,不修改既有迁移),逐对与 `design.md` 的「迁移计划」及 `tasks.md` 第 1 节一致:
|
||||
1. `000232`:H5 弹窗配置加类型列(枚举两值、非空、默认通用公告),up 回填既有行为通用公告。
|
||||
2. `000233`:退款主表加来源支付单号、原支付渠道交易流水号、线下退款处理流水号与登记时间、登记人列;退款审批尝试表加申请人备注列(逐次冻结;主表备注列已被审批备注占用,不得复用或改写)。
|
||||
3. `000234`:优先轮询事实表加资产类型与资产 ID、设备号快照、代理归属、尝试上限(默认 3)、执行开始与结束时间、下次计划执行时间、出队时间、集成交互日志标识与有效期起算时间;重建状态 CHECK 以容纳「已关闭」与「已过期」两个终态(活动项部分唯一索引谓词保持不变);为下次计划执行时间建部分索引;有效期起算时间在 up 中回填为迁移时刻。
|
||||
4. `000235`:通道阈值锁表加命中累计流量、阈值数值、阈值单位、判定时间、触发来源、解锁时间与复机结果;判定时间回填创建时间、命中数值留空;新增按通道、卡与判定时间的查询索引。
|
||||
5. `000236`:手机号—资产关联加最近解绑人名称快照列,列注释写明审计侧仍只写脱敏值。
|
||||
6. `000237`:提现审批尝试表加资料资格版本标识、校验时间、是否通过、未通过稳定原因列。
|
||||
- **无需 DDL 的缺口**:员工账单(账单编号即账单主键标识,核销通过时间取自申请表既有审批终态时间)、商户池列表投影(池表已有更新时间列)、佣金明细导出列与报表序号列(导出执行期 join 补齐)。
|
||||
- **回滚兼容**:新列一律可空或带默认值,读侧对空值按「无」处理;既有优先轮询行的有效期起算时间非空但下次计划执行时间为空时,读侧 MUST NOT 据此判定已过期。
|
||||
- **受影响外部契约**:企业微信退款审批场景需新增控件(套餐已用量与总量、资产类型、设备类型与型号、原支付渠道交易流水号),代理注册审批场景需新增「业务员」控件;「控件缺失即明确失败」仅对本变更新增字段强制映射,既有未映射的可选控件保持静默跳过。提现的资格版本与校验结果冻结在本地审批尝试快照并在提现详情可查,不要求新增企业微信控件。模板控件映射仍由维护者在生产按既有场景接口配置,本变更不写入模板。
|
||||
- **非目标(沿用既有已记录裁剪)**:本期不做优先轮询 P0/P1/P2 分级与优先级配置、不恢复「普通轮询异常补偿」场景、不做换货佣金回溯、不实现自动续费「有余额不停机」、不实现按资产导入批次配置是否需要绑定手机号、不新增「财务」命名角色。
|
||||
- **上线前置**:本变更仅涉及两个需新增企微控件的场景——退款审批(套餐已用量与总量、资产类型、设备类型与型号、原支付渠道交易流水号)与代理注册审批(业务员);其控件映射需由维护者确认后再启用,其余既有场景的控件映射不受本变更影响。
|
||||
@@ -0,0 +1,112 @@
|
||||
# close-august-iteration-gaps 上线与回滚说明
|
||||
|
||||
对应 tasks.md §14.1 / §14.2 / §14.4。上线顺序与回滚口径依据 design.md「Migration Plan」与 proposal.md「Impact」。
|
||||
|
||||
## 1. 上线顺序(必须先迁移,再发二进制,再配置企微,最后开放新字段使用)
|
||||
|
||||
### 第一步:执行六对迁移(按编号顺序,不修改既有迁移)
|
||||
|
||||
| 顺序 | 迁移编号 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `000232_add_h5_popup_type` | `tb_h5_popup_configuration` 加 `popup_type`(枚举 `promotion`/`announcement`,非空,默认 `announcement`)+ 取值 CHECK;up 以默认值回填既有行 |
|
||||
| 2 | `000233_extend_refund_settlement_fields` | 退款主表加 `source_payment_no`(`varchar(64)`)、`original_channel_trade_no`(**`varchar(100)`**,与来源列 `tb_payment.third_party_trade_no` 一致)、`offline_settlement_no`(`varchar(128)`)、`offline_settled_at`、`offline_settled_by`;退款审批尝试表加 `remark`(申请人备注,逐次冻结) |
|
||||
| 3 | `000234_extend_polling_priority_item` | 优先轮询事实表加 `asset_type`、`asset_id`、`device_no_snapshot`、`agent_shop_id_snapshot`、`attempt_limit`(默认 3)、`started_at`、`finished_at`、`next_run_at`、`dequeued_at`、`integration_log_id`、`priority_effective_from`;重建状态 CHECK 容纳 `closed` 与 `expired`;活动项部分唯一索引谓词保持 `status IN ('pending','processing')` 不变;为 `next_run_at` 建部分索引;既有活动项 `priority_effective_from` 回填为迁移时刻,资产列按既有设备—卡绑定回填 |
|
||||
| 4 | `000235_extend_carrier_threshold_hit` | 阈值锁表加命中累计流量、阈值数值、阈值单位、判定时间、触发来源、解锁时间、复机结果;判定时间回填既有行创建时间(并置默认 `now()`),命中数值与阈值留空;新增按通道、卡与判定时间的查询索引 |
|
||||
| 5 | `000236_add_phone_association_invalidator_name` | 手机号—资产关联加 `invalidator_name_snapshot`(非空默认空串) |
|
||||
| 6 | `000237_extend_withdrawal_attempt_qualification` | 提现审批尝试表加资料资格版本标识、校验时间、是否通过、未通过稳定原因 |
|
||||
|
||||
执行方式:维护者按既有迁移脚本(`scripts/migrate.sh`)执行,六对全部落盘后再统一 `up`(本次测试环境即如此,避免先落盘的号被后落盘的低号跳过)。执行后用只读查询核对列/默认值/CHECK/索引(见 `verification.md` 第 2 节)。
|
||||
|
||||
### 第二步:发布 API / Worker 二进制
|
||||
|
||||
手工发布,步骤见 `docs/deployment/production-runbook.md`。发布前确认工作树包含本变更全部生产代码改动(`go build ./cmd/api ./cmd/worker` 通过)。
|
||||
|
||||
### 第三步:维护者配置企业微信控件映射(仅本变更新增字段强制映射)
|
||||
|
||||
- 退款审批场景:套餐已用量与总量、资产类型、设备类型与型号、原支付渠道交易流水号。
|
||||
- 代理注册审批场景:业务员。
|
||||
- 以 `GET /api/admin/wecom/scenes` 返回的启用记录与 `control_mapping` 作为验收证据。既有未映射的可选控件保持既有静默跳过行为。
|
||||
- 未配置前:退款/注册审批提交时对缺失的新增控件**明确失败并提示缺失控件**(不静默丢弃)。
|
||||
|
||||
### 第四步:开放新字段使用
|
||||
|
||||
新端点与新增筛选按第 3 节启用;前端按第 4 节的参数改名同步。
|
||||
|
||||
### 上线前置清单(两项,待维护者执行)
|
||||
|
||||
| 项 | 内容 | 验收证据 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 在生产配置企业微信控件映射:退款审批的套餐用量(已用量与总量)、资产类型、设备类型与型号、原渠道流水号;代理注册审批的业务员 | `GET /api/admin/wecom/scenes` 返回的启用记录与 `control_mapping` | **待维护者执行** |
|
||||
| 2 | 员工账单时间参数改名的前端同步:`created_from`/`created_to` → `start_time`/`end_time`(旧参数名与旧格式一律拒绝,无兼容别名) | 前端发布后员工账单列表与详情的时间筛选可用 | **待维护者执行** |
|
||||
|
||||
两项上线前置未完成前:新字段的企微审批材料会在提交时**明确失败并提示缺失控件**(不静默丢弃);员工账单的旧时间参数调用会被**参数非法拒绝**。
|
||||
|
||||
## 2. 回滚步骤
|
||||
|
||||
### 2.1 代码回退
|
||||
|
||||
代码回退到上一版本后,数据库停留在 237 形态,各新增列**可空或带默认值**,旧二进制不写这些列也不会违反约束:
|
||||
|
||||
| 表 | 列 | 回滚期旧代码不写时的取值 |
|
||||
| --- | --- | --- |
|
||||
| `tb_h5_popup_configuration` | `popup_type` | 默认 `announcement` |
|
||||
| `tb_refund_request` | `source_payment_no`(`varchar(64)`)/ `original_channel_trade_no`(`varchar(100)`,与来源列 `tb_payment.third_party_trade_no` 一致)/ `offline_settlement_no`(`varchar(128)`) | 默认空串 |
|
||||
| `tb_refund_request` | `offline_settled_at` | NULL(可空) |
|
||||
| `tb_refund_request` | `offline_settled_by` | 默认 `0` |
|
||||
| `tb_refund_request_attempt` | `remark` | 默认空串 |
|
||||
| `tb_polling_priority_item` | `asset_type` / `device_no_snapshot` / `integration_log_id` | 默认空串 |
|
||||
| `tb_polling_priority_item` | `asset_id` / `agent_shop_id_snapshot` / `started_at` / `finished_at` / `next_run_at` / `dequeued_at` / `priority_effective_from` | NULL(可空) |
|
||||
| `tb_polling_priority_item` | `attempt_limit` | 默认 `3` |
|
||||
| `tb_carrier_traffic_threshold_lock` | `hit_traffic_mb` / `hit_threshold_value` | NULL(可空) |
|
||||
| `tb_carrier_traffic_threshold_lock` | `hit_threshold_unit` / `trigger_source` / `resume_result` | 默认空串 |
|
||||
| `tb_carrier_traffic_threshold_lock` | `judged_at` | **默认 `now()`** |
|
||||
| `tb_carrier_traffic_threshold_lock` | `unlocked_at` | NULL(可空) |
|
||||
| `tb_phone_asset_association` | `invalidator_name_snapshot` | 默认空串 |
|
||||
| `tb_commission_withdrawal_request_attempt` | `qualification_version_id` / `qualification_passed` | 默认 `0` |
|
||||
| `tb_commission_withdrawal_request_attempt` | `qualification_checked_at` | NULL(可空) |
|
||||
| `tb_commission_withdrawal_request_attempt` | `qualification_failure_reason` | 默认空串 |
|
||||
|
||||
`judged_at` 单独说明:该列需要非空(判定时间是命中记录的查询与排序依据),但若不给默认值,回退到上一版本二进制时旧代码写入停机锁不带该列就会违反 NOT NULL,使达量停机事实无法落库。因此保留 `DEFAULT now()`,让旧代码插入的命中事实以插入时刻作为判定时间,与「既有行以创建时间回填」的语义自洽。
|
||||
|
||||
### 2.2 读侧兼容
|
||||
|
||||
- 读侧对上述空值与默认值一律按「无」处理,不因新列缺失或为空阻断查询(命中读数/阈值历史行为空按「无」展示,判定时间取既有创建时间)。
|
||||
- 既有优先轮询行的有效期起算时间(`priority_effective_from`)为迁移时刻,**不会**在上线后被首轮到期扫描批量判为已过期;既有行的 `next_run_at` 为空时,读侧 MUST NOT 据此判定已过期。
|
||||
- 历史审批尝试不受本变更的必填与留痕影响:退款原因必填只对上线后新提交与重提生效,历史申请与补发历史审批不追溯;提现历史尝试的资格校验留痕为空按「历史尝试无资格校验留痕」处理。
|
||||
|
||||
### 2.3 企微控件配置回滚
|
||||
|
||||
代码回滚时需**同步移除**本变更新增的控件映射(退款审批的套餐用量/资产类型/设备类型与型号/原支付渠道交易流水号,代理注册审批的业务员),避免模板残留未使用控件。
|
||||
|
||||
### 2.4 时间参数替换回滚
|
||||
|
||||
员工账单列表「产生时间」参数改名属有意的破坏性收敛,**旧参数名不再被接受**;回滚需**同步前端**(见第 4 节)。
|
||||
|
||||
### 2.5 迁移 down
|
||||
|
||||
如需实际回滚 Schema,按 6→1 逆序执行六对迁移的 down;注意各 down 均声明不可逆事实的丢弃(H5 类型、退款结算标识与备注、优先轮询留痕、命中数字证据、解绑人名称快照、资格校验留痕),其中 `000234` 的 down 在存在 `closed`/`expired` 终态行时会拒绝执行。
|
||||
|
||||
## 3. 新增端点与失效字段默认值
|
||||
|
||||
### 3.1 新增端点(方法 / 路径 / 权限主体)
|
||||
|
||||
| 方法 | 路径 | 权限主体 |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/admin/refunds/order-options` | 退款组门禁(超级管理员/平台/代理,企业账号拒绝)+ 订单数据范围 |
|
||||
| POST | `/api/admin/refunds/:id/offline-settlement` | 同退款组门禁 + 订单数据范围 |
|
||||
| POST | `/api/admin/polling-priority-items/:id/close` | 超级管理员与平台账号 |
|
||||
| POST | `/api/admin/polling-priority-items/:id/retrigger` | 与人工入队一致(含数据范围内的代理账号) |
|
||||
| GET | `/api/admin/carrier-traffic-threshold-hits` | 超级管理员与平台账号 + 卡数据范围 |
|
||||
|
||||
同时新增周期任务类型:优先轮询有效期到期扫描与出队(Worker 调度注册,`@every` 周期)。
|
||||
|
||||
### 3.2 失效字段默认值(新增列的上线默认值)
|
||||
|
||||
见 2.1 表;一句话概括:新增列一律可空或带默认值(空串 / `0` / `3` / `announcement` / `now()`),既有行零变更。
|
||||
|
||||
## 4. 调用方同步要求
|
||||
|
||||
- **员工账单列表**:既有「产生时间」参数由 `created_from`/`created_to` **改名为 `start_time`/`end_time`**,并改用统一严格解析器。旧参数名(`created_from`/`created_to`)与旧格式(date-only、无时区串、空格分隔、小数秒、`±hhmm`)**一律以参数非法错误拒绝,不保留别名或兼容**;前端 MUST 同步改用 `start_time`/`end_time`。
|
||||
- 员工账单「核销通过时间」使用 `decided_start_time`/`decided_end_time`,与主字段同一解析器、同一闭区间(带时区 RFC3339 秒级、含两端)。
|
||||
- 优先轮询项查询、通道阈值命中查询的筛选时间字段亦使用 `start_time`/`end_time`。
|
||||
- 员工账单列表与统计响应新增「未核销金额」与「剩余可核销金额」两个并列字段;详情新增操作日志与来源订单快照投影。
|
||||
@@ -0,0 +1,107 @@
|
||||
## Purpose
|
||||
|
||||
按原始需求(`111.md` §14.6、§14.7、§17.5)补齐代理分销与提现链路的三项留痕与展示:注册审批材料带出将同步的业务员、提现申请记录当次资料资格版本与校验结果、店铺投影返回下级代理数量。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 提现冻结与企业微信终审
|
||||
|
||||
提现申请 SHALL 先校验申请店铺存在有效资料资格,再冻结可提现余额、金额、手续费、收款信息和可选发票快照,并创建企业微信审批。余额不足、资格无效或非本人代理时,系统 MUST NOT 创建提现申请、审批实例或任何冻结。发票为申请级材料,若上传 MUST 按当时有效合同主体校验并冻结至该次审批快照。本地 MUST NOT 提供人工通过或驳回终审。审批 MUST 使用业务类型 `commission_withdrawal_approval`。
|
||||
|
||||
提现申请 SHALL 记录本次提现所依据的有效资料资格版本标识与当次校验结果,校验结果至少包含校验时间、是否通过,以及未通过时的稳定原因(资格不存在、已失效、已停用或资料未通过审批)。校验通过时,该记录 MUST 随每次提交或重提冻结进当次审批尝试快照,并在提现详情可查询(此处「审批材料」指本地不可变审批尝试快照,本能力 MUST NOT 要求新增企业微信模板控件),详情返回的资格校验结果与快照一致:通过时未通过原因为空字符串;上线前产生、无资格校验留痕的历史尝试按「无留痕」返回(版本标识为 0、校验时间为空、是否通过为 0、未通过原因为空字符串)。校验未通过时,系统 MUST 按既有资料资格校验的拒绝语义处理:MUST NOT 创建提现申请、审批实例与审批尝试,因此该情形 MUST NOT 产生带未通过原因的审批尝试快照,未通过稳定原因 MUST 记录在该次拒绝的结果与审计事实中。两种情形均 MUST NOT 改写历史尝试的校验结果。该记录 MUST NOT 改变既有冻结、扣减、释放、驳回重提与通过后撤销的任何语义。
|
||||
|
||||
每次提交或重提 MUST 新增一条不可变的审批尝试记录,冻结该次金额、手续费、实际到账、收款信息与申请级发票快照,并为其创建独立的审批实例;提现申请行只保存最新审批实例标识用于列表投影,MUST NOT 作为历史事实来源。企业微信通过即视为已到账:系统 MUST 保持 `WithdrawalStatusApproved`(2)并在同一事务内仅一次从冻结余额扣减,同时写入到账时间;MUST NOT 使用已到账状态值 4。
|
||||
|
||||
企业微信驳回时,系统 MUST 仅一次释放本次尝试的冻结余额并记录释放时间;`cancelled` 与 `deleted` MUST 按驳回同等处理。驳回后代理可修改金额、收款信息和本次发票重新提交,每次新建审批尝试记录与审批实例;资料资格保持有效。企业微信对已通过申请撤销时,系统 MUST NOT 回滚已到账金额、MUST NOT 重新冻结,MUST 保持通过状态并写入正交的异常标记与原因供详情展示与人工处理,MUST NOT 自动重提。
|
||||
|
||||
重复、乱序或结果未知的回调 MUST NOT 造成重复扣减、重复释放或第二笔冻结;结果未知时申请保持在途并由既有查询恢复机制收敛。
|
||||
|
||||
#### Scenario: 资格无效或余额不足不创建提现
|
||||
|
||||
- **WHEN** 代理店铺不存在有效资料资格、资格已失效,或可提现余额不足
|
||||
- **THEN** 系统拒绝创建提现申请,不创建审批实例且不冻结任何余额
|
||||
|
||||
#### Scenario: 提现审批通过
|
||||
|
||||
- **WHEN** 企业微信对一笔待审提现返回最终通过且该结果首次被消费
|
||||
- **THEN** 系统仅一次从冻结余额扣减,申请保持已通过状态并记录到账时间
|
||||
|
||||
#### Scenario: 提现审批驳回
|
||||
|
||||
- **WHEN** 企业微信最终驳回一笔提现申请
|
||||
- **THEN** 系统仅一次释放该次尝试的冻结余额并记录释放时间,保留审批快照,并允许在资格仍有效时修改后重提
|
||||
|
||||
#### Scenario: 驳回后重提
|
||||
|
||||
- **WHEN** 代理修改金额、收款信息或本次发票后重新提交被驳回的提现申请
|
||||
- **THEN** 系统新增审批尝试记录并创建新的审批实例,历史尝试、快照与审批结果不被覆盖
|
||||
|
||||
#### Scenario: 企微取消或删除
|
||||
|
||||
- **WHEN** 企业微信对待审提现返回取消或删除
|
||||
- **THEN** 系统按驳回同等处理,仅一次释放冻结余额并记录释放时间
|
||||
|
||||
#### Scenario: 通过后撤销
|
||||
|
||||
- **WHEN** 企业微信对已通过的提现申请返回通过后撤销
|
||||
- **THEN** 系统不回滚已到账金额、不重新冻结、不自动重提,仅写入异常标记与原因供详情展示
|
||||
|
||||
#### Scenario: 重复回调不重复扣减
|
||||
|
||||
- **WHEN** 同一审批终态被重复投递或乱序到达
|
||||
- **THEN** 系统至多扣减或释放一次,不产生第二笔冻结
|
||||
|
||||
#### Scenario: 资料校验结果可追溯
|
||||
|
||||
- **WHEN** 代理在有效资料资格下提交提现申请,随后该资格被替换
|
||||
- **THEN** 该次提现尝试仍返回提交时冻结的资格版本标识与校验结果,不被新版本改写
|
||||
|
||||
#### Scenario: 资格失效时的校验原因
|
||||
|
||||
- **GIVEN** 代理店铺资料资格已被超级管理员作废
|
||||
- **WHEN** 代理发起提现并被拒绝
|
||||
- **THEN** 拒绝结果与审计记录包含稳定原因(资格已失效),且不创建提现申请、审批实例或冻结
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 注册审批材料带出业务员
|
||||
|
||||
代理注册审批材料 SHALL 在既有字段之外带出「业务员」,取上级代理店铺当前业务员的名称快照,用于审批人在企业微信侧确认将同步给新代理的初始业务员。上级店铺当前无业务员时该字段 MUST 以空值提交并在材料中标记为无,MUST NOT 以提交人、上级店铺主账号或其它账号填充。该字段 MUST 为只读快照:审批通过时写入新店铺的初始业务员仍按既有规则取上级业务员当时值,MUST NOT 因审批材料快照与通过时值不同而改写通过结果或回滚。企业微信模板控件缺失导致该字段无法提交时 MUST 明确失败并提示需配置控件,MUST NOT 静默丢弃。
|
||||
|
||||
#### Scenario: 审批材料展示将同步的业务员
|
||||
|
||||
- **GIVEN** 上级代理店铺存在启用的业务员
|
||||
- **WHEN** 创建代理注册审批实例
|
||||
- **THEN** 审批材料包含该业务员名称快照
|
||||
|
||||
#### Scenario: 上级无业务员
|
||||
|
||||
- **GIVEN** 上级代理店铺当前没有启用业务员
|
||||
- **WHEN** 创建代理注册审批实例
|
||||
- **THEN** 业务员字段以空值提交并标记为无,不使用其它账号填充
|
||||
|
||||
#### Scenario: 快照与实际同步值不同
|
||||
|
||||
- **GIVEN** 审批材料创建后上级店铺业务员发生变更
|
||||
- **WHEN** 企业微信最终通过该注册申请
|
||||
- **THEN** 新店铺初始业务员按通过时上级业务员写入,历史审批材料快照不被改写
|
||||
|
||||
### Requirement: 店铺下级代理数量投影
|
||||
|
||||
店铺列表与详情 SHALL 返回该店铺的直接下级代理数量,口径为未删除且上级店铺标识等于该店铺的店铺数。数量 MUST 一次批量聚合完成,MUST NOT 逐店查询放大查询次数。数量 MUST 受既有店铺数据范围约束:范围外店铺按不可见处理。该字段 MUST 只读,MUST NOT 影响上下级关系、佣金关系、通知范围或任何既有写入语义。
|
||||
|
||||
#### Scenario: 展示下级数量
|
||||
|
||||
- **GIVEN** 某代理店铺存在三家直接下级店铺
|
||||
- **WHEN** 查询该店铺详情或列表
|
||||
- **THEN** 下级代理数量返回三
|
||||
|
||||
#### Scenario: 无下级返回零
|
||||
|
||||
- **WHEN** 查询一家没有任何直接下级的店铺
|
||||
- **THEN** 下级代理数量返回零而非空值
|
||||
|
||||
#### Scenario: 数据范围外不泄露
|
||||
|
||||
- **WHEN** 平台用户查询其数据范围外店铺
|
||||
- **THEN** 系统不返回该店铺及其下级数量
|
||||
@@ -0,0 +1,46 @@
|
||||
## Purpose
|
||||
|
||||
按原始需求(`111.md` §18.2)补齐佣金明细导出的列定义,使导出结果可直接用于按业务员、用户组与设备维度统计,并与原充值订单的下单时间对齐。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 回溯明细关联查询与导出
|
||||
|
||||
系统 SHALL 在佣金明细中分别展示原发放佣金和回溯扣款记录,并允许从任一记录查询其关联的退款单、原佣金或全部回溯明细。回溯记录必须显示负数金额、不可提现标识、来源退款单号、原佣金记录号、生成时间和回溯后佣金钱包实际余额。原佣金与回溯记录 MUST 合并为同一列表的同一分页与同一排序口径,MUST NOT 因来源表不同而丢失或重复任一条事实。
|
||||
|
||||
佣金明细及导出 MUST 使用既有佣金数据范围:代理仅可读取自身及其既有可见范围内的事实,平台与超级管理员遵循既有范围;无权记录不得通过关联 ID、汇总或导出泄露存在性。
|
||||
|
||||
佣金明细导出 MUST 使用佣金记录粒度,原佣金与回溯记录各占一行,并冻结创建时筛选条件、操作者与可见范围。导出列 MUST 按下列顺序输出且至少包含:店铺、业务员、用户组、资产类型、设备类型、设备型号、资产标识、订单号、下单时间、佣金金额、佣金来源、佣金状态、关联佣金明细、入账后金额与创建时间。其中订单号 MUST 与原发放记录一致(回溯记录沿用被回溯记录订单号),下单时间 MUST 取原充值订单的创建时间,资产类型 MUST 区分卡与设备,设备类型、设备型号、业务员与用户组 MUST 按导出执行时该资产与店铺的当前归属补充;缺失值 MUST 以空值导出并 MUST NOT 阻断导出,MUST NOT 因个别行缺值丢失整行或整列。列名与金额单位 MUST 与既有导出口径一致,导出粒度、创建期冻结与执行期只读语义 MUST NOT 改变。回溯记录金额 MUST 为负数、可提现标识为不可提现。回溯后余额为对应钱包变动提交后的实际余额,可为负数;金额保持分,展示层转元 MUST NOT 改变负数或余额事实。
|
||||
|
||||
#### Scenario: 代理查询越权回溯记录
|
||||
|
||||
- **WHEN** 代理使用回溯记录 ID、原佣金 ID 或退款单号查询其数据范围外的回溯关系
|
||||
- **THEN** 系统按既有数据范围返回不存在或空结果,不泄露关联事实
|
||||
|
||||
#### Scenario: 原佣金与回溯合并分页
|
||||
|
||||
- **GIVEN** 同一店铺同时存在原佣金记录与回溯记录
|
||||
- **WHEN** 查询佣金明细列表并翻页
|
||||
- **THEN** 两类记录按同一排序口径出现在同一结果集内,任一条不缺失也不重复
|
||||
|
||||
#### Scenario: 回溯记录导出
|
||||
|
||||
- **WHEN** 导出含回溯记录的佣金明细
|
||||
- **THEN** 原佣金与回溯记录各占一行,回溯行金额为负数、可提现标识为不可提现、含关联佣金明细与退款单标识,且回溯后余额为负数时原样导出
|
||||
|
||||
#### Scenario: 导出包含归属与设备维度列
|
||||
|
||||
- **GIVEN** 一条佣金记录关联的资产为绑定设备的卡,且其店铺存在业务员与用户组
|
||||
- **WHEN** 导出佣金明细
|
||||
- **THEN** 该行的业务员、用户组、资产类型、设备类型与设备型号按导出执行时的当前归属与资产属性导出
|
||||
|
||||
#### Scenario: 下单时间取原订单创建时间
|
||||
|
||||
- **WHEN** 导出原佣金记录与其对应的回溯记录
|
||||
- **THEN** 两行的下单时间均为被关联原充值订单的创建时间
|
||||
|
||||
#### Scenario: 归属缺失不阻断导出
|
||||
|
||||
- **GIVEN** 某佣金记录关联店铺当前没有业务员或资产无设备型号
|
||||
- **WHEN** 导出佣金明细
|
||||
- **THEN** 对应列以空值导出,其余行与列正常产出
|
||||
@@ -0,0 +1,35 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 资产与卡详情的优先轮询状态投影
|
||||
|
||||
资产解析接口(`GET /api/admin/assets/resolve/{identifier}`)的卡与设备两个分支 SHALL 返回该资产或卡的优先轮询状态投影,至少包含:是否处于优先轮询中、最近触发场景、最近轮询结果、最近轮询时间与失败原因。本能力 MUST NOT 为投影新增独立端点,投影 MUST 落在该既有解析端点内。投影口径 MUST 与该卡在优先轮询事实表中的未完成活动项一致:存在待执行或执行中的项时「是否处于优先轮询中」为真,最近触发场景取最近触发类型,最近轮询结果取最近一次执行结果(执行成功、轮询失败、已关闭、已过期或处理中),最近轮询时间取最近一次执行或触发时间,失败原因取最近一次可安全展示的失败原因。无任何优先轮询事实时字段以否或空值返回,MUST NOT 以普通轮询结果填充。
|
||||
|
||||
投影 MUST 只读:读取 MUST NOT 创建、合并、推进或出队任何优先轮询项,MUST NOT 触发上游调用。投影 MUST 受既有资产数据权限约束,越权资产 MUST 按不存在处理,MUST NOT 通过该字段泄露范围外资产。
|
||||
|
||||
#### Scenario: 资产存在未完成优先项
|
||||
|
||||
- **GIVEN** 某卡的资产存在待执行的优先轮询项
|
||||
- **WHEN** 查询该资产详情
|
||||
- **THEN** 响应返回处于优先轮询中为真、最近触发场景与最近轮询时间
|
||||
|
||||
#### Scenario: 优先项失败展示原因
|
||||
|
||||
- **GIVEN** 某卡的优先轮询项已因尝试次数达到上限进入失败终态
|
||||
- **WHEN** 查询该卡详情
|
||||
- **THEN** 响应返回最近轮询结果为轮询失败及可安全展示的失败原因
|
||||
|
||||
#### Scenario: 无优先轮询事实
|
||||
|
||||
- **WHEN** 查询一个从未进入优先轮询、也无历史优先项的资产详情
|
||||
- **THEN** 各投影字段以否或空值返回,不使用普通轮询结果填充
|
||||
|
||||
#### Scenario: 读取不改变队列
|
||||
|
||||
- **GIVEN** 某卡处于优先轮询执行中
|
||||
- **WHEN** 连续查询该资产详情
|
||||
- **THEN** 优先轮询项状态、触发次数与尝试次数均不因读取而改变
|
||||
|
||||
#### Scenario: 越权资产
|
||||
|
||||
- **WHEN** 调用者查询其数据范围外资产的详情
|
||||
- **THEN** 响应与资产不存在不可区分,不返回任何优先轮询状态
|
||||
@@ -0,0 +1,34 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 通道阈值命中事实留痕与查询
|
||||
|
||||
系统 SHALL 在创建通道阈值停机锁的同一事务内持久化该次命中事实,至少包含:触发时该卡当前计费周期的累计流量读数、本次判定的阈值数值与单位、计费周期起点、判定时间与触发来源(自动判定或重放)。累计流量 MUST 取与停机判定相同的网关累计读数换算值,MUST NOT 使用本地用量统计或套餐真流量;阈值 MUST 取该次判定实际生效的通道配置值(含改阈值前的历史值),使历史命中在配置变更后仍可解释。
|
||||
|
||||
周期恢复 SHALL 同步记录解锁时间与复机结果(未发起复机时的跳过原因、发起后的确认结果或转人工标记),MUST NOT 覆盖停机时的命中事实。本次上线前已存在的锁行 SHALL 保留其创建时间作为判定时间,命中数值与阈值 MAY 为空(历史读数不可重建),读侧 MUST 按「无」展示而不阻断查询。系统 MUST 提供受既有权限控制的命中记录查询,支持按运营商通道、卡、计费周期与判定时间范围筛选,并返回上述字段;查询 MUST 受数据范围约束,越权与不存在 MUST 不可区分,MUST NOT 返回凭证或上游响应原文。留痕与查询 MUST NOT 改变既有达量判定、停机调用、持锁拒绝复机与新周期解锁语义,MUST NOT 因留痕失败而放行或延迟停机。
|
||||
|
||||
#### Scenario: 达量命中留下数字证据
|
||||
|
||||
- **WHEN** 某卡当前周期累计流量达到通道阈值并创建停机锁
|
||||
- **THEN** 命中事实保存该次判定的累计流量、阈值、单位、周期起点与判定时间,可按卡与周期查询
|
||||
|
||||
#### Scenario: 改阈值后历史命中仍可解释
|
||||
|
||||
- **GIVEN** 某卡已按旧阈值命中并持锁
|
||||
- **WHEN** 通道阈值随后被修改
|
||||
- **THEN** 该次命中事实仍返回命中时的阈值与单位,不被新配置改写
|
||||
|
||||
#### Scenario: 新周期恢复留痕
|
||||
|
||||
- **WHEN** 新计费周期处理持锁卡并按其条件解锁或复机
|
||||
- **THEN** 记录解锁时间与复机结果或跳过原因,停机命中事实保持不变
|
||||
|
||||
#### Scenario: 越权查询命中记录
|
||||
|
||||
- **WHEN** 调用者查询其数据范围外卡的命中记录
|
||||
- **THEN** 响应与记录不存在不可区分
|
||||
|
||||
#### Scenario: 留痕不影响停机语义
|
||||
|
||||
- **GIVEN** 命中事实写入遇到异常
|
||||
- **WHEN** 达量判定与停机任务执行
|
||||
- **THEN** 系统按既有可靠机制处理停机,MUST NOT 因留痕失败而放行或延迟停机
|
||||
@@ -0,0 +1,72 @@
|
||||
## Purpose
|
||||
|
||||
补齐员工代收款账单列表与详情在原始需求(`111.md` §5.2.1、§5.2.2)中要求、当前实现未提供的查询筛选与投影能力,并将「未核销金额」与「剩余可核销金额」两个口径显式区分,避免列表与统计口径混淆。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 员工账单列表查询与筛选
|
||||
|
||||
账单列表 SHALL 在既有可见性范围内支持以下筛选:账单编号(精确)、来源订单号(精确)、来源业务类型、账单状态、产生时间范围、核销通过时间范围、欠款人姓名或账号(模糊)、客户名称(模糊)。账单编号 MUST 为账单自身的唯一标识(即账单记录标识);系统 MUST NOT 为满足本要求新增编号字段或编号生成规则。欠款人姓名或账号与客户名称的模糊匹配 MUST 由服务端按去空白后的子串匹配实现,MUST NOT 要求调用方先获得账号 ID 或客户 ID。
|
||||
|
||||
产生时间范围 MUST 使用统一参数名 `start_time` 与 `end_time`;核销通过时间范围 MUST 使用 `decided_start_time` 与 `decided_end_time`。两类时间参数 MUST 使用同一严格解析契约:取值为带显式时区的 RFC3339 秒级时间,解析结果归一为 UTC 瞬时,筛选区间为闭区间(含两端),开始时间晚于结束时间时拒绝请求;旧参数名与旧格式(含仅日期的取值)MUST 被拒绝,MUST NOT 保留静默兼容或别名。
|
||||
|
||||
核销通过时间 MUST 取该账单经「状态为已通过的分摊」关联到的申请表审批终态时间;账单从未存在已通过分摊时该筛选 MUST 不命中该账单。系统 MUST NOT 使用分摊记录的释放时间作为核销通过时间:该字段在既有实现中同时承担「审批通过转为已核销」与「驳回或释放预占」两种语义。
|
||||
|
||||
列表响应 SHALL 同时返回「未核销金额」与「剩余可核销金额」两个金额:未核销金额 MUST 为应收金额减已核销金额;剩余可核销金额 MUST 为应收金额减已核销金额再减审批中预占金额,已关闭账单两者均为零。已核销金额大于应收金额(异常数据)时,两个金额 MUST 均按 0 返回,MUST NOT 返回负数。列表 MUST NOT 因新增模糊筛选改变既有可见性规则:欠款人仅可见本人账单,超级管理员可见全部。
|
||||
|
||||
#### Scenario: 按账单编号精确查询
|
||||
|
||||
- **WHEN** 超级管理员以账单编号筛选账单列表
|
||||
- **THEN** 系统返回该编号对应账单,且不返回其它账单
|
||||
|
||||
#### Scenario: 按核销通过时间筛选
|
||||
|
||||
- **WHEN** 调用方以核销通过时间范围筛选
|
||||
- **THEN** 系统只返回在该范围内存在已通过分摊的账单,从未核销的账单不出现在结果中;筛选依据为该账单已通过分摊关联的申请表审批终态时间
|
||||
|
||||
#### Scenario: 拒绝旧时间参数与旧格式
|
||||
|
||||
- **WHEN** 调用方携带旧参数名或仅日期、无时区、带小数秒的时间取值请求账单列表
|
||||
- **THEN** 系统以参数非法错误拒绝请求,且不返回任何账单
|
||||
|
||||
#### Scenario: 按员工姓名或客户名称模糊查询
|
||||
|
||||
- **WHEN** 调用方提交员工姓名子串或客户名称子串
|
||||
- **THEN** 系统返回欠款人账号名称或客户名称包含该子串的账单,无需调用方提供账号 ID 或客户 ID
|
||||
|
||||
#### Scenario: 未核销金额与剩余可核销金额
|
||||
|
||||
- **GIVEN** 一张应收 100 元、已核销 30 元且存在审批中预占 20 元的账单
|
||||
- **WHEN** 查询账单列表
|
||||
- **THEN** 未核销金额返回 70 元,剩余可核销金额返回 50 元
|
||||
|
||||
#### Scenario: 已关闭账单两个金额为零
|
||||
|
||||
- **WHEN** 查询一张已关闭账单
|
||||
- **THEN** 未核销金额与剩余可核销金额均返回零,已核销金额保持关闭时的值
|
||||
|
||||
### Requirement: 员工账单详情投影
|
||||
|
||||
账单详情 SHALL 在既有账单、来源、客户与员工信息之外,返回该账单的操作日志与来源订单快照。操作日志 MUST 取该账单维度既有审计事实的投影,至少包含动作、操作账号名称、操作时间与变更前后摘要,MUST NOT 包含支付凭证内容、完整收款信息或其它敏感原文。来源订单快照 MUST 取来源订单或来源充值记录的既有只读字段投影,至少包含来源单号、来源业务类型、来源实收或充值金额、来源创建时间与资产标识;客户与员工信息 MUST 复用账单创建时冻结的快照。来源记录不可读或字段缺失时 MUST 返回空值 MUST NOT 阻断详情响应,MUST NOT 为此新增来源业务写入路径。
|
||||
|
||||
#### Scenario: 详情返回操作日志
|
||||
|
||||
- **WHEN** 授权账号查询一条存在创建、提交与审批终态的账单详情
|
||||
- **THEN** 响应的操作日志按时间升序返回各次动作、操作账号名称、操作时间与前后值摘要
|
||||
|
||||
#### Scenario: 详情返回来源订单快照
|
||||
|
||||
- **GIVEN** 账单来源为后台线下套餐订单或代理线下充值
|
||||
- **WHEN** 查询该账单详情
|
||||
- **THEN** 响应返回来源单号、来源业务类型、来源金额、来源创建时间与资产标识
|
||||
|
||||
#### Scenario: 来源记录不可读
|
||||
|
||||
- **GIVEN** 账单来源记录已被物理删除或字段为空
|
||||
- **WHEN** 查询该账单详情
|
||||
- **THEN** 来源订单快照对应字段返回空值,详情其余内容正常返回
|
||||
|
||||
#### Scenario: 操作日志不含敏感原文
|
||||
|
||||
- **WHEN** 账单存在包含凭证上传与收款方式变更的操作
|
||||
- **THEN** 操作日志只返回对象键引用、编码与名称摘要,不返回凭证内容或完整收款文本
|
||||
@@ -0,0 +1,85 @@
|
||||
## Purpose
|
||||
|
||||
把员工代收款账单列表、优先轮询项查询与通道阈值命中查询纳入统一时间筛选契约:前者的「产生时间」由既有日期参数改为统一参数并改用严格解析,并新增「核销通过时间」筛选字段;后两者由本变更新建或扩展的查询入口直接采用统一参数与闭区间语义。同时规定同一端点存在第二个时间字段时的命名、解析与边界规则,消除原受影响端点表中「上述端点本期 MUST NOT 新增时间参数」与本变更的冲突。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### 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`,按时刻闭区间比较;保留既有的剩余天数上下限筛选 | 必改 |
|
||||
| `GET /api/admin/employee-collection-bills` | `created_from`/`created_to`(date-only `2006-01-02`) | 账单产生时间与核销通过时间 | 产生时间改名为 `start_time`/`end_time` 并改用严格解析,旧参数名与旧格式一律拒绝;另新增 `decided_start_time`/`decided_end_time` 按核销通过时间闭区间筛选 | 必改 |
|
||||
| `GET /api/admin/polling-priority-items` | 无时间参数 | 优先轮询项入队时间 | 新增 `start_time`/`end_time` | 必改 |
|
||||
| `GET /api/admin/carrier-traffic-threshold-hits` | 端点本次新建 | 阈值命中判定时间 | 新建即使用 `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`、`created_from`/`created_to`、date-only、无时区串、空格分隔时间、小数秒与 `±hhmm` 偏移;被拒绝时 MUST 以参数非法错误返回,MUST NOT 静默忽略该条件后返回全量或按旧格式筛选。未列入上表的其余端点(资产钱包流水、客户钱包流水、设备资产列表及其导出、代理主钱包流水及其导出、手机号—资产关联列表、佣金统计类接口、审计类列表、轮询告警历史、企业卡与企业设备授权列表、IoT 卡资产列表、套餐导出、退款导出)本期 MUST NOT 新增时间参数、MUST NOT 改变参数名与格式;本次已纳入上表的端点不再受该限制。
|
||||
|
||||
同一端点存在两个时间筛选字段时,主时间字段 MUST 使用统一的 `start_time` 与 `end_time`;第二个字段 MUST 使用「业务语义 + 时间后缀」的固定命名,本变更的核销通过时间为 `decided_start_time` 与 `decided_end_time`。两类字段 MUST 使用同一解析器与同一闭区间语义,MUST NOT 为第二个字段引入不同的格式接受范围、边界规则或静默兼容。计费周期起点不属于本条的第二个范围筛选字段:它是精确匹配键,沿用既有参数名 `period_start`,取值 MUST 复用同一共享严格解析器与同一格式接受范围(同一瞬时作为闭区间两端解析),MUST NOT 复制第二套时间解析。
|
||||
|
||||
受影响端点与本次新增筛选 MUST 复用既有共享严格解析器,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** 请求以 `start_time`/`end_time` 与 `decided_start_time`/`decided_end_time` 查询员工代收款账单列表
|
||||
- **THEN** 系统分别按账单产生时间与核销通过时间执行闭区间筛选,两个条件的边界都包含端点
|
||||
|
||||
#### Scenario: 员工账单列表拒绝旧参数名与旧格式
|
||||
|
||||
- **WHEN** 请求携带 `created_from`/`created_to`,或对时间参数提交 date-only、无时区或小数秒取值
|
||||
- **THEN** 系统以参数非法错误码拒绝请求,MUST NOT 按旧格式筛选或忽略该条件
|
||||
|
||||
#### Scenario: 优先轮询项与阈值命中按统一参数筛选
|
||||
|
||||
- **WHEN** 请求以 `start_time` 与 `end_time` 查询优先轮询项列表或通道阈值命中记录
|
||||
- **THEN** 系统分别按入队时间与判定时间执行闭区间筛选,且开始时间晚于结束时间时以参数非法错误码拒绝
|
||||
|
||||
#### Scenario: 未列入端点保持既有行为
|
||||
|
||||
- **WHEN** 调用未列入上表的端点并携带其既有时间参数(含宽松格式)
|
||||
- **THEN** 系统保持该端点既有参数名、既有格式接受范围与既有筛选结果
|
||||
@@ -0,0 +1,50 @@
|
||||
## Purpose
|
||||
|
||||
按原始需求(`111.md` §13.3、§13.5)为运营主动弹窗补齐「弹窗类型」配置字段与对应的默认优先级分级,使「套餐政策推广」与「通用公告」在数据上可区分,并保持风险换卡提醒始终高于两者。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 运营弹窗实时匹配
|
||||
|
||||
系统 SHALL 允许超级管理员和平台用户管理全局运营弹窗的标题、内容、**弹窗类型**、有效期、启停、优先级、店铺/设备类型/卡类型范围、四种页面(首页、资产详情、套餐购买、资产钱包充值)、频率和一个可选受控操作。弹窗类型 MUST 为必填单选,取值范围为 `套餐政策推广` 与 `通用公告`;创建 MUST 提供该字段,更新按既有部分更新语义(未传时保持原值),一经传入 MUST 按同一取值域校验。类型变更而请求未显式传优先级时,该配置的显式优先级 MUST 保持不变(类型缺省优先级只在创建路径生效,类别序仍保证 `套餐政策推广` 优先于 `通用公告`)。范围同一维度多选为任一匹配,未配置范围即全量;已配置范围而当前资产在该维度没有可判定值时该配置不命中。H5 请求必须携带当前页面资产标识。操作仅可为套餐购买或资产钱包充值,不得配置任意 URL。
|
||||
|
||||
候选排序 SHALL 先按有效优先级类别再按显式优先级:风险换卡提醒高于全部运营弹窗;运营弹窗内 `套餐政策推广` MUST 高于 `通用公告`;同一类别内按显式优先级降序、相同优先级取最近更新时间最新。类别顺序 MUST 表达为显式的类别排序,MUST NOT 依赖类型取值的字典序(否则「通用公告」会排在「套餐政策推广」之前,与本条相反)。显式优先级缺省时 MUST 取该类型的默认值。既有配置在本次上线时 MUST 被赋予一个确定类型(默认 `通用公告`),其显式优先级取值与同类别内相对顺序 MUST 保持不变。
|
||||
|
||||
运营弹窗仅在客户请求候选时实时匹配并创建或复用通知;每客户每配置支持仅一次或每天一次。候选只返回优先级最高一条,同优先级取最近更新时间最新,启停操作同样更新最近更新时间。配置修改形成新版本,既有通知保留原快照且不被改写;修改后的仅一次配置可向原命中客户重新投放。配置到期或停用停止新投放,历史通知在通知中心展示 90 天。
|
||||
|
||||
#### Scenario: 风险与运营候选同时命中
|
||||
|
||||
- **WHEN** 当前资产同时满足风险换卡和多个运营弹窗条件
|
||||
- **THEN** 系统仅返回风险换卡候选,并保持通知未读
|
||||
|
||||
#### Scenario: 优先级与最近更新排序
|
||||
|
||||
- **WHEN** 多条同类型运营配置同时命中且优先级相同
|
||||
- **THEN** 系统只返回最近更新时间最新的一条
|
||||
|
||||
#### Scenario: 推广优先于公告
|
||||
|
||||
- **GIVEN** 一条 `套餐政策推广` 配置与一条 `通用公告` 配置同时命中,且公告配置的显式优先级更高
|
||||
- **WHEN** 客户请求候选
|
||||
- **THEN** 系统返回 `套餐政策推广` 配置,类型类别优先于显式优先级
|
||||
|
||||
#### Scenario: 既有配置类型补齐
|
||||
|
||||
- **GIVEN** 一条本次上线前创建的运营配置
|
||||
- **WHEN** 上线迁移完成
|
||||
- **THEN** 该配置具有确定类型(默认 `通用公告`)且其显式优先级与同类别内顺序不变
|
||||
|
||||
#### Scenario: 独立卡的设备类型维度
|
||||
|
||||
- **WHEN** 命中的运营配置配置了设备类型范围,但当前资产是未绑定设备的独立卡
|
||||
- **THEN** 系统不命中该配置
|
||||
|
||||
#### Scenario: 配置修改后重新投放
|
||||
|
||||
- **WHEN** 已按仅一次频率向客户投放过的配置被修改
|
||||
- **THEN** 配置版本递增,既有通知的内容与快照保持不变,该客户可再次命中一次
|
||||
|
||||
#### Scenario: 到期或停用
|
||||
|
||||
- **WHEN** 配置已到期或被停用
|
||||
- **THEN** 系统停止新投放,既有通知在展示期内仍可见
|
||||
@@ -0,0 +1,79 @@
|
||||
## Purpose
|
||||
|
||||
补齐商户池在原始需求(`111.md` §6.2、§6.4)中要求、当前实现未提供的列表投影字段,并把金额/笔数「全部成员达标」语义按统计周期分开定义、明确单成员池与时间轮询起点的行为,消除主 Spec 文字与实现之间的张力。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 商户池唯一性与轮询配置
|
||||
|
||||
系统 SHALL 为每种支付方式最多启用一个商户池;停用历史池可保留,但不得同时启用多个同支付方式池。商户池成员支付方式 MUST 与池一致,成员按明确顺序排列;金额/笔数方式必须配置 `每轮累计`、`自然日累计` 或 `自然月累计` 统计周期,时间方式必须配置最小为 1 分钟的数值、单位和起始时间。
|
||||
|
||||
金额和笔数轮询只统计已确认支付成功结果,不在预下单时预占,也不因退款回冲。支付创建时 MUST 冻结本次路由所属的统计世代;首次成功只按支付 ID 一次性计入冻结商户和冻结统计世代,当前选路只读取当前统计世代;金额与笔数累计 MUST 只统计与当前统计世代匹配的成功事实(自然日与自然月方式另加支付时间不早于当期窗口起点)。达到阈值的成员在当前周期跳过;全部成员均达到阈值时的行为按统计周期区分:`每轮累计` MUST 视为当轮结束,以保存成功时间为新起点开启新一轮并从零重新累计,MUST NOT 因当轮全部达标而拒绝创建,开启新一轮 MUST 以条件更新递增统计世代,仅在当前世代未被并发改变时成功,更新冲突 MUST 按并发冲突拒绝创建本次支付且 MUST NOT 在旧世代上重复累计;`自然日累计` 与 `自然月累计` MUST 拒绝创建新支付单并提示当前周期暂无可用商户。池内只有一个启用成员时该成员 MUST 被固定选中:MUST NOT 因达到阈值而在当前周期被跳过,也 MUST NOT 因「全部成员达标」而拒绝创建,该规则优先于上句的自然周期拒绝语义。时间轮询自起始时间按固定时段和成员顺序选择,成员停用即时跳下一个可用成员但不重置时段;修改时间周期数值、单位、起始时间或成员顺序并保存成功后,系统 MUST 以保存成功时间为新起点并从前述成员列表第一项重新计算时段,MUST NOT 沿用请求提交前的起点。预下单失败不得自动切换或重试,失败单不计入统计;客户再次发起时重新选择。修改阈值保留当前统计,修改统计周期、金额/笔数方式、时间周期、起始时间或每轮排序按 PRD 规则开启新周期;自然周期排序调整保留未移除成员累计。
|
||||
|
||||
#### Scenario: 并发预下单未预占额度
|
||||
|
||||
- **WHEN** 多个客户并发创建金额或笔数轮询支付单且当前成员尚未达到阈值
|
||||
- **THEN** 系统可使这些支付单均命中当前成员,只有后续确认成功的支付才计入累计,已创建支付单不因轮询切换改挂商户
|
||||
|
||||
#### Scenario: 迟到首次成功归属冻结统计世代
|
||||
|
||||
- **GIVEN** 支付已创建但尚未成功,之后商户池切换统计周期、成员或排序
|
||||
- **WHEN** 该支付首次确认成功
|
||||
- **THEN** 系统仅将金额或笔数写入该支付创建时冻结的商户和统计世代,不得改写当前选路统计或重复累计
|
||||
|
||||
#### Scenario: 每轮累计全部达标开启新一轮
|
||||
|
||||
- **GIVEN** 启用商户池统计周期为每轮累计,且本轮全部成员均已达到阈值
|
||||
- **WHEN** 客户创建新的支付单
|
||||
- **THEN** 系统开启新一轮、各成员从零累计,并选中成员顺序第一项,不返回暂无可用商户
|
||||
|
||||
#### Scenario: 当期没有可用商户
|
||||
|
||||
- **WHEN** 启用商户池中不存在启用且未达阈值的成员,或商户池已停用
|
||||
- **THEN** 系统拒绝创建新支付单并提示暂无可用商户,不回退到旧综合支付配置
|
||||
|
||||
#### Scenario: 自然周期全部达标拒绝创建
|
||||
|
||||
- **GIVEN** 启用商户池统计周期为自然日累计或自然月累计,且当前周期全部成员均已达到阈值
|
||||
- **WHEN** 客户创建新的支付单
|
||||
- **THEN** 系统拒绝创建并提示当前周期暂无可用商户,不回退到旧综合支付配置
|
||||
|
||||
#### Scenario: 单成员池达标后仍可收款
|
||||
|
||||
- **GIVEN** 启用商户池只有一个启用成员,且该成员在当前自然日或自然月周期内已达到阈值
|
||||
- **WHEN** 客户创建新的支付单
|
||||
- **THEN** 系统仍选中该成员,不返回暂无可用商户
|
||||
|
||||
#### Scenario: 修改时间轮询配置后起点重算
|
||||
|
||||
- **GIVEN** 时间轮询商户池已存在起始时间与成员顺序
|
||||
- **WHEN** 授权账号修改周期数值、单位、起始时间或成员顺序并保存成功
|
||||
- **THEN** 系统以保存成功时间作为新起点,后续时段从成员列表第一项重新计算,不沿用请求提交前的时段
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 商户池列表投影
|
||||
|
||||
商户池列表与详情 SHALL 在既有配置字段之外返回:启用成员数量与成员总数量、当前命中成员、最近配置更新时间。当前命中成员 MUST 以与支付创建相同的选择规则只读计算:金额/笔数方式取成员顺序中第一个未达阈值的成员,「每轮累计」全部达标时取成员顺序第一项;时间方式按当前时段计算结果。该计算 MUST 为只读,MUST NOT 创建支付单、MUST NOT 推进统计世代、MUST NOT 计入成功累计。商户池停用或没有可用成员时当前命中成员 SHALL 返回空值 MUST NOT 阻断列表响应。成员数量与当前命中成员 MUST 按该页池集合批量计算,MUST NOT 逐池放大查询次数。
|
||||
|
||||
#### Scenario: 列表展示商户数量与更新时间
|
||||
|
||||
- **WHEN** 授权账号查询商户池列表
|
||||
- **THEN** 每行返回启用成员数、成员总数与最近配置更新时间
|
||||
|
||||
#### Scenario: 当前命中成员按顺序计算
|
||||
|
||||
- **GIVEN** 金额轮询商户池的成员顺序为 A、B,且 A 尚未达到阈值
|
||||
- **WHEN** 查询商户池列表
|
||||
- **THEN** 当前命中成员返回 A,且查询本身不产生支付单、不推进统计世代
|
||||
|
||||
#### Scenario: 停用池无当前成员
|
||||
|
||||
- **WHEN** 查询一个已停用且无可用成员的商户池
|
||||
- **THEN** 当前命中成员为空值,其余字段正常返回
|
||||
|
||||
#### Scenario: 批量计算不放大查询
|
||||
|
||||
- **GIVEN** 一页返回多个商户池
|
||||
- **WHEN** 查询该页列表
|
||||
- **THEN** 系统以批量方式读取成员与成功累计,查询次数不随池数量线性增长
|
||||
@@ -0,0 +1,39 @@
|
||||
## Purpose
|
||||
|
||||
按原始需求(`111.md` §22.4.1)为报表导出补齐「序号」列,使导出的 Excel 与页面列表一致可直接对行定位。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 报表权限、数据范围与导出冻结
|
||||
|
||||
仅超级管理员与平台用户 SHALL 查询或导出报表;其余用户类型 MUST 被拒绝,且无权限与目标不存在 MUST NOT 形成可枚举差异。
|
||||
|
||||
查询与导出 MUST 按请求人可见店铺范围过滤快照行;越权与不存在 MUST 统一按资源不可见处理。
|
||||
|
||||
导出 MUST 复用既有异步导出任务:创建时冻结筛选条件、操作者与可见店铺范围,派发时冻结表头,执行期 MUST 只读快照表并施加冻结的店铺范围。导出列 MUST 与页面展示字段一致并包含合计行;第一列 MUST 为「序号」,按导出结果的展示顺序从 1 开始连续递增,合计行的序号列 MUST 留空(不写入任何数字或文字),且合计行其余各列的既有表示 MUST NOT 因新增序号列而改变。分母为零的比率或卡均 MUST 写作「-」;导出 MUST NOT 包含顶部文字总结。执行期 MUST 按任务内冻结的账号类型复核导出资格,MUST NOT 因创建者角色、店铺归属或筛选条件变化扩大数据集或重新解释筛选。
|
||||
|
||||
#### Scenario: 无权用户类型请求报表
|
||||
|
||||
- **WHEN** 代理、企业或个人客户账号请求报表或创建报表导出
|
||||
- **THEN** 系统拒绝请求且不返回任何统计结果,不区分无权限与不存在
|
||||
|
||||
#### Scenario: 导出与列表同筛选同口径
|
||||
|
||||
- **WHEN** 以同一筛选条件调用汇总查询并创建导出
|
||||
- **THEN** 导出行集合与汇总结果一致,列与页面展示字段一致且包含合计行
|
||||
|
||||
#### Scenario: 导出包含序号列
|
||||
|
||||
- **GIVEN** 一次激活情况或套餐续费导出产出 N 条分组行与一行合计
|
||||
- **WHEN** 读取导出文件
|
||||
- **THEN** 首列为序号,分组行序号为 1 至 N 连续递增,合计行的序号列不写入数字
|
||||
|
||||
#### Scenario: 创建后权限变化
|
||||
|
||||
- **WHEN** 导出任务创建后创建者的可见店铺范围发生变化再执行该任务
|
||||
- **THEN** 导出结果仍不超过创建时冻结的范围与筛选条件
|
||||
|
||||
#### Scenario: 执行期账号类型复核
|
||||
|
||||
- **WHEN** 导出任务执行时任务内冻结的账号类型不是超级管理员或平台用户
|
||||
- **THEN** 任务失败并记录安全失败摘要,不产出数据文件
|
||||
@@ -0,0 +1,119 @@
|
||||
## Purpose
|
||||
|
||||
按原始需求(`111.md` §12.3、§12.5、§12.6、§17.2)补齐退款链路的必填校验、申请材料、渠道标识落库与审批材料字段,并提供按订单类型查询可选退款方式的能力,使退款审批与财务对账具备完整依据。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 退款实收金额与方式矩阵
|
||||
|
||||
系统 SHALL 从来源订单或原成功支付记录派生并冻结权威实收金额,提交人不得填写或修改;无法确定权威实收金额或该金额非正时拒绝创建申请。冻结来源为:线上支付取该订单原成功支付记录金额;资产钱包、代理主钱包和后台线下套餐订单取订单实际收款或实际扣款金额。退款金额 MUST 为正分且不得超过冻结实收金额,系统 MUST 在申请创建、审批提交和企业微信通过后的执行前重复校验该上限。一笔订单最多一张最终成功退款申请,成功后该订单不得再申请退款。代理充值预存款业务单不在本期退款范围。
|
||||
|
||||
可选方式按来源订单的实际支付方式确定,不匹配的方式返回「该订单不支持此退款方式」:线上微信、支付宝或富友支付仅可选原路退款或客户收款信息退款;资产钱包支付仅退回原资产钱包;代理预存款或主钱包支付仅退回原代理钱包;后台线下和员工代收套餐订单仅可选客户收款信息退款。系统 MUST NOT 为本能力新增人工退款方式开关。客户收款信息退款必须同时提供客户收款信息自由文本和至少一个客户凭证附件,且不得复用公司收款方式字典。
|
||||
|
||||
线上订单原路可退条件按渠道契约在创建、提交和执行前重复预检:冻结实际商户的退款必需凭证不完整、服务商类型不具备退款能力、或原交易超出渠道可退时限时禁用原路并说明原因,只允许客户收款信息退款。富友原交易的原始日期必须回传渠道;未回传时仅支持 30 天内原交易,回传后可退 360 天内原交易,超出该范围的申请不得选择原路。
|
||||
|
||||
系统 SHALL 提供按来源订单查询可选退款方式的只读接口,返回可选方式集合、每种方式当前是否可用与不可用原因、原收款商户标识与名称、原支付渠道交易流水号与商户退款能力校验结果。该查询 MUST 与创建、重提使用同一方式判定实现,且原路可退的凭证判定 MUST 复用执行前预检所用的同一凭证判定来源,MUST NOT 引入第二套独立判定或独立开关;审批提交只消费已冻结的申请材料,MUST NOT 在提交环节引入新的方式判定。判定所需事实缺失时 MUST 返回不可用原因而非报错。查询 MUST 受既有订单数据范围约束,越权与订单不存在 MUST 不可区分。
|
||||
|
||||
#### Scenario: 无权威实收金额
|
||||
|
||||
- **WHEN** 来源订单无法取得权威实收金额,或取得的金额非正
|
||||
- **THEN** 系统拒绝创建退款申请,不允许提交人以自填金额替代
|
||||
|
||||
#### Scenario: 提交人试图修改冻结实收金额
|
||||
|
||||
- **WHEN** 创建或重提请求携带与派生值不同的实收金额
|
||||
- **THEN** 系统仍使用派生值作为冻结实收金额,不接受请求值
|
||||
|
||||
#### Scenario: 钱包订单申请退款
|
||||
|
||||
- **WHEN** 已支付套餐订单的实际支付方式为资产钱包或代理主钱包
|
||||
- **THEN** 系统只提供退回对应原钱包方式,不展示原路或客户收款信息退款
|
||||
|
||||
#### Scenario: 原路凭证不完整
|
||||
|
||||
- **GIVEN** 订单为线上支付且其冻结实际商户缺少该服务商类型退款必需凭证
|
||||
- **WHEN** 调用者申请退款
|
||||
- **THEN** 系统禁用原路退款并返回原因,只允许客户收款信息退款
|
||||
|
||||
#### Scenario: 富友原交易超出可退时限
|
||||
|
||||
- **WHEN** 富友原交易的原始支付时间早于可退时限
|
||||
- **THEN** 系统禁用原路退款并说明原因,只允许客户收款信息退款
|
||||
|
||||
#### Scenario: 查询可选退款方式
|
||||
|
||||
- **GIVEN** 一张线上支付订单的原收款商户凭证不完整
|
||||
- **WHEN** 授权账号查询该订单的可选退款方式
|
||||
- **THEN** 系统返回客户收款信息退款为可用、原路退款为不可用及缺失凭证原因,并返回原收款商户与原支付渠道交易流水号
|
||||
|
||||
#### Scenario: 查询越权订单
|
||||
|
||||
- **WHEN** 调用者查询其数据范围外订单的可选退款方式
|
||||
- **THEN** 响应与订单不存在不可区分
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 退款原因必填与申请人备注
|
||||
|
||||
退款申请创建与重新提交时 SHALL 强制填写退款原因:原因去除首尾空白后为空 MUST 以参数非法错误拒绝,MUST NOT 落库申请或审批尝试。该校验 MUST 只作用于创建与重提用例:历史申请与补发历史审批的路径不受本约束、MUST NOT 因历史原因缺失而阻断。退款申请 SHALL 支持可选申请人备注,创建与重提均可填写或替换,并 MUST 冻结进当次审批尝试快照;既有申请的历史尝试快照 MUST NOT 因重提被改写,且申请人备注 MUST NOT 复用或改写退款单既有的审批备注字段(该字段语义与展示口径不变)。退款原因与备注的冻结语义 MUST 与既有实收金额、方式、金额、材料快照一致,MUST 随每次重提新增而非覆盖。
|
||||
|
||||
#### Scenario: 原因为空被拒绝
|
||||
|
||||
- **WHEN** 创建或重提请求的退款原因缺失或仅含空白字符
|
||||
- **THEN** 系统以参数非法错误拒绝,且不创建申请、不创建审批实例
|
||||
|
||||
#### Scenario: 备注冻结进当次快照
|
||||
|
||||
- **WHEN** 提交人填写备注后提交退款申请
|
||||
- **THEN** 该次审批尝试快照保存该备注,后续重提修改备注 MUST NOT 改写历史尝试快照
|
||||
|
||||
### Requirement: 退款渠道标识与线下处理流水号
|
||||
|
||||
退款单 MUST 持久化并可查询「来源支付单号」与「原支付渠道交易流水号」,不得仅通过订单与支付单关联在读取时推导;两者取创建申请时冻结的原成功支付事实;重提时两者 MUST 与冻结实收金额同批从同一原成功支付事实重新冻结,历史尝试快照 MUST NOT 被改写。原路退款成功后的渠道退款流水号沿用既有事实,MUST NOT 被本要求改变。退款申请 SHALL 支持登记线下退款处理流水号或凭证编号:仅客户收款信息退款(线下到账)方式允许由授权账号在申请创建后补录或更正,其余退款方式 MUST NOT 登记(登记请求 MUST 以状态非法拒绝);每次登记 MUST 写入审计(操作者、时间、前后值),MUST NOT 因登记改变退款状态、实收金额、套餐失效或佣金回溯规则。列表、详情与导出 SHALL 返回来源支付单号、原支付渠道交易流水号与线下退款处理流水号。
|
||||
|
||||
#### Scenario: 退款单保存来源支付事实
|
||||
|
||||
- **GIVEN** 来源订单存在成功支付记录
|
||||
- **WHEN** 创建退款申请
|
||||
- **THEN** 退款单持久化该来源支付单号与原支付渠道交易流水号,列表、详情与导出均返回这两个值
|
||||
|
||||
#### Scenario: 原支付事实缺失
|
||||
|
||||
- **GIVEN** 来源订单为后台线下订单或员工代收订单,没有线上支付记录
|
||||
- **WHEN** 创建退款申请
|
||||
- **THEN** 两个字段以空值保存并返回,不阻断申请创建
|
||||
|
||||
#### Scenario: 登记线下退款处理流水号
|
||||
|
||||
- **GIVEN** 一笔客户收款信息退款已完成
|
||||
- **WHEN** 授权账号登记线下退款处理流水号或凭证编号
|
||||
- **THEN** 系统保存该值并写审计,退款状态、实收金额与佣金回溯行为不变
|
||||
|
||||
#### Scenario: 登记操作留痕
|
||||
|
||||
- **WHEN** 同一字段被再次更正
|
||||
- **THEN** 审计记录包含操作者、时间与前后值,历史值可追溯
|
||||
|
||||
### Requirement: 企业微信退款审批材料字段
|
||||
|
||||
退款审批材料 SHALL 在既有字段之外带出:资产类型(卡或设备)、设备类型与设备型号(资产为设备时)、当前退款套餐已用量与套餐总量、原支付渠道交易流水号。套餐已用量与总量 MUST 采用与退款展示字段相同的解析规则与真流量口径;资产类型 MUST 沿用退款申请的下单快照推导;设备类型与设备型号 MUST 取退款申请关联设备,无关联设备时以空值提交;原支付渠道交易流水号 MUST 取创建申请时冻结的原成功支付事实。上述字段 MUST 在提交时冻结进当次审批尝试快照,使审批人在企业微信侧可直接看到用量数字。任一材料在提交时不可解析 MUST 以空值或零值提交并 MUST NOT 阻断审批提交;材料 MUST NOT 包含凭证内容、完整收款文本或商户密钥。
|
||||
|
||||
企业微信模板控件缺失导致本次新增字段无法提交时 MUST 明确失败并提示需配置控件,MUST NOT 静默丢弃该字段;本约束 MUST 只作用于本次新增字段,既有未映射的可选控件 MUST 保持既有跳过行为。
|
||||
|
||||
#### Scenario: 审批材料带出套餐用量
|
||||
|
||||
- **GIVEN** 退款申请关联的套餐使用记录存在真已用量与真总量
|
||||
- **WHEN** 提交企业微信退款审批
|
||||
- **THEN** 审批材料包含资产类型、设备类型与型号(适用时)、套餐已用量与总量及原支付渠道交易流水号
|
||||
|
||||
#### Scenario: 套餐事实不可解析
|
||||
|
||||
- **GIVEN** 退款申请未关联任何套餐使用记录
|
||||
- **WHEN** 提交企业微信退款审批
|
||||
- **THEN** 套餐已用量与总量以零值或空值提交,审批提交正常完成
|
||||
|
||||
#### Scenario: 模板缺少控件
|
||||
|
||||
- **GIVEN** 企业微信退款审批场景未配置套餐用量控件映射
|
||||
- **WHEN** 提交退款审批
|
||||
- **THEN** 系统明确失败并指出缺失控件,不静默丢弃该字段
|
||||
@@ -0,0 +1,95 @@
|
||||
## Purpose
|
||||
|
||||
按原始需求(`111.md` §15.7)为后台关联查看补齐「绑定资产数量」与「最近解绑人」两个投影字段,使运营无需逐条计数即可判断某手机号还可绑定多少资产、并在解绑后直接看到处理人。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 后台查看关联
|
||||
|
||||
仅超级管理员和平台用户可在其资产数据范围内查看关联手机号。资产详情 MUST 列出该资产全部当前关联手机号;资产列表、卡与设备两类导出以及批量任务结果中,具备资产数据权限的账号可见完整关联手机号,不做脱敏。按资产查询 MUST 一次批量聚合完成。后台 MUST NOT 提供补录或修改关联的入口。
|
||||
|
||||
关联查询 SHALL 额外返回两项投影:该手机号当前有效关联资产数量,以及每条关系的最近解绑人与最近解绑时间。数量口径 MUST 与该手机号「最多关联十项」的上限判定完全一致(只统计当前有效关系、卡与设备分别计一项),MUST 按当页涉及的手机号集合一次批量聚合,MUST NOT 逐手机号或逐资产放大查询次数。最近解绑人 MUST 取最近一次解除该关系的后台账号名称与账号标识快照,并随既有解绑方式、解绑时间、解绑原因一并返回;关系仍有效时该字段为空,MUST NOT 以创建人或最后更新人填充。以上字段 MUST 只读,MUST NOT 改变既有解绑、换绑、上限判定与审计规则。
|
||||
|
||||
#### Scenario: 数据范围外不可见
|
||||
|
||||
- **WHEN** 平台用户查询其资产数据范围之外的资产的关联手机号
|
||||
- **THEN** 系统不返回该资产的关联手机号
|
||||
|
||||
#### Scenario: 列表一次聚合
|
||||
|
||||
- **WHEN** 请求一页包含多项资产的列表
|
||||
- **THEN** 系统按该页资产集合一次读取关联手机号并按资产装配,不逐资产查询
|
||||
|
||||
#### Scenario: 解绑后仍可查看完整快照
|
||||
|
||||
- **WHEN** 一次批量解绑任务执行完成、关系已失效后查看该任务结果
|
||||
- **THEN** 结果中仍返回该次解除时记录的完整手机号快照
|
||||
|
||||
#### Scenario: 两类导出均含关联手机号
|
||||
|
||||
- **WHEN** 导出 IoT 卡场景或设备场景
|
||||
- **THEN** 两类导出均包含关联手机号列,且设备导出列组解析对新增列保持正确
|
||||
|
||||
#### Scenario: 数量与上限口径一致
|
||||
|
||||
- **GIVEN** 某手机号当前有效关联十项资产
|
||||
- **WHEN** 查询其中任一关联
|
||||
- **THEN** 返回的当前有效关联资产数量为十,与上限判定使用的计数相同
|
||||
|
||||
#### Scenario: 已解绑关系展示解绑人
|
||||
|
||||
- **GIVEN** 一条关系已被后台账号 A 通过单项解绑失效
|
||||
- **WHEN** 查询该关系
|
||||
- **THEN** 返回最近解绑人为 A 的名称与标识、解绑时间与解绑原因
|
||||
|
||||
#### Scenario: 有效关系无解绑人
|
||||
|
||||
- **WHEN** 查询一条仍有效的关联
|
||||
- **THEN** 最近解绑人与最近解绑时间均为空,且不以创建人或更新时间填充
|
||||
|
||||
#### Scenario: 数量批量聚合
|
||||
|
||||
- **GIVEN** 一页返回多项资产或手机号的关联
|
||||
- **WHEN** 查询该页
|
||||
- **THEN** 系统按该页涉及的手机号集合一次读取计数,查询次数不随行数线性增长
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 短信验证码校验失败次数限制
|
||||
|
||||
系统 SHALL 对短信验证码校验实施失败次数限制,作用域覆盖注册、绑定、换绑与换证的全部短信验证码校验流程:失败计数按窗口续期维护——每次校验失败在窗口内累加计数并重置窗口时长,相邻两次失败间隔不超过窗口时长即持续累加;计数达到上限后短时间内的后续校验 MUST 被拒绝并返回可定位的限流错误码与提示;校验成功 MUST 清零该手机号的失败计数。失败计数与锁定 MUST 以手机号维度按原子操作维护,并发提交 MUST NOT 绕过计数;锁定检查 MUST 先于验证码比对,锁定期内即使提交正确验证码也 MUST NOT 通过;锁定期为自计数达到上限起的一个窗口时长,锁定期间的提交在比对前即被拒绝且不再计数,因此锁定不因再次提交而续期;锁定到期后 MUST 自动恢复可校验状态,MUST NOT 需要人工解锁。阈值与窗口 MUST 为固定常量或既有服务端配置,MUST NOT 新增对外可配置项或接口。错误提示 MUST NOT 泄露验证码正确性、剩余失败次数或内部键名。
|
||||
|
||||
失败计数的写入或读取失败 MUST NOT 阻断校验流程的成功路径:计数不可用时系统 MUST 放行并记录,MUST NOT 因限流计数故障阻断注册、绑定、换绑、换证与登录的成功路径。发送侧既有频率限制、验证码有效期与一次性消费语义 MUST 保持不变;校验失败 MUST NOT 消费验证码,MUST NOT 影响既有链路的成功结果。
|
||||
|
||||
#### Scenario: 连续失败达到上限
|
||||
|
||||
- **WHEN** 同一手机号在窗口内连续提交错误验证码达到上限
|
||||
- **THEN** 后续校验被拒绝并返回限流错误,即使提交的是正确验证码也不通过
|
||||
|
||||
#### Scenario: 锁定到期恢复
|
||||
|
||||
- **GIVEN** 某手机号因失败次数达到上限被短时锁定
|
||||
- **WHEN** 锁定窗口结束且提交正确验证码
|
||||
- **THEN** 系统正常通过校验
|
||||
|
||||
#### Scenario: 成功后清零
|
||||
|
||||
- **GIVEN** 某手机号已有若干次失败计数但未达上限
|
||||
- **WHEN** 一次校验成功
|
||||
- **THEN** 该手机号失败计数被清零,后续失败重新计数
|
||||
|
||||
#### Scenario: 并发提交不绕过计数
|
||||
|
||||
- **WHEN** 同一手机号并发提交多个错误验证码
|
||||
- **THEN** 失败计数按实际提交次数累加,不因并发而丢失计数
|
||||
|
||||
#### Scenario: 失败不消费验证码
|
||||
|
||||
- **WHEN** 一次校验因验证码错误或限流失败
|
||||
- **THEN** 该验证码保持可再次校验(在有效期与计数限制允许范围内),不产生任何业务事实
|
||||
|
||||
#### Scenario: 计数不可用不阻断成功路径
|
||||
|
||||
- **GIVEN** 失败计数所依赖的存储不可用
|
||||
- **WHEN** 注册、绑定、换绑、换证或登录提交正确验证码
|
||||
- **THEN** 校验正常通过,不因计数故障被拒绝,且计数不可用的事实被记录
|
||||
@@ -0,0 +1,91 @@
|
||||
## Purpose
|
||||
|
||||
按原始需求(`111.md` §23.4、§23.6、§23.7、§23.9、§23.10)补齐优先轮询的出队出口与事实留痕:增加人工关闭、有效期到期出队与失败后人工重触发,补全队列持久化与读侧字段,并提供异常与重试记录的可查入口。
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 优先项尝试次数与出队
|
||||
|
||||
优先轮询项 SHALL 使用自身固定的最大尝试次数常量(3),MUST NOT 新增可维护的参数配置项,MUST NOT 改变普通轮询的失败重试策略。尝试次数 MUST 只在真正发起执行后累加。未达上限的可恢复失败 MUST 回到活动状态并沿用既有轮询间隔重新排期;达到上限 MUST 记录失败终态与可安全展示的失败原因后出队。卡不存在等业务校验类失败 MUST 直接进入失败终态且不重试。执行成功 MUST 记录完成终态并出队,资产继续按普通轮询运行。
|
||||
|
||||
优先项 SHALL 支持人工关闭:具备卡数据权限的超级管理员与平台账号可对未完成(待执行或执行中)的项执行关闭,关闭 MUST 填写原因并 MUST 记录操作者、时间与原因审计;关闭后该项进入已关闭终态并出队,MUST NOT 再发起任何上游调用,MUST NOT 删除既有执行结果与失败原因。
|
||||
|
||||
优先项 SHALL 具备有效期:有效期取值 MUST 为固定常量(本能力不新增可维护配置项),长度与尝试上限常量一并由服务端固定。入队时间起超过有效期的未完成项 MUST 转入已过期终态并出队,MUST 保留触发类型、尝试次数与最近失败原因,MUST NOT 自动删除事实。既有未完成项 SHALL 自本次上线时刻起算有效期,MUST NOT 在上线后被批量判定为已过期。到期处理 MUST 由独立的周期任务承载,MUST NOT 依赖查询或读侧隐式触发。已过期与已失败的项 SHALL 支持人工重新触发:重新触发 MUST 复用人工优先入队入口为该卡该任务类型建立新的活动项并将尝试次数置零,MUST 记录操作者与原因,MUST NOT 绕过既有并发上限、停复机持锁拒绝与任务类型范围。
|
||||
|
||||
本能力 MUST NOT 建立独立异常记录表:异常、超限与重试事实由优先项自身状态、尝试次数与执行结果承载。
|
||||
|
||||
#### Scenario: 可恢复失败未达上限
|
||||
|
||||
- **WHEN** 一次优先执行因上游调用超时、失败或响应无效而可恢复失败,且尝试次数未达上限
|
||||
- **THEN** 该项回到活动状态并按既有轮询间隔重新排期
|
||||
|
||||
#### Scenario: 达到最大尝试次数
|
||||
|
||||
- **WHEN** 尝试次数达到上限仍未能成功执行
|
||||
- **THEN** 该项记录失败终态与可安全展示的失败原因后出队,该卡继续普通轮询
|
||||
|
||||
#### Scenario: 卡不存在
|
||||
|
||||
- **WHEN** 优先执行时该卡已不存在
|
||||
- **THEN** 该项直接进入失败终态,不重试也不发起上游调用
|
||||
|
||||
#### Scenario: 执行成功
|
||||
|
||||
- **WHEN** 优先执行按既有轮询内容完成
|
||||
- **THEN** 该项记录完成终态并出队,资产继续存在于普通轮询
|
||||
|
||||
#### Scenario: 人工关闭
|
||||
|
||||
- **WHEN** 授权账号对一项待执行或执行中的优先项填写原因后执行关闭
|
||||
- **THEN** 该项进入已关闭终态并出队,不再发起上游调用,原因与操作者写入审计
|
||||
|
||||
#### Scenario: 有效期到期
|
||||
|
||||
- **GIVEN** 一项优先项自入队起已超过固定有效期且仍未完成
|
||||
- **WHEN** 调度或维护流程处理该项
|
||||
- **THEN** 该项转入已过期终态并出队,触发类型、尝试次数与最近失败原因被保留
|
||||
|
||||
#### Scenario: 失败项人工重触发
|
||||
|
||||
- **GIVEN** 一项优先项已因达到尝试上限失败或因超期出队
|
||||
- **WHEN** 授权账号填写原因后重新触发
|
||||
- **THEN** 系统为该卡该任务类型建立新的活动项且尝试次数从零开始,并记录操作者与原因
|
||||
|
||||
#### Scenario: 重触发不绕过既有边界
|
||||
|
||||
- **GIVEN** 目标卡持运营商通道阈值停机锁
|
||||
- **WHEN** 授权账号对该卡重新触发优先轮询
|
||||
- **THEN** 系统按既有持锁拒绝语义处理,不发起复机
|
||||
|
||||
### Requirement: 优先轮询事实与查询的可追溯
|
||||
|
||||
系统 SHALL 记录入队(含合并)、领取、每次失败、重试、完成、人工关闭、超期、人工重触发与失败出队的触发类型、触发来源、操作来源、时间与结果。人工入队与人工重触发 MUST 填写原因,并与事实一同保存。
|
||||
|
||||
优先项事实 SHALL 持久化并可查询下列字段:资产类型与资产标识、设备号(资产绑定设备时)、卡 ICCID、店铺标识与代理归属、任务类型、触发类型与触发来源集合、触发次数与最近触发时间、尝试次数与最大尝试次数、执行开始时间、执行结束时间与执行耗时、下一次计划执行时间、入队时间、出队时间、执行结果与失败原因、接口请求与响应摘要引用(复用既有集成交互日志标识,MUST NOT 复制凭证或响应原文)。
|
||||
|
||||
系统 MUST 提供最小可读接口(列表与详情,支持筛选与分页),筛选至少支持状态、任务类型、触发类型、时间范围,以及「异常与重试」视图所需的条件(失败终态、已过期、尝试次数达到上限、存在失败原因)。该视图 MUST 复用同一事实表与同一权限,MUST NOT 新增独立异常事实表。权限 MUST 与人工入队一致:超级管理员与平台账号可查询全部,代理账号仅可查询其自身及下级店铺资产,企业账号 MUST 被拒绝;查询 MUST 按数据权限下推,越权与不存在 MUST 不可区分。查询 MUST NOT 提供优先级分级;有效期、出队时间与最大尝试次数 SHALL 作为只读字段返回;人工关闭与人工重触发 SHALL 为独立受权限控制的写入口,MUST NOT 由查询接口隐式触发。
|
||||
|
||||
#### Scenario: 代理查询范围外资产
|
||||
|
||||
- **WHEN** 代理账号查询不在其数据范围内的优先轮询项
|
||||
- **THEN** 响应与查询不存在的优先轮询项不可区分
|
||||
|
||||
#### Scenario: 企业账号入队或查询
|
||||
|
||||
- **WHEN** 企业账号提交优先入队或查询优先轮询项
|
||||
- **THEN** 系统拒绝
|
||||
|
||||
#### Scenario: 查询结果字段
|
||||
|
||||
- **WHEN** 授权账号查询优先轮询项列表或详情
|
||||
- **THEN** 响应给出资产标识、设备号(适用时)、店铺与代理归属、任务类型、触发类型与来源、触发次数、尝试次数与最大尝试次数、最近与下一次执行时间、执行结果与失败原因、入队与出队时间,且不含优先级分级
|
||||
|
||||
#### Scenario: 异常与重试视图
|
||||
|
||||
- **WHEN** 授权账号以失败终态或尝试次数达到上限作为筛选条件查询优先轮询项
|
||||
- **THEN** 系统返回对应项及其失败原因与尝试次数,数据来源于同一事实表
|
||||
|
||||
#### Scenario: 执行耗时与摘要引用
|
||||
|
||||
- **WHEN** 一项优先执行完成并产生上游交互
|
||||
- **THEN** 该项返回执行开始与结束时间、耗时以及集成交互日志标识,且不返回上游响应原文
|
||||
@@ -0,0 +1,141 @@
|
||||
# 实施任务:close-august-iteration-gaps
|
||||
|
||||
自动化测试按项目决策为 N/A;验证在维护者指定的测试环境(`junhong_cmp_test` 与测试 Redis,ENG-TEST-001)进行,仅创建与清理本 Change 的 fixture,禁止重置整库。
|
||||
|
||||
## 1. 迁移与数据基线
|
||||
|
||||
- [x] 1.1 新增成对迁移 `000232_add_h5_popup_type`:`tb_h5_popup_configuration` 加 `popup_type`(枚举 `promotion`/`announcement`,非空,默认 `announcement`)+ 取值 CHECK;up 以默认值回填既有行并核对行数;down 删列与约束
|
||||
- [x] 1.2 新增成对迁移 `000233_extend_refund_settlement_fields`:退款主表加 `source_payment_no`、`original_channel_trade_no`、`offline_settlement_no`、`offline_settled_at`、`offline_settled_by`;退款审批尝试表加 `remark`(申请人备注,逐次冻结);列注释写明冻结来源与补录语义,MUST NOT 复用或改写主表既有 `remark`(审批备注)
|
||||
- [x] 1.3 新增成对迁移 `000234_extend_polling_priority_item`:加 `asset_type`、`asset_id`、`device_no_snapshot`、`agent_shop_id_snapshot`、`attempt_limit`(默认 3)、`started_at`、`finished_at`、`next_run_at`、`dequeued_at`、`integration_log_id`、`priority_effective_from`;重建状态 CHECK 容纳 `closed` 与 `expired`;活动项部分唯一索引谓词保持 `status IN ('pending','processing')` 不变;为 `next_run_at` 建部分索引;既有活动项 `priority_effective_from` 回填为迁移时刻;资产列按既有设备—卡绑定回填,独立卡回填为卡自身
|
||||
- [x] 1.4 新增成对迁移 `000235_extend_carrier_threshold_hit`:锁表加命中累计流量、阈值数值、阈值单位、判定时间、触发来源、解锁时间、复机结果;判定时间回填既有行创建时间,命中数值与阈值留空;新增按通道、卡与判定时间的查询索引
|
||||
- [x] 1.5 新增成对迁移 `000236_add_phone_association_invalidator_name`:手机号—资产关联加最近解绑人名称快照列,注释写明审计侧仍只写脱敏值
|
||||
- [x] 1.6 新增成对迁移 `000237_extend_withdrawal_attempt_qualification`:提现审批尝试表加资料资格版本标识、校验时间、是否通过、未通过稳定原因列
|
||||
- [x] 1.7 在测试环境以 `scripts/migrate.sh` 与显式 `DB_*` 参数逐对执行 up / down / up,核对加列、默认值、回填结果与既有行可读性
|
||||
- [x] 1.8 核对既有优先轮询活动项不会被上线后首轮到期扫描批量过期;核对读侧对空列与默认值兼容(`next_run_at` 为空不判过期、命中数值为空按「无」展示)
|
||||
|
||||
## 2. 员工账单列表与详情
|
||||
|
||||
- [x] 2.1 扩展 `EmployeeCollectionBillListRequest`:新增账单编号(映射账单主键标识)精确、核销通过时间 `decided_start_time`/`decided_end_time`、欠款人姓名或账号模糊、客户名称模糊;既有「产生时间」参数由 `created_from`/`created_to` 改名为 `start_time`/`end_time` 并改用严格解析;补 OpenAPI 描述
|
||||
- [x] 2.2 在 `internal/query/employeecollection/bill.go` 实现新增筛选:编号精确;核销通过时间取「状态为已通过的分摊」关联到的申请表审批终态时间的相关子查询;姓名与客户名称子串匹配(保持输入长度上限);保持既有可见性与分页口径
|
||||
- [x] 2.3 明确并注释核销通过时间的列语义:MUST NOT 使用分摊表的释放时间(该列同时承担通过与释放两种语义)
|
||||
- [x] 2.4 列表响应并列返回未核销金额与剩余可核销金额,已关闭账单两者为零,保留既有剩余可核销校验逻辑
|
||||
- [x] 2.5 账单详情新增操作日志投影(动作、操作账号名称、时间、前后值摘要,不含凭证内容与完整收款原文)
|
||||
- [x] 2.6 账单详情新增来源订单快照投影(来源单号、业务类型、来源金额、来源创建时间、资产标识),来源不可读或字段缺失时返回空值且不阻断
|
||||
- [x] 2.7 三个时间筛选字段复用共享严格解析器与闭区间;旧参数名与旧格式(含 date-only)一律以参数非法拒绝
|
||||
- [x] 2.8 在测试环境 smoke:按编号、核销通过时间、姓名子串、客户名称子串各查询一次并核对预占场景下两个金额字段之差等于预占额;详情含操作日志与来源快照,来源缺失不阻断
|
||||
|
||||
## 3. 商户池列表与轮询边界
|
||||
|
||||
- [x] 3.1 `PaymentMerchantPoolResponse` 新增启用成员数、成员总数、当前命中成员、最近更新时间字段
|
||||
- [x] 3.2 抽取只读取路函数:金额/笔数取首个未达标成员、每轮累计全达标取首位、时间方式按当前时段;调用后 MUST NOT 推进统计世代、MUST NOT 创建支付单;在 `ListPools` 中按页批量计算成员计数与命中成员
|
||||
- [x] 3.3 调整 `chooseMerchant`:每轮累计全达标视为当轮结束并以条件更新递增统计世代(更新冲突按并发冲突拒绝创建);自然日累计与自然月累计全达标拒绝创建;单启用成员固定被选中且优先于自然周期拒绝
|
||||
- [x] 3.4 调整 `SavePool`:修改时间周期数值/单位、起始时间或成员顺序并保存成功时,由服务端以保存成功时间重算起点,忽略请求携带的旧起点
|
||||
- [x] 3.5 确认列表投影只读:查询前后统计世代不变、不新增支付单、不计入成功累计
|
||||
- [x] 3.6 在测试环境 smoke:单成员池达标后仍可下单;自然日/自然月全达标拒付并提示暂无可用商户;每轮累计全达标开新轮且世代递增;改时间配置后起点等于保存时刻;列表查询前后世代与支付单数不变
|
||||
|
||||
## 4. H5 运营弹窗类型
|
||||
|
||||
- [x] 4.1 配置实体与 DTO 新增 `popup_type` 必填字段(创建与更新均校验取值域),更新 OpenAPI 描述
|
||||
- [x] 4.2 候选排序改为先按类别(风险换卡 > 推广 > 公告)再按显式优先级,缺省优先级取类型默认值;类别顺序使用显式排序表达式,MUST NOT 依赖类型取值的字典序
|
||||
- [x] 4.3 迁移回填后的既有配置 smoke:确认同类内相对顺序与显式优先级不变,且推广配置优先于显式优先级更高的公告配置
|
||||
- [x] 4.4 `popup_type` 仅在创建时必填;更新不传保持原值(一经传入仍 MUST 按同一取值域校验);类型变更且请求未显式传优先级时保持原优先级(类型缺省优先级只在创建路径生效)——措辞已按 Spec 口径收窄,见 h5-popup-notification 主 Spec 与 delta
|
||||
|
||||
## 5. 退款申请、展示与审批材料
|
||||
|
||||
- [x] 5.1 退款创建与重提用例强制退款原因必填(去空白后非空),拒绝时不落库申请与审批尝试;MUST NOT 把该校验加入审批尝试构造与补发历史审批路径(历史申请与补发不追溯)
|
||||
- [x] 5.2 退款创建与重提新增申请人备注,冻结进当次审批尝试快照(尝试表新列),历史快照不被改写;主表既有备注语义不变
|
||||
- [x] 5.3 退款单写入冻结的来源支付单号与原支付渠道交易流水号;无线上支付记录的订单以空值保存
|
||||
- [x] 5.4 新增线下退款处理流水号补录与更正用例与接口,写审计(操作者、时间、前后值),不改变退款状态、实收金额、套餐失效与佣金回溯规则
|
||||
- [x] 5.5 列表、详情与导出返回来源支付单号、原支付渠道交易流水号、线下退款处理流水号
|
||||
- [x] 5.6 新增按订单查询可选退款方式接口:返回方式集合、可用性与不可用原因、原收款商户标识与名称、原渠道交易流水号、退款能力校验结果;复用既有方式判定与凭证判定实现;字面量路径先于同组参数路由注册
|
||||
- [x] 5.7 退款审批材料新增资产类型、设备类型、设备型号、套餐已用量与总量、原支付渠道交易流水号;用量复用既有展示解析规则与真流量口径;设备维度取退款请求关联设备;冻结进审批尝试快照
|
||||
- [x] 5.8 控件强制范围仅限本次新增字段:场景白名单标注必须映射,场景映射校验缺映射即明确失败,表单构建缺快照值即明确失败并提示缺失控件;既有未映射的可选控件保持静默跳过
|
||||
- [x] 5.9 提交与企微通过后执行前不新增第二套方式判定(沿用既有方式判定与凭证判定实现)
|
||||
- [x] 5.10 在测试环境 smoke:原因为空被拒;备注冻结且历史尝试不变;线下订单两个流水号为空;可选方式接口返回原路不可用原因;企微材料含套餐用量与设备维度;模板缺控件时提交明确失败
|
||||
|
||||
## 6. 优先轮询出口、留痕与详情投影
|
||||
|
||||
- [x] 6.1 入队与融合写入时补齐新列:资产类型与资产 ID、设备号快照、代理归属、尝试上限、有效期起算时间
|
||||
- [x] 6.2 执行路径写执行开始/结束时间与集成交互日志标识;调度路径写下次计划执行时间;出队时写出队时间
|
||||
- [x] 6.3 新增人工关闭用例与接口:原因必填、写审计、未完成项转已关闭终态出队、不再发起上游调用;权限限定超级管理员与平台账号
|
||||
- [x] 6.4 新增独立周期任务类型承载有效期到期处理:固定常量有效期,超期未完成项转已过期终态出队并保留触发类型、尝试次数与最近失败原因;按既有周期任务注册形态注册到任务处理器与 Worker 调度;不新增可维护配置项
|
||||
- [x] 6.5 新增失败或已过期项的人工重触发:复用人工入队入口建立新活动项、尝试次数归零、写操作者与原因;不绕过并发上限、停复机持锁拒绝与任务类型范围
|
||||
- [x] 6.6 查询接口补齐返回字段(资产标识、设备号、店铺与代理归属、任务类型、触发与尝试次数、最大尝试次数、最近与下一次执行时间、执行起止与耗时、入队与出队时间、结果与失败原因、集成交互日志标识),并新增状态/任务类型/触发类型/时间范围与失败或超限筛选;时间范围走统一参数与闭区间
|
||||
- [x] 6.7 既有资产解析端点的卡与设备两个分支返回只读优先轮询状态投影(是否在优先轮询中、最近触发场景、最近轮询结果、最近轮询时间、失败原因),读取不改变队列且受数据范围约束
|
||||
- [x] 6.8 确认新终态(已关闭、已过期)在状态约束、活动项唯一键与读侧投影下均可读且不占活动项键位
|
||||
- [x] 6.9 更新路由说明文案:移除「不提供有效期与人工重触发入口」等与本次相反的描述
|
||||
- [x] 6.10 在测试环境 smoke:人工关闭、超期出队、失败项重触发、异常与重试筛选视图、资产详情投影读取前后队列不变
|
||||
|
||||
## 7. 通道阈值命中留痕
|
||||
|
||||
- [x] 7.1 达量判定事务内写入命中事实(累计流量读数、阈值、单位、周期起点、判定时间、触发来源);留痕写入失败 MUST NOT 放行或延迟停机
|
||||
- [x] 7.2 周期恢复路径写入解锁时间与复机结果或跳过原因,且不覆盖停机命中事实
|
||||
- [x] 7.3 新增顶层路径的命中记录查询端点(自带超级管理员与平台用户门禁、按卡数据范围下推、越权与不存在不可区分),支持按通道、卡、计费周期与判定时间范围筛选;时间范围走统一参数与闭区间
|
||||
- [x] 7.4 历史行兼容:判定时间为既有行创建时间,命中数值与阈值为空时按「无」展示
|
||||
- [x] 7.5 在测试环境 smoke:造一条达量命中并核对返回的用量与阈值数字,改阈值后历史命中仍返回命中时数值,新周期恢复留痕不覆盖命中事实
|
||||
|
||||
## 8. 代理注册审批与提现留痕
|
||||
|
||||
- [x] 8.1 代理注册审批材料新增「业务员」字段(上级店铺当前业务员名称快照,为空时以无标记提交),控件缺失时明确失败(仅新增字段强制映射)
|
||||
- [x] 8.2 提现提交与重提记录本次依据的资料资格版本标识与校验结果(时间、是否通过、未通过稳定原因),冻结进审批尝试快照并在提现详情返回;不改写历史尝试
|
||||
- [x] 8.3 店铺列表与详情返回直接下级代理数量,按页批量聚合、受数据范围约束、无下级返回零
|
||||
- [x] 8.4 落地迁移 000237 的列语义与稳定原因取值(资格不存在、已失效、已停用、资料未通过审批)
|
||||
- [x] 8.5 在测试环境 smoke:上级无业务员时的材料字段、资格被替换后历史尝试仍返回原校验结果、下级数量统计
|
||||
|
||||
## 9. 手机号关联与验证码限制
|
||||
|
||||
- [x] 9.1 关联查询返回手机号当前有效关联数量,与既有「最多十项」上限判定共用同一计数口径并按页批量聚合
|
||||
- [x] 9.2 关联查询返回最近解绑人名称快照、账号标识与解绑时间;有效关系该三字段为空且不以创建人或更新时间填充
|
||||
- [x] 9.3 短信验证码校验在 Redis 以手机号维度原子累加失败计数,达到阈值后短时锁定并返回既有请求过多错误码;锁定检查先于验证码比对,锁定期内即使验证码正确也拒绝,窗口内校验成功清零;阈值与窗口为固定常量,提示不泄露验证码正确性、剩余次数与内部键名
|
||||
- [x] 9.4 计数写入失败时放行并记录,MUST NOT 因限流计数故障阻断注册、绑定、换绑、换证与登录成功路径;校验失败不消费验证码的既有语义保持不变
|
||||
- [x] 9.5 在测试环境 smoke:连续错误达阈值后被拒(含提交正确验证码)、锁定到期恢复、成功后清零、失败不消费验证码
|
||||
|
||||
## 10. 导出列补齐
|
||||
|
||||
- [x] 10.1 佣金明细导出按固定列序补齐十五列(店铺、业务员、用户组、资产类型、设备类型、设备型号、资产标识、订单号、下单时间、佣金金额、佣金来源、佣金状态、关联佣金明细、入账后金额、创建时间);归属与设备列按导出执行时当前事实补充,缺值以空值导出;回溯行金额为负、可提现为不可提现
|
||||
- [x] 10.2 报表导出首列新增序号:分组行按展示顺序连续递增,合计行序号列留空且不改合计行其他列的既有表示
|
||||
- [x] 10.3 确认两类导出不改既有导出粒度、创建期冻结与执行期只读语义
|
||||
- [x] 10.4 在测试环境或本地导出 smoke:核对列名、列序、负数与空值处理 —— CSV 产物级已验证(表头 20 列、前 15 列为规定列序、缺值空、回溯行负值、报表序号与合计行空);HTTP 异步导出链路(POST /api/admin/export-tasks → asynq 分片 → 对象存储下载)未覆盖
|
||||
|
||||
## 11. 时间筛选契约
|
||||
|
||||
- [x] 11.1 本变更已新增 `export-time-filter` delta:受影响端点表纳入员工账单列表(含既有产生时间参数改名)、优先轮询项查询、通道阈值命中查询,并写明各端点筛选的时间字段;实现须与该 delta 一致
|
||||
- [x] 11.2 三处新增筛选一律复用既有共享严格解析器与闭区间语义,不得复制既有账单日期解析或客户端可选时间解析
|
||||
- [x] 11.3 员工账单产生时间使用统一参数名,旧参数名与旧格式一律拒绝;核销通过时间使用 `decided_start_time`/`decided_end_time`
|
||||
- [x] 11.4 优先轮询查询按入队时间、阈值命中查询按判定时间使用统一参数名
|
||||
- [x] 11.5 在测试环境 smoke:三处筛选的拒绝集(小数秒、无时区、date-only、空格分隔、`±hhmm`、未补零、非法日历日期)与边界语义(含两端、开始晚于结束拒绝)与既有契约一致
|
||||
|
||||
## 12. 端点、路由、文档与生成物
|
||||
|
||||
- [x] 12.1 按下列清单落地新增端点(方法、路径、权限主体);变更既有端点不新增路由
|
||||
|
||||
| 方法 | 路径 | 权限主体 | 来源任务 |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/admin/refunds/order-options` | 退款组门禁(超级管理员/平台/代理,企业账号拒绝)+ 订单数据范围 | 5.6 |
|
||||
| POST | `/api/admin/refunds/:id/offline-settlement` | 同退款组门禁 + 订单数据范围 | 5.4 |
|
||||
| POST | `/api/admin/polling-priority-items/:id/close` | 超级管理员与平台账号 | 6.3 |
|
||||
| POST | `/api/admin/polling-priority-items/:id/retrigger` | 与人工入队一致(含数据范围内的代理账号) | 6.5 |
|
||||
| GET | `/api/admin/carrier-traffic-threshold-hits` | 超级管理员与平台账号 + 卡数据范围 | 7.3 |
|
||||
|
||||
- [x] 12.2 新端点经 `internal/routes.Register` 注册 RouteSpec;需要子路径的新端点必须注册在同段参数路由之前(字面量优先),并在验证项中核对路由顺序实际生效
|
||||
- [x] 12.3 通道阈值命中查询使用顶层路径,避免与通道详情的参数路由冲突
|
||||
- [x] 12.4 本变更复用既有 Handler,因此不修改 `cmd/api/docs.go`、`cmd/gendocs/main.go` 与 `pkg/openapi/handlers.go`;若实现中确需新增 Handler 类型,必须同步这三处与 `internal/bootstrap` 装配(ENG-ROUTE-001)
|
||||
- [x] 12.5 更新既有路由说明文案(含优先轮询详情中与本次相反的描述)与受影响 DTO 的 OpenAPI 描述
|
||||
- [x] 12.6 在 Spec 同步阶段把新端点以反引号形式补入对应主 Spec 的「可达操作索引」,与证据矩阵双向一致
|
||||
- [x] 12.7 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`,并确认连续两次生成结果一致
|
||||
|
||||
## 13. 证据链同步与验证
|
||||
|
||||
- [x] 13.1 同步 `docs/verification/context-reset/requirement-evidence.json`:为本次新增需求逐条补证据行(键名与需求名严格一致、路径与验证命令真实可复现)
|
||||
- [x] 13.2 同步 `docs/verification/context-reset/entry-capability-requirement-matrix.json`:为新端点补 http 行、为新增周期任务类型补异步行,并把新需求名追加进相应既有行的 `requirements`
|
||||
- [x] 13.3 复核既有被修改需求的证据行仍可复现(键不变,仅路径与验证输出可能需更新)
|
||||
- [x] 13.4 运行 `openspec doctor --json`、`openspec validate --all` 与 `./scripts/context-health.sh`,要求输出「Context 健康检查通过」并核对本变更与主 Spec 一致 —— 主 Spec 同步(需求名 169→180)后记录员在真实仓库重跑:`./scripts/context-health.sh` → `Context 健康检查通过`、exit=0;`openspec validate --all` → 36 passed / 0 failed、exit=0;`openspec doctor --json` → healthy=true、exit=0(见 verification.md 第 4 节)
|
||||
- [ ] 13.5 在测试环境完成端到端 smoke,逐组核对:①员工账单筛选、两金额口径与详情投影;②商户池四类语义与列表只读;③H5 弹窗类型与类别排序;④退款原因、备注、渠道标识、补录、可选方式与企微材料;⑤优先轮询关闭、超期、重触发、异常视图与详情投影;⑥阈值命中留痕与查询;⑦代理业务员、资格校验留痕与下级数量;⑧手机号关联数量与最近解绑人;⑨验证码失败限制;⑩佣金明细导出十五列;⑪报表导出序号列;⑫迁移 up/down/up、时间筛选拒绝集与路由顺序 —— 未勾:①②③④⑤⑥⑦⑧⑨⑩⑪各域均已有 smoke 证据(见 verification.md),未覆盖项为 HTTP 异步导出链路(POST /api/admin/export-tasks → asynq 分片 → 对象存储下载)、真实企微提交(退款/注册审批模板控件未在生产配置)、跨域一次总冒烟;上述未覆盖项补齐后再勾
|
||||
- [x] 13.6 记录验证证据(命令与原文输出)到本变更的验证说明,供归档核对
|
||||
|
||||
## 14. 上线前置与回滚
|
||||
|
||||
- [x] 14.1 整理上线说明:六对迁移清单与执行顺序、新增端点与参数变更、失效字段默认值
|
||||
- [x] 14.2 整理回滚步骤:代码回退后新增列可空或带默认值、读侧对空值按「无」处理、既有优先轮询行不因空的下次执行时间被判过期
|
||||
- [ ] 14.3 **待维护者执行**:在生产配置企业微信退款审批(套餐已用量与总量、资产类型、设备类型与型号、原支付渠道交易流水号)与代理注册审批(业务员)新增控件的映射,并以 `GET /api/admin/wecom/scenes` 返回的启用记录与 `control_mapping` 作为验收证据 —— 待维护者在生产配置企微控件映射并以 `GET /api/admin/wecom/scenes` 的 `control_mapping` 作为验收证据
|
||||
- [x] 14.4 说明员工账单时间参数替换的调用方同步要求(旧参数名不再被接受,需同步前端)
|
||||
@@ -0,0 +1,493 @@
|
||||
# close-august-iteration-gaps 验证说明
|
||||
|
||||
对应 tasks.md §13.6:记录验证证据(命令与原文输出),供归档核对。
|
||||
|
||||
本文件只记录**真实发生过**的命令与输出,来源为三个实施会话的汇报:
|
||||
- `writer-refund-agent`(退款 / 代理提现域)
|
||||
- `writer-polling-etc`(迁移执行者;优先轮询 / 通道阈值 / 员工账单 / 商户池 / H5 弹窗 / 手机号与验证码 / 导出列)
|
||||
- `shared-artifacts-owner`(证据链与生成物)
|
||||
|
||||
会话未逐字保留原始输出的项,一律标注「证据缺失」或标注证据强度,不做补写、改写或美化。凡标「记录员复核」的,是本次记录会话以只读方式(dbhub MCP 只读查询、文件读取、git status)重新核对的结果。
|
||||
|
||||
---
|
||||
|
||||
## 1. 环境与边界
|
||||
|
||||
- 目标库:`junhong_cmp_test`(PostgreSQL 17.6,`cxd.whcxd.cn:16159`,`sslmode=disable`)。
|
||||
- 目标 Redis:测试 Redis **DB 6**(`cxd.whcxd.cn:16299`)。
|
||||
- 依据 ENG-TEST-001:验证只在维护者指定的测试环境执行,**仅创建并清理本 Change 自己的 fixture,未重置整库**;诊断查询走 dbhub MCP 只读(ENG-DB-002),未用 psql 等客户端直连(`scripts/migrate.sh` 内部对 legacy 表的检查属脚本既有行为)。
|
||||
- 迁移执行者唯一:六对迁移的 up/down/up 全程由 `writer-polling-etc` 一人驱动,退款负责人未执行任何 `migrate` 命令。
|
||||
- **受控路径声明**:下列验证属「受控路径 / 组件级」,不是真实外部端到端——
|
||||
- 退款域:直接调用应用层与 `fiber` `app.Test`,企微侧使用 fake `ProviderPort`(不写企微渠道上下文);临时脚手架 `cmd/refundsmoke` 与账内 smoke 测试文件跑完已删除(`ls cmd/` 仅剩 api、audit-coverage、audit-retention-simulate、foundation-check、gendocs、migration-finalize、worker)。
|
||||
- 商户池:独立临时 schema `mp_smoke_<ts>` 内 AutoMigrate 建表,未触碰 public;另有对 public 商户池的**只读**投影核对。
|
||||
- 手机号 / 验证码:临时 schema `paa_smoke_assoc` + 真实 Service/Store;Redis 侧真实读写 DB 6。
|
||||
- 导出:以产品同一写出函数 `writeExportFile` 在临时目录落下 CSV 产物(单分片形态),未启动 API/Worker。
|
||||
- 通道阈值 / 优先轮询:临时测试文件 + dbhub 只读,跑完删除。
|
||||
|
||||
---
|
||||
|
||||
## 2. 六对迁移 up/down/up
|
||||
|
||||
### 2.1 迁移清单(磁盘现状,记录员读取 `migrations/00023{2..7}_*.{up,down}.sql` 核对)
|
||||
|
||||
| 编号 | 内容 | 新列可空性 / 默认值 |
|
||||
| --- | --- | --- |
|
||||
| 000232 `add_h5_popup_type` | `tb_h5_popup_configuration` 加 `popup_type` + 取值 CHECK | NOT NULL DEFAULT `'announcement'` |
|
||||
| 000233 `extend_refund_settlement_fields` | `tb_refund_request` 加 `source_payment_no`(`varchar(64)`)/`original_channel_trade_no`(**`varchar(100)`**,与来源列 `tb_payment.third_party_trade_no` 一致)/`offline_settlement_no`(`varchar(128)`)/`offline_settled_at`/`offline_settled_by`;`tb_refund_request_attempt` 加 `remark` | 前三项 NOT NULL DEFAULT `''`;`offline_settled_at` 可空;`offline_settled_by` NOT NULL DEFAULT `0`;`remark` TEXT NOT NULL DEFAULT `''` |
|
||||
| 000234 `extend_polling_priority_item` | 加 11 列;重建状态 CHECK 容纳 `closed`/`expired`;`next_run_at` 部分索引;回填资产列与 `priority_effective_from` | `asset_type` NOT NULL DEFAULT `''`;`asset_id` 可空;`device_no_snapshot` NOT NULL DEFAULT `''`;`agent_shop_id_snapshot` 可空;`attempt_limit` NOT NULL DEFAULT `3`;`started_at`/`finished_at`/`next_run_at`/`dequeued_at`/`priority_effective_from` 可空;`integration_log_id` NOT NULL DEFAULT `''` |
|
||||
| 000235 `extend_carrier_threshold_hit` | 锁表加 7 列 + 两个判定时间索引 | `hit_traffic_mb`/`hit_threshold_value` 可空;`hit_threshold_unit` NOT NULL DEFAULT `''`;`judged_at` NOT NULL **DEFAULT `now()`**;`trigger_source`/`resume_result` NOT NULL DEFAULT `''`;`unlocked_at` 可空 |
|
||||
| 000236 `add_phone_association_invalidator_name` | 加 `invalidator_name_snapshot` | NOT NULL DEFAULT `''` |
|
||||
| 000237 `extend_withdrawal_attempt_qualification` | 加 4 列 | `qualification_version_id` NOT NULL DEFAULT `0`;`qualification_checked_at` 可空;`qualification_passed` NOT NULL DEFAULT `0`;`qualification_failure_reason` NOT NULL DEFAULT `''` |
|
||||
|
||||
### 2.2 执行命令与关键输出原文
|
||||
|
||||
(来源:writer-polling-etc 汇报;原始输出未在最终汇报中逐字保留的,按会话记录列出关键行并标注「证据缺失」)
|
||||
|
||||
基线(`./scripts/migrate.sh version`):
|
||||
```
|
||||
target=erp_pgsql@cxd.whcxd.cn:16159/junhong_cmp_test sslmode=disable
|
||||
正在加载 .env 文件...
|
||||
当前迁移版本:
|
||||
231
|
||||
✓ 迁移操作完成
|
||||
```
|
||||
|
||||
逐对 `up 1`(x6,232→237)首个往返的输出形态(原文):
|
||||
```
|
||||
--- up 1 (round 1) rc=0 ---
|
||||
正在加载 .env 文件...
|
||||
正在向上迁移 1 步...
|
||||
迁移完成,开始检查 legacy 表...
|
||||
正在检查 legacy 表 tb_user / tb_order...
|
||||
⚠ 检测到以下 legacy 表仍然存在:
|
||||
- tb_order
|
||||
```
|
||||
(legacy 表检查告警是脚本既有行为,与本次迁移无关。)
|
||||
|
||||
往返顺序:`up` 逐对 232→237 → `down` 逐对 237→232(回到 231)→ `up` 逐对 232→237。结束后 `schema_migrations`:`version=237`、`dirty=false`(writer-polling-etc 汇报)。
|
||||
|
||||
> 说明:**逐对 up/down 的完整原始输出(含每次版本号回显)未被三份汇报逐字保留,证据缺失**;可核对的是上述往返结果与最终 version/dirty。可由下列命令复现:`set -a && . ./.env && set +a && ./scripts/migrate.sh version` → 逐对 `./scripts/migrate.sh up 1` ×6 → `./scripts/migrate.sh down 1` ×6 → `./scripts/migrate.sh up 1` ×6,再用 dbhub 只读查询 `information_schema.columns` / `pg_constraint` / `pg_indexes` 核对(记录员复核命令见 2.4)。
|
||||
|
||||
### 2.3 回填与非空 fixture 证据
|
||||
|
||||
- 非空回填 fixture 由维护者授权后执行:顺序 `down 6` → 造 fixture(h5 弹窗行、polling 优先项活动行、通道阈值锁行)→ `up 6` → 核对回填 → 删除 fixture → 复核计数回原值。临时程序 `tmp_migration_fixture`(跑完已 `rm -rf`)。
|
||||
- 会话记录的核对点:弹窗 `popup_type=announcement`;活动项 `priority_effective_from=迁移时刻`且非活动行不回填;锁行 `judged_at=创建时间`、命中数值与阈值 NULL;`000234` 回填 329 行获得资产列。
|
||||
- **证据缺失**:该 fixture 往返的逐条原始输出(fixture 主键、回填前后计数)未在最终汇报中逐字保留。可由下列命令复现:`./scripts/migrate.sh down 6` → 一次性 Go fixture 程序 seed(h5 弹窗行、polling 优先项活动行、通道阈值锁行)→ `./scripts/migrate.sh up 6` → dbhub 只读核对 `popup_type=announcement`、活动项 `priority_effective_from=迁移时刻`、锁行 `judged_at=created_at` 且命中数值/阈值为 NULL → cleanup 后复核计数回原值。会话实际使用的临时程序为 `tmp_migration_fixture`(跑完 `rm -rf`)。
|
||||
|
||||
### 2.4 记录员复核(本轮只读,`xd://mcp__postgres_execute_sql_main`)
|
||||
|
||||
30 个新列全部存在,类型 / 可空 / 默认值与设计一致,例如:
|
||||
```
|
||||
tb_h5_popup_configuration.popup_type character varying nullable=NO default='announcement'
|
||||
tb_carrier_traffic_threshold_lock.judged_at timestamp with tz nullable=NO default=now()
|
||||
tb_carrier_traffic_threshold_lock.hit_traffic_mb numeric nullable=YES default=null
|
||||
tb_polling_priority_item.attempt_limit integer nullable=NO default=3
|
||||
tb_polling_priority_item.next_run_at timestamp with tz nullable=YES default=null
|
||||
tb_refund_request.offline_settled_by bigint nullable=NO default=0
|
||||
tb_refund_request_attempt.remark text nullable=NO default=''
|
||||
tb_commission_withdrawal_request_attempt.qualification_passed smallint nullable=NO default=0
|
||||
```
|
||||
|
||||
约束(`pg_get_constraintdef` 原文节选):
|
||||
```
|
||||
ck_polling_priority_item_status: CHECK (status IN ('pending','processing','completed','failed','closed','expired'))
|
||||
ck_polling_priority_item_terminal_result: CHECK ((status='completed' AND result='success') OR (status='failed' AND result='failed') OR status IN ('pending','processing','closed','expired'))
|
||||
ck_polling_priority_item_asset_type: CHECK (asset_type IN ('','iot_card','device'))
|
||||
ck_carrier_traffic_threshold_lock_resume_result: CHECK (resume_result IN ('','resumed','skipped','manual'))
|
||||
chk_h5_popup_configuration_popup_type: CHECK (popup_type IN ('promotion','announcement'))
|
||||
chk_commission_withdrawal_attempt_qualification_reason: CHECK (qualification_passed = 0 OR qualification_failure_reason = '')
|
||||
```
|
||||
|
||||
索引(`pg_indexes` 原文节选,确认活动项唯一键谓词未变):
|
||||
```
|
||||
uq_polling_priority_item_active: USING btree (card_id, task_type) WHERE deleted_at IS NULL AND status IN ('pending','processing')
|
||||
idx_polling_priority_item_next_run: USING btree (next_run_at) WHERE deleted_at IS NULL AND status IN ('pending','processing') AND next_run_at IS NOT NULL
|
||||
idx_carrier_traffic_threshold_lock_carrier_judged: USING btree (carrier_id, judged_at DESC, id DESC) WHERE deleted_at IS NULL
|
||||
idx_carrier_traffic_threshold_lock_card_judged: USING btree (card_id, judged_at DESC, id DESC) WHERE deleted_at IS NULL
|
||||
```
|
||||
|
||||
### 2.5 记录员发现的与汇报不一致处(如实标注)
|
||||
|
||||
- writer-polling-etc 汇报「清理后 `polling_total=329`」;记录员本轮只读查询 `tb_polling_priority_item` 得 `total=333`、`active=0`、无软删。其 4 条 smoke fixture(id=1109–1112,`card_id=8022`,`created_at=2026-09-18 03:18:04+00`)**确已删除**(按 card 8022 逐行核对,现存行最高 id=1091);当前 id=1114–1117(`card_id=8018`,`created_at=2026-09-18 03:30:07+00`)为清理之后由测试环境自身轮询产生的 4 条真实业务行(**测试环境自身产生的行,非本 Change fixture**)。因此 333 = 329 + 4,属测试库持续产生业务行的基线漂移,不是清理失败。
|
||||
|
||||
---
|
||||
|
||||
## 3. 各域 smoke 命令与关键输出
|
||||
|
||||
### 3.1 退款 / 代理提现(来源会话:writer-refund-agent)
|
||||
|
||||
命令(原文):
|
||||
```
|
||||
set -a; . ./.env; set +a; JUNHONG_DATABASE_HOST="$DB_HOST" JUNHONG_DATABASE_PORT="$DB_PORT" JUNHONG_DATABASE_USER="$DB_USER" JUNHONG_DATABASE_PASSWORD="$DB_PASSWORD" JUNHONG_DATABASE_DBNAME="$DB_NAME" JUNHONG_DATABASE_SSLMODE="$DB_SSLMODE" JUNHONG_REDIS_ADDRESS=127.0.0.1 JUNHONG_REDIS_PORT=6379 JUNHONG_JWT_SECRET_KEY="smoke-secret-key-0123456789abcdefghijklmnop" go run ./cmd/refundsmoke
|
||||
```
|
||||
关键输出原文(节选):
|
||||
```
|
||||
SMOKE TARGET | db=junhong_cmp_test host=cxd.whcxd.cn
|
||||
CHECK PASS | 退款原因为空被拒且未建申请与尝试 | err=退款原因不能为空 code=1001 refunds=0 attempts=0
|
||||
CHECK PASS | 退款创建成功 | refund_id=506 refund_no=RF20260918112351115237
|
||||
CHECK PASS | 线下订单两流水号为空 | source_payment_no="" original_channel_trade_no=""
|
||||
CHECK PASS | 申请人备注不写入主表审批备注 | 主表 remark=""
|
||||
CHECK PASS | 申请人备注冻结进尝试 | attempt_id=163 remark="smoke-applicant-remark-1"
|
||||
CHECK PASS | 备注只进新尝试且历史尝试不变 | attempt1="smoke-applicant-remark-1" attempt2="smoke-applicant-remark-2"
|
||||
CHECK PASS | 线下订单原路能力不可用且有原因 | capability={Available:false Unavailable:该订单的退款方式矩阵不包含原路退款}
|
||||
CHECK PASS | 企微材料含套餐用量(真流量口径) | used_mb=123 total_mb=456
|
||||
CHECK PASS | 企微材料含资产类型与设备类型型号 | asset_type=device device_type=smoke_type device_model=smoke_model
|
||||
CHECK PASS | 场景白名单标记新增字段必须映射且不扩大既有字段 | required_fields=7
|
||||
CHECK PASS | 场景映射缺必须字段明确失败 | err=企业微信场景缺少必须映射的业务字段: 资产类型
|
||||
CHECK PASS | 提现尝试冻结资格版本与校验结果 | version=49 passed=1 checked_at=2026-09-18 11:23:57 +0800
|
||||
CHECK PASS | 资格被替换后历史尝试仍返回原校验结果 | historical_version=49 replacement_version=50
|
||||
CHECK PASS | 店铺列表返回直接下级代理数量 | total=28 parent_shop_id=1629 count=2
|
||||
SMOKE SUMMARY | pass=31 fail=0
|
||||
```
|
||||
脚手架 `cmd/refundsmoke/`、`internal/routes/refund_route_order_smoke_test.go`、`internal/infrastructure/wecom/required_mapping_smoke_test.go` 跑完已删除(`git status --short | grep smoke` 无命中;`go build ./internal/... ./pkg/...` 无输出成功)。
|
||||
|
||||
清理证据:业务表全部回零,例如 `tb_refund_request (refund_no LIKE 'SMOKE-AUG-GAP%')=0`、`tb_approval_instance (submitter 为 smoke-*)=0`;追加型事实 `tb_audit_event` 保留 24 行(按指示未删)。
|
||||
|
||||
### 3.2 员工账单(来源会话:writer-polling-etc;本地 API @127.0.0.1:3010 对测试库)
|
||||
|
||||
命令:启动 `JUNHONG_SERVER_ADDRESS=:3010 go run ./cmd/api`,`POST /api/auth/login` 取 token 后 pytest/UDP 式调用列表接口。
|
||||
|
||||
四类筛选输出原文(`eval` 结果,节选;行内被会话显示截断处标 `…`):
|
||||
```
|
||||
A bill_id=32 -> (200, 0, 'total=1', [{'id': 32, 'receivable_amount': 1000, 'received_amount': 0, 'unsettled_amount': 1000, 'reserved_amount': 0, 'remaining_amount': 1000, 'status': 0}])
|
||||
B bill_id=27 -> (200, 0, 'total=1', [{'id': 27, 'receivable_amount': 10900, 'receiv… ← 截断
|
||||
C debtor_keyword=lxp -> 1 行(bill 27)
|
||||
D customer_keyword -> 命中(按店铺名子串)
|
||||
```
|
||||
预占场景断言(会话总结原文):`B: bill_id=27 … with reserved 10900 → unsettled 10900, remaining 0; difference = 10900 = reserved ✓`(两金额字段之差等于预占额 10900)。
|
||||
|
||||
- **核销通过时间正例(来源会话:writer-polling-etc 末次汇报原文,运行方式:本地 API @127.0.0.1:3010 + 测试库自建 fixture)**——证明筛选依据是申请表 `decided_at` 而非分摊表 `released_at`:
|
||||
|
||||
fixture(自建并记录主键;故意让分摊 `released_at` 与申请 `decided_at` 不同):
|
||||
```
|
||||
bill id=34(source_no=SMOKE-AUG26-DECIDED-BILL、receivable=500、status=2)
|
||||
application id=13(status=1、decided_at=2026-09-15T04:30:00Z)
|
||||
allocation(status=1、released_at=2026-09-16T04:30:00Z)
|
||||
```
|
||||
四类筛选与正/反例输出原文(HTTP + 库内结果):
|
||||
```
|
||||
GET /api/admin/employee-collection-bills?decided_start_time=2026-09-15T04:29:00Z&decided_end_time=2026-09-15T04:31:00Z
|
||||
→ http=200 code=0 total=1 ids=[34]
|
||||
GET /api/admin/employee-collection-bills?decided_start_time=2026-09-16T04:29:00Z&decided_end_time=2026-09-16T04:31:00Z(只含分摊 released_at)
|
||||
→ total=0 ids=[]
|
||||
GET /api/admin/employee-collection-bills?bill_id=34
|
||||
→ total=1 ids=[34]
|
||||
GET /api/admin/employee-collection-bills?start_time=2026-09-14T00:00:00Z&end_time=2026-09-14T23:59:59Z
|
||||
→ total=0
|
||||
```
|
||||
fixture 前后既有行计数:`bills 6→7、applications 2→3、allocations 2→3、attempts 2→2`;清理后回到 `6/2/2/2`。
|
||||
|
||||
> 证据强度:A/B/C 列表筛选、两个金额口径与核销通过时间正例均为**真实 HTTP + 真实测试库**(核销通过时间正例含自建并清理的 fixture,主键与逐字输出见上)。
|
||||
|
||||
### 3.3 商户池(来源会话:writer-polling-etc.MerchantPoolWorker)
|
||||
|
||||
命令(临时内在包测试,跑完删除):
|
||||
```
|
||||
go test ./internal/application/merchantpayment -run 'TestSmoke' -v
|
||||
→ TestSmokeRawOutputs PASS (5.64s)、TestSmokeRealPoolListReadOnly PASS (0.80s)、ok 6.970s
|
||||
```
|
||||
§3.6 四条原始输出(节选):
|
||||
```
|
||||
① 单成员池(成员 id=3 smoke-w1,阈值 1,已达 1 笔/500 分)创建支付:选中商户=id=3 name=smoke-w1,err=<nil>,池世代 1→1
|
||||
②-a 自然日(成员 1、2 均达标)创建支付:AppError{code=1175, message="当前周期暂无可用商户"}
|
||||
②-b 自然月(世代 2,成员 1、2 均达标)创建支付:AppError{code=1175, message="当前周期暂无可用商户"}(1175 = CodeNoPaymentConfig)
|
||||
③ 每轮累计(世代 3,成员 1、2 均达标)创建支付:选中商户=id=1 name=smoke-a1(顺序首位),err=<nil>,池世代 3→4
|
||||
④ 时间池创建起点=2026-09-18T08:23:58+08:00;更新请求起点=…01:23:58… → 保存后起点=2026-09-18T11:23:58+08:00
|
||||
```
|
||||
列表只读(前后数字原文):
|
||||
```
|
||||
前:routing_epoch=4,tb_payment 行数=1,tb_payment_merchant_routing_success 行数=7
|
||||
后:routing_epoch=4,tb_payment 行数=1,tb_payment_merchant_routing_success 行数=7
|
||||
ListPools 共 5 条 SQL:count(tb_payment_merchant_pool);SELECT * … ORDER BY id DESC LIMIT 20;tb_payment_merchant_pool_member WHERE pool_id IN (3,2,1);tb_payment_merchant WHERE (id IN (…) AND status=1);tb_payment_merchant_routing_success WHERE (pool_id=3 AND routing_epoch=4) OR (…)
|
||||
```
|
||||
真实 public 池只读(同一测试库,仅 SELECT):`tb_payment_merchant_pool 行数=2,ListPools total=2`;查询前后 `routing_epoch=1`、`tb_payment=129`、`routing_success=0` 不变。临时 schema `mp_smoke%` 经 `information_schema.schemata` 确认为 0 行残留。
|
||||
|
||||
### 3.4 H5 运营弹窗(来源会话:writer-polling-etc)
|
||||
|
||||
- 实现核对:`popup_type` 必填且校验取值域;候选排序以显式 `CASE` 类别表达式(风险换卡 > 推广 > 公告)为第一键,再按显式优先级、`updated_at`、`id`;缺省优先级按类型取值。
|
||||
- 迁移回填 smoke(§4.3):按维护者授权,先 `down 6` 造 H5 弹窗配置 fixture,再 `up 6` 核对既有配置获得 `popup_type=announcement`;fixture 已删除,当前 `tb_h5_popup_configuration` 为 0 行(记录员复核)。
|
||||
- **证据缺失**:该回填 smoke 的逐字原始输出(fixture 主键、回填前后行内容)未被最终汇报逐字保留。可由下列命令复现:`./scripts/migrate.sh down 6` → 通过 `POST /api/admin/h5-popup-configurations`(旧代码)创建一条运营弹窗配置 → `./scripts/migrate.sh up 6` → dbhub 只读查询该行 `popup_type` 应为 `announcement` → 删除该 fixture。
|
||||
- §4.4(创建/更新缺 `popup_type` 以参数非法拒绝):**证据缺失**,未在汇报中找到原始输出。可由下列命令复现:`POST /api/admin/h5-popup-configurations`(不带 `popup_type`)应返回参数非法;实现侧取值域校验已写入且 `chk_h5_popup_configuration_popup_type` 约束在库中存在(记录员复核)。
|
||||
|
||||
### 3.5 优先轮询(来源会话:writer-polling-etc.PriorityPollingWorker)
|
||||
|
||||
命令与结果(原文节选):
|
||||
```
|
||||
go build -o /tmp/omptest_api ./cmd/api; go build -o /tmp/omptest_worker ./cmd/worker → api_exit=0,worker_exit=0
|
||||
临时 smoke 程序对 junhong_cmp_test 实测 31 项后删除 → 失败项=0;已清理 fixture 行数=6
|
||||
临时测试 internal/task TestPriorityRoundSmoke → PASS:attempts=2、started/finished/dequeued 已写、next_run_at=+30s、近 5 分钟生命周期审计 5 行
|
||||
临时测试 cmd/worker TestPollingPriorityExpiryTaskSmoke → PASS:mux 注册生效、真实执行一次到期任务 → expired+dequeued 且保留 attempts/reason、expire 审计 1 行
|
||||
临时测试 internal/routes TestRegisterPollingPriorityRoutesOrder → PASS:5 条路由命中处理器、/:id/unknown=404、POST /:id 与 DELETE /:id/close=405
|
||||
只读 SQL 核验 tb_audit_event(近 40 分钟):close success=2/denied=5、expire success=2、retrigger success=2/denied=3、enqueue=10、claim=2、complete=1、fail=1、retry=1
|
||||
```
|
||||
31 项覆盖:入队新列、执行起止与日志标识、出队时间、人工关闭(代理/二次关闭拒绝、保留失败原因、键位回收)、超期出队保留事实、重触发(越权拒绝、尝试次数归零)、全部筛选与时间拒绝集、企业账号拒绝、投影只读性(前后状态快照一致)与空事实语义。
|
||||
|
||||
未验证推断(会话自述):真实 Gateway 上游调用路径未运行;Asynq 定时任务在真实 Worker 进程内的调度触发未端到端执行(仅验证注册无错 + 处理器直接调用正确终结超期项)。
|
||||
|
||||
### 3.6 通道阈值命中(来源会话:writer-polling-etc.CarrierThresholdWorker)
|
||||
|
||||
命令(临时测试,跑完删除):
|
||||
```
|
||||
go test ./internal/query/carrierthreshold -run TestAug26ThresholdHitSmoke -v → PASS
|
||||
go test ./internal/routes -run TestAug26CarrierThresholdHitRouteSmoke -v → PASS
|
||||
```
|
||||
关键原文(节选):
|
||||
```
|
||||
①命中记录 {"hit_traffic_mb":1500.5,"hit_threshold_value":100,"hit_threshold_unit":"MB","trigger_source":"polling","judged_at":"2026-09-05T18:00:00+08:00","period_start":"2026-09-01T00:00:00+08:00","status":"locked","unlocked_at":null,"resume_result":""}
|
||||
②改阈值 200/GB 后历史命中仍为 100/MB,新周期命中为 200/GB+manual_sync
|
||||
③跨期仅解锁 → resume_result=skipped、unlocked_at 非空、命中事实不变
|
||||
④运营商软删 → resume_result=manual、anomaly_flag=1、命中事实不变
|
||||
⑤代理账号 403、范围外卡 total=0;⑥闭区间含两端 total=1;7 组非法时间输入全部 CodeInvalidParam
|
||||
⑦page_size=500 被裁剪为 100
|
||||
⑧历史行 {"hit_traffic_mb":null,"hit_threshold_value":null,"hit_threshold_unit":"","judged_at":"2026-08-20T11:00:00+08:00","created_at":"2026-08-20T11:00:00+08:00"}
|
||||
路由:平台账号 GET 返回 status=200 body={"code":0,"data":{"items":[],"total":0,"page":1,"size":20}};/api/admin/carriers/carrier-traffic-threshold-hits 仍走参数路由返回 400 "无效的运营商 ID"(证明无路由冲突)
|
||||
```
|
||||
DB 痕迹(只读):`tb_carrier_traffic_threshold_lock 0 行`、fixture 0 条、`schema_migrations=237`(该 worker 未执行 migrate)。
|
||||
|
||||
### 3.7 手机号关联与验证码失败限制(来源会话:writer-polling-etc.PhoneSmsWorker)
|
||||
|
||||
命令与结果(原文节选):
|
||||
```
|
||||
Redis:JUNHONG_REDIS_DB=6 go run ./tmp_paa_smoke(Redis = cxd.whcxd.cn:16299 DB 6)→ 17 PASS / 0 FAIL
|
||||
第5次错误验证码被拒且计数达到上限 code=1001 count=5
|
||||
失败计数键带固定窗口过期时间 ttl=10m0s
|
||||
锁定期内正确验证码被限流拒绝 code=1008 err=验证码校验失败次数过多,请稍后再试
|
||||
校验失败不消费验证码 stored="246810"
|
||||
窗口内校验成功后失败计数清零并重新计数 before=2 existsAfterSuccess=0 after=1
|
||||
并发提交失败计数按实际次数累加 concurrent=6 普通失败=6 限流=0 计数=6
|
||||
计数不可用时正确验证码正常通过(放行)
|
||||
DB store/service(junhong_cmp_test,临时 schema paa_smoke_assoc):
|
||||
批量计数整批只发出一条 SQL statements=1
|
||||
查询次数不随行数线性增长(整页一次批量聚合) 1行 statements=4 / 4行 statements=4
|
||||
有效关联数量与「最多关联十项」上限判定同口径 counts=[3 3 3 3]
|
||||
有效关系的解绑人三字段为空;已失效关系返回 name="smoke解绑人" id=9900 at=2026-09-18T11:07:50+08:00 method=backend_single
|
||||
解绑后计数同步下降且返回本次解绑人名称快照 ValidAssociationCount:2 InvalidatorName:smoke解绑人A
|
||||
真实表有效关系的名称快照列为空 validRowsWithSnapshot=0;临时 schema 无残留 residualCount=0;真实表未被 fixture 污染 realRows=0
|
||||
```
|
||||
未验证(会话自述):真实 admin HTTP 链路 + 真实表未端到端跑(投影/计数在结构同构的临时 schema 上验证,真实表仅 2 条只读断言);注册/绑定/换绑入口的 HTTP 返回码未实跑(错误码透传属代码/单测级);锁定到期恢复用 `PEXPIRE 200ms` 模拟,未等真实 10 分钟窗口。
|
||||
|
||||
### 3.8 导出列(来源会话:writer-polling-etc.ExportWorker)
|
||||
|
||||
命令(一次性程序,跑完删除):
|
||||
```
|
||||
source .env.local
|
||||
go test ./internal/task -run TestSmokeAug26ExportCSVProducts -v → PASS (2.83s)
|
||||
→ 验证后:rm 该文件;gofmt -l internal/exporter/(无输出)、go build ./internal/exporter(0)、go vet ./internal/exporter(无输出)
|
||||
```
|
||||
佣金明细导出表头原文(CSV 第一行,20 列;前 15 列为规定列序):
|
||||
```
|
||||
店铺,业务员,用户组,资产类型,设备类型,设备型号,资产标识,订单号,下单时间,佣金金额,佣金来源,佣金状态,关联佣金明细,入账后金额,创建时间,记录来源,记录ID,来源退款单号,是否可提现,佣金入账时间
|
||||
```
|
||||
代表性数据行原文(含回溯负数、缺值为空):
|
||||
```
|
||||
AUG26COL店铺甲,AUG26COL_biz,AUG26COL用户组一,物联网卡,MiFi,CDB001,AUG26COLCARD001,AUG26COLORD001,2026-08-01 10:20:30,-10.00,成本价差,回溯,474,40.00,2026-08-03 09:00:00,回溯明细,14,AUG26COLREF001,不可提现,
|
||||
AUG26COL店铺乙,,,设备,,,AUG26COLDEV002,AUG26COLORD002,2026-08-02 11:22:33,20.00,一次性佣金,已发放,,20.00,2026-08-04 08:00:00,原佣金,475,,,
|
||||
```
|
||||
报表导出序号列原文(激活情况,节选):
|
||||
```
|
||||
序号,店铺,采购数量,累计激活数,激活率,新增激活数,累计在网数,活跃用户数,累计用量(GB),单用户卡均(GB),含零预测卡均(GB),不含零预测卡均(GB)
|
||||
1,AUG26COL店铺乙,1,1,1.00,-,1,0,0.00,0.00,0.00,-
|
||||
2,AUG26COL店铺甲,1,1,1.00,-,1,1,1.00,1.00,1.55,1.55
|
||||
,合计,2,2,1.00,-,2,1,1.00,0.50,0.78,1.55
|
||||
```
|
||||
列表行数与导出行数一致性(数字):佣金明细 `list_total=2 list_rows=2 export_total=2 export_rows=2`;激活情况/套餐续费 `导出行=3(=分组行 2 + 合计行 1)`。
|
||||
清理:fixture 各表计数 `account=0 shop=0 device=0 card=0 order=0 commission=0 clawback=0 group=0 report_head=0 report_activation=0 report_renewal=0`;CSV 临时目录已删。
|
||||
**未覆盖(会话明确不声称)**:HTTP 端到端(`POST /api/admin/export-tasks` → asynq 派发/分片 → 对象存储上传/下载 URL);XLSX 产物形态(本次只跑 CSV)。
|
||||
|
||||
### 3.9 时间筛选契约(来源:writer-polling-etc 会话 + 各域汇报)
|
||||
|
||||
员工账单旧参数拒绝(raw 原文节选,HTTP @:3010):
|
||||
```
|
||||
legacy created_from http=400 code=1001 msg=账单时间筛选参数已统一为 start_time 与 end_time,不再接受 created_from
|
||||
legacy created_to http=400 code=1001 msg=账单时间筛选参数已统一为 start_time 与 end_time,不再接受 created_to
|
||||
date-only …
|
||||
```
|
||||
通道阈值命中:7 组非法时间输入全部 `CodeInvalidParam`、闭区间含两端 `total=1`(见 3.6)。
|
||||
优先轮询列表:时间范围走同一 `utils.ParseTimeRange`,拒绝集在 31 项 smoke 内通过(见 3.5)。
|
||||
|
||||
### 3.10 路由顺序(来源:PriorityPollingWorker / CarrierThresholdWorker 路由测试;记录员静态复核)
|
||||
|
||||
- 优先轮询:`POST ""`(入队)→ `GET ""`(列表)→ `POST "/:id/close"` → `POST "/:id/retrigger"` → `GET "/:id"`;路由测试实测 5 条命中、`/:id/unknown=404`、`POST /:id` 与 `DELETE /:id/close=405`。
|
||||
- 退款:`GET "/order-options"` 注册于 `GET "/:id"` 之前(记录员读取 `internal/routes/refund.go` 复核:第 48 行 order-options 早于第 57 行 `/:id`)。
|
||||
- 通道阈值命中:顶层组 `/carrier-traffic-threshold-hits` 注册于 `/carriers` 组之前(记录员读取 `internal/routes/carrier.go` 复核);路由测试实测 `/api/admin/carriers/carrier-traffic-threshold-hits` 仍走参数路由返回 400,证明无冲突。
|
||||
|
||||
---
|
||||
|
||||
## 4. 门禁命令与结果
|
||||
|
||||
| 命令 | 结果 | 来源 |
|
||||
| --- | --- | --- |
|
||||
| `gofmt` | 各域改动文件 `gofmt -l` 输出为空;shared-artifacts-owner 未改 Go 文件,不适用 | 各写入会话 + shared-artifacts-owner |
|
||||
| `go build ./cmd/api ./cmd/worker` | `go_build_exit=0`(仅一条与仓库无关的模块缓存 stat cache 告警) | writer-polling-etc 会话原文 |
|
||||
| `go run cmd/gendocs/main.go` 连续两次一致 | 第一次输出 `成功在以下位置生成 OpenAPI 文档: …/docs/admin-openapi.yaml`;`cmp` 无差异,`md5 = c2dd9172234500a60861452f344854c3` | shared-artifacts-owner |
|
||||
| 生成物复核 | 记录员本轮 `md5 -q docs/admin-openapi.yaml` = `c2dd9172234500a60861452f344854c3`(与上一致) | 记录员复核 |
|
||||
| `./scripts/context-health.sh` | **记录员本轮重跑:`- Validating...` / `Context 健康检查通过` / `context_health_exit=0`**(主 Spec 已由 shared-artifacts-owner 在不归档的前提下把本变更 12 份 delta 同步进 `openspec/specs/**`,主 Spec 需求名 169→180,经记录员只读复核 `grep -c '^### Requirement:' openspec/specs/` = 180) | 记录员重跑(会话:shared-artifacts-owner 完成主 Spec 同步后) |
|
||||
| `openspec validate --all` | `Totals: 36 passed, 0 failed (36 items)`,`exit=0`(记录员本轮重跑同样 36/0、`validate_all_exit=0`) | shared-artifacts-owner + 记录员重跑 |
|
||||
| `openspec validate --all --strict` | `Totals: 18 passed, 18 failed (36 items)`,`exit=1`;与未改动基线逐行 diff 为空,失败均为既有告警级规则(Purpose < 50 字符、Requirement > 500 字符) | shared-artifacts-owner |
|
||||
| `openspec doctor --json` | `{"root":{…,"healthy":true,"status":[]},"store":null,"references":[],"status":[]}`,`exit=0` | shared-artifacts-owner |
|
||||
|
||||
归档预演(shared-artifacts-owner,在 `/tmp` 全量副本):`openspec archive close-august-iteration-gaps --yes --json` → `archivedAs=2026-09-18-close-august-iteration-gaps`、`specsUpdated=true`、`totals={added:11, modified:10, removed:0, renamed:0}`;修正副本内 delta 路径风格后 `./scripts/context-health.sh` 输出 `Context 健康检查通过`、`exit=0`。
|
||||
|
||||
主 Spec 同步(不归档):shared-artifacts-owner 已把本变更 12 份 delta 的需求同步进 `openspec/specs/**`(主 Spec 需求名 169→180),记录员本轮在**真实仓库**重跑 `./scripts/context-health.sh` → `Context 健康检查通过`、`exit=0`;`openspec validate --all` → `Totals: 36 passed, 0 failed (36 items)`、`exit=0`。`docs/admin-openapi.yaml` 经脚本重生成后 md5 仍为 `c2dd9172234500a60861452f344854c3`(与重跑前一致)。
|
||||
|
||||
### 4.1 最终验收命令与原文(记录员本轮真实执行)
|
||||
|
||||
命令与输出为**本轮在真实仓库逐条实跑**所得,原文照录:
|
||||
|
||||
1. `gofmt -l`(对象为 `git status --porcelain -- '*.go'` 列出的全部现存 Go 文件,共 94 个)
|
||||
```
|
||||
gofmt -l <上述文件列表>
|
||||
(无输出)
|
||||
gofmt_exit=0
|
||||
```
|
||||
2. `go build ./cmd/api ./cmd/worker`
|
||||
```
|
||||
go: writing stat cache: open /Users/break/go/pkg/mod/cache/download/github.com/break/junhong_cmp_fiber/@v/v0.0.0-20260918014228-5e78809b9333.info161583859.tmp: permission denied
|
||||
build_exit=0
|
||||
```
|
||||
(唯一输出为与仓库源码无关的本机模块缓存 stat cache 告警。)
|
||||
3. `go run cmd/gendocs/main.go` 连续两次 + `cmp`
|
||||
```
|
||||
=== run1 ===
|
||||
2026/09/18 15:25:30 成功在以下位置生成 OpenAPI 文档: /Users/break/csxjProject/junhong_cmp_fiber/docs/admin-openapi.yaml
|
||||
run1_exit=0
|
||||
=== run2 ===
|
||||
2026/09/18 15:25:31 成功在以下位置生成 OpenAPI 文档: /Users/break/csxjProject/junhong_cmp_fiber/docs/admin-openapi.yaml
|
||||
run2_exit=0
|
||||
=== cmp run1 vs run2 ===
|
||||
cmp_exit=0 (无差异)
|
||||
md5:生成前 c2dd9172234500a60861452f344854c3 / run1 c2dd9172234500a60861452f344854c3 / run2 c2dd9172234500a60861452f344854c3 / 当前工作树文件 c2dd9172234500a60861452f344854c3
|
||||
```
|
||||
4. `./scripts/context-health.sh`
|
||||
```
|
||||
- Validating...
|
||||
Context 健康检查通过
|
||||
context_health_exit=0
|
||||
```
|
||||
5. `openspec validate --all`
|
||||
```
|
||||
- Validating...
|
||||
…(36 项逐条 ✓,含 change/close-august-iteration-gaps)…
|
||||
Totals: 36 passed, 0 failed (36 items)
|
||||
validate_all_exit=0
|
||||
```
|
||||
6. `openspec validate --all --strict`(如实记录)
|
||||
```
|
||||
Totals: 18 passed, 18 failed (36 items)
|
||||
Details: openspec validate agent-funds-commission --type spec
|
||||
validate_strict_exit=1
|
||||
```
|
||||
18 项失败均为既有告警级规则(Purpose < 50 字符、Requirement > 500 字符),与本次改动无因果关系。**「与 HEAD 基线逐行一致」的本轮复核方式**:以 `git archive HEAD | tar -x -C /tmp/record_head_baseline`(只读仓库、只写 `/tmp`)在同一 openspec 版本下重跑基线,得
|
||||
```
|
||||
Totals: 17 passed, 18 failed (35 items)
|
||||
head_strict_exit=1
|
||||
```
|
||||
(少的一项即本变更 `change/close-august-iteration-gaps`,HEAD 尚无该目录);对两边输出中 `✓/✗ spec/*` 行集合逐行 `diff` → 无差异(各 34 行、`spec_only_diff_exit=0`),失败 spec 集合与逐行结论完全一致。
|
||||
7. `openspec doctor --json`
|
||||
```
|
||||
{
|
||||
"root": {
|
||||
"path": "/Users/break/csxjProject/junhong_cmp_fiber",
|
||||
"source": "nearest",
|
||||
"healthy": true,
|
||||
"status": []
|
||||
},
|
||||
"store": null,
|
||||
"references": [],
|
||||
"status": []
|
||||
}
|
||||
doctor_exit=0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 未覆盖与待维护者项
|
||||
|
||||
1. **企微模板控件映射(tasks 14.3,待维护者)**:退款审批(套餐已用量与总量、资产类型、设备类型与型号、原支付渠道交易流水号)与代理注册审批(业务员)新增控件的映射需在生产按既有场景接口配置,并以 `GET /api/admin/wecom/scenes` 返回的启用记录与 `control_mapping` 作为验收证据。本变更不写入模板。
|
||||
2. **HTTP 异步导出链路未覆盖**:`POST /api/admin/export-tasks` → asynq 分片 → 对象存储下载/URL 未端到端执行(导出证据为 CSV 产物级,见 3.8);XLSX 产物形态亦未覆盖。
|
||||
3. **`openspec validate --all --strict` 既有基线失败**:18 passed / 18 failed,均为既有告警级规则,与本次改动无因果关系(与 HEAD 基线逐行 diff 为空;本轮复核方式见 4.1 第 6 条)。
|
||||
4. **主 Spec 同步与归档前门禁(已解决)**:主 Spec 已在不归档的前提下同步本变更 12 份 delta(需求名 169→180),`./scripts/context-health.sh` 在真实仓库重跑输出「Context 健康检查通过」`exit=0`、`openspec validate --all` 36/0 `exit=0`(见第 4 节)。`openspec archive` 的最终执行仍属维护者动作。
|
||||
5. **H5 §4.3/§4.4 的逐字原始输出未保留**(证据缺失,见 3.4,附可复现命令);员工账单 §2.8 核销通过时间正例的逐字输出已按实施会话原文补入 3.2(含 fixture 主键与四条 HTTP 结果)。
|
||||
6. **跨域一次总冒烟未做**:各域 smoke 分域执行,未做一次覆盖全部端点的统一冒烟。
|
||||
7. **未验证推断**(会话自述,非缺陷):优先轮询真实 Gateway 上游调用路径、Asynq 定时任务在真实 Worker 进程内的调度触发、注册/绑定/换绑入口 HTTP 返回码,均只到代码/注册无错/组件级验证。
|
||||
|
||||
---
|
||||
|
||||
## 6. 独立审查与修复
|
||||
|
||||
来源:独立审查会话(只读复核 + 受控验证)的结论,由终轮记录员照实登记;本节登记过程不改动代码、迁移、主 Spec 与证据 JSON。
|
||||
|
||||
### 6.1 审查结论
|
||||
|
||||
- 判定:**阻断 0 项、应修 1 项(F1)、建议 5 项、观察 8 项**。
|
||||
- `go build ./cmd/api ./cmd/worker` → **exit=0**。
|
||||
- 脚手架无残留:`git status --porcelain | grep -E '_test\.go|tmp_|zz_'` → 无命中(exit=1);`ls cmd/` → 仅 `api`、`audit-coverage`、`audit-retention-simulate`、`foundation-check`、`gendocs`、`migration-finalize`、`worker`,无 `cmd/refundsmoke`。
|
||||
|
||||
### 6.2 F1 处置:契约侧澄清(不改代码)
|
||||
|
||||
处置方式为**契约侧澄清**,实现不动;delta 与主 Spec 同一句改写为:
|
||||
|
||||
> 校验通过时才把「资格版本标识 + 校验时间 + 是否通过」冻结进当次审批尝试快照并在提现详情可查;校验未通过时按既有资料资格校验拒绝语义 MUST NOT 创建提现申请 / 审批实例 / 审批尝试,稳定原因记在该次拒绝结果与审计事实;两种情形均不改写历史尝试的校验结果;历史尝试按「无留痕」返回(版本 0、校验时间为空、是否通过 0、未通过原因为空串)。
|
||||
|
||||
Requirement 名与 Scenario 未变。落点:`openspec/changes/close-august-iteration-gaps/specs/agent-distribution-withdrawal/spec.md` 与 `openspec/specs/agent-distribution-withdrawal/spec.md` 的「提现冻结与企业微信终审」(记录员本轮只读复核两边同句一致)。
|
||||
|
||||
### 6.3 S1 处置:列宽放宽(代码 + 迁移)
|
||||
|
||||
- `migrations/000233_extend_refund_settlement_fields.up.sql`:`original_channel_trade_no` 由 `VARCHAR(64)` 放宽为 **`VARCHAR(100)`**,对齐来源列 `tb_payment.third_party_trade_no`(该列在 `migrations/000118_create_tb_payment.up.sql:11` 为 `VARCHAR(100)`);`internal/model/refund.go` 字段同步为 `type:varchar(100)` 并更新注释(第 53 行);**down 脚本不变**。
|
||||
- 唯一迁移执行者随后执行 `down 5`(237→232)→ `up 5`(232→237)。执行者汇报的往返核对(**执行者汇报,本轮记录员未复跑该往返**):
|
||||
- `down 5` 后:残留新列=0、`refunds=44` 不变、`version=232` / `dirty=false`;
|
||||
- `up 5` 后核对:`original_channel_trade_no=varchar(100)`、`source_payment_no=varchar(64)`、`offline_settlement_no=varchar(128)`、`offline_settled_at=timestamptz NULL`、`offline_settled_by=int8 NOT NULL default 0`、`attempt.remark=text NOT NULL default ''`、既有 44 行该列为空串可读、`version=237` / `dirty=false`。
|
||||
- 记录员本轮只读复核(`information_schema.columns` + `schema_migrations`,测试库,与上述一致)原文:
|
||||
```
|
||||
tb_refund_request.original_channel_trade_no character varying max_len=100 nullable=NO default=''
|
||||
tb_refund_request.source_payment_no character varying max_len=64 nullable=NO default=''
|
||||
tb_refund_request.offline_settlement_no character varying max_len=128 nullable=NO default=''
|
||||
tb_refund_request.offline_settled_at timestamp with time zone nullable=YES default=null
|
||||
tb_refund_request.offline_settled_by bigint nullable=NO default=0
|
||||
tb_refund_request_attempt.remark text nullable=NO default=''
|
||||
schema_migrations: version=237 dirty=false;tb_refund_request: refunds=44,该列为空串 44 行
|
||||
```
|
||||
|
||||
### 6.4 S4 处置:缺失快照值的检查前移(代码)
|
||||
|
||||
`internal/infrastructure/wecom/approval_form.go`:把「新增必须字段缺快照值」的检查**前移到任何 `attachments.Upload` 之前**(在 `mappings` 循环之外,按 `RequiredSceneFields(businessType)` 扫描快照)。记录员静态复核:该检查循环位于 `approval_form.go:74-81`,早于循环内的 `buildControlValue`(:94)与其内部的 `attachments.Upload`(:161)。
|
||||
|
||||
受控验证原文(附件端口为 **fake**):
|
||||
|
||||
```
|
||||
映射缺必须字段 → 明确失败,upload_calls=0
|
||||
快照缺 refund_package_used_mb → 明确失败,upload_calls=0
|
||||
字段齐备 + 额外映射既有可选字段但快照无值 → err=nil controls=8 upload_calls=1(可选字段仍静默跳过)
|
||||
```
|
||||
|
||||
**口径限定**:该验证证明的是「校验发生在调用 `Upload` 之前」,**不等于**「不访问企业微信」——附件端口为 fake,未触达真实企微。
|
||||
|
||||
### 6.5 O6 处置:过强注释收窄(注释)
|
||||
|
||||
`internal/application/carrierthreshold/evaluate.go` 的过强断言收窄为:
|
||||
|
||||
> 命中事实与停机锁是同一条 INSERT 的列,业务数据无法触发 hit_* CHECK;数据库级故障与既有流量观测一致地整体回滚,停机顺延到下一次观测重试。
|
||||
|
||||
(记录员静态复核:`evaluate.go:79-83` 注释已为该口径。)
|
||||
|
||||
### 6.6 S3 登记(不改代码,归入既有漂移)
|
||||
|
||||
`internal/store/postgres/phone_asset_association_store.go` 的 `CountValidByPhone` 本次仅改**实现体**,方法本体为既有代码,**故未删除**;其注释「用于十项上限判定」为既有文本,本次仅在原注释后追加「实现委托批量计数,保证同一计数口径」。据此归入**「既有漂移」**,**不得写成「本次新增死代码」**。
|
||||
|
||||
### 6.7 文档侧澄清(Requirement 名与 Scenario 均未变)
|
||||
|
||||
1. **h5-popup-notification**:`popup_type` 创建必填;更新按部分更新语义(未传保持原值,一经传入按同一取值域校验);类型变更且请求未显式传优先级时保持原优先级(类型缺省优先级只在创建路径生效)。
|
||||
2. **phone-asset-association**:失败计数为「每次失败累加并刷新窗口时长,相邻失败间隔不超过窗口即持续累加」;锁定为一个窗口时长,且锁定期间提交不计数、不续期。
|
||||
3. **export-time-filter**:计费周期起点 `period_start` 为**精确匹配键**,不属于第二个范围筛选字段,仍复用同一严格解析器。
|
||||
4. **employee-collection-bill**:已核销金额大于应收金额(异常数据)时,未核销金额与剩余可核销金额均按 `0` 返回。
|
||||
5. **order-refund-exchange**:仅「客户收款信息退款(线下到账)」方式可登记线下处理流水号(其余以状态非法拒绝);重提时来源支付事实与实收金额同批从同一原成功支付事实重新冻结。
|
||||
|
||||
### 6.8 未改动观察(如实登记)
|
||||
|
||||
- **O4**:店铺下级数量计数未显式叠加数据范围过滤;复核结论为**不可越界**,仅存在 Redis 缓存窗口。
|
||||
- **O7**:验证码「验证码不存在或已过期」与「验证码错误」两类文案差异属**既有 As-Is**;如需统一须另立变更。
|
||||
|
||||
### 6.9 本轮未复核事项(证据边界)
|
||||
|
||||
- S1 的 `down 5` / `up 5` 往返过程与「残留新列=0」为**唯一执行者汇报**,终轮记录员未复跑该往返(迁移号现停留 237);本轮复核的是往返后的列形态与版本状态(见 6.3)。
|
||||
- 6.4 的 `upload_calls` 三项结论来自审查会话的受控验证原文,终轮记录员本轮**未重跑**该脚手架(脚手架已按纪律清理)。
|
||||
Reference in New Issue
Block a user