diff --git a/README.md b/README.md index 6547b669..652aaaa4 100644 --- a/README.md +++ b/README.md @@ -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) | 约定: diff --git a/docs/ affiliate_dash_发货平台/affiliate-dash对接.md b/docs/ affiliate_dash_发货平台/affiliate-dash对接.md index 756c297e..89564dde 100644 --- a/docs/ affiliate_dash_发货平台/affiliate-dash对接.md +++ b/docs/ affiliate_dash_发货平台/affiliate-dash对接.md @@ -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 报错)。 diff --git a/docs/91卡券/3.异步卡密下单.md b/docs/91卡券/3.异步卡密下单.md index f1fbb30e..882876d1 100644 --- a/docs/91卡券/3.异步卡密下单.md +++ b/docs/91卡券/3.异步卡密下单.md @@ -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}`; diff --git a/docs/91卡券/5.当前项目对接配置.md b/docs/91卡券/5.当前项目对接配置.md index 1abe1d65..10508901 100644 --- a/docs/91卡券/5.当前项目对接配置.md +++ b/docs/91卡券/5.当前项目对接配置.md @@ -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 履约链路的设计。 diff --git a/docs/91卡券/6.开发设计.md b/docs/91卡券/6.开发设计.md index 06057b9e..949b34af 100644 --- a/docs/91卡券/6.开发设计.md +++ b/docs/91卡券/6.开发设计.md @@ -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` 标记失败),前端暂无专属页面。 diff --git a/docs/cloudflared_tunnel.txt b/docs/cloudflared_tunnel.txt deleted file mode 100644 index 348134b4..00000000 --- a/docs/cloudflared_tunnel.txt +++ /dev/null @@ -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/.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/...`),请确保两台电脑的用户名一致;如果不一致,记得修改配置文件里的路径。 - ---- - -**总结:** -既然不同时运行,那就把第二台电脑伪装成第一台电脑的“克隆体”。只需拷贝那几个关键文件,你就能实现在家、在公司用同一个域名访问不同的机器了。 \ No newline at end of file diff --git a/docs/finance-implementation-plan.md b/docs/finance-implementation-plan.md deleted file mode 100644 index cf9a4fbf..00000000 --- a/docs/finance-implementation-plan.md +++ /dev/null @@ -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` diff --git a/docs/pgBackRest备份方案分析.md b/docs/pgBackRest备份方案分析.md deleted file mode 100644 index 16049f1c..00000000 --- a/docs/pgBackRest备份方案分析.md +++ /dev/null @@ -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 到指定时间成功。 -- 恢复后的订单、后台用户、迁移记录和图片引用可用。 - diff --git a/docs/寄卖库存功能设计与实施方案.md b/docs/寄卖库存功能设计与实施方案.md deleted file mode 100644 index 511b728a..00000000 --- a/docs/寄卖库存功能设计与实施方案.md +++ /dev/null @@ -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 页面已删除。 diff --git a/docs/寄卖系统优化方案.md b/docs/寄卖系统优化方案.md index b0697468..b7cfb807 100644 --- a/docs/寄卖系统优化方案.md +++ b/docs/寄卖系统优化方案.md @@ -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) diff --git a/docs/寄卖系统迁移079上库前检查.sql b/docs/寄卖系统迁移079上库前检查.sql index b165493a..0d2d1028 100644 --- a/docs/寄卖系统迁移079上库前检查.sql +++ b/docs/寄卖系统迁移079上库前检查.sql @@ -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 $$ diff --git a/docs/履约配置/TypeScript类型收紧.md b/docs/履约配置/TypeScript类型收紧.md index bf1e6c30..6469bafe 100644 --- a/docs/履约配置/TypeScript类型收紧.md +++ b/docs/履约配置/TypeScript类型收紧.md @@ -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` -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` 状态列表 diff --git a/docs/履约配置/UID领取与旧单兼容.md b/docs/履约配置/UID领取与旧单兼容.md index d484bc38..5a9afb43 100644 --- a/docs/履约配置/UID领取与旧单兼容.md +++ b/docs/履约配置/UID领取与旧单兼容.md @@ -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` 短路建单 / 绑定 / 提交 / 同步) ## 短链 diff --git a/docs/履约配置/cloudtentacles发货平台.md b/docs/履约配置/cloudtentacles发货平台.md index 32699ea6..bda9dfcb 100644 --- a/docs/履约配置/cloudtentacles发货平台.md +++ b/docs/履约配置/cloudtentacles发货平台.md @@ -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列表 ``` { diff --git a/docs/履约配置/open91请求.md b/docs/履约配置/open91请求.md index 0cadb837..a399751a 100644 --- a/docs/履约配置/open91请求.md +++ b/docs/履约配置/open91请求.md @@ -22,6 +22,6 @@ `测试-关联商品1----3676797936` -系统会把 `测试-关联商品1` 当作商品名字匹配 cloudtentacles,把 `3676797936` 当作快手小店 shopId 用于核销。 +系统会把 `测试-关联商品1` 当作商品名字,走 affiliate-dash 的匹配链路(skuMapping 指定匹配 → 商品路由规则 → 全局优先级,默认仅 `affiliate_dash`,未命中落 `manual_dispatch`);把 `3676797936` 当作快手小店 shopId 用于核销。 这是最重要的信息 diff --git a/docs/履约配置/多发货平台与电子凭证整体链路.md b/docs/履约配置/多发货平台与电子凭证整体链路.md index 291ce73f..c07f6feb 100644 --- a/docs/履约配置/多发货平台与电子凭证整体链路.md +++ b/docs/履约配置/多发货平台与电子凭证整体链路.md @@ -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 diff --git a/docs/履约配置/工程债-lewan拆分与短链.md b/docs/履约配置/工程债-lewan拆分与短链.md deleted file mode 100644 index f64b003b..00000000 --- a/docs/履约配置/工程债-lewan拆分与短链.md +++ /dev/null @@ -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`。 diff --git a/docs/履约配置/快手Cloud领取mock.md b/docs/履约配置/快手Cloud领取mock.md deleted file mode 100644 index f65e4c36..00000000 --- a/docs/履约配置/快手Cloud领取mock.md +++ /dev/null @@ -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 diff --git a/docs/服务器图片迁移到阿里云OSS.md b/docs/服务器图片迁移到阿里云OSS.md deleted file mode 100644 index 97c47981..00000000 --- a/docs/服务器图片迁移到阿里云OSS.md +++ /dev/null @@ -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` 卷释放磁盘。 diff --git a/docs/行业电子凭证/技术方案.md b/docs/行业电子凭证/技术方案.md index 756778c3..4acc3876 100644 --- a/docs/行业电子凭证/技术方案.md +++ b/docs/行业电子凭证/技术方案.md @@ -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 仓库,仅通过环境变量注入 diff --git a/docs/行业电子凭证/解决方案/技术方案/1.基础技术.md b/docs/行业电子凭证/解决方案/技术方案/1.基础技术.md deleted file mode 100644 index e229065b..00000000 --- a/docs/行业电子凭证/解决方案/技术方案/1.基础技术.md +++ /dev/null @@ -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¶m=%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×tamp=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¶m={"categoryId":12,"relItemId":123456,"title":"短袖"}&signMethod=MD5×tamp=1583271919000&version=1&signSecret=abc - - - -保留=符号,用&符号将多个参数及其值组装在一起,根据上面的示例得到的排序结果为:access_token=xxx&appkey=ks123&method=open.xxx.xxx¶m={"title":"短袖", "relItemId":123456, "categoryId":12}&signMethod=MD5×tamp=1583271919000&version=1 - -排序好参数后,在末尾加入signSecret(在应用创建审核通过后由平台分配,在“应用中心-应用列表-应用详情”中可见)进行对应算法的签名计算 - -MD5(access_token=xxx&appkey=ks123&method=open.xxx.xxx.xxx¶m={"title":"短袖", "relItemId":123456, "categoryId":12}&signMethod=MD5×tamp=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¶m=%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×tamp=1583271919000&sign=af2d80958e77e17f1d973003b7b7aec2 -说明:param是json对象,需要排序 - -4.API调用 -确认完成了授权流程后进行API的调用测试,内容详情可见《API调用说明》,也可使用平台提供的API测试工具测试 - -![alt text](image.png) \ No newline at end of file diff --git a/docs/行业电子凭证/解决方案/技术方案/image.png b/docs/行业电子凭证/解决方案/技术方案/image.png deleted file mode 100644 index 1d90660c..00000000 Binary files a/docs/行业电子凭证/解决方案/技术方案/image.png and /dev/null differ diff --git a/docs/部署启动与数据库重建.md b/docs/部署启动与数据库重建.md index 843e16d6..2313c9d8 100644 --- a/docs/部署启动与数据库重建.md +++ b/docs/部署启动与数据库重建.md @@ -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