feat(glt): 实现送水订单配送员提成结算功能
- 修改经销商订单结算任务,按确认收货状态结算订单(deliveryStatus=20) - 在送水订单控制器中添加配送员提成结算注释说明 - 扩展送水订单服务接口,新增超时自动确认收货方法 - 实现送水订单配送员提成结算逻辑,支持拍照上传和用户确认收货两种触发方式 - 添加配送员提成幂等处理,避免重复入账 - 创建租户10584送水订单超时自动确认收货定时任务 - 实现超时订单自动确认收货并触发配送员提成结算功能
This commit is contained in:
@@ -163,6 +163,7 @@ public class GltTicketOrderController extends BaseController {
|
||||
Integer tenantId = getTenantId();
|
||||
requireActiveRider(loginUser.getUserId(), tenantId);
|
||||
String sendEndImg = body == null ? null : body.getSendEndImg();
|
||||
// 配送员提成结算:在 service 内部按“拍照上传/用户确认收货”规则幂等处理。
|
||||
gltTicketOrderService.delivered(id, loginUser.getUserId(), tenantId, sendEndImg);
|
||||
return success("确认送达");
|
||||
}
|
||||
@@ -175,6 +176,7 @@ public class GltTicketOrderController extends BaseController {
|
||||
if (loginUser == null) {
|
||||
return fail("请先登录");
|
||||
}
|
||||
// 配送员提成结算:在 service 内部按规则幂等处理。
|
||||
gltTicketOrderService.confirmReceive(id, loginUser.getUserId(), getTenantId());
|
||||
return success("确认收货成功");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 送水订单Service
|
||||
@@ -78,4 +79,17 @@ public interface GltTicketOrderService extends IService<GltTicketOrder> {
|
||||
*/
|
||||
void confirmReceive(Integer id, Integer userId, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 超时自动确认收货:
|
||||
* - 扫描已送达待确认(30)且送达时间(sendEndTime)超过指定小时数的订单
|
||||
* - 自动置为已完成(40),并写 receiveConfirmTime / receiveConfirmType=30
|
||||
*
|
||||
* @param tenantId 租户ID
|
||||
* @param now 当前时间
|
||||
* @param timeoutHours 超时小时数(如24)
|
||||
* @param batchSize 每次处理条数上限
|
||||
* @return 本次自动确认条数
|
||||
*/
|
||||
int autoConfirmTimeout(Integer tenantId, LocalDateTime now, int timeoutHours, int batchSize);
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.core.web.PageParam;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.mapper.UserMapper;
|
||||
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||
import com.gxwebsoft.glt.entity.GltUserTicketLog;
|
||||
@@ -14,11 +16,21 @@ import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||
import com.gxwebsoft.glt.service.GltUserTicketLogService;
|
||||
import com.gxwebsoft.glt.service.GltUserTicketService;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerCapital;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerUser;
|
||||
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
|
||||
import com.gxwebsoft.shop.service.ShopDealerUserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@@ -28,10 +40,14 @@ import java.util.List;
|
||||
* @author 科技小王子
|
||||
* @since 2026-02-05 18:50:20
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper, GltTicketOrder> implements GltTicketOrderService {
|
||||
|
||||
public static final int CHANGE_TYPE_WRITE_OFF = 20;
|
||||
private static final BigDecimal RIDER_UNIT_COMMISSION = new BigDecimal("0.10");
|
||||
private static final int RIDER_COMMISSION_SCALE = 2;
|
||||
private static final int TENANT_ID_10584 = 10584;
|
||||
|
||||
@Resource
|
||||
private GltUserTicketMapper gltUserTicketMapper;
|
||||
@@ -42,6 +58,18 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
||||
@Resource
|
||||
private GltUserTicketLogService gltUserTicketLogService;
|
||||
|
||||
@Resource
|
||||
private TransactionTemplate transactionTemplate;
|
||||
|
||||
@Resource
|
||||
private ShopDealerUserService shopDealerUserService;
|
||||
|
||||
@Resource
|
||||
private ShopDealerCapitalService shopDealerCapitalService;
|
||||
|
||||
@Resource
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Override
|
||||
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
|
||||
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
|
||||
@@ -252,6 +280,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delivered(Integer id, Integer riderId, Integer tenantId, String sendEndImg) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("订单id不能为空");
|
||||
@@ -279,6 +308,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
||||
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_DELIVERING)
|
||||
.update();
|
||||
if (ok) {
|
||||
// 配送员拍照上传送达后可触发提成结算(幂等,重复调用不会重复入账)
|
||||
settleRiderCommissionIfEligible(id, tenantId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -297,12 +328,14 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
||||
&& (order.getDeliveryStatus() == DELIVERY_STATUS_WAIT_CONFIRM
|
||||
|| order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED)) {
|
||||
// 幂等:重复送达视为成功
|
||||
settleRiderCommissionIfEligible(id, tenantId, true);
|
||||
return;
|
||||
}
|
||||
throw new BusinessException("订单状态不允许确认送达");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void confirmReceive(Integer id, Integer userId, Integer tenantId) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("订单id不能为空");
|
||||
@@ -327,6 +360,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
||||
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
|
||||
.update();
|
||||
if (ok) {
|
||||
// 用户确认收货完成后触发配送员提成结算(幂等,重复调用不会重复入账)
|
||||
settleRiderCommissionIfEligible(id, tenantId, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -343,9 +378,220 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
||||
}
|
||||
if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED) {
|
||||
// 幂等:重复确认收货视为成功
|
||||
settleRiderCommissionIfEligible(id, tenantId, false);
|
||||
return;
|
||||
}
|
||||
throw new BusinessException("订单状态不允许确认收货");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int autoConfirmTimeout(Integer tenantId, LocalDateTime now, int timeoutHours, int batchSize) {
|
||||
if (tenantId == null) {
|
||||
return 0;
|
||||
}
|
||||
if (now == null) {
|
||||
now = LocalDateTime.now();
|
||||
}
|
||||
int hours = Math.max(timeoutHours, 1);
|
||||
int limit = Math.max(batchSize, 1);
|
||||
LocalDateTime deadline = now.minusHours(hours);
|
||||
|
||||
List<GltTicketOrder> candidates = this.lambdaQuery()
|
||||
.select(GltTicketOrder::getId)
|
||||
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||
.eq(GltTicketOrder::getDeleted, 0)
|
||||
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
|
||||
.isNotNull(GltTicketOrder::getSendEndTime)
|
||||
.le(GltTicketOrder::getSendEndTime, deadline)
|
||||
.orderByAsc(GltTicketOrder::getSendEndTime)
|
||||
.orderByAsc(GltTicketOrder::getId)
|
||||
.last("limit " + limit)
|
||||
.list();
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int confirmed = 0;
|
||||
for (GltTicketOrder item : candidates) {
|
||||
Integer id = item != null ? item.getId() : null;
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final LocalDateTime nowFinal = now;
|
||||
final LocalDateTime deadlineFinal = deadline;
|
||||
Boolean ok = transactionTemplate.execute(status -> {
|
||||
boolean updated = this.lambdaUpdate()
|
||||
.set(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_FINISHED)
|
||||
.set(GltTicketOrder::getReceiveConfirmTime, nowFinal)
|
||||
.set(GltTicketOrder::getReceiveConfirmType, RECEIVE_CONFIRM_TYPE_TIMEOUT)
|
||||
.set(GltTicketOrder::getUpdateTime, nowFinal)
|
||||
.eq(GltTicketOrder::getId, id)
|
||||
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||
.eq(GltTicketOrder::getDeleted, 0)
|
||||
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
|
||||
.le(GltTicketOrder::getSendEndTime, deadlineFinal)
|
||||
.update();
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
// 超时自动确认收货后,也按“完成”逻辑触发配送员提成结算(幂等)。
|
||||
settleRiderCommissionIfEligible(id, tenantId, false);
|
||||
return true;
|
||||
});
|
||||
if (Boolean.TRUE.equals(ok)) {
|
||||
confirmed++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("送水订单超时自动确认收货失败 - tenantId={}, ticketOrderId={}", tenantId, id, e);
|
||||
}
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
private void settleRiderCommissionIfEligible(Integer ticketOrderId, Integer tenantId, boolean requirePhoto) {
|
||||
if (ticketOrderId == null || tenantId == null) {
|
||||
return;
|
||||
}
|
||||
// 目前仅租户10584启用该提成规则,避免影响其他租户历史逻辑。
|
||||
if (tenantId != TENANT_ID_10584) {
|
||||
return;
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult(status -> {
|
||||
// 锁定送水订单行:避免并发下重复结算(如:配送员送达&用户确认收货同时触发)
|
||||
GltTicketOrder order = this.lambdaQuery()
|
||||
.eq(GltTicketOrder::getId, ticketOrderId)
|
||||
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||
.eq(GltTicketOrder::getDeleted, 0)
|
||||
.last("limit 1 for update")
|
||||
.one();
|
||||
if (order == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer riderId = order.getRiderId();
|
||||
if (riderId == null || riderId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer deliveryStatus = order.getDeliveryStatus();
|
||||
if (requirePhoto) {
|
||||
// 配送员拍照上传触发:至少需要到“待客户确认”或“已完成”状态,且存在送达照片。
|
||||
if (deliveryStatus == null || (deliveryStatus != DELIVERY_STATUS_WAIT_CONFIRM && deliveryStatus != DELIVERY_STATUS_FINISHED)) {
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.hasText(order.getSendEndImg())) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 用户确认收货触发:必须为“已完成”状态。
|
||||
if (deliveryStatus == null || deliveryStatus != DELIVERY_STATUS_FINISHED) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int qty = order.getTotalNum() == null ? 0 : order.getTotalNum();
|
||||
if (qty <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
BigDecimal money = RIDER_UNIT_COMMISSION
|
||||
.multiply(BigDecimal.valueOf(qty))
|
||||
.setScale(RIDER_COMMISSION_SCALE, RoundingMode.HALF_UP);
|
||||
if (money.signum() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String orderNo = "gltTicketOrder:" + order.getId();
|
||||
String comments = "配送员提成(ticketOrderId=" + order.getId() + ",unit=" + RIDER_UNIT_COMMISSION + ",qty=" + qty + ")";
|
||||
|
||||
// 幂等:同一送水订单同一配送员只结算一次
|
||||
boolean already = shopDealerCapitalService.count(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ShopDealerCapital>()
|
||||
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||
.eq(ShopDealerCapital::getFlowType, 10)
|
||||
.eq(ShopDealerCapital::getUserId, riderId)
|
||||
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||
.likeRight(ShopDealerCapital::getComments, "配送员提成(ticketOrderId=" + order.getId() + ",")
|
||||
) > 0;
|
||||
if (already) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 送水订单提成:先入冻结金额 freeze_money(与分销订单佣金一致)
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
boolean updated = shopDealerUserService.update(
|
||||
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ShopDealerUser>()
|
||||
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||
.eq(ShopDealerUser::getUserId, riderId)
|
||||
.setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString())
|
||||
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
|
||||
.set(ShopDealerUser::getUpdateTime, now)
|
||||
);
|
||||
|
||||
if (!updated) {
|
||||
// 配送员可能未开通分销账户:创建后再尝试入账一次(与分销结算逻辑保持一致)
|
||||
ShopDealerUser existed = shopDealerUserService.getOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ShopDealerUser>()
|
||||
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||
.eq(ShopDealerUser::getUserId, riderId)
|
||||
.last("limit 1")
|
||||
);
|
||||
if (existed == null) {
|
||||
ShopDealerUser newDealerUser = new ShopDealerUser();
|
||||
newDealerUser.setTenantId(tenantId);
|
||||
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);
|
||||
try {
|
||||
User sysUser = userMapper.selectByIdIgnoreTenant(riderId);
|
||||
if (sysUser != null) {
|
||||
newDealerUser.setRealName(sysUser.getRealName() != null ? sysUser.getRealName() : sysUser.getNickname());
|
||||
newDealerUser.setMobile(sysUser.getPhone());
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
// 基础信息补齐失败不影响入账
|
||||
}
|
||||
newDealerUser.setCreateTime(now);
|
||||
newDealerUser.setUpdateTime(now);
|
||||
shopDealerUserService.save(newDealerUser);
|
||||
}
|
||||
|
||||
updated = shopDealerUserService.update(
|
||||
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ShopDealerUser>()
|
||||
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||
.eq(ShopDealerUser::getUserId, riderId)
|
||||
.setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString())
|
||||
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
|
||||
.set(ShopDealerUser::getUpdateTime, now)
|
||||
);
|
||||
if (!updated) {
|
||||
log.warn("配送员提成入账失败:未找到/创建分销账户 - tenantId={}, ticketOrderId={}, riderId={}", tenantId, order.getId(), riderId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ShopDealerCapital cap = new ShopDealerCapital();
|
||||
cap.setUserId(riderId);
|
||||
cap.setOrderNo(orderNo);
|
||||
cap.setFlowType(10);
|
||||
cap.setMoney(money);
|
||||
cap.setComments(comments);
|
||||
cap.setToUserId(order.getUserId());
|
||||
cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||
cap.setTenantId(tenantId);
|
||||
cap.setCreateTime(now);
|
||||
cap.setUpdateTime(now);
|
||||
shopDealerCapitalService.save(cap);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -133,12 +133,13 @@ public class DealerOrderSettlement10584Task {
|
||||
}
|
||||
|
||||
private List<ShopOrder> findUnsettledPaidOrders() {
|
||||
// 订单付款成功即触发分佣:本任务会将佣金先计入 ShopDealerUser.freezeMoney。
|
||||
// 以确认收货为准:仅结算 deliveryStatus=20 的订单(租户10584约定)。
|
||||
return shopOrderService.list(
|
||||
new LambdaQueryWrapper<ShopOrder>()
|
||||
.eq(ShopOrder::getTenantId, TENANT_ID)
|
||||
.eq(ShopOrder::getDeleted, 0)
|
||||
.eq(ShopOrder::getPayStatus, true)
|
||||
.eq(ShopOrder::getDeliveryStatus, 20)
|
||||
.eq(ShopOrder::getIsSettled, 0)
|
||||
.orderByAsc(ShopOrder::getOrderId)
|
||||
.last("limit " + MAX_ORDERS_PER_RUN)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.gxwebsoft.glt.task;
|
||||
|
||||
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
|
||||
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 租户10584:送水订单超时自动确认收货任务
|
||||
*
|
||||
* <p>扫描已送达待确认(30)且送达时间超过24小时的订单,自动置为已完成(40)。</p>
|
||||
* <p>自动确认后会触发配送员提成结算(幂等)。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "glt.ticket-order.auto-confirm10584", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class GltTicketOrderAutoConfirm10584Task {
|
||||
|
||||
private static final int TENANT_ID = 10584;
|
||||
private static final int TIMEOUT_HOURS = 24;
|
||||
|
||||
private final GltTicketOrderService gltTicketOrderService;
|
||||
|
||||
@Value("${glt.ticket-order.auto-confirm10584.batch-size:200}")
|
||||
private int batchSize;
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
@Scheduled(cron = "${glt.ticket-order.auto-confirm10584.cron:0 */1 * * * ?}")
|
||||
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
|
||||
public void run() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.warn("送水订单超时自动确认任务仍在执行中,本轮跳过 - tenantId={}", TENANT_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int confirmed = gltTicketOrderService.autoConfirmTimeout(TENANT_ID, now, TIMEOUT_HOURS, Math.max(batchSize, 1));
|
||||
if (confirmed > 0) {
|
||||
log.info("送水订单超时自动确认完成 - tenantId={}, confirmed={}, now={}", TENANT_ID, confirmed, now);
|
||||
}
|
||||
} finally {
|
||||
running.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user