# 星火跃迁：连锁总部入驻后端契约（首店可选）

版本：2026-10-11。本文描述星火跃迁连锁申请、资料验证、账号开通和门店授权接口。

## 已实现的业务边界

总部可独立提交，也可携带首店。联系人及法人验证手机、上传私密材料、查询、补件与运营审核机制保持一致。`chainInfo.includeFirstStore=false` 仅创建总部账号和连锁主体；`true` 同时核验并开通首店。省略此字段的历史申请按 `true` 处理。`chainInfo` 可省略，既有单店申请保持兼容。

申请中的 `storeCount` 是计划门店数量，不会自动开通相应数量的门店，也不会授予其他商家门店的管理权。首次申请总部企业与营业执照主体必须一致。后续跨主体加盟店应另行取得授权，不能通过公开申请提交 `existingSubjectId` 接管其他主体。

连锁是经营组织形态，业务域仍为 `waimai`、`supermarket`、`ecommerce`、`hotel` 或 `homestay`。批准时创建 `merchant_subjects.subject_type = chain`，其 `config.onboardingType = large_chain`。食品经营、品牌经营、酒店和民宿各自资质要求继续生效；民宿仍须先创建准备主体、完成私密资质审核后显式激活。

## 公共响应和路径

以下路径均包含 `/api`。常规响应为 `{ "success": true, "message": "success", "data": ... }`；失败为 `{ "success": false, "message": "具体错误", "data": null }`，同时使用相应 HTTP 状态码。

| 方法与路径 | 请求与凭证 | 成功结果 |
|---|---|---|
| `POST /api/merchant/onboarding/phone-verifications` | JSON `{phone}` | 202；`verificationId`, `expiresAt`, `resendAfterSeconds` |
| `POST /api/merchant/onboarding/phone-verifications/{verificationId}/verify` | JSON `{phone, code}`；6 位验证码 | `verificationId`, `verificationToken`, `phoneMasked`, `expiresAt` |
| `POST /api/merchant/onboarding/assets/{asset_kind}` | multipart 单文件；下述上传凭证 | 201；`assetId`, `assetPath`, `assetKind`, `contentType`, `sizeBytes`, `widthPixels`, `heightPixels` |
| `GET /api/merchant/onboarding/assets/{assetId}` | 下述上传凭证；不能直接公开访问 | 私密图片字节；禁止缓存 |
| `POST /api/merchant/onboarding` | 下述完整表单和手机验证凭证 | `applicationId`, `applicationToken`（仅首次创建返回）, `status`, `step`, `submittedAt`, `message` |
| `GET /api/merchant/onboarding/progress?id={applicationId}` | `x-onboarding-token` 请求头 | `applicationId`, `status`, `step`, `submittedAt`, `updatedAt`, 脱敏 `summary`, `review.rejectReason`, `review.supplementReason`, `accountSetupRequired` |
| `GET /api/merchant/onboarding/application?id={applicationId}` | `x-onboarding-token` 请求头 | 原申请 `shopInfo`, `credentials`, `legalInfo`, 可选垂类资料和 `chainInfo`，供补件回填 |
| `POST /api/merchant/onboarding/account-password` | JSON `{applicationId, newPassword}`；申请 token 和新联系人手机验证凭证 | `accountReady: true`；随后用联系人手机号和所设密码登录 |

验证码有效期为 10 分钟；正常发送冷却为 60 秒。验证成功的手机凭证有效期为 24 小时，提交时原子消费。新申请需要联系人和法人手机分别完成验证；两者同号时可共用同一凭证。重提未改变的手机号无需再次验证，改号必须提交新号码凭证。任何材料上传失败均不能用任意公网图片 URL 替代。

### 请求头

首次提交：

```text
x-onboarding-contact-verification-id: <联系人 verificationId>
x-onboarding-contact-verification-token: <联系人 verificationToken>
x-onboarding-legal-verification-id: <法人 verificationId>
x-onboarding-legal-verification-token: <法人 verificationToken>
```

首次上传使用已验证手机的 `x-onboarding-verification-id` 与 `x-onboarding-verification-token`。

补件上传和读取材料使用 `x-onboarding-application-id` 与 **`x-onboarding-application-token`**。重提申请使用 `x-onboarding-application-id` 与 **`x-onboarding-token`**。这两组名称目前不同，客户端必须按接口区分。

申请编号和 token 应作为可下载、用户自行保管的进度凭证。token 不应放入 URL、分析事件或公开页面。后端仅保存 token 摘要，不会在查询进度时重新返回明文 token。

