Przeglądaj źródła

第三方接口调整提交代码

liuchengsen 1 miesiąc temu
rodzic
commit
3f2724211c

+ 5 - 2
docs/第三方店铺同步接口文档.md

@@ -140,10 +140,13 @@
 
 
 ### 5.2 入驻信息处理
 ### 5.2 入驻信息处理
 
 
+> 💡 **核心原则:以第三方传入的最新数据为准**,非空字段直接覆盖本地记录。
+
 | 场景 | 行为 |
 | 场景 | 行为 |
 |------|------|
 |------|------|
 | 无入驻记录 | 创建新记录,写入全部证照和店铺信息 |
 | 无入驻记录 | 创建新记录,写入全部证照和店铺信息 |
-| 已有入驻记录 | 更新非空字段(证照 URL、店铺信息等) |
+| 已有入驻记录(非 APPROVED) | 以第三方传入字段覆盖本地,同时更新审核状态 |
+| 已有入驻记录(已 APPROVED) | 以第三方传入字段覆盖本地,重置为 PENDING 待重新审核;奖励标记保留,不重复发放 |
 
 
 ### 5.3 审核状态
 ### 5.3 审核状态
 
 
@@ -153,7 +156,7 @@
 | `1` | `APPROVED` | 审核通过,**首次通过赠送 30 天 PRO 高级会员** |
 | `1` | `APPROVED` | 审核通过,**首次通过赠送 30 天 PRO 高级会员** |
 | `2` | `REJECTED` | 审核失败 |
 | `2` | `REJECTED` | 审核失败 |
 
 
-> 💡 已 APPROVED 的状态不会因后续调用而降级
+> 💡 已 APPROVED 的记录再次同步时,更新入驻字段并重置为 PENDING 待重新审核;奖励仅首次发放,不会重复
 
 
 ### 5.4 shopType 映射
 ### 5.4 shopType 映射
 
 

+ 9 - 6
docs/第三方登录接口文档.md

@@ -313,16 +313,19 @@ X-Api-Key: {我方提供的API Key}
 - `status=1`:审核状态设为 `APPROVED`(审核通过),**首次通过赠送 30 天 PRO 高级会员**
 - `status=1`:审核状态设为 `APPROVED`(审核通过),**首次通过赠送 30 天 PRO 高级会员**
 - `status=0` 或不传:审核状态设为 `PENDING`(待运营审核)
 - `status=0` 或不传:审核状态设为 `PENDING`(待运营审核)
 - `status=2`:审核状态设为 `REJECTED`(审核失败)
 - `status=2`:审核状态设为 `REJECTED`(审核失败)
-- 已 APPROVED 状态**不降级**(不会因后续调用覆盖)
 - PENDING/REJECTED 状态可随 `status` 更新(含互转、升级到 APPROVED)
 - PENDING/REJECTED 状态可随 `status` 更新(含互转、升级到 APPROVED)
+- **已 APPROVED 的记录再次同步时**:更新入驻字段并重置为 PENDING,需重新审核;奖励仅首次发放,不会重复
 
 
 ### 4.4 入驻信息更新规则
 ### 4.4 入驻信息更新规则
 
 
+> 💡 **核心原则:以第三方传入的最新数据为准**,非空字段直接覆盖本地记录。
+
 | 场景 | 行为 |
 | 场景 | 行为 |
 |------|------|
 |------|------|
 | **首次同步**(无入驻记录) | 创建新记录,写入全部字段 |
 | **首次同步**(无入驻记录) | 创建新记录,写入全部字段 |
-| **已存在记录** | **仅更新审核状态**,店名、证照图片等所有字段保留原值 |
-| 需修改入驻信息 | 通过运营端人工审核后更新 |
+| **已存在记录(非 APPROVED)** | 以第三方传入字段覆盖本地,同时更新审核状态 |
+| **已存在记录(已 APPROVED)** | 以第三方传入字段覆盖本地,状态重置为 PENDING 待重新审核;奖励标记保留,不重复发放 |
+| 需修改入驻信息 | 第三方再次调用同步接口即可更新,无需人工介入 |
 
 
 ### 4.5 请求示例
 ### 4.5 请求示例
 
 
@@ -411,10 +414,10 @@ Authorization: Bearer {currentAccessToken}
 | 8 | `skipRefreshToken=true` 时 refreshToken 返回 null |
 | 8 | `skipRefreshToken=true` 时 refreshToken 返回 null |
 | 9 | 每次刷新后旧 refreshToken 立即作废,请保存最新的 |
 | 9 | 每次刷新后旧 refreshToken 立即作废,请保存最新的 |
 | 10 | 同步接口仅更新非空字段,不会清空已有数据 |
 | 10 | 同步接口仅更新非空字段,不会清空已有数据 |
-| 11 | `status=1` 时入驻状态直接审核通过,**首次**通过赠送 30 天 PRO 高级会员 |
-| 12 | 已审核通过的入驻记录(APPROVED)不会因后续调用而降级 |
+| 11 | `status=1` 时入驻状态直接审核通过,**首次**通过赠送 30 天 PRO 高级会员(不重复) |
+| 12 | 已审核通过的入驻记录再次同步时,更新字段并重置为 PENDING 待重新审核,不重复奖励 |
 | 13 | 传入 `openId`/`unionId` 后,小程序支付无需再次 `wx.login` 授权 |
 | 13 | 传入 `openId`/`unionId` 后,小程序支付无需再次 `wx.login` 授权 |
-| 14 | **入驻信息已存在时仅更新审核状态**,其他字段保留原值,需通过运营端修改 |
+| 14 | **入驻信息已存在时允许更新字段**,审核通过后的记录重新同步会触发重新审核流程 |
 
 
 ---
 ---
 
 

+ 2 - 2
zhijiayun-activity/src/main/java/com/xuekairui/activity/service/CheckInConfigService.java

@@ -138,7 +138,7 @@ public class CheckInConfigService {
     public void updatePendingPeriodsConfig(CheckInConfig config) {
     public void updatePendingPeriodsConfig(CheckInConfig config) {
         com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CheckInPeriod> wrapper =
         com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CheckInPeriod> wrapper =
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
-        wrapper.eq(CheckInPeriod::getStatus, "PENDING");
+        wrapper.in(CheckInPeriod::getStatus, "PENDING", "ACTIVE");
         List<CheckInPeriod> pendingPeriods = periodMapper.selectList(wrapper);
         List<CheckInPeriod> pendingPeriods = periodMapper.selectList(wrapper);
         if (pendingPeriods.isEmpty()) {
         if (pendingPeriods.isEmpty()) {
             return;
             return;
@@ -150,7 +150,7 @@ public class CheckInConfigService {
             period.setRewardLevel(orDefault(config.getRewardLevel(), "PRO"));
             period.setRewardLevel(orDefault(config.getRewardLevel(), "PRO"));
             periodMapper.updateById(period);
             periodMapper.updateById(period);
         }
         }
-        log.info("已同步{}个PENDING周期的区分信息", pendingPeriods.size());
+        log.info("已同步{}个PENDING/ACTIVE周期的区分信息", pendingPeriods.size());
     }
     }
     
     
     /**
     /**

+ 16 - 0
zhijiayun-gateway/src/main/resources/db/migration-v20.sql

@@ -0,0 +1,16 @@
+-- ============================================
+-- 迁移 v20:入驻信息审核通过后允许再修改重审 + 奖励防重复
+-- 说明:
+--   1. t_business_license 新增 reward_granted 字段,标记入驻奖励是否已发放
+--      首次审核通过发放奖励后置1,后续重新编辑→重新审核通过不再发放奖励
+--   2. 同时支持第三方和内部调用修改已审核通过的入驻信息后重置为PENDING重新审核
+-- ============================================
+
+-- 新增 reward_granted 字段:入驻奖励是否已发放(0-未发放 1-已发放)
+-- 用于防止入驻信息审核通过→编辑→重新审核通过时的重复奖励
+SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_business_license' AND COLUMN_NAME = 'reward_granted');
+SET @sql = IF(@col_exists = 0,
+    "ALTER TABLE `t_business_license` ADD COLUMN `reward_granted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '入驻奖励是否已发放:0-未发放 1-已发放(首次审核通过后置1,防止重新审核时重复奖励)' AFTER `show_verified_badge`",
+    'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

+ 4 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/entity/BusinessLicense.java

@@ -126,6 +126,10 @@ public class BusinessLicense {
     @TableField("show_verified_badge")
     @TableField("show_verified_badge")
     private Boolean showVerifiedBadge;
     private Boolean showVerifiedBadge;
 
 
+    /** 入驻奖励是否已发放:0-未发放 1-已发放(首次审核通过后置1,防止重新审核重复奖励) */
+    @TableField("reward_granted")
+    private Boolean rewardGranted;
+
     /** 来源:MANUAL-手动上传, MINIAPP_SYNC-外部系统同步 */
     /** 来源:MANUAL-手动上传, MINIAPP_SYNC-外部系统同步 */
     @TableField("sync_source")
     @TableField("sync_source")
     private String syncSource;
     private String syncSource;

+ 6 - 4
zhijiayun-user/src/main/java/com/xuekairui/user/enums/LicenseStatus.java

@@ -3,7 +3,7 @@ package com.xuekairui.user.enums;
 /**
 /**
  * 入驻信息审核状态枚举
  * 入驻信息审核状态枚举
  * <p>
  * <p>
- * 四个状态及其流转:
+ * 状态流转:
  * <pre>
  * <pre>
  *   NOT_SUBMITTED(待入驻,无 DB 记录)
  *   NOT_SUBMITTED(待入驻,无 DB 记录)
  *        │  用户提交入驻信息
  *        │  用户提交入驻信息
@@ -14,11 +14,13 @@ package com.xuekairui.user.enums;
  *   APPROVED    REJECTED
  *   APPROVED    REJECTED
  *  (已通过)    (已驳回)
  *  (已通过)    (已驳回)
  *      │           │
  *      │           │
- *      │   用户重新提交
- *      │           │
+ *      │  编辑/第三方同步     │  用户重新提交
  *      ▼           ▼
  *      ▼           ▼
- *   保持        PENDING
+ *   PENDING      PENDING
+ *  (重新审核)   (重新审核)
  * </pre>
  * </pre>
+ * <p>审核通过后允许编辑(运营端/第三方),编辑后状态重置为PENDING需重新审核。
+ * 首次审核通过的奖励(会员+邀请)通过 rewardGranted 标记保证仅发放一次。
  *
  *
  * @author ProPrice Team
  * @author ProPrice Team
  * @since 2026-07-09
  * @since 2026-07-09

+ 89 - 26
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -741,36 +741,29 @@ public class AuthService {
                 grantLicenseApprovedReward(userId, license.getId());
                 grantLicenseApprovedReward(userId, license.getId());
             }
             }
         } else {
         } else {
-            // 已存在记录:保存第三方变更的入驻信息到数据库
-            // 尤其是双方都审核通过(APPROVED)的情况,需要记录第三方变更的字段
+            // 已存在记录:更新入驻字段,原已审核通过的记录重置为PENDING重新审核
             boolean upgradedToApproved = false;
             boolean upgradedToApproved = false;
             boolean wasApproved = LicenseStatus.APPROVED.name().equals(existing.getReviewStatus());
             boolean wasApproved = LicenseStatus.APPROVED.name().equals(existing.getReviewStatus());
 
 
-            // 更新审核状态
             if (!wasApproved) {
             if (!wasApproved) {
-                // 我方未审核通过:允许第三方直接覆盖本地
+                // 本地未审核通过:允许第三方直接覆盖状态
                 existing.setReviewStatus(reviewStatus);
                 existing.setReviewStatus(reviewStatus);
                 upgradedToApproved = LicenseStatus.APPROVED.name().equals(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 {
             } else {
-                // 本地已审核通过,第三方变为非通过状态:仅更新状态,保留本地字段
-                existing.setReviewStatus(reviewStatus);
+                // 本地已审核通过,第三方再次同步时更新字段并重置为PENDING重新审核
+                // rewardGranted保持不变,确保不重复奖励
+                existing.setReviewStatus(LicenseStatus.PENDING.name());
+                existing.setShowVerifiedBadge(false);
+                log.info("第三方店铺同步,原已审核通过,更新入驻字段并重置为PENDING待重新审核: licenseId={}, userId={}",
+                        existing.getId(), userId);
             }
             }
 
 
+            // 更新第三方传入的入驻字段(非空即覆盖)
+            applyThirdPartyLicenseFields(existing, req, storeName, terminalType);
             businessLicenseService.updateById(existing);
             businessLicenseService.updateById(existing);
 
 
-            log.info("第三方店铺同步已存在记录: licenseId={}, userId={}, wasApproved={}, thirdStatus={}, upgradedToApproved={}",
-                    existing.getId(), userId, wasApproved, reviewStatus, upgradedToApproved);
+            log.info("第三方店铺同步已存在记录: licenseId={}, userId={}, wasApproved={}, newStatus={}, upgradedToApproved={}",
+                    existing.getId(), userId, wasApproved, existing.getReviewStatus(), upgradedToApproved);
 
 
             if (upgradedToApproved) {
             if (upgradedToApproved) {
                 grantLicenseApprovedReward(userId, existing.getId());
                 grantLicenseApprovedReward(userId, existing.getId());
@@ -1003,16 +996,29 @@ public class AuthService {
                 grantLicenseApprovedReward(userId, license.getId());
                 grantLicenseApprovedReward(userId, license.getId());
             }
             }
         } else {
         } else {
-            // 已存在记录:仅允许更新审核状态,其他字段保留原值(后续由运营端确认更新)
+            // 已存在记录:更新入驻字段,原已审核通过的记录重置为PENDING重新审核
+            boolean wasApproved = LicenseStatus.APPROVED.name().equals(existing.getReviewStatus());
             boolean upgradedToApproved = false;
             boolean upgradedToApproved = false;
-            if (!LicenseStatus.APPROVED.name().equals(existing.getReviewStatus())) {
+
+            if (!wasApproved) {
+                // 本地未审核通过:允许第三方直接覆盖状态
                 existing.setReviewStatus(reviewStatus);
                 existing.setReviewStatus(reviewStatus);
                 upgradedToApproved = LicenseStatus.APPROVED.name().equals(reviewStatus);
                 upgradedToApproved = LicenseStatus.APPROVED.name().equals(reviewStatus);
+            } else {
+                // 本地已审核通过,第三方再次同步时更新字段并重置为PENDING重新审核
+                // rewardGranted保持不变,确保不重复奖励
+                existing.setReviewStatus(LicenseStatus.PENDING.name());
+                existing.setShowVerifiedBadge(false);
+                log.info("第三方用户{}重新同步入驻信息,原已审核通过,重置为PENDING待重新审核: licenseId={}",
+                        userId, existing.getId());
             }
             }
+
+            // 更新第三方传入的入驻字段(非空即覆盖)
+            applySyncInfoLicenseFields(existing, request, storeName);
             businessLicenseService.updateById(existing);
             businessLicenseService.updateById(existing);
 
 
-            log.info("第三方用户{}重新同步入驻信息,已存在记录仅更新状态: licenseId={}, oldStatus={}, newStatus={}",
-                    userId, existing.getId(), existing.getReviewStatus(), reviewStatus);
+            log.info("第三方用户{}重新同步入驻信息: licenseId={}, wasApproved={}, newStatus={}",
+                    userId, existing.getId(), wasApproved, existing.getReviewStatus());
 
 
             // 从非APPROVED升级到APPROVED:赠送30天高级会员 + 推送Redis事件
             // 从非APPROVED升级到APPROVED:赠送30天高级会员 + 推送Redis事件
             if (upgradedToApproved) {
             if (upgradedToApproved) {
@@ -1025,16 +1031,66 @@ public class AuthService {
         return s != null && !s.isBlank();
         return s != null && !s.isBlank();
     }
     }
 
 
+    /**
+     * 将 SyncInfoRequest 中的入驻字段覆盖到本地记录(非空即覆盖)
+     */
+    private void applySyncInfoLicenseFields(
+            com.xuekairui.user.entity.BusinessLicense existing,
+            SyncInfoRequest req, String storeName) {
+        if (storeName != null) {
+            existing.setStoreName(storeName);
+        }
+        if (isNotBlank(req.getTerminalType())) {
+            existing.setTerminalType(req.getTerminalType());
+        }
+        if (isNotBlank(req.getProvince())) {
+            existing.setProvince(req.getProvince());
+        }
+        if (isNotBlank(req.getCity())) {
+            existing.setCity(req.getCity());
+        }
+        if (isNotBlank(req.getDistrict())) {
+            existing.setDistrict(req.getDistrict());
+        }
+        if (isNotBlank(req.getPharmacyAddress())) {
+            existing.setStoreAddress(req.getPharmacyAddress());
+        }
+        if (isNotBlank(req.getContactPerson())) {
+            existing.setContactPerson(req.getContactPerson());
+        }
+        if (isNotBlank(req.getContactPhone())) {
+            existing.setContactPhone(req.getContactPhone());
+        }
+        if (isNotBlank(req.getBusinessLicenseUrl())) {
+            existing.setLicenseImageUrl(req.getBusinessLicenseUrl());
+        }
+        if (isNotBlank(req.getDrugLicenseUrl())) {
+            existing.setDrugLicenseUrl(req.getDrugLicenseUrl());
+        }
+        if (isNotBlank(req.getMedicalDeviceClass2Url())) {
+            existing.setMedicalDeviceClass2Url(req.getMedicalDeviceClass2Url());
+        }
+        if (isNotBlank(req.getMedicalDeviceClass3Url())) {
+            existing.setMedicalDeviceClass3Url(req.getMedicalDeviceClass3Url());
+        }
+        if (isNotBlank(req.getBusinessLicenseNo())) {
+            existing.setCreditCode(req.getBusinessLicenseNo());
+        }
+    }
+
     /**
     /**
      * 首次入驻审核通过:赠送30天高级会员 + 推送Redis事件供活动模块消费
      * 首次入驻审核通过:赠送30天高级会员 + 推送Redis事件供活动模块消费
      * <p>与 BusinessLicenseService.approve() 的奖励逻辑保持一致。
      * <p>与 BusinessLicenseService.approve() 的奖励逻辑保持一致。
-     * <p>幂等性检查:若用户已有其他审核通过的入驻记录,不再重复赠送,避免高级会员过期时间累加。
+     * <p>幂等性检查:通过 rewardGranted 标记保证同一入驻记录仅奖励一次,
+     * 即使审核通过后重新编辑→重新审核,也不会重复发放。
      */
      */
     private void grantLicenseApprovedReward(Long userId, Long licenseId) {
     private void grantLicenseApprovedReward(Long userId, Long licenseId) {
-        if (businessLicenseService.hasOtherApprovedLicensePublic(userId, licenseId)) {
-            log.info("用户{}已有其他审核通过的入驻记录,跳过重复赠送高级会员: licenseId={}", userId, licenseId);
+        com.xuekairui.user.entity.BusinessLicense license = businessLicenseService.getById(licenseId);
+        if (license != null && Boolean.TRUE.equals(license.getRewardGranted())) {
+            log.info("用户{}的入驻记录奖励已发放,跳过重复赠送: licenseId={}", userId, licenseId);
             return;
             return;
         }
         }
+
         membershipService.grantOrExtendMembership(
         membershipService.grantOrExtendMembership(
                 userId,
                 userId,
                 MembershipLevel.PRO,
                 MembershipLevel.PRO,
@@ -1043,7 +1099,14 @@ public class AuthService {
                 "入驻信息审核通过赠送30天高级会员");
                 "入驻信息审核通过赠送30天高级会员");
         log.info("入驻信息审核通过(第三方同步),赠送用户{} 30天高级会员", userId);
         log.info("入驻信息审核通过(第三方同步),赠送用户{} 30天高级会员", userId);
 
 
+        // 标记奖励已发放,防止重复
+        if (license != null) {
+            license.setRewardGranted(true);
+            businessLicenseService.updateById(license);
+        }
+
         // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼等)
         // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼等)
+        // 活动模块通过 uk_invitee 唯一索引保证同一被邀请人仅奖励一次
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
         TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
             @Override
             @Override
             public void afterCommit() {
             public void afterCommit() {

+ 27 - 4
zhijiayun-user/src/main/java/com/xuekairui/user/service/BusinessLicenseService.java

@@ -322,9 +322,11 @@ public class BusinessLicenseService {
      * <p>
      * <p>
      * 业务规则:
      * 业务规则:
      * <ul>
      * <ul>
-     *   <li>可编辑全部入驻数据(含已审核 APPROVED),不受 PENDING 限制</li>
+     *   <li>可编辑全部入驻数据(含已审核 APPROVED),编辑后状态重置为 PENDING 需重新审核</li>
      *   <li>必填项约束同样生效:营业执照、药品经营许可必填,二类/三类至少一个</li>
      *   <li>必填项约束同样生效:营业执照、药品经营许可必填,二类/三类至少一个</li>
      *   <li>资质图片覆盖原内容(非空即覆盖,空值保留原值)</li>
      *   <li>资质图片覆盖原内容(非空即覆盖,空值保留原值)</li>
+     *   <li>编辑时隐藏认证标识(重新审核通过后重新展示)</li>
+     *   <li>首次奖励标记(rewardGranted)保持不变,确保不重复奖励</li>
      *   <li>记录变更前/变更后快照,便于追溯</li>
      *   <li>记录变更前/变更后快照,便于追溯</li>
      *   <li>事务提交后同步到第三方系统,失败不阻塞主流程</li>
      *   <li>事务提交后同步到第三方系统,失败不阻塞主流程</li>
      * </ul>
      * </ul>
@@ -343,6 +345,9 @@ public class BusinessLicenseService {
         // 校验资质必填项
         // 校验资质必填项
         validateRequiredQualifications(request);
         validateRequiredQualifications(request);
 
 
+        // 记录是否原已审核通过(用于判断是否需要重置状态)
+        boolean wasApproved = LicenseStatus.APPROVED.name().equals(license.getReviewStatus());
+
         // 变更前快照
         // 变更前快照
         Map<String, Object> beforeData = buildSnapshot(license);
         Map<String, Object> beforeData = buildSnapshot(license);
 
 
@@ -374,6 +379,16 @@ public class BusinessLicenseService {
         }
         }
         license.setReviewerId(reviewerId);
         license.setReviewerId(reviewerId);
         license.setReviewTime(LocalDateTime.now());
         license.setReviewTime(LocalDateTime.now());
+
+        // 原来已审核通过的记录,编辑后重置为 PENDING 需重新审核
+        // 隐藏认证标识,保留 rewardGranted 确保不重复奖励
+        if (wasApproved) {
+            license.setReviewStatus(LicenseStatus.PENDING.name());
+            license.setShowVerifiedBadge(false);
+            log.info("管理员编辑已审核通过的入驻信息,重置为PENDING待重新审核: licenseId={}, userId={}",
+                    licenseId, userId);
+        }
+
         businessLicenseMapper.updateById(license);
         businessLicenseMapper.updateById(license);
         log.info("管理员{}编辑入驻信息: licenseId={}, userId={}", reviewerId, licenseId, userId);
         log.info("管理员{}编辑入驻信息: licenseId={}, userId={}", reviewerId, licenseId, userId);
 
 
@@ -456,8 +471,8 @@ public class BusinessLicenseService {
         license.setShowVerifiedBadge(true);
         license.setShowVerifiedBadge(true);
         businessLicenseMapper.updateById(license);
         businessLicenseMapper.updateById(license);
 
 
-        // 首次审核通过,赠送 30 天高级会员
-        if (!hasOtherApprovedLicense(userId, licenseId)) {
+        // 首次审核通过,赠送 30 天高级会员(rewardGranted 保证同一入驻记录仅发一次奖励)
+        if (!Boolean.TRUE.equals(license.getRewardGranted())) {
             membershipService.grantOrExtendMembership(
             membershipService.grantOrExtendMembership(
                     userId,
                     userId,
                     MembershipLevel.PRO,
                     MembershipLevel.PRO,
@@ -466,7 +481,12 @@ public class BusinessLicenseService {
                     "入驻信息审核通过赠送30天高级会员");
                     "入驻信息审核通过赠送30天高级会员");
             log.info("入驻信息审核通过,赠送用户{} 30天高级会员", userId);
             log.info("入驻信息审核通过,赠送用户{} 30天高级会员", userId);
 
 
-            // 事务提交后推送 Redis 事件,供活动模块消费
+            // 标记奖励已发放
+            license.setRewardGranted(true);
+            businessLicenseMapper.updateById(license);
+
+            // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼活动)
+            // 活动模块通过 uk_invitee 唯一索引保证同一被邀请人仅奖励一次
             TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
             TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                 @Override
                 @Override
                 public void afterCommit() {
                 public void afterCommit() {
@@ -477,6 +497,9 @@ public class BusinessLicenseService {
                     }
                     }
                 }
                 }
             });
             });
+        } else {
+            log.info("入驻信息重新审核通过,奖励已发放,跳过重复奖励: licenseId={}, userId={}",
+                    licenseId, userId);
         }
         }
 
 
         // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程
         // 事务提交后同步客户入驻信息到第三方系统(智价云药店版),失败不阻塞主流程