Procházet zdrojové kódy

测试文件调整

liuchengsen před 1 měsícem
rodič
revize
8b092fc848
19 změnil soubory, kde provedl 455 přidání a 56 odebrání
  1. 21 18
      docs/第三方登录接口文档.md
  2. 17 1
      zhijiayun-gateway/src/main/resources/application-dev.yml
  3. 8 1
      zhijiayun-gateway/src/main/resources/application-prod.yml
  4. 59 4
      zhijiayun-gateway/src/main/resources/application.yml
  5. 13 0
      zhijiayun-gateway/src/main/resources/db/migration-v14.sql
  6. 7 3
      zhijiayun-gateway/src/main/resources/db/schema.sql
  7. 3 3
      zhijiayun-gateway/src/test/resources/test-schema.sql
  8. 9 8
      zhijiayun-payment/src/main/java/com/xuekairui/payment/config/PaymentProperties.java
  9. 2 2
      zhijiayun-payment/src/main/java/com/xuekairui/payment/mapper/PaymentOrderMapper.java
  10. 4 4
      zhijiayun-test/src/main/resources/static/activity-test.html
  11. 7 1
      zhijiayun-user/src/main/java/com/xuekairui/user/dto/BusinessLicenseReviewResponse.java
  12. 2 2
      zhijiayun-user/src/main/java/com/xuekairui/user/dto/ExternalTokenRequest.java
  13. 5 1
      zhijiayun-user/src/main/java/com/xuekairui/user/entity/BusinessLicense.java
  14. 2 1
      zhijiayun-user/src/main/java/com/xuekairui/user/mapper/BusinessLicenseMapper.java
  15. 11 4
      zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java
  16. 35 0
      zhijiayun-user/src/main/java/com/xuekairui/user/service/BusinessLicenseService.java
  17. 50 0
      zhijiayun-user/src/main/java/com/xuekairui/user/service/ShopResourceSyncService.java
  18. 3 3
      zhijiayun-user/src/main/java/com/xuekairui/user/service/WechatMiniProgramService.java
  19. 197 0
      zhijiayun-user/src/main/java/com/xuekairui/user/service/impl/ShopResourceSyncServiceImpl.java

+ 21 - 18
docs/第三方登录接口文档.md

@@ -1,8 +1,8 @@
 # 第三方登录接口文档
 
-> 版本:V1.5  
-> 更新日期:2026-07-09  
-> 变更说明:新增 openId、unionId 参数,第三方可传入小程序 openId/unionId 保存到用户表,后续支付等场景无需重复授权
+> 版本:V1.6  
+> 更新日期:2026-07-10  
+> 变更说明:`licenseApproved`(Boolean)改为 `status`(Integer:0-审核中 / 1-审核成功 / 2-审核失败),支持审核失败状态;调用第三方同步接口同步传 `status`
 
 ---
 
@@ -16,7 +16,7 @@
 - 支持第三方传入入驻信息(资质图片URL等),同步写入 `t_business_license`,免去额外调用入驻接口
 - 不递增 `tokenVersion`,不踢出 PC 端已有会话
 - 新用户自动初始化 **PLUS 会员(普通会员)**
-- 第三方已审核用户(`licenseApproved=true`)自动赠送 **30 天 PRO 高级会员**
+- 第三方已审核用户(`status=1`)自动赠送 **30 天 PRO 高级会员**
 - 支持传入小程序 `openId` / `unionId`,一次性保存后后续支付等场景直接从用户表读取,**无需重复授权**
 
 ---
@@ -93,16 +93,18 @@ X-Api-Key: {我方提供的API Key}
 | `drugLicenseUrl` | String | 否 | 药品经营许可证图片URL | `drug_license_url` |
 | `medicalDeviceClass2Url` | String | 否 | 二类医疗器械备案图片URL | `medical_device_class2_url` |
 | `medicalDeviceClass3Url` | String | 否 | 三类医疗器械备案图片URL | `medical_device_class3_url` |
-| `licenseApproved` | Boolean | 否 | **审核状态标记**。`true` = 第三方已审核通过,无需我方再次审核,直接设为 APPROVED 并赠送 30 天高级会员;不传/false = 待审核 PENDING | — |
+| `status` | Integer | 否 | **审核状态**:`0`-审核中 / `1`-审核成功 / `2`-审核失败(不传默认`0`)。`1` = 第三方已审核通过,无需我方再次审核,直接设为 APPROVED 并赠送 30 天高级会员 | — |
 
 > 💡 **审核状态说明**:
-> - 如果第三方系统已完成资质审核,传 `licenseApproved: true`,我方将直接标记为"审核通过"并赠送 30 天 PRO 高级会员
-> - 如果第三方尚未审核或不确定,不传该字段(默认 `PENDING`),由我方运营审核
+> - 如果第三方系统已完成资质审核,传 `status: 1`,我方将直接标记为"审核通过"并赠送 30 天 PRO 高级会员
+> - 如果第三方审核失败,传 `status: 2`,我方将标记为"审核失败"
+> - 如果第三方尚未审核或不确定,传 `status: 0` 或不传(默认 `PENDING`),由我方运营审核
 > - 已审核通过的入驻记录不会因后续调用而降级
 > - 只要请求中包含任一入驻信息字段(storeName / terminalType / businessLicenseUrl / drugLicenseUrl / medicalDeviceClass2Url / medicalDeviceClass3Url),即自动写入 `t_business_license` 表
