From 53b52995df4e7d49a7fab0deb59d96ae32902433 Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Thu, 24 Sep 2026 10:55:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AE=A1=E5=AE=B6=E5=A9=86?= =?UTF-8?q?=E7=BD=91=E7=82=B9=E5=90=8D=E7=A7=B0=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../integration/ngp/NgpStoreDirectory.java | 57 ++++++++++++- .../ngp/NgpStoreDirectoryCacheRepository.java | 82 +++++++++++++++++++ .../ngp/monthly/NgpMonthlyWeeklyService.java | 10 ++- .../NgpCachedStoreNameResolver.java | 60 ++++++++++++++ .../performance/NgpPerformanceDayLoader.java | 6 +- .../NgpPerformanceTotalService.java | 6 +- .../monthly/NgpMonthlyWeeklyServiceTest.java | 7 +- .../NgpCachedStoreNameResolverTest.java | 37 +++++++++ .../NgpPerformanceDayLoaderTest.java | 2 +- .../NgpPerformanceTotalServiceTest.java | 4 +- 10 files changed, 260 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/company/agentfoundation/integration/ngp/NgpStoreDirectoryCacheRepository.java create mode 100644 src/main/java/com/company/agentfoundation/ngp/performance/NgpCachedStoreNameResolver.java create mode 100644 src/test/java/com/company/agentfoundation/ngp/performance/NgpCachedStoreNameResolverTest.java diff --git a/src/main/java/com/company/agentfoundation/integration/ngp/NgpStoreDirectory.java b/src/main/java/com/company/agentfoundation/integration/ngp/NgpStoreDirectory.java index d172640..ae73618 100644 --- a/src/main/java/com/company/agentfoundation/integration/ngp/NgpStoreDirectory.java +++ b/src/main/java/com/company/agentfoundation/integration/ngp/NgpStoreDirectory.java @@ -1,23 +1,38 @@ package com.company.agentfoundation.integration.ngp; import com.fasterxml.jackson.databind.JsonNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; @Component public class NgpStoreDirectory { + private static final Logger log = LoggerFactory.getLogger(NgpStoreDirectory.class); private static final long CACHE_MILLIS = Duration.ofHours(1).toMillis(); private static final int MAX_PAGES = 1_000; private final NgpApiClient apiClient; + private final NgpStoreDirectoryCacheRepository cacheRepository; private final Object refreshLock = new Object(); private volatile Snapshot snapshot = new Snapshot(Map.of(), 0L); - public NgpStoreDirectory(NgpApiClient apiClient) { + @Autowired + public NgpStoreDirectory( + NgpApiClient apiClient, + NgpStoreDirectoryCacheRepository cacheRepository + ) { this.apiClient = apiClient; + this.cacheRepository = cacheRepository; + } + + NgpStoreDirectory(NgpApiClient apiClient) { + this(apiClient, null); } public Map namesById() { @@ -27,9 +42,43 @@ public class NgpStoreDirectory { synchronized (refreshLock) { current = snapshot; if (current.fresh(now)) return current.namesById(); - Map loaded = loadAll(); - snapshot = new Snapshot(loaded, now); - return loaded; + Optional persisted = persistedDirectory(); + if (persisted.isPresent() && persisted.get().fresh(now, CACHE_MILLIS)) { + snapshot = new Snapshot(persisted.get().namesById(), persisted.get().loadedAtMillis()); + return snapshot.namesById(); + } + try { + Map loaded = loadAll(); + snapshot = new Snapshot(loaded, now); + persist(loaded, now); + return loaded; + } catch (RuntimeException exception) { + if (persisted.isPresent() && !persisted.get().namesById().isEmpty()) { + log.warn("Serving stale NGP store directory after refresh failed: {}", exception.getMessage()); + snapshot = new Snapshot(persisted.get().namesById(), now); + return snapshot.namesById(); + } + throw exception; + } + } + } + + private Optional persistedDirectory() { + if (cacheRepository == null) return Optional.empty(); + try { + return cacheRepository.findAll(); + } catch (RuntimeException exception) { + log.warn("Unable to read persisted NGP store directory: {}", exception.getMessage()); + return Optional.empty(); + } + } + + private void persist(Map namesById, long loadedAtMillis) { + if (cacheRepository == null) return; + try { + cacheRepository.replace(namesById, loadedAtMillis); + } catch (RuntimeException exception) { + log.warn("Unable to persist NGP store directory: {}", exception.getMessage()); } } diff --git a/src/main/java/com/company/agentfoundation/integration/ngp/NgpStoreDirectoryCacheRepository.java b/src/main/java/com/company/agentfoundation/integration/ngp/NgpStoreDirectoryCacheRepository.java new file mode 100644 index 0000000..d1160b5 --- /dev/null +++ b/src/main/java/com/company/agentfoundation/integration/ngp/NgpStoreDirectoryCacheRepository.java @@ -0,0 +1,82 @@ +package com.company.agentfoundation.integration.ngp; + +import jakarta.annotation.PostConstruct; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Repository +public class NgpStoreDirectoryCacheRepository { + private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai"); + + private final JdbcTemplate jdbcTemplate; + + public NgpStoreDirectoryCacheRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @PostConstruct + public void initializeSchema() { + jdbcTemplate.execute(""" + CREATE TABLE IF NOT EXISTS ngp_store_directory_cache ( + store_id VARCHAR(191) NOT NULL PRIMARY KEY, + store_name VARCHAR(255) NOT NULL, + refreshed_at DATETIME(3) NOT NULL, + KEY idx_ngp_store_directory_refreshed_at (refreshed_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + """); + } + + public Optional findAll() { + Map namesById = new LinkedHashMap<>(); + List refreshTimes = jdbcTemplate.query(""" + SELECT store_id, store_name, refreshed_at + FROM ngp_store_directory_cache + ORDER BY store_id + """, + (resultSet, rowNum) -> { + namesById.put(resultSet.getString("store_id"), resultSet.getString("store_name")); + return resultSet.getTimestamp("refreshed_at").toLocalDateTime(); + }); + if (namesById.isEmpty() || refreshTimes.isEmpty()) return Optional.empty(); + LocalDateTime newest = refreshTimes.stream().max(LocalDateTime::compareTo).orElseThrow(); + long loadedAtMillis = newest.atZone(BUSINESS_ZONE).toInstant().toEpochMilli(); + return Optional.of(new CachedDirectory(Map.copyOf(namesById), loadedAtMillis)); + } + + @Transactional + public void replace(Map namesById, long loadedAtMillis) { + jdbcTemplate.update("DELETE FROM ngp_store_directory_cache"); + if (namesById.isEmpty()) return; + Timestamp refreshedAt = Timestamp.valueOf(LocalDateTime.ofInstant( + Instant.ofEpochMilli(loadedAtMillis), BUSINESS_ZONE)); + List> rows = List.copyOf(namesById.entrySet()); + jdbcTemplate.batchUpdate(""" + INSERT INTO ngp_store_directory_cache (store_id, store_name, refreshed_at) + VALUES (?, ?, ?) + """, + rows, + rows.size(), + (PreparedStatement statement, Map.Entry row) -> { + statement.setString(1, row.getKey()); + statement.setString(2, row.getValue()); + statement.setTimestamp(3, refreshedAt); + }); + } + + public record CachedDirectory(Map namesById, long loadedAtMillis) { + boolean fresh(long now, long ttlMillis) { + return !namesById.isEmpty() && now - loadedAtMillis <= ttlMillis; + } + } +} diff --git a/src/main/java/com/company/agentfoundation/ngp/monthly/NgpMonthlyWeeklyService.java b/src/main/java/com/company/agentfoundation/ngp/monthly/NgpMonthlyWeeklyService.java index 2310167..72f695f 100644 --- a/src/main/java/com/company/agentfoundation/ngp/monthly/NgpMonthlyWeeklyService.java +++ b/src/main/java/com/company/agentfoundation/ngp/monthly/NgpMonthlyWeeklyService.java @@ -3,6 +3,7 @@ package com.company.agentfoundation.ngp.monthly; import com.company.agentfoundation.config.FoundationProperties; import com.company.agentfoundation.ngp.performance.NgpDailyStoreTotal; import com.company.agentfoundation.ngp.performance.NgpPerformanceCacheRepository; +import com.company.agentfoundation.ngp.performance.NgpCachedStoreNameResolver; import com.company.agentfoundation.ngp.performance.NgpPerformanceDayLoader; import com.company.agentfoundation.ngp.performance.NgpPerformanceRefreshCoordinator; import org.slf4j.Logger; @@ -40,6 +41,7 @@ public class NgpMonthlyWeeklyService { private final NgpPerformanceCacheRepository repository; private final NgpPerformanceRefreshCoordinator refreshCoordinator; + private final NgpCachedStoreNameResolver storeNameResolver; private final FoundationProperties.Ngp config; private final Clock clock; @@ -47,19 +49,22 @@ public class NgpMonthlyWeeklyService { public NgpMonthlyWeeklyService( NgpPerformanceCacheRepository repository, NgpPerformanceRefreshCoordinator refreshCoordinator, + NgpCachedStoreNameResolver storeNameResolver, FoundationProperties properties ) { - this(repository, refreshCoordinator, properties, Clock.system(BUSINESS_ZONE)); + this(repository, refreshCoordinator, storeNameResolver, properties, Clock.system(BUSINESS_ZONE)); } NgpMonthlyWeeklyService( NgpPerformanceCacheRepository repository, NgpPerformanceRefreshCoordinator refreshCoordinator, + NgpCachedStoreNameResolver storeNameResolver, FoundationProperties properties, Clock clock ) { this.repository = repository; this.refreshCoordinator = refreshCoordinator; + this.storeNameResolver = storeNameResolver; this.config = properties.getNgp(); this.clock = clock; } @@ -100,7 +105,8 @@ public class NgpMonthlyWeeklyService { refreshStaleInBackground(staleDates); List weeks = buildWeeks(month, today); - List dailyTotals = repository.findDailyTotals(monthStart, dataEnd); + List dailyTotals = storeNameResolver.canonicalize( + repository.findDailyTotals(monthStart, dataEnd)); List requestedStoreNames = normalizeRequestedStoreNames(request); return buildResponse(month, today, weeks, dailyTotals, requestedStoreNames); } diff --git a/src/main/java/com/company/agentfoundation/ngp/performance/NgpCachedStoreNameResolver.java b/src/main/java/com/company/agentfoundation/ngp/performance/NgpCachedStoreNameResolver.java new file mode 100644 index 0000000..d99c7fd --- /dev/null +++ b/src/main/java/com/company/agentfoundation/ngp/performance/NgpCachedStoreNameResolver.java @@ -0,0 +1,60 @@ +package com.company.agentfoundation.ngp.performance; + +import com.company.agentfoundation.integration.ngp.NgpStoreDirectory; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Component +public class NgpCachedStoreNameResolver { + private final NgpStoreDirectory storeDirectory; + + public NgpCachedStoreNameResolver(NgpStoreDirectory storeDirectory) { + this.storeDirectory = storeDirectory; + } + + public Map canonicalize(Map totals) { + if (totals.values().stream().noneMatch(row -> unresolved(row.storeName()))) return totals; + Map namesById = storeDirectory.namesById(); + Map resolved = new LinkedHashMap<>(); + totals.forEach((storeKey, row) -> { + String storeName = resolve(namesById, storeKey, row.storeName()); + String canonicalKey = NgpPerformanceDayLoader.normalize(storeName); + NgpStoreTotal current = resolved.get(canonicalKey); + long totalCents = row.totalCents() + (current == null ? 0L : current.totalCents()); + resolved.put(canonicalKey, new NgpStoreTotal(storeName, totalCents)); + }); + return Map.copyOf(resolved); + } + + public List canonicalize(List totals) { + if (totals.stream().noneMatch(row -> unresolved(row.storeName()))) return totals; + Map namesById = storeDirectory.namesById(); + List resolved = new ArrayList<>(totals.size()); + for (NgpDailyStoreTotal row : totals) { + String storeName = resolve(namesById, row.storeKey(), row.storeName()); + resolved.add(new NgpDailyStoreTotal( + row.businessDate(), + NgpPerformanceDayLoader.normalize(storeName), + storeName, + row.totalCents())); + } + return List.copyOf(resolved); + } + + private String resolve(Map namesById, String storeKey, String storeName) { + if (!unresolved(storeName)) return storeName; + String resolved = namesById.getOrDefault(storeName, ""); + if (resolved.isBlank()) resolved = namesById.getOrDefault(storeKey, ""); + return resolved.isBlank() ? storeName : resolved; + } + + private boolean unresolved(String storeName) { + if (storeName == null || storeName.isBlank()) return false; + String clean = storeName.trim(); + return clean.matches("\\d{6,}") || clean.matches("未命名网点\\s+\\d{6,}"); + } +} diff --git a/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoader.java b/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoader.java index 55000c1..3aaccc9 100644 --- a/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoader.java +++ b/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoader.java @@ -111,7 +111,11 @@ public class NgpPerformanceDayLoader { String storeName = firstText( bill, "eshopFullname", "eshopFullName", "eshopName", "ofullname", "oFullName", "storeName", "shopName"); - if (storeName.isBlank() && !storeId.isBlank()) storeName = storeNamesById.getOrDefault(storeId, ""); + String directoryName = storeNamesById.getOrDefault(storeId, ""); + if (directoryName.isBlank() && !storeName.isBlank()) { + directoryName = storeNamesById.getOrDefault(storeName, ""); + } + if (!directoryName.isBlank()) storeName = directoryName; if (storeName.isBlank()) { storeName = storeId.isBlank() ? "未命名网点" : "未命名网点 " + storeId; } diff --git a/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceTotalService.java b/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceTotalService.java index 5eec6a7..292aada 100644 --- a/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceTotalService.java +++ b/src/main/java/com/company/agentfoundation/ngp/performance/NgpPerformanceTotalService.java @@ -28,15 +28,18 @@ public class NgpPerformanceTotalService { private final NgpPerformanceCacheRepository repository; private final NgpPerformanceRefreshCoordinator refreshCoordinator; + private final NgpCachedStoreNameResolver storeNameResolver; private final FoundationProperties.Ngp config; public NgpPerformanceTotalService( NgpPerformanceCacheRepository repository, NgpPerformanceRefreshCoordinator refreshCoordinator, + NgpCachedStoreNameResolver storeNameResolver, FoundationProperties properties ) { this.repository = repository; this.refreshCoordinator = refreshCoordinator; + this.storeNameResolver = storeNameResolver; this.config = properties.getNgp(); } @@ -68,7 +71,8 @@ public class NgpPerformanceTotalService { refreshStaleInBackground(staleDates); String storeInput = request.storeName() == null ? "" : request.storeName().trim(); - Map totals = repository.aggregate(request.startDate(), request.endDate()); + Map totals = storeNameResolver.canonicalize( + repository.aggregate(request.startDate(), request.endDate())); long totalCents = selectStores(totals, storeInput).stream().mapToLong(NgpStoreTotal::totalCents).sum(); BigDecimal totalAmount = BigDecimal.valueOf(totalCents, 2).setScale(2, RoundingMode.UNNECESSARY); return new NgpPerformanceTotalResponse( diff --git a/src/test/java/com/company/agentfoundation/ngp/monthly/NgpMonthlyWeeklyServiceTest.java b/src/test/java/com/company/agentfoundation/ngp/monthly/NgpMonthlyWeeklyServiceTest.java index 6d5b915..7bd4c4d 100644 --- a/src/test/java/com/company/agentfoundation/ngp/monthly/NgpMonthlyWeeklyServiceTest.java +++ b/src/test/java/com/company/agentfoundation/ngp/monthly/NgpMonthlyWeeklyServiceTest.java @@ -1,6 +1,8 @@ package com.company.agentfoundation.ngp.monthly; import com.company.agentfoundation.config.FoundationProperties; +import com.company.agentfoundation.integration.ngp.NgpStoreDirectory; +import com.company.agentfoundation.ngp.performance.NgpCachedStoreNameResolver; import com.company.agentfoundation.ngp.performance.NgpDailyStoreTotal; import com.company.agentfoundation.ngp.performance.NgpPerformanceCacheRepository; import com.company.agentfoundation.ngp.performance.NgpPerformanceRefreshCoordinator; @@ -36,7 +38,9 @@ class NgpMonthlyWeeklyServiceTest { void setUp() { repository = mock(NgpPerformanceCacheRepository.class); coordinator = mock(NgpPerformanceRefreshCoordinator.class); - service = new NgpMonthlyWeeklyService(repository, coordinator, new FoundationProperties(), CLOCK); + NgpCachedStoreNameResolver resolver = new NgpCachedStoreNameResolver(mock(NgpStoreDirectory.class)); + service = new NgpMonthlyWeeklyService( + repository, coordinator, resolver, new FoundationProperties(), CLOCK); } @Test @@ -143,6 +147,7 @@ class NgpMonthlyWeeklyServiceTest { try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { context.registerBean(NgpPerformanceCacheRepository.class, () -> mock(NgpPerformanceCacheRepository.class)); context.registerBean(NgpPerformanceRefreshCoordinator.class, () -> mock(NgpPerformanceRefreshCoordinator.class)); + context.registerBean(NgpCachedStoreNameResolver.class, () -> mock(NgpCachedStoreNameResolver.class)); context.registerBean(FoundationProperties.class, FoundationProperties::new); context.register(NgpMonthlyWeeklyService.class); context.refresh(); diff --git a/src/test/java/com/company/agentfoundation/ngp/performance/NgpCachedStoreNameResolverTest.java b/src/test/java/com/company/agentfoundation/ngp/performance/NgpCachedStoreNameResolverTest.java new file mode 100644 index 0000000..6992a57 --- /dev/null +++ b/src/test/java/com/company/agentfoundation/ngp/performance/NgpCachedStoreNameResolverTest.java @@ -0,0 +1,37 @@ +package com.company.agentfoundation.ngp.performance; + +import com.company.agentfoundation.integration.ngp.NgpStoreDirectory; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class NgpCachedStoreNameResolverTest { + @Test + void resolvesNumericStoreNamesInExistingTotalsAndDailyRows() { + NgpStoreDirectory directory = mock(NgpStoreDirectory.class); + when(directory.namesById()).thenReturn(Map.of( + "1621085730283489767", "跃动内容电商(抖店)")); + NgpCachedStoreNameResolver resolver = new NgpCachedStoreNameResolver(directory); + + Map totals = resolver.canonicalize(Map.of( + "1621085730283489767", new NgpStoreTotal("1621085730283489767", 12345L))); + List daily = resolver.canonicalize(List.of( + new NgpDailyStoreTotal( + LocalDate.of(2026, 9, 1), + "1621085730283489767", + "1621085730283489767", + 12345L))); + + String expectedKey = NgpPerformanceDayLoader.normalize("跃动内容电商(抖店)"); + assertThat(totals).containsEntry( + expectedKey, new NgpStoreTotal("跃动内容电商(抖店)", 12345L)); + assertThat(daily).containsExactly(new NgpDailyStoreTotal( + LocalDate.of(2026, 9, 1), expectedKey, "跃动内容电商(抖店)", 12345L)); + } +} diff --git a/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoaderTest.java b/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoaderTest.java index 6057778..53b439e 100644 --- a/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoaderTest.java +++ b/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceDayLoaderTest.java @@ -67,7 +67,7 @@ class NgpPerformanceDayLoaderTest { {"taskId":"B","otypeId":"101","detailList":[ {"refundState":4,"disedTaxedTotal":"10.00"} ]}, - {"taskId":"C","otypeId":"102","detailList":[ + {"taskId":"C","otypeId":"102","eshopFullname":"102","detailList":[ {"refundState":4,"disedTaxedTotal":"-2.00"} ]} ]}} diff --git a/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceTotalServiceTest.java b/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceTotalServiceTest.java index c199b3b..3bccc82 100644 --- a/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceTotalServiceTest.java +++ b/src/test/java/com/company/agentfoundation/ngp/performance/NgpPerformanceTotalServiceTest.java @@ -1,6 +1,7 @@ package com.company.agentfoundation.ngp.performance; import com.company.agentfoundation.config.FoundationProperties; +import com.company.agentfoundation.integration.ngp.NgpStoreDirectory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,7 +26,8 @@ class NgpPerformanceTotalServiceTest { repository = mock(NgpPerformanceCacheRepository.class); coordinator = mock(NgpPerformanceRefreshCoordinator.class); FoundationProperties properties = new FoundationProperties(); - service = new NgpPerformanceTotalService(repository, coordinator, properties); + NgpCachedStoreNameResolver resolver = new NgpCachedStoreNameResolver(mock(NgpStoreDirectory.class)); + service = new NgpPerformanceTotalService(repository, coordinator, resolver, properties); } @Test