Compare commits

...

2 Commits

Author SHA1 Message Date
7982b8f963 feat(task): 添加配送奖励功能并优化套票发放流程
- 新增配送奖励计算和发放逻辑,按订单商品的配送费率计算奖励金额
- 添加配送奖励资金流水记录,分配FLOW_TYPE_DELIVERY_REWARD类型
- 优化套票发放流程,移除自动核销机制,改为用户主动履约核销
- 实现套票分批释放功能,支持按期数或月度平均释放策略
- 调整订单结算规则,绑定水票模板的订单支付成功即可分润
- 修复并发情况下的订单重复结算问题,增加分布式锁机制
- 更新日志输出中的分佣/分润术语统一,提升代码可读性
2026-03-26 16:56:00 +08:00
0823c42cbc fix(order): 修复订单状态更新逻辑
- 添加条件判断确保只有当订单状态为2时才更新为0
- 防止已发货或已完成的订单被错误重置为待支付状态
- 保持支付成功后的其他订单属性更新逻辑不变
2026-03-23 11:07:53 +08:00
19 changed files with 1142 additions and 188 deletions

View File

@@ -52,6 +52,34 @@ public class GltTicketOrderController extends BaseController {
return success(gltTicketOrderService.pageRel(param)); return success(gltTicketOrderService.pageRel(param));
} }
@PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员端:分页查询可接单的送水订单")
@GetMapping("/rider/available")
public ApiResult<PageResult<GltTicketOrder>> riderAvailablePage(GltTicketOrderParam param) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录", null);
}
Integer tenantId = getTenantId();
param.setTenantId(tenantId);
// 仅允许配送员访问
requireActiveRider(loginUser.getUserId(), tenantId);
// 查询未分配的待配送订单riderId 为空或0
// 设置为0表示查询未分配的订单XML中会处理为 IS NULL OR = 0
param.setRiderId(0);
if (param.getDeliveryStatus() == null) {
param.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
}
// 配送员端默认按期望配送时间优先
if (StrUtil.isBlank(param.getSort())) {
param.setSort("sendTime asc, createTime desc");
}
// 使用现有的关联查询方法,通过参数控制
return success(gltTicketOrderService.pageRel(param));
}
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员端:分页查询我的送水订单") @Operation(summary = "配送员端:分页查询我的送水订单")
@GetMapping("/rider/page") @GetMapping("/rider/page")
@@ -65,8 +93,12 @@ public class GltTicketOrderController extends BaseController {
// 仅允许配送员访问 // 仅允许配送员访问
requireActiveRider(loginUser.getUserId(), tenantId); requireActiveRider(loginUser.getUserId(), tenantId);
// 关键修复:配送员只能看到分配给自己的订单
param.setRiderId(loginUser.getUserId()); param.setRiderId(loginUser.getUserId());
// 默认查询待配送和配送中的订单
if (param.getDeliveryStatus() == null) { if (param.getDeliveryStatus() == null) {
// 可以通过参数传递多个状态,这里简化为只查待配送
param.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING); param.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
} }
// 配送员端默认按期望配送时间优先 // 配送员端默认按期望配送时间优先
@@ -241,8 +273,33 @@ public class GltTicketOrderController extends BaseController {
@Operation(summary = "修改送水订单") @Operation(summary = "修改送水订单")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) { public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) {
if (gltTicketOrder == null || gltTicketOrder.getId() == null) {
return fail("订单ID不能为空");
}
Integer tenantId = getTenantId();
// 根据 addressId 同步更新订单地址快照
if (gltTicketOrder.getAddressId() != null) {
ShopUserAddress userAddress = shopUserAddressService.getByIdRel(gltTicketOrder.getAddressId());
if (userAddress == null) {
return fail("收货地址不存在");
}
if (tenantId != null && userAddress.getTenantId() != null && !tenantId.equals(userAddress.getTenantId())) {
return fail("收货地址不存在或无权限");
}
GltTicketOrder oldOrder = gltTicketOrderService.getById(gltTicketOrder.getId());
if (oldOrder == null) {
return fail("订单不存在");
}
Integer targetUserId = gltTicketOrder.getUserId() != null ? gltTicketOrder.getUserId() : oldOrder.getUserId();
if (targetUserId != null && userAddress.getUserId() != null && !targetUserId.equals(userAddress.getUserId())) {
return fail("收货地址不存在或无权限");
}
gltTicketOrder.setAddressId(userAddress.getId());
gltTicketOrder.setAddress(buildAddressSnapshot(userAddress));
}
if (gltTicketOrderService.updateById(gltTicketOrder)) { if (gltTicketOrderService.updateById(gltTicketOrder)) {
Integer tenantId = getTenantId();
// 后台指派配送员(直接改 riderId同步商城订单为“已发货”(deliveryStatus=20) // 后台指派配送员(直接改 riderId同步商城订单为“已发货”(deliveryStatus=20)
if (gltTicketOrder != null if (gltTicketOrder != null
&& gltTicketOrder.getId() != null && gltTicketOrder.getId() != null

View File

@@ -47,7 +47,6 @@ public class GltTicketTemplateController extends BaseController {
return success(gltTicketTemplateService.listRel(param)); return success(gltTicketTemplateService.listRel(param));
} }
@PreAuthorize("hasAuthority('glt:gltTicketTemplate:list')")
@Operation(summary = "根据id查询水票") @Operation(summary = "根据id查询水票")
@GetMapping("/{id}") @GetMapping("/{id}")
public ApiResult<GltTicketTemplate> get(@PathVariable("id") Integer id) { public ApiResult<GltTicketTemplate> get(@PathVariable("id") Integer id) {

View File

@@ -35,6 +35,10 @@ public class GltTicketOrder implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String orderNo; private String orderNo;
@Schema(description = "订单状态")
@TableField(exist = false)
private Integer orderStatus;
@Schema(description = "门店ID") @Schema(description = "门店ID")
private Integer storeId; private Integer storeId;

View File

@@ -58,6 +58,9 @@ public class GltTicketTemplate implements Serializable {
@Schema(description = "首期释放时机0=支付成功当刻1=下个月同日") @Schema(description = "首期释放时机0=支付成功当刻1=下个月同日")
private Integer firstReleaseMode; private Integer firstReleaseMode;
@Schema(description = "步长")
private Integer step;
@Schema(description = "用户ID") @Schema(description = "用户ID")
private Integer userId; private Integer userId;

View File

@@ -88,6 +88,10 @@ public class GltUserTicket implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String phone; private String phone;
@Schema(description = "订单状态")
@TableField(exist = false)
private Integer orderStatus;
@Schema(description = "排序(数字越小越靠前)") @Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber; private Integer sortNumber;

View File

@@ -11,7 +11,7 @@
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 COALESCE(o.order_no, f.order_no) as orderNo, o.order_status as orderStatus
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
@@ -32,7 +32,12 @@
AND a.store_id = #{param.storeId} AND a.store_id = #{param.storeId}
</if> </if>
<if test="param.riderId != null"> <if test="param.riderId != null">
AND a.rider_id = #{param.riderId} <if test="param.riderId == 0">
AND (a.rider_id IS NULL OR a.rider_id = 0)
</if>
<if test="param.riderId != 0">
AND a.rider_id = #{param.riderId}
</if>
</if> </if>
<if test="param.deliveryStatus != null"> <if test="param.deliveryStatus != null">
AND a.delivery_status = #{param.deliveryStatus} AND a.delivery_status = #{param.deliveryStatus}

View File

@@ -4,12 +4,12 @@
<!-- 关联查询sql --> <!-- 关联查询sql -->
<sql id="selectSql"> <sql id="selectSql">
SELECT a.*, u.nickname, u.avatar, u.phone, m.name AS templateName, o.pay_price AS payPrice SELECT a.*, u.nickname, u.avatar, u.phone, m.name AS templateName, o.pay_price AS payPrice, o.order_status as orderStatus
FROM glt_user_ticket a FROM glt_user_ticket a
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 glt_ticket_template m ON a.template_id = m.id LEFT JOIN glt_ticket_template m ON a.template_id = m.id
<!-- 使用 order_id + tenant_id 关联,避免 order_no 跨租户/重复导致 a.id 数据被 JOIN 放大 --> <!-- 使用 order_id + tenant_id 关联,避免 order_no 跨租户/重复导致 a.id 数据被 JOIN 放大 -->
LEFT JOIN shop_order o ON a.order_id = o.order_id AND a.tenant_id = o.tenant_id AND o.deleted = 0 LEFT JOIN shop_order o ON a.order_no = o.order_no AND a.tenant_id = o.tenant_id AND o.deleted = 0
<where> <where>
<if test="param.id != null"> <if test="param.id != null">
AND a.id = #{param.id} AND a.id = #{param.id}
@@ -26,6 +26,9 @@
<if test="param.orderNo != null"> <if test="param.orderNo != null">
AND a.order_no LIKE CONCAT('%', #{param.orderNo}, '%') AND a.order_no LIKE CONCAT('%', #{param.orderNo}, '%')
</if> </if>
<if test="param.orderStatus != null">
AND o.order_status = #{param.orderStatus}
</if>
<if test="param.orderGoodsId != null"> <if test="param.orderGoodsId != null">
AND a.order_goods_id = #{param.orderGoodsId} AND a.order_goods_id = #{param.orderGoodsId}
</if> </if>

View File

@@ -1,5 +1,6 @@
package com.gxwebsoft.glt.param; package com.gxwebsoft.glt.param;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
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;
@@ -38,6 +39,9 @@ public class GltTicketOrderParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer riderId; private Integer riderId;
@Schema(description = "订单编号")
private String orderNo;
@Schema(description = "配送状态10待配送、20配送中、30待客户确认、40已完成") @Schema(description = "配送状态10待配送、20配送中、30待客户确认、40已完成")
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer deliveryStatus; private Integer deliveryStatus;
@@ -83,4 +87,8 @@ public class GltTicketOrderParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer deleted; private Integer deleted;
@Schema(description = "订单状态")
@QueryField(type = QueryType.EQ)
private Integer orderStatus;
} }

View File

@@ -40,6 +40,10 @@ public class GltUserTicketParam extends BaseParam {
@Schema(description = "订单编号") @Schema(description = "订单编号")
private String orderNo; private String orderNo;
@Schema(description = "订单状态")
@QueryField(type = QueryType.EQ)
private Integer orderStatus;
@Schema(description = "订单商品ID") @Schema(description = "订单商品ID")
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer orderGoodsId; private Integer orderGoodsId;

View File

@@ -2,27 +2,22 @@ package com.gxwebsoft.glt.service;
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.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltTicketTemplate; import com.gxwebsoft.glt.entity.GltTicketTemplate;
import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketLog; import com.gxwebsoft.glt.entity.GltUserTicketLog;
import com.gxwebsoft.glt.entity.GltUserTicketRelease; import com.gxwebsoft.glt.entity.GltUserTicketRelease;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.entity.ShopOrderGoods; import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.service.ShopOrderGoodsService; import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService; import com.gxwebsoft.shop.service.ShopOrderService;
import com.gxwebsoft.shop.service.ShopUserAddressService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -43,8 +38,6 @@ import java.util.Set;
public class GltTicketIssueService { public class GltTicketIssueService {
public static final int CHANGE_TYPE_ISSUE = 10; public static final int CHANGE_TYPE_ISSUE = 10;
/** 变更类型:起始送水自动核销(按模板 startSendQty 在发放时自动消耗) */
public static final int CHANGE_TYPE_START_SEND_WRITE_OFF = 12;
private enum IssueOutcome { private enum IssueOutcome {
ISSUED, ISSUED,
@@ -60,8 +53,6 @@ public class GltTicketIssueService {
private final GltUserTicketService gltUserTicketService; private final GltUserTicketService gltUserTicketService;
private final GltUserTicketReleaseService gltUserTicketReleaseService; private final GltUserTicketReleaseService gltUserTicketReleaseService;
private final GltUserTicketLogService gltUserTicketLogService; private final GltUserTicketLogService gltUserTicketLogService;
private final GltTicketOrderService gltTicketOrderService;
private final ShopUserAddressService shopUserAddressService;
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
/** /**
@@ -163,11 +154,13 @@ public class GltTicketIssueService {
if (shouldCompleteOrder) { if (shouldCompleteOrder) {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
// 任务执行完后将订单置为“已完成”order_status=1 // 任务执行完后将订单置为“已完成”,避免后续扫描重复处理(幂等虽可挡住,但会产生大量无意义查询)。
shopOrderService.update( shopOrderService.update(
new LambdaUpdateWrapper<ShopOrder>() new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, order.getOrderId()) .eq(ShopOrder::getOrderId, order.getOrderId())
.eq(ShopOrder::getTenantId, tenantId) .eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getOrderStatus, 0)
.set(ShopOrder::getOrderStatus, 1)
.set(ShopOrder::getHasTakeGift, true) .set(ShopOrder::getHasTakeGift, true)
.set(ShopOrder::getUpdateTime, now) .set(ShopOrder::getUpdateTime, now)
); );
@@ -186,11 +179,29 @@ public class GltTicketIssueService {
return IssueOutcome.SKIPPED; return IssueOutcome.SKIPPED;
} }
// 并发幂等兜底:
// - 该任务可能在多实例部署下并发执行;若不加锁,在没有唯一索引的情况下可能重复发放/重复核销。
// - 这里先对商城订单行加行锁,保证同一订单在同一时刻只会被一个事务处理。
// (注意:需数据库支持 SELECT ... FOR UPDATE且 shop_order.order_id 为主键/有索引)
if (order.getOrderId() != null) {
shopOrderService.getOne(
new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, order.getOrderId())
.eq(ShopOrder::getTenantId, tenantId)
.last("for update")
);
}
// 同一商品允许存在多条模板记录(历史数据/误操作)。为避免取到“错误模板”,这里做确定性排序取第一条。
// 排序规则与后台 getByGoodsId 保持一致sortNumber 越小越靠前,其次取最新创建时间。
GltTicketTemplate template = gltTicketTemplateService.getOne( GltTicketTemplate template = gltTicketTemplateService.getOne(
new LambdaQueryWrapper<GltTicketTemplate>() new LambdaQueryWrapper<GltTicketTemplate>()
.eq(GltTicketTemplate::getTenantId, tenantId) .eq(GltTicketTemplate::getTenantId, tenantId)
.eq(GltTicketTemplate::getGoodsId, og.getGoodsId()) .eq(GltTicketTemplate::getGoodsId, og.getGoodsId())
.eq(GltTicketTemplate::getDeleted, 0) .eq(GltTicketTemplate::getDeleted, 0)
.orderByAsc(GltTicketTemplate::getSortNumber)
.orderByDesc(GltTicketTemplate::getCreateTime)
.orderByDesc(GltTicketTemplate::getId)
.last("limit 1") .last("limit 1")
); );
@@ -238,7 +249,7 @@ public class GltTicketIssueService {
int giftMultiplier = template.getGiftMultiplier() != null ? template.getGiftMultiplier() : 0; int giftMultiplier = template.getGiftMultiplier() != null ? template.getGiftMultiplier() : 0;
int giftQty = buyQty * Math.max(giftMultiplier, 0); int giftQty = buyQty * Math.max(giftMultiplier, 0);
// 购买量buyQty应立即可用赠送量giftQty进入冻结并按计划释放。 // 总票数(购买量 + 赠送量)
int totalQty = buyQty + giftQty; int totalQty = buyQty + giftQty;
if (totalQty <= 0) { if (totalQty <= 0) {
@@ -249,6 +260,15 @@ public class GltTicketIssueService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
boolean useReleasePeriods = template.getReleasePeriods() != null && template.getReleasePeriods() > 0;
// 释放期数releasePeriods为高优先级
// - 配置了期数:按期数平均分摊 totalQty每期释放不再“先把购买桶数一次性释放”。
// - 未配置期数:保持原逻辑(购买量立即可用,赠送量冻结并按计划释放)。
int initAvailableQty = useReleasePeriods ? 0 : buyQty;
int initFrozenQty = useReleasePeriods ? totalQty : giftQty;
int initReleasedQty = useReleasePeriods ? 0 : buyQty;
GltUserTicket userTicket = new GltUserTicket(); GltUserTicket userTicket = new GltUserTicket();
userTicket.setTemplateId(template.getId()); userTicket.setTemplateId(template.getId());
userTicket.setGoodsId(og.getGoodsId()); userTicket.setGoodsId(og.getGoodsId());
@@ -256,11 +276,10 @@ public class GltTicketIssueService {
userTicket.setOrderNo(order.getOrderNo()); userTicket.setOrderNo(order.getOrderNo());
userTicket.setOrderGoodsId(og.getId()); userTicket.setOrderGoodsId(og.getId());
userTicket.setTotalQty(totalQty); userTicket.setTotalQty(totalQty);
userTicket.setAvailableQty(buyQty); userTicket.setAvailableQty(initAvailableQty);
userTicket.setFrozenQty(giftQty); userTicket.setFrozenQty(initFrozenQty);
userTicket.setUsedQty(0); userTicket.setUsedQty(0);
// 初始可用量来自“购买量”,视为已释放 userTicket.setReleasedQty(initReleasedQty);
userTicket.setReleasedQty(buyQty);
userTicket.setOrderGoodsQty(og.getTotalNum()); userTicket.setOrderGoodsQty(og.getTotalNum());
userTicket.setUserId(order.getUserId()); userTicket.setUserId(order.getUserId());
userTicket.setSortNumber(0); userTicket.setSortNumber(0);
@@ -273,12 +292,36 @@ public class GltTicketIssueService {
gltUserTicketService.save(userTicket); gltUserTicketService.save(userTicket);
// 生成释放计划(按月) // 生成释放计划
// - 配置 releasePeriods按 totalQty 生成每期释放量periods 优先)
// - 未配置 releasePeriods按 giftQty 生成每期释放量
LocalDateTime baseTime = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime(); LocalDateTime baseTime = order.getPayTime() != null ? order.getPayTime() : order.getCreateTime();
if (baseTime == null) { if (baseTime == null) {
baseTime = now; baseTime = now;
} }
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, giftQty, now); int planQty = useReleasePeriods ? totalQty : giftQty;
List<GltUserTicketRelease> releases = buildReleasePlan(template, userTicket, baseTime, planQty, now);
// 若启用了 releasePeriods 且首期释放时机为“支付成功当刻”,则将首期释放量直接计入可用,
// 避免用户刚购买后短时间内无可用水票;后续期数仍由自动释放任务按 release_time 释放。
if (useReleasePeriods && !releases.isEmpty() && !Objects.equals(template.getFirstReleaseMode(), 1)) {
GltUserTicketRelease first = releases.get(0);
Integer firstQtyObj = first.getReleaseQty();
LocalDateTime firstTime = first.getReleaseTime();
int firstQty = firstQtyObj != null ? firstQtyObj : 0;
if (firstQty > 0 && (firstTime == null || !firstTime.isAfter(now))) {
first.setStatus(1);
first.setUpdateTime(now);
userTicket.setAvailableQty((userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0) + firstQty);
userTicket.setFrozenQty((userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0) - firstQty);
userTicket.setReleasedQty((userTicket.getReleasedQty() != null ? userTicket.getReleasedQty() : 0) + firstQty);
userTicket.setUpdateTime(now);
if (!gltUserTicketService.updateById(userTicket)) {
throw new IllegalStateException("首期释放:更新用户水票失败 userTicketId=" + userTicket.getId());
}
}
}
if (!releases.isEmpty()) { if (!releases.isEmpty()) {
gltUserTicketReleaseService.saveBatch(releases); gltUserTicketReleaseService.saveBatch(releases);
} }
@@ -287,11 +330,11 @@ public class GltTicketIssueService {
GltUserTicketLog issueLog = new GltUserTicketLog(); GltUserTicketLog issueLog = new GltUserTicketLog();
issueLog.setUserTicketId(userTicket.getId()); issueLog.setUserTicketId(userTicket.getId());
issueLog.setChangeType(CHANGE_TYPE_ISSUE); issueLog.setChangeType(CHANGE_TYPE_ISSUE);
issueLog.setChangeAvailable(buyQty); issueLog.setChangeAvailable(userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0);
issueLog.setChangeFrozen(giftQty); issueLog.setChangeFrozen(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
issueLog.setChangeUsed(0); issueLog.setChangeUsed(0);
issueLog.setAvailableAfter(buyQty); issueLog.setAvailableAfter(userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0);
issueLog.setFrozenAfter(giftQty); issueLog.setFrozenAfter(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
issueLog.setUsedAfter(0); issueLog.setUsedAfter(0);
issueLog.setOrderId(order.getOrderId()); issueLog.setOrderId(order.getOrderId());
issueLog.setOrderNo(order.getOrderNo()); issueLog.setOrderNo(order.getOrderNo());
@@ -305,53 +348,17 @@ public class GltTicketIssueService {
issueLog.setUpdateTime(now); issueLog.setUpdateTime(now);
gltUserTicketLogService.save(issueLog); gltUserTicketLogService.save(issueLog);
// 按模板配置:自动“使用掉第一次水票”(起始送水数量) // 按整改需求:水票购买(囤券预付费)与水票核销(下单履约)应为两次独立用户动作;
// 因此模板 startSendQty 不再在“发放”阶段自动核销/自动生成送水订单。
Integer startSendQtyObj = template.getStartSendQty(); Integer startSendQtyObj = template.getStartSendQty();
int startSendQty = startSendQtyObj != null ? startSendQtyObj : 0; int startSendQty = startSendQtyObj != null ? startSendQtyObj : 0;
if (startSendQty > 0) { if (startSendQty > 0) {
int availableBefore = userTicket.getAvailableQty() != null ? userTicket.getAvailableQty() : 0; log.info("套票模板配置了 startSendQty但不再自动送水/自动核销 - tenantId={}, orderNo={}, templateId={}, userTicketId={}, startSendQty={}",
int usedBefore = userTicket.getUsedQty() != null ? userTicket.getUsedQty() : 0; tenantId, order.getOrderNo(), template.getId(), userTicket.getId(), startSendQty);
int toUse = Math.min(startSendQty, availableBefore);
if (toUse > 0) {
userTicket.setAvailableQty(availableBefore - toUse);
userTicket.setUsedQty(usedBefore + toUse);
userTicket.setUpdateTime(now);
if (!gltUserTicketService.updateById(userTicket)) {
throw new IllegalStateException("起始送水自动核销:更新用户水票失败 userTicketId=" + userTicket.getId());
}
// 起始送水:自动核销成功后,生成一条送水订单(用于配送端/后台跟踪)
GltTicketOrder ticketOrder = buildStartSendTicketOrder(tenantId, order, userTicket, toUse, now);
if (!gltTicketOrderService.save(ticketOrder)) {
throw new IllegalStateException("起始送水自动核销:创建送水订单失败 userTicketId=" + userTicket.getId());
}
GltUserTicketLog writeOffLog = new GltUserTicketLog();
writeOffLog.setUserTicketId(userTicket.getId());
writeOffLog.setChangeType(CHANGE_TYPE_START_SEND_WRITE_OFF);
writeOffLog.setChangeAvailable(-toUse);
writeOffLog.setChangeFrozen(0);
writeOffLog.setChangeUsed(toUse);
writeOffLog.setAvailableAfter(userTicket.getAvailableQty());
writeOffLog.setFrozenAfter(userTicket.getFrozenQty() != null ? userTicket.getFrozenQty() : 0);
writeOffLog.setUsedAfter(userTicket.getUsedQty());
// 关联送水订单(保持与“用户下单核销”的日志一致),并在备注里保留来源商城订单号便于追溯
writeOffLog.setOrderId(ticketOrder.getId());
writeOffLog.setOrderNo(ticketOrder.getId() == null ? null : String.valueOf(ticketOrder.getId()));
writeOffLog.setUserId(order.getUserId());
writeOffLog.setSortNumber(0);
writeOffLog.setComments("起始送水自动核销(来源商城订单:" + safe(order.getOrderNo()) + ")");
writeOffLog.setStatus(0);
writeOffLog.setDeleted(0);
writeOffLog.setTenantId(tenantId);
writeOffLog.setCreateTime(now);
writeOffLog.setUpdateTime(now);
gltUserTicketLogService.save(writeOffLog);
}
} }
log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, totalQty={}", log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, orderGoodsQty={}, buyQty={}, giftQty={}, startSendQty={}, totalQty={}",
tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), totalQty); tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), og.getTotalNum(), buyQty, giftQty, startSendQty, totalQty);
return IssueOutcome.ISSUED; return IssueOutcome.ISSUED;
} }
@@ -380,12 +387,13 @@ public class GltTicketIssueService {
if (releasePeriods != null && releasePeriods > 0) { if (releasePeriods != null && releasePeriods > 0) {
int base = totalQty / releasePeriods; int base = totalQty / releasePeriods;
int remainder = totalQty % releasePeriods; int remainder = totalQty % releasePeriods;
for (int i = 1; i <= releasePeriods; i++) { // periodNo 从 0 开始第0期、第1期……更贴近任务执行计数
int qty = base + (i <= remainder ? 1 : 0); for (int i = 0; i < releasePeriods; i++) {
int qty = base + (i < remainder ? 1 : 0);
if (qty <= 0) { if (qty <= 0) {
continue; continue;
} }
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i - 1), now)); list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i), now));
} }
return list; return list;
} }
@@ -395,13 +403,14 @@ public class GltTicketIssueService {
: 10; : 10;
int periods = (totalQty + monthlyReleaseQty - 1) / monthlyReleaseQty; int periods = (totalQty + monthlyReleaseQty - 1) / monthlyReleaseQty;
int remaining = totalQty; int remaining = totalQty;
for (int i = 1; i <= periods; i++) { // periodNo 从 0 开始第0期、第1期……
for (int i = 0; i < periods; i++) {
int qty = Math.min(monthlyReleaseQty, remaining); int qty = Math.min(monthlyReleaseQty, remaining);
if (qty <= 0) { if (qty <= 0) {
break; break;
} }
remaining -= qty; remaining -= qty;
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i - 1), now)); list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i), now));
} }
return list; return list;
@@ -436,77 +445,4 @@ public class GltTicketIssueService {
return LocalDateTime.of(adjusted, time); return LocalDateTime.of(adjusted, time);
} }
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private GltTicketOrder buildStartSendTicketOrder(Integer tenantId,
ShopOrder shopOrder,
GltUserTicket userTicket,
int totalNum,
LocalDateTime now) {
GltTicketOrder o = new GltTicketOrder();
o.setUserTicketId(userTicket.getId());
o.setUserId(shopOrder.getUserId());
o.setStoreId(shopOrder.getStoreId());
o.setWarehouseId(shopOrder.getWarehouseId());
o.setRiderId(shopOrder.getRiderId());
o.setTotalNum(totalNum);
o.setPrice(BigDecimal.ZERO);
o.setBuyerRemarks(shopOrder.getBuyerRemarks());
// 地址快照:优先使用地址表快照;兜底使用商城订单上的 address 字段
Integer addressId = shopOrder.getAddressId();
o.setAddressId(addressId);
String addressSnapshot = null;
if (addressId != null) {
ShopUserAddress addr = shopUserAddressService.getOne(new LambdaQueryWrapper<ShopUserAddress>()
.eq(ShopUserAddress::getId, addressId)
.eq(ShopUserAddress::getUserId, shopOrder.getUserId())
.eq(ShopUserAddress::getTenantId, tenantId)
.last("limit 1"));
addressSnapshot = buildAddressSnapshot(addr);
}
if (addressSnapshot == null || addressSnapshot.isBlank()) {
addressSnapshot = shopOrder.getAddress();
}
o.setAddress(addressSnapshot);
String preferredSendTime = shopOrder.getSendStartTime();
if (preferredSendTime == null || preferredSendTime.isBlank()) {
preferredSendTime = now.format(DATETIME_FMT);
}
o.setSendTime(preferredSendTime);
o.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
o.setSortNumber(0);
o.setComments("起始送水自动下单(来源商城订单:" + safe(shopOrder.getOrderNo()) + ")");
o.setStatus(0);
o.setDeleted(0);
o.setTenantId(tenantId);
o.setCreateTime(now);
o.setUpdateTime(now);
return o;
}
private static String buildAddressSnapshot(ShopUserAddress addr) {
if (addr == null) {
return null;
}
if (addr.getFullAddress() != null && !addr.getFullAddress().isBlank()) {
return addr.getFullAddress();
}
// 兼容旧数据fullAddress 为空时,拼接省市区 + 详细地址
StringBuilder sb = new StringBuilder();
if (addr.getProvince() != null) sb.append(addr.getProvince());
if (addr.getCity() != null) sb.append(addr.getCity());
if (addr.getRegion() != null) sb.append(addr.getRegion());
if (addr.getAddress() != null) sb.append(addr.getAddress());
String s = sb.toString();
if (!s.isBlank()) {
return s;
}
return addr.getAddress();
}
private static String safe(String s) {
return s == null ? "" : s;
}
} }

View File

@@ -0,0 +1,355 @@
package com.gxwebsoft.glt.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.shop.entity.ShopStoreRider;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService;
import com.gxwebsoft.shop.service.ShopStoreRiderService;
import com.gxwebsoft.shop.service.ShopUserAddressService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 送水订单自动派单:
* - 扫描待配送且未指派配送员的 glt_ticket_order
* - 取收货地址坐标(优先 ShopOrder.addressLat/addressLng 订单快照;兜底 addressId -> shop_user_address.lat/lng
* - 在同门店、在线、启用、开启自动派单且有坐标的配送员中,按距离最近优先;
* - 派单写入 glt_ticket_order.rider_id即配送员 userId并同步商城订单 riderId/deliveryStatus复用 accept 逻辑)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class GltTicketOrderAutoDispatchService {
private final GltTicketOrderService gltTicketOrderService;
private final GltUserTicketService gltUserTicketService;
private final ShopStoreRiderService shopStoreRiderService;
private final ShopUserAddressService shopUserAddressService;
private final ShopOrderService shopOrderService;
private final ShopOrderGoodsService shopOrderGoodsService;
/**
* 自动派单(按距离最近)。
*
* @param tenantId 租户ID
* @param batchSize 单次扫描最大处理条数
* @return 成功派单数量
*/
public int autoDispatchWaitingOrders(Integer tenantId, int batchSize) {
if (tenantId == null || tenantId <= 0) {
return 0;
}
if (batchSize <= 0) {
batchSize = 50;
}
List<GltTicketOrder> waiting = gltTicketOrderService.list(
new LambdaQueryWrapper<GltTicketOrder>()
.select(GltTicketOrder::getId, GltTicketOrder::getUserTicketId, GltTicketOrder::getStoreId, GltTicketOrder::getAddressId, GltTicketOrder::getCreateTime)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.and(w -> w.eq(GltTicketOrder::getStatus, 0).or().isNull(GltTicketOrder::getStatus))
.and(w -> w.eq(GltTicketOrder::getDeliveryStatus, GltTicketOrderService.DELIVERY_STATUS_WAITING)
.or().isNull(GltTicketOrder::getDeliveryStatus))
.and(w -> w.isNull(GltTicketOrder::getRiderId).or().eq(GltTicketOrder::getRiderId, 0))
.orderByAsc(GltTicketOrder::getCreateTime)
.orderByAsc(GltTicketOrder::getId)
.last("limit " + batchSize)
);
if (waiting == null || waiting.isEmpty()) {
return 0;
}
int dispatched = 0;
// 简化实现:按门店分组,避免对同一门店重复查配送员/在途单数。
Map<Integer, CandidatePool> poolByStoreId = new HashMap<>();
for (GltTicketOrder order : waiting) {
if (order == null || order.getId() == null) {
continue;
}
LngLat receiver = resolveReceiverLngLat(tenantId, order);
if (receiver == null) {
log.debug("自动派单跳过:无法获取收货坐标 - tenantId={}, ticketOrderId={}, addressId={}",
tenantId, order.getId(), order.getAddressId());
continue;
}
Integer storeId = order.getStoreId();
int storeKey = storeId == null ? -1 : storeId;
CandidatePool pool = poolByStoreId.computeIfAbsent(storeKey, k -> buildCandidatePool(tenantId, storeId));
if (pool == null || pool.candidates.isEmpty()) {
log.debug("自动派单跳过:无可用配送员 - tenantId={}, ticketOrderId={}, storeId={}",
tenantId, order.getId(), storeId);
continue;
}
ShopStoreRider best = pickNearest(pool, receiver);
if (best == null || best.getUserId() == null || best.getUserId() <= 0) {
log.debug("自动派单跳过:未选出配送员 - tenantId={}, ticketOrderId={}, storeId={}",
tenantId, order.getId(), storeId);
continue;
}
try {
// 复用“接单”的原子更新 + 同步商城订单逻辑;这里的 riderId 即配送员 userId。
gltTicketOrderService.accept(order.getId(), best.getUserId(), tenantId);
dispatched++;
// 派单成功后,更新 pool 中该骑手的在途数(便于同批次后续订单使用 maxOnhandOrders
pool.onhandCount.merge(best.getUserId(), 1, Integer::sum);
} catch (Exception e) {
// accept 本身包含幂等条件riderId 为空才会写入);此处吞掉异常,避免一单影响整批。
log.warn("自动派单失败 - tenantId={}, ticketOrderId={}, storeId={}, riderUserId={}",
tenantId, order.getId(), storeId, best.getUserId(), e);
}
}
return dispatched;
}
private CandidatePool buildCandidatePool(Integer tenantId, Integer storeId) {
List<ShopStoreRider> riders = shopStoreRiderService.list(
new LambdaQueryWrapper<ShopStoreRider>()
.eq(ShopStoreRider::getTenantId, tenantId)
.eq(ShopStoreRider::getIsDelete, 0)
.eq(ShopStoreRider::getStatus, 1)
.eq(ShopStoreRider::getWorkStatus, 1) // 仅在线
.eq(ShopStoreRider::getAutoDispatchEnabled, 1)
.eq(storeId != null, ShopStoreRider::getStoreId, storeId)
);
if (riders == null || riders.isEmpty()) {
return new CandidatePool(List.of(), Map.of());
}
// 过滤无 userId/无坐标,并提前解析坐标(避免每单重复 parse
List<ShopStoreRider> candidates = riders.stream()
.filter(Objects::nonNull)
.filter(r -> r.getUserId() != null && r.getUserId() > 0)
.filter(r -> parseLngLat(r.getLongitude(), r.getLatitude()) != null)
.toList();
if (candidates.isEmpty()) {
return new CandidatePool(List.of(), Map.of());
}
List<Integer> riderUserIds = candidates.stream().map(ShopStoreRider::getUserId).distinct().toList();
Map<Integer, Integer> onhand = loadOnhandCounts(tenantId, riderUserIds);
return new CandidatePool(candidates, onhand);
}
private Map<Integer, Integer> loadOnhandCounts(Integer tenantId, List<Integer> riderUserIds) {
if (riderUserIds == null || riderUserIds.isEmpty()) {
return new HashMap<>();
}
// 统计各配送员“未完成”的在途单数10/20/30
QueryWrapper<GltTicketOrder> qw = new QueryWrapper<>();
qw.select("rider_id AS riderId", "COUNT(1) AS cnt")
.eq("tenant_id", tenantId)
.eq("deleted", 0)
.in("rider_id", riderUserIds)
.in("delivery_status",
GltTicketOrderService.DELIVERY_STATUS_WAITING,
GltTicketOrderService.DELIVERY_STATUS_DELIVERING,
GltTicketOrderService.DELIVERY_STATUS_WAIT_CONFIRM
)
.groupBy("rider_id");
Map<Integer, Integer> map = new HashMap<>();
List<Map<String, Object>> rows = gltTicketOrderService.listMaps(qw);
if (rows == null || rows.isEmpty()) {
return map;
}
for (Map<String, Object> row : rows) {
if (row == null) {
continue;
}
Object riderIdObj = row.get("riderId");
Object cntObj = row.get("cnt");
if (!(riderIdObj instanceof Number) || !(cntObj instanceof Number)) {
continue;
}
map.put(((Number) riderIdObj).intValue(), ((Number) cntObj).intValue());
}
return map;
}
private ShopStoreRider pickNearest(CandidatePool pool, LngLat receiver) {
// 优先距离;距离相同用 dispatchPriority 兜底(越大越优先);再用 userId 稳定排序
return pool.candidates.stream()
.filter(r -> {
Integer userId = r.getUserId();
if (userId == null || userId <= 0) {
return false;
}
int max = r.getMaxOnhandOrders() == null ? 0 : r.getMaxOnhandOrders();
if (max <= 0) {
return true; // 0表示不限制
}
int onhand = pool.onhandCount.getOrDefault(userId, 0);
return onhand < max;
})
.min(Comparator
.comparingDouble((ShopStoreRider r) -> distanceMeters(receiver, parseLngLat(r.getLongitude(), r.getLatitude())))
.thenComparingInt((ShopStoreRider r) -> -(r.getDispatchPriority() == null ? 0 : r.getDispatchPriority()))
.thenComparing(r -> r.getUserId() == null ? Integer.MAX_VALUE : r.getUserId())
)
.orElse(null);
}
private static double distanceMeters(LngLat a, LngLat b) {
if (a == null || b == null) {
return Double.POSITIVE_INFINITY;
}
// Haversine
double lat1 = Math.toRadians(a.lat);
double lat2 = Math.toRadians(b.lat);
double dLat = lat2 - lat1;
double dLng = Math.toRadians(b.lng - a.lng);
double sinLat = Math.sin(dLat / 2);
double sinLng = Math.sin(dLng / 2);
double aa = sinLat * sinLat + Math.cos(lat1) * Math.cos(lat2) * sinLng * sinLng;
double c = 2 * Math.atan2(Math.sqrt(aa), Math.sqrt(1 - aa));
return 6371000.0 * c; // Earth radius (meters)
}
private LngLat resolveReceiverLngLat(Integer tenantId, GltTicketOrder order) {
if (order == null) {
return null;
}
// 1) 优先使用“商城订单快照坐标”ShopOrder.addressLat/addressLng避免地址表坐标被事后修改影响历史订单派单。
LngLat byOrderSnapshot = resolveReceiverLngLatFromShopOrderSnapshot(tenantId, order);
if (byOrderSnapshot != null) {
return byOrderSnapshot;
}
// 2) 兜底:用地址表坐标(适用于没有关联商城订单的水票下单等场景)
Integer addressId = order.getAddressId();
if (addressId == null || addressId <= 0) {
return null;
}
ShopUserAddress addr = shopUserAddressService.getOne(new LambdaQueryWrapper<ShopUserAddress>()
.select(ShopUserAddress::getLat, ShopUserAddress::getLng)
.eq(ShopUserAddress::getId, addressId)
.eq(ShopUserAddress::getTenantId, tenantId)
.last("limit 1"));
return addr == null ? null : parseLngLat(addr.getLng(), addr.getLat());
}
private LngLat resolveReceiverLngLatFromShopOrderSnapshot(Integer tenantId, GltTicketOrder ticketOrder) {
Integer userTicketId = ticketOrder.getUserTicketId();
if (userTicketId == null || userTicketId <= 0) {
return null;
}
GltUserTicket userTicket = gltUserTicketService.getOne(new LambdaQueryWrapper<GltUserTicket>()
.select(GltUserTicket::getOrderId, GltUserTicket::getOrderNo, GltUserTicket::getOrderGoodsId)
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, userTicketId)
.last("limit 1"));
if (userTicket == null) {
return null;
}
Integer shopOrderId = userTicket.getOrderId();
String shopOrderNo = userTicket.getOrderNo();
// 兼容:历史数据只写了 orderGoodsId
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();
}
}
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
.select(ShopOrder::getAddressLat, ShopOrder::getAddressLng)
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.last("limit 1");
if (shopOrderId != null && shopOrderId > 0) {
qw.eq(ShopOrder::getOrderId, shopOrderId);
} else if (StringUtils.hasText(shopOrderNo)) {
qw.eq(ShopOrder::getOrderNo, shopOrderNo);
} else {
return null;
}
ShopOrder shopOrder = shopOrderService.getOne(qw);
if (shopOrder == null) {
return null;
}
return parseLngLat(shopOrder.getAddressLng(), shopOrder.getAddressLat());
}
private static LngLat parseLngLat(String lngRaw, String latRaw) {
Double lng = parseDoubleOrNull(lngRaw);
Double lat = parseDoubleOrNull(latRaw);
if (lng == null || lat == null) {
return null;
}
// 若明显是 (lat,lng) 则交换
if (Math.abs(lng) <= 90 && Math.abs(lat) > 90 && Math.abs(lat) <= 180) {
double tmp = lng;
lng = lat;
lat = tmp;
}
if (Math.abs(lat) > 90 || Math.abs(lng) > 180) {
return null;
}
return new LngLat(lng, lat);
}
private static Double parseDoubleOrNull(String raw) {
if (raw == null) {
return null;
}
String s = raw.trim();
if (s.isEmpty()) {
return null;
}
try {
return Double.parseDouble(s);
} catch (Exception e) {
return null;
}
}
private record LngLat(double lng, double lat) {
}
private static final class CandidatePool {
final List<ShopStoreRider> candidates;
final Map<Integer, Integer> onhandCount;
CandidatePool(List<ShopStoreRider> candidates, Map<Integer, Integer> onhandCount) {
this.candidates = candidates;
this.onhandCount = new HashMap<>(onhandCount == null ? Map.of() : onhandCount);
}
}
}

View File

@@ -0,0 +1,167 @@
package com.gxwebsoft.glt.service;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketRelease;
import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 水票撤销(订单取消/退款成功后的清理):
* - 取消/隐藏用户水票glt_user_ticket.deleted=1
* - 删除未完成的释放计划glt_user_ticket_release.deleted=1, status!=1
* - 删除未完成的送水订单glt_ticket_order.deleted=1, delivery_status!=40
*
* <p>说明:该操作需保证幂等;若无关联水票则无任何副作用。</p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class GltTicketRevokeService {
/** release.status已释放 */
private static final int RELEASE_STATUS_DONE = 1;
/** ticketOrder.deliveryStatus已完成 */
private static final int TICKET_ORDER_DELIVERY_STATUS_FINISHED = 40;
private final GltUserTicketService gltUserTicketService;
private final GltUserTicketReleaseService gltUserTicketReleaseService;
private final GltTicketOrderService gltTicketOrderService;
private final ShopOrderGoodsService shopOrderGoodsService;
@Transactional(rollbackFor = Exception.class)
public int revokeByShopOrder(Integer tenantId, Integer shopOrderId, String shopOrderNo, String reason) {
if (tenantId == null) {
return 0;
}
if (shopOrderId == null && StrUtil.isBlank(shopOrderNo)) {
return 0;
}
LambdaQueryWrapper<GltUserTicket> qw = new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0);
if (shopOrderId != null && StrUtil.isNotBlank(shopOrderNo)) {
qw.and(w -> w.eq(GltUserTicket::getOrderId, shopOrderId).or().eq(GltUserTicket::getOrderNo, shopOrderNo));
} else if (shopOrderId != null) {
qw.eq(GltUserTicket::getOrderId, shopOrderId);
} else {
qw.eq(GltUserTicket::getOrderNo, shopOrderNo);
}
List<GltUserTicket> tickets = gltUserTicketService.list(qw);
// 兼容历史数据:部分水票只记录了 orderGoodsId未记录 orderId/orderNo
if ((tickets == null || tickets.isEmpty()) && shopOrderId != null) {
try {
List<ShopOrderGoods> goodsList = shopOrderGoodsService.list(
new LambdaQueryWrapper<ShopOrderGoods>()
.select(ShopOrderGoods::getId)
.eq(ShopOrderGoods::getTenantId, tenantId)
.eq(ShopOrderGoods::getOrderId, shopOrderId)
);
List<Integer> orderGoodsIds = goodsList == null ? List.of() : goodsList.stream()
.map(ShopOrderGoods::getId)
.filter(id -> id != null && id > 0)
.distinct()
.toList();
if (!orderGoodsIds.isEmpty()) {
tickets = gltUserTicketService.list(
new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.in(GltUserTicket::getOrderGoodsId, orderGoodsIds)
);
}
} catch (Exception e) {
log.warn("撤销水票通过orderGoodsId兜底反查失败 - tenantId={}, shopOrderId={}", tenantId, shopOrderId, e);
}
}
if (tickets == null || tickets.isEmpty()) {
return 0;
}
LocalDateTime now = LocalDateTime.now();
int revoked = 0;
// 不强制覆盖 comments避免影响后台人工备注reason 仅用于日志。
String reasonForLog = StrUtil.isBlank(reason) ? "订单取消/退款撤销水票" : reason.trim();
// 去重(避免 orderId/orderNo 与 orderGoodsId 两种路径重复命中)
Set<Integer> seen = new HashSet<>();
for (GltUserTicket t : tickets) {
if (t == null || t.getId() == null) {
continue;
}
if (!seen.add(t.getId())) {
continue;
}
Integer userTicketId = t.getId();
// 1) 删除未完成的送水订单(避免继续配送/接单/确认)
try {
LambdaUpdateWrapper<GltTicketOrder> uw = new LambdaUpdateWrapper<GltTicketOrder>()
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.eq(GltTicketOrder::getUserTicketId, userTicketId)
// 兼容历史/脏数据deliveryStatus 为空时也按“未完成”处理
.and(w -> w.ne(GltTicketOrder::getDeliveryStatus, TICKET_ORDER_DELIVERY_STATUS_FINISHED)
.or().isNull(GltTicketOrder::getDeliveryStatus))
.set(GltTicketOrder::getDeleted, 1)
.set(GltTicketOrder::getUpdateTime, now);
gltTicketOrderService.update(null, uw);
} catch (Exception e) {
log.warn("撤销送水订单失败(继续尝试撤销水票/释放计划) - tenantId={}, shopOrderId={}, userTicketId={}",
tenantId, shopOrderId, userTicketId, e);
}
// 2) 删除未完成的释放计划(防止后续继续自动释放)
try {
LambdaUpdateWrapper<GltUserTicketRelease> uw = new LambdaUpdateWrapper<GltUserTicketRelease>()
.eq(GltUserTicketRelease::getTenantId, tenantId)
.eq(GltUserTicketRelease::getDeleted, 0)
.eq(GltUserTicketRelease::getUserTicketId, userTicketId.longValue())
// status 为空时也视为“未完成”
.and(w -> w.ne(GltUserTicketRelease::getStatus, RELEASE_STATUS_DONE)
.or().isNull(GltUserTicketRelease::getStatus))
.set(GltUserTicketRelease::getDeleted, 1)
.set(GltUserTicketRelease::getUpdateTime, now);
gltUserTicketReleaseService.update(null, uw);
} catch (Exception e) {
log.warn("撤销水票释放计划失败(继续尝试撤销水票) - tenantId={}, shopOrderId={}, userTicketId={}",
tenantId, shopOrderId, userTicketId, e);
}
// 3) 撤销水票本身(软删除;幂等)
boolean ok = gltUserTicketService.update(
null,
new LambdaUpdateWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, userTicketId)
.set(GltUserTicket::getDeleted, 1)
.set(GltUserTicket::getUpdateTime, now)
);
if (ok) {
revoked++;
}
}
log.info("撤销水票完成 - tenantId={}, shopOrderId={}, shopOrderNo={}, tickets={}, reason={}",
tenantId, shopOrderId, shopOrderNo, revoked, reasonForLog);
return revoked;
}
}

View File

@@ -54,6 +54,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
private static final BigDecimal RIDER_UNIT_COMMISSION = new BigDecimal("0.10"); private static final BigDecimal RIDER_UNIT_COMMISSION = new BigDecimal("0.10");
private static final int RIDER_COMMISSION_SCALE = 2; private static final int RIDER_COMMISSION_SCALE = 2;
private static final int TENANT_ID_10584 = 10584; private static final int TENANT_ID_10584 = 10584;
private static final DateTimeFormatter SEND_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Resource @Resource
private GltUserTicketMapper gltUserTicketMapper; private GltUserTicketMapper gltUserTicketMapper;
@@ -155,6 +156,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
gltTicketOrder.setStatus(0); gltTicketOrder.setStatus(0);
gltTicketOrder.setDeleted(0); gltTicketOrder.setDeleted(0);
gltTicketOrder.setTenantId(tenantId); gltTicketOrder.setTenantId(tenantId);
// 关联商城订单号(用于后台/对账/追踪);优先取水票上的 orderNo缺失则按 orderId/orderGoodsId 兜底反查。
gltTicketOrder.setOrderNo(resolveShopOrderNo(userTicket, tenantId));
if (gltTicketOrder.getDeliveryStatus() == null) { if (gltTicketOrder.getDeliveryStatus() == null) {
gltTicketOrder.setDeliveryStatus(DELIVERY_STATUS_WAITING); gltTicketOrder.setDeliveryStatus(DELIVERY_STATUS_WAITING);
} }
@@ -164,6 +167,10 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (gltTicketOrder.getCreateTime() == null) { if (gltTicketOrder.getCreateTime() == null) {
gltTicketOrder.setCreateTime(now); gltTicketOrder.setCreateTime(now);
} }
// “立刻送水”下单场景不再需要前端选择配送时间;若未传则默认当前时间,便于排序与派单。
if (!StringUtils.hasText(gltTicketOrder.getSendTime())) {
gltTicketOrder.setSendTime(now.format(SEND_TIME_FMT));
}
gltTicketOrder.setUpdateTime(now); gltTicketOrder.setUpdateTime(now);
if (!this.save(gltTicketOrder)) { if (!this.save(gltTicketOrder)) {
throw new BusinessException("创建订单失败"); throw new BusinessException("创建订单失败");
@@ -203,6 +210,43 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
return gltTicketOrder; return gltTicketOrder;
} }
private String resolveShopOrderNo(GltUserTicket userTicket, Integer tenantId) {
if (userTicket == null || tenantId == null) {
return null;
}
if (StringUtils.hasText(userTicket.getOrderNo())) {
return userTicket.getOrderNo();
}
Integer orderId = userTicket.getOrderId();
// 兜底:历史数据可能只写了 orderGoodsId未写 orderId/orderNo
if (orderId == null && 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) {
orderId = og.getOrderId();
}
}
if (orderId == null) {
return null;
}
ShopOrder order = shopOrderService.getOne(
new LambdaQueryWrapper<ShopOrder>()
.select(ShopOrder::getOrderNo)
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderId, orderId)
.last("limit 1")
);
return order == null ? null : order.getOrderNo();
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void accept(Integer id, Integer riderId, Integer tenantId) { public void accept(Integer id, Integer riderId, Integer tenantId) {

View File

@@ -12,10 +12,14 @@ 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.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerUser; import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopGoods;
import com.gxwebsoft.shop.entity.ShopOrder; 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.ShopDealerOrderService; import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.service.ShopDealerUserService; import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopGoodsService;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService; import com.gxwebsoft.shop.service.ShopOrderService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -25,6 +29,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -32,6 +37,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -59,12 +65,36 @@ public class DealerCommissionUnfreeze10584Task {
private static final int MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN = 200; private static final int MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN = 200;
private static final int MAX_CAPITALS_PER_RUN = 500; private static final int MAX_CAPITALS_PER_RUN = 500;
private static final int FLOW_TYPE_DELIVERY_REWARD = 60; // 配送奖励(直接入可提现金额)
/**
* 兼容两种录入方式:
* - 0.05 表示 5%(比例)
* - 5 表示 5%(百分比)
*/
private static BigDecimal normalizeDeliveryRate(BigDecimal rawRate) {
if (rawRate == null || rawRate.signum() <= 0) {
return null;
}
// 如果录入 >= 1按“百分比”处理1 => 1%
if (rawRate.compareTo(BigDecimal.ONE) >= 0) {
return rawRate.movePointLeft(2);
}
return rawRate;
}
@Resource @Resource
private TransactionTemplate transactionTemplate; private TransactionTemplate transactionTemplate;
@Resource @Resource
private ShopOrderService shopOrderService; private ShopOrderService shopOrderService;
@Resource
private ShopOrderGoodsService shopOrderGoodsService;
@Resource
private ShopGoodsService shopGoodsService;
@Resource @Resource
private ShopDealerCapitalService shopDealerCapitalService; private ShopDealerCapitalService shopDealerCapitalService;
@@ -85,7 +115,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/30 * * * * ?}") @Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/20 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {
@@ -110,12 +140,36 @@ public class DealerCommissionUnfreeze10584Task {
return; return;
} }
// 配送奖励(与佣金解冻独立):按订单发放,幂等保证不会重复入账
int rewarded = 0;
for (String orderNo : eligibleOrderNos) {
try {
if (settleDeliveryRewardIfNeeded(orderNo)) {
rewarded++;
}
} catch (Exception e) {
log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e);
}
}
List<ShopDealerCapital> capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos); List<ShopDealerCapital> capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
if (capitals.isEmpty()) { if (capitals.isEmpty()) {
// 若本轮没有取到佣金明细,回退再按“最早确认收货”的订单扫一轮,尽量覆盖历史遗留未解冻。 // 若本轮没有取到佣金明细,回退再按“最早确认收货”的订单扫一轮,尽量覆盖历史遗留未解冻。
eligibleOrderNos.clear(); eligibleOrderNos.clear();
eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, false)); eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, false));
eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds)); eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds));
// 兜底扫描出来的订单也补发配送奖励(幂等)
for (String orderNo : eligibleOrderNos) {
try {
if (settleDeliveryRewardIfNeeded(orderNo)) {
rewarded++;
}
} catch (Exception e) {
log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e);
}
}
capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos); capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
} }
@@ -140,11 +194,186 @@ public class DealerCommissionUnfreeze10584Task {
log.info("分销佣金解冻完成 - tenantId={}, eligibleOrderNos={}, scannedCapitals={}, unfrozen={}", log.info("分销佣金解冻完成 - tenantId={}, eligibleOrderNos={}, scannedCapitals={}, unfrozen={}",
TENANT_ID, eligibleOrderNos.size(), capitals.size(), unfrozen); TENANT_ID, eligibleOrderNos.size(), capitals.size(), unfrozen);
} }
if (rewarded > 0) {
log.info("配送奖励发放完成 - tenantId={}, eligibleOrderNos={}, rewarded={}", TENANT_ID, eligibleOrderNos.size(), rewarded);
}
} finally { } finally {
running.set(false); running.set(false);
} }
} }
private boolean settleDeliveryRewardIfNeeded(String orderNo) {
if (orderNo == null || orderNo.isBlank()) {
return false;
}
ShopOrder order = shopOrderService.getOne(
new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderNo, orderNo)
.last("limit 1")
);
if (order == null) {
return false;
}
Integer riderId = order.getRiderId();
if (riderId == null || riderId <= 0) {
return false;
}
// 快速幂等检查:已发放则跳过(事务内仍会二次校验避免并发重复)
boolean already = shopDealerCapitalService.count(
new LambdaQueryWrapper<ShopDealerCapital>()
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
.eq(ShopDealerCapital::getOrderNo, orderNo)
) > 0;
if (already) {
return false;
}
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
LocalDateTime now = LocalDateTime.now();
// 锁定配送员资金明细 marker确保并发幂等
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
new LambdaQueryWrapper<ShopDealerCapital>()
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
.eq(ShopDealerCapital::getOrderNo, orderNo)
.last("limit 1 for update")
);
if (existedMarker != null) {
return false;
}
Integer orderId = order.getOrderId();
if (orderId == null) {
return false;
}
List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.list(
new LambdaQueryWrapper<ShopOrderGoods>()
.eq(ShopOrderGoods::getTenantId, TENANT_ID)
.eq(ShopOrderGoods::getOrderId, orderId)
);
if (orderGoodsList == null || orderGoodsList.isEmpty()) {
return false;
}
List<Integer> goodsIds = orderGoodsList.stream()
.map(ShopOrderGoods::getGoodsId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (goodsIds.isEmpty()) {
return false;
}
Map<Integer, BigDecimal> goodsDeliveryMoneyMap = shopGoodsService.list(
new LambdaQueryWrapper<ShopGoods>()
.eq(ShopGoods::getTenantId, TENANT_ID)
.in(ShopGoods::getGoodsId, goodsIds)
).stream().collect(java.util.stream.Collectors.toMap(
ShopGoods::getGoodsId,
g -> g.getDeliveryMoney() != null ? g.getDeliveryMoney() : BigDecimal.ZERO,
(a, b) -> a
));
BigDecimal reward = BigDecimal.ZERO;
for (ShopOrderGoods og : orderGoodsList) {
Integer goodsId = og.getGoodsId();
if (goodsId == null) {
continue;
}
int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
if (qty <= 0) {
continue;
}
BigDecimal rawRate = goodsDeliveryMoneyMap.getOrDefault(goodsId, BigDecimal.ZERO);
BigDecimal rate = normalizeDeliveryRate(rawRate);
if (rate == null || rate.signum() <= 0) {
continue;
}
BigDecimal unitPrice = og.getPrice() != null ? og.getPrice() : BigDecimal.ZERO;
if (unitPrice.signum() <= 0) {
continue;
}
BigDecimal lineAmount = unitPrice.multiply(BigDecimal.valueOf(qty));
reward = reward.add(lineAmount.multiply(rate));
}
reward = reward.setScale(2, RoundingMode.HALF_UP);
if (reward.signum() <= 0) {
return false;
}
// 锁定/创建配送员分销账户
ShopDealerUser dealerUser = shopDealerUserService.getOne(
new LambdaQueryWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getUserId, riderId)
.last("limit 1 for update")
);
if (dealerUser == null) {
ShopDealerUser newDealerUser = new ShopDealerUser();
newDealerUser.setTenantId(TENANT_ID);
newDealerUser.setUserId(riderId);
newDealerUser.setType(0);
newDealerUser.setIsDelete(0);
newDealerUser.setSortNumber(0);
newDealerUser.setFirstNum(0);
newDealerUser.setSecondNum(0);
newDealerUser.setThirdNum(0);
newDealerUser.setMoney(BigDecimal.ZERO);
newDealerUser.setFreezeMoney(BigDecimal.ZERO);
newDealerUser.setTotalMoney(BigDecimal.ZERO);
newDealerUser.setCreateTime(now);
newDealerUser.setUpdateTime(now);
shopDealerUserService.save(newDealerUser);
dealerUser = shopDealerUserService.getOne(
new LambdaQueryWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getUserId, riderId)
.last("limit 1 for update")
);
if (dealerUser == null) {
log.warn("配送奖励入账失败:未找到/创建分销账户 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
return false;
}
}
BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
BigDecimal totalMoneyVal = dealerUser.getTotalMoney() != null ? dealerUser.getTotalMoney() : BigDecimal.ZERO;
dealerUser.setMoney(moneyVal.add(reward));
dealerUser.setTotalMoney(totalMoneyVal.add(reward));
dealerUser.setUpdateTime(now);
if (!shopDealerUserService.updateById(dealerUser)) {
log.warn("配送奖励入账失败:更新分销账户失败 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
return false;
}
ShopDealerCapital cap = new ShopDealerCapital();
cap.setUserId(riderId);
cap.setOrderNo(orderNo);
cap.setFlowType(FLOW_TYPE_DELIVERY_REWARD);
cap.setMoney(reward);
cap.setComments("配送奖励");
cap.setToUserId(order.getUserId());
cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
cap.setTenantId(TENANT_ID);
cap.setCreateTime(now);
cap.setUpdateTime(now);
shopDealerCapitalService.save(cap);
log.info("配送奖励发放成功 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
return true;
}));
}
private Set<Integer> loadWaterFormIds() { private Set<Integer> loadWaterFormIds() {
return gltTicketTemplateService.list( return gltTicketTemplateService.list(
new LambdaQueryWrapper<GltTicketTemplate>() new LambdaQueryWrapper<GltTicketTemplate>()

View File

@@ -3,6 +3,8 @@ 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.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.GltTicketTemplate;
import com.gxwebsoft.glt.service.GltTicketTemplateService;
import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder; import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerReferee; import com.gxwebsoft.shop.entity.ShopDealerReferee;
@@ -87,6 +89,9 @@ public class DealerOrderSettlement10584Task {
@Resource @Resource
private UserMapper userMapper; private UserMapper userMapper;
@Resource
private GltTicketTemplateService gltTicketTemplateService;
/** /**
* 每10秒执行一次。 * 每10秒执行一次。
*/ */
@@ -94,7 +99,8 @@ public class DealerOrderSettlement10584Task {
@IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常") @IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常")
public void settleTenant10584Orders() { public void settleTenant10584Orders() {
try { try {
List<ShopOrder> orders = findUnsettledPaidOrders(); Set<Integer> waterFormIds = loadWaterFormIds();
List<ShopOrder> orders = findUnsettledPaidOrders(waterFormIds);
if (orders.isEmpty()) { if (orders.isEmpty()) {
return; return;
} }
@@ -105,7 +111,7 @@ public class DealerOrderSettlement10584Task {
DealerBasicSetting dealerBasicSetting = findDealerBasicSetting(); DealerBasicSetting dealerBasicSetting = findDealerBasicSetting();
ShopDealerUser totalDealerUser = findTotalDealerUser(); ShopDealerUser totalDealerUser = findTotalDealerUser();
if (totalDealerUser == null || totalDealerUser.getUserId() == null) { if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
log.warn("未找到总经销商账号,订单仍可结算但不会发放总经销商分润 - tenantId={}", TENANT_ID); log.warn("未找到分红账号,订单仍可结算但不会发放分红 - tenantId={}", TENANT_ID);
} }
log.debug("租户{}分销设置 - level={}", TENANT_ID, dealerBasicSetting.level); log.debug("租户{}分销设置 - level={}", TENANT_ID, dealerBasicSetting.level);
@@ -118,7 +124,7 @@ public class DealerOrderSettlement10584Task {
try { try {
transactionTemplate.executeWithoutResult(status -> { transactionTemplate.executeWithoutResult(status -> {
// 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理) // 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理)
if (!claimOrderToSettle(order.getOrderId())) { if (!claimOrderToSettle(order.getOrderId(), waterFormIds)) {
return; return;
} }
settleOneOrder(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level); settleOneOrder(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level);
@@ -132,28 +138,61 @@ public class DealerOrderSettlement10584Task {
} }
} }
private List<ShopOrder> findUnsettledPaidOrders() { private List<ShopOrder> findUnsettledPaidOrders(Set<Integer> waterFormIds) {
// 以确认收货为准:仅结算 deliveryStatus=20 的订单(租户10584约定)。 // 租户10584约定
return shopOrderService.list( // - 普通订单以发货为准deliveryStatus=20才结算
new LambdaQueryWrapper<ShopOrder>() // - 绑定水票模板的订单:支付成功即可体现分润(无需等发货状态变更)。
.eq(ShopOrder::getTenantId, TENANT_ID) LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getDeleted, 0) .eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getPayStatus, true) .eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getDeliveryStatus, 20) .eq(ShopOrder::getPayStatus, true)
.eq(ShopOrder::getIsSettled, 0) .eq(ShopOrder::getIsSettled, 0)
.orderByAsc(ShopOrder::getOrderId) // 退款/取消订单不结算,避免“退款后仍发放分红/分润/佣金”
.last("limit " + MAX_ORDERS_PER_RUN) .and(w -> w.notIn(ShopOrder::getOrderStatus, 2, 4, 5, 6, 7).or().isNull(ShopOrder::getOrderStatus));
);
if (waterFormIds != null && !waterFormIds.isEmpty()) {
qw.and(w -> w.eq(ShopOrder::getDeliveryStatus, 20).or().in(ShopOrder::getFormId, waterFormIds));
} else {
qw.eq(ShopOrder::getDeliveryStatus, 20);
}
qw.orderByAsc(ShopOrder::getOrderId).last("limit " + MAX_ORDERS_PER_RUN);
return shopOrderService.list(qw);
} }
private boolean claimOrderToSettle(Integer orderId) { private boolean claimOrderToSettle(Integer orderId, Set<Integer> waterFormIds) {
return shopOrderService.update( LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
new LambdaUpdateWrapper<ShopOrder>() .eq(ShopOrder::getOrderId, orderId)
.eq(ShopOrder::getOrderId, orderId) .eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getTenantId, TENANT_ID) .eq(ShopOrder::getIsSettled, 0)
.eq(ShopOrder::getIsSettled, 0) // 二次防御:退款/取消订单不允许被“认领结算”
.set(ShopOrder::getIsSettled, 1) .and(w -> w.notIn(ShopOrder::getOrderStatus, 2, 4, 5, 6, 7).or().isNull(ShopOrder::getOrderStatus));
);
if (waterFormIds != null && !waterFormIds.isEmpty()) {
uw.and(w -> w.eq(ShopOrder::getDeliveryStatus, 20).or().in(ShopOrder::getFormId, waterFormIds));
} else {
uw.eq(ShopOrder::getDeliveryStatus, 20);
}
uw.set(ShopOrder::getIsSettled, 1);
return shopOrderService.update(uw);
}
private Set<Integer> loadWaterFormIds() {
try {
return gltTicketTemplateService.list(
new LambdaQueryWrapper<GltTicketTemplate>()
.eq(GltTicketTemplate::getTenantId, TENANT_ID)
.eq(GltTicketTemplate::getDeleted, 0)
.isNotNull(GltTicketTemplate::getGoodsId)
).stream()
.map(GltTicketTemplate::getGoodsId)
.filter(Objects::nonNull)
.collect(java.util.stream.Collectors.toSet());
} catch (Exception e) {
log.warn("读取水票模板goodsId失败将按普通订单规则结算 - tenantId={}", TENANT_ID, e);
return Collections.emptySet();
}
} }
private void settleOneOrder( private void settleOneOrder(
@@ -201,10 +240,10 @@ public class DealerOrderSettlement10584Task {
// 1) 直推/间推shop_dealer_referee // 1) 直推/间推shop_dealer_referee
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig, dealerLevel); DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig, dealerLevel);
// 2) 门店分上级:从下单用户开始逐级向上找,命中 ShopDealerUser.type=1 的最近两级(直推门店/间推门店)。 // 2) 门店分上级:从下单用户开始逐级向上找,命中 ShopDealerUser.type=1 的最近两级(直推门店/间推门店)。
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, goodsQty, commissionConfig, level1ParentCache, shopRoleCache); ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, goodsQty, commissionConfig, level1ParentCache, shopRoleCache);
// 3) 总经销商分润:固定比率,每个订单都分。 // 3) 分红:固定比率,每个订单都分。
TotalDealerCommission totalDealerCommission = settleTotalDealerCommission(order, baseAmount, goodsQty, totalDealerUser); TotalDealerCommission totalDealerCommission = settleTotalDealerCommission(order, baseAmount, goodsQty, totalDealerUser);
// 4) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准) // 4) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
@@ -265,7 +304,7 @@ public class DealerOrderSettlement10584Task {
directMoney, directMoney,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("直推佣金", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty) buildCommissionComment("分佣", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty)
); );
} }
if (normalizedLevel >= 2) { if (normalizedLevel >= 2) {
@@ -320,7 +359,7 @@ public class DealerOrderSettlement10584Task {
Map<Integer, Boolean> shopRoleCache Map<Integer, Boolean> shopRoleCache
) { ) {
List<Integer> shopRoleReferees = findFirstTwoShopRoleReferees(order.getUserId(), level1ParentCache, shopRoleCache); List<Integer> shopRoleReferees = findFirstTwoShopRoleReferees(order.getUserId(), level1ParentCache, shopRoleCache);
log.info("门店分命中结果(type=1门店角色取前两级) - orderNo={}, buyerUserId={}, shopRoleReferees={}", log.info("门店分命中结果(type=1门店角色取前两级) - orderNo={}, buyerUserId={}, shopRoleReferees={}",
order.getOrderNo(), order.getUserId(), shopRoleReferees); order.getOrderNo(), order.getUserId(), shopRoleReferees);
if (shopRoleReferees.isEmpty()) { if (shopRoleReferees.isEmpty()) {
return ShopRoleCommission.empty(); return ShopRoleCommission.empty();
@@ -330,14 +369,14 @@ public class DealerOrderSettlement10584Task {
// 仅找到一个门店:按(直推+间推)汇总发放 // 仅找到一个门店:按(直推+间推)汇总发放
BigDecimal singleStoreValue = safeValue(commissionConfig.storeDirectValue).add(safeValue(commissionConfig.storeSimpleValue)); BigDecimal singleStoreValue = safeValue(commissionConfig.storeDirectValue).add(safeValue(commissionConfig.storeSimpleValue));
BigDecimal money = calcMoneyByCommissionType(baseAmount, singleStoreValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType); BigDecimal money = calcMoneyByCommissionType(baseAmount, singleStoreValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType);
log.info("发放(仅1门店) - orderNo={}, firstDividendUserId={}, commissionType={}, value={}, money={}", log.info("发放(仅1门店) - orderNo={}, firstDividendUserId={}, commissionType={}, value={}, money={}",
order.getOrderNo(), shopRoleReferees.get(0), commissionConfig.commissionType, singleStoreValue, money); order.getOrderNo(), shopRoleReferees.get(0), commissionConfig.commissionType, singleStoreValue, money);
creditDealerCommission( creditDealerCommission(
shopRoleReferees.get(0), shopRoleReferees.get(0),
money, money,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("门店直推佣金(仅1门店)", commissionConfig.commissionType, singleStoreValue, goodsQty) buildCommissionComment("门店直推分润(仅1门店)", commissionConfig.commissionType, singleStoreValue, goodsQty)
); );
return new ShopRoleCommission(shopRoleReferees.get(0), money, null, BigDecimal.ZERO); return new ShopRoleCommission(shopRoleReferees.get(0), money, null, BigDecimal.ZERO);
} }
@@ -347,7 +386,7 @@ public class DealerOrderSettlement10584Task {
calcMoneyByCommissionType(baseAmount, commissionConfig.storeDirectValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType); calcMoneyByCommissionType(baseAmount, commissionConfig.storeDirectValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType);
BigDecimal storeSimpleMoney = BigDecimal storeSimpleMoney =
calcMoneyByCommissionType(baseAmount, commissionConfig.storeSimpleValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType); calcMoneyByCommissionType(baseAmount, commissionConfig.storeSimpleValue, goodsQty, DIVIDEND_SCALE, commissionConfig.commissionType);
log.info("发放(2人) - orderNo={}, firstDividendUserId={}, commissionType={}, firstValue={}, firstMoney={}, secondDividendUserId={}, secondValue={}, secondMoney={}", log.info("发放(2人) - orderNo={}, firstDividendUserId={}, commissionType={}, firstValue={}, firstMoney={}, secondDividendUserId={}, secondValue={}, secondMoney={}",
order.getOrderNo(), order.getOrderNo(),
shopRoleReferees.get(0), shopRoleReferees.get(0),
commissionConfig.commissionType, commissionConfig.commissionType,
@@ -361,14 +400,14 @@ public class DealerOrderSettlement10584Task {
storeDirectMoney, storeDirectMoney,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("门店直推佣金", commissionConfig.commissionType, commissionConfig.storeDirectValue, goodsQty) buildCommissionComment("门店直推分润", commissionConfig.commissionType, commissionConfig.storeDirectValue, goodsQty)
); );
creditDealerCommission( creditDealerCommission(
shopRoleReferees.get(1), shopRoleReferees.get(1),
storeSimpleMoney, storeSimpleMoney,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("门店间推佣金", commissionConfig.commissionType, commissionConfig.storeSimpleValue, goodsQty) buildCommissionComment("门店间推分润", commissionConfig.commissionType, commissionConfig.storeSimpleValue, goodsQty)
); );
return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney); return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney);
} }
@@ -387,14 +426,14 @@ public class DealerOrderSettlement10584Task {
rate = TOTAL_DEALER_DIVIDEND_RATE; rate = TOTAL_DEALER_DIVIDEND_RATE;
} }
BigDecimal money = calcMoneyByCommissionType(baseAmount, rate, goodsQty, DIVIDEND_SCALE, 20); BigDecimal money = calcMoneyByCommissionType(baseAmount, rate, goodsQty, DIVIDEND_SCALE, 20);
log.info("总经销商分润发放 - orderNo={}, totalDealerUserId={}, rate={}, money={}", log.info("分红发放 - orderNo={}, totalDealerUserId={}, rate={}, money={}",
order.getOrderNo(), totalDealerUser.getUserId(), rate, money); order.getOrderNo(), totalDealerUser.getUserId(), rate, money);
creditDealerCommission( creditDealerCommission(
totalDealerUser.getUserId(), totalDealerUser.getUserId(),
money, money,
order, order,
order.getUserId(), order.getUserId(),
buildCommissionComment("总经销商分润", 20, rate, goodsQty) buildCommissionComment("分红", 20, rate, goodsQty)
); );
return new TotalDealerCommission(totalDealerUser.getUserId(), money); return new TotalDealerCommission(totalDealerUser.getUserId(), money);
} }
@@ -451,7 +490,7 @@ public class DealerOrderSettlement10584Task {
} }
/** /**
* 门店分规则: * 门店分规则:
* - 门店角色为 ShopDealerUser.type=1 * - 门店角色为 ShopDealerUser.type=1
* - 从下单用户开始,沿 shop_dealer_referee(level=1) 链路逐级向上找; * - 从下单用户开始,沿 shop_dealer_referee(level=1) 链路逐级向上找;
* - 遇到第一个 type=1 用户命中为“直推门店用户”,继续向上找到第二个 type=1 用户命中为“间推门店用户”。 * - 遇到第一个 type=1 用户命中为“直推门店用户”,继续向上找到第二个 type=1 用户命中为“间推门店用户”。
@@ -641,7 +680,7 @@ public class DealerOrderSettlement10584Task {
.last("limit 1") .last("limit 1")
); );
if (existed != null) { if (existed != null) {
// 允许“补发”门店分时回填分字段,避免订单已结算但分字段一直为空,影响排查/对账。 // 允许“补发”门店分时回填分字段,避免订单已结算但分字段一直为空,影响排查/对账。
LambdaUpdateWrapper<ShopDealerOrder> uw = new LambdaUpdateWrapper<ShopDealerOrder>() LambdaUpdateWrapper<ShopDealerOrder> uw = new LambdaUpdateWrapper<ShopDealerOrder>()
.eq(ShopDealerOrder::getTenantId, TENANT_ID) .eq(ShopDealerOrder::getTenantId, TENANT_ID)
.eq(ShopDealerOrder::getOrderNo, order.getOrderNo()); .eq(ShopDealerOrder::getOrderNo, order.getOrderNo());
@@ -676,7 +715,7 @@ public class DealerOrderSettlement10584Task {
} }
if (needUpdate) { if (needUpdate) {
shopDealerOrderService.update(uw); shopDealerOrderService.update(uw);
log.info("ShopDealerOrder已存在回填门店分字段 - orderNo={}, firstDividendUser={}, secondDividendUser={}", log.info("ShopDealerOrder已存在回填门店分字段 - orderNo={}, firstDividendUser={}, secondDividendUser={}",
order.getOrderNo(), shopRoleCommission.storeDirectUserId, shopRoleCommission.storeSimpleUserId); order.getOrderNo(), shopRoleCommission.storeDirectUserId, shopRoleCommission.storeSimpleUserId);
} else { } else {
log.info("ShopDealerOrder已存在跳过写入 - orderNo={}", order.getOrderNo()); log.info("ShopDealerOrder已存在跳过写入 - orderNo={}", order.getOrderNo());
@@ -697,7 +736,7 @@ public class DealerOrderSettlement10584Task {
dealerOrder.setThirdUserId(dealerRefereeCommission.thirdDealerId); dealerOrder.setThirdUserId(dealerRefereeCommission.thirdDealerId);
dealerOrder.setThirdMoney(dealerRefereeCommission.thirdMoney); dealerOrder.setThirdMoney(dealerRefereeCommission.thirdMoney);
// 门店(角色shop)两级分单独落字段(详细以 ShopDealerCapital 为准) // 门店(角色shop)两级分单独落字段(详细以 ShopDealerCapital 为准)
dealerOrder.setFirstDividendUser(shopRoleCommission.storeDirectUserId); dealerOrder.setFirstDividendUser(shopRoleCommission.storeDirectUserId);
dealerOrder.setFirstDividend(shopRoleCommission.storeDirectMoney); dealerOrder.setFirstDividend(shopRoleCommission.storeDirectMoney);
dealerOrder.setSecondDividendUser(shopRoleCommission.storeSimpleUserId); dealerOrder.setSecondDividendUser(shopRoleCommission.storeSimpleUserId);

View File

@@ -18,7 +18,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* GLT 套票发放任务: * GLT 套票发放任务:
* - 每30秒扫描一次今日订单tenantId=10584, formId in 套票模板 goodsId, payStatus=1, orderStatus=0 * - 每30秒扫描一次今日订单tenantId=10584, formId in 套票模板 goodsId, payStatus=1, orderStatus=0
* - 为订单生成用户套票账户 + 释放计划(幂等) * - 为订单生成用户套票账户 + 释放计划(幂等)
* - 若模板配置了 startSendQty则发放时自动核销对应数量用于“第一次送水”场景 * - 按整改需求:发放阶段不再自动核销/自动下单;“送水下单核销”由用户在履约时主动触发
*/ */
@Slf4j @Slf4j
@Component @Component

View File

@@ -0,0 +1,54 @@
package com.gxwebsoft.glt.task;
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.glt.service.GltTicketOrderAutoDispatchService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* GLT 送水订单自动派单任务tenantId=10584
* - 扫描未指派配送员的待配送订单glt_ticket_order.delivery_status=10 且 rider_id 为空/0
* - 依据收货坐标(优先 ShopOrder.addressLat/addressLng 订单快照;兜底地址表 lat/lng与 配送员坐标(shop_store_rider.longitude/latitude) 计算距离,派给最近的配送员
* - 写入 rider_id配送员 userId并同步关联商城订单的 riderId/deliveryStatus复用 accept 逻辑)
*
* 默认不启用;需要在配置中显式打开:
* - glt.ticket.dispatch10584.enabled=true
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "glt.ticket.dispatch10584", name = "enabled", havingValue = "true", matchIfMissing = false)
public class GltTicketOrderAutoDispatch10584Task {
private static final int TENANT_ID = 10584;
private final GltTicketOrderAutoDispatchService gltTicketOrderAutoDispatchService;
private final AtomicBoolean running = new AtomicBoolean(false);
@Value("${glt.ticket.dispatch10584.batchSize:50}")
private int batchSize;
@Scheduled(cron = "${glt.ticket.dispatch10584.cron:0/20 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() {
if (!running.compareAndSet(false, true)) {
log.warn("送水订单自动派单任务仍在执行中,本轮跳过 - tenantId={}", TENANT_ID);
return;
}
try {
int n = gltTicketOrderAutoDispatchService.autoDispatchWaitingOrders(TENANT_ID, batchSize);
if (n > 0) {
log.info("送水订单自动派单完成 - tenantId={}, dispatched={}", TENANT_ID, n);
}
} finally {
running.set(false);
}
}
}

View File

@@ -0,0 +1,41 @@
package com.gxwebsoft.shop.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 历史退款订单:补偿撤销水票/释放计划/送水单 的修复请求。
*/
@Data
public class RefundedOrderGltRepairRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "租户ID不传则使用当前请求租户")
private Integer tenantId;
@Schema(description = "指定订单ID列表优先使用为空则走时间窗口扫描")
private List<Integer> orderIds;
@Schema(description = "指定订单号列表(可选;为空则走时间窗口扫描)")
private List<String> orderNos;
@Schema(description = "退款时间起yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime refundTimeStart;
@Schema(description = "退款时间止yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime refundTimeEnd;
@Schema(description = "每次处理条数上限默认200")
private Integer batchSize;
@Schema(description = "是否仅预览默认true仅统计不落库")
private Boolean dryRun;
}

View File

@@ -490,7 +490,9 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
if (result.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) { if (result.getTradeState().equals(Transaction.TradeStateEnum.SUCCESS)) {
shopOrder.setPayStatus(true); shopOrder.setPayStatus(true);
shopOrder.setPayTime(shopOrder.getCreateTime()); shopOrder.setPayTime(shopOrder.getCreateTime());
shopOrder.setOrderStatus(0); if(shopOrder.getOrderStatus().equals(2)){
shopOrder.setOrderStatus(0);
}
shopOrder.setTransactionId(result.getTransactionId()); shopOrder.setTransactionId(result.getTransactionId());
updateById(shopOrder); updateById(shopOrder);
return true; return true;