-> - `licenseApproved=true`:审核状态设为 `APPROVED`(审核通过),首次通过赠送 30 天 PRO 高级会员
-> - 未传 `licenseApproved` 或 `false`:审核状态设为 `PENDING`(待运营审核)
-> - 已存在入驻记录:已 APPROVED 状态不降级,PENDING/REJECTED 状态可随 `licenseApproved=true` 升级
+> - `status=1`:审核状态设为 `APPROVED`(审核通过),首次通过赠送 30 天 PRO 高级会员
+> - `status=0` 或不传:审核状态设为 `PENDING`(待运营审核)
+> - `status=2`:审核状态设为 `REJECTED`(审核失败)
+> - 已存在入驻记录:已 APPROVED 状态不降级,PENDING/REJECTED 状态可随 `status` 更新(含互转、升级到 APPROVED)
 > - 省/市/区/地址/联系人/电话等字段会自动从用户信息字段中提取填入
 >
 > 💡 **字段更新规则**:
@@ -147,7 +149,7 @@ X-Api-Key: {我方提供的API Key}
   "drugLicenseUrl": "https://oss.example.com/ypjy.jpg",
   "medicalDeviceClass2Url": "https://oss.example.com/elqx.jpg",
   "medicalDeviceClass3Url": "https://oss.example.com/slqx.jpg",
-  "licenseApproved": true
+  "status": 1
 }
 ```
 
@@ -307,7 +309,7 @@ Authorization: Bearer {currentAccessToken}
 | 7 | 用户信息字段(username、药店信息等)每次调用都会同步更新,建议传入最新数据 |
 | 8 | `username` 会保存到我方的 `nickname` 字段,不需要额外维护用户名表 |
 | 9 | 传入入驻信息字段后自动创建/更新入驻记录,无需再调用 `/api/business-license/upload` |
-| 10 | `licenseApproved=true` 时入驻状态直接审核通过,首次通过赠送 **30 天 PRO 高级会员** |
+| 10 | `status=1` 时入驻状态直接审核通过,首次通过赠送 **30 天 PRO 高级会员** |
 | 11 | 已审核通过的入驻记录(APPROVED)不会因后续调用而降级 |
 | 12 | 传入 `openId`/`unionId` 后,小程序支付无需再次 `wx.login` 授权,后端直接从 `t_user.wechat_mini_open_id` 读取 |
 
@@ -338,7 +340,7 @@ curl -X POST "https://your-domain.com/api/auth/external/miniapp-token" \
     "terminalType": "1",
     "businessLicenseUrl": "https://oss.example.com/yyzz.jpg",
     "drugLicenseUrl": "https://oss.example.com/ypjy.jpg",
-    "licenseApproved": true
+    "status": 1
   }'
 ```
 
@@ -364,7 +366,7 @@ String json = "{"
     + "\"terminalType\": \"1\","
     + "\"businessLicenseUrl\": \"https://oss.example.com/yyzz.jpg\","
     + "\"drugLicenseUrl\": \"https://oss.example.com/ypjy.jpg\","
-    + "\"licenseApproved\": true"
+    + "\"status\": 1"
     + "}";
 
 RequestBody body = RequestBody.create(json, MediaType.parse("application/json"));
@@ -407,7 +409,7 @@ data = {
     "terminalType": "1",
     "businessLicenseUrl": "https://oss.example.com/yyzz.jpg",
     "drugLicenseUrl": "https://oss.example.com/ypjy.jpg",
-    "licenseApproved": True
+    "status": 1
 }
 
 response = requests.post(url, json=data, headers=headers)
@@ -448,13 +450,13 @@ print(response.json())
 | `drugLicenseUrl` | `drug_license_url` | `drugLicenseUrl` | 药品经营许可证图片URL |
 | `medicalDeviceClass2Url` | `medical_device_class2_url` | `medicalDeviceClass2Url` | 二类医疗器械备案图片URL |
 | `medicalDeviceClass3Url` | `medical_device_class3_url` | `medicalDeviceClass3Url` | 三类医疗器械备案图片URL |
-| `licenseApproved` | `review_status` | `reviewStatus` | 审核状态标记,`true` → APPROVED / `false` → PENDING |
+| `status` | `review_status` | `reviewStatus` | 审核状态:`0`→PENDING / `1`→APPROVED / `2`→REJECTED |
 
 > 💡 入驻记录保存时,以下字段由系统自动填入(无需第三方传入):
 > - `store_address` ← 从 `pharmacyAddress` 提取
 > - `credit_code` ← 从 `businessLicenseNo` 提取
 > - `sync_source` ← 固定为 `MINIAPP_SYNC`
-> - `review_status` ← 取决于 `licenseApproved`:`true` → `APPROVED`,`false`/不传 → `PENDING`
+> - `review_status` ← 取决于 `status`:`1` → `APPROVED`,`2` → `REJECTED`,`0`/不传 → `PENDING`
 > - `show_verified_badge` ← 首次审核通过时设为 `true`
 
 ---
