feat: add commerce adjustment operations

This commit is contained in:
Codex
2026-06-29 22:28:19 +08:00
parent f3577e257c
commit 9b17f25dd9
12 changed files with 1444 additions and 12 deletions

View File

@@ -22,14 +22,14 @@
- `apps/worker` 后台任务进程CRM webhook 队列消费、generic/钉钉/飞书/企微机器人发送、签名、失败重试和日志commerce worker 可补偿查询微信/支付宝支付和退款状态provider-bills worker 可下载微信/支付宝官方账单并导入资金对账assets worker 可复检托管资源元数据、执行内置安全扫描并自动下架异常资源imports worker 可执行大批量导入public-banks worker 可自动同步公共题库采纳副本exports worker 可渲染 PDF/Word 导出文件和每日一练 ZIP 图片素材包。
- 销售/代理分佣结算基础闭环:租户默认比例、成员比例、激活码批次比例、订单/激活码归因、结算单生成、审核、线下打款状态、CSV/JSON 导出、打款凭证登记/复核和权限隔离。
- 订单售后基础闭环:退款请求、审核、处理状态流、微信/支付宝发起退款、微信/支付宝退款查询确认、微信/支付宝退款通知 webhook、退款金额累计、部分/全额退款订单状态、全额退款权益撤销、退款事件和审计日志。
- 资金对账和差错工单闭环:租户财务/运营可通过 `/api/commerce/reconciliation/*` 导入或预览支付/退款账单行,也可创建微信/支付宝官方账单下载任务;后端按租户隔离比对本地订单、支付、退款记录,识别已匹配、金额不一致、状态不一致、供应商有本地无、本地有供应商无、重复行和无效行,并写入对账批次、明细和审计日志;异常明细可创建差错工单,支持分配、开始处理、升级、解决、忽略、重开和事件留痕。工单只做财务审核闭环,不直接修改订单、支付、退款或权益。
- 资金对账、异常订单和财务凭证闭环:租户财务/运营可通过 `/api/commerce/reconciliation/*` 导入或预览支付/退款账单行,也可创建微信/支付宝官方账单下载任务;后端按租户隔离比对本地订单、支付、退款记录,识别已匹配、金额不一致、状态不一致、供应商有本地无、本地有供应商无、重复行和无效行,并写入对账批次、明细和审计日志;异常明细可创建差错工单,支持分配、开始处理、升级、解决、忽略、重开和事件留痕`/api/commerce/operations/anomalies` 聚合异常订单风险,`/api/commerce/adjustment-vouchers*` 支持人工调整凭证、复核、事件轨迹和报表。工单和凭证只做财务审核闭环,不直接修改订单、支付、退款或权益。
- PocketBase schema/数据导入器雏形和导入后校验脚本。
- 本地 Supabase reset、烟测 seed、API 集成测试、完整重构检查命令。
还没有达到生产交付的部分:
- Supabase Auth/JWT、租户角色模板、班级/教师/学生范围权限已可联调;生产前还要做真实云端 Auth/JWKS 回归和 RLS 深测。
- 阿里云/腾讯云短信、微信小程序登录、微信网页登录、QQ 登录、手机号绑定/换绑、微信支付、支付宝主链路、微信/支付宝发起退款/查询确认/退款通知、支付/退款补偿 worker 已完成本地适配;资金对账已支持手工/API 账单导入、微信/支付宝官方账单下载任务、provider-bills worker 自动导入比对差错工单处理异常订单运营台人工调整凭证复核报表真实生产账号联调还没接完。
- 阿里云/腾讯云短信、微信小程序登录、微信网页登录、QQ 登录、手机号绑定/换绑、微信支付、支付宝主链路、微信/支付宝发起退款/查询确认/退款通知、支付/退款补偿 worker 已完成本地适配;资金对账已支持手工/API 账单导入、微信/支付宝官方账单下载任务、provider-bills worker 自动导入比对差错工单处理异常订单运营台人工调整凭证复核报表真实生产账号、真实回调域名和真实生产账单抽样验收还没接完。
- OSS/COS/Supabase Storage 上传下载签名 provider 已接入上传后校验、PDF/图片预览、资源访问事件、动态水印上下文、锁定资源 CDN 边界、资源复检 worker、内置 `metadata_rules` 安全扫描和外部 HTTP 杀毒/内容安全 scanner 接入层已完成。生产还要配置真实扫描服务 endpoint/token并继续补转码/CDN 级水印、CDN 刷新和对象生命周期策略。
- Excel/CSV 导入解析已完成并复用 `content_import_jobs/items/issues` 管线;大批量异步导入 worker 基础已接入,支持 queued job 消费、重试和审计;导入后复检、模板下载和字段映射 API 已完成,前端 UI 待接。
- 题库导出已完成服务端结构化 payload、PDF/Word 二进制 worker、每日一练基础导出和每日一练 ZIP 图片素材包;后续还要补更精细试卷模板、多模板排版和导出操作台体验。
@@ -316,7 +316,7 @@ API 身份上下文:
- 题库入口和分类使用 `content_entries/content_nodes`;题目列表和练习规则使用 `question_collections/practice_blueprints`,前端不要再把旧树字段当成唯一业务结构。
- 批量导入必须先写 `content_import_jobs/items/issues`,保留原始 payload、规范化 payload、逐行问题和审计记录。题目、单词、知识手册、分数线和视频 JSON/CSV/Excel 导入已走这套后台校验管线;大批量任务可提交 `executionMode=async`,由 imports worker 消费,前端只轮询 job 状态和展示 issues。
- 题库导出必须由后端按权限生成,不允许前端直接读取数据库拼导出文件;不开启答案/解析时顶层题目和复合题子题都必须脱敏PDF/Word/每日一练 ZIP 只通过 exports worker 写入 `content_assets` 后再签名下载/预览。
- 支付 webhook 必须先设计幂等键和验签流程,再进入生产使用;生产环境还应定时运行 commerce worker 兜底供应商漏通知和处理中退款,并定时运行 provider-bills worker 下载官方账单核对本地订单。官方账单下载任务只保存下载域名、hash 和对账批次 ID不向前端暴露下载 URL 或商户密钥。对账差错工单只允许记录财务处理结论和凭证,不允许前端工单接口直接篡改订单、支付、退款或权益状态。
- 支付 webhook 必须先设计幂等键和验签流程,再进入生产使用;生产环境还应定时运行 commerce worker 兜底供应商漏通知和处理中退款,并定时运行 provider-bills worker 下载官方账单核对本地订单。官方账单下载任务只保存下载域名、hash 和对账批次 ID不向前端暴露下载 URL 或商户密钥。对账差错工单和人工调整凭证只允许记录财务处理结论、附件引用和审计事件,不允许前端工单接口或凭证审批接口直接篡改订单、支付、退款或权益状态。
## 最近一次验证
@@ -335,7 +335,7 @@ npm run audit:runtime
git diff --check
```
结果:通过。`npm run test:api` 覆盖资源访问事件、锁定 CDN 资源拒绝、provider-managed CDN 显式放行、学生短 TTL 下载/预览、访问记录查询、安全扫描门禁、官方账单下载任务权限和脱敏响应。`npm run test:worker:commerce` 覆盖支付/退款补偿、微信/支付宝官方账单下载、账单 hash 校验、导入 `provider_download` 对账批次和密钥不泄露。`npm run test:worker:assets` 覆盖托管资源复检、内置安全扫描、外部 HTTP scanner 通过/失败/不可用 fail-closed、扫描失败/跳过事件和异常资源自动下架。`npm run test:worker:exports` 覆盖导出 worker 生成可信资源并标记 `securityScanStatus=passed``npm run audit:runtime` 无 high/critical 漏洞;当前运行时依赖树仍有 `exceljs -> uuid` 的 moderate 级提示,修复需要破坏性降级 `exceljs`,后续应在导入 Excel 回归充分后单独处理。
结果:通过。`npm run test:api` 覆盖资源访问事件、锁定 CDN 资源拒绝、provider-managed CDN 显式放行、学生短 TTL 下载/预览、访问记录查询、安全扫描门禁、官方账单下载任务权限和脱敏响应、异常订单运营台、人工调整凭证提交/复核/事件/报表、租户隔离,以及凭证审批不修改订单/支付/权益`npm run test:worker:commerce` 覆盖支付/退款补偿、微信/支付宝官方账单下载、账单 hash 校验、导入 `provider_download` 对账批次和密钥不泄露。`npm run test:worker:assets` 覆盖托管资源复检、内置安全扫描、外部 HTTP scanner 通过/失败/不可用 fail-closed、扫描失败/跳过事件和异常资源自动下架。`npm run test:worker:exports` 覆盖导出 worker 生成可信资源并标记 `securityScanStatus=passed``npm run audit:runtime` 无 high/critical 漏洞;当前运行时依赖树仍有 `exceljs -> uuid` 的 moderate 级提示,修复需要破坏性降级 `exceljs`,后续应在导入 Excel 回归充分后单独处理。
注意:`apps/taro` 是静态构建工程,线上发布 `apps/taro/dist/**`,不发布 `node_modules`。Taro 4.2.0 当前构建工具链仍会触发 `npm run audit:taro:toolchain` 的上游 high/critical 提示,不能用 `npm audit fix --force` 降级到 Taro 3 破坏构建;上线验收时以 `audit:runtime`、构建产物、前端密钥检查和静态服务器配置为准,并持续跟进 Taro 官方修复。
@@ -347,4 +347,4 @@ git diff --check
2. 继续补 Taro 前端:学生端视频/反馈/模考报告/订单收银台,租户后台写入表单/导入操作台/公共题库同步/角色模板 UI平台后台租户详情/审计/自动计费增强,小程序兼容验证。
3. 对象存储真实 AV/内容安全扫描服务联调、CDN 防盗链、转码/CDN 级水印和生命周期策略。
4. 题库导出模板精排、导出操作台、真实数据 dry-run、导入字段映射 UI 和复检结果操作台。
5. 真实 OAuth/短信/支付生产账号联调、异常订单运营台、人工调整凭证复核、公共题库版本通知/冲突处理操作台、积分活动深化,以及排行榜防刷/预聚合。
5. 真实 OAuth/短信/支付生产账号联调、真实生产账单抽样验收、财务操作台前端体验、公共题库版本通知/冲突处理操作台、积分活动深化,以及排行榜防刷/预聚合。

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,14 @@ import {
refundNotifyRoute,
updateRefundStatusRoute,
} from './routes.js';
import {
adjustmentVoucherEventsRoute,
adjustmentVoucherReportRoute,
adjustmentVouchersRoute,
commerceOperationsAnomaliesRoute,
createAdjustmentVoucherRoute,
updateAdjustmentVoucherStatusRoute,
} from './adjustments.js';
import {
createReconciliationIssueRoute,
importReconciliationRoute,
@@ -54,6 +62,12 @@ export const commerceRoutes: RouteDefinition[] = [
['GET', '/api/commerce/reconciliation/batches', reconciliationBatchesRoute],
['GET', '/api/commerce/reconciliation/items', reconciliationItemsRoute],
['GET', '/api/commerce/reconciliation/anomalies', reconciliationAnomaliesRoute],
['GET', '/api/commerce/operations/anomalies', commerceOperationsAnomaliesRoute],
['GET', '/api/commerce/adjustment-vouchers', adjustmentVouchersRoute],
['POST', '/api/commerce/adjustment-vouchers', createAdjustmentVoucherRoute],
['POST', '/api/commerce/adjustment-vouchers/status', updateAdjustmentVoucherStatusRoute],
['GET', '/api/commerce/adjustment-vouchers/events', adjustmentVoucherEventsRoute],
['GET', '/api/commerce/adjustment-vouchers/report', adjustmentVoucherReportRoute],
['POST', '/api/commerce/reconciliation/provider-bills/request', requestProviderBillDownloadRoute],
['GET', '/api/commerce/reconciliation/provider-bills/jobs', providerBillDownloadJobsRoute],
['POST', '/api/commerce/reconciliation/issues/create', createReconciliationIssueRoute],

View File

@@ -92,6 +92,7 @@ export function tenantPermissionCatalog() {
{ key: 'tenant:payment:write', label: '商户配置管理' },
{ key: 'tenant:reconciliation:read', label: '资金对账查看' },
{ key: 'tenant:reconciliation:write', label: '资金对账导入' },
{ key: 'tenant:reconciliation:review', label: '资金差错复核' },
{ key: 'tenant:reconciliation:download', label: '官方账单下载' },
{ key: 'tenant:refund:read', label: '退款查看' },
{ key: 'tenant:refund:write', label: '退款申请/处理' },

View File

@@ -287,16 +287,23 @@ body: { "code": "<qq oauth code>", "redirectUri": "https://h5.example.com/auth/q
- `GET /api/commerce/reconciliation/issues`:按状态、严重级别、负责人、批次、订单号筛选差错工单。
- `POST /api/commerce/reconciliation/issues/status`:执行 `start/assign/resolve/ignore/escalate/reopen` 状态流转。
- `GET /api/commerce/reconciliation/issues/events`:查看工单创建、分配、处理、解决、忽略、重开等事件轨迹。
- `GET /api/commerce/operations/anomalies`:聚合未关闭对账工单、失败官方账单任务、支付事件错误、长时间 pending 支付/退款,供租户财务/售后运营台使用。
- `GET /api/commerce/adjustment-vouchers`:查询人工调整凭证。
- `POST /api/commerce/adjustment-vouchers`:创建人工调整凭证,可关联差错工单、对账明细、订单、支付或退款。
- `POST /api/commerce/adjustment-vouchers/status`:执行 `submitted/approved/rejected/voided` 复核流转。
- `GET /api/commerce/adjustment-vouchers/events`:查看凭证事件轨迹。
- `GET /api/commerce/adjustment-vouchers/report`:按日期输出凭证状态、类型、来源和日趋势统计。
权限点:
- `tenant:reconciliation:read`:查看/预览对账。
- `tenant:reconciliation:write`:导入对账批次、创建/处理差错工单。
- `tenant:reconciliation:review`:审批、驳回或作废人工调整凭证。
- `tenant:reconciliation:download`:创建微信/支付宝官方账单下载任务。
官方账单下载由 `apps/worker --job provider-bills` 执行。API 只创建 `commerce_bill_download_jobs`,不会在前端返回供应商 `download_url`、商户私钥、微信 API v3 key 或支付宝应用私钥。worker 使用租户 `tenant_payment_accounts.config_public.secretRef` 找到 `app_private.tenant_secrets(secret_scope='payment')`,在后端签名申请下载 URL校验微信返回的 `hash_type/hash_value`,解析 JSON/CSV/ZIP 账单后复用 `importReconciliationBatch` 写入 `commerce_reconciliation_batches/items``source='provider_download'`
对账差错工单只生成差异台账、处理记录和审计,不自动修改订单、支付、退款和权益。`resolve/ignore` 只是财务审核结论,例如 `manual_adjustment``provider_confirmed``false_positive`最终落账仍必须走退款状态机、支付补偿、手工支付确认或后续专门的人工调整命令。人工调整凭证附件、财务复核报表、异常订单运营台和真实生产账单格式抽样验收仍需要继续补。
对账差错工单和人工调整凭证只生成差异台账、处理记录、凭证附件引用和审计,不自动修改订单、支付、退款和权益。`resolve/ignore` 只是财务审核结论,例如 `manual_adjustment``provider_confirmed``false_positive`;调整凭证审批也只表示财务复核通过。最终落账仍必须走退款状态机、支付补偿、手工支付确认或后续专门的落账命令。真实生产账单格式抽样验收和前端财务操作台仍需要继续补。
B 端合作商年费、服务费、服务器资源费不走学生端 `orders`,而是走平台账务:

View File

@@ -122,7 +122,7 @@
| 资金流水对账 | 可联调 | `commerce_reconciliation_batches/items` + `/api/commerce/reconciliation/preview/import/batches/items/anomalies`;租户后台需 `tenant:reconciliation:read/write`,支持支付/退款账单行手工或 API 导入、来源 hash、批次统计、逐行匹配、金额/状态差异、本地缺失、供应商缺失、重复行、无效行和审计;对账只生成差异,不自动改订单/权益 |
| 对账差错工单 | 可联调 | `commerce_reconciliation_issues/events` + `/api/commerce/reconciliation/issues*`;异常明细可创建工单,支持分配、开始处理、升级、解决、忽略、重开、事件轨迹和审计;处理结论只作为财务审核记录,不直接修改订单、支付、退款或权益 |
| 官方账单下载 | 可联调 | `commerce_bill_download_jobs` + `/api/commerce/reconciliation/provider-bills/request/jobs` + `apps/worker --job provider-bills`;租户后台需 `tenant:reconciliation:download` 创建任务worker 后端使用租户商户密钥申请微信/支付宝官方账单下载 URL、校验 hash、解析 JSON/CSV/ZIP 账单并复用同一套 `provider_download` 对账导入响应只暴露任务状态、下载域名、hash 和对账批次 ID不暴露下载 URL 或密钥 |
| 异常订单运营台和财务报表 | 待补齐 | 后续补人工调整凭证附件/复核、异常订单运营台、财务复核报表和真实生产账单格式抽样验收 |
| 异常订单运营台和财务报表 | 可联调 | `commerce_adjustment_vouchers/events` + `/api/commerce/operations/anomalies``/api/commerce/adjustment-vouchers*`;支持聚合未关闭对账工单、失败官方账单任务、支付事件错误、长时间 pending 支付/退款,支持人工调整凭证提交、审批、驳回、作废、事件轨迹和复核报表;需 `tenant:reconciliation:read/write/review`,凭证只做审计证据,不直接修改订单、支付、退款或权益;真实生产账单格式抽样和前端财务操作台待继续验收 |
## 租户后台与平台后台

View File

@@ -27,7 +27,7 @@
| 刷题题库 | 已建题库、题目、题目版本、内容入口、任意深度分类树、考试意向标记、题目集合、练习蓝图、导入任务台账、导出任务台账、公共题库授权/采纳表、租户内容通知表 | 已支持核心映射JSON/CSV/Excel 导入可落到新入口/节点/集合,阅读理解/案例分析子题沿用 `subQuestions/sub_questions` | 题目列表、内容入口、分类树、集合题目、顺序/随机/全真模拟 session、答题提交、复合题 `subAnswers` 判分和报告明细、租户后台题目录入/更新、JSON/CSV/Excel 预览/导入、JSON/试卷 payload 导出、PDF/Word 异步导出 worker、每日一练九宫格 metadata、PDF/Word 运营版式、ZIP 图片素材包、异步导入 worker、平台公共题库授权、租户采纳快照、手动同步、自动同步 worker、同步通知、冲突查询和单条/批量冲突处理已实现 | 核心 API 集成测试含导航、组卷、复合题后台录入/练习/判分/报告、导入、导出权限/脱敏、每日一练导出 metadata、异步 PDF/Word/每日一练 ZIP job 创建、exports worker、公共题库授权、采纳后组卷、同步新增题、通知隔离/已读/自动 resolved、租户自改冲突保护、单条/批量冲突处理和 worker 自动同步断言 | 新题库导航和组卷基础闭环可跑,阅读理解/案例分析多小题第一版可联调,公共题库采纳/手动/自动同步、同步通知、冲突查询/处理、导入后复检、模板下载、字段映射 API、JSON/PDF/Word/每日一练 ZIP 基础导出可联调;公共题库生产调度/失败告警、更精细导出模板和更完整运营消息仍需补齐 |
| 错题本 | 已建 `wrong_questions` | 已支持旧错题归一化 | 错题列表、答题自动入错题、移出错题已实现 | 仅烟测 | 基础功能已实现,复习计划和统计未完成 |
| 收藏夹 | 已建 `favorite_questions` | 已支持旧收藏归一化 | 收藏/取消收藏、收藏列表已实现 | 仅烟测 | 基础功能已实现 |
| 用户订阅/题库会员/SVIP | 已建 `orders``payments``entitlements``svip_plans`、激活码 | 已映射旧 SVIP/会员权益 | 下单、订单详情/状态轮询、手工支付确认权限保护、微信/支付宝支付、微信/支付宝发起退款、微信/支付宝退款查询确认、微信/支付宝退款通知 webhook、激活码预检查/兑换、优惠券抵扣、零元订单自动开通、权益查询、支付/退款补偿、微信/支付宝官方账单下载、资金对账差错工单已实现 | API 集成测试、commerce worker 集成测试 | 商城主链路可联调,异常订单运营台、真实生产账单格式验收生产账号联调待补 |
| 用户订阅/题库会员/SVIP | 已建 `orders``payments``entitlements``svip_plans`、激活码 | 已映射旧 SVIP/会员权益 | 下单、订单详情/状态轮询、手工支付确认权限保护、微信/支付宝支付、微信/支付宝发起退款、微信/支付宝退款查询确认、微信/支付宝退款通知 webhook、激活码预检查/兑换、优惠券抵扣、零元订单自动开通、权益查询、支付/退款补偿、微信/支付宝官方账单下载、资金对账差错工单、异常订单运营台和人工调整凭证已实现 | API 集成测试、commerce worker 集成测试 | 商城主链路可联调,真实生产账单格式验收生产账号联调和前端售后/财务体验待补 |
| 背单词 | 已建单词单元、单词、进度、收藏表,并可绑定 `content_entries/content_nodes` | 已支持内容和部分用户状态映射 | 单元/单词只读、进度、收藏、统计、每日复习计划、租户后台单词维护 API、旧模板/新模板 JSON 预览导入、排行榜已实现 | 核心 API 集成测试含导入和排行榜断言 | 学生端学习状态、后台维护、批量 JSON 导入和基础排行榜已实现,更细复习参数和后台统计待完善 |
| 知识手册 | 已建手册科目、章节、条目,并可绑定 `content_entries/content_nodes` | 已支持内容导入 | 只读 API、租户后台手册科目/章节/条目维护 API、嵌套 JSON 预览导入已实现 | 核心 API 集成测试含导入断言 | 学生端阅读、后台维护和批量 JSON 导入基础可用,富文本资源/版本管理待补 |
| 分数线 | 已建院校、专业、字段、记录表 | 已支持导入映射 | 字段、院校、专业、记录、趋势、年份、租户后台维护 API、JSON 预览导入已实现 | 核心 API 集成测试含导入断言 | 查询、后台维护和批量 JSON 导入基础闭环已实现,复杂动态筛选和 AI 择校上下文待补 |

View File

@@ -30,7 +30,7 @@
| 背单词 | `VocabularyPage.tsx``VocabularyQuiz.tsx` | 部分覆盖 | 单词列表、进度、收藏、统计、每日计划和后端复习调度已覆盖;后续补收藏练习体验、发音/音频策略、排行榜和更精细的间隔算法参数 |
| 知识手册 | `Handbook*.tsx` | 已覆盖 | 前端需做好 Markdown/公式/图片渲染和搜索体验 |
| 分数线 | `ScorelinePage.tsx` | 已覆盖 | 动态字段/趋势、后台维护和 JSON 批量导入已有;后续补复杂筛选优化和 AI 择校数据上下文 |
| 商城/SVIP | `Store.tsx``SvipModal.tsx` | 部分覆盖 | 套餐、订单、订单详情/状态轮询、权益、激活码预检查/兑换、优惠券领取/下单抵扣、微信支付/支付宝 provider 主链路、内部退款状态机、微信/支付宝发起退款、退款查询确认、退款通知 webhook、支付/退款补偿 worker、全额退款权益撤销、资金对账手工/API 导入比对、微信/支付宝官方账单下载 worker、异常查询、差错工单和事件轨迹已有;缺异常订单运营台、人工调整凭证复核报表前端收银台/售后体验 |
| 商城/SVIP | `Store.tsx``SvipModal.tsx` | 部分覆盖 | 套餐、订单、订单详情/状态轮询、权益、激活码预检查/兑换、优惠券领取/下单抵扣、微信支付/支付宝 provider 主链路、内部退款状态机、微信/支付宝发起退款、退款查询确认、退款通知 webhook、支付/退款补偿 worker、全额退款权益撤销、资金对账手工/API 导入比对、微信/支付宝官方账单下载 worker、异常查询、差错工单和事件轨迹异常订单运营台、人工调整凭证复核报表已有;缺前端收银台/售后体验、真实生产账单抽样验收和财务操作台 UI |
| 个人中心 | `Profile.tsx` | 部分覆盖 | 基本资料、手机号绑定/换绑、权益、订单统计、练习历史、学习统计、签到积分、考试倒计时、趋势和勋章展示 API 已有;缺学习报告可视化 |
| 资料下载 | `QuestionExporterPublishModal.tsx` 等 | 部分覆盖 | 资源台账、上传确认、签名下载、PDF/图片预览、动态水印上下文、worker 复检、内置安全扫描和外部 HTTP scanner 接入层已有;缺前端水印渲染、深度防盗链、真实 AV/内容安全服务联调和生命周期策略 |
| AI 择校推荐 | 业务规划新增 | 部分覆盖 | 已有 SVIP 门禁、学生输入 schema、地区/分数线上下文、稳定 JSON 输出、报告台账、审计和 Taro 学生端基础页;真实 AI provider、prompt 版本管理后台、报告 PDF 渲染和更细推荐算法待补 |

View File

@@ -29,6 +29,7 @@
- 支付/退款补偿 worker 已完成:`apps/worker --job commerce` 可查询微信/支付宝支付和处理中退款,补偿漏通知订单,支付成功幂等开通权益,退款成功幂等更新退款/订单/支付并在全额退款时撤销订单权益。
- 资金对账和差错工单闭环已完成:`commerce_reconciliation_batches/items``/api/commerce/reconciliation/*` 支持手工/API 导入供应商账单行、预览差异、生成批次统计、查询异常、租户隔离、权限点 `tenant:reconciliation:read/write` 和审计日志;`commerce_reconciliation_issues/events` 支持异常明细创建工单、分配、开始处理、升级、解决、忽略、重开和事件留痕,且不直接修改订单/支付/退款/权益。
- 微信/支付宝官方账单下载地基已完成:`commerce_bill_download_jobs``POST /api/commerce/reconciliation/provider-bills/request``GET /api/commerce/reconciliation/provider-bills/jobs``apps/worker --job provider-bills` 已接入worker 负责后端签名申请下载 URL、hash 校验、JSON/CSV/ZIP 账单解析、复用 `provider_download` 对账导入、任务状态回写和密钥脱敏。
- 异常订单运营台和人工调整凭证已完成后端第一版:`/api/commerce/operations/anomalies` 聚合未关闭对账工单、失败官方账单任务、支付事件错误、长时间 pending 支付/退款;`/api/commerce/adjustment-vouchers*` 支持凭证提交、审批、驳回、作废、事件轨迹和复核报表,使用 `tenant:reconciliation:review` 做独立复核权限,且审批凭证不会直接修改订单、支付、退款或权益。
- 内容资源复检与安全扫描 worker 已完成:`apps/worker --job assets` 可复检 `content_assets` 中的托管对象元数据,并执行内置 `metadata_rules` 和可选外部 HTTP scanner正常资源写回复检/扫描证据,异常资源自动置为 `failed/skipped + draft``security_scan_status=failed`,外部 scanner 不可用默认 fail-closed并写入审计、扫描事件和安全标记。
- 题库导出 worker 已完成:`apps/worker --job exports` 可抢占 `pdf/docx/daily_practice_zip` 导出任务,渲染 PDF/Word、水印或每日一练图片素材包写入对象存储或本地开发存储创建 `content_assets` 并回填 `assetId/hash/size``daily_practice` 已支持每日一练九宫格 metadata、PDF/Word 基础版式、9 张 PNG/SVG 卡片和拼图 ZIP。
- 租户后台媒体运营报表已完成:`/api/tenant-content/media-analytics/summary``asset-events``video-events` 可按 7/30/90 天、资源、视频、用户和水印 traceId 查询资料访问、视频播放、拒绝访问、Top 资源/视频和日趋势;仅开放给 owner/admin/operator 或 `content:analytics:read` 权限角色,前端不会拿到签名 URL 或播放 token。
@@ -76,7 +77,7 @@
- 已完成微信支付 JSAPI、支付宝 WAP/H5 的创建支付参数和 webhook 幂等开通权益。
- 已完成内部退款状态机、退款申请/审核/处理接口、微信/支付宝发起退款、微信/支付宝退款查询确认、微信/支付宝退款通知 webhook、部分/全额退款状态、全额退款权益撤销和审计事件。
- 已完成支付/退款补偿 worker可兜底供应商漏通知、处理中退款和重复执行幂等。
- 已完成资金对账手工/API 导入比对、微信/支付宝官方账单下载任务、批次/明细/异常查询、差错工单状态流和审计;继续补真实生产账单格式抽样验收、人工调整凭证附件、财务复核报表和异常订单运营台
- 已完成资金对账手工/API 导入比对、微信/支付宝官方账单下载任务、批次/明细/异常查询、差错工单状态流、异常订单运营台、人工调整凭证和财务复核报表;继续补真实生产账单格式抽样验收和前端财务操作台体验
- 租户自有商户收款和平台代收/服务商模式。
2. 国内登录和短信
@@ -115,7 +116,7 @@
8. 订单和营销体验
- 已完成订单详情、订单状态轮询、激活码预检查、优惠券前台领取、下单抵扣计算和内部退款状态机。
- 已完成支付/退款补偿 worker、官方账单下载 worker、资金对账导入比对差错工单;继续补异常订单运营台优惠券核销报表复杂活动规则。
- 已完成支付/退款补偿 worker、官方账单下载 worker、资金对账导入比对差错工单异常订单运营台和人工调整凭证复核;继续补优惠券核销报表复杂活动规则和前端售后操作台
9. 积分和反馈增强
- 已完成每日签到、积分流水、反馈提交、租户后台处理、奖励积分幂等。

View File

@@ -1853,7 +1853,7 @@ approved/processing -> failed
- 退款通知地址由支付账户或 `submit_provider_refund.providerNotifyUrl` 配置,后端公开接收路径为 `POST /api/commerce/refunds/notify/wechat_pay?tenantId=<tenantId>``POST /api/commerce/refunds/notify/alipay?tenantId=<tenantId>`。这是支付平台回调地址Taro 前端不要主动调用。
- 退款通知只会推进已经审核/处理中的退款申请;未审核的 `requested` 退款不能被外部通知直接落账。
- 已经 `succeeded` 的退款不能再次查询或再次标记成功,避免订单退款金额重复累加。前端应按接口返回状态展示,不要假设点击后立即到账。
- 自动补偿 worker 已接入:支付漏通知和处理中退款会由后端定时查询供应商并幂等落账。资金对账已支持租户后台手工/API 导入供应商账单、微信/支付宝官方账单下载任务、查询差异差错工单处理异常订单运营台人工调整凭证复核后续继续补。生产联调时仍需保留人工确认/失败登记入口。
- 自动补偿 worker 已接入:支付漏通知和处理中退款会由后端定时查询供应商并幂等落账。资金对账已支持租户后台手工/API 导入供应商账单、微信/支付宝官方账单下载任务、查询差异差错工单处理异常订单运营台人工调整凭证复核报表。生产联调时仍需保留人工确认/失败登记入口。
### 租户后台资金对账
@@ -2057,6 +2057,112 @@ ignored 无效行或不符合本次 billType
- 对账差异和差错工单只是运营判断依据,`resolve/ignore` 不会落账。最终订单修正必须走退款、补偿、人工确认或后续专门的人工调整接口。
- 当前后端支持 JSON 行手工导入;官方账单下载 worker 支持微信/支付宝账单 JSON/CSV/ZIP 解析,并复用同一套对账导入逻辑。真实生产接入时仍要用真实账单文件抽样验收字段映射。
### 异常订单运营台和调整凭证
租户后台财务/售后页可以用异常运营台作为入口。该页只展示待处理风险和凭证复核状态,不允许前端直接修改订单、支付、退款或权益。
异常运营台:
```text
GET /api/commerce/operations/anomalies?provider=wechat_pay&limit=50
权限tenant:reconciliation:read
```
返回中 `items[].type` 可能是:
```text
reconciliation_issue 未关闭对账差错工单
provider_bill_job 官方账单下载失败或运行超时
payment_event_error 支付/退款通知处理异常
stuck_pending_payment 长时间 pending 支付
stuck_refund 长时间待处理/处理中退款
```
创建人工调整凭证:
```text
POST /api/commerce/adjustment-vouchers
权限tenant:reconciliation:write
body: {
"voucherNo": "ADJ-20260629-001",
"reconciliationIssueId": "<issueId>",
"adjustmentType": "write_off",
"direction": "decrease",
"amountCents": 990,
"title": "供应商缺失账单人工核销凭证",
"description": "仅作为财务复核证据",
"assetId": "<contentAssetId可选>",
"externalUrl": "https://finance.example.com/proofs/ADJ-20260629-001",
"metadata": {
"operatorRemark": "后台上传凭证"
}
}
```
可关联的来源字段:
```text
reconciliationIssueId 对账差错工单
reconciliationItemId 对账明细
orderId / orderNo 订单
paymentId 支付记录
refundRequestId/refundNo 退款申请
sourceType=manual 纯人工凭证
```
凭证字段:
```text
adjustmentType:
manual_payment_confirm | refund_correction | provider_confirmed |
local_corrected | write_off | duplicate | other
direction:
increase | decrease | none
status:
draft | submitted | approved | rejected | voided
```
查询凭证:
```text
GET /api/commerce/adjustment-vouchers?status=submitted&orderNo=<orderNo>
权限tenant:reconciliation:read
```
复核凭证:
```text
POST /api/commerce/adjustment-vouchers/status
权限tenant:reconciliation:review
body: {
"voucherId": "<voucherId>",
"status": "approved | rejected | voided",
"reviewNote": "财务复核意见",
"metadata": {
"reviewChannel": "tenant-admin"
}
}
```
查看凭证轨迹和报表:
```text
GET /api/commerce/adjustment-vouchers/events?voucherId=<voucherId>
GET /api/commerce/adjustment-vouchers/report?startDate=2026-06-01&endDate=2026-06-29
权限tenant:reconciliation:read
```
前端处理规则:
- 学生端不要接这些接口。
- `tenant:reconciliation:write` 可创建凭证,`tenant:reconciliation:review` 才能审批、驳回或作废凭证。
- 后端会校验凭证来源和附件 `content_assets` 必须属于当前租户。
-`approved/rejected/voided` 的凭证不能翻转到其它关闭状态。
- 凭证审批不会自动改订单、支付、退款和权益;真正落账仍要走退款状态机、支付补偿、手工支付确认或后续专门落账命令。
- 前端可在差错工单 `resolve` 时把 `metadata.voucherNo` 一并传入,用于人读检索,但不要把它当成落账动作。
### 激活码预检查与兑换
兑换前建议先调用:

View File

@@ -2541,6 +2541,180 @@ async function testCommerce() {
});
assert.equal(crossTenantProviderBillJobsDenied.code, 'TENANT_ADMIN_REQUIRED', 'provider bill jobs must be tenant isolated');
const operationReconItems = await request('/api/commerce/reconciliation/items', {
userId: TENANT_ADMIN_USER_ID,
query: { batchId: reconciliationImport.item.id, matchStatus: 'amount_mismatch' },
});
const operationReconItem = operationReconItems.items?.find(item => item.matchStatus === 'amount_mismatch');
assert.ok(operationReconItem?.id, 'operation anomaly test should have an open actionable reconciliation item');
const operationIssue = await request('/api/commerce/reconciliation/issues/create', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
itemId: operationReconItem.id,
summary: '运营台展示未关闭对账差错',
note: '保持 open 供运营台测试',
metadata: { source: 'operation-anomaly-test' },
},
});
assert.equal(operationIssue.item?.status, 'open', 'operation anomaly issue should remain open');
const studentOperationsDenied = await request('/api/commerce/operations/anomalies', {
expectStatus: 403,
});
assert.equal(studentOperationsDenied.code, 'TENANT_ADMIN_REQUIRED', 'students must not access finance operation anomalies');
const operationAnomalies = await request('/api/commerce/operations/anomalies', {
userId: TENANT_ADMIN_USER_ID,
query: { provider: 'manual' },
});
assert.ok(
operationAnomalies.items?.some(item => item.type === 'reconciliation_issue' && item.id === operationIssue.item.id),
'operation anomaly console should surface open reconciliation issues',
);
const voucherSourceOrderBefore = await request('/api/commerce/orders/status', {
query: { orderNo: reconMissingProviderOrder.item.orderNo },
});
const voucherSourcePaymentBefore = voucherSourceOrderBefore.item?.payment;
const voucherEntitlementsBefore = await request('/api/commerce/entitlements', {});
const studentAdjustmentDenied = await request('/api/commerce/adjustment-vouchers', {
method: 'POST',
body: {
reconciliationIssueId: reconciliationIssue.item.id,
title: 'student should not create adjustment voucher',
},
expectStatus: 403,
});
assert.equal(studentAdjustmentDenied.code, 'TENANT_ADMIN_REQUIRED', 'students must not create adjustment vouchers');
const adjustmentVoucherNo = `ADJ-INTEGRATION-${Date.now()}`;
const adjustmentVoucher = await request('/api/commerce/adjustment-vouchers', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
voucherNo: adjustmentVoucherNo,
reconciliationIssueId: reconciliationIssue.item.id,
adjustmentType: 'write_off',
direction: 'decrease',
amountCents: reconMissingProviderOrder.item.amountCents,
title: '供应商缺失账单人工核销凭证',
description: '仅作为财务复核证据,不直接修改订单状态',
externalUrl: 'https://finance.example.test/proofs/adj-integration-001',
metadata: { source: 'api-integration-test' },
},
});
assert.ok(adjustmentVoucher.item?.id, 'tenant admin should create adjustment voucher');
assert.equal(adjustmentVoucher.item?.status, 'submitted', 'adjustment voucher should default to submitted');
assert.equal(adjustmentVoucher.item?.reconciliationIssueId, reconciliationIssue.item.id, 'voucher should link reconciliation issue');
assert.equal(adjustmentVoucher.item?.orderNo, reconMissingProviderOrder.item.orderNo, 'voucher should copy order identity from issue');
const duplicateAdjustmentVoucher = await request('/api/commerce/adjustment-vouchers', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
voucherNo: adjustmentVoucherNo,
reconciliationIssueId: reconciliationIssue.item.id,
title: 'duplicate voucher number should be rejected',
},
expectStatus: 409,
});
assert.equal(duplicateAdjustmentVoucher.code, 'ADJUSTMENT_VOUCHER_DUPLICATE', 'duplicate voucher number should return business conflict');
const invalidAdjustmentSource = await request('/api/commerce/adjustment-vouchers', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
reconciliationIssueId: 'not-a-uuid',
title: 'invalid source id should be rejected',
},
expectStatus: 400,
});
assert.equal(invalidAdjustmentSource.code, 'INVALID_UUID', 'adjustment voucher should reject malformed source UUIDs');
const listedAdjustmentVouchers = await request('/api/commerce/adjustment-vouchers', {
userId: TENANT_ADMIN_USER_ID,
query: { status: 'submitted', orderNo: reconMissingProviderOrder.item.orderNo },
});
assert.ok(
listedAdjustmentVouchers.items?.some(item => item.id === adjustmentVoucher.item.id),
'adjustment voucher list should filter submitted vouchers by order number',
);
const adjustmentReport = await request('/api/commerce/adjustment-vouchers/report', {
userId: TENANT_ADMIN_USER_ID,
query: { startDate: billDate, endDate: billDate },
});
assert.ok(adjustmentReport.item?.totalCount >= 1, 'adjustment voucher report should include created voucher');
assert.ok(
adjustmentReport.item?.byStatus?.some(item => item.status === 'submitted' && item.count >= 1),
'adjustment voucher report should aggregate by status',
);
const approvedAdjustmentVoucher = await request('/api/commerce/adjustment-vouchers/status', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
voucherId: adjustmentVoucher.item.id,
status: 'approved',
reviewNote: '财务复核通过,仅保留审计凭证',
metadata: { reviewedFrom: 'api-integration-test' },
},
});
assert.equal(approvedAdjustmentVoucher.item?.status, 'approved', 'reviewer should approve submitted adjustment voucher');
assert.equal(approvedAdjustmentVoucher.item?.reviewedBy, TENANT_ADMIN_USER_ID, 'voucher approval should record reviewer');
const adjustmentVoucherEvents = await request('/api/commerce/adjustment-vouchers/events', {
userId: TENANT_ADMIN_USER_ID,
query: { voucherId: adjustmentVoucher.item.id },
});
assert.ok(
adjustmentVoucherEvents.items?.some(item => item.eventType === 'submitted' && item.toStatus === 'submitted'),
'voucher events should include submit event',
);
assert.ok(
adjustmentVoucherEvents.items?.some(item => item.eventType === 'approved' && item.toStatus === 'approved'),
'voucher events should include approval event',
);
const closedVoucherChangeDenied = await request('/api/commerce/adjustment-vouchers/status', {
userId: TENANT_ADMIN_USER_ID,
method: 'POST',
body: {
voucherId: adjustmentVoucher.item.id,
status: 'rejected',
reviewNote: 'closed voucher should not flip to rejected',
},
expectStatus: 409,
});
assert.equal(closedVoucherChangeDenied.code, 'ADJUSTMENT_VOUCHER_CLOSED', 'closed adjustment voucher must be immutable');
const crossTenantVoucherDenied = await request('/api/commerce/adjustment-vouchers', {
tenantId: PARTNER_TENANT_ID,
userId: TENANT_ADMIN_USER_ID,
query: { voucherNo: adjustmentVoucherNo },
expectStatus: 403,
});
assert.equal(crossTenantVoucherDenied.code, 'TENANT_ADMIN_REQUIRED', 'adjustment vouchers must be tenant isolated');
const voucherSourceOrderAfter = await request('/api/commerce/orders/status', {
query: { orderNo: reconMissingProviderOrder.item.orderNo },
});
const voucherEntitlementsAfter = await request('/api/commerce/entitlements', {});
assert.equal(voucherSourceOrderAfter.item?.status, voucherSourceOrderBefore.item?.status, 'adjustment voucher approval must not mutate order status');
assert.equal(
voucherSourceOrderAfter.item?.payment?.status,
voucherSourcePaymentBefore?.status,
'adjustment voucher approval must not mutate payment status',
);
assert.equal(
voucherEntitlementsAfter.items?.length,
voucherEntitlementsBefore.items?.length,
'adjustment voucher approval must not grant or revoke entitlements',
);
const fakeWechatPay = await startFakeWechatPayServer();
const wechatAccount = await request('/api/tenant-admin/payment-accounts', {
userId: TENANT_ADMIN_USER_ID,
@@ -6210,6 +6384,7 @@ async function testTenantMemberPermissionsAndAudit() {
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'badges:grant'), 'permission matrix should expose badge grant permission');
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:read'), 'permission matrix should expose reconciliation read permission');
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:write'), 'permission matrix should expose reconciliation write permission');
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:review'), 'permission matrix should expose reconciliation review permission');
assert.ok(permissionMatrix.menuGroups?.some(item => item.key === 'sales'), 'permission matrix should expose menu groups');
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('dashboard:read'), 'tenant operator defaults should include dashboard read');
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('marketing:*'), 'permission matrix should include role defaults');

View File

@@ -0,0 +1,99 @@
create table if not exists public.commerce_adjustment_vouchers (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
voucher_no text not null,
source_type text not null default 'manual'
check (source_type in ('reconciliation_issue', 'reconciliation_item', 'order', 'payment', 'refund', 'manual')),
source_id uuid,
reconciliation_issue_id uuid references public.commerce_reconciliation_issues(id) on delete set null,
reconciliation_item_id uuid references public.commerce_reconciliation_items(id) on delete set null,
order_id uuid references public.orders(id) on delete set null,
payment_id uuid references public.payments(id) on delete set null,
refund_request_id uuid references public.commerce_refund_requests(id) on delete set null,
order_no text,
refund_no text,
provider text,
provider_trade_no text,
provider_refund_no text,
adjustment_type text not null default 'other'
check (adjustment_type in (
'manual_payment_confirm',
'refund_correction',
'provider_confirmed',
'local_corrected',
'write_off',
'duplicate',
'other'
)),
direction text not null default 'none'
check (direction in ('increase', 'decrease', 'none')),
amount_cents integer not null default 0 check (amount_cents >= 0),
status text not null default 'draft'
check (status in ('draft', 'submitted', 'approved', 'rejected', 'voided')),
title text not null,
description text,
asset_id uuid references public.content_assets(id) on delete set null,
external_url text,
submitted_by uuid references public.platform_users(id) on delete set null,
submitted_at timestamptz,
reviewed_by uuid references public.platform_users(id) on delete set null,
reviewed_at timestamptz,
review_note text,
voided_by uuid references public.platform_users(id) on delete set null,
voided_at timestamptz,
metadata jsonb not null default '{}'::jsonb,
created_by uuid references public.platform_users(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (tenant_id, voucher_no)
);
create table if not exists public.commerce_adjustment_voucher_events (
id uuid primary key default gen_random_uuid(),
tenant_id uuid not null references public.tenants(id) on delete cascade,
voucher_id uuid not null references public.commerce_adjustment_vouchers(id) on delete cascade,
from_status text,
to_status text,
event_type text not null,
actor_user_id uuid references public.platform_users(id) on delete set null,
note text,
details jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now(),
check (from_status is null or from_status in ('draft', 'submitted', 'approved', 'rejected', 'voided')),
check (to_status is null or to_status in ('draft', 'submitted', 'approved', 'rejected', 'voided'))
);
create index if not exists idx_commerce_adjustment_vouchers_tenant_status
on public.commerce_adjustment_vouchers(tenant_id, status, created_at desc);
create index if not exists idx_commerce_adjustment_vouchers_source
on public.commerce_adjustment_vouchers(tenant_id, source_type, source_id, created_at desc);
create index if not exists idx_commerce_adjustment_vouchers_issue
on public.commerce_adjustment_vouchers(tenant_id, reconciliation_issue_id, status, created_at desc);
create index if not exists idx_commerce_adjustment_vouchers_order
on public.commerce_adjustment_vouchers(tenant_id, order_no, status, created_at desc);
create index if not exists idx_commerce_adjustment_voucher_events_voucher
on public.commerce_adjustment_voucher_events(tenant_id, voucher_id, created_at desc);
alter table public.commerce_adjustment_vouchers enable row level security;
alter table public.commerce_adjustment_voucher_events enable row level security;
drop policy if exists tenant_isolation on public.commerce_adjustment_vouchers;
create policy tenant_isolation on public.commerce_adjustment_vouchers
for all
using (tenant_id = app.current_tenant_id() or app.is_platform_admin())
with check (tenant_id = app.current_tenant_id() or app.is_platform_admin());
drop policy if exists tenant_isolation on public.commerce_adjustment_voucher_events;
create policy tenant_isolation on public.commerce_adjustment_voucher_events
for all
using (tenant_id = app.current_tenant_id() or app.is_platform_admin())
with check (tenant_id = app.current_tenant_id() or app.is_platform_admin());
drop trigger if exists set_updated_at on public.commerce_adjustment_vouchers;
create trigger set_updated_at
before update on public.commerce_adjustment_vouchers
for each row execute function app.touch_updated_at();