This commit is contained in:
tanglong 2026-09-09 10:27:02 +08:00
parent 533a0fd498
commit 07e0303067
7 changed files with 101 additions and 45 deletions

View File

@ -6,11 +6,17 @@
```json
{
"month": "2026-09"
"month": "2026-09",
"storeNames": [
"跃动内容电商(抖店)",
"跃动工厂(淘宝1)"
]
}
```
`month` 可省略,省略时使用 Asia/Shanghai 时区的当前月份。不能查询未来月份。
`storeNames` 可省略或传空数组,此时查询所有网店;传入后仅返回指定网店,并按传入顺序展示。
只查询一个网店时也可以传 `storeName` 字符串;该字段同样支持逗号或顿号分隔多个名称。
## 周区间
@ -34,9 +40,8 @@
- 缓存补齐期间返回 HTTP `202` 和 `retryAfterSeconds`。
- 缓存就绪后返回 HTTP `200`,不再查询 NGP 全量数据。
网点范围和显示顺序由 `foundation.ngp.monthly-store-names` 配置,目前包含 20 个网店。
即使某个配置网点当月
没有业绩,响应中仍保留该网点,金额返回 `null`,以便表格显示 `-`。
传入 `storeNames` 时,即使指定网店当月没有业绩,响应中仍保留该网店,金额返回
`null`,以便表格显示 `-`。不传时返回缓存中所有有数据的网店。
## 成功响应示例

View File

@ -310,7 +310,6 @@ public class FoundationProperties {
private int maxResponseBytes = 5 * 1024 * 1024;
private int currentCacheSeconds = 600;
private int historyCacheSeconds = 86400;
private List<String> monthlyStoreNames = new ArrayList<>();
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
@ -348,8 +347,6 @@ public class FoundationProperties {
public void setCurrentCacheSeconds(int currentCacheSeconds) { this.currentCacheSeconds = currentCacheSeconds; }
public int getHistoryCacheSeconds() { return historyCacheSeconds; }
public void setHistoryCacheSeconds(int historyCacheSeconds) { this.historyCacheSeconds = historyCacheSeconds; }
public List<String> getMonthlyStoreNames() { return monthlyStoreNames; }
public void setMonthlyStoreNames(List<String> monthlyStoreNames) { this.monthlyStoreNames = monthlyStoreNames; }
}
public static class Semantic {

View File

@ -1,4 +1,10 @@
package com.company.agentfoundation.ngp.monthly;
public record NgpMonthlyWeeklyRequest(String month) {
import java.util.List;
public record NgpMonthlyWeeklyRequest(
String month,
List<String> storeNames,
String storeName
) {
}

View File

@ -9,6 +9,7 @@ public record NgpMonthlyWeeklyResponse(
String month,
String monthLabel,
String title,
List<String> requestedStoreNames,
LocalDateTime generatedAt,
List<NgpMonthlyWeek> weeks,
List<NgpMonthlyStoreRow> stores,

View File

@ -21,6 +21,7 @@ import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -86,23 +87,20 @@ public class NgpMonthlyWeeklyService {
List<NgpMonthlyWeek> weeks = buildWeeks(month, today);
List<NgpDailyStoreTotal> dailyTotals = repository.findDailyTotals(monthStart, dataEnd);
return buildResponse(month, today, weeks, dailyTotals);
List<String> requestedStoreNames = normalizeRequestedStoreNames(request);
return buildResponse(month, today, weeks, dailyTotals, requestedStoreNames);
}
private NgpMonthlyWeeklyResponse buildResponse(
YearMonth month,
LocalDate today,
List<NgpMonthlyWeek> weeks,
List<NgpDailyStoreTotal> dailyTotals
List<NgpDailyStoreTotal> dailyTotals,
List<String> requestedStoreNames
) {
Map<String, StoreAccumulator> stores = new LinkedHashMap<>();
List<String> configuredStoreNames = config.getMonthlyStoreNames() == null
? List.of()
: config.getMonthlyStoreNames().stream().filter(name -> name != null && !name.isBlank()).toList();
boolean restrictStores = !configuredStoreNames.isEmpty();
configuredStoreNames.forEach(storeName -> stores.put(
NgpPerformanceDayLoader.normalize(storeName),
new StoreAccumulator(storeName.trim(), weeks.size())));
boolean restrictStores = !requestedStoreNames.isEmpty();
if (restrictStores) initializeRequestedStores(stores, requestedStoreNames, dailyTotals, weeks.size());
long[] weekTotals = new long[weeks.size()];
for (NgpDailyStoreTotal daily : dailyTotals) {
int weekIndex = weekIndex(weeks, daily.businessDate());
@ -136,6 +134,7 @@ public class NgpMonthlyWeeklyService {
month.toString(),
monthLabel,
month.getYear() + "年" + monthLabel + "业绩周统计",
requestedStoreNames,
LocalDateTime.now(clock),
weeks,
rows,
@ -146,6 +145,55 @@ public class NgpMonthlyWeeklyService {
);
}
private void initializeRequestedStores(
Map<String, StoreAccumulator> stores,
List<String> requestedStoreNames,
List<NgpDailyStoreTotal> dailyTotals,
int weekCount
) {
Map<String, String> availableStores = new LinkedHashMap<>();
dailyTotals.forEach(row -> availableStores.putIfAbsent(
NgpPerformanceDayLoader.normalize(row.storeKey()), row.storeName()));
for (String requested : requestedStoreNames) {
String normalized = NgpPerformanceDayLoader.normalize(requested);
String selectedKey = normalized;
String selectedName = availableStores.get(normalized);
if (selectedName == null) {
List<Map.Entry<String, String>> partial = availableStores.entrySet().stream()
.filter(entry -> entry.getKey().contains(normalized) || normalized.contains(entry.getKey()))
.toList();
if (partial.size() == 1) {
selectedKey = partial.get(0).getKey();
selectedName = partial.get(0).getValue();
} else if (partial.size() > 1) {
String candidates = partial.stream().limit(20).map(Map.Entry::getValue)
.reduce((left, right) -> left + "、" + right).orElse("");
throw new IllegalArgumentException(
"网店名称“" + requested + "”匹配多个结果,请使用完整名称:" + candidates);
}
}
stores.putIfAbsent(
selectedKey,
new StoreAccumulator(selectedName == null ? requested : selectedName, weekCount));
}
}
private List<String> normalizeRequestedStoreNames(NgpMonthlyWeeklyRequest request) {
if (request == null) return List.of();
LinkedHashSet<String> unique = new LinkedHashSet<>();
if (request.storeNames() != null) {
for (String storeName : request.storeNames()) {
if (storeName != null && !storeName.isBlank()) unique.add(storeName.trim());
}
}
if (request.storeName() != null) {
for (String storeName : request.storeName().split("[,,、]")) {
if (!storeName.isBlank()) unique.add(storeName.trim());
}
}
return List.copyOf(unique);
}
private List<NgpMonthlyWeek> buildWeeks(YearMonth month, LocalDate today) {
List<NgpMonthlyWeek> result = new ArrayList<>();
LocalDate cursor = month.atDay(1);

View File

@ -149,27 +149,6 @@ foundation:
max-response-bytes: 5242880
current-cache-seconds: 600
history-cache-seconds: 86400
monthly-store-names:
- 跃动内容电商(小红书2店)
- 跃动内容电商(微信小店)
- 跃动内容电商(抖店)
- 跃动内容电商(小红书)
- 跃动内容电商(快手)
- 跃动工厂(拼多多4)
- 跃动工厂(淘宝1)
- 跃动工厂(淘宝2)
- 跃动工厂(天猫店)
- 跃动工厂(天猫2)
- 跃动工厂(京东pop)
- 跃动工厂(拼多多3)
- 跃动工厂(抖店2)
- 跃动工厂(快手)
- 跃动工厂(微信小店1)
- 跃动工厂(小红书1)
- 跃动工厂(拼多多2)
- 跃动工厂(抖店1)
- 跃动工厂(拼多多1)
- 跃动工厂(拼多多5)
semantic:
agent-id: crm-assistant
minimum-confidence: 0.60

View File

@ -42,7 +42,7 @@ class NgpMonthlyWeeklyServiceTest {
void currentMonthOnlyRequiresDatesThroughToday() {
when(repository.findRefreshTime(any())).thenReturn(Optional.empty());
NgpMonthlyWeeklyResult result = service.query(new NgpMonthlyWeeklyRequest(null));
NgpMonthlyWeeklyResult result = service.query(new NgpMonthlyWeeklyRequest(null, null, null));
assertThat(result).isInstanceOf(NgpMonthlyWeeklyPendingResponse.class);
assertThat(((NgpMonthlyWeeklyPendingResponse) result).pendingDays()).isEqualTo(9);
@ -65,7 +65,7 @@ class NgpMonthlyWeeklyServiceTest {
new NgpDailyStoreTotal(LocalDate.of(2026, 9, 9), "b", "网点B", 50000)
));
NgpMonthlyWeeklyResult result = service.query(new NgpMonthlyWeeklyRequest("2026-09"));
NgpMonthlyWeeklyResult result = service.query(new NgpMonthlyWeeklyRequest("2026-09", null, null));
assertThat(result).isInstanceOf(NgpMonthlyWeeklyResponse.class);
NgpMonthlyWeeklyResponse response = (NgpMonthlyWeeklyResponse) result;
@ -80,10 +80,7 @@ class NgpMonthlyWeeklyServiceTest {
}
@Test
void keepsConfiguredStoreOrderAndExcludesOtherStores() {
FoundationProperties properties = new FoundationProperties();
properties.getNgp().setMonthlyStoreNames(List.of("网点B", "网点A", "无数据网点"));
service = new NgpMonthlyWeeklyService(repository, coordinator, properties, CLOCK);
void keepsRequestedStoreOrderAndExcludesOtherStores() {
when(repository.findRefreshTime(any())).thenReturn(Optional.of(LocalDateTime.of(2026, 9, 9, 11, 59)));
when(repository.findDailyTotals(any(), any())).thenReturn(List.of(
new NgpDailyStoreTotal(LocalDate.of(2026, 9, 1), "网点a", "网点A", 10000),
@ -91,7 +88,7 @@ class NgpMonthlyWeeklyServiceTest {
));
NgpMonthlyWeeklyResponse response = (NgpMonthlyWeeklyResponse) service.query(
new NgpMonthlyWeeklyRequest("2026-09"));
new NgpMonthlyWeeklyRequest("2026-09", List.of("网点B", "网点A", "无数据网点"), null));
assertThat(response.stores()).extracting(NgpMonthlyStoreRow::storeName)
.containsExactly("网点B", "网点A", "无数据网点");
@ -99,5 +96,28 @@ class NgpMonthlyWeeklyServiceTest {
assertThat(response.stores().get(1).subtotal()).isEqualByComparingTo("100.00");
assertThat(response.stores().get(2).subtotal()).isNull();
assertThat(response.grandTotal()).isEqualByComparingTo("100.00");
assertThat(response.requestedStoreNames()).containsExactly("网点B", "网点A", "无数据网点");
}
@Test
void acceptsCommaSeparatedStoreNameAndQueriesAllWhenItIsBlank() {
when(repository.findRefreshTime(any())).thenReturn(Optional.of(LocalDateTime.of(2026, 9, 9, 11, 59)));
when(repository.findDailyTotals(any(), any())).thenReturn(List.of(
new NgpDailyStoreTotal(LocalDate.of(2026, 9, 1), "网点a", "网点A", 10000),
new NgpDailyStoreTotal(LocalDate.of(2026, 9, 1), "网点b", "网点B", 20000),
new NgpDailyStoreTotal(LocalDate.of(2026, 9, 1), "网点c", "网点C", 30000)
));
NgpMonthlyWeeklyResponse filtered = (NgpMonthlyWeeklyResponse) service.query(
new NgpMonthlyWeeklyRequest("2026-09", null, "网点B、网点A"));
assertThat(filtered.stores()).extracting(NgpMonthlyStoreRow::storeName)
.containsExactly("网点B", "网点A");
assertThat(filtered.grandTotal()).isEqualByComparingTo("300.00");
NgpMonthlyWeeklyResponse all = (NgpMonthlyWeeklyResponse) service.query(
new NgpMonthlyWeeklyRequest("2026-09", null, " "));
assertThat(all.stores()).extracting(NgpMonthlyStoreRow::storeName)
.containsExactly("网点A", "网点B", "网点C");
assertThat(all.grandTotal()).isEqualByComparingTo("600.00");
}
}