@@ -474,4 +476,5 @@ print(response.json())
 | 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.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` |

+ 17 - 1
zhijiayun-gateway/src/main/resources/application-dev.yml

@@ -43,6 +43,16 @@ sms:
   test-mode: true
   test-code: "123456"
 
+# 微信配置(开发环境)
+wechat:
+  # 微信开放平台(网站扫码登录)
+  open:
+    app-id: ${WECHAT_APP_ID:wxd5f6a9d072f9a9f0}
+    app-secret: ${WECHAT_APP_SECRET:67d696a9f0a9d072f9a9f0d5f6a9d072}
+    qr-connect:
+      redirect-uri: ${WECHAT_REDIRECT_URI:http://localhost:8001/api/auth/wechat/callback}
+      scope: snsapi_login
+
 # 邀请码配置
 invite:
   base-url: http://localhost:8001
@@ -98,7 +108,7 @@ payment:
 #    alipay-root-cert-path: ${ALIPAY_ROOT_CERT_PATH:certs/dev/alipay/alipayRootCert.crt}
   # 微信支付(开发/测试环境):将测试商户证书文件复制到 certs/dev/wechat/ 目录下
   wechat:
-    app-id: ${WECHAT_PAY_APP_ID:wxf2f254f9981651dd}
+    app-id: ${WECHAT_PAY_APP_ID:wx24a9669e84ba1f0b}
     mch-id: ${WECHAT_PAY_MCH_ID:1612111355}
     api-v3-key: ${WECHAT_PAY_API_V3_KEY:iD1xB1mH6zO7pP2wD1rU4aD2nH7oL2lN}
     # 三份证书文件(从微信支付商户平台下载,放入 certs/dev/wechat/):
@@ -110,3 +120,9 @@ payment:
     public-key-path: ${WECHAT_PAY_PUBLIC_KEY_PATH:certs/dev/wechat/pub_key.pem}              # 微信支付公钥文件
     public-key-id: ${WECHAT_PAY_PUBLIC_KEY_ID:}                                              # 微信支付公钥ID
     notify-url: ${WECHAT_PAY_NOTIFY_URL:https://priceapi.kailin.com.cn/api/payment/callback/wechat}
+  # 微信小程序支付(JSAPI 调起支付签名 appId)
+  # 小程序 JSAPI 下单签名的 appId 必须为小程序自身的 appId,与 Native 扫码的 appId 区分
+  # 不配置时自动回退到 payment.wechat.app-id(当两者相同时可省略)
+  miniapp:
+    app-id: ${WECHAT_PAY_MINIAPP_APP_ID:wx24a9669e84ba1f0b}
+    app-secret: ${WECHAT_PAY_MINIAPP_APP_SECRET:}

+ 8 - 1
zhijiayun-gateway/src/main/resources/application-prod.yml

@@ -47,6 +47,7 @@ jwt:
 
 # 微信配置:生产部署时通过环境变量覆盖
 wechat:
+  # 微信开放平台(网站扫码登录)
   open:
     app-id: ${WECHAT_APP_ID:wxd5f6a9d072f9a9f0}
     app-secret: ${WECHAT_APP_SECRET:67d696a9f0a9d072f9a9f0d5f6a9d072}
@@ -151,7 +152,7 @@ payment:
     sign-type: RSA2
   # 微信支付(生产环境):将生产证书文件复制到 certs/prod/wechat/ 目录下
   wechat:
-    app-id: ${WECHAT_PAY_APP_ID:wxf2f254f9981651dd}
+    app-id: ${WECHAT_PAY_APP_ID:wx24a9669e84ba1f0b}
     mch-id: ${WECHAT_PAY_MCH_ID:1612111355}
     api-v3-key: ${WECHAT_PAY_API_V3_KEY:iD1xB1mH6zO7pP2wD1rU4aD2nH7oL2lN}
     # 三份证书文件(从微信支付商户平台下载,放入 certs/prod/wechat/):
@@ -163,3 +164,9 @@ payment:
     public-key-path: ${WECHAT_PAY_PUBLIC_KEY_PATH:certs/prod/wechat/pub_key.pem}              # 微信支付公钥文件
     public-key-id: ${WECHAT_PAY_PUBLIC_KEY_ID:}                                              # 微信支付公钥ID
     notify-url: ${WECHAT_PAY_NOTIFY_URL:https://priceapi.kailin.com.cn/api/payment/callback/wechat}
+  # 微信小程序支付(JSAPI 调起支付签名 appId)
+  # 小程序 JSAPI 下单签名的 appId 必须为小程序自身的 appId,与 Native 扫码的 appId 区分
+  # 不配置时自动回退到 payment.wechat.app-id(当两者相同时可省略)
+  miniapp:
+    app-id: ${WECHAT_PAY_MINIAPP_APP_ID:wx24a9669e84ba1f0b}
+    app-secret: ${WECHAT_PAY_MINIAPP_APP_SECRET:}

+ 59 - 4
zhijiayun-gateway/src/main/resources/application.yml

@@ -37,10 +37,6 @@ wechat:
     qr-connect:
       redirect-uri: https://your-domain.com/api/auth/wechat/callback
       scope: snsapi_login
-  # 微信小程序配置
-  miniapp:
-    app-id: ${WECHAT_MINIAPP_APP_ID:}
-    app-secret: ${WECHAT_MINIAPP_APP_SECRET:}
 
 # 外部系统(小程序后台)API 配置
 # 注意:API Key 鉴权已升级为数据库管理(t_system_config 表,configType=EXTERNAL_API,config_key=miniapp_external_api)
@@ -53,6 +49,12 @@ external-system:
     api-key: ${EXTERNAL_MINIAPP_API_KEY:a3f8c21e67b94d0e5f1a6c8d3b9e2071}
     connect-timeout: 5000
     read-timeout: 10000
+  # 第三方"智价云药店版"店铺资源同步接口(审核入驻信息通过后推送客户信息)
+  shop-resource:
+    base-url: ${EXTERNAL_SHOP_RESOURCE_API_URL:}
+    app-id: ${EXTERNAL_SHOP_RESOURCE_APP_ID:wx246605ec671bf08d}
+    connect-timeout: 5000
+    read-timeout: 10000
 
 # 应用下载与站点配置
 app:
@@ -126,6 +128,12 @@ payment:
     serial-no: ${WECHAT_PAY_SERIAL_NO:}
     notify-url: ${WECHAT_PAY_NOTIFY_URL:https://your-domain.com/api/payment/callback/wechat}
     platform-cert-path: ${WECHAT_PAY_PLATFORM_CERT_PATH:}
+  # 微信小程序支付(JSAPI 调起支付签名 appId)
+  # 小程序 JSAPI 下单签名的 appId 必须为小程序自身的 appId,与 Native 扫码的 appId 区分
+  # 不配置时自动回退到 payment.wechat.app-id(当两者相同时可省略)
+  miniapp:
+    app-id: ${WECHAT_PAY_MINIAPP_APP_ID:}
+    app-secret: ${WECHAT_PAY_MINIAPP_APP_SECRET:}
   # 支付宝配置(电脑网站支付 alipay.trade.page.pay)
   alipay:
     app-id: ${ALIPAY_APP_ID:}
@@ -164,12 +172,59 @@ gateway:
     default-limit: 100
     default-window: 60
     rules:
+      # 短信验证码:防短信轰炸,10次/分钟
       - path-prefix: /api/auth/sms
         limit: 10
         window: 60
+      # 登录接口:防暴力破解,20次/分钟
       - path-prefix: /api/auth/login
         limit: 20
         window: 60
+      # 第三方换取Token(X-Api-Key鉴权):防暴力试探,30次/分钟
+      - path-prefix: /api/auth/external
+        limit: 30
+        window: 60
+      # 微信登录回调:防刷,20次/分钟
+      - path-prefix: /api/auth/wechat
+        limit: 20
+        window: 60
+      # Token刷新/注册:防刷,30次/分钟
+      - path-prefix: /api/auth/refresh
+        limit: 30
+        window: 60
+      - path-prefix: /api/auth/register
+        limit: 30
+        window: 60
+      # 邀请点击追踪(公开POST写操作,最易被刷):30次/分钟
+      - path-prefix: /api/invite/click
+        limit: 30
+        window: 60
+      # 邀请落地页/解析链接(公开GET):60次/分钟
+      - path-prefix: /api/invite/page
+        limit: 60
+        window: 60
+      - path-prefix: /api/invite/resolve-link
+        limit: 60
+        window: 60
+      # 支付回调(微信/支付宝服务器调用):放宽至60次/分钟,避免正常回调被限
+      - path-prefix: /api/payment/callback
+        limit: 60
+        window: 60
+      - path-prefix: /api/payment/anxin/notify
+        limit: 60
+        window: 60
+      - path-prefix: /api/payment/agreement/notify
+        limit: 60
+        window: 60
+      # 支付宝开放能力回调
+      - path-prefix: /api/payment/alipay
+        limit: 60
+        window: 60
+      # 爬虫查询:60次/分钟
       - path-prefix: /api/crawler
         limit: 60
         window: 60
+      # 搜索建议(公开):60次/分钟
+      - path-prefix: /api/search/suggestions
+        limit: 60
+        window: 60

+ 13 - 0
zhijiayun-gateway/src/main/resources/db/migration-v14.sql

@@ -0,0 +1,13 @@
+-- ============================================
+-- 迁移 v14:入驻信息增加第三方同步状态字段
+-- 说明:
+--   1. t_business_license 新增 sync_status 字段,记录同步到第三方系统(智价云药店版)的状态
+--   2. 状态取值:NOT_SYNCED-未同步 / SYNCED-已同步 / SYNC_FAILED-同步失败
+--   3. 第三方接口不可用时不影响本地审核流程,仅标记同步状态便于后续补推
+-- ============================================
+
+-- sync_status
+SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_business_license' AND COLUMN_NAME = 'sync_status');
+SET @sql = IF(@col_exists = 0, "ALTER TABLE `t_business_license` ADD COLUMN `sync_status` VARCHAR(20) NOT NULL DEFAULT 'NOT_SYNCED' COMMENT '第三方同步状态:NOT_SYNCED-未同步 SYNCED-已同步 SYNC_FAILED-同步失败' AFTER `external_id`", 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

+ 7 - 3
zhijiayun-gateway/src/main/resources/db/schema.sql

@@ -319,10 +319,14 @@ CREATE TABLE IF NOT EXISTS `t_payment_order` (
     PRIMARY KEY (`id`),
     UNIQUE KEY `uk_order_no` (`order_no`),
     KEY `idx_user_id` (`user_id`),
-    KEY `idx_status` (`status`),
-    KEY `idx_expire_time` (`expire_time`)
+    KEY `idx_status_expire_time` (`status`, `expire_time`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付订单表';
 
+-- 支付订单表补充联合索引(兼容旧库的幂等 ALTER:用 idx_status_expire_time 替代原单列 idx_status / idx_expire_time)
+SET @idx_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_payment_order' AND INDEX_NAME = 'idx_status_expire_time');
+SET @sql = IF(@idx_exists = 0, "ALTER TABLE `t_payment_order` ADD KEY `idx_status_expire_time` (`status`, `expire_time`)", 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
 -- 支付订单表补充字段(自动续费扣款相关)
 -- order_type / agreement_id(兼容旧库的幂等 ALTER)
 SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_payment_order' AND COLUMN_NAME = 'order_type');
@@ -674,4 +678,4 @@ CREATE TABLE IF NOT EXISTS `t_alipay_anxin_order_record` (
     KEY `idx_order_id` (`order_id`),
     KEY `idx_user_id` (`user_id`),
     KEY `idx_card_id` (`card_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付宝安心付扣款订单记录表';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付宝安心付扣款订单记录表';

