Files
guilixu-admin/.workbuddy/memory/2026-08-17.md
T
gxwebsoft 47fb8d1bd0 feat(zone): 将专区白名单弹窗改为右侧抽屉
- 用户要求专区白名单入口交互与商品一致,改为右侧抽屉弹出
- UserSelectModal 组件外层由 a-modal 改为 a-drawer,保持内部逻辑不变
- 组件 props 和 emits 完全兼容,调用方式无需更改
- 前端重新构建即可,无需后端改动
2026-08-17 03:23:45 +08:00

19 KiB
Raw Blame History

2026-08-17 工作日志

专区商品「移除」假成功 bug 修复

  • 现象:guilixu.websoft.top/special/zone 移除专区商品提示成功,刷新(重查库)后商品仍在。
  • 根因:ShopHomeSectionController.removeGoodslistByIds + updateBatchByIdshop_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.vuepageUsers(@/api/system/user → sys_user 后台员工) 选用户;后端 ShopHomeSectionController.checkPermissiongetLoginUser().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 新增
    • importShopHomeSectionServiceShopHomeSectionUserServiceQueryWrapperjava.util.Set/LinkedHashSet
    • @Resource 注入 homeSectionServicehomeSectionUserService
    • createOrdervalidateDeliveryRegionIfNeeded 之后调用 validateSectionPermissionIfNeeded(request, shopOrder, loginUser)
    • 新方法逻辑:遍历 request.goodsItems → 取 ShopGoods.sectionIds → 找 restricted=1 的专区 → 校验 loginUser.getUserId() 是否在 shop_home_section_usercount>0),否则抛 BusinessException("该商品属于受限专区,仅限指定白名单用户购买")
  • 作用:前端 check-permission 的后端兜底,防止直接调 /shop/shop-order 下单绕过白名单。
  • 部署:需重新编译部署 guilixu-javashop-api)。前端无需改动。
  • 注:本机无 maven,未本地编译;改动为标准 MP 用法(IService.count(QueryWrapper) / getById)。
  • 待确认:用户 "2.帮我补" 指令只写了前半句,已按上文补后端强校验;若其本意是"补菜单记录"(专区管理入口,动态路由待插入),需另出 INSERT SQL(菜单表结构待确认)。

专区白名单弹窗 checkbox 不显示(第三次修复,根因确认)

  • 现象:白名单弹窗数据正常(12 条用户),但表格无 checkbox 勾选列,用户无法选择。
  • 已尝试修复(前两次均未生效):
    1. 8-16reactivecomputed + 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 有前置条件:必须传 selectioncurrentselectionType="radio" 三者之一,否则返回 undefined
    • 返回 undefined → tableRowSelection 也返回 undefined → ant-design-vue Table 收到 rowSelection=undefined不渲染 checkbox 列
    • 我们的弹窗三个条件都不满足(没传 :selection / :current / selectionType),所以不管 row-selection 配置多正确都不会出勾选框
  • 最终修复:模板 <ele-pro-table>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 行):
    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
    • 受控 selectedRowKeyswatch 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<T>={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=xxxpage=pages/shop/section-detail;按专区 tenantIdWxMiniappAccessTokenService.getAccessToken(tenantId) 取 tokenprod 用 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-javashop-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 白名单的「添加/移除」功能此前已完整实现,本次只是排查确认:
    • 后端 ShopHomeSectionControllerGET /{id}/users(查)、PUT /{id}/users(覆盖式保存);ShopHomeSectionUserService.listBySectionIdselectBySectionId(XML)。
    • 前端 index.vue:列表「白名单」按钮 → openUserslistSectionUsers 加载已选 → 弹窗;onSaveUserssetSectionUsers 保存。
    • 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.setUsersline ~119)。
    • 查询侧(selectBySectionId)按 section_id 隔离,未动;下单强校验 checkPermission 也按 section_id 隔离,一致。
  • 用户体系统一结论(沿用 01:50 澄清):买家=sys_userpageUsers 选人正确,白名单 user_id 与下单校验 userId 同体系。
  • 部署:重新编译部署 guilixu-javashop-api)生效后,后台 /special/zone 每条专区「白名单」即可勾选添加、取消移除并保存。

白名单弹窗搜索报错修复([object PointerEvent] 误作 limit 传给后端)

  • 现象:白名单弹窗点搜索触发 400BindException ... Field error in object 'userParam' on field 'limit': rejected value [[object PointerEvent]]
  • 根因:重写后的 UserSelectModal.vue 模板 @search="reload" / @pressEnter="reload"。antd a-input-searchsearch 事件签名是 (value, event),故 reload 实际收到 reload(搜索词, PointerEvent)reload(page, limit) 把第二个参数当 limit 传给 pageUserspageSize 变成 PointerEvent → 序列化 [object PointerEvent] 传给后端 limit 字段 → 类型转换失败。
  • 修复:模板改为 @search="() => reload(1)" / @pressEnter="() => reload(1)",只传页码、不传事件对象;reload(1)limit=undefinedpageSize 回退 pagination.pageSize
  • 改动:UserSelectModal.vue 两行模板。
  • 部署:刷新 admin 前端(npm run dev 热更新或重新构建)即可。

白名单保存报 Duplicate entry(逻辑删除×唯一索引死局)

  • 现象:保存专区白名单 PUT /{id}/usersSQLIntegrityConstraintViolationException: 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 id="physicsDeleteBySection"> DELETE FROM shop_home_section_user WHERE section_id=? <if tenantId not null>AND tenant_id=?</if> </delete>(自定义 SQL,MP 不会注入逻辑删除 → 真物理删除);
    • ShopHomeSectionController.setUsershomeSectionUserService.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<User>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.tslistSectionUsers 返回类型改 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 / @confirmopenUsers 简化为只置 currentSectionId+开弹窗;删除 onSaveUserscurrentUserIds、未用 importsetSectionUsers/listSectionUsers 在 index 内已无引用)。
  • 后端 users 关键字搜索:sys_user UserParam.keywords 已覆盖 username/user_id/nickname/real_name/alias/phone(含手机、姓名、昵称),满足需求。
  • 部署:重新编译部署 guilixu-javashop-api+ 重新构建 admin 前端。本机无 maven 未编译;改动为标准 MP/MyBatis 用法,风险低。

专区白名单弹窗改为右侧抽屉(modal → drawer

  • 用户要求:专区「白名单」入口的交互与同页「商品」一致,改成右边弹出的抽屉。
  • 改动:src/views/special/zone/components/UserSelectModal.vue 外层容器由 <a-modal> 改为 <a-drawer placement="right" :width="820">,内部搜索/候选/已添加列表逻辑原样保留,props/emits(visible/sectionId/update:visible) 不变。
  • index.vue<UserSelectModal v-model:visible="showUser" :sectionId="currentSectionId" /> 调用方式无需改动(与商品抽屉同为右侧弹出)。
  • 部署:重新构建 admin 前端(npm run dev 热更新或 build)即可,无需后端改动。