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; }