增加购物统计功能
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.rj.controller;
|
||||
|
||||
import com.rj.dto.LbBuyerShoppingGenerateStatRequest;
|
||||
import com.rj.dto.LbBuyerShoppingPullRequest;
|
||||
import com.rj.entity.LbBuyerShopping;
|
||||
import com.rj.service.ILbBuyerShoppingService;
|
||||
@@ -63,6 +64,20 @@ public class LbBuyerShoppingController {
|
||||
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")
|
||||
@Operation(summary = "分页查询")
|
||||
public ResponseEntity<Map<String, Object>> list(
|
||||
@@ -76,6 +91,8 @@ public class LbBuyerShoppingController {
|
||||
@RequestParam(required = false) String sellerMobile,
|
||||
@RequestParam(required = false) String buyerNickname,
|
||||
@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 totalMoneyMax,
|
||||
@Parameter(description = "创建时间起,格式 yyyy-MM-dd HH:mm:ss")
|
||||
@@ -92,7 +109,7 @@ public class LbBuyerShoppingController {
|
||||
@RequestParam(required = false) String confirmTimeEnd) {
|
||||
Map<String, Object> result = lbBuyerShoppingService.pageQuery(
|
||||
current, size, sellerId, merchandiseId, buyerId, orderSn,
|
||||
sellerNickname, sellerMobile, buyerNickname, buyerMobile,
|
||||
sellerNickname, sellerMobile, buyerNickname, buyerMobile, dataType,
|
||||
totalMoneyMin, totalMoneyMax,
|
||||
createdAtStart, createdAtEnd, buyTimeStart, buyTimeEnd,
|
||||
confirmTimeStart, confirmTimeEnd);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -84,4 +84,20 @@ public class LbBuyerShopping implements Serializable {
|
||||
@TableField("buyer_mobile")
|
||||
@Schema(description = "买家手机号")
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.rj.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.rj.dto.LbBuyerShoppingGenerateStatRequest;
|
||||
import com.rj.dto.LbBuyerShoppingPullRequest;
|
||||
import com.rj.entity.LbBuyerShopping;
|
||||
|
||||
@@ -28,6 +29,7 @@ public interface ILbBuyerShoppingService extends IService<LbBuyerShopping> {
|
||||
String sellerMobile,
|
||||
String buyerNickname,
|
||||
String buyerMobile,
|
||||
String dataType,
|
||||
BigDecimal totalMoneyMin,
|
||||
BigDecimal totalMoneyMax,
|
||||
String createdAtStart,
|
||||
@@ -41,4 +43,9 @@ public interface ILbBuyerShoppingService extends IService<LbBuyerShopping> {
|
||||
* 按手机号模拟第三方登录获取 token,分页拉取买方订单列表并解析入库。
|
||||
*/
|
||||
Map<String, Object> pullFromThirdParty(LbBuyerShoppingPullRequest request);
|
||||
|
||||
/**
|
||||
* 按买家 ID 将明细汇总为 {@code day_stat} 或 {@code sum_data} 并写入表。
|
||||
*/
|
||||
Map<String, Object> generateBuyerStat(LbBuyerShoppingGenerateStatRequest request);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
8
src/main/sql/lb_buyer_shopping_alter_add_stat_fields.sql
Normal file
8
src/main/sql/lb_buyer_shopping_alter_add_stat_fields.sql
Normal 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`;
|
||||
Reference in New Issue
Block a user