From 35c0810b268661c79f776fe922f7e3834cba4186 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期日, 27 九月 2026 20:12:45 +0800
Subject: [PATCH] 更新....

---
 talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java |  162 ++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 139 insertions(+), 23 deletions(-)

diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
index b437641..b1d51ea 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/auth/AdminAuthInterceptor.java
@@ -1,58 +1,169 @@
 package com.talentcard.server.business.admin.auth;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.talentcard.server.common.ApiResponse;
+import com.talentcard.server.common.BusinessException;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.HandlerInterceptor;
-import org.springframework.web.server.ResponseStatusException;
-import org.springframework.http.HttpStatus;
+import tools.jackson.databind.ObjectMapper;
 
+/**
+ * 管理后台的登录与页面权限拦截。
+ *
+ * <p>鉴权失败时直接写响应,不再抛异常:仓库里有一批按控制器限定的 {@code @RestControllerAdvice}
+ * ({@code com.talentcard.server.config} 下的 {@code *ExceptionHandler}),会把落在自己控制器上的
+ * 任何异常统一改写成 500「服务器内部错误 / 某某处理失败」。权限不足这类本该是 403 的响应被它们吞掉
+ * 之后,前端既看不出原因,也不会跳回登录页。直接写响应可以绕开这些兜底,保证状态码和提示语如实送达。
+ *
+ * <p>权限码按「这请求属于哪个页面」推导,并且只使用角色管理里真正能勾选的权限码——否则员工账号
+ * 配好了权限也进不去对应模块(例如体检预约的接口曾要求一个角色界面根本不提供的权限码)。
+ */
 @Component
 public class AdminAuthInterceptor implements HandlerInterceptor {
     public static final String OPERATOR_ID_ATTRIBUTE = "authenticatedAdminOperatorId";
     public static final String OPERATOR_NAME_ATTRIBUTE = "authenticatedAdminOperatorName";
 
-    private final AdminAuthService service;
+    private static final String FORBIDDEN_MESSAGE = "当前账号没有该操作权限";
 
-    public AdminAuthInterceptor(AdminAuthService service) {
+    /** 服务预约申请是「一个接口服务多个页面」(按 applicationType 区分),各归到自己的页面权限。 */
+    private static final Map<String, String> SERVICE_APPLICATION_PAGES = Map.of(
+            "MEDICAL_RESERVATION", "medical-reservations",
+            "MEDICAL_APPLICATION", "medical-applications",
+            "FUEL_SERVICE", "fuel-services",
+            "HOTEL_RESERVATION", "hotel-reservations",
+            "SPOUSE_EMPLOYMENT", "spouse-employment",
+            "PARKING_PLATE", "parking-plates"
+    );
+    private static final Pattern SERVICE_APPLICATION_ID =
+            Pattern.compile("^/api/admin/service-appointment-applications/(\\d+)(/.*)?$");
+
+    /** 家属卡接口同时挂在「家属卡申请」和「家属卡管理」两个页面下,两边对应的权限都认。 */
+    private static final List<String> FAMILY_CARD_VIEW_PERMISSIONS =
+            List.of("admin.family-card-applications.view", "admin.family-cards.view");
+    private static final List<String> FAMILY_CARD_UNBIND_PERMISSIONS =
+            List.of("admin.family-card-applications.audit", "admin.family-cards.disable");
+
+    private final AdminAuthService service;
+    private final JdbcTemplate jdbc;
+    private final ObjectMapper objectMapper;
+
+    public AdminAuthInterceptor(AdminAuthService service, JdbcTemplate jdbc, ObjectMapper objectMapper) {
         this.service = service;
+        this.jdbc = jdbc;
+        this.objectMapper = objectMapper;
     }
 
     @Override
-    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
-        Long operatorId = service.authenticate(request.getHeader(HttpHeaders.AUTHORIZATION));
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+        Long operatorId;
+        String operatorName;
+        try {
+            operatorId = service.authenticate(request.getHeader(HttpHeaders.AUTHORIZATION));
+            operatorName = service.operatorName(operatorId);
+        } catch (BusinessException exception) {
+            write(response, exception.errorCode().code(), exception.getMessage());
+            return false;
+        }
         request.setAttribute(OPERATOR_ID_ATTRIBUTE, operatorId);
-        request.setAttribute(OPERATOR_NAME_ATTRIBUTE, service.operatorName(operatorId));
-        String permission = permission(request.getMethod(), request.getRequestURI());
-        if (!permission.isBlank() && !service.hasPermission(operatorId, permission)) {
-            if (request.getRequestURI().startsWith("/api/admin/management/merchant-settlement/onboardings")) {
-                throw new com.talentcard.server.common.BusinessException(com.talentcard.server.common.ErrorCode.FORBIDDEN, "当前账号没有该操作权限");
-            }
-            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "当前账号没有该操作权限");
+        request.setAttribute(OPERATOR_NAME_ATTRIBUTE, operatorName);
+        List<String> permissions = permissions(request);
+        if (!permissions.isEmpty() && !service.hasAnyPermission(operatorId, permissions)) {
+            write(response, HttpStatus.FORBIDDEN.value(), FORBIDDEN_MESSAGE);
+            return false;
         }
         return true;
     }
 
-    private String permission(String method, String uri) {
+    private void write(HttpServletResponse response, int code, String message) throws IOException {
+        response.setStatus(code);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        objectMapper.writeValue(response.getWriter(),
+                ApiResponse.failure(code, message == null || message.isBlank() ? FORBIDDEN_MESSAGE : message));
+    }
+
+    /** 本次请求允许的权限码;返回空表示这个接口不校验页面权限。 */
+    private List<String> permissions(HttpServletRequest request) {
+        String method = request.getMethod();
+        String uri = request.getRequestURI();
         if (uri.startsWith("/api/admin/management/merchant-settlement/onboardings")) {
-            return "admin.merchant-settlement." + ("GET".equalsIgnoreCase(method) ? "view" : "edit");
+            return List.of("admin.merchant-settlement." + ("GET".equalsIgnoreCase(method) ? "view" : "edit"));
         }
         if (uri.startsWith("/api/admin/management/roles")) {
-            return managementPermission(method, uri, "roles");
+            return only(managementPermission(method, uri, "roles"));
         }
         if (uri.startsWith("/api/admin/management/staff")) {
-            return managementPermission(method, uri, "staff");
+            return only(managementPermission(method, uri, "staff"));
+        }
+        if (uri.startsWith("/api/admin/service-appointment-applications")) {
+            return serviceApplicationPermissions(request);
+        }
+        if (uri.startsWith("/api/admin/dependent-card-applies")) {
+            return dependentCardPermissions(method, uri);
         }
         String page = page(uri);
-        if (page.isBlank()) return "";
-        String action = "GET".equalsIgnoreCase(method) ? "view" : "edit";
-        if ("POST".equalsIgnoreCase(method)) action = "create";
-        if ("DELETE".equalsIgnoreCase(method)) action = "delete";
+        return page.isBlank() ? List.of() : only(pagePermission(method, uri, page));
+    }
+
+    /** 服务预约申请:列表/详情看模块的「查看」,导出看「下载」,处理/驳回看「审核」。 */
+    private List<String> serviceApplicationPermissions(HttpServletRequest request) {
+        String method = request.getMethod();
+        String uri = request.getRequestURI();
+        String action = uri.endsWith("/export") ? "export"
+                : "PATCH".equalsIgnoreCase(method) ? "audit" : "view";
+        String page = serviceApplicationPage(request);
+        // 归属不出来的(历史数据、通用页)沿用原来的 appointment-management 权限,避免放开成不校验。
+        if (page.isBlank()) page = "appointment-management";
+        return List.of("admin." + page + "." + action);
+    }
+
+    private String serviceApplicationPage(HttpServletRequest request) {
+        String type = request.getParameter("applicationType");
+        if (type == null || type.isBlank()) return applicationTypeById(request.getRequestURI());
+        return SERVICE_APPLICATION_PAGES.getOrDefault(type.trim().toUpperCase(Locale.ROOT), "");
+    }
+
+    /** 详情、处理这类只带 id 的请求拿不到 applicationType,回表查一次才知道属于哪个模块。 */
+    private String applicationTypeById(String uri) {
+        Matcher matcher = SERVICE_APPLICATION_ID.matcher(uri);
+        if (!matcher.matches()) return "";
+        List<String> types = jdbc.queryForList(
+                "select application_type from service_application where id = ? and deleted = 0 limit 1",
+                String.class, Long.valueOf(matcher.group(1)));
+        if (types.isEmpty()) return "";
+        String type = types.get(0);
+        return type == null ? "" : SERVICE_APPLICATION_PAGES.getOrDefault(type.trim().toUpperCase(Locale.ROOT), "");
+    }
+
+    private List<String> dependentCardPermissions(String method, String uri) {
+        if (uri.endsWith("/unbind")) return FAMILY_CARD_UNBIND_PERMISSIONS;
+        if (uri.endsWith("/audit")) return List.of("admin.family-card-applications.audit");
+        if ("GET".equalsIgnoreCase(method)) return FAMILY_CARD_VIEW_PERMISSIONS;
+        return List.of("admin.family-card-applications.edit");
+    }
+
+    private String pagePermission(String method, String uri, String page) {
+        if ("GET".equalsIgnoreCase(method)) return "admin." + page + ".view";
+        if ("POST".equalsIgnoreCase(method)) return "admin." + page + ".create";
+        if ("DELETE".equalsIgnoreCase(method)) return "admin." + page + ".delete";
         if ("PATCH".equalsIgnoreCase(method)) {
-            action = uri.endsWith("/audit") || uri.endsWith("/status") ? (uri.endsWith("/audit") ? "audit" : "disable") : "edit";
+            String action = uri.endsWith("/audit") ? "audit" : uri.endsWith("/status") ? "disable" : "edit";
+            return "admin." + page + "." + action;
         }
-        return "admin." + page + "." + action;
+        return "admin." + page + ".edit";
     }
 
     private String page(String uri) {
@@ -87,4 +198,9 @@
         else return "";
         return "admin." + page + "." + action;
     }
+
+    /** 空权限码表示「这个请求不校验页面权限」,不能当成一个空字符串去比权限。 */
+    private List<String> only(String permission) {
+        return permission == null || permission.isBlank() ? List.of() : List.of(permission);
+    }
 }

--
Gitblit v1.7.1