增加购物统计功能

This commit is contained in:
2026-06-06 22:20:58 +08:00
parent 7413485718
commit 4c074a0191
6 changed files with 310 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ package com.rj.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.LbBuyerShoppingGenerateStatRequest;
import com.rj.dto.LbBuyerShoppingPullRequest;
import com.rj.dto.hxr.HxrBuyerOrderApiContext;
import com.rj.dto.hxr.HxrUserLoginApiContext;
@@ -16,8 +17,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@@ -36,6 +40,21 @@ public class LbBuyerShoppingServiceImpl
extends ServiceImpl<LbBuyerShoppingMapper, LbBuyerShopping>
implements ILbBuyerShoppingService {
/** 详细数据 */
public static final String DATA_TYPE_DETAIL = "detail_data";
/** 天统计 */
public static final String DATA_TYPE_DAY_STAT = "day_stat";
/** 总和统计 */
public static final String DATA_TYPE_SUM = "sum_data";
private static final long DAY_STAT_ID_BASE = 20_000_000_000_000_000L;
private static final long SUM_STAT_ID_BASE = 21_000_000_000_000_000L;
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final String DEFAULT_SIMULATE_LOGIN_PASSWORD = "123456";
@Autowired
@@ -109,6 +128,9 @@ public class LbBuyerShoppingServiceImpl
if (entity.getTotalMoney() == null) {
entity.setTotalMoney(existing.getTotalMoney());
}
if (entity.getDataType() == null || entity.getDataType().isBlank()) {
entity.setDataType(existing.getDataType());
}
boolean ok = this.updateById(entity);
result.put("success", ok);
@@ -157,6 +179,7 @@ public class LbBuyerShoppingServiceImpl
String sellerMobile,
String buyerNickname,
String buyerMobile,
String dataType,
BigDecimal totalMoneyMin,
BigDecimal totalMoneyMax,
String createdAtStart,
@@ -199,6 +222,9 @@ public class LbBuyerShoppingServiceImpl
if (buyerMobile != null && !buyerMobile.trim().isEmpty()) {
queryWrapper.like(LbBuyerShopping::getBuyerMobile, buyerMobile.trim());
}
if (dataType != null && !dataType.trim().isEmpty()) {
queryWrapper.eq(LbBuyerShopping::getDataType, dataType.trim());
}
if (totalMoneyMin != null) {
queryWrapper.ge(LbBuyerShopping::getTotalMoney, totalMoneyMin);
}
@@ -526,6 +552,220 @@ public class LbBuyerShoppingServiceImpl
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> generateBuyerStat(LbBuyerShoppingGenerateStatRequest request) {
Map<String, Object> result = new HashMap<>();
try {
if (request == null) {
result.put("success", false);
result.put("message", "请求体不能为空");
return result;
}
if (request.getBuyerId() == null) {
result.put("success", false);
result.put("message", "buyerId不能为空");
return result;
}
String statType = request.getStatType() != null ? request.getStatType().trim() : "";
if (statType.isEmpty()) {
result.put("success", false);
result.put("message", "statType不能为空");
return result;
}
if (!DATA_TYPE_DAY_STAT.equals(statType) && !DATA_TYPE_SUM.equals(statType)) {
result.put("success", false);
result.put("message", "statType 仅支持 day_stat 或 sum_data");
return result;
}
Long buyerId = request.getBuyerId();
List<LbBuyerShopping> details = listDetailRowsByBuyerId(buyerId);
if (details.isEmpty()) {
result.put("success", true);
result.put("message", "该买家无明细订单");
result.put("generated", 0);
result.put("sourceCount", 0);
return result;
}
List<LbBuyerShopping> statRows;
int skippedNoBuyTime = 0;
if (DATA_TYPE_DAY_STAT.equals(statType)) {
Map<LocalDate, List<LbBuyerShopping>> byDay = new LinkedHashMap<>();
for (LbBuyerShopping row : details) {
LocalDate day = resolveBuyDate(row.getBuyTime());
if (day == null) {
skippedNoBuyTime++;
continue;
}
byDay.computeIfAbsent(day, k -> new ArrayList<>()).add(row);
}
if (byDay.isEmpty()) {
result.put("success", false);
result.put("message", "明细 buy_time 均无法解析为日期,无法按天汇总");
result.put("skippedNoBuyTime", skippedNoBuyTime);
return result;
}
statRows = new ArrayList<>(byDay.size());
for (Map.Entry<LocalDate, List<LbBuyerShopping>> entry : byDay.entrySet()) {
statRows.add(buildDayStatRow(buyerId, entry.getKey(), entry.getValue()));
}
} else {
statRows = List.of(buildSumStatRow(buyerId, details));
}
int upserted = upsertStatRows(statRows);
result.put("success", upserted >= 0);
result.put("message", upserted >= 0 ? "统计完成" : "保存失败");
result.put("generated", Math.max(upserted, 0));
result.put("sourceCount", details.size());
result.put("skippedNoBuyTime", skippedNoBuyTime);
if (upserted >= 0) {
result.put("data", statRows);
}
return result;
} catch (Exception e) {
log.error("买方购物统计异常 buyerId={}", request != null ? request.getBuyerId() : null, e);
result.put("success", false);
result.put("message", "统计异常:" + e.getMessage());
result.put("generated", 0);
return result;
}
}
private List<LbBuyerShopping> listDetailRowsByBuyerId(Long buyerId) {
LambdaQueryWrapper<LbBuyerShopping> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(LbBuyerShopping::getBuyerId, buyerId)
.eq(LbBuyerShopping::getDataType, DATA_TYPE_DETAIL)
.orderByAsc(LbBuyerShopping::getBuyTime)
.orderByAsc(LbBuyerShopping::getId);
return this.list(queryWrapper);
}
private LbBuyerShopping buildDayStatRow(Long buyerId, LocalDate day, List<LbBuyerShopping> dayRows) {
StatAggregate aggregate = aggregateRows(dayRows);
Long id = resolveDayStatRowId(buyerId, day);
LbBuyerShopping existing = this.getById(id);
LbBuyerShopping stat = new LbBuyerShopping();
stat.setId(id);
stat.setBuyerId(buyerId);
stat.setDataType(DATA_TYPE_DAY_STAT);
stat.setStatTotalMoney(aggregate.totalMoney);
stat.setTotalOrderCount(aggregate.orderCount);
stat.setAvgAmount(aggregate.avgAmount);
stat.setTotalMoney(aggregate.totalMoney);
stat.setBuyTime(day.atStartOfDay());
stat.setOrderSn("day_stat_" + buyerId + "_" + day.format(DAY_FMT));
copyBuyerProfile(stat, dayRows.get(0));
stat.setCreatedAt(existing != null && existing.getCreatedAt() != null
? existing.getCreatedAt()
: LocalDateTime.now());
return stat;
}
private LbBuyerShopping buildSumStatRow(Long buyerId, List<LbBuyerShopping> detailRows) {
StatAggregate aggregate = aggregateRows(detailRows);
Long id = resolveSumStatRowId(buyerId);
LbBuyerShopping existing = this.getById(id);
LbBuyerShopping stat = new LbBuyerShopping();
stat.setId(id);
stat.setBuyerId(buyerId);
stat.setDataType(DATA_TYPE_SUM);
stat.setStatTotalMoney(aggregate.totalMoney);
stat.setTotalOrderCount(aggregate.orderCount);
stat.setAvgAmount(aggregate.avgAmount);
stat.setTotalMoney(aggregate.totalMoney);
stat.setOrderSn("sum_data_" + buyerId);
copyBuyerProfile(stat, detailRows.get(0));
stat.setCreatedAt(existing != null && existing.getCreatedAt() != null
? existing.getCreatedAt()
: LocalDateTime.now());
return stat;
}
private static StatAggregate aggregateRows(List<LbBuyerShopping> rows) {
BigDecimal moneySum = BigDecimal.ZERO;
for (LbBuyerShopping row : rows) {
if (row.getTotalMoney() != null) {
moneySum = moneySum.add(row.getTotalMoney());
}
}
int orderCount = rows.size();
BigDecimal avgAmount = orderCount > 0
? moneySum.divide(BigDecimal.valueOf(orderCount), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
return new StatAggregate(moneySum, orderCount, avgAmount);
}
private static void copyBuyerProfile(LbBuyerShopping target, LbBuyerShopping sample) {
if (sample == null) {
return;
}
target.setBuyerNickname(sample.getBuyerNickname());
target.setBuyerMobile(sample.getBuyerMobile());
}
private Long resolveDayStatRowId(Long buyerId, LocalDate day) {
LocalDateTime dayStart = day.atStartOfDay();
LocalDateTime dayEnd = day.plusDays(1).atStartOfDay();
LambdaQueryWrapper<LbBuyerShopping> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(LbBuyerShopping::getBuyerId, buyerId)
.eq(LbBuyerShopping::getDataType, DATA_TYPE_DAY_STAT)
.ge(LbBuyerShopping::getBuyTime, dayStart)
.lt(LbBuyerShopping::getBuyTime, dayEnd)
.last("LIMIT 1");
LbBuyerShopping existing = this.getOne(queryWrapper, false);
if (existing != null && existing.getId() != null) {
return existing.getId();
}
long dayKey = day.getYear() * 10000L + day.getMonthValue() * 100L + day.getDayOfMonth();
long buyerSlot = Math.floorMod(buyerId, 1_000_000L);
return DAY_STAT_ID_BASE + buyerSlot * 10_000L + dayKey;
}
private Long resolveSumStatRowId(Long buyerId) {
LambdaQueryWrapper<LbBuyerShopping> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(LbBuyerShopping::getBuyerId, buyerId)
.eq(LbBuyerShopping::getDataType, DATA_TYPE_SUM)
.last("LIMIT 1");
LbBuyerShopping existing = this.getOne(queryWrapper, false);
if (existing != null && existing.getId() != null) {
return existing.getId();
}
return SUM_STAT_ID_BASE + Math.floorMod(buyerId, 1_000_000_000L);
}
private static LocalDate resolveBuyDate(LocalDateTime buyTime) {
return buyTime != null ? buyTime.toLocalDate() : null;
}
private int upsertStatRows(List<LbBuyerShopping> statRows) {
if (statRows == null || statRows.isEmpty()) {
return 0;
}
int upserted = 0;
for (LbBuyerShopping statRow : statRows) {
if (statRow.getId() == null) {
continue;
}
boolean ok = this.getById(statRow.getId()) != null
? this.updateById(statRow)
: this.save(statRow);
if (ok) {
upserted++;
} else {
return -1;
}
}
return upserted;
}
private record StatAggregate(BigDecimal totalMoney, int orderCount, BigDecimal avgAmount) {
}
private static int toInt(Object value) {
if (value instanceof Number number) {
return number.intValue();
@@ -541,6 +781,9 @@ public class LbBuyerShoppingServiceImpl
}
private void applyDefaults(LbBuyerShopping entity) {
if (entity.getDataType() == null || entity.getDataType().isBlank()) {
entity.setDataType(DATA_TYPE_DETAIL);
}
if (entity.getTotalMoney() == null) {
entity.setTotalMoney(BigDecimal.ZERO);
}