liuchengsen 1 месяц назад
Родитель
Сommit
449538ddb3

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

@@ -6,9 +6,9 @@
 spring:
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://8.135.32.236:3306/zhijiayun_pharmacy?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
-    username: user
-    password: qPa5*gt8o.
+    url: jdbc:mysql://localhost:3308/zhijiayun_pharmacy?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+    username: root
+    password: 123456
     hikari:
       minimum-idle: 5
       maximum-pool-size: 20

+ 26 - 0
zhijiayun-gateway/src/main/resources/db/migration-v19.sql

@@ -0,0 +1,26 @@
+-- ============================================
+-- 迁移 v19:入驻信息第三方同步差异记录表
+-- 说明:
+--   当我方与第三方都已审核通过(APPROVED)但入驻信息字段不一致时,
+--   不直接覆盖本地数据,而是将差异记录到 t_license_sync_diff 表,
+--   等待运营人工确认后再调整。
+--   status: 0-待处理 1-已采纳第三方 2-已保留本地 3-已手动调整
+-- ============================================
+
+CREATE TABLE IF NOT EXISTS `t_license_sync_diff` (
+    `id`              BIGINT   NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `license_id`      BIGINT   NOT NULL                COMMENT '入驻信息ID(关联 t_business_license.id)',
+    `user_id`         BIGINT   NOT NULL                COMMENT '用户ID',
+    `field_name`      VARCHAR(64)  NOT NULL           COMMENT '不一致的字段名(snake_case)',
+    `local_value`     TEXT         DEFAULT NULL        COMMENT '本地当前值',
+    `third_party_value` TEXT       DEFAULT NULL        COMMENT '第三方传入值',
+    `status`          TINYINT  NOT NULL DEFAULT 0      COMMENT '处理状态:0-待处理 1-已采纳第三方 2-已保留本地 3-已手动调整',
+    `handler_id`      BIGINT   DEFAULT NULL            COMMENT '处理人ID(管理员)',
+    `handle_remark`   VARCHAR(500) DEFAULT NULL        COMMENT '处理备注',
+    `handle_time`     DATETIME DEFAULT NULL            COMMENT '处理时间',
+    `create_time`     DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间(差异检测时间)',
+    PRIMARY KEY (`id`),
+    KEY `idx_license_id` (`license_id`),
+    KEY `idx_user_id` (`user_id`),
+    KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入驻信息第三方同步差异记录';

+ 35 - 2
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteService.java

@@ -426,9 +426,42 @@ public class InviteService {
             }
             out.flush();
         } catch (java.io.IOException e) {
-            log.error("流式下载安装包失败: {}", file.getAbsolutePath(), e);
-            throw new BusinessException(ErrorCode.BUSINESS_ERROR, "下载失败,请重试");
+            if (isClientAbortException(e)) {
+                log.info("用户中断下载安装包(客户端断开): file={}, error={}",
+                        file.getAbsolutePath(), e.getMessage());
+            } else {
+                log.error("流式下载安装包失败: {}", file.getAbsolutePath(), e);
+                throw new BusinessException(ErrorCode.BUSINESS_ERROR, "下载失败,请重试");
+            }
+        }
+    }
+
+    /**
+     * 判断 IOException 是否为客户端主动中断下载导致
+     * <p>
+     * 用户取消下载/关闭浏览器/网络中断时会抛出此类异常,属于正常行为,
+     * 不应记录错误堆栈,仅记录一条 info 日志即可。
+     * 典型特征:ClientAbortException、Broken pipe、Connection reset by peer
+     */
+    private static boolean isClientAbortException(Throwable e) {
+        if (e == null) {
+            return false;
+        }
+        String className = e.getClass().getName();
+        if (className.contains("ClientAbortException")) {
+            return true;
+        }
+        String msg = e.getMessage();
+        if (msg != null) {
+            String lower = msg.toLowerCase();
+            if (lower.contains("broken pipe")
+                    || lower.contains("connection reset")
+                    || lower.contains("connection abort")
+                    || lower.contains("client abort")) {
+                return true;
+            }
         }
+        return isClientAbortException(e.getCause());
     }
 
     /**

+ 64 - 2
zhijiayun-user/src/main/java/com/xuekairui/user/controller/AdminLicenseController.java

@@ -50,7 +50,6 @@ public class AdminLicenseController {
      * <p>
      * 必须选择一个未提交过入驻信息的客户,必填基础信息,
      * 必须上传营业执照、药品经营许可、(二类/三类医疗器械二选一)。
-     *
      * POST /api/admin/license
      */
     @PostMapping
