feat(撞款+用户): ①扫描基线与店铺表全量对齐——无结果文件的店以空态(0记录+分组标签)注入扫描,撞款控制台超管可见全部店铺(实测 shop_count 83=全量) ②用户列表支持 role 筛选参数 ③修复 toItem creatorMap.getOrDefault(null) 在不可变 Map 的 NPE

This commit is contained in:
2026-09-06 23:47:38 +08:00
parent f00fdcd42a
commit d3f5a371e3
4 changed files with 46 additions and 5 deletions
@@ -38,11 +38,12 @@ public class AdminUserController {
@RequestParam(required = false) String username, @RequestParam(required = false) String username,
@RequestParam(name = "search", required = false) String search, @RequestParam(name = "search", required = false) String search,
@RequestParam(name = "created_by_id", required = false) Long createdById, @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); AdminUserEntity currentUser = adminAuthSupport.requireAdminOrInternal(request);
String kw = (username == null || username.isBlank()) ? search : username; String kw = (username == null || username.isBlank()) ? search : username;
Long filterCreatedBy = createdById != null ? createdById : adminId; 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); return ApiResponse.success(vo);
} }
@@ -42,7 +42,7 @@ public class AdminUserService {
private final PermissionMenuService permissionMenuService; private final PermissionMenuService permissionMenuService;
public AdminUserListVo listUsers(AdminUserEntity currentUser, Integer page, Integer pageSize, public AdminUserListVo listUsers(AdminUserEntity currentUser, Integer page, Integer pageSize,
String username, Long createdById) { String username, Long createdById, String roleFilter) {
String role = adminAuthSupport.currentRole(currentUser); String role = adminAuthSupport.currentRole(currentUser);
if (role == null) { if (role == null) {
throw new BusinessException(403, "需要管理员权限"); throw new BusinessException(403, "需要管理员权限");
@@ -60,6 +60,10 @@ public class AdminUserService {
Long filterCreatedBy = "super_admin".equals(role) ? createdById : null; Long filterCreatedBy = "super_admin".equals(role) ? createdById : null;
LambdaQueryWrapper<AdminUserEntity> query = new LambdaQueryWrapper<>(); LambdaQueryWrapper<AdminUserEntity> query = new LambdaQueryWrapper<>();
String roleFilterValue = roleFilter == null ? "" : roleFilter.trim();
if (!roleFilterValue.isEmpty()) {
query.eq(AdminUserEntity::getRole, roleFilterValue);
}
if ("super_admin".equals(role)) { if ("super_admin".equals(role)) {
if (!kw.isEmpty()) { if (!kw.isEmpty()) {
query.like(AdminUserEntity::getUsername, kw); query.like(AdminUserEntity::getUsername, kw);
@@ -318,7 +322,9 @@ public class AdminUserService {
vo.setAdmin(entity.getIsAdmin() != null && entity.getIsAdmin() == 1); vo.setAdmin(entity.getIsAdmin() != null && entity.getIsAdmin() == 1);
vo.setRole(entity.getRole() == null || entity.getRole().isEmpty() ? "normal" : entity.getRole()); vo.setRole(entity.getRole() == null || entity.getRole().isEmpty() ? "normal" : entity.getRole());
vo.setCreatedById(entity.getCreatedById()); vo.setCreatedById(entity.getCreatedById());
vo.setCreatorUsername(creatorMap.getOrDefault(entity.getCreatedById(), "")); // 不可变 Map(Map.of) 的 getOrDefault 不允许 null keycreatedById 为 null 时直接空串。
Long creatorId = entity.getCreatedById();
vo.setCreatorUsername(creatorId == null ? "" : creatorMap.getOrDefault(creatorId, ""));
LocalDateTime createdAt = entity.getCreatedAt(); LocalDateTime createdAt = entity.getCreatedAt();
vo.setCreatedAt(createdAt == null ? "" : createdAt.format(CREATED_AT_FORMATTER)); vo.setCreatedAt(createdAt == null ? "" : createdAt.format(CREATED_AT_FORMATTER));
vo.setPinyinAbbr(PinyinAbbrUtil.abbr(entity.getUsername())); vo.setPinyinAbbr(PinyinAbbrUtil.abbr(entity.getUsername()));
@@ -68,6 +68,21 @@ public interface ShopDuplicateCheckSourceMapper {
""") """)
List<ShopGroupLabelDto> selectGroupLabels(@Param("shopNames") List<String> shopNames); List<ShopGroupLabelDto> selectGroupLabels(@Param("shopNames") List<String> 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<ShopGroupLabelDto> selectAllShopLabels();
/** /**
* 主管(admin 角色)可管店铺:自己创建或指派给自己的分组(created_by_id=me OR user_id=me * 主管(admin 角色)可管店铺:自己创建或指派给自己的分组(created_by_id=me OR user_id=me
* 下的店铺名,trim 后去重。无分组时返回空集合(什么都看不到),与 Python 语义一致。 * 下的店铺名,trim 后去重。无分组时返回空集合(什么都看不到),与 Python 语义一致。
@@ -26,6 +26,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -214,7 +215,8 @@ public class ShopDataDuplicateCheckScanService {
return message == null || message.isBlank() ? ex.getClass().getSimpleName() : message; return message == null || message.isBlank() ? ex.getClass().getSimpleName() : message;
} }
/** 逐店并发拉取/解析结果文件;单店失败仅告警跳过,不中断整体。 */ /** 逐店并发拉取/解析结果文件;单店失败仅告警跳过,不中断整体。
* 扫描基线与店铺表全量对齐:无结果文件的店以空态(0 记录)注入,保证撞款控制台可见全部店铺。 */
private List<ShopParsed> collectShopParsed(DistributedJobLockService.LockHandle lock) { private List<ShopParsed> collectShopParsed(DistributedJobLockService.LockHandle lock) {
List<ShopSourceRowDto> rows = sourceMapper.selectLatestResultRows(); List<ShopSourceRowDto> rows = sourceMapper.selectLatestResultRows();
List<ShopParsed> parsedShops = new ArrayList<>(); List<ShopParsed> parsedShops = new ArrayList<>();
@@ -222,6 +224,7 @@ public class ShopDataDuplicateCheckScanService {
return parsedShops; return parsedShops;
} }
Map<String, String> groupLabels = loadGroupLabels(rows); Map<String, String> groupLabels = loadGroupLabels(rows);
Set<String> parsedNames = new HashSet<>();
ExecutorService executor = Executors.newFixedThreadPool(Math.min(PARSE_THREADS, rows.size())); ExecutorService executor = Executors.newFixedThreadPool(Math.min(PARSE_THREADS, rows.size()));
try { try {
List<Future<ShopParsed>> futures = new ArrayList<>(rows.size()); List<Future<ShopParsed>> futures = new ArrayList<>(rows.size());
@@ -236,6 +239,7 @@ public class ShopDataDuplicateCheckScanService {
ShopParsed item = future.get(); ShopParsed item = future.get();
if (item != null) { if (item != null) {
parsedShops.add(item); parsedShops.add(item);
parsedNames.add(normalizeShopName(item.shopName()));
} }
} catch (ExecutionException ex) { } catch (ExecutionException ex) {
log.warn("[shop-duplicate-check] 并发解析店铺结果文件异常: {}", ex.getCause() == null log.warn("[shop-duplicate-check] 并发解析店铺结果文件异常: {}", ex.getCause() == null
@@ -257,6 +261,21 @@ public class ShopDataDuplicateCheckScanService {
executor.shutdownNow(); executor.shutdownNow();
} }
} }
// 基线扩展:店铺表全量中尚未解析到结果文件行的店 → 空态注入(0 记录、分组标签来自店铺表)。
List<ShopGroupLabelDto> 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; return parsedShops;
} }