Procházet zdrojové kódy

设置项按 PHP defaultData 补默认值,修复分类页空白

- 现象:/#/pages/category/index 三个模板组件都不渲染,页面只有搜索框
- 根因:库内只有 register / sms / trade 三项设置,Java 的 getItem 缺项一律返回 [],
  于是 /api/setting/data 的 pageCategoryTemplate 是 [],前端 initSetting 取
  setting['page_category_template'].style 得到 undefined,任何 v-if 都不成立
- PHP 侧 common\model\store\Setting::getAll() 会 resetOptions(defaultData, data) 用默认值补齐
  (defaultData 里 page_category_template.style 默认 30 即「分类+商品」)
- 修改:ApiSettingService 增加 DEFAULT_VALUES(app_theme / page_category_template /
  points / recharge / register / customer)与 mergeDefaults(默认打底、实际覆盖、对象递归),
  公开设置项统一走 getItemWithDefaults;顺带修复 appTheme 为 null、积分/充值/客服为空数组
- 键名口径:默认值写驼峰,出参经 pickCamel 统一转驼峰(库内 PHP 数据的 points_name /
  is_entrance 两种写法都认),不破坏「出参零 snake key」;前端读下划线由 fieldAlias 补别名
- 回归:batch1 54(新增「缺项按默认值补齐」断言)、batch2 97、batch3 99、batch4 73、
  uniapp 契约 9,全部 0 失败
tanlie před 8 hodinami
rodič
revize
a8ecc2ae6f

+ 8 - 0
tools/test_api_batch1.py

@@ -110,6 +110,14 @@ def main():
         "appTheme", "pageCategoryTemplate", "points", "recharge", "register", "customer"}, list(setting))
     check("setting/data register 有 registerMethod",
           "registerMethod" in (setting.get("register") or {}), setting.get("register"))
+    # 库内只有 register/sms/trade,其余项必须按 PHP defaultData() 补齐,
+    # 否则客户端读 pageCategoryTemplate.style 得到 undefined,分类页三个模板组件都不渲染
+    check("setting/data 缺项按 PHP 默认值补齐",
+          (setting.get("pageCategoryTemplate") or {}).get("style") == 30
+          and (setting.get("appTheme") or {}).get("mainBg") == "#fa2209"
+          and (setting.get("points") or {}).get("pointsName") == "积分"
+          and (setting.get("recharge") or {}).get("isEntrance") == 1,
+          {k: setting.get(k) for k in ("appTheme", "pageCategoryTemplate", "points", "recharge")})
 
     # ---------- 地区 / 分类 / 内容
     r = get("/api/region/all")

+ 152 - 6
yoshop-java/src/main/java/cn/yoshop/api/service/ApiSettingService.java

