Sfoglia il codice sorgente

新增商家与用户共用字典查询接口并明确中文提交规范

qmj 4 giorni fa
parent
commit
c4521b6f79

+ 1 - 1
AGENTS.md

@@ -10,7 +10,7 @@
 - 匹配现有代码风格。只移除本次修改造成的无用 import、变量或方法。
 - 修复缺陷时优先用可复现检查或测试证明问题,再验证修复结果;多步骤任务给出简短、可验证的执行计划。
 - 当用户说“只改 X”时,只改 X。简单的格式、显示或单文件修改不要创建任务计划,也不要启动 Agent。
-- Git 提交信息使用中文。这是用户明确要求长期保留的偏好,后续 commit/push 均遵守。
+- Git 提交信息使用中文,提交标题和正文均使用中文,可保留必要的代码标识符。这是用户明确要求长期保留的偏好,后续 commit/push 均遵守。
 
 ## OMG 支付执行纪律与本次复盘
 

+ 46 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/common/AppDictController.java

@@ -0,0 +1,46 @@
+package com.ruoyi.app.common;
+
+import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.service.ISysDictTypeService;
+import com.ruoyi.system.utils.Auth;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/** 商家与用户客户端共用字典查询,沿用客户端 token 会话认证。 */
+@RestController
+@RequestMapping("/app/dict")
+public class AppDictController extends BaseController {
+    private final ISysDictTypeService dictTypeService;
+
+    public AppDictController(ISysDictTypeService dictTypeService) {
+        this.dictTypeService = dictTypeService;
+    }
+
+    public record DictOption(String dictLabel, String dictValue, Long dictSort, String isDefault) { }
+
+    @Anonymous
+    @Auth
+    @GetMapping("/data")
+    public AjaxResult data(@RequestHeader String token,
+                           @RequestParam(required = false) String dictType) {
+        if (dictType == null || dictType.isBlank() || dictType.trim().length() > 100) {
+            return error(MessageUtils.message("app.dict.type.invalid"));
+        }
+        // 用户确认客户端可查询任意类型;复用既有缓存和按 dict_sort 排序的查询。
+        var rows = dictTypeService.selectDictDataByType(dictType.trim());
+        List<DictOption> options = rows == null ? List.of() : rows.stream()
+                .filter(row -> "0".equals(row.getStatus()))
+                .map(row -> new DictOption(row.getDictLabel(), row.getDictValue(),
+                        row.getDictSort(), row.getIsDefault()))
+                .toList();
+        return success(options);
+    }
+}

+ 2 - 0
ruoyi-admin/src/main/resources/i18n/messages.properties

@@ -526,3 +526,5 @@ invoice.print.saveFailed=打印记录保存失败,请使用原请求号重试
 invoice.print.tooLarge=发票明细过长,无法生成打印文件
 
 invoice.print.preparing=发票打印文件正在准备,请稍后重试
+
+app.dict.type.invalid=字典类型不能为空且不能超过100个字符

+ 2 - 0
ruoyi-admin/src/main/resources/i18n/messages_en_US.properties

@@ -512,3 +512,5 @@ invoice.print.saveFailed=Could not save the print record. Retry with the same re
 invoice.print.tooLarge=Invoice details exceed the supported print size.
 
 invoice.print.preparing=Invoice print files are being prepared. Please try again shortly.
+
+app.dict.type.invalid=Dictionary type is required and must not exceed 100 characters

+ 2 - 0
ruoyi-admin/src/main/resources/i18n/messages_th_TH.properties

@@ -495,3 +495,5 @@ merchant.order.print.invalidSnapshot=ข้อมูลใบรายการ
 # 029 device phone switch
 no.oauth.device.phone.changed=โปรดเข้าสู่ระบบด้วยหมายเลขโทรศัพท์ที่เกี่ยวข้องและรหัสยืนยันก่อนเข้าสู่ระบบผ่านบัญชีภายนอก
 no.oauth.binding.conflict=การเชื่อมโยงบัญชีภายนอกเปลี่ยนแปลงแล้ว โปรดเข้าสู่ระบบอีกครั้ง
+
+app.dict.type.invalid=ต้องระบุประเภทพจนานุกรมและต้องไม่เกิน 100 อักขระ

+ 2 - 0
ruoyi-admin/src/main/resources/i18n/messages_vi.properties

@@ -50,3 +50,5 @@ invoice.print.saveFailed=Không thể lưu lần in. Hãy thử lại với cùn
 invoice.print.tooLarge=Chi tiết hóa đơn vượt quá kích thước in được hỗ trợ.
 
 invoice.print.preparing=Tệp in hóa đơn đang được chuẩn bị. Vui lòng thử lại sau.
+
+app.dict.type.invalid=Loại từ điển là bắt buộc và không được vượt quá 100 ký tự

+ 2 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties

@@ -514,3 +514,5 @@ invoice.print.saveFailed=打印记录保存失败,请使用原请求号重试
 invoice.print.tooLarge=发票明细过长,无法生成打印文件
 
 invoice.print.preparing=发票打印文件正在准备,请稍后重试
+
+app.dict.type.invalid=字典类型不能为空且不能超过100个字符

+ 2 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties

@@ -513,3 +513,5 @@ invoice.print.saveFailed=列印紀錄儲存失敗,請使用原請求號重試
 invoice.print.tooLarge=發票明細過長,無法產生列印檔案
 
 invoice.print.preparing=發票列印檔案正在準備,請稍後重試
+
+app.dict.type.invalid=字典類型不能為空且不能超過100個字元

+ 87 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/common/AppDictControllerTest.java

@@ -0,0 +1,87 @@
+package com.ruoyi.app.common;
+
+import com.ruoyi.common.core.domain.entity.SysDictData;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.service.ISysDictTypeService;
+import com.ruoyi.system.utils.Auth;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.mockito.MockedStatic;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+class AppDictControllerTest {
+    private final ISysDictTypeService dictionaries = mock(ISysDictTypeService.class);
+    private final AppDictController controller = new AppDictController(dictionaries);
+
+    private MockedStatic<MessageUtils> messages;
+
+    @BeforeEach void setUpMessages() {
+        messages = mockStatic(MessageUtils.class);
+        messages.when(() -> MessageUtils.message(anyString())).thenAnswer(call -> call.getArgument(0));
+    }
+
+    @AfterEach void closeMessages() { messages.close(); }
+
+    @Test void returnsConfiguredOptionsWithoutAdministrativeMetadata() throws Exception {
+        var row = option("分成", "0", "0");
+        row.setRemark("internal note");
+        when(dictionaries.selectDictDataByType("sys_billing_type")).thenReturn(List.of(row));
+        MockMvcBuilders.standaloneSetup(controller).build()
+                .perform(get("/app/dict/data").header("token", "client-token")
+                        .param("dictType", " sys_billing_type "))
+                .andExpect(status().isOk()).andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data[0].dictLabel").value("分成"))
+                .andExpect(jsonPath("$.data[0].dictValue").value("0"))
+                .andExpect(jsonPath("$.data[0].dictSort").value(1))
+                .andExpect(jsonPath("$.data[0].isDefault").value("N"))
+                .andExpect(jsonPath("$.data[0].remark").doesNotExist());
+    }
+
+    @Test void supportsAnyConfiguredTypeAndOmitsDisabledItems() {
+        when(dictionaries.selectDictDataByType("custom_client_options"))
+                .thenReturn(List.of(option("A", "a", "0"), option("B", "b", "1")));
+        assertEquals(List.of(new AppDictController.DictOption("A", "a", 1L, "N")),
+                controller.data("token", "custom_client_options").get("data"));
+    }
+
+    @Test void missingOrEmptyDictionaryReturnsArray() {
+        assertEquals(List.of(), controller.data("token", "unknown_type").get("data"));
+        when(dictionaries.selectDictDataByType("empty_type")).thenReturn(List.of());
+        assertEquals(List.of(), controller.data("token", "empty_type").get("data"));
+    }
+
+    @Test void invalidTypeDoesNotQueryStorage() {
+        for (String value : new String[]{null, "", "  ", "x".repeat(101)}) {
+            assertEquals(500, controller.data("token", value).get("code"));
+        }
+        verifyNoInteractions(dictionaries);
+    }
+
+    @Test void missingTokenHeaderIsRejectedBeforeQuery() throws Exception {
+        MockMvcBuilders.standaloneSetup(controller).build()
+                .perform(get("/app/dict/data").param("dictType", "sys_billing_type"))
+                .andExpect(status().isBadRequest());
+        verifyNoInteractions(dictionaries);
+    }
+
+    @Test void endpointUsesClientTokenSessionAuthentication() throws Exception {
+        var method = AppDictController.class.getMethod("data", String.class, String.class);
+        assertTrue(method.getAnnotation(Auth.class).token());
+    }
+
+    private static SysDictData option(String label, String value, String status) {
+        var row = new SysDictData();
+        row.setDictLabel(label); row.setDictValue(value); row.setStatus(status);
+        row.setDictSort(1L); row.setIsDefault("N");
+        return row;
+    }
+}

+ 6 - 0
specs/003-merchant-billing/plan.md

@@ -209,3 +209,9 @@ foodie-store/                                     # 商家前端
 ```bash
 git commit -m "feat(billing): 商家端账单功能 — 列表查询、汇总、mdId 过滤"
 ```
+
+## 2026-11-04 客户端字典接口增量
+
+1. 独立 `AppDictController` 接收显式请求头及查询参数,复用 `ISysDictTypeService.selectDictDataByType`;响应投影为客户端选项。
+2. 新增参数错误国际化消息;使用现有 `@Anonymous + @Auth` 接入客户端会话认证,不修改平台后台字典接口。
+3. 定向验证响应契约、任意类型、空结果、停用项、参数与认证边界,并构建后端。无数据库迁移。

+ 4 - 0
specs/003-merchant-billing/spec.md

@@ -146,3 +146,7 @@
 ### 实现说明
 
 mdId 过滤采用 `(#{mdId} IS NULL OR md_id = #{mdId})` 模式,mdId 不传时查询全部数据,传入时按门店过滤。涉及 join 查询的方法使用表别名前缀 `b.md_id`。
+
+## 2026-11-04 客户端共用字典查询
+
+新增 `GET /app/dict/data?dictType=sys_billing_type`,商家及用户端共用 `token` 请求头,使用现有客户端会话认证。用户明确确认允许已登录客户端查询任意字典类型,不增加类型白名单。只返回启用项,复用现有缓存及排序;返回 `dictLabel`、`dictValue`、`dictSort`、`isDefault`,不返回后台审计字段。缺失、空白或超过100字符的类型返回国际化业务错误;不存在或无启用项返回空数组。标签使用后台维护原文,不自动翻译或改写类型5的业务含义。本增量仅提供共用接口,不改商家页面或字典数据。

+ 8 - 0
specs/003-merchant-billing/tasks.md

@@ -113,3 +113,11 @@ description: "Task list for 商家前端账单功能 feature implementation"
 - 无数据库 Schema 变更(UserBilling 已有 md_id 字段)
 - mdId 过滤采用 `(#{mdId} IS NULL OR md_id = #{mdId})` 模式,不传时查全部
 - 前端日/周/月账单明细页(getzddaylist / getzdmeeklist / getzdmatlist)尚未在前端实现,仅后端接口已就绪
+
+## 2026-11-04 客户端共用字典接口
+
+- [x] T016 新增按dictType查询的共用接口,支持商家/用户token与任意类型,返回启用项精简字段。
+- [x] T017 补齐参数国际化与接口契约测试。
+- [x] T018 运行字典与认证定向回归及JDK21构建,记录结果。
+
+验证:JDK21执行 `mvn -q -pl ruoyi-admin -am -Dtest=AppDictControllerTest,AuthAspectTest -Dsurefire.failIfNoSpecifiedTests=false package`,退出码0,11项测试通过(字典6、认证5),失败/错误/跳过均0;差异检查通过。日志 `ruoyi-admin/target/app-dict-query.log`。未修改客户端页面,未进行线上字典联调、全量回归或部署;提交状态以 Git 记录为准。