库存管理

This commit is contained in:
wyn
2026-07-15 09:05:55 +08:00
parent e1ff7d4b9e
commit 68d6b88695
12 changed files with 2447 additions and 29 deletions

View File

@@ -31,6 +31,7 @@ import com.peanut.modules.common.dao.UserCourseBuyDao;
import com.peanut.modules.common.entity.*; import com.peanut.modules.common.entity.*;
import com.peanut.modules.common.service.*; import com.peanut.modules.common.service.*;
import com.peanut.modules.master.service.CourseCatalogueService; import com.peanut.modules.master.service.CourseCatalogueService;
import com.peanut.modules.master.service.InventoryManagementService;
import com.peanut.modules.pay.alipay.dto.ReFundDTO; import com.peanut.modules.pay.alipay.dto.ReFundDTO;
import com.peanut.modules.pay.alipay.service.AliPayService; import com.peanut.modules.pay.alipay.service.AliPayService;
import com.peanut.modules.pay.weChatPay.dto.WeChatRefundInfo; import com.peanut.modules.pay.weChatPay.dto.WeChatRefundInfo;
@@ -127,6 +128,8 @@ public class BuyOrderController {
private BuyOrderRefundLogService buyOrderRefundLogService; private BuyOrderRefundLogService buyOrderRefundLogService;
@Autowired @Autowired
private AliPayService aliPayService; private AliPayService aliPayService;
@Autowired
private InventoryManagementService inventoryManagementService;
@RequestMapping(value = "/decomposeShipment", method = RequestMethod.POST) @RequestMapping(value = "/decomposeShipment", method = RequestMethod.POST)
public R decomposeShipment(@RequestBody BuyOrderListRequestVo requestVo) { public R decomposeShipment(@RequestBody BuyOrderListRequestVo requestVo) {
@@ -445,7 +448,7 @@ public class BuyOrderController {
price = shopProductService.getVipPrice(product); price = shopProductService.getVipPrice(product);
} }
} }
if (!handleStock(buyOrderProduct, product)) { if (!handleStock(buyOrderProduct, product, buyOrder)) {
return R.error(500, "库存不足"); return R.error(500, "库存不足");
} }
if (StringUtils.isNotEmpty(mechStr)){ if (StringUtils.isNotEmpty(mechStr)){
@@ -511,6 +514,16 @@ public class BuyOrderController {
buyOrder.setOrderStatus("0"); buyOrder.setOrderStatus("0");
buyOrderService.save(buyOrder); buyOrderService.save(buyOrder);
// 订单已有 orderId 后再写 inv 出库流水(只记 order_id不记 order_sn
for (BuyOrderProduct buyOrderProduct : buyOrderProductList) {
ShopProduct product = shopProductService.getById(buyOrderProduct.getProductId());
if (product != null && isBookProduct(product)
&& inventoryManagementService.hasInvStockRecord(buyOrderProduct.getProductId(), buyOrder, product)) {
inventoryManagementService.saleByBookOrder(
buyOrderProduct.getProductId(), buyOrderProduct.getQuantity(), buyOrder, product);
}
}
//解决购物车相关问题 //解决购物车相关问题
for (BuyOrderProduct buyOrderProduct : buyOrderProductList) { for (BuyOrderProduct buyOrderProduct : buyOrderProductList) {
buyOrderProduct.setOrderId(buyOrder.getOrderId()); buyOrderProduct.setOrderId(buyOrder.getOrderId());
@@ -1259,6 +1272,14 @@ public class BuyOrderController {
return (activityPrice == null || activityPrice.equals(BigDecimal.ZERO)) ? product.getPrice() : activityPrice; return (activityPrice == null || activityPrice.equals(BigDecimal.ZERO)) ? product.getPrice() : activityPrice;
} }
private boolean isBookProduct(ShopProduct product) {
if (product == null || product.getGoodsType() == null) {
return false;
}
System.out.println("=======GoodsType======"+product.getGoodsType());
return "02".equals(product.getGoodsType()) || "03".equals(product.getGoodsType()) || "04".equals(product.getGoodsType());
}
/** /**
* 处理商品库存 * 处理商品库存
* TODO 新版本上线后删除此方法 * TODO 新版本上线后删除此方法
@@ -1295,6 +1316,27 @@ public class BuyOrderController {
shopProductService.updateById(product); shopProductService.updateById(product);
return true; return true;
} }
private boolean handleStock(BuyOrderProduct buyOrderProduct, ShopProduct product, BuyOrder buyOrder) {
int quantity = buyOrderProduct.getQuantity();
if (product.getProductStock() - quantity < 0) {
return false;
}
if (isBookProduct(product)) {
if (inventoryManagementService.hasInvStockRecord(buyOrderProduct.getProductId(), buyOrder, product)) {
Boolean hasProduct = inventoryManagementService.validateBookInvStock(
buyOrderProduct.getProductId(), quantity, buyOrder, product);
if (!hasProduct) {
return false;
}
// inv 出库放到 buyOrder 落库后执行,才能写入 order_id
}
}
product.setProductStock(product.getProductStock() - quantity);
product.setSumSales(product.getSumSales() + quantity);
shopProductService.updateById(product);
return true;
}

View File

@@ -0,0 +1,6 @@
package com.peanut.modules.book.service;
public interface BuyOrderBatchDeliveryAsyncService {
void executeBatchDelivery(Integer taskId);
}

View File

@@ -28,6 +28,7 @@ import com.peanut.modules.book.vo.response.*;
import com.peanut.modules.common.entity.*; import com.peanut.modules.common.entity.*;
import com.peanut.modules.common.service.*; import com.peanut.modules.common.service.*;
import com.peanut.modules.common.vo.CourseCatalogueVo; import com.peanut.modules.common.vo.CourseCatalogueVo;
import com.peanut.modules.master.service.InventoryManagementService;
import com.peanut.modules.oss.service.OssService; import com.peanut.modules.oss.service.OssService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -125,6 +126,8 @@ public class BuyOrderServiceImpl extends ServiceImpl<BuyOrderDao, BuyOrder> impl
private BuyOrderBatchDeliveryItemDao batchDeliveryItemDao; private BuyOrderBatchDeliveryItemDao batchDeliveryItemDao;
@Autowired @Autowired
private BuyOrderBatchDeliveryAsyncService batchDeliveryAsyncService; private BuyOrderBatchDeliveryAsyncService batchDeliveryAsyncService;
@Autowired
private InventoryManagementService inventoryManagementService;
//private static final int BATCH_DELIVERY_MAX_SIZE = 100; //private static final int BATCH_DELIVERY_MAX_SIZE = 100;
@Override @Override
@@ -1228,6 +1231,8 @@ public class BuyOrderServiceImpl extends ServiceImpl<BuyOrderDao, BuyOrder> impl
removeCourseToUser(buyOrder); removeCourseToUser(buyOrder);
//回滚库存 //回滚库存
shopProductService.rollbackStock(buyOrder); shopProductService.rollbackStock(buyOrder);
// 回滚 inv 售出流水:回加采购 remain_quantity售出记录 del_flag=1
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
} }
} }

View File

@@ -0,0 +1,29 @@
package com.peanut.modules.common.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("buy_order_batch_delivery_item")
public class BuyOrderBatchDeliveryItem {
public static final int STATUS_PENDING = 0;
public static final int STATUS_SUCCESS = 1;
public static final int STATUS_FAILED = 2;
@TableId(type = IdType.AUTO)
private Integer id;
private Integer taskId;
private Integer orderId;
private String orderSn;
private Integer buyOrderProductId;
/** 0待处理 1成功 2失败 */
private Integer status;
private String failMessage;
private Date createTime;
private Date updateTime;
}

View File

@@ -0,0 +1,31 @@
package com.peanut.modules.common.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("buy_order_batch_delivery_task")
public class BuyOrderBatchDeliveryTask {
public static final int STATUS_PENDING = 0;
public static final int STATUS_RUNNING = 1;
public static final int STATUS_COMPLETED = 2;
public static final int STATUS_FAILED = 3;
@TableId(type = IdType.AUTO)
private Integer id;
private String expressCompanyCode;
private Integer totalCount;
private Integer successCount;
private Integer failCount;
/** 0待处理 1处理中 2已完成 3异常终止 */
private Integer status;
private String failMessage;
private Date createTime;
private Date updateTime;
private Date finishTime;
}

View File

@@ -0,0 +1,79 @@
package com.peanut.modules.common.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("inv_stock_record")
public class InvStockRecord {
public static final int BIZ_TYPE_PURCHASE = 1;
public static final int BIZ_TYPE_SALE = 2;
/** 采购来源:灵枢 */
public static final String SOURCE_LS = "ls";
/** 采购来源:众秒 */
public static final String SOURCE_ZM = "zm";
/** 采购来源:其他 */
public static final String SOURCE_OTHER = "other";
/** 采购来源:出版社 */
public static final String SOURCE_PUBLISHER = "publisher";
/** 出库类型:捐赠 */
public static final String OUTBOUND_DONATION = "donation";
/** 出库类型:淘宝 */
public static final String OUTBOUND_TAOBAO = "taobao";
/** 出库类型APP */
public static final String OUTBOUND_APP = "app";
/** 出库类型:套书采购绑定(内部组装,导出售出明细时排除) */
public static final String OUTBOUND_BOOKS = "books";
public static final int MERCHANT_LS = 1;
public static final int MERCHANT_ZM = 2;
/** 订单类型:灵枢 */
public static final String ORDER_TYPE_LS = "lsorder";
/** 订单类型:众秒 */
public static final String ORDER_TYPE_ZM = "order";
@TableId(type = IdType.AUTO)
private Long id;
private Integer productId;
/** 商户标识 1灵枢 2众秒 */
private Integer merchantId;
/**
* 采购来源ls灵枢 zm众秒 other其他 publisher出版社
* 售出时也可存出库类型(兼容)
*/
private String source;
/** 出库类型donation捐赠 taobao淘宝 app仅售出有值 */
private String outboundType;
/** 业务类型 1采购 2售出 */
private Integer bizType;
/** 数量:采购正数,售出负数 */
private Integer quantity;
/** 剩余库存:采购时等于 quantity售出时从采购记录扣减 */
private Integer remainQuantity;
private BigDecimal unitCost;
/** 关联订单ID图书订单售出时有值 */
private Integer orderId;
/** 平台/外部订单编号(淘宝等批量导入、图书订单售出时有值) */
private String orderSn;
/** 订单来源(图书订单售出时有值) */
private Integer come;
/** 出库所扣减的采购流水ID售出时有值退库时回加 remain_quantity */
private Long purchaseRecordId;
/** 套书采购流水ID套书绑定单品出库时有值指向本次套书采购记录 id */
private Long setPurchaseRecordId;
private String remark;
private Date createTime;
/** 0正常 1作废订单超时取消等软删售出流水 */
@TableLogic
private Integer delFlag;
}

View File

@@ -0,0 +1,228 @@
package com.peanut.modules.master.controller;
import com.peanut.common.utils.R;
import com.peanut.modules.common.entity.InvStockRecord;
import com.peanut.modules.master.dto.inventory.InvStockRecordDto;
import com.peanut.modules.master.service.InventoryManagementService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
/**
* 库存管理(采购、售出)
*/
@Slf4j
@RestController("inventoryManagement")
@RequestMapping("master/inventoryManagement")
public class InventoryManagementController {
@Autowired
private InventoryManagementService inventoryManagementService;
@RequestMapping("/getStockRecordList")
public R getStockRecordList(@RequestBody Map<String, Object> params) {
return R.ok().put("result", inventoryManagementService.pageStockRecords(params));
}
@RequestMapping("/getStockRecordDetail")
public R getStockRecordDetail(@RequestBody Map<String, Object> params) {
return R.ok().put("result", inventoryManagementService.getStockRecordDetail(
Long.parseLong(params.get("id").toString())));
}
/**
* 采购/售出;
* 采购 sourcels灵枢 zm众秒 other其他 publisher出版社默认 other跨商户采购自动两步
* 列表返回时 source 按 sys_dict_data(dict_label=inventory_source_type) 的 dict_type→dict_value 转换;
* 售出出库类型outboundType 或 source取自 sys_dict_data(dict_label=inventory_outbound_type) 的 dict_type
*/
@RequestMapping("/purchaseProduct")
public R purchaseProduct(@RequestBody InvStockRecordDto dto) {
try {
if (dto.getBiz_type() != null && dto.getBiz_type() == InvStockRecord.BIZ_TYPE_SALE) {
if ((dto.getOutboundType() == null || dto.getOutboundType().isBlank())
&& dto.getSource() != null && !dto.getSource().isBlank()) {
dto.setOutboundType(dto.getSource());
}
} else if (dto.getSource() == null || dto.getSource().isBlank()) {
dto.setSource(InvStockRecord.SOURCE_OTHER);
}
inventoryManagementService.purchaseProduct(dto);
return R.ok();
} catch (RuntimeException e) {
return R.error(e.getMessage());
}
}
/**
* Excel批量售出参数merchantId、outboundType、file
* Excel列create_time, productName, quantity, ordersn
* 任一条解析/匹配失败则全部不入库
*/
@RequestMapping("/batchOutbound")
public R batchSaleByExcel(@RequestParam("merchantId") Integer merchantId,
@RequestParam("outboundType") String outboundType,
@RequestParam("file") MultipartFile file) {
try {
int count = inventoryManagementService.batchSaleByExcel(merchantId, outboundType, file);
return R.ok().put("count", count);
} catch (RuntimeException e) {
return R.error(e.getMessage());
}
}
@RequestMapping("/stockStatistics")
public R stockStatistics(@RequestBody Map<String, Object> params) {
if (params.get("merchantId") == null || params.get("merchantId").toString().isBlank()) {
return R.error("merchantId为必填");
}
return R.ok().put("result", inventoryManagementService.stockStatistics(params));
}
/** 采购/售出明细全量导出(忽略 current/limit支持 merchantId、productName、startTime、endTime、bizType(1入库/2售出可选) */
@RequestMapping("/exportStockRecordList")
public void exportStockRecordList(HttpServletResponse response, @RequestBody Map<String, Object> params) {
List<Map<String, Object>> maps = inventoryManagementService.exportStockRecordList(params);
Integer bizType = parseBizType(params.get("bizType"));
XSSFWorkbook wb = new XSSFWorkbook();
String fileName;
if (bizType != null && bizType == InvStockRecord.BIZ_TYPE_PURCHASE) {
writePurchaseExportSheet(wb, maps);
fileName = "入库明细.xlsx";
} else if (bizType != null && bizType == InvStockRecord.BIZ_TYPE_SALE) {
writeSaleExportSheet(wb, maps);
fileName = "售出明细.xlsx";
} else {
List<Map<String, Object>> purchaseRows = maps.stream()
.filter(map -> "purchase".equals(cellStr(map.get("exportCategory"))))
.toList();
List<Map<String, Object>> saleRows = maps.stream()
.filter(map -> "sale".equals(cellStr(map.get("exportCategory"))))
.toList();
writePurchaseExportSheet(wb, purchaseRows);
writeSaleExportSheet(wb, saleRows);
fileName = "采购售出明细.xlsx";
}
writeExcel(response, wb, fileName);
}
private Integer parseBizType(Object bizTypeObj) {
if (bizTypeObj == null || bizTypeObj.toString().isBlank()) {
return null;
}
return Integer.parseInt(bizTypeObj.toString().trim());
}
private void writePurchaseExportSheet(XSSFWorkbook wb, List<Map<String, Object>> maps) {
Sheet sheet = wb.createSheet(wb.getNumberOfSheets() == 0 ? "入库明细" : "入库明细");
Row titleRow = sheet.createRow(0);
titleRow.createCell(0).setCellValue("商品ID");
titleRow.createCell(1).setCellValue("商品名称");
titleRow.createCell(2).setCellValue("供应商类型");
titleRow.createCell(3).setCellValue("入库时间");
titleRow.createCell(4).setCellValue("数量");
titleRow.createCell(5).setCellValue("剩余库存");
int rowIndex = 1;
for (Map<String, Object> map : maps) {
Row row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(cellStr(map.get("productId")));
row.createCell(1).setCellValue(cellStr(map.get("productName")));
row.createCell(2).setCellValue(cellStr(map.get("sourceLabel")));
row.createCell(3).setCellValue(cellStr(map.get("createTime")));
row.createCell(4).setCellValue(cellStr(map.get("quantity")));
row.createCell(5).setCellValue(cellStr(map.get("remainQuantity")));
}
}
private void writeSaleExportSheet(XSSFWorkbook wb, List<Map<String, Object>> maps) {
String sheetName = wb.getNumberOfSheets() == 0 ? "售出明细" : "售出明细";
Sheet sheet = wb.createSheet(sheetName);
Row titleRow = sheet.createRow(0);
titleRow.createCell(0).setCellValue("商品ID");
titleRow.createCell(1).setCellValue("商品名称");
titleRow.createCell(2).setCellValue("出库类型");
titleRow.createCell(3).setCellValue("成本");
titleRow.createCell(4).setCellValue("数量");
titleRow.createCell(5).setCellValue("订单编号");
titleRow.createCell(6).setCellValue("出库时间");
titleRow.createCell(7).setCellValue("come");
int rowIndex = 1;
for (Map<String, Object> map : maps) {
Row row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(cellStr(map.get("productId")));
row.createCell(1).setCellValue(cellStr(map.get("productName")));
row.createCell(2).setCellValue(cellStr(map.get("outboundTypeLabel")));
row.createCell(3).setCellValue(cellStr(map.get("unitCost")));
row.createCell(4).setCellValue(cellStr(map.get("quantity")));
row.createCell(5).setCellValue(cellStr(map.get("orderSn")));
row.createCell(6).setCellValue(cellStr(map.get("createTime")));
row.createCell(7).setCellValue(cellStr(map.get("come")));
}
}
/** 商品库存统计全量导出(忽略 current/limit支持 merchantId、productName、startTime、endTime、isAlert */
@RequestMapping("/exportStockStatistics")
public void exportStockStatistics(HttpServletResponse response, @RequestBody Map<String, Object> params) {
List<Map<String, Object>> maps = inventoryManagementService.exportStockStatistics(params);
XSSFWorkbook wb = new XSSFWorkbook();
Sheet sheet = wb.createSheet("商品库存统计");
Row titleRow = sheet.createRow(0);
titleRow.createCell(0).setCellValue("商品名称");
titleRow.createCell(1).setCellValue("采购总数");
titleRow.createCell(2).setCellValue("售出总数");
titleRow.createCell(3).setCellValue("剩余库存");
titleRow.createCell(4).setCellValue("剩余比例");
int rowIndex = 1;
for (Map<String, Object> map : maps) {
Row row = sheet.createRow(rowIndex);
row.createCell(0).setCellValue(cellStr(map.get("productName")));
row.createCell(1).setCellValue(cellStr(map.get("purchaseTotal")));
row.createCell(2).setCellValue(cellStr(map.get("saleTotal")));
row.createCell(3).setCellValue(cellStr(map.get("remainTotal")));
row.createCell(4).setCellValue(cellStr(map.get("remainRate")));
rowIndex++;
}
writeExcel(response, wb, "商品库存统计.xlsx");
}
private String cellStr(Object value) {
return value == null ? "" : value.toString();
}
private void writeExcel(HttpServletResponse response, XSSFWorkbook wb, String fileName) {
OutputStream outputStream = null;
try {
fileName = URLEncoder.encode(fileName, "UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-disposition", "attachment;filename=" + fileName);
outputStream = response.getOutputStream();
wb.write(outputStream);
} catch (IOException e) {
log.error("导出Excel失败", e);
} finally {
try {
if (outputStream != null) {
outputStream.flush();
outputStream.close();
}
wb.close();
} catch (IOException e) {
log.error("关闭Excel流失败", e);
}
}
}
}

View File

@@ -0,0 +1,11 @@
package com.peanut.modules.master.dto.inventory;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class InvCostItemDto {
private BigDecimal unitCost;
private Integer quantity;
}

View File

@@ -0,0 +1,20 @@
package com.peanut.modules.master.dto.inventory;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class InvStockRecordDto {
private Integer productId;
/** 商户标识 1灵枢 2众秒 */
private Integer merchantId;
/** 采购来源ls灵枢 zm众秒 other其他 publisher出版社售出时可兼作出库类型 */
private String source;
/** 出库类型售出可选donation捐赠 taobao淘宝 app */
private String outboundType;
private Integer quantity;
private Integer biz_type;
private BigDecimal unitCost;
private String remark;
}

View File

@@ -0,0 +1,59 @@
package com.peanut.modules.master.service;
import com.alipay.api.domain.Product;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.peanut.modules.common.entity.BuyOrder;
import com.peanut.modules.common.entity.ShopProduct;
import com.peanut.modules.master.dto.inventory.InvStockRecordDto;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
public interface InventoryManagementService {
Page<Map<String, Object>> pageStockRecords(Map<String, Object> params);
Map<String, Object> getStockRecordDetail(Long id);
void purchaseProduct(InvStockRecordDto dto);
/**
* 图书订单售出:写入 inv_stock_record 售出流水。
* 天医币支付固定扣众秒库存merchant_id=2其它支付按商品 payMerchant。
*/
void saleByBookOrder(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product);
/**
* inv_stock_record 中是否存在该商品记录。
* 天医币支付只查众秒merchant_id=2其它支付查对应商户无则任一商户有流水也算
*/
boolean hasInvStockRecord(Integer productId, BuyOrder buyOrder, ShopProduct product);
/**
* 校验图书库存流水:无记录则通过;有记录则剩余库存须不少于购买数量。
* 天医币支付固定校验众秒库存。
*/
boolean validateBookInvStock(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product);
Page<Map<String, Object>> stockStatistics(Map<String, Object> params);
List<Map<String, Object>> exportStockRecordList(Map<String, Object> params);
List<Map<String, Object>> exportStockStatistics(Map<String, Object> params);
/**
* 按订单回滚库存流水售出:回加采购批次 remain_quantity售出记录 del_flag=1
*/
void rollbackSaleByOrderId(Integer orderId);
/**
* Excel 批量售出:全部解析/匹配成功才入库,任一条失败则全部不入库
*
* @param merchantId 商户ID
* @param outboundType 出库类型 donation/taobao/app
* @param file Excelcreate_time, productName, quantity, ordersn
* @return 成功入库条数
*/
int batchSaleByExcel(Integer merchantId, String outboundType, MultipartFile file);
}

View File

@@ -3,17 +3,23 @@ package com.peanut.modules.mq.Consumer;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.peanut.config.Constants; import com.peanut.config.Constants;
import com.peanut.config.DelayQueueConfig; import com.peanut.config.DelayQueueConfig;
import com.peanut.modules.book.service.BuyOrderService;
import com.peanut.modules.common.dao.BuyOrderProductDao; import com.peanut.modules.common.dao.BuyOrderProductDao;
import com.peanut.modules.common.dao.MyUserDao;
import com.peanut.modules.common.dao.ShopProductDao; import com.peanut.modules.common.dao.ShopProductDao;
import com.peanut.modules.common.entity.BuyOrder; import com.peanut.modules.common.entity.BuyOrder;
import com.peanut.modules.common.entity.BuyOrderProduct; import com.peanut.modules.common.entity.BuyOrderProduct;
import com.peanut.modules.common.entity.MyUserEntity;
import com.peanut.modules.common.entity.ShopProduct; import com.peanut.modules.common.entity.ShopProduct;
import com.peanut.modules.book.service.BuyOrderService;
import com.peanut.modules.common.service.CouponService; import com.peanut.modules.common.service.CouponService;
import com.peanut.modules.master.service.InventoryManagementService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
/** /**
@@ -21,6 +27,7 @@ import java.util.List;
* @Author: Cauchy * @Author: Cauchy
* @CreateTime: 2023/10/10 * @CreateTime: 2023/10/10
*/ */
@Slf4j
@Component @Component
public class OrderCancelConsumer { public class OrderCancelConsumer {
@@ -30,36 +37,70 @@ public class OrderCancelConsumer {
BuyOrderProductDao buyOrderProductDao; BuyOrderProductDao buyOrderProductDao;
@Autowired @Autowired
ShopProductDao shopProductDao; ShopProductDao shopProductDao;
@Autowired
MyUserDao myUserDao;
@Autowired
InventoryManagementService inventoryManagementService;
@Autowired
CouponService couponService;
/**
* 30分钟未支付取消。
* 临时限制:仅 tester_flag=1 的测试账号订单会执行取消与回滚,正式账号直接跳过。
*/
@RabbitListener(queues = DelayQueueConfig.ORDER_CANCEL_DEAD_LETTER_QUEUE) @RabbitListener(queues = DelayQueueConfig.ORDER_CANCEL_DEAD_LETTER_QUEUE)
@Transactional(rollbackFor = Exception.class)
public void orderConsumer(String orderId) { public void orderConsumer(String orderId) {
if (StringUtils.isNotEmpty(orderId)){ if (StringUtils.isEmpty(orderId)) {
BuyOrder buyOrder = buyOrderService.getById(orderId); return;
if(buyOrder == null){ }
return; BuyOrder buyOrder = buyOrderService.getById(orderId);
} if (buyOrder == null) {
if(Constants.ORDER_STATUS_TO_BE_PAID.equals(buyOrder.getOrderStatus())){ return;
buyOrder.setOrderStatus(Constants.ORDER_STATUS_OUT_OF_TIME); }
//回滚优惠卷 if (!Constants.ORDER_STATUS_TO_BE_PAID.equals(buyOrder.getOrderStatus())) {
if (buyOrder.getCouponId()!=null&&buyOrder.getCouponId()!=0){ return;
buyOrder.setCouponId(null); }
} // TODO 上线全量后删除此测试账号限制
//回滚库存 if (!isTesterOrder(buyOrder)) {
LambdaQueryWrapper<BuyOrderProduct> wrapper = new LambdaQueryWrapper<>(); log.info("订单超时取消跳过:非测试账号, orderId={}, userId={}", buyOrder.getOrderId(), buyOrder.getUserId());
wrapper.eq(BuyOrderProduct::getOrderId,buyOrder.getOrderId()); return;
List<BuyOrderProduct> buyOrderProducts = buyOrderProductDao.selectList(wrapper); }
for (BuyOrderProduct b : buyOrderProducts){ buyOrder.setOrderStatus(Constants.ORDER_STATUS_OUT_OF_TIME);
ShopProduct shopProduct = shopProductDao.selectById(b.getProductId());
if (shopProduct!=null){ // 回滚优惠券buy_order.coupon_id 存的是 coupon_history.id保留订单 couponId 便于查单
shopProduct.setProductStock(shopProduct.getProductStock()+b.getQuantity()); Integer couponHistoryId = buyOrder.getCouponId();
shopProductDao.updateById(shopProduct); if (couponHistoryId != null && couponHistoryId != 0) {
} couponService.rollbackCoupon(couponHistoryId);
} }
buyOrderService.updateById(buyOrder);
} // 回滚商品库存与销量
if(Constants.ORDER_STATUS_OUT_OF_TIME.equals(buyOrder.getOrderStatus())){ LambdaQueryWrapper<BuyOrderProduct> wrapper = new LambdaQueryWrapper<>();
buyOrderService.removeById(buyOrder); wrapper.eq(BuyOrderProduct::getOrderId, buyOrder.getOrderId());
List<BuyOrderProduct> buyOrderProducts = buyOrderProductDao.selectList(wrapper);
for (BuyOrderProduct b : buyOrderProducts) {
ShopProduct shopProduct = shopProductDao.selectById(b.getProductId());
if (shopProduct != null) {
int stock = shopProduct.getProductStock() == null ? 0 : shopProduct.getProductStock();
int sales = shopProduct.getSumSales() == null ? 0 : shopProduct.getSumSales();
int qty = b.getQuantity();
shopProduct.setProductStock(stock + qty);
shopProduct.setSumSales(Math.max(0, sales - qty));
shopProductDao.updateById(shopProduct);
} }
} }
// 回滚 inv 售出流水:回加采购 remain_quantity售出记录 del_flag=1
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
buyOrderService.updateById(buyOrder);
buyOrderService.removeById(buyOrder);
}
private boolean isTesterOrder(BuyOrder buyOrder) {
if (buyOrder.getUserId() == null) {
return false;
}
MyUserEntity user = myUserDao.selectById(buyOrder.getUserId());
return user != null && user.getTesterFlag() != null && user.getTesterFlag() == 1;
} }
} }