增加购物统计功能

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

@@ -1,5 +1,6 @@
package com.rj.controller; package com.rj.controller;
import com.rj.dto.LbBuyerShoppingGenerateStatRequest;
import com.rj.dto.LbBuyerShoppingPullRequest; import com.rj.dto.LbBuyerShoppingPullRequest;
import com.rj.entity.LbBuyerShopping; import com.rj.entity.LbBuyerShopping;
import com.rj.service.ILbBuyerShoppingService; import com.rj.service.ILbBuyerShoppingService;
@@ -63,6 +64,20 @@ public class LbBuyerShoppingController {
return toResponse(result); return toResponse(result);
} }
@PostMapping("/generate-stat")
@Operation(
summary = "按买家生成天统计或总和统计",
description =
"根据 buyerId 汇总 detail_data 明细day_stat 按购买日期每天生成一条统计"
+ "stat_total_money、total_order_count、avg_amount"
+ "sum_data 生成该买家全部订单的一条总和统计")
public ResponseEntity<Map<String, Object>> generateStat(
@Parameter(description = "买家 ID 与统计类型day_stat / sum_data", required = true)
@RequestBody LbBuyerShoppingGenerateStatRequest request) {
Map<String, Object> result = lbBuyerShoppingService.generateBuyerStat(request);
return toResponse(result);
}
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "分页查询") @Operation(summary = "分页查询")
public ResponseEntity<Map<String, Object>> list( public ResponseEntity<Map<String, Object>> list(
@@ -76,6 +91,8 @@ public class LbBuyerShoppingController {
@RequestParam(required = false) String sellerMobile, @RequestParam(required = false) String sellerMobile,
@RequestParam(required = false) String buyerNickname, @RequestParam(required = false) String buyerNickname,
@RequestParam(required = false) String buyerMobile, @RequestParam(required = false) String buyerMobile,
@Parameter(description = "数据类型detail_data=详细数据day_stat=天统计sum_data=总和统计")
@RequestParam(required = false) String dataType,
@RequestParam(required = false) BigDecimal totalMoneyMin, @RequestParam(required = false) BigDecimal totalMoneyMin,
@RequestParam(required = false) BigDecimal totalMoneyMax, @RequestParam(required = false) BigDecimal totalMoneyMax,
@Parameter(description = "创建时间起,格式 yyyy-MM-dd HH:mm:ss") @Parameter(description = "创建时间起,格式 yyyy-MM-dd HH:mm:ss")
@@ -92,7 +109,7 @@ public class LbBuyerShoppingController {
@RequestParam(required = false) String confirmTimeEnd) { @RequestParam(required = false) String confirmTimeEnd) {
Map<String, Object> result = lbBuyerShoppingService.pageQuery( Map<String, Object> result = lbBuyerShoppingService.pageQuery(
current, size, sellerId, merchandiseId, buyerId, orderSn, current, size, sellerId, merchandiseId, buyerId, orderSn,
sellerNickname, sellerMobile, buyerNickname, buyerMobile, sellerNickname, sellerMobile, buyerNickname, buyerMobile, dataType,
totalMoneyMin, totalMoneyMax, totalMoneyMin, totalMoneyMax,
createdAtStart, createdAtEnd, buyTimeStart, buyTimeEnd, createdAtStart, createdAtEnd, buyTimeStart, buyTimeEnd,
confirmTimeStart, confirmTimeEnd); confirmTimeStart, confirmTimeEnd);

View File

@@ -0,0 +1,18 @@
package com.rj.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 按买家 ID 将 lb_buyer_shopping 明细汇总为天统计或总和统计并写入表。
*/
@Data
@Schema(description = "买方购物生成统计请求")
public class LbBuyerShoppingGenerateStatRequest {
@Schema(description = "买家 ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long buyerId;
@Schema(description = "统计类型day_stat=天统计sum_data=总和统计", requiredMode = Schema.RequiredMode.REQUIRED)
private String statType;
}

View File

@@ -84,4 +84,20 @@ public class LbBuyerShopping implements Serializable {
@TableField("buyer_mobile") @TableField("buyer_mobile")
@Schema(description = "买家手机号") @Schema(description = "买家手机号")
private String buyerMobile; private String buyerMobile;
@TableField("data_type")
@Schema(description = "数据类型detail_data=详细数据day_stat=天统计sum_data=总和统计")
private String dataType;
@TableField("stat_total_money")
@Schema(description = "总金额")
private BigDecimal statTotalMoney;
@TableField("total_order_count")
@Schema(description = "总单数")
private Integer totalOrderCount;
@TableField("avg_amount")
@Schema(description = "平均金额")
private BigDecimal avgAmount;
} }

View File

@@ -1,6 +1,7 @@
package com.rj.service; package com.rj.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.rj.dto.LbBuyerShoppingGenerateStatRequest;
import com.rj.dto.LbBuyerShoppingPullRequest; import com.rj.dto.LbBuyerShoppingPullRequest;
import com.rj.entity.LbBuyerShopping; import com.rj.entity.LbBuyerShopping;
@@ -28,6 +29,7 @@ public interface ILbBuyerShoppingService extends IService<LbBuyerShopping> {
String sellerMobile, String sellerMobile,
String buyerNickname, String buyerNickname,
String buyerMobile, String buyerMobile,
String dataType,
BigDecimal totalMoneyMin, BigDecimal totalMoneyMin,
BigDecimal totalMoneyMax, BigDecimal totalMoneyMax,
String createdAtStart, String createdAtStart,
@@ -41,4 +43,9 @@ public interface ILbBuyerShoppingService extends IService<LbBuyerShopping> {
* 按手机号模拟第三方登录获取 token分页拉取买方订单列表并解析入库。 * 按手机号模拟第三方登录获取 token分页拉取买方订单列表并解析入库。
*/ */
Map<String, Object> pullFromThirdParty(LbBuyerShoppingPullRequest request); Map<String, Object> pullFromThirdParty(LbBuyerShoppingPullRequest request);
/**
* 按买家 ID 将明细汇总为 {@code day_stat} 或 {@code sum_data} 并写入表。
*/
Map<String, Object> generateBuyerStat(LbBuyerShoppingGenerateStatRequest request);
} }

View File

@@ -3,6 +3,7 @@ package com.rj.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.LbBuyerShoppingGenerateStatRequest;
import com.rj.dto.LbBuyerShoppingPullRequest; import com.rj.dto.LbBuyerShoppingPullRequest;
import com.rj.dto.hxr.HxrBuyerOrderApiContext; import com.rj.dto.hxr.HxrBuyerOrderApiContext;
import com.rj.dto.hxr.HxrUserLoginApiContext; 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.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
@@ -36,6 +40,21 @@ public class LbBuyerShoppingServiceImpl
extends ServiceImpl<LbBuyerShoppingMapper, LbBuyerShopping> extends ServiceImpl<LbBuyerShoppingMapper, LbBuyerShopping>
implements ILbBuyerShoppingService { 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"; private static final String DEFAULT_SIMULATE_LOGIN_PASSWORD = "123456";
@Autowired @Autowired
@@ -109,6 +128,9 @@ public class LbBuyerShoppingServiceImpl
if (entity.getTotalMoney() == null) { if (entity.getTotalMoney() == null) {
entity.setTotalMoney(existing.getTotalMoney()); entity.setTotalMoney(existing.getTotalMoney());
} }
if (entity.getDataType() == null || entity.getDataType().isBlank()) {
entity.setDataType(existing.getDataType());
}
boolean ok = this.updateById(entity); boolean ok = this.updateById(entity);
result.put("success", ok); result.put("success", ok);
@@ -157,6 +179,7 @@ public class LbBuyerShoppingServiceImpl
String sellerMobile, String sellerMobile,
String buyerNickname, String buyerNickname,
String buyerMobile, String buyerMobile,
String dataType,
BigDecimal totalMoneyMin, BigDecimal totalMoneyMin,
BigDecimal totalMoneyMax, BigDecimal totalMoneyMax,
String createdAtStart, String createdAtStart,
@@ -199,6 +222,9 @@ public class LbBuyerShoppingServiceImpl
if (buyerMobile != null && !buyerMobile.trim().isEmpty()) { if (buyerMobile != null && !buyerMobile.trim().isEmpty()) {
queryWrapper.like(LbBuyerShopping::getBuyerMobile, buyerMobile.trim()); queryWrapper.like(LbBuyerShopping::getBuyerMobile, buyerMobile.trim());
} }
if (dataType != null && !dataType.trim().isEmpty()) {
queryWrapper.eq(LbBuyerShopping::getDataType, dataType.trim());
}
if (totalMoneyMin != null) { if (totalMoneyMin != null) {
queryWrapper.ge(LbBuyerShopping::getTotalMoney, totalMoneyMin); 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) { private static int toInt(Object value) {
if (value instanceof Number number) { if (value instanceof Number number) {
return number.intValue(); return number.intValue();
@@ -541,6 +781,9 @@ public class LbBuyerShoppingServiceImpl
} }
private void applyDefaults(LbBuyerShopping entity) { private void applyDefaults(LbBuyerShopping entity) {
if (entity.getDataType() == null || entity.getDataType().isBlank()) {
entity.setDataType(DATA_TYPE_DETAIL);
}
if (entity.getTotalMoney() == null) { if (entity.getTotalMoney() == null) {
entity.setTotalMoney(BigDecimal.ZERO); entity.setTotalMoney(BigDecimal.ZERO);
} }

View File

@@ -0,0 +1,8 @@
-- 升级脚本lb_buyer_shopping 增加数据类型与统计字段
SET NAMES utf8mb4;
ALTER TABLE `lb_buyer_shopping`
ADD COLUMN `data_type` VARCHAR(32) NOT NULL DEFAULT 'detail_data' COMMENT '数据类型detail_data=详细数据day_stat=天统计sum_data=总和统计' AFTER `buyer_mobile`,
ADD COLUMN `stat_total_money` DECIMAL(18, 2) NULL DEFAULT NULL COMMENT '总金额' AFTER `data_type`,
ADD COLUMN `total_order_count` INT NULL DEFAULT NULL COMMENT '总单数' AFTER `stat_total_money`,
ADD COLUMN `avg_amount` DECIMAL(18, 2) NULL DEFAULT NULL COMMENT '平均金额' AFTER `total_order_count`;