OKOKTA 商城 · 后端交接契约 v1.3.0 · 2026-10-01 · 北京时间

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消费方
会员中枢 Hubhttps://{host}/api/v1(开发 http://localhost:4000/api/v1)买家端、运营后台、小程序
商城 Store APIhttps://{host}/store/v1(开发 http://localhost:9000/store/v1)买家端、小程序
商城 Admin APIhttps://{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错误码场景
401AUTH_TOKEN_EXPIREDJWT 过期/无效
404MEMBER_NOT_FOUND / ORDER_NOT_FOUND / CHAPTER_NOT_FOUND资源不存在
422MEMBER_TYPE_SCHEMA_INVALID注册动态字段未过 Ajv 校验(details 返回字段级错误)
409STOCK_INSUFFICIENT / ORDER_STATE_INVALID / DUPLICATE_REQUEST库存不足 / 状态机非法迁移 / 幂等冲突
412SETTLEMENT_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_typesHubtype_code—五种类型模板(IND/STU/PRO/ENT/HON),version 管理
membersHubmember_id (uuid)member_no会员档案,含 JSONB type_snapshot
chaptersHubchapter_idcode (BJ/SH/…)组织树 + 冗余 member_count(异步刷新)
seller_profilesHub(已实现)id (bigint→string)—店铺档案,member_id 唯一,audit_status 状态机,medusa_vendor_id 对接 Commerce(审核通过自动回填,§9.5)
audit_tasksHub(已实现)id (bigint→string)—审核任务:卖家资质 / 商品 / 内容三类目标,status 六状态机 + reviewer 留痕(§9)
productsCommerceproduct_idsku商品,审核状态机
carts / cart_itemsCommercecart_id—游客可匿名,登录后合并
orders / order_itemsCommerceorder_idorder_no订单,行级价格快照
settlement_rulesCommercerule_id—全局/支会/卖家三级作用域,当前全局 90/6/4
settlements / settlement_splitsCommercesettlement_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_nostring必编号规则 OKTA-{TYPE}-{REGION}-{YEAR}-{SEQ:5};REGION 取支会 code(BJ/SH/HZ/SZ/CD/JP/KR/SG/NA);SEQ 按「类型+地区+年」组合序列;与入会同一事务生成,不重用不回滚。示例 OKTA-ENT-BJ-2026-00001
user_idstring?选SSO 锚点;接 Keycloak 后按 subject 回填
type_snapshotjsonb必含 discount_rate;商城定价只读快照折扣,不读模板现值
chapter_idstring必主属挂靠;结算时支会份额路由到此支会(见 §8.3)
statusenum必expired 会员下单按游客零售价,前端按接口返回展示

member_types 示例值(与原型 data.js 一致)

