|
@@ -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 环境跑一遍三档排序,核对首屏商品顺序确实变化
|