增加购物统计功能
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user