@@ -66,7 +65,6 @@ public class AdminLicenseController {
      * 管理员编辑入驻信息(可修改客户信息、替换资质图片,保存后即时生效)
      * <p>
      * 可编辑全部入驻数据(含已审核),必填项约束同样生效。
-     *
      * PUT /api/admin/license/{licenseId}
      */
     @PutMapping("/{licenseId}")
@@ -95,6 +93,57 @@ public class AdminLicenseController {
         return Result.success();
     }
 
+    /**
+     * 运营端手动同步入驻信息到第三方(智价云药店版)
+     * <p>
+     * 运营在后台选择某条入驻记录,点击"同步到第三方"按钮时调用。
+     * 同步执行(非异步),便于运营立即看到同步结果。
+     * POST /api/admin/license/{licenseId}/sync-third
+     */
+    @PostMapping("/{licenseId}/sync-third")
+    public Result<String> syncToThirdParty(
+            @PathVariable Long licenseId,
+            HttpServletRequest httpRequest) {
+        Long reviewerId = (Long) httpRequest.getAttribute("userId");
+        String result = businessLicenseService.manualSyncToThirdParty(licenseId, reviewerId);
+        return Result.success(result);
+    }
+
+    /**
+     * 查询入驻信息同步差异列表(运营端)
+     * <p>
+     * 当我方与第三方都已审核通过但字段不一致时,差异记录保存到 t_license_sync_diff。
+     * 运营在此查看待处理差异,决定采纳第三方值、保留本地值或手动调整。
+     * GET /api/admin/license/sync-diff?status=0
+     */
+    @GetMapping("/sync-diff")
+    public Result<List<com.xuekairui.user.entity.LicenseSyncDiff>> listSyncDiff(
+            @RequestParam(required = false) Integer status) {
+        return Result.success(businessLicenseService.listSyncDiff(status));
+    }
+
+    /**
+     * 处理入驻信息同步差异(运营端)
+     * <p>
+     * 运营对每条差异记录决定处理方式:
+     * <ul>
+     *   <li>ADOPT_THIRD:采纳第三方值,将本地字段更新为第三方值</li>
+     *   <li>KEEP_LOCAL:保留本地值,不修改数据</li>
+     *   <li>MANUAL:运营已通过编辑接口手动修改入驻信息</li>
+     * </ul>
+     *
+     * POST /api/admin/license/sync-diff/{diffId}/handle
+     */
+    @PostMapping("/sync-diff/{diffId}/handle")
+    public Result<Void> handleSyncDiff(
+            @PathVariable Long diffId,
+            @Valid @RequestBody HandleDiffRequest request,
+            HttpServletRequest httpRequest) {
+        Long reviewerId = (Long) httpRequest.getAttribute("userId");
+        businessLicenseService.handleSyncDiff(diffId, request.getAction(), request.getRemark(), reviewerId);
+        return Result.success();
+    }
+
     @Data
     public static class ReviewRequest {
         @NotNull(message = "入驻信息ID不能为空")
@@ -106,6 +155,19 @@ public class AdminLicenseController {
         private String rejectReason;
     }
 
+    /**
+     * 处理入驻信息同步差异请求
+     */
+    @Data
+    public static class HandleDiffRequest {
+        /** 处理动作:ADOPT_THIRD-采纳第三方 / KEEP_LOCAL-保留本地 / MANUAL-手动调整 */
+        @NotBlank(message = "处理动作不能为空")
+        private String action;
+
+        /** 处理备注(可选) */
+        private String remark;
+    }
+
     /**
      * 管理员新增入驻信息请求
      */

+ 74 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/entity/LicenseSyncDiff.java

@@ -0,0 +1,74 @@
+package com.xuekairui.user.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 入驻信息第三方同步差异记录实体
+ * <p>
+ * 当我方与第三方都已审核通过(APPROVED)但入驻信息字段不一致时,
+ * 不直接覆盖本地数据,而是将差异记录到此表,等待运营人工确认。
+ *
+ * @author ProPrice Team
+ * @since 2026-07-13
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("t_license_sync_diff")
+public class LicenseSyncDiff {
+
+    /** 主键ID */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 入驻信息ID(关联 t_business_license.id) */
+    @TableField("license_id")
+    private Long licenseId;
+
+    /** 用户ID */
+    @TableField("user_id")
+    private Long userId;
+
+    /** 不一致的字段名(snake_case) */
+    @TableField("field_name")
+    private String fieldName;
+
+    /** 本地当前值 */
+    @TableField("local_value")
+    private String localValue;
+
+    /** 第三方传入值 */
+    @TableField("third_party_value")
+    private String thirdPartyValue;
+
+    /** 处理状态:0-待处理 1-已采纳第三方 2-已保留本地 3-已手动调整 */
+    @TableField("status")
+    private Integer status;
+
+    /** 处理人ID(管理员) */
+    @TableField("handler_id")
+    private Long handlerId;
+
+    /** 处理备注 */
+    @TableField("handle_remark")
+    private String handleRemark;
+
+    /** 处理时间 */
+    @TableField("handle_time")
+    private LocalDateTime handleTime;
+
+    /** 创建时间(差异检测时间) */
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}

+ 3 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/enums/OperationSource.java

@@ -18,7 +18,9 @@ public enum OperationSource {
     /** 用户自主提交(客户端用户自行操作) */
     USER_SUBMIT("USER_SUBMIT", "用户自主提交"),
     /** 外部系统(第三方系统同步触发) */
-    EXTERNAL("EXTERNAL", "外部系统");
+    EXTERNAL("EXTERNAL", "外部系统"),
+    /** 系统同步(第三方系统回写本地触发) */
+    SYSTEM_SYNC("SYSTEM_SYNC", "系统同步");
 
     /** 来源代码(存入数据库的值) */
     private final String code;

+ 4 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/enums/OperationType.java

@@ -70,6 +70,10 @@ public enum OperationType {
     LICENSE_RE_REJECT("LICENSE", "RE_REJECT", "LICENSE", "管理员重新驳回入驻信息", true),
     /** 删除入驻信息 */
     LICENSE_DELETE("LICENSE", "DELETE", "LICENSE", "删除入驻信息", true),
+    /** 手动同步入驻信息到第三方 */
+    LICENSE_SYNC_THIRD("LICENSE", "SYNC_THIRD", "LICENSE", "手动同步入驻信息到第三方", true),
+    /** 第三方变更入驻信息回写本地 */
+    LICENSE_THIRD_UPDATE("LICENSE", "THIRD_UPDATE", "LICENSE", "第三方变更入驻信息回写本地", true),
 
     // ==================== 邀请系统模块 ====================
     /** 生成邀请码 */

+ 3 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/enums/OperatorRole.java

@@ -21,7 +21,9 @@ public enum OperatorRole {
     /** 运营人员 */
     ADMIN("ADMIN", "运营人员"),
     /** 超级管理员 */
-    SUPER_ADMIN("SUPER_ADMIN", "超级管理员");
+    SUPER_ADMIN("SUPER_ADMIN", "超级管理员"),
+    /** 系统(第三方系统同步触发,无人工操作) */
+    SYSTEM("SYSTEM", "系统");
 
     /** 角色代码(存入数据库的值) */
     private final String code;

+ 12 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/LicenseSyncDiffMapper.java

@@ -0,0 +1,12 @@
+package com.xuekairui.user.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.xuekairui.user.entity.LicenseSyncDiff;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 入驻信息第三方同步差异记录 Mapper
+ */
+@Mapper
+public interface LicenseSyncDiffMapper extends BaseMapper<LicenseSyncDiff> {
+}

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

@@ -27,6 +27,7 @@ import com.xuekairui.user.dto.WechatLoginRequest;
 import com.xuekairui.user.entity.User;
 import com.xuekairui.user.enums.LicenseStatus;
 import com.xuekairui.user.enums.MembershipLevel;
+import com.xuekairui.user.enums.OperationSource;
 import com.xuekairui.user.enums.OperationType;
 import com.xuekairui.user.enums.OperatorRole;
 import com.xuekairui.user.event.UserRegisteredEvent;
@@ -44,6 +45,7 @@ import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import java.time.LocalDateTime;
+import java.util.Map;
 
 /**
  * 认证服务
@@ -67,6 +69,8 @@ public class AuthService {
     private final ExternalUserSyncService externalUserSyncService;
     private final CertificateSyncService certificateSyncService;
     private final BusinessLicenseService businessLicenseService;
+    private final ShopResourceSyncService shopResourceSyncService;
+    private final com.xuekairui.user.mapper.LicenseSyncDiffMapper licenseSyncDiffMapper;
     private final com.xuekairui.user.mapper.PlatformAccountMapper platformAccountMapper;
     private final com.xuekairui.user.mapper.CrawlerUsageLogMapper crawlerUsageLogMapper;
     private final com.xuekairui.user.mapper.SearchRecordMapper searchRecordMapper;
@@ -136,6 +140,9 @@ public class AuthService {
         }
         auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
                 OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
+
+        // 登录时检查入驻信息,未同步成功则补推一次到第三方
+        syncShopResourceOnLogin(user.getId());
         return response;
     }
 
@@ -160,6 +167,7 @@ public class AuthService {
             user.setLastLoginIp(ip);
             user.setLoginSource("PC");
             userMapper.updateById(user);
+            syncShopResourceOnLogin(user.getId());
             return buildLoginResponse(user);
         }
 
@@ -262,6 +270,7 @@ public class AuthService {
         }
 
         // 4. 生成正式Token
+        syncShopResourceOnLogin(wechatUser.getId());
         return buildLoginResponse(wechatUser);
     }
 
@@ -639,7 +648,9 @@ public class AuthService {
 
     /** 映射第三方 shop_type → 内部 terminalType */
     private static String mapShopType(String shopType) {
-        if (shopType == null) return null;
+        if (shopType == null) {
+            return null;
+        }
         return switch (shopType) {
             case "1" -> "SINGLE";
             case "2" -> "CHAIN";
@@ -648,9 +659,11 @@ public class AuthService {
         };
     }
 
-    /** 映射第三方 status → Integer */
-    private static Integer mapStatus(String status) {
-        if (status == null || status.isBlank()) return 0;
+    /** 映射第三方 status → int */
+    private static int mapStatus(String status) {
+        if (status == null || status.isBlank()) {
+            return 0;
+        }
         try {
             return Integer.parseInt(status);
         } catch (NumberFormatException e) {
@@ -687,7 +700,7 @@ public class AuthService {
     private void syncBusinessLicenseFromShopReport(SyncShopReportRequest req, Long userId) {
         String storeName = isNotBlank(req.getContactShop()) ? req.getContactShop() : null;
         String terminalType = mapShopType(req.getShopType());
-        Integer status = mapStatus(req.getStatus());
+        int status = mapStatus(req.getStatus());
 
         boolean hasLicenseField = isNotBlank(storeName)
                 || isNotBlank(terminalType)
@@ -701,7 +714,7 @@ public class AuthService {
         }
 
         String reviewStatus;
-        if (status == null || status == 0) {
+        if (status == 0) {
             reviewStatus = LicenseStatus.PENDING.name();
         } else if (status == 1) {
             reviewStatus = LicenseStatus.APPROVED.name();
@@ -739,16 +752,36 @@ public class AuthService {
                 grantLicenseApprovedReward(userId, license.getId());
             }
         } else {
-            // 已存在记录:仅允许更新审核状态,其他字段保留原值(后续由运营端确认更新)
+            // 已存在记录:保存第三方变更的入驻信息到数据库
+            // 尤其是双方都审核通过(APPROVED)的情况,需要记录第三方变更的字段
             boolean upgradedToApproved = false;
-            if (!LicenseStatus.APPROVED.name().equals(existing.getReviewStatus())) {
+            boolean wasApproved = LicenseStatus.APPROVED.name().equals(existing.getReviewStatus());
+
+            // 更新审核状态
+            if (!wasApproved) {
+                // 我方未审核通过:允许第三方直接覆盖本地
                 existing.setReviewStatus(reviewStatus);
                 upgradedToApproved = LicenseStatus.APPROVED.name().equals(reviewStatus);
+                applyThirdPartyLicenseFields(existing, req, storeName, terminalType);
+            } else if (LicenseStatus.APPROVED.name().equals(reviewStatus)) {
+                // 双方都审核通过:检测字段是否不一致
+                // 不直接覆盖本地,而是保存差异记录,等待运营人工确认
+                Map<String, String[]> diff = detectLicenseDiff(existing, req, storeName, terminalType);
+                if (!diff.isEmpty()) {
+                    log.info("第三方与本地入驻信息存在差异(双方均已审核通过),保存差异记录待运营确认: userId={}, licenseId={}, diffFields={}",
+                            userId, existing.getId(), diff.keySet());
+                    saveLicenseSyncDiffRecords(userId, existing.getId(), diff);
+                    recordThirdPartyUpdateAudit(req.getPhone(), userId, existing.getId(), diff);
+                }
+            } else {
+                // 本地已审核通过,第三方变为非通过状态:仅更新状态,保留本地字段
+                existing.setReviewStatus(reviewStatus);
             }
+
             businessLicenseService.updateById(existing);
 
-            log.info("第三方店铺同步已存在记录仅更新状态: licenseId={}, userId={}, oldStatus={}, newStatus={}",
-                    existing.getId(), userId, existing.getReviewStatus(), reviewStatus);
+            log.info("第三方店铺同步已存在记录: licenseId={}, userId={}, wasApproved={}, thirdStatus={}, upgradedToApproved={}",
+                    existing.getId(), userId, wasApproved, reviewStatus, upgradedToApproved);
 
             if (upgradedToApproved) {
                 grantLicenseApprovedReward(userId, existing.getId());
@@ -756,6 +789,149 @@ public class AuthService {
         }
     }
 
+    /**
+     * 将双方都审核通过时的字段差异保存到 t_license_sync_diff 表,等待运营确认
+     */
+    private void saveLicenseSyncDiffRecords(Long userId, Long licenseId, Map<String, String[]> diff) {
+        try {
+            diff.forEach((field, values) -> {
+                com.xuekairui.user.entity.LicenseSyncDiff record =
+                        com.xuekairui.user.entity.LicenseSyncDiff.builder()
+                                .licenseId(licenseId)
+                                .userId(userId)
+                                .fieldName(field)
+                                .localValue(values[0])
+                                .thirdPartyValue(values[1])
+                                .status(0)
+                                .build();
+                licenseSyncDiffMapper.insert(record);
+            });
+            log.info("保存入驻信息差异记录成功: licenseId={}, count={}", licenseId, diff.size());
+        } catch (Exception e) {
+            log.warn("保存入驻信息差异记录失败(不阻断同步): licenseId={}, error={}", licenseId, e.getMessage());
+        }
+    }
+
+    /**
+     * 将第三方传入的入驻字段覆盖到本地记录(非空即覆盖)
+     */
+    private void applyThirdPartyLicenseFields(
+            com.xuekairui.user.entity.BusinessLicense existing,
+            SyncShopReportRequest req, String storeName, String terminalType) {
+        if (storeName != null) {
+            existing.setStoreName(storeName);
+        }
+        if (terminalType != null) {
+            existing.setTerminalType(terminalType);
+        }
+        if (isNotBlank(req.getContactProvince())) {
+            existing.setProvince(req.getContactProvince());
+        }
+        if (isNotBlank(req.getContactCity())) {
+            existing.setCity(req.getContactCity());
+        }
+        if (isNotBlank(req.getContactArea())) {
+            existing.setDistrict(req.getContactArea());
+        }
+        if (isNotBlank(req.getContactAddr())) {
+            existing.setStoreAddress(req.getContactAddr());
+        }
+        if (isNotBlank(req.getContactName())) {
+            existing.setContactPerson(req.getContactName());
+        }
+        if (isNotBlank(req.getContactPhone())) {
+            existing.setContactPhone(req.getContactPhone());
+        }
+        if (isNotBlank(req.getBusinessLicenseImage())) {
+            existing.setLicenseImageUrl(req.getBusinessLicenseImage());
+        }
+        if (isNotBlank(req.getDrugBusinessLicenseImage())) {
+            existing.setDrugLicenseUrl(req.getDrugBusinessLicenseImage());
+        }
+        if (isNotBlank(req.getTwoMedicalDeviceRegistration())) {
+            existing.setMedicalDeviceClass2Url(req.getTwoMedicalDeviceRegistration());
+        }
+        if (isNotBlank(req.getThreeMedicalDeviceBusinessLicense())) {
+            existing.setMedicalDeviceClass3Url(req.getThreeMedicalDeviceBusinessLicense());
+        }
+    }
+
+    /**
+     * 检测本地入驻信息与第三方传入字段的不一致项
+     * <p>返回 Map:key=字段名,value=[本地值, 第三方值]
+     */
+    private Map<String, String[]> detectLicenseDiff(
+            com.xuekairui.user.entity.BusinessLicense existing,
+            SyncShopReportRequest req, String storeName, String terminalType) {
+        Map<String, String[]> diff = new java.util.LinkedHashMap<>();
+        if (storeName != null && !storeName.equals(existing.getStoreName())) {
+            diff.put("storeName", new String[]{existing.getStoreName(), storeName});
+        }
+        if (terminalType != null && !terminalType.equals(existing.getTerminalType())) {
+            diff.put("terminalType", new String[]{existing.getTerminalType(), terminalType});
+        }
+        putIfDiff(diff, "province", existing.getProvince(), req.getContactProvince());
+        putIfDiff(diff, "city", existing.getCity(), req.getContactCity());
+        putIfDiff(diff, "district", existing.getDistrict(), req.getContactArea());
+        putIfDiff(diff, "storeAddress", existing.getStoreAddress(), req.getContactAddr());
+        putIfDiff(diff, "contactPerson", existing.getContactPerson(), req.getContactName());
+        putIfDiff(diff, "contactPhone", existing.getContactPhone(), req.getContactPhone());
+        putIfDiff(diff, "licenseImageUrl", existing.getLicenseImageUrl(), req.getBusinessLicenseImage());
+        putIfDiff(diff, "drugLicenseUrl", existing.getDrugLicenseUrl(), req.getDrugBusinessLicenseImage());
+        putIfDiff(diff, "medicalDeviceClass2Url",
+                existing.getMedicalDeviceClass2Url(), req.getTwoMedicalDeviceRegistration());
+        putIfDiff(diff, "medicalDeviceClass3Url",
+                existing.getMedicalDeviceClass3Url(), req.getThreeMedicalDeviceBusinessLicense());
+        return diff;
+    }
+
+    private static void putIfDiff(Map<String, String[]> diff, String field, String local, String third) {
+        if (third != null && !third.isBlank() && !third.equals(local)) {
+            diff.put(field, new String[]{local, third});
+        }
+    }
+
+    /**
+     * 记录第三方变更入驻信息的审计日志
+      * <p>
+     * 第三方同步入驻信息时,我们不对对方谁操作做验证,
+     * 只通过对方传过来的用户手机号判断变更了谁的信息。
+     * 审计日志中记录:被变更用户的手机号、用户ID、变更前后的字段值。
+     *
+     * @param phone     第三方传入的用户手机号(用于识别被变更的用户)
+     * @param userId    被变更的用户ID
+     * @param licenseId 入驻信息ID
+     * @param diff      变更字段差异(key=字段名,value=[本地值, 第三方值])
+     */
+    private void recordThirdPartyUpdateAudit(String phone, Long userId, Long licenseId, Map<String, String[]> diff) {
+        try {
+            Map<String, Object> beforeData = new java.util.LinkedHashMap<>();
+            Map<String, Object> afterData = new java.util.LinkedHashMap<>();
+            diff.forEach((field, values) -> {
+                beforeData.put(field, values[0]);
+                afterData.put(field, values[1]);
+            });
+
+            // 目标标识:用户手机号 + 用户ID,便于审计时定位"变更了谁的信息"
+            String targetIdentifier = String.format("PHONE:%s|USER:%d|LICENSE:%d",
+                    phone != null ? phone : "UNKNOWN", userId, licenseId);
+
+            auditLogService.logSuccess(
+                    0L,
+                    OperatorRole.SYSTEM,
+                    OperationType.LICENSE_THIRD_UPDATE,
+                    targetIdentifier,
+                    licenseId,
+                    beforeData,
+                    afterData,
+                    String.format("第三方变更入驻信息(双方均已审核通过,待运营确认):用户手机号 %s(ID=%d),共 %d 个字段不一致: %s",
+                            phone != null ? phone : "UNKNOWN", userId, diff.size(), diff.keySet()),
+                    OperationSource.SYSTEM_SYNC);
+        } catch (Exception e) {
+            log.warn("记录第三方变更入驻信息审计日志失败: licenseId={}, error={}", licenseId, e.getMessage());
+        }
+    }
+
     /**
      * 第三方登录接口同步入驻信息(适配 ExternalTokenRequest → SyncInfoRequest)
      */
@@ -1101,6 +1277,110 @@ public class AuthService {
                 .build();
     }
 
+    /**
+     * 登录时检查入驻信息并同步到第三方(智价云药店版)
+     * <p>
+     * 仅在我方系统自行登录(短信/密码/微信/小程序)时触发,第三方登录不走此逻辑。
+     * 幂等策略:仅当入驻记录存在且 status != 1(未同步成功)时才补推一次,
+     * 避免每次登录都重复调用第三方接口。
+     * 同步在事务提交后异步执行,不阻塞登录响应,失败仅记录日志。
+     */
+    private void syncShopResourceOnLogin(Long userId) {
+        if (userId == null) {
+            return;
+        }
+        try {
+            com.xuekairui.user.entity.BusinessLicense license =
+                    businessLicenseService.getByUserId(userId);
+            if (license == null) {
+                return;
+            }
+            // status=1 表示已同步成功,跳过;0=未同步,2=同步失败,需补推
+            if (license.getStatus() != null && license.getStatus() == 1) {
+                return;
+            }
+            User user = userMapper.selectById(userId);
+            if (user == null) {
+                return;
+            }
+            String phone = user.getPhone();
+            String userName = user.getNickname() != null
+                    ? user.getNickname() : String.valueOf(userId);
+            String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
+            String targetIdentifier = String.format("PHONE:%s|USER:%d|LICENSE:%d",
+                    phone != null ? phone : "UNKNOWN", userId, license.getId());
+
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    try {
+                        ShopResourceSyncService.SyncResult result =
+                                shopResourceSyncService.syncShopResource(user, license);
+                        if (result != null && result.getUserId() != null) {
+                            license.setExternalId(result.getUserId());
+                            license.setStatus(1);
+                            businessLicenseService.updateById(license);
+                            log.info("登录时同步入驻信息到第三方成功: userId={}, externalUserId={}",
+                                    userId, result.getUserId());
+
+                            auditLogService.logSuccess(
+                                    0L,
+                                    OperatorRole.SYSTEM,
+                                    OperationType.LICENSE_SYNC_THIRD,
+                                    targetIdentifier,
+                                    license.getId(),
+                                    null,
+                                    Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                                            "phone", phone != null ? phone : "UNKNOWN",
+                                            "trigger", "LOGIN",
+                                            "externalUserId", result.getUserId(), "newUser", result.getNewUser()),
+                                    String.format("登录时同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方成功,第三方用户ID:%s",
+                                            userName, phone != null ? phone : "UNKNOWN", storeName, result.getUserId()),
+                                    OperationSource.SYSTEM_SYNC);
+                        } else {
+                            license.setStatus(2);
+                            businessLicenseService.updateById(license);
+                            log.warn("登录时同步入驻信息到第三方未成功(未返回user_id): userId={}", userId);
+
+                            auditLogService.logSuccess(
+                                    0L,
+                                    OperatorRole.SYSTEM,
+                                    OperationType.LICENSE_SYNC_THIRD,
+                                    targetIdentifier,
+                                    license.getId(),
+                                    null,
+                                    Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                                            "phone", phone != null ? phone : "UNKNOWN",
+                                            "trigger", "LOGIN", "result", "no_user_id"),
+                                    String.format("登录时同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方未成功:第三方未返回用户ID",
+                                            userName, phone != null ? phone : "UNKNOWN", storeName),
+                                    OperationSource.SYSTEM_SYNC);
+                        }
+                    } catch (Exception e) {
+                        log.warn("登录时同步入驻信息到第三方失败(不阻断登录): userId={}, error={}",
+                                userId, e.getMessage());
+
+                        auditLogService.logSuccess(
+                                0L,
+                                OperatorRole.SYSTEM,
+                                OperationType.LICENSE_SYNC_THIRD,
+                                targetIdentifier,
+                                license.getId(),
+                                null,
+                                Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                                        "phone", phone != null ? phone : "UNKNOWN",
+                                        "trigger", "LOGIN", "error", e.getMessage()),
+                                String.format("登录时同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方失败:%s",
+                                        userName, phone != null ? phone : "UNKNOWN", storeName, e.getMessage()),
+                                OperationSource.SYSTEM_SYNC);
+                    }
+                }
+            });
+        } catch (Exception e) {
+            log.warn("登录时检查入驻信息失败(不阻断登录): userId={}, error={}", userId, e.getMessage());
+        }
+    }
+
     /**
      * 转换用户信息响应
      */
@@ -1215,6 +1495,7 @@ public class AuthService {
         LoginResponse response = buildLoginResponse(user);
         auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
                 OperationType.USER_LOGIN, "USER:" + user.getId(), user.getId());
+        syncShopResourceOnLogin(user.getId());
         return response;
     }
 
@@ -1360,6 +1641,7 @@ public class AuthService {
         auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
                 isNewLocal ? OperationType.USER_REGISTER : OperationType.USER_LOGIN,
                 "USER:" + user.getId(), user.getId());
+        syncShopResourceOnLogin(user.getId());
         return response;
     }
 
@@ -1416,7 +1698,7 @@ public class AuthService {
             eventPublisher.publishEvent(new UserRegisteredEvent(
                     this, wechatUser.getId(), request.getInviteCode()));
         }
-
+        syncShopResourceOnLogin(wechatUser.getId());
         return buildMiniAppLoginResponse(wechatUser);
     }
 

+ 278 - 5
zhijiayun-user/src/main/java/com/xuekairui/user/service/BusinessLicenseService.java

@@ -46,6 +46,7 @@ public class BusinessLicenseService {
     private final OperationAuditLogService auditLogService;
     private final UserMapper userMapper;
     private final ShopResourceSyncService shopResourceSyncService;
+    private final com.xuekairui.user.mapper.LicenseSyncDiffMapper licenseSyncDiffMapper;
 
     // ======================== 查询方法 ========================
 
@@ -187,7 +188,7 @@ public class BusinessLicenseService {
             TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                 @Override
                 public void afterCommit() {
-                    syncShopResourceToThirdParty(submitter, targetLicense);
+                    syncShopResourceToThirdParty(submitter, targetLicense, "SUBMIT");
                 }
             });
         }
@@ -272,6 +273,14 @@ public class BusinessLicenseService {
                         "reviewStatus", LicenseStatus.PENDING.name(), "syncSource", "MANUAL"),
                 String.format("管理员新增客户「%s」(药店:%s)的入驻信息,待审核确认", userName, storeName),
                 OperationSource.ADMIN_CREATE);
+
+        // 事务提交后同步到第三方系统(staus=PENDING→0),失败不阻塞主流程
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+            @Override
+            public void afterCommit() {
+                syncShopResourceToThirdParty(submitter, license, "CREATE");
+            }
+        });
     }
 
     /**
@@ -367,7 +376,7 @@ public class BusinessLicenseService {
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
             @Override
             public void afterCommit() {
-                syncShopResourceToThirdParty(submitter, license);
+                syncShopResourceToThirdParty(submitter, license, "EDIT");
             }
         });
 
@@ -466,7 +475,7 @@ public class BusinessLicenseService {
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
             @Override
             public void afterCommit() {
-                syncShopResourceToThirdParty(submitter, license);
+                syncShopResourceToThirdParty(submitter, license, "APPROVE");
             }
         });
 
@@ -496,8 +505,20 @@ public class BusinessLicenseService {
      * 同步成功后将第三方 user_id 回写到入驻记录的 externalId 字段,并标记 status=1。
      * 第三方不可用或同步失败时标记 status=2,不影响本地主流程,
      * 后续可通过 status 筛选失败记录进行补推。
+     * <p>
+     * 审计日志记录同步结果,通过用户手机号标识"同步了谁的信息"。
+     *
+     * @param trigger 触发场景:APPROVE-审核通过 / REJECT-审核驳回 / EDIT-编辑 / CREATE-新增 / SUBMIT-用户提交
      */
