# 2026-08-17 工作日志 ## 专区商品「移除」假成功 bug 修复 - 现象:guilixu.websoft.top/special/zone 移除专区商品提示成功,刷新(重查库)后商品仍在。 - 根因:`ShopHomeSectionController.removeGoods` 用 `listByIds` + `updateBatchById` 改 `shop_goods.section_ids`;单专区商品移除后 section_ids 变 null,但 `ShopGoods.sectionIds` 无更新策略注解,MP 全局默认 `NOT_NULL` 会忽略 null 值 → 不生成 `SET section_ids=NULL` → 库字段保持原值(假成功)。多专区商品(变非 null)正常,故仅单专区场景坏。 - 修复:`addGoods`/`removeGoods` 改用 `UpdateWrapper.set("section_ids", newVal).eq("goods_id", id)` 显式更新(newVal 可为 null),绕过字段策略。文件:guilixu-java `ShopHomeSectionController.java`。 - 部署:需重新编译部署 guilixu-java(线上 shop-api)后端生效;前端无需改动。添加用同样模式统一修复。 - 验证:部署后用单专区商品移除测试,刷新不应再出现。 - 注:本机环境无 maven,未本地编译;改动为标准 MP 用法(IService.update(Wrapper) / UpdateWrapper.set 允许 null)。 ## 专区白名单功能诊断(功能当前不可用) - 设计意图:专区设为"受限专区"(restricted=1) 后,该专区商品仅白名单用户可用余额购买;小程序结算页 checkout.tsx 已调 `check-permission` 前端拦截。 - 致命错配:后台白名单弹窗 `UserSelectModal.vue` 用 `pageUsers`(@/api/system/user → sys_user 后台员工) 选用户;后端 `ShopHomeSectionController.checkPermission` 用 `getLoginUser().getUserId()`(= shop_user 小程序买家 userId)。两套用户体系 id 不一致 → 白名单里加的任何人(后台员工)对真实买家都匹配不上 → 受限专区对买家永远拦截/强制余额,白名单形同虚设。 - 次要隐患:后端创建订单接口未做白名单强校验,仅前端拦截,可被绕过。 - 修复方向:① 白名单弹窗数据源从 `pageUsers`(sys_user) 改为 `pageShopUser`(@/api/shop/shopUser → shop_user 买家),使存的 user_id 与校验 userId 同体系;② 后端下单接口加白名单强校验防绕过。后台已有 pageShopUser 可用。 ## 专区白名单:用户体系澄清 + 后端强校验(用户 2026-08-17 01:50 指令) - 用户澄清:本项目的买家(小程序用户)就是 `sys_user` 体系,`pageShopUser` 实际不用/返回空。故白名单弹窗用 `pageUsers`(sys_user) 是**正确**的,之前"用户体系错配"判断为误判。 - 白名单弹窗 `UserSelectModal.vue` 现状(8-16 已改):`pageUsers({ keywords, ... })`,**不传 isStaff**(搜索覆盖全部 sys_user)。源码无需再改。已 grep 确认 zone 目录下无残留 isStaff/pageShopUser。 - 后端补「受限专区白名单强校验」:`OrderBusinessService.java` 新增 - import:`ShopHomeSectionService`、`ShopHomeSectionUserService`、`QueryWrapper`、`java.util.Set/LinkedHashSet`; - `@Resource` 注入 `homeSectionService`、`homeSectionUserService`; - `createOrder` 在 `validateDeliveryRegionIfNeeded` 之后调用 `validateSectionPermissionIfNeeded(request, shopOrder, loginUser)`; - 新方法逻辑:遍历 `request.goodsItems` → 取 `ShopGoods.sectionIds` → 找 restricted=1 的专区 → 校验 `loginUser.getUserId()` 是否在 `shop_home_section_user`(count>0),否则抛 `BusinessException("该商品属于受限专区,仅限指定白名单用户购买")`。 - 作用:前端 `check-permission` 的后端兜底,防止直接调 `/shop/shop-order` 下单绕过白名单。 - 部署:需重新编译部署 guilixu-java(shop-api)。前端无需改动。 - 注:本机无 maven,未本地编译;改动为标准 MP 用法(IService.count(QueryWrapper) / getById)。 - 待确认:用户 "2.帮我补" 指令只写了前半句,已按上文补后端强校验;若其本意是"补菜单记录"(专区管理入口,动态路由待插入),需另出 INSERT SQL(菜单表结构待确认)。 ## 专区白名单弹窗 checkbox 不显示(第三次修复,根因确认) - 现象:白名单弹窗数据正常(12 条用户),但表格无 checkbox 勾选列,用户无法选择。 - 已尝试修复(前两次均未生效): 1. 8-16:`reactive` → `computed` + `columnWidth: 48`(与 shopCoupon 对齐) 2. 同期:去掉 `isStaff: true` - **真正根因**(读 `ele-pro-table` 源码 `node_modules/ele-admin-pro/es/ele-pro-table/index.js` 第 94-114 行确认): - `ele-pro-table` 内部 `tableSelectionType` computed 有前置条件:必须传 `selection`、`current` 或 `selectionType="radio"` 三者之一,否则返回 `undefined` - 返回 undefined → `tableRowSelection` 也返回 undefined → ant-design-vue Table 收到 `rowSelection=undefined` → **不渲染 checkbox 列** - 我们的弹窗三个条件都不满足(没传 :selection / :current / selectionType),所以不管 row-selection 配置多正确都不会出勾选框 - **最终修复**:模板 `` 加 `selection-type="checkbox"`(第 18 行),使源码判断走通:`props2.selectionType !== "radio"` 为 true(是 "checkbox")→ 不走 early return → 返回 "checkbox" → checkbox 列正常渲染。 - 改动文件:`src/views/special/zone/components/UserSelectModal.vue`(仅模板加 1 个 prop) - 部署:重新构建前端即可(`npm run dev` 热更新或 `npm run build` 部署)。 ## 专区白名单弹窗 checkbox 修复(8-17 纠正:8-16 修复实际无效) - 反馈:用户截图仍无勾选框,8-16 加的 `selection-type="checkbox"` 未生效。 - **重读 ele-pro-table 源码确认 8-16 根因判断错误**(`node_modules/ele-admin-pro/es/ele-pro-table/index.js` 94-103 行): ```js const noSelection = typeof props2.selection === "undefined"; const noCurrent = typeof props2.current === "undefined"; if (noSelection && noCurrent && props2.selectionType !== "radio") { return; // 不渲染 } ``` 三个条件**同时满足**才 return。即 `noSelection && noCurrent && selectionType !== "radio"`。我传的 `selection-type="checkbox"` 让 `selectionType !== "radio"` 为 true,反而**满足**了 early return 条件 → 不渲染。这正是 8-16 修复无效的真因。 - 另外发现 `tableRowSelection.selectedRowKeys` 强制用内部 ref(源码 111 行),**不支持外部初始化已选**——即使绕过 selection 限制,弹窗打开时已选的人也不会显示勾选。 - **正确修复**:弃用 ele-pro-table 的 selection 机制,将 `UserSelectModal.vue` 重写为 antd `a-table`: - 受控 `selectedRowKeys`,watch props.visible 时用 `props.selectedUserIds` 初始化 → 打开弹窗已选即勾选 - `rowSelection` computed:`{ columnWidth:48, selectedRowKeys, onChange }` - datasource 用 `pageUsers` 包成 Promise,分页用 antd `pagination` reactive,`@change` 调 reload - search `a-input-search`,@search/@pressEnter reload - confirm emit `selectedRowKeys` - 字段已与 User 模型对齐(userId/realName/mobile/nickname/organizationName),`PageResult={list,count}`。 - 改动文件:`src/views/special/zone/components/UserSelectModal.vue`(整体重写约 130 行)。 - 部署:刷新页面(`npm run dev` 热更新)或重新构建 admin 前端即可。无需后端改动。 ## 专区小程序码(扫码直接进入专区) - 需求:/special/zone 专区管理加「二维码」入口,生成该专区的小程序码,用户微信扫码直接进入小程序对应专区。 - 后端(guilixu-java `ShopHomeSectionController.java`):新增 `GET /api/shop/shop-home-section/{id}/qrcode` - 调微信 `getwxacodeunlimit` 生成无限场景码;`scene=sectionId=xxx`,`page=pages/shop/section-detail`;按专区 `tenantId` 经 `WxMiniappAccessTokenService.getAccessToken(tenantId)` 取 token;prod 用 `release` 否则 `trial`(与现有 getOrderQRCodeUnlimited 一致)。 - 复用 Hutool `HttpRequest` 调微信,返回 PNG 流;微信出错时返回 JSON 并识别 `application/json`。 - 用户端小程序(guilixu-taro `pages/shop/section-detail.tsx`):解析 `useRouter().params.scene`(小程序码扫码透传的 `sectionId=xxx`),兼容原 `?sectionId=` 普通跳转。 - 管理后台(guilixu-admin `views/special/zone/index.vue` + `api/shop/shopZone`):列表操作列加「二维码」按钮 → 弹窗显示小程序码图片;新增 `getSectionQrcode(id)`(responseType blob → objectURL,遇 JSON 错误解析 message 并 reject)。 - 部署:① 重新编译部署 guilixu-java(shop-api)使新接口生效;② 重新构建前端 admin 与 taro 小程序(section-detail 需重新发布/体验版,因小程序码 page 指向已发布页面)。 - 注:本机无 maven,后端未本地编译;改动完全复用 WxLoginController 既有 getwxacodeunlimit 写法。 - 限制:getwxacodeunlimit 的 page 必须已存在于小程序(pages/shop/section-detail 已在 app.config 的 pages 列表);scene 仅限可见字符、≤32 位,`sectionId=xxx` 满足。 ## 专区小程序码报 41030 invalid page 修复 - 现象:后台点「二维码」生成小程序码,微信返回 `{"errcode":41030,"errmsg":"invalid page"}`。 - 根因:原代码按 profile 决定 env_version——active=dev 时走 `trial`(体验版)。微信 41030=所请求版本的小程序代码包里**没有该页面**。页面 `pages/shop/section-detail` 在源码 app.config 存在,但体验版小程序没上传含此页面的代码 → 失败。 - 修复: - 后端(ShopHomeSectionController.getSectionQrcode):去掉按 profile 判断,改为 `envVersion` 请求参数,`@RequestParam(defaultValue="release")`,`env_version` 直接用该参数(release/trial/develop)。 - 前端(admin):`getSectionQrcode(id, envVersion='release')` 支持传参;弹窗加「正式版/体验版/开发版」RadioGroup(默认正式版),切换即重新生成。 - 关键前提(务必告诉用户):**生成成功 ≠ 扫码能进专区**。 1. 生成码所请求的版本(正式/体验)其代码包必须包含 `pages/shop/section-detail` 页面,否则仍 41030。 2. 扫码能正确进专区,还要求把「能解析 scene 的 section-detail.tsx」发布到**同一版本**:正式版需发版、体验版需上传体验版、开发版需开发者工具预览。 3. 若默认正式版仍 41030,说明线上版本也没有此页面 → 需先发布小程序(该页面虽在 app.config,但可能未上线)。 - 部署:需重新编译部署 guilixu-java + 重新构建发布 taro 小程序(到目标版本)。 ## 专区白名单「添加/移除」功能确认 + tenantId 修复 - 现状确认:`shop_home_section_user` 白名单的「添加/移除」功能**此前已完整实现**,本次只是排查确认: - 后端 `ShopHomeSectionController`:`GET /{id}/users`(查)、`PUT /{id}/users`(覆盖式保存);`ShopHomeSectionUserService.listBySectionId` → `selectBySectionId`(XML)。 - 前端 `index.vue`:列表「白名单」按钮 → `openUsers` 调 `listSectionUsers` 加载已选 → 弹窗;`onSaveUsers` 调 `setSectionUsers` 保存。 - `UserSelectModal.vue`:用 `pageUsers`(sys_user) 选人,勾选=添加、取消=移除、保存=覆盖提交;columns 与 User 模型字段(userId/realName/mobile/nickname/organizationName)已核对匹配。 - **真 bug 修复**:后端 `setUsers` 新建 `ShopHomeSectionUser` 时**未设 tenantId**(实体有该字段,本项目手动多租户)。若表 `tenant_id` 有 NOT NULL 约束会插入报错;即便不报错也缺租户归属。 - 修复:从专区 `homeSectionService.getById(id).getTenantId()` 取租户,set 到每条白名单记录(`ShopHomeSectionController.setUsers`,line ~119)。 - 查询侧(`selectBySectionId`)按 `section_id` 隔离,未动;下单强校验 `checkPermission` 也按 section_id 隔离,一致。 - 用户体系统一结论(沿用 01:50 澄清):买家=sys_user,`pageUsers` 选人正确,白名单 user_id 与下单校验 userId 同体系。 - 部署:重新编译部署 guilixu-java(shop-api)生效后,后台 /special/zone 每条专区「白名单」即可勾选添加、取消移除并保存。 ## 白名单弹窗搜索报错修复([object PointerEvent] 误作 limit 传给后端) - 现象:白名单弹窗点搜索触发 400:`BindException ... Field error in object 'userParam' on field 'limit': rejected value [[object PointerEvent]]`。 - 根因:重写后的 `UserSelectModal.vue` 模板 `@search="reload"` / `@pressEnter="reload"`。antd `a-input-search` 的 `search` 事件签名是 `(value, event)`,故 `reload` 实际收到 `reload(搜索词, PointerEvent)`;`reload(page, limit)` 把第二个参数当 `limit` 传给 `pageUsers`,`pageSize` 变成 PointerEvent → 序列化 `[object PointerEvent]` 传给后端 `limit` 字段 → 类型转换失败。 - 修复:模板改为 `@search="() => reload(1)"` / `@pressEnter="() => reload(1)"`,只传页码、不传事件对象;`reload(1)` 时 `limit=undefined` → `pageSize` 回退 `pagination.pageSize`。 - 改动:`UserSelectModal.vue` 两行模板。 - 部署:刷新 admin 前端(`npm run dev` 热更新或重新构建)即可。 ## 白名单保存报 Duplicate entry(逻辑删除×唯一索引死局) - 现象:保存专区白名单 `PUT /{id}/users` 报 `SQLIntegrityConstraintViolationException: Duplicate entry '1-35771-1' for key 'shop_home_section_user.uk_section_user'`,失败 SQL 是 `UPDATE shop_home_section_user SET deleted=1 WHERE tenant_id=10606 AND deleted=0 AND section_id=?`(MP 逻辑删除)。 - 根因(已确证):`add_shop_home_section.sql:40` 定义 `UNIQUE KEY uk_section_user (section_id, user_id, deleted)`——唯一索引**包含 deleted 列**;而实体 `ShopHomeSectionUser` 上有 `@TableLogic`(逻辑删除)。`setUsers` 用「`remove` 全部 + `saveBatch`」覆盖式保存,`remove` 被 MP 转成 `UPDATE SET deleted=1`;历史多次保存已囤积 `deleted=1` 旧行,下次再把某条 `deleted=0` 行改成 `deleted=1` 就与旧 `deleted=1` 行撞唯一键。 - 修复(绕过逻辑删除,用物理删除): - `ShopHomeSectionUserMapper.java` 新增 `int physicsDeleteBySection(@Param sectionId, @Param tenantId)`; - `ShopHomeSectionUserMapper.xml` 新增 ` DELETE FROM shop_home_section_user WHERE section_id=? AND tenant_id=? `(自定义 SQL,MP 不会注入逻辑删除 → 真物理删除); - `ShopHomeSectionController.setUsers` 把 `homeSectionUserService.remove(new QueryWrapper...eq("section_id", id))` 换成 `homeSectionUserMapper.physicsDeleteBySection(id, currentTenantId)`,再 `saveBatch`。每次保存前把该专区所有行(含 deleted=1 残留)真正删掉再插新行,不再囤积,不撞键。 - `selectBySectionId` 已带 `deleted=0` 过滤,查询侧无影响。 - 说明:此表使用逻辑删除 + 含 deleted 的唯一索引本就是反模式;本修复用物理删除规避,安全且低风险(本机无 maven 未编译)。前端无需改动。 - 遗留(无害):其他未再保存过的专区可能仍有历史 deleted=1 重复行,因 `selectBySectionId` 过滤 deleted=0 且保存已改物理删除,不影响业务;如需彻底清可用 `DELETE FROM shop_home_section_user WHERE deleted=1 GROUP BY section_id,user_id HAVING COUNT(*)>1` 之类语句(谨慎,先备份)。 ## 白名单弹窗改为商品式(打开列已加、搜索追加、单条即时增删) - 用户反馈:旧版覆盖式勾选弹窗「勾了别的用户后前面勾过的又不见了」。`UserSelectModal.vue` 重写,统一成与专区「商品」抽屉一致的交互:打开只列已添加的白名单用户;搜索手机/姓名/昵称出候选,点「+添加」即时入库、点「移除」即时删。 - 根因(旧交互):覆盖式保存依赖 `selectedRowKeys` 跨打开/重渲染保持,易丢;且 `setUsers` 覆盖写遇到唯一键冲突会整体失败。商品式用「单条即时增删」彻底规避。 - 后端(guilixu-java `ShopHomeSectionController`): - `GET /{id}/users` 改为**联表返回用户详情** `List`(realName/mobile/nickname/organizationName/userId),不再只返回关系行(旧返回 ShopHomeSectionUser 仅含 userId,前端展示空白)。新增注入 `UserService`,先取关系行再 `userService.listByIds` 取详情。 - 新增 `POST /{id}/users` `addUsers`:追加用户,先查已存在 userIds 跳过(防 uk_section_user 唯一键冲突),再 `saveBatch`,带 tenantId。 - 新增 `DELETE /{id}/users` `removeUsers`:物理删除指定用户,调 `homeSectionUserMapper.physicsDeleteBySectionAndUsers(id, userIds, tenantId)`(新增 mapper/XML 方法,foreach user_id IN,绕过 @TableLogic)。 - 旧 `PUT /{id}/users` `setUsers`(覆盖式)保留兼容,前端不再使用。 - 原 `users` 用的 `homeSectionUserService.listBySectionId` 不再调用(改用 `list(new QueryWrapper...)`)。 - 前端: - `api/shop/shopZone/index.ts`:`listSectionUsers` 返回类型改 `User[]`(import system `User`,移除 `SectionUser`);新增 `addSectionUsers(id, userIds)`(POST)、`removeSectionUsers(id, userIds)`(DELETE)。 - `components/UserSelectModal.vue` 整体重写:props(`visible`,`sectionId`),内部 watch(visible) 调 `listSectionUsers` 加载已加名单;`pageUsers({keywords})` 搜索候选(排除已加);`addOne`/`removeOne` 即时调接口并重载;移除 footer 与 `@confirm`,改用 `:footer="null"`。 - `index.vue`:弹窗调用去掉 `:selectedUserIds` / `@confirm`;`openUsers` 简化为只置 `currentSectionId`+开弹窗;删除 `onSaveUsers`、`currentUserIds`、未用 import(`setSectionUsers`/`listSectionUsers` 在 index 内已无引用)。 - 后端 `users` 关键字搜索:sys_user `UserParam.keywords` 已覆盖 username/user_id/nickname/real_name/alias/phone(含手机、姓名、昵称),满足需求。 - 部署:重新编译部署 guilixu-java(shop-api)+ 重新构建 admin 前端。本机无 maven 未编译;改动为标准 MP/MyBatis 用法,风险低。