feat(task): 添加配送奖励功能并优化套票发放流程
- 新增配送奖励计算和发放逻辑,按订单商品的配送费率计算奖励金额 - 添加配送奖励资金流水记录,分配FLOW_TYPE_DELIVERY_REWARD类型 - 优化套票发放流程,移除自动核销机制,改为用户主动履约核销 - 实现套票分批释放功能,支持按期数或月度平均释放策略 - 调整订单结算规则,绑定水票模板的订单支付成功即可分润 - 修复并发情况下的订单重复结算问题,增加分布式锁机制 - 更新日志输出中的分佣/分润术语统一,提升代码可读性
This commit is contained in:
@@ -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 (gltTicketOrderService.updateById(gltTicketOrder)) {
|
if (gltTicketOrder == null || gltTicketOrder.getId() == null) {
|
||||||
|
return fail("订单ID不能为空");
|
||||||
|
}
|
||||||
Integer tenantId = getTenantId();
|
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)) {
|
||||||
// 后台指派配送员(直接改 riderId)时,同步商城订单为“已发货”(deliveryStatus=20)
|
// 后台指派配送员(直接改 riderId)时,同步商城订单为“已发货”(deliveryStatus=20)
|
||||||
if (gltTicketOrder != null
|
if (gltTicketOrder != null
|
||||||
&& gltTicketOrder.getId() != null
|
&& gltTicketOrder.getId() != null
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +32,13 @@
|
|||||||
AND a.store_id = #{param.storeId}
|
AND a.store_id = #{param.storeId}
|
||||||
</if>
|
</if>
|
||||||
<if test="param.riderId != null">
|
<if test="param.riderId != null">
|
||||||
|
<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}
|
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}
|
||||||
</if>
|
</if>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 起始送水:自动核销成功后,生成一条送水订单(用于配送端/后台跟踪)
|
log.info("套票发放成功 - tenantId={}, orderNo={}, orderGoodsId={}, templateId={}, userTicketId={}, orderGoodsQty={}, buyQty={}, giftQty={}, startSendQty={}, totalQty={}",
|
||||||
GltTicketOrder ticketOrder = buildStartSendTicketOrder(tenantId, order, userTicket, toUse, now);
|
tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), og.getTotalNum(), buyQty, giftQty, startSendQty, totalQty);
|
||||||
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={}",
|
|
||||||
tenantId, order.getOrderNo(), og.getId(), template.getId(), userTicket.getId(), 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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>()
|
// - 绑定水票模板的订单:支付成功即可体现分润(无需等发货状态变更)。
|
||||||
|
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
|
||||||
.eq(ShopOrder::getTenantId, TENANT_ID)
|
.eq(ShopOrder::getTenantId, TENANT_ID)
|
||||||
.eq(ShopOrder::getDeleted, 0)
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
.eq(ShopOrder::getPayStatus, true)
|
.eq(ShopOrder::getPayStatus, true)
|
||||||
.eq(ShopOrder::getDeliveryStatus, 20)
|
|
||||||
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean claimOrderToSettle(Integer orderId) {
|
qw.orderByAsc(ShopOrder::getOrderId).last("limit " + MAX_ORDERS_PER_RUN);
|
||||||
return shopOrderService.update(
|
return shopOrderService.list(qw);
|
||||||
new LambdaUpdateWrapper<ShopOrder>()
|
}
|
||||||
|
|
||||||
|
private boolean claimOrderToSettle(Integer orderId, Set<Integer> waterFormIds) {
|
||||||
|
LambdaUpdateWrapper<ShopOrder> uw = 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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user