+ 3 - 3
zhijiayun-gateway/src/test/resources/test-schema.sql

@@ -422,6 +422,7 @@ CREATE TABLE IF NOT EXISTS t_business_license (
     show_verified_badge  TINYINT(1)    NOT NULL DEFAULT 0,
     sync_source          VARCHAR(20)   DEFAULT NULL COMMENT '来源:MANUAL/MINIAPP_SYNC',
     external_id          VARCHAR(64)   DEFAULT NULL COMMENT '外部系统记录ID',
+    sync_status          VARCHAR(20)   NOT NULL DEFAULT 'NOT_SYNCED' COMMENT '第三方同步状态:NOT_SYNCED/SYNCED/SYNC_FAILED',
     create_time          TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
     update_time          TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
     INDEX idx_user_id (user_id),
@@ -577,8 +578,7 @@ CREATE TABLE IF NOT EXISTS t_payment_order (
     update_time          TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
     CONSTRAINT uk_order_no UNIQUE (order_no),
     INDEX idx_user_id (user_id),
-    INDEX idx_status (status),
-    INDEX idx_expire_time (expire_time)
+    INDEX idx_status_expire_time (status, expire_time)
 );
 
 -- 支付订单重复支付记录表
@@ -622,4 +622,4 @@ CREATE TABLE IF NOT EXISTS t_system_config (
 
 -- 测试用默认外部 API 配置(config_key 为语义标识,apiKey 存在 config_value JSON 中)
 INSERT IGNORE INTO t_system_config (config_key, config_value, config_type, config_name, enabled, remark) VALUES
-    ('miniapp_external_api', '{"apiKey":"a3f8c21e67b94d0e5f1a6c8d3b9e2071","allowedIps":""}', 'EXTERNAL_API', '测试配置', 1, '测试环境默认API Key');
+    ('miniapp_external_api', '{"apiKey":"a3f8c21e67b94d0e5f1a6c8d3b9e2071","allowedIps":""}', 'EXTERNAL_API', '测试配置', 1, '测试环境默认API Key');

+ 9 - 8
zhijiayun-payment/src/main/java/com/xuekairui/payment/config/PaymentProperties.java

@@ -83,19 +83,20 @@ public class PaymentProperties {
     }
 
     /**
-     * 微信小程序支付配置(JSAPI 调起支付专用
+     * 微信小程序配置(登录 + JSAPI 支付集中管理
      *
-     * <p>小程序 JSAPI 下单签名的 appId 必须为小程序自身的 appId。
-     * 如果 Native 扫码支付用的是公众号/网站应用 appId,
-     * 则需要通过此配置独立指定小程序 appId,避免相互干扰。
+     * <p>小程序登录(jscode2session 换 openid)和 JSAPI 支付(调起支付签名)
+     * 共用同一个小程序 appId/appSecret,统一在此配置,避免散落到 wechat.miniapp。
      *
-     * <p>配置项:payment.miniapp.app-id
-     * <p>不配置时自动回退到 payment.wechat.app-id(兼容旧配置)。
+     * <p>JSAPI 下单签名的 appId 优先取 payment.miniapp.app-id,
+     * 未配置时回退到 payment.wechat.app-id(兼容旧配置)。
      */
     @Data
     public static class Miniapp {
-        /** 微信小程序 appId(JSAPI 下单签名用) */
+        /** 微信小程序 appId(登录 + JSAPI 签名用) */
         private String appId = "";
+        /** 微信小程序 appSecret(jscode2session / 全局 access_token 用) */
+        private String appSecret = "";
     }
 
     /**
@@ -182,4 +183,4 @@ public class PaymentProperties {
         /** 签名算法类型 */
         private String signType = "RSA2";
     }
-}
+}

