From 92baf41073649e5955046e1c3ccfcdf217bdecd4 Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Sun, 11 Oct 2026 16:34:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B6=88=E9=99=A4=20GROUP=20BY=20?= =?UTF-8?q?=E9=9D=9E=E5=88=86=E7=BB=84=E5=88=97=E6=8A=A5=E9=94=99=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=9E=E9=94=81=E6=A0=87=E8=AF=86=E8=84=8F?= =?UTF-8?q?=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL ONLY_FULL_GROUP_BY 下,MyBatis-Plus 的 list(wrapper) 默认 SELECT 实体全部字段,配合 groupBy 后非分组列既不在 GROUP BY 中也不函数依赖分组键, 导致 SQLSyntaxErrorException。即便放宽模式,这些字段也只是组内随机一行的值。 根因是借用 SQL GROUP BY 实现"按某列去重",共 7 处,统一改为一次查询 + 内存分组,返回值结构与原实现一致: - MerchantServiceImpl.groupByChain(本次线上报错点) - YxeProductServiceImpl.listByGroup / listByChain / listByCate(2 个重载) - YxeTaskProductServiceImpl.productGroupByTask - YxeShareServiceImpl.listByMerchant(2 个重载) 分组父节点用独立副本(BeanUtils.copyProperties)而非复用子列表实例, 否则 parent.setChildren(children) 会造成对象自引用,JSON 序列化报 "Direct self-reference leading to cycle"。 附带消除 N+1:groupByChain 原为 1+12 次查询、重复拉取 8105 行。 同时 groupByChain 返回稳定 chainId(连锁名,与字典 sys_dict_data.dict_id=175 的 dict_data_code 一致),从源头消除前端回退到 `chain_${index}` 位置型占位符, 避免其经审核端写入 yxe_self_report.chain_id 后按连锁等值筛选永久漏查。 SQL: fix_self_report_chain_id_placeholder_2026-10-11.sql(本地已执行 5 行) --- ...report_chain_id_placeholder_2026-10-11.sql | 42 +++++++ .../service/impl/MerchantServiceImpl.java | 30 +++-- .../service/impl/YxeProductServiceImpl.java | 107 +++++++++++------- .../yxe/service/impl/YxeShareServiceImpl.java | 25 ++-- .../impl/YxeTaskProductServiceImpl.java | 30 +++-- 5 files changed, 162 insertions(+), 72 deletions(-) create mode 100644 fix_self_report_chain_id_placeholder_2026-10-11.sql diff --git a/fix_self_report_chain_id_placeholder_2026-10-11.sql b/fix_self_report_chain_id_placeholder_2026-10-11.sql new file mode 100644 index 0000000..95dafa0 --- /dev/null +++ b/fix_self_report_chain_id_placeholder_2026-10-11.sql @@ -0,0 +1,42 @@ +-- 用途:修正 yxe_self_report.chain_id 中残留的位置型占位符(chain_0、chain_3、chain_6 …) +-- 日期:2026-10-11 +-- 背景:前端 normalizeMerchantChain 在接口未返回 chainId 时会回退成 `chain_${index}`, +-- 该值经审核端 saveMerchantEdit 写入 chain_id 落库。而按连锁筛选走的是等值匹配 +-- (YxeSelfReportMapper.xml: a.chain_id = #{param.chainId}),占位符导致这些记录 +-- 在按连锁筛选时被永久漏掉。本系统没有连锁表,chain 名称即事实上的连锁标识 +-- (连锁字典 sys_dict_data.dict_id=175 的 dict_data_code 同样是连锁名)。 +-- 影响:5 行(chain_6 -> 老百姓 3 行;chain_3 -> 百姓人家 2 行) +-- 性质:幂等,可重复执行;以 chain_name 为准回填 chain_id +-- 配套代码:MerchantServiceImpl.groupByChain 返回稳定 chainId; +-- AuditSelfReportItem.vue 兜底链改用连锁名并在写入前拦截 chain_ 前缀 + +-- 执行前核对(应为 5 行) +SELECT id, chain_id, chain_name +FROM yxe_self_report +WHERE chain_id REGEXP '^chain_[0-9]+$' +ORDER BY id; + +-- 修正 +UPDATE yxe_self_report +SET chain_id = chain_name +WHERE chain_id REGEXP '^chain_[0-9]+$' + AND chain_name IS NOT NULL + AND chain_name <> ''; + +-- 执行后核对(remaining_placeholder 与 mismatch 均应为 0) +SELECT SUM(chain_id REGEXP '^chain_[0-9]+$') AS remaining_placeholder, + SUM(chain_id <> chain_name) AS mismatch, + COUNT(*) AS total +FROM yxe_self_report; + +-- ============================================================ +-- 执行记录 +-- ============================================================ +-- 2026-10-11 本地开发库(127.0.0.1:3306/yaoxiaoer,root) +-- 执行结果:受影响 5 行 +-- 执行后核对:remaining_placeholder = 0,mismatch = 0 +-- 功能验证:按连锁筛选命中数 +-- chainId=老百姓 279 -> 282(找回 id 119、130、169) +-- chainId=百姓人家 174 -> 176(找回 id 138、139) +-- +-- 生产库:尚未执行。需经确认后按同一条语句复核执行。 diff --git a/src/main/java/com/gxwebsoft/shop/service/impl/MerchantServiceImpl.java b/src/main/java/com/gxwebsoft/shop/service/impl/MerchantServiceImpl.java index fa3b653..6b28bf5 100644 --- a/src/main/java/com/gxwebsoft/shop/service/impl/MerchantServiceImpl.java +++ b/src/main/java/com/gxwebsoft/shop/service/impl/MerchantServiceImpl.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -78,22 +79,31 @@ public class MerchantServiceImpl extends ServiceImpl i @Override public List> groupByChain() { - List merchantList = list( + // 一次性查出全部有效商户,在内存中按 chain 分组。 + // 这里不能用 SQL 的 GROUP BY chain:list() 会 SELECT 实体全部字段, + // 而 id/name/code 等列既不在 GROUP BY 中、也不函数依赖于 chain, + // 在 ONLY_FULL_GROUP_BY 下直接报错;即便放宽模式,取到的也只是组内随机一行的值。 + List allMerchant = list( new LambdaQueryWrapper() .isNotNull(Merchant::getChain) - .groupBy(Merchant::getChain) ); - List> miniList = new ArrayList<>(); - for (Merchant merchant : merchantList) { + Map> chainGroup = new LinkedHashMap<>(); + for (Merchant merchant : allMerchant) { + chainGroup.computeIfAbsent(merchant.getChain(), k -> new ArrayList<>()).add(merchant); + } + + List> miniList = new ArrayList<>(); + for (Map.Entry> entry : chainGroup.entrySet()) { Map map = new HashMap<>(); - map.put("chain", merchant.getChain()); - List childrenMerchant = list( - new LambdaQueryWrapper() - .eq(Merchant::getChain, merchant.getChain()) - ); + map.put("chain", entry.getKey()); + // 同时返回 chainId。本系统没有连锁表,chain 名称就是事实上的连锁标识 + // (连锁字典 sys_dict_data.dict_id=175 的 dict_data_code 同样是连锁名)。 + // 显式给出稳定 id,前端就不必回退到 `chain_${index}` 这种位置型占位符; + // 那种占位符一旦被写进 yxe_self_report.chain_id,按连锁等值筛选就查不到该记录。 + map.put("chainId", entry.getKey()); List> childrenList = new ArrayList<>(); - for (Merchant merchantChild : childrenMerchant) { + for (Merchant merchantChild : entry.getValue()) { Map mapChild = new HashMap<>(); mapChild.put("id", merchantChild.getId()); mapChild.put("name", merchantChild.getName()); diff --git a/src/main/java/com/gxwebsoft/yxe/service/impl/YxeProductServiceImpl.java b/src/main/java/com/gxwebsoft/yxe/service/impl/YxeProductServiceImpl.java index 1b991bf..97d4ad9 100644 --- a/src/main/java/com/gxwebsoft/yxe/service/impl/YxeProductServiceImpl.java +++ b/src/main/java/com/gxwebsoft/yxe/service/impl/YxeProductServiceImpl.java @@ -12,6 +12,7 @@ import com.gxwebsoft.yxe.entity.YxeProduct; import com.gxwebsoft.yxe.param.YxeProductParam; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -20,7 +21,10 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -109,15 +113,20 @@ public class YxeProductServiceImpl extends ServiceImpl listByGroup(YxeProductParam param) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() - .groupBy(YxeProduct::getChain); - queryWrapper.eq(YxeProduct::getStatus, 0); + .eq(YxeProduct::getStatus, 0); if (param != null && param.getChain() != null) { queryWrapper.eq(YxeProduct::getChain, param.getChain()); } - List list = list(queryWrapper); + // 按 chain 去重:查出符合条件的产品后在内存里归组。 + // 不能用 SQL 的 GROUP BY chain —— list() 会 SELECT 实体全部字段, + // 这些非分组列在 ONLY_FULL_GROUP_BY 下非法,放宽模式也只是取组内随机一行的值。 + // 注意:这里不加 chain IS NOT NULL,保持与原 GROUP BY 行为一致(NULL 自成一组)。 + List list = distinctByKey(list(queryWrapper), YxeProduct::getChain); List all = list(); for (YxeProduct product : list) { - product.setChildren(all.stream().filter(p -> p.getChain().equals(product.getChain())).collect(Collectors.toList())); + product.setChildren(all.stream() + .filter(p -> Objects.equals(p.getChain(), product.getChain())) + .collect(Collectors.toList())); } return list; } @@ -210,67 +219,77 @@ public class YxeProductServiceImpl extends ServiceImpl listByChain() { - List list = list( + // 一次查出全部有效产品,在内存中按 chain 归组,避免 SQL GROUP BY 选非分组列。 + List all = list( new LambdaQueryWrapper() .eq(YxeProduct::getStatus, 0) .isNotNull(YxeProduct::getChain) - .groupBy(YxeProduct::getChain) + .orderByDesc(YxeProduct::getCreateTime) ); - for (YxeProduct product : list) { - product.setChildren(list( - new LambdaQueryWrapper() - .eq(YxeProduct::getStatus, 0) - .eq(YxeProduct::getChain, product.getChain()) - .orderByDesc(YxeProduct::getCreateTime) - )); + Map> chainGroup = new LinkedHashMap<>(); + for (YxeProduct product : all) { + chainGroup.computeIfAbsent(product.getChain(), k -> new ArrayList<>()).add(product); + } + List list = new ArrayList<>(); + for (Map.Entry> entry : chainGroup.entrySet()) { + List children = entry.getValue(); + // 父节点必须是组内首条产品的“独立副本”。原实现中父行来自 GROUP BY 查询、 + // 子列表来自另一次查询,是两个不同对象;若直接把子列表中的实例当父节点, + // parent.setChildren(children) 会让对象引用自身,JSON 序列化时报 + // "Direct self-reference leading to cycle"。 + YxeProduct parent = new YxeProduct(); + BeanUtils.copyProperties(children.get(0), parent); + parent.setChildren(children); + list.add(parent); } return list; } @Override public List listByCate() { - List list = list( - new LambdaQueryWrapper() - .eq(YxeProduct::getStatus, 0) - .isNotNull(YxeProduct::getCate) - .groupBy(YxeProduct::getCate) - ); - for (YxeProduct product : list) { - product.setChildren(list( - new LambdaQueryWrapper() - .eq(YxeProduct::getStatus, 0) - .eq(YxeProduct::getCate, product.getCate()) - .orderByDesc(YxeProduct::getCreateTime) - )); - } - return list; + return listByCate(null); } @Override public List listByCate(YxeProductParam param) { - LambdaQueryWrapper wrapper = + // 一次查出全部有效产品,在内存中按 cate 归组,避免 SQL GROUP BY 选非分组列。 + LambdaQueryWrapper allWrapper = new LambdaQueryWrapper() .eq(YxeProduct::getStatus, 0) .isNotNull(YxeProduct::getCate) - .groupBy(YxeProduct::getCate); + .orderByDesc(YxeProduct::getCreateTime); if (param != null) { - if (param.getChain() != null) wrapper.eq(YxeProduct::getChain, param.getChain()); - if (param.getType() != null) wrapper.eq(YxeProduct::getType, param.getType()); + if (param.getChain() != null) allWrapper.eq(YxeProduct::getChain, param.getChain()); + if (param.getType() != null) allWrapper.eq(YxeProduct::getType, param.getType()); } - List list = list(wrapper); - for (YxeProduct product : list) { - LambdaQueryWrapper childWrapper = - new LambdaQueryWrapper() - .eq(YxeProduct::getStatus, 0) - .eq(YxeProduct::getCate, product.getCate()) - .orderByDesc(YxeProduct::getCreateTime); - if (param != null) { - if (param.getChain() != null) childWrapper.eq(YxeProduct::getChain, param.getChain()); - if (param.getType() != null) childWrapper.eq(YxeProduct::getType, param.getType()); - } - product.setChildren(list(childWrapper)); + List all = list(allWrapper); + + Map> cateGroup = new LinkedHashMap<>(); + for (YxeProduct product : all) { + cateGroup.computeIfAbsent(product.getCate(), k -> new ArrayList<>()).add(product); + } + List list = new ArrayList<>(); + for (Map.Entry> entry : cateGroup.entrySet()) { + List children = entry.getValue(); + // 同上:父节点用独立副本,避免对象自引用导致 JSON 序列化成环。 + YxeProduct parent = new YxeProduct(); + BeanUtils.copyProperties(children.get(0), parent); + parent.setChildren(children); + list.add(parent); } return list; } + /** + * 按指定字段去重,保留每组首次出现的元素(保持原查询顺序)。 + * 用于替代原来用 SQL GROUP BY 实现的“按某列取唯一值”语义。 + */ + private List distinctByKey(List source, java.util.function.Function keyGetter) { + Map seen = new LinkedHashMap<>(); + for (YxeProduct item : source) { + seen.putIfAbsent(keyGetter.apply(item), item); + } + return new ArrayList<>(seen.values()); + } + } diff --git a/src/main/java/com/gxwebsoft/yxe/service/impl/YxeShareServiceImpl.java b/src/main/java/com/gxwebsoft/yxe/service/impl/YxeShareServiceImpl.java index 2dea1e5..e5bafa5 100644 --- a/src/main/java/com/gxwebsoft/yxe/service/impl/YxeShareServiceImpl.java +++ b/src/main/java/com/gxwebsoft/yxe/service/impl/YxeShareServiceImpl.java @@ -204,23 +204,32 @@ public class YxeShareServiceImpl extends ServiceImpl i @Override public List listByMerchant() { - return list( - new LambdaQueryWrapper() - .eq(YxeShare::getStatus, 1) - .groupBy(YxeShare::getMerchantName) - ); + return listByMerchant(null); } @Override public List listByMerchant(YxeShareParam param) { + // 一次查出全部已通过的分享记录,在内存中按 merchantName 去重。 + // 不能用 SQL GROUP BY merchant_name:list() 会 SELECT 实体全部字段, + // 这些非分组列在 ONLY_FULL_GROUP_BY 下非法,放宽模式也只是取组内随机一行的值。 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(YxeShare::getStatus, 1); - if (param.getProvince() != null && param.getCity() != null) { + if (param != null && param.getProvince() != null && param.getCity() != null) { queryWrapper.eq(YxeShare::getProvince, param.getProvince()); queryWrapper.eq(YxeShare::getCity, param.getCity()); } - queryWrapper.groupBy(YxeShare::getMerchantName); - return list(queryWrapper); + return distinctByMerchantName(list(queryWrapper)); + } + + /** + * 按 merchantName 去重,保留每个门店首次出现的记录(保持原查询顺序)。 + */ + private List distinctByMerchantName(List source) { + Map seen = new LinkedHashMap<>(); + for (YxeShare item : source) { + seen.putIfAbsent(item.getMerchantName(), item); + } + return new ArrayList<>(seen.values()); } @Override diff --git a/src/main/java/com/gxwebsoft/yxe/service/impl/YxeTaskProductServiceImpl.java b/src/main/java/com/gxwebsoft/yxe/service/impl/YxeTaskProductServiceImpl.java index 47787d1..25f1524 100644 --- a/src/main/java/com/gxwebsoft/yxe/service/impl/YxeTaskProductServiceImpl.java +++ b/src/main/java/com/gxwebsoft/yxe/service/impl/YxeTaskProductServiceImpl.java @@ -10,6 +10,7 @@ import com.gxwebsoft.yxe.entity.YxeTaskProduct; import com.gxwebsoft.yxe.param.YxeTaskProductParam; import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageResult; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; @@ -130,23 +131,32 @@ public class YxeTaskProductServiceImpl extends ServiceImpl productList = new ArrayList<>(); if (!productIds.isEmpty()) { - productList = productService.list(new LambdaQueryWrapper() - .in(YxeProduct::getId, productIds) - .groupBy(YxeProduct::getMakerAbb) + // 取出任务下的全部产品后在内存里按 makerAbb 归组。 + // 不能用 SQL GROUP BY:list() 会 SELECT 实体全部字段,这些非分组列在 + // ONLY_FULL_GROUP_BY 下非法,放宽模式也只是取组内随机一行的值。 + List allProducts = productService.list( + new LambdaQueryWrapper() + .in(YxeProduct::getId, productIds) ); - for (YxeProduct product : productList) { - List children = productService.list( - new LambdaQueryWrapper() - .in(YxeProduct::getId, productIds) - .eq(YxeProduct::getMakerAbb, product.getMakerAbb()) - ); + Map> makerGroup = new LinkedHashMap<>(); + for (YxeProduct product : allProducts) { + makerGroup.computeIfAbsent(product.getMakerAbb(), k -> new ArrayList<>()).add(product); + } + for (Map.Entry> entry : makerGroup.entrySet()) { + List children = entry.getValue(); for (YxeProduct child : children) { YxeTaskProduct taskProduct = list.stream().filter(item -> item.getProductId().equals(child.getId())).findFirst().orElse(null); if (taskProduct != null) { child.setTaskIncome(taskProduct.getAmount()); } } - product.setChildren(children); + // 父节点必须是组内首条产品的“独立副本”:原实现中父行来自 GROUP BY 查询、 + // 子列表来自另一次查询,是两个不同对象;直接复用子列表实例会让对象自引用, + // JSON 序列化时报 "Direct self-reference leading to cycle"。 + YxeProduct parent = new YxeProduct(); + BeanUtils.copyProperties(children.get(0), parent); + parent.setChildren(children); + productList.add(parent); } } return productList;