alphaH преди 1 седмица
родител
ревизия
d2ca563b77

+ 2 - 2
.codegraph/daemon.pid

@@ -1,6 +1,6 @@
 {
-  "pid": 24184,
+  "pid": 38076,
   "version": "0.9.9",
   "socketPath": "\\\\.\\pipe\\codegraph-9515f3f05b4112da",
-  "startedAt": 1782720324347
+  "startedAt": 1787191246347
 }

BIN
frontend/dist.zip


+ 4 - 0
java/src/main/java/com/payment/platform/core/security/SecurityConfig.java

@@ -36,6 +36,10 @@ public class SecurityConfig {
      */
     /** 对应 Python white_api_list_path + Swagger */
     private static final String[] WHITE_LIST = {
+            // OpenAPI 账户接口由 TenantApiKeyAuthFilter 独立认证(API Key + 签名),
+            // 此处放行授权层,避免 filter 漏跑时返回 Spring Security 默认空 403。
+            // filter 认证失败时无 request attribute,Controller 会抛 401 JSON 响应。
+            "/payment/openapi/account/**",
             "/system/auth/login",
             "/system/auth/login/mini",
             "/system/auth/login/sms",

+ 31 - 1
java/src/main/java/com/payment/platform/core/security/TenantApiKeyAuthFilter.java

@@ -17,6 +17,10 @@ import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.stereotype.Component;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.util.ContentCachingRequestWrapper;
@@ -24,7 +28,9 @@ import org.springframework.web.util.ContentCachingRequestWrapper;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.OffsetDateTime;
+import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
 /**
  * 租户 API Key 认证过滤器 — 完整镜像 Python app/core/apikey.py L16-136 的 TenantApiKeyAuth
@@ -96,7 +102,10 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
     @Override
     protected boolean shouldNotFilter(HttpServletRequest request) {
         // 仅拦截 OpenAPI 账户端点 (Python: OpenapiRouter 中使用了 TenantApiKeyAuth 的路由)
-        String path = request.getRequestURI();
+        // 注意: 必须用 getServletPath()(不含 context-path)而非 getRequestURI()。
+        // 后端 context-path=/api/v1,getRequestURI() 返回 /api/v1/payment/openapi/...
+        // 与 OPENAPI_ACCOUNT_PATHS 精确匹配永远不命中,导致 API Key 认证被整体跳过。
+        String path = request.getServletPath();
         for (String p : OPENAPI_ACCOUNT_PATHS) {
             if (path.equals(p)) {
                 return false; // 需要过滤
@@ -179,6 +188,24 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
         wrappedRequest.setAttribute("openapi.apiKey", apiKey);
         wrappedRequest.setAttribute("openapi.apiKeyId", keyEntity.getId());
 
+        // ---- 6b. 写入 SecurityContext ----
+        //     SecurityConfig 的 anyRequest().authenticated() 需要非匿名认证才能通过授权。
+        //     若只设置 request attribute 而不设置 SecurityContext,授权过滤器会把请求视为
+        //     匿名 → 默认 403 空响应(对齐 JwtAuthFilter 认证成功后的 setAuthentication)。
+        //     注意: principal 必须是 LoginUser(带 tenantId)而非裸 Long —
+        //     TenantInnerInterceptor.getTenantId() 只识别 LoginUser,裸 Long 会让后续
+        //     业务查询(pay_account 等)被追加 tenant_id=0 而查不到数据。
+        LoginUser openapiUser = new LoginUser();
+        openapiUser.setTenantId(keyEntity.getTenantId());
+        openapiUser.setUsername("openapi:" + apiKey);
+        openapiUser.setStatus("0");
+        openapiUser.setIsSuperuser(false);
+        openapiUser.setAuthorities(List.of(new SimpleGrantedAuthority("ROLE_OPENAPI")));
+        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
+                openapiUser, null, openapiUser.getAuthorities());
+        authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+        SecurityContextHolder.getContext().setAuthentication(authToken);
+
         // ---- 7. 记录成功日志 (Python L109-119) ----
         logApiCall(keyEntity.getId(), keyEntity.getTenantId(), request, response, 200, startTime);
 
@@ -251,6 +278,8 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
                             int responseCode, long startTime) {
         try {
             TenantApiLogEntity logEntry = new TenantApiLogEntity();
+            // sys_tenant_api_log.uuid NOT NULL 且无默认值,必须显式赋值(对齐 ApikeyService.create 模式)
+            logEntry.setUuid(UUID.randomUUID().toString());
             logEntry.setApiKeyId(apiKeyId);
             if (tenantId != null) logEntry.setTenantId(tenantId);
             logEntry.setEndpoint(request.getRequestURI());     // Python L160: endpoint
@@ -260,6 +289,7 @@ public class TenantApiKeyAuthFilter extends OncePerRequestFilter {
             // Python L167: response_time = (time.time() - start_time) * 1000  (毫秒)
             float responseTimeMs = (System.currentTimeMillis() - startTime);
             logEntry.setResponseTime(responseTimeMs);
+            logEntry.setTenantId(tenantId);
             // Python L173: request_data = None (避免记录敏感数据)
             logEntry.setRequestData(null);
 

+ 5 - 1
java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java

@@ -64,7 +64,11 @@ public class TenantInnerInterceptor extends TenantLineInnerInterceptor {
             "pay_expense_institution", // 费控制度(通知处理无认证上下文)
             "pay_facetoface_order",  // 当面付申请单(定时轮询无认证上下文)
             "pay_f2f_trade",          // 当面付收款记录(定时轮询无认证上下文)
-            "pay_account"            // 资金账户(签约/进件通知处理无认证上下文)
+            "pay_account",           // 资金账户(签约/进件通知处理无认证上下文)
+            "sys_tenant_api_key",    // 开放API Key(TenantApiKeyAuthFilter 认证阶段无租户上下文,
+                                     //   API Key 全局唯一,须跨租户查询;管理端登录后仍按租户隔离)
+            "open_transfer",         // 开放转账映射(支付宝通知回调 notifyTransferResult 无认证上下文)
+            "open_conf"              // 开放配置(回调通知 resolveReturnUrl 无认证上下文)
     );
 
     public TenantInnerInterceptor() {

+ 3 - 0
java/src/main/java/com/payment/platform/module/payment/openapi/dto/OpenConfCreateDTO.java

@@ -42,6 +42,9 @@ public class OpenConfCreateDTO {
     @Schema(description = "加密密钥")
     private String encryptKey;
 
+    @Schema(description = "状态: ENABLED/DISABLED,不传默认 ENABLED")
+    private String status;
+
     @Schema(description = "描述")
     private String description;
 }

+ 14 - 0
java/src/main/java/com/payment/platform/module/payment/openapi/service/OpenapiService.java

@@ -99,6 +99,20 @@ public class OpenapiService {
         if (dto.getDescription() != null) {
             entity.setDescription(dto.getDescription());
         }
+        // open_conf.app_id / status 均为 NOT NULL 且无默认值,创建时必须显式赋值。
+        // 默认值对齐 Python 版 save_conf_service 创建分支:
+        //   - app_id     = get_snowflake_id_str(tenant_id) → 19位雪花ID + tenantId
+        //   - gateway_url = "https://api.qcsj88888.com"(硬编码平台网关)
+        //   - status     = model.py ORM 默认 "ENABLED"
+        if (StrUtil.isBlank(entity.getAppId())) {
+            entity.setAppId(SnowflakeIdGenerator.nextIdStr(tenantId.intValue()));
+        }
+        if (StrUtil.isBlank(entity.getGatewayUrl())) {
+            entity.setGatewayUrl("https://api.qcsj88888.com");
+        }
+        if (StrUtil.isBlank(entity.getStatus())) {
+            entity.setStatus("ENABLED");
+        }
         confMapper.insert(entity);
         return BeanUtil.copyProperties(confMapper.selectById(entity.getId()), OpenConfVO.class);
     }

Файловите разлики са ограничени, защото са твърде много
+ 0 - 730
logs/payment-platform-error.log


Файловите разлики са ограничени, защото са твърде много
+ 0 - 114
logs/payment-platform.log


Някои файлове не бяха показани, защото твърде много файлове са промени