type_codeversion年费(CNY)discount_rate附加校验
IND 个人会员3300.98schema: display_name + mobile(正则) + email?
STU 学生会员3150.95requires_verification=true;schema 增加 student_no
PRO 专业会员2880.92—
ENT 企业会员38800.90schema: 统一社会信用代码(18 位正则) + 联系人手机;唯一可申请卖家入驻的类型
HON 荣誉会员10(终身)0.88invitation_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_auditSeller
POST/admin/v1/products商品创建(后台)卖家提交 → pending_audit;审核通过 → activeProduct

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 JWTCart
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 → 登记结算 pending202
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→ 2reason 必填 ≤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/sellersorg-admin / platform-admin卖家名册(后台)?page&page_size(≤100)&audit_status(0..3)分页 { items, total, page, pageSize }(items 含会员与类型)
GET/api/v1/sellers/:idorg-admin / platform-admin卖家详情(后台):id 为字符串主键{ ...seller, member, auditTasks }(完整审核历史 + 主属支会)
HTTP场景(apply / me)语义
400store_name_i18n.zh 必填 / DTO 校验失败参数不合法(ValidationPipe whitelist)
401me 无令牌(AUTH_MODE=off 明确 401)请先经 Keycloak 登录
403userId 与登录身份不一致且无代录角色;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 环境可完整演练闭环
liveMEDUSA_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-syncorg-admin / platform-admin同步 / 补建 vendor(审核通过自动触发;live 失败重试入口)—(幂等,已回填返回 already-synced){ ok:true, vendorId, source:'local'|'medusa', skipped? }
POST/api/v1/sellers/:id/vendor-bindorg-admin / platform-admin手动绑定真实 vendor id(覆盖演示回填 / 补录){ vendorId(1–64 字,拒绝 vendor_local_ 前缀) }更新后 seller(medusaVendorId 已回填)
HTTP场景(vendor-sync / vendor-bind)语义
400vendor id 为空 / 超长 / 演示前缀(bind)与 validateBindVendorId 纯函数同构,前端同规则前置校验
404店铺档案不存在:id 为字符串主键
409档案非「已通过」(待审 / 已驳回 / 已冻结)vendor 操作仅对已通过店铺开放(状态机守卫)
503live 模式 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 环境可完整演练
liveMEDUSA_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-syncorg-admin / platform-admin同步 / 补建 customer(注册自动触发;live 失败重试入口,存量会员补建)—(幂等,已回填返回 already-synced){ ok:true, customerId, groupId?, source:'local'|'medusa', skipped? }
POST/api/v1/members/:id/customer-bindorg-admin / platform-admin手动绑定真实 customer id(覆盖演示回填 / 补录){ customerId(1–64 字,拒绝 customer_local_ 前缀) }更新后 member(medusaCustomerId 已回填)
HTTP场景(customer-sync / customer-bind)语义
400customer id 为空 / 超长 / 演示前缀(bind)与 validateBindCustomerId 纯函数同构,前端同规则前置校验
404会员不存在:id 为 bigint 主键(字符串传递)
503live 模式 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/meuser_id 回填;member_no 展示
五种类型模板DB.memberTypesGET /api/v1/member-types详情页对照表 + 会员中心表
支会树DB.chaptersGET /api/v1/chapters一次全量,前端组树
商品列表/详情DB.productsGET /store/v1/products、GET /store/v1/products/:idJWT 身份 → 响应含 member_price_cny
会员价计算DB.priceFor / DB.unitPrice服务端 pricing 字段删除前端折扣计算,只读回显
加购/改量/移除Store.addItem / setQty / removeItem(localStorage)POST /carts/:id/items、PATCH …/:productId游客 cart_id 存 localStorage,登录合并
购物车金额Store.cartRows / totalsGET /carts/:id(附 pricing)运费阈值也来自服务端
提交订单Store.placeOrder(本地生成)POST /carts/:id/complete + Idempotency-Key跳转以响应 order_no 为准
支付Store.payOrder(本地翻状态)POST /orders/:no/pay → 回调置 paidP0 依赖微信分账资质
订单列表/详情DB.demoOrders + Store.ordersForGET /store/v1/orders、GET /store/v1/orders/:no按身份(JWT 或 member_no)
分账预览DB.splitRule / DB.split / Store.settlementPreviewGET /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.sellerProfileGET /api/v1/sellers/me含最近 5 条审核记录
审核队列 / 看板Store.auditQueue / DB.demoAuditTasksGET /api/v1/audit-tasks + /statsadmin.html 已接线;BigInt id 按字符串处理
人审裁决 / 冻结Store.decideAuditPOST /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对接注意事项与遗留项

#事项约定
1type_snapshot 不可变入会事务内冻结;模板升版、权益调整均不回写快照;商品定价只读快照 discount_rate。原型会员中心已展示「模板 v3 / 快照 v2」并存示例
2user_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+)

下一步建议

  1. 后端按 §4–§8 评审字段与状态机,48h 内回执差异清单(本文档 v1.3 迭代);§9 已按 member-hub 实际实现定稿,以代码为准
  2. 同步启动 微信支付分账资质 材料准备(P0,1–3 个月周期)
  3. Keycloak SSO 已打通买家端登录流(okta-web 客户端 + PKCE S256);管理端角色(org-admin / platform-admin)在 realm 中配置后即可联调 §9.4 工作台
  4. vendor 对接(§9.5)与 customer 归组同步(§9.6)local / live 双模式均已联调通过(live 实测:vendor 13 项 / customer 16 项全过,双端锚点一致);环境变量 MEDUSA_MODE / MEDUSA_URL / MEDUSA_ADMIN_TOKEN 见 .env.example,切换模式后经「卖家入驻」/「会员名册」页重试同步或手动绑定验证
  5. customer group 的价格挂载(分组价 price list 关联会员价)由 Commerce 侧 Phase 2 落地;中枢侧锚点与分组已就绪(§9.6)
  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.02026-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.02026-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.02026-09-30增补 §9 卖家入驻与人审工作台(对齐 member-hub 实际实现:SellerProfile / AuditTask 实体、双状态机、9 个接口与错误码);§3 数据模型、§6.1 落位说明、§10 映射表、§11 遗留项同步更新;接口点位 39 → 48
v1.0.02026-09-30初版:五模块实体 + 39 个接口点位 + Mock 映射表 + 对接注意事项;随 Web 原型 v1.0 交付