Bladeren bron

修改omg控制器

qmj 16 uur geleden
bovenliggende
commit
397312670e

+ 4 - 1
.claude/homunculus/instincts/personal/controller-typed-request-contracts.md

@@ -12,9 +12,12 @@ source: "explicit-user-instruction"
 
 - Receive authentication tokens with the exact controller parameter form `@RequestHeader String token`.
 - Do not use `HttpServletRequest` to read tokens or inject it only for token access.
-- Receive business request parameters through explicit DTO types, never `Map<String, String>` or another Map as a handler-method input.
+- For POST handlers that receive business input, use an explicit DTO and annotate it with `@RequestBody`; do not rely on implicit binding.
+- For GET handlers, annotate every query parameter explicitly with `@RequestParam`. Continue to use `@RequestHeader` for tokens and `@PathVariable` for URL path variables.
+- Never use `Map<String, String>` or another Map as a handler-method input.
 - Bind third-party form callbacks to DTOs; convert a DTO to an internal Map only when a signature or gateway SDK requires it.
 
 ## Evidence
 
 - The user explicitly established this as a permanent rule for all controllers on 2026-08-10.
+- `PosOrderController` demonstrates the required forms with `@RequestBody OrderDTO` on POST handlers and explicit `@RequestParam` annotations on GET query parameters.

+ 20 - 0
.claude/homunculus/instincts/personal/validate-requests-with-i18n.md

@@ -0,0 +1,20 @@
+---
+id: validate-requests-with-i18n
+trigger: "when defining or validating Controller request DTOs in foodie_server"
+confidence: 0.9
+domain: "code-style"
+source: "explicit-user-instruction"
+---
+
+# Validate request data with internationalized messages
+
+## Action
+
+- Keep request DTOs as data carriers; do not add Bean Validation annotations such as `@NotNull`, `@NotBlank`, or `@Size` to DTO fields.
+- Do not rely on `@Valid` or `@Validated` on Controller DTO parameters to produce validation responses.
+- Validate business request parameters in the Controller or Service layer.
+- Resolve every user-facing validation error through the project's internationalization mechanism, such as `MessageUtils.message(...)`; do not hardcode a single-language validation message.
+
+## Evidence
+
+- The user explicitly required DTO validation to be avoided because validation error messages must support internationalization on 2026-08-10.

+ 26 - 2
CLAUDE.md

@@ -108,8 +108,32 @@ ALTER TABLE pos_order ADD COLUMN delivery_status BIGINT DEFAULT NULL COMMENT '
 创建或修改任何 Spring Controller 时,必须遵守以下规则:
 
 1. 需要登录 token 的接口必须直接声明 `@RequestHeader String token`,禁止通过 `HttpServletRequest` 读取 token,也不得仅为读取 token 而注入 `HttpServletRequest`。
-2. 控制器接收业务请求参数时必须使用类型明确的 DTO,禁止使用 `Map<String, String>`(或其它 Map)作为请求处理方法的入参。
-3. 第三方 `form-urlencoded` 回调同样使用 DTO(例如 `@ModelAttribute`)接收;如签名 SDK 必须使用 Map,只允许在 Controller 边界之后由 DTO 转换为内部 Map,不得把 Map 暴露为接口入参。
+2. POST 接口接收业务参数时,必须使用类型明确的 DTO,并在参数前显式添加 `@RequestBody`,例如 `@RequestBody OrderDTO orderDTO`;禁止使用未标注注解的隐式绑定。
+3. GET 接口接收查询参数时,每个查询参数必须显式添加 `@RequestParam`,例如 `@RequestParam Integer page`;token 仍使用 `@RequestHeader`,URL 路径变量仍使用 `@PathVariable`。
+4. 禁止使用 `Map<String, String>`(或其它 Map)作为请求处理方法的入参。
+5. 第三方 `form-urlencoded` 回调同样使用 DTO(例如 `@ModelAttribute`)接收;如签名 SDK 必须使用 Map,只允许在 Controller 边界之后由 DTO 转换为内部 Map,不得把 Map 暴露为接口入参。
+6. DTO 只用于承载请求数据,禁止在 DTO 字段上使用 `@NotNull`、`@NotBlank`、`@Size` 等 Bean Validation 注解,也不要依赖 Controller 参数上的 `@Valid` / `@Validated` 返回校验错误。业务参数校验必须放在 Controller 或 Service 中,并通过 `MessageUtils.message(...)` 等项目国际化机制返回错误信息,禁止硬编码仅支持单一语言的校验消息。
+
+## 后端构建与模块边界
+
+1. 本项目 Maven 编译目标为 JDK 21。当前机器执行构建时使用 `C:\Users\qmj\.jdks\graalvm-jdk-21.0.7`,只在当前命令环境临时设置 `JAVA_HOME` 和 `PATH`,不要修改用户的全局 Java 配置。
+2. 模块依赖方向为 `ruoyi-admin -> ruoyi-system`,禁止 `ruoyi-system` 反向依赖或导入 `com.ruoyi.app.*`。
+3. 同时依赖外部 HTTP(项目使用 httpclient4 + fastjson2)与应用层 Service 的集成代码应放在 `ruoyi-admin`,不要放入 `ruoyi-system`。
+4. 后端 Java/XML/YAML 与前端文件均可能使用 CRLF;编辑时保留原换行风格,禁止因小改动格式化或重写整个文件。
+
+## spec-kit 工作流
+
+1. 用户提到 `spec kit`、`spec-kit` 或 `speckit` 时,指 GitHub `github/spec-kit`,项目配置位于 `.specify/`,规格位于 `specs/`。
+2. 新功能默认按 `specify -> plan -> tasks -> implement` 流程进行。
+3. 已有功能追加或变更需求时,直接更新现有 `spec.md`、`plan.md`、`tasks.md` 并顺延任务,不重新启动完整 spec-kit 流程,除非用户明确要求。
+
+## Java 注释安全
+
+Java 块注释或 Javadoc 的正文中禁止出现额外的 `*/` 字符序列。描述 `ATM_*`、`CVS_*` 等通配值时,改写为“ATM 系列”“CVS 系列”,避免提前关闭注释导致编译失败。
+
+## 餐桌码适用范围
+
+餐桌码功能必须覆盖普通商家,不得仅按夜市摊主或夜市管理员设计权限。普通商家也可以查看自己门店的餐桌码及其关联订单。
 
 <!-- MANUAL ADDITIONS END -->
 # CLAUDE.md

+ 21 - 5
ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java

@@ -33,7 +33,6 @@ import com.ruoyi.system.utils.Auth;
 import com.ruoyi.system.utils.JwtUtil;
 import com.ruoyi.system.utils.OrderLogHelper;
 import jakarta.servlet.http.HttpServletResponse;
-import jakarta.validation.Valid;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -123,7 +122,10 @@ public class OmgPayController extends BaseController {
     @PostMapping("/create")
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult create(@RequestHeader String token,
-                             @Valid @ModelAttribute @RequestBody OmgOrderRequest request) {
+                             @RequestBody(required = false) OmgOrderRequest request) {
+        if (invalidOrderRequest(request)) {
+            return error(MessageUtils.message("no.order.id.error"));
+        }
         String orderid = request.getOrderid();
         String userId;
         try {
@@ -239,7 +241,7 @@ public class OmgPayController extends BaseController {
     @Anonymous
     @PostMapping(value = "/notify", produces = "text/plain;charset=UTF-8")
     @Transactional(rollbackFor = Exception.class)
-    public String notify(@ModelAttribute @RequestBody OmgCallbackRequest callback,
+    public String notify(@ModelAttribute OmgCallbackRequest callback,
                          @RequestHeader(value = "X-Forwarded-For", required = false) String forwardedFor) {
         Map<String, String> form;
         try {
@@ -507,7 +509,10 @@ public class OmgPayController extends BaseController {
     @Auth
     @PostMapping("/refund")
     public AjaxResult refund(@RequestHeader String token,
-                             @Valid @ModelAttribute OmgOrderRequest request) {
+                             @RequestBody(required = false) OmgOrderRequest request) {
+        if (invalidOrderRequest(request)) {
+            return error(MessageUtils.message("no.order.id.error"));
+        }
         String orderid = request.getOrderid();
         String userId;
         try {
@@ -699,7 +704,10 @@ public class OmgPayController extends BaseController {
     @RepeatSubmit(interval = 2000, message = "查询过于频繁")
     @PostMapping("/query")
     public AjaxResult query(@RequestHeader String token,
-                            @Valid @ModelAttribute OmgOrderRequest request) {
+                            @RequestBody(required = false) OmgOrderRequest request) {
+        if (invalidOrderRequest(request)) {
+            return error(MessageUtils.message("no.order.id.error"));
+        }
         String orderid = request.getOrderid();
         String userId;
         try {
@@ -933,6 +941,14 @@ public class OmgPayController extends BaseController {
         return firstIp.isEmpty() || firstIp.length() > 64 ? "unknown" : firstIp;
     }
 
+    private boolean invalidOrderRequest(OmgOrderRequest request) {
+        if (request == null || request.getOrderid() == null) {
+            return true;
+        }
+        String orderid = request.getOrderid();
+        return orderid.trim().isEmpty();
+    }
+
     private int toInt(String s, int def) {
         if (s == null || s.isEmpty()) {
             return def;

+ 2 - 5
ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgOrderRequest.java

@@ -1,18 +1,15 @@
 package com.ruoyi.app.pay.dto;
 
-import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 /**
  * OMG 订单操作请求。
  *
- * <p>字段名保留 {@code orderid},兼容现有 query/form 请求契约。</p>
+ * <p>字段名保留 {@code orderid},兼容现有 API 请求契约。业务校验由 Controller 负责,
+ * 以便错误信息统一走项目国际化机制。</p>
  */
 @Data
 public class OmgOrderRequest {
 
-    @NotBlank(message = "订单号不能为空")
-    @Size(max = 64, message = "订单号长度不能超过64个字符")
     private String orderid;
 }

+ 3 - 3
ruoyi-admin/src/main/resources/application.yml

@@ -42,13 +42,13 @@ omg:
   # 幕前支付根地址(测试 payment-stage.funpoint.com.tw;正式 payment.funpoint.com.tw)
   base-url: https://payment-stage.funpoint.com.tw
   # 支付结果服务端回调(须公网,OMG POST 回调 /pay/omg/notify,平台回纯串 1|OK)
-  return-url: https://your-public-domain/pay/omg/notify
+  return-url: https://foodieapi.waimai-paotui.com/pay/omg/notify
   # 支付完成前端结果页(须公网,与 return-url 不可相同;仅引导不改订单状态)
   order-result-url: https://your-h5-domain/#/pages/payResult
   # ATM/超商取号服务端回调(OMG POST 回调 /pay/omg/paymentInfo)
-  payment-info-url: https://your-public-domain/pay/omg/paymentInfo
+  payment-info-url: https://foodieapi.waimai-paotui.com/pay/omg/paymentInfo
   # ATM/超商取号前端展示页(可选)
-  client-redirect-url: https://your-h5-domain/#/pages/payInfo
+  client-redirect-url: https://foodieapi.waimai-paotui.com/#/pages/payInfo
   # 漏单补单定时兜底(方案B,OmgReconcileTask)—— 回调丢失不丢单
   reconcile:
     # 调度固定延迟(毫秒,上一轮跑完才开始计时),默认 3 分钟

+ 87 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/pay/OmgPayControllerTest.java

@@ -5,8 +5,11 @@ import ch.qos.logback.classic.spi.ILoggingEvent;
 import ch.qos.logback.core.read.ListAppender;
 import com.ruoyi.app.order.OrderLifecycleService;
 import com.ruoyi.app.pay.dto.OmgCallbackRequest;
+import com.ruoyi.app.pay.dto.OmgOrderRequest;
 import com.ruoyi.app.pay.dto.OmgRefundOutcome;
 import com.ruoyi.app.utils.omg.OmgPay;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.domain.PosOrderOmgPayment;
 import com.ruoyi.system.domain.PosOrderOmgRefund;
@@ -21,6 +24,9 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.MutablePropertyValues;
 import org.springframework.test.util.ReflectionTestUtils;
 import org.springframework.validation.DataBinder;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestParam;
 
@@ -38,6 +44,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -75,6 +82,32 @@ class OmgPayControllerTest {
         }
     }
 
+    @Test
+    void orderCommandEndpointsUseRequestBodyWithoutAutomaticBeanValidation() {
+        for (String methodName : List.of("create", "refund", "query")) {
+            Parameter request = Arrays.stream(method(methodName).getParameters())
+                    .filter(parameter -> parameter.getType() == OmgOrderRequest.class)
+                    .findFirst()
+                    .orElseThrow();
+
+            RequestBody requestBody = request.getAnnotation(RequestBody.class);
+            assertTrue(requestBody != null, methodName + " should bind its DTO with @RequestBody");
+            assertFalse(requestBody.required(), methodName + " should let business validation handle an empty body");
+            assertFalse(request.isAnnotationPresent(ModelAttribute.class));
+            assertFalse(Arrays.stream(request.getAnnotations())
+                    .anyMatch(annotation -> annotation.annotationType().getPackageName()
+                            .startsWith("jakarta.validation")));
+        }
+    }
+
+    @Test
+    void orderRequestDtoDoesNotContainBeanValidationAnnotations() {
+        assertFalse(Arrays.stream(OmgOrderRequest.class.getDeclaredFields())
+                .flatMap(field -> Arrays.stream(field.getAnnotations()))
+                .anyMatch(annotation -> annotation.annotationType().getPackageName()
+                        .startsWith("jakarta.validation")));
+    }
+
     @Test
     void omgCallbacksReceiveTypedDto() {
         for (String methodName : List.of("notify", "returnCallback", "paymentInfoCallback")) {
@@ -83,6 +116,60 @@ class OmgPayControllerTest {
         }
     }
 
+    @Test
+    void omgFormCallbacksUseModelAttributeInsteadOfRequestBody() {
+        for (String methodName : List.of("notify", "returnCallback", "paymentInfoCallback")) {
+            Parameter callback = Arrays.stream(method(methodName).getParameters())
+                    .filter(parameter -> parameter.getType() == OmgCallbackRequest.class)
+                    .findFirst()
+                    .orElseThrow();
+
+            assertTrue(callback.isAnnotationPresent(ModelAttribute.class));
+            assertFalse(callback.isAnnotationPresent(RequestBody.class));
+        }
+    }
+
+    @Test
+    void paymentInfoOrderIdRemainsAPathVariable() {
+        Parameter orderId = Arrays.stream(method("getPaymentInfo").getParameters())
+                .filter(parameter -> "orderid".equals(parameter.getName()))
+                .findFirst()
+                .orElseThrow();
+
+        assertTrue(orderId.isAnnotationPresent(PathVariable.class));
+        assertFalse(orderId.isAnnotationPresent(RequestParam.class));
+    }
+
+    @Test
+    void invalidOrderRequestsReturnInternationalizedBusinessError() {
+        OmgPayController controller = new OmgPayController();
+        OmgOrderRequest blank = new OmgOrderRequest();
+        OmgOrderRequest whitespace = new OmgOrderRequest();
+        whitespace.setOrderid("   ");
+
+        try (var messages = mockStatic(MessageUtils.class)) {
+            messages.when(() -> MessageUtils.message("no.order.id.error")).thenReturn("localized order error");
+
+            for (AjaxResult result : List.of(
+                    controller.create("token", null),
+                    controller.refund("token", blank),
+                    controller.query("token", whitespace))) {
+                assertEquals("localized order error", result.get(AjaxResult.MSG_TAG));
+            }
+        }
+    }
+
+    @Test
+    void orderRequestValidationDoesNotImposeALengthLimit() {
+        OmgOrderRequest request = new OmgOrderRequest();
+        request.setOrderid("x".repeat(1000));
+
+        Boolean invalid = ReflectionTestUtils.invokeMethod(
+                new OmgPayController(), "invalidOrderRequest", request);
+
+        assertFalse(Boolean.TRUE.equals(invalid));
+    }
+
     @Test
     void callbackDtoRetainsOriginalOmgParameterNames() {
         OmgCallbackRequest request = new OmgCallbackRequest();