# 摊位资质认证(034):App 端接入说明 **日期**:2026-10-10 **分支**:`test-202609v2`(后端已实现并过定向测试;测试库 `stall_qualification` 两张表已建) **一句话**:摊位(`userType=4` 账号)在 App 里提交认证资料、查看审核进度、驳回后重提;**审核通过前摊位不能营业和接新单**。 --- ## 1. 功能与角色 - 认证归属**摊位**(`storeId`),不归属登录账号。一个摊位可以有多个管理账号,共用同一套认证资料;换账号、停用账号都不影响已通过的资质。 - App 端只做**摊主侧**:填表、传图、存草稿、提交、看进度、看驳回原因、重提。 - 平台审核(通过/驳回)在平台管理 PC 端操作,App 不涉及。夜市主看进度是 PC 端功能(接口见第 8 节,App 如需也可用)。 - 所有摊位同一张表单、同一套必填规则,**没有**个人经营/公司经营的分类分支。 ## 2. 通用约定 | 项 | 说明 | |---|---| | 鉴权 | 请求头 `token: <登录令牌>`,账号必须是有效的 `userType=4`(摊主)且已绑定 `isStall=1` 的摊位;否则返回 `merchant.store.access.denied` | | 响应包裹 | `{ "code": 200, "msg": "...", "data": ... }`;业务失败 `code=500`,`msg` 为服务端按语言下发的本地化文案(zh/tw/en/vi/th),**App 直接展示 msg 即可,不要自己映射错误文案** | | 图片 BASE | 新上传图片是公开路径,直接 `BASE_URL + 路径` 展示,无需登录 | ## 3. 状态模型 `application.status`: | 值 | 含义 | App 表现 | |---|---|---| | `0` | 草稿 | 可继续编辑、可提交 | | `1` | 待审核 | **只读**,不能改不能重复提交(服务端拒绝 `stall.qualification.pending`) | | `2` | 通过 | 当前有效资质;重新提交期间保持有效,新申请通过才切换 | | `3` | 驳回 | 展示 `rejectReason`,按新版本重新填写提交 | `GET /stall/qualification` 里 `currentApplication` 为 `null` 表示从未创建过申请(此时 `version=0`)。 **版本并发**:`view.version` 是乐观锁版本号。每次保存/提交/审核都会 +1。保存请求必须带**当前** `version`;不匹配返回 `stall.qualification.conflict`(提示用户刷新重新加载,**不要自动重试覆盖**——可能是另一个管理账号刚改过)。提交成功后从响应里取新的 `version` 存本地。 ## 4. 接口 ### 4.1 查询资质视图 ```http GET /stall/qualification token: <摊主登录令牌> ``` ```json { "code": 200, "data": { "storeId": 194, "stallName": "阿美蚵仔煎", "stallAddress": "台北市士林夜市 108 摊", "nightMarketId": 12, "version": 3, "currentApplication": { "...": "完整申请对象,见 4.4" }, "approvedApplication": { "...": "当前有效版本,可能为 null" }, "history": [ { "id": 88, "storeId": 194, "applicationVersion": 2, "status": 3, "stallNameSnapshot": "阿美蚵仔煎", "stallAddressSnapshot": "台北市士林夜市 108 摊", "nightMarketIdSnapshot": 12, "submittedAt": "2026-10-10T09:00:00+08:00", "reviewedAt": "2026-10-10T11:00:00+08:00", "rejectReason": "证件照片不清晰" } ] } } ``` - 读操作不会创建草稿;从未提交过时 `version=0`、两个 application 都是 `null`、`history=[]`。 - `history` 只有摘要(无姓名/证件号/电话/附件),按 `applicationVersion` 倒序。 - 摊位名称、地址、所属夜市由服务端从 `pos_store` 固化快照,**App 不提供修改入口**。 ### 4.2 保存草稿 / 提交审核 ```http POST /stall/qualification/draft # 存草稿,允许不完整 POST /stall/qualification/submit # 提交审核,四组资料必填 Content-Type: application/json token: <摊主登录令牌> ``` 请求体(两个接口相同): ```json { "version": 3, "responsibleName": "王大明", "contactPhone": "0912345678", "identityNumber": "A123456789", "identityImageUrls": ["/profile/upload/2026/10/10/xxx.jpg"], "stallImageUrls": ["/profile/upload/2026/10/10/yyy.png"], "foodRegistrationNumber": "F-200000000-00000", "foodRegistrationImageUrls": ["/profile/upload/2026/10/10/zzz.jpg"] } ``` | 字段 | 必填(提交时) | 限制 | |---|---|---| | `version` | 是(草稿也要) | 必须等于当前 `view.version`,否则冲突错误 | | `responsibleName` | 是 | ≤100 字符 | | `contactPhone` | 是 | ≤32 字符;与账号登录手机号独立,可以不同 | | `identityNumber` | 是 | ≤64 字符(身份证/护照号) | | `identityImageUrls` | 是 | 1~5 张 | | `stallImageUrls` | 是 | 1~5 张 | | `foodRegistrationNumber` | 是 | ≤64 字符 | | `foodRegistrationImageUrls` | 是 | 1~5 张 | 规则: - 文本两端 trim,trim 后为空视为缺失;超长返回 `stall.qualification.input.invalid`。 - 草稿也校验已填图片引用的合法性(数量上限、文件存在),只是允许字段不齐。 - 不接收 `storeId`、状态、审核人等字段——归属由 token 决定。 - 草稿重复保存是**覆盖同一条草稿**(`applicationVersion` 不变);从已提交/已审核状态再保存(提交后想改、驳回重提)会创建**新版本申请**,旧记录不可变。 - 响应为保存后的完整 `QualificationView`(同 4.1 结构,`currentApplication` 即刚保存的申请),**从响应更新本地 `version`**。 ### 4.3 图片上传(商品同款通用上传) ```http POST /utils/Upload Content-Type: multipart/form-data (字段名 file;接口匿名,可不带 token) ``` 响应: ```json { "code": 200, "msg": "上传成功", "data": "/profile/upload/2026/10/10/20261010xxxx.jpg" } ``` - `data` 就是可保存进 `xxxImageUrls` 的路径,直接使用。 - 后端会校验真实图片格式并规范文件名特殊字符,App 无需预处理文件名。 **展示规则(重要)**: | 引用形态 | 判别 | 展示方式 | |---|---|---| | 新公共路径 | 以 `/profile/upload/` 开头 | `BASE_URL + 引用`,公开访问,无需登录 | | 旧私有引用 | `"/.jpg|.png"` 形态 | 走 4.4 鉴权接口取 blob 再展示 | ### 4.4 历史申请详情 / 旧私有附件 ```http GET /stall/qualification/applications/{id} token: <摊主登录令牌> ``` 返回完整 `StallQualificationApplication`(含材料、提交/审核时间、驳回原因)。只能看**本摊位**的,看别人的返回 `merchant.store.access.denied`。用于点开 `history` 记录查看当时提交的材料。 ```http GET /stall/qualification/files/{storeId}/{fileName} token: <摊主登录令牌> ``` 旧私有附件读取(`stall-qualification` 目录的历史文件),返回图片二进制(`Cache-Control: no-store`),App 需带 token 请求并转 blob 展示;只能读本人摊位的文件。**新上传不要用 `/stall/qualification/files`(POST)**,那是旧版兼容接口。 ## 5. 营业门禁(App 需要处理的联动) `isStall=1` 且无通过记录时(含从未提交): | 场景 | 行为 | |---|---| | 摊主 App 创建订单(代客/堂食/外送) | 服务端拒绝,`msg` = 摊位材料尚未审核通过,暂不能营业或建立新订单 | | 用户 App 向该摊位下单 | 同上被拒 | | 门店状态 | 对外按闭店(`state=1`)展示 | | 已有订单 | 出餐、配送、支付后续**不受影响** | **建议**:摊主端遇到 `stall.qualification.approval.required` 文案(或进店页发现未认证)时,引导跳转认证页;认证页根据 4.1 的状态渲染对应操作。 ## 6. 错误文案(服务端已本地化,直接展示 msg) | 场景 | zh-TW 文案示例 | |---|---| | 版本冲突/已被他人操作 | 資料已變更或已審核,請重新整理後重試 | | 缺 version | 請重新整理後再儲存,缺少資料版本 | | 待审核中重复提交/修改 | 資料正在審核,暫不可修改或重複提交 | | 提交时材料不齐 | 請補齊負責人姓名電話、證件、攤位照片及食品業者登錄資料… | | 参数/超长/图片超5张 | 審核資料參數不正確 | | 图片无效/不存在/非本摊位 | 審核圖片無效、超過限制或不屬於目前攤位 | | 上传处理失败 | 審核材料檔案處理失敗,請稍後重試 | | 营业门禁 | 攤位材料尚未審核通過,暫不能營業或建立新訂單 | | 非本摊位/账号无效 | (merchant.store.access.denied 对应文案) | ## 7. 建议页面流程 ``` 进入认证页 └─ GET /stall/qualification ├─ currentApplication = null → 空表单(version=0),填完可存草稿/提交 ├─ status = 0 (草稿) → 回填表单,继续编辑或提交 ├─ status = 1 (待审核) → 只读展示材料 + "审核中" ├─ status = 3 (驳回) → 展示 rejectReason;重填提交走新版本 └─ status = 2 (通过) → 展示有效资料;approvedApplication 为当前有效版本 ``` - 图片选择后先 `POST /utils/Upload`,成功再把返回路径塞进对应数组;**上传中禁止点提交**。 - 待审核状态下隐藏保存/提交按钮(服务端同样会拦)。 - 提交成功后用响应刷新整个视图和本地 `version`。 ## 8. 夜市主进度(可选,type=3 账号) ```http GET /stall/qualification/market token: <夜市主登录令牌> ``` 返回本人旗下摊位数组:`{ storeId, status(-1=未创建), approved, ... }`,**不含**姓名、证件号、电话和附件。App 若不做夜市主进度页可忽略。 ## 9. 注意事项 - `userType` 对照:`0` 用户、`1` 商家、`2` 骑手、`3` 夜市主、`4` 摊主。本功能仅 `4`。 - 平台审核接口(`/system/stallQualification*`)走平台管理端 Authorization 登录,与 App 无关,不要在 App 调用。 - 提交后的材料**不可变**:想改只能等驳回后重提,或(已通过后)提交新版本申请;已通过资质在新申请审核期间保持有效。 - 证件图片链接为公开访问(用户已确认);但姓名、证件号、电话不出现在任何列表/进度接口,App 同样不要在日志里打印这些字段。