### 新账号首次设置密码

进度 `accountSetupRequired = true` 时，申请人重新验证联系人手机；向 `account-password` 发送 `x-onboarding-token`、`x-onboarding-contact-verification-id` 和 `x-onboarding-contact-verification-token`。提交申请时使用过的手机凭证已消费，不能再次使用。

密码复用系统统一策略：至少 15 个字符，UTF-8 最多 72 字节，无控制字符，且不在常见/泄漏口令库中；不人为增加必须混合大小写等额外规则。接口不返回初始明文密码或 JWT。成功后通过 `POST /api/merchant-admin/login` 的 `{phone, password}` 登录。

仅 `approved` 或 `provisional`、本申请新建账号、尚未设置密码的申请可以使用。服务端重新核对申请 ID 与 bearer、新手机号 proof、精确 user/merchant owner 关系、账号和门店状态；新账号必须仍处于 `must_change_password` 状态。在一个事务中消费手机证明、更新密码摘要、递增 `credential_version`、清除首设标记并记录时间。原有商家账号不会被此接口重置，失败不会消费凭证。

原 `PUT /api/employee/change-password` 的 `{oldPassword, newPassword}` 继续使用。现增加明确有效的商家 owner 资格，owner 不必伪造员工档案；读取及最终写入均要求有效账号、成员/员工和门店状态，仍验证旧密码、统一新密码策略、旧摘要并发比较，并递增凭据版本。临时改密 token 仍只能访问原限定改密路由。

## 提交表单

根对象和嵌套对象使用严格 camelCase，未知字段拒绝；请求上限 64 KiB。基础结构：

```json
{
  "shopInfo": {
    "businessType": "waimai",
    "shopName": "星火首店",
    "contactName": "申请联系人",
    "contactPhone": "13900000001",
    "mainCategory": "waimai",
    "subCategories": ["快餐"],
    "categoryText": "快餐",
    "storeType": "chain_store",
    "address": "首店详细地址",
    "storeFrontImage": "<storefront_photo 上传返回的 assetPath>"
  },
  "credentials": {
    "province": "上海市", "city": "上海市", "district": "浦东新区",
    "address": "首店详细地址", "creditCode": "91310000MA1K000001",
    "businessLicense": "<business_license assetPath>",
    "foodLicense": "<food_business_license assetPath>",
    "extraLicenses": []
  },
  "legalInfo": {
    "name": "法定代表人", "phone": "13900000001",
    "idFront": "<legal_id_front assetPath>", "idBack": "<legal_id_back assetPath>"
  },
  "chainInfo": {
    "companyName": "星火连锁有限公司", "creditCode": "91310000MA1K000001",
    "brandName": "星火", "storeCount": 200,
    "operationMode": "mixed", "developmentMode": "self_developed",
    "technicalContactName": "技术负责人", "technicalContactPhone": "13900000001",
    "technicalContactEmail": "tech@example.com",
    "brandProof": "<extra_license assetPath A>",
    "applicantAuthorization": "<extra_license assetPath B>"
  }
}
```

上例是字段模板，图片占位符必须替换为实际私密上传结果；示例信用代码仅用于格式示范，企业真实性及材料内容由审核核验。

### 连锁专属规则

| 字段 | 规则 |
|---|---|
| `companyName` | 必填，最多 255 字，无控制字符 |
| `creditCode` | 18 位统一社会信用代码字符格式；与 `credentials.creditCode` 一致；不代表已完成外部工商核验 |
| `brandName` | 必填，最多 120 字 |
| `storeCount` | 整数，2–100000；计划规模，非已开通门店数 |
| `operationMode` | `direct` / `franchise` / `mixed` |
| `developmentMode` | `self_developed` / `service_provider` |
| `serviceProviderName` / `serviceProviderCreditCode` | 委托研发必填，分别最多 255 字和 18 位信用代码；自研模式不能同时声明服务商 |
| `technicalContactName` | 必填，最多 50 字 |
| `technicalContactPhone` | 中国大陆手机号格式，最多 32 字；仅格式校验，不表示手机已验证 |
| `technicalContactEmail` | 邮箱格式，最多 254 字 |
| `brandProof` | 单独上传品牌权属或完整授权链证明图片，上传类别 `extra_license` |
| `applicantAuthorization` | 单独上传总部对申请人的授权证明图片，上传类别 `extra_license`；不能与品牌证明使用同一个材料 ID |

