Ver código fonte

邀请活动更新

liuchengsen 1 mês atrás
pai
commit
1dd1b1d6ff

+ 54 - 20
docs/第三方登录接口文档.md

@@ -1,8 +1,8 @@
 # 第三方登录接口文档
 
-> 版本:V1.2  
+> 版本:V1.4  
 > 更新日期:2026-07-09  
-> 变更说明:新增入驻信息字段(店铺名称、终端类型、资质图片URL等),同步写入 t_business_license 表
+> 变更说明:响应 userInfo 补全完整返回字段(userName、药店信息、邀请人信息等);修正 nickname 优先级描述(nickname > username)
 
 ---
 
@@ -15,7 +15,8 @@
 - 支持第三方传入用户名、药店信息等,自动保存到用户表
 - 支持第三方传入入驻信息(资质图片URL等),同步写入 `t_business_license`,免去额外调用入驻接口
 - 不递增 `tokenVersion`,不踢出 PC 端已有会话
-- 新用户自动初始化 PLUS 会员
+- 新用户自动初始化 **PLUS 会员(普通会员)**
+- 第三方已审核用户(`licenseApproved=true`)自动赠送 **30 天 PRO 高级会员**
 
 ---
 
@@ -68,7 +69,7 @@ X-Api-Key: {我方提供的API Key}
 
 | 参数名 | 类型 | 必填 | 说明 | 对应我方字段 |
 |--------|------|------|------|--------------|
-| `username` | String | 否 | 用户名(保存到 nickname 和 userName 字段,优先级高于 nickname) | `nickname` / `userName` |
+| `username` | String | 否 | 用户名(保存到 userName 字段;若未传 nickname,则自动作为 nickname 的兜底值) | `nickname` / `userName` |
 | `nickname` | String | 否 | 昵称(不传则使用 username,再不传则按手机号生成) | `nickname` |
 | `pharmacyName` | String | 否 | 药店名称 | `pharmacy_name` |
 | `province` | String | 否 | 省 | `province` |
@@ -89,18 +90,23 @@ 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 | — |
 
-> 💡 **入驻信息同步规则**:
+> 💡 **审核状态说明**:
+> - 如果第三方系统已完成资质审核,传 `licenseApproved: true`,我方将直接标记为"审核通过"并赠送 30 天 PRO 高级会员
+> - 如果第三方尚未审核或不确定,不传该字段(默认 `PENDING`),由我方运营审核
+> - 已审核通过的入驻记录不会因后续调用而降级
 > - 只要请求中包含任一入驻信息字段(storeName / terminalType / businessLicenseUrl / drugLicenseUrl / medicalDeviceClass2Url / medicalDeviceClass3Url),即自动写入 `t_business_license` 表
-> - 新用户自动创建执照记录(`review_status=PENDING`,`sync_source=MINIAPP_SYNC`)
-> - 已存在用户仅更新非空字段(不会清空已有数据),审核状态重置为待审核
+> - `licenseApproved=true`:审核状态设为 `APPROVED`(审核通过),首次通过赠送 30 天 PRO 高级会员
+> - 未传 `licenseApproved` 或 `false`:审核状态设为 `PENDING`(待运营审核)
+> - 已存在入驻记录:已 APPROVED 状态不降级,PENDING/REJECTED 状态可随 `licenseApproved=true` 升级
 > - 省/市/区/地址/联系人/电话等字段会自动从用户信息字段中提取填入
 >
 > 💡 **字段更新规则**:
 > - 新用户会写入所有传入字段
 > - 已存在用户仅更新非空字段(不会清空已有数据)
 >
-> 💡 **nickname 优先级**:`username` > `nickname` > 手机号生成(如 `用户8000`)
+> 💡 **nickname 优先级**:`nickname` > `username` > 手机号生成(如 `用户8000`)
 >
 > 💡 **建议**:第三方每次调用时传入最新的用户信息,我方会自动同步更新,无需额外维护。
 
@@ -135,7 +141,8 @@ X-Api-Key: {我方提供的API Key}
   "businessLicenseUrl": "https://oss.example.com/yyzz.jpg",
   "drugLicenseUrl": "https://oss.example.com/ypjy.jpg",
   "medicalDeviceClass2Url": "https://oss.example.com/elqx.jpg",