@@ -36,6 +36,15 @@ public class ApiSettingService {
     @Value("${yoshop.base-url}")
     private String baseUrl;
 
+    /**
+     * 对齐 PHP common\model\store\Setting::defaultData()。
+     * <p>PHP 的 getAll() 是 {@code resetOptions(defaultData, data)}:库内没有该项(或该项缺字段)时
+     * 用默认值补齐。Java 侧此前缺项一律返回空数组,导致客户端拿到
+     * {@code pageCategoryTemplate = []},分类页读 {@code setting.style} 得到 undefined,
+     * 三个模板组件都不渲染(页面空白)。这里只列公开设置项用到的字段。
+     */
+    private static final Map<String, Map<String, Object>> DEFAULT_VALUES = buildDefaults();
+
     // ---------------------------------------------------------------- 设置项
 
     /**
@@ -54,6 +63,16 @@ public class ApiSettingService {
         return decoded == null ? new ArrayList<>() : decoded;
     }
 
+    /**
+     * 读取设置项并按 PHP 默认值补齐(对应 getAll() 里的 resetOptions(defaultData, data))。
+     * 用于需要向客户端暴露的公开设置项。
+     */
+    public Object getItemWithDefaults(String key) {
+        Object actual = getItem(key);
+        Map<String, Object> defaults = DEFAULT_VALUES.get(key);
+        return defaults == null ? actual : mergeDefaults(actual, defaults);
+    }
+
     /**
      * 商城公共设置(仅暴露可公开项),对齐 PHP Setting::getPublic:
      * appTheme / pageCategoryTemplate / points / recharge / register / customer。
@@ -61,15 +80,17 @@ public class ApiSettingService {
     public Map<String, Object> getPublicSetting() {
         Map<String, Object> data = new LinkedHashMap<>();
         // 店铺页面风格设置:PHP 取 getItem(app_theme)['data']
-        Object appThemeItem = getItem("app_theme");
+        Object appThemeItem = getItemWithDefaults("app_theme");
         Object appTheme = appThemeItem instanceof Map<?, ?> map ? map.get("data") : null;
         data.put("appTheme", appTheme);
-        data.put("pageCategoryTemplate", getItem("page_category_template"));
-        data.put("points", pick(getItem("points"), "points_name", "describe"));
-        data.put("recharge", pick(getItem("recharge"), "is_entrance", "is_custom", "describe"));
-        data.put("register", pick(getItem("register"),
+        data.put("pageCategoryTemplate", getItemWithDefaults("page_category_template"));
+        data.put("points", pickCamel(getItemWithDefaults("points"), "points_name", "describe"));
+        data.put("recharge", pickCamel(getItemWithDefaults("recharge"),
+                "is_entrance", "is_custom", "describe"));
+        data.put("register", pick(getItemWithDefaults("register"),
                 "registerMethod", "isManualBind", "isOauthMpweixin", "isOauthMobileMpweixin"));
-        data.put("customer", pick(getItem("customer"), "enabled", "provider", "config"));
+        data.put("customer", pick(getItemWithDefaults("customer"),
+                "enabled", "provider", "config"));
         return data;
     }
 
@@ -119,6 +140,131 @@ public class ApiSettingService {
 
     // ---------------------------------------------------------------- 工具
 
+    /** 对齐 PHP common\model\store\Setting::defaultData(),只保留公开设置项用到的字段 */
+    private static Map<String, Map<String, Object>> buildDefaults() {
+        Map<String, Map<String, Object>> all = new LinkedHashMap<>();
+        // 店铺页面风格设置(PHP getAppTheme 取这里的 data 字段)
+        all.put("app_theme", kv(
+                "mode", 10,
+                "themeTemplateIdx", 0,
+                "data", kv(
+                        "gradualChange", 1,
+                        "mainBg", "#fa2209",
+                        "mainBg2", "#ff6335",
+                        "mainText", "#ffffff",
+                        "viceBg", "#ffb100",
+                        "viceBg2", "#ffb900",
+                        "viceText", "#ffffff")));
+        // 分类页模板:style=30 即「分类+商品」,前端按它选择渲染哪个组件
+        all.put("page_category_template", kv(
+                "style", 30,
+                "shareTitle", "全部分类",
+                "showAddCart", true,
+                "cartStyle", 1));
+        // 积分设置
+        all.put("points", kv(
+                "pointsName", "积分",
+                "describe", "1. 积分不可兑现、不可转让, 仅可在本平台使用。\n"
+                        + "2. 您在本平台参加特定活动也可使用积分, 详细使用规则以具体活动时的规则为准。\n"
+                        + "3. 积分的数值精确到个位(小数点后全部舍弃, 不进行四舍五入)。\n"
+                        + "4. 每次购物赠送的积分将在订单完成后(包括退换货流程完成)到账。\n"));
+        // 用户充值设置
+        all.put("recharge", kv(
+                "isEntrance", 1,
+                "isCustom", 1,
+                "describe", "1. 账户充值仅限微信在线方式支付,充值金额实时到账;\n"
+                        + "2. 账户充值套餐赠送的金额即时到账;\n"
+                        + "3. 账户余额有效期:自充值日起至用完即止;\n"
+                        + "4. 若有其它疑问,可拨打客服电话400-000-1234"));
+        // 账户注册设置
+        all.put("register", kv(
+                "registerMethod", 10,
+                "isManualBind", 1,
+                "isOauthMpweixin", 1,
+                "isOauthMobileMpweixin", 1));
+        // 商城客服设置
+        all.put("customer", kv(
+                "enabled", 1,
+                "provider", "mpwxkf",
+                "config", kv("mpwxkf", new ArrayList<>())));
+        return all;
+    }
+
+    /** 便捷构造有序 Map(保持与 PHP 默认值相同的字段顺序) */
+    private static Map<String, Object> kv(Object... pairs) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        for (int i = 0; i + 1 < pairs.length; i += 2) {
+            map.put(String.valueOf(pairs[i]), pairs[i + 1]);
+        }
+        return map;
+    }
+
+    /**
+     * 对齐 PHP resetOptions:默认值打底、实际值覆盖,两边都是对象时递归合并;
+     * 实际值不是对象(库内缺项时返回的 [])则直接用默认值。
+     */
+    private static Object mergeDefaults(Object actual, Map<?, ?> defaults) {
+        if (!(actual instanceof Map<?, ?> actualMap)) {
+            return defaults;
+        }
+        Map<String, Object> merged = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : defaults.entrySet()) {
+            merged.put(String.valueOf(entry.getKey()), entry.getValue());
+        }
+        for (Map.Entry<?, ?> entry : actualMap.entrySet()) {
+            String key = String.valueOf(entry.getKey());
+            Object defaultValue = merged.get(key);
+            Object actualValue = entry.getValue();
+            if (defaultValue instanceof Map<?, ?> && actualValue instanceof Map<?, ?>) {
+                merged.put(key, mergeDefaults(actualValue, (Map<?, ?>) defaultValue));
+            } else {
+                merged.put(key, actualValue);
+            }
+        }
+        return merged;
+    }
+
+    /**
+     * 对齐 PHP helper::pick,但输出键名统一转驼峰(Java 出参口径)。
+     * 源数据里下划线与驼峰两种写法都认:库内的 PHP 设置多为 points_name,默认值表用 pointsName。
+     */
+    private static Object pickCamel(Object source, String... columns) {
+        if (!(source instanceof Map<?, ?> sourceMap)) {
+            return new ArrayList<>();
+        }
+        Map<String, String> wanted = new LinkedHashMap<>();
+        for (String column : columns) {
+            wanted.put(column, toCamel(column));
+            wanted.put(toCamel(column), toCamel(column));
+        }
+        Map<String, Object> picked = new LinkedHashMap<>();
+        for (Map.Entry<?, ?> entry : sourceMap.entrySet()) {
+            String output = wanted.get(String.valueOf(entry.getKey()));
+            if (output != null) {
+                picked.put(output, entry.getValue());
+            }
+        }
+        return picked;
+    }
+
+    /** points_name / is_entrance → pointsName / isEntrance */
+    private static String toCamel(String key) {
+        if (!key.contains("_")) {
+            return key;
+        }
+        StringBuilder sb = new StringBuilder();
+        boolean upperNext = false;
+        for (char c : key.toCharArray()) {
+            if (c == '_') {
+                upperNext = true;
+                continue;
+            }
+            sb.append(upperNext ? Character.toUpperCase(c) : c);
+            upperNext = false;
+        }
+        return sb.toString();
+    }
+
     /**
      * 对齐 PHP helper::pick:按源数据顺序取指定键。
      * 源不是对象(缺失项返回的 []) 时同样返回 [],与 PHP 空数组一致。

+ 5 - 0
联调说明.md

@@ -121,6 +121,11 @@ NODE_OPTIONS=--openssl-legacy-provider ./node_modules/.bin/vue-cli-service serve
 
 - 基础地址:`/api/**`,与 PHP 的 `/index.php?s=/api/xxx` 对应
 - 请求头:`platform`(MP-WEIXIN / H5)、`Access-Token`(用户 token,可无)
+- 设置项:`/api/setting/data`、`/api/store/data` 里的 `setting` 与 PHP 一致——
+  **库内缺项时按 `common\model\store\Setting::defaultData()` 补默认值**(PHP 的 `getAll()` 是
+  `resetOptions(defaultData, data)`)。当前库只有 `register / sms / trade` 三项,所以
+  `appTheme` / `pageCategoryTemplate` / `points` / `recharge` / `customer` 都来自默认值;
+  缺了这层补齐,客户端读 `pageCategoryTemplate.style` 会得到 undefined(分类页空白)
 - 登录态:Redis `yoshop:api:token:{token}`,值=用户 ID;`/api/**` 只解析不拦截,
   需登录的接口(购物车、订单、用户中心、售后、充值、收银台等)由接口内部判定并返回 401「请先登录后再访问」
 - 登录:`/api/passport/login`(手机号 + 短信验证码,自动注册)、`/api/passport/loginMpWx`(小程序一键登录)、