Compare commits
5 Commits
wyn
...
24bb7b5eac
| Author | SHA1 | Date | |
|---|---|---|---|
| 24bb7b5eac | |||
| 21ba335057 | |||
| cbbfaecf80 | |||
| 3ffa65863c | |||
| 5a4c0fb4fc |
28
db/mysql.sql
28
db/mysql.sql
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
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 province = provinceService.getOne(provinceQueryWrapper);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单地址
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
// 先按出库流水回加 inv remain + 出库商品 shop;再回加订单行中未出现在出库流水的商品 shop(如套书本身)
|
||||
java.util.Set<Integer> invRestoredShopIds =
|
||||
inventoryManagementService.rollbackSaleByOrderId(buyOrder.getOrderId());
|
||||
shopProductService.rollbackStock(buyOrder, invRestoredShopIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,8 @@ public class ModifyOrderAddressRequestVo {
|
||||
* 详细地址
|
||||
*/
|
||||
private String address;
|
||||
/**
|
||||
* 用户地址 ID(App 从地址列表选择时传入;后台管理员修改时会将订单 addressId 置为 null)
|
||||
*/
|
||||
private Integer addressId;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,10 @@ public class InventoryManagementController {
|
||||
|
||||
/**
|
||||
* 采购/售出;
|
||||
* 采购 source:ls灵枢 zm众秒 other其他 publisher出版社,默认 other;跨商户采购自动两步;
|
||||
* 采购 source:ls灵枢 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) {
|
||||
|
||||
@@ -20,19 +20,21 @@ public interface InventoryManagementService {
|
||||
|
||||
/**
|
||||
* 图书订单售出:写入 inv_stock_record 售出流水。
|
||||
* 天医币支付固定扣众秒库存(merchant_id=2);其它支付按商品 payMerchant。
|
||||
* 天医币支付固定扣众秒(merchant_id=2);其它支付严格按商品 payMerchant(1→灵枢,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
|
||||
|
||||
@@ -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 (restoreQty <= 0) {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
// @TableLogic:deleteById 将 del_flag 置为 1
|
||||
}
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
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);
|
||||
|
||||
@@ -17,7 +17,7 @@ spring:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
druid:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://rm-2zev4157t67trxuu3yo.mysql.rds.aliyuncs.com:3306/e_book_test?rewriteBatchedStatements=true
|
||||
url: jdbc:mysql://rm-2zev4157t67trxuu3yo.mysql.rds.aliyuncs.com:3306/e_book_test2?rewriteBatchedStatements=true
|
||||
# username: root
|
||||
# password: HSXY1234hsxy
|
||||
# password: Jgll2023Nutty
|
||||
@@ -53,10 +53,10 @@ spring:
|
||||
initSQL: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci
|
||||
|
||||
rabbitmq:
|
||||
host: 47.93.127.115
|
||||
host: 127.0.0.1
|
||||
port: 5672
|
||||
username: admin
|
||||
password: 751019
|
||||
username: guest
|
||||
password: guest
|
||||
virtualHost: /
|
||||
aliyun:
|
||||
oss:
|
||||
@@ -99,8 +99,8 @@ wxpay:
|
||||
serialNo: 679AECB2F7AC4183033F713828892BA640E4EEE3
|
||||
apiV3Key: 4aYFklzaULeGlr7oJPZ6rHWKcxjihZUF
|
||||
wechatPayCertificateUrl: F:\hs\nuttyreading-server\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
|
||||
privateKeyUrl: F:\hs\nuttyreading-server\src\main\resources\cent\apiclient_key.pem
|
||||
keyPemPath: F:\hs\nuttyreading-server\src\main\resources\cent\apiclient_key.pem
|
||||
notifyUrl: http://z6f8f828.natappfree.cc/pb/pay/payNotify
|
||||
refundNotifyUrl: https://testapi.nuttyreading.com/pay/refundNotify
|
||||
#灵枢商户号
|
||||
|
||||
Reference in New Issue
Block a user