Bläddra i källkod

第三方接口调整提交代码

liuchengsen 1 månad sedan
förälder
incheckning
cb708bf259

+ 3 - 0
zhijiayun-activity/src/main/java/com/xuekairui/activity/dto/ActivityRewardRecordResponse.java

@@ -60,6 +60,9 @@ public class ActivityRewardRecordResponse {
     /** 状态:0-正常 1-已撤销 */
     private Integer status;
 
+    /** 被邀请人药店名称(取入驻信息 t_business_license.store_name) */
+    private String pharmacyName;
+
     /* ====== 签到有礼特有 ====== */
 
     /** 周期内签到天数 */

+ 3 - 2
zhijiayun-activity/src/main/java/com/xuekairui/activity/entity/CheckInRecord.java

@@ -45,8 +45,9 @@ public class CheckInRecord {
     @Builder.Default
     private Integer rewardAmount = 0;
 
-    /** 签到端:WINDOWS/ANDROID/MINIAPP */
-    private String clientSource;
+    /** 签到端:PC/WINDOWS/ANDROID/MINIAPP,后续可扩展 */
+    @Builder.Default
+    private String clientSource = "PC";
 
     @TableField(fill = FieldFill.INSERT)
     private LocalDateTime createTime;

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

@@ -16,6 +16,7 @@ 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.enums.OperationSource;
 import com.xuekairui.user.enums.OperationType;
 import com.xuekairui.user.enums.OperatorRole;
 import com.xuekairui.user.mapper.BusinessLicenseMapper;
@@ -223,6 +224,8 @@ public class ActivityInviteService implements ActivityInviteGate {
     @Transactional
     public ActivityInviteConfig updateConfig(ActivityInviteConfigRequest request) {
         ActivityInviteConfig config = getConfig();
+        // 变更前快照
+        Map<String, Object> beforeData = configSnapshot(config);
         config.setEnabled(request.getEnabled());
         config.setStartTime(request.getStartTime());
         config.setEndTime(request.getEndTime());
@@ -237,11 +240,32 @@ public class ActivityInviteService implements ActivityInviteGate {
         } else {
             configMapper.updateById(config);
         }
+        // 变更后快照
+        Map<String, Object> afterData = configSnapshot(config);
+        auditLogService.logSuccess(
+                0L, OperatorRole.ADMIN, OperationType.INVITE_REWARD_GRANT,
+                "ACTIVITY_INVITE_CONFIG:" + CONFIG_ID, CONFIG_ID,
+                beforeData, afterData,
+                "邀请有礼活动配置变更",
+                OperationSource.ADMIN_CREATE);
         log.info("邀请有礼活动配置已更新: enabled={}, trigger={}, start={}, end={}",
                 config.getEnabled(), config.getRewardTrigger(), config.getStartTime(), config.getEndTime());
         return config;
     }
 
+    private Map<String, Object> configSnapshot(ActivityInviteConfig c) {
+        Map<String, Object> s = new HashMap<>();
+        s.put("enabled", c.getEnabled());
+        s.put("startTime", c.getStartTime());
+        s.put("endTime", c.getEndTime());
+        s.put("inviterRewardDays", c.getInviterRewardDays());
+        s.put("maxInviterReward", c.getMaxInviterReward());
+        s.put("rewardTrigger", c.getRewardTrigger());
+        s.put("requireMiniappInviter", c.getRequireMiniappInviter());
+        s.put("remark", c.getRemark());
+        return s;
+    }
+
     // ==========================================
     // 事件处理:入驻审核通过后发放邀请人奖励
     // ==========================================
@@ -316,13 +340,29 @@ public class ActivityInviteService implements ActivityInviteGate {
         wrapper.orderByDesc(ActivityInviteRewardRecord::getCreateTime);
         Page<ActivityInviteRewardRecord> recordPage = rewardRecordMapper.selectPage(page, wrapper);
 
+        // 批量查被邀请人的药店名称
+        List<Long> inviteeIds = recordPage.getRecords().stream()
+                .map(ActivityInviteRewardRecord::getInviteeId).distinct().toList();
+        Map<Long, String> storeNameMap = new HashMap<>();
+        if (!inviteeIds.isEmpty()) {
+            List<BusinessLicense> licenses = businessLicenseMapper.selectList(
+                    new LambdaQueryWrapper<BusinessLicense>()
+                            .in(BusinessLicense::getUserId, inviteeIds));
+            for (BusinessLicense l : licenses) {
+                if (l.getStoreName() != null && !l.getStoreName().isBlank()) {
+                    storeNameMap.put(l.getUserId(), l.getStoreName());
+                }
+            }
+        }
+
         Page<ActivityRewardRecordResponse> resultPage = new Page<>(pageNum, pageSize, recordPage.getTotal());
-        List<ActivityRewardRecordResponse> list = toRewardResponseList(recordPage.getRecords());
+        List<ActivityRewardRecordResponse> list = toRewardResponseList(recordPage.getRecords(), storeNameMap);
         resultPage.setRecords(list);
         return resultPage;
     }
 
-    private List<ActivityRewardRecordResponse> toRewardResponseList(List<ActivityInviteRewardRecord> records) {
+    private List<ActivityRewardRecordResponse> toRewardResponseList(
+            List<ActivityInviteRewardRecord> records, Map<Long, String> storeNameMap) {
         return records.stream()
                 .map(r -> ActivityRewardRecordResponse.builder()
                         .id(r.getId())
@@ -335,11 +375,17 @@ public class ActivityInviteService implements ActivityInviteGate {
                         .inviterId(r.getInviterId())
                         .inviteeId(r.getInviteeId())
                         .status(r.getStatus())
+                        .pharmacyName(storeNameMap.get(r.getInviteeId()))
                         .createTime(r.getCreateTime())
                         .build())
                 .toList();
     }
 
+    /** 无 storeNameMap 的重载(兼容旧调用) */
+    private List<ActivityRewardRecordResponse> toRewardResponseList(List<ActivityInviteRewardRecord> records) {
+        return toRewardResponseList(records, Map.of());
+    }
+
     // ==========================================
     // 私有辅助
     // ==========================================

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

@@ -253,11 +253,27 @@ public class CheckInConfigService {
     // 周期管理(运营端)
     // ==========================================
 
+    @Transactional
     public List<CheckInPeriod> listPeriods() {
         com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CheckInPeriod> wrapper =
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
         wrapper.orderByAsc(CheckInPeriod::getStartDate);
         List<CheckInPeriod> periods = periodMapper.selectList(wrapper);
+
+        // 周期为空时兜底:根据当前配置自动生成(避免首次访问无数据显示)
+        if (periods.isEmpty()) {
+            CheckInConfig config = getConfig();
+            if (config.getEnabled() != null && config.getEnabled() == 1
+                    && config.getStartTime() != null && config.getEndTime() != null) {
+                String cycleMode = config.getCycleMode() != null ? config.getCycleMode() : "FRI_THU";
+                generatePeriodsInRange(
+                        config.getStartTime().toLocalDate(),
+                        config.getEndTime().toLocalDate(),
+                        cycleMode, config);
+                periods = periodMapper.selectList(wrapper);
+            }
+        }
+
         for (CheckInPeriod period : periods) {
             String dynamicStatus = determineStatus(period.getStartDate(), period.getEndDate());
             if (!dynamicStatus.equals(period.getStatus())) {

+ 20 - 1
zhijiayun-activity/src/main/java/com/xuekairui/activity/service/CheckInService.java

@@ -55,10 +55,27 @@ public class CheckInService {
     private final CheckInRewardRecordMapper rewardRecordMapper;
     private final MembershipService membershipService;
 
+    /**
+     * 确保签到周期已生成(兜底:周期为空时根据配置自动生成,避免签到/发奖因无周期而失败)
+     */
+    private void ensurePeriodsExist() {
+        Long count = periodMapper.selectCount(
+                new LambdaQueryWrapper<CheckInPeriod>().last("LIMIT 1"));
+        if (count == null || count == 0) {
+            CheckInConfig config = checkinConfigService.getConfig();
+            if (config.getEnabled() != null && config.getEnabled() == 1
+                    && config.getStartTime() != null && config.getEndTime() != null) {
+                checkinConfigService.regeneratePeriods(config);
+                log.info("签到周期为空,已自动根据配置生成周期");
+            }
+        }
+    }
+
     /**
      * 查询今日签到状态 + 当前周期进度
      */
     public CheckInStatusResponse getCheckInStatus(Long userId) {
+        ensurePeriodsExist();
         CheckInConfig config = checkinConfigService.getConfig();
         LocalDate today = LocalDate.now();
 
@@ -127,6 +144,7 @@ public class CheckInService {
     @Transactional
     public CheckInResultResponse doCheckIn(Long userId, String clientSource) {
         checkinConfigService.ensureEnabled();
+        ensurePeriodsExist();
         LocalDate today = LocalDate.now();
 
         CheckInRecord exist = findRecord(userId, today);
@@ -148,7 +166,7 @@ public class CheckInService {
                 .continuousDays(continuousDays)
                 .rewardType("MEMBERSHIP")
                 .rewardAmount(0)
-                .clientSource(clientSource)
+                .clientSource(clientSource != null && !clientSource.isBlank() ? clientSource : "PC")
                 .build();
         checkinRecordMapper.insert(record);
 
@@ -285,6 +303,7 @@ public class CheckInService {
      */
     @Transactional
     public void tryGrantCycleReward(Long userId) {
+        ensurePeriodsExist();
         CheckInConfig config = checkinConfigService.getConfig();
         if (!checkinConfigService.isActive(config)) {
             return;

+ 2 - 2
zhijiayun-activity/src/main/resources/mapper/CheckInRecordMapper.xml

@@ -45,9 +45,9 @@
           AND checkin_date &lt;= #{endDate}
     </select>
 
-    <!-- 运营端:统计指定日期的签到人数 -->
+    <!-- 运营端:统计指定日期的签到人数(去重) -->
     <select id="countByDate" resultType="java.lang.Long">
-        SELECT COUNT(*)
+        SELECT COUNT(DISTINCT user_id)
         FROM t_checkin_record
         WHERE checkin_date = #{date}
     </select>

+ 1 - 1
zhijiayun-invite/src/main/java/com/xuekairui/invite/dto/InviteConversionStatsResponse.java

@@ -29,7 +29,7 @@ public class InviteConversionStatsResponse {
     /** 待注册人数(点击但未注册) */
     private Integer pendingCount;
     
-    /** 注册转化率(百分比) */
+    /** 邀请→注册转化率(百分比,= 已注册/总邀请) */
     private Double conversionRate;
     
     /** 累计奖励爬虫次数 */

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

@@ -41,4 +41,7 @@ public class InviteRewardResponse {
 
     /** 入驻状态:NOT_SUBMITTED-未入驻 / PENDING-待审核 / APPROVED-已通过 / REJECTED-已驳回 */
     private String licenseStatus;
+
+    /** 药店名称(取入驻信息 t_business_license.store_name) */
+    private String pharmacyName;
 }

+ 78 - 31
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteConfigService.java

@@ -7,11 +7,17 @@ import com.xuekairui.common.ErrorCode;
 import com.xuekairui.invite.dto.InviteConfigRequest;
 import com.xuekairui.invite.entity.InviteConfig;
 import com.xuekairui.invite.mapper.InviteConfigMapper;
+import com.xuekairui.user.enums.OperationSource;
+import com.xuekairui.user.enums.OperationType;
+import com.xuekairui.user.enums.OperatorRole;
+import com.xuekairui.user.service.OperationAuditLogService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 邀请配置服务(运营管理)
@@ -23,47 +29,60 @@ import java.util.List;
 public class InviteConfigService {
 
     private final InviteConfigMapper inviteConfigMapper;
+    private final OperationAuditLogService auditLogService;
 
     /**
-     * 获取当前生效的邀请配置
+     * 获取当前生效的邀请配置(优先 status=1,LIMIT 1 避免全表加载)
      */
     public InviteConfig getActiveConfig() {
-        List<InviteConfig> configs = inviteConfigMapper.selectList(null);
-        if (configs.isEmpty()) {
-            // 返回默认配置
-            return InviteConfig.builder()
-                    .rewardType("MEMBERSHIP")
-                    .rewardMonths(1)
-                    .rewardCrawlerCount(5)
-                    .maxDailyReward(50)
-                    .inviteCodeExpireDays(30)
-                    .maxInvitePerDay(10)
-                    .maxTotalInvites(30)
-                    .landingTitle("邀请你加入智价云(药店版)")
-                    .landingDesc("登录智价云(药店版),邀请好友各得好礼")
-                    .appName("智价云(药店版)")
-                    .status(1)
-                    .build();
-        }
-        // 返回第一个启用的配置,如果没有启用的则返回第一个
-        return configs.stream()
-                .filter(c -> c.getStatus() != null && c.getStatus() == 1)
-                .findFirst()
-                .orElse(configs.get(0));
+        // 优先查启用的配置
+        InviteConfig config = inviteConfigMapper.selectOne(
+                new LambdaQueryWrapper<InviteConfig>()
+                        .eq(InviteConfig::getStatus, 1)
+                        .orderByDesc(InviteConfig::getCreateTime)
+                        .last("LIMIT 1"));
+        if (config != null) {
+            return config;
+        }
+        // 无启用配置时返回默认
+        return InviteConfig.builder()
+                .rewardType("MEMBERSHIP")
+                .rewardMonths(1)
+                .rewardCrawlerCount(0)
+                .maxDailyReward(50)
+                .inviteCodeExpireDays(30)
+                .maxInvitePerDay(10)
+                .maxTotalInvites(30)
+                .landingTitle("邀请你加入智价云(药店版)")
+                .landingDesc("登录智价云(药店版),邀请好友各得好礼")
+                .appName("智价云(药店版)")
+                .status(1)
+                .build();
     }
 
     /**
-     * 更新当前生效的邀请配置(无则自动创建)
+     * 更新当前生效的邀请配置(无已存在配置时报错,需先创建)
      */
     public InviteConfig updateConfig(InviteConfigRequest request) {
-        InviteConfig config = getActiveConfig();
+        InviteConfig config = inviteConfigMapper.selectOne(
+                new LambdaQueryWrapper<InviteConfig>()
+                        .eq(InviteConfig::getStatus, 1)
+                        .orderByDesc(InviteConfig::getCreateTime)
+                        .last("LIMIT 1"));
+        if (config == null) {
+            throw new BusinessException(ErrorCode.BUSINESS_ERROR, "暂无可更新的邀请配置,请先创建");
+        }
+        Map<String, Object> beforeData = configSnapshot(config);
         buildConfigFromRequest(config, request);
+        inviteConfigMapper.updateById(config);
 
-        if (config.getId() != null) {
-            inviteConfigMapper.updateById(config);
-        } else {
-            inviteConfigMapper.insert(config);
-        }
+        Map<String, Object> afterData = configSnapshot(config);
+        auditLogService.logSuccess(
+                0L, OperatorRole.ADMIN, OperationType.INVITE_CONFIG_UPDATE,
+                "INVITE_CONFIG:" + config.getId(), config.getId(),
+                beforeData, afterData,
+                "邀请配置变更",
+                OperationSource.ADMIN_CREATE);
 
         log.info("邀请配置已更新: rewardType={}, rewardMonths={}, appDownloadUrl={}, miniappPath={}",
                 config.getRewardType(), config.getRewardMonths(), config.getAppDownloadUrl(), config.getMiniappPath());
@@ -104,15 +123,23 @@ public class InviteConfigService {
     }
 
     /**
-     * 更新指定ID的邀请配置
+     * 更新指定ID的邀请配置(含变更前后快照)
      */
     public InviteConfig updateConfig(Long id, InviteConfigRequest request) {
         InviteConfig config = inviteConfigMapper.selectById(id);
         if (config == null) {
             throw new BusinessException(ErrorCode.NOT_FOUND, "邀请配置不存在");
         }
+        Map<String, Object> beforeData = configSnapshot(config);
         buildConfigFromRequest(config, request);
         inviteConfigMapper.updateById(config);
+        Map<String, Object> afterData = configSnapshot(config);
+        auditLogService.logSuccess(
+                0L, OperatorRole.ADMIN, OperationType.INVITE_CONFIG_UPDATE,
+                "INVITE_CONFIG:" + id, id,
+                beforeData, afterData,
+                "邀请配置变更(指定ID)",
+                OperationSource.ADMIN_CREATE);
         log.info("更新邀请配置: id={}, rewardType={}, rewardMonths={}", config.getId(), config.getRewardType(), config.getRewardMonths());
         return config;
     }
@@ -222,4 +249,24 @@ public class InviteConfigService {
         }
         return config;
     }
+
+    /** 邀请配置快照(用于变更前后对比审计) */
+    private Map<String, Object> configSnapshot(InviteConfig c) {
+        Map<String, Object> s = new HashMap<>();
+        s.put("status", c.getStatus());
+        s.put("rewardType", c.getRewardType());
+        s.put("rewardMonths", c.getRewardMonths());
+        s.put("rewardCrawlerCount", c.getRewardCrawlerCount());
+        s.put("maxDailyReward", c.getMaxDailyReward());
+        s.put("inviteCodeExpireDays", c.getInviteCodeExpireDays());
+        s.put("maxInvitePerDay", c.getMaxInvitePerDay());
+        s.put("maxTotalInvites", c.getMaxTotalInvites());
+        s.put("appName", c.getAppName());
+        s.put("landingTitle", c.getLandingTitle());
+        s.put("landingDesc", c.getLandingDesc());
+        s.put("appDownloadUrl", c.getAppDownloadUrl());
+        s.put("miniappPath", c.getMiniappPath());
+        s.put("remark", c.getRemark());
+        return s;
+    }
 }

+ 29 - 12
zhijiayun-invite/src/main/java/com/xuekairui/invite/service/InviteService.java

@@ -286,7 +286,7 @@ public class InviteService {
         String landingTitle = config.getLandingTitle();
         if (landingTitle == null || landingTitle.isBlank()) {
             // 没有配置时,使用邀请人+药店名(如有)个性化展示
-            String pharmacyName = inviter != null ? inviter.getPharmacyName() : null;
+            String pharmacyName = inviter != null ? getPharmacyStoreName(inviter) : null;
             if (pharmacyName != null && !pharmacyName.isBlank()) {
                 landingTitle = inviterNickname + " · " + pharmacyName + " 邀请您使用";
             } else {
@@ -297,9 +297,9 @@ public class InviteService {
             if (landingTitle.contains("{inviter}")) {
                 landingTitle = landingTitle.replace("{inviter}", inviterNickname);
             }
-            if (inviter != null && inviter.getPharmacyName() != null
+            if (inviter != null && getPharmacyStoreName(inviter) != null
                     && landingTitle.contains("{pharmacy}")) {
-                landingTitle = landingTitle.replace("{pharmacy}", inviter.getPharmacyName());
+                landingTitle = landingTitle.replace("{pharmacy}", getPharmacyStoreName(inviter));
             }
         }
 
@@ -529,10 +529,10 @@ public class InviteService {
     }
 
     @NotNull
-    private static String getString(InviteConfig config, User inviter, String inviterNickname) {
+    private String getString(InviteConfig config, User inviter, String inviterNickname) {
         String landingTitle = config.getLandingTitle();
         if (landingTitle == null || landingTitle.isBlank()) {
-            String pharmacyName = inviter != null ? inviter.getPharmacyName() : null;
+            String pharmacyName = inviter != null ? getPharmacyStoreName(inviter) : null;
             if (pharmacyName != null && !pharmacyName.isBlank()) {
                 landingTitle = inviterNickname + " · " + pharmacyName + " 邀请您使用";
             } else {
@@ -542,9 +542,9 @@ public class InviteService {
             if (landingTitle.contains("{inviter}")) {
                 landingTitle = landingTitle.replace("{inviter}", inviterNickname);
             }
-            if (inviter != null && inviter.getPharmacyName() != null
+            if (inviter != null && getPharmacyStoreName(inviter) != null
                     && landingTitle.contains("{pharmacy}")) {
-                landingTitle = landingTitle.replace("{pharmacy}", inviter.getPharmacyName());
+                landingTitle = landingTitle.replace("{pharmacy}", getPharmacyStoreName(inviter));
             }
         }
         return landingTitle;
@@ -956,8 +956,9 @@ public class InviteService {
                 : inviteCodeMapper.selectByIds(codeIds).stream()
                         .collect(Collectors.toMap(InviteCode::getId, InviteCode::getCode));
 
-        // 批量查入驻状态
+        // 批量查入驻状态 + 药店名称
         Map<Long, String> licenseStatusMap = businessLicenseService.getLicenseStatusByUserIds(inviteeIds);
+        Map<Long, String> storeNameMap = businessLicenseService.getStoreNameByUserIds(inviteeIds);
 
         // 邀请人信息只查一次
         String inviterNickname = getUserNickname(userId);
@@ -984,6 +985,7 @@ public class InviteService {
                     .rewardCount(r.getRewardGranted())
                     .inviteCode(r.getInviteCodeId() != null ? codeMap.get(r.getInviteCodeId()) : null)
                     .licenseStatus(licenseStatusMap.getOrDefault(r.getInviteeId(), LicenseStatus.NOT_SUBMITTED.name()))
+                    .pharmacyName(storeNameMap.get(r.getInviteeId()))
                     .inviteTime(r.getCreateTime() != null ?
                             r.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null)
                     .build();
@@ -1022,7 +1024,7 @@ public class InviteService {
                 .inviterNickname(getDisplayName(inviter))
                 .inviterPhone(inviter != null ? ValidateUtil.maskPhone(inviter.getPhone()) : null)
                 .inviterAvatar(inviter != null ? inviter.getAvatar() : null)
-                .pharmacyName(inviter != null ? inviter.getPharmacyName() : null)
+                .pharmacyName(inviter != null ? getPharmacyStoreName(inviter) : null)
                 .inviteCode(inviteCode != null ? inviteCode.getCode() : null)
                 .inviteTime(relation.getCreateTime() != null ?
                         relation.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null)
@@ -1113,6 +1115,21 @@ public class InviteService {
         return getDisplayName(userMapper.selectById(userId));
     }
 
+    /**
+     * 获取用户关联的药店名称(以入驻信息 t_business_license.store_name 为准)
+     */
+    private String getPharmacyStoreName(User user) {
+        if (user == null) {
+            return null;
+        }
+        com.xuekairui.user.entity.BusinessLicense license =
+                businessLicenseService.getByUserId(user.getId());
+        if (license != null && license.getStoreName() != null && !license.getStoreName().isBlank()) {
+            return license.getStoreName();
+        }
+        return null;
+    }
+
     /**
      * 实体转响应(含邀请链接,文案从配置读取)
      */
@@ -1330,10 +1347,10 @@ public class InviteService {
                 .count();
         int pendingCount = totalInvited - registeredCount;
 
-        // 计算转化率
+        // 计算邀请→注册转化率(注册人数/总邀请人数,不含点击数噪声)
         double conversionRate = 0.0;
-        if (clickedCount > 0) {
-            conversionRate = Math.round((double) registeredCount / clickedCount * 10000.0) / 100.0;
+        if (totalInvited > 0) {
+            conversionRate = Math.round((double) registeredCount / totalInvited * 10000.0) / 100.0;
         }
 
         // 累计奖励

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

@@ -77,6 +77,26 @@ public class BusinessLicenseService {
         return businessLicenseMapper.selectByUserId(userId);
     }
 
+    /**
+     * 批量查询用户入驻状态的药店名称,返回 userId → storeName
+     * 未提交的用户不在 Map 中
+     */
+    public Map<Long, String> getStoreNameByUserIds(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) {
+            if (l.getStoreName() != null && !l.getStoreName().isBlank()) {
+                result.put(l.getUserId(), l.getStoreName());
+            }
+        }
+        return result;
+    }
+
     /**
      * 批量查询用户入驻状态,返回 userId → reviewStatus
      * 未提交的用户不在 Map 中(视为 NOT_SUBMITTED)