Bläddra i källkod

feat(admin-auth): 后台管理端微信扫码登录端点(SC-G-03)

- /api/admin-auth/wechat-scan-qr:复用 WechatWebLoginService state 机制生成小程序码
- /api/admin-auth/wechat-scan-poll:按后台角色白名单签发管理端 JWT(不调用 ssoLogin,admin 可登录),成功后清 state 防重放
- JwtInterceptor/WebConfig 放行两个公开端点
Sisyphus 1 vecka sedan
förälder
incheckning
6c7e09fe8

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -60,6 +60,8 @@ private static final String[] PUBLIC_PATHS = {
          "/api/admin-auth/login",
          "/api/admin-auth/login-by-password",
          "/api/admin-auth/account-roles",
+         "/api/admin-auth/wechat-scan-qr",
+         "/api/admin-auth/wechat-scan-poll",
          "/api/articles/list",
          "/api/articles/detail",
          "/api/articles/categories",

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java

@@ -108,6 +108,8 @@ public class WebConfig implements WebMvcConfigurer {
                 "/api/admin-auth/login",
                 "/api/admin-auth/login-by-password",
                 "/api/admin-auth/account-roles",
+                "/api/admin-auth/wechat-scan-qr",
+                "/api/admin-auth/wechat-scan-poll",
                 "/api/product/list",
                 "/api/product/detail",
                 "/api/special-price/info",
@@ -158,6 +160,8 @@ public class WebConfig implements WebMvcConfigurer {
                 "/api/admin-auth/login",
                 "/api/admin-auth/login-by-password",
                 "/api/admin-auth/account-roles",
+                "/api/admin-auth/wechat-scan-qr",
+                "/api/admin-auth/wechat-scan-poll",
                 "/api/product/list",
                 "/api/product/detail",
                 "/api/special-price/info",

+ 117 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java

@@ -12,6 +12,7 @@ import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.LoginAttemptService;
 import com.etotem.cfc.service.UserService;
 import com.etotem.cfc.service.VerificationCodeService;
+import com.etotem.cfc.service.WechatWebLoginService;
 import lombok.extern.slf4j.Slf4j;
 import javax.annotation.Resource;
 import org.springframework.beans.factory.annotation.Value;
@@ -24,6 +25,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 @Slf4j
 @RestController
@@ -47,6 +49,9 @@ public class AdminAuthController {
     @Resource
     private LoginAttemptService loginAttemptService;
 
+    @Resource
+    private WechatWebLoginService wechatWebLoginService;
+
     @Value("${admin.skip-captcha:false}")
     private boolean skipCaptcha;
 
@@ -330,6 +335,118 @@ public class AdminAuthController {
         return Result.success(result);
     }
 
+    /**
+     * 后台管理端微信扫码登录:获取小程序码。
+     *
+     * <p>复用小程序端「小程序码扫码 SSO」的 state 机制({@link WechatWebLoginService}),
+     * 但与 {@code /api/auth/wechat-web/*} 完全独立:此处不调用 {@code UserService.ssoLogin}
+     * (该方法拒绝 admin 账号),改为按后台角色白名单签发管理端 JWT。
+     */
+    @PostMapping("/wechat-scan-qr")
+    public Result<Map<String, Object>> wechatScanQr() {
+        String state = UUID.randomUUID().toString().replace("-", "");
+        wechatWebLoginService.putState(state, "P");
+        Map<String, Object> data = new HashMap<>();
+        data.put("state", state);
+        try {
+            String base64 = wechatWebLoginService.generateQrImage(state);
+            data.put("qrImage", base64 == null || base64.isEmpty() ? "" : "data:image/png;base64," + base64);
+        } catch (RuntimeException e) {
+            log.error("生成后台登录小程序码失败: state={}", state, e);
+            wechatWebLoginService.removeState(state);
+            return Result.error("生成二维码失败:" + e.getMessage());
+        }
+        return Result.success(data);
+    }
+
+    /**
+     * 后台管理端微信扫码登录:轮询扫码结果。
+     *
+     * <p>status: pending(待扫码)/ expired(二维码过期)/ success(已签发 JWT)/ error(无管理权限等)。
+     * 成功返回字段与 {@code /api/admin-auth/login} 对齐,token 的 role claim 为选中后台身份。
+     */
+    @PostMapping("/wechat-scan-poll")
+    public Result<Map<String, Object>> wechatScanPoll(@RequestBody Map<String, String> body) {
+        String state = body != null ? body.get("state") : null;
+        Map<String, Object> data = new HashMap<>();
+        if (state == null || state.trim().isEmpty()) {
+            return Result.error("缺少 state");
+        }
+        Object stored = wechatWebLoginService.getState(state);
+        if (stored == null) {
+            data.put("status", "expired");
+            return Result.success(data);
+        }
+        String value = stored.toString();
+        if ("P".equals(value)) {
+            data.put("status", "pending");
+            return Result.success(data);
+        }
+        if (!value.startsWith("U")) {
+            data.put("status", "error");
+            data.put("message", "未知状态");
+            return Result.success(data);
+        }
+
+        // 已确认扫码即为终态:先清除 state,防止重复轮询重放
+        wechatWebLoginService.removeState(state);
+
+        Long userId;
+        try {
+            userId = Long.valueOf(value.substring(1));
+        } catch (NumberFormatException e) {
+            data.put("status", "error");
+            data.put("message", "二维码状态异常");
+            return Result.success(data);
+        }
+
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            data.put("status", "error");
+            data.put("message", "用户不存在");
+            return Result.success(data);
+        }
+
+        // parent/child 等 C 端账号不允许登录后台
+        List<String> backendRoles = AdminRoles.backendRolesOf(user.getRoles(), user.getRole());
+        if (backendRoles.isEmpty()) {
+            log.warn("用户 {} 扫码登录后台管理,无后台角色,角色为: {}", userId, user.getRole());
+            data.put("status", "error");
+            data.put("message", "该账号没有管理权限");
+            return Result.success(data);
+        }
+
+        // 身份选择:多角色账号可指定以哪个角色登录(对齐 /login 的身份选择逻辑)
+        String selected = backendRoles.get(0);
+        String loginRole = body.get("loginRole");
+        if (loginRole != null && !loginRole.isEmpty()) {
+            String normalized = AdminRoles.normalize(loginRole);
+            if (!backendRoles.contains(normalized)) {
+                data.put("status", "error");
+                data.put("message", "该账号没有【" + AdminRoles.roleLabel(normalized) + "】身份");
+                return Result.success(data);
+            }
+            selected = normalized;
+        }
+
+        List<String> roles = userService.getUserRoles(user.getId());
+        if (roles.isEmpty()) {
+            roles = Collections.singletonList(selected);
+        }
+
+        String token = jwtConfig.generateToken(user.getId(), buildTokenRoles(selected, roles));
+        log.info("后台微信扫码登录成功: userId={}, role={}", userId, selected);
+
+        data.put("status", "success");
+        data.put("token", token);
+        data.put("adminId", user.getId());
+        data.put("username", user.getNickname() != null ? user.getNickname() : user.getPhone());
+        data.put("realName", user.getRealName() != null ? user.getRealName() : user.getNickname());
+        data.put("role", selected);
+        data.put("roles", roles);
+        return Result.success(data);
+    }
+
     /**
      * 查询账号可用的后台身份列表(登录页身份选择下拉框用,公开接口)
      * 返回: [{role: 'admin', label: '超级管理员'}, ...]