连锁材料不必再重复填入 `credentials.extraLicenses`。两份材料都进入现有私密附件清单，提交和批准时校验素材类别、手机证明或申请归属；不是仅验证 URL 字符串。

图片接受真实 JPEG、PNG、WebP，单文件最多 6 MiB、单边最多 12000 像素、总像素最多 4000 万。服务端解码重编码为 JPEG。每个申请/验证范围最多 48 张、合计 192 MiB；PDF 当前不能作为此接口附件上传。

## 垂类附加资料

- 外卖：经营品类及食品经营许可仍为必填。
- 电商：新版申请提交 `ecommerceQualification` 字符串字段对象，由 `ecommerce-qualification-schema.json` 定义主体、普通 / 一般贸易、店铺类型、主营类目、证件有效期和对应私密材料。商家端、官网、提交校验和批准校验使用同一规则；材料通过既有 `extra_license` 私密通道上传，仍严格验证类型、手机凭证及申请归属。主体为个人店时免营业执照及信用代码，连锁必须为企业主体；线上经营免门脸图。商标、各级授权、食品 / 图书 / 药品 / 旅行社许可、一般贸易材料按所选分支必填。新增材料不能直接使用公开图片地址。
- 包含电商门店时，`ecommerceInfo` 仍提交 `productSource`、`warehouseAddress`、`returnAddress`、`afterSalePhone`、`settlementAccount` 及兼容字段；新版品牌证据以 `ecommerceQualification` 为准。`qualityReport`、`warehouseProof`、`afterSaleCommitment`、`settlementProof` 在新版分类资质申请中按需补充；不含新版资质对象的历史申请保留原审核要求。
- 酒店：`hotelInfo.address`，产权分支 `propertyBasis = ownership` + `propertyOwnershipProof`，或租赁分支 `lease_authorization` + `leaseAuthorization` + `leaseExpiresOn`；另需 `localLodgingPermit`, `localLodgingPermitExpiresOn`, `fireSafetyMaterial`, `fireSafetyExpiresOn`, `publicSecurityRecord`, `publicSecurityExpiresOn`。卫生许可及有效期如提供须成对出现。期限必须有效。
- 民宿：提交基础资料和详细地址，不混用 `hotelInfo`；后续依据民宿专属模型和私密资质审核开通可售资格。

## 审批与主体管理

运营端 `GET /api/ops/cases`、`GET /api/ops/cases/merchant_onboarding:{id}` 读取申请。`POST /api/ops/cases/merchant_onboarding:{id}/action` 接受 `{action, reason}`；常规动作 `approve`、`reject`、`supplement`，民宿使用 `provision_homestay`、`activate_homestay`。处理原因必填。需有效客服/管理员身份、对应业务域及操作权限；必要的管理员二次认证仍生效，公开申请凭证不能审批。

私密材料审核入口是 `GET /api/ops/merchant-onboarding/{applicationId}/assets/{assetId}`，沿用业务域、角色和访问审计。

批准时重新校验本次申请范围和私密材料归属。仅总部申请核验企业、法人、品牌与授权证明，不创建 merchant、hotel 或结算策略；包含首店时才核验全部门店资质并创建首店及 owner 关系。只绑定本申请首店；同申请重入时核对主体来源，拒绝覆盖其他申请已创建的连锁主体。新建连锁主体保留独立业务域和生命周期；民宿准备主体不得因连锁类型跳过激活条件。

登录后的 `GET /api/subjects/current` 与 `GET /api/subjects/{id}/stores` 查看允许范围内主体及门店。`POST /api/subjects/{id}/stores` 目前是单店绑定，使用 snake_case `{merchant_id, settlement_mode?, operation_mode?, external_store_code?, online_store_code?}`，只接受调用者有权管理且未归属其他主体的门店。`settings:manage` 和 `chain:branch:manage` 权限不能由官网表单授予。当前已有主体的商家不能再通过 `POST /api/subjects` 重复建主体。

## 部署前置条件与当前限制