+ 2 - 2
zhijiayun-payment/src/main/java/com/xuekairui/payment/mapper/PaymentOrderMapper.java

@@ -21,6 +21,6 @@ public interface PaymentOrderMapper extends BaseMapper<PaymentOrder> {
     @Select("SELECT * FROM t_payment_order WHERE user_id = #{userId} ORDER BY create_time DESC")
     List<PaymentOrder> selectByUserId(@Param("userId") Long userId);
 
-    @Select("SELECT * FROM t_payment_order WHERE status = 'PENDING' AND expire_time < #{now}")
+    @Select("SELECT id FROM t_payment_order WHERE status = 'PENDING' AND expire_time < #{now}")
     List<PaymentOrder> selectExpiredPending(@Param("now") LocalDateTime now);
-}
+}

+ 4 - 4
zhijiayun-test/src/main/resources/static/activity-test.html

@@ -1158,7 +1158,7 @@ curl -X POST "$" + "{BASE_URL}/api/auth/external/miniapp-token" \\
             <div class="form-group"><label>三类医疗器械备案(medicalDeviceClass3Url)</label><input type="text" id="external-login-medicalDeviceClass3Url" placeholder="https://oss.example.com/slqx.jpg" value=""></div>
             <div class="form-group"><label>小程序openId(openId)</label><input type="text" id="external-login-openId" placeholder="oJx1x5..." value=""></div>
             <div class="form-group"><label>开放平台unionId(unionId)</label><input type="text" id="external-login-unionId" placeholder="oABCD1234..." value=""></div>
-            <div class="form-group"><label style="display:flex;align-items:center;gap:6px;"><input type="checkbox" id="external-login-licenseApproved" style="width:auto;"> 第三方已审核(licenseApproved)</label><span style="font-size:12px;color:#888;">勾选=直接APPROVED+送30天PRO</span></div>
+            <div class="form-group"><label>审核状态(status)</label><select id="external-login-status" style="width:auto;"><option value="0">0-审核中</option><option value="1">1-审核成功</option><option value="2">2-审核失败</option></select><span style="font-size:12px;color:#888;">1=直接APPROVED+送30天PRO</span></div>
         </div>
         <button class="btn btn-primary" onclick="runExternalMiniappToken()">执行测试</button>
         <div class="response-box" id="external-login-box" style="display:none;">
@@ -1643,9 +1643,9 @@ async function runExternalMiniappToken() {
         const val = document.getElementById(sectionId + '-' + f).value;
         if (val) body[f] = val;
     });
