From d3f5a371e35dd5ee4b39b7679d74c96585df3248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sun, 6 Sep 2026 23:47:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=92=9E=E6=AC=BE+=E7=94=A8=E6=88=B7):=20?= =?UTF-8?q?=E2=91=A0=E6=89=AB=E6=8F=8F=E5=9F=BA=E7=BA=BF=E4=B8=8E=E5=BA=97?= =?UTF-8?q?=E9=93=BA=E8=A1=A8=E5=85=A8=E9=87=8F=E5=AF=B9=E9=BD=90=E2=80=94?= =?UTF-8?q?=E2=80=94=E6=97=A0=E7=BB=93=E6=9E=9C=E6=96=87=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E5=BA=97=E4=BB=A5=E7=A9=BA=E6=80=81(0=E8=AE=B0=E5=BD=95+?= =?UTF-8?q?=E5=88=86=E7=BB=84=E6=A0=87=E7=AD=BE)=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=EF=BC=8C=E6=92=9E=E6=AC=BE=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=8F=B0=E8=B6=85=E7=AE=A1=E5=8F=AF=E8=A7=81=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E5=BA=97=E9=93=BA(=E5=AE=9E=E6=B5=8B=20shop=5Fcount=2083=3D?= =?UTF-8?q?=E5=85=A8=E9=87=8F)=20=E2=91=A1=E7=94=A8=E6=88=B7=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=94=AF=E6=8C=81=20role=20=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E5=8F=82=E6=95=B0=20=E2=91=A2=E4=BF=AE=E5=A4=8D=20toItem=20cre?= =?UTF-8?q?atorMap.getOrDefault(null)=20=E5=9C=A8=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E5=8F=98=20Map=20=E7=9A=84=20NPE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminUserController.java | 5 +++-- .../admin/service/AdminUserService.java | 10 +++++++-- .../ShopDuplicateCheckSourceMapper.java | 15 +++++++++++++ .../ShopDataDuplicateCheckScanService.java | 21 ++++++++++++++++++- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/controller/AdminUserController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/controller/AdminUserController.java index 60c69875..5f81fca8 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/controller/AdminUserController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/controller/AdminUserController.java @@ -38,11 +38,12 @@ public class AdminUserController { @RequestParam(required = false) String username, @RequestParam(name = "search", required = false) String search, @RequestParam(name = "created_by_id", required = false) Long createdById, - @RequestParam(name = "admin_id", required = false) Long adminId) { + @RequestParam(name = "admin_id", required = false) Long adminId, + @RequestParam(name = "role", required = false) String role) { AdminUserEntity currentUser = adminAuthSupport.requireAdminOrInternal(request); String kw = (username == null || username.isBlank()) ? search : username; Long filterCreatedBy = createdById != null ? createdById : adminId; - AdminUserListVo vo = adminUserService.listUsers(currentUser, page, pageSize, kw, filterCreatedBy); + AdminUserListVo vo = adminUserService.listUsers(currentUser, page, pageSize, kw, filterCreatedBy, role); return ApiResponse.success(vo); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java index 9188f94b..d768cb50 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java @@ -42,7 +42,7 @@ public class AdminUserService { private final PermissionMenuService permissionMenuService; public AdminUserListVo listUsers(AdminUserEntity currentUser, Integer page, Integer pageSize, - String username, Long createdById) { + String username, Long createdById, String roleFilter) { String role = adminAuthSupport.currentRole(currentUser); if (role == null) { throw new BusinessException(403, "需要管理员权限"); @@ -60,6 +60,10 @@ public class AdminUserService { Long filterCreatedBy = "super_admin".equals(role) ? createdById : null; LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + String roleFilterValue = roleFilter == null ? "" : roleFilter.trim(); + if (!roleFilterValue.isEmpty()) { + query.eq(AdminUserEntity::getRole, roleFilterValue); + } if ("super_admin".equals(role)) { if (!kw.isEmpty()) { query.like(AdminUserEntity::getUsername, kw); @@ -318,7 +322,9 @@ public class AdminUserService { vo.setAdmin(entity.getIsAdmin() != null && entity.getIsAdmin() == 1); vo.setRole(entity.getRole() == null || entity.getRole().isEmpty() ? "normal" : entity.getRole()); vo.setCreatedById(entity.getCreatedById()); - vo.setCreatorUsername(creatorMap.getOrDefault(entity.getCreatedById(), "")); + // 不可变 Map(Map.of) 的 getOrDefault 不允许 null key:createdById 为 null 时直接空串。 + Long creatorId = entity.getCreatedById(); + vo.setCreatorUsername(creatorId == null ? "" : creatorMap.getOrDefault(creatorId, "")); LocalDateTime createdAt = entity.getCreatedAt(); vo.setCreatedAt(createdAt == null ? "" : createdAt.format(CREATED_AT_FORMATTER)); vo.setPinyinAbbr(PinyinAbbrUtil.abbr(entity.getUsername())); diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopduplicatecheck/mapper/ShopDuplicateCheckSourceMapper.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopduplicatecheck/mapper/ShopDuplicateCheckSourceMapper.java index 5153779c..e6542f91 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopduplicatecheck/mapper/ShopDuplicateCheckSourceMapper.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopduplicatecheck/mapper/ShopDuplicateCheckSourceMapper.java @@ -68,6 +68,21 @@ public interface ShopDuplicateCheckSourceMapper { """) List selectGroupLabels(@Param("shopNames") List shopNames); + /** + * 全量店铺名+分组标签(biz_shop_manage 全集):扫描基线对齐店铺表—— + * 无结果文件的店以空态(0 记录)参与扫描,保证撞款控制台超管可见全部分组店铺。 + */ + @Select(""" + SELECT TRIM(sm.shop_name) AS shopName, + GROUP_CONCAT(DISTINCT COALESCE(NULLIF(g.group_name, ''), NULLIF(sm.group_name, '')) + ORDER BY sm.id SEPARATOR '、') AS groupName + FROM biz_shop_manage sm + LEFT JOIN biz_shop_manage_group g ON g.id = sm.group_id + WHERE TRIM(COALESCE(sm.shop_name, '')) <> '' + GROUP BY TRIM(sm.shop_name) + """) + List selectAllShopLabels(); + /** * 主管(admin 角色)可管店铺:自己创建或指派给自己的分组(created_by_id=me OR user_id=me) * 下的店铺名,trim 后去重。无分组时返回空集合(什么都看不到),与 Python 语义一致。 diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopduplicatecheck/service/ShopDataDuplicateCheckScanService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopduplicatecheck/service/ShopDataDuplicateCheckScanService.java index c0786b1c..dcc01bbc 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopduplicatecheck/service/ShopDataDuplicateCheckScanService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopduplicatecheck/service/ShopDataDuplicateCheckScanService.java @@ -26,6 +26,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -214,7 +215,8 @@ public class ShopDataDuplicateCheckScanService { return message == null || message.isBlank() ? ex.getClass().getSimpleName() : message; } - /** 逐店并发拉取/解析结果文件;单店失败仅告警跳过,不中断整体。 */ + /** 逐店并发拉取/解析结果文件;单店失败仅告警跳过,不中断整体。 + * 扫描基线与店铺表全量对齐:无结果文件的店以空态(0 记录)注入,保证撞款控制台可见全部店铺。 */ private List collectShopParsed(DistributedJobLockService.LockHandle lock) { List rows = sourceMapper.selectLatestResultRows(); List parsedShops = new ArrayList<>(); @@ -222,6 +224,7 @@ public class ShopDataDuplicateCheckScanService { return parsedShops; } Map groupLabels = loadGroupLabels(rows); + Set parsedNames = new HashSet<>(); ExecutorService executor = Executors.newFixedThreadPool(Math.min(PARSE_THREADS, rows.size())); try { List> futures = new ArrayList<>(rows.size()); @@ -236,6 +239,7 @@ public class ShopDataDuplicateCheckScanService { ShopParsed item = future.get(); if (item != null) { parsedShops.add(item); + parsedNames.add(normalizeShopName(item.shopName())); } } catch (ExecutionException ex) { log.warn("[shop-duplicate-check] 并发解析店铺结果文件异常: {}", ex.getCause() == null @@ -257,6 +261,21 @@ public class ShopDataDuplicateCheckScanService { executor.shutdownNow(); } } + // 基线扩展:店铺表全量中尚未解析到结果文件行的店 → 空态注入(0 记录、分组标签来自店铺表)。 + List allShops = sourceMapper.selectAllShopLabels(); + int injectedEmptyShops = 0; + for (ShopGroupLabelDto all : allShops) { + String name = normalizeShopName(all.getShopName()); + if (name.isEmpty() || parsedNames.contains(name)) { + continue; + } + String groupName = all.getGroupName() == null ? "" : all.getGroupName(); + parsedShops.add(new ShopParsed(name, groupName, List.of(), List.of())); + injectedEmptyShops++; + } + if (injectedEmptyShops > 0) { + log.info("[shop-duplicate-check] 扫描基线含空态店铺 {} 家(无结果文件,以 0 记录参与撞款视图)", injectedEmptyShops); + } return parsedShops; }