1. 部署包含本次 Rust 代码的后端，官网 `/api` 同源转发到该实例；跨域部署需允许以上自定义请求头，且仅允许受信任的官网来源。
2. PostgreSQL 已安装现有商家主体、入驻申请、手机号验证和私密材料迁移。本增量复用 JSONB 和 `extra_license`，没有新增数据库迁移。
3. 配置 `ONBOARDING_PHONE_CODE_HMAC_SECRET_FILE`、`ONBOARDING_PHONE_CODE_HMAC_KEY_ID`、`ONBOARDING_REQUEST_IP_HMAC_SECRET_FILE`、`ONBOARDING_REQUEST_IP_HMAC_KEY_ID`。密钥文件不可放入前端包。
4. 配置阿里云短信 `ALIYUN_SMS_CREDENTIALS_FILE`、`ALIYUN_SMS_SIGN_NAME`、`ALIYUN_SMS_TEMPLATE_CODE`；凭据文件只允许运行用户读取。生产不能依赖 development/test 的示例手机号验证码。
5. 提供持久化私密上传目录、应用写入权限和可信代理客户端 IP 配置。私密材料目录不能被静态服务器直接公开映射。
6. 运营人员具备对应业务域及审核权限，完成真实工商、品牌授权链、申请人授权和行业资质审查。后端字段验证不替代外部真实性核验。
7. 支持总部独立认证及可选首店开通；分店邀请登记和逐店确认见 [总部邀请与系统 API](chain-management.md)。集团批量自动开店、跨商业主体迁移和集团统一结算仍需独立完整审核链路。不可把声明门店数当作已完成批量开通。
8. 生产首设密码必须启用既有密码策略：`PASSWORD_BLOCKLIST_PATH`、`PASSWORD_BLOCKLIST_SHA256`、`PASSWORD_BLOCKLIST_VERSION`；用户表需具有既有 `must_change_password` 和 `credential_version` 字段。策略不可用时后端拒绝设置弱或未经策略校验的密码。

## 验证记录

- `cargo check --lib --offline` 通过，包括连锁申请、审批、新账号首设密码和 owner 改密实现。
- 主工程 `cargo test --lib chain --offline` 被本轮之前存在的缺失文件阻断：`rust-backend/src/services/merchant_analytics/data.rs:25` 引用不存在的 `data/tests.rs`。没有删改无关测试来绕过此问题，因此不能声称全套 Rust 测试通过。
- `artifacts/openapi-chain-backend-20261011/run-focused.py` 创建独立聚焦测试包，直接引用共享连锁验证模块、逐字提取本次真实事务函数与权限条件，使用真实新建 PostgreSQL 15 隔离数据库。7 项测试通过，其中 3 项数据库测试验证总部仅开通首店、跨申请覆盖拒绝、账号首设失败回滚及一次消费、凭据版本更新、无员工档案的有效 owner 改密资格、禁用成员/门店拒绝及旧密码摘要并发比较。
- 聚焦测试包的 `AppError` 仅为测试适配，不包含完整 HTTP、中间件、短信提供商和线上迁移验证；该结果不代替生产端到端验收。临时数据库已停止并清理，未部署生产、未发送真实申请或短信。

重跑聚焦测试时，将 `TEST_DATABASE_URL` 指向专用测试 PostgreSQL；测试会创建并删除隔离库，禁止使用生产服务的凭据。脚本从后端目录调用 Cargo，复用仓库 toolchain、registry 配置和锁文件，不修改主项目清单或锁文件。


## 总部独立认证与控制台

- 官网新申请默认 `chainInfo.includeFirstStore=false`；历史申请缺省为 `true`。
- 总部仍提交 `shopInfo.businessType`（拟接入业务）、联系人、总部注册地址、区域、总部营业执照与法人身份证明；`shopInfo.shopName` 使用企业名称作为申请摘要，不会创建商户。
- 仅总部仍提交拟经营电商的 `ecommerceQualification` 主体和分类资质；不提交 `ecommerceInfo`、`hotelInfo`、`homestayInfo`、首店门脸或外卖食品许可；服务端拒绝混入经营材料的错误请求。
- 获批回执 `review.subjectId`、`review.merchantUserId`，没有 `review.merchantId`；通过已有手机验证初设密码流程启用账号。
- `/api/merchant-admin/login` 返回 `headquartersOnly:true` 和限定用途凭据；不允许调用普通门店经营接口。
- `GET /api/headquarters/current`：当前认证总部；`GET /api/headquarters/stores`：门店所有者明确授权的目录。
- `POST /api/headquarters/stores`：`{merchantId,storeAccessToken}`。由同业务、已入驻获批门店的有效所有者登录取得临时访问凭据，服务端重新核验账号、凭据版本、所有者关系、业务与审批状态后记录授权。不得仅凭门店 ID 授权。
- 此接口仅授权门店目录，不迁移法律主体、结算关系或交易权限。官网改用邀请，由门店本人在自己的登录会话确认；不在总部页面收集门店密码。总部应用 API 权限独立逐店授予，见 [总部邀请与系统 API](chain-management.md)。
