# 第三方登录接口文档 > 版本:V1.6 > 更新日期:2026-07-10 > 变更说明:`licenseApproved`(Boolean)改为 `status`(Integer:0-审核中 / 1-审核成功 / 2-审核失败),支持审核失败状态;调用第三方同步接口同步传 `status` --- ## 1. 概述 本接口用于第三方系统通过用户手机号换取登录 Token,实现免密登录。系统会自动查找或注册用户,返回访问令牌和刷新令牌。同时支持第三方系统传入用户详细信息(用户名、药店名称、地址、联系人等),由我方负责存储,第三方无需我方再次维护用户信息。 **核心特性:** - 按手机号自动查找或注册用户 - 支持第三方传入用户名、药店信息等,自动保存到用户表 - 支持第三方传入入驻信息(资质图片URL等),同步写入 `t_business_license`,免去额外调用入驻接口 - 不递增 `tokenVersion`,不踢出 PC 端已有会话 - 新用户自动初始化 **PLUS 会员(普通会员)** - 第三方已审核用户(`status=1`)自动赠送 **30 天 PRO 高级会员** - 支持传入小程序 `openId` / `unionId`,一次性保存后后续支付等场景直接从用户表读取,**无需重复授权** --- ## 2. 接口信息 | 项目 | 说明 | |------|------| | **接口名称** | 外部系统换取小程序 Token | | **请求方式** | `POST` | | **接口路径** | `/api/auth/external/miniapp-token` | | **Content-Type** | `application/json` | | **字符编码** | `UTF-8` | --- ## 3. 鉴权方式 使用 **API Key** 鉴权,在请求头中传入: ``` X-Api-Key: {我方提供的API Key} ``` > **API Key 格式建议**:32 位随机十六进制字符串(大小写字母 + 数字)。 > 参考示例:`a3f8c21e67b94d0e5f1a6c8d3b9e2071` > > ⚠️ 请妥善保管 API Key,不要泄露到前端代码或客户端。请在后端服务中调用此接口。 --- ## 4. 请求参数 ### 4.1 请求头(Headers) | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | `X-Api-Key` | String | 是 | 接口鉴权密钥 | | `Content-Type` | String | 是 | 固定值 `application/json` | ### 4.2 请求体(Body) #### 基础字段 | 参数名 | 类型 | 必填 | 说明 | |--------|------|------|------| | `phone` | String | 是 | 用户手机号 | | `inviteCode` | String | 否 | 邀请码(仅新用户注册时生效) | #### 用户信息字段(第三方传入,自动保存到用户表) | 参数名 | 类型 | 必填 | 说明 | 对应我方字段 | |--------|------|------|------|--------------| | `username` | String | 否 | 用户名(保存到 userName 字段;若未传 nickname,则自动作为 nickname 的兜底值) | `nickname` / `userName` | | `nickname` | String | 否 | 昵称(不传则使用 username,再不传则按手机号生成) | `nickname` | | `pharmacyName` | String | 否 | 药店名称 | `pharmacy_name` | | `province` | String | 否 | 省 | `province` | | `city` | String | 否 | 市 | `city` | | `district` | String | 否 | 区 | `district` | | `pharmacyAddress` | String | 否 | 药店详细地址 | `pharmacy_address` | | `contactPerson` | String | 否 | 联系人 | `contact_person` | | `contactPhone` | String | 否 | 联系电话 | `contact_phone` | | `businessLicenseNo` | String | 否 | 营业执照号(统一社会信用代码) | `business_license_no` | | `openId` | String | 否 | 微信小程序 openId(第三方从小程序 wx.login 获取后传入,保存到 wechat_mini_open_id。传入后支付场景无需重复授权) | `wechat_mini_open_id` | | `unionId` | String | 否 | 微信开放平台 unionId(用于跨应用用户统一标识,保存到 wechat_union_id) | `wechat_union_id` | #### 入驻信息字段(对应 /api/business-license/upload,均为可选,传入后同步写入 t_business_license) | 参数名 | 类型 | 必填 | 说明 | 对应我方字段 | |--------|------|------|------|--------------| | `storeName` | String | 否 | 店铺名称(不传则使用 pharmacyName) | `store_name` | | `terminalType` | String | 否 | 终端类型:`1`-单店 / `2`-连锁 / `3`-诊所、社康等 | `terminal_type` | | `businessLicenseUrl` | String | 否 | 营业执照图片URL | `license_image_url` | | `drugLicenseUrl` | String | 否 | 药品经营许可证图片URL | `drug_license_url` | | `medicalDeviceClass2Url` | String | 否 | 二类医疗器械备案图片URL | `medical_device_class2_url` | | `medicalDeviceClass3Url` | String | 否 | 三类医疗器械备案图片URL | `medical_device_class3_url` | | `status` | Integer | 否 | **审核状态**:`0`-审核中 / `1`-审核成功 / `2`-审核失败(不传默认`0`)。`1` = 第三方已审核通过,无需我方再次审核,直接设为 APPROVED 并赠送 30 天高级会员 | — | > 💡 **审核状态说明**: > - 如果第三方系统已完成资质审核,传 `status: 1`,我方将直接标记为"审核通过"并赠送 30 天 PRO 高级会员 > - 如果第三方审核失败,传 `status: 2`,我方将标记为"审核失败" > - 如果第三方尚未审核或不确定,传 `status: 0` 或不传(默认 `PENDING`),由我方运营审核 > - 已审核通过的入驻记录不会因后续调用而降级 > - 只要请求中包含任一入驻信息字段(storeName / terminalType / businessLicenseUrl / drugLicenseUrl / medicalDeviceClass2Url / medicalDeviceClass3Url),即自动写入 `t_business_license` 表 > - `status=1`:审核状态设为 `APPROVED`(审核通过),首次通过赠送 30 天 PRO 高级会员 > - `status=0` 或不传:审核状态设为 `PENDING`(待运营审核) > - `status=2`:审核状态设为 `REJECTED`(审核失败) > - 已存在入驻记录:已 APPROVED 状态不降级,PENDING/REJECTED 状态可随 `status` 更新(含互转、升级到 APPROVED) > - 省/市/区/地址/联系人/电话等字段会自动从用户信息字段中提取填入 > > 💡 **字段更新规则**: > - 新用户会写入所有传入字段 > - 已存在用户仅更新非空字段(不会清空已有数据) > > 💡 **nickname 优先级**:`nickname` > `username` > 手机号生成(如 `用户8000`) > > 💡 **建议**:第三方每次调用时传入最新的用户信息,我方会自动同步更新,无需额外维护。 ### 4.3 请求示例 #### 最简请求(仅必填字段) ```json { "phone": "13800138000" } ``` #### 完整请求(含用户信息 + 入驻信息) ```json { "phone": "13800138000", "inviteCode": "ABC123", "username": "yaohuigou001", "nickname": "药汇购001", "pharmacyName": "北京同仁堂大药房", "province": "北京市", "city": "北京市", "district": "朝阳区", "pharmacyAddress": "建国路88号", "contactPerson": "张三", "contactPhone": "13800138000", "businessLicenseNo": "91110105MA12345678", "openId": "oJx1x5xxxxxxxxxxxxxxxxx", "unionId": "oABCD1234xxxxxxxxxxxxxx", "storeName": "北京同仁堂大药房(朝阳分店)", "terminalType": "1", "businessLicenseUrl": "https://oss.example.com/yyzz.jpg", "drugLicenseUrl": "https://oss.example.com/ypjy.jpg", "medicalDeviceClass2Url": "https://oss.example.com/elqx.jpg", "medicalDeviceClass3Url": "https://oss.example.com/slqx.jpg", "status": 1 } ``` --- ## 5. 响应参数 ### 5.1 通用响应结构 ```json { "code": 200, "message": "success", "data": { } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `code` | int | 状态码,`200` 表示成功 | | `message` | String | 提示信息 | | `data` | Object | 登录响应数据 | ### 5.2 data 字段 | 参数名 | 类型 | 说明 | |--------|------|------| | `userId` | String | 用户ID(字符串,避免JS大数精度丢失) | | `accessToken` | String | 访问令牌 | | `refreshToken` | String | 刷新令牌 | | `expiresIn` | Long | accessToken有效期(秒),默认`7200` | | `newUser` | Boolean | 是否为新注册用户 | | `needBindPhone` | Boolean | 始终返回`false` | | `role` | String | 固定值`"USER"` | | `userInfo` | Object | 用户基本信息 | ### 5.3 userInfo 字段说明 | 参数名 | 类型 | 说明 | |--------|------|------| | `id` | String | 用户ID | | `phone` | String | 手机号 | | `nickname` | String | 昵称(即第三方传入的 nickname 或 username) | | `userName` | String | 第三方传入的原始用户名 | | `avatar` | String | 头像 URL | | `wechatBound` | Boolean | 是否已绑定微信 | | `loginSource` | String | 登录来源,`"MINIAPP"` 表示小程序 | | `pharmacyName` | String | 药店名称 | | `province` | String | 省 | | `city` | String | 市 | | `district` | String | 区 | | `membershipLevel` | String | 会员等级代码(PLUS / PRO / ULTRA) | | `membershipLevelName` | String | 会员等级名称 | | `licenseStatus` | String | 营业执照审核状态:`NOT_SUBMITTED` / `PENDING` / `APPROVED` / `REJECTED` | | `inviterId` | String | 邀请人用户ID(未绑定邀请码则为 null) | | `inviterNickname` | String | 邀请人昵称 | | `hasBoundInviteCode` | Boolean | 是否已绑定邀请码 | | `createTime` | String | 注册时间 | ### 5.4 响应示例 ```json { "code": 200, "message": "success", "data": { "userId": "1234567890123456789", "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "eyJhbGciOiJIUzI1NiJ9...", "expiresIn": 7200, "newUser": false, "needBindPhone": false, "role": "USER", "userInfo": { "id": "1234567890123456789", "phone": "138****8000", "nickname": "药汇购001", "userName": "yaohuigou001", "avatar": null, "wechatBound": false, "loginSource": "MINIAPP", "pharmacyName": "北京同仁堂大药房", "province": "北京市", "city": "北京市", "district": "朝阳区", "membershipLevel": "PRO", "membershipLevelName": "高级会员", "licenseStatus": "APPROVED", "inviterId": "1234567890123456788", "inviterNickname": "邀请人昵称", "hasBoundInviteCode": true, "createTime": "2025-01-01T12:00:00" } } } ``` --- ## 6. 错误码 | 错误码 | 说明 | 处理建议 | |--------|------|----------| | `200` | 成功 | — | | `401` | API Key 无效或缺失 | 检查`X-Api-Key`请求头 | | `400` | 参数校验失败 | 检查`phone`是否为空 | | `500` | 服务器内部错误 | 重试,持续失败联系我方 | --- ## 7. Token 使用说明 ### 7.1 携带 Token 后续所有业务接口请求需在请求头中携带 accessToken: ``` Authorization: Bearer {accessToken} ``` ### 7.2 Token 刷新 accessToken 过期后,使用 refreshToken 换取新 Token: ``` POST /api/auth/refresh Content-Type: application/json { "refreshToken": "{refreshToken}" } ``` > 刷新后旧 refreshToken 立即失效(Token 旋转机制)。 ### 7.3 心跳保活 建议客户端定时调用心跳接口,保持登录状态: ``` POST /api/auth/heartbeat Authorization: Bearer {currentAccessToken} ``` > 建议间隔 = accessToken有效期 × 80%,约5760秒。 --- ## 8. 注意事项 | # | 说明 | |---|------| | 1 | PC端严格单设备,小程序与PC互不干扰,同一账号可同时登录 | | 2 | 手机号未注册时自动创建账号并初始化 **PLUS 会员(普通会员)** | | 3 | `inviteCode`仅新用户注册时生效 | | 4 | API Key请在后端调用,勿暴露到客户端 | | 5 | accessToken默认2小时,refreshToken默认7天 | | 6 | 每次刷新后旧refreshToken立即作废,请保存最新的 | | 7 | 用户信息字段(username、药店信息等)每次调用都会同步更新,建议传入最新数据 | | 8 | `username` 会保存到我方的 `nickname` 字段,不需要额外维护用户名表 | | 9 | 传入入驻信息字段后自动创建/更新入驻记录,无需再调用 `/api/business-license/upload` | | 10 | `status=1` 时入驻状态直接审核通过,首次通过赠送 **30 天 PRO 高级会员** | | 11 | 已审核通过的入驻记录(APPROVED)不会因后续调用而降级 | | 12 | 传入 `openId`/`unionId` 后,小程序支付无需再次 `wx.login` 授权,后端直接从 `t_user.wechat_mini_open_id` 读取 | --- ## 9. 调用示例 ### cURL ```bash curl -X POST "https://your-domain.com/api/auth/external/miniapp-token" \ -H "Content-Type: application/json" \ -H "X-Api-Key: your-api-key-here" \ -d '{ "phone": "13800138000", "username": "yaohuigou001", "pharmacyName": "北京同仁堂大药房", "province": "北京市", "city": "北京市", "district": "朝阳区", "pharmacyAddress": "建国路88号", "contactPerson": "张三", "contactPhone": "13800138000", "businessLicenseNo": "91110105MA12345678", "openId": "oJx1x5xxxxxxxxxxxxxxxxx", "unionId": "oABCD1234xxxxxxxxxxxxxx", "storeName": "北京同仁堂大药房(朝阳分店)", "terminalType": "1", "businessLicenseUrl": "https://oss.example.com/yyzz.jpg", "drugLicenseUrl": "https://oss.example.com/ypjy.jpg", "status": 1 }' ``` ### Java (OkHttp) ```java OkHttpClient client = new OkHttpClient(); String json = "{" + "\"phone\": \"13800138000\"," + "\"username\": \"yaohuigou001\"," + "\"pharmacyName\": \"北京同仁堂大药房\"," + "\"province\": \"北京市\"," + "\"city\": \"北京市\"," + "\"district\": \"朝阳区\"," + "\"pharmacyAddress\": \"建国路88号\"," + "\"contactPerson\": \"张三\"," + "\"contactPhone\": \"13800138000\"," + "\"businessLicenseNo\": \"91110105MA12345678\"," + "\"openId\": \"oJx1x5xxxxxxxxxxxxxxxxx\"," + "\"unionId\": \"oABCD1234xxxxxxxxxxxxxx\"," + "\"storeName\": \"北京同仁堂大药房(朝阳分店)\"," + "\"terminalType\": \"1\"," + "\"businessLicenseUrl\": \"https://oss.example.com/yyzz.jpg\"," + "\"drugLicenseUrl\": \"https://oss.example.com/ypjy.jpg\"," + "\"status\": 1" + "}"; RequestBody body = RequestBody.create(json, MediaType.parse("application/json")); Request request = new Request.Builder() .url("https://your-domain.com/api/auth/external/miniapp-token") .post(body) .addHeader("X-Api-Key", "your-api-key-here") .addHeader("Content-Type", "application/json") .build(); try (Response response = client.newCall(request).execute()) { System.out.println(response.body().string()); } ``` ### Python (requests) ```python import requests url = "https://your-domain.com/api/auth/external/miniapp-token" headers = { "X-Api-Key": "your-api-key-here", "Content-Type": "application/json" } data = { "phone": "13800138000", "username": "yaohuigou001", "pharmacyName": "北京同仁堂大药房", "province": "北京市", "city": "北京市", "district": "朝阳区", "pharmacyAddress": "建国路88号", "contactPerson": "张三", "contactPhone": "13800138000", "businessLicenseNo": "91110105MA12345678", "openId": "oJx1x5xxxxxxxxxxxxxxxxx", "unionId": "oABCD1234xxxxxxxxxxxxxx", "storeName": "北京同仁堂大药房(朝阳分店)", "terminalType": "1", "businessLicenseUrl": "https://oss.example.com/yyzz.jpg", "drugLicenseUrl": "https://oss.example.com/ypjy.jpg", "status": 1 } response = requests.post(url, json=data, headers=headers) print(response.json()) ``` --- ## 10. 字段映射表 第三方传入字段与我方数据库字段的对应关系: ### 10.1 用户信息(写入 t_users) | 第三方传入字段 | 我方数据库字段 | 我方实体字段 | 说明 | |----------------|----------------|--------------|------| | `phone` | `phone` | `phone` | 手机号(必填) | | `username` | `nickname` | `nickname` | 用户名(同时写入 nickname 和 userName) | | `nickname` | `nickname` | `nickname` | 昵称(优先级低于 username) | | `pharmacyName` | `pharmacy_name` | `pharmacyName` | 药店名称(也作为 storeName 兜底) | | `province` | `province` | `province` | 省 | | `city` | `city` | `city` | 市 | | `district` | `district` | `district` | 区 | | `pharmacyAddress` | `pharmacy_address` | `pharmacyAddress` | 药店详细地址 | | `contactPerson` | `contact_person` | `contactPerson` | 联系人 | | `contactPhone` | `contact_phone` | `contactPhone` | 联系电话 | | `businessLicenseNo` | `business_license_no` | `businessLicenseNo` | 营业执照号 | | `openId` | `wechat_mini_open_id` | `wechatMiniOpenId` | 微信小程序 openId | | `unionId` | `wechat_union_id` | `wechatUnionId` | 微信开放平台 unionId | ### 10.2 入驻信息(写入 t_business_license,全部可选) | 第三方传入字段 | 我方数据库字段 | 我方实体字段 | 说明 | |----------------|----------------|--------------|------| | `storeName` | `store_name` | `storeName` | 店铺名称(不传则用 pharmacyName) | | `terminalType` | `terminal_type` | `terminalType` | 终端类型 | | `businessLicenseUrl` | `license_image_url` | `licenseImageUrl` | 营业执照图片URL | | `drugLicenseUrl` | `drug_license_url` | `drugLicenseUrl` | 药品经营许可证图片URL | | `medicalDeviceClass2Url` | `medical_device_class2_url` | `medicalDeviceClass2Url` | 二类医疗器械备案图片URL | | `medicalDeviceClass3Url` | `medical_device_class3_url` | `medicalDeviceClass3Url` | 三类医疗器械备案图片URL | | `status` | `review_status` | `reviewStatus` | 审核状态:`0`→PENDING / `1`→APPROVED / `2`→REJECTED | > 💡 入驻记录保存时,以下字段由系统自动填入(无需第三方传入): > - `store_address` ← 从 `pharmacyAddress` 提取 > - `credit_code` ← 从 `businessLicenseNo` 提取 > - `sync_source` ← 固定为 `MINIAPP_SYNC` > - `review_status` ← 取决于 `status`:`1` → `APPROVED`,`2` → `REJECTED`,`0`/不传 → `PENDING` > - `show_verified_badge` ← 首次审核通过时设为 `true` --- ## 11. 联系方式 如有问题,请联系我方技术支持。 --- ## 12. 变更记录 | 版本 | 日期 | 变更内容 | |------|------|----------| | V1.0 | 2026-07-07 | 初始版本:基础登录接口 | | V1.1 | 2026-07-07 | 扩展请求参数:支持传入用户名、药店信息等,自动保存到用户表 | | V1.2 | 2026-07-09 | 新增入驻信息字段(storeName / terminalType / businessLicenseUrl / drugLicenseUrl / medicalDeviceClass2Url / medicalDeviceClass3Url),传入后同步写入 t_business_license,无需额外调用入驻接口 | | V1.3 | 2026-07-09 | 新增 `licenseApproved` 参数支持第三方已审核入驻状态直通;审核通过自动赠送 30 天 PRO 高级会员;已 APPROVED 状态不降级;响应新增 `licenseStatus` 字段 | | V1.4 | 2026-07-09 | 响应 userInfo 补全完整返回字段(userName、pharmacyName、province、city、district、inviterId、inviterNickname、hasBoundInviteCode);修正 nickname 优先级描述(nickname > username) | | V1.5 | 2026-07-09 | 新增 `openId`、`unionId` 参数,第三方传入后保存到 `wechat_mini_open_id` / `wechat_union_id`;后续小程序支付等场景直接从用户表读取,无需重复 wx.login 授权 | V1.6 | 2026-07-10 | `licenseApproved`(Boolean)改为 `status`(Integer:`0`-审核中 / `1`-审核成功 / `2`-审核失败),支持审核失败状态;已 APPROVED 不降级,PENDING/REJECTED 可随 `status` 互转或升级;调用第三方同步接口同步传 `status` |