00文档信息与读者
| 版本 | v1.3.0(增补 §9.6 Medusa customer 归组同步:注册自动建 customer 按类型归组回填 medusa_customer_id;v1.2 §9.5 Medusa vendor 对接双模式;v1.1 §9 入驻与人审工作台;v1.0 初版) |
| 日期 | 2026-10-01(北京时间) |
| 范围 | OKTA 商城 Web 买家端(:3000)与运营后台(:3001)→ 会员中枢(NestJS :4000)+ 商城内核(Medusa :9000)的接口契约;同契约适用于后续 Taro 微信小程序端 |
| 读者 | 会员中枢后端、商城后端、结算/支付后端、前端(React/TS)、测试 |
| 前端原型 | 双击打开的静态原型 —— 原型中 assets/js/data.js 即本契约各实体的示例值库,字段一一对应 |
本文档定义五个核心业务域的数据接口:会员(member)、商品(product)、订单(order)、结算(settlement)、支会(chapter),并增补卖家入驻与审核流(seller / audit,§9,会员中枢已实现)与 Medusa 集成(vendor §9.5 / customer 归组 §9.6,双模式)。所有金额、折扣、分账一律由服务端计算;前端展示层只消费返回值。
01系统架构与职责边界
Web 买家端Next.js :3000
运营后台Next.js :3001
微信小程序Taro · Phase 3
▼ HTTPS · JSON(UTF-8) · Bearer JWT
API 网关Nginx · Phase 2 聚合
Phase 1 前端直连双服务(CORS 放行 3000/3001)
▼
会员中枢 HubNestJS :4000
注册 · 编号 · 类型模板 · 支会树 · 权益
商城内核 CommerceMedusa :9000
卖家 · 商品 · 购物车 · 订单 · 结算
▼
PostgreSQLHub 库:member_types / members / chapters · Commerce 库:sellers / products / carts / orders / settlement_*
外部依赖 ↘
Keycloak SSO:8080 · user_id 锚点
微信支付(分账)资质申请中 · P0 前置项
1.1职责边界(谁负责什么)
| 职责 | 归属 | 说明 |
| 会员注册核心链路 | Hub(单事务) | Ajv 动态字段校验 → 编号生成 → 类型快照冻结 → 主属挂靠(chapter)→ 权益发放;五步一个事务,失败整体回滚;事务提交后同步 Medusa customer 按类型归组(customer 对接已实现,见 §9.6) |
| 类型模板管理 | Hub | 模板 CRUD;schema 变更自动递增 version;历史快照不回写 |
| 支会树 | Hub | 总会 → 双区 → 九支会;商城侧只读 |
| 卖家入驻 / 审核流 | Hub(Phase 1 已实现,见 §9) | 店铺档案 + 审核任务 + 人审裁决全链路已落在会员中枢;商品上下架状态机仍归 Commerce,经 medusa_vendor_id 锚点对接(vendor 对接已实现,见 §9.5) |
| 购物车 / 订单 / 支付编排 | Commerce | 会员折扣与运费服务端计算;支付回调驱动状态机 |
| 分账 | Commerce(结算子域) | 规则版本化;预览(preview)→ 待分账(pending)→ 已分账(settled) |
| 价格 / 折扣 / 分账金额计算 | 服务端(强制) | 前端传入的任何价格字段一律忽略,仅作展示回显 |
02通用技术约定
2.1基础地址
| 服务面 | Base URL | 消费方 |
| 会员中枢 Hub | https://{host}/api/v1(开发 http://localhost:4000/api/v1) | 买家端、运营后台、小程序 |
| 商城 Store API | https://{host}/store/v1(开发 http://localhost:9000/store/v1) | 买家端、小程序 |
| 商城 Admin API | https://{host}/admin/v1 | 运营后台(审核流、规则配置) |
2.2认证与身份
- 请求头
Authorization: Bearer <keycloak_jwt>;JWT subject = Keycloak user_id(uuid),即 members.user_id 预留锚点;SSO 接入(Phase 2 Keycloak)后回填,member_no 保持不变
- 游客购物车:不携带 JWT,服务端签发匿名
cart_id;登录/绑定会员后调 POST /carts/:id/attach 合并
- 运营后台走 Keycloak realm 角色:
org-admin / platform-admin(realm_access.roles 声明,会员中枢按此鉴权);会员中枢另有 AUTH_MODE=off|required 开关——off 放行不解析(Phase 1 演示默认),required 校验 Bearer 令牌 + 角色
2.3语言与国际化
- 请求头
Accept-Language: zh-CN | ja-JP | ko-KR | en-US
- 推荐:多语字段返回全量对象
{ "zh": …, "ja": …, "ko": …, "en": … }(字段名后缀 _i18n),前端本地选言(原型已按此实现,切换语言不发请求)
- 错误信息同理:
message_i18n 返回全量四语
2.4时间 / 货币 / 精度
- 时间:ISO 8601 带偏移,统一北京时间,如
2026-09-28T14:32:08+08:00;展示层一律北京时间
- 货币:Phase 1 全场景人民币 CNY;金额字段命名
*_cny,JSON number 两位小数;计算精度为"分"(round half-up),多币种为 Phase 2+
2.5分页 / 幂等
- 分页:
?page=1&page_size=20(max 100);响应外层 { items: [...], page, page_size, total }
- 幂等:下单 / 支付 / 退款请求必须携带
Idempotency-Key(uuid);服务端 24h 内去重,重复请求返回首次结果
2.6错误结构
{
"error": {
"code": "MEMBER_TYPE_SCHEMA_INVALID",
"message_i18n": { "zh": "动态字段校验失败:手机号格式不正确", "ja": "…", "ko": "…", "en": "…" },
"trace_id": "01J8X9K2M3Q7Z",
"details": [ { "field": "mobile", "rule": "pattern" } ]
}
}
| HTTP | 错误码 | 场景 |
| 401 | AUTH_TOKEN_EXPIRED | JWT 过期/无效 |
| 404 | MEMBER_NOT_FOUND / ORDER_NOT_FOUND / CHAPTER_NOT_FOUND | 资源不存在 |
| 422 | MEMBER_TYPE_SCHEMA_INVALID | 注册动态字段未过 Ajv 校验(details 返回字段级错误) |
| 409 | STOCK_INSUFFICIENT / ORDER_STATE_INVALID / DUPLICATE_REQUEST | 库存不足 / 状态机非法迁移 / 幂等冲突 |
| 412 | SETTLEMENT_RULE_NOT_FOUND | 订单完成但无生效分账规则(配置事故兜底) |
03数据模型总览
/* 关系总览(→ 一对多,⇢ 快照冻结) */
MemberType 1 ──✦── Member 入会时冻结 type_snapshot(模板升版不回写)
Chapter 1 ────── Member 会员归属支会(分账路由键)
Chapter self-ref parent_id 组织树:总会 → 双区 → 九支会
Member(ENT)1 ──0..1─ SellerProfile canSell 类型方可入驻(Hub 实现,§9);member_id 唯一
SellerProfile1 ────── AuditTask target_type=0 卖家资质(无外键,target_id 弱引用)
Member 1 ────── AuditTask reviewer_id 审核人留痕(可空)
Category 1 ──✦── Product 商品(Commerce 侧)
Member 1 ────── Order ──1── OrderItem OrderItem 冻结 title/price 快照
Order 1 ────── Settlement ── Split[] SplitRule 版本化,尾差由支会吸收
| 实体 / 表 | 归属服务 | 主键 | 对外编号 | 说明 |
member_types | Hub | type_code | — | 五种类型模板(IND/STU/PRO/ENT/HON),version 管理 |
members | Hub | member_id (uuid) | member_no | 会员档案,含 JSONB type_snapshot |
chapters | Hub | chapter_id | code (BJ/SH/…) | 组织树 + 冗余 member_count(异步刷新) |
seller_profiles | Hub(已实现) | id (bigint→string) | — | 店铺档案,member_id 唯一,audit_status 状态机,medusa_vendor_id 对接 Commerce(审核通过自动回填,§9.5) |
audit_tasks | Hub(已实现) | id (bigint→string) | — | 审核任务:卖家资质 / 商品 / 内容三类目标,status 六状态机 + reviewer 留痕(§9) |
products | Commerce | product_id | sku | 商品,审核状态机 |
carts / cart_items | Commerce | cart_id | — | 游客可匿名,登录后合并 |
orders / order_items | Commerce | order_id | order_no | 订单,行级价格快照 |
settlement_rules | Commerce | rule_id | — | 全局/支会/卖家三级作用域,当前全局 90/6/4 |
settlements / settlement_splits | Commerce | settlement_id | — | 一单一结算单,多收款方明细 |
04会员模块(Hub)
4.1实体定义
interface Member {
member_id: string; // uuid,内部主键
member_no: string; // OKTA-ENT-BJ-2026-00001,对外唯一,不变
user_id: string | null;// Keycloak subject(SSO 锚点,Phase 2 回填)
display_name: string;
email: string | null; phone: string | null;
type_code: 'IND'|'STU'|'PRO'|'ENT'|'HON';
type_version: number; // 快照对应的模板版本
type_snapshot: TypeSnapshot; // 入会事务内冻结,见 4.2
chapter_id: string; // 归属支会 → 分账路由键
status: 'active'|'pending'|'expired'|'frozen';
joined_at: string; valid_until: string; created_at: string; updated_at: string;
}
interface TypeSnapshot { // 只增不改;模板升版不回写
type_code: string; version: number; name: string;
discount_rate: number; benefits_count: number; captured_at: string;
}
interface MemberType {
type_code: string; version: number; // schema 变更自动递增
name_i18n: I18nText; annual_fee_cny: number; discount_rate: number;
requires_verification: boolean; invitation_only: boolean;
status: string;
benefits: I18nText[]; // 权益文案(四语)
dynamic_schema: object | null; // Ajv JSON Schema,注册时校验 dynamic_fields
}
members 字段表
| 字段 | 类型 | 必填 | 说明 / 示例 |
member_no | string | 必 | 编号规则 OKTA-{TYPE}-{REGION}-{YEAR}-{SEQ:5};REGION 取支会 code(BJ/SH/HZ/SZ/CD/JP/KR/SG/NA);SEQ 按「类型+地区+年」组合序列;与入会同一事务生成,不重用不回滚。示例 OKTA-ENT-BJ-2026-00001 |
user_id | string? | 选 | SSO 锚点;接 Keycloak 后按 subject 回填 |
type_snapshot | jsonb | 必 | 含 discount_rate;商城定价只读快照折扣,不读模板现值 |
chapter_id | string | 必 | 主属挂靠;结算时支会份额路由到此支会(见 §8.3) |
status | enum | 必 | expired 会员下单按游客零售价,前端按接口返回展示 |
member_types 示例值(与原型 data.js 一致)
| type_code | version | 年费(CNY) | discount_rate | 附加校验 |
| IND 个人会员 | 3 | 30 | 0.98 | schema: display_name + mobile(正则) + email? |
| STU 学生会员 | 3 | 15 | 0.95 | requires_verification=true;schema 增加 student_no |
| PRO 专业会员 | 2 | 88 | 0.92 | — |
| ENT 企业会员 | 3 | 880 | 0.90 | schema: 统一社会信用代码(18 位正则) + 联系人手机;唯一可申请卖家入驻的类型 |
| HON 荣誉会员 | 1 | 0(终身) | 0.88 | invitation_only=true(邀请制) |
4.2接口清单
| 方法 | 路径 | 说明 | 请求要点 | 响应 |
POST | /api/v1/members | 注册(核心链路) | { type_code, chapter_id, dynamic_fields, benefits_opt_in? };dynamic_fields 先过模板 dynamic_schema Ajv 校验 | 201 Member;五步单事务:校验→编号→快照→挂靠→权益 |
GET | /api/v1/members/me | 当前登录会员档案 | Bearer JWT(user_id 反查) | Member;小程序/Web 登录后的首请求 |
GET | /api/v1/members/:memberNo | 按编号查询 | — | Member |
GET | /api/v1/members | 名册(运营后台) | ?chapter_id&status&type_code&q&page&page_size | 分页 Member[] |
PATCH | /api/v1/members/:memberNo | 资料修改 | 可改字段白名单(不含 type/chapter) | Member |
POST | /api/v1/members/:memberNo/renew | 续费 | 沿用原快照;延 valid_until 一年 | Member;到期前 30 天开放 |
POST | /api/v1/members/:memberNo/change-type | 类型变更/升级 | { type_code } | 生成新快照(新 version + captured_at);member_no 不变 |
GET | /api/v1/member-types | 类型模板列表(生效版) | — | MemberType[](含 dynamic_schema) |
GET | /api/v1/member-types/:typeCode | 模板详情 | ?version 可取历史版 | MemberType;运营后台审核对照用 |
编号唯一性:member_no 上建唯一索引;SEQ 序列表(编号序列表,Phase 1 已建)以「type+region+year」为键行锁自增,事务内取号,避免并发重号。
05支会模块(Hub)
5.1实体定义
interface Chapter {
chapter_id: string; // ch-bj / ch-tokyo …
code: string; // BJ / SH / HZ / SZ / CD / JP / KR / SG / NA / CN-R / INTL-R / HQ
parent_id: string | null; // 自引用组织树
region: 'hq'|'domestic-region'|'overseas-region'|'domestic'|'overseas';
name_i18n: I18nText;
country_code: string; timezone: string; // 支会活动时间展示用
member_count: number; // 冗余计数,定时任务刷新
status: string; sort_order: number;
}
- 组织树固定三层:OKTA 总会(HQ)→ 国内区 / 海外区 → 九个支会(国内 5:北京/上海/杭州/深圳/成都;海外 4:东京/首尔/新加坡英语区/北美英语区)
- 删除约束:仍有会员挂靠的支会不可删除,必须先迁移会员;海外支会 timezone 用于活动时间换算展示
- 会员的
chapter_id 是分账路由键——买家归属支会收取 4% 支会基金(与卖家支会无关,见 §8.3)
5.2接口清单
| 方法 | 路径 | 说明 | 响应 |
GET | /api/v1/chapters | 全量树(一次返回,含层级) | Chapter[](前端自行组树,原型 chapters.html 已实现) |
GET | /api/v1/chapters/:chapterId | 支会详情 | Chapter + member_count |
POST | /api/v1/chapters(admin) | 新增支会节点(运营后台) | Phase 2:支会主页 / 活动日历另立契约 |
06商品模块(Commerce)
6.1卖家 Seller(入驻审核制)
interface Seller {
seller_id: string;
member_no: string | null; // 企业会员编号(ENT);官方直营 = null
kind: 'official'|'member';
shop_name_i18n: I18nText;
chapter_id: string; // 店铺归属支会(与买家支会无关)
status: 'pending_audit'|'active'|'suspended';
applied_at?: string; approved_at?: string;
}
- 入驻前置校验(双服务协作):Commerce 调 Hub 校验
member_no 存在且 type_code = 'ENT'、status=active
- 状态机:
pending_audit → active →(违规)suspended;suspended 店铺全部商品强制 offline
Phase 1 实现落位(重要):卖家入驻档案与审核流已由会员中枢 Hub 实现(seller_profiles + audit_tasks,见 §9)。本节的 Commerce 侧 Seller 即 Medusa vendor:Hub 审核通过后自动创建 vendor 并回填 seller_profiles.medusa_vendor_id(唯一键,varchar 64;双模式实现见 §9.5)——local 演示回填派生 id,live 真调 POST /admin/vendors;运营亦可手动绑定真实 id。两侧状态映射:audit_status=1(通过)→ active,3(冻结)→ suspended(vendor 禁用/恢复为 Phase 2 待办)。
6.2商品 Product 与类目
interface Product {
product_id: string; sku: string; // sku 对外唯一:OKTA-M-101 / YX-T-401 / AL-H-402 …
seller_id: string; category_id: string; // cat-merch/周边 · cat-course/课程 · cat-event/活动 · cat-curate/甄选
title_i18n: I18nText; summary_i18n: I18nText;
price_cny: number; // 零售价
member_pricing: 'type_discount'; // Phase 1:会员价 = price_cny × 会员 type_snapshot.discount_rate
stock: number; // 数字商品用哨兵值 999(不递减)
status: 'draft'|'pending_audit'|'active'|'offline';
created_at: string;
}
定价模型:Phase 1 采用「类型折扣」——同一商品对所有会员按各自类型折扣率计价(原型商品详情页的「会员价对照表」即此模型的 UI 直观呈现)。若 Phase 2 需要逐商品会员价,演进为 member_pricing: 'per_type' 并增加 member_prices 字段,接口签名不变。
6.3接口清单
| 方法 | 路径 | 说明 | 请求要点 | 响应 |
GET | /store/v1/products | 商品列表 | ?category_id&q&sort=price_asc|price_desc|default&page&page_size;携带 JWT 时响应内含 member_price_cny(按身份快照折扣计算) | 分页 Product[] |
GET | /store/v1/products/:id | 商品详情 | — | Product + seller 摘要 + 五类型对照价 prices_by_type[](详情页对照表) |
GET | /store/v1/categories | 类目列表 | — | Category[] |
POST | /admin/v1/sellers | 卖家入驻申请(后台) | member_no + 店铺资料;状态 pending_audit | Seller |
POST | /admin/v1/products | 商品创建(后台) | 卖家提交 → pending_audit;审核通过 → active | Product |
07订单模块(Commerce)
7.1购物车 Cart
interface Cart {
cart_id: string;
member_no: string | null; // 游客 = null;绑定后合并
items: { product_id: string; qty: number }[];
updated_at: string;
}
- 金额预计算:
GET /carts/:id 响应附带 pricing(每行 unit_price_cny 按会员快照折扣 + 汇总 amounts),前端不再自行计算折扣(原型 Store.totals 为本地替身)
- 加购/改量时服务端校验库存(数字商品哨兵 999 恒通过)
7.2订单 Order 与订单行
interface Order {
order_no: string; // OKTA-SO-YYYYMMDD-NNNNN,日序号
member_no: string | null; chapter_id: string | null; // 买家归属支会 → 分账路由
delivery: { name: string; phone: string; region: string; detail: string };
items: OrderItem[];
amounts: { goods_subtotal: number; // 折后商品金额 = 分账基数
member_discount_cny: number; shipping_cny: number; payable_cny: number };
status: 'pending'|'paid'|'shipping'|'completed'|'cancelled';
payment: { channel: 'wechat_pay'; state: 'unpaid'|'paid'|'refunded';
prepay_id?: string; paid_at?: string };
created_at: string;
}
interface OrderItem {
product_id: string; sku: string; qty: number;
unit_price_cny: number; line_total_cny: number;
title_snapshot_i18n: I18nText; // 下单时标题快照:改价/改名/下架不影响历史订单
}
- 状态机:
pending → paid → shipping → completed;pending → cancelled(用户取消或超时 2 小时未支付自动关闭);paid → refunded 走退款流程(Phase 2)
- 运费规则(参数化存储,禁止硬编码):满 ¥199 免运费(游客)/ 满 ¥99 免运费(会员),未满收 ¥12;运费不参与分账
- 下单即快照:unit_price、title、分账规则版本全部写入订单行/结算单,后续模板或规则调整不影响存量订单
7.3接口清单
| 方法 | 路径 | 说明 | 请求要点 | 响应 |
POST | /store/v1/carts | 创建购物车(游客) | — | Cart(cart_id 前端存 localStorage) |
POST | /store/v1/carts/:id/items | 加购 | { product_id, qty };服务端校验库存并按身份算单价 | Cart + pricing |
PATCH | /store/v1/carts/:id/items/:productId | 改数量 | { qty }(qty=0 即删除) | Cart + pricing |
POST | /store/v1/carts/:id/attach | 登录后合并游客购物车 | Bearer JWT | Cart |
POST | /store/v1/carts/:id/complete | 结算成单 | { delivery } + Idempotency-Key;服务端计算折扣/运费、生成结算预览、扣库存 | 201 Order(status=pending) |
POST | /store/v1/orders/:orderNo/pay | 微信预支付 | P0 依赖分账资质;返回小程序/JSAPI 调起参数 | { prepay_id, pay_params } |
POST | /store/v1/payments/wechat/notify | 支付回调(服务间) | 微信签名校验 → 置 paid → 登记结算 pending | 202 |
GET | /store/v1/orders | 订单列表 | ?member_no&page&page_size(或按 JWT 身份) | 分页 Order[] |
GET | /store/v1/orders/:orderNo | 订单详情(含 settlement) | — | Order + Settlement |
POST | /store/v1/orders/:orderNo/cancel | 取消(仅 pending) | 释放库存 | Order |
08结算模块(Commerce · 分账)
8.1分账规则 SplitRule(版本化)
interface SplitRule {
rule_id: string; // 当前生效:rule-global-v3
scope: 'global'|'chapter'|'seller'; // Phase 1 仅 global;预留支会/卖家级覆盖
seller_ratio: number; // 0.90
platform_ratio: number; // 0.06
chapter_ratio: number; // 0.04
version: number; effective_from: string; status: string;
// 约束:三 ratio 之和必须 = 1.00(服务端校验);生效中规则不可改,只能新建更高 version
}
8.2结算单 Settlement 与分账明细
interface Settlement {
settlement_id: string; order_no: string;
base_amount_cny: number; // = order.amounts.goods_subtotal(运费不分账)
rule_version: number; // 成单时冻结的规则版本
status: 'preview'|'pending'|'settled'|'partial_refunded';
splits: SettlementSplit[];
}
interface SettlementSplit {
payee_type: 'seller'|'platform'|'chapter';
payee_id: string; // seller_id | 'okta-platform' | chapter_id
ratio: number; amount_cny: number;
}
8.3计算规则(与原型 DB.split / settlementPreview 严格一致)
| # | 规则 | 说明 |
| 1 | 基数 = 折后商品金额 | 会员折扣后、不含运费:base = goods_subtotal |
| 2 | 卖家份额按订单行归属 | 多卖家订单:每行的 90% 归该行商品的 seller;跨行尾差由金额最大的卖家吸收(原型:按占比分摊,末位吸收) |
| 3 | 精度:分,round half-up | 两轮:先 seller/platform 各自四舍五入,尾差由支会(末位收款方)吸收,保证 Σsplits = base(示例:base 391.02 → 351.92 / 23.46 / 15.64) |
| 4 | 支会份额路由 | 买家归属支会(order.chapter_id)收取 4% 支会基金,与卖家支会无关——写入契约避免歧义 |
| 5 | 状态时机 | 成单 → preview;支付回调成功 → pending;微信分账可分账日(规则配置 T+1)→ settled |
8.4接口清单
| 方法 | 路径 | 说明 | 响应 |
GET | /store/v1/orders/:orderNo/settlement-preview | 分账预览(结算页/订单展开用) | Settlement(status=preview) |
GET | /store/v1/orders/:orderNo/settlement | 结算单(真实状态) | Settlement |
GET | /admin/v1/settlement/rules | 规则配置(运营后台) | SplitRule[] |
POST | /admin/v1/settlement/rules | 新建规则版本(只能加版本) | SplitRule |
微信支付分账 · P0 合规前置项
- 需要实体商户号 + 「分账」权限申请,周期 1–3 个月——材料准备现在启动,不阻塞开发但卡上线
- 微信侧默认分账上限 30%;本方案平台 6% + 支会 4% 合计 10%,在上限内,无需提升申请
- 分账接收方需预先登记:各卖家商户号、平台商户号、各支会收款主体(国内支会须为可签约主体)
- 资质未批期间:订单闭环照常(pending→paid),分账状态停在
pending,线下人工结算兜底
09卖家入驻与人审工作台(会员中枢 · Phase 1 已实现)
本章节与会员中枢实际代码一一对应(services/member-hub/src/seller / src/audit / src/vendor),前端原型 seller.html / admin.html 已按此接线。与 v1.0 初版「卖家归 Commerce」的设想相比,Phase 1 将入驻档案与审核流整体落位 Hub;v1.2 起 vendor 对接双模式亦已实现(§9.5):审核通过自动创建 Medusa vendor 并回填 medusa_vendor_id,闭环「通过 → 开店」。
命名与序列化:Hub 接口直接返回 Prisma 实体,JSON 字段为 camelCase(storeNameI18n / auditStatus),与本文档其余章节的 snake_case 设计稿不同;BigInt 主键经全局拦截器序列化为字符串("id": "7"),前端一律按字符串处理 ID。
9.1实体定义(seller_profiles / audit_tasks)
interface SellerProfile { // 表 seller_profiles(Hub 库)
id: string; // bigint → JSON 字符串
memberId: string; // member_id 唯一:一名会员仅一份店铺档案
storeNameI18n: { zh: string; ja?; ko?; en? }; // zh 必填非空(服务端校验)
credentials?: Record<string, unknown>; // 资质材料 JSON(营业执照 / 授权书等)
medusaVendorId: string | null; // Medusa vendor 唯一键(varchar 64;审核通过自动回填,§9.5)
rating: number; // decimal(3,2),默认 0
auditStatus: 0 | 1 | 2 | 3; // 0 待审 1 通过 2 驳回 3 冻结
createdAt: string; updatedAt: string;
}
interface AuditTask { // 表 audit_tasks(Hub 库)
id: string;
targetType: 0 | 1 | 2; // 0 卖家资质 1 商品 2 内容
targetId: string; // 目标主键(无外键,弱引用;卖家任务= seller_profile.id)
lang: 'zh'|'ja'|'ko'|'en'; // 审核语言上下文,默认 zh
machineFlags?: object; // 机审输出;Phase 1 = { machine:'skipped', note:'未接内容安全 API,直通人审' }
status: 0|1|2|3|4|5; // 0 草稿 1 机审中 2 人审队列 3 已通过 4 已驳回 5 已冻结
result: 1 | 2 | null; // 1 通过 2 驳回(未裁决为 null)
reason: string | null; // 驳回理由 ≤500 字
reviewerId: string | null; // 按 token sub 回查 member 留痕;查不到留空不阻塞
createdAt: string; updatedAt: string;
}
- 开店资格:以入会时
type_snapshot.canSell 为准(快照缺省回退类型模板 can_sell)——与会员价同规则,运营改模板不影响存量会员资格
- 事务不变量:档案 + 任务在同一事务内创建;裁决与档案联动也在同一事务内,失败整体回滚
- 索引:
audit_tasks(status, lang, target_type)(idx_audit_queue)支撑队列过滤;seller_profiles.member_id、medusa_vendor_id 唯一
9.2双状态机与迁移规则(audit-flow.ts 纯函数,前端同构复刻)
| 动作 | 前置状态 | 任务迁移 | 档案迁移 | 备注 |
POST /sellers/apply(首申) | 无档案 | 新建 status=2(Phase 1 机审直通) | 新建 0 | 档案 + 任务同事务 |
POST /sellers/apply(补件重提) | 档案 = 2(已驳回) | 新建 status=2(新任务号入队) | → 0 | 仅已驳回可重提(canResubmit);材料更新,首次申请时间保留 |
review approve | 任务 = 2 或 5 | → 3 / result=1 | → 1(开通店铺) | canReview:仅人审队列 / 已冻结可裁决;事务外自动同步 vendor 并回填(§9.5),live 失败不回滚裁决,vendorSync 随响应返回 |
review reject | 任务 = 2 或 5 | → 4 / result=2 | → 2 | reason 必填 ≤500 字,留痕给卖家补件 |
freeze | 任务 = 2 或 3 | → 5 | → 3(风控复查) | canFreeze:仅人审队列 / 已通过可冻结;冻结态经 review 恢复 |
- 终态锁定:任务 3 / 4 不可再裁决、不可冻结(409 状态机锁定)——裁决不可撤回,申诉走新任务
- 档案互斥:待审(0)重复申请 → 409;已通过(1)重复申请 → 409;已冻结(3)申请 → 403(联系运营)
- Phase 2 商品 / 内容送审接入后,
target_type=1/2 走 机审(1)→ 人审队列(2)同套状态机
9.3接口清单 · 卖家端(4 个)
| 方法 | 路径 | 权限 | 说明 | 请求要点 | 响应 |
POST | /api/v1/sellers/apply | 登录本人;org-admin / platform-admin 可代录 | 入驻申请 / 补件重提 | { userId(Keycloak sub,4–64 字), storeNameI18n(zh 必填), credentials?, lang? } | { seller, task }(同事务创建) |
GET | /api/v1/sellers/me | 登录(Bearer) | 我的店铺 | — | { seller, tasks }(最近 5 条审核记录) |
GET | /api/v1/sellers | org-admin / platform-admin | 卖家名册(后台) | ?page&page_size(≤100)&audit_status(0..3) | 分页 { items, total, page, pageSize }(items 含会员与类型) |
GET | /api/v1/sellers/:id | org-admin / platform-admin | 卖家详情(后台) | :id 为字符串主键 | { ...seller, member, auditTasks }(完整审核历史 + 主属支会) |
| HTTP | 场景(apply / me) | 语义 |
| 400 | store_name_i18n.zh 必填 / DTO 校验失败 | 参数不合法(ValidationPipe whitelist) |
| 401 | me 无令牌(AUTH_MODE=off 明确 401) | 请先经 Keycloak 登录 |
| 403 | userId 与登录身份不一致且无代录角色;can_sell=false;档案已冻结 | 无资格 / 无权限 / 风控限制 |
| 404 | 账户未入会;me 未入会 / 尚未申请开店 | 开店前置:先成为会员 |
| 409 | 已通过再申请;已有进行中审核;当前状态不支持重提 | 状态机互斥(canResubmit 仅已驳回) |
9.4接口清单 · 人审工作台(5 个,全部 org-admin / platform-admin)
| 方法 | 路径 | 说明 | 请求要点 | 响应 |
GET | /api/v1/audit-tasks/stats | 各状态计数(看板) | — | { "0": n, …, "5": n }(groupBy status) |
GET | /api/v1/audit-tasks | 审核队列 | ?page&page_size&status(0..5)&target_type(0..2)&lang;排序 status asc, createdAt asc | 分页 items;卖家任务每项附 storeName(target_id 无外键,服务端手动补 join) |
GET | /api/v1/audit-tasks/:id | 任务详情 | — | { ...task, seller }(卖家资质任务附店铺档案 + 会员类型 + reviewer) |
POST | /api/v1/audit-tasks/:id/review | 人审裁决 | { action: 'approve' | 'reject', reason?(≤500,reject 必填) } | 更新后 task + vendorSync(卖家资质 approve 时返回:成功 { ok:true, vendorId, source } / live 失败 { ok:false, error } / 非该场景 null,见 §9.5) |
POST | /api/v1/audit-tasks/:id/freeze | 风控冻结 | —(reviewer 按令牌留痕) | 更新后 task(status=5 + 档案=3) |
| HTTP | 场景(review / freeze) | 语义 |
| 400 | 驳回未填理由 | 驳回必须填写理由(≤500 字)——前端驳回面板同校验 |
| 404 | 任务不存在 | 内置演示样例等只读任务不可裁决 |
| 409 | 当前状态不可人审(仅 2/5)/ 不可冻结(仅 2/3) | 状态机锁定,终态不可改判 |
前端原型对应:seller.html(八态渲染:匿名 / 未入会 / 无资格 / 表单 / 待审 / 已通过 / 已驳回 / 已冻结)与 admin.html(六状态看板 + 队列过滤 + 详情裁决面板)均为真实 API + 离线演示双模式:接口不可达时回退本地状态机(Store.auditQueue / decideAudit,与 audit-flow.ts 同构),冒烟回归 60 项断言两侧一致。
9.5Medusa vendor 对接(双模式 · v1.2 已实现)
审核通过后由中枢事务外自动创建 Medusa vendor 并回填 seller_profiles.medusa_vendor_id,闭环「通过 → 开店」。实现位于 src/vendor(vendor-flow.ts 纯函数 + medusa.service.ts 适配器 + vendor.service.ts 编排),运营后台 admin-web 已提供重试与手动绑定操作。
| 运行模式 | 触发条件 | 行为 |
local(默认) | MEDUSA_MODE=local 或 live 配置缺项(缺 URL / token 自动降级) | 演示回退:不发起网络请求,以幂等键派生 vendor id(vendor_local_<member_no 小写>)直接回填,无 Medusa 环境可完整演练闭环 |
live | MEDUSA_MODE=live 且 MEDUSA_URL + MEDUSA_ADMIN_TOKEN 齐备 | 真调 Medusa v2 POST /admin/vendors(secret Admin API Key 按 Medusa 2.21 约定以 Authorization: Basic <sk_…> 传递,sk_ 前缀无需 base64 编码;8s 超时);Medusa 生成真实 vendor id 回填 |
- 幂等语义:
medusa_vendor_id 已回填时同步直接返回 skipped:'already-synced',不重复创建;演示 id 由 member_no 派生,天然幂等
- 失败不阻塞裁决:live 网络失败抛 503,但裁决已落库不回滚,错误经
review 响应的 vendorSync:{ ok:false, error } 返回;后台可重试同步或手动绑定真实 id
- 状态机守卫:仅档案
auditStatus=1(已通过)可同步 / 绑定;其余状态 409
- 来源判定(前端徽标同构):
vendor_local_ 前缀 → 演示回填;其余 → Medusa 真实。手动绑定校验拒绝演示前缀(由系统生成,运营须粘贴 Medusa 后台真实 id,≤64 字符)
- vendor 显示名:store_name_i18n zh → en →
OKTA Store <member_no> 逐级回退
| 方法 | 路径 | 权限 | 说明 | 请求要点 | 响应 |
POST | /api/v1/sellers/:id/vendor-sync | org-admin / platform-admin | 同步 / 补建 vendor(审核通过自动触发;live 失败重试入口) | —(幂等,已回填返回 already-synced) | { ok:true, vendorId, source:'local'|'medusa', skipped? } |
POST | /api/v1/sellers/:id/vendor-bind | org-admin / platform-admin | 手动绑定真实 vendor id(覆盖演示回填 / 补录) | { vendorId(1–64 字,拒绝 vendor_local_ 前缀) } | 更新后 seller(medusaVendorId 已回填) |
| HTTP | 场景(vendor-sync / vendor-bind) | 语义 |
| 400 | vendor id 为空 / 超长 / 演示前缀(bind) | 与 validateBindVendorId 纯函数同构,前端同规则前置校验 |
| 404 | 店铺档案不存在 | :id 为字符串主键 |
| 409 | 档案非「已通过」(待审 / 已驳回 / 已冻结) | vendor 操作仅对已通过店铺开放(状态机守卫) |
| 503 | live 模式 Medusa 不可达 / 响应异常 | message 含失败原因与重试指引;sync 端点可直接重试 |
运行模式观测:GET /api/v1/health 返回 { status, db, authMode, vendorMode }——运营后台「卖家入驻」页据 vendorMode 显示模式徽标(local 演示 / live 对接)。环境变量:MEDUSA_MODE / MEDUSA_URL / MEDUSA_ADMIN_TOKEN(见 .env.example)。
9.6Medusa customer 归组同步(双模式 · v1.3 已实现)
会员注册事务提交后,由中枢事务外自动创建 Medusa customer 并按会员类型归入 customer group(会员价定价分组依据),回填 member.medusa_customer_id,结果随注册响应的 customerSync 字段返回。实现位于 src/customer(customer-flow.ts 纯函数 + customer.service.ts 编排,复用 medusa.service.ts 同一双模式实例),运营后台「会员名册」页已提供重试同步与手动绑定操作。
| 运行模式 | 触发条件 | 行为 |
local(默认) | MEDUSA_MODE=local 或 live 配置缺项(缺 URL / token 自动降级) | 演示回退:不发起网络请求,派生 id 直接回填(customer:customer_local_<member_no 小写>;分组:cgroup_local_<typeCode 小写>),无 Medusa 环境可完整演练 |
live | MEDUSA_MODE=live 且 MEDUSA_URL + MEDUSA_ADMIN_TOKEN 齐备 | GET /admin/customer-groups 按 metadata.typeCode 匹配分组(无则 POST /admin/customer-groups 创建)→ POST /admin/customers 建 customer → POST /admin/customer-groups/:id/customers 归组(Admin API Key 同 §9.5 HTTP Basic 约定;8s 超时) |
- 幂等语义:
medusa_customer_id 已回填时同步直接返回 skipped:'already-synced',不重复创建;演示 id 由 member_no 派生,天然幂等
- email 解析(逐级回退):
extData.email → extData.contact_email → 登录令牌 email → <user_id>@member.okta.local 派生占位;公司名 extData.company_name 优先,回退 OKTA 会员 <member_no>
- 归组约定(双端锚点):分组 metadata
{ source:'okta-hub', typeCode },显示名 = 类型中文名(zh → en → 类型编码回退);同类型会员共享一个分组;local 模式归组无网络语义,跳过
- 失败不阻塞入会:live 网络失败抛 503,但注册已落库不回滚,错误经注册响应的
customerSync:{ ok:false, error } 返回;后台可重试同步或手动绑定真实 id
- 来源判定(前端徽标同构):
customer_local_ 前缀 → 演示回填;其余 → Medusa 真实。手动绑定校验拒绝演示前缀(由系统生成,运营须粘贴 Medusa 后台真实 customer id,≤64 字符)
| 方法 | 路径 | 权限 | 说明 | 请求要点 | 响应 |
POST | /api/v1/members/:id/customer-sync | org-admin / platform-admin | 同步 / 补建 customer(注册自动触发;live 失败重试入口,存量会员补建) | —(幂等,已回填返回 already-synced) | { ok:true, customerId, groupId?, source:'local'|'medusa', skipped? } |
POST | /api/v1/members/:id/customer-bind | org-admin / platform-admin | 手动绑定真实 customer id(覆盖演示回填 / 补录) | { customerId(1–64 字,拒绝 customer_local_ 前缀) } | 更新后 member(medusaCustomerId 已回填) |
| HTTP | 场景(customer-sync / customer-bind) | 语义 |
| 400 | customer id 为空 / 超长 / 演示前缀(bind) | 与 validateBindCustomerId 纯函数同构,前端同规则前置校验 |
| 404 | 会员不存在 | :id 为 bigint 主键(字符串传递) |
| 503 | live 模式 Medusa 不可达 / 响应异常 | message 含失败原因与重试指引;注册已生效,sync 端点可直接重试 |
数据落位:迁移 002_member_medusa_customer_id 新增 member.medusa_customer_id varchar(64) UNIQUE(幂等锚点);运行模式与 vendor 共用 /health.vendorMode(同一 MedusaService 实例),运营后台「会员名册」页据此显示 customer 模式徽标(与「卖家入驻」页共用同一环境变量组)。
10前端 Mock ↔ 真实接口映射(交接核心表)
原型的每个本地替身函数对应唯一真实接口;替换顺序建议自上而下(价格/折扣最先,支付最后)。
| 前端调用点 | 原型 Mock(文件) | 替换为真实接口 | 备注 |
| 会员身份档案 | Store.getIdentity / DB.members(data.js) | Keycloak 登录 → GET /api/v1/members/me | user_id 回填;member_no 展示 |
| 五种类型模板 | DB.memberTypes | GET /api/v1/member-types | 详情页对照表 + 会员中心表 |
| 支会树 | DB.chapters | GET /api/v1/chapters | 一次全量,前端组树 |
| 商品列表/详情 | DB.products | GET /store/v1/products、GET /store/v1/products/:id | JWT 身份 → 响应含 member_price_cny |
| 会员价计算 | DB.priceFor / DB.unitPrice | 服务端 pricing 字段 | 删除前端折扣计算,只读回显 |
| 加购/改量/移除 | Store.addItem / setQty / removeItem(localStorage) | POST /carts/:id/items、PATCH …/:productId | 游客 cart_id 存 localStorage,登录合并 |
| 购物车金额 | Store.cartRows / totals | GET /carts/:id(附 pricing) | 运费阈值也来自服务端 |
| 提交订单 | Store.placeOrder(本地生成) | POST /carts/:id/complete + Idempotency-Key | 跳转以响应 order_no 为准 |
| 支付 | Store.payOrder(本地翻状态) | POST /orders/:no/pay → 回调置 paid | P0 依赖微信分账资质 |
| 订单列表/详情 | DB.demoOrders + Store.ordersFor | GET /store/v1/orders、GET /store/v1/orders/:no | 按身份(JWT 或 member_no) |
| 分账预览 | DB.splitRule / DB.split / Store.settlementPreview | GET /orders/:no/settlement-preview | 结算页与订单展开共用 |
| 开店资格判定 | DB.canSell(快照 → 模板回退) | 服务端同规则校验(POST /api/v1/sellers/apply 前置,403 反馈) | 与会员价共用 type_snapshot,§9.1 |
| 入驻申请 / 重提 | Store.applySeller / sellerProfiles(store.js) | POST /api/v1/sellers/apply | 已真实接线,离线回退本地演示(seller.html) |
| 我的店铺 | Store.sellerProfile | GET /api/v1/sellers/me | 含最近 5 条审核记录 |
| 审核队列 / 看板 | Store.auditQueue / DB.demoAuditTasks | GET /api/v1/audit-tasks + /stats | admin.html 已接线;BigInt id 按字符串处理 |
| 人审裁决 / 冻结 | Store.decideAudit | POST /api/v1/audit-tasks/:id/review / /freeze | 状态机与 audit-flow.ts 同构(§9.2);admin-web 通过后展示 vendorSync 结果(§9.5) |
| vendor 同步 / 手动绑定 | admin-web VendorActions(vendor-actions.tsx) | POST /api/v1/sellers/:id/vendor-sync / /vendor-bind | 仅「已通过」店铺开放;绑定校验与 validateBindVendorId 同构;/health.vendorMode 驱动模式徽标(§9.5) |
| customer 同步 / 手动绑定 | admin-web CustomerActions(customer-actions.tsx,会员名册页) | POST /api/v1/members/:id/customer-sync / /customer-bind | 全体会员开放(补建入口);绑定校验与 validateBindCustomerId 同构;/health.vendorMode 驱动模式徽标(§9.6) |
| 四语切换 | I18N 词典(i18n.js) | 保持前端本地词典;服务端持续返回全量 _i18n 对象 | 切换语言不发请求 |
替换完成后,data.js 中的演示订单与演示会员仅保留为测试夹具;静态 QA 的 i18n key 校验脚本可继续用于四语回归。
11对接注意事项与遗留项
| # | 事项 | 约定 |
| 1 | type_snapshot 不可变 | 入会事务内冻结;模板升版、权益调整均不回写快照;商品定价只读快照 discount_rate。原型会员中心已展示「模板 v3 / 快照 v2」并存示例 |
| 2 | user_id 预留锚点 | SSO 接入后按 Keycloak subject 回填;期间所有接口按 member_no 寻址 |
| 3 | 安全红线 | 金额/折扣/分账全部服务端计算;前端传入价格一律忽略;Admin API 与 Store API 权限严格隔离 |
| 4 | 小程序复用 | 本契约同样适用于 Taro 端(同一 API 面);新增 wx.login code2session 与支付调起差异,另立增补契约 |
| 5 | 多币种 | Phase 2+:JPY/KRW/USD 汇率展示 + CNY 结算;Phase 1 全 CNY |
| 6 | 库存语义 | 实物递减;数字商品哨兵 999 不递减;活动门票按票量递减(预售期免超卖以支付成功为准) |
| 7 | 性能基线(Phase 1 量级) | 商品列表 P95 < 200ms;成单 P95 < 400ms;支会树 < 100ms(可缓存 5 分钟) |
| 8 | 可观测性 | error.trace_id 全链路透传;注册五步链路与分账登记必须有结构化日志与告警 |
| 9 | 遗留项 | ① 支会主页/活动日历(Phase 2)② 退款流程与 partial_refunded(Phase 2)③ 支会/卖家级分账规则覆盖(Phase 2,scope 字段已预留)④ 商品/内容送审(Phase 2:接内容安全 API,target_type=1/2 走 机审 1 → 人审 2,状态机已就绪)⑤ Medusa 内核集成(vendor §9.5 与 customer 归组 §9.6 均已实现:双模式 + 自动回填 + 重试/绑定;price list 挂分组定价 Phase 2)⑥ 抖音/支付宝小程序壳(Phase 3+) |
下一步建议
- 后端按 §4–§8 评审字段与状态机,48h 内回执差异清单(本文档 v1.3 迭代);§9 已按 member-hub 实际实现定稿,以代码为准
- 同步启动 微信支付分账资质 材料准备(P0,1–3 个月周期)
- Keycloak SSO 已打通买家端登录流(okta-web 客户端 + PKCE S256);管理端角色(org-admin / platform-admin)在 realm 中配置后即可联调 §9.4 工作台
- vendor 对接(§9.5)与 customer 归组同步(§9.6)local / live 双模式均已联调通过(live 实测:vendor 13 项 / customer 16 项全过,双端锚点一致);环境变量
MEDUSA_MODE / MEDUSA_URL / MEDUSA_ADMIN_TOKEN 见 .env.example,切换模式后经「卖家入驻」/「会员名册」页重试同步或手动绑定验证
- customer group 的价格挂载(分组价 price list 关联会员价)由 Commerce 侧 Phase 2 落地;中枢侧锚点与分组已就绪(§9.6)
- 前端按 §10 映射表逐项替换 Mock,先价格与购物车,后订单与支付;卖家入驻与人审工作台已双模式接线,只需删离线回退分支
A附录 · 演示数据与变更记录
A.1演示数据说明(assets/js/data.js)
- 会员 2 名(ENT 星阑科技 / IND 林晓)+ 类型 5 种(四语模板);11 件商品(4 类目、3 店铺);支会树 12 节点(总会 + 双区 + 9 支会);演示订单 3 笔(shipping / completed / pending 各一)
- 演示订单的金额与分账数字均按 §8.3 规则计算,可直接作为单元测试断言用例(如 base 391.02 → 351.92 / 23.46 / 15.64)
- 原型身份切换器(游客 / IND / ENT)用于验收「折扣随身份变化」的定价链路
- 演示审核任务:内置 4 条只读样例(覆盖已通过 / 已驳回 / 已冻结等终态)+ 本机申请镜像任务(可裁决),与
seller.html 八态渲染、admin.html 六状态看板联动,可直接作为审核流验收用例
A.2变更记录
| 版本 | 日期 | 说明 |
v1.3.0 | 2026-10-01 | 增补 §9.6 Medusa customer 归组同步(local/live 双模式、注册自动建 customer 按类型归组回填 medusa_customer_id + customerSync 响应、重试/手动绑定 2 个管理端点、迁移 002 medusa_customer_id、错误码 400/404/503);§0 版本、§1 职责表、§9.5 鉴权描述修正(Admin API Key 统一 HTTP Basic)、§10 映射表、§11 遗留项⑤与下一步建议同步更新;接口点位 50 → 52 |
v1.2.0 | 2026-10-01 | 增补 §9.5 Medusa vendor 对接(local/live 双模式、审核通过自动回填 + vendorSync 响应、重试/手动绑定 2 个管理端点、错误码 400/404/409/503、/health.vendorMode 观测);§0 版本、§1 职责表、§9 引言/实体注释/状态机/裁决响应、§10 映射表、§11 遗留项⑤与下一步建议同步更新;接口点位 48 → 50 |
v1.1.0 | 2026-09-30 | 增补 §9 卖家入驻与人审工作台(对齐 member-hub 实际实现:SellerProfile / AuditTask 实体、双状态机、9 个接口与错误码);§3 数据模型、§6.1 落位说明、§10 映射表、§11 遗留项同步更新;接口点位 39 → 48 |
v1.0.0 | 2026-09-30 | 初版:五模块实体 + 39 个接口点位 + Mock 映射表 + 对接注意事项;随 Web 原型 v1.0 交付 |