-    private void syncShopResourceToThirdParty(User submitter, BusinessLicense license) {
+    private void syncShopResourceToThirdParty(User submitter, BusinessLicense license, String trigger) {
+        Long userId = license.getUserId();
+        String phone = submitter != null ? submitter.getPhone() : null;
+        String userName = submitter != null && submitter.getNickname() != null
+                ? submitter.getNickname() : String.valueOf(userId);
+        String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
+        String targetIdentifier = String.format("PHONE:%s|USER:%d|LICENSE:%d",
+                phone != null ? phone : "UNKNOWN", userId, license.getId());
+
         try {
             ShopResourceSyncService.SyncResult result =
                     shopResourceSyncService.syncShopResource(submitter, license);
@@ -507,17 +528,60 @@ public class BusinessLicenseService {
                 businessLicenseMapper.updateById(license);
                 log.info("同步客户入驻信息到第三方成功: licenseId={}, externalUserId={}, newUser={}",
                         license.getId(), result.getUserId(), result.getNewUser());
+
+                auditLogService.logSuccess(
+                        0L,
+                        OperatorRole.SYSTEM,
+                        OperationType.LICENSE_SYNC_THIRD,
+                        targetIdentifier,
+                        license.getId(),
+                        null,
+                        Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                                "phone", phone != null ? phone : "UNKNOWN",
+                                "trigger", trigger,
+                                "externalUserId", result.getUserId(), "newUser", result.getNewUser()),
+                        String.format("同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方成功[%s],第三方用户ID:%s",
+                                userName, phone != null ? phone : "UNKNOWN", storeName, trigger, result.getUserId()),
+                        OperationSource.SYSTEM_SYNC);
             } else {
                 license.setStatus(2);
                 businessLicenseMapper.updateById(license);
                 log.warn("同步客户入驻信息到第三方未成功(第三方未返回user_id),标记为2: licenseId={}",
                         license.getId());
+
+                auditLogService.logSuccess(
+                        0L,
+                        OperatorRole.SYSTEM,
+                        OperationType.LICENSE_SYNC_THIRD,
+                        targetIdentifier,
+                        license.getId(),
+                        null,
+                        Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                                "phone", phone != null ? phone : "UNKNOWN",
+                                "trigger", trigger, "result", "no_user_id"),
+                        String.format("同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方未成功[%s]:第三方未返回用户ID",
+                                userName, phone != null ? phone : "UNKNOWN", storeName, trigger),
+                        OperationSource.SYSTEM_SYNC);
             }
         } catch (Exception e) {
             license.setStatus(2);
             businessLicenseMapper.updateById(license);
             log.warn("同步客户入驻信息到第三方失败,标记为2,不影响主流程: licenseId={}, error={}",
                     license.getId(), e.getMessage());
+
+            auditLogService.logSuccess(
+                    0L,
+                    OperatorRole.SYSTEM,
+                    OperationType.LICENSE_SYNC_THIRD,
+                    targetIdentifier,
+                    license.getId(),
+                    null,
+                    Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                            "phone", phone != null ? phone : "UNKNOWN",
+                            "trigger", trigger, "error", e.getMessage()),
+                    String.format("同步客户「%s」(手机号:%s,药店:%s)的入驻信息到第三方失败[%s]:%s",
+                            userName, phone != null ? phone : "UNKNOWN", storeName, trigger, e.getMessage()),
+                    OperationSource.SYSTEM_SYNC);
         }
     }
 
@@ -594,11 +658,220 @@ public class BusinessLicenseService {
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
             @Override
             public void afterCommit() {
-                syncShopResourceToThirdParty(submitter, license);
+                syncShopResourceToThirdParty(submitter, license, "REJECT");
             }
         });
     }
 
+    /**
+     * 运营端手动同步入驻信息到第三方系统(智价云药店版)
+     * <p>
+     * 运营在后台选择某条入驻记录,点击"同步到第三方"按钮时调用。
+     * 同步成功后回写 externalId 并标记 status=1,失败标记 status=2。
+     * 与审核流程不同,此方法同步执行(非 afterCommit),便于运营立即看到同步结果。
+     *
+     * @param licenseId  入驻信息 ID
+     * @param reviewerId 操作管理员 ID
+     * @return 同步结果描述(成功/失败原因)
+     */
+    @Transactional
+    public String manualSyncToThirdParty(Long licenseId, Long reviewerId) {
+        BusinessLicense license = getById(licenseId);
+        if (license == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND, "入驻信息记录不存在");
+        }
+
+        Long userId = license.getUserId();
+        User submitter = userMapper.selectById(userId);
+        if (submitter == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND, "客户(ID=" + userId + ")不存在");
+        }
+
+        String userName = submitter.getNickname() != null
+                ? submitter.getNickname() : String.valueOf(userId);
+        String storeName = license.getStoreName() != null ? license.getStoreName() : "未知药店";
+
+        try {
+            ShopResourceSyncService.SyncResult result =
+                    shopResourceSyncService.syncShopResource(submitter, license);
+            if (result != null && result.getUserId() != null) {
+                license.setExternalId(result.getUserId());
+                license.setStatus(1);
+                businessLicenseMapper.updateById(license);
+                log.info("运营手动同步入驻信息到第三方成功: licenseId={}, externalUserId={}, newUser={}",
+                        licenseId, result.getUserId(), result.getNewUser());
+
+                auditLogService.logSuccess(
+                        reviewerId != null ? reviewerId : 0L,
+                        OperatorRole.ADMIN,
+                        OperationType.LICENSE_SYNC_THIRD,
+                        "LICENSE:" + license.getId(),
+                        license.getId(),
+                        null,
+                        Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                                "externalUserId", result.getUserId(), "newUser", result.getNewUser()),
+                        String.format("手动同步客户「%s」(药店:%s)的入驻信息到第三方,第三方用户ID:%s",
+                                userName, storeName, result.getUserId()),
+                        OperationSource.ADMIN_CREATE);
+                return "同步成功,第三方用户ID:" + result.getUserId();
+            } else {
+                license.setStatus(2);
+                businessLicenseMapper.updateById(license);
+                log.warn("运营手动同步入驻信息到第三方未成功(未返回user_id): licenseId={}", licenseId);
+
+                auditLogService.logSuccess(
+                        reviewerId != null ? reviewerId : 0L,
+                        OperatorRole.ADMIN,
+                        OperationType.LICENSE_SYNC_THIRD,
+                        "LICENSE:" + license.getId(),
+                        license.getId(),
+                        null,
+                        Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                                "result", "no_user_id"),
+                        String.format("手动同步客户「%s」(药店:%s)的入驻信息到第三方未成功:第三方未返回用户ID",
+                                userName, storeName),
+                        OperationSource.ADMIN_CREATE);
+                return "同步未成功:第三方未返回用户ID,请稍后重试";
+            }
+        } catch (Exception e) {
+            license.setStatus(2);
+            businessLicenseMapper.updateById(license);
+            log.warn("运营手动同步入驻信息到第三方失败: licenseId={}, error={}", licenseId, e.getMessage());
+
+            auditLogService.logSuccess(
+                    reviewerId != null ? reviewerId : 0L,
+                    OperatorRole.ADMIN,
+                    OperationType.LICENSE_SYNC_THIRD,
+                    "LICENSE:" + license.getId(),
+                    license.getId(),
+                    null,
+                    Map.of("userId", userId, "userName", userName, "storeName", storeName,
+                            "error", e.getMessage()),
+                    String.format("手动同步客户「%s」(药店:%s)的入驻信息到第三方失败:%s",
+                            userName, storeName, e.getMessage()),
+                    OperationSource.ADMIN_CREATE);
+            return "同步失败:" + e.getMessage();
+        }
+    }
+
+    // ======================== 运营侧:第三方同步差异处理 ========================
+
+    /**
+     * 查询入驻信息同步差异列表(运营端)
+     * <p>
+     * 当我方与第三方都已审核通过但字段不一致时,差异记录保存到 t_license_sync_diff。
+     * 运营在此查看待处理差异,决定采纳第三方值、保留本地值或手动调整。
+     *
+     * @param status 处理状态筛选(可选):0-待处理 1-已采纳第三方 2-已保留本地 3-已手动调整
+     * @return 差异记录列表(按创建时间倒序)
+     */
+    public List<com.xuekairui.user.entity.LicenseSyncDiff> listSyncDiff(Integer status) {
+        LambdaQueryWrapper<com.xuekairui.user.entity.LicenseSyncDiff> wrapper =
+                new LambdaQueryWrapper<com.xuekairui.user.entity.LicenseSyncDiff>()
+                        .orderByDesc(com.xuekairui.user.entity.LicenseSyncDiff::getCreateTime);
+        if (status != null) {
+            wrapper.eq(com.xuekairui.user.entity.LicenseSyncDiff::getStatus, status);
+        }
+        return licenseSyncDiffMapper.selectList(wrapper);
+    }
+
+    /**
+     * 处理入驻信息同步差异(运营端)
+     * <p>
+     * 运营对每条差异记录决定处理方式:
+     * <ul>
+     *   <li>采纳第三方(action=ADOPT_THIRD):将本地字段更新为第三方值</li>
+     *   <li>保留本地(action=KEEP_LOCAL):不修改本地数据,仅标记差异已处理</li>
+     *   <li>手动调整(action=MANUAL):运营手动修改入驻信息后,标记差异已处理</li>
+     * </ul>
+     *
+     * @param diffId     差异记录 ID
+     * @param action     处理动作:ADOPT_THIRD / KEEP_LOCAL / MANUAL
+     * @param remark     处理备注(可选)
+     * @param reviewerId 操作管理员 ID
+     */
+    @Transactional
+    public void handleSyncDiff(Long diffId, String action, String remark, Long reviewerId) {
+        com.xuekairui.user.entity.LicenseSyncDiff diff = licenseSyncDiffMapper.selectById(diffId);
+        if (diff == null) {
+            throw new BusinessException(ErrorCode.NOT_FOUND, "差异记录不存在");
+        }
+        if (diff.getStatus() != null && diff.getStatus() != 0) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST, "该差异记录已处理,无法重复操作");
+        }
+
+        int newStatus;
+        if ("ADOPT_THIRD".equals(action)) {
+            // 采纳第三方:将本地入驻信息字段更新为第三方值
+            applyDiffToLicense(diff.getLicenseId(), diff.getFieldName(), diff.getThirdPartyValue());
+            newStatus = 1;
+        } else if ("KEEP_LOCAL".equals(action)) {
+            // 保留本地:不修改数据
+            newStatus = 2;
+        } else if ("MANUAL".equals(action)) {
+            // 手动调整:运营已通过编辑接口修改入驻信息
+            newStatus = 3;
+        } else {
+            throw new BusinessException(ErrorCode.PARAM_ERROR,
+                    "处理动作无效,请使用 ADOPT_THIRD / KEEP_LOCAL / MANUAL");
+        }
+
+        diff.setStatus(newStatus);
+        diff.setHandlerId(reviewerId);
+        diff.setHandleRemark(remark);
+        diff.setHandleTime(LocalDateTime.now());
+        licenseSyncDiffMapper.updateById(diff);
+
+        log.info("运营处理入驻信息差异: diffId={}, licenseId={}, action={}, reviewerId={}",
+                diffId, diff.getLicenseId(), action, reviewerId);
+
+        auditLogService.logSuccess(
+                reviewerId != null ? reviewerId : 0L,
+                OperatorRole.ADMIN,
+                OperationType.LICENSE_THIRD_UPDATE,
+                "LICENSE_DIFF:" + diff.getId(),
+                diff.getLicenseId(),
+                Map.of("fieldName", diff.getFieldName(),
+                        "localValue", diff.getLocalValue(),
+                        "thirdPartyValue", diff.getThirdPartyValue()),
+                Map.of("action", action, "status", newStatus, "remark", remark),
+                String.format("处理入驻信息差异(%s):字段=%s,动作=%s",
+                        diff.getFieldName(), diff.getFieldName(), action),
+                OperationSource.ADMIN_CREATE);
+    }
+
+    /**
+     * 将差异记录中的第三方值应用到本地入驻信息
+     */
+    private void applyDiffToLicense(Long licenseId, String fieldName, String thirdPartyValue) {
+        BusinessLicense license = getById(licenseId);
+        if (license == null) {
+            log.warn("采纳第三方差异时入驻记录不存在: licenseId={}", licenseId);
+            return;
+        }
+        switch (fieldName) {
+            case "storeName" -> license.setStoreName(thirdPartyValue);
+            case "terminalType" -> license.setTerminalType(thirdPartyValue);
+            case "province" -> license.setProvince(thirdPartyValue);
+            case "city" -> license.setCity(thirdPartyValue);
+            case "district" -> license.setDistrict(thirdPartyValue);
+            case "storeAddress" -> license.setStoreAddress(thirdPartyValue);
+            case "contactPerson" -> license.setContactPerson(thirdPartyValue);
+            case "contactPhone" -> license.setContactPhone(thirdPartyValue);
+            case "licenseImageUrl" -> license.setLicenseImageUrl(thirdPartyValue);
+            case "drugLicenseUrl" -> license.setDrugLicenseUrl(thirdPartyValue);
+            case "medicalDeviceClass2Url" -> license.setMedicalDeviceClass2Url(thirdPartyValue);
+            case "medicalDeviceClass3Url" -> license.setMedicalDeviceClass3Url(thirdPartyValue);
+            default -> {
+                log.warn("未知的差异字段名,跳过应用: fieldName={}", fieldName);
+                return;
+            }
+        }
+        businessLicenseMapper.updateById(license);
+        log.info("采纳第三方值更新入驻信息: licenseId={}, field={}, value={}",
+                licenseId, fieldName, thirdPartyValue);
+    }
+
     /**
      * 校验入驻信息是否可审核(存在 + 状态为 PENDING)
      */