From 45878b90059c893828961f61457e193e7c946913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 7 Feb 2026 17:56:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(glt):=20=E5=AE=9E=E7=8E=B0=E9=80=81?= =?UTF-8?q?=E6=B0=B4=E8=AE=A2=E5=8D=95=E9=85=8D=E9=80=81=E5=91=98=E6=8F=90?= =?UTF-8?q?=E6=88=90=E7=BB=93=E7=AE=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改经销商订单结算任务,按确认收货状态结算订单(deliveryStatus=20) - 在送水订单控制器中添加配送员提成结算注释说明 - 扩展送水订单服务接口,新增超时自动确认收货方法 - 实现送水订单配送员提成结算逻辑,支持拍照上传和用户确认收货两种触发方式 - 添加配送员提成幂等处理,避免重复入账 - 创建租户10584送水订单超时自动确认收货定时任务 - 实现超时订单自动确认收货并触发配送员提成结算功能 --- .../controller/GltTicketOrderController.java | 2 + .../glt/service/GltTicketOrderService.java | 14 + .../impl/GltTicketOrderServiceImpl.java | 246 ++++++++++++++++++ .../task/DealerOrderSettlement10584Task.java | 3 +- .../GltTicketOrderAutoConfirm10584Task.java | 56 ++++ 5 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gxwebsoft/glt/task/GltTicketOrderAutoConfirm10584Task.java diff --git a/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java b/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java index 62cca65..b0672cd 100644 --- a/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java +++ b/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java @@ -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("确认收货成功"); } diff --git a/src/main/java/com/gxwebsoft/glt/service/GltTicketOrderService.java b/src/main/java/com/gxwebsoft/glt/service/GltTicketOrderService.java index 0b7ce5a..92511a0 100644 --- a/src/main/java/com/gxwebsoft/glt/service/GltTicketOrderService.java +++ b/src/main/java/com/gxwebsoft/glt/service/GltTicketOrderService.java @@ -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 { */ 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); + } diff --git a/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java b/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java index f0c89d0..82f8f08 100644 --- a/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java +++ b/src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java @@ -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 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 pageRel(GltTicketOrderParam param) { PageParam page = new PageParam<>(param); @@ -252,6 +280,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl 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() + .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() + .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() + .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() + .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); + }); + } + } diff --git a/src/main/java/com/gxwebsoft/glt/task/DealerOrderSettlement10584Task.java b/src/main/java/com/gxwebsoft/glt/task/DealerOrderSettlement10584Task.java index 56e254f..6ccfb49 100644 --- a/src/main/java/com/gxwebsoft/glt/task/DealerOrderSettlement10584Task.java +++ b/src/main/java/com/gxwebsoft/glt/task/DealerOrderSettlement10584Task.java @@ -133,12 +133,13 @@ public class DealerOrderSettlement10584Task { } private List findUnsettledPaidOrders() { - // 订单付款成功即触发分佣:本任务会将佣金先计入 ShopDealerUser.freezeMoney。 + // 以确认收货为准:仅结算 deliveryStatus=20 的订单(租户10584约定)。 return shopOrderService.list( new LambdaQueryWrapper() .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) diff --git a/src/main/java/com/gxwebsoft/glt/task/GltTicketOrderAutoConfirm10584Task.java b/src/main/java/com/gxwebsoft/glt/task/GltTicketOrderAutoConfirm10584Task.java new file mode 100644 index 0000000..a6f54c4 --- /dev/null +++ b/src/main/java/com/gxwebsoft/glt/task/GltTicketOrderAutoConfirm10584Task.java @@ -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:送水订单超时自动确认收货任务 + * + *

扫描已送达待确认(30)且送达时间超过24小时的订单,自动置为已完成(40)。

+ *

自动确认后会触发配送员提成结算(幂等)。

+ */ +@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); + } + } +} +