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(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);
}
@@ -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<AdminUserEntity> 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 keycreatedById 为 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()));
@@ -68,6 +68,21 @@ public interface ShopDuplicateCheckSourceMapper {
""")
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
* 下的店铺名,trim 后去重。无分组时返回空集合(什么都看不到),与 Python 语义一致。
@@ -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<ShopParsed> collectShopParsed(DistributedJobLockService.LockHandle lock) {
List<ShopSourceRowDto> rows = sourceMapper.selectLatestResultRows();
List<ShopParsed> parsedShops = new ArrayList<>();
@@ -222,6 +224,7 @@ public class ShopDataDuplicateCheckScanService {
return parsedShops;
}
Map<String, String> groupLabels = loadGroupLabels(rows);
Set<String> parsedNames = new HashSet<>();
ExecutorService executor = Executors.newFixedThreadPool(Math.min(PARSE_THREADS, rows.size()));
try {
List<Future<ShopParsed>> 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<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;
}