Compare commits

...

7 Commits

Author SHA1 Message Date
fdb3e7a4bd chore: 合并 2026-08-06 15:57:58 +08:00
3cfd76e6f1 feat:增加获取课程下一章节详情接口 2026-08-06 15:47:26 +08:00
wyn
24bb7b5eac Merge branch 'master' of https://git.nuttyreading.com/zm/nuttyreading-server 2026-08-05 09:29:29 +08:00
wyn
21ba335057 强制提交 2026-08-05 09:28:32 +08:00
wyn
cbbfaecf80 强制提交 2026-08-04 14:51:33 +08:00
wyn
3ffa65863c 套装书下单直接出库单品 2026-08-04 13:42:47 +08:00
5a4c0fb4fc feat:增加APP端修改地址接口 2026-07-27 11:38:06 +08:00
14 changed files with 400 additions and 126 deletions

View File

@@ -354,3 +354,31 @@ CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROU
CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
-- 批量发货异步任务
CREATE TABLE IF NOT EXISTS buy_order_batch_delivery_task (
id INT AUTO_INCREMENT PRIMARY KEY,
express_company_code VARCHAR(10) NOT NULL,
total_count INT NOT NULL DEFAULT 0,
success_count INT NOT NULL DEFAULT 0,
fail_count INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1处理中 2已完成 3异常终止',
fail_message VARCHAR(1000) NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
finish_time DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS buy_order_batch_delivery_item (
id INT AUTO_INCREMENT PRIMARY KEY,
task_id INT NOT NULL,
order_id INT NOT NULL,
order_sn VARCHAR(64) NULL,
buy_order_product_id INT NOT NULL,
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1成功 2失败',
fail_message VARCHAR(500) NULL,
create_time DATETIME NULL,
update_time DATETIME NULL,
INDEX idx_task_id (task_id),
INDEX idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -1091,26 +1091,30 @@ public class BuyOrderController {
* @param addressRequestVo 地址请求 value object
* @return R
*/
@RequestMapping(value = "/modifyConsigneeAddress", method = RequestMethod.POST)
public R modifyOrderAddress(@RequestBody ModifyOrderAddressRequestVo addressRequestVo) {
QueryWrapper<BuyOrder> buyOrderQueryWrapper = new QueryWrapper<>();
buyOrderQueryWrapper.eq("order_sn", addressRequestVo.getOrderSn());
BuyOrder buyOrder = buyOrderService.getOne(buyOrderQueryWrapper);
String provinceCode = addressRequestVo.getProvinceCode();
QueryWrapper<Province> provinceQueryWrapper = new QueryWrapper<>();
provinceQueryWrapper.eq("region_code", provinceCode);
Province province = provinceService.getOne(provinceQueryWrapper);
private R saveConsigneeAddress(ModifyOrderAddressRequestVo addressRequestVo, BuyOrder buyOrder, Province province) {
if (province == null) {
String provinceCode = addressRequestVo.getProvinceCode();
QueryWrapper<Province> provinceQueryWrapper = new QueryWrapper<>();
provinceQueryWrapper.eq("region_code", provinceCode);
province = provinceService.getOne(provinceQueryWrapper);
}
if (province == null) return R.error(500, "省份信息不存在");
buyOrder.setProvince(province.getProvName());
String cityCode = addressRequestVo.getCityCode();
QueryWrapper<City> cityQueryWrapper = new QueryWrapper<>();
cityQueryWrapper.eq("region_code", cityCode);
City city = cityService.getOne(cityQueryWrapper);
if (city == null) return R.error(500, "城市信息不存在");
buyOrder.setCity(city.getCityName());
String countyCode = addressRequestVo.getCountyCode();
QueryWrapper<County> countyQueryWrapper = new QueryWrapper<>();
countyQueryWrapper.eq("region_code", countyCode);
County county = countyService.getOne(countyQueryWrapper);
if (county == null) return R.error(500, "区县信息不存在");
buyOrder.setDistrict(county.getCountyName());
buyOrder.setShippingUser(addressRequestVo.getConsigneeName());
buyOrder.setUserPhone(addressRequestVo.getConsigneeMobile());
buyOrder.setAddress(addressRequestVo.getAddress());
@@ -1118,10 +1122,86 @@ public class BuyOrderController {
if (str.contains("+")||str.contains("&")) {
return R.error(500, "信息中不能含有“+”、“&”符号!");
}
buyOrderService.updateById(buyOrder);
return R.ok();
}
/**
* 修改订单地址
*
* @param addressRequestVo 地址请求 value object
* @return R
*/
@RequestMapping(value = "/modifyConsigneeAddress", method = RequestMethod.POST)
public R modifyOrderAddress(@RequestBody ModifyOrderAddressRequestVo addressRequestVo) {
QueryWrapper<BuyOrder> buyOrderQueryWrapper = new QueryWrapper<>();
buyOrderQueryWrapper.eq("order_sn", addressRequestVo.getOrderSn());
BuyOrder buyOrder = buyOrderService.getOne(buyOrderQueryWrapper);
if (buyOrder == null) {
return R.error(500,"订单不存在");
}
// 后台修改地址时addressId 设为 0
buyOrder.setAddressId(0);
return saveConsigneeAddress(addressRequestVo, buyOrder, null);
}
/**
* APP修改订单地址
*
* @param addressRequestVo 地址请求 value object
* @return R
*/
@RequestMapping(value = "/modifyConsigneeAddressApp", method = RequestMethod.POST)
public R modifyOrderAddressApp(@RequestBody ModifyOrderAddressRequestVo addressRequestVo) {
QueryWrapper<BuyOrder> buyOrderQueryWrapper = new QueryWrapper<>();
buyOrderQueryWrapper.eq("order_sn", addressRequestVo.getOrderSn());
BuyOrder buyOrder = buyOrderService.getOne(buyOrderQueryWrapper);
if (buyOrder == null) {
return R.error(500,"订单不存在");
}
MyUserEntity userEntity = myUserService.getById(ShiroUtils.getUId());
if (userEntity == null) return R.error(500, "用户不存在");
Integer currentUserId = userEntity.getId();
if (!currentUserId.equals(buyOrder.getUserId())) {
return R.error(500, "无权修改该订单地址");
}
String provinceCode = addressRequestVo.getProvinceCode();
QueryWrapper<Province> provinceQueryWrapper = new QueryWrapper<>();
provinceQueryWrapper.eq("region_code", provinceCode);
Province province = provinceService.getOne(provinceQueryWrapper);
if (province == null) {
return R.error(500, "省份信息不存在");
}
// 仅 App 用户:已支付运费时,新地址省份 code 必须与原订单省份 code 相同;无运费可改任意省
if (buyOrder.getShippingMoney() != null && buyOrder.getShippingMoney().compareTo(BigDecimal.ZERO) > 0) {
QueryWrapper<Province> oldProvinceQueryWrapper = new QueryWrapper<>();
oldProvinceQueryWrapper.eq("prov_name", buyOrder.getProvince());
Province oldProvince = provinceService.getOne(oldProvinceQueryWrapper);
if (oldProvince == null || !provinceCode.equals(oldProvince.getRegionCode())) {
return R.error(500, "该订单已支付运费,只能修改为同省份地址");
}
}
// App 传入 addressId 时更新
if (addressRequestVo.getAddressId() != null) {
buyOrder.setAddressId(addressRequestVo.getAddressId());
} else {
buyOrder.setAddressId(0);
}
// 仅 App 用户修改时 addressModified 每次 +1后台修改不加
buyOrder.setAddressModified(buyOrder.getAddressModified() + 1);
return saveConsigneeAddress(addressRequestVo, buyOrder, province);
}
/**
* 获取订单地址
*

View File

@@ -38,6 +38,14 @@ public interface ShopProductService extends IService<ShopProduct> {
PageUtils queryPageactivityprice(Map<String, Object> params);
void rollbackStock(BuyOrder buyOrder);
/**
* 回滚订单行 shop_product 库存与销量。
* @param skipShopProductIds 已由 inv 出库回滚加过 shop 的商品,跳过库存回加(仍回滚销量)
*/
void rollbackStock(BuyOrder buyOrder, java.util.Set<Integer> skipShopProductIds);
default void rollbackStock(BuyOrder buyOrder) {
rollbackStock(buyOrder, null);
}
}

View File

@@ -1201,6 +1201,7 @@ public class BuyOrderServiceImpl extends ServiceImpl<BuyOrderDao, BuyOrder> impl
return refundFee;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void refundOrder(BuyOrder buyOrder, MyUserEntity user, int refundId){
if(buyOrder.getCouponId()!=null && buyOrder.getCouponId()!=0){
couponService.rollbackCoupon(buyOrder.getCouponId());
@@ -1229,10 +1230,10 @@ public class BuyOrderServiceImpl extends ServiceImpl<BuyOrderDao, BuyOrder> impl
//撤回课程权限
log.info("====remove========="+buyOrder.getOrderSn());
removeCourseToUser(buyOrder);
//回滚库存
shopProductService.rollbackStock(buyOrder);
// 回滚 inv 售出流水:回加采购 remain_quantity售出记录 del_flag=1
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
// 先按出库流水回加 inv remain + 出库商品 shop再回加订单行中未出现在出库流水的商品 shop如套书本身
java.util.Set<Integer> invRestoredShopIds =
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
shopProductService.rollbackStock(buyOrder, invRestoredShopIds);
}
}

View File

@@ -141,14 +141,23 @@ public class ShopProductServiceImpl extends ServiceImpl<ShopProductDao, ShopProd
}
@Override
public void rollbackStock(BuyOrder buyOrder){
public void rollbackStock(BuyOrder buyOrder, java.util.Set<Integer> skipShopProductIds){
QueryWrapper<BuyOrderProduct> buyOrderProductQueryWrapper = new QueryWrapper<>();
buyOrderProductQueryWrapper.eq("order_id", buyOrder.getOrderId());
List<BuyOrderProduct> buyOrderProductList = buyOrderProductService.list(buyOrderProductQueryWrapper);
for (BuyOrderProduct buyOrderProduct : buyOrderProductList) {
Integer productId = buyOrderProduct.getProductId();
ShopProduct product = this.getById(productId);
product.setProductStock(product.getProductStock() + buyOrderProduct.getQuantity());
if (product == null) {
continue;
}
// inv 出库回滚已加过 shop 的商品不再重复加库存;销量仍回滚
if (skipShopProductIds == null || !skipShopProductIds.contains(productId)) {
int stock = product.getProductStock() == null ? 0 : product.getProductStock();
product.setProductStock(stock + buyOrderProduct.getQuantity());
}
int sales = product.getSumSales() == null ? 0 : product.getSumSales();
product.setSumSales(Math.max(0, sales - buyOrderProduct.getQuantity()));
this.updateById(product);
}
}

View File

@@ -37,4 +37,8 @@ public class ModifyOrderAddressRequestVo {
* 详细地址
*/
private String address;
/**
* 用户地址 IDApp 从地址列表选择时传入;后台管理员修改时会将订单 addressId 置为 null
*/
private Integer addressId;
}

View File

@@ -46,9 +46,10 @@ public class InventoryManagementController {
/**
* 采购/售出;
* 采购 sourcels灵枢 zm众秒 other其他 publisher出版社默认 other跨商户采购自动两步
* 采购 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
* 售出出库类型outboundType 或 source取自 sys_dict_data(dict_label=inventory_outbound_type) 的 dict_type
* 套书售出拆成单本出库
*/
@RequestMapping("/purchaseProduct")
public R purchaseProduct(@RequestBody InvStockRecordDto dto) {

View File

@@ -20,19 +20,21 @@ public interface InventoryManagementService {
/**
* 图书订单售出:写入 inv_stock_record 售出流水。
* 天医币支付固定扣众秒库存merchant_id=2其它支付按商品 payMerchant。
* 天医币支付固定扣众秒merchant_id=2其它支付严格按商品 payMerchant1→灵枢0→众秒不跨商户回退
* 套书shop_product_book 关联多本)拆成单本商品分别出库扣库存。
*/
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);
@@ -43,12 +45,14 @@ public interface InventoryManagementService {
List<Map<String, Object>> exportStockStatistics(Map<String, Object> params);
/**
* 按订单回滚库存流水售出:回加采购批次 remain_quantity售出记录 del_flag=1
* 按订单回滚出库:根据 order_id 查出全部售出流水,回加采购 remain
* 并按出库商品回加对应 shop_product返回已回加 shop 的商品ID集合供业务侧避免重复回加。
*/
void rollbackSaleByOrderId(Integer orderId);
java.util.Set<Integer> rollbackSaleByOrderId(Integer orderId);
/**
* Excel 批量售出:全部解析/匹配成功才入库,任一条失败则全部不入库
* Excel 批量售出:全部解析/匹配成功才入库,任一条失败则全部不入库
* 套书行按 App 逻辑拆成单本出库。
*
* @param merchantId 商户ID
* @param outboundType 出库类型 donation/taobao/app

View File

@@ -2,6 +2,7 @@ package com.peanut.modules.master.service.impl;
import com.alipay.api.domain.Product;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
@@ -228,6 +229,13 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
}
if (dto.getBiz_type() == InvStockRecord.BIZ_TYPE_SALE) {
String outboundType = resolveOutboundType(dto.getOutboundType());
ShopProduct saleProduct = shopProductDao.selectById(dto.getProductId());
if (isSetBookProduct(saleProduct)) {
// 套书拆单出库:成本取各单本 FIFO 批次,不用请求里的套书 unitCost
saleSetBook(dto.getMerchantId(), dto.getQuantity(), saleProduct, outboundType,
null, dto.getRemark(), null, null, null);
return;
}
List<CostSplit> splits = deductPurchaseStockFifo(dto.getProductId(), dto.getMerchantId(), dto.getQuantity());
for (CostSplit split : splits) {
BigDecimal saleUnitCost = (dto.getUnitCost() != null && dto.getUnitCost().compareTo(BigDecimal.ZERO) > 0)
@@ -244,6 +252,11 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
throw new RuntimeException("单位成本必须大于0");
}
ShopProduct product = shopProductDao.selectById(dto.getProductId());
if (isSetBookProduct(product)) {
throw new RuntimeException("套书不支持入库");
}
// 采购:跨商户调拨 = 来源方按成本批次多条出库 + 本商户一条采购(成本取接口传值)
// 灵枢→众秒:灵枢出库 outbound_type=zm众秒→灵枢众秒出库 outbound_type=ls
if (isCrossMerchantTransfer(dto.getMerchantId(), dto.getSource())) {
@@ -261,47 +274,11 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
}
// 普通采购source=other 等)
ShopProduct product = shopProductDao.selectById(dto.getProductId());
if (isSetBookProduct(product)) {
purchaseSetBook(dto, product);
return;
}
insertRecord(dto.getProductId(), dto.getMerchantId(), normalizeSource(dto.getSource()),
InvStockRecord.BIZ_TYPE_PURCHASE, dto.getQuantity(), dto.getUnitCost(), dto.getRemark());
adjustShopProductStock(dto.getProductId(), dto.getQuantity());
}
/**
* 套装书采购:套书库存增加,套内每本单品按当前商户 FIFO 出库并扣减 shop_product 库存。
* 套内图书从 shop_product_book 按套书 product_id 关联查询。
*/
private void purchaseSetBook(InvStockRecordDto dto, ShopProduct setProduct) {
List<Integer> bookIds = listBookIdsByProductId(setProduct.getProductId());
if (bookIds.isEmpty()) {
throw new RuntimeException("套书未在shop_product_book中关联图书");
}
int merchantId = dto.getMerchantId();
int quantity = dto.getQuantity();
validateSetBookComponentStock(bookIds, merchantId, quantity, setProduct.getProductId());
String saleSource = merchantIdToSource(merchantId);
String outboundRemark = "套书采购出库[" + setProduct.getProductName() + ",productId=" + setProduct.getProductId() + "]";
Long setPurchaseRecordId = insertRecord(dto.getProductId(), merchantId, normalizeSource(dto.getSource()),
null, InvStockRecord.BIZ_TYPE_PURCHASE, quantity, dto.getUnitCost(), dto.getRemark(),
null, null, null, null, null);
for (Integer bookId : bookIds) {
ShopProduct component = findComponentProductByBookId(bookId, setProduct.getProductId());
List<CostSplit> splits = deductPurchaseStockFifo(component.getProductId(), merchantId, quantity);
for (CostSplit split : splits) {
insertRecord(component.getProductId(), merchantId, saleSource, InvStockRecord.OUTBOUND_BOOKS,
InvStockRecord.BIZ_TYPE_SALE, -split.quantity, split.unitCost, outboundRemark,
null, null, null, split.purchaseRecordId, setPurchaseRecordId);
}
adjustShopProductStock(component.getProductId(), -quantity);
}
adjustShopProductStock(dto.getProductId(), quantity);
}
/** 套书判定shop_product_book 中关联图书数 > 1 */
private boolean isSetBookProduct(ShopProduct product) {
if (product == null || product.getProductId() == null) {
@@ -346,7 +323,7 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
if (candidateId == null) {
continue;
}
// 同一 book_id 可能关联套书与单品,优先取 shop_product_book 中仅关联 1 本书的单品 product_id
// 必须是 shop_product_book 中只绑定了 1 本书的单品 product_id
Long bookCount = shopProductBookDao.selectCount(
new LambdaQueryWrapper<ShopProductBookEntity>()
.eq(ShopProductBookEntity::getProductId, candidateId)
@@ -355,9 +332,6 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
componentProductId = candidateId;
break;
}
if (componentProductId == null) {
componentProductId = candidateId;
}
}
if (componentProductId == null) {
return null;
@@ -431,7 +405,13 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
if (quantity <= 0) {
throw new RuntimeException("售出数量必须大于0");
}
int merchantId = resolveBookOrderMerchantId(productId, buyOrder, product, quantity);
int merchantId = resolveBookOrderMerchantId(buyOrder, product);
if (isSetBookProduct(product)) {
saleSetBook(merchantId, quantity, product, InvStockRecord.OUTBOUND_APP,
null, "套书订单售出[" + product.getProductName() + ",productId=" + productId + "]",
buyOrder, null, null);
return;
}
List<CostSplit> splits = deductPurchaseStockFifo(productId, merchantId, quantity);
for (CostSplit split : splits) {
@@ -440,11 +420,45 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
}
}
/**
* 套书出库:按 shop_product_book 拆成单本商品,分别 FIFO 扣库存并写售出流水。
* App 订单、手工出库、批量出库共用。
*/
private void saleSetBook(Integer merchantId, int quantity, ShopProduct setProduct, String outboundType,
BigDecimal overrideUnitCost, String remark, BuyOrder buyOrder,
Date createTime, String orderSn) {
Integer setProductId = setProduct.getProductId();
List<Integer> bookIds = listBookIdsByProductId(setProductId);
if (bookIds.isEmpty()) {
throw new RuntimeException("套书未在shop_product_book中关联图书");
}
validateSetBookComponentStock(bookIds, merchantId, quantity, setProductId);
String outboundRemark = (remark == null || remark.isBlank())
? "套书出库[" + setProduct.getProductName() + ",productId=" + setProductId + "]"
: remark;
for (Integer bookId : bookIds) {
ShopProduct component = findComponentProductByBookId(bookId, setProductId);
List<CostSplit> splits = deductPurchaseStockFifo(component.getProductId(), merchantId, quantity);
for (CostSplit split : splits) {
BigDecimal saleUnitCost = (overrideUnitCost != null && overrideUnitCost.compareTo(BigDecimal.ZERO) > 0)
? overrideUnitCost : split.unitCost;
insertSaleRecord(component.getProductId(), merchantId, outboundType,
-split.quantity, saleUnitCost, outboundRemark, buyOrder, createTime, orderSn,
split.purchaseRecordId);
}
adjustShopProductStock(component.getProductId(), -quantity);
}
}
/**
* 按订单回滚出库:出库记录有哪些商品,就回加哪些商品的 remain 与 shop_product。
* @return 已回加 shop 的商品ID供取消/退款跳过订单行重复回加
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void rollbackSaleByOrderId(Integer orderId) {
public Set<Integer> rollbackSaleByOrderId(Integer orderId) {
if (orderId == null) {
return;
return Collections.emptySet();
}
List<InvStockRecord> saleRecords = invStockRecordDao.selectList(
new LambdaQueryWrapper<InvStockRecord>()
@@ -453,43 +467,69 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
.orderByDesc(InvStockRecord::getId)
);
if (saleRecords == null || saleRecords.isEmpty()) {
return;
return Collections.emptySet();
}
Map<Integer, Integer> shopRestoreMap = new LinkedHashMap<>();
// 先全部回加 remain失败则整单回滚避免只软删出库流水、库存没加回
for (InvStockRecord sale : saleRecords) {
int restoreQty = sale.getQuantity() == null ? 0 : Math.abs(sale.getQuantity());
if (restoreQty > 0) {
if (sale.getPurchaseRecordId() != null) {
restorePurchaseRemainById(sale.getPurchaseRecordId(), restoreQty);
} else {
restorePurchaseRemain(sale.getProductId(), sale.getMerchantId(), sale.getUnitCost(), restoreQty);
}
if (restoreQty <= 0) {
continue;
}
// @TableLogicdeleteById 将 del_flag 置为 1
if (sale.getPurchaseRecordId() != null) {
restorePurchaseRemainById(sale.getPurchaseRecordId(), restoreQty);
} else {
restorePurchaseRemain(sale.getProductId(), sale.getMerchantId(), sale.getUnitCost(), restoreQty);
}
Integer saleProductId = sale.getProductId();
if (saleProductId != null) {
shopRestoreMap.merge(saleProductId, restoreQty, Integer::sum);
}
}
for (InvStockRecord sale : saleRecords) {
invStockRecordDao.deleteById(sale.getId());
}
for (Map.Entry<Integer, Integer> entry : shopRestoreMap.entrySet()) {
adjustShopProductStock(entry.getKey(), entry.getValue());
}
return shopRestoreMap.keySet();
}
/** 按采购流水ID精确回加 remain_quantity(不超过该批次原始 quantity */
/** 按采购流水ID原子回加 remain_quantity;失败抛错,禁止静默跳过 */
private void restorePurchaseRemainById(Long purchaseRecordId, int restoreQty) {
if (purchaseRecordId == null || restoreQty <= 0) {
throw new RuntimeException("回滚库存参数无效purchaseRecordId=" + purchaseRecordId + ", qty=" + restoreQty);
}
// 原子加回,避免 select+updateById 静默不生效
Integer rows = invStockRecordDao.update(null,
new LambdaUpdateWrapper<InvStockRecord>()
.eq(InvStockRecord::getId, purchaseRecordId)
.eq(InvStockRecord::getBizType, InvStockRecord.BIZ_TYPE_PURCHASE)
.apply("IFNULL(remain_quantity,0) + {0} <= IFNULL(quantity,0)", restoreQty)
.setSql("remain_quantity = IFNULL(remain_quantity,0) + " + restoreQty));
if (rows != null && rows > 0) {
return;
}
InvStockRecord purchase = invStockRecordDao.selectById(purchaseRecordId);
if (purchase == null || purchase.getBizType() == null
|| purchase.getBizType() != InvStockRecord.BIZ_TYPE_PURCHASE) {
return;
|| !Integer.valueOf(InvStockRecord.BIZ_TYPE_PURCHASE).equals(purchase.getBizType())) {
throw new RuntimeException("回滚库存失败采购流水不存在或类型错误purchaseRecordId=" + purchaseRecordId);
}
int originQty = purchase.getQuantity() == null ? 0 : purchase.getQuantity();
int remainQty = purchase.getRemainQuantity() == null ? 0 : purchase.getRemainQuantity();
int capacity = Math.max(0, originQty - remainQty);
int add = Math.min(capacity, restoreQty);
if (add <= 0) {
return;
if (add > 0) {
Integer updated = invStockRecordDao.update(null,
new LambdaUpdateWrapper<InvStockRecord>()
.eq(InvStockRecord::getId, purchaseRecordId)
.eq(InvStockRecord::getBizType, InvStockRecord.BIZ_TYPE_PURCHASE)
.setSql("remain_quantity = IFNULL(remain_quantity,0) + " + add));
if (updated == null || updated <= 0) {
throw new RuntimeException("回滚库存失败更新采购剩余未生效purchaseRecordId=" + purchaseRecordId);
}
}
purchase.setRemainQuantity(remainQty + add);
invStockRecordDao.updateById(purchase);
if (add < restoreQty) {
// 容量不够时按旧逻辑兜底补足(兼容脏数据)
restorePurchaseRemain(purchase.getProductId(), purchase.getMerchantId(),
purchase.getUnitCost(), restoreQty - add);
}
@@ -499,9 +539,12 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
* 按成本单价逆 FIFO 回加采购批次剩余库存(不超过该批次原始 quantity
*/
private void restorePurchaseRemain(Integer productId, Integer merchantId, BigDecimal unitCost, int restoreQty) {
if (productId == null || merchantId == null || restoreQty <= 0) {
if (restoreQty <= 0) {
return;
}
if (productId == null || merchantId == null) {
throw new RuntimeException("回滚库存失败缺少商品或商户productId=" + productId + ", merchantId=" + merchantId);
}
LambdaQueryWrapper<InvStockRecord> wrapper = new LambdaQueryWrapper<InvStockRecord>()
.eq(InvStockRecord::getProductId, productId)
.eq(InvStockRecord::getMerchantId, merchantId)
@@ -523,8 +566,15 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
continue;
}
int add = Math.min(capacity, remaining);
purchase.setRemainQuantity(remainQty + add);
invStockRecordDao.updateById(purchase);
Integer updated = invStockRecordDao.update(null,
new LambdaUpdateWrapper<InvStockRecord>()
.eq(InvStockRecord::getId, purchase.getId())
.eq(InvStockRecord::getBizType, InvStockRecord.BIZ_TYPE_PURCHASE)
.apply("IFNULL(remain_quantity,0) + {0} <= IFNULL(quantity,0)", add)
.setSql("remain_quantity = IFNULL(remain_quantity,0) + " + add));
if (updated == null || updated <= 0) {
continue;
}
remaining -= add;
}
if (remaining > 0 && unitCost != null) {
@@ -541,6 +591,9 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
if (productId == null) {
return false;
}
if (isSetBookProduct(product)) {
return hasSetBookComponentInvRecord(productId, buyOrder);
}
if (isPeanutCoinPay(buyOrder)) {
return hasInvOnMerchant(productId, InvStockRecord.MERCHANT_ZM);
}
@@ -551,12 +604,50 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
return count != null && count > 0;
}
/** 套书:任一单本组件有流水即进入 inv 校验(天医币只认众秒) */
private boolean hasSetBookComponentInvRecord(Integer setProductId, BuyOrder buyOrder) {
List<Integer> bookIds = listBookIdsByProductId(setProductId);
if (bookIds.isEmpty()) {
return false;
}
boolean peanut = isPeanutCoinPay(buyOrder);
for (Integer bookId : bookIds) {
ShopProduct component = findComponentProductByBookId(bookId, setProductId);
if (component == null || component.getProductId() == null) {
continue;
}
if (peanut) {
if (hasInvOnMerchant(component.getProductId(), InvStockRecord.MERCHANT_ZM)) {
return true;
}
} else {
Long count = invStockRecordDao.selectCount(
new LambdaQueryWrapper<InvStockRecord>()
.eq(InvStockRecord::getProductId, component.getProductId())
);
if (count != null && count > 0) {
return true;
}
}
}
return false;
}
@Override
public boolean validateBookInvStock(Integer productId, int quantity, BuyOrder buyOrder, ShopProduct product) {
if (!hasInvStockRecord(productId, buyOrder, product)) {
return true;
}
int merchantId = resolveBookOrderMerchantId(productId, buyOrder, product, quantity);
int merchantId = resolveBookOrderMerchantId(buyOrder, product);
if (isSetBookProduct(product)) {
List<Integer> bookIds = listBookIdsByProductId(productId);
try {
validateSetBookComponentStock(bookIds, merchantId, quantity, productId);
return true;
} catch (RuntimeException ex) {
return false;
}
}
int remain = sumRemainStockByMerchant(productId, merchantId);
return remain > 0 && remain >= quantity;
}
@@ -566,35 +657,16 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
return buyOrder != null && Constants.PAYMENT_METHOD_VIRTUAL.equals(buyOrder.getPaymentMethod());
}
private int resolveBookOrderMerchantId(Integer productId, BuyOrder buyOrder, ShopProduct product, int needQty) {
/**
* 天医币固定众秒;其它支付严格按 payMerchant 映射,不因目标商户剩余为 0 而改扣另一商户。
* payMerchant=1 → 灵枢;否则 → 众秒。
*/
private int resolveBookOrderMerchantId(BuyOrder buyOrder, ShopProduct product) {
if (isPeanutCoinPay(buyOrder)) {
return InvStockRecord.MERCHANT_ZM;
}
int payMerchant = product == null || product.getPayMerchant() == null ? 0 : product.getPayMerchant();
return resolveInvMerchantId(productId, payMerchant, needQty);
}
/**
* 优先按 payMerchant 对应商户扣库存;该商户剩余不足时,回退到另一商户(兼容入库商户与 pay_merchant 不一致)。
*/
private int resolveInvMerchantId(Integer productId, int payMerchant, int needQty) {
int preferred = payMerchant == 1 ? InvStockRecord.MERCHANT_LS : InvStockRecord.MERCHANT_ZM;
int other = preferred == InvStockRecord.MERCHANT_LS ? InvStockRecord.MERCHANT_ZM : InvStockRecord.MERCHANT_LS;
int preferredRemain = sumRemainStockByMerchant(productId, preferred);
if (preferredRemain >= needQty) {
return preferred;
}
int otherRemain = sumRemainStockByMerchant(productId, other);
if (otherRemain >= needQty) {
return other;
}
if (preferredRemain > 0 || hasInvOnMerchant(productId, preferred)) {
return preferred;
}
if (otherRemain > 0 || hasInvOnMerchant(productId, other)) {
return other;
}
return preferred;
return payMerchant == 1 ? InvStockRecord.MERCHANT_LS : InvStockRecord.MERCHANT_ZM;
}
private boolean hasInvOnMerchant(Integer productId, int merchantId) {
@@ -874,11 +946,39 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
Map<Integer, Integer> needQtyMap = new LinkedHashMap<>();
Map<Integer, String> productNameMap = new HashMap<>();
for (BatchSaleRow row : rows) {
needQtyMap.merge(row.productId, row.quantity, Integer::sum);
if (row.matchedName != null && !row.matchedName.isBlank()) {
productNameMap.putIfAbsent(row.productId, row.matchedName);
ShopProduct matched = shopProductDao.selectById(row.productId);
if (isSetBookProduct(matched)) {
List<Integer> bookIds = listBookIdsByProductId(row.productId);
if (bookIds.isEmpty()) {
errors.add("" + row.excelRowNum + "套书未在shop_product_book中关联图书productId=" + row.productId);
continue;
}
for (Integer bookId : bookIds) {
ShopProduct component = findComponentProductByBookId(bookId, row.productId);
if (component == null || component.getProductId() == null) {
String bookName = resolveBookName(bookId);
String bookPart = bookName.isBlank()
? "书籍ID[" + bookId + "]"
: "" + bookName + "》(书籍ID:" + bookId + ")";
errors.add("" + row.excelRowNum + "行:套书拆分失败," + bookPart + "无只绑1本的单品商品");
continue;
}
needQtyMap.merge(component.getProductId(), row.quantity, Integer::sum);
String cName = component.getProductName();
if (cName != null && !cName.isBlank()) {
productNameMap.putIfAbsent(component.getProductId(), cName);
}
}
} else {
needQtyMap.merge(row.productId, row.quantity, Integer::sum);
if (row.matchedName != null && !row.matchedName.isBlank()) {
productNameMap.putIfAbsent(row.productId, row.matchedName);
}
}
}
if (!errors.isEmpty()) {
throw new RuntimeException("全部未入库:" + String.join("", errors));
}
for (Map.Entry<Integer, Integer> entry : needQtyMap.entrySet()) {
int remain = sumRemainStockByMerchant(entry.getKey(), merchantId);
if (remain < entry.getValue()) {
@@ -892,8 +992,14 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
}
for (BatchSaleRow row : rows) {
List<CostSplit> splits = deductPurchaseStockFifo(row.productId, merchantId, row.quantity);
ShopProduct matched = shopProductDao.selectById(row.productId);
String remark = "批量导入售出 orderSn=" + row.orderSn;
if (isSetBookProduct(matched)) {
saleSetBook(merchantId, row.quantity, matched, normalizedOutbound,
null, remark, null, row.createTime, row.orderSn);
continue;
}
List<CostSplit> splits = deductPurchaseStockFifo(row.productId, merchantId, row.quantity);
for (CostSplit split : splits) {
insertSaleRecord(row.productId, merchantId, normalizedOutbound,
-split.quantity, split.unitCost, remark, null, row.createTime, row.orderSn, split.purchaseRecordId);
@@ -1254,7 +1360,7 @@ public class InventoryManagementServiceImpl extends ServiceImpl<InvStockRecordDa
if (component == null || component.getProductId() == null) {
continue;
}
// findComponentProductByBookId 已优先只绑 1 本书;再兜底确认
// findComponentProductByBookId 已要求只绑 1 本书;再兜底确认
if (listBookIdsByProductId(component.getProductId()).size() != 1) {
continue;
}

View File

@@ -74,23 +74,27 @@ public class OrderCancelConsumer {
couponService.rollbackCoupon(couponHistoryId);
}
// 回滚商品库存与销量
// 先按出库流水回加 inv remain + 出库商品 shop_product
java.util.Set<Integer> invRestoredShopIds =
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
// 回滚订单行 shop出库流水已回加过的商品只回销量不再加库存单本避免双加套书订单行仍会加回套书库存
LambdaQueryWrapper<BuyOrderProduct> wrapper = new LambdaQueryWrapper<>();
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);
if (invRestoredShopIds == null || !invRestoredShopIds.contains(b.getProductId())) {
int stock = shopProduct.getProductStock() == null ? 0 : shopProduct.getProductStock();
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);

View File

@@ -148,6 +148,17 @@ public class CourseController {
return R.ok().put("data",chapterDetail);
}
/**
* 获取下一章节详情
* @param param
* @return
*/
@RequestMapping("/getNextCourseCatalogueChapter")
public R getNextCourseCatalogueChapter(@RequestBody ParamTo param){
Map<String, Object> chapterDetail = courseCatalogueChapterService.getNextChapterDetail(param.getId());
return R.ok().put("data",chapterDetail);
}
/**
* 开通免费课程
* @param map

View File

@@ -10,4 +10,5 @@ public interface CourseCatalogueChapterService extends IService<CourseCatalogueC
List<CourseCatalogueChapterEntity> getCourseCatalogueChapterList(int id);
Map<String,Object> getChapterDetail(Integer chapterId);
Map<String, Object> getNextChapterDetail(Integer chapterId);
}

View File

@@ -83,6 +83,23 @@ public class CourseCatalogueChapterServiceImpl extends ServiceImpl<CourseCatalog
return flag;
}
@Override
public Map<String, Object> getNextChapterDetail(Integer chapterId) {
CourseCatalogueChapterEntity currentChapter = this.getById(chapterId);
if (currentChapter==null) return null;
CourseCatalogueChapterEntity nextChapter = this.getOne(
new LambdaQueryWrapper<CourseCatalogueChapterEntity>()
.eq(CourseCatalogueChapterEntity::getCatalogueId, currentChapter.getCatalogueId())
.gt(CourseCatalogueChapterEntity::getSort, currentChapter.getSort() )
.orderByAsc(CourseCatalogueChapterEntity::getSort)
.last("limit 1")
);
if (nextChapter==null) return null;
return getChapterDetail(nextChapter.getId());
}
private int getCurrentVideo(Integer chapterId){
MPJLambdaWrapper<CourseCatalogueChapterVideoEntity> wrapper = new MPJLambdaWrapper<>();

View File

@@ -98,7 +98,7 @@ wxpay:
mchId: 1612860909
serialNo: 679AECB2F7AC4183033F713828892BA640E4EEE3
apiV3Key: 4aYFklzaULeGlr7oJPZ6rHWKcxjihZUF
wechatPayCertificateUrl: F:\hs\nuttyreading-server\src\main\resources\cent\wechatpay_7B5676E3CDF56680D0414A009CE501C844DBE2D6.pem
wechatPayCertificateUrl: F:\hs\nuttyreading-java\src\main\resources\cent\wechatpay_7B5676E3CDF56680D0414A009CE501C844DBE2D6.pem
privateKeyUrl: F:\hs\nuttyreading-java\src\main\resources\cent\apiclient_key.pem
keyPemPath: F:\hs\nuttyreading-java\src\main\resources\cent\apiclient_key.pem
notifyUrl: http://z6f8f828.natappfree.cc/pb/pay/payNotify
@@ -107,9 +107,9 @@ wxpay:
lsMchId: 1700371158
lsSerialNo: 3132D23C3130B74B87890DC6E7C80256C75113B9
lsApiV3Key: 4aYFklzaULeGlr7oJPZ6rHWKcxjihZUF
lsWechatPayCertificateUrl: F:\hs\nuttyreading-server\src\main\resources\cent\lscent\wechatpay_30CFE19A12EDB4D9E0C7DF01DBF457C657566D04.pem
lsPrivateKeyUrl: F:\hs\nuttyreading-server\src\main\resources\cent\lscent\apiclient_key.pem
lsKeyPemPath: F:\hs\nuttyreading-server\src\main\resources\cent\lscent\apiclient_key.pem
lsWechatPayCertificateUrl: F:\hs\nuttyreading-java\src\main\resources\cent\lscent\wechatpay_30CFE19A12EDB4D9E0C7DF01DBF457C657566D04.pem
lsPrivateKeyUrl: F:\hs\nuttyreading-java\src\main\resources\cent\lscent\apiclient_key.pem
lsKeyPemPath: F:\hs\nuttyreading-java\src\main\resources\cent\lscent\apiclient_key.pem
lsNotifyUrl: http://z6f8f828.natappfree.cc/pb/pay/lsPayNotify
lsRefundNotifyUrl: https://testapi.nuttyreading.com/pay/refundNotify