Просмотр исходного кода

docs(spec): 商城按价格排序功能任务书(前端单文件方案,后端零改动)

Agnes Agent 5 дней назад
Родитель
Сommit
ad71c79d98
1 измененных файлов с 106 добавлено и 0 удалено
  1. 106 0
      docs/superpowers/specs/2026-11-07-shop-price-sort.md

+ 106 - 0
docs/superpowers/specs/2026-11-07-shop-price-sort.md

@@ -0,0 +1,106 @@
+# 商城:增加按价格排序功能
+
+> 任务编号:待派发(cfc-frontend)
+> 日期:2026-11-07
+> 触发:用户 "修改小程序商城页面,增加按价格排序的功能"
+
+## 一、需求
+
+商城商品列表页(`cfc-frontend/pages/shop/index/index.vue`)增加**按价格排序**功能:
+- 排序选项:**综合**(默认,后端 sort_order asc + created_at desc)/ **价格升序** / **价格降序**
+- 切换排序后重新拉取商品列表(重置到第 1 页),滚动加载沿用当前排序
+- 排序栏 UI 位于分类 chips(Level1/Level2)与商品列表之间
+
+## 二、根因(已定位)
+
+商城当前**没有可用的价格排序**:
+- `loadProducts()`(L221-251)调 `productList({page, size})`,**未传有效 sort**
+- `loadRecommended()`(L327)传 `sort:'sales'`(**字符串**),但后端拦截器要求**数组**格式 `[{column, order}]` → 字符串被忽略,实际无效
+- 后端 `SortInterceptor` + `SortUtil` 已完整支持数组格式排序(见第三节),**后端零改动即可**
+
+## 三、现状(cfclub @ origin 2026-11-07,基线 0bdc78def)
+
+### 前端 `cfc-frontend/pages/shop/index/index.vue`(704 行)
+- **模板**:搜索栏(L18-27)→ Level1 分类 chips(L29-44)→ Level2 子分类 chips(L46-66)→ 商品列表 `scroll-view.product-list`(L68-148)
+- **排序栏插入点**:L66(Level2 chips 结束 `</scroll-view>`)与 L68(商品列表 `scroll-view` 开始)**之间**
+- `data`(L159-176):`categories/currentCat1/currentCat2/products/page/size/loading/loadingMore/refreshing/noMore/isLoggedIn/isMember/recommended/cartCount`
+- `onCat1Select`(L205-213)/`onCat2Select`(L214-220):切换分类 → `page=1; products=[]; noMore=false; loadProducts()`
+- `loadProducts`(L221-247):`const params = {page: this.page, size: this.size}`,按 `currentCat2`/`currentCat1` 补 `categoryId`,`productList(params)`
+- `onRefresh`(L248-253)/`onLoadMore`(L254-275):刷新/滚动加载,同样构建 `params`(**滚动加载也需带 sort**)
+- `loadRecommended`(L327 附近):`productList({page:1, size:6, sort:'sales'})` —— 字符串 sort,无效,**本任务不改动推荐商品逻辑**(推荐区独立,保持现状)
+- import(L154):`goodsCategory, productList, getMyMembership, cartCount, reportAppEnter`
+
+### 后端(零改动,机制已存在)
+- `cfc-backend/src/main/java/com/etotem/cfc/config/SortInterceptor.java`:`preHandle` 从**请求体**读 `sort`(数组 `[{column, order}]`),白名单过滤(仅保留 column/order,order 转小写)→ `SortContext.set(sanitized)`
+- `cfc-backend/src/main/java/com/etotem/cfc/util/SortUtil.java`:`applySort(wrapper, SortContext.get(), null)`,列名正则 `^[a-zA-Z_][a-zA-Z0-9_]*$` 防注入;`price` 列符合白名单;驼峰→下划线映射;双 ORDER BY 防护(反射清 wrapper 内 orderBy 段)
+- `ProductService.list`(L147 附近):默认 `orderByAsc(Product::getSortOrder).orderByDesc(createdAt)`,随后 `SortUtil.applySort(wrapper)` 注入用户排序
+- **结论**:前端只需传 `sort: [{column:'price', order:'asc'|'desc'}]`(POST body),后端自动生效,无需改任何后端代码
+
+## 四、方案(仅改前端 1 文件 `cfc-frontend/pages/shop/index/index.vue`,后端零改动)
+
+### 4.1 模板:新增排序栏(插入在 L66 与 L68 之间)
+```html
+<!-- 排序栏:综合 / 价格↑ / 价格↓ -->
+<view class="sort-bar">
+  <view
+    :class="['sort-item', sortType === 'default' ? 'active' : '']"
+    @click="onSortSelect('default')"
+  >综合</view>
+  <view
+    :class="['sort-item', sortType === 'price_asc' ? 'active' : '']"
+    @click="onSortSelect('price_asc')"
+  >价格↑</view>
+  <view
+    :class="['sort-item', sortType === 'price_desc' ? 'active' : '']"
+    @click="onSortSelect('price_desc')"
+  >价格↓</view>
+</view>
+```
+
+### 4.2 data:新增 `sortType: 'default'`
+
+### 4.3 methods:
+1. **`buildSort()`**:返回排序数组(供 productList 入参)
+   - `price_asc` → `[{column:'price', order:'asc'}]`
+   - `price_desc` → `[{column:'price', order:'desc'}]`
+   - `default` → `[]`(不传 sort,后端走默认 sort_order)
+2. **`onSortSelect(type)`**:若 `type === this.sortType` return;`this.sortType = type; this.page=1; this.products=[]; this.noMore=false; this.loadProducts()`
+3. **`loadProducts`(L221-247 改)**:`params` 构建后追加 `const sort = this.buildSort(); if (sort.length) params.sort = sort`
+4. **`onLoadMore`(L254-275 改)**:同样在 `params` 构建后追加 `sort`(滚动加载沿用当前排序)
+5. **`onCat1Select`/`onCat2Select` 不改逻辑**(切换分类时 `page=1; products=[]` 已就绪,loadProducts 会自动带当前 sortType——**保留当前排序,符合电商惯例**)
+
+### 4.4 样式:新增 `.sort-bar` / `.sort-item` / `.sort-item.active`
+- 与分类 chips 视觉协调(复用 `$uni-space-*` 间距令牌、`$facade-violet` 品牌色高亮 active)
+- `sort-bar`:横向 flex、内边距 `0 $uni-space-lg`、底部间距
+- `sort-item`:横向排列、间距,active 态品牌色加粗
+
+## 五、验收标准
+
+1. 商城商品列表出现「综合 / 价格↑ / 价格↓」排序栏
+2. 点击「价格↑」→ 商品按价格从低到高排列(第 1 页);「价格↓」→ 从高到低
+3. 点击「综合」→ 恢复默认排序(sort_order asc + created_at desc)
+4. 切换分类(Level1/Level2)后**保留当前排序**,商品按该排序加载
+5. 滚动加载(onLoadMore)沿用当前排序,不跳回默认
+6. 下拉刷新沿用当前排序
+7. 推荐商品区(recommend-section)**不受影响**(保持原逻辑)
+8. 构建 `npm run build:mp-weixin` exit 0
+
+## 六、硬约束
+
+- **只改 `cfc-frontend/pages/shop/index/index.vue`**;不改后端、不改 `utils/api.js`(`productList` 原样透传 body)、不改 `pages.json`
+- **不改动推荐商品逻辑**(`loadRecommended` 的 `sort:'sales'` 字符串保持原样,本任务不触碰;推荐区独立于排序栏)
+- 排序参数必须是**数组** `[{column, order}]`(字符串会被后端拦截器忽略——这是现有 bug 的根源,勿复现)
+- 基于 `origin/cfclub` 建 `feature/shop-price-sort` 分支,commit + push,**不自行合入 cfclub**
+- 完成后 `git status --porcelain` 干净(不残留未提交改动)
+
+## 七、风险
+
+- 后端 `SortUtil` 双 ORDER BY 防护依赖反射清空 wrapper orderBy——若某环境该反射初始化失败会回退旧行为,`sort` 可能不生效或产生双 ORDER BY;**开发会话以实际接口返回验证排序生效**,若后端未生效则如实报告(不要前端硬编码排序)
+- `price` 列名须与 `product` 表实际列名一致(`SortUtil` 驼峰→下划线映射 `price`→`price`,已确认)
+- 排序切换与分类切换的 `page` 重置须一致,避免滚动加载携带旧 sort 造成分页错乱
+
+## 八、验证数据(供开发/核验参考)
+
+- 后端 `POST /api/product/list`,body 含 `{"page":1,"size":20,"sort":[{"column":"price","order":"asc"}]}` → 返回按 price asc
+- 对比:不传 sort → 默认 sort_order asc + created_at desc
+- 建议开发会话在 dev 环境跑一遍三档排序,核对首屏商品顺序确实变化