task-280(验收反馈): 重复检测对齐reference P0(分组筛选生效/抽屉聚合+汇总徽章/台账计数预载/total_dup计数/矩阵表头吸顶;后端含工作区在途console化代码)
This commit is contained in:
+52
@@ -128,6 +128,58 @@ public class AdminShopDataDuplicateCheckController {
|
||||
return ApiResponse.success(queryService.detailData(scan, visibleKeys, safePage, safeSize, filters));
|
||||
}
|
||||
|
||||
@GetMapping("/duplicate-check-console")
|
||||
@Operation(summary = "撞款控制台快照:指标/店铺/命中筛选的撞款全集/分组统计")
|
||||
public ApiResponse<Object> console(
|
||||
HttpServletRequest request,
|
||||
@RequestParam(name = "asin", required = false) String asin,
|
||||
@RequestParam(name = "shop_name", required = false) String shopName,
|
||||
@RequestParam(name = "group", required = false) String group,
|
||||
@RequestParam(name = "country", required = false) String country,
|
||||
@RequestParam(name = "site", required = false) String site,
|
||||
@RequestParam(name = "date_from", required = false) String dateFrom,
|
||||
@RequestParam(name = "date_to", required = false) String dateTo) {
|
||||
RequestOperator operator = requireDuplicateCheckAccess(request);
|
||||
ShopDataDuplicateCheckQueryService.Filters filters =
|
||||
ShopDataDuplicateCheckQueryService.Filters.clean(asin, shopName, country, site, dateFrom, dateTo);
|
||||
DuplicateScanView scan = scanService.loadLatest();
|
||||
if (scan == null) {
|
||||
return ApiResponse.success(queryService.pendingConsole());
|
||||
}
|
||||
Set<String> visibleKeys = queryService.resolveVisibleShopKeys(operator.id(), operator.superAdmin());
|
||||
Set<String> effectiveKeys = queryService.withGroupFilter(visibleKeys, scan, group);
|
||||
return ApiResponse.success(queryService.consoleData(scan, effectiveKeys, filters));
|
||||
}
|
||||
|
||||
@GetMapping("/duplicate-check-ledger")
|
||||
@Operation(summary = "全部 ASIN 台账(含未撞款):筛选 + 列排序 + 分页")
|
||||
public ApiResponse<Object> ledger(
|
||||
HttpServletRequest request,
|
||||
@RequestParam(name = "page", required = false) String page,
|
||||
@RequestParam(name = "page_size", required = false) String pageSize,
|
||||
@RequestParam(name = "sort_key", required = false) String sortKey,
|
||||
@RequestParam(name = "sort_dir", required = false) String sortDir,
|
||||
@RequestParam(name = "asin", required = false) String asin,
|
||||
@RequestParam(name = "shop_name", required = false) String shopName,
|
||||
@RequestParam(name = "group", required = false) String group,
|
||||
@RequestParam(name = "country", required = false) String country,
|
||||
@RequestParam(name = "site", required = false) String site,
|
||||
@RequestParam(name = "date_from", required = false) String dateFrom,
|
||||
@RequestParam(name = "date_to", required = false) String dateTo) {
|
||||
RequestOperator operator = requireDuplicateCheckAccess(request);
|
||||
int safePage = parsePage(page);
|
||||
int safeSize = clamp(parseSize(pageSize, 100), 1, 200);
|
||||
ShopDataDuplicateCheckQueryService.Filters filters =
|
||||
ShopDataDuplicateCheckQueryService.Filters.clean(asin, shopName, country, site, dateFrom, dateTo);
|
||||
DuplicateScanView scan = scanService.loadLatest();
|
||||
if (scan == null) {
|
||||
return ApiResponse.success(queryService.pendingLedger(safePage, safeSize));
|
||||
}
|
||||
Set<String> visibleKeys = queryService.resolveVisibleShopKeys(operator.id(), operator.superAdmin());
|
||||
Set<String> effectiveKeys = queryService.withGroupFilter(visibleKeys, scan, group);
|
||||
return ApiResponse.success(queryService.ledgerData(scan, effectiveKeys, safePage, safeSize, sortKey, sortDir, filters));
|
||||
}
|
||||
|
||||
@GetMapping("/duplicate-check-export")
|
||||
@Operation(summary = "撞款导出 CSV(UTF-8 BOM,逐行=一条上架记录)")
|
||||
public void export(
|
||||
|
||||
+216
@@ -11,6 +11,7 @@ import com.nanri.aiimage.modules.shopduplicatecheck.service.ShopDataDuplicateChe
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.service.support.DuplicateCheckCsvWriter;
|
||||
import com.nanri.aiimage.modules.shopduplicatecheck.service.support.DuplicateCheckTimeNormalizer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -37,6 +38,7 @@ import java.util.Set;
|
||||
* 内存模型/排序语义与 Python 后台逐字对齐;返回结构为可直接序列化的 Map(键 snake_case,
|
||||
* 空态与 Python 响应完全一致:pending 时 summary 为 {} 空对象)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ShopDataDuplicateCheckQueryService {
|
||||
@@ -86,6 +88,29 @@ public class ShopDataDuplicateCheckQueryService {
|
||||
return keys;
|
||||
}
|
||||
|
||||
/** 分组筛选:group 非空时取该分组全部成员店铺键;与 visibleKeys 求交集(null=全量时直接取组键)。 */
|
||||
public Set<String> withGroupFilter(Set<String> visibleKeys, DuplicateScanView view, String groupName) {
|
||||
String group = trim(groupName);
|
||||
if (group.isEmpty()) {
|
||||
return visibleKeys;
|
||||
}
|
||||
Set<String> memberKeys = new LinkedHashSet<>();
|
||||
for (DuplicateShop shop : view.payload().shops()) {
|
||||
for (String g : splitGroups(safe(shop.groupName()))) {
|
||||
if (g.equals(group)) {
|
||||
memberKeys.add(shopKey(shop.shopName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (visibleKeys == null) {
|
||||
log.info("[duplicate-check] 分组筛选 group={} 命中成员店铺 {} 家", group, memberKeys.size());
|
||||
return memberKeys;
|
||||
}
|
||||
memberKeys.retainAll(visibleKeys);
|
||||
log.info("[duplicate-check] 分组筛选 group={} 与数据范围交集后成员店铺 {} 家", group, memberKeys.size());
|
||||
return memberKeys;
|
||||
}
|
||||
|
||||
/** 超管/null → 全量;否则保留可见店铺(shops 保持缓存序)。 */
|
||||
public List<DuplicateShop> cropShops(List<DuplicateShop> shops, Set<String> visibleKeys) {
|
||||
if (visibleKeys == null || shops == null) {
|
||||
@@ -177,6 +202,197 @@ public class ShopDataDuplicateCheckQueryService {
|
||||
"page", page, "page_size", pageSize, "scanned_at", "");
|
||||
}
|
||||
|
||||
/* ================= 控制台 & 台账(reference console 对齐的只读聚合) ================= */
|
||||
|
||||
private static final Set<String> LEDGER_SORT_KEYS =
|
||||
Set.of("asin", "brand", "store_count", "record_count", "earliest", "latest");
|
||||
/** 店铺 group_name 可能以分隔符连接多分组,统一按分隔符拆分。 */
|
||||
private static final String GROUP_SPLIT = "[、,,/;;]";
|
||||
|
||||
/** 台账单行聚合:{asin, brand, stores[], groups[], countries[], record_count, earliest, latest, occurrences}。 */
|
||||
private record LedgerRow(String asin, String brand, List<String> stores, List<String> groups,
|
||||
List<String> countries, int recordCount, String earliest, String latest,
|
||||
List<DuplicateOccurrence> occurrences) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部 ASIN 台账(含单店未撞款):筛选 + 可选列排序 + 分页。
|
||||
* sortKey 白名单外回退 earliest,sortDir 非 desc 视为 asc。
|
||||
*/
|
||||
public Map<String, Object> ledgerData(DuplicateScanView view, Set<String> visibleKeys,
|
||||
int page, int pageSize, String sortKey, String sortDir,
|
||||
Filters filters) {
|
||||
List<DuplicateItem> cropped = cropItems(view.payload().items(), visibleKeys);
|
||||
List<DuplicateItem> matched = filterItems(cropped, false, filters);
|
||||
String key = LEDGER_SORT_KEYS.contains(safe(sortKey).toLowerCase(Locale.ROOT))
|
||||
? safe(sortKey).toLowerCase(Locale.ROOT) : "earliest";
|
||||
boolean asc = !"desc".equals(safe(sortDir).trim().toLowerCase(Locale.ROOT));
|
||||
List<LedgerRow> rows = new ArrayList<>(matched.size());
|
||||
for (DuplicateItem item : matched) {
|
||||
rows.add(ledgerRow(item));
|
||||
}
|
||||
rows.sort((a, b) -> ledgerCompare(a, b, key, asc));
|
||||
int total = rows.size();
|
||||
int from = Math.min((page - 1) * pageSize, total);
|
||||
int to = Math.min(from + pageSize, total);
|
||||
List<Map<String, Object>> slice = new ArrayList<>();
|
||||
for (int i = from; i < to; i++) {
|
||||
slice.add(ledgerMap(rows.get(i)));
|
||||
}
|
||||
return mapOf("pending", false, "items", slice, "total", total,
|
||||
"page", page, "page_size", pageSize, "scanned_at", safe(view.scannedAt()));
|
||||
}
|
||||
|
||||
public Map<String, Object> pendingLedger(int page, int pageSize) {
|
||||
return mapOf("pending", true, "items", Collections.emptyList(), "total", 0,
|
||||
"page", page, "page_size", pageSize, "scanned_at", "");
|
||||
}
|
||||
|
||||
/** 撞款控制台一次快照:summary/shops(全局可见口径) + 命中筛选的撞款全集 + 分组统计。 */
|
||||
public Map<String, Object> consoleData(DuplicateScanView view, Set<String> visibleKeys, Filters filters) {
|
||||
List<DuplicateShop> shops = cropShops(view.payload().shops(), visibleKeys);
|
||||
List<DuplicateItem> cropped = cropItems(view.payload().items(), visibleKeys);
|
||||
String source = view.summary() == null ? "" : safe(view.summary().source());
|
||||
DuplicateScanSummary summary = recomputeSummary(cropped, shops, source);
|
||||
List<DuplicateItem> dup = filterItems(cropped, true, filters);
|
||||
List<Map<String, Object>> groups = groupStats(shops, filterItems(cropped, false, filters));
|
||||
return mapOf("pending", false, "scanned_at", safe(view.scannedAt()), "summary", summary,
|
||||
"shops", shops, "dup", dup, "groups", groups, "total_dup", dup.size());
|
||||
}
|
||||
|
||||
public Map<String, Object> pendingConsole() {
|
||||
return mapOf("pending", true, "scanned_at", "", "summary", Collections.emptyMap(),
|
||||
"shops", Collections.emptyList(), "dup", Collections.emptyList(),
|
||||
"groups", Collections.emptyList(), "total_dup", 0);
|
||||
}
|
||||
|
||||
/** 单条台账行:品牌取首个非空;店铺/分组/站点保持出现顺序去重;最早/最近上架取归一化时间。 */
|
||||
private LedgerRow ledgerRow(DuplicateItem item) {
|
||||
LinkedHashSet<String> stores = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> groups = new LinkedHashSet<>();
|
||||
LinkedHashSet<String> countries = new LinkedHashSet<>();
|
||||
String brand = "";
|
||||
String earliest = "";
|
||||
String latest = "";
|
||||
for (DuplicateOccurrence occ : item.occurrences()) {
|
||||
if (!safe(occ.shopName()).isEmpty()) {
|
||||
stores.add(occ.shopName());
|
||||
}
|
||||
for (String group : splitGroups(safe(occ.groupName()))) {
|
||||
groups.add(group);
|
||||
}
|
||||
String country = safe(occ.country()).trim().toUpperCase(Locale.ROOT);
|
||||
if (!country.isEmpty()) {
|
||||
countries.add(country);
|
||||
}
|
||||
if (brand.isEmpty()) {
|
||||
brand = safe(occ.brand());
|
||||
}
|
||||
String normalized = DuplicateCheckTimeNormalizer.normalizeSortTime(safe(occ.date()));
|
||||
if (!normalized.isEmpty()) {
|
||||
if (earliest.isEmpty() || normalized.compareTo(earliest) < 0) {
|
||||
earliest = normalized;
|
||||
}
|
||||
if (latest.isEmpty() || normalized.compareTo(latest) > 0) {
|
||||
latest = normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new LedgerRow(item.asin(), brand, List.copyOf(stores), List.copyOf(groups),
|
||||
List.copyOf(countries), item.recordCount(), earliest, latest,
|
||||
new ArrayList<>(item.occurrences()));
|
||||
}
|
||||
|
||||
private Map<String, Object> ledgerMap(LedgerRow row) {
|
||||
return mapOf("asin", row.asin(), "brand", row.brand(), "store_count", row.stores().size(),
|
||||
"stores", row.stores(), "groups", row.groups(), "countries", row.countries(),
|
||||
"record_count", row.recordCount(), "earliest", row.earliest(), "latest", row.latest(),
|
||||
"occurrences", row.occurrences());
|
||||
}
|
||||
|
||||
private int ledgerCompare(LedgerRow a, LedgerRow b, String key, boolean asc) {
|
||||
int cmp;
|
||||
switch (key) {
|
||||
case "asin" -> cmp = safe(a.asin()).compareTo(safe(b.asin()));
|
||||
case "brand" -> cmp = safe(a.brand()).compareTo(safe(b.brand()));
|
||||
case "store_count" -> cmp = Integer.compare(a.stores().size(), b.stores().size());
|
||||
case "record_count" -> cmp = Integer.compare(a.recordCount(), b.recordCount());
|
||||
case "latest" -> cmp = safe(a.latest()).compareTo(safe(b.latest()));
|
||||
default -> cmp = safe(a.earliest()).compareTo(safe(b.earliest()));
|
||||
}
|
||||
return asc ? cmp : -cmp;
|
||||
}
|
||||
|
||||
/** 可见店铺按 group_name 拆分组;再在命中行上统计组内 唯一ASIN/上架记录/撞款ASIN。 */
|
||||
private List<Map<String, Object>> groupStats(List<DuplicateShop> shops, List<DuplicateItem> matched) {
|
||||
LinkedHashMap<String, Set<String>> members = new LinkedHashMap<>();
|
||||
for (DuplicateShop shop : shops) {
|
||||
String display = shop.shopName() == null ? "" : shop.shopName().trim();
|
||||
if (display.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
for (String group : splitGroups(safe(shop.groupName()))) {
|
||||
members.computeIfAbsent(group, k -> new LinkedHashSet<>()).add(shopKey(display));
|
||||
}
|
||||
}
|
||||
if (members.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> groupNames = new ArrayList<>(members.keySet());
|
||||
int[] asinUnique = new int[groupNames.size()];
|
||||
int[] recordCount = new int[groupNames.size()];
|
||||
int[] dupCount = new int[groupNames.size()];
|
||||
for (DuplicateItem item : matched) {
|
||||
Set<String> shopsOnItem = new LinkedHashSet<>();
|
||||
for (DuplicateOccurrence occ : item.occurrences()) {
|
||||
String key = shopKey(occ.shopName());
|
||||
if (!key.isEmpty()) {
|
||||
shopsOnItem.add(key);
|
||||
}
|
||||
}
|
||||
for (int gi = 0; gi < groupNames.size(); gi++) {
|
||||
Set<String> memberKeys = members.get(groupNames.get(gi));
|
||||
int inGroup = 0;
|
||||
for (String key : shopsOnItem) {
|
||||
if (memberKeys.contains(key)) {
|
||||
inGroup++;
|
||||
}
|
||||
}
|
||||
if (inGroup > 0) {
|
||||
asinUnique[gi]++;
|
||||
for (DuplicateOccurrence occ : item.occurrences()) {
|
||||
if (memberKeys.contains(shopKey(occ.shopName()))) {
|
||||
recordCount[gi]++;
|
||||
}
|
||||
}
|
||||
if (inGroup >= 2) {
|
||||
dupCount[gi]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Map<String, Object>> result = new ArrayList<>(groupNames.size());
|
||||
for (int gi = 0; gi < groupNames.size(); gi++) {
|
||||
result.add(mapOf("name", groupNames.get(gi), "shop_count", members.get(groupNames.get(gi)).size(),
|
||||
"asin_unique", asinUnique[gi], "record_count", recordCount[gi], "dup_count", dupCount[gi]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<String> splitGroups(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> out = new ArrayList<>();
|
||||
for (String token : value.split(GROUP_SPLIT)) {
|
||||
String group = token.trim();
|
||||
if (!group.isEmpty() && !out.contains(group)) {
|
||||
out.add(group);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public void writeExport(DuplicateScanView view, Set<String> visibleKeys,
|
||||
String viewMode, Filters filters, OutputStream out) throws IOException {
|
||||
boolean monitor = "monitor".equals(viewMode);
|
||||
|
||||
+119
@@ -137,6 +137,113 @@ class ShopDataDuplicateCheckQueryServiceTest {
|
||||
assertThat(list.get("scanned_at")).isEqualTo("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void consoleSuperAdminReturnsDupSetGroupsAndSummary() {
|
||||
Map<String, Object> data = service.consoleData(view, null, noFilter());
|
||||
assertThat(data.get("pending")).isEqualTo(false);
|
||||
assertThat(data.get("total_dup")).isEqualTo(3);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<DuplicateItem> dup = (List<DuplicateItem>) data.get("dup");
|
||||
assertThat(dup).extracting(DuplicateItem::asin)
|
||||
.containsExactlyInAnyOrder("A0000001", "E0000001", "F0000001");
|
||||
assertThat(dup).allSatisfy(item -> assertThat(item.shopCount()).isGreaterThanOrEqualTo(2));
|
||||
DuplicateScanSummary summary = (DuplicateScanSummary) data.get("summary");
|
||||
assertThat(summary.asinTotal()).isEqualTo(6);
|
||||
assertThat(summary.recordTotal()).isEqualTo(10);
|
||||
assertThat(summary.duplicateAsinTotal()).isEqualTo(3);
|
||||
assertThat(summary.siteCount()).isEqualTo(3);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) data.get("groups");
|
||||
assertThat(groups).extracting(map -> map.get("name"))
|
||||
.containsExactlyInAnyOrder("GroupA", "GroupC", "GroupD");
|
||||
Map<String, Object> groupA = groups.stream()
|
||||
.filter(map -> "GroupA".equals(map.get("name"))).findFirst().orElseThrow();
|
||||
assertThat(groupA.get("shop_count")).isEqualTo(2);
|
||||
assertThat(groupA.get("asin_unique")).isEqualTo(4);
|
||||
assertThat(groupA.get("record_count")).isEqualTo(6);
|
||||
assertThat(groupA.get("dup_count")).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void consoleLeaderCropRemovesCrossGroupDupAndKeepsOwnGroups() {
|
||||
Set<String> visible = Set.of("shopa", "shopb");
|
||||
Map<String, Object> data = service.consoleData(view, visible, noFilter());
|
||||
assertThat(data.get("total_dup")).isEqualTo(2);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<DuplicateItem> dup = (List<DuplicateItem>) data.get("dup");
|
||||
assertThat(dup).extracting(DuplicateItem::asin)
|
||||
.containsExactlyInAnyOrder("A0000001", "E0000001");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> groups = (List<Map<String, Object>>) data.get("groups");
|
||||
assertThat(groups).hasSize(1);
|
||||
assertThat(groups.get(0).get("name")).isEqualTo("GroupA");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<DuplicateShop> shops = (List<DuplicateShop>) data.get("shops");
|
||||
assertThat(shops).extracting(DuplicateShop::shopName).containsExactly("ShopA", "ShopB");
|
||||
}
|
||||
|
||||
@Test
|
||||
void consolePendingShapes() {
|
||||
Map<String, Object> data = service.pendingConsole();
|
||||
assertThat(data.get("pending")).isEqualTo(true);
|
||||
assertThat(data.get("dup")).isEqualTo(List.of());
|
||||
assertThat(data.get("groups")).isEqualTo(List.of());
|
||||
assertThat(data.get("summary")).isEqualTo(Map.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ledgerSingleAsinRowDerivesStoresGroupsCountriesAndDates() {
|
||||
ShopDataDuplicateCheckQueryService.Filters filters =
|
||||
ShopDataDuplicateCheckQueryService.Filters.clean("E0000001", "", "", "", "", "");
|
||||
Map<String, Object> data = service.ledgerData(view, null, 1, 200, "asin", "asc", filters);
|
||||
assertThat(data.get("total")).isEqualTo(1);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) data.get("items");
|
||||
Map<String, Object> e = items.get(0);
|
||||
assertThat(e.get("asin")).isEqualTo("E0000001");
|
||||
assertThat(e.get("brand")).isEqualTo("BrandE");
|
||||
assertThat(e.get("store_count")).isEqualTo(3);
|
||||
assertThat(e.get("stores")).isEqualTo(List.of("ShopA", "ShopB", "ShopC"));
|
||||
assertThat(e.get("groups")).isEqualTo(List.of("GroupA", "GroupC"));
|
||||
assertThat(e.get("countries")).isEqualTo(List.of("UK", "FR"));
|
||||
assertThat(e.get("record_count")).isEqualTo(3);
|
||||
assertThat(String.valueOf(e.get("earliest"))).startsWith("2026-08-29");
|
||||
assertThat(String.valueOf(e.get("latest"))).startsWith("2026-08-31");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ledgerStoreCountDescPutsThreeShopAsinFirst() {
|
||||
Map<String, Object> data = service.ledgerData(view, null, 1, 200, "store_count", "desc", noFilter());
|
||||
assertThat(data.get("total")).isEqualTo(6);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) data.get("items");
|
||||
assertThat(items.get(0).get("asin")).isEqualTo("E0000001");
|
||||
assertThat(items.get(0).get("store_count")).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ledgerLeaderCropCountsOnlyVisibleStores() {
|
||||
Set<String> visible = Set.of("shopa", "shopb");
|
||||
ShopDataDuplicateCheckQueryService.Filters filters =
|
||||
ShopDataDuplicateCheckQueryService.Filters.clean("E0000001", "", "", "", "", "");
|
||||
Map<String, Object> data = service.ledgerData(view, visible, 1, 200, "", "", filters);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) data.get("items");
|
||||
assertThat(items).hasSize(1);
|
||||
assertThat(items.get(0).get("store_count")).isEqualTo(2);
|
||||
assertThat(items.get(0).get("stores")).isEqualTo(List.of("ShopA", "ShopB"));
|
||||
assertThat(items.get(0).get("record_count")).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ledgerPendingShapes() {
|
||||
Map<String, Object> data = service.pendingLedger(2, 100);
|
||||
assertThat(data.get("pending")).isEqualTo(true);
|
||||
assertThat(data.get("total")).isEqualTo(0);
|
||||
assertThat(data.get("page")).isEqualTo(2);
|
||||
assertThat(data.get("page_size")).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportCsvBomHeaderAndMonitorRows() throws Exception {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
@@ -157,4 +264,16 @@ class ShopDataDuplicateCheckQueryServiceTest {
|
||||
assertThat(text).contains(",法国,");
|
||||
assertThat(lines[1]).doesNotContain(",UK,");
|
||||
}
|
||||
|
||||
@Test
|
||||
void groupFilterNarrowsToGroupMembers() {
|
||||
// 分组筛选:GroupA 只保留 ShopA/ShopB 键。
|
||||
Set<String> keys = service.withGroupFilter(null, view, "GroupA");
|
||||
assertThat(keys).containsExactlyInAnyOrder("shopa", "shopb");
|
||||
// 与数据范围交集:可见集只剩 ShopA 时,交集只剩 ShopA。
|
||||
Set<String> cropped = service.withGroupFilter(Set.of("shopa"), view, "GroupA");
|
||||
assertThat(cropped).containsExactly("shopa");
|
||||
// 空分组名不裁剪。
|
||||
assertThat(service.withGroupFilter(null, view, " ")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user