Преглед на файлове

第三方接口调整提交代码

liuchengsen преди 1 месец
родител
ревизия
34751431e9

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

@@ -12,9 +12,12 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.time.DayOfWeek;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.temporal.TemporalAdjusters;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 签到配置服务(运营端)
@@ -28,11 +31,17 @@ public class CheckInConfigService {
 
     private static final Long CONFIG_ID = 1L;
 
+    /** 周期模式 → 周期起始星期映射(当前支持两种周期类型) */
+    private static final Map<String, DayOfWeek> PERIOD_START_DAY = Map.of(
+            "FRI_THU", DayOfWeek.FRIDAY,
+            "MON_SUN", DayOfWeek.MONDAY
+    );
+
     private final CheckInConfigMapper checkinConfigMapper;
     private final CheckInPeriodMapper periodMapper;
 
     /**
-     * 获取签到配置(不存在则初始化默认值)
+     * 获取签到配置(不存在则初始化默认值并生成周期
      */
     public CheckInConfig getConfig() {
         CheckInConfig config = checkinConfigMapper.selectById(CONFIG_ID);
@@ -51,37 +60,47 @@ public class CheckInConfigService {
                     .remark("签到送会员:每周期累计签到达标后发放奖励")
                     .build();
             checkinConfigMapper.insert(config);
+            regeneratePeriods(config);
         }
         return config;
     }
 
     /**
      * 更新签到全局配置(自动根据活动时间和周期模式生成周期)
+     * <p>活动停用(enabled=0)时不生成周期;启用且活动时间或周期模式变化时,删除所有 PENDING 周期并重新生成;
+     * 周期区分信息(requiredDays/rewardMode/rewardDays/rewardLevel)变化时,同步更新到所有 PENDING 周期。
      */
     @Transactional
     public CheckInConfig updateConfig(CheckInConfigRequest request) {
         CheckInConfig config = getConfig();
-        
+
         boolean timeChanged = !java.util.Objects.equals(config.getStartTime(), request.getStartTime())
                 || !java.util.Objects.equals(config.getEndTime(), request.getEndTime());
         boolean modeChanged = request.getCycleMode() != null && !request.getCycleMode().equals(config.getCycleMode());
-        
+        boolean rewardChanged = !java.util.Objects.equals(config.getRequiredDays(), request.getRequiredDays())
+                || !java.util.Objects.equals(config.getRewardMode(), request.getRewardMode())
+                || !java.util.Objects.equals(config.getRewardDays(), request.getRewardDays())
+                || !java.util.Objects.equals(config.getRewardLevel(), request.getRewardLevel());
+
         config.setEnabled(request.getEnabled());
         config.setStartTime(request.getStartTime());
         config.setEndTime(request.getEndTime());
-        if (request.getRewardLevel() != null) config.setRewardLevel(request.getRewardLevel());
-        if (request.getCycleMode() != null) config.setCycleMode(request.getCycleMode());
-        if (request.getRequiredDays() != null) config.setRequiredDays(request.getRequiredDays());
-        if (request.getRewardMode() != null) config.setRewardMode(request.getRewardMode());
-        if (request.getRewardDays() != null) config.setRewardDays(request.getRewardDays());
-        if (request.getMaxRewardCount() != null) config.setMaxRewardCount(request.getMaxRewardCount());
-        if (request.getRemark() != null) config.setRemark(request.getRemark());
+        config.setRewardLevel(request.getRewardLevel());
+        config.setCycleMode(request.getCycleMode());
+        config.setRequiredDays(request.getRequiredDays());
+        config.setRewardMode(request.getRewardMode());
+        config.setRewardDays(request.getRewardDays());
+        config.setMaxRewardCount(request.getMaxRewardCount());
+        config.setRemark(request.getRemark());
         checkinConfigMapper.updateById(config);
 
-        if (timeChanged || modeChanged) {
+        boolean active = request.getEnabled() != null && request.getEnabled() == 1;
+        if (active && (timeChanged || modeChanged)) {
             regeneratePeriods(config);
+        } else if (active && rewardChanged) {
+            updatePendingPeriodsConfig(config);
         }
-        
+
         log.info("签到全局配置已更新: enabled={}, cycleMode={}, requiredDays={}, rewardDays={}",
                 config.getEnabled(), config.getCycleMode(), config.getRequiredDays(), config.getRewardDays());
         return config;
@@ -110,106 +129,97 @@ public class CheckInConfigService {
         
         generatePeriodsInRange(activityStart, activityEnd, cycleMode, config);
     }
+
+    /**
+     * 将周期区分信息(requiredDays/rewardMode/rewardDays/rewardLevel)同步更新到所有 PENDING 周期
+     * <p>不删除重建,避免丢失 ACTIVE/CLOSED 周期的历史数据。
+     */
+    @Transactional
+    public void updatePendingPeriodsConfig(CheckInConfig config) {
+        com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CheckInPeriod> wrapper =
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<>();
+        wrapper.eq(CheckInPeriod::getStatus, "PENDING");
+        List<CheckInPeriod> pendingPeriods = periodMapper.selectList(wrapper);
+        if (pendingPeriods.isEmpty()) {
+            return;
+        }
+        for (CheckInPeriod period : pendingPeriods) {
+            period.setRequiredDays(config.getRequiredDays());
+            period.setRewardMode(orDefault(config.getRewardMode(), "CUMULATIVE"));
+            period.setRewardDays(config.getRewardDays());
+            period.setRewardLevel(orDefault(config.getRewardLevel(), "PRO"));
+            periodMapper.updateById(period);
+        }
+        log.info("已同步{}个PENDING周期的区分信息", pendingPeriods.size());
+    }
     
     /**
      * 在指定时间范围内生成周期
+     * <p>根据周期模式(FRI_THU / MON_SUN)定位每个周期的起始星期,每个周期固定7天。
+     * 第一个周期从活动开始日所在周的周期起始星期开始(用 previousOrSame 回溯到本周起始日,避免漏掉当前周期)。
      */
     private void generatePeriodsInRange(LocalDate start, LocalDate end, String cycleMode, CheckInConfig config) {
-        LocalDate current = start;
+        DayOfWeek periodStartDay = PERIOD_START_DAY.getOrDefault(cycleMode.toUpperCase(), DayOfWeek.MONDAY);
+        LocalDate periodStart = start.with(TemporalAdjusters.previousOrSame(periodStartDay));
         int periodIndex = 1;
-        
-        while (!current.isAfter(end)) {
-            LocalDate periodStart;
-            LocalDate periodEnd;
-            
-            if ("FRI_THU".equalsIgnoreCase(cycleMode)) {
-                periodStart = getNextFriday(current);
-                periodEnd = periodStart.plusDays(6);
-            } else {
-                periodStart = getNextMonday(current);
-                periodEnd = periodStart.plusDays(6);
-            }
-            
-            if (periodStart.isAfter(end)) {
-                break;
-            }
-            
+
+        while (!periodStart.isAfter(end)) {
+            LocalDate periodEnd = periodStart.plusDays(6);
             if (periodEnd.isAfter(end)) {
                 periodEnd = end;
             }
-            
+
             CheckInPeriod period = CheckInPeriod.builder()
-                    .periodName("第" + periodIndex + "周期(" + periodStart.getMonthValue() + "/" + periodStart.getDayOfMonth() 
+                    .periodName("第" + periodIndex + "周期(" + periodStart.getMonthValue() + "/" + periodStart.getDayOfMonth()
                             + "~" + periodEnd.getMonthValue() + "/" + periodEnd.getDayOfMonth() + ")")
                     .cycleMode(cycleMode)
                     .startDate(periodStart)
                     .endDate(periodEnd)
                     .requiredDays(config.getRequiredDays())
-                    .rewardMode(config.getRewardMode() != null ? config.getRewardMode() : "CUMULATIVE")
+                    .rewardMode(orDefault(config.getRewardMode(), "CUMULATIVE"))
                     .rewardDays(config.getRewardDays())
-                    .rewardLevel(config.getRewardLevel() != null ? config.getRewardLevel() : "PRO")
+                    .rewardLevel(orDefault(config.getRewardLevel(), "PRO"))
                     .status(determineStatus(periodStart, periodEnd))
                     .sort(periodIndex)
                     .build();
-            
+
             periodMapper.insert(period);
             log.info("自动生成周期: {} {}~{}", period.getPeriodName(), periodStart, periodEnd);
-            
-            current = periodEnd.plusDays(1);
+
+            periodStart = periodStart.plusWeeks(1);
             periodIndex++;
         }
     }
-    
-    /**
-     * 获取下一个周五
-     */
-    private LocalDate getNextFriday(LocalDate date) {
-        int dayOfWeek = date.getDayOfWeek().getValue();
-        int daysUntilFriday = (5 - dayOfWeek + 7) % 7;
-        if (daysUntilFriday == 0 && date.getDayOfWeek().getValue() != 5) {
-            daysUntilFriday = 7;
-        }
-        return date.plusDays(daysUntilFriday == 0 ? 0 : daysUntilFriday);
-    }
-    
-    /**
-     * 获取下一个周一
-     */
-    private LocalDate getNextMonday(LocalDate date) {
-        int dayOfWeek = date.getDayOfWeek().getValue();
-        int daysUntilMonday = (1 - dayOfWeek + 7) % 7;
-        if (daysUntilMonday == 0 && date.getDayOfWeek().getValue() != 1) {
-            daysUntilMonday = 7;
-        }
-        return date.plusDays(daysUntilMonday == 0 ? 0 : daysUntilMonday);
-    }
 
     /**
-     * 校验签到功能是否开启
+     * 校验签到功能是否在活动时间范围内(停用也可签到,只是不发奖励)
      */
     public void ensureEnabled() {
         CheckInConfig config = getConfig();
-        if (config.getEnabled() == null || config.getEnabled() != 1) {
-            throw new BusinessException(ErrorCode.BUSINESS_ERROR, "签到功能已关闭");
-        }
         LocalDateTime now = LocalDateTime.now();
-        if (config.getStartTime() != null && now.isBefore(config.getStartTime())) {
+        if (isBeforeStart(config, now)) {
             throw new BusinessException(ErrorCode.BUSINESS_ERROR, "签到活动尚未开始");
         }
-        if (config.getEndTime() != null && now.isAfter(config.getEndTime())) {
+        if (isAfterEnd(config, now)) {
             throw new BusinessException(ErrorCode.BUSINESS_ERROR, "签到活动已结束");
         }
     }
 
     /**
-     * 判断活动是否进行中
+     * 判断活动是否进行中(enabled=1 且在活动时间范围内)
      */
     public boolean isActive(CheckInConfig config) {
         if (config.getEnabled() == null || config.getEnabled() != 1) return false;
         LocalDateTime now = LocalDateTime.now();
-        if (config.getStartTime() != null && now.isBefore(config.getStartTime())) return false;
-        if (config.getEndTime() != null && now.isAfter(config.getEndTime())) return false;
-        return true;
+        return !isBeforeStart(config, now) && !isAfterEnd(config, now);
+    }
+
+    private boolean isBeforeStart(CheckInConfig config, LocalDateTime now) {
+        return config.getStartTime() != null && now.isBefore(config.getStartTime());
+    }
+
+    private boolean isAfterEnd(CheckInConfig config, LocalDateTime now) {
+        return config.getEndTime() != null && now.isAfter(config.getEndTime());
     }
 
     // ==========================================
@@ -234,15 +244,15 @@ public class CheckInConfigService {
         CheckInConfig config = getConfig();
         CheckInPeriod period = CheckInPeriod.builder()
                 .periodName(request.getPeriodName())
-                .cycleMode(request.getCycleMode() != null ? request.getCycleMode() : config.getCycleMode())
+                .cycleMode(orDefault(request.getCycleMode(), config.getCycleMode()))
                 .startDate(request.getStartDate())
                 .endDate(request.getEndDate())
-                .requiredDays(request.getRequiredDays() != null ? request.getRequiredDays() : config.getRequiredDays())
-                .rewardMode(request.getRewardMode() != null ? request.getRewardMode() : config.getRewardMode())
-                .rewardDays(request.getRewardDays() != null ? request.getRewardDays() : config.getRewardDays())
-                .rewardLevel(request.getRewardLevel() != null ? request.getRewardLevel() : config.getRewardLevel())
+                .requiredDays(orDefault(request.getRequiredDays(), config.getRequiredDays()))
+                .rewardMode(orDefault(request.getRewardMode(), config.getRewardMode()))
+                .rewardDays(orDefault(request.getRewardDays(), config.getRewardDays()))
+                .rewardLevel(orDefault(request.getRewardLevel(), config.getRewardLevel()))
                 .status(determineStatus(request.getStartDate(), request.getEndDate()))
-                .sort(request.getSort() != null ? request.getSort() : 0)
+                .sort(orDefault(request.getSort(), 0))
                 .build();
         periodMapper.insert(period);
         log.info("签到周期已创建: id={}, {}~{}", period.getId(), period.getStartDate(), period.getEndDate());
@@ -255,15 +265,15 @@ public class CheckInConfigService {
         if (period == null) {
             throw new BusinessException(ErrorCode.PARAM_ERROR, "周期不存在");
         }
-        if (request.getPeriodName() != null) period.setPeriodName(request.getPeriodName());
-        if (request.getCycleMode() != null) period.setCycleMode(request.getCycleMode());
-        if (request.getStartDate() != null) period.setStartDate(request.getStartDate());
-        if (request.getEndDate() != null) period.setEndDate(request.getEndDate());
-        if (request.getRequiredDays() != null) period.setRequiredDays(request.getRequiredDays());
-        if (request.getRewardMode() != null) period.setRewardMode(request.getRewardMode());
-        if (request.getRewardDays() != null) period.setRewardDays(request.getRewardDays());
-        if (request.getRewardLevel() != null) period.setRewardLevel(request.getRewardLevel());
-        if (request.getSort() != null) period.setSort(request.getSort());
+        period.setPeriodName(orDefault(request.getPeriodName(), period.getPeriodName()));
+        period.setCycleMode(orDefault(request.getCycleMode(), period.getCycleMode()));
+        period.setStartDate(orDefault(request.getStartDate(), period.getStartDate()));
+        period.setEndDate(orDefault(request.getEndDate(), period.getEndDate()));
+        period.setRequiredDays(orDefault(request.getRequiredDays(), period.getRequiredDays()));
+        period.setRewardMode(orDefault(request.getRewardMode(), period.getRewardMode()));
+        period.setRewardDays(orDefault(request.getRewardDays(), period.getRewardDays()));
+        period.setRewardLevel(orDefault(request.getRewardLevel(), period.getRewardLevel()));
+        period.setSort(orDefault(request.getSort(), period.getSort()));
         period.setStatus(determineStatus(period.getStartDate(), period.getEndDate()));
         periodMapper.updateById(period);
         log.info("签到周期已更新: id={}, {}~{}", period.getId(), period.getStartDate(), period.getEndDate());
@@ -283,4 +293,9 @@ public class CheckInConfigService {
         if (today.isAfter(end)) return "CLOSED";
         return "ACTIVE";
     }
+
+    /** 值为 null 时返回默认值 */
+    private static <T> T orDefault(T value, T defaultValue) {
+        return value != null ? value : defaultValue;
+    }
 }

+ 11 - 4
zhijiayun-activity/src/main/java/com/xuekairui/activity/service/CheckInService.java

@@ -285,6 +285,11 @@ public class CheckInService {
      */
     @Transactional
     public void tryGrantCycleReward(Long userId) {
+        CheckInConfig config = checkinConfigService.getConfig();
+        if (!checkinConfigService.isActive(config)) {
+            return;
+        }
+
         LocalDate today = LocalDate.now();
 
         CheckInPeriod lastClosed = periodMapper.findLastClosedByDate(today);
@@ -301,7 +306,6 @@ public class CheckInService {
         }
 
         long rewarded = rewardRecordMapper.countRewardedByUser(userId);
-        CheckInConfig config = checkinConfigService.getConfig();
         int maxReward = config.getMaxRewardCount() != null ? config.getMaxRewardCount() : 3;
         if (rewarded >= maxReward) {
             return;
@@ -314,7 +318,9 @@ public class CheckInService {
         int rewardDays = lastClosed.getRewardDays() != null ? lastClosed.getRewardDays() : 7;
         String rewardLevelStr = lastClosed.getRewardLevel() != null ? lastClosed.getRewardLevel() : "PRO";
         MembershipLevel level = MembershipLevel.fromCode(rewardLevelStr);
-        if (level == null) level = MembershipLevel.PRO;
+        if (level == null) {
+            level = MembershipLevel.PRO;
+        }
 
         membershipService.grantOrExtendMembership(userId, level, rewardDays,
                 "CHECKIN_PERIOD", "签到周期奖励:" + lastClosed.getStartDate() + "~" + lastClosed.getEndDate()
@@ -394,8 +400,9 @@ public class CheckInService {
                         .ge(CheckInRecord::getCheckinDate, start)
                         .le(CheckInRecord::getCheckinDate, end)
                         .orderByAsc(CheckInRecord::getCheckinDate));
-        if (records.isEmpty()) return 0;
-
+        if (records.isEmpty()) {
+            return 0;
+        }
         int maxLen = 1;
         int curLen = 1;
         for (int i = 1; i < records.size(); i++) {

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

@@ -58,8 +58,8 @@ wechat:
 
 # 阿里云短信:生产部署时通过环境变量覆盖
 sms:
-  access-key-id: ${ALIBABA_CLOUD_ACCESS_KEY_ID:LTAI5tDwjfteBvivYN41r8sJ}
-  access-key-secret: ${ALIBABA_CLOUD_ACCESS_KEY_SECRET:yowuOGi2nYYnrqGpO3qcz94C4brcPp}
+  access-key-id: ${ALIBABA_CLOUD_ACCESS_KEY_ID:LTAI5t5pWgfa1BMztEuWBjdK}
+  access-key-secret: ${ALIBABA_CLOUD_ACCESS_KEY_SECRET:wU7FLzEr1NqLg2rJrmAu7Ibn69np0u}
   sign-name: ${SMS_SIGN_NAME:开邻}
   template-code: ${SMS_TEMPLATE_CODE:SMS_483305249}
   code-length: 6
@@ -69,8 +69,8 @@ sms:
 # 阿里云OSS配置(生产环境)
 oss:
   endpoint: ${OSS_ENDPOINT:oss-cn-shenzhen.aliyuncs.com}
-  access-key-id: ${ALIBABA_CLOUD_ACCESS_KEY_ID:LTAI5tDwjfteBvivYN41r8sJ}
-  access-key-secret: ${ALIBABA_CLOUD_ACCESS_KEY_SECRET:yowuOGi2nYYnrqGpO3qcz94C4brcPp}
+  access-key-id: ${ALIBABA_CLOUD_ACCESS_KEY_ID:LTAI5t5pWgfa1BMztEuWBjdK}
+  access-key-secret: ${ALIBABA_CLOUD_ACCESS_KEY_SECRET:wU7FLzEr1NqLg2rJrmAu7Ibn69np0u}
   bucket-name: ${OSS_BUCKET_NAME:kailin-saas}
   base-path: ${OSS_BASE_PATH:uploads/}
   url-prefix: ${OSS_URL_PREFIX:}

+ 3 - 3
zhijiayun-user/src/main/java/com/xuekairui/user/controller/AuthController.java

@@ -277,7 +277,7 @@ public class AuthController {
      * Header: X-Api-Key: {external-system.miniapp.api-key}
      */
     @PostMapping("/external/miniapp-token")
-    public Result<LoginResponse> externalMiniAppToken(
+    public Result<ExternalTokenResponse> externalMiniAppToken(
             @Valid @RequestBody ExternalTokenRequest request,
             HttpServletRequest httpRequest) {
         // API Key + IP 白名单鉴权(通过数据库配置校验)
@@ -285,7 +285,7 @@ public class AuthController {
         String ip = ValidateUtil.getClientIp(httpRequest);
         systemConfigService.validateAccess(apiKey, ip);
 
-        LoginResponse response = authService.externalMiniAppToken(request, ip);
+        ExternalTokenResponse response = authService.externalMiniAppToken(request, ip);
         return Result.success(response);
     }
 
@@ -306,4 +306,4 @@ public class AuthController {
         authService.syncExternalInfo(request);
         return Result.success();
     }
-}
+}

+ 34 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/dto/ExternalTokenResponse.java

@@ -0,0 +1,34 @@
+package com.xuekairui.user.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 外部系统换取 Token 响应(snake_case,匹配第三方 OpenAPI 格式)
+ */
+@Data
+@Builder
+public class ExternalTokenResponse {
+
+    @JsonSerialize(using = ToStringSerializer.class)
+    @JsonProperty("user_id")
+    private Long userId;
+
+    @JsonProperty("access_token")
+    private String accessToken;
+
+    @JsonProperty("refresh_token")
+    private String refreshToken;
+
+    @JsonProperty("expires_in")
+    private Long expiresIn;
+
+    @JsonProperty("new_user")
+    private Boolean newUser;
+
+    @JsonProperty("role")
+    private String role;
+}

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

@@ -75,4 +75,4 @@ public class SyncShopReportRequest {
     /** 小程序APPID */
     @JsonProperty("app_id")
     private String appId;
-}
+}

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

@@ -16,4 +16,4 @@ public class SyncShopReportResponse {
 
     @JsonProperty("new_user")
     private Boolean newUser;
-}
+}

+ 10 - 21
zhijiayun-user/src/main/java/com/xuekairui/user/service/AuthService.java

@@ -5,25 +5,7 @@ import com.xuekairui.common.BusinessException;
 import com.xuekairui.common.ErrorCode;
 import com.xuekairui.common.ValidateUtil;
 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.SyncInfoRequest;
-import com.xuekairui.user.dto.SyncShopReportRequest;
-import com.xuekairui.user.dto.SyncShopReportResponse;
-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.dto.*;
 import com.xuekairui.user.entity.User;
 import com.xuekairui.user.enums.LicenseStatus;
 import com.xuekairui.user.enums.MembershipLevel;
@@ -280,7 +262,7 @@ public class AuthService {
      * <p>查找或自动注册用户。根据 loginSource 决定过期策略:MINIAPP=30天不踢PC / 其他=1小时。
      */
     @Transactional
-    public LoginResponse externalMiniAppToken(ExternalTokenRequest request, String ip) {
+    public ExternalTokenResponse externalMiniAppToken(ExternalTokenRequest request, String ip) {
         // 登录来源:不传默认 MINIAPP(向后兼容)
         String loginSource = request.getLoginSource() != null && !request.getLoginSource().isBlank()
                 ? request.getLoginSource() : "MINIAPP";
@@ -379,7 +361,14 @@ public class AuthService {
         auditLogService.logSuccess(user.getId(), user.getNickname(), OperatorRole.USER.getCode(),
                 isNewUser ? OperationType.USER_REGISTER : OperationType.USER_LOGIN,
                 "USER:" + user.getId(), user.getId());
-        return response;
+        return ExternalTokenResponse.builder()
+                .userId(response.getUserId())
+                .accessToken(response.getAccessToken())
+                .refreshToken(response.getRefreshToken())
+                .expiresIn(response.getExpiresIn())
+                .newUser(isNewUser)
+                .role(response.getRole())
+                .build();
     }
 
     public LoginResponse heartbeat(Long userId, String role, String loginSource) {