-    // licenseApproved 复选
-    const licApproved = document.getElementById(sectionId + '-licenseApproved');
-    if (licApproved && licApproved.checked) body.licenseApproved = true;
+    // status 下拉
+    const statusSel = document.getElementById(sectionId + '-status');
+    if (statusSel) body.status = parseInt(statusSel.value, 10);
     if (!body.phone) { alert('请填写手机号'); return; }
 
     const box = document.getElementById(sectionId + '-box');

+ 7 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/dto/BusinessLicenseReviewResponse.java

@@ -72,6 +72,12 @@ public class BusinessLicenseReviewResponse {
     /** 来源 */
     private String syncSource;
 
+    /** 外部系统记录ID */
+    private String externalId;
+
+    /** 第三方同步状态:NOT_SYNCED-未同步 SYNCED-已同步 SYNC_FAILED-同步失败 */
+    private String syncStatus;
+
     /** 是否展示认证标识 */
     private Boolean showVerifiedBadge;
 
@@ -100,4 +106,4 @@ public class BusinessLicenseReviewResponse {
 
     /** 提交者用户名(第三方系统传入) */
     private String submitterUserName;
-}
+}

+ 2 - 2
zhijiayun-user/src/main/java/com/xuekairui/user/dto/ExternalTokenRequest.java

@@ -72,6 +72,6 @@ public class ExternalTokenRequest {
     /** 可选:三类医疗器械备案图片URL */
     private String medicalDeviceClass3Url;
 
-    /** 可选:第三方是否已审核通过(true=已审核,跳过我方审核) */
-    private Boolean licenseApproved;
+    /** 可选:入驻审核状态:0-审核中 / 1-审核成功 / 2-审核失败(不传默认0-审核中) */
+    private Integer status;
 }

+ 5 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/entity/BusinessLicense.java

@@ -133,6 +133,10 @@ public class BusinessLicense {
     /** 外部系统记录ID */
     @TableField("external_id")
     private String externalId;
+
+    /** 第三方同步状态:NOT_SYNCED-未同步 SYNCED-已同步 SYNC_FAILED-同步失败 */
+    @TableField("sync_status")
+    private String syncStatus;
     
     /** 备注 */
     @TableField("remark")
@@ -145,4 +149,4 @@ public class BusinessLicense {
     /** 更新时间 */
     @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
     private LocalDateTime updateTime;
-}
+}

+ 2 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/BusinessLicenseMapper.java

@@ -27,6 +27,7 @@ public interface BusinessLicenseMapper extends BaseMapper<BusinessLicense> {
             "bl.store_address, bl.contact_person, bl.contact_phone, bl.credit_code, " +
             "bl.license_image_url, bl.drug_license_url, bl.medical_device_class2_url, bl.medical_device_class3_url, " +
             "bl.review_status, bl.reject_reason, bl.sync_source, bl.show_verified_badge, " +
+            "bl.external_id, bl.sync_status, " +
             "bl.reviewer_id, bl.review_time, bl.create_time, bl.update_time, " +
             "bl.user_id AS submitterUserId, " +
             "u.phone AS submitterPhone, u.nickname AS submitterNickname, u.user_name AS submitterUserName " +
@@ -41,4 +42,4 @@ public interface BusinessLicenseMapper extends BaseMapper<BusinessLicense> {
             "</script>")
     List<BusinessLicenseReviewResponse> selectReviewList(@Param("reviewStatus") String reviewStatus,
                                                          @Param("keyword") String keyword);
-}
+}

+ 11 - 4
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -539,9 +539,16 @@ public class AuthService {
         com.xuekairui.user.entity.BusinessLicense existing =
                 businessLicenseService.getByUserId(userId);
 
-        // 审核状态:第三方已审核 → 直接 APPROVED,否则 PENDING
-        String reviewStatus = Boolean.TRUE.equals(request.getLicenseApproved())
-                ? LicenseStatus.APPROVED.name() : LicenseStatus.PENDING.name();
+        // 审核状态:0-审核中→PENDING / 1-审核成功→APPROVED / 2-审核失败→REJECTED(不传默认0)
+        Integer status = request.getStatus();
+        String reviewStatus;
+        if (status == null || status == 0) {
+            reviewStatus = LicenseStatus.PENDING.name();
+        } else if (status == 1) {
+            reviewStatus = LicenseStatus.APPROVED.name();
+        } else {
+            reviewStatus = LicenseStatus.REJECTED.name();
+        }
 
         if (existing == null) {
             com.xuekairui.user.entity.BusinessLicense license =
@@ -612,7 +619,7 @@ public class AuthService {
             if (isNotBlank(request.getMedicalDeviceClass3Url())) {
                 existing.setMedicalDeviceClass3Url(request.getMedicalDeviceClass3Url());
             }
-            // 已 APPROVED 的不降级;第三方已审核的可从 PENDING/REJECTED 升级到 APPROVED
+            // 已 APPROVED 的不降级(不覆盖);非 APPROVED 状态可随第三方 status 更新(含 PENDING/REJECTED 互转、升级到 APPROVED)
             boolean upgradedToApproved = false;
             if (!LicenseStatus.APPROVED.name().equals(existing.getReviewStatus())) {
                 existing.setReviewStatus(reviewStatus);

+ 35 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/BusinessLicenseService.java

@@ -44,6 +44,7 @@ public class BusinessLicenseService {
     private final ActivityEventRedisService activityEventRedisService;
     private final OperationAuditLogService auditLogService;
     private final UserMapper userMapper;
+    private final ShopResourceSyncService shopResourceSyncService;
 
     // ======================== 查询方法 ========================
 
@@ -224,6 +225,8 @@ public class BusinessLicenseService {
                     } catch (Exception e) {
                         log.warn("存入入驻审核通过事件失败,不影响主流程: userId={}", userId, e);
                     }
+                    // 同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程
+                    syncShopResourceAfterApproved(submitter, license);
                 }
             });
         }
@@ -245,6 +248,38 @@ public class BusinessLicenseService {
                 licenseId, userId, userName, reviewerId);
     }
 
