周汇总
This commit is contained in:
parent
5e8d919f0d
commit
5217a3e96c
69
docs/ngp-monthly-weekly-api.md
Normal file
69
docs/ngp-monthly-weekly-api.md
Normal file
@ -0,0 +1,69 @@
|
||||
# NGP 本月每周业绩统计接口
|
||||
|
||||
## 请求
|
||||
|
||||
`POST /api/ngp/performance/monthly-weekly`
|
||||
|
||||
```json
|
||||
{
|
||||
"month": "2026-09"
|
||||
}
|
||||
```
|
||||
|
||||
`month` 可省略,省略时使用 Asia/Shanghai 时区的当前月份。不能查询未来月份。
|
||||
|
||||
## 周区间
|
||||
|
||||
按周一至周日切分,月初和月末使用当月实际边界。例如 2026 年 9 月:
|
||||
|
||||
- `9/1-9/6`
|
||||
- `9/7-9/13`
|
||||
- `9/14-9/20`
|
||||
- `9/21-9/27`
|
||||
- `9/28-9/30`
|
||||
|
||||
当前月份只读取截至今天的日期,未来周金额返回 `null`。
|
||||
|
||||
## 缓存
|
||||
|
||||
接口复用 `ngp_performance_daily_total` 和 `ngp_performance_daily_status`:
|
||||
|
||||
- 已缓存且未过期的日期直接从 MySQL 读取。
|
||||
- 只为缺失或过期的日期启动后台 NGP 查询。
|
||||
- 同一日期同时只运行一个刷新任务。
|
||||
- 缓存补齐期间返回 HTTP `202` 和 `retryAfterSeconds`。
|
||||
- 缓存就绪后返回 HTTP `200`,不再查询 NGP 全量数据。
|
||||
|
||||
网点范围和显示顺序由 `foundation.ngp.monthly-store-names` 配置。即使某个配置网点当月
|
||||
没有业绩,响应中仍保留该网点,金额返回 `null`,以便表格显示 `-`。
|
||||
|
||||
## 成功响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"month": "2026-09",
|
||||
"monthLabel": "九月",
|
||||
"title": "2026年九月业绩周统计",
|
||||
"weeks": [
|
||||
{
|
||||
"index": 0,
|
||||
"startDate": "2026-09-01",
|
||||
"endDate": "2026-09-06",
|
||||
"label": "9/1-9/6",
|
||||
"complete": true
|
||||
}
|
||||
],
|
||||
"stores": [
|
||||
{
|
||||
"storeName": "跃动内容电商(抖店)",
|
||||
"weeklyAmounts": [506482.76, null, null, null, null],
|
||||
"subtotal": 506482.76
|
||||
}
|
||||
],
|
||||
"weekTotals": [1691720.10, null, null, null, null],
|
||||
"grandTotal": 1691720.10,
|
||||
"formattedGrandTotal": "1,691,720.10",
|
||||
"currency": "CNY"
|
||||
}
|
||||
```
|
||||
@ -53,4 +53,4 @@ NGP 的 AppKey、AppSecret、access token 和 refresh token 位于 `application.
|
||||
`foundation.ngp` 节点。access token 失效时服务会使用 refresh token 自动刷新,并在当前
|
||||
进程内保存新 token。配置文件含明文凭据,只能保存在私有仓库并限制读取权限。
|
||||
|
||||
历史日期结果默认缓存 24 小时,包含今天的结果默认缓存 60 秒。
|
||||
历史日期结果默认缓存 24 小时,包含今天的结果默认缓存 10 分钟。
|
||||
|
||||
@ -308,8 +308,9 @@ public class FoundationProperties {
|
||||
private int readTimeoutMs = 60000;
|
||||
private int maxRetries = 2;
|
||||
private int maxResponseBytes = 5 * 1024 * 1024;
|
||||
private int currentCacheSeconds = 60;
|
||||
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; }
|
||||
@ -347,6 +348,8 @@ 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 {
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package com.company.agentfoundation.ngp.monthly;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public record NgpMonthlyStoreRow(
|
||||
String storeName,
|
||||
List<BigDecimal> weeklyAmounts,
|
||||
BigDecimal subtotal
|
||||
) {
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.company.agentfoundation.ngp.monthly;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record NgpMonthlyWeek(
|
||||
int index,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
String label,
|
||||
boolean complete
|
||||
) {
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.company.agentfoundation.ngp.monthly;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/ngp/performance")
|
||||
public class NgpMonthlyWeeklyController {
|
||||
private final NgpMonthlyWeeklyService service;
|
||||
|
||||
public NgpMonthlyWeeklyController(NgpMonthlyWeeklyService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@PostMapping("/monthly-weekly")
|
||||
public ResponseEntity<NgpMonthlyWeeklyResult> query(
|
||||
@RequestBody(required = false) NgpMonthlyWeeklyRequest request
|
||||
) {
|
||||
NgpMonthlyWeeklyResult result = service.query(request);
|
||||
if (result instanceof NgpMonthlyWeeklyPendingResponse pending) {
|
||||
return ResponseEntity.accepted()
|
||||
.header(HttpHeaders.RETRY_AFTER, String.valueOf(pending.retryAfterSeconds()))
|
||||
.body(pending);
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.company.agentfoundation.ngp.monthly;
|
||||
|
||||
public record NgpMonthlyWeeklyPendingResponse(
|
||||
boolean success,
|
||||
String status,
|
||||
String month,
|
||||
String message,
|
||||
int pendingDays,
|
||||
int retryAfterSeconds
|
||||
) implements NgpMonthlyWeeklyResult {
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package com.company.agentfoundation.ngp.monthly;
|
||||
|
||||
public record NgpMonthlyWeeklyRequest(String month) {
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.company.agentfoundation.ngp.monthly;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record NgpMonthlyWeeklyResponse(
|
||||
boolean success,
|
||||
String month,
|
||||
String monthLabel,
|
||||
String title,
|
||||
LocalDateTime generatedAt,
|
||||
List<NgpMonthlyWeek> weeks,
|
||||
List<NgpMonthlyStoreRow> stores,
|
||||
List<BigDecimal> weekTotals,
|
||||
BigDecimal grandTotal,
|
||||
String formattedGrandTotal,
|
||||
String currency
|
||||
) implements NgpMonthlyWeeklyResult {
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.company.agentfoundation.ngp.monthly;
|
||||
|
||||
public sealed interface NgpMonthlyWeeklyResult
|
||||
permits NgpMonthlyWeeklyResponse, NgpMonthlyWeeklyPendingResponse {
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
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.NgpPerformanceDayLoader;
|
||||
import com.company.agentfoundation.ngp.performance.NgpPerformanceRefreshCoordinator;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.YearMonth;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class NgpMonthlyWeeklyService {
|
||||
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final String[] CHINESE_MONTHS = {
|
||||
"一月", "二月", "三月", "四月", "五月", "六月",
|
||||
"七月", "八月", "九月", "十月", "十一月", "十二月"
|
||||
};
|
||||
|
||||
private final NgpPerformanceCacheRepository repository;
|
||||
private final NgpPerformanceRefreshCoordinator refreshCoordinator;
|
||||
private final FoundationProperties.Ngp config;
|
||||
private final Clock clock;
|
||||
|
||||
public NgpMonthlyWeeklyService(
|
||||
NgpPerformanceCacheRepository repository,
|
||||
NgpPerformanceRefreshCoordinator refreshCoordinator,
|
||||
FoundationProperties properties
|
||||
) {
|
||||
this(repository, refreshCoordinator, properties, Clock.system(BUSINESS_ZONE));
|
||||
}
|
||||
|
||||
NgpMonthlyWeeklyService(
|
||||
NgpPerformanceCacheRepository repository,
|
||||
NgpPerformanceRefreshCoordinator refreshCoordinator,
|
||||
FoundationProperties properties,
|
||||
Clock clock
|
||||
) {
|
||||
this.repository = repository;
|
||||
this.refreshCoordinator = refreshCoordinator;
|
||||
this.config = properties.getNgp();
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public NgpMonthlyWeeklyResult query(NgpMonthlyWeeklyRequest request) {
|
||||
LocalDate today = LocalDate.now(clock);
|
||||
YearMonth currentMonth = YearMonth.from(today);
|
||||
YearMonth month = parseMonth(request == null ? null : request.month(), currentMonth);
|
||||
if (month.isAfter(currentMonth)) throw new IllegalArgumentException("不能查询未来月份");
|
||||
|
||||
LocalDate monthStart = month.atDay(1);
|
||||
LocalDate monthEnd = month.atEndOfMonth();
|
||||
LocalDate dataEnd = month.equals(currentMonth) ? today : monthEnd;
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
List<LocalDate> missingDates = monthStart.datesUntil(dataEnd.plusDays(1))
|
||||
.filter(date -> !fresh(date, repository.findRefreshTime(date).orElse(null), now))
|
||||
.toList();
|
||||
if (!missingDates.isEmpty()) {
|
||||
refreshCoordinator.ensureRefresh(missingDates);
|
||||
int retrySeconds = Math.max(config.getPendingRetrySeconds(), 1);
|
||||
return new NgpMonthlyWeeklyPendingResponse(
|
||||
false,
|
||||
"PROCESSING",
|
||||
month.toString(),
|
||||
"后台正在补齐本月 " + missingDates.size() + " 天的缓存,请稍后重试",
|
||||
missingDates.size(),
|
||||
retrySeconds
|
||||
);
|
||||
}
|
||||
|
||||
List<NgpMonthlyWeek> weeks = buildWeeks(month, today);
|
||||
List<NgpDailyStoreTotal> dailyTotals = repository.findDailyTotals(monthStart, dataEnd);
|
||||
return buildResponse(month, today, weeks, dailyTotals);
|
||||
}
|
||||
|
||||
private NgpMonthlyWeeklyResponse buildResponse(
|
||||
YearMonth month,
|
||||
LocalDate today,
|
||||
List<NgpMonthlyWeek> weeks,
|
||||
List<NgpDailyStoreTotal> dailyTotals
|
||||
) {
|
||||
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())));
|
||||
long[] weekTotals = new long[weeks.size()];
|
||||
for (NgpDailyStoreTotal daily : dailyTotals) {
|
||||
int weekIndex = weekIndex(weeks, daily.businessDate());
|
||||
if (weekIndex < 0) continue;
|
||||
String storeKey = NgpPerformanceDayLoader.normalize(daily.storeKey());
|
||||
StoreAccumulator store = stores.get(storeKey);
|
||||
if (store == null && restrictStores) continue;
|
||||
if (store == null) {
|
||||
store = new StoreAccumulator(daily.storeName(), weeks.size());
|
||||
stores.put(storeKey, store);
|
||||
}
|
||||
store.weeklyCents[weekIndex] += daily.totalCents();
|
||||
store.subtotalCents += daily.totalCents();
|
||||
weekTotals[weekIndex] += daily.totalCents();
|
||||
}
|
||||
|
||||
List<NgpMonthlyStoreRow> rows = stores.values().stream()
|
||||
.filter(store -> restrictStores || store.subtotalCents != 0)
|
||||
.map(store -> new NgpMonthlyStoreRow(
|
||||
store.storeName,
|
||||
amountColumns(store.weeklyCents, weeks, today),
|
||||
store.subtotalCents == 0 ? null : amount(store.subtotalCents)))
|
||||
.toList();
|
||||
List<BigDecimal> totals = amountColumns(weekTotals, weeks, today);
|
||||
long grandTotalCents = 0;
|
||||
for (long cents : weekTotals) grandTotalCents += cents;
|
||||
BigDecimal grandTotal = amount(grandTotalCents);
|
||||
String monthLabel = CHINESE_MONTHS[month.getMonthValue() - 1];
|
||||
return new NgpMonthlyWeeklyResponse(
|
||||
true,
|
||||
month.toString(),
|
||||
monthLabel,
|
||||
month.getYear() + "年" + monthLabel + "业绩周统计",
|
||||
LocalDateTime.now(clock),
|
||||
weeks,
|
||||
rows,
|
||||
totals,
|
||||
grandTotal,
|
||||
formatAmount(grandTotal),
|
||||
"CNY"
|
||||
);
|
||||
}
|
||||
|
||||
private List<NgpMonthlyWeek> buildWeeks(YearMonth month, LocalDate today) {
|
||||
List<NgpMonthlyWeek> result = new ArrayList<>();
|
||||
LocalDate cursor = month.atDay(1);
|
||||
LocalDate monthEnd = month.atEndOfMonth();
|
||||
int index = 0;
|
||||
while (!cursor.isAfter(monthEnd)) {
|
||||
LocalDate weekEnd = cursor.with(TemporalAdjusters.nextOrSame(java.time.DayOfWeek.SUNDAY));
|
||||
if (weekEnd.isAfter(monthEnd)) weekEnd = monthEnd;
|
||||
result.add(new NgpMonthlyWeek(
|
||||
index++,
|
||||
cursor,
|
||||
weekEnd,
|
||||
cursor.getMonthValue() + "/" + cursor.getDayOfMonth()
|
||||
+ "-" + weekEnd.getMonthValue() + "/" + weekEnd.getDayOfMonth(),
|
||||
!weekEnd.isAfter(today)
|
||||
));
|
||||
cursor = weekEnd.plusDays(1);
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private List<BigDecimal> amountColumns(long[] cents, List<NgpMonthlyWeek> weeks, LocalDate today) {
|
||||
List<BigDecimal> result = new ArrayList<>();
|
||||
for (int index = 0; index < cents.length; index++) {
|
||||
boolean futureWeek = weeks.get(index).startDate().isAfter(today);
|
||||
result.add(futureWeek || cents[index] == 0 ? null : amount(cents[index]));
|
||||
}
|
||||
return Collections.unmodifiableList(result);
|
||||
}
|
||||
|
||||
private int weekIndex(List<NgpMonthlyWeek> weeks, LocalDate date) {
|
||||
for (NgpMonthlyWeek week : weeks) {
|
||||
if (!date.isBefore(week.startDate()) && !date.isAfter(week.endDate())) return week.index();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean fresh(LocalDate date, LocalDateTime refreshedAt, LocalDateTime now) {
|
||||
if (refreshedAt == null) return false;
|
||||
int ttlSeconds = date.isBefore(now.toLocalDate())
|
||||
? config.getHistoryCacheSeconds() : config.getCurrentCacheSeconds();
|
||||
return Duration.between(refreshedAt, now).getSeconds() <= Math.max(ttlSeconds, 0);
|
||||
}
|
||||
|
||||
private YearMonth parseMonth(String raw, YearMonth defaultMonth) {
|
||||
if (raw == null || raw.isBlank()) return defaultMonth;
|
||||
try {
|
||||
return YearMonth.parse(raw.trim());
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw new IllegalArgumentException("month 必须使用 YYYY-MM 格式", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal amount(long cents) {
|
||||
return BigDecimal.valueOf(cents, 2).setScale(2, RoundingMode.UNNECESSARY);
|
||||
}
|
||||
|
||||
private String formatAmount(BigDecimal amount) {
|
||||
NumberFormat formatter = NumberFormat.getNumberInstance(Locale.CHINA);
|
||||
formatter.setGroupingUsed(true);
|
||||
formatter.setMinimumFractionDigits(2);
|
||||
formatter.setMaximumFractionDigits(2);
|
||||
return formatter.format(amount);
|
||||
}
|
||||
|
||||
private static final class StoreAccumulator {
|
||||
private final String storeName;
|
||||
private final long[] weeklyCents;
|
||||
private long subtotalCents;
|
||||
|
||||
private StoreAccumulator(String storeName, int weekCount) {
|
||||
this.storeName = storeName;
|
||||
this.weeklyCents = new long[weekCount];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.company.agentfoundation.ngp.performance;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record NgpDailyStoreTotal(
|
||||
LocalDate businessDate,
|
||||
String storeKey,
|
||||
String storeName,
|
||||
long totalCents
|
||||
) {
|
||||
}
|
||||
@ -72,6 +72,24 @@ public class NgpPerformanceCacheRepository {
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
public List<NgpDailyStoreTotal> findDailyTotals(LocalDate startDate, LocalDate endDate) {
|
||||
return jdbcTemplate.query("""
|
||||
SELECT business_date, store_key, store_name, total_cents
|
||||
FROM ngp_performance_daily_total
|
||||
WHERE business_date BETWEEN ? AND ?
|
||||
ORDER BY store_name, business_date
|
||||
""",
|
||||
(resultSet, rowNum) -> new NgpDailyStoreTotal(
|
||||
resultSet.getObject("business_date", LocalDate.class),
|
||||
resultSet.getString("store_key"),
|
||||
resultSet.getString("store_name"),
|
||||
resultSet.getLong("total_cents")
|
||||
),
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void replaceDay(LocalDate date, Map<String, NgpStoreTotal> totals, LocalDateTime refreshedAt) {
|
||||
jdbcTemplate.update("DELETE FROM ngp_performance_daily_total WHERE business_date = ?", date);
|
||||
|
||||
@ -150,7 +150,7 @@ public class NgpPerformanceDayLoader {
|
||||
return "";
|
||||
}
|
||||
|
||||
static String normalize(String value) {
|
||||
public static String normalize(String value) {
|
||||
return value == null ? "" : value.trim().replaceAll("\\s+", " ").toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
|
||||
@ -147,8 +147,30 @@ foundation:
|
||||
read-timeout-ms: 60000
|
||||
max-retries: 2
|
||||
max-response-bytes: 5242880
|
||||
current-cache-seconds: 60
|
||||
current-cache-seconds: 600
|
||||
history-cache-seconds: 86400
|
||||
monthly-store-names:
|
||||
- 跃动内容电商(小红书2店)
|
||||
- 跃动内容电商(微信小店)
|
||||
- 跃动内容电商(抖店)
|
||||
- 跃动内容电商(小红书)
|
||||
- 跃动内容电商(快手)
|
||||
- 跃动京东自营店
|
||||
- 拼多多-跃动官方旗舰店
|
||||
- 淘宝-跃动体育用品
|
||||
- 淘宝-索菲格体育用品
|
||||
- 天猫-跃动运动旗舰店
|
||||
- 跃动运动户外专营店
|
||||
- 跃动京东pop
|
||||
- 拼多多-盛跃动盈运动户外专卖店
|
||||
- 跃动运动健身旗舰店-抖店
|
||||
- 跃动运动器械旗舰店-快手
|
||||
- 跃动运动健身-微信小店
|
||||
- 小红书-盛跃动盈科技的店
|
||||
- 拼多多-乐动体育运动专营店
|
||||
- 抖店2-跃动健身训练旗舰店
|
||||
- 拼多多-盛跃动盈体育用品专营店
|
||||
- 盛跃动盈户外专营店
|
||||
semantic:
|
||||
agent-id: crm-assistant
|
||||
minimum-confidence: 0.60
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package com.company.agentfoundation.ngp.monthly;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class NgpMonthlyWeeklyControllerTest {
|
||||
@Test
|
||||
void returnsAcceptedWhileDailyCacheIsBeingFilled() throws Exception {
|
||||
NgpMonthlyWeeklyService service = mock(NgpMonthlyWeeklyService.class);
|
||||
when(service.query(any())).thenReturn(new NgpMonthlyWeeklyPendingResponse(
|
||||
false, "PROCESSING", "2026-09", "正在补齐缓存", 9, 10));
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new NgpMonthlyWeeklyController(service)).build();
|
||||
|
||||
mockMvc.perform(post("/api/ngp/performance/monthly-weekly")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"month\":\"2026-09\"}"))
|
||||
.andExpect(status().isAccepted())
|
||||
.andExpect(header().string("Retry-After", "10"))
|
||||
.andExpect(jsonPath("$.status").value("PROCESSING"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
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.NgpPerformanceRefreshCoordinator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class NgpMonthlyWeeklyServiceTest {
|
||||
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-09-09T04:00:00Z"), ZONE);
|
||||
|
||||
private NgpPerformanceCacheRepository repository;
|
||||
private NgpPerformanceRefreshCoordinator coordinator;
|
||||
private NgpMonthlyWeeklyService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
repository = mock(NgpPerformanceCacheRepository.class);
|
||||
coordinator = mock(NgpPerformanceRefreshCoordinator.class);
|
||||
service = new NgpMonthlyWeeklyService(repository, coordinator, new FoundationProperties(), CLOCK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentMonthOnlyRequiresDatesThroughToday() {
|
||||
when(repository.findRefreshTime(any())).thenReturn(Optional.empty());
|
||||
|
||||
NgpMonthlyWeeklyResult result = service.query(new NgpMonthlyWeeklyRequest(null));
|
||||
|
||||
assertThat(result).isInstanceOf(NgpMonthlyWeeklyPendingResponse.class);
|
||||
assertThat(((NgpMonthlyWeeklyPendingResponse) result).pendingDays()).isEqualTo(9);
|
||||
ArgumentCaptor<List<LocalDate>> dates = ArgumentCaptor.forClass(List.class);
|
||||
verify(coordinator).ensureRefresh(dates.capture());
|
||||
assertThat(dates.getValue()).containsExactly(
|
||||
LocalDate.of(2026, 9, 1), LocalDate.of(2026, 9, 2), LocalDate.of(2026, 9, 3),
|
||||
LocalDate.of(2026, 9, 4), LocalDate.of(2026, 9, 5), LocalDate.of(2026, 9, 6),
|
||||
LocalDate.of(2026, 9, 7), LocalDate.of(2026, 9, 8), LocalDate.of(2026, 9, 9));
|
||||
verify(repository, times(9)).findRefreshTime(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildsMondayToSundayColumnsAndStoreSubtotalsFromDailyCache() {
|
||||
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, 6), "a", "网点A", 20000),
|
||||
new NgpDailyStoreTotal(LocalDate.of(2026, 9, 7), "a", "网点A", 40000),
|
||||
new NgpDailyStoreTotal(LocalDate.of(2026, 9, 9), "b", "网点B", 50000)
|
||||
));
|
||||
|
||||
NgpMonthlyWeeklyResult result = service.query(new NgpMonthlyWeeklyRequest("2026-09"));
|
||||
|
||||
assertThat(result).isInstanceOf(NgpMonthlyWeeklyResponse.class);
|
||||
NgpMonthlyWeeklyResponse response = (NgpMonthlyWeeklyResponse) result;
|
||||
assertThat(response.weeks()).extracting(NgpMonthlyWeek::label)
|
||||
.containsExactly("9/1-9/6", "9/7-9/13", "9/14-9/20", "9/21-9/27", "9/28-9/30");
|
||||
assertThat(response.stores()).hasSize(2);
|
||||
assertThat(response.stores().get(0).weeklyAmounts().get(0)).isEqualByComparingTo("300.00");
|
||||
assertThat(response.stores().get(0).weeklyAmounts().get(1)).isEqualByComparingTo("400.00");
|
||||
assertThat(response.stores().get(0).weeklyAmounts().get(2)).isNull();
|
||||
assertThat(response.grandTotal()).isEqualByComparingTo("1200.00");
|
||||
assertThat(response.formattedGrandTotal()).isEqualTo("1,200.00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsConfiguredStoreOrderAndExcludesOtherStores() {
|
||||
FoundationProperties properties = new FoundationProperties();
|
||||
properties.getNgp().setMonthlyStoreNames(List.of("网点B", "网点A", "无数据网点"));
|
||||
service = new NgpMonthlyWeeklyService(repository, coordinator, properties, CLOCK);
|
||||
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), "其他网点", "其他网点", 99900)
|
||||
));
|
||||
|
||||
NgpMonthlyWeeklyResponse response = (NgpMonthlyWeeklyResponse) service.query(
|
||||
new NgpMonthlyWeeklyRequest("2026-09"));
|
||||
|
||||
assertThat(response.stores()).extracting(NgpMonthlyStoreRow::storeName)
|
||||
.containsExactly("网点B", "网点A", "无数据网点");
|
||||
assertThat(response.stores().get(0).subtotal()).isNull();
|
||||
assertThat(response.stores().get(1).subtotal()).isEqualByComparingTo("100.00");
|
||||
assertThat(response.stores().get(2).subtotal()).isNull();
|
||||
assertThat(response.grandTotal()).isEqualByComparingTo("100.00");
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user