package com.doumee.service.business.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.doumee.biz.system.AreasBiz; import com.doumee.biz.system.OperationConfigBiz; import com.doumee.biz.system.SystemDictDataBiz; import com.doumee.config.wx.WxMiniConfig; import com.doumee.config.wx.WxMiniUtilService; import com.doumee.core.constants.Constants; import com.doumee.core.constants.ResponseStatus; import com.doumee.core.exception.BusinessException; import com.doumee.core.model.PageData; import com.doumee.core.model.PageWrap; import com.doumee.core.utils.DateUtil; import com.doumee.core.utils.Tencent.MapUtil; import com.doumee.core.utils.Utils; import com.doumee.dao.business.*; import com.doumee.dao.business.model.*; import com.doumee.dao.system.SystemUserMapper; import com.doumee.dao.system.model.SystemDictData; import com.doumee.dao.system.model.SystemUser; import com.doumee.dao.dto.CalculateLocalPriceDTO; import com.doumee.dao.dto.CalculateRemotePriceDTO; import com.doumee.dao.dto.CommentOrderDTO; import com.doumee.dao.dto.CreateOrderDTO; import com.doumee.dao.dto.DispatchDTO; import com.doumee.dao.dto.MyOrderDTO; import com.doumee.dao.dto.OrderItemDTO; import com.doumee.dao.vo.*; import com.doumee.service.business.OrderLogService; import com.doumee.service.business.OrdersService; import com.doumee.service.business.AreasService; import com.github.binarywang.wxpay.bean.request.BaseWxPayRequest; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; import com.github.binarywang.wxpay.exception.WxPayException; import com.github.xiaoymin.knife4j.core.util.CollectionUtils; import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * 寄存订单信息Service实现 * @author rk * @date 2026/04/10 */ @Service public class OrdersServiceImpl implements OrdersService { @Autowired private OrdersMapper ordersMapper; @Autowired private MemberMapper memberMapper; @Autowired private ShopInfoMapper shopInfoMapper; @Autowired private DriverInfoMapper driverInfoMapper; @Autowired private CategoryMapper categoryMapper; @Autowired private MultifileMapper multifileMapper; @Autowired private OrdersDetailMapper ordersDetailMapper; @Autowired private SystemDictDataBiz systemDictDataBiz; @Autowired private OrderLogService orderLogService; @Autowired private OrdersRefundMapper ordersRefundMapper; @Autowired private OtherOrdersMapper otherOrdersMapper; @Autowired private OrderCommentMapper orderCommentMapper; @Autowired private RevenueMapper revenueMapper; @Autowired private WxMiniUtilService wxMiniUtilService; @Autowired private SystemUserMapper systemUserMapper; @Autowired private PricingRuleMapper pricingRuleMapper; @Autowired private RedisTemplate redisTemplate; @Autowired private AreasBiz areasBiz; @Autowired private OperationConfigBiz operationConfigBiz; @Autowired private AreasService areasService; @Override public Integer create(Orders orders) { ordersMapper.insert(orders); return orders.getId(); } @Override public void deleteById(Integer id) { ordersMapper.update(new UpdateWrapper().lambda() .set(Orders::getDeleted, Constants.ONE) .eq(Orders::getId, id)); } @Override public void delete(Orders orders) { UpdateWrapper deleteWrapper = new UpdateWrapper<>(orders); ordersMapper.delete(deleteWrapper); } @Override public void deleteByIdInBatch(List ids) { if (ids == null || ids.isEmpty()) { return; } ordersMapper.deleteBatchIds(ids); } @Override public void updateById(Orders orders) { ordersMapper.updateById(orders); } @Override public void updateByIdInBatch(List ordersList) { if (ordersList == null || ordersList.isEmpty()) { return; } for (Orders orders : ordersList) { this.updateById(orders); } } @Override public Orders findById(Integer id) { Orders orders = ordersMapper.selectById(id); if (Objects.isNull(orders)) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } return orders; } @Override public Orders findOne(Orders orders) { QueryWrapper wrapper = new QueryWrapper<>(orders); return ordersMapper.selectOne(wrapper); } @Override public List findList(Orders orders) { QueryWrapper wrapper = new QueryWrapper<>(orders); return ordersMapper.selectList(wrapper); } @Override public PageData findPage(PageWrap pageWrap) { IPage page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity()); MPJLambdaWrapper queryWrapper = new MPJLambdaWrapper() .selectAll(Orders.class) .selectAs(Category::getDetail, Orders::getOrderLevel) .select("s1.name", Orders::getDepositShopName) .leftJoin(Category.class, Category::getId, Orders::getGoodType) .leftJoin(DriverInfo.class, DriverInfo::getId, Orders::getAcceptDriver) .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID") .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID"); ; Utils.MP.blankToNull(pageWrap.getModel()); pageWrap.getModel().setDeleted(Constants.ZERO); queryWrapper.eq(pageWrap.getModel().getDeleted() != null, Orders::getDeleted, pageWrap.getModel().getDeleted()); queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getCode()), Orders::getCode, pageWrap.getModel().getCode()); queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getDepositShopName()), "s1.name", pageWrap.getModel().getDepositShopName()); queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getTakeShopName()), "s2.name", pageWrap.getModel().getTakeShopName()); queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getGoodsInfo()), Orders::getGoodsInfo, pageWrap.getModel().getGoodsInfo()); queryWrapper.ge(pageWrap.getModel().getCreateStartTime() != null, Orders::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateStartTime())); queryWrapper.le(pageWrap.getModel().getCreateEndTime() != null, Orders::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateEndTime())); queryWrapper.eq(pageWrap.getModel().getDepositShopId() != null, Orders::getDepositShopId, pageWrap.getModel().getDepositShopId()); queryWrapper.eq(pageWrap.getModel().getType() != null, Orders::getType, pageWrap.getModel().getType()); queryWrapper.eq(pageWrap.getModel().getStatus() != null, Orders::getStatus, pageWrap.getModel().getStatus()); queryWrapper.eq(pageWrap.getModel().getTakeShopId() != null, Orders::getTakeShopId, pageWrap.getModel().getTakeShopId()); queryWrapper.and(pageWrap.getModel().getDriverKeyword() != null, i->i.like(DriverInfo::getName, pageWrap.getModel().getDriverKeyword()) .or().like(DriverInfo::getTelephone, pageWrap.getModel().getDriverKeyword())); for (PageWrap.SortData sortData : pageWrap.getSorts()) { if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) { queryWrapper.orderByDesc(sortData.getProperty()); } else { queryWrapper.orderByAsc(sortData.getProperty()); } } return PageData.from(ordersMapper.selectJoinPage(page, Orders.class, queryWrapper)); } @Override public OrderSummaryVO findSummary(PageWrap pageWrap) { // 构建与findPage相同的查询条件 MPJLambdaWrapper queryWrapper = new MPJLambdaWrapper() .leftJoin(Category.class, Category::getId, Orders::getGoodType) .leftJoin(DriverInfo.class, DriverInfo::getId, Orders::getAcceptDriver) .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID") .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID"); Utils.MP.blankToNull(pageWrap.getModel()); pageWrap.getModel().setDeleted(Constants.ZERO); queryWrapper.eq(pageWrap.getModel().getDeleted() != null, Orders::getDeleted, pageWrap.getModel().getDeleted()); queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getCode()), Orders::getCode, pageWrap.getModel().getCode()); queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getDepositShopName()), "s1.name", pageWrap.getModel().getDepositShopName()); queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getTakeShopName()), "s2.name", pageWrap.getModel().getTakeShopName()); queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getGoodsInfo()), Orders::getGoodsInfo, pageWrap.getModel().getGoodsInfo()); queryWrapper.ge(pageWrap.getModel().getCreateStartTime() != null, Orders::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateStartTime())); queryWrapper.le(pageWrap.getModel().getCreateEndTime() != null, Orders::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateEndTime())); queryWrapper.eq(pageWrap.getModel().getDepositShopId() != null, Orders::getDepositShopId, pageWrap.getModel().getDepositShopId()); queryWrapper.eq(pageWrap.getModel().getType() != null, Orders::getType, pageWrap.getModel().getType()); queryWrapper.eq(pageWrap.getModel().getStatus() != null, Orders::getStatus, pageWrap.getModel().getStatus()); queryWrapper.eq(pageWrap.getModel().getTakeShopId() != null, Orders::getTakeShopId, pageWrap.getModel().getTakeShopId()); queryWrapper.and(pageWrap.getModel().getDriverKeyword() != null, i->i.like(DriverInfo::getName, pageWrap.getModel().getDriverKeyword()) .or().like(DriverInfo::getTelephone, pageWrap.getModel().getDriverKeyword())); queryWrapper.select( "IFNULL(SUM(t.total_amount), 0) as total_amount_sum", "IFNULL(SUM(CASE WHEN t.settlement_status = 1 THEN t.total_amount ELSE 0 END), 0) as settled_total_amount_sum", "IFNULL(SUM(t.driver_fee), 0) as driver_fee_sum", "IFNULL(SUM(CASE WHEN t.settlement_status = 1 THEN t.driver_fee ELSE 0 END), 0) as settled_driver_fee_sum" ); queryWrapper.groupBy("1=1"); List> result = ordersMapper.selectJoinMaps(queryWrapper); OrderSummaryVO vo = new OrderSummaryVO(); if (result != null && !result.isEmpty()) { Map row = result.get(0); vo.setTotalAmountSum(toLong(row.get("total_amount_sum"))); vo.setSettledTotalAmountSum(toLong(row.get("settled_total_amount_sum"))); vo.setDriverFeeSum(toLong(row.get("driver_fee_sum"))); vo.setSettledDriverFeeSum(toLong(row.get("settled_driver_fee_sum"))); } else { vo.setTotalAmountSum(0L); vo.setSettledTotalAmountSum(0L); vo.setDriverFeeSum(0L); vo.setSettledDriverFeeSum(0L); } return vo; } private Long toLong(Object val) { if (val == null) return 0L; if (val instanceof Number) return ((Number) val).longValue(); return Long.parseLong(val.toString()); } @Override public BigDecimal calculateInsuranceFee(BigDecimal declaredValue) { if (declaredValue == null || declaredValue.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO; } String rateStr = systemDictDataBiz.queryByCode(Constants.OPERATION_CONFIG, Constants.OP_INSURANCE_RATE).getCode(); BigDecimal rate = new BigDecimal(rateStr); return declaredValue.multiply(rate).setScale(2, BigDecimal.ROUND_HALF_UP); } /** * 计算就地存取预估费用 * * 计算规则: * 1. 根据城市+物品类型 查询 pricing_rule(type=0),fieldA=categoryId, fieldB=单价(分/天) * 2. 每项小计 = 单价 × 数量 × 天数 * 3. 物品价格 = 各项小计之和 * 4. 保价费用 = 报价金额 × 保价费率(字典 INSURANCE_RATE),元转分 * 5. 总价格 = 物品价格 + 保价费用 * * @param dto 就地存取计价请求参数 * @return 价格计算结果 */ @Override public PriceCalculateVO calculateLocalPrice(CalculateLocalPriceDTO dto) { // 天数校验,最少1天 int days = dto.getEstimatedDepositDays() != null && dto.getEstimatedDepositDays() > 0 ? dto.getEstimatedDepositDays() : 1; // 收集所有物品类型ID List categoryIds = new ArrayList<>(); for (OrderItemDTO item : dto.getItems()) { categoryIds.add(item.getCategoryId()); } // 批量查询计价规则 pricing_rule type=0:fieldA=categoryId, fieldB=单价(分/天) List fieldAList = new ArrayList<>(); for (Integer cid : categoryIds) { fieldAList.add(String.valueOf(cid)); } List rules = pricingRuleMapper.selectList(new QueryWrapper().lambda() .eq(PricingRule::getDeleted, Constants.ZERO) .eq(PricingRule::getType, Constants.ZERO) .eq(PricingRule::getCityId, dto.getCityId()) .in(PricingRule::getFieldA, fieldAList)); Map ruleMap = new HashMap<>(); for (PricingRule r : rules) { ruleMap.put(r.getFieldA(), r); } // 批量查询物品类型名称 List categories = categoryMapper.selectBatchIds(categoryIds); Map categoryNameMap = new HashMap<>(); Map categoryDetailMap = new HashMap<>(); for (Category c : categories) { categoryNameMap.put(c.getId(), c.getName()); categoryDetailMap.put(c.getId(), c.getDetail()); } // 计算每项物品费用:小计 = 单价 × 数量 × 天数 List itemList = new ArrayList<>(); long itemPriceTotal = 0L; for (OrderItemDTO item : dto.getItems()) { PricingRule rule = ruleMap.get(String.valueOf(item.getCategoryId())); if (rule == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "未找到该城市物品类型的计价规则"); } long unitPrice = Long.parseLong(rule.getFieldB()); long subtotal = unitPrice * item.getQuantity() * days; ItemPriceVO vo = new ItemPriceVO(); vo.setCategoryId(item.getCategoryId()); vo.setCategoryName(categoryNameMap.getOrDefault(item.getCategoryId(), "")); vo.setDetail(categoryDetailMap.get(item.getCategoryId())); vo.setQuantity(item.getQuantity()); vo.setUnitPrice(unitPrice); vo.setLocallyPrice(unitPrice); vo.setSubtotal(subtotal); itemList.add(vo); itemPriceTotal += subtotal; } // 保价费用:报价金额 × 保价费率(字典 INSURANCE_RATE),元→分 long insuranceFeeFen = 0L; if (Boolean.TRUE.equals(dto.getInsured()) && dto.getDeclaredAmount() != null) { BigDecimal insuranceFeeYuan = calculateInsuranceFee(dto.getDeclaredAmount()); insuranceFeeFen = insuranceFeeYuan.multiply(new BigDecimal(100)).longValue(); } // 总价格 = 物品价格 + 保价费用 long totalPrice = itemPriceTotal + insuranceFeeFen; PriceCalculateVO result = new PriceCalculateVO(); result.setItemList(itemList); result.setItemPrice(itemPriceTotal); result.setInsuranceFee(insuranceFeeFen); result.setTotalPrice(totalPrice); result.setDays(days); result.setUrgentFee(0L); return result; } /** * 计算异地存取预估费用 * * 计算规则: * 1. 调用腾讯地图API计算寄件点与取件点的驾车距离(米→公里) * 2. 根据城市+物品类型 查询 pricing_rule(type=1): * fieldB=起步距离(km), fieldC=起步价(分), fieldD=超出距离单位(km), fieldE=超出距离单价(分) * 3. 每项运费单价: * - 距离 ≤ 起步距离 → 单价 = 起步价 * - 距离 > 起步距离 → 单价 = 起步价 + ceil((距离-起步距离)/超出距离单位) × 超出距离单价 * 4. 小计 = 运费单价 × 数量 * 5. 物品价格 = 各项小计之和 * 6. 保价费用 = 报价金额 × 保价费率(字典 INSURANCE_RATE),元转分 * 7. 加急费用 = 物品价格 × 加急系数(字典 URGENT_COEFFICIENT) * 8. 总价格 = 物品价格 + 保价费用 + 加急费用 * * @param dto 异地存取计价请求参数 * @return 价格计算结果 */ @Override public PriceCalculateVO calculateRemotePrice(CalculateRemotePriceDTO dto) { // 1. 调用腾讯地图距离矩阵API计算驾车距离 String from = dto.getFromLat() + "," + dto.getFromLgt(); String to = dto.getToLat() + "," + dto.getToLgt(); JSONObject distanceResult = MapUtil.distanceSingle("driving", from, to); BigDecimal distance = distanceResult.getBigDecimal("distance"); // distance 单位为米,转为公里 BigDecimal distanceKm = distance.divide(new BigDecimal(1000), 2, RoundingMode.HALF_UP); // 收集所有物品类型ID List categoryIds = new ArrayList<>(); for (OrderItemDTO item : dto.getItems()) { categoryIds.add(item.getCategoryId()); } // 2. 批量查询配送计价规则 pricing_rule type=1 List fieldAList = new ArrayList<>(); for (Integer cid : categoryIds) { fieldAList.add(String.valueOf(cid)); } List rules = pricingRuleMapper.selectList(new QueryWrapper().lambda() .eq(PricingRule::getDeleted, Constants.ZERO) .eq(PricingRule::getType, Constants.ONE) .eq(PricingRule::getCityId, dto.getCityId()) .in(PricingRule::getFieldA, fieldAList)); Map ruleMap = new HashMap<>(); for (PricingRule r : rules) { ruleMap.put(r.getFieldA(), r); } // 查询就地存取计价规则 pricing_rule type=0,用于获取 locallyPrice List localRules = pricingRuleMapper.selectList(new QueryWrapper().lambda() .eq(PricingRule::getDeleted, Constants.ZERO) .eq(PricingRule::getType, Constants.ZERO) .eq(PricingRule::getCityId, dto.getCityId()) .in(PricingRule::getFieldA, fieldAList)); Map localRuleMap = new HashMap<>(); for (PricingRule r : localRules) { localRuleMap.put(r.getFieldA(), r); } // 批量查询物品类型名称 List categories = categoryMapper.selectBatchIds(categoryIds); Map categoryNameMap = new HashMap<>(); Map categoryDetailMap = new HashMap<>(); for (Category c : categories) { categoryNameMap.put(c.getId(), c.getName()); categoryDetailMap.put(c.getId(), c.getDetail()); } // 3. 逐项计算运费:起步价 + 超出部分阶梯价 List itemList = new ArrayList<>(); long itemPriceTotal = 0L; for (OrderItemDTO item : dto.getItems()) { PricingRule rule = ruleMap.get(String.valueOf(item.getCategoryId())); if (rule == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "未找到该城市物品类型的配送计价规则"); } // fieldB=起步距离(km), fieldC=起步价(分), fieldD=超出距离单位(km), fieldE=超出距离单价(分) BigDecimal startDistance = new BigDecimal(rule.getFieldB()); long startPrice = Long.parseLong(rule.getFieldC()); BigDecimal extraDistanceUnit = new BigDecimal(rule.getFieldD()); long extraPricePerUnit = Long.parseLong(rule.getFieldE()); // 阶梯计价:距离 ≤ 起步距离取起步价,超出按 ceil(超出距离/单位) × 单价累加 long unitPrice; if (distanceKm.compareTo(startDistance) <= 0) { unitPrice = startPrice; } else { BigDecimal extraKm = distanceKm.subtract(startDistance); BigDecimal extraCount = extraKm.divide(extraDistanceUnit, 0, RoundingMode.CEILING); unitPrice = startPrice + extraCount.longValue() * extraPricePerUnit; } long subtotal = unitPrice * item.getQuantity(); // 就地存取单价 PricingRule localRule = localRuleMap.get(String.valueOf(item.getCategoryId())); Long locallyPrice = localRule != null ? Long.parseLong(localRule.getFieldB()) : null; ItemPriceVO vo = new ItemPriceVO(); vo.setCategoryId(item.getCategoryId()); vo.setCategoryName(categoryNameMap.getOrDefault(item.getCategoryId(), "")); vo.setDetail(categoryDetailMap.get(item.getCategoryId())); vo.setQuantity(item.getQuantity()); vo.setUnitPrice(unitPrice); vo.setLocallyPrice(locallyPrice); vo.setSubtotal(subtotal); vo.setStartDistance(startDistance); vo.setStartPrice(startPrice); vo.setExtraDistance(extraDistanceUnit); vo.setExtraPrice(extraPricePerUnit); itemList.add(vo); itemPriceTotal += subtotal; } // 4. 保价费用:报价金额 × 保价费率(字典 INSURANCE_RATE),元→分 long insuranceFeeFen = 0L; if (Boolean.TRUE.equals(dto.getInsured()) && dto.getDeclaredAmount() != null) { BigDecimal insuranceFeeYuan = calculateInsuranceFee(dto.getDeclaredAmount()); insuranceFeeFen = insuranceFeeYuan.multiply(new BigDecimal(100)).longValue(); } // 5. 加急费用:物品价格 × 加急系数(字典 URGENT_COEFFICIENT) long urgentFeeFen = 0L; if (Boolean.TRUE.equals(dto.getUrgent())) { String urgentRateStr = systemDictDataBiz.queryByCode( Constants.OPERATION_CONFIG, Constants.OP_URGENT_COEFFICIENT).getCode(); BigDecimal urgentRate = new BigDecimal(urgentRateStr); urgentFeeFen = new BigDecimal(itemPriceTotal).multiply(urgentRate) .setScale(0, RoundingMode.HALF_UP).longValue(); } // 6. 总价格 = 物品价格 + 保价费用 + 加急费用 long totalPrice = itemPriceTotal + insuranceFeeFen + urgentFeeFen; PriceCalculateVO result = new PriceCalculateVO(); result.setItemList(itemList); result.setItemPrice(itemPriceTotal); result.setInsuranceFee(insuranceFeeFen); result.setUrgentFee(urgentFeeFen); result.setTotalPrice(totalPrice); result.setDistance(distanceKm); return result; } @Override public long count(Orders orders) { QueryWrapper wrapper = new QueryWrapper<>(orders); return ordersMapper.selectCount(wrapper); } /** * 创建订单 * * 业务流程: * 1. 参数校验:必填字段、时间顺序、物品不重复、异地必填服务时效 * 2. 查询寄件店铺信息(获取经纬度、地址) * 3. 调用计价接口计算费用(calculateLocalPrice / calculateRemotePrice) * 4. 生成订单编号:JC + yyyyMMddHHmmss + 4位随机数 * 5. 创建订单主表 Orders(状态=待支付) * 6. 创建订单明细表 OrdersDetail(每项物品尺寸) * 7. 创建附件记录 Multifile(物品图片 objType=12) * * @param dto 创建订单请求参数 * @param memberId 当前登录会员ID * @return 订单ID */ @Override @Transactional(rollbackFor = Exception.class) public PayResponse createOrder(CreateOrderDTO dto, Integer memberId) { String lockKey = Constants.GOODS_ORDER_CREATE_LOCK + memberId; //判断前端是否在同一页面创建了两次订单 if (redisTemplate.hasKey(lockKey)) { throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"正在创建订单,请勿重复调用!"); } else { redisTemplate.opsForValue().set(lockKey, "", 5, TimeUnit.SECONDS); } Date now = new Date(); // ========== 1. 参数校验 ========== // 预计到店存件时间必须小于预计到店取件时间 java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm"); Date depositTime; Date takeTime; try { depositTime = sdf.parse(dto.getExpectedDepositTime()); takeTime = sdf.parse(dto.getExpectedTakeTime()); } catch (java.text.ParseException e) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "时间格式错误,正确格式:yyyy-MM-dd HH:mm"); } if (!depositTime.before(takeTime)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "预计到店存件时间必须小于预计到店取件时间"); } // 物品尺寸不能重复 List categoryIds = new ArrayList<>(); for (OrderItemDTO item : dto.getItems()) { if (categoryIds.contains(item.getCategoryId())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "物品尺寸不能重复"); } categoryIds.add(item.getCategoryId()); } // 物品图片最多3张 if (dto.getGoodsImages() != null && dto.getGoodsImages().size() > 3) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "物品图片最多3张"); } // ========== 2. 校验物品类型 ========== Category goodTypeCategory = categoryMapper.selectById(dto.getGoodType()); if (goodTypeCategory == null || Constants.equalsInteger(goodTypeCategory.getDeleted(), Constants.ONE)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "物品类型不存在"); } if (!Constants.equalsInteger(goodTypeCategory.getType(), Constants.TWO)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "物品类型参数错误"); } // ========== 3. 查询寄件店铺信息 ========== ShopInfo depositShop = shopInfoMapper.selectById(dto.getDepositShopId()); if (depositShop == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "寄件店铺不存在"); } // ========== 4. 计算费用 ========== // 异地寄存:校验取件点 BigDecimal takeLat = null; BigDecimal takeLgt = null; String takeLocationValue = null; ShopInfo takeShop = null; if (Constants.ONE.equals(dto.getType())) { // 异地必填服务时效 if (dto.getIsUrgent() == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "异地寄存服务时效不能为空"); } // 取件点:店铺 or 自选点,至少提供一组 if (dto.getTakeShopId() != null) { takeShop = shopInfoMapper.selectById(dto.getTakeShopId()); if (takeShop == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "取件店铺不存在"); } takeLat = BigDecimal.valueOf(takeShop.getLatitude()); takeLgt = BigDecimal.valueOf(takeShop.getLongitude()); takeLocationValue = takeShop.getAddress(); } else if (dto.getTakeLat() != null && dto.getTakeLgt() != null && StringUtils.isNotBlank(dto.getTakeLocation())) { takeLat = dto.getTakeLat(); takeLgt = dto.getTakeLgt(); takeLocationValue = dto.getTakeLocation(); } else { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请选择取件店铺或输入自选取件地址"); } } else { // 就地存取:取件门店同寄件门店 takeShop = depositShop; } // ========== 3. 计算费用 ========== PriceCalculateVO priceResult; if (Constants.ZERO.equals(dto.getType())) { // 就地寄存:计算天数 long diffMs = takeTime.getTime() - depositTime.getTime(); int days = (int) Math.max(1, (diffMs / (1000 * 60 * 60 * 24)) + 1); CalculateLocalPriceDTO priceDTO = new CalculateLocalPriceDTO(); priceDTO.setCityId(dto.getCityId()); priceDTO.setEstimatedDepositDays(days); priceDTO.setItems(dto.getItems()); priceDTO.setInsured(dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0); priceDTO.setDeclaredAmount(dto.getDeclaredAmount()); priceResult = calculateLocalPrice(priceDTO); } else { // 异地寄存 CalculateRemotePriceDTO priceDTO = new CalculateRemotePriceDTO(); priceDTO.setCityId(dto.getCityId()); priceDTO.setFromLat(BigDecimal.valueOf(depositShop.getLatitude())); priceDTO.setFromLgt(BigDecimal.valueOf(depositShop.getLongitude())); priceDTO.setToLat(takeLat); priceDTO.setToLgt(takeLgt); priceDTO.setItems(dto.getItems()); priceDTO.setInsured(dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0); priceDTO.setDeclaredAmount(dto.getDeclaredAmount()); priceDTO.setUrgent(Constants.ONE.equals(dto.getIsUrgent())); priceResult = calculateRemotePrice(priceDTO); } // ========== 5. 生成订单编号 ========== String orderCode = "JC" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now) + String.format("%04d", new java.util.Random().nextInt(10000)); // 生成32位唯一第三方订单编号 String orderTradeNo = generateOrderTradeNo(); // ========== 6. 创建订单主表 ========== Orders orders = new Orders(); orders.setCode(orderCode); orders.setOutTradeNo(orderTradeNo); orders.setMemberId(memberId); orders.setType(dto.getType()); orders.setCityId(String.valueOf(dto.getCityId())); orders.setStatus(Constants.ZERO); // 待支付 orders.setPayStatus(Constants.ZERO); // 未支付 orders.setCommentStatus(Constants.ZERO); // 未评价 orders.setSettlementStatus(Constants.ZERO); // 未结算 orders.setDeleted(Constants.ZERO); orders.setCreateTime(now); orders.setUpdateTime(now); // 寄件信息 orders.setDepositShopId(dto.getDepositShopId()); // 存件地点:省市区全路径 + 地址描述 String depositLocationRemark = depositShop.getAddress(); if (depositShop.getAreaId() != null) { Areas depositArea = areasBiz.resolveArea(depositShop.getAreaId()); if (depositArea != null) { depositLocationRemark = depositArea.getProvinceName() + depositArea.getCityName() + depositArea.getName() + depositShop.getAddress(); } } orders.setDepositLocation(depositLocationRemark); orders.setDepositLocationRemark(depositShop.getAddress()); orders.setDepositLat(BigDecimal.valueOf(depositShop.getLatitude())); orders.setDepositLgt(BigDecimal.valueOf(depositShop.getLongitude())); // 取件信息 orders.setTakeUser(dto.getTakeUser()); orders.setTakePhone(dto.getTakePhone()); orders.setExpectedDepositTime(depositTime); orders.setExpectedTakeTime(takeTime); // 计算预计存放天数 long dayDiff = (takeTime.getTime() - depositTime.getTime()) / (1000 * 60 * 60 * 24); orders.setEstimatedDepositDays((int) Math.max(1, dayDiff + 1)); if (Constants.ONE.equals(dto.getType())) { // 异地:取件点信息 orders.setTakeShopId(dto.getTakeShopId()); orders.setTakeLocation(takeLocationValue); orders.setTakeLat(takeLat); orders.setTakeLgt(takeLgt); orders.setIsUrgent(dto.getIsUrgent()); } else { // 就地:取件点同寄件店铺 orders.setTakeShopId(dto.getDepositShopId()); orders.setTakeLocation(depositShop.getAddress()); orders.setTakeLat(BigDecimal.valueOf(depositShop.getLatitude())); orders.setTakeLgt(BigDecimal.valueOf(depositShop.getLongitude())); orders.setIsUrgent(Constants.ZERO); } // 物品信息 orders.setGoodType(dto.getGoodType()); // 拼接物品信息:物品类型名称、尺寸名称*数量(数组字符串) List goodsParts = new ArrayList<>(); for (ItemPriceVO itemVO : priceResult.getItemList()) { goodsParts.add(itemVO.getCategoryName() + "*" + itemVO.getQuantity()); } orders.setGoodsInfo(goodTypeCategory.getName() + "、" + String.join(",", goodsParts)); orders.setRemark(dto.getRemark()); orders.setSelfTake(Constants.ZERO); // 费用信息(分) orders.setBasicAmount(priceResult.getItemPrice()); orders.setEstimatedAmount(priceResult.getTotalPrice()); orders.setTotalAmount(priceResult.getTotalPrice()); orders.setUrgentAmount(priceResult.getUrgentFee()); if (dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0) { orders.setDeclaredAmount(dto.getDeclaredAmount().multiply(new BigDecimal(100)).longValue()); } else { orders.setDeclaredAmount(0L); } orders.setDeclaredFee(priceResult.getInsuranceFee()); orders.setPrice(priceResult.getItemPrice()); // 薪酬计算与占比存储 calculateAndSetFeeAllocation(orders, depositShop, takeShop); ordersMapper.insert(orders); Integer orderId = orders.getId(); // ========== 7. 创建订单明细 ========== for (ItemPriceVO itemVO : priceResult.getItemList()) { OrdersDetail detail = new OrdersDetail(); detail.setOrderId(orderId); detail.setLuggageId(itemVO.getCategoryId()); detail.setLuggageName(itemVO.getCategoryName()); detail.setLuggageDetail(itemVO.getDetail()); detail.setNum(itemVO.getQuantity()); detail.setUnitPrice(itemVO.getUnitPrice()); detail.setStartDistance(itemVO.getStartDistance()); detail.setStartPrice(itemVO.getStartPrice()); detail.setExtraDistance(itemVO.getExtraDistance()); detail.setExtraPrice(itemVO.getExtraPrice()); detail.setLocallyPrice(itemVO.getLocallyPrice()); detail.setDeleted(Constants.ZERO); detail.setCreateTime(now); ordersDetailMapper.insert(detail); } // ========== 8. 保存物品图片附件 ========== if (dto.getGoodsImages() != null && !dto.getGoodsImages().isEmpty()) { int sortNum = 1; for (String imgUrl : dto.getGoodsImages()) { Multifile multifile = new Multifile(); multifile.setObjId(orderId); multifile.setObjType(Constants.FileType.ORDER_FILE.getKey()); multifile.setType(Constants.ZERO); multifile.setFileurl(imgUrl); multifile.setIsdeleted(Constants.ZERO); multifile.setCreateDate(now); multifile.setSortnum(sortNum++); multifileMapper.insert(multifile); } } // ========== 9. 唤起微信支付 ========== Member member = memberMapper.selectById(memberId); if (member == null || StringUtils.isBlank(member.getOpenid())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付"); } PayResponse payResponse = wxPay(orders, member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER); payResponse.setLockKey(lockKey); return payResponse; } @Override @Transactional(rollbackFor = Exception.class) public PayResponse continuePay(Integer orderId, Integer memberId) { // 1. 查询订单 Orders orders = ordersMapper.selectById(orderId); if (Objects.isNull(orders) || Constants.equalsInteger(orders.getDeleted(), Constants.ONE)) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } // 2. 校验订单归属 if (!Constants.equalsInteger(orders.getMemberId(), memberId)) { throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "无权操作该订单"); } // 3. 校验订单状态:仅待支付可继续支付 if (!Constants.equalsInteger(orders.getStatus(), Constants.ZERO)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不支持继续支付"); } // 4. 校验支付金额 if (orders.getTotalAmount() == null || orders.getTotalAmount() <= 0) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单金额异常,无法发起支付"); } // 5. 重新生成第三方订单编号(避免重复) String orderTradeNo = generateOrderTradeNo(); orders.setOutTradeNo(orderTradeNo); orders.setUpdateTime(new Date()); ordersMapper.updateById(orders); // 6. 唤起微信支付 Member member = memberMapper.selectById(memberId); if (member == null || StringUtils.isBlank(member.getOpenid())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付"); } return wxPay(orders, member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER); } /** * 唤起微信小程序支付 * * @param orders 订单实体(需要 code、totalAmount) * @param openid 用户微信openid * @param ordersAttach 订单支付类型 * @return PayResponse 包含微信调起参数和订单主键 */ private PayResponse wxPay(Orders orders, String openid, Constants.OrdersAttach ordersAttach) { try { WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest(); request.setBody(ordersAttach.getName()); request.setAttach(ordersAttach.getKey()); request.setOutTradeNo(orders.getOutTradeNo()); // totalAmount 单位为分,WeChat Pay setTotalFee 也是分,直接转int long totalFee = orders.getTotalAmount() != null ? orders.getTotalAmount() : 0L; request.setTotalFee((int) totalFee); request.setTimeStart(DateUtil.DateToString(new Date(), "yyyyMMddHHmmss")); request.setSpbillCreateIp(Constants.getIpAddr()); request.setOpenid(openid); Object response = WxMiniConfig.wxPayService.createOrder(request); PayResponse payResponse = new PayResponse(); payResponse.setResponse(response); payResponse.setOrderId(orders.getId()); return payResponse; } catch (WxPayException e) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "支付调起失败:" + e.getMessage()); } } @Override public OrderDetailVO findDetail(Integer id) { Orders order = ordersMapper.selectById(id); if (Objects.isNull(order)) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } OrderDetailVO vo = new OrderDetailVO(); vo.setOrder(order); // 图片路径前缀 String imgPrefix = getOrdersPrefix(); // 下单图片 (type=12) vo.setOrderFiles(getFileUrls(id, Constants.FileType.ORDER_FILE.getKey(), imgPrefix)); // 会员信息 if (order.getMemberId() != null) { Member member = memberMapper.selectById(order.getMemberId()); if (member != null) { vo.setMemberName(member.getName()); vo.setMemberPhone(member.getTelephone()); } } // 寄存门店信息 if (order.getDepositShopId() != null) { ShopInfo depositShop = shopInfoMapper.selectById(order.getDepositShopId()); if (depositShop != null) { vo.setDepositShopName(depositShop.getName()); vo.setDepositShopPhone(depositShop.getLinkPhone()); } } // 取件门店信息 if (order.getTakeShopId() != null) { ShopInfo takeShop = shopInfoMapper.selectById(order.getTakeShopId()); if (takeShop != null) { vo.setTakeShopName(takeShop.getName()); vo.setTakeShopAddress(takeShop.getAddress()); vo.setTakeShopPhone(takeShop.getLinkPhone()); } } // 接单司机信息 if (order.getAcceptDriver() != null) { DriverInfo driverInfo = driverInfoMapper.selectById(order.getAcceptDriver()); if (driverInfo != null) { vo.setDriverName(driverInfo.getName()); } } // 配送附件图片 vo.setDepositImages(getFileUrls(id, Constants.FileType.ORDER_DEPOSIT.getKey(), imgPrefix)); vo.setStoreInImages(getFileUrls(id, Constants.FileType.ORDER_TAKE.getKey(), imgPrefix)); vo.setDriverTakeImages(getFileUrls(id, Constants.FileType.DRIVER_TAKE.getKey(), imgPrefix)); vo.setDriverDoneImages(getFileUrls(id, Constants.FileType.DRIVER_DONE.getKey(), imgPrefix)); vo.setStoreOutImages(getFileUrls(id, Constants.FileType.STORE_OUT.getKey(), imgPrefix)); // 物品明细 vo.setDetailList(buildDetailList(id)); Category category = categoryMapper.selectById(order.getGoodType()); if(CollectionUtils.isNotEmpty(vo.getDetailList())&&Objects.nonNull(category)){ for (OrderItemVO v:vo.getDetailList()) { v.setTypeName(category.getName()); } } // 取消/退款状态时查询退款记录 Integer status = order.getStatus(); if (status != null && (status == Constants.OrderStatus.overdue.getStatus() || status == Constants.OrderStatus.closed.getStatus() || status == Constants.OrderStatus.cancelOverdue.getStatus() || status == Constants.OrderStatus.cancelling.getStatus() || status == Constants.OrderStatus.cancelled.getStatus())) { OrdersRefund ordersRefund = ordersRefundMapper.selectOne( new QueryWrapper().lambda() .eq(OrdersRefund::getOrderId, id) .eq(OrdersRefund::getDeleted, Constants.ZERO) .orderByDesc(OrdersRefund::getCreateTime) .last("limit 1")); if (ordersRefund != null) { vo.setOrdersRefund(ordersRefund); // 退款方式:1=平台直接取消 → 返回平台操作人名称 if (Constants.equalsInteger(ordersRefund.getType(), Constants.ONE) && ordersRefund.getUserId() != null) { // userId 关联 system_user 表,查询操作人名称 vo.setPlatformUserName(getPlatformUserName(ordersRefund.getUserId())); } // 退款方式:2=已存件申请取消 → 返回退款取件图片 (multifile objType=14) if (Constants.equalsInteger(ordersRefund.getType(), Constants.TWO)) { vo.setRefundTakeImages(getFileUrls(id, Constants.FileType.REFUND_TAKE.getKey(), imgPrefix)); } } } return vo; } @Override public OrderDispatchVO findDispatchInfo(Integer id) { Orders order = ordersMapper.selectById(id); if (Objects.isNull(order)) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } OrderDispatchVO vo = new OrderDispatchVO(); vo.setCode(order.getCode()); vo.setPayAmountYuan(order.getPayAmount() != null ? Constants.getFormatMoney(order.getPayAmount()) : 0); vo.setType(order.getType()); vo.setTypeDesc(order.getType() != null && order.getType() == Constants.ONE ? "异地存取" : "就地存取"); vo.setDetailList(buildDetailList(id)); return vo; } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void dispatch(DispatchDTO dto) { // 参数校验 if (dto == null || dto.getOrderId() == null || dto.getUrgentFee() == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单主键和加急费用不能为空"); } Orders order = ordersMapper.selectById(dto.getOrderId()); if (Objects.isNull(order)) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } // 前置条件校验:异地存取 + 已寄存 if (!Constants.ONE.equals(order.getType())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅支持异地存取订单派单"); } if (!Integer.valueOf(Constants.OrderStatus.deposited.getStatus()).equals(order.getStatus())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅已寄存状态订单可派单"); } String optUserName = getCurrentUserName(); // 加急费日志(每次单独记录本次加急费) Constants.OrderLogType urgentLogType = Constants.OrderLogType.urgent; OrderLog feeLog = new OrderLog(); feeLog.setOrderId(order.getId()); feeLog.setTitle(urgentLogType.getTitle()); feeLog.setLogInfo(urgentLogType.getStatusInfo().replace("{param}", dto.getUrgentFee().toPlainString())); feeLog.setObjType(urgentLogType.getStatus()); feeLog.setOrderStatus(order.getStatus()); feeLog.setOptUserType(3); feeLog.setOptUserName(optUserName); feeLog.setCreateTime(new Date()); feeLog.setDeleted(Constants.ZERO); orderLogService.create(feeLog); // 加急费用 元→分 long urgentFeeFen = dto.getUrgentFee().multiply(new BigDecimal(100)).longValue(); // 使用 UpdateWrapper 精确更新,不影响其他字段 com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper updateWrapper = new UpdateWrapper().lambda() .eq(Orders::getId, order.getId()) .set(Orders::getIsUrgent, Constants.ONE) .set(Orders::getPlatformRewardAmount, urgentFeeFen) .set(Orders::getUpdateTime, new Date()); // 异地寄存且有取件门店时,生成司机核销码 if (order.getTakeShopId() != null) { String driverVerifyCode = generateVerifyCode(); updateWrapper.set(Orders::getDriverVerifyCode, driverVerifyCode); } // 备注 if (StringUtils.isNotBlank(dto.getRemark())) { updateWrapper.set(Orders::getRemark, dto.getRemark()); } // 指派司机(非必填) if (dto.getDriverId() != null) { updateWrapper.set(Orders::getAssignDriverId, dto.getDriverId()); Member driver = memberMapper.selectById(dto.getDriverId()); String driverName = driver != null ? driver.getName() : String.valueOf(dto.getDriverId()); Constants.OrderLogType dispatchLogType = Constants.OrderLogType.dispatch; OrderLog driverLog = new OrderLog(); driverLog.setOrderId(order.getId()); driverLog.setTitle(dispatchLogType.getTitle()); driverLog.setLogInfo(dispatchLogType.getStatusInfo().replace("{param}", driverName)); driverLog.setObjType(dispatchLogType.getStatus()); driverLog.setOrderStatus(order.getStatus()); driverLog.setOptUserType(3); driverLog.setOptUserName(optUserName); driverLog.setCreateTime(new Date()); driverLog.setDeleted(Constants.ZERO); orderLogService.create(driverLog); } ordersMapper.update(updateWrapper); } private String getCurrentUserName() { try { com.doumee.core.model.LoginUserInfo user = (com.doumee.core.model.LoginUserInfo) org.apache.shiro.SecurityUtils.getSubject().getPrincipal(); return user != null ? user.getUsername() : "系统"; } catch (Exception e) { return "系统"; } } /** * 构建订单物品明细列表 */ private List buildDetailList(Integer orderId) { List details = ordersDetailMapper.selectList( new QueryWrapper().lambda() .eq(OrdersDetail::getOrderId, orderId) .eq(OrdersDetail::getDeleted, Constants.ZERO)); return buildDetailList(details); } /** * 根据已查询的明细构建物品列表(避免重复查询) */ private List buildDetailList(List details) { List items = new ArrayList<>(); if (details != null) { for (OrdersDetail d : details) { OrderItemVO item = new OrderItemVO(); item.setLuggageName(d.getLuggageName()); item.setLuggageDetail(d.getLuggageDetail()); item.setNum(d.getNum()); double unitPriceYuan = d.getUnitPrice() != null ? Constants.getFormatMoney(d.getUnitPrice()) : 0; item.setUnitPriceYuan(unitPriceYuan); item.setSubtotal(unitPriceYuan * (d.getNum() != null ? d.getNum() : 0)); items.add(item); } } return items; } /** * 生成32位唯一第三方订单编号(时间戳17位 + 随机数15位) */ private String generateOrderTradeNo() { return new java.text.SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + String.format("%015d", Math.abs(new java.util.Random().nextLong() % 1000000000000000L)); } /** * 生成6位数字核销码(Redis验重) * 使用 SETNX 抢占,保证唯一;使用完毕后调用 releaseVerifyCode 从 Redis 移除 */ private String generateVerifyCode() { Random random = new Random(); String redisKey = Constants.REDIS_VERIFY_CODE_KEY; for (int i = 0; i < 200; i++) { String code = String.format("%06d", random.nextInt(1000000)); Boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey + code, "1", 24, TimeUnit.HOURS); if (success != null && success) { return code; } } throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "核销码生成失败,请重试"); } /** * 释放核销码占位(核销完成后调用,移除 Redis 中的 key) */ public void releaseVerifyCode(String code) { if (StringUtils.isNotBlank(code)) { redisTemplate.delete(Constants.REDIS_VERIFY_CODE_KEY + code); } } private String getOrdersPrefix() { try { return systemDictDataBiz.queryByCode(Constants.OSS, Constants.RESOURCE_PATH).getCode() + systemDictDataBiz.queryByCode(Constants.OSS, Constants.ORDERS_FILES).getCode(); } catch (Exception e) { return ""; } } private String getPlatformUserName(Integer userId) { SystemUser user = systemUserMapper.selectById(userId); return user != null ? user.getRealname() : null; } private List getFileUrls(Integer orderId, int objType, String prefix) { List files = multifileMapper.selectList( new QueryWrapper().lambda() .eq(Multifile::getObjId, orderId) .eq(Multifile::getObjType, objType) .eq(Multifile::getIsdeleted, Constants.ZERO) .orderByAsc(Multifile::getSortnum)); List urls = new ArrayList<>(); if (files != null) { for (Multifile f : files) { if (StringUtils.isNotBlank(f.getFileurl())) { urls.add(prefix + f.getFileurl()); } } } return urls; } /** * 计算并设置订单薪酬分配(司机、存件门店、取件门店) * 从 pricing_rule (type=4) 读取分成比例,根据门店企业/个人类型区分 * * @param orders 订单实体(需要 totalAmount、cityId 已设置) * @param depositShop 寄件门店(需要 companyType) * @param takeShop 取件门店(需要 companyType,就地存取时与 depositShop 相同) */ private void calculateAndSetFeeAllocation(Orders orders, ShopInfo depositShop, ShopInfo takeShop) { Long totalAmount = orders.getTotalAmount() != null ? orders.getTotalAmount() : 0L; if (totalAmount <= 0) { orders.setDriverFee(0L); orders.setDepositShopFee(0L); orders.setTakeShopFee(0L); orders.setDriverFeeRata(BigDecimal.ZERO); orders.setDepositShopFeeRata(BigDecimal.ZERO); orders.setTakeShopFeeRata(BigDecimal.ZERO); return; } Integer cityId = Integer.valueOf(orders.getCityId()); // 司机占比:fieldA=4(配送员) BigDecimal driverRata = getRevenueShareRata(cityId, Constants.FOUR); // 寄件门店占比:fieldA=0(企业寄)/1(个人寄) int depositFieldA = Constants.equalsInteger(depositShop.getCompanyType(), Constants.ONE) ? Constants.ZERO : Constants.ONE; BigDecimal depositShopRata = getRevenueShareRata(cityId, depositFieldA); // 取件门店占比:fieldA=2(企业取)/3(个人取) int takeFieldA = Constants.equalsInteger(takeShop.getCompanyType(), Constants.ONE) ? Constants.TWO : Constants.THREE; BigDecimal takeShopRata = getRevenueShareRata(cityId, takeFieldA); // 计算薪酬(分):totalAmount 为分,rata 为比例值(如 0.15 表示 15%) long driverFee = new BigDecimal(totalAmount).multiply(driverRata).longValue(); long depositShopFee = new BigDecimal(totalAmount).multiply(depositShopRata).longValue(); long takeShopFee = totalAmount - driverFee - depositShopFee; orders.setDriverFee(driverFee); orders.setDepositShopFee(depositShopFee); orders.setTakeShopFee(takeShopFee); orders.setDriverFeeRata(driverRata); orders.setDepositShopFeeRata(depositShopRata); orders.setTakeShopFeeRata(takeShopRata); } /** * 从 pricing_rule 表获取分成比例(type=4) * * @param cityId 城市主键 * @param fieldA 类型:0=企业寄, 1=个人寄, 2=企业取, 3=个人取, 4=配送员 * @return 分成比例(如 0.15 表示 15%) */ private BigDecimal getRevenueShareRata(Integer cityId, int fieldA) { PricingRule rule = pricingRuleMapper.selectOne(new QueryWrapper().lambda() .eq(PricingRule::getDeleted, Constants.ZERO) .eq(PricingRule::getType, Constants.FOUR) .eq(PricingRule::getCityId, cityId) .eq(PricingRule::getFieldA, String.valueOf(fieldA)) .last("limit 1")); if (rule != null && StringUtils.isNotBlank(rule.getFieldC())) { return new BigDecimal(rule.getFieldC()); } return BigDecimal.ZERO; } @Override public PageData findMyOrderPage(PageWrap pageWrap, Integer memberId) { MyOrderDTO model = pageWrap.getModel(); Integer status = model != null ? model.getStatus() : null; Integer combinedStatus = model != null ? model.getCombinedStatus() : null; // 解析合并状态为具体状态列表 List statusList = null; if (combinedStatus != null) { Constants.OrderCombinedStatus combined = Constants.OrderCombinedStatus.getByKey(combinedStatus); if (combined != null) { statusList = new ArrayList<>(); for (int s : combined.getStatuses()) { statusList.add(s); } } } IPage p = new Page<>(pageWrap.getPage(), pageWrap.getCapacity()); MPJLambdaWrapper wrapper = new MPJLambdaWrapper() .selectAll(Orders.class) .select("s1.name", Orders::getDepositShopName) .select("s1.link_name", Orders::getDepositShopLinkName) .select("s1.link_phone", Orders::getDepositShopLinkPhone) .select("s2.name", Orders::getTakeShopName) .select("s2.address", Orders::getTakeShopAddress) .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID") .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID") .eq(Orders::getDeleted, Constants.ZERO) .eq(Orders::getMemberId, memberId) .eq(status != null, Orders::getStatus, status) .in(statusList != null, Orders::getStatus, statusList) .orderByDesc(Orders::getCreateTime); IPage orderPage = ordersMapper.selectJoinPage(p, Orders.class, wrapper); List voList = new ArrayList<>(); if (orderPage != null && orderPage.getRecords() != null) { for (Orders o : orderPage.getRecords()) { MyOrderVO vo = new MyOrderVO(); vo.setId(o.getId()); vo.setCode(o.getCode()); vo.setType(o.getType()); vo.setStatus(o.getStatus()); vo.setCreateTime(o.getCreateTime()); vo.setExpectedTakeTime(o.getExpectedTakeTime()); // 存件门店(关联查询直接取值) vo.setDepositShopName(o.getDepositShopName()); vo.setDepositShopLinkName(o.getDepositShopLinkName()); vo.setDepositShopPhone(o.getDepositShopLinkPhone()); // 取件信息:有取件门店取门店,无则取用户自选取件点 if (o.getTakeShopId() != null) { vo.setTakeShopName(o.getTakeShopName()); vo.setTakeShopAddress(o.getTakeShopAddress()); } else { vo.setTakeLocation(o.getTakeLocation()); vo.setTakeLocationRemark(o.getTakeLocationRemark()); } // 取件联系人 vo.setTakeUser(o.getTakeUser()); vo.setTakePhone(o.getTakePhone()); // 费用(分) vo.setDeclaredFee(o.getDeclaredFee()); vo.setEstimatedAmount(o.getEstimatedAmount()); // 查询物品明细(一次查询,同时用于物品列表和逾期计算) List details = ordersDetailMapper.selectList( new QueryWrapper().lambda() .eq(OrdersDetail::getOrderId, o.getId()) .eq(OrdersDetail::getDeleted, Constants.ZERO)); // 物品明细 vo.setDetailList(buildDetailList(details)); // 逾期信息(仅待取件状态计算) if (Integer.valueOf(Constants.OrderStatus.arrived.getStatus()).equals(o.getStatus())) { OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(o, details); vo.setOverdue(overdueInfo.getOverdue()); vo.setOverdueDays(overdueInfo.getOverdueDays()); vo.setOverdueFee(overdueInfo.getOverdueFee()); } voList.add(vo); } } IPage vPage = new Page<>(pageWrap.getPage(), pageWrap.getCapacity()); PageData pageData = PageData.from(vPage); pageData.setRecords(voList); pageData.setTotal(orderPage.getTotal()); pageData.setPage(orderPage.getCurrent()); pageData.setCapacity(orderPage.getSize()); return pageData; } @Override public PageData findShopOrderPage(PageWrap pageWrap, Integer shopId) { MyOrderDTO model = pageWrap.getModel(); Integer status = model != null ? model.getStatus() : null; Integer combinedStatus = model != null ? model.getCombinedStatus() : null; // 解析合并状态为具体状态列表 List statusList = null; if (combinedStatus != null) { Constants.OrderCombinedStatus combined = Constants.OrderCombinedStatus.getByKey(combinedStatus); if (combined != null) { statusList = new ArrayList<>(); for (int s : combined.getStatuses()) { statusList.add(s); } } } IPage p = new Page<>(pageWrap.getPage(), pageWrap.getCapacity()); MPJLambdaWrapper wrapper = new MPJLambdaWrapper() .selectAll(Orders.class) .select("s1.name", Orders::getDepositShopName) .select("s1.link_name", Orders::getDepositShopLinkName) .select("s1.link_phone", Orders::getDepositShopLinkPhone) .select("s2.name", Orders::getTakeShopName) .select("s2.address", Orders::getTakeShopAddress) .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID") .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID") .eq(Orders::getPayStatus, Constants.ONE) .and(w -> w.eq(Orders::getDepositShopId, shopId).or().eq(Orders::getTakeShopId, shopId)) .eq(status != null, Orders::getStatus, status) .in(statusList != null, Orders::getStatus, statusList) .orderByDesc(Orders::getCreateTime); IPage orderPage = ordersMapper.selectJoinPage(p, Orders.class, wrapper); List voList = new ArrayList<>(); if (orderPage != null && orderPage.getRecords() != null) { for (Orders o : orderPage.getRecords()) { MyOrderVO vo = new MyOrderVO(); vo.setId(o.getId()); vo.setCode(o.getCode()); vo.setType(o.getType()); vo.setStatus(o.getStatus()); vo.setCreateTime(o.getCreateTime()); vo.setExpectedTakeTime(o.getExpectedTakeTime()); vo.setDepositShopName(o.getDepositShopName()); vo.setDepositShopLinkName(o.getDepositShopLinkName()); vo.setDepositShopPhone(o.getDepositShopLinkPhone()); // 门店角色:存件门店=1,取件门店=2 if (Constants.equalsInteger(o.getDepositShopId(), shopId)) { vo.setShopRole(Constants.ONE); } else if (Constants.equalsInteger(o.getTakeShopId(), shopId)) { vo.setShopRole(Constants.TWO); } if (o.getTakeShopId() != null) { vo.setTakeShopName(o.getTakeShopName()); vo.setTakeShopAddress(o.getTakeShopAddress()); } else { vo.setTakeLocation(o.getTakeLocation()); vo.setTakeLocationRemark(o.getTakeLocationRemark()); } vo.setTakeUser(o.getTakeUser()); vo.setTakePhone(o.getTakePhone()); vo.setDeclaredFee(o.getDeclaredFee()); vo.setEstimatedAmount(o.getEstimatedAmount()); List details = ordersDetailMapper.selectList( new QueryWrapper().lambda() .eq(OrdersDetail::getOrderId, o.getId()) .eq(OrdersDetail::getDeleted, Constants.ZERO)); vo.setDetailList(buildDetailList(details)); if (Integer.valueOf(Constants.OrderStatus.arrived.getStatus()).equals(o.getStatus())) { OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(o, details); vo.setOverdue(overdueInfo.getOverdue()); vo.setOverdueDays(overdueInfo.getOverdueDays()); vo.setOverdueFee(overdueInfo.getOverdueFee()); } voList.add(vo); } } IPage vPage = new Page<>(pageWrap.getPage(), pageWrap.getCapacity()); PageData pageData = PageData.from(vPage); pageData.setRecords(voList); pageData.setTotal(orderPage.getTotal()); pageData.setPage(orderPage.getCurrent()); pageData.setCapacity(orderPage.getSize()); return pageData; } @Override public MyOrderDetailVO findMyOrderDetail(Integer id, Integer memberId) { Orders order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getId, id) .eq(Orders::getMemberId, memberId) .eq(Orders::getDeleted, Constants.ZERO)); if (order == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } return buildOrderDetailVO(order, true); } @Override public MyOrderDetailVO findShopOrderDetail(Integer orderId, String verifyCode) { Orders order = null; if (orderId != null) { order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getId, orderId) .eq(Orders::getDeleted, Constants.ZERO)); } else if (StringUtils.isNotBlank(verifyCode)) { order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getMemberVerifyCode, verifyCode) .eq(Orders::getDeleted, Constants.ZERO) .last("limit 1")); if (order == null) { order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getDriverVerifyCode, verifyCode) .eq(Orders::getDeleted, Constants.ZERO) .last("limit 1")); } } if (order == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在"); } return buildOrderDetailVO(order, false); } /** * 构建订单详情VO(会员端/门店端复用) * * @param order 订单实体 * @param memberViewMode true=会员端(按条件返回核销码),false=门店端(始终返回核销码) */ private MyOrderDetailVO buildOrderDetailVO(Orders order, boolean memberViewMode) { MyOrderDetailVO vo = new MyOrderDetailVO(); vo.setId(order.getId()); vo.setStatus(order.getStatus()); vo.setType(order.getType()); vo.setCode(order.getCode()); vo.setOutTradeNo(order.getOutTradeNo()); vo.setRemark(order.getRemark()); vo.setCreateTime(order.getCreateTime()); vo.setPayTime(order.getPayTime()); vo.setExpectedDepositTime(order.getExpectedDepositTime()); vo.setExpectedTakeTime(order.getExpectedTakeTime()); vo.setArriveTime(order.getArriveTime()); // 费用(分) vo.setBasicAmount(order.getBasicAmount()); vo.setDeclaredAmount(order.getDeclaredAmount()); vo.setDeclaredFee(order.getDeclaredFee()); vo.setUrgentAmount(order.getUrgentAmount()); vo.setActualPayAmount(order.getPayAmount()); // 标记 vo.setExceptionStatus(order.getExceptionStatus()); // 是否超出取件时间 vo.setPastTakeTime(order.getExpectedTakeTime() != null && new Date().after(order.getExpectedTakeTime())); // 订单状态描述 + 倒计时 vo.setStatusDesc(buildStatusDesc(order)); if (Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.waitPay.getStatus())) { vo.setPayCountdownMs(calcPayCountdownMs(order)); } // 存件门店 if (order.getDepositShopId() != null) { ShopInfo depositShop = shopInfoMapper.selectById(order.getDepositShopId()); if (depositShop != null) { vo.setDepositShopName(depositShop.getName()); vo.setDepositShopLinkName(depositShop.getLinkName()); vo.setDepositShopPhone(depositShop.getLinkPhone()); vo.setDepositShopAddress(depositShop.getAddress()); } } // 取件信息 if (order.getTakeShopId() != null) { ShopInfo takeShop = shopInfoMapper.selectById(order.getTakeShopId()); if (takeShop != null) { vo.setTakeShopName(takeShop.getName()); vo.setTakeShopAddress(takeShop.getAddress()); } } else { vo.setTakeLocation(order.getTakeLocation()); vo.setTakeLocationRemark(order.getTakeLocationRemark()); } // 取件联系人 vo.setTakeUser(order.getTakeUser()); vo.setTakePhone(order.getTakePhone()); // 物品类型名称 if (order.getGoodType() != null) { Category category = categoryMapper.selectById(order.getGoodType()); if (category != null) { vo.setGoodTypeName(category.getName()); } } // 下单照片 String imgPrefix = getOrdersPrefix(); vo.setOrderImages(getFileUrls(order.getId(), Constants.FileType.ORDER_FILE.getKey(), imgPrefix)); // 物品明细 List details = ordersDetailMapper.selectList( new QueryWrapper().lambda() .eq(OrdersDetail::getOrderId, order.getId()) .eq(OrdersDetail::getDeleted, Constants.ZERO)); vo.setDetailList(buildDetailList(details)); // 逾期信息 OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(order, details); vo.setOverdue(overdueInfo.getOverdue()); vo.setOverdueDays(overdueInfo.getOverdueDays()); vo.setOverdueFee(overdueInfo.getOverdueFee()); // 核销码 Integer status = order.getStatus(); if (memberViewMode) { // 会员端:待寄存(1)返回;待取件(5)时,就地寄存无取件门店不返回 boolean returnCode = false; if (Constants.equalsInteger(status, Constants.OrderStatus.waitDeposit.getStatus())) { returnCode = true; } else if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) { if (!(Constants.equalsInteger(order.getType(), Constants.ZERO) && order.getTakeShopId() == null)) { returnCode = true; } } if (returnCode) { vo.setMemberVerifyCode(order.getMemberVerifyCode()); } } else { // 门店端:始终返回会员核销码 vo.setMemberVerifyCode(order.getMemberVerifyCode()); } return vo; } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void cancelOrder(Integer orderId, Integer memberId, String reason) { Orders order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getId, orderId) .eq(Orders::getMemberId, memberId) .eq(Orders::getDeleted, Constants.ZERO)); if (order == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } // 仅异地寄存可取消 if (!Constants.equalsInteger(order.getType(), Constants.ONE)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅异地寄存订单可取消"); } Integer status = order.getStatus(); if (status == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单状态异常"); } Date now = new Date(); // 待支付:直接取消 if (Constants.equalsInteger(status, Constants.OrderStatus.waitPay.getStatus())) { order.setStatus(Constants.OrderStatus.cancelled.getStatus()); order.setCancelTime(now); ordersMapper.updateById(order); saveCancelLog(order, "会员取消订单(待支付)", reason, memberId); return; } // 待寄存:直接取消,全额退款 if (Constants.equalsInteger(status, Constants.OrderStatus.waitDeposit.getStatus())) { // 记录退款信息 OrdersRefund refund = new OrdersRefund(); refund.setOrderId(orderId); refund.setType(0); // 未寄存直接取消 refund.setCancelInfo(reason); refund.setCreateTime(now); refund.setDeleted(Constants.ZERO); // 调用微信退款,全额退款 String refundCode = wxMiniUtilService.wxRefund(order.getOutTradeNo(), order.getPayAmount(), order.getPayAmount()); refund.setRefundCode(refundCode); refund.setRefundTime(new Date()); ordersRefundMapper.insert(refund); order.setStatus(Constants.OrderStatus.cancelled.getStatus()); order.setCancelTime(now); order.setRefundAmount(order.getPayAmount()); ordersMapper.updateById(order); saveCancelLog(order, "会员取消订单(待寄存,全额退款)", reason, memberId); return; } // 已寄存/已接单:进入取消中状态 if (Constants.equalsInteger(status, Constants.OrderStatus.deposited.getStatus()) || Constants.equalsInteger(status, Constants.OrderStatus.accepted.getStatus())) { order.setStatus(Constants.OrderStatus.cancelling.getStatus()); order.setCancelTime(now); ordersMapper.updateById(order); saveCancelLog(order, "会员申请取消订单(已寄存/已接单)", reason, memberId); return; } throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许取消"); } /** * 保存取消订单操作日志 */ private void saveCancelLog(Orders order, String title, String reason, Integer memberId) { OrderLog log = new OrderLog(); log.setOrderId(order.getId()); log.setTitle(title); log.setLogInfo(reason); log.setObjType(Constants.ORDER_LOG_CANCEL); log.setOrderStatus(order.getStatus()); log.setOptUserId(memberId); log.setOptUserType(0); // 0=用户 log.setCreateTime(new Date()); log.setDeleted(Constants.ZERO); orderLogService.create(log); } /** * 保存门店核销日志 */ private void saveShopVerifyLog(Orders order, String title, String logInfo, String remark, Integer shopId) { OrderLog log = new OrderLog(); log.setOrderId(order.getId()); log.setTitle(title); log.setLogInfo(logInfo); log.setRemark(remark); log.setOrderStatus(order.getStatus()); log.setOptUserId(shopId); log.setOptUserType(2); // 2=门店 log.setCreateTime(new Date()); log.setDeleted(Constants.ZERO); orderLogService.create(log); } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void handleStorageOrderPayNotify(String outTradeNo, String wxTradeNo) { Orders order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getOutTradeNo, outTradeNo) .eq(Orders::getDeleted, Constants.ZERO) .last("limit 1")); if (order == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在: " + outTradeNo); } // 幂等:已支付则跳过 if (Constants.equalsInteger(order.getPayStatus(), Constants.ONE)) { return; } Date now = new Date(); order.setStatus(Constants.OrderStatus.waitDeposit.getStatus()); // 待寄存 order.setPayStatus(Constants.ONE); // 已支付 order.setPayTime(now); order.setWxExternalNo(wxTradeNo); order.setUpdateTime(now); // 生成会员核销码 order.setMemberVerifyCode(generateVerifyCode()); ordersMapper.updateById(order); } @Override @Transactional(rollbackFor = Exception.class) public PayResponse payOverdueFee(Integer orderId, Integer memberId) { // 1. 查询寄存订单 Orders order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getId, orderId) .eq(Orders::getMemberId, memberId) .eq(Orders::getDeleted, Constants.ZERO)); if (order == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在"); } // 2. 校验状态:待取件(5) + 逾期(1) if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.arrived.getStatus())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不支持逾期支付"); } if (!Constants.equalsInteger(order.getOverdueStatus(), Constants.ONE)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不存在逾期费用"); } if (order.getOverdueAmount() == null || order.getOverdueAmount() <= 0) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "逾期费用异常,无法发起支付"); } // 3. 查询会员 Member member = memberMapper.selectById(memberId); if (member == null || StringUtils.isBlank(member.getOpenid())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付"); } // 4. 创建逾期费用订单 String outTradeNo = generateOrderTradeNo(); Date now = new Date(); OtherOrders otherOrders = new OtherOrders(); otherOrders.setType(Constants.TWO); // 2=逾期费用订单 otherOrders.setMemberId(memberId); otherOrders.setOrderId(orderId); otherOrders.setPayAccount(order.getOverdueAmount()); otherOrders.setPayStatus(Constants.ZERO); otherOrders.setCode("OD" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now) + orderId); otherOrders.setOutTradeNo(outTradeNo); otherOrders.setDeleted(Constants.ZERO); otherOrders.setCreateTime(now); otherOrdersMapper.insert(otherOrders); // 5. 唤起微信支付 return wxPayForOtherOrder(otherOrders, member.getOpenid(), Constants.OrdersAttach.OVERDUE_FEE); } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void handleOverdueFeePayNotify(String outTradeNo, String wxTradeNo) { // 1. 查找逾期费用订单 OtherOrders otherOrders = otherOrdersMapper.selectOne(new QueryWrapper().lambda() .eq(OtherOrders::getOutTradeNo, outTradeNo) .eq(OtherOrders::getDeleted, Constants.ZERO) .last("limit 1")); if (otherOrders == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "逾期费用订单不存在: " + outTradeNo); } // 2. 幂等:已支付则跳过 if (Constants.equalsInteger(otherOrders.getPayStatus(), Constants.ONE)) { return; } Date now = new Date(); // 3. 更新逾期费用订单状态 otherOrders.setPayStatus(Constants.ONE); otherOrders.setPayTime(now); otherOrders.setWxExternalNo(wxTradeNo); otherOrders.setUpdateTime(now); otherOrdersMapper.updateById(otherOrders); // 4. 更新寄存订单逾期状态为已支付(2),更新总金额,重算三方收益 if (otherOrders.getOrderId() != null) { Orders order = ordersMapper.selectById(otherOrders.getOrderId()); if (order != null) { order.setOverdueStatus(Constants.TWO); // 2=已支付 // 总金额 = 原金额 + 逾期费用 Long overdueFee = otherOrders.getPayAccount() != null ? otherOrders.getPayAccount() : 0L; long newTotal = (order.getTotalAmount() != null ? order.getTotalAmount() : 0L) + overdueFee; order.setTotalAmount(newTotal); order.setUpdateTime(now); ordersMapper.updateById(order); // 重算三方收益 calculateAndSaveOrderFees(order.getId()); } } } @Override public void deleteMyOrder(Integer orderId, Integer memberId) { Orders order = ordersMapper.selectById(orderId); if (order == null || !Constants.equalsInteger(order.getDeleted(), Constants.ZERO)) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } if (!Constants.equalsInteger(order.getMemberId(), memberId)) { throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "无权操作此订单"); } // 仅已完成(7)、已取消(99)、已退款(96)可删除 int status = Constants.formatIntegerNum(order.getStatus()); if (status != Constants.OrderStatus.finished.getStatus() && status != Constants.OrderStatus.cancelled.getStatus() && status != Constants.OrderStatus.closed.getStatus()) { throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "当前订单状态不可删除"); } ordersMapper.update(new UpdateWrapper().lambda() .set(Orders::getDeleted, Constants.ONE) .set(Orders::getUpdateTime, new Date()) .eq(Orders::getId, orderId)); } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public PayResponse payShopDeposit(Integer shopId) { // 1. 查询门店信息 ShopInfo shopInfo = shopInfoMapper.selectById(shopId); if (shopInfo == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "门店不存在"); } // 2. 校验状态:审批通过(1)才能支付押金 if (!Constants.equalsInteger(shopInfo.getAuditStatus(), Constants.ONE)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前门店状态不支持支付押金"); } if (shopInfo.getDepositAmount() == null || shopInfo.getDepositAmount() <= 0) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "押金金额异常,无法发起支付"); } // 3. 查询会员openid Member member = memberMapper.selectById(shopInfo.getRegionMemberId()); if (member == null || StringUtils.isBlank(member.getOpenid())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付"); } // 4. 创建押金订单 String outTradeNo = generateOrderTradeNo(); Date now = new Date(); OtherOrders otherOrders = new OtherOrders(); otherOrders.setType(Constants.ZERO); // 0=店铺押金订单 otherOrders.setMemberId(shopInfo.getRegionMemberId()); otherOrders.setPayAccount(shopInfo.getDepositAmount()); otherOrders.setPayStatus(Constants.ZERO); otherOrders.setCode("SD" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now) + shopId); otherOrders.setOutTradeNo(outTradeNo); otherOrders.setDeleted(Constants.ZERO); otherOrders.setCreateTime(now); otherOrdersMapper.insert(otherOrders); // 5. 唤起微信支付 return wxPayForOtherOrder(otherOrders, member.getOpenid(), Constants.OrdersAttach.SHOP_DEPOSIT); } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void handleShopDepositPayNotify(String outTradeNo, String wxTradeNo) { // 1. 查找押金订单 OtherOrders otherOrders = otherOrdersMapper.selectOne(new QueryWrapper().lambda() .eq(OtherOrders::getOutTradeNo, outTradeNo) .eq(OtherOrders::getDeleted, Constants.ZERO) .last("limit 1")); if (otherOrders == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "押金订单不存在: " + outTradeNo); } // 2. 幂等:已支付则跳过 if (Constants.equalsInteger(otherOrders.getPayStatus(), Constants.ONE)) { return; } Date now = new Date(); // 3. 更新押金订单状态 otherOrders.setPayStatus(Constants.ONE); otherOrders.setPayTime(now); otherOrders.setWxExternalNo(wxTradeNo); otherOrders.setUpdateTime(now); otherOrdersMapper.updateById(otherOrders); // 4. 查询门店信息(通过注册会员主键关联) ShopInfo shopInfo = shopInfoMapper.selectOne(new QueryWrapper().lambda() .eq(ShopInfo::getRegionMemberId, otherOrders.getMemberId()) .eq(ShopInfo::getDeleted, Constants.ZERO) .last("limit 1")); if (shopInfo == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "门店不存在"); } // 5. 更新门店状态:已支付押金 shopInfo.setAuditStatus(Constants.THREE); // 3=已支付押金 shopInfo.setPayStatus(Constants.ONE); shopInfo.setPayTime(now); shopInfo.setWxExternalNo(wxTradeNo); shopInfo.setCode(otherOrders.getCode()); Member member = memberMapper.selectById(otherOrders.getMemberId()); if (member != null) { shopInfo.setPayMemberOpenId(member.getOpenid()); } shopInfo.setUpdateTime(now); shopInfoMapper.updateById(shopInfo); // 6. 押金支付完成后,若城市未开通则自动开通 if (shopInfo.getAreaId() != null) { Areas shopArea = areasBiz.resolveArea(shopInfo.getAreaId()); if (shopArea != null && shopArea.getParentId() != null) { Areas cityArea = areasBiz.resolveArea(shopArea.getParentId()); if (cityArea != null && !Constants.equalsInteger(cityArea.getStatus(), Constants.ONE)) { cityArea.setStatus(Constants.ONE); cityArea.setEditDate(now); areasService.updateById(cityArea); areasService.cacheData(); } } } } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void settleOrders() { // 1. 读取结算天数配置 SystemDictData settlementConfig = systemDictDataBiz.queryByCode(Constants.OPERATION_CONFIG, Constants.OP_SETTLEMENT_DATE); if (settlementConfig == null || StringUtils.isBlank(settlementConfig.getCode())) { return; } int days = Integer.parseInt(settlementConfig.getCode()); // 结算截止时间 = 当前时间 - N天 Calendar cal = Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, -days); Date deadline = cal.getTime(); // 2. 查询已完成的待结算订单(完成时间 <= 截止时间) List ordersList = ordersMapper.selectList(new QueryWrapper().lambda() .eq(Orders::getDeleted, Constants.ZERO) .eq(Orders::getStatus, Constants.OrderStatus.finished.getStatus()) .eq(Orders::getSettlementStatus, Constants.ZERO) .le(Orders::getFinishTime, deadline)); if (ordersList == null || ordersList.isEmpty()) { return; } Date now = new Date(); for (Orders order : ordersList) { // 3. 更新订单结算状态 ordersMapper.update(new UpdateWrapper().lambda() .set(Orders::getSettlementStatus, Constants.ONE) .set(Orders::getSettlementTime, now) .set(Orders::getUpdateTime, now) .eq(Orders::getId, order.getId())); // 4. 查询关联的待入账 Revenue 记录 List revenues = revenueMapper.selectList(new QueryWrapper().lambda() .eq(Revenue::getObjId, order.getId()) .eq(Revenue::getObjType, Constants.ZERO) .eq(Revenue::getVaildStatus, Constants.ZERO) .eq(Revenue::getDeleted, Constants.ZERO)); for (Revenue revenue : revenues) { Long amount = revenue.getAmount() != null ? revenue.getAmount() : 0L; // 更新 Revenue 为已入账 revenueMapper.update(new UpdateWrapper().lambda() .set(Revenue::getVaildStatus, Constants.ONE) .set(Revenue::getUpdateTime, now) .eq(Revenue::getId, revenue.getId())); // 根据 memberType 更新余额 if (Constants.equalsInteger(revenue.getMemberType(), Constants.ONE)) { // 司机:通过 memberId 查 DriverInfo,更新 balance / totalBalance DriverInfo driver = driverInfoMapper.selectOne(new QueryWrapper().lambda() .eq(DriverInfo::getMemberId, revenue.getMemberId()) .eq(DriverInfo::getDeleted, Constants.ZERO) .last("limit 1")); if (driver != null) { driverInfoMapper.update(new UpdateWrapper().lambda() .setSql(" BALANCE = IFNULL(BALANCE, 0) + " + amount) .setSql(" TOTAL_BALANCE = IFNULL(TOTAL_BALANCE, 0) + " + amount) .eq(DriverInfo::getId, driver.getId())); } } else if (Constants.equalsInteger(revenue.getMemberType(), Constants.TWO)) { // 门店:通过 memberId 查 ShopInfo(regionMemberId),更新 balance / totalBalance ShopInfo shop = shopInfoMapper.selectOne(new QueryWrapper().lambda() .eq(ShopInfo::getRegionMemberId, revenue.getMemberId()) .eq(ShopInfo::getDeleted, Constants.ZERO) .last("limit 1")); if (shop != null) { shopInfoMapper.update(new UpdateWrapper().lambda() .setSql(" BALANCE = IFNULL(BALANCE, 0) + " + amount) .setSql(" TOTAL_BALANCE = IFNULL(TOTAL_BALANCE, 0) + " + amount) .eq(ShopInfo::getId, shop.getId())); } } } } } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void commentOrder(CommentOrderDTO dto, Integer memberId) { // 1. 校验订单 Orders order = ordersMapper.selectById(dto.getOrderId()); if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在"); } if (!Constants.equalsInteger(order.getMemberId(), memberId)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "无权评价该订单"); } if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.finished.getStatus())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不支持评价"); } if (Constants.equalsInteger(order.getCommentStatus(), Constants.ONE)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单已评价"); } // 2. 异地寄存订单:取件门店和司机评分校验 boolean isRemote = Constants.equalsInteger(order.getType(), Constants.ONE); if (isRemote) { if (dto.getDriverScore() == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "异地寄存订单必须评价司机"); } if (order.getTakeShopId() != null && dto.getTakeScore() == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请评价取件门店"); } } Date now = new Date(); // 3. 更新订单评价状态 order.setCommentStatus(Constants.ONE); order.setCommentInfo(dto.getContent()); order.setCommentDepositLevel(dto.getDepositScore()); order.setCommentTakeLevel(dto.getTakeScore()); order.setCommentDriverLevel(dto.getDriverScore()); order.setCommentTime(now); order.setUpdateTime(now); ordersMapper.updateById(order); // 4. 创建评价记录 // 4.1 存件门店 OrderComment depositComment = new OrderComment(); depositComment.setOrderId(order.getId()); depositComment.setOrderCode(order.getCode()); depositComment.setMemberId(memberId); depositComment.setTargetType(Constants.ONE); // 1=存件门店 depositComment.setTargetId(order.getDepositShopId()); depositComment.setScore(dto.getDepositScore()); depositComment.setContent(dto.getContent()); depositComment.setDeleted(Constants.ZERO); depositComment.setCreateTime(now); orderCommentMapper.insert(depositComment); // 4.2 取件门店(异地寄存且有取件门店) if (isRemote && order.getTakeShopId() != null && dto.getTakeScore() != null) { OrderComment takeComment = new OrderComment(); takeComment.setOrderId(order.getId()); takeComment.setOrderCode(order.getCode()); takeComment.setMemberId(memberId); takeComment.setTargetType(Constants.TWO); // 2=取件门店 takeComment.setTargetId(order.getTakeShopId()); takeComment.setScore(dto.getTakeScore()); takeComment.setContent(dto.getContent()); takeComment.setDeleted(Constants.ZERO); takeComment.setCreateTime(now); orderCommentMapper.insert(takeComment); } // 4.3 司机(异地寄存) if (isRemote && order.getAcceptDriver() != null && dto.getDriverScore() != null) { OrderComment driverComment = new OrderComment(); driverComment.setOrderId(order.getId()); driverComment.setOrderCode(order.getCode()); driverComment.setMemberId(memberId); driverComment.setTargetType(Constants.THREE); // 3=司机 driverComment.setTargetId(order.getAcceptDriver()); driverComment.setScore(dto.getDriverScore()); driverComment.setContent(dto.getContent()); driverComment.setDeleted(Constants.ZERO); driverComment.setCreateTime(now); orderCommentMapper.insert(driverComment); } // 5. 更新门店/司机平均评分 updateTargetScore(Constants.ONE, order.getDepositShopId()); if (isRemote && order.getTakeShopId() != null) { updateTargetScore(Constants.TWO, order.getTakeShopId()); } if (isRemote && order.getAcceptDriver() != null) { updateTargetScore(Constants.THREE, order.getAcceptDriver()); } } /** * 更新评价对象(门店/司机)的平均评分 */ private void updateTargetScore(Integer targetType, Integer targetId) { List comments = orderCommentMapper.selectList(new QueryWrapper().lambda() .eq(OrderComment::getDeleted, Constants.ZERO) .eq(OrderComment::getTargetType, targetType) .eq(OrderComment::getTargetId, targetId)); if (comments.isEmpty()) { return; } double avg = comments.stream() .mapToInt(OrderComment::getScore) .average() .orElse(0.0); BigDecimal score = BigDecimal.valueOf(avg).setScale(1, BigDecimal.ROUND_HALF_UP); Date now = new Date(); if (Constants.equalsInteger(targetType, Constants.ONE) || Constants.equalsInteger(targetType, Constants.TWO)) { ShopInfo shopInfo = shopInfoMapper.selectById(targetId); if (shopInfo != null) { shopInfo.setScore(score); shopInfo.setUpdateTime(now); shopInfoMapper.updateById(shopInfo); } } else if (Constants.equalsInteger(targetType, Constants.THREE)) { DriverInfo driverInfo = driverInfoMapper.selectById(targetId); if (driverInfo != null) { driverInfo.setScore(score); driverInfo.setUpdateTime(now); driverInfoMapper.updateById(driverInfo); } } } /** * 唤起微信支付(其他订单) */ private PayResponse wxPayForOtherOrder(OtherOrders otherOrders, String openid, Constants.OrdersAttach ordersAttach) { try { WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest(); request.setBody(ordersAttach.getName()); request.setAttach(ordersAttach.getKey()); request.setOutTradeNo(otherOrders.getOutTradeNo()); long totalFee = otherOrders.getPayAccount() != null ? otherOrders.getPayAccount() : 0L; request.setTotalFee((int) totalFee); request.setTimeStart(DateUtil.DateToString(new Date(), "yyyyMMddHHmmss")); request.setSpbillCreateIp(Constants.getIpAddr()); request.setOpenid(openid); Object response = WxMiniConfig.wxPayService.createOrder(request); PayResponse payResponse = new PayResponse(); payResponse.setResponse(response); payResponse.setOrderId(otherOrders.getId()); return payResponse; } catch (WxPayException e) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "支付调起失败:" + e.getMessage()); } } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void shopVerifyOrder(String verifyCode, Integer shopId, List images, String remark) { if (StringUtils.isBlank(verifyCode)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "核销码不能为空"); } // 根据核销码查找订单(会员核销码) Orders order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getMemberVerifyCode, verifyCode) .eq(Orders::getDeleted, Constants.ZERO) .last("limit 1")); if (order == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "核销码无效"); } // 查询门店名称用于日志 String shopName = ""; ShopInfo shopInfo = shopInfoMapper.selectById(shopId); if (shopInfo != null) { shopName = shopInfo.getName() != null ? shopInfo.getName() : ""; } Integer status = order.getStatus(); Date now = new Date(); if (Constants.equalsInteger(status, Constants.OrderStatus.waitDeposit.getStatus())) { // 待寄存(1) → 已寄存(2),两种类型通用 // 校验当前门店是否为订单的存件门店 if (!shopId.equals(order.getDepositShopId())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店,无法核销"); } order.setStatus(Constants.OrderStatus.deposited.getStatus()); order.setDepositTime(now); // 释放当前核销码,生成新的核销码供取件时使用 releaseVerifyCode(verifyCode); order.setMemberVerifyCode(generateVerifyCode()); ordersMapper.updateById(order); // 保存寄存图片(obj_type=2 订单寄存图片,最多3张) saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_DEPOSIT.getKey(), shopId); // 记录订单日志 saveShopVerifyLog(order, "门店确认寄存", "门店【" + shopName + "】确认寄存", remark, shopId); } else if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) { // 异地寄存 + 无取件门店 → 无法核销(客户自取,无门店操作) if (Constants.equalsInteger(order.getType(), Constants.ONE) && order.getTakeShopId() == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单无取件门店,无法核销"); } // 校验取件门店与当前登录门店一致 if (!shopId.equals(order.getTakeShopId())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店,无法核销"); } // 待取件(5) → 已完成(7) order.setStatus(Constants.OrderStatus.finished.getStatus()); order.setConfirmArriveTime(now); ordersMapper.updateById(order); // 订单完成,释放核销码 releaseVerifyCode(verifyCode); // 保存出库图片(obj_type=13 门店出库图片,最多3张) saveVerifyImages(order.getId(), images, Constants.FileType.STORE_OUT.getKey(), shopId); // 生成收益记录 calculateAndSaveOrderFees(order.getId()); generateRevenueRecords(order.getId()); // 记录订单日志 saveShopVerifyLog(order, "门店确认取件", "门店【" + shopName + "】确认取件,订单完成", remark, shopId); } else { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许核销"); } } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void confirmStoreOut(Integer orderId, Integer shopId, List images, String remark) { // 1. 查询订单 Orders order = ordersMapper.selectById(orderId); if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在"); } // 2. 校验状态:待取件(5) if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.arrived.getStatus())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许出库"); } // 3. 校验逾期状态:0=未逾期 或 2=已支付 if (order.getOverdueStatus() != null && Constants.equalsInteger(order.getOverdueStatus(), Constants.ONE)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单存在逾期未支付费用,请先完成逾期费用支付"); } // 4. 校验确认到店时间不为空 if (order.getConfirmArriveTime() == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单尚未确认到店,无法出库"); } // 5. 校验门店与订单关系 if (Constants.equalsInteger(order.getType(), Constants.ZERO)) { // 就地寄存:取件门店即存件门店 if (!shopId.equals(order.getDepositShopId())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店"); } } else { // 异地寄存:校验取件门店 if (order.getTakeShopId() == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单无取件门店,无法出库"); } if (!shopId.equals(order.getTakeShopId())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店"); } } // 6. 查询门店名称 String shopName = ""; ShopInfo shopInfo = shopInfoMapper.selectById(shopId); if (shopInfo != null) { shopName = shopInfo.getName() != null ? shopInfo.getName() : ""; } // 7. 更新订单状态为已完成 Date now = new Date(); order.setStatus(Constants.OrderStatus.finished.getStatus()); order.setFinishTime(now); order.setUpdateTime(now); ordersMapper.updateById(order); // 8. 释放核销码 if (StringUtils.isNotBlank(order.getMemberVerifyCode())) { releaseVerifyCode(order.getMemberVerifyCode()); } // 9. 保存出库图片(obj_type=13 门店出库图片,最多3张) saveVerifyImages(order.getId(), images, Constants.FileType.STORE_OUT.getKey(), shopId); // 10. 如果存在退款金额,先保存退款记录再调用微信退款 // 退款记录在退款调用前落库,避免退款成功但本地异常导致无记录 if (order.getRefundAmount() != null && order.getRefundAmount() > 0 && StringUtils.isNotBlank(order.getOutTradeNo()) && order.getPayAmount() != null && order.getPayAmount() > 0) { OrdersRefund refundRecord = new OrdersRefund(); refundRecord.setOrderId(orderId); refundRecord.setType(3); // 出库退款 refundRecord.setCreateTime(now); refundRecord.setRefundRemark(remark); refundRecord.setDeleted(Constants.ZERO); ordersRefundMapper.insert(refundRecord); // 调用微信退款(放在最后,确保前置操作全部成功) String refundCode = wxMiniUtilService.wxRefund( order.getOutTradeNo(), order.getPayAmount(), order.getRefundAmount()); // 退款成功后回填退款单号和时间 refundRecord.setRefundCode(refundCode); refundRecord.setRefundTime(new Date()); ordersRefundMapper.updateById(refundRecord); } // 11. 生成收益记录 calculateAndSaveOrderFees(orderId); generateRevenueRecords(orderId); // 12. 记录订单日志 String logInfo = "门店【" + shopName + "】确认出库,订单完成"; if (order.getRefundAmount() != null && order.getRefundAmount() > 0) { logInfo += ",退款" + Constants.getFormatMoney(order.getRefundAmount()) + "元"; } saveShopVerifyLog(order, "门店确认出库", logInfo, remark, shopId); } @Override public void calculateAndSaveOrderFees(Integer orderId) { Orders order = ordersMapper.selectById(orderId); if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在"); } Long totalAmount = order.getTotalAmount() != null ? order.getTotalAmount() : 0L; // 费率(为空时默认0) BigDecimal depositRate = order.getDepositShopFeeRata() != null ? order.getDepositShopFeeRata() : BigDecimal.ZERO; BigDecimal takeRate = order.getTakeShopFeeRata() != null ? order.getTakeShopFeeRata() : BigDecimal.ZERO; BigDecimal driverRate = order.getDriverFeeRata() != null ? order.getDriverFeeRata() : BigDecimal.ZERO; Long exceptionFeeVal = order.getExceptionFee() != null ? order.getExceptionFee() : 0L; //存件门店收益 Long depositShopFee = new BigDecimal(totalAmount) .multiply(depositRate) .setScale(0, RoundingMode.HALF_UP) .longValue(); Long takeShopFee = 0L; Long driverFee = 0L; if (Constants.equalsInteger(order.getType(), Constants.TWO)) { // 异地寄存:存件门店 + 司机 driverFee = new BigDecimal(totalAmount) .multiply(driverRate) .setScale(0, RoundingMode.HALF_UP) .longValue() + exceptionFeeVal; // 异地寄存且有取件门店:加上取件门店收益 if (order.getTakeShopId() != null) { takeShopFee = new BigDecimal(totalAmount) .multiply(takeRate) .setScale(0, RoundingMode.HALF_UP) .longValue(); } } ordersMapper.update(new UpdateWrapper().lambda() .eq(Orders::getId, orderId) .set(Orders::getDepositShopFee, depositShopFee) .set(Orders::getTakeShopFee, takeShopFee) .set(Orders::getDriverFee, driverFee) .set(Orders::getUpdateTime, new Date())); } /** * 生成门店/司机收益记录(未结算) * 订单完成时调用,读取订单上已计算好的费用字段 */ private void generateRevenueRecords(Integer orderId) { Orders order = ordersMapper.selectById(orderId); if (order == null) { return; } Date now = new Date(); Long depositShopFee = order.getDepositShopFee() != null ? order.getDepositShopFee() : 0L; Long takeShopFee = order.getTakeShopFee() != null ? order.getTakeShopFee() : 0L; Long driverFee = order.getDriverFee() != null ? order.getDriverFee() : 0L; // 存件门店收益 if (depositShopFee > 0 && order.getDepositShopId() != null) { ShopInfo depositShop = shopInfoMapper.selectById(order.getDepositShopId()); if (depositShop != null && depositShop.getRegionMemberId() != null) { revenueMapper.insert(buildRevenue(depositShop.getRegionMemberId(), Constants.TWO, depositShopFee, orderId, order.getCode())); } } // 取件门店收益(异地寄存且有取件门店) if (takeShopFee > 0 && order.getTakeShopId() != null) { ShopInfo takeShop = shopInfoMapper.selectById(order.getTakeShopId()); if (takeShop != null && takeShop.getRegionMemberId() != null) { revenueMapper.insert(buildRevenue(takeShop.getRegionMemberId(), Constants.TWO, takeShopFee, orderId, order.getCode())); } } // 司机收益(异地寄存) if (driverFee > 0 && order.getAcceptDriver() != null) { DriverInfo driver = driverInfoMapper.selectById(order.getAcceptDriver()); if (driver != null && driver.getMemberId() != null) { revenueMapper.insert(buildRevenue(driver.getMemberId(), Constants.ONE, driverFee, orderId, order.getCode())); } } } /** * 构建收益记录 */ private Revenue buildRevenue(Integer memberId, Integer memberType, Long amount, Integer orderId, String orderNo) { Revenue revenue = new Revenue(); revenue.setMemberId(memberId); revenue.setMemberType(memberType); // 1=司机, 2=门店 revenue.setType(Constants.ZERO); // 0=完成订单 revenue.setOptType(Constants.ONE); // 1=收入 revenue.setAmount(amount); revenue.setVaildStatus(Constants.ZERO); // 0=入账中(未结算) revenue.setObjId(orderId); revenue.setObjType(Constants.ZERO); // 0=订单业务 revenue.setStatus(Constants.ZERO); // 0=成功 revenue.setOrderNo(orderNo); revenue.setDeleted(Constants.ZERO); revenue.setCreateTime(new Date()); return revenue; } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void driverVerifyOrder(String verifyCode, List images, String remark, Integer driverId) { if (StringUtils.isBlank(verifyCode)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "核销码不能为空"); } // 根据司机核销码查找订单 Orders order = ordersMapper.selectOne(new QueryWrapper().lambda() .eq(Orders::getDriverVerifyCode, verifyCode) .eq(Orders::getDeleted, Constants.ZERO) .last("limit 1")); if (order == null) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "核销码无效"); } // 仅异地寄存 + 有取件门店 + 派送中(4) 可核销 if (!Constants.equalsInteger(order.getType(), Constants.ONE)) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅异地寄存订单支持司机核销"); } if (order.getTakeShopId() == null) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单无取件门店,无需司机核销"); } if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.delivering.getStatus())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许核销"); } // 派送中(4) → 已到店(5) order.setStatus(Constants.OrderStatus.arrived.getStatus()); order.setArriveTime(new Date()); if (StringUtils.isNotBlank(remark)) { order.setRemark(remark); } ordersMapper.updateById(order); // 释放司机核销码 releaseVerifyCode(verifyCode); // 保存附件(obj_type=3 门店入库图片,最多3张) saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_TAKE.getKey(), driverId); } /** * 保存核销附件到 multifile 表 * * @param orderId 订单主键 * @param images 图片地址列表(最多3张) * @param objType 附件类型 * @param creator 创建人编码 */ private void saveVerifyImages(Integer orderId, List images, int objType, Integer creator) { if (images == null || images.isEmpty()) return; List saveImages = images.size() > 3 ? images.subList(0, 3) : images; Date now = new Date(); int sortNum = 1; for (String imgUrl : saveImages) { if (StringUtils.isBlank(imgUrl)) continue; Multifile multifile = new Multifile(); multifile.setObjId(orderId); multifile.setObjType(objType); multifile.setFileurl(imgUrl); multifile.setType(Constants.ZERO); multifile.setCreator(creator); multifile.setCreateDate(now); multifile.setIsdeleted(Constants.ZERO); multifile.setSortnum(sortNum++); multifileMapper.insert(multifile); } } @Override @Transactional(rollbackFor = {Exception.class, BusinessException.class}) public void confirmCustomerArrived(Integer orderId, Integer shopId) { // 1. 查询订单 Orders order = ordersMapper.selectById(orderId); if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) { throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在"); } // 2. 校验订单状态:待取件(5) if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.arrived.getStatus())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许该操作"); } // 3. 校验门店与订单关系 if (Constants.equalsInteger(order.getType(), Constants.ONE) && order.getTakeShopId() != null) { // 异地寄存有取件门店:校验取件门店 if (!shopId.equals(order.getTakeShopId())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店"); } } else if (Constants.equalsInteger(order.getType(), Constants.ZERO)) { // 就地寄存:校验存件门店(取件门店同存件门店) if (!shopId.equals(order.getDepositShopId())) { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店"); } } else { throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单无取件门店,无法确认到店"); } // 4. 查询门店名称(用于日志) String shopName = ""; ShopInfo shopInfo = shopInfoMapper.selectById(shopId); if (shopInfo != null) { shopName = shopInfo.getName() != null ? shopInfo.getName() : ""; } // 5. 计算逾期费用 List details = ordersDetailMapper.selectList( new QueryWrapper().lambda() .eq(OrdersDetail::getOrderId, orderId) .eq(OrdersDetail::getDeleted, Constants.ZERO)); OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(order, details); Date now = new Date(); if (overdueInfo.getOverdue() && overdueInfo.getOverdueDays() > 0) { // 存在逾期:标记逾期状态,订单保持当前状态 order.setConfirmArriveTime(now); order.setOverdueStatus(Constants.ONE); order.setOverdueDays(overdueInfo.getOverdueDays()); order.setOverdueAmount(overdueInfo.getOverdueFee()); order.setUpdateTime(now); ordersMapper.updateById(order); // 记录订单日志 saveShopVerifyLog(order, "确认顾客到店(逾期)", "门店【" + shopName + "】确认顾客到店,逾期" + overdueInfo.getOverdueDays() + "天,逾期费用" + Constants.getFormatMoney(overdueInfo.getOverdueFee()) + "元", null, shopId); } else { // 未逾期:标记逾期状态为0,订单保持当前状态 order.setConfirmArriveTime(now); order.setOverdueStatus(Constants.ZERO); // 就地寄存:计算是否需要退款 if (Constants.equalsInteger(order.getType(), Constants.ZERO) && !CollectionUtils.isEmpty(details)) { int actualDays = calcActualDepositDays(now, order.getDepositTime()); order.setDepositDays(actualDays); int estimatedDays = order.getEstimatedDepositDays() != null ? order.getEstimatedDepositDays() : 1; int refundDays = estimatedDays - actualDays; if (refundDays > 0) { // 退款金额 = 退款天数 × Σ(物品单价 × 数量) long dailyBaseFee = 0L; for (OrdersDetail d : details) { dailyBaseFee += (d.getUnitPrice() != null ? d.getUnitPrice() : 0L) * (d.getNum() != null ? d.getNum() : 0); } long refundAmount = (long) refundDays * dailyBaseFee; order.setRefundAmount(refundAmount); } } order.setUpdateTime(now); ordersMapper.updateById(order); // 退款导致总金额变化,重算三方收益 if (order.getRefundAmount() != null && order.getRefundAmount() > 0) { long newTotal = (order.getTotalAmount() != null ? order.getTotalAmount() : 0L) - order.getRefundAmount(); order.setTotalAmount(newTotal); ordersMapper.update(new UpdateWrapper().lambda() .eq(Orders::getId, orderId) .set(Orders::getTotalAmount, newTotal)); calculateAndSaveOrderFees(orderId); } // 记录订单日志 String logInfo = "门店【" + shopName + "】确认顾客到店,未逾期"; if (order.getRefundAmount() != null && order.getRefundAmount() > 0) { logInfo += ",需退款" + Constants.getFormatMoney(order.getRefundAmount()) + "元"; } saveShopVerifyLog(order, "确认顾客到店", logInfo, null, shopId); } } /** * 构建订单状态描述 */ private String buildStatusDesc(Orders order) { boolean isLocal = Constants.equalsInteger(order.getType(), Constants.ZERO); Integer status = order.getStatus(); if (status == null) return ""; if (Constants.equalsInteger(status, Constants.OrderStatus.waitPay.getStatus())) { String minutes = ""; try { minutes = operationConfigBiz.getConfig().getAutoCancelTime(); } catch (Exception ignored) {} return "请在" + (StringUtils.isNotBlank(minutes) ? minutes : "") + "分钟内完成支付,超时订单将自动取消"; } if (Constants.equalsInteger(status, Constants.OrderStatus.waitDeposit.getStatus())) { return isLocal ? "订单已支付,请等待门店确认接单" : "订单已支付,请等待门店确认接单"; } if (Constants.equalsInteger(status, Constants.OrderStatus.deposited.getStatus())) { return isLocal ? "行李已寄存,请凭取件码前往指定门店取件" : "门店已接单,正在为您安排取件司机"; } if (Constants.equalsInteger(status, Constants.OrderStatus.accepted.getStatus())) { return isLocal ? "行李已寄存,请凭取件码前往指定门店取件" : "已有司机抢单,正前往取件地点"; } if (Constants.equalsInteger(status, Constants.OrderStatus.delivering.getStatus())) { return "司机已取件,正运往目的地"; } if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) { return "行李已送达服务点,请及时前往取件"; } if (Constants.equalsInteger(status, Constants.OrderStatus.finished.getStatus())) { if (Constants.equalsInteger(order.getCommentStatus(), Constants.ONE)) { return "感谢您的用心评价,祝您出行顺利,旅途愉快!"; } return "订单已完成,感谢您的支持,请对本次服务做出评价"; } if (Constants.equalsInteger(status, Constants.OrderStatus.cancelled.getStatus())) { return "订单已取消,感谢您的支持,欢迎下次再会!"; } if (Constants.equalsInteger(status, Constants.OrderStatus.cancelling.getStatus())) { return "退款申请已提交,平台会尽快为您处理退款"; } if (Constants.equalsInteger(status, Constants.OrderStatus.closed.getStatus())) { return "退款已成功原路返回,请注意查收"; } return ""; } /** * 计算支付倒计时毫秒 */ private Long calcPayCountdownMs(Orders order) { try { String minutesStr = operationConfigBiz.getConfig().getAutoCancelTime(); if (StringUtils.isBlank(minutesStr)) return -1L; int minutes = Integer.parseInt(minutesStr); long deadline = order.getCreateTime().getTime() + minutes * 60 * 1000L; long remaining = deadline - System.currentTimeMillis(); return remaining > 0 ? remaining : -1L; } catch (Exception e) { return -1L; } } public OverdueFeeVO calculateOverdueFee(Integer orderId) { Orders order = ordersMapper.selectById(orderId); if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) { throw new BusinessException(ResponseStatus.DATA_EMPTY); } // 查询订单明细 List details = ordersDetailMapper.selectList( new QueryWrapper().lambda() .eq(OrdersDetail::getOrderId, orderId) .eq(OrdersDetail::getDeleted, Constants.ZERO)); return calculateOverdueFeeInternal(order, details); } /** * 逾期费用内部计算(不查库,接受预查询的数据) * 供分页等已查询明细的业务场景复用,避免重复查询 */ private OverdueFeeVO calculateOverdueFeeInternal(Orders order, List details) { if (CollectionUtils.isEmpty(details)) { OverdueFeeVO vo = new OverdueFeeVO(); vo.setOverdue(false); vo.setOverdueDays(0); vo.setOverdueFee(0L); vo.setDailyBaseFee(0L); return vo; } // 物品基础日费用 = Σ(单价 × 数量) long dailyBaseFee = 0L; for (OrdersDetail d : details) { dailyBaseFee += (d.getUnitPrice() != null ? d.getUnitPrice() : 0L) * (d.getNum() != null ? d.getNum() : 0); } Date now = new Date(); int overdueDays; long overdueFee; if (Constants.equalsInteger(order.getType(), Constants.ZERO)) { // ========== 就地寄存 ========== overdueDays = calcLocalOverdueDays(now, order.getExpectedTakeTime()); overdueFee = (long) overdueDays * dailyBaseFee; OverdueFeeVO vo = new OverdueFeeVO(); vo.setOverdue(overdueDays > 0); vo.setOverdueDays(overdueDays); vo.setOverdueFee(overdueFee); vo.setDailyBaseFee(dailyBaseFee); return vo; } else { // ========== 异地寄存 ========== // 条件:存在取件门店 且 订单处于已到店状态(5) if (order.getTakeShopId() == null || !Constants.equalsInteger(order.getStatus(), Constants.FIVE)) { OverdueFeeVO vo = new OverdueFeeVO(); vo.setOverdue(false); vo.setOverdueDays(0); vo.setOverdueFee(0L); vo.setDailyBaseFee(dailyBaseFee); vo.setDiscountRate(null); return vo; } overdueDays = calcRemoteOverdueDays(now, order.getArriveTime()); // 折扣比率 String discountStr = operationConfigBiz.getConfig().getUnpickedDiscount(); BigDecimal discountRate = StringUtils.isNotBlank(discountStr) ? new BigDecimal(discountStr) : BigDecimal.ONE; overdueFee = new BigDecimal(overdueDays) .multiply(new BigDecimal(dailyBaseFee)) .multiply(discountRate) .setScale(0, RoundingMode.HALF_UP) .longValue(); OverdueFeeVO vo = new OverdueFeeVO(); vo.setOverdue(overdueDays > 0); vo.setOverdueDays(overdueDays); vo.setOverdueFee(overdueFee); vo.setDailyBaseFee(dailyBaseFee); vo.setDiscountRate(discountStr); return vo; } } /** * 计算实际寄存天数(depositTime 到 now 的天数差,最少1天) */ private int calcActualDepositDays(Date now, Date depositTime) { if (depositTime == null || now == null) { return 1; } Calendar depositCal = Calendar.getInstance(); depositCal.setTime(depositTime); depositCal.set(Calendar.HOUR_OF_DAY, 0); depositCal.set(Calendar.MINUTE, 0); depositCal.set(Calendar.SECOND, 0); depositCal.set(Calendar.MILLISECOND, 0); Calendar nowCal = Calendar.getInstance(); nowCal.setTime(now); nowCal.set(Calendar.HOUR_OF_DAY, 0); nowCal.set(Calendar.MINUTE, 0); nowCal.set(Calendar.SECOND, 0); nowCal.set(Calendar.MILLISECOND, 0); long diffMs = nowCal.getTimeInMillis() - depositCal.getTimeInMillis(); int days = (int) (diffMs / (1000 * 60 * 60 * 24)); return Math.max(days, 1); } /** * 就地寄存逾期天数计算 * 过了预计取件时间当天的12点后才算一天 */ private int calcLocalOverdueDays(Date now, Date expectedTakeTime) { if (expectedTakeTime == null || !now.after(expectedTakeTime)) { return 0; } // 基准时间 = 预计取件日期的12:00 Calendar baseCal = Calendar.getInstance(); baseCal.setTime(expectedTakeTime); baseCal.set(Calendar.HOUR_OF_DAY, 12); baseCal.set(Calendar.MINUTE, 0); baseCal.set(Calendar.SECOND, 0); baseCal.set(Calendar.MILLISECOND, 0); Date baseTime = baseCal.getTime(); if (!now.after(baseTime)) { return 0; } // 逾期天数 = 当前日期 - 基准日期(按天取差) Calendar nowCal = Calendar.getInstance(); nowCal.setTime(now); nowCal.set(Calendar.HOUR_OF_DAY, 0); nowCal.set(Calendar.MINUTE, 0); nowCal.set(Calendar.SECOND, 0); nowCal.set(Calendar.MILLISECOND, 0); Calendar baseDateCal = Calendar.getInstance(); baseDateCal.setTime(baseTime); baseDateCal.set(Calendar.HOUR_OF_DAY, 0); baseDateCal.set(Calendar.MINUTE, 0); baseDateCal.set(Calendar.SECOND, 0); baseDateCal.set(Calendar.MILLISECOND, 0); long diffMs = nowCal.getTimeInMillis() - baseDateCal.getTimeInMillis(); int days = (int) (diffMs / (1000 * 60 * 60 * 24)); return Math.max(days, 0); } /** * 异地寄存逾期天数计算 * 过了转移到店时间当天的晚上12点(24:00)后才算第一天 */ private int calcRemoteOverdueDays(Date now, Date arriveTime) { if (arriveTime == null || !now.after(arriveTime)) { return 0; } // 基准时间 = 转移到店日期的次日 00:00(即当天24:00) Calendar baseCal = Calendar.getInstance(); baseCal.setTime(arriveTime); baseCal.set(Calendar.HOUR_OF_DAY, 0); baseCal.set(Calendar.MINUTE, 0); baseCal.set(Calendar.SECOND, 0); baseCal.set(Calendar.MILLISECOND, 0); baseCal.add(Calendar.DAY_OF_MONTH, 1); // 次日00:00 = 当天24:00 Date baseTime = baseCal.getTime(); if (!now.after(baseTime)) { return 0; } // 逾期天数 = 当前日期 - 基准日期 Calendar nowCal = Calendar.getInstance(); nowCal.setTime(now); nowCal.set(Calendar.HOUR_OF_DAY, 0); nowCal.set(Calendar.MINUTE, 0); nowCal.set(Calendar.SECOND, 0); nowCal.set(Calendar.MILLISECOND, 0); Calendar baseDateCal = Calendar.getInstance(); baseDateCal.setTime(baseTime); baseDateCal.set(Calendar.HOUR_OF_DAY, 0); baseDateCal.set(Calendar.MINUTE, 0); baseDateCal.set(Calendar.SECOND, 0); baseDateCal.set(Calendar.MILLISECOND, 0); long diffMs = nowCal.getTimeInMillis() - baseDateCal.getTimeInMillis(); int days = (int) (diffMs / (1000 * 60 * 60 * 24)); return Math.max(days, 0); } }