+    /**
+     * 审核通过后同步客户入驻信息到第三方系统(智价云药店版)
+     * <p>
+     * 将客户手机号 + 店铺信息推送到第三方接口,第三方返回该客户的访问令牌。
+     * 同步成功后将第三方 user_id 回写到入驻记录的 externalId 字段,并标记 sync_status=SYNCED。
+     * 第三方不可用或同步失败时标记 sync_status=SYNC_FAILED,不影响本地审核流程,
+     * 后续可通过 sync_status 筛选失败记录进行补推。
+     */
+    private void syncShopResourceAfterApproved(User submitter, BusinessLicense license) {
+        try {
+            ShopResourceSyncService.SyncResult result =
+                    shopResourceSyncService.syncShopResource(submitter, license);
+            if (result != null && result.getUserId() != null) {
+                license.setExternalId(result.getUserId());
+                license.setSyncStatus("SYNCED");
+                businessLicenseMapper.updateById(license);
+                log.info("同步客户入驻信息到第三方成功: licenseId={}, externalUserId={}, newUser={}",
+                        license.getId(), result.getUserId(), result.getNewUser());
+            } else {
+                license.setSyncStatus("SYNC_FAILED");
+                businessLicenseMapper.updateById(license);
+                log.warn("同步客户入驻信息到第三方未成功(第三方未返回user_id),标记为SYNC_FAILED: licenseId={}",
+                        license.getId());
+            }
+        } catch (Exception e) {
+            license.setSyncStatus("SYNC_FAILED");
+            businessLicenseMapper.updateById(license);
+            log.warn("同步客户入驻信息到第三方失败,标记为SYNC_FAILED,不影响主流程: licenseId={}, error={}",
+                    license.getId(), e.getMessage());
+        }
+    }
+
     /**
      * 审核驳回
      *

+ 50 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/ShopResourceSyncService.java

@@ -0,0 +1,50 @@
+package com.xuekairui.user.service;
+
+import com.xuekairui.user.entity.BusinessLicense;
+import com.xuekairui.user.entity.User;
+
+/**
+ * 店铺资源同步服务(对接第三方"智价云药店版"系统)
+ * <p>
+ * 在我方审核入驻信息通过后,将客户店铺信息同步推送至第三方系统,
+ * 由第三方为其开通账号并返回访问令牌。
+ * <p>
+ * 容错策略:同步失败不阻塞主流程,仅记录日志。
+ *
+ * @author ProPrice Team
+ * @since 2026-07-10
+ */
+public interface ShopResourceSyncService {
+
+    /**
+     * 同步客户入驻信息到第三方系统
+     * <p>
+     * 将用户手机号 + 入驻信息(店铺名称、地址、联系人、资质图片等)推送到第三方接口,
+     * 第三方返回该客户的 access_token / refresh_token / user_id。
+     *
+     * @param user    提交用户(提供手机号)
+     * @param license 已审核通过的入驻信息
+     * @return 同步结果,失败返回 null
+     */
+    SyncResult syncShopResource(User user, BusinessLicense license);
+
+    /**
+     * 同步结果
+     */
+    @lombok.Data
+    @lombok.Builder
+    @lombok.NoArgsConstructor
+    @lombok.AllArgsConstructor
+    class SyncResult {
+        /** 第三方系统用户ID */
+        private String userId;
+        /** 访问令牌 */
+        private String accessToken;
+        /** 刷新令牌 */
+        private String refreshToken;
+        /** 令牌有效期(秒) */
+        private Long expiresIn;
+        /** 是否新用户 */
+        private Boolean newUser;
+    }
+}

+ 3 - 3
zhijiayun-user/src/main/java/com/xuekairui/user/service/WechatMiniProgramService.java

@@ -35,10 +35,10 @@ import java.util.concurrent.TimeUnit;
 @RequiredArgsConstructor
 public class WechatMiniProgramService {
 
-    @Value("${wechat.miniapp.app-id:}")
+    @Value("${payment.miniapp.app-id:}")
     private String appId;
 
-    @Value("${wechat.miniapp.app-secret:}")
+    @Value("${payment.miniapp.app-secret:}")
     private String appSecret;
 
     private final RedisTemplate<String, Object> redisTemplate;
@@ -183,4 +183,4 @@ public class WechatMiniProgramService {
         /** 会话密钥 */
         private String sessionKey;
     }
-}
+}

+ 197 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/service/impl/ShopResourceSyncServiceImpl.java

@@ -0,0 +1,197 @@
+package com.xuekairui.user.service.impl;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.xuekairui.user.entity.BusinessLicense;
+import com.xuekairui.user.entity.User;
+import com.xuekairui.user.service.ShopResourceSyncService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 店铺资源同步服务 HTTP 实现(基于 Hutool HttpUtil,与项目现有风格一致)
+ * <p>
+ * 对接第三方"智价云药店版"系统接口:POST /api/sync_shop_resource/report_user
+ * 参数全部以 query 形式传递,返回 JSON。
+ * <p>
+ * 容错策略:外部调用失败时返回 null,不抛异常阻塞主流程。
+ *
+ * @author ProPrice Team
+ * @since 2026-07-10
+ */
+@Slf4j
+@Service
+public class ShopResourceSyncServiceImpl implements ShopResourceSyncService {
+
+    @Value("${external-system.shop-resource.base-url:}")
+    private String baseUrl;
+
+    @Value("${external-system.shop-resource.app-id:wx246605ec671bf08d}")
+    private String appId;
+
+    @Value("${external-system.shop-resource.connect-timeout:5000}")
+    private int connectTimeout;
+
+    @Value("${external-system.shop-resource.read-timeout:10000}")
+    private int readTimeout;
+
+    @Override
+    public SyncResult syncShopResource(User user, BusinessLicense license) {
+        if (isNotConfigured()) {
+            return null;
+        }
+        if (license == null) {
+            log.warn("同步店铺资源跳过:入驻信息为空");
+            return null;
+        }
+
+        try {
+            Map<String, String> params = buildParams(user, license);
+            String url = buildUrl(params);
+            log.info("同步店铺资源到第三方: phone={}, storeName={}",
+                    user != null ? user.getPhone() : null, license.getStoreName());
+
+            String response = doPost(url);
+            if (response == null) {
+                return null;
+            }
+            return parseResponse(response);
+        } catch (Exception e) {
+            log.warn("同步店铺资源到第三方失败: licenseId={}, error={}",
+                    license.getId(), e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 构造请求参数(query 形式)
+     */
+    private Map<String, String> buildParams(User user, BusinessLicense license) {
+        Map<String, String> params = new LinkedHashMap<>();
+        if (user != null && user.getPhone() != null) {
+            params.put("phone", user.getPhone());
+        }
+        params.put("contact_province", nullToEmpty(license.getProvince()));
+        params.put("contact_city", nullToEmpty(license.getCity()));
+        params.put("contact_area", nullToEmpty(license.getDistrict()));
+        params.put("contact_addr", nullToEmpty(license.getStoreAddress()));
+        params.put("contact_name", nullToEmpty(license.getContactPerson()));
+        params.put("contact_shop", nullToEmpty(license.getStoreName()));
+        params.put("shop_type", mapTerminalType(license.getTerminalType()));
+        params.put("contact_phone", nullToEmpty(license.getContactPhone()));
+        params.put("business_license_image", nullToEmpty(license.getLicenseImageUrl()));
+        params.put("drug_business_license_image", nullToEmpty(license.getDrugLicenseUrl()));
+        if (license.getMedicalDeviceClass2Url() != null) {
+            params.put("two_medical_device_registration", license.getMedicalDeviceClass2Url());
+        }
+        if (license.getMedicalDeviceClass3Url() != null) {
+            params.put("three_medical_device_business_license", license.getMedicalDeviceClass3Url());
+        }
+        params.put("app_id", appId);
+        params.put("status", mapReviewStatus(license.getReviewStatus()));
+        return params;
+    }
+
+    /**
+     * 审核状态映射:我方枚举 → 第三方数字编码
+     * PENDING-审核中→0, APPROVED-审核成功→1, REJECTED-审核失败→2, 其他→0
+     */
+    private String mapReviewStatus(String reviewStatus) {
+        if (reviewStatus == null) {
+            return "0";
+        }
+        return switch (reviewStatus) {
+            case "APPROVED" -> "1";
+            case "REJECTED" -> "2";
+            default -> "0";
+        };
+    }
+
+    /**
+     * 终端类型映射:我方枚举 → 第三方数字编码
+     * SINGLE-单店→1, CHAIN-连锁→2, CLINIC/COMMUNITY_HEALTH-诊所社康等→3
+     */
+    private String mapTerminalType(String terminalType) {
+        if (terminalType == null) {
+            return "3";
+        }
+        return switch (terminalType) {
+            case "SINGLE" -> "1";
+            case "CHAIN" -> "2";
+            default -> "3";
+        };
+    }
+
+    /**
+     * 构造带 query 参数的完整 URL
+     */
+    private String buildUrl(Map<String, String> params) {
+        StringBuilder sb = new StringBuilder(baseUrl);
+        sb.append("/api/sync_shop_resource/report_user");
+        sb.append('?');
+        boolean first = true;
+        for (Map.Entry<String, String> e : params.entrySet()) {
+            if (!first) {
+                sb.append('&');
+            }
+            first = false;
+            sb.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8));
+            sb.append('=');
+            sb.append(URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
+        }
+        return sb.toString();
+    }
+
+    private String doPost(String url) {
+        try (HttpResponse response = HttpRequest.post(url)
+                .timeout(connectTimeout + readTimeout)
+                .execute()) {
+            if (response.isOk()) {
+                return response.body();
+            }
+            log.warn("第三方同步店铺资源 POST {} 返回: {} body={}",
+                    url, response.getStatus(), response.body());
+        }
+        return null;
+    }
+
+    private SyncResult parseResponse(String body) {
+        JSONObject json = JSONUtil.parseObj(body);
+        String code = json.getStr("code");
+        if (!"success".equalsIgnoreCase(code)) {
+            log.warn("第三方同步店铺资源返回失败: code={}, msg={}", code, json.getStr("msg"));
+            return null;
+        }
+        JSONObject data = json.getJSONObject("data");
+        if (data == null) {
+            return null;
+        }
+        return SyncResult.builder()
+                .userId(data.getStr("user_id"))
+                .accessToken(data.getStr("access_token"))
+                .refreshToken(data.getStr("refresh_token"))
+                .expiresIn(data.getLong("expires_in"))
+                .newUser(data.getBool("new_user"))
+                .build();
+    }
+
+    private boolean isNotConfigured() {
+        if (baseUrl == null || baseUrl.isBlank()) {
+            log.debug("第三方店铺资源 base-url 未配置,跳过同步");
+            return true;
+        }
+        return false;
+    }
+
+    private static String nullToEmpty(String s) {
+        return s != null ? s : "";
+    }
+}