docs: 清理过时文档(以代码为准)

删除(整篇已被代码淘汰 / 重复副本 / 与仓库无关):
- cloudflared_tunnel.txt:全仓无 cloudflared 配置,个人终端记录,无任何引用
- 履约配置/工程债-lewan拆分与短链.md:文中 services/kuaishou-cloud 与 kuaishou-cloud-executor 已删除
- 履约配置/快手Cloud领取mock.md:mock:claim / mock:feifei 与 dev-mock 卡片已移除(只剩 mock:open91)
- 行业电子凭证/解决方案/技术方案/1.基础技术.md(+ 其孤立 image.png):0.基础技术.md 的 75 行截断副本
- 寄卖库存功能设计与实施方案.md:手工入库已 409 停用、收集链接表已随 079 删除、统计看板未实现
- 服务器图片迁移到阿里云OSS.md:MinIO→OSS 迁移已完成(生产 STORAGE_MODE=oss)
- pgBackRest备份方案分析.md:全仓无 pgBackRest 落地痕迹,文档自述暂缓实施
- finance-implementation-plan.md:财务模块已落地(072 迁移 / RBAC / 页面接口),已实现部分与代码重复
  (未落地的二期三期路线图可从本提交的历史版本取回)

改写为与代码一致(已下线通道不再当作现行能力):
- 履约配置/多发货平台与电子凭证整体链路.md:kuaishou-cloud / kuaishou-feifei 标注为已下线 legacy stub,
  现行自动通道 affiliate-dash、人工 manual_dispatch;核销时机与券表按实现校正
- 履约配置/{UID领取与旧单兼容, open91请求, TypeScript类型收紧}.md:lewan/feifei/cloudtentacles 表述改为
  现行 affiliate-dash 链路;normalize 具名类型 KuaishouCloudFlow/FeifeiFlow → AffiliateDashFlow
-  affiliate_dash_发货平台/affiliate-dash对接.md:§13/§14/§19/§21/§24/§25 的已删文件与已下线通道引用校正,
  §21/§22 加「当时快照 / 落地后现状」横幅,回调去重表按 013_affiliate_dash_webhook_events.sql 改写
- 91卡券/{3.异步卡密下单, 5.当前项目对接配置, 6.开发设计}.md:删掉不存在的后台入口、
  cloudtentacles/kuaishou_ct_assisted 依赖、cardsEncoding 与 .js 代码落点校正
- 行业电子凭证/技术方案.md:checklist 指向不存在的 测试用例.md → gen-test-cases.ts + 5.测试指南.md
- 履约配置/cloudtentacles发货平台.md:补「本仓库未接入该通道」说明(保持外部参考)
- 部署启动与数据库重建.md:补齐生产必需变量 APP_DOMAIN / CORS_ALLOWED_ORIGINS / STORAGE_MODE / STORAGE_OSS_*

其它:
- README.md:迁移序号范围 002~009 → 002~080
- 寄卖系统迁移079上库前检查.sql:第 2 段注释按批次 Y 的自动去重更新;新增 2.1「重复退款风险」只读排查
  (已在一次性 PostgreSQL 16 上跑通 001→080 迁移:整份脚本 exit=0,脏数据下 2/2.1 均按预期报警)
- 寄卖系统优化方案.md:删掉链路图里已下线的「客服手工录入」,去掉指向已删设计稿的引用
This commit is contained in:
yml2213
2026-09-17 16:57:27 +08:00
parent 617794f2cf
commit ccb5b6f4d6
23 changed files with 231 additions and 1016 deletions
+1 -1
View File
@@ -119,7 +119,7 @@ SKIP_CHECKS=1 git push
| 文件 | 含义 |
| --- | --- |
| `001_init.sql` | 空库基线,建立完整业务结构 |
| `002_xxx.sql` ~ `009_xxx.sql` | 后续结构变更的增量 SQL(含接单平台 004~009) |
| `002_xxx.sql` ~ `080_xxx.sql` | 后续结构变更的增量 SQL(接单平台 004~009,寄卖相关 077~080) |
约定:
@@ -19,7 +19,7 @@ order_site 在 affiliate_dash 开通一个商户账号 + API 客户端,下单
| **order_site** | 91 进单、商品匹配、履约任务、统一领取页、玩家信息收集、回调接收、电子凭证核销、人工兜底 | 不直接对接皮肤源头;不管理 affiliate_dash 钱包 |
| **affiliate_dash** | 商户建单扣款、发货数据查询、绑定、对源头履约、回调推送 | 不接触 91 卡券,不生成 order_site 的 claimUrl |
> 对接方式与现有 `kuaishou-feifei` 执行器同构:order_site 新增一个 `affiliate_dash` executor,复用一个 `preparePaidTask` + `resolveDeliveryLink`,对外仍然只返回本站统一领取链接。
> 对接方式沿用既有 executor 模式(`preparePaidTask` + `resolveDeliveryLink`):order_site 新增一个 `affiliate_dash` executor,对外仍然只返回本站统一领取链接。历史上 `kuaishou-feifei` / `kuaishou-cloud` 也走该模式,现已下线,只保留只读 legacy stub(`services/fulfillment/executors/legacy-retired-executor.ts`)。
---
@@ -343,8 +343,8 @@ order_site 处理要求:
## 13. 参考代码位置
- order_site executor 模式:`apps/backend/src/services/fulfillment/executors/kuaishou-feifei-executor.ts`(最接近,同为「建单 + 统一领取链接」)、`registry.ts`(注册)、`types.ts`(接口定义)
- order_site 平台对接样例:`apps/backend/src/services/platforms/kuaishou-feifei/`
- order_site executor 模式:`apps/backend/src/services/fulfillment/executors/affiliate-dash-executor.ts`(现行唯一自动履约 executor,「建单 + 统一领取链接」)、`registry.ts`(注册)、`types.ts`(接口定义);`kuaishou-feifei-executor.ts` 已随通道下线删除,历史通道在 `registry.ts:18-19` 映射到 `legacy-retired-executor.ts`(只读 stub)
- order_site 平台对接样例:`apps/backend/src/services/platforms/affiliate-dash/`(旧 `services/platforms/kuaishou-feifei/` 已删除)
- order_site 履约配置:`apps/backend/src/repositories/fulfillment-profile-repo.ts`、`apps/backend/src/routes/admin/platform-config/fulfillment-routing.ts`
- affiliate_dash 接口契约(在线文档/字段定义):`frontend/src/openapi/endpoints.ts`;鉴权实现:`backend/internal/middleware/open_auth.go`(商户侧)、`backend/internal/service/callback.go`(回调签名 `BuildCallbackSign`)
@@ -372,9 +372,9 @@ order_site 处理要求:
### 14.2 order_site 落地要点
- **client_order_no 优先用平台订单号**:单笔任务直接使用订单号;拆单使用 `订单号-序号`,均在 affiliate-dash 的 64 字符限制内。订单和任务集合创建后保持稳定,因此 `preparePaidTask` 失败重试会重放同一个幂等单号。
- **preparePaidTask 同步拿到 order_no**(201 响应即含),写入 `context_json.affiliateDash.orderNo`;无需等回调。失败必须降级 `MANUAL_REVIEW` + `deps.notifyTaskAutoManualReview`(范式:kuaishou-feifei-executor.ts:36-51)。
- **preparePaidTask 同步拿到 order_no**(201 响应即含),写入 `context_json.affiliateDash.orderNo`;无需等回调。失败必须降级 `MANUAL_REVIEW` + `deps.notifyTaskAutoManualReview`(范式:`services/fulfillment/executors/affiliate-dash-executor.ts:18-53`)。
- **状态迁移路径**:见 §8 注 —— 领取页打开 `link_generated → claimed`,绑定 `→ waiting_binding`,submit 成功/回调 delivering `→ redeeming`,delivered `→ redeemed`(+核销),ship_failed `→ retry_pending`,cancelled `→ manual_review → closed`。
- **回调幂等**:affiliate_dash 投递带 `X-Event-ID`,order_site 现无独立去重表 —— 建议 migration `013_affiliate_dash.sql` 新建 `webhook_events(event_id UNIQUE)` 去重表;事件处理用**状态合并式更新**(仿 `syncKuaishouFeifeiTaskStatus`,重复通知可重入)。接收方注意 `app.ts:31-38` 的 `express.json` 已保存 `req.rawBody`(验签必需),挂载回调路由时确认 rawBody 可用。
- **回调幂等**:affiliate_dash 投递带 `X-Event-ID`,去重表已落地——migration `013_affiliate_dash_webhook_events.sql` 建 `webhook_events` 表并加 `(provider, event_id)` 唯一索引,写入见 `apps/backend/src/repositories/webhook-event-repo.ts:20`,过期清理见 `services/scheduler/data-retention-job.ts:42`;事件处理用**状态合并式更新**(`syncAffiliateDashTaskStatus`,重复通知可重入)。接收方注意 `app.ts:31-38` 的 `express.json` 已保存 `req.rawBody`(验签必需),挂载回调路由时确认 rawBody 可用。
- **核销联动**:`delivered` 回调 → 复用 `consumeKuaishouIndustryVouchersForTask`(`platforms/kuaishou-industry/voucher-service.ts:143`),成功 → `redeemed/completed`,失败 → `manual_review`。
- **领取页分发**:`flowType` 扩展 `'affiliate_dash'`;`buildClaimDetailPayload`(`services/claim/kuaishou-cloud-claim-context.ts:89-92`)按 executor_key 增加分支,返回 affiliate_dash 专用 payload(商品/预期账号/当前状态/二维码相关);前端 `ClaimPage.tsx:337-373` 增加渲染分支。
@@ -385,7 +385,7 @@ order_site 处理要求:
| # | 文件 | 改动 |
|---|------|------|
| 1 | `services/fulfillment/executors/types.ts` | `FULFILLMENT_EXECUTOR_KEYS` 加 `AFFILIATE_DASH`;按需加 `isAffiliateDashExecutor` 守卫 |
| 2 | `services/fulfillment/executors/affiliate-dash-executor.ts`(新) | 仿 kuaishou-feifei-executor.ts:`{ key, preparePaidTask, resolveDeliveryLink }` |
| 2 | `services/fulfillment/executors/affiliate-dash-executor.ts`(新) | 实现 `{ key, preparePaidTask, resolveDeliveryLink }`(`kuaishou-feifei-executor.ts` 已随通道下线删除) |
| 3 | `services/fulfillment/executors/registry.ts` | `EXECUTORS` Map 注册 |
| 4 | `services/fulfillment/affiliate-dash/index.ts`(新) | 业务实现:prepare / sync 状态合并 + flow normalize |
| 5 | `services/platforms/affiliate-dash/`(新) | `config.ts` + `http-client.ts`(签名 §3.1 + 请求封装 + 超时)+ `order-service.ts` + `notify-service.ts`(验签,`timingSafeEqual` 模式) |
@@ -528,7 +528,7 @@ order_site 处理要求:
## 19. 核销时机优化(v2.4.1 · 已完成)
**需求**:核销时机从「delivered 回调」提前到「给用户链接(91 卡信息到手、建单成功)」——参考 kuaishou-feifei 的「给用户链接即核销」模式。
**需求**:核销时机从「delivered 回调」提前到「给用户链接(91 卡信息到手、建单成功)」——affiliate-dash 的现行实现即按此模式(kuaishou-feifei 的「给用户链接即核销」模式已由 affiliate-dash 承接)。
**改动**(`apps/backend/src/services/fulfillment/affiliate-dash/index.ts`):
@@ -564,6 +564,8 @@ order_site 处理要求:
## 21. 履约匹配逻辑深度分析(v2.6)
> ⚠ **本节是 2026-07 当时的分析快照**:文中「cloudtentacles / feifei / affiliate_dash 三平台按商品名匹配、按全局优先级选择」已不成立——cloudtentacles 与 kuaishou-cloud / kuaishou-feifei 通道均已下线,现行可路由通道只有 `affiliate_dash` 与 `manual_dispatch`,`DEFAULT_EXECUTOR_PRIORITY = [affiliate_dash]`(`services/fulfillment/routing-config-service.ts:64-69`)。保留本节作为当时的决策记录。
### 21.1 匹配链路全貌
```
@@ -617,6 +619,8 @@ order_site 处理要求:
## 22. 履约匹配优化实施(v2.7 · 已完成)
> ⚠ **落地后的现状(2026-09)**:§22.1 第 2 条写的「默认优先级 cloud → feifei → affiliate_dash」已过时。现行可路由通道只有 `affiliate_dash` 与 `manual_dispatch`,`DEFAULT_EXECUTOR_PRIORITY = [affiliate_dash]`(`apps/backend/src/services/fulfillment/routing-config-service.ts:64-69`);cloudtentacles 与 kuaishou-cloud/feifei 通道均已下线(`executors/registry.ts:18-19` 映射到 `legacy-retired-executor.ts` 只读 stub)。
**背景确认**:91 商品编码全部为**中文名**(如「幸运币90个」「星际漫游服装礼包」),无独立编码体系;运营按「中文名 → affiliate_dash sku」配置显式映射表(33 条,见 §22.3)。
### 22.1 匹配策略(两级)
@@ -707,8 +711,8 @@ order_site 处理要求:
| `AdminPlatformFulfillmentPage.tsx` | 移除 affiliate-dash tab、`AffiliateDashFulfillmentPanel` 组件及全部 affiliate 相关 import/类型 |
**页面归属**:
- **平台配置**(`AdminPlatformShopsPage`):affiliate-dash、kuaishou-feifei、kuaishou-lewan、行业电子凭证、内部通知 —— 平台接入凭据
- **履约配置**(`AdminPlatformFulfillmentPage`):履约路由、kuaishou-feifei 履约、kuaishou-lewan 履约 —— 通道路由与履约参数
- **平台配置**(`AdminPlatformShopsPage`):内部通知、行业电子凭证、affiliate-dash、拼多多 (PDD) —— 平台接入凭据
- **履约配置**(`AdminPlatformFulfillmentPage`):履约路由(affiliate_dash / manual_dispatch 通道开关 + 优先级 + 商品规则) —— 通道路由与履约参数
**验证**:前端 `tsc -b --noEmit` 通过;后端未改动。
@@ -749,7 +753,7 @@ order_site 处理要求:
| 第 3 步(确认并提交发货) | 移除信息区(含三项),仅保留已绑定提示 + 提交按钮 |
| 第 4 步(结果页) | 移除信息区(领取商品/单号/订单状态/发货状态/金额全部移除),仅保留结果标题 + 描述 |
**实现**:`ClaimAffiliateDashSteps.tsx` —— `AffiliateDashClaimPanel` 移除 `onOpenBindUrl` prop 与按钮、info grid 精简为三项且仅在未绑定(第 2 步)显示;`AffiliateDashResultStep` 移除 info grid;`ClaimPage.tsx` 同步移除 `onOpenBindUrl` 传参(`openBindUrl` 函数保留,lewan 仍用)。
**实现**:`ClaimAffiliateDashSteps.tsx` —— `AffiliateDashClaimPanel` 移除 `onOpenBindUrl` prop 与按钮、info grid 精简为三项且仅在未绑定(第 2 步)显示;`AffiliateDashResultStep` 移除 info grid;`ClaimPage.tsx` 同步移除 `onOpenBindUrl` 传参(`openBindUrl` 相关代码已随后续通道下线一并删除,全库已无该符号)。
**附带修复**:`product-resolution-service.ts` 透传拉取失败 warn 日志补 `logIntegration` import(上一轮遗漏导致后端 typecheck 报错)。
+1 -1
View File
@@ -94,7 +94,7 @@ your_secret_keybuyNum=1&callbackUrl=https://cb.example.com/notify/91/order&maxAm
1. 校验签名与时间戳;
2. 按 `orderNo` 做幂等;
3. 将订单作为 `91卡券` 来源订单写入当前项目;
4. 若 `productNo` 已映射到快手 Cloud 履约商品,则创建履约任务;
4. 若 `productNo` 已映射到 affiliate-dash 履约商品,则创建履约任务;
5. 若 `productNo` 尚未配置,则订单进入后台“待补全”队列,由运营补齐履约配置后重试生成任务;
6. 若传入 `maxAmount`,则校验成本是否超限;
7. 履约任务生成后,生成当前项目领取链接,例如:`https://你的域名/#/claim/{token}`;
+15 -13
View File
@@ -59,7 +59,7 @@ https://你的域名/api/v1/open/91
| 91字段 | 当前项目用途 | 说明 |
| --- | --- | --- |
| `orderNo` | 外部订单号 / 幂等键 | 建议映射为内部 `platformOrderId` |
| `productNo` | 商品和店铺映射 | 默认按商品名匹配 cloudtentacles 商品;可用 `商品名----店铺编号` 携带快手小店 `shopId` |
| `productNo` | 商品和店铺映射 | 默认按商品名匹配 affiliate-dash 商品;可用 `商品名----店铺编号` 携带快手小店 `shopId` |
| `buyNum` | 购买数量 | 生成对应数量的履约任务 |
| `maxAmount` | 成本上限校验 | 可选,超限返回失败 |
| `callbackUrl` | 备用回调地址 | 先保留,不作为主流程依赖 |
@@ -67,18 +67,18 @@ https://你的域名/api/v1/open/91
| `version` | 版本 | 固定 `1.0` |
| `sign` | 验签 | 必填 |
### 5.2 当前项目内部建议映射
### 5.2 当前项目内部映射(现状)
建议新增一个独立来源:
代码中已按独立来源落地:
- `provider = '91kaquan'`
- `platform = 'kuaishou'`
- `shopId = '91kaquan'`、`shopName = '91卡券'`(默认取 `platforms.ninetyone.shopId / shopName`)
商品绑定建议:
商品绑定现状:
- `productNo` 对应当前项目内部 `skuCode`
- 再由现有快手履约配置,匹配到 `kuaishou_ct_assisted` 履约链路
- 若 `productNo` 暂未配置,订单会先进入后台“91卡券待补全订单”队列,不直接丢弃。
- `productNo`(`商品名----店铺编号` 时取前半段作商品名)经 affiliate-dash 的 SKU 映射表(中文商品名 -> 英文 sku)匹配,未命中时再做 sku 透传兜底,命中后走 affiliate-dash 履约链路;
- 若 `productNo` 暂未命中映射,订单先以 `order_status = 'pending_config'` 落库为待补全订单,不直接丢弃;可通过后台管理接口 `/api/v1/admin/platform-config/ninetyone/orders` 查看与重试。
## 6. 异步下单接口配置
@@ -289,18 +289,20 @@ Content-Type: application/json;charset=utf-8
建议在你方给 91 的商品配置中约定:
- `productNo`:对应当前项目内部的快手履约 SKU
- `productNo`:对应当前项目内部的快手履约商品名(会经 affiliate-dash SKU 映射表转成英文 sku)
- 商品名称:明确标注“自动发链接”
- 发货类型:异步卡密
- 买家收到内容:领取链接
后台补全路径:
后台补全路径(当前实现):
1. 进入“平台配置 -> 91卡券接入”查看待补全订单;
2. 根据订单中的 `productNo` 到“履约配置中心 -> 快手 Cloud 新履约”新增或补齐规则;
3. 规则保存后回到“91卡券接入”,点击“重试生成任务”;
1. 未命中 affiliate-dash SKU 映射的订单会以 `order_status = 'pending_config'` 落库;
2. 查询待补全订单:`GET /api/v1/admin/platform-config/ninetyone/orders`;
3. 补齐该 `productNo` 的 affiliate-dash SKU 映射后,调用 `POST /api/v1/admin/platform-config/ninetyone/orders/:id/retry` 重试生成任务(也可调用 `.../:id/fail` 标记无法履约);
4. 任务生成后,查询接口会在领取链接准备好时返回 `orderStatus = 20`。
说明:前端「平台配置」页当前只有 `notifications / kuaishouIndustry / affiliateDash / pdd` 四个 tab,尚无 91 卡券专属页面;上述接口需直接调用。
## 12. 对接方需确认的固定项
发给 `91卡券` 客服/对接方时,建议一次性确认以下配置:
@@ -322,4 +324,4 @@ Content-Type: application/json;charset=utf-8
- `claimUrl` 作为最终卡密内容,通过 `cards.cardNo` 返回;
- 所有快手专属引导和后续交互,都留在当前项目领取页中完成。
这样对当前项目改动最小,也最符合现在快手 Cloud 履约链路的设计。
这样对当前项目改动最小,也最符合现在 affiliate-dash 履约链路的设计。
+32 -23
View File
@@ -11,7 +11,7 @@
1. `91卡券` 调用我方异步下单接口;
2. 我方将订单作为独立接入平台订单写入当前项目;
3. 已配置商品自动走现有快手 Cloud 履约链路;
3. 已配置商品自动走现有 affiliate-dash 履约链路;
4. 未配置商品进入后台待补全队列,可手动补齐履约信息后重试;
5. 我方生成领取链接 `https://你的域名/#/claim/{token}`;
6. `91卡券` 调用查询订单接口;
@@ -93,7 +93,7 @@ platforms: {
shopId: '91kaquan',
shopName: '91卡券',
timestampToleranceSeconds: 600,
cardsEncoding: 'base64json'
cardsEncoding: 'aes-256-ecb-base64'
}
}
```
@@ -155,10 +155,10 @@ platforms: {
这样可直接复用现有:
- 91 商品名到 cloudtentacles 商品名的匹配;
- cloudtentacles 覆盖规则(套装、多数量发货);
- 快手 Cloud task 创建;
- claim token 生成。
- 91 商品名到 affiliate-dash sku 的匹配(`resolveAffiliateDashSkuByProductNo`,含 sku 透传兜底);
- affiliate-dash 履约 task 创建;
- 按数量拆 task(`delivery-task-service.ts` 按 `unit_index` 为每个数量各建一个 task);
- claim token / 领取链接生成。
### 7.4 成功判定
@@ -185,12 +185,13 @@ platforms: {
### 8.2 claimUrl 判定
对订单下的每个 task:
对订单下的每个 task,调用 `resolveTaskDeliveryLink(task)`(`apps/backend/src/services/fulfillment/delivery-link-service.ts`),由该 task 对应的履约执行器解析可交付链接:
1. 优先读取 `primary_claim_token`;
2. 若没有,则读取 `claim_token`;
3. 若仍没有,且是 `kuaishou_ct_assisted`,调用现有 `ensureTaskClaimLink(task)` 补生成;
4. 用 `buildClaimUrl(token)` 组装最终链接。
1. 读取 `primary_claim_token`,为空则回退 `claim_token`;
2. 同时读取 `primary_claim_expires_at` / `claim_expires_at` 作为过期时间;
3. 有 token 才返回 `claimUrl = buildClaimUrl(token)`,否则返回 `null`(视为暂不可交付)。
说明:`kuaishou_ct_assisted`(乐玩)/ kuaishou-feifei 通道已下线,其历史 task 由只读 stub 处理,不再生成领取链接;领取链接现由 affiliate-dash 执行器的 `resolveDeliveryLink` 提供。
### 8.3 查询状态判定
@@ -249,11 +250,12 @@ Node 侧实现等价为:
当前项目的 `91` 下单流程直接使用 `productNo` 做履约匹配:
- 默认把 `productNo` 当作商品名,匹配 cloudtentacles 商品列表里的同名商品;
- 如果 `productNo` 使用 `商品名----店铺编号` 格式,前半段用于商品匹配,后半段作为快手小店 `shopId` 用于核销;
- 特殊商品通过 cloudtentacles 覆盖规则处理,例如一个 91 商品发多个 cloudtentacles 商品,或某个商品发多次。
- 默认把 `productNo` 当作商品名(中文),经 affiliate-dash 的 SKU 映射表(`apps/backend/src/services/platforms/affiliate-dash/sku-mapping.ts`)转成 affiliate-dash 商品的英文 sku;
- 若映射表未命中,再用 `productNo` 与 affiliate-dash 商品列表的 sku 做透传兜底匹配(`matchAffiliateDashSkuPassthrough`);
- 如果 `productNo` 使用 `商品名----店铺编号` 格式,前半段作为商品名匹配、后半段作为快手小店 `shopId`(`parseOpen91ProductNo`);
- 匹配成功后由履约路由选中 affiliate-dash 执行器,多数量在 `delivery-task-service.ts` 里按数量拆成多个 task。
最终目标是让 91 卡券只传商品名,系统按商品名和覆盖规则自动创建快手 Cloud 履约任务。
最终目标是让 91 卡券只传商品名,系统按 SKU 映射与透传兜底自动创建 affiliate-dash 履约任务。
## 11. 日志与排障
@@ -273,17 +275,24 @@ Node 侧实现等价为:
建议新增:
- `apps/backend/src/routes/open-91.js`
- `apps/backend/src/services/open-91/shared.js`
- `apps/backend/src/services/open-91/order-create-service.js`
- `apps/backend/src/services/open-91/order-query-service.js`
- `apps/backend/src/routes/open-91.ts`(挂载于 `apps/backend/src/app.ts:103`,前缀 `/api/v1/open/91`)
- `apps/backend/src/services/open-91/config.ts`
- `apps/backend/src/services/open-91/payload.ts`
- `apps/backend/src/services/open-91/crypto.ts`
- `apps/backend/src/services/open-91/response.ts`
- `apps/backend/src/services/open-91/order-create-service.ts`
- `apps/backend/src/services/open-91/order-query-service.ts`
- `apps/backend/src/services/open-91/consume-on-deliver.ts`
需要修改:
- `apps/backend/src/config/defaults.ts`
- `apps/backend/src/config/runtime.js`
- `apps/backend/src/types/runtime-config.js`
- `apps/backend/src/index.js`
- `apps/backend/src/config/runtime.ts`
- `apps/backend/src/config/env-overrides.ts`
- `apps/backend/src/types/runtime-config.ts`
- `apps/backend/src/services/platforms/ninetyone/order-service.ts`
- `apps/backend/src/services/fulfillment/product-resolution-service.ts`
- `apps/backend/src/index.ts`
## 13. 测试范围
@@ -309,4 +318,4 @@ Node 侧实现等价为:
- 不实现 `callbackUrl` 主动回调;
- 不在 `91` 查询成功后回写快手侧最终兑换结果;
- 不实现除 `aes-256-ecb-base64` 之外的卡密加密算法;
- 不新增独立顶级菜单,后台入口复用“平台配置 -> 91卡券接入”。
- 不新增独立顶级菜单;91 待补全订单目前只有后端管理接口(`/api/v1/admin/platform-config/ninetyone/orders` 查询、`.../:id/retry` 重试、`.../:id/fail` 标记失败),前端暂无专属页面。
-63
View File
@@ -1,63 +0,0 @@
最快解决(强制使用 HTTP/2 协议)防止clash干扰
cloudflared tunnel --protocol http2 run --url http://localhost:80 mac-mini-tunnel
cloudflared tunnel create mac-mini-tunnel
yml@ymlMacBook-Air ~ % cloudflared tunnel create mac-mini-tunnel
Tunnel credentials written to /Users/yml/.cloudflared/562f9639-71f2-44e0-a5ef-b34bd4e215e5.json. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel.
Created tunnel mac-mini-tunnel with id 562f9639-71f2-44e0-a5ef-b34bd4e215e5
yml@ymlMacBook-Air ~ %
221329.cc.cd
4. # 例子:把 web.yourdomain.com 指向刚才创建的隧道
cloudflared tunnel route dns mac-mini-tunnel 221329.cc.cd
5. 启动隧道(临时/测试)
如果你只是临时跑一个服务(比如你 OrbStack 里的容器映射到了本地 80 端口):
cloudflared tunnel run --url http://localhost:80 mac-mini-tunnel
-----
如果两台电脑**不会同时运行**(例如一台是公司的 iMac,一台是家里的 MacBook),那么处理起来就简单得多了。
在这种情况下,你可以直接采用 **“共用同一个 Tunnel ID”** 的方案。这在 Cloudflare 的术语中叫做多副本(Replicas),虽然你不是为了负载均衡,但它能完美解决你的需求。
### 实现方案:共用 Tunnel
你只需要确保两台电脑拥有完全一致的“身份令牌”和“配置文件”。
1. **迁移证书与密钥:**
* 在第一台电脑(已配置好)上,找到这两个文件:
* `~/.cloudflared/cert.pem`(登录证书)
* `~/.cloudflared/<Tunnel-ID>.json`(隧道的身份密钥)
* 将它们拷贝到第二台电脑的 `~/.cloudflared/` 目录下。
2. **同步配置文件:**
* 将 `config.yml` 也拷贝过去。
* **注意:** 确保两台电脑上运行的本地服务端口是一致的(比如都是 `localhost:8080`)。
3. **直接运行:**
* 在任意一台电脑上运行 `cloudflared tunnel run <隧道名>` 即可。
---
### 这样做的优势
* **域名无缝切换:** 你不需要在 Cloudflare 后台修改任何 DNS 记录。只要其中一台电脑开启了隧道,`example.yourdomain.com` 就会立即指向那台电脑。
* **无需重复配置:** 所有的路由规则(Ingress rules)都是一致的,维护起来非常省心。
### 注意事项
* **如果真的“不小心”同时运行了:**
Cloudflare 会把它们当成一个隧道的两个节点。流量会以类似“轮询”的方式随机分配到两台电脑上。如果这时你的两台电脑跑的服务内容不一样,访问者可能会看到一会儿是 A 电脑的内容,一会儿是 B 电脑的内容。
* **路径一致性:**
在 `config.yml` 中,`credentials-file` 的路径如果写的是绝对路径(如 `/Users/jack/...`),请确保两台电脑的用户名一致;如果不一致,记得修改配置文件里的路径。
---
**总结:**
既然不同时运行,那就把第二台电脑伪装成第一台电脑的“克隆体”。只需拷贝那几个关键文件,你就能实现在家、在公司用同一个域名访问不同的机器了。
-75
View File
@@ -1,75 +0,0 @@
# 财务功能实施计划
## 目标
在后台左侧主导航增加“财务”,提供按中国时区日期筛选的全站财务总览、每日账目和资金明细。首版复用现有订单、打手资金申请、钱包流水和售后数据,不改变接单、充值、提现的现有业务流程。
## 已实现范围
- `/admin/finance` 页面和主导航入口。
- `/api/v1/admin/finance/summary`:返回日期范围内的日报和汇总指标。
- `/api/v1/admin/finance/transactions`:返回分页资金明细,并支持账目类型、渠道筛选。
- 日期范围支持 `YYYY-MM-DD HH:mm`,精确到分钟;日报仍按 `Asia/Shanghai` 自然日切桶。
- 日界线固定为 `Asia/Shanghai`,前端金额展示为元,数据库金额仍使用分。
- 总览指标:订单收款、订单退款、打手报酬、已确认提现、充值入账、售后追缴退还、净现金流、当前钱包余额/冻结/待解冻/待提现。
- 渠道日报按“日期 + 渠道 + 账目类型”汇总笔数和金额,订单渠道显示为 `provider/platform`,打手资金显示为提现/充值渠道。
- 原“接单平台 → 资金”页签继续用于打手资金申请审核和资金配置。
- 新增独立 `finance` 后台角色:仅显示财务和资金审核入口,可访问财务报表、资金申请列表/审核,不可访问订单、任务和系统概览。
- 管理员拥有全部财务权限,普通运营保留查看/审核权限,客服不获得财务权限。
## 财务口径
1. 订单收款按 `paid_at`(缺失时回退 `created_at`)归属日期。
2. 退款首版按订单 `pay_status=refunded` 的订单金额统计;部分退款明细需要后续接入独立退款流水。
3. 打手报酬取 `reward_settlement`、`sharing_reward` 钱包流水。
4. 已确认提现取资金申请 `status=approved` 且有 `reviewed_at` 的金额,避免和钱包 `withdraw_paid` 零金额流水重复计算。
5. 充值入账取已通过的充值申请;押金冻结/解冻只作为资金占用变化,不计入收入。
6. 净现金流 = 收款 + 充值 + 打手资金回收 - 订单退款 - 打手提现 - 售后追缴退还。
## 后续迭代
### 二期:人工打款可追踪
- 将提现审核和实际打款拆分。
- 增加打款时间、操作人、流水号、付款凭证和备注。
- 增加“审核通过待打款”“已打款”状态,历史 `approved` 数据保持兼容。
### 三期:统一财务总账
- 新增 append-only `finance_transactions` 表。
- 对订单收款、退款、充值、提现、结算和售后资金动作双写统一流水。
- 增加对账、CSV/Excel 导出、幂等键和异常金额检查。
## RBAC 设计
权限键:
- `finance.view`:财务总览、日报和明细
- `finance.review`:充值/提现申请审核
- `finance.config`:财务规则配置
- `finance.export`:导出财务数据
角色矩阵:
| 角色 | 财务报表 | 资金审核 | 财务配置 | 订单/任务 |
|---|---|---|---|---|
| 管理员 | 是 | 是 | 是 | 是 |
| 普通运营 | 是 | 是 | 否 | 是 |
| 财务 | 是 | 是 | 否 | 否 |
| 客服 | 否 | 否 | 否 | 是(客服范围) |
## 验收标准
- 日期范围包含起止两天,日报为空时仍返回完整日期行。
- 日报各列合计与顶部总览一致。
- 提现不被申请表和钱包流水重复计入。
- 每笔明细包含来源类型、来源编号、发生时间和关联单号/渠道。
- 选择具体时间段时只统计起止时间内的事件;渠道日报能分别展示同一渠道不同日期的金额。
- 刷新、分页和账目类型筛选保持当前日期范围。
- 管理员、普通运营可访问;客服访问接口返回角色错误且不显示主导航。
## 主要文件
- 前端:`apps/frontend/src/pages/admin/AdminFinancePage.tsx`、`apps/frontend/src/services/admin/finance.ts`、`apps/frontend/src/types/admin/finance.ts`、`apps/frontend/src/layouts/AdminLayout.tsx`、`apps/frontend/src/App.tsx`
- 后端:`apps/backend/src/services/admin/admin-finance-service.ts`、`apps/backend/src/routes/admin/finance.ts`、`apps/backend/src/routes/admin.ts`
- 测试:`apps/backend/src/services/admin/admin-finance-service.test.ts`
-192
View File
@@ -1,192 +0,0 @@
# pgBackRest 生产备份方案分析
> 状态:暂缓实施。当前继续使用 `pg_dump -Fc` + OSS manifest 备份方案,待安排维护窗口后再启用 WAL 归档。
>
> 记录日期:2026-08-23
## 结论
PostgreSQL 没有 MySQL binlog 的同名机制,但有等价能力:
```text
基础物理备份 + WAL 归档 = 按时间点恢复(PITR)
```
当前 Compose 是单节点 PostgreSQL。启用 `archive_mode` 和 `wal_level` 通常需要重启 PostgreSQL,因此不能承诺绝对零停机。
预计生产维护窗口:
| 阶段 | 是否停机 | 预计耗时 |
| --- | --- | --- |
| 构建 pgBackRest 镜像 | 否 | 几分钟 |
| 创建 stanza、校验 OSS | 否 | 1~3 分钟 |
| 重启 PostgreSQL 启用 WAL 归档 | 是 | 5~30 秒 |
| backend/web 等待恢复 | 是 | 20~90 秒 |
| 首次全量备份 | 否 | 几分钟到几十分钟 |
| 建议预留维护窗口 | 是 | 2~5 分钟 |
实际时间取决于数据库大小、磁盘恢复速度、容器启动时间和 OSS 网络质量。当前 `pg_dump` 文件约 161 MB,首次物理备份预计不会很大,但仍应按实际 PostgreSQL 数据目录大小估算。
## 当前方案
目前生产使用:
- `pg_dump -Fc` 全库逻辑备份。
- 备份文件上传 OSS。
- manifest 保存大小、SHA-256、迁移记录和创建时间。
- `apps/backend/data` 配置单独打包上传。
- `restore-db.sh --verify` 在临时 PostgreSQL 容器中演练恢复。
该方案可以恢复到某个 dump 的时间点,但不能恢复最近一次 dump 之后的几分钟变更。
pgBackRest 不能替代当前逻辑备份,建议两套并存:
```text
pgBackRest:生产灾难恢复、PITR、分钟级 RPO
pg_dump:逻辑迁移、单表恢复、跨版本恢复、第二套保险
```
## 需要增加的组件
当前 PostgreSQL 镜像没有 pgBackRest,需要构建自定义镜像:
```dockerfile
FROM docker.m.daocloud.io/library/postgres:16-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends pgbackrest ca-certificates \
&& rm -rf /var/lib/apt/lists/*
```
需要增加或挂载:
- `/etc/pgbackrest/pgbackrest.conf`
- `/var/spool/pgbackrest/`
- pgBackRest 日志目录
- OSS 备份凭证
- pgBackRest repository 配置
不能只在正在运行的容器里执行 `apt install pgbackrest`,因为容器重建后会丢失。
## OSS 规划
可以复用当前图片 OSS 桶,但必须使用独立前缀:
```text
backups/postgres/ # 当前 pg_dump
backups/configs/ # 应用配置
backups/pgbackrest/ # 基础备份和 WAL
```
生产更推荐新建独立备份桶,并创建独立 RAM 用户。原因:数据库备份包含订单、用户和后台数据,不能与图片读写权限混用。
建议 OSS 配置:
- 使用 ECS 同地域内网端点,避免公网流量费用。
- 开启版本控制。
- 配置生命周期规则。
- 备份桶保持私有。
- 必要时启用 pgBackRest 仓库加密,并安全保存加密密码。
pgBackRest 使用 OSS S3 兼容接口,最终配置必须通过 `pgbackrest check` 和一次完整备份实测,不能只凭配置文件判断兼容性。
## PostgreSQL 配置
需要启用:
```text
wal_level=replica
archive_mode=on
archive_command=pgbackrest --stanza=order-site archive-push %p
archive_timeout=60
```
说明:
- `wal_level=replica` 通常需要重启。
- `archive_mode=on` 必须重启。
- `archive_command` 可以 reload,但建议统一写入 Compose 配置。
- `archive_timeout=60` 让低写入量时 WAL 最多约 60 秒切换归档一次。
- OSS 不可用时 WAL 会在本地积压,不能手动删除 `pg_wal` 文件。
启用前先检查当前状态:
```bash
docker compose exec postgres psql \
-U postgres -d order_site \
-c "SHOW wal_level; SHOW archive_mode; SHOW archive_command;"
```
如果 `archive_mode` 已经是 `on`,后续可能只需要配置 pgBackRest 和 reload;如果是 `off`,需要完整重启 PostgreSQL。
## 计划中的切换步骤
当前暂不执行。正式实施时按以下顺序:
1. 继续保留并执行现有 `bash deploy/backup-db.sh`。
2. 创建独立 OSS 备份桶或规划独立 `backups/pgbackrest/` 前缀。
3. 创建只允许备份前缀的 RAM 用户。
4. 构建包含 pgBackRest 的 PostgreSQL 镜像。
5. 准备 `pgbackrest.conf`,不要把密钥提交到 Git。
6. 在 PostgreSQL 仍在线时执行 `stanza-create`。
7. 选择低峰期停止 backend/web。
8. 重启 PostgreSQL,启用 `archive_mode` 和 `archive_command`。
9. 等待 PostgreSQL 健康检查通过。
10. 执行 `pgbackrest check`。
11. 执行第一次 full backup。
12. 检查 OSS 中是否出现基础备份和 WAL 文件。
13. 在临时 PostgreSQL 或测试服务器执行一次 PITR 恢复。
14. 恢复 backend/web,并抽查后台、订单和图片访问。
不要执行以下命令,避免误删当前数据库:
```bash
docker compose down -v
docker volume rm order_site_postgres_data
```
增加 pgBackRest 只需要替换镜像、挂载配置并重启,不需要删除 `postgres_data` 数据卷。
## 备份策略建议
```text
每周:full 基础备份
每天:diff 差异备份
持续:WAL 归档到 OSS
每天:保留当前 pg_dump 逻辑备份
```
WAL 归档不依赖 cron,由 PostgreSQL 的 `archive_command` 持续触发。需要监控:
- WAL 归档失败次数。
- `/var/lib/postgresql/data/pg_wal` 占用空间。
- OSS 上传延迟和失败率。
- 最近一次 full/diff 备份时间。
- 最近一段 WAL 是否已经出现在 OSS。
## 恢复时间预期
pgBackRest PITR 恢复也不是零停机操作。当前单节点架构下,恢复生产数据库需要停止 backend/web,并替换或恢复 PostgreSQL 数据目录。
恢复耗时主要取决于:
- 基础备份大小。
- WAL 数量。
- 磁盘读写速度。
- WAL 回放量。
- PostgreSQL 启动和健康检查时间。
如果基础备份较小、目标时间接近当前时间,预计几分钟;数据库规模扩大后需要重新压测,不能按当前 161 MB dump 固定估计。
## 后续验收清单
- `pgbackrest stanza-create` 成功。
- `pgbackrest check` 成功。
- 第一次 full backup 成功。
- OSS 中能看到基础备份和 WAL。
- PostgreSQL 重启后 `archive_mode=on`。
- `pg_stat_archiver` 中归档成功数持续增加。
- OSS 暂时不可用时,WAL 会积压但不会静默丢失。
- 临时环境 PITR 到指定时间成功。
- 恢复后的订单、后台用户、迁移记录和图片引用可用。
@@ -1,273 +0,0 @@
# 和平精英寄售仓库 · 系统设计与实施方案
> **文档定位**:功能技术与产品设计方案(推荐实现标准)
> **核心游戏**:和平精英(地铁逃生模式 / 账号与物资寄售)
> **更新时间**:2026-09-13
---
## 一、 业务定位与设计原则
本模块针对 **《和平精英》**(重点针对地铁逃生模式的苏尔南手枪、极品附魔武器与排位账号)建设专属的 **「寄售仓库」**,取代原有的线下手工 Excel 台账,并深度融入订单与接单履约链路:
### 核心设计原则:
1. **去繁就简、数据结构化**:吸取线下台账对“收购成本、收购人、卖家账号/付款凭证、排号/出库店铺”的核心关注点,将杂乱的手工记录转化为规范清晰的系统实体。
2. **高度自动化,告别手工录入**:
* 履约出库时,系统**自动提取并反向回填**订单号、快手/拼多多店铺名、出库时间,无需人工再去登记“安排到哪张单”;
* 客服创建/审核时,系统**自动绑定当前登录人员**为收购人,自动记录操作时间。
3. **闭环降本**:在工单发布到打手大厅前,系统通过规则引擎智能比对内部在库物品,支持一键直接履约结单,**直接省去外部打手赏金(¥15~¥80/单)**。
4. **风控自主可控**:每一件入库物品提供独立的 **【允许匹配开关】**,随手一关即可排除在自动匹配推荐之外,便于自用、高价单卖或线下保留。
5. **逆向容错(支持撤单与回库重选)**:全链路支持买家退款、客服选错或账号异常时的**一键撤单解绑**,库存安全退回复库,工单可随时重新匹配或转推大厅。
---
## 二、 核心数据模型设计
将原本 Excel 的离散字段归纳为清晰的三层架构:**基础规格属性**、**收货与卖家资料**、**流转履约与匹配控制**。
### 数据表:`consignment_inventories`(寄售仓库主表)
| 分类 | 字段名 | 类型 | 默认值 | 业务含义与说明 |
| :--- | :--- | :--- | :--- | :--- |
| **主键标识** | `id` | `BIGSERIAL` | 自增 | 内部唯一自增 ID |
| | `inventory_no` | `TEXT` | 唯一 | 系统自动生成的业务编号(如 `INV26091312345`) |
| **规格属性** | `game_name` | `TEXT` | `和平精英` | 游戏名称 |
| (多维匹配) | `category_id` | `BIGINT` | `NULL` | 关联的大类(如 苏尔南手枪) |
| | `item_name` | `TEXT` | 必填 | 规范物品标题(如 `安卓微信区 暗潮 1钻`) |
| | `system` | `TEXT` | `""` | 操作系统:`苹果` / `安卓`(硬约束) |
| | `server_zone` | `TEXT` | `""` | 游戏大区:`微信区` / `QQ区`(硬约束) |
| | `series` | `TEXT` | `""` | 附魔系列:`暗潮` / `怒嚎` / `血宴` 等(硬约束) |
| | `diamond_level` | `INTEGER` | `1` | 钻级:`1` ~ `6` 钻 |
| | `brilliance_count` | `INTEGER` | `0` | 璀璨数:`0`(无)、`1`、`2` 等 |
| **收货采购信息** | `cost_price_fen` | `INTEGER` | `0` | **收货成本价**(单位分,如 10500 代表 105 元) |
| (财务与人效) | `buyer_name` | `TEXT` | `""` | **收购人**(如 王斌、周杰雄、周顺,默认取当前登录客服) |
| | `inbound_batch` | `TEXT` | `""` | **收货批次/班次代码**(如 `8.30-1`、`8.30-4`,便于交接对账) |
| | `inbound_at` | `TIMESTAMPTZ`| `NOW()` | 正式入库时间 |
| **卖家与凭据** | `seller_contact` | `TEXT` | `""` | 卖家微信号 / 联系电话 |
| (留痕防纠纷) | `seller_game_id` | `TEXT` | `""` | 卖家游戏角色名 / UID |
| | `seller_payment_info`| `TEXT` | `""` | 卖家收款抬头与卡号(如 `王大锤商行 331682...`) |
| | `credentials_json` | `JSONB` | `{}` | 敏感交付信息(游戏账号、密码、换绑辅助),权限保护 |
| | `screenshots_json` | `JSONB` | `[]` | 截图凭证数组(卖家ID主页图、词条属性图、名片图等) |
| | `remark` | `TEXT` | `""` | 运营其他备注说明 |
| **履约与状态** | `allow_matching` | `BOOLEAN` | `TRUE` | **★ 允许匹配开关**(开:参与匹配;关:暂停匹配) |
| (出库流转) | `status` | `TEXT` | `in_stock` | 资产状态:`in_stock`(在库待匹配) / `consumed`(已出库) / `archived`(已作废) |
| | `bound_order_id` | `BIGINT` | `NULL` | 履约绑定的内部系统订单 ID(出库时系统自动写入) |
| | `bound_work_order_id`| `BIGINT`| `NULL` | 履约绑定的接单工单 ID(出库时系统自动写入) |
| | `bound_platform_order_id`| `TEXT`| `""` | 外部订单号(如快手/拼多多单号 `260830-085784...`,出库自动回填) |
| | `shop_name` | `TEXT` | `""` | 售出店铺名称(如 `拼多多向日葵`、`稚嫩游戏`,出库自动回填) |
| | `consumed_at` | `TIMESTAMPTZ`| `NULL` | 出库履约结单时间 |
---
## 三、 三大核心业务场景与操作流
### 1. ~~定向收集链接生成与用户填报(自动化收货渠道)~~ —— **已下线**
> 2026-09 起:寄卖提报统一走**接单平台打手端提报**(`/api/v1/worker/consignment/publish`),
> 对外收集链接(H5 填报页、`consignment_collect_links`、`consignment_submissions.link_id`)
> 已整条移除,详见迁移 `079_drop_consignment_collect_links.sql`。本节保留仅作历史记录。
```
[客服后台] ──► 点击【新建收集链接】
├── 勾选目标:分类=苏尔南手枪 / 系统=安卓 / 区服=微信
├── 勾选品质:系列=暗潮 / 钻级=1钻 / 璀璨=0
├── 输入协商收货价:如 105 元,输入批次号:如 8.30-1
└── 点击【生成链接】 ──► 复制专属 H5 链接发给卖家
│
[卖家手机] ◄────────────────┘
- 打开 H5:顶部显示《和平精英 安卓微信区 暗潮 1钻 回收单》(规格锁定只读)
- 填入:游戏账号、密码、角色ID
- 上传:角色主页图、暗潮与1钻属性截图
- 填入:收款卡号/抬头(如 王大锤商行...)、卖家联系电话
- 点击提交
│
[后台审核工作台] ◄──────────┘ (状态:pending_review)
- 客服打开「寄卖审核」弹窗,核查截图无误
- 可修正实收价格与字段
- 点击【审核通过并入库】 ──► 正式存入寄售仓库(in_stock,允许匹配=开)
```
---
### 2. 线下人工快速录入(直接收货登记)
对于客服在线下直接谈好的现货:
* 在「寄售仓库」面板点击 **【快速手工录入】**。
* 录入表单模块化聚合:
* **商品规格**:点选 系统、区服、系列(暗潮/怒嚎/血宴)、钻级(1~6)、璀璨数。
* **采购成本与批次**:输入收购价、当前收购人(默认填入当前操作员)、批次代码(如 8.30-1)。
* **卖家与凭据**:输入卖家联系方式、收款方式、上传品质截图、填入账号密码。
* 保存后直接进入在库列表,即刻就绪,参与工单匹配。
---
### 3. 订单发布前拦截与一键内部履约(降本核心)
```
[买家下单] ──► 拼多多向日葵 店铺进单,工单状态为 unassigned (待发布大厅)
│
▼ 匹配引擎实时计算 (仅扫描 allow_matching = true 的在库库存)
[工单列表行] ──► 醒目标记 Tag:🟢 匹配内部库存(2件)
│
▼ 客服点击「发布并核销」
[发布拦截弹窗] ──► 拦截并弹出《检测到可用寄售库存》推荐卡片:
├── 卡片 A:【全部匹配】安卓微信 暗潮 1钻 0璀璨(成本 ¥105,收购人:周杰雄)
└── 卡片 B:【部分匹配】安卓微信 暗潮 1钻 1璀璨(成本 ¥128,多出【1璀璨】,高配充低配)
│
┌───────────┴───────────┐
▼ ▼
【选用内部库存直接履约】 【忽略并继续发布到大厅】
1. 选中卡片 A 点击确认 维持原有逻辑,推送到大厅由打手抢单
2. 系统原子事务操作:
- 工单直接 completed(不发大厅,0 赏金支出!)
- 自动顺带核销快手电子凭证
- 库存状态变为 consumed
- 自动回填:
bound_platform_order_id = 买家平台订单号
shop_name = 拼多多向日葵
consumed_at = 当前时间
```
---
## 四、 智能匹配引擎逻辑(全部匹配 vs 部分匹配)
### 1. 过滤基准条件(必须满足,否则不匹配)
* 资产状态必须为 `in_stock`(在库可用);
* **【允许匹配开关】必须为 `TRUE`**(若为 `FALSE` 则完全跳过,绝不打扰);
* 系统与区服必须完全一致(苹果不能配安卓,微信不能配QQ);
* 词条系列必须完全一致(暗潮 只能配 暗潮,不能拿 怒嚎 充数)。
### 2. 钻级与璀璨的匹配算法
* **全部匹配(Exact Match,得分 100 分)**:
* 钻级完全相同(如 订单=1钻,库存=1钻);
* 璀璨完全相同(如 订单=0璀璨,库存=0璀璨)。
* 推荐标为绿色,无降级风险。
* **部分匹配 / 超集匹配(Partial Match,得分 80 分)**:
* 核心特征全部满足,但库存规格更高(如 订单=1钻无璀璨,库存=1钻1璀璨,或钻级更高)。
* 推荐标为黄色,并提示 **“⚠️ 该库存品质更高(多出 1 璀璨),属于高配充低配,请确认是否愿意降级使用”**,由客服做最终决断。
### 3. 排序策略
1. 优先全部匹配(100分) $\gt$ 部分匹配(80分)。
2. 同等匹配度下,收购成本最低优先(毛利最大化)。
3. 同等成本下,入库时间最早优先(FIFO 快速清库存)。
---
## 五、 异常撤单与库存回退重选机制(退款 / 选错 / 无法发货)
在实际运营中,若发生买家退款、客服选错或账号异常,系统提供完整的**逆向撤销与回库重选闭环**:
```
【 已寄售履约工单 (completed) 】
│
点击【撤销寄售履约】
│
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
【场景 1:选错库存重新选】 【场景 2:买家退款取消工单】 【场景 3:物资账号异常作废】
- 原库存解绑 ──► in_stock - 原库存解绑 ──► in_stock - 原库存变更为 ──► archived
- 工单回退 ────► unassigned - 工单状态 ────► cancelled - 工单回退 ────► unassigned
- 客服可重新选用新库存 - 终止发货,库存回库继续卖 - 客服选用其他正常库存发货
或直接点击发布到大厅
```
### 1. 工单视角:【撤销寄售履约 / 重新选择】
在工单列表或工单详情页,对「已通过内部库存履约、且**尚未验收**」的工单(状态为 `in_progress` / `pending_acceptance` / `problem`)提供 **【撤销履约】** 操作:
* **前置约束(2027-02-21 加固)**:已验收(`accepted`)与已取消(`cancelled`)的工单**一律禁止撤销** —— 钱已结算、货已交付,撤销会造成同一件现货二次出货并重新冻结已退还的押金;买家退款/纠纷改走售后流程。服务端在事务内 `FOR UPDATE` 重新读单并校验状态白名单,撤销原因必填,前端按钮同样按状态 + `consignment.fulfill` 权限渲染。
* **分支 A:【回退为未分配(重新选库存 或 发大厅)】**
* **原库存处理**:状态从 `consumed` 自动恢复为 `in_stock`,清除绑定的订单号与售出店铺,重新进入待匹配池(**仅当该行仍绑定本工单时**才允许改写,否则 409)。
* **工单处理**:状态安全回退为 `unassigned`(未分配),清空指派人、时限、验收草稿与本次发货记录。
* **操作结果**:客服可以立即再次点击「发布并核销」选择其他匹配库存,或者直接发布到大厅由打手抢单!
* **分支 B:【买家申请退款(取消工单并退库)】**
* **原库存处理**:安全解绑并自动回退为在库 `in_stock`,避免资产报废损失。
* **工单处理**:状态流转为 `cancelled`(已取消),不再继续履约。
* **分支 C:【物资账号异常(作废该库存并重新选)】**
* **原库存处理**:该把手枪/账号密码失效或被封,直接变更为 `archived`(已作废),记录作废原因,不再参与匹配,并释放发布押金。
* **工单处理**:工单回退为 `unassigned`,供客服挑选另一把正常手枪继续履约。
### 2. 仓库视角:【退回复库 / 重新上架】
在「寄售仓库」管理列表中,对状态为 `consumed`(已出库)的物品行,提供 **【退回复库】** 按钮:
* 客服可手动将某件出库的物品解绑原订单,直接重置为 `in_stock` 在库待匹配状态;适合线下沟通中买家临时更换需求、跑单或线下对账异常时的人工修正。
* **前置约束(2027-02-21 加固)**:若该物品仍被一张「进行中的工单」占用(`bound_work_order_id` 指向的工单状态不是 `accepted`/`cancelled`),【退回复库】与【下架作废】都会被拒绝,并提示先撤销履约或撤单 —— 避免把别的工单正在交付的现货抢走。
* **匹配开关语义**:回库**不再**强制打开 `allow_matching`,而是保留运营的手动设置(该开关是"暂停匹配"的运营闸门,自动回库不应替运营做决定)。编辑接口也不再接受直接改 `status`,状态只能经由带守卫的动作变更。
### 2.1 工单终止时的自动解绑(2027-02-21 新增)
工单进入「不再交付」的状态时,会在**同一事务内**把占用的现货放回在库池(`releaseConsignmentInventoryBindingForWorkOrderWithClient`),不再依赖运营手工退库:
* 覆盖路径:打手撤单(`cancelled_by_worker`)、后台取消指派(`unassigned_by_admin`)、后台退回大厅(`returned_to_hall_by_admin`)、后台撤单取消订单(`cancelled_by_admin`)、打手退单申请审核通过、超时处置(`timeout_reopen` / `timeout_cancel_release` / `timeout_cancel_deduct`)、已取消工单重新启用(兜底清理历史绑定)。
* **售后处置不在其列**:售后(`resolveAdminAfterSalesCase`)只对**已验收**工单做资金追缴/退赔,工单状态保持 `accepted`,货物已交付,无需解绑。若买家退回实物,由运营在寄售仓库执行【退回复库】(已验收工单不再是"进行中",守卫会放行)并填写原因。
* 发布押金保持冻结:现货回到在库池等同于"重新挂卖",符合「发布冻结、驳回/验收释放」的既有口径;卖家要彻底取回物品,由运营执行【下架作废】后释放押金。
### 3. 数据安全与事务保障
* 撤单过程全部置于数据库事务(`withTransaction`)中,确保库存状态与工单状态原子同步,绝不会出现“工单回退了但库存未恢复”的错乱。
* 撤销时事务内对工单行 `SELECT ... FOR UPDATE`,库存 UPDATE 带 `status = 'consumed' AND bound_work_order_id = $工单` 条件,命中 0 行即 409,防止与「验收」并发时丢失更新或抢占其他工单的绑定。
* 一件现货同一时刻最多绑定一张工单:迁移 `079` 增加部分唯一索引 `uniq_consignment_inventories_bound_work_order` 兜底。
* 每次撤单操作均在工单事件(`work_order_events`)中留下完整审计轨迹(记录撤销人、撤回时间、原绑定单号与原因)。
---
## 六、 自动化统计报表规划(告别手工 Excel 汇总)
在后台提供清晰的统计看板,解决运营与财务的核心诉求:
1. **收购人人效统计(Buyer Stats)**:
* 维度:王斌、周杰雄、周顺 等。
* 指标:收货总量(件)、收货总花费(元)、已售出件数、库存去化率(%)、平均单价。
2. **店铺出库贡献统计(Shop Stats)**:
* 维度:拼多多向日葵、稚嫩游戏、佳佳游戏、小鱼儿 等。
* 指标:各店铺消耗寄售库存单数、**直接节省的打手赏金总额(元)**、平均履约时效。
3. **批次核算统计(Batch Stats)**:
* 维度:按 `8.30-1`、`8.30-4` 等批次代码汇总。
* 指标:批次总支出、已回款金额、剩余未售资产成本,便于每天下班对账。
4. **品类规格周转(Spec Stats)**:
* 统计 暗潮、怒嚎、血宴 各钻级手枪的在库均价、库存留存天数与断货预警。
---
## 七、 界面交互与原型设计
### 1. 接单平台 · 寄售仓库管理页(`ConsignmentInventoryPanel`)
* **顶部数据指标卡**:
* 【在库总数】 / 【在库总成本】 / 【累计节省赏金】 / 【本月出库单数】。
* **高级筛选工具条**:
* 下拉筛选:收购人(王斌/周杰雄等)、售出店铺(拼多多向日葵等)、批次(如 8.30-1)。
* 点选切换:系统(全部/苹果/安卓)、区服(全部/微信/QQ)、系列(全部/暗潮/怒嚎/血宴)、钻级(全部/1钻~6钻)。
* 状态标签切换:【全部】/【在库待匹配】/【已出库】/【已作废】。
* **表格列表**:
* 支持彩色 Badge:收购人(王斌-紫色、周杰雄-粉色等)、店铺名(拼多多向日葵-橙色等)。
* **行内 Switch 开关**:【允许匹配】随手可切,切换即刻生效。
* 凭据与截图:支持点击缩略图查看大图(包含角色主页图、手枪词条图)。
* 敏感交付信息:点击【查看账号密码】,弹窗显示并带有一键复制。
* 操作:【编辑】、【作废】、针对已出库物品提供【退回复库】。
### 2. 工单列表与撤销重选(`WorkOrdersPanel`)
* **发布前拦截**:点击「发布并核销」时,弹窗推荐匹配库存,支持一键选用内部履约结单。
* **撤销履约入口**:对已完成内部履约的工单,提供操作按钮【撤销寄售履约】:
* 弹出确认弹窗,选择“选错重选 / 买家退款 / 物资作废”;
* 点击确认后原子回退状态,工单即刻变回未分配状态,支持重新选或发大厅。
### 3. ~~客服创建收集链接弹窗(`CreateCollectLinkModal`)~~ —— **已下线**
> 随收集链接渠道一并移除,客服不再生成对外链接;提报入口为接单平台打手端页面。
### 4. 寄卖提报审核面板(`ConsignmentSubmissionsPanel`)
* 展示卖家提交的待审单据。
* 审核弹窗:双栏布局,左边看截图(支持放大看词条),右边核对资料,一键【审核入库】或【驳回】。
---
## 八、 技术落地与阶段划分
1. **阶段一:数据库与模型层**
* 创建数据表 `consignment_inventories`、`consignment_collect_links`、`consignment_submissions`。
* 配置对应 RBAC 权限(`consignment.view`, `consignment.manage`, `consignment.review`, `consignment.fulfill`)。
2. **阶段二:后端核心服务与匹配引擎**
* 实现库存 CRUD、多维筛选及统计报表查询;
* 实现和平精英规格匹配算法(暗潮/怒嚎/血宴 + 1~6钻 + 1~6璀璨 + `allow_matching` 开关判断);
* 实现发布拦截检测接口与一键内部履约原子事务;
* **实现工单撤销履约与库存回退重选服务(`revokeConsignmentFulfillment`)**。
3. **阶段三:前端管理后台**
* 接单平台新增「寄售仓库」面板(包含多维筛选、彩色标签、Switch 开关、卡密查看、退回复库);
* 新增「寄售审核」面板(提报来源为接单平台打手端);
* 改造工单发布拦截与履约弹窗;
* 增加工单列表/详情中的「撤销寄售履约」弹窗与重选逻辑。
4. **阶段四:~~移动端 H5 填报端~~ —— 已下线**
* 原设计为移动端极简收集页;实际改为接单平台打手端提报,H5 页面已删除。
+2 -2
View File
@@ -51,7 +51,6 @@
```
[打手接单平台提报] ── 唯一入口(H5 定向收集链接已整体下线)
[客服手工录入] ── 凭据/截图字段名已对齐(本轮修复)
│
▼
提报审核 → consignment_inventories(in_stock, allow_matching)
@@ -72,7 +71,8 @@
> **状态说明(2027-02-21 更新)**:下列清单为第一轮审计结论。本轮已落地批次 A/B 的核心内容,
> 逐条状态见每项标题后的标记;未完成项保留原文,作为下一轮输入。
> 已交付的改动见 `docs/寄卖库存功能设计与实施方案.md` §五(状态机语义)与迁移 `079_drop_consignment_collect_links.sql`。
> 已交付的改动见 `consignment-repo.ts` / `consignment-fulfillment-service.ts` / `consignment-deposit-repo.ts` 与迁移 `079_drop_consignment_collect_links.sql`。
> (原设计稿《寄卖库存功能设计与实施方案》已随代码淘汰删除,历史全文见 git 记录。)
### ✅ 本轮已落地(2027-02-21)
+57 -1
View File
@@ -25,7 +25,11 @@ HAVING COUNT(*) > 1;
-- 2. 同一提报单重复冻结了发布押金?
-- 会让唯一索引 uniq_worker_wallet_ledgers_consignment_publish_freeze 创建失败。
-- 期望:0 行。若有行,需人工决定保留哪笔冻结(多冻的部分按 deposit_release 冲正)。
-- 期望:0 行。注意 079 自身会先去重再建索引——每组(打手+提报单)保留最早一条冻结流水,
-- 多余行改 payload 标记 kind=consignment_publish_superseded(移出押金口径与索引范围,
-- 原行留审计)并按 LEAST(多冻总额, 冻结池实有) 退还押金、写 consignment_publish_dedup_refund
-- 修正流水。所以有行也能建起索引;但若该提报单的押金此前已被释放/没收过,
-- 自动退款会重复退(见 2.1),必须先人工确认。
SELECT
payload_json ->> 'consignmentSubmissionId' AS submission_id,
COUNT(*) AS freeze_rows,
@@ -36,6 +40,58 @@ WHERE ledger_type = 'deposit_freeze'
GROUP BY 1
HAVING COUNT(*) > 1;
-- 2.1(只读)重复退款风险:同提报单有多条冻结流水,且该提报单的押金已经被释放/没收过。
-- 判据:already_settled_fen > 0 时,079 的自动退款会把「已经退过的钱」再退一次——
-- 钱从同一打手其它提报单的冻结额里出,之后那张单释放/没收会因为
-- frozen < outstanding 抛 consignment_deposit_balance_inconsistent(接口 500)。
-- 命中就必须先人工处理,不要直接上库。safe_refund_should_be_fen = 应当退还的金额。
-- 期望:0 行。
WITH fr AS (
SELECT worker_id, payload_json ->> 'consignmentSubmissionId' AS sub, id, ABS(amount) AS amt
FROM worker_wallet_ledgers
WHERE ledger_type = 'deposit_freeze'
AND COALESCE(payload_json ->> 'kind', '') = 'consignment_publish'
AND COALESCE(payload_json ->> 'consignmentSubmissionId', '') <> ''
),
g AS (
SELECT
worker_id,
sub,
COUNT(*) AS freeze_rows,
SUM(amt) AS freeze_total_fen,
(ARRAY_AGG(amt ORDER BY id ASC))[1] AS kept_fen
FROM fr
GROUP BY 1, 2
),
s AS (
SELECT
worker_id,
payload_json ->> 'consignmentSubmissionId' AS sub,
COALESCE(SUM(CASE WHEN ledger_type IN ('deposit_release', 'deposit_deduction')
THEN ABS(amount) ELSE 0 END), 0) AS settled_fen
FROM worker_wallet_ledgers
WHERE COALESCE(payload_json ->> 'kind', '') = 'consignment_publish'
AND COALESCE(payload_json ->> 'consignmentSubmissionId', '') <> ''
GROUP BY 1, 2
)
SELECT
g.worker_id,
g.sub AS submission_id,
g.freeze_rows,
g.freeze_total_fen - g.kept_fen AS dup_fen,
COALESCE(s.settled_fen, 0) AS already_settled_fen,
COALESCE(w.frozen_deposit_amount, 0) AS wallet_frozen_fen,
LEAST(g.freeze_total_fen - g.kept_fen, COALESCE(w.frozen_deposit_amount, 0))
AS migration_will_refund_fen,
GREATEST(0, (g.freeze_total_fen - COALESCE(s.settled_fen, 0)) - g.kept_fen)
AS safe_refund_should_be_fen
FROM g
LEFT JOIN s ON s.worker_id = g.worker_id AND s.sub = g.sub
LEFT JOIN worker_wallets w ON w.worker_id = g.worker_id
WHERE g.freeze_rows > 1
AND COALESCE(s.settled_fen, 0) > 0
ORDER BY 1, 2;
-- 3. 待删除对象的影响面(收集链接渠道下线)。
-- 用 DO 块 + 动态 SQL,保证"表/列已被删掉"时脚本也能跑(否则会直接报错中断)。
DO $$
+1 -1
View File
@@ -11,7 +11,7 @@
4. **TaskContext**:`types/task-context.ts`,`parseTaskContext` 返回 `TaskContext`
5. **ClaimIdentity 契约**:`types/claim-identity.ts`(`ClaimIdentity` / `ClaimIdentityPayload` / `ClaimIdentityAdminSummary`)
6. **Admin 任务详情契约**:`types/admin/task-detail.ts`,`getAdminTaskDetail(): Promise<AdminTaskDetailView>`
7. **normalize 出口具名类型**:`KuaishouCloudFlow`、`KuaishouFeifeiFlow`
7. **normalize 出口具名类型**:`AffiliateDashFlow`(`apps/backend/src/services/fulfillment/affiliate-dash/index.ts:304`,由 `normalizeAffiliateDashFlow` 返回);原 `KuaishouCloudFlow` / `KuaishouFeifeiFlow` 已随通道下线移除(全库 grep 0 命中)
8. **前后端 claimIdentity 字段对齐**:前端 `ClaimIdentityInfo` / `AdminClaimIdentitySummary`
9. **测试 fixture**:`delivery-task-service.test.ts` 减少 `as any`;生产路径去掉 `as any` 状态列表
@@ -2,11 +2,11 @@
## 规则(现行)
1. **所有 lewan 自动发货**必须存在 `context.claimIdentity.expectedUid`。
1. **所有自动履约(affiliate-dash)发货**必须存在 `context.claimIdentity.expectedUid`。
2. 绑定角色 ID(`rid`)必须与 expectedUid 一致,否则禁止 confirm / redeem / dispatch。
3. **旧单无 UID**:用户打开领取页补填 UID 后继续;后台发货也会被拦截并提示补 UID。
4. **不再**使用「仅确认角色、无 UID」的静默兼容路径。
5. feifei:用户填 UID 后拼入 H5;对外仍只返回本站 claimUrl。
5. affiliate-dash:用户填 UID 后作为 `data.game_account` 传给 affiliate-dash(`services/fulfillment/affiliate-dash/index.ts:446-447`);对外仍只返回本站 claimUrl。
## 运营可见
@@ -21,7 +21,7 @@
- Step1:填 UID
- Step2:绑定;匹配成功后可 **一键确认兑换**(无需再点「下一步」)
- mock 任务仍可不调真实 CT
- dev-mock 任务可不调真实 affiliate-dash 平台(`flow.mock.enabled` 短路建单 / 绑定 / 提交 / 同步)
## 短链
@@ -1,3 +1,6 @@
> ⚠ 本仓库当前**未接入** cloudtentacles 通道:该履约通道已下线,`apps/backend/src/services/platforms/` 下已无对应适配器,`fulfillment/executors/registry.ts:18-19` 对历史通道只保留只读 legacy stub(`legacy-retired-executor.ts`)。本文是第三方(cloudtentacles)官方接口样例,仅作外部参考。
> 现行自动履约通道是 affiliate-dash(`apps/backend/src/services/platforms/affiliate-dash/`,走 `/api/client/v1`);通道现状见 `docs/ affiliate_dash_发货平台/affiliate-dash对接.md`。
查询SKU列表
```
{
+1 -1
View File
@@ -22,6 +22,6 @@
`测试-关联商品1----3676797936`
系统会把 `测试-关联商品1` 当作商品名字匹配 cloudtentacles,把 `3676797936` 当作快手小店 shopId 用于核销。
系统会把 `测试-关联商品1` 当作商品名字,走 affiliate-dash 的匹配链路(skuMapping 指定匹配 → 商品路由规则 → 全局优先级,默认仅 `affiliate_dash`,未命中落 `manual_dispatch`);把 `3676797936` 当作快手小店 shopId 用于核销。
这是最重要的信息
@@ -7,8 +7,8 @@
| 模块 | 定位 | 说明 |
| --- | --- | --- |
| 91 卡券 | 订单来源 + 发链接通道 | 91 调用我方下单接口,我方生成领取链接;91 查询订单时拿到领取链接,并发送给用户。 |
| kuaishou-cloud | 发货平台 1 | 已对接,用于发放它支持的皮肤、道具。 |
| kuaishou-feifei | 发货平台 2 | 新增发货平台,作为 kuaishou-cloud 的补充,覆盖 kuaishou-cloud 没有的皮肤、道具。 |
| affiliate-dash | 现行发货平台(自动履约) | 当前唯一自动履约通道,order_site 作为其商户走开放接口 `/api/client/v1`,发放皮肤、道具。 |
| kuaishou-cloud / kuaishou-feifei | 已下线发货平台 | 历史乐玩 / 飞飞通道已下线,executor 仅保留只读 legacy stub(`legacy-retired-executor.ts`),不再发货。 |
| 快手电子凭证 | 领取流程里的自动核销能力 | 用来替代用户在领取页手动输入核销码的步骤,不替代 91,也不替代发货平台。 |
因此整体设计不是“新增订单来源”,而是:
@@ -16,7 +16,7 @@
```text
91 负责进单和把领取链接发给用户
我方领取页负责统一承接用户
kuaishou-cloud / kuaishou-feifei 负责真实发货
kuaishou-cloud / kuaishou-feifei 通道已下线,affiliate-dash 负责真实发货
快手电子凭证负责把领取页里的核销码输入步骤自动化
```
@@ -26,8 +26,8 @@ kuaishou-cloud / kuaishou-feifei 负责真实发货
91 卡券下单
-> 我方创建内部订单和履约任务
-> 按商品选择发货平台
-> kuaishou-cloud
-> kuaishou-feifei
-> affiliate-dash(自动履约)
-> manual_dispatch(人工兜底)
-> 91 卡券查询订单
-> 我方返回统一 claimUrl
-> 91 卡券把 claimUrl 发给用户
@@ -35,7 +35,7 @@ kuaishou-cloud / kuaishou-feifei 负责真实发货
-> 系统检查快手电子凭证状态
-> 已有关联电子凭证:跳过手动输入核销码
-> 未关联电子凭证:保留兜底处理
-> 用户继续完成绑定 / 跳转发货平台 H5
-> 用户继续完成绑定 / 提交发货
-> 发货平台完成履约
-> 我方更新任务状态
-> 我方完成快手电子凭证核销闭环
@@ -54,7 +54,7 @@ kuaishou-cloud / kuaishou-feifei 负责真实发货
claimUrl = https://你的域名/#/claim/{token}
```
不要直接把 kuaishou-cloud 绑定链接或 kuaishou-feifei 的 H5 链接返回给 91。原因是领取页需要统一处理:
不要直接把 affiliate-dash 的绑定链接返回给 91。原因是领取页需要统一处理:
- 商品最终走哪个发货平台;
- 是否已经通过快手电子凭证完成自动核销;
@@ -67,48 +67,51 @@ claimUrl = https://你的域名/#/claim/{token}
```text
91 productNo
-> 商品匹配
-> 命中 kuaishou-cloud 商品
-> executor_key = kuaishou_ct_assisted
-> 命中 kuaishou-feifei 商品
-> executor_key = kuaishou_feifei
-> 商品匹配(skuMapping 指定匹配:91 编码 / 商品名 -> affiliate-dash sku)
-> 命中且 affiliate-dash 通道可用
-> executor_key = affiliate_dash
-> 未命中
-> 进入待补全 / 人工处理
-> 按履约路由配置:商品路由规则 -> 全局优先级
-> 仍未命中 -> 待补全 / 人工履约(manual_dispatch)
```
优先级建议先做成可配置规则,不在代码里硬编码。第一阶段可以采用:
优先级由可配置规则决定(`services/fulfillment/routing-config-service.ts`),当前顺序:
1. 显式覆盖规则优先;
2. 再按商品名匹配 kuaishou-cloud;
3. 再按商品名或 `product_code` 匹配 kuaishou-feifei;
4. 都未命中则进入待补全队列。
1. 指定匹配(显式映射)优先;
2. 再按商品路由规则(商品名 exact / contains);
3. 再按全局优先级 `defaultExecutorPriority`(当前默认仅 `affiliate_dash`);
4. 都未命中则回落人工履约(`manual_dispatch`)或保持未配置。
## 5. kuaishou-feifei 接入方式
## 5. kuaishou-feifei 接入方式(已下线)
kuaishou-feifei 的主要交付物是 `h5.recharge_url`。
kuaishou-feifei 通道已下线:历史交付物是 `h5.recharge_url`,相关 executor / API client / H5 对接均已移除,历史任务由只读 legacy stub(`legacy-retired-executor.ts`)承接,不再发货。
创建任务后,系统应向 kuaishou-feifei 创建订单,并把关键字段保存到任务上下文:
现行自动履约通道是 affiliate-dash:order_site 作为其商户走 `/api/client/v1`,关键字段保存到任务上下文 `context_json.affiliateDash`(具名类型 `AffiliateDashFlow`):
```json
{
"kuaishouFeifei": {
"platformOrderNo": "内部幂等单号",
"orderNo": "发货平台内部单号",
"productCode": "商品编码",
"rechargeStatus": 15,
"statusLabel": "待绑定",
"h5": {
"rechargeUrl": "http://..."
},
"affiliateDash": {
"flowType": "affiliate_dash",
"sku": "affiliate-dash sku",
"productName": "商品名",
"orderNo": "affiliate-dash 平台单号",
"clientOrderNo": "内部幂等单号",
"orderStatus": "paid / delivering / delivered / ship_failed / cancelled",
"canShip": true,
"bindUrl": "http://...",
"qrUrl": "http://...",
"bound": false,
"submitStatus": "",
"consumeStatus": "",
"lastSyncedAt": "2026-07-07T00:00:00.000Z"
}
}
```
领取页根据当前任务的 executor 展示不同内容:
领取页根据当前任务的 executor 展示不同内容(`services/claim/kuaishou-cloud-claim-context.ts`):
- `kuaishou_ct_assisted`:展示现有 kuaishou-cloud 绑定流程;
- `kuaishou_feifei`:展示或跳转 kuaishou-feifei 的 `h5.recharge_url`;
- `affiliate_dash`:本站领取页自建流程(查 delivery -> 填 UID -> bind -> 轮询 bind-result -> submit);
- `kuaishou_ct_assisted` / `kuaishou_feifei`:通道已下线,仅展示历史信息与「该履约渠道已下线」提示;
- `manual_dispatch`:进入人工兜底。
## 6. 快手电子凭证的嵌入点
@@ -154,25 +157,28 @@ kuaishou-feifei 的主要交付物是 `h5.recharge_url`。
## 7. 核销完成时机
电子凭证不应该在用户刚打开领取页时就立刻标记为已消费。
电子凭证的核销时机与领取页打开解耦:不因用户打开领取页就标记已消费。
推荐时机:
当前实现(`services/fulfillment/affiliate-dash/index.ts:87-117,212-241`)在**建单成功、91 侧可取到领取链接时**即核销(91 卡信息一经消耗),不再等发货完成:
```text
发货平台实际履约成功
准备履约任务成功(拿到领取链接)
-> 我方调用快手电子凭证核销
-> 凭证状态更新为 CONSUMED(consumeStatus = success)
发货回调 delivered
-> 只做状态收敛:consumeStatus 已 success / not_required 则短路
-> failed / 空才补核销(兜底)
-> 我方更新 task 为 completed / redeemed
-> 我方触发快手电子凭证核销回调
-> 电子凭证状态更新为 CONSUMED
```
这样可以避免用户打开链接但未完成绑定时,快手侧已经显示核销成功。
核销失败不阻塞下单与链接发放(`consumeStatus = failed` + warn 日志),由 delivered 回调兜底。
不同发货平台的成功判定:
| 发货平台 | 成功判定 |
| --- | --- |
| kuaishou-cloud | 当前发货流程完成,任务进入完成态。 |
| kuaishou-feifei | 查询或异步通知返回 `recharge_status = 30`。 |
| affiliate-dash | `submit` 成功 / 回调 `order.shipping.updated` 且 `order_status = delivered`,任务进入完成态。 |
| kuaishou-cloud / kuaishou-feifei | 已下线,不再产生新的成功判定;历史任务只读。 |
失败时:
@@ -185,18 +191,18 @@ kuaishou-feifei 的主要交付物是 `h5.recharge_url`。
当前项目已经有以下基础能力:
- 91 卡券下单和查询;
- kuaishou-cloud 发货流程;
- 快手电子凭证 `send-code / query-code / destroy-code / consume-code` 接口骨架;
- 领取页手动输入核销码流程。
- affiliate-dash 自动发货流程(唯一自动履约通道);
- 快手电子凭证 `send-code / query-code / destroy-code / consume-code` 接口;
- 领取页手动输入核销码流程(异常兜底)。
后续需要把它们串起来:
后续调整方向(当前状态):
1. 91 查询仍返回统一 `claimUrl`。
2. 商品匹配支持多发货平台。
3. kuaishou-feifei 新增独立 executor 和 API client。
4. 领取页根据 task executor 展示对应发货流程。
5. 快手电子凭证状态进入 task context,用于跳过手动输入核销码。
6. 发货成功后再触发快手电子凭证核销。
1. 91 查询仍返回统一 `claimUrl`(已完成)。
2. 商品匹配:skuMapping 指定匹配 -> 商品路由规则 -> 全局优先级(默认仅 `affiliate_dash`),未命中回落人工履约(已完成)。
3. kuaishou-feifei / kuaishou-cloud 已下线,executor 仅保留只读 legacy stub,不再新增独立 executor 和 API client(已处理)。
4. 领取页根据 task executor 展示对应发货流程(affiliate_dash 自建流程;下线通道展示历史信息)(已完成)。
5. 快手电子凭证状态进入 task context,用于跳过手动输入核销码(已完成)。
6. 建单成功(拿到领取链接)即触发快手电子凭证核销,发货回调只做状态收敛(已完成)。
7. 保留手动输入核销码作为异常兜底,而不是主路径。
## 9. 数据关联
@@ -323,26 +329,18 @@ send-code.oid
### 11.1 当前逻辑
当前实现中,`send-code` 返回的 `eticket.id` 来自本地 task id:
当前实现中,`send-code` 返回的 `eticket.id` 是独立券表的业务券号 `voucher_code`,不再依赖 task id:
```text
eticket.id = fulfillment_tasks.id
eticket.id = kuaishou_industry_vouchers.voucher_code
voucher_code = 'KSV' + Crockford Base32(10 字节随机数)
```
真实测试单中:
来源:`services/platforms/kuaishou-industry/send-code-service.ts:86-116`(按 `oid + unit_index` upsert 券记录后返回)、`services/platforms/kuaishou-industry/voucher-service.ts:95`(`id / code = voucher_code`)、`repositories/kuaishou-industry-voucher-repo.ts:26-53`(`KSV` 前缀 + Crockford Base32 生成,唯一冲突自动重试)。
```text
eticket.id = 2038
```
历史测试单(2026-07-07)里 `eticket.id = 2038`,那是早期把 task id 当券号的实现,已废弃。
这说明当前“核销码/券号”不是快手生成的,而是我方创建 task 后,把 task id 当作电子凭证 id 返回给快手。
这个方式有两个问题:
1. task 属于当前实现里的 `kuaishou-industry` 孤立订单链,尚未绑定到 91 订单 task。
2. task id 虽然全局唯一,但它和任务生命周期耦合太强;后续如果任务重建、补配置、切换发货平台,会影响电子凭证反查和幂等。
### 11.2 推荐目标
### 11.2 券号要求
电子凭证券号应该满足:
@@ -354,58 +352,55 @@ eticket.id = 2038
| 可延迟绑定 | 如果快手 `send-code` 早于 91 task 准备完成,券号也能先落库,后续再绑定 task。 |
| 可核销 | 发货完成后,能用同一张券更新为 `CONSUMED`,并记录核销流水。 |
### 11.3 推荐数据模型
### 11.3 已落地数据模型
建议新增独立的电子凭证券表,而不是继续只依赖 task id:
已新增独立的电子凭证券表 `kuaishou_industry_vouchers`(`db/migrations/001_init.sql:272-297`,`eticket_type` 见 `db/migrations/003_kuaishou_industry_voucher_eticket_type.sql`),不再只依赖 task id:
```text
kuaishou_industry_vouchers
id BIGSERIAL PRIMARY KEY
voucher_code TEXT UNIQUE NOT NULL
voucher_code TEXT NOT NULL UNIQUE
oid TEXT NOT NULL
order_id BIGINT NULL
task_id BIGINT NULL
order_id BIGINT NULL REFERENCES orders(id) ON DELETE SET NULL
task_id BIGINT NULL REFERENCES fulfillment_tasks(id) ON DELETE SET NULL
unit_index INTEGER NOT NULL
token TEXT NOT NULL
status TEXT NOT NULL -- UNUSED / CONSUMED / DESTROYED
valid_start_time BIGINT NOT NULL
valid_end_time BIGINT NOT NULL
seller_id TEXT NOT NULL DEFAULT ''
token TEXT NOT NULL DEFAULT ''
eticket_type TEXT NOT NULL DEFAULT ''
status TEXT NOT NULL DEFAULT 'UNUSED' -- UNUSED / CONSUMED / DESTROYED
valid_start_time BIGINT NOT NULL DEFAULT 0
valid_end_time BIGINT NOT NULL DEFAULT 0
consume_serial_num TEXT NOT NULL DEFAULT ''
consume_details_json JSONB NOT NULL DEFAULT '[]'::jsonb
consumed_at TIMESTAMPTZ NULL
destroyed_at TIMESTAMPTZ NULL
raw_payload_json JSONB NOT NULL DEFAULT '{}'::jsonb
send_callback_status TEXT NOT NULL DEFAULT 'success'
send_callback_attempt_count / send_callback_last_error / send_callback_response_json / send_callback_sent_at
created_at TIMESTAMPTZ NOT NULL
updated_at TIMESTAMPTZ NOT NULL
唯一约束:
UNIQUE(oid, unit_index)
UNIQUE(voucher_code)
UNIQUE(oid, unit_index)
```
券号生成建议:
券号生成(`repositories/kuaishou-industry-voucher-repo.ts:26-53`):
```text
voucher_code = String(kuaishou_industry_vouchers.id)
voucher_code = 'KSV' + Crockford Base32(10 字节随机数)
```
理由:
- 数字字符串兼容性最好;
- 数据库主键保证不重复;
- 重复 `send-code` 时通过 `UNIQUE(oid, unit_index)` 找回原记录,返回原 `voucher_code`;
- 业务券号与 task 生命周期解耦,任务重建 / 换发货通道不影响反查;
- 数据库 `UNIQUE(voucher_code)` 保证不重复,生成冲突自动重试(最多 5 次);
- 重复 `send-code` 时通过 `UNIQUE(oid, unit_index)` + `ON CONFLICT ... DO UPDATE` 找回原记录,返回原 `voucher_code`;
- 不依赖 task 是否已经创建。
如果后续确认快手完全支持任意字符串,也可以升级为带前缀的业务券号,例如:
```text
voucher_code = KSEV-{oid}-{unitIndex}
```
但第一阶段建议优先使用数字字符串,降低快手侧兼容风险。
### 11.4 send-code 幂等规则
`send-code` 处理流程应改为:
`send-code` 处理流程(已实现):
```text
收到 oid + num
@@ -424,17 +419,17 @@ voucher_code = KSEV-{oid}-{unitIndex}
第一次 send-code:
oid = 2618800083429561
num = 1
eticket.id = 2038
eticket.id = KSVASXT6Q955WT4SS36
第二次 send-code:
oid = 2618800083429561
num = 1
eticket.id 仍然 = 2038
eticket.id 仍然 = KSVASXT6Q955WT4SS36
```
### 11.5 有效期生成规则
真实测试单里快手没有传 `certActualStartTime / certActualEndTime`,并且 `certStartTime / certEndTime` 也可能都是 0。当前系统返回了:
真实测试单里快手没有传 `certActualStartTime / certActualEndTime`,并且 `certStartTime / certEndTime` 也可能都是 0。当时系统返回了:
```text
validStartTime = 0
@@ -476,12 +471,12 @@ validStartTime = send-code 处理时的当前毫秒时间戳
validEndTime = validStartTime + 3 * 86400000
```
### 11.6 后续核销规则
### 11.6 核销规则(已实现)
发货完成后,不再要求用户输入快手核销码,而是系统根据 voucher 主动完成核销闭环:
不再要求用户输入快手核销码,而是系统根据 voucher 主动完成核销闭环。affiliate-dash 在**建单成功(拿到领取链接)**时即核销,`delivered` 回调只做状态收敛与兜底补核销(`services/fulfillment/affiliate-dash/index.ts:87-117,212-241` / `services/platforms/kuaishou-industry/voucher-service.ts:241-291`):
```text
发货平台履约成功
建单成功 / affiliate-dash 已消耗(91 卡信息到手)
-> 找到 task 绑定的 voucher
-> 检查 voucher.status = UNUSED
-> 生成稳定 consume_serial_num
@@ -1,53 +0,0 @@
# 工程债:lewan 拆分、默认角色、短链
## 命名约定
| 叫法 | 用途 |
| --- | --- |
| lewan / kuaishou-lewan | 业务/文案 |
| `kuaishou_ct_assisted` | `executor_key` |
| `kuaishou-cloud/` | 履约领域模块路径 |
| `cloudtentacles` | 外部发货平台 API |
## lewan 模块拆分(已做)
```text
kuaishou-cloud/
index.ts # barrel 导出
prepare-fulfillment.ts # prepare + 刷新 bindUrl
rebind-role.ts
probe-bind-url.ts
refresh-role-info.ts
role-state.ts # 默认角色快照 + ready 状态(UID 主闸)
delivery-plan.ts
ensure-claim-link.ts
dispatch-role-sync.ts
redeem-fulfillment.ts # 一键兑换用例
confirm-role.ts # 确认角色用例
dispatch-fulfillment.ts # 发货编排
dispatch-stock.ts # 库存/采购/下发
dispatch-failure.ts # 发货失败落库
return-fulfillment.ts # 退号 + 电子凭证核销收尾
mock-helpers.ts
task-finalization.ts # 兼容 re-export
domain.ts
executors/
kuaishou-cloud-executor # preparePaid + prepareBinding + rebind/refresh/confirm + redeem + dispatch + return
registry # prepare/rebind/refresh/confirm/redeem/dispatch/return 统一入口
```
对外 import 路径仍为 `.../kuaishou-cloud/index.js`。
claim / admin 应优先走 `executors/registry`,不要直连子模块动作。
## 默认角色
- 主闸:`claimIdentity.expectedUid` 与绑定 `rid` 一致。
- `isDefaultRole` 仅诊断文案;有角色信息即 `ready` 并落库 rid。
- `hasKuaishouCloudCustomerRole` 改为「是否已有绑定角色」,不再要求不同于默认角色。
## 短链
- `/s/:code` **只读解析**历史链接。
- feifei claim **不再**依赖 shortLink 交付;若仍调用 `ensureShortLinkForTarget` 且 source 为 feifei claim,会打 warn。
- 新业务:本站 claimUrl + H5 拼 `uid`。
@@ -1,61 +0,0 @@
# 开发 Mock(领取 / 91 查单)
优先使用 **后台 GUI**:登录运营后台 → 左侧 **开发 / 开发 Mock**(仅非 production 环境显示)。
生产镜像默认 `NODE_ENV=production`,接口会 403。本地 docker-compose.dev 默认可用;特殊环境可设 `ENABLE_DEV_MOCK=1`。
## GUI 能做什么
| 卡片 | 作用 |
| --- | --- |
| lewan 领取 | 写库生成 91 订单 + claim,强制 UID 匹配流(不调 CloudTentacles) |
| feifei 领取 | 写库生成 feifei 任务 + 本站 claim;H5 带 uid(不调真实 feifei) |
| 91 查单 | 生成带签名的 `POST /api/v1/open/91/orders/query`,可本机执行 |
推荐路径:
1. lewan 选 `binding` + UID `10001` → 生成 → 打开领取链接 → 确认兑换
2. 用同一 orderNo 点「模拟 91 查单」验证 cards / claimUrl
## CLI(可选)
```bash
# lewan
docker compose -f docker-compose.dev.yml exec -T backend \
npm run mock:claim -- --step=binding --uid=10001
# feifei
docker compose -f docker-compose.dev.yml exec -T backend \
npm run mock:feifei -- --step=ready --uid=166909256
# 91 查单(需 KAQUAN91_SECRET)
docker compose -f docker-compose.dev.yml exec -T backend \
npm run mock:open91 -- --baseUrl=http://127.0.0.1:3000 --mode=query --orderNo=MOCK91...
```
## 步骤说明
### lewan `--step`
| step | 含义 |
| --- | --- |
| `uid` | 待填 UID(Step1) |
| `binding` | 已预填 UID,角色 ID 已匹配(推荐) |
| `confirm` | 待确认兑换 |
| `result` | 已模拟完成 |
兑换走 `context.mock.enabled=true`,不会真实发货。
### feifei `--step`
| step | 含义 |
| --- | --- |
| `uid` | 待填 UID |
| `ready` | 已填 UID,可打开带 uid 的 H5 |
| `completed` / `failed` | 模拟终态 |
## 与真实环境边界
- Mock **不**替代真实 CloudTentacles / feifei 联调
- 91 `create` CLI 仍会走真实商品匹配;造单测领取请用 GUI / `mock:claim` / `mock:feifei`
- 写入的是开发库真实行,注意别在生产 volume 上开 Mock
@@ -1,71 +0,0 @@
# 服务器图片迁移到阿里云 OSS
本流程只操作服务器上的 MinIO。Mac 本地继续使用 `.env.mac-docker.example` 的 MinIO 配置,不会把本地图片迁走。
## 1. 准备 OSS
在阿里云 OSS 创建与现有 MinIO bucket 对应的 bucket,并创建只允许该 bucket 的 RAM 账号。建议 OSS 使用 S3 兼容端点,例如 `https://oss-cn-hangzhou.aliyuncs.com`,不要把 bucket 名写进 endpoint。
编辑服务器 `.env`,先保留 MinIO 为主存储,并填好 OSS 目标配置:
```dotenv
STORAGE_MODE=dual
STORAGE_ENDPOINT=http://minio:9000
STORAGE_BUCKET=order-site
STORAGE_ACCESS_KEY_ID=原 MinIO 账号
STORAGE_SECRET_ACCESS_KEY=原 MinIO 密钥
STORAGE_REGION=us-east-1
STORAGE_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
STORAGE_OSS_BUCKET=OSS bucket 名
STORAGE_OSS_ACCESS_KEY_ID=OSS RAM AccessKey ID
STORAGE_OSS_SECRET_ACCESS_KEY=OSS RAM AccessKey Secret
STORAGE_OSS_REGION=oss-cn-hangzhou
```
重建后端容器使配置生效。`dual` 模式下新上传的原图和缩略图会同时写入 MinIO、OSS;读取仍优先 MinIO,失败时回退 OSS,所以迁移期间无需停机。
## 2. 增量复制与校验
在服务器项目目录执行。容器镜像已内置迁移程序,下面命令不会停止现有后端:
```bash
docker compose exec backend node dist/storage-migrate.js copy
docker compose exec backend node dist/storage-migrate.js verify
```
复制可以重复执行,目标中已有且大小、ETag 一致的对象会跳过。程序默认 8 路并发,可通过 `docker compose exec -e STORAGE_MIGRATION_CONCURRENCY=16 ...` 调整(最大 32)。校验会比较源、目标每个对象的 SHA-256、大小和 Content-Type;复制阶段的 ETag 仅用于幂等跳过,最终仍会对所有对象做完整内容校验。复制期间仍有新上传时,复制完成后再次执行 `copy` 和 `verify`,直到校验通过且没有遗漏。
## 3. 切换到 OSS
确认校验通过后,把服务器 `.env` 的 `STORAGE_MODE` 改为 `oss`,重新创建 backend:
```bash
docker compose up -d --no-deps backend
curl -fsS https://你的域名/health/ready
```
此时新旧图片都只从 OSS 读取,数据库中的对象 key 和访问 URL 不需要改动。
## 4. MinIO 退场
先观察 OSS 模式运行一段时间,确认图片上传、后台预览、打手端验收图片均正常,再停止并移除 MinIO 容器(若仍由旧版编排管理):
```bash
docker compose --profile storage stop minio
docker compose --profile storage rm -f minio
```
生产编排(`docker-compose.yml` 与 `deploy/ubuntu-deploy.sh`)已移除 MinIO 服务与 `storage` profile,此后部署不会再启动 MinIO。
不要删除 `minio_data` 卷(实际名称 `order_site_minio_data`),至少保留一个回滚窗口。若需要回滚,把 `.env` 的 `STORAGE_MODE` 改回 `dual`,并手动用旧卷拉起一个 MinIO 容器接入 compose 网络(`--network-alias minio` 保证 backend 仍可用 `http://minio:9000` 访问它):
```bash
docker run -d --name minio-rollback \
--network order_site_default \
--network-alias minio \
-v order_site_minio_data:/data \
docker.m.daocloud.io/minio/minio:latest \
server /data --address ":9000" --console-address ":9001"
```
确认 OSS 已稳定且完成密钥轮换后,再单独评估删除 `minio_data` 卷释放磁盘。
+1 -1
View File
@@ -243,7 +243,7 @@ npm run test:industry:http -- --baseUrl=https://ks.khhao.com --load --endpoint=q
- [ ] 三个出站回调(发码/核销/销毁)已实现并配置 `KUASHOU_INDUSTRY_SEND_CALLBACK_ENABLED=true`
- [ ] access_token 已通过店主主账号 OAuth 获取,48h 自动刷新
- [ ] 生产环境 appKey / signSecret 与测试环境独立配置
- [ ] 测试用例文档(`测试用例.md`)已填写完整提交快手审核
- [ ] 测试用例已用 `apps/backend/scripts/gen-test-cases.ts`(`npm run test:industry:gen`)生成并整理完整,可提交快手审核(测试步骤见 `docs/行业电子凭证/5.测试指南.md`)
- [ ] 压力测试通过:发码 100 TPS、查询 500 QPS、销毁 100 TPS,平均耗时 < 100ms
- [ ] 所有错误场景(签名错/参数缺失/appkey 错)均能正确处理
- [ ] signSecret 未提交到 git 仓库,仅通过环境变量注入
@@ -1,76 +0,0 @@
基础技术
更新时间:2025-03-26
阅读数:5722
本处介绍开放平台的基础协议和通用接入方式
1.环境说明
两套环境相互独立,数据不可相互使用
环境 环境地址 说明
线上环境 https://open.kwaixiaodian.com 线上环境可直接使用
线下环境 https://gw-merchant-staging.test.gifshow.com 测试环境调用需要提供出口IP,并联系快手对接人添加ip白名单
2.授权流程
2.1 如何获取token
首先需要拿到appkey、appsecret、signSecret信息,线下环境找快手提供,线上环境到“开放平台-控制台-应用中心-应用详情”查看,请求示例如下
https://gw-merchant-staging.test.gifshow.com/integration/virtual/topup/mobile/order/callback?access_token=ChFvYXV0aC5hY2Nlc3NUb2tlbhJAErnBVtjx5FPdE0AITaSq4xlW6XaTOaV_McGinj-hFivkyxAw1SZ3i28bLEa6xP-C4UuKlRz6uh3lukYRLDcAdRoSeCxQKnQhAPTTL4YL0_NqmqD-IiApwDDKEL1wBW7EbWH0ZRDO3UUNSCxMwkuF7nb57Fn7AygFMAE&appkey=ks683702719562282620&method=integration.virtual.topup.mobile.order.callback&param=%7B%22orderId%22%3A%22100%22%2C%22businessTime%22%3A%222021-06-08T14%3A16%3A01%2B0800%22%2C%22mobile%22%3A%2215990013607%22%2C%22status%22%3A%22FAILED%22%2C%22amount%22%3A%225000%22%2C%22bizType%22%3A20%7D&signMethod=MD5&timestamp=1623132961782&version=1&sign=c091ca3414a9d6429a0fda47c383d202
注:如果回调失败需要以指数周期(1m、2m、4m、8m...)重试
注意:
测试环境需要把自己的出口ip给到快手,配置白名单,否则无权限访问。配置对接人:屈国庆,王哲,杨天问
测试环境和线上环境,是两套token,不可混用。测试token,如下方式获取。线上token通过上面sdk调用,有效期48h。
Token授权码:参考文档(2.2节 商家开发者角色)
授权一定要用店主的主账号来授权,否则token换取到的sellerId就是子账号的id
线上token验证:https://open.kwaixiaodian.com/commonTool/tokenAuth/accessToken?cateId=0&typeIndex=1,需要快手app扫描登录
测试环境获取token的步骤(参考文档(2.2节 商家开发者角色)):
测试环境:优先使用集成中心自己调试
线上环境:需要按照开放平台的授权文档,通过你的店铺由店主授权(不要用子账号授权),然后用你的redirect_uri来接收code。
用授权码code换取长时令牌refreshToken以及访问令牌accessToken
示例:https://gw-merchant-staging.test.gifshow.com/oauth2/access_token?app_id=ks683702719562282620&app_secret=zSCiWmGOk_diMCU9k3zHcg&grant_type=code&code=bab4f0378269db7a1603de9d5a381615d7d2c451ffbb32f8ede78ac8f05b6171f7d4eb8c
用长时令牌refreshToken刷新访问令牌accessToken
示例:
https://gw-merchant-staging.test.gifshow.com/oauth2/refresh_token?app_id=ks683702719562282620&app_secret=zSCiWmGOk_diMCU9k3zHcg&grant_type=refresh_token&refresh_token=ChJvYXV0aC5yZWZyZXNoVG9rZW4SkAGpBE83BKP9TeiwbEP1-IIQL2G08s8rU-OETIzfs5wFN2HeZgUaR_mfGjKHjHzfV-sicHG4IZmxVKlxQhdAZIdf01AR51ZtPN7sHY3eeW6RMnx8LPKo92V1yk-GnkXiiPMWpn9Vmhq0dk_U_aKh5Jwg7mMySO8IEDoz-fPk9Rc2rjUK3inqxNd7rqTQ9fz316oaEsFIUEDt4EyD090nGWRnwQ5g3SIgY-ctBM_4jrFXxBA9EF1jMaP6as57lNYIyOZf9Qlqq64oBTAB
后面就可以一直用第三步来刷新accessToken
3.签名算法
3.1 协议
https协议,支持GET/POST,调用地址、appkey、signSecret由快手提供,返回结果为json字符串。
3.2 签名
官方签名说明
快手电商开放平台的所有开放API调用都需要进行加签,服务端会根据请求参数,对签名进行验证,签名不合法的请求将会被拒绝。当前平台支持的签名算法为MD5(signMethod=MD5)
下面将详细介绍签名流程:
对所有API系统参数和请求参数(不包括sign参数和byte[]类型的参数),根据参数名称进行字典顺序排序;
排序前的顺序是:appkey=ks123,version=1,method=open.xxx.xxx,signMethod=MD5,access_token=xxxx,timestamp=1583271919000,param={"title":"短袖", "relItemId":123456, "categoryId":12}
排序后的顺序是:appkey=ks123&param={"categoryId":12,"relItemId":123456,"title":"短袖"}&signMethod=MD5&timestamp=1583271919000&version=1&signSecret=abc
保留=符号,用&符号将多个参数及其值组装在一起,根据上面的示例得到的排序结果为:access_token=xxx&appkey=ks123&method=open.xxx.xxx&param={"title":"短袖", "relItemId":123456, "categoryId":12}&signMethod=MD5&timestamp=1583271919000&version=1
排序好参数后,在末尾加入signSecret(在应用创建审核通过后由平台分配,在“应用中心-应用列表-应用详情”中可见)进行对应算法的签名计算
MD5(access_token=xxx&appkey=ks123&method=open.xxx.xxx.xxx&param={"title":"短袖", "relItemId":123456, "categoryId":12}&signMethod=MD5&timestamp=1583271919000&version=1&signSecret=xxxxxx)=sign
签名加好后,将sign添加到请求参数中,并对param内容进行encode(双引号"和冒号:也需要encode),这里注意请求参数里面是不包括signSecret的,signSecret只是作为加签因子用于签名sign的计算,
所以千万不要将signSecret当作请求参数传输,请求参数内容见第3点API调用参数说明的表格内容,请求url样例:
https://open.kwaixiaodian.com/open/xxx/xxx?access_token=xxx&appkey=ks123&method=open.xxx.xxx.xxx&param=%7B%22title%22%3A%22%E7%9F%AD%E8%A2%96%22%2C%20%22relItemId%22%3A123456%2C%20%22categoryId%22%3A12%7D&version=1&signMethod=MD5&timestamp=1583271919000&sign=af2d80958e77e17f1d973003b7b7aec2
说明:param是json对象,需要排序
4.API调用
确认完成了授权流程后进行API的调用测试,内容详情可见《API调用说明》,也可使用平台提供的API测试工具测试
![alt text](image.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

+10
View File
@@ -145,6 +145,7 @@ cp .env.server.example .env
至少配置:
```text
APP_DOMAIN
CADDY_SITE_ADDR
WORKER_SITE_ADDR
CLAIM_BASE_URL
@@ -152,8 +153,17 @@ POSTGRES_PASSWORD
DATABASE_URL
ADMIN_SESSION_SECRET
ADMIN_DEFAULT_USERS_JSON
CORS_ALLOWED_ORIGINS
STORAGE_MODE
STORAGE_OSS_ENDPOINT
STORAGE_OSS_BUCKET
STORAGE_OSS_ACCESS_KEY_ID
STORAGE_OSS_SECRET_ACCESS_KEY
STORAGE_OSS_REGION
```
其中 `APP_DOMAIN`、`CORS_ALLOWED_ORIGINS` 与 `STORAGE_MODE=oss` / `STORAGE_OSS_*` 为生产必需:`docker-compose.yml:95` 的 `STORAGE_MODE` 生产默认即 `oss`,`apps/backend/src/config/runtime-validation.ts:107-114` 在生产环境强制校验 `STORAGE_OSS_*` 凭据并使用显式 CORS 域名白名单;`.env.server.example` 已给出这些变量的模板值,其中带 `change-me` 的占位会被 `deploy/ubuntu-deploy.sh` 的占位值检查拦住,需替换为真实值。
容器内连接 Compose PostgreSQL 时,主机名用 `postgres`:
```text