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<>(); List<String> errorMessages = new ArrayList<>();
int insertedCount = 0; int insertedCount = 0;
Set<String> touchedMatchNames = new HashSet<>(); Set<String> touchedMatchNames = new HashSet<>();
String refreshWarning = null;
try { try {
List<CreditCompanyImportParam> list = null; List<CreditCompanyImportParam> list = null;
@@ -324,13 +325,31 @@ public class CreditCompanyController extends BaseController {
} }
} }
} }
try {
creditCompanyRecordCountService.refreshAll(touchedCompanyIds); 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()) { if (errorMessages.isEmpty()) {
return success("成功入库" + insertedCount + "条数据", null); String msg = "成功入库" + insertedCount + "条数据";
if (refreshWarning != null) {
msg = msg + "" + refreshWarning;
}
return success(msg, null);
} else { } 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) { } catch (Exception e) {

View File

@@ -82,16 +82,24 @@ public class CreditCompanyRecordCountService {
} }
// 表/字段名来自固定枚举,不允许外部传入,避免 SQL 注入。 // 表/字段名来自固定枚举,不允许外部传入,避免 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 = "" String sql = ""
+ "UPDATE credit_company c " + "UPDATE credit_company c "
+ "SET " + type.getCompanyColumn() + " = (" + "LEFT JOIN ("
+ " SELECT COUNT(1) " + " SELECT company_id, COUNT(1) AS cnt "
+ " FROM " + type.getSourceTable() + " t " + " FROM " + type.getSourceTable() + " "
+ " WHERE t.company_id = c.id AND t.deleted = 0" + " 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)"; + "WHERE c.id IN (:ids)";
final int inChunkSize = 1000; final int inChunkSize = 500;
for (int i = 0; i < ids.size(); i += inChunkSize) { for (int i = 0; i < ids.size(); i += inChunkSize) {
List<Integer> chunk = ids.subList(i, Math.min(ids.size(), i + inChunkSize)); List<Integer> chunk = ids.subList(i, Math.min(ids.size(), i + inChunkSize));
namedJdbc.update(sql, new MapSqlParameterSource("ids", chunk)); namedJdbc.update(sql, new MapSqlParameterSource("ids", chunk));

View File

@@ -242,6 +242,25 @@ public class GltTicketOrderController extends BaseController {
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) { public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) {
if (gltTicketOrderService.updateById(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 success("修改成功");
} }
return fail("修改失败"); return fail("修改失败");

View File

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

View File

@@ -10,13 +10,16 @@
u.nickname, u.phone, u.avatar, u.nickname, u.phone, u.avatar,
d.name as receiverName, d.phone as receiverPhone, d.name as receiverName, d.phone as receiverPhone,
d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion, 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 FROM glt_ticket_order a
LEFT JOIN shop_store b ON a.store_id = b.id 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_warehouse w ON a.warehouse_id = w.id
LEFT JOIN shop_store_rider c ON a.rider_id = c.user_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 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 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> <where>
<if test="param.id != null"> <if test="param.id != null">

View File

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

View File

@@ -64,6 +64,20 @@ public interface GltTicketOrderService extends IService<GltTicketOrder> {
*/ */
void accept(Integer id, Integer riderId, Integer tenantId); 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。 * 配送员开始配送10 -> 20并写 sendStartTime。
*/ */

View File

@@ -1,6 +1,8 @@
package com.gxwebsoft.glt.service.impl; package com.gxwebsoft.glt.service.impl;
import cn.hutool.core.util.StrUtil; 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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.exception.BusinessException; import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.web.PageParam; 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.glt.service.GltUserTicketService;
import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerUser; 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.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerUserService; import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -70,6 +76,12 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
@Resource @Resource
private UserMapper userMapper; private UserMapper userMapper;
@Resource
private ShopOrderService shopOrderService;
@Resource
private ShopOrderGoodsService shopOrderGoodsService;
@Override @Override
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) { public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param); PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
@@ -192,6 +204,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void accept(Integer id, Integer riderId, Integer tenantId) { public void accept(Integer id, Integer riderId, Integer tenantId) {
if (id == null) { if (id == null) {
throw new BusinessException("订单id不能为空"); throw new BusinessException("订单id不能为空");
@@ -204,9 +217,10 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} }
// 原子接单:避免并发抢单 // 原子接单:避免并发抢单
LocalDateTime now = LocalDateTime.now();
boolean ok = this.lambdaUpdate() boolean ok = this.lambdaUpdate()
.set(GltTicketOrder::getRiderId, riderId) .set(GltTicketOrder::getRiderId, riderId)
.set(GltTicketOrder::getUpdateTime, LocalDateTime.now()) .set(GltTicketOrder::getUpdateTime, now)
.eq(GltTicketOrder::getId, id) .eq(GltTicketOrder::getId, id)
.eq(GltTicketOrder::getTenantId, tenantId) .eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0) .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)) .and(w -> w.isNull(GltTicketOrder::getRiderId).or().eq(GltTicketOrder::getRiderId, 0))
.update(); .update();
if (ok) { if (ok) {
// 接单成功后同步商城订单发货状态10未发货 -> 20已发货
updateShopOrderDeliveryStatusAfterAccept(id, tenantId, riderId, now);
return; return;
} }
@@ -233,6 +249,146 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
throw new BusinessException("订单状态不允许接单"); 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 @Override
public void start(Integer id, Integer riderId, Integer tenantId) { public void start(Integer id, Integer riderId, Integer tenantId) {
if (id == null) { if (id == null) {
@@ -310,6 +466,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (ok) { if (ok) {
// 配送员拍照上传送达后可触发提成结算(幂等,重复调用不会重复入账) // 配送员拍照上传送达后可触发提成结算(幂等,重复调用不会重复入账)
settleRiderCommissionIfEligible(id, tenantId, true); settleRiderCommissionIfEligible(id, tenantId, true);
// 配送员确认送达后,同步商城订单为“已完成”(orderStatus=1)
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, now);
return; return;
} }
@@ -329,6 +487,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|| order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED)) { || order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED)) {
// 幂等:重复送达视为成功 // 幂等:重复送达视为成功
settleRiderCommissionIfEligible(id, tenantId, true); settleRiderCommissionIfEligible(id, tenantId, true);
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, LocalDateTime.now());
return; return;
} }
throw new BusinessException("订单状态不允许确认送达"); throw new BusinessException("订单状态不允许确认送达");
@@ -362,6 +521,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (ok) { if (ok) {
// 用户确认收货完成后触发配送员提成结算(幂等,重复调用不会重复入账) // 用户确认收货完成后触发配送员提成结算(幂等,重复调用不会重复入账)
settleRiderCommissionIfEligible(id, tenantId, false); settleRiderCommissionIfEligible(id, tenantId, false);
// 送水订单完成后,同步商城订单为“已完成”(orderStatus=1)
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, now);
return; return;
} }
@@ -379,6 +540,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED) { if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED) {
// 幂等:重复确认收货视为成功 // 幂等:重复确认收货视为成功
settleRiderCommissionIfEligible(id, tenantId, false); settleRiderCommissionIfEligible(id, tenantId, false);
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, LocalDateTime.now());
return; return;
} }
throw new BusinessException("订单状态不允许确认收货"); throw new BusinessException("订单状态不允许确认收货");
@@ -437,6 +599,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} }
// 超时自动确认收货后,也按“完成”逻辑触发配送员提成结算(幂等)。 // 超时自动确认收货后,也按“完成”逻辑触发配送员提成结算(幂等)。
settleRiderCommissionIfEligible(id, tenantId, false); settleRiderCommissionIfEligible(id, tenantId, false);
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, nowFinal);
return true; return true;
}); });
if (Boolean.TRUE.equals(ok)) { if (Boolean.TRUE.equals(ok)) {
@@ -449,6 +612,125 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
return confirmed; 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) { private void settleRiderCommissionIfEligible(Integer ticketOrderId, Integer tenantId, boolean requirePhoto) {
if (ticketOrderId == null || tenantId == null) { if (ticketOrderId == null || tenantId == null) {
return; return;

View File

@@ -1,6 +1,7 @@
package com.gxwebsoft.glt.task; package com.gxwebsoft.glt.task;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.glt.entity.GltTicketOrder; import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltTicketTemplate; 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.GltTicketTemplateService;
import com.gxwebsoft.glt.service.GltUserTicketService; import com.gxwebsoft.glt.service.GltUserTicketService;
import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerUser; import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.service.ShopDealerCapitalService; import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.service.ShopDealerUserService; import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopOrderService; import com.gxwebsoft.shop.service.ShopOrderService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -65,6 +68,9 @@ public class DealerCommissionUnfreeze10584Task {
@Resource @Resource
private ShopDealerCapitalService shopDealerCapitalService; private ShopDealerCapitalService shopDealerCapitalService;
@Resource
private ShopDealerOrderService shopDealerOrderService;
@Resource @Resource
private ShopDealerUserService shopDealerUserService; private ShopDealerUserService shopDealerUserService;
@@ -79,7 +85,7 @@ public class DealerCommissionUnfreeze10584Task {
private final AtomicBoolean running = new AtomicBoolean(false); 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 精确过滤") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {
@@ -355,12 +361,56 @@ public class DealerCommissionUnfreeze10584Task {
marker.setUpdateTime(now); marker.setUpdateTime(now);
shopDealerCapitalService.save(marker); shopDealerCapitalService.save(marker);
// 佣金全部解冻完成后,将分销订单状态置为“已解冻”(0)。
// 以当前任务生成的 flowType=50 marker 数量作为完成度判断,避免提前将订单置为已解冻。
setDealerOrderUnfrozenIfCompleted(orderNo, now);
log.info("佣金解冻成功 - tenantId={}, orderNo={}, dealerUserId={}, amount={}, capitalId={}", log.info("佣金解冻成功 - tenantId={}, orderNo={}, dealerUserId={}, amount={}, capitalId={}",
TENANT_ID, orderNo, dealerUserId, amount, capitalId); TENANT_ID, orderNo, dealerUserId, amount, capitalId);
return true; 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) { private String buildUnfreezeMarkerComment(Integer capitalId) {
return "佣金解冻(capitalId=" + capitalId + ")"; return "佣金解冻(capitalId=" + capitalId + ")";
} }

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ public class GltTicketOrderAutoConfirm10584Task {
private final AtomicBoolean running = new AtomicBoolean(false); 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 精确过滤") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { 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); 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") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;释放记录自带 tenantId更新时会校验 tenantId")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {

View File

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

View File

@@ -120,9 +120,15 @@ public class ShopDealerOrder implements Serializable {
@Schema(description = "佣金结算(0未结算 1已结算)") @Schema(description = "佣金结算(0未结算 1已结算)")
private Integer isSettled; private Integer isSettled;
@Schema(description = "佣金冻结(1解冻中 0已解冻)")
private Integer isUnfreeze;
@Schema(description = "结算时间") @Schema(description = "结算时间")
private LocalDateTime settleTime; private LocalDateTime settleTime;
@Schema(description = "解冻时间")
private LocalDateTime unfreezeTime;
@Schema(description = "备注") @Schema(description = "备注")
private String comments; 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.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -101,6 +102,7 @@ public class ShopGoods implements Serializable {
private BigDecimal secondDividend; private BigDecimal secondDividend;
@Schema(description = "库存计算方式(10下单减库存 20付款减库存)") @Schema(description = "库存计算方式(10下单减库存 20付款减库存)")
@JsonAlias({"cdeductStockType"})
private Integer deductStockType; private Integer deductStockType;
@Schema(description = "交付方式(0不启用)") @Schema(description = "交付方式(0不启用)")

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,8 @@ import java.util.Map;
@Slf4j @Slf4j
@Service @Service
public class OrderBusinessService { public class OrderBusinessService {
private static final int DEDUCT_STOCK_TYPE_ORDER = 10; // 下单减库存
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存
@Resource @Resource
private ShopOrderService shopOrderService; private ShopOrderService shopOrderService;
@@ -713,6 +715,15 @@ public class OrderBusinessService {
*/ */
private void deductStock(OrderCreateRequest request) { private void deductStock(OrderCreateRequest request) {
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) { 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) { if (item.getSkuId() != null) {
// 多规格商品扣减SKU库存 // 多规格商品扣减SKU库存
ShopGoodsSku sku = shopGoodsSkuService.getById(item.getSkuId()); ShopGoodsSku sku = shopGoodsSkuService.getById(item.getSkuId());

View File

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

View File

@@ -24,6 +24,7 @@ import java.util.List;
@Slf4j @Slf4j
@Service @Service
public class OrderCancelServiceImpl implements OrderCancelService { public class OrderCancelServiceImpl implements OrderCancelService {
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存:下单不扣库存,因此未支付取消无需回退
@Autowired @Autowired
private ShopOrderService shopOrderService; private ShopOrderService shopOrderService;
@@ -182,6 +183,20 @@ public class OrderCancelServiceImpl implements OrderCancelService {
} }
for (ShopOrderGoods orderGood : orderGoods) { 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) { if (orderGood.getSkuId() != null && orderGood.getSkuId() > 0) {
// 多规格商品恢复SKU库存 // 多规格商品恢复SKU库存
restoreSkuStock(orderGood); restoreSkuStock(orderGood);

View File

@@ -1,8 +1,10 @@
package com.gxwebsoft.shop.service.impl; package com.gxwebsoft.shop.service.impl;
import cn.hutool.core.util.StrUtil; 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.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.ConfigProperties;
import com.gxwebsoft.common.core.config.CertificateProperties; import com.gxwebsoft.common.core.config.CertificateProperties;
import com.gxwebsoft.common.core.utils.*; import com.gxwebsoft.common.core.utils.*;
@@ -86,10 +88,13 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
private ShopOrderDeliveryService shopOrderDeliveryService; private ShopOrderDeliveryService shopOrderDeliveryService;
@Resource @Resource
private ShopExpressService shopExpressService; private ShopExpressService shopExpressService;
@Resource
private ShopGoodsSkuService shopGoodsSkuService;
private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L; private static final long USER_ORDER_STATS_CACHE_SECONDS = 60L;
private static final long WECHAT_PREPAY_SNAPSHOT_TTL_MINUTES = 30L; 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 String WECHAT_PREPAY_SNAPSHOT_KEY_PREFIX = "wxpay:prepay:snapshot:";
private static final int DEDUCT_STOCK_TYPE_PAY = 20; // 付款减库存
@Data @Data
private static class WechatPrepaySnapshot { private static class WechatPrepaySnapshot {
@@ -769,10 +774,15 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
try { try {
Transaction result = service.queryOrderByOutTradeNo(queryRequest); Transaction result = service.queryOrderByOutTradeNo(queryRequest);
if (result.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) { if (result.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) {
if (Boolean.TRUE.equals(shopOrder.getPayStatus())) {
// 已是支付成功状态,避免重复触发支付成功后的业务逻辑(销量/库存等)
return true;
}
shopOrder.setPayStatus(true); shopOrder.setPayStatus(true);
shopOrder.setPayTime(LocalDateTime.now()); shopOrder.setPayTime(LocalDateTime.now());
shopOrder.setTransactionId(result.getTransactionId()); shopOrder.setTransactionId(result.getTransactionId());
updateById(shopOrder); updateById(shopOrder);
handlePaymentSuccess(shopOrder);
return true; return true;
} }
} catch (ServiceException e) { } catch (ServiceException e) {
@@ -801,6 +811,9 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
*/ */
private void handlePaymentSuccess(ShopOrder order) { private void handlePaymentSuccess(ShopOrder order) {
try { try {
// 0. 付款减库存:支付成功后扣库存(下单时不扣)
deductStockAfterPaidIfNeeded(order);
// 1. 使用优惠券 // 1. 使用优惠券
if (order.getCouponId() != null && order.getCouponId() > 0) { if (order.getCouponId() != null && order.getCouponId() > 0) {
markCouponAsUsed(order); 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,7 +1549,6 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
@Override @Override
public boolean syncPaymentStatus(String orderNo, Integer paymentStatus, String transactionId, String payTime, Integer tenantId) { public boolean syncPaymentStatus(String orderNo, Integer paymentStatus, String transactionId, String payTime, Integer tenantId) {
try {
// 查询订单 // 查询订单
ShopOrder order = getByOrderNo(orderNo, tenantId); ShopOrder order = getByOrderNo(orderNo, tenantId);
if (order == null) { if (order == null) {
@@ -1461,7 +1557,7 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
} }
// 如果订单已经是支付成功状态,不需要更新 // 如果订单已经是支付成功状态,不需要更新
if (order.getPayStatus() && paymentStatus == 1) { if (Boolean.TRUE.equals(order.getPayStatus()) && paymentStatus == 1) {
log.info("订单已经是支付成功状态,无需更新: orderNo={}", orderNo); log.info("订单已经是支付成功状态,无需更新: orderNo={}", orderNo);
return true; return true;
} }
@@ -1479,16 +1575,15 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
if (updated) { if (updated) {
log.info("订单支付状态同步成功: orderNo={}, paymentStatus={}, transactionId={}", log.info("订单支付状态同步成功: orderNo={}, paymentStatus={}, transactionId={}",
orderNo, paymentStatus, transactionId); orderNo, paymentStatus, transactionId);
// paymentStatus=1时同步触发支付成功后业务逻辑包括付款减库存/销量等)
if (paymentStatus != null && paymentStatus == 1) {
handlePaymentSuccess(order);
}
} else { } else {
log.warn("订单支付状态同步失败: orderNo={}, paymentStatus={}", orderNo, paymentStatus); log.warn("订单支付状态同步失败: orderNo={}, paymentStatus={}", orderNo, paymentStatus);
} }
return updated; return updated;
} catch (Exception e) {
log.error("同步订单支付状态异常: orderNo={}, error={}", orderNo, e.getMessage(), e);
return false;
}
} }

View File

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