-  "medicalDeviceClass3Url": "https://oss.example.com/slqx.jpg"
+  "medicalDeviceClass3Url": "https://oss.example.com/slqx.jpg",
+  "licenseApproved": true
 }
 ```
 
@@ -178,12 +185,21 @@ X-Api-Key: {我方提供的API Key}
 |--------|------|------|
 | `id` | String | 用户ID |
 | `phone` | String | 手机号 |
-| `nickname` | String | 昵称(即第三方传入的 username 或 nickname) |
+| `nickname` | String | 昵称(即第三方传入的 nickname 或 username) |
+| `userName` | String | 第三方传入的原始用户名 |
 | `avatar` | String | 头像 URL |
 | `wechatBound` | Boolean | 是否已绑定微信 |
 | `loginSource` | String | 登录来源,`"MINIAPP"` 表示小程序 |
-| `membershipLevel` | String | 会员等级代码 |
+| `pharmacyName` | String | 药店名称 |
+| `province` | String | 省 |
+| `city` | String | 市 |
+| `district` | String | 区 |
+| `membershipLevel` | String | 会员等级代码(PLUS / PRO / ULTRA) |
 | `membershipLevelName` | String | 会员等级名称 |
+| `licenseStatus` | String | 营业执照审核状态:`NOT_SUBMITTED` / `PENDING` / `APPROVED` / `REJECTED` |
+| `inviterId` | String | 邀请人用户ID(未绑定邀请码则为 null) |
+| `inviterNickname` | String | 邀请人昵称 |
+| `hasBoundInviteCode` | Boolean | 是否已绑定邀请码 |
 | `createTime` | String | 注册时间 |
 
 ### 5.4 响应示例
@@ -203,12 +219,21 @@ X-Api-Key: {我方提供的API Key}
     "userInfo": {
       "id": "1234567890123456789",
       "phone": "138****8000",
-      "nickname": "yaohuigou001",
+      "nickname": "药汇购001",
+      "userName": "yaohuigou001",
       "avatar": null,
       "wechatBound": false,
       "loginSource": "MINIAPP",
-      "membershipLevel": "PLUS",
-      "membershipLevelName": "PLUS会员",
+      "pharmacyName": "北京同仁堂大药房",
+      "province": "北京市",
+      "city": "北京市",
+      "district": "朝阳区",
+      "membershipLevel": "PRO",
+      "membershipLevelName": "高级会员",
+      "licenseStatus": "APPROVED",
+      "inviterId": "1234567890123456788",
+      "inviterNickname": "邀请人昵称",
+      "hasBoundInviteCode": true,
       "createTime": "2025-01-01T12:00:00"
     }
   }
@@ -269,7 +294,7 @@ Authorization: Bearer {currentAccessToken}
 | # | 说明 |
 |---|------|
 | 1 | PC端严格单设备,小程序与PC互不干扰,同一账号可同时登录 |
-| 2 | 手机号未注册时自动创建账号并初始化PLUS会员 |
+| 2 | 手机号未注册时自动创建账号并初始化 **PLUS 会员(普通会员)** |
 | 3 | `inviteCode`仅新用户注册时生效 |
 | 4 | API Key请在后端调用,勿暴露到客户端 |
 | 5 | accessToken默认2小时,refreshToken默认7天 |
@@ -277,6 +302,8 @@ Authorization: Bearer {currentAccessToken}
 | 7 | 用户信息字段(username、药店信息等)每次调用都会同步更新,建议传入最新数据 |
 | 8 | `username` 会保存到我方的 `nickname` 字段,不需要额外维护用户名表 |
 | 9 | 传入入驻信息字段后自动创建/更新入驻记录,无需再调用 `/api/business-license/upload` |
+| 10 | `licenseApproved=true` 时入驻状态直接审核通过,首次通过赠送 **30 天 PRO 高级会员** |
+| 11 | 已审核通过的入驻记录(APPROVED)不会因后续调用而降级 |
 
 ---
 
@@ -302,7 +329,8 @@ curl -X POST "https://your-domain.com/api/auth/external/miniapp-token" \
     "storeName": "北京同仁堂大药房(朝阳分店)",
     "terminalType": "1",
     "businessLicenseUrl": "https://oss.example.com/yyzz.jpg",
-    "drugLicenseUrl": "https://oss.example.com/ypjy.jpg"
+    "drugLicenseUrl": "https://oss.example.com/ypjy.jpg",
+    "licenseApproved": true
   }'
 ```
 
@@ -325,7 +353,8 @@ String json = "{"
     + "\"storeName\": \"北京同仁堂大药房(朝阳分店)\","
     + "\"terminalType\": \"1\","
     + "\"businessLicenseUrl\": \"https://oss.example.com/yyzz.jpg\","
-    + "\"drugLicenseUrl\": \"https://oss.example.com/ypjy.jpg\""
+    + "\"drugLicenseUrl\": \"https://oss.example.com/ypjy.jpg\","
+    + "\"licenseApproved\": true"
     + "}";
 
 RequestBody body = RequestBody.create(json, MediaType.parse("application/json"));
@@ -365,7 +394,8 @@ data = {
     "storeName": "北京同仁堂大药房(朝阳分店)",
     "terminalType": "1",
     "businessLicenseUrl": "https://oss.example.com/yyzz.jpg",
-    "drugLicenseUrl": "https://oss.example.com/ypjy.jpg"
+    "drugLicenseUrl": "https://oss.example.com/ypjy.jpg",
+    "licenseApproved": True
 }
 
 response = requests.post(url, json=data, headers=headers)
@@ -404,12 +434,14 @@ 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 |
 
 > 💡 入驻记录保存时,以下字段由系统自动填入(无需第三方传入):
 > - `store_address` ← 从 `pharmacyAddress` 提取
 > - `credit_code` ← 从 `businessLicenseNo` 提取
 > - `sync_source` ← 固定为 `MINIAPP_SYNC`
-> - `review_status` ← 固定为 `PENDING`(待运营审核)
+> - `review_status` ← 取决于 `licenseApproved`:`true` → `APPROVED`,`false`/不传 → `PENDING`
+> - `show_verified_badge` ← 首次审核通过时设为 `true`
 
 ---
 
@@ -425,4 +457,6 @@ print(response.json())
 |------|------|----------|
 | V1.0 | 2026-07-07 | 初始版本:基础登录接口 |
 | V1.1 | 2026-07-07 | 扩展请求参数:支持传入用户名、药店信息等,自动保存到用户表 |
-| V1.2 | 2026-07-09 | 新增入驻信息字段(storeName / terminalType / businessLicenseUrl / drugLicenseUrl / medicalDeviceClass2Url / medicalDeviceClass3Url),传入后同步写入 t_business_license,无需额外调用入驻接口 |
+| V1.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) |

+ 6 - 0
zhijiayun-activity/pom.xml

@@ -27,6 +27,12 @@
             <artifactId>zhijiayun-user</artifactId>
         </dependency>
 
+        <!-- 邀请模块(补发邀请奖励需查询邀请关系) -->
+        <dependency>
+            <groupId>com.xuekairui</groupId>
+            <artifactId>zhijiayun-invite</artifactId>
+        </dependency>
+
         <!-- Spring Boot -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 33 - 0
zhijiayun-activity/src/main/java/com/xuekairui/activity/controller/AdminActivityController.java

@@ -21,6 +21,7 @@ import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
+import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -31,7 +32,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 活动管理接口(运营端)
@@ -159,6 +162,27 @@ public class AdminActivityController {
         return Result.success("已撤销 " + count + " 条邀请奖励记录");
     }
 
+    // ========== 运营补发:活动时间窗内邀请奖励补发 ==========
+
+    /**
+     * 对活动时间窗内的邀请关系批量补发奖励。
+     * <p>
+     * 场景:活动配置晚于实际邀请时间,运营配置活动后对窗口内已入驻的被邀请人补发。
+     * 不传 startTime/endTime 时默认使用当前活动配置的时间窗口。
+     *
+     * <pre>
+     * POST /api/admin/activity/invite/retroactive-grant
+     * Body: { "startTime": "2026-07-09T00:00:00", "endTime": "2026-07-30T23:59:59" }
+     * </pre>
+     */
+    @PostMapping("/invite/retroactive-grant")
+    public Result<Map<String, Integer>> retroactiveGrant(
+            @RequestBody(required = false) RetroactiveGrantRequest request) {
+        LocalDateTime startTime = request != null ? request.getStartTime() : null;
+        LocalDateTime endTime = request != null ? request.getEndTime() : null;
+        return Result.success(activityInviteService.retroactiveGrant(startTime, endTime));
+    }
+
     @Data
     public static class RevokeRewardRequest {
         @NotNull(message = "被邀请人ID不能为空")
@@ -167,4 +191,13 @@ public class AdminActivityController {
         @NotBlank(message = "撤销原因不能为空")
         private String reason;
     }
+
+    @Data
+    public static class RetroactiveGrantRequest {
+        @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+        private LocalDateTime startTime;
+
+        @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+        private LocalDateTime endTime;
+    }
 }

+ 0 - 1
zhijiayun-activity/src/main/java/com/xuekairui/activity/dto/ActivityInviteConfigRequest.java

@@ -24,7 +24,6 @@ public class ActivityInviteConfigRequest {
     private LocalDateTime endTime;
 
     private Integer inviterRewardDays;
-    private Integer inviteeRewardDays;
     private Integer maxInviterReward;
 
     /** REGISTRATION / LICENSE_APPROVED */

+ 1 - 4
zhijiayun-activity/src/main/java/com/xuekairui/activity/entity/ActivityInviteConfig.java

@@ -30,15 +30,12 @@ public class ActivityInviteConfig {
     @Builder.Default
     private Integer inviterRewardDays = 30;
 
-    @Builder.Default
-    private Integer inviteeRewardDays = 30;
-
     @Builder.Default
     private Integer maxInviterReward = 30;
 
     /** 奖励触发模式:REGISTRATION / LICENSE_APPROVED */
     @Builder.Default
-    private String rewardTrigger = "REGISTRATION";
+    private String rewardTrigger = "LICENSE_APPROVED";
 
     @Builder.Default
     private Integer requireMiniappInviter = 0;

+ 2 - 4
zhijiayun-activity/src/main/java/com/xuekairui/activity/scheduler/ActivityEventScheduler.java

@@ -43,10 +43,8 @@ public class ActivityEventScheduler {
         while ((event = eventRedisService.popLicenseApprovedEvent()) != null) {
             try {
                 Long userId = toLong(event.get("userId"));
-                Long licenseId = toLong(event.get("licenseId"));
-                String licenseType = (String) event.get("licenseType");
-                log.info("消费入驻审核通过事件: userId={}, licenseId={}", userId, licenseId);
-                activityInviteService.onLicenseApproved(userId, licenseId, licenseType);
+                log.info("消费入驻审核通过事件: userId={}", userId);
+                activityInviteService.onLicenseApproved(userId);
                 eventRedisService.ackLicenseApprovedEvent(event);
             } catch (Exception e) {
                 log.error("处理入驻审核通过事件失败,事件将保留在备份队列待重试: event={}", event, e);

+ 129 - 46
zhijiayun-activity/src/main/java/com/xuekairui/activity/service/ActivityInviteService.java

@@ -10,8 +10,13 @@ import com.xuekairui.activity.entity.ActivityInviteRewardRecord;
 import com.xuekairui.activity.mapper.ActivityInviteConfigMapper;
 import com.xuekairui.activity.mapper.ActivityInviteRewardRecordMapper;
 import com.xuekairui.common.activity.ActivityInviteGate;
+import com.xuekairui.invite.entity.InviteRelation;
+import com.xuekairui.invite.mapper.InviteRelationMapper;
+import com.xuekairui.user.entity.BusinessLicense;
 import com.xuekairui.user.entity.User;
+import com.xuekairui.user.enums.LicenseStatus;
 import com.xuekairui.user.enums.MembershipLevel;
+import com.xuekairui.user.mapper.BusinessLicenseMapper;
 import com.xuekairui.user.mapper.UserMapper;
 import com.xuekairui.user.service.MembershipService;
 import lombok.RequiredArgsConstructor;
@@ -21,7 +26,10 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
 /**
  * 邀请有礼活动服务(统一管理所有邀请奖励发放)。
@@ -45,8 +53,6 @@ import java.util.List;
 @RequiredArgsConstructor
 public class ActivityInviteService implements ActivityInviteGate {
 
-    /** 奖励触发模式:注册即发 */
-    public static final String TRIGGER_REGISTRATION = "REGISTRATION";
     /** 奖励触发模式:入驻审核通过后发 */
     public static final String TRIGGER_LICENSE_APPROVED = "LICENSE_APPROVED";
 
@@ -54,43 +60,132 @@ public class ActivityInviteService implements ActivityInviteGate {
 
     private final ActivityInviteConfigMapper configMapper;
     private final ActivityInviteRewardRecordMapper rewardRecordMapper;
+    private final InviteRelationMapper inviteRelationMapper;
     private final UserMapper userMapper;
+    private final BusinessLicenseMapper businessLicenseMapper;
     private final MembershipService membershipService;
 
     // ==========================================
     // ActivityInviteGate 门面实现(供 InviteService 调用)
     // ==========================================
 
+    /**
+     * 发放邀请奖励(统一入口,根据 rewardTrigger 配置决定策略)。
+     * <p>
+     * LICENSE_APPROVED:不发放,等待入驻审核通过后触发。
+     * REGISTRATION:立即发放,受活动时间窗和 30 次上限约束。
+     */
     @Override
     public void grantInviteReward(Long inviterId, Long inviteeId) {
         ActivityInviteConfig config = getConfig();
 
-        // 活动未开启 → 按原逻辑直接发奖,不受活动限制
+        // 活动未开启 → 不发放
         if (!isActive(config)) {
-            doGrantReward(inviterId, inviteeId, 30);
+            log.info("邀请有礼活动未开启,跳过发奖: inviter={}, invitee={}", inviterId, inviteeId);
             return;
         }
 
-        // 活动开启 → 校验参与条件:邀请人需为小程序用户
-        if (config.getRequireMiniappInviter() != null && config.getRequireMiniappInviter() == 1) {
-            User inviter = userMapper.selectById(inviterId);
-            if (inviter == null || inviter.getWechatMiniOpenId() == null) {
-                log.info("邀请有礼活动:邀请人{}非小程序用户,跳过发奖: invitee={}", inviterId, inviteeId);
-                return;
-            }
-        }
-
-        // 活动开启 → 根据触发模式决定
+        // LICENSE_APPROVED 模式 → 不发放,等待入驻审核通过
         if (TRIGGER_LICENSE_APPROVED.equalsIgnoreCase(config.getRewardTrigger())) {
-            // LICENSE_APPROVED 模式:注册环节暂不发奖,等待入驻审核通过后由 onLicenseApproved 触发
             log.info("邀请有礼活动(LICENSE_APPROVED模式),注册环节暂不发奖: inviter={}, invitee={}", inviterId, inviteeId);
             return;
         }
 
-        // REGISTRATION 模式(默认):活动期间内注册即发奖,但受 30 次上限约束
+        // REGISTRATION 模式 → 立即发放
         grantWithLimit(inviterId, inviteeId, config);
     }
 
+    /**
+     * 已入驻用户补填邀请码时发放奖励(依赖活动配置)。
+     * <p>
+     * 场景:被邀请人已通过入驻审核,后补填邀请码 → 邀请人应获得奖励。
+     * 活动未开启则不发放。幂等由 uk_invitee 唯一索引保障。
+     */
+    @Override
+    public void grantRewardForApprovedUser(Long inviterId, Long inviteeId) {
+        ActivityInviteConfig config = getConfig();
+        if (!isActive(config)) {
+            log.info("邀请有礼活动未开启,跳过已入驻补填发奖: inviter={}, invitee={}", inviterId, inviteeId);
+            return;
+        }
+        grantWithLimit(inviterId, inviteeId, config);
+        log.info("已入驻用户补填邀请码,发放邀请奖励: inviter={}, invitee={}", inviterId, inviteeId);
+    }
+
+    /**
+     * 运营补发:对活动时间窗内的邀请关系批量补发奖励。
+     * <p>
+     * 场景:活动配置晚于实际邀请时间,运营配置活动后对窗口内的邀请进行补发。
+     * 仅对已入驻(审核通过)的被邀请人补发。
+     * 每个被邀请人只发一次(uk_invitee 兜底),邀请人受 30 次上限约束。
+     *
+     * @param startTime 活动开始时间(选填,默认取当前配置)
+     * @param endTime   活动结束时间(选填,默认取当前配置)
+     * @return {granted: 已补发数, skipped: 跳过数, total: 窗口内邀请总数}
+     */
+    @Transactional
+    public Map<String, Integer> retroactiveGrant(LocalDateTime startTime, LocalDateTime endTime) {
+        ActivityInviteConfig config = getConfig();
+        if (startTime == null) {
+            startTime = config.getStartTime();
+        }
+        if (endTime == null) {
+            endTime = config.getEndTime();
+        }
+
+        // 查询活动时间窗内的所有邀请关系
+        List<InviteRelation> relations = inviteRelationMapper.selectList(
+                new LambdaQueryWrapper<InviteRelation>()
+                        .ge(InviteRelation::getCreateTime, startTime)
+                        .le(InviteRelation::getCreateTime, endTime)
+                        .eq(InviteRelation::getStatus, 1));
+
+        int total = relations.size();
+        int granted = 0;
+        int skipped = 0;
+
+        for (InviteRelation r : relations) {
+            Long inviteeId = r.getInviteeId();
+            Long inviterId = r.getInviterId();
+
+            // 跳过无效数据
+            if (inviterId == null || inviteeId == null) {
+                skipped++;
+                continue;
+            }
+
+            // 检查是否已发过奖(uk_invitee 唯一索引保障,此处提前过滤减少 DB 异常)
+            Long alreadyRewarded = rewardRecordMapper.selectCount(
+                    new LambdaQueryWrapper<ActivityInviteRewardRecord>()
+                            .eq(ActivityInviteRewardRecord::getInviteeId, inviteeId));
+            if (alreadyRewarded != null && alreadyRewarded > 0) {
+                skipped++;
+                continue;
+            }
+
+            // 检查被邀请人是否已入驻审核通过
+            Long approvedCount = businessLicenseMapper.selectCount(
+                    new LambdaQueryWrapper<BusinessLicense>()
+                            .eq(BusinessLicense::getUserId, inviteeId)
+                            .eq(BusinessLicense::getReviewStatus, LicenseStatus.APPROVED.name()));
+            if (approvedCount == null || approvedCount == 0) {
+                skipped++;
+                continue;
+            }
+
+            // 补发奖励
+            grantWithLimit(inviterId, inviteeId, config);
+            granted++;
+        }
+
+        Map<String, Integer> result = new HashMap<>();
+        result.put("total", total);
+        result.put("granted", granted);
+        result.put("skipped", skipped);
+        log.info("补发邀请奖励完成: total={}, granted={}, skipped={}", total, granted, skipped);
+        return result;
+    }
+
     // ==========================================
     // 配置管理
     // ==========================================
@@ -102,8 +197,8 @@ public class ActivityInviteService implements ActivityInviteGate {
                     .id(CONFIG_ID).enabled(0)
                     .startTime(LocalDateTime.now())
                     .endTime(LocalDateTime.now())
-                    .inviterRewardDays(30).inviteeRewardDays(30).maxInviterReward(30)
-                    .rewardTrigger(TRIGGER_REGISTRATION).requireMiniappInviter(0)
+                    .inviterRewardDays(30).maxInviterReward(30)
+                    .rewardTrigger(TRIGGER_LICENSE_APPROVED).requireMiniappInviter(0)
                     .build();
         }
         return config;
@@ -115,12 +210,11 @@ public class ActivityInviteService implements ActivityInviteGate {
         config.setEnabled(request.getEnabled());
         config.setStartTime(request.getStartTime());
         config.setEndTime(request.getEndTime());
-        if (request.getInviterRewardDays() != null) config.setInviterRewardDays(request.getInviterRewardDays());
-        if (request.getInviteeRewardDays() != null) config.setInviteeRewardDays(request.getInviteeRewardDays());
-        if (request.getMaxInviterReward() != null) config.setMaxInviterReward(request.getMaxInviterReward());
-        if (request.getRewardTrigger() != null) config.setRewardTrigger(request.getRewardTrigger());
-        if (request.getRequireMiniappInviter() != null) config.setRequireMiniappInviter(request.getRequireMiniappInviter());
-        if (request.getRemark() != null) config.setRemark(request.getRemark());
+        Optional.ofNullable(request.getInviterRewardDays()).ifPresent(config::setInviterRewardDays);
+        Optional.ofNullable(request.getMaxInviterReward()).ifPresent(config::setMaxInviterReward);
+        Optional.ofNullable(request.getRewardTrigger()).ifPresent(config::setRewardTrigger);
+        Optional.ofNullable(request.getRequireMiniappInviter()).ifPresent(config::setRequireMiniappInviter);
+        Optional.ofNullable(request.getRemark()).ifPresent(config::setRemark);
         if (config.getId() == null) {
             config.setId(CONFIG_ID);
             configMapper.insert(config);
@@ -133,30 +227,30 @@ public class ActivityInviteService implements ActivityInviteGate {
     }
 
     // ==========================================
-    // 事件处理:入驻审核通过后发放邀请人奖励(LICENSE_APPROVED 模式专用)
+    // 事件处理:入驻审核通过后发放邀请人奖励
     // ==========================================
 
     /**
-     * 入驻审核通过后发放邀请人奖励。
+     * 入驻审核通过后发放邀请人奖励(依赖活动配置,不再区分触发模式)
      * <p>
-     * 幂等:t_activity_invite_reward_record 的 uk_invitee 唯一索引保证同一被邀请人仅奖励一次
+     * 活动未开启则不发放。幂等由 uk_invitee 唯一索引保障
      */
     @Transactional
-    public void onLicenseApproved(Long userId, Long licenseId, String licenseType) {
-        ActivityInviteConfig config = getConfig();
-        if (!isActive(config) || !TRIGGER_LICENSE_APPROVED.equalsIgnoreCase(config.getRewardTrigger())) {
+    public void onLicenseApproved(Long userId) {
+        User invitee = userMapper.selectById(userId);
+        if (invitee == null || invitee.getInviterId() == null) {
+            log.debug("被邀请人无邀请人,跳过邀请奖励: inviteeId={}", userId);
             return;
         }
 
-        Long inviteeId = userId;
-        User invitee = userMapper.selectById(inviteeId);
-        if (invitee == null || invitee.getInviterId() == null) {
-            log.debug("被邀请人无邀请人,跳过邀请奖励: inviteeId={}", inviteeId);
+        ActivityInviteConfig config = getConfig();
+        if (!isActive(config)) {
+            log.info("邀请有礼活动未开启,跳过入驻发奖: invitee={}", userId);
             return;
         }
 
         Long inviterId = invitee.getInviterId();
-        grantWithLimit(inviterId, inviteeId, config);
+        grantWithLimit(inviterId, userId, config);
     }
 
     // ==========================================
@@ -202,17 +296,6 @@ public class ActivityInviteService implements ActivityInviteGate {
         return resultPage;
     }
 
-    /**
-     * 用户端:查询我作为邀请人获得的奖励记录。
-     */
-    public List<ActivityRewardRecordResponse> listMyInviteRewards(Long userId) {
-        LambdaQueryWrapper<ActivityInviteRewardRecord> wrapper = new LambdaQueryWrapper<ActivityInviteRewardRecord>()
-                .eq(ActivityInviteRewardRecord::getInviterId, userId)
-                .orderByDesc(ActivityInviteRewardRecord::getCreateTime);
-        List<ActivityInviteRewardRecord> records = rewardRecordMapper.selectList(wrapper);
-        return toRewardResponseList(records);
-    }
-
     private List<ActivityRewardRecordResponse> toRewardResponseList(List<ActivityInviteRewardRecord> records) {
         return records.stream()
                 .map(r -> ActivityRewardRecordResponse.builder()

+ 22 - 10
zhijiayun-common/src/main/java/com/xuekairui/common/activity/ActivityInviteGate.java

@@ -1,27 +1,39 @@
-package com.xuekairui.common.activity;
+ package com.xuekairui.common.activity;
 
 /**
  * 邀请有礼活动门面(可选实现,由活动模块提供)。
  * <p>
- * 邀请模块不再直接发放邀请奖励,而是通过该接口委托给活动模块统一处理。
- * 活动模块根据当前活动配置(是否开启、触发模式、时间窗、次数上限等)决定是否发放及如何发放。
+ * 邀请模块不直接发放邀请奖励,而是通过该接口委托给活动模块统一处理。
+ * 活动模块根据当前活动配置(rewardTrigger)决定发放策略:
+ * <ul>
+ *   <li>LICENSE_APPROVED(默认):注册/补填时不发奖,等待被邀请人入驻审核通过后触发</li>
+ *   <li>REGISTRATION:注册/补填时立即发奖,受活动时间窗和次数上限约束</li>
+ * </ul>
+ * 已入驻用户补填邀请码时通过 {@link #grantRewardForApprovedUser} 发放(同样依赖活动开关)。
  * <p>
  * 默认无实现时:邀请模块保持兼容,不发放奖励(活动模块未加载)。
  */
 public interface ActivityInviteGate {
 
     /**
-     * 发放邀请奖励(统一入口)。
+     * 发放邀请奖励(统一入口,由活动模块根据 rewardTrigger 配置决定)。
      * <p>
-     * 由活动模块根据当前活动配置决定:
-     * <ul>
-     *   <li>活动未开启 → 按原逻辑发放 30 天高级会员</li>
-     *   <li>活动开启 + REGISTRATION 模式 → 立即发放 30 天高级会员</li>
-     *   <li>活动开启 + LICENSE_APPROVED 模式 → 暂不发放,等待被邀请人入驻审核通过后由事件触发</li>
-     * </ul>
+     * LICENSE_APPROVED 模式:不发放,等待入驻审核通过后由 onLicenseApproved 触发。
+     * REGISTRATION 模式:立即发放 30 天高级会员(受活动时间窗、30 次上限约束)。
      *
      * @param inviterId 邀请人ID
      * @param inviteeId 被邀请人ID
      */
     void grantInviteReward(Long inviterId, Long inviteeId);
+
+    /**
+     * 已入驻用户补填邀请码时发放奖励(依赖活动配置,活动未开启则不发放)。
+     * <p>
+     * 场景:被邀请人已通过入驻审核,后补填邀请码 → 邀请人应获得奖励。
+     * 幂等由 uk_invitee 唯一索引保障。
+     *
+     * @param inviterId 邀请人ID
+     * @param inviteeId 被邀请人ID(已入驻)
+     */
+    void grantRewardForApprovedUser(Long inviterId, Long inviteeId);
 }

+ 3 - 0
zhijiayun-invite/src/main/java/com/xuekairui/invite/dto/InviteRewardResponse.java

@@ -38,4 +38,7 @@ public class InviteRewardResponse {
 
     /** 邀请码 */
     private String inviteCode;
+
+    /** 入驻状态:NOT_SUBMITTED-未入驻 / PENDING-待审核 / APPROVED-已通过 / REJECTED-已驳回 */
+    private String licenseStatus;
 }

+ 38 - 33
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteService.java

@@ -16,12 +16,14 @@ import com.xuekairui.user.dto.CrawlerUserDetailResponse;
 import com.xuekairui.user.entity.CrawlerPlatform;
 import com.xuekairui.user.entity.CrawlerQuotaGrant;
 import com.xuekairui.user.entity.User;
+import com.xuekairui.user.enums.LicenseStatus;
 import com.xuekairui.user.mapper.CrawlerUsageLogMapper;
 import com.xuekairui.user.mapper.UserMapper;
 import com.xuekairui.user.service.CrawlerQuotaGrantService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -48,7 +50,11 @@ public class InviteService {
     private final UserMapper userMapper;
     private final CrawlerUsageLogMapper crawlerUsageLogMapper;
     private final com.xuekairui.user.service.CrawlerService crawlerService;
-    private final java.util.Optional<ActivityInviteGate> activityInviteGate;
+    private final com.xuekairui.user.service.BusinessLicenseService businessLicenseService;
+
+    /** 活动模块可选依赖:未加载时跳过活动相关发奖 */
+    @Autowired(required = false)
+    private ActivityInviteGate activityInviteGate;
 
     /** 邀请链接基础URL,可通过配置覆盖 */
     @Value("${invite.base-url:https://app.zhijiayun.com}")
@@ -537,16 +543,17 @@ public class InviteService {
         // 7. 更新邀请码使用次数
         inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
         inviteCodeMapper.updateById(inviteCode);
-
-        // 8. 奖励发放统一委托给活动模块
-        grantInviteReward(inviterId, inviteeId, config);
-        log.info("邀请成功: inviter={}, invitee={}", inviterId, inviteeId);
-
-        // 9. 写入被邀请人的 inviter_id(不可变,防止删 t_invite_relation 绕过)
+        
+        // 8. 写入被邀请人的 inviter_id(不可变,防止删 t_invite_relation 绕过)
         if (invitee != null && invitee.getInviterId() == null) {
             invitee.setInviterId(inviterId);
             userMapper.updateById(invitee);
         }
+
+        // 9. 委托活动模块根据 rewardTrigger 配置决定是否发放奖励
+        if (activityInviteGate != null) {
+            activityInviteGate.grantInviteReward(inviterId, inviteeId);
+        }
     }
 
     /**
@@ -657,33 +664,24 @@ public class InviteService {
         user.setInviterId(inviterId);
         userMapper.updateById(user);
 
-        // 10. 奖励发放统一委托给活动模块
-        grantInviteReward(inviterId, userId, config);
+        // 10. 根据活动配置决定是否发奖
+        com.xuekairui.user.entity.BusinessLicense license = businessLicenseService.getByUserId(userId);
+        if (license != null && LicenseStatus.APPROVED.name().equals(license.getReviewStatus())) {
+            // 已入驻 → 发奖(依赖活动开关)
+            if (activityInviteGate != null) {
+                activityInviteGate.grantRewardForApprovedUser(inviterId, userId);
+            } else {
+                log.warn("活动模块未加载,无法发放已入驻用户补填奖励: inviter={}, invitee={}", inviterId, userId);
+            }
+        } else if (activityInviteGate != null) {
+            // 未入驻 → 委托活动模块根据 rewardTrigger 配置决定
+            activityInviteGate.grantInviteReward(inviterId, userId);
+        }
+
         log.info("补填邀请码成功: invitee={}, inviter={}", userId, inviterId);
         return "补填邀请码成功!邀请人获赠1个月会员时长";
     }
 
-    // ==========================================
-    // 邀请奖励统一入口
-    // ==========================================
-
-    /**
-     * 发放邀请奖励(统一入口)。
-     * <p>
-     * 全部委托给活动模块(ActivityInviteGate)处理,由活动模块根据当前活动配置
-     * (是否开启、触发模式、时间窗、次数上限等)决定是否发放及如何发放。
-     * 若活动模块未加载(无 gate 实现),则跳过发奖。
-     * <p>
-     * 由 processInviteRegistration 和 bindInviteCode 统一调用。
-     */
-    private void grantInviteReward(Long inviterId, Long inviteeId, InviteConfig config) {
-        if (activityInviteGate.isPresent()) {
-            activityInviteGate.get().grantInviteReward(inviterId, inviteeId);
-        } else {
-            log.warn("活动模块未加载,无法发放邀请奖励: inviter={}, invitee={}", inviterId, inviteeId);
-        }
-    }
-
     // ==========================================
     // 运营手动发放
     // ==========================================
@@ -835,6 +833,9 @@ public class InviteService {
                 : inviteCodeMapper.selectByIds(codeIds).stream()
                         .collect(Collectors.toMap(InviteCode::getId, InviteCode::getCode));
 
+        // 批量查入驻状态
+        Map<Long, String> licenseStatusMap = businessLicenseService.getLicenseStatusByUserIds(inviteeIds);
+
         // 邀请人信息只查一次
         String inviterNickname = getUserNickname(userId);
 
@@ -859,6 +860,7 @@ public class InviteService {
                     .registeredText(isRegistered ? "已注册" : "待注册")
                     .rewardCount(r.getRewardGranted())
                     .inviteCode(r.getInviteCodeId() != null ? codeMap.get(r.getInviteCodeId()) : null)
+                    .licenseStatus(licenseStatusMap.getOrDefault(r.getInviteeId(), LicenseStatus.NOT_SUBMITTED.name()))
                     .inviteTime(r.getCreateTime() != null ?
                             r.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null)
                     .build();
@@ -924,7 +926,6 @@ public class InviteService {
     // ==========================================
 
     /** 邀请码字符集(32个,去掉易混淆的 I/O/0/1) */
-    /** 邀请码正则片段:8位不含易混淆字符 */
     private static final String CODE_RE = "[A-HJ-NP-Z2-9]{8}";
 
     /** 纯邀请码精确匹配 */
@@ -1060,10 +1061,14 @@ public class InviteService {
         if (userId == null && phone != null && !phone.isBlank()) {
             User u = userMapper.selectOne(
                     new LambdaQueryWrapper<User>().eq(User::getPhone, phone));
-            if (u == null) throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+            if (u == null) {
+                throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+            }
             userId = u.getId();
         }
-        if (userId == null) throw new BusinessException(ErrorCode.PARAM_ERROR, "用户ID和手机号至少填写一个");
+        if (userId == null) {
+            throw new BusinessException(ErrorCode.PARAM_ERROR, "用户ID和手机号至少填写一个");
+        }
         User user = userMapper.selectById(userId);
         if (user == null) {
             throw new BusinessException(ErrorCode.USER_NOT_FOUND);

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

@@ -65,4 +65,7 @@ public class ExternalTokenRequest {
 
     /** 可选:三类医疗器械备案图片URL */
     private String medicalDeviceClass3Url;
+
+    /** 可选:第三方是否已审核通过(true=已审核,跳过我方审核) */
+    private Boolean licenseApproved;
 }

+ 3 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/dto/UserInfoResponse.java

@@ -78,6 +78,9 @@ public class UserInfoResponse {
     /** 是否已绑定邀请码 */
     private Boolean hasBoundInviteCode;
 
+    /** 营业执照审核状态:NOT_SUBMITTED / PENDING / APPROVED / REJECTED */
+    private String licenseStatus;
+
     /**
      * userId(兼容字段,等同于 id)
      */

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

@@ -4,7 +4,23 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.ErrorCode;
 import com.xuekairui.common.ValidateUtil;
-import com.xuekairui.user.dto.*;
+import com.xuekairui.common.event.ActivityEventRedisService;
+import com.xuekairui.user.dto.ChangePhoneRequest;
+import com.xuekairui.user.dto.DashboardResponse;
+import com.xuekairui.user.dto.ExternalTokenRequest;
+import com.xuekairui.user.dto.LoginResponse;
+import com.xuekairui.user.dto.MiniProgramLoginRequest;
+import com.xuekairui.user.dto.MiniProgramPhoneRequest;
+import com.xuekairui.user.dto.PasswordLoginRequest;
+import com.xuekairui.user.dto.RegisterRequest;
+import com.xuekairui.user.dto.SendCodeRequest;
+import com.xuekairui.user.dto.SetPasswordRequest;
+import com.xuekairui.user.dto.SmsLoginRequest;
+import com.xuekairui.user.dto.UserGateStatus;
+import com.xuekairui.user.dto.UserInfoResponse;
+import com.xuekairui.user.dto.UserLevelResponse;
+import com.xuekairui.user.dto.WechatBindRequest;
+import com.xuekairui.user.dto.WechatLoginRequest;
 import com.xuekairui.user.entity.User;
 import com.xuekairui.user.enums.LicenseStatus;
 import com.xuekairui.user.enums.MembershipLevel;
@@ -20,6 +36,8 @@ import org.slf4j.MDC;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import java.time.LocalDateTime;
 
@@ -49,6 +67,7 @@ public class AuthService {
     private final com.xuekairui.user.mapper.CrawlerUsageLogMapper crawlerUsageLogMapper;
     private final com.xuekairui.user.mapper.SearchRecordMapper searchRecordMapper;
     private final com.xuekairui.user.mapper.PurchaseIntentMapper purchaseIntentMapper;
+    private final ActivityEventRedisService activityEventRedisService;
 
     /**
      * 发送验证码
@@ -511,6 +530,10 @@ 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();
+
         if (existing == null) {
             com.xuekairui.user.entity.BusinessLicense license =
                     com.xuekairui.user.entity.BusinessLicense.builder()
@@ -528,11 +551,18 @@ public class AuthService {
                             .creditCode(request.getBusinessLicenseNo())
                             .medicalDeviceClass2Url(request.getMedicalDeviceClass2Url())
                             .medicalDeviceClass3Url(request.getMedicalDeviceClass3Url())
-                            .reviewStatus(LicenseStatus.PENDING.name())
+                            .reviewStatus(reviewStatus)
                             .showVerifiedBadge(false)
                             .syncSource("MINIAPP_SYNC")
                             .build();
             businessLicenseService.insert(license);
+            log.info("第三方用户{}同步入驻信息,创建新记录: licenseId={}, status={}",
+                    userId, license.getId(), reviewStatus);
+
+            // 第三方已审核通过:赠送30天高级会员 + 推送Redis事件
+            if (LicenseStatus.APPROVED.name().equals(reviewStatus)) {
+                grantLicenseApprovedReward(userId, license.getId());
+            }
         } else {
             if (storeName != null && !storeName.isBlank()) {
                 existing.setStoreName(storeName);
@@ -573,9 +603,19 @@ public class AuthService {
             if (isNotBlank(request.getMedicalDeviceClass3Url())) {
                 existing.setMedicalDeviceClass3Url(request.getMedicalDeviceClass3Url());
             }
-            existing.setReviewStatus(LicenseStatus.PENDING.name());
+            // 已 APPROVED 的不降级;第三方已审核的可从 PENDING/REJECTED 升级到 APPROVED
+            boolean upgradedToApproved = false;
+            if (!LicenseStatus.APPROVED.name().equals(existing.getReviewStatus())) {
+                existing.setReviewStatus(reviewStatus);
+                upgradedToApproved = LicenseStatus.APPROVED.name().equals(reviewStatus);
+            }
             existing.setSyncSource("MINIAPP_SYNC");
             businessLicenseService.updateById(existing);
+
+            // 从非APPROVED升级到APPROVED:赠送30天高级会员 + 推送Redis事件
+            if (upgradedToApproved) {
+                grantLicenseApprovedReward(userId, existing.getId());
+            }
         }
     }
 
@@ -583,6 +623,32 @@ public class AuthService {
         return s != null && !s.isBlank();
     }
 
+    /**
+     * 首次入驻审核通过:赠送30天高级会员 + 推送Redis事件供活动模块消费
+     * <p>与 BusinessLicenseService.approve() 的奖励逻辑保持一致。
+     */
+    private void grantLicenseApprovedReward(Long userId, Long licenseId) {
+        membershipService.grantOrExtendMembership(
+                userId,
+                MembershipLevel.PRO,
+                30,
+                "LICENSE",
+                "入驻信息审核通过赠送30天高级会员");
+        log.info("入驻信息审核通过(第三方同步),赠送用户{} 30天高级会员", userId);
+
+        // 事务提交后推送 Redis 事件,供活动模块消费(邀请有礼等)
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+            @Override
+            public void afterCommit() {
+                try {
+                    activityEventRedisService.pushLicenseApproved(userId, licenseId, "BUSINESS_LICENSE");
+                } catch (Exception e) {
+                    log.warn("存入入驻审核通过事件失败,不影响主流程: userId={}", userId, e);
+                }
+            }
+        });
+    }
+
     private UserGateStatus buildGateStatus(Long userId) {
         // 执照审核状态
         com.xuekairui.user.entity.BusinessLicense license =
@@ -658,6 +724,7 @@ public class AuthService {
         map.put("inviterId", info.getInviterId());
         map.put("inviterNickname", info.getInviterNickname());
         map.put("hasBoundInviteCode", info.getHasBoundInviteCode());
+        map.put("licenseStatus", info.getLicenseStatus());
         map.put("levelCode", info.getLevelCode());
         map.put("version", info.getVersion());
         return map;
@@ -797,6 +864,18 @@ public class AuthService {
                 log.warn("获取用户等级信息失败: userId={}, error={}", user.getId(), e.getMessage());
             }
         }
+
+        // 营业执照审核状态
+        String licenseStatus = LicenseStatus.NOT_SUBMITTED.name();
+        try {
+            com.xuekairui.user.entity.BusinessLicense license =
+                    businessLicenseService.getByUserId(user.getId());
+            if (license != null && license.getReviewStatus() != null) {
+                licenseStatus = license.getReviewStatus();
+            }
+        } catch (Exception e) {
+            log.warn("查询营业执照状态失败: userId={}, error={}", user.getId(), e.getMessage());
+        }
         // 邀请人信息
         assert user != null;
         Long inviterId = user.getInviterId();
@@ -830,6 +909,7 @@ public class AuthService {
                 .inviterId(inviterId)
                 .inviterNickname(inviterNickname)
                 .hasBoundInviteCode(inviterId != null)
+                .licenseStatus(licenseStatus)
                 .build();
     }
 

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

@@ -18,7 +18,9 @@ import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import java.time.LocalDateTime;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 入驻信息服务(封装所有 BusinessLicenseMapper 操作 + 审核业务逻辑)
@@ -68,6 +70,24 @@ public class BusinessLicenseService {
         return businessLicenseMapper.selectByUserId(userId);
     }
 
+    /**
+     * 批量查询用户入驻状态,返回 userId → reviewStatus
+     * 未提交的用户不在 Map 中(视为 NOT_SUBMITTED)
+     */
+    public Map<Long, String> getLicenseStatusByUserIds(List<Long> userIds) {
+        if (userIds == null || userIds.isEmpty()) {
+            return Map.of();
+        }
+        List<BusinessLicense> licenses = businessLicenseMapper.selectList(
+                new LambdaQueryWrapper<BusinessLicense>()
+                        .in(BusinessLicense::getUserId, userIds));
+        Map<Long, String> result = new HashMap<>();
+        for (BusinessLicense l : licenses) {
+            result.put(l.getUserId(), l.getReviewStatus());
+        }
+        return result;
+    }
+
     /**
      * 插入新入驻信息(通用方法,供同步等场景使用)
      */