Merge remote-tracking branch 'origin/master'

This commit is contained in:
2026-02-11 18:02:38 +08:00
25 changed files with 635 additions and 107 deletions

View File

@@ -149,6 +149,7 @@ public class CreditCompanyController extends BaseController {
List<String> errorMessages = new ArrayList<>();
int insertedCount = 0;
Set<String> touchedMatchNames = new HashSet<>();
String refreshWarning = null;
try {
List<CreditCompanyImportParam> list = null;
@@ -324,13 +325,31 @@ public class CreditCompanyController extends BaseController {
}
}
}
creditCompanyRecordCountService.refreshAll(touchedCompanyIds);
try {
creditCompanyRecordCountService.refreshAll(touchedCompanyIds);
} catch (Exception ex) {
// 导入本身已经成功写入,回填计数字段失败不应导致整个导入失败(可后续单独重试刷新)。
String msg = ex.getMessage();
if (msg != null && msg.length() > 300) {
msg = msg.substring(0, 300) + "...";
}
refreshWarning = "关联记录数回填失败:" + (msg != null ? msg : ex.getClass().getSimpleName());
ex.printStackTrace();
}
}
if (errorMessages.isEmpty()) {
return success("成功入库" + insertedCount + "条数据", null);
String msg = "成功入库" + insertedCount + "条数据";
if (refreshWarning != null) {
msg = msg + "" + refreshWarning;
}
return success(msg, null);
} else {
return success("导入完成,入库" + insertedCount + "条,失败" + errorMessages.size() + "", errorMessages);
String msg = "导入完成,入库" + insertedCount + "条,失败" + errorMessages.size() + "";
if (refreshWarning != null) {
msg = msg + "" + refreshWarning;
}
return success(msg, errorMessages);
}
} catch (Exception e) {

View File

@@ -82,16 +82,24 @@ public class CreditCompanyRecordCountService {
}
// 表/字段名来自固定枚举,不允许外部传入,避免 SQL 注入。
//
// 这里不要用「UPDATE ... SET col=(SELECT COUNT... WHERE t.company_id=c.id)」的相关子查询:
// 如果关联表缺少 (company_id, deleted) 索引,会导致对每个 company_id 都进行一次全表扫描,极慢,
// 进而容易触发 JDBC/MySQL 侧的超时/断链Communications link failure
//
// 用聚合后再 JOIN 的方式:每个 chunk 只扫描一次关联表。
String sql = ""
+ "UPDATE credit_company c "
+ "SET " + type.getCompanyColumn() + " = ("
+ " SELECT COUNT(1) "
+ " FROM " + type.getSourceTable() + " t "
+ " WHERE t.company_id = c.id AND t.deleted = 0"
+ ") "
+ "LEFT JOIN ("
+ " SELECT company_id, COUNT(1) AS cnt "
+ " FROM " + type.getSourceTable() + " "
+ " WHERE deleted = 0 AND company_id IN (:ids) "
+ " GROUP BY company_id"
+ ") t ON t.company_id = c.id "
+ "SET c." + type.getCompanyColumn() + " = IFNULL(t.cnt, 0) "
+ "WHERE c.id IN (:ids)";
final int inChunkSize = 1000;
final int inChunkSize = 500;
for (int i = 0; i < ids.size(); i += inChunkSize) {
List<Integer> chunk = ids.subList(i, Math.min(ids.size(), i + inChunkSize));
namedJdbc.update(sql, new MapSqlParameterSource("ids", chunk));

View File

@@ -242,6 +242,25 @@ public class GltTicketOrderController extends BaseController {
@PutMapping()
public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) {
if (gltTicketOrderService.updateById(gltTicketOrder)) {
Integer tenantId = getTenantId();
// 后台指派配送员(直接改 riderId同步商城订单为“已发货”(deliveryStatus=20)
if (gltTicketOrder != null
&& gltTicketOrder.getId() != null
&& gltTicketOrder.getRiderId() != null
&& gltTicketOrder.getRiderId() > 0) {
gltTicketOrderService.markShopOrderShippedAfterRiderAssigned(
gltTicketOrder.getId(),
tenantId,
gltTicketOrder.getRiderId()
);
}
// 后台直接改“已完成”(deliveryStatus=40)时,同步商城订单为“已完成”(orderStatus=1)
if (gltTicketOrder != null
&& gltTicketOrder.getId() != null
&& gltTicketOrder.getDeliveryStatus() != null
&& gltTicketOrder.getDeliveryStatus() == GltTicketOrderService.DELIVERY_STATUS_FINISHED) {
gltTicketOrderService.markShopOrderCompletedAfterTicketFinished(gltTicketOrder.getId(), tenantId);
}
return success("修改成功");
}
return fail("修改失败");

View File

@@ -31,6 +31,10 @@ public class GltTicketOrder implements Serializable {
@Schema(description = "用户水票ID")
private Integer userTicketId;
@Schema(description = "订单编号")
@TableField(exist = false)
private String orderNo;
@Schema(description = "门店ID")
private Integer storeId;

View File

@@ -10,13 +10,16 @@
u.nickname, u.phone, u.avatar,
d.name as receiverName, d.phone as receiverPhone,
d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion,
d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng
d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng,
COALESCE(o.order_no, f.order_no) as orderNo
FROM glt_ticket_order a
LEFT JOIN shop_store b ON a.store_id = b.id
LEFT JOIN shop_store_warehouse w ON a.warehouse_id = w.id
LEFT JOIN shop_store_rider c ON a.rider_id = c.user_id
LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id
LEFT JOIN shop_user_address d ON a.address_id = d.id
LEFT JOIN glt_user_ticket f ON a.user_ticket_id = f.id
LEFT JOIN shop_order o ON f.order_id = o.order_id AND f.tenant_id = o.tenant_id AND o.deleted = 0
<where>
<if test="param.id != null">

View File

@@ -168,9 +168,6 @@ public class GltTicketIssueService {
new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, order.getOrderId())
.eq(ShopOrder::getTenantId, tenantId)
.set(ShopOrder::getOrderStatus, 1)
// 同步更新发货状态为“已发货”
.set(ShopOrder::getDeliveryStatus, 20)
.set(ShopOrder::getHasTakeGift, true)
.set(ShopOrder::getUpdateTime, now)
);

View File

@@ -64,6 +64,20 @@ public interface GltTicketOrderService extends IService<GltTicketOrder> {
*/
void accept(Integer id, Integer riderId, Integer tenantId);
/**
* 指派/接单成功后,同步关联商城订单发货状态为“已发货”(deliveryStatus=20)。
*
* <p>用于后台指派配送员(不走接单接口)等场景的状态兜底同步。</p>
*/
void markShopOrderShippedAfterRiderAssigned(Integer ticketOrderId, Integer tenantId, Integer riderId);
/**
* 送水订单完成后,同步关联商城订单为“已完成”(orderStatus=1)。
*
* <p>用于后台直接改 deliveryStatus=40 等不经过 confirmReceive/autoConfirmTimeout 的兜底同步。</p>
*/
void markShopOrderCompletedAfterTicketFinished(Integer ticketOrderId, Integer tenantId);
/**
* 配送员开始配送10 -> 20并写 sendStartTime。
*/

View File

@@ -1,6 +1,8 @@
package com.gxwebsoft.glt.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.web.PageParam;
@@ -18,8 +20,12 @@ import com.gxwebsoft.glt.service.GltUserTicketLogService;
import com.gxwebsoft.glt.service.GltUserTicketService;
import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.stereotype.Service;
@@ -70,6 +76,12 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
@Resource
private UserMapper userMapper;
@Resource
private ShopOrderService shopOrderService;
@Resource
private ShopOrderGoodsService shopOrderGoodsService;
@Override
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
@@ -192,6 +204,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
}
@Override
@Transactional(rollbackFor = Exception.class)
public void accept(Integer id, Integer riderId, Integer tenantId) {
if (id == null) {
throw new BusinessException("订单id不能为空");
@@ -204,9 +217,10 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
}
// 原子接单:避免并发抢单
LocalDateTime now = LocalDateTime.now();
boolean ok = this.lambdaUpdate()
.set(GltTicketOrder::getRiderId, riderId)
.set(GltTicketOrder::getUpdateTime, LocalDateTime.now())
.set(GltTicketOrder::getUpdateTime, now)
.eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
@@ -215,6 +229,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
.and(w -> w.isNull(GltTicketOrder::getRiderId).or().eq(GltTicketOrder::getRiderId, 0))
.update();
if (ok) {
// 接单成功后同步商城订单发货状态10未发货 -> 20已发货
updateShopOrderDeliveryStatusAfterAccept(id, tenantId, riderId, now);
return;
}
@@ -233,6 +249,146 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
throw new BusinessException("订单状态不允许接单");
}
@Override
public void markShopOrderShippedAfterRiderAssigned(Integer ticketOrderId, Integer tenantId, Integer riderId) {
updateShopOrderDeliveryStatusAfterAccept(ticketOrderId, tenantId, riderId, LocalDateTime.now());
}
@Override
public void markShopOrderCompletedAfterTicketFinished(Integer ticketOrderId, Integer tenantId) {
updateShopOrderOrderStatusAfterTicketFinished(ticketOrderId, tenantId, LocalDateTime.now());
}
private void updateShopOrderDeliveryStatusAfterAccept(Integer ticketOrderId, Integer tenantId, Integer riderId, LocalDateTime now) {
if (ticketOrderId == null || tenantId == null) {
return;
}
// 找到关联水票的商城订单glt_user_ticket.orderId / orderNo
GltTicketOrder ticketOrder = this.lambdaQuery()
.select(GltTicketOrder::getId, GltTicketOrder::getUserTicketId, GltTicketOrder::getRiderId)
.eq(GltTicketOrder::getId, ticketOrderId)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.last("limit 1")
.one();
if (ticketOrder == null || ticketOrder.getUserTicketId() == null) {
return;
}
Integer actualRiderId = (riderId != null && riderId > 0) ? riderId : ticketOrder.getRiderId();
GltUserTicket userTicket = gltUserTicketService.getOne(
new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, ticketOrder.getUserTicketId())
.last("limit 1")
);
if (userTicket == null) {
return;
}
Integer shopOrderId = userTicket.getOrderId();
String shopOrderNo = userTicket.getOrderNo();
boolean resolvedByOrderGoodsId = false;
// 兼容历史数据:部分水票可能只写了 orderGoodsId未写 orderId/orderNo此处兜底通过 orderGoodsId 反查 ShopOrder.orderId。
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo) && userTicket.getOrderGoodsId() != null) {
ShopOrderGoods og = shopOrderGoodsService.getOne(
new LambdaQueryWrapper<ShopOrderGoods>()
.select(ShopOrderGoods::getOrderId)
.eq(ShopOrderGoods::getTenantId, tenantId)
.eq(ShopOrderGoods::getId, userTicket.getOrderGoodsId())
.last("limit 1")
);
if (og != null) {
shopOrderId = og.getOrderId();
resolvedByOrderGoodsId = shopOrderId != null;
}
}
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo)) {
log.warn("同步商城订单发货状态失败:未找到关联商城订单 - tenantId={}, ticketOrderId={}, userTicketId={}, userTicket.orderId={}, userTicket.orderNo={}, userTicket.orderGoodsId={}",
tenantId, ticketOrderId, userTicket.getId(), userTicket.getOrderId(), userTicket.getOrderNo(), userTicket.getOrderGoodsId());
return;
}
// 若是通过 orderGoodsId 兜底反查到 orderId则顺便回填 glt_user_ticket.order_id/order_no减少后续同步/查询依赖兜底分支。
if (resolvedByOrderGoodsId && userTicket.getOrderId() == null && shopOrderId != null) {
if (!StringUtils.hasText(shopOrderNo)) {
ShopOrder order = shopOrderService.getOne(
new LambdaQueryWrapper<ShopOrder>()
.select(ShopOrder::getOrderNo)
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderId, shopOrderId)
.last("limit 1")
);
if (order != null) {
shopOrderNo = order.getOrderNo();
}
}
LambdaUpdateWrapper<GltUserTicket> backfill = new LambdaUpdateWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, userTicket.getId());
backfill.set(GltUserTicket::getOrderId, shopOrderId);
if (!StringUtils.hasText(userTicket.getOrderNo()) && StringUtils.hasText(shopOrderNo)) {
backfill.set(GltUserTicket::getOrderNo, shopOrderNo);
}
backfill.set(GltUserTicket::getUpdateTime, now);
try {
gltUserTicketService.update(backfill);
} catch (Exception e) {
log.debug("回填水票关联商城订单信息失败(不影响主流程) - tenantId={}, userTicketId={}, orderId={}, orderNo={}",
tenantId, userTicket.getId(), shopOrderId, shopOrderNo, e);
}
}
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
// deliveryStatus 已经是 20 时也可能需要补写/更新 riderId因此条件包含 riderId 不一致的场景
.and(w -> {
w.ne(ShopOrder::getDeliveryStatus, 20).or().isNull(ShopOrder::getDeliveryStatus);
if (actualRiderId != null && actualRiderId > 0) {
w.or().ne(ShopOrder::getRiderId, actualRiderId).or().isNull(ShopOrder::getRiderId);
}
})
.set(ShopOrder::getDeliveryStatus, 20)
.set(ShopOrder::getUpdateTime, now);
if (actualRiderId != null && actualRiderId > 0) {
uw.set(ShopOrder::getRiderId, actualRiderId);
}
if (shopOrderId != null) {
uw.eq(ShopOrder::getOrderId, shopOrderId);
} else {
uw.eq(ShopOrder::getOrderNo, shopOrderNo);
}
boolean updated = shopOrderService.update(uw);
if (updated) {
return;
}
// 幂等:若已是 20则视为成功否则记录日志便于排查关联关系/数据缺失
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getDeliveryStatus, 20);
if (actualRiderId != null && actualRiderId > 0) {
qw.eq(ShopOrder::getRiderId, actualRiderId);
}
if (shopOrderId != null) {
qw.eq(ShopOrder::getOrderId, shopOrderId);
} else {
qw.eq(ShopOrder::getOrderNo, shopOrderNo);
}
if (shopOrderService.count(qw) <= 0) {
log.warn("接单/指派成功但同步商城订单发货状态/配送员失败 - tenantId={}, ticketOrderId={}, riderId={}, shopOrderId={}, shopOrderNo={}",
tenantId, ticketOrderId, actualRiderId, shopOrderId, shopOrderNo);
}
}
@Override
public void start(Integer id, Integer riderId, Integer tenantId) {
if (id == null) {
@@ -310,6 +466,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (ok) {
// 配送员拍照上传送达后可触发提成结算(幂等,重复调用不会重复入账)
settleRiderCommissionIfEligible(id, tenantId, true);
// 配送员确认送达后,同步商城订单为“已完成”(orderStatus=1)
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, now);
return;
}
@@ -329,6 +487,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|| order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED)) {
// 幂等:重复送达视为成功
settleRiderCommissionIfEligible(id, tenantId, true);
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, LocalDateTime.now());
return;
}
throw new BusinessException("订单状态不允许确认送达");
@@ -362,6 +521,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (ok) {
// 用户确认收货完成后触发配送员提成结算(幂等,重复调用不会重复入账)
settleRiderCommissionIfEligible(id, tenantId, false);
// 送水订单完成后,同步商城订单为“已完成”(orderStatus=1)
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, now);
return;
}
@@ -379,6 +540,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED) {
// 幂等:重复确认收货视为成功
settleRiderCommissionIfEligible(id, tenantId, false);
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, LocalDateTime.now());
return;
}
throw new BusinessException("订单状态不允许确认收货");
@@ -437,6 +599,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
}
// 超时自动确认收货后,也按“完成”逻辑触发配送员提成结算(幂等)。
settleRiderCommissionIfEligible(id, tenantId, false);
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, nowFinal);
return true;
});
if (Boolean.TRUE.equals(ok)) {
@@ -449,6 +612,125 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
return confirmed;
}
private void updateShopOrderOrderStatusAfterTicketFinished(Integer ticketOrderId, Integer tenantId, LocalDateTime now) {
if (ticketOrderId == null || tenantId == null) {
return;
}
if (now == null) {
now = LocalDateTime.now();
}
// 找到关联水票的商城订单glt_user_ticket.orderId / orderNo
GltTicketOrder ticketOrder = this.lambdaQuery()
.select(GltTicketOrder::getId, GltTicketOrder::getUserTicketId)
.eq(GltTicketOrder::getId, ticketOrderId)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.last("limit 1")
.one();
if (ticketOrder == null || ticketOrder.getUserTicketId() == null) {
return;
}
GltUserTicket userTicket = gltUserTicketService.getOne(
new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, ticketOrder.getUserTicketId())
.last("limit 1")
);
if (userTicket == null) {
return;
}
Integer shopOrderId = userTicket.getOrderId();
String shopOrderNo = userTicket.getOrderNo();
boolean resolvedByOrderGoodsId = false;
// 兼容历史数据:部分水票可能只写了 orderGoodsId未写 orderId/orderNo此处兜底通过 orderGoodsId 反查 ShopOrder.orderId。
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo) && userTicket.getOrderGoodsId() != null) {
ShopOrderGoods og = shopOrderGoodsService.getOne(
new LambdaQueryWrapper<ShopOrderGoods>()
.select(ShopOrderGoods::getOrderId)
.eq(ShopOrderGoods::getTenantId, tenantId)
.eq(ShopOrderGoods::getId, userTicket.getOrderGoodsId())
.last("limit 1")
);
if (og != null) {
shopOrderId = og.getOrderId();
resolvedByOrderGoodsId = shopOrderId != null;
}
}
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo)) {
log.warn("送水订单完成但未找到关联商城订单,无法同步完成状态 - tenantId={}, ticketOrderId={}, userTicketId={}, userTicket.orderId={}, userTicket.orderNo={}, userTicket.orderGoodsId={}",
tenantId, ticketOrderId, userTicket.getId(), userTicket.getOrderId(), userTicket.getOrderNo(), userTicket.getOrderGoodsId());
return;
}
// 若是通过 orderGoodsId 兜底反查到 orderId则顺便回填 glt_user_ticket.order_id/order_no减少后续同步/查询依赖兜底分支。
if (resolvedByOrderGoodsId && userTicket.getOrderId() == null && shopOrderId != null) {
if (!StringUtils.hasText(shopOrderNo)) {
ShopOrder order = shopOrderService.getOne(
new LambdaQueryWrapper<ShopOrder>()
.select(ShopOrder::getOrderNo)
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderId, shopOrderId)
.last("limit 1")
);
if (order != null) {
shopOrderNo = order.getOrderNo();
}
}
LambdaUpdateWrapper<GltUserTicket> backfill = new LambdaUpdateWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, userTicket.getId());
backfill.set(GltUserTicket::getOrderId, shopOrderId);
if (!StringUtils.hasText(userTicket.getOrderNo()) && StringUtils.hasText(shopOrderNo)) {
backfill.set(GltUserTicket::getOrderNo, shopOrderNo);
}
backfill.set(GltUserTicket::getUpdateTime, now);
try {
gltUserTicketService.update(backfill);
} catch (Exception e) {
log.debug("回填水票关联商城订单信息失败(不影响主流程) - tenantId={}, userTicketId={}, orderId={}, orderNo={}",
tenantId, userTicket.getId(), shopOrderId, shopOrderNo, e);
}
}
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.and(w -> w.ne(ShopOrder::getOrderStatus, 1).or().isNull(ShopOrder::getOrderStatus))
.set(ShopOrder::getOrderStatus, 1)
.set(ShopOrder::getUpdateTime, now);
if (shopOrderId != null) {
uw.eq(ShopOrder::getOrderId, shopOrderId);
} else {
uw.eq(ShopOrder::getOrderNo, shopOrderNo);
}
boolean updated = shopOrderService.update(uw);
if (updated) {
return;
}
// 幂等:若已是 1则视为成功否则记录日志便于排查关联关系/数据缺失
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderStatus, 1);
if (shopOrderId != null) {
qw.eq(ShopOrder::getOrderId, shopOrderId);
} else {
qw.eq(ShopOrder::getOrderNo, shopOrderNo);
}
if (shopOrderService.count(qw) <= 0) {
log.warn("送水订单完成但同步商城订单完成状态失败 - tenantId={}, ticketOrderId={}, shopOrderId={}, shopOrderNo={}",
tenantId, ticketOrderId, shopOrderId, shopOrderNo);
}
}
private void settleRiderCommissionIfEligible(Integer ticketOrderId, Integer tenantId, boolean requirePhoto) {
if (ticketOrderId == null || tenantId == null) {
return;

View File

@@ -1,6 +1,7 @@
package com.gxwebsoft.glt.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltTicketTemplate;
@@ -9,9 +10,11 @@ import com.gxwebsoft.glt.service.GltTicketOrderService;
import com.gxwebsoft.glt.service.GltTicketTemplateService;
import com.gxwebsoft.glt.service.GltUserTicketService;
import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopOrderService;
import lombok.extern.slf4j.Slf4j;
@@ -65,6 +68,9 @@ public class DealerCommissionUnfreeze10584Task {
@Resource
private ShopDealerCapitalService shopDealerCapitalService;
@Resource
private ShopDealerOrderService shopDealerOrderService;
@Resource
private ShopDealerUserService shopDealerUserService;
@@ -79,7 +85,7 @@ public class DealerCommissionUnfreeze10584Task {
private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0 */1 * * * ?}")
@Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/30 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() {
if (!running.compareAndSet(false, true)) {
@@ -355,12 +361,56 @@ public class DealerCommissionUnfreeze10584Task {
marker.setUpdateTime(now);
shopDealerCapitalService.save(marker);
// 佣金全部解冻完成后,将分销订单状态置为“已解冻”(0)。
// 以当前任务生成的 flowType=50 marker 数量作为完成度判断,避免提前将订单置为已解冻。
setDealerOrderUnfrozenIfCompleted(orderNo, now);
log.info("佣金解冻成功 - tenantId={}, orderNo={}, dealerUserId={}, amount={}, capitalId={}",
TENANT_ID, orderNo, dealerUserId, amount, capitalId);
return true;
}));
}
private void setDealerOrderUnfrozenIfCompleted(String orderNo, LocalDateTime now) {
if (orderNo == null || orderNo.isBlank()) {
return;
}
long totalCommissions = shopDealerCapitalService.count(
new LambdaQueryWrapper<ShopDealerCapital>()
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
.eq(ShopDealerCapital::getFlowType, 10)
.eq(ShopDealerCapital::getOrderNo, orderNo)
);
if (totalCommissions <= 0) {
return;
}
long unfrozenMarkers = shopDealerCapitalService.count(
new LambdaQueryWrapper<ShopDealerCapital>()
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
.eq(ShopDealerCapital::getFlowType, 50)
.eq(ShopDealerCapital::getOrderNo, orderNo)
.like(ShopDealerCapital::getComments, "佣金解冻(capitalId=")
);
if (unfrozenMarkers < totalCommissions) {
return;
}
boolean updated = shopDealerOrderService.update(
new LambdaUpdateWrapper<ShopDealerOrder>()
.eq(ShopDealerOrder::getTenantId, TENANT_ID)
.eq(ShopDealerOrder::getOrderNo, orderNo)
.set(ShopDealerOrder::getIsUnfreeze, 1)
.set(ShopDealerOrder::getUnfreezeTime, now)
.set(ShopDealerOrder::getUpdateTime, now)
);
if (!updated) {
log.warn("已完成佣金解冻但更新分销订单isUnfreeze失败/无记录 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
}
}
private String buildUnfreezeMarkerComment(Integer capitalId) {
return "佣金解冻(capitalId=" + capitalId + ")";
}

View File

@@ -38,7 +38,7 @@ import java.util.*;
/**
* 租户10584分销订单结算任务
* <p>
* 每20秒执行一次查询“已付款且未结算”的订单按指定规则计算佣金并先计入分销商冻结金额freezeMoney并将订单置为已结算。
* 每10秒执行一次查询“已付款且未结算”的订单按指定规则计算佣金并先计入分销商冻结金额freezeMoney并将订单置为已结算。
*/
@Slf4j
@Component
@@ -88,9 +88,9 @@ public class DealerOrderSettlement10584Task {
private UserMapper userMapper;
/**
* 每30秒执行一次。
* 每10秒执行一次。
*/
@Scheduled(cron = "0/20 * * * * ?")
@Scheduled(cron = "0/10 * * * * ?")
@IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常")
public void settleTenant10584Orders() {
try {

View File

@@ -16,7 +16,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
/**
* GLT 套票发放任务:
* - 每分钟扫描一次今日订单tenantId=10584, formId in 套票模板 goodsId, payStatus=1, orderStatus=0
* - 每30秒扫描一次今日订单tenantId=10584, formId in 套票模板 goodsId, payStatus=1, orderStatus=0
* - 为订单生成用户套票账户 + 释放计划(幂等)
* - 若模板配置了 startSendQty则发放时自动核销对应数量用于“第一次送水”场景
*/
@@ -33,7 +33,7 @@ public class GltTicketIssue10584Task {
private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(cron = "${glt.ticket.issue10584.cron:0 */1 * * * ?}")
@Scheduled(cron = "${glt.ticket.issue10584.cron:0/15 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() {
if (!running.compareAndSet(false, true)) {

View File

@@ -34,7 +34,7 @@ public class GltTicketOrderAutoConfirm10584Task {
private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(cron = "${glt.ticket-order.auto-confirm10584.cron:0 */1 * * * ?}")
@Scheduled(cron = "${glt.ticket-order.auto-confirm10584.cron:0/33 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() {
if (!running.compareAndSet(false, true)) {
@@ -53,4 +53,3 @@ public class GltTicketOrderAutoConfirm10584Task {
}
}
}

View File

@@ -30,7 +30,7 @@ public class GltUserTicketAutoReleaseTask {
private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(cron = "${glt.ticket.auto-release.cron:0 */1 * * * ?}")
@Scheduled(cron = "${glt.ticket.auto-release.cron:0 */10 * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;释放记录自带 tenantId更新时会校验 tenantId")
public void run() {
if (!running.compareAndSet(false, true)) {

View File

@@ -1,14 +1,14 @@
package com.gxwebsoft.shop.controller;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.service.ShopUserService;
import com.gxwebsoft.shop.entity.ShopUser;
import com.gxwebsoft.shop.param.ShopUserParam;
import com.gxwebsoft.shop.service.ShopUserService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -21,7 +21,7 @@ import java.util.List;
* 用户记录表控制器
*
* @author 科技小王子
* @since 2025-10-03 13:41:09
* @since 2026-02-10 00:37:07
*/
@Tag(name = "用户记录表管理")
@RestController
@@ -46,11 +46,12 @@ public class ShopUserController extends BaseController {
return success(shopUserService.listRel(param));
}
@Operation(summary = "根据userId查询用户记录表")
@GetMapping("/{userId}")
public ApiResult<ShopUser> get(@PathVariable("userId") Integer userId) {
@PreAuthorize("hasAuthority('shop:shopUser:list')")
@Operation(summary = "根据id查询用户记录表")
@GetMapping("/{id}")
public ApiResult<ShopUser> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopUserService.getByIdRel(userId));
return success(shopUserService.getByIdRel(id));
}
@PreAuthorize("hasAuthority('shop:shopUser:save')")
@@ -59,10 +60,10 @@ public class ShopUserController extends BaseController {
@PostMapping()
public ApiResult<?> save(@RequestBody ShopUser shopUser) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
shopUser.setUserId(loginUser.getUserId());
}
// User loginUser = getLoginUser();
// if (loginUser != null) {
// shopUser.setUserId(loginUser.getUserId());
// }
if (shopUserService.save(shopUser)) {
return success("添加成功");
}

View File

@@ -120,9 +120,15 @@ public class ShopDealerOrder implements Serializable {
@Schema(description = "佣金结算(0未结算 1已结算)")
private Integer isSettled;
@Schema(description = "佣金冻结(1解冻中 0已解冻)")
private Integer isUnfreeze;
@Schema(description = "结算时间")
private LocalDateTime settleTime;
@Schema(description = "解冻时间")
private LocalDateTime unfreezeTime;
@Schema(description = "备注")
private String comments;

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -101,6 +102,7 @@ public class ShopGoods implements Serializable {
private BigDecimal secondDividend;
@Schema(description = "库存计算方式(10下单减库存 20付款减库存)")
@JsonAlias({"cdeductStockType"})
private Integer deductStockType;
@Schema(description = "交付方式(0不启用)")

View File

@@ -1,37 +1,34 @@
package com.gxwebsoft.shop.entity;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import java.time.LocalDate;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* 用户记录表
*
* @author 科技小王子
* @since 2025-10-03 13:41:09
* @since 2026-02-10 00:37:07
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(description = "用户记录表")
@Schema(name = "ShopUser对象", description = "用户记录表")
public class ShopUser implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "用户id")
@TableId(value = "user_id", type = IdType.AUTO)
private Integer userId;
@Schema(description = "用户类型 0个人用户 1企业用户 2其他")
@Schema(description = "用户类型 0普通用户 1企业用户 2特殊用户")
private Integer type;
@Schema(description = "账号")
@@ -71,6 +68,7 @@ public class ShopUser implements Serializable {
private String idCard;
@Schema(description = "出生日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate birthday;
@Schema(description = "所在国家")
@@ -143,7 +141,7 @@ public class ShopUser implements Serializable {
private Integer age;
@Schema(description = "是否线下会员")
private Boolean offline;
private Integer offline;
@Schema(description = "关注数")
private Integer followers;
@@ -178,6 +176,9 @@ public class ShopUser implements Serializable {
@Schema(description = "是否管理员")
private Boolean isAdmin;
@Schema(description = "默认账号(适用于不同租户存在相同的手机号码)")
private Boolean isDefault;
@Schema(description = "是否企业管理员")
private Boolean isOrganizationAdmin;
@@ -209,6 +210,7 @@ public class ShopUser implements Serializable {
private Integer expireTime;
@Schema(description = "最后结算时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime settlementTime;
@Schema(description = "资质")
@@ -246,9 +248,11 @@ public class ShopUser implements Serializable {
private Integer tenantId;
@Schema(description = "注册时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -12,7 +12,7 @@ import java.util.List;
* 用户记录表Mapper
*
* @author 科技小王子
* @since 2025-10-03 13:41:09
* @since 2026-02-10 00:37:07
*/
public interface ShopUserMapper extends BaseMapper<ShopUser> {

View File

@@ -7,9 +7,6 @@
SELECT a.*
FROM shop_user a
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
@@ -160,6 +157,9 @@
<if test="param.isAdmin != null">
AND a.is_admin = #{param.isAdmin}
</if>
<if test="param.isDefault != null">
AND a.is_default = #{param.isDefault}
</if>
<if test="param.isOrganizationAdmin != null">
AND a.is_organization_admin = #{param.isOrganizationAdmin}
</if>

View File

@@ -1,37 +1,32 @@
package com.gxwebsoft.shop.param;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.math.BigDecimal;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 用户记录表查询参数
*
* @author 科技小王子
* @since 2025-10-03 13:41:08
* @since 2026-02-10 00:37:06
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "用户记录表查询参数")
@Schema(name = "ShopUserParam对象", description = "用户记录表查询参数")
public class ShopUserParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "id")
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "用户id")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "用户类型 0个人用户 1企业用户 2其他")
@Schema(description = "用户类型 0普通用户 1企业用户 2特殊用户")
@QueryField(type = QueryType.EQ)
private Integer type;
@@ -157,7 +152,7 @@ public class ShopUserParam extends BaseParam {
@Schema(description = "是否线下会员")
@QueryField(type = QueryType.EQ)
private Boolean offline;
private Integer offline;
@Schema(description = "关注数")
@QueryField(type = QueryType.EQ)
@@ -199,6 +194,10 @@ public class ShopUserParam extends BaseParam {
@QueryField(type = QueryType.EQ)
private Boolean isAdmin;
@Schema(description = "默认账号(适用于不同租户存在相同的手机号码)")
@QueryField(type = QueryType.EQ)
private Boolean isDefault;
@Schema(description = "是否企业管理员")
@QueryField(type = QueryType.EQ)
private Boolean isOrganizationAdmin;

View File

@@ -32,6 +32,8 @@ import java.util.Map;
@Slf4j
@Service
public class OrderBusinessService {
private static final int DEDUCT_STOCK_TYPE_ORDER = 10; // 下单减库存
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存
@Resource
private ShopOrderService shopOrderService;
@@ -713,6 +715,15 @@ public class OrderBusinessService {
*/
private void deductStock(OrderCreateRequest request) {
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) {
// 付款减库存的商品:创建订单时不扣库存
ShopGoods goodsForType = shopGoodsService.getById(item.getGoodsId());
Integer deductStockType = goodsForType != null ? goodsForType.getDeductStockType() : null;
if (deductStockType != null && deductStockType == DEDUCT_STOCK_TYPE_PAY) {
log.debug("跳过下单扣库存(付款减库存) - goodsId={}, skuId={}, qty={}",
item.getGoodsId(), item.getSkuId(), item.getQuantity());
continue;
}
if (item.getSkuId() != null) {
// 多规格商品扣减SKU库存
ShopGoodsSku sku = shopGoodsSkuService.getById(item.getSkuId());

View File

@@ -11,7 +11,7 @@ import java.util.List;
* 用户记录表Service
*
* @author 科技小王子
* @since 2025-10-03 13:41:09
* @since 2026-02-10 00:37:07
*/
public interface ShopUserService extends IService<ShopUser> {

View File

@@ -24,6 +24,7 @@ import java.util.List;
@Slf4j
@Service
public class OrderCancelServiceImpl implements OrderCancelService {
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存:下单不扣库存,因此未支付取消无需回退
@Autowired
private ShopOrderService shopOrderService;
@@ -182,6 +183,20 @@ public class OrderCancelServiceImpl implements OrderCancelService {
}
for (ShopOrderGoods orderGood : orderGoods) {
// 付款减库存的商品:创建订单时未扣库存,未支付取消时无需回退(避免库存被“加多”)
try {
ShopGoods goods = shopGoodsService.getById(orderGood.getGoodsId());
if (goods != null && goods.getDeductStockType() != null && goods.getDeductStockType() == DEDUCT_STOCK_TYPE_PAY) {
log.debug("跳过未支付取消的库存回退(付款减库存) - orderId={}, goodsId={}, skuId={}, qty={}",
order.getOrderId(), orderGood.getGoodsId(), orderGood.getSkuId(), orderGood.getTotalNum());
continue;
}
} catch (Exception e) {
// 查不到商品或查询异常时,保持原有回退逻辑,避免出现“库存无法回退”的更坏情况
log.warn("读取商品扣库存方式失败,继续执行库存回退 - orderId={}, goodsId={}",
order.getOrderId(), orderGood.getGoodsId(), e);
}
if (orderGood.getSkuId() != null && orderGood.getSkuId() > 0) {
// 多规格商品恢复SKU库存
restoreSkuStock(orderGood);

View File

@@ -1,8 +1,10 @@
package com.gxwebsoft.shop.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.context.TenantContext;
import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.config.CertificateProperties;
import com.gxwebsoft.common.core.utils.*;
@@ -86,10 +88,13 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
private ShopOrderDeliveryService shopOrderDeliveryService;
@Resource
private ShopExpressService shopExpressService;
@Resource
private ShopGoodsSkuService shopGoodsSkuService;
private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L;
private static final long WECHAT_PREPAY_SNAPSHOT_TTL_MINUTES = 30L;
private static final String WECHAT_PREPAY_SNAPSHOT_KEY_PREFIX = "wxpay:prepay:snapshot:";
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存
@Data
private static class WechatPrepaySnapshot {
@@ -769,10 +774,15 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
try {
Transaction result = service.queryOrderByOutTradeNo(queryRequest);
if (result.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) {
if (Boolean.TRUE.equals(shopOrder.getPayStatus())) {
// 已是支付成功状态,避免重复触发支付成功后的业务逻辑(销量/库存等)
return true;
}
shopOrder.setPayStatus(true);
shopOrder.setPayTime(LocalDateTime.now());
shopOrder.setTransactionId(result.getTransactionId());
updateById(shopOrder);
handlePaymentSuccess(shopOrder);
return true;
}
} catch (ServiceException e) {
@@ -801,6 +811,9 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
*/
private void handlePaymentSuccess(ShopOrder order) {
try {
// 0. 付款减库存:支付成功后扣库存(下单时不扣)
deductStockAfterPaidIfNeeded(order);
// 1. 使用优惠券
if (order.getCouponId() != null && order.getCouponId() > 0) {
markCouponAsUsed(order);
@@ -816,6 +829,90 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
}
}
/**
* 付款减库存:支付成功后按商品设置扣减库存。
*
* 注意:此处使用忽略租户隔离 + 显式 tenantId 条件,避免支付回调没有 tenantId header 时更新失败。
* 若扣减失败(库存不足/数据缺失),仅记录日志,不阻断支付回调主流程。
*/
private void deductStockAfterPaidIfNeeded(ShopOrder order) {
if (order == null || order.getOrderId() == null || order.getTenantId() == null) {
return;
}
final List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.getListByOrderIdIgnoreTenant(order.getOrderId());
if (CollectionUtils.isEmpty(orderGoodsList)) {
return;
}
TenantContext.runIgnoreTenant(() -> {
for (ShopOrderGoods og : orderGoodsList) {
if (og == null || og.getGoodsId() == null) {
continue;
}
int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
if (qty <= 0) {
continue;
}
ShopGoods goods = shopGoodsService.getById(og.getGoodsId());
Integer deductStockType = goods != null ? goods.getDeductStockType() : null;
if (deductStockType == null || deductStockType != DEDUCT_STOCK_TYPE_PAY) {
continue;
}
try {
if (og.getSkuId() != null && og.getSkuId() > 0) {
// 多规格:扣 SKU 库存
boolean updated = shopGoodsSkuService.update(
new LambdaUpdateWrapper<ShopGoodsSku>()
.eq(ShopGoodsSku::getId, og.getSkuId())
.eq(ShopGoodsSku::getTenantId, order.getTenantId())
.apply("IFNULL(stock,0) >= {0}", qty)
.setSql("stock = IFNULL(stock,0) - " + qty)
);
if (!updated) {
log.warn("支付成功后扣SKU库存失败 - tenantId={}, orderId={}, skuId={}, goodsId={}, qty={}",
order.getTenantId(), order.getOrderId(), og.getSkuId(), og.getGoodsId(), qty);
// 兜底库存不足时至少将库存置0避免出现“明明已售出但库存仍大于0”的情况
shopGoodsSkuService.update(
new LambdaUpdateWrapper<ShopGoodsSku>()
.eq(ShopGoodsSku::getId, og.getSkuId())
.eq(ShopGoodsSku::getTenantId, order.getTenantId())
.apply("IFNULL(stock,0) < {0}", qty)
.setSql("stock = 0")
);
}
} else {
// 单规格:扣商品库存
boolean updated = shopGoodsService.update(
new LambdaUpdateWrapper<ShopGoods>()
.eq(ShopGoods::getGoodsId, og.getGoodsId())
.eq(ShopGoods::getTenantId, order.getTenantId())
.apply("IFNULL(stock,0) >= {0}", qty)
.setSql("stock = IFNULL(stock,0) - " + qty)
);
if (!updated) {
log.warn("支付成功后扣商品库存失败 - tenantId={}, orderId={}, goodsId={}, qty={}",
order.getTenantId(), order.getOrderId(), og.getGoodsId(), qty);
// 兜底库存不足时至少将库存置0避免出现“明明已售出但库存仍大于0”的情况
shopGoodsService.update(
new LambdaUpdateWrapper<ShopGoods>()
.eq(ShopGoods::getGoodsId, og.getGoodsId())
.eq(ShopGoods::getTenantId, order.getTenantId())
.apply("IFNULL(stock,0) < {0}", qty)
.setSql("stock = 0")
);
}
}
} catch (Exception e) {
log.warn("支付成功后扣库存异常 - tenantId={}, orderId={}, goodsId={}, skuId={}, qty={}",
order.getTenantId(), order.getOrderId(), og.getGoodsId(), og.getSkuId(), qty, e);
}
}
});
}
/**
* 标记优惠券为已使用
*/
@@ -1452,43 +1549,41 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
@Override
public boolean syncPaymentStatus(String orderNo, Integer paymentStatus, String transactionId, String payTime, Integer tenantId) {
try {
// 查询订单
ShopOrder order = getByOrderNo(orderNo, tenantId);
if (order == null) {
log.warn("同步支付状态失败:订单不存在, orderNo={}, tenantId={}", orderNo, tenantId);
return false;
}
// 如果订单已经是支付成功状态,不需要更新
if (order.getPayStatus() && paymentStatus == 1) {
log.info("订单已经是支付成功状态,无需更新: orderNo={}", orderNo);
return true;
}
// 更新订单状态
boolean updated = this.lambdaUpdate()
.eq(ShopOrder::getOrderNo, orderNo)
.eq(ShopOrder::getTenantId, tenantId)
.set(ShopOrder::getPayStatus, paymentStatus == 1)
.set(transactionId != null, ShopOrder::getTransactionId, transactionId)
.set(payTime != null, ShopOrder::getPayTime, payTime)
.set(ShopOrder::getUpdateTime, LocalDateTime.now())
.update();
if (updated) {
log.info("订单支付状态同步成功: orderNo={}, paymentStatus={}, transactionId={}",
orderNo, paymentStatus, transactionId);
} else {
log.warn("订单支付状态同步失败: orderNo={}, paymentStatus={}", orderNo, paymentStatus);
}
return updated;
} catch (Exception e) {
log.error("同步订单支付状态异常: orderNo={}, error={}", orderNo, e.getMessage(), e);
// 查询订单
ShopOrder order = getByOrderNo(orderNo, tenantId);
if (order == null) {
log.warn("同步支付状态失败:订单不存在, orderNo={}, tenantId={}", orderNo, tenantId);
return false;
}
// 如果订单已经是支付成功状态,不需要更新
if (Boolean.TRUE.equals(order.getPayStatus()) && paymentStatus == 1) {
log.info("订单已经是支付成功状态,无需更新: orderNo={}", orderNo);
return true;
}
// 更新订单状态
boolean updated = this.lambdaUpdate()
.eq(ShopOrder::getOrderNo, orderNo)
.eq(ShopOrder::getTenantId, tenantId)
.set(ShopOrder::getPayStatus, paymentStatus == 1)
.set(transactionId != null, ShopOrder::getTransactionId, transactionId)
.set(payTime != null, ShopOrder::getPayTime, payTime)
.set(ShopOrder::getUpdateTime, LocalDateTime.now())
.update();
if (updated) {
log.info("订单支付状态同步成功: orderNo={}, paymentStatus={}, transactionId={}",
orderNo, paymentStatus, transactionId);
// paymentStatus=1时同步触发支付成功后业务逻辑包括付款减库存/销量等)
if (paymentStatus != null && paymentStatus == 1) {
handlePaymentSuccess(order);
}
} else {
log.warn("订单支付状态同步失败: orderNo={}, paymentStatus={}", orderNo, paymentStatus);
}
return updated;
}

View File

@@ -1,12 +1,12 @@
package com.gxwebsoft.shop.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.shop.mapper.ShopUserMapper;
import com.gxwebsoft.shop.service.ShopUserService;
import com.gxwebsoft.shop.entity.ShopUser;
import com.gxwebsoft.shop.param.ShopUserParam;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopUser;
import com.gxwebsoft.shop.mapper.ShopUserMapper;
import com.gxwebsoft.shop.param.ShopUserParam;
import com.gxwebsoft.shop.service.ShopUserService;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -15,7 +15,7 @@ import java.util.List;
* 用户记录表Service实现
*
* @author 科技小王子
* @since 2025-10-03 13:41:09
* @since 2026-02-10 00:37:07
*/
@Service
public class ShopUserServiceImpl extends ServiceImpl<ShopUserMapper, ShopUser> implements ShopUserService {