修复管家婆网点名称映射
This commit is contained in:
parent
b44c2805b9
commit
53b52995df
@ -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<String, String> namesById() {
|
||||
@ -27,9 +42,43 @@ public class NgpStoreDirectory {
|
||||
synchronized (refreshLock) {
|
||||
current = snapshot;
|
||||
if (current.fresh(now)) return current.namesById();
|
||||
Map<String, String> loaded = loadAll();
|
||||
snapshot = new Snapshot(loaded, now);
|
||||
return loaded;
|
||||
Optional<NgpStoreDirectoryCacheRepository.CachedDirectory> 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<String, String> 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<NgpStoreDirectoryCacheRepository.CachedDirectory> 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<String, String> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<CachedDirectory> findAll() {
|
||||
Map<String, String> namesById = new LinkedHashMap<>();
|
||||
List<LocalDateTime> 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<String, String> 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<Map.Entry<String, String>> 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<String, String> row) -> {
|
||||
statement.setString(1, row.getKey());
|
||||
statement.setString(2, row.getValue());
|
||||
statement.setTimestamp(3, refreshedAt);
|
||||
});
|
||||
}
|
||||
|
||||
public record CachedDirectory(Map<String, String> namesById, long loadedAtMillis) {
|
||||
boolean fresh(long now, long ttlMillis) {
|
||||
return !namesById.isEmpty() && now - loadedAtMillis <= ttlMillis;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<NgpMonthlyWeek> weeks = buildWeeks(month, today);
|
||||
List<NgpDailyStoreTotal> dailyTotals = repository.findDailyTotals(monthStart, dataEnd);
|
||||
List<NgpDailyStoreTotal> dailyTotals = storeNameResolver.canonicalize(
|
||||
repository.findDailyTotals(monthStart, dataEnd));
|
||||
List<String> requestedStoreNames = normalizeRequestedStoreNames(request);
|
||||
return buildResponse(month, today, weeks, dailyTotals, requestedStoreNames);
|
||||
}
|
||||
|
||||
@ -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<String, NgpStoreTotal> canonicalize(Map<String, NgpStoreTotal> totals) {
|
||||
if (totals.values().stream().noneMatch(row -> unresolved(row.storeName()))) return totals;
|
||||
Map<String, String> namesById = storeDirectory.namesById();
|
||||
Map<String, NgpStoreTotal> 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<NgpDailyStoreTotal> canonicalize(List<NgpDailyStoreTotal> totals) {
|
||||
if (totals.stream().noneMatch(row -> unresolved(row.storeName()))) return totals;
|
||||
Map<String, String> namesById = storeDirectory.namesById();
|
||||
List<NgpDailyStoreTotal> 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<String, String> 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,}");
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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<String, NgpStoreTotal> totals = repository.aggregate(request.startDate(), request.endDate());
|
||||
Map<String, NgpStoreTotal> 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(
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<String, NgpStoreTotal> totals = resolver.canonicalize(Map.of(
|
||||
"1621085730283489767", new NgpStoreTotal("1621085730283489767", 12345L)));
|
||||
List<NgpDailyStoreTotal> 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));
|
||||
}
|
||||
}
|
||||
@ -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"}
|
||||
]}
|
||||
]}}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user