|
@@ -43,7 +43,6 @@ import com.ylx.order.domain.vo.merchant.OrderPageVO;
|
|
|
import com.ylx.order.enums.*;
|
|
import com.ylx.order.enums.*;
|
|
|
import com.ylx.order.mapper.TOrderMapper;
|
|
import com.ylx.order.mapper.TOrderMapper;
|
|
|
import com.ylx.order.service.IAfterSalesServiceService;
|
|
import com.ylx.order.service.IAfterSalesServiceService;
|
|
|
-import com.ylx.order.service.IGeoRiskInfo;
|
|
|
|
|
import com.ylx.order.service.OrderStatusFlowService;
|
|
import com.ylx.order.service.OrderStatusFlowService;
|
|
|
import com.ylx.order.service.TOrderService;
|
|
import com.ylx.order.service.TOrderService;
|
|
|
import com.ylx.project.domain.Project;
|
|
import com.ylx.project.domain.Project;
|
|
@@ -333,154 +332,6 @@ public class TOrderServiceImpl extends BaseServiceImpl<TOrderMapper, TOrder> imp
|
|
|
log.info("订单状态变更: orderId={}, oldStatus={}, newStatus={}", orderId, oldStatus, newStatus);
|
|
log.info("订单状态变更: orderId={}, oldStatus={}, newStatus={}", orderId, oldStatus, newStatus);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 将 TOrder 转换为 OrderDateQueryVo,并处理 serviceTime 字段
|
|
|
|
|
- */
|
|
|
|
|
- private OrderDateQueryVo convertToVo(TOrder order) {
|
|
|
|
|
- OrderDateQueryVo vo = new OrderDateQueryVo();
|
|
|
|
|
- // 拷贝相同字段
|
|
|
|
|
- BeanUtil.copyProperties(order, vo);
|
|
|
|
|
- // 手动设置字段名不一致或需要特殊处理的
|
|
|
|
|
- vo.setOrderStatus(order.getStatus());
|
|
|
|
|
- vo.setOrderStatusName(getOrderStatusName(order.getStatus()));
|
|
|
|
|
- // 服务时间范围:预约开始时间 + 项目时长(分钟)
|
|
|
|
|
- // 使用 startTime 和 completedTime 构建服务时间展示
|
|
|
|
|
- String serviceTime = buildServiceTime(order.getStartTime(), order.getCompletedTime());
|
|
|
|
|
- vo.setServiceTime(serviceTime);
|
|
|
|
|
-
|
|
|
|
|
- // 获取订单售后数据
|
|
|
|
|
- this.afterSalesServiceService.fillCurrentAfterSaleInfo(vo, order.getId());
|
|
|
|
|
-
|
|
|
|
|
- return vo;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 订单状态码 -> 中文描述
|
|
|
|
|
- */
|
|
|
|
|
- private String getOrderStatusName(Integer status) {
|
|
|
|
|
- return OrderStatusEnum.getInfoByCode(status);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 拼接服务时间字符串,例如 “2026-06-08 14:30 (90分钟)”
|
|
|
|
|
- */
|
|
|
|
|
- private String buildServiceTime(LocalDateTime startTime, LocalDateTime completedTime) {
|
|
|
|
|
- if (startTime == null || completedTime == null) {
|
|
|
|
|
- return "";
|
|
|
|
|
- }
|
|
|
|
|
- String date = startTime.format(dateFormatter);
|
|
|
|
|
- String start = startTime.format(timeFormatter);
|
|
|
|
|
- String end = completedTime.format(timeFormatter);
|
|
|
|
|
- return date + " " + start + "-" + end;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 组装订单对象
|
|
|
|
|
- */
|
|
|
|
|
- private TOrder buildOrder(OrderSubmitDTO dto, Project project, MaTechnician maTechnician, TAddress address, List<TAddress> merchantAddressList, BigDecimal basePrice, BigDecimal trafficFee, BigDecimal couponDiscount, BigDecimal finalAmount, Long userId) {
|
|
|
|
|
- TOrder order = new TOrder();
|
|
|
|
|
- order.setOrderNo(orderNumberGenerator.generateNextOrderNumber(""));
|
|
|
|
|
- order.setUserId(userId);
|
|
|
|
|
- order.setProjectId(dto.getProjectId());
|
|
|
|
|
- order.setProjectType(project.getType());
|
|
|
|
|
- order.setProjectName(project.getTitle());
|
|
|
|
|
- order.setProjectCover(project.getCover());
|
|
|
|
|
- order.setHighlight(project.getHighlight());
|
|
|
|
|
- order.setAppointmentStartTime(dto.getAppointmentStartTime());
|
|
|
|
|
-
|
|
|
|
|
- // 根据项目的unitType字段将小时转换为分钟
|
|
|
|
|
- Integer projectDuration = this.convertToMinutes(project.getStandardDuration(), project.getUnitType());
|
|
|
|
|
- order.setProjectDuration(projectDuration);
|
|
|
|
|
- order.setMerchantId(dto.getMerchantId());
|
|
|
|
|
- order.setMerchantType(maTechnician.getTechType());
|
|
|
|
|
- order.setMerchantNickName(maTechnician.getTeNickName());
|
|
|
|
|
- order.setMerchantAvatar(maTechnician.getTeAvatar());
|
|
|
|
|
- order.setContactPersonName(address.getUserName());
|
|
|
|
|
- order.setContactPhoneNumber(address.getPhone());
|
|
|
|
|
- order.setContactAddressInfo(address.getDetailAddress());
|
|
|
|
|
- order.setCityCode(address.getCityCode());
|
|
|
|
|
- order.setBasePrice(basePrice);
|
|
|
|
|
- order.setCouponDiscount(couponDiscount);
|
|
|
|
|
- order.setTrafficFee(trafficFee);
|
|
|
|
|
- order.setFinalAmount(finalAmount);
|
|
|
|
|
- order.setCouponId(dto.getCouponId());
|
|
|
|
|
- order.setCreateTime(DateUtils.getNowDate());
|
|
|
|
|
- order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
|
|
|
|
|
- order.setExecStatus(ExecStatusEnum.WAIT_DEPART.getCode());
|
|
|
|
|
- order.setDispatchedStatus(DispatchedStatusEnum.UN_DISPATCHED.getCode());
|
|
|
|
|
- order.setPaymentMethod(dto.getPaymentMethod());
|
|
|
|
|
-
|
|
|
|
|
- // 经纬度安全赋值
|
|
|
|
|
- order.setUserLatitude(address.getLatitude());
|
|
|
|
|
- order.setUserLongitude(address.getLongitude());
|
|
|
|
|
-
|
|
|
|
|
- merchantAddressList.stream().filter(a -> a.getType().equals(ADDR_TYPE_REAL)).findFirst().ifPresent(addr -> {
|
|
|
|
|
- order.setMerchantLongitude(addr.getLongitude());
|
|
|
|
|
- order.setMerchantLatitude(addr.getLatitude());
|
|
|
|
|
- });
|
|
|
|
|
- merchantAddressList.stream().filter(a -> a.getType().equals(ADDR_TYPE_VIRTUAL)).findFirst().ifPresent(addr -> {
|
|
|
|
|
- order.setVirtualLongitude(addr.getLongitude());
|
|
|
|
|
- order.setVirtualLatitude(addr.getLatitude());
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // 设置支付状态
|
|
|
|
|
- if (ObjectUtil.equals(PaymentMethodEnum.BALANCE.getCode(), dto.getPaymentMethod())) {
|
|
|
|
|
- order.setStatus(OrderStatusEnum.PENDING_DISPATCH.getCode());
|
|
|
|
|
- order.setPaidTime(LocalDateTime.now());
|
|
|
|
|
- } else {
|
|
|
|
|
- order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
|
|
|
|
|
- }
|
|
|
|
|
- return order;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 处理余额支付 (核心优化:数据库原子操作防并发)
|
|
|
|
|
- */
|
|
|
|
|
- private void handleBalancePayment(Long userId, BigDecimal finalAmount, TOrder order) {
|
|
|
|
|
- // 1. 数据库层面原子扣减余额,并校验余额是否充足
|
|
|
|
|
- boolean deductSuccess = wxUserService.lambdaUpdate()
|
|
|
|
|
- .setSql("d_balance = d_balance - {0}")
|
|
|
|
|
- .eq(TWxUser::getId, userId)
|
|
|
|
|
- // 两个位置共用同一个参数,逗号传参
|
|
|
|
|
- .apply("d_balance >= {0}", finalAmount)
|
|
|
|
|
- .update();
|
|
|
|
|
-
|
|
|
|
|
- if (!deductSuccess) {
|
|
|
|
|
- throw new ServiceException("余额不足,扣款失败");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 2. 重新查询最新余额用于记录流水
|
|
|
|
|
- TWxUser user = this.wxUserService.getById(userId);
|
|
|
|
|
-
|
|
|
|
|
- // 3. 记录购物金明细
|
|
|
|
|
- ShoppingFundsDetailAddDto detailDto = new ShoppingFundsDetailAddDto();
|
|
|
|
|
- detailDto.setUserId(userId.toString());
|
|
|
|
|
- detailDto.setAmount(finalAmount);
|
|
|
|
|
- detailDto.setOrderNo(order.getOrderNo());
|
|
|
|
|
- detailDto.setExpenseType(ShoppingFundsExpenseTypeEnum.CONSUMPTION.getCode());
|
|
|
|
|
- detailDto.setBalance(user.getdBalance());
|
|
|
|
|
- this.shoppingFundsDetailService.addShoppingFundsDetail(detailDto);
|
|
|
|
|
-
|
|
|
|
|
- // 4. 记录状态流转流水
|
|
|
|
|
- OrderUpdateStatusDTO dto = new OrderUpdateStatusDTO();
|
|
|
|
|
- dto.setOrderId(order.getId());
|
|
|
|
|
- dto.setStatus(order.getStatus());
|
|
|
|
|
- this.updateOrderStatus(dto);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 创建微信支付订单(事务外执行,减少事务时长)
|
|
|
|
|
- */
|
|
|
|
|
- private Map<String, Object> createWxPayOrder(TOrder order, WxLoginUser wxLoginUser) {
|
|
|
|
|
- try {
|
|
|
|
|
- return wxPayV3Service.createV3JsapiOrder(order.getOrderNo(), order.getFinalAmount(), "购买情绪价值商品", wxLoginUser.getCOpenid(), WxPayTypeEnum.EMOTION_GOODS.getCode());
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.error("微信支付下单失败,订单号: {}", order.getOrderNo(), e);
|
|
|
|
|
- throw new ServiceException("支付服务异常,请稍后重试");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* 根据订单ID查询订单详情
|
|
* 根据订单ID查询订单详情
|
|
|
*/
|
|
*/
|
|
@@ -489,153 +340,12 @@ public class TOrderServiceImpl extends BaseServiceImpl<TOrderMapper, TOrder> imp
|
|
|
// 1. 根据ID查询订单主表
|
|
// 1. 根据ID查询订单主表
|
|
|
TOrder order = validateAndGetOrder(orderId, null);
|
|
TOrder order = validateAndGetOrder(orderId, null);
|
|
|
|
|
|
|
|
- // 2. 查询订单状态流转记录
|
|
|
|
|
- LambdaQueryWrapper<OrderStatusFlow> flowWrapper = Wrappers.<OrderStatusFlow>lambdaQuery().eq(OrderStatusFlow::getOrderId, order.getId()).orderByAsc(OrderStatusFlow::getCreateTime);
|
|
|
|
|
- List<OrderStatusFlow> flowList = orderStatusFlowService.getBaseMapper().selectList(flowWrapper);
|
|
|
|
|
-
|
|
|
|
|
- // 3. 组装VO
|
|
|
|
|
- return buildOrderDetailVO(order, flowList);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 将订单实体和流转记录转换为详情VO
|
|
|
|
|
- */
|
|
|
|
|
- private OrderDetailVO buildOrderDetailVO(TOrder order, List<OrderStatusFlow> flowList) {
|
|
|
|
|
- OrderDetailVO vo = new OrderDetailVO();
|
|
|
|
|
-
|
|
|
|
|
- // 基础订单信息
|
|
|
|
|
- vo.setOrderStatus(order.getStatus());
|
|
|
|
|
- vo.setOrderStatusName(OrderStatusEnum.getInfoByCode(order.getStatus()));
|
|
|
|
|
- vo.setMerchantAvatar(order.getMerchantAvatar());
|
|
|
|
|
- // 1. 获取服务次数(从项目表获取 projectUsersNum)
|
|
|
|
|
- Integer serviceNum = null;
|
|
|
|
|
- if (order.getProjectId() != null && order.getMerchantId() != null) {
|
|
|
|
|
- MaProject project = maProjectMapper.selectByProjectIdAndMerchantId(order.getProjectId(), order.getMerchantId());
|
|
|
|
|
- if (project != null && project.getProjectUsersNum() != null) {
|
|
|
|
|
- serviceNum = project.getProjectUsersNum().intValue();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- vo.setServiceNum(serviceNum);
|
|
|
|
|
-
|
|
|
|
|
- // 查询技师表
|
|
|
|
|
- if (order.getMerchantId() != null) {
|
|
|
|
|
- MaTechnician technician = maTechnicianMapper.selectById(order.getMerchantId());
|
|
|
|
|
- if (technician != null) {
|
|
|
|
|
- //评分
|
|
|
|
|
- vo.setStar(technician.getNStar());
|
|
|
|
|
- //简介
|
|
|
|
|
- vo.setBrief(technician.getTeBrief());
|
|
|
|
|
- } else {
|
|
|
|
|
- log.warn("技师信息不存在,merchantId: {}", order.getMerchantId());
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // 无 merchantId,字段置空(或可后续扩展)
|
|
|
|
|
- vo.setStar(null);
|
|
|
|
|
- vo.setBrief(null);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- vo.setProjectName(order.getProjectName());
|
|
|
|
|
- vo.setProjectDuration(order.getProjectDuration());
|
|
|
|
|
- vo.setHighlight(order.getHighlight());
|
|
|
|
|
- vo.setFinalAmount(order.getFinalAmount());
|
|
|
|
|
- vo.setBasePrice(order.getBasePrice());
|
|
|
|
|
- vo.setTrafficFee(order.getTrafficFee());
|
|
|
|
|
- vo.setCouponDiscount(order.getCouponDiscount());
|
|
|
|
|
- vo.setOrderNo(order.getOrderNo());
|
|
|
|
|
- vo.setOrderId(String.valueOf(order.getId()));
|
|
|
|
|
- vo.setMerchantName(order.getMerchantNickName());
|
|
|
|
|
- vo.setMerchantId(order.getMerchantId());
|
|
|
|
|
- // 时间处理
|
|
|
|
|
- if (order.getAppointmentStartTime() != null) {
|
|
|
|
|
- LocalDateTime start = order.getAppointmentStartTime();
|
|
|
|
|
- // 预约结束时间 = 开始时间 + 项目时长(分钟)
|
|
|
|
|
- LocalDateTime end = start.plusMinutes(order.getProjectDuration() == null ? 0 : order.getProjectDuration());
|
|
|
|
|
-
|
|
|
|
|
- // 原始格式
|
|
|
|
|
- vo.setAppointmentStartTime(start.format(DATE_TIME_FORMATTER));
|
|
|
|
|
- vo.setAppointmentEndTime(end.format(DATE_TIME_FORMATTER));
|
|
|
|
|
-
|
|
|
|
|
- // 生成“5月12日 16:00-18:00”格式
|
|
|
|
|
- String timeRange = formatAppointmentTimeRange(start, end);
|
|
|
|
|
- vo.setAppointmentTimeRange(timeRange);
|
|
|
|
|
- }
|
|
|
|
|
- if (order.getCreateTime() != null) {
|
|
|
|
|
- vo.setCreateTime(DATE_FORMAT.format(order.getCreateTime()));
|
|
|
|
|
- }
|
|
|
|
|
- if (order.getPaidTime() != null) {
|
|
|
|
|
- vo.setPaidTime(order.getPaidTime().format(DATE_TIME_FORMATTER));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- vo.setContactAddresInfo(order.getContactAddressInfo());
|
|
|
|
|
- // 联系信息组合:姓名 + 手机号(中间4位脱敏)
|
|
|
|
|
- String contactInfo = buildContactInfo(order.getContactPersonName(), order.getContactPhoneNumber());
|
|
|
|
|
- vo.setContactInfo(contactInfo);
|
|
|
|
|
-
|
|
|
|
|
- vo.setPaymentMethod(order.getPaymentMethod());
|
|
|
|
|
- vo.setPaymentMethodName(PaymentMethodEnum.getNameByCode(order.getPaymentMethod()));
|
|
|
|
|
-
|
|
|
|
|
- // 获取订单售后数据
|
|
|
|
|
- this.afterSalesServiceService.fillCurrentAfterSaleInfo(vo, order.getId());
|
|
|
|
|
-
|
|
|
|
|
- // 订单状态流转列表转换
|
|
|
|
|
- List<OrderStatusFlowVO> flowVOList = flowList.stream().map(this::convertToFlowVO).sorted(Comparator.comparing(OrderStatusFlowVO::getCreateTime)).collect(Collectors.toList());
|
|
|
|
|
- vo.setOrderStatusFlowList(flowVOList);
|
|
|
|
|
-
|
|
|
|
|
- return vo;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 格式化预约时间范围
|
|
|
|
|
- *
|
|
|
|
|
- * @param start 预约开始时间
|
|
|
|
|
- * @param end 预约结束时间
|
|
|
|
|
- * @return 示例:5月12日 16:00-18:00
|
|
|
|
|
- */
|
|
|
|
|
- private String formatAppointmentTimeRange(LocalDateTime start, LocalDateTime end) {
|
|
|
|
|
- if (start == null) {
|
|
|
|
|
- return "";
|
|
|
|
|
- }
|
|
|
|
|
- // 日期格式:M月d日(不带年份,同一年内)
|
|
|
|
|
- DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("M月d日");
|
|
|
|
|
- String dateStr = start.format(dateFormatter);
|
|
|
|
|
- // 时间格式:HH:mm
|
|
|
|
|
- DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
|
|
|
|
|
- String startTime = start.format(timeFormatter);
|
|
|
|
|
- String endTime = (end != null) ? end.format(timeFormatter) : "";
|
|
|
|
|
- return dateStr + " " + startTime + "-" + endTime;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 构建联系信息(姓名 + 脱敏手机号)
|
|
|
|
|
- * 示例:王先生,188****5555
|
|
|
|
|
- */
|
|
|
|
|
- private String buildContactInfo(String name, String phone) {
|
|
|
|
|
- if (!StringUtils.hasText(name) && !StringUtils.hasText(phone)) {
|
|
|
|
|
- return "";
|
|
|
|
|
- }
|
|
|
|
|
- String maskedPhone = "";
|
|
|
|
|
- if (StringUtils.hasText(phone) && phone.length() >= 11) {
|
|
|
|
|
- maskedPhone = phone.substring(0, 3) + "****" + phone.substring(7);
|
|
|
|
|
- } else if (StringUtils.hasText(phone)) {
|
|
|
|
|
- maskedPhone = phone; // 长度不足则直接显示
|
|
|
|
|
- }
|
|
|
|
|
- if (StringUtils.hasText(name)) {
|
|
|
|
|
- return name + "," + maskedPhone;
|
|
|
|
|
- } else {
|
|
|
|
|
- return maskedPhone;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 2. 查询订单状态流转记录
|
|
|
|
|
+ LambdaQueryWrapper<OrderStatusFlow> flowWrapper = Wrappers.<OrderStatusFlow>lambdaQuery().eq(OrderStatusFlow::getOrderId, order.getId()).orderByAsc(OrderStatusFlow::getCreateTime);
|
|
|
|
|
+ List<OrderStatusFlow> flowList = orderStatusFlowService.getBaseMapper().selectList(flowWrapper);
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 将 OrderStatusFlow 转换为 OrderStatusFlowVO
|
|
|
|
|
- */
|
|
|
|
|
- private OrderStatusFlowVO convertToFlowVO(OrderStatusFlow flow) {
|
|
|
|
|
- OrderStatusFlowVO vo = new OrderStatusFlowVO();
|
|
|
|
|
- vo.setStatusName(OrderStatusEnum.getInfoByCode(flow.getStatus()));
|
|
|
|
|
- if (flow.getCreateTime() != null) {
|
|
|
|
|
- vo.setCreateTime(DATE_FORMAT.format(flow.getCreateTime()));
|
|
|
|
|
- }
|
|
|
|
|
- return vo;
|
|
|
|
|
|
|
+ // 3. 组装VO
|
|
|
|
|
+ return buildOrderDetailVO(order, flowList);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -729,15 +439,15 @@ public class TOrderServiceImpl extends BaseServiceImpl<TOrderMapper, TOrder> imp
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 5. 查询当前城市有效地理围栏,提前过滤非法围栏
|
|
// 5. 查询当前城市有效地理围栏,提前过滤非法围栏
|
|
|
- List<TGeoFence> validFenceList = geoFenceService.selectValidFences(dto.getCityCode());
|
|
|
|
|
- List<TGeoFence> usableFence = filterUsableFence(validFenceList);
|
|
|
|
|
|
|
+ List<TGeoFence> validFenceList = this.geoFenceService.selectValidFences(dto.getCityCode());
|
|
|
|
|
+ List<TGeoFence> usableFence = this.geoFenceService.filterUsableFence(validFenceList);
|
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 批量遍历订单,计算风险区域+客户标签
|
|
// 6. 批量遍历订单,计算风险区域+客户标签
|
|
|
LocalDateTime now = DateUtils.getNowLocalDateTime();
|
|
LocalDateTime now = DateUtils.getNowLocalDateTime();
|
|
|
for (OrderPageVO vo : voRecords) {
|
|
for (OrderPageVO vo : voRecords) {
|
|
|
// 计算高风险围栏信息
|
|
// 计算高风险围栏信息
|
|
|
- calcOrderGeoFenceInfo(vo, usableFence);
|
|
|
|
|
|
|
+ this.geoFenceService.calcOrderGeoFenceInfo(vo, usableFence);
|
|
|
// 计算客户标签
|
|
// 计算客户标签
|
|
|
Integer tagCode = calcCustomerTagCode(vo, merchantId, now);
|
|
Integer tagCode = calcCustomerTagCode(vo, merchantId, now);
|
|
|
vo.setCustomerTagCode(tagCode);
|
|
vo.setCustomerTagCode(tagCode);
|
|
@@ -1046,131 +756,419 @@ public class TOrderServiceImpl extends BaseServiceImpl<TOrderMapper, TOrder> imp
|
|
|
@Override
|
|
@Override
|
|
|
public Page<RecentMerchantVO> getRecentMerchants(RecentOrderQueryDTO dto) {
|
|
public Page<RecentMerchantVO> getRecentMerchants(RecentOrderQueryDTO dto) {
|
|
|
|
|
|
|
|
- // 1. 获取当前登录用户
|
|
|
|
|
- Long userId = this.getCurrentUserId();
|
|
|
|
|
- dto.setUserId(userId);
|
|
|
|
|
|
|
+ // 1. 获取当前登录用户
|
|
|
|
|
+ Long userId = this.getCurrentUserId();
|
|
|
|
|
+ dto.setUserId(userId);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 构建分页对象
|
|
|
|
|
+ Page<RecentMerchantVO> page = new Page<>(dto.getCurrent(), dto.getSize());
|
|
|
|
|
+ page = this.baseMapper.getRecentMerchants(page, dto);
|
|
|
|
|
+
|
|
|
|
|
+ return page;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public MerchantOrderDetailVO getMerchantOrderDetail(MerchantOrderDetailDTO dto) {
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 根据ID查询订单主表
|
|
|
|
|
+ TOrder order = validateAndGetOrder(dto.getOrderId(), null);
|
|
|
|
|
+
|
|
|
|
|
+ MerchantOrderDetailVO vo = new MerchantOrderDetailVO<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 基础字段映射 (使用 BeanUtil 或手动 set)
|
|
|
|
|
+ BeanUtil.copyProperties(order, vo);
|
|
|
|
|
+ vo.setOrderStatus(order.getStatus());
|
|
|
|
|
+ vo.setOrderStatusName(OrderStatusEnum.getInfoByCode(order.getStatus()));
|
|
|
|
|
+
|
|
|
|
|
+ // 特殊处理 ID 转 String (前端 Long 精度丢失问题)
|
|
|
|
|
+ vo.setOrderId(String.valueOf(order.getId()));
|
|
|
|
|
+
|
|
|
|
|
+ // 格式化时间范围 (例如: 5月12日 16:00-18:00)
|
|
|
|
|
+ vo.setAppointmentTimeRange(formatTimeRange(order.getAppointmentStartTime(), order.getProjectDuration()));
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 联系信息脱敏处理
|
|
|
|
|
+ vo.setContactPhoneNumber(PhoneUtils.maskPhoneNumberStrict(order.getContactPhoneNumber()));
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 计算距离与告警提示 (依赖地理围栏表 t_geo_fence)
|
|
|
|
|
+ List<TGeoFence> validFenceList = geoFenceService.selectValidFences(order.getCityCode());
|
|
|
|
|
+ List<TGeoFence> usableFence = this.geoFenceService.filterUsableFence(validFenceList);
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 查询关联的售后信息 (如果有)
|
|
|
|
|
+ this.afterSalesServiceService.fillCurrentAfterSaleInfo(vo, order.getId());
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 计算高风险围栏信息
|
|
|
|
|
+ this.geoFenceService.calcOrderGeoFenceInfo(vo, usableFence);
|
|
|
|
|
+
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<BookingSlotsVO> bookingSlots(BookingSlotsDTO dto) {
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 获取项目信息(关键:服务时长)
|
|
|
|
|
+ Project project = this.projectService.getById(dto.getProjectId());
|
|
|
|
|
+ if (ObjectUtil.isNull(project)) {
|
|
|
|
|
+ throw new ServiceException("项目不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Integer standardDuration = project.getStandardDuration();
|
|
|
|
|
+ Integer unitType = project.getUnitType();
|
|
|
|
|
+ String unitTypeName = unitDictCache.getOrDefault(unitType, "未知单位");
|
|
|
|
|
+
|
|
|
|
|
+ Integer durationMinutes = convertToMinutes(standardDuration, unitType);
|
|
|
|
|
+ if (ObjectUtil.isNull(durationMinutes) || durationMinutes <= 0) {
|
|
|
|
|
+ throw new ServiceException("项目时长配置异常");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 生成所有候选开始时间(半点粒度,步进30分钟)
|
|
|
|
|
+ List<LocalTime> candidateStarts = TimeSlotUtil.getAvailableTimeSlots(dto.getAppointmentDate(), APPOINT_BUFFER_MINUTES);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 查询该商户当天的【有效占用】订单
|
|
|
|
|
+ LocalDateTime dayStart = dto.getAppointmentDate().atStartOfDay();
|
|
|
|
|
+ LocalDateTime dayEnd = dto.getAppointmentDate().atTime(LocalTime.MAX);
|
|
|
|
|
+ List<TOrder> validOrders = this.baseMapper.selectValidOrdersByMerchantAndDate(
|
|
|
|
|
+ dto.getMerchantId(), dto.getProjectId(), dayStart, dayEnd);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 遍历候选时段,构建 VO
|
|
|
|
|
+ List<BookingSlotsVO> voList = new ArrayList<>(candidateStarts.size());
|
|
|
|
|
+ for (LocalTime startTime : candidateStarts) {
|
|
|
|
|
+ LocalDateTime slotStart = LocalDateTime.of(dto.getAppointmentDate(), startTime);
|
|
|
|
|
+ LocalDateTime actualEnd = slotStart.plusMinutes(durationMinutes);
|
|
|
|
|
+
|
|
|
|
|
+ BookingSlotsVO vo = new BookingSlotsVO();
|
|
|
|
|
+ vo.setStartTime(startTime);
|
|
|
|
|
+ vo.setStandardDuration(standardDuration);
|
|
|
|
|
+ vo.setUnitTypeName(unitTypeName);
|
|
|
|
|
+ vo.setEndTime(actualEnd.toLocalTime());
|
|
|
|
|
+ vo.setIsDay(maProjectFareSettingService.isDayTimePeriod(slotStart));
|
|
|
|
|
+
|
|
|
|
|
+ boolean exceedsDay = !actualEnd.toLocalDate().isEqual(dto.getAppointmentDate());
|
|
|
|
|
+ vo.setAvailable(!exceedsDay && isAppointmentSlotAvailable(slotStart, durationMinutes, validOrders));
|
|
|
|
|
+ voList.add(vo);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return voList;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void startGoOut(MerchantOrderOperateDTO dto) {
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 获取当前登录商户
|
|
|
|
|
+ WxLoginUser loginUser = getCurrentWxLoginUser();
|
|
|
|
|
+ Long merchantId = Long.parseLong(loginUser.getId());
|
|
|
|
|
+ Long orderId = dto.getOrderId();
|
|
|
|
|
+
|
|
|
|
|
+ // 查询订单
|
|
|
|
|
+ TOrder order = validateAndGetOrder(orderId, merchantId);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取履约状态
|
|
|
|
|
+ Integer execStatus = order.getExecStatus();
|
|
|
|
|
+ if (!ExecStatusEnum.WAIT_DEPART.getCode().equals(execStatus)) {
|
|
|
|
|
+ throw new ServiceException("仅【待出发/待签到】订单可修改为已出发,当前状态:" + ExecStatusEnum.getInfoByCode(execStatus));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新出发信息、履约信息
|
|
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
|
|
+ LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class)
|
|
|
|
|
+ .set(TOrder::getDepartedTime, now)
|
|
|
|
|
+ .set(TOrder::getUpdateTime, now)
|
|
|
|
|
+ .set(TOrder::getUpdateBy, loginUser.getCNickName())
|
|
|
|
|
+ .set(TOrder::getExecStatus, ExecStatusEnum.DEPARTED.getCode())
|
|
|
|
|
+ .eq(TOrder::getId, orderId);
|
|
|
|
|
+ int count = baseMapper.update(null, update);
|
|
|
|
|
+ if (count <= 0) {
|
|
|
|
|
+ throw new ServiceException("订单履约状态已变更,请刷新重试");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将 TOrder 转换为 OrderDateQueryVo,并处理 serviceTime 字段
|
|
|
|
|
+ */
|
|
|
|
|
+ private OrderDateQueryVo convertToVo(TOrder order) {
|
|
|
|
|
+ OrderDateQueryVo vo = new OrderDateQueryVo();
|
|
|
|
|
+ // 拷贝相同字段
|
|
|
|
|
+ BeanUtil.copyProperties(order, vo);
|
|
|
|
|
+ // 手动设置字段名不一致或需要特殊处理的
|
|
|
|
|
+ vo.setOrderStatus(order.getStatus());
|
|
|
|
|
+ vo.setOrderStatusName(getOrderStatusName(order.getStatus()));
|
|
|
|
|
+ // 服务时间范围:预约开始时间 + 项目时长(分钟)
|
|
|
|
|
+ // 使用 startTime 和 completedTime 构建服务时间展示
|
|
|
|
|
+ String serviceTime = buildServiceTime(order.getStartTime(), order.getCompletedTime());
|
|
|
|
|
+ vo.setServiceTime(serviceTime);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取订单售后数据
|
|
|
|
|
+ this.afterSalesServiceService.fillCurrentAfterSaleInfo(vo, order.getId());
|
|
|
|
|
+
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 订单状态码 -> 中文描述
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getOrderStatusName(Integer status) {
|
|
|
|
|
+ return OrderStatusEnum.getInfoByCode(status);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 拼接服务时间字符串,例如 “2026-06-08 14:30 (90分钟)”
|
|
|
|
|
+ */
|
|
|
|
|
+ private String buildServiceTime(LocalDateTime startTime, LocalDateTime completedTime) {
|
|
|
|
|
+ if (startTime == null || completedTime == null) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+ String date = startTime.format(dateFormatter);
|
|
|
|
|
+ String start = startTime.format(timeFormatter);
|
|
|
|
|
+ String end = completedTime.format(timeFormatter);
|
|
|
|
|
+ return date + " " + start + "-" + end;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 组装订单对象
|
|
|
|
|
+ */
|
|
|
|
|
+ private TOrder buildOrder(OrderSubmitDTO dto, Project project, MaTechnician maTechnician, TAddress address, List<TAddress> merchantAddressList, BigDecimal basePrice, BigDecimal trafficFee, BigDecimal couponDiscount, BigDecimal finalAmount, Long userId) {
|
|
|
|
|
+ TOrder order = new TOrder();
|
|
|
|
|
+ order.setOrderNo(orderNumberGenerator.generateNextOrderNumber(""));
|
|
|
|
|
+ order.setUserId(userId);
|
|
|
|
|
+ order.setProjectId(dto.getProjectId());
|
|
|
|
|
+ order.setProjectType(project.getType());
|
|
|
|
|
+ order.setProjectName(project.getTitle());
|
|
|
|
|
+ order.setProjectCover(project.getCover());
|
|
|
|
|
+ order.setHighlight(project.getHighlight());
|
|
|
|
|
+ order.setAppointmentStartTime(dto.getAppointmentStartTime());
|
|
|
|
|
+
|
|
|
|
|
+ // 根据项目的unitType字段将小时转换为分钟
|
|
|
|
|
+ Integer projectDuration = this.convertToMinutes(project.getStandardDuration(), project.getUnitType());
|
|
|
|
|
+ order.setProjectDuration(projectDuration);
|
|
|
|
|
+ order.setMerchantId(dto.getMerchantId());
|
|
|
|
|
+ order.setMerchantType(maTechnician.getTechType());
|
|
|
|
|
+ order.setMerchantNickName(maTechnician.getTeNickName());
|
|
|
|
|
+ order.setMerchantAvatar(maTechnician.getTeAvatar());
|
|
|
|
|
+ order.setContactPersonName(address.getUserName());
|
|
|
|
|
+ order.setContactPhoneNumber(address.getPhone());
|
|
|
|
|
+ order.setContactAddressInfo(address.getDetailAddress());
|
|
|
|
|
+ order.setCityCode(address.getCityCode());
|
|
|
|
|
+ order.setBasePrice(basePrice);
|
|
|
|
|
+ order.setCouponDiscount(couponDiscount);
|
|
|
|
|
+ order.setTrafficFee(trafficFee);
|
|
|
|
|
+ order.setFinalAmount(finalAmount);
|
|
|
|
|
+ order.setCouponId(dto.getCouponId());
|
|
|
|
|
+ order.setCreateTime(DateUtils.getNowDate());
|
|
|
|
|
+ order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
|
|
|
|
|
+ order.setExecStatus(ExecStatusEnum.WAIT_DEPART.getCode());
|
|
|
|
|
+ order.setDispatchedStatus(DispatchedStatusEnum.UN_DISPATCHED.getCode());
|
|
|
|
|
+ order.setPaymentMethod(dto.getPaymentMethod());
|
|
|
|
|
+
|
|
|
|
|
+ // 经纬度安全赋值
|
|
|
|
|
+ order.setUserLatitude(address.getLatitude());
|
|
|
|
|
+ order.setUserLongitude(address.getLongitude());
|
|
|
|
|
+
|
|
|
|
|
+ merchantAddressList.stream().filter(a -> a.getType().equals(ADDR_TYPE_REAL)).findFirst().ifPresent(addr -> {
|
|
|
|
|
+ order.setMerchantLongitude(addr.getLongitude());
|
|
|
|
|
+ order.setMerchantLatitude(addr.getLatitude());
|
|
|
|
|
+ });
|
|
|
|
|
+ merchantAddressList.stream().filter(a -> a.getType().equals(ADDR_TYPE_VIRTUAL)).findFirst().ifPresent(addr -> {
|
|
|
|
|
+ order.setVirtualLongitude(addr.getLongitude());
|
|
|
|
|
+ order.setVirtualLatitude(addr.getLatitude());
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 设置支付状态
|
|
|
|
|
+ if (ObjectUtil.equals(PaymentMethodEnum.BALANCE.getCode(), dto.getPaymentMethod())) {
|
|
|
|
|
+ order.setStatus(OrderStatusEnum.PENDING_DISPATCH.getCode());
|
|
|
|
|
+ order.setPaidTime(LocalDateTime.now());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
|
|
|
|
|
+ }
|
|
|
|
|
+ return order;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理余额支付 (核心优化:数据库原子操作防并发)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void handleBalancePayment(Long userId, BigDecimal finalAmount, TOrder order) {
|
|
|
|
|
+ // 1. 数据库层面原子扣减余额,并校验余额是否充足
|
|
|
|
|
+ boolean deductSuccess = wxUserService.lambdaUpdate()
|
|
|
|
|
+ .setSql("d_balance = d_balance - {0}")
|
|
|
|
|
+ .eq(TWxUser::getId, userId)
|
|
|
|
|
+ // 两个位置共用同一个参数,逗号传参
|
|
|
|
|
+ .apply("d_balance >= {0}", finalAmount)
|
|
|
|
|
+ .update();
|
|
|
|
|
+
|
|
|
|
|
+ if (!deductSuccess) {
|
|
|
|
|
+ throw new ServiceException("余额不足,扣款失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 重新查询最新余额用于记录流水
|
|
|
|
|
+ TWxUser user = this.wxUserService.getById(userId);
|
|
|
|
|
|
|
|
- // 2. 构建分页对象
|
|
|
|
|
- Page<RecentMerchantVO> page = new Page<>(dto.getCurrent(), dto.getSize());
|
|
|
|
|
- page = this.baseMapper.getRecentMerchants(page, dto);
|
|
|
|
|
|
|
+ // 3. 记录购物金明细
|
|
|
|
|
+ ShoppingFundsDetailAddDto detailDto = new ShoppingFundsDetailAddDto();
|
|
|
|
|
+ detailDto.setUserId(userId.toString());
|
|
|
|
|
+ detailDto.setAmount(finalAmount);
|
|
|
|
|
+ detailDto.setOrderNo(order.getOrderNo());
|
|
|
|
|
+ detailDto.setExpenseType(ShoppingFundsExpenseTypeEnum.CONSUMPTION.getCode());
|
|
|
|
|
+ detailDto.setBalance(user.getdBalance());
|
|
|
|
|
+ this.shoppingFundsDetailService.addShoppingFundsDetail(detailDto);
|
|
|
|
|
|
|
|
- return page;
|
|
|
|
|
|
|
+ // 4. 记录状态流转流水
|
|
|
|
|
+ OrderUpdateStatusDTO dto = new OrderUpdateStatusDTO();
|
|
|
|
|
+ dto.setOrderId(order.getId());
|
|
|
|
|
+ dto.setStatus(order.getStatus());
|
|
|
|
|
+ this.updateOrderStatus(dto);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @Override
|
|
|
|
|
- public MerchantOrderDetailVO getMerchantOrderDetail(MerchantOrderDetailDTO dto) {
|
|
|
|
|
-
|
|
|
|
|
- // 1. 根据ID查询订单主表
|
|
|
|
|
- TOrder order = validateAndGetOrder(dto.getOrderId(), null);
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建微信支付订单(事务外执行,减少事务时长)
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, Object> createWxPayOrder(TOrder order, WxLoginUser wxLoginUser) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return wxPayV3Service.createV3JsapiOrder(order.getOrderNo(), order.getFinalAmount(), "购买情绪价值商品", wxLoginUser.getCOpenid(), WxPayTypeEnum.EMOTION_GOODS.getCode());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("微信支付下单失败,订单号: {}", order.getOrderNo(), e);
|
|
|
|
|
+ throw new ServiceException("支付服务异常,请稍后重试");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- MerchantOrderDetailVO vo = new MerchantOrderDetailVO<>();
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将订单实体和流转记录转换为详情VO
|
|
|
|
|
+ */
|
|
|
|
|
+ private OrderDetailVO buildOrderDetailVO(TOrder order, List<OrderStatusFlow> flowList) {
|
|
|
|
|
+ OrderDetailVO vo = new OrderDetailVO();
|
|
|
|
|
|
|
|
- // 2. 基础字段映射 (使用 BeanUtil 或手动 set)
|
|
|
|
|
- BeanUtil.copyProperties(order, vo);
|
|
|
|
|
|
|
+ // 基础订单信息
|
|
|
vo.setOrderStatus(order.getStatus());
|
|
vo.setOrderStatus(order.getStatus());
|
|
|
vo.setOrderStatusName(OrderStatusEnum.getInfoByCode(order.getStatus()));
|
|
vo.setOrderStatusName(OrderStatusEnum.getInfoByCode(order.getStatus()));
|
|
|
|
|
+ vo.setMerchantAvatar(order.getMerchantAvatar());
|
|
|
|
|
+ // 1. 获取服务次数(从项目表获取 projectUsersNum)
|
|
|
|
|
+ Integer serviceNum = null;
|
|
|
|
|
+ if (order.getProjectId() != null && order.getMerchantId() != null) {
|
|
|
|
|
+ MaProject project = maProjectMapper.selectByProjectIdAndMerchantId(order.getProjectId(), order.getMerchantId());
|
|
|
|
|
+ if (project != null && project.getProjectUsersNum() != null) {
|
|
|
|
|
+ serviceNum = project.getProjectUsersNum().intValue();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setServiceNum(serviceNum);
|
|
|
|
|
|
|
|
- // 特殊处理 ID 转 String (前端 Long 精度丢失问题)
|
|
|
|
|
|
|
+ // 查询技师表
|
|
|
|
|
+ if (order.getMerchantId() != null) {
|
|
|
|
|
+ MaTechnician technician = maTechnicianMapper.selectById(order.getMerchantId());
|
|
|
|
|
+ if (technician != null) {
|
|
|
|
|
+ //评分
|
|
|
|
|
+ vo.setStar(technician.getNStar());
|
|
|
|
|
+ //简介
|
|
|
|
|
+ vo.setBrief(technician.getTeBrief());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.warn("技师信息不存在,merchantId: {}", order.getMerchantId());
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 无 merchantId,字段置空(或可后续扩展)
|
|
|
|
|
+ vo.setStar(null);
|
|
|
|
|
+ vo.setBrief(null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ vo.setProjectName(order.getProjectName());
|
|
|
|
|
+ vo.setProjectDuration(order.getProjectDuration());
|
|
|
|
|
+ vo.setHighlight(order.getHighlight());
|
|
|
|
|
+ vo.setFinalAmount(order.getFinalAmount());
|
|
|
|
|
+ vo.setBasePrice(order.getBasePrice());
|
|
|
|
|
+ vo.setTrafficFee(order.getTrafficFee());
|
|
|
|
|
+ vo.setCouponDiscount(order.getCouponDiscount());
|
|
|
|
|
+ vo.setOrderNo(order.getOrderNo());
|
|
|
vo.setOrderId(String.valueOf(order.getId()));
|
|
vo.setOrderId(String.valueOf(order.getId()));
|
|
|
|
|
+ vo.setMerchantName(order.getMerchantNickName());
|
|
|
|
|
+ vo.setMerchantId(order.getMerchantId());
|
|
|
|
|
+ // 时间处理
|
|
|
|
|
+ if (order.getAppointmentStartTime() != null) {
|
|
|
|
|
+ LocalDateTime start = order.getAppointmentStartTime();
|
|
|
|
|
+ // 预约结束时间 = 开始时间 + 项目时长(分钟)
|
|
|
|
|
+ LocalDateTime end = start.plusMinutes(order.getProjectDuration() == null ? 0 : order.getProjectDuration());
|
|
|
|
|
|
|
|
- // 格式化时间范围 (例如: 5月12日 16:00-18:00)
|
|
|
|
|
- vo.setAppointmentTimeRange(formatTimeRange(order.getAppointmentStartTime(), order.getProjectDuration()));
|
|
|
|
|
|
|
+ // 原始格式
|
|
|
|
|
+ vo.setAppointmentStartTime(start.format(DATE_TIME_FORMATTER));
|
|
|
|
|
+ vo.setAppointmentEndTime(end.format(DATE_TIME_FORMATTER));
|
|
|
|
|
|
|
|
- // 3. 联系信息脱敏处理
|
|
|
|
|
- vo.setContactPhoneNumber(PhoneUtils.maskPhoneNumberStrict(order.getContactPhoneNumber()));
|
|
|
|
|
|
|
+ // 生成“5月12日 16:00-18:00”格式
|
|
|
|
|
+ String timeRange = formatAppointmentTimeRange(start, end);
|
|
|
|
|
+ vo.setAppointmentTimeRange(timeRange);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (order.getCreateTime() != null) {
|
|
|
|
|
+ vo.setCreateTime(DATE_FORMAT.format(order.getCreateTime()));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (order.getPaidTime() != null) {
|
|
|
|
|
+ vo.setPaidTime(order.getPaidTime().format(DATE_TIME_FORMATTER));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 4. 计算距离与告警提示 (依赖地理围栏表 t_geo_fence)
|
|
|
|
|
- List<TGeoFence> validFenceList = geoFenceService.selectValidFences(order.getCityCode());
|
|
|
|
|
- List<TGeoFence> usableFence = filterUsableFence(validFenceList);
|
|
|
|
|
|
|
+ vo.setContactAddresInfo(order.getContactAddressInfo());
|
|
|
|
|
+ // 联系信息组合:姓名 + 手机号(中间4位脱敏)
|
|
|
|
|
+ String contactInfo = buildContactInfo(order.getContactPersonName(), order.getContactPhoneNumber());
|
|
|
|
|
+ vo.setContactInfo(contactInfo);
|
|
|
|
|
|
|
|
- // 5. 查询关联的售后信息 (如果有)
|
|
|
|
|
|
|
+ vo.setPaymentMethod(order.getPaymentMethod());
|
|
|
|
|
+ vo.setPaymentMethodName(PaymentMethodEnum.getNameByCode(order.getPaymentMethod()));
|
|
|
|
|
+
|
|
|
|
|
+ // 获取订单售后数据
|
|
|
this.afterSalesServiceService.fillCurrentAfterSaleInfo(vo, order.getId());
|
|
this.afterSalesServiceService.fillCurrentAfterSaleInfo(vo, order.getId());
|
|
|
|
|
|
|
|
- // 6. 计算高风险围栏信息
|
|
|
|
|
- calcOrderGeoFenceInfo(vo, usableFence);
|
|
|
|
|
|
|
+ // 订单状态流转列表转换
|
|
|
|
|
+ List<OrderStatusFlowVO> flowVOList = flowList.stream().map(this::convertToFlowVO).sorted(Comparator.comparing(OrderStatusFlowVO::getCreateTime)).collect(Collectors.toList());
|
|
|
|
|
+ vo.setOrderStatusFlowList(flowVOList);
|
|
|
|
|
|
|
|
return vo;
|
|
return vo;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @Override
|
|
|
|
|
- public List<BookingSlotsVO> bookingSlots(BookingSlotsDTO dto) {
|
|
|
|
|
-
|
|
|
|
|
- // 1. 获取项目信息(关键:服务时长)
|
|
|
|
|
- Project project = this.projectService.getById(dto.getProjectId());
|
|
|
|
|
- if (ObjectUtil.isNull(project)) {
|
|
|
|
|
- throw new ServiceException("项目不存在");
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 格式化预约时间范围
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param start 预约开始时间
|
|
|
|
|
+ * @param end 预约结束时间
|
|
|
|
|
+ * @return 示例:5月12日 16:00-18:00
|
|
|
|
|
+ */
|
|
|
|
|
+ private String formatAppointmentTimeRange(LocalDateTime start, LocalDateTime end) {
|
|
|
|
|
+ if (start == null) {
|
|
|
|
|
+ return "";
|
|
|
}
|
|
}
|
|
|
|
|
+ // 日期格式:M月d日(不带年份,同一年内)
|
|
|
|
|
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("M月d日");
|
|
|
|
|
+ String dateStr = start.format(dateFormatter);
|
|
|
|
|
+ // 时间格式:HH:mm
|
|
|
|
|
+ DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
|
|
|
|
|
+ String startTime = start.format(timeFormatter);
|
|
|
|
|
+ String endTime = (end != null) ? end.format(timeFormatter) : "";
|
|
|
|
|
+ return dateStr + " " + startTime + "-" + endTime;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- Integer standardDuration = project.getStandardDuration();
|
|
|
|
|
- Integer unitType = project.getUnitType();
|
|
|
|
|
- String unitTypeName = unitDictCache.getOrDefault(unitType, "未知单位");
|
|
|
|
|
-
|
|
|
|
|
- Integer durationMinutes = convertToMinutes(standardDuration, unitType);
|
|
|
|
|
- if (ObjectUtil.isNull(durationMinutes) || durationMinutes <= 0) {
|
|
|
|
|
- throw new ServiceException("项目时长配置异常");
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建联系信息(姓名 + 脱敏手机号)
|
|
|
|
|
+ * 示例:王先生,188****5555
|
|
|
|
|
+ */
|
|
|
|
|
+ private String buildContactInfo(String name, String phone) {
|
|
|
|
|
+ if (!StringUtils.hasText(name) && !StringUtils.hasText(phone)) {
|
|
|
|
|
+ return "";
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 2. 生成所有候选开始时间(半点粒度,步进30分钟)
|
|
|
|
|
- List<LocalTime> candidateStarts = TimeSlotUtil.getAvailableTimeSlots(dto.getAppointmentDate(), APPOINT_BUFFER_MINUTES);
|
|
|
|
|
-
|
|
|
|
|
- // 3. 查询该商户当天的【有效占用】订单
|
|
|
|
|
- LocalDateTime dayStart = dto.getAppointmentDate().atStartOfDay();
|
|
|
|
|
- LocalDateTime dayEnd = dto.getAppointmentDate().atTime(LocalTime.MAX);
|
|
|
|
|
- List<TOrder> validOrders = this.baseMapper.selectValidOrdersByMerchantAndDate(
|
|
|
|
|
- dto.getMerchantId(), dto.getProjectId(), dayStart, dayEnd);
|
|
|
|
|
-
|
|
|
|
|
- // 4. 遍历候选时段,构建 VO
|
|
|
|
|
- List<BookingSlotsVO> voList = new ArrayList<>(candidateStarts.size());
|
|
|
|
|
- for (LocalTime startTime : candidateStarts) {
|
|
|
|
|
- LocalDateTime slotStart = LocalDateTime.of(dto.getAppointmentDate(), startTime);
|
|
|
|
|
- LocalDateTime actualEnd = slotStart.plusMinutes(durationMinutes);
|
|
|
|
|
-
|
|
|
|
|
- BookingSlotsVO vo = new BookingSlotsVO();
|
|
|
|
|
- vo.setStartTime(startTime);
|
|
|
|
|
- vo.setStandardDuration(standardDuration);
|
|
|
|
|
- vo.setUnitTypeName(unitTypeName);
|
|
|
|
|
- vo.setEndTime(actualEnd.toLocalTime());
|
|
|
|
|
- vo.setIsDay(maProjectFareSettingService.isDayTimePeriod(slotStart));
|
|
|
|
|
-
|
|
|
|
|
- boolean exceedsDay = !actualEnd.toLocalDate().isEqual(dto.getAppointmentDate());
|
|
|
|
|
- vo.setAvailable(!exceedsDay && isAppointmentSlotAvailable(slotStart, durationMinutes, validOrders));
|
|
|
|
|
- voList.add(vo);
|
|
|
|
|
|
|
+ String maskedPhone = "";
|
|
|
|
|
+ if (StringUtils.hasText(phone) && phone.length() >= 11) {
|
|
|
|
|
+ maskedPhone = phone.substring(0, 3) + "****" + phone.substring(7);
|
|
|
|
|
+ } else if (StringUtils.hasText(phone)) {
|
|
|
|
|
+ maskedPhone = phone; // 长度不足则直接显示
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return voList;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @Override
|
|
|
|
|
- @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
- public void startGoOut(MerchantOrderOperateDTO dto) {
|
|
|
|
|
-
|
|
|
|
|
- // 1. 获取当前登录商户
|
|
|
|
|
- WxLoginUser loginUser = getCurrentWxLoginUser();
|
|
|
|
|
- Long merchantId = Long.parseLong(loginUser.getId());
|
|
|
|
|
- Long orderId = dto.getOrderId();
|
|
|
|
|
-
|
|
|
|
|
- // 查询订单
|
|
|
|
|
- TOrder order = validateAndGetOrder(orderId, merchantId);
|
|
|
|
|
-
|
|
|
|
|
- // 获取履约状态
|
|
|
|
|
- Integer execStatus = order.getExecStatus();
|
|
|
|
|
- if (!ExecStatusEnum.WAIT_DEPART.getCode().equals(execStatus)) {
|
|
|
|
|
- throw new ServiceException("仅【待出发/待签到】订单可修改为已出发,当前状态:" + ExecStatusEnum.getInfoByCode(execStatus));
|
|
|
|
|
|
|
+ if (StringUtils.hasText(name)) {
|
|
|
|
|
+ return name + "," + maskedPhone;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return maskedPhone;
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 更新出发信息、履约信息
|
|
|
|
|
- LocalDateTime now = LocalDateTime.now();
|
|
|
|
|
- LambdaUpdateWrapper<TOrder> update = Wrappers.lambdaUpdate(TOrder.class)
|
|
|
|
|
- .set(TOrder::getDepartedTime, now)
|
|
|
|
|
- .set(TOrder::getUpdateTime, now)
|
|
|
|
|
- .set(TOrder::getUpdateBy, loginUser.getCNickName())
|
|
|
|
|
- .set(TOrder::getExecStatus, ExecStatusEnum.DEPARTED.getCode())
|
|
|
|
|
- .eq(TOrder::getId, orderId);
|
|
|
|
|
- int count = baseMapper.update(null, update);
|
|
|
|
|
- if (count <= 0) {
|
|
|
|
|
- throw new ServiceException("订单履约状态已变更,请刷新重试");
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将 OrderStatusFlow 转换为 OrderStatusFlowVO
|
|
|
|
|
+ */
|
|
|
|
|
+ private OrderStatusFlowVO convertToFlowVO(OrderStatusFlow flow) {
|
|
|
|
|
+ OrderStatusFlowVO vo = new OrderStatusFlowVO();
|
|
|
|
|
+ vo.setStatusName(OrderStatusEnum.getInfoByCode(flow.getStatus()));
|
|
|
|
|
+ if (flow.getCreateTime() != null) {
|
|
|
|
|
+ vo.setCreateTime(DATE_FORMAT.format(flow.getCreateTime()));
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+ return vo;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -1310,62 +1308,6 @@ public class TOrderServiceImpl extends BaseServiceImpl<TOrderMapper, TOrder> imp
|
|
|
dto.setLongitude(lon);
|
|
dto.setLongitude(lon);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 过滤可用围栏:剔除坐标/半径为空、半径<=0的无效围栏,减少内层循环次数
|
|
|
|
|
- */
|
|
|
|
|
- private List<TGeoFence> filterUsableFence(List<TGeoFence> allFence) {
|
|
|
|
|
- if (CollUtil.isEmpty(allFence)) {
|
|
|
|
|
- return Collections.emptyList();
|
|
|
|
|
- }
|
|
|
|
|
- return allFence.stream()
|
|
|
|
|
- .filter(fence -> !ObjectUtil.hasNull(fence.getLatitude(), fence.getLongitude(), fence.getRadiusKm()))
|
|
|
|
|
- .filter(fence -> fence.getRadiusKm().compareTo(BigDecimal.ZERO) > 0)
|
|
|
|
|
- .collect(Collectors.toList());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * 计算单条订单围栏距离、是否高风险区域、围栏备注
|
|
|
|
|
- */
|
|
|
|
|
- private void calcOrderGeoFenceInfo(IGeoRiskInfo orderVO, List<TGeoFence> usableFence) {
|
|
|
|
|
- // 用户坐标为空直接标记非风险
|
|
|
|
|
- BigDecimal userLat = orderVO.getUserLatitude();
|
|
|
|
|
- BigDecimal userLon = orderVO.getUserLongitude();
|
|
|
|
|
- if (ObjectUtil.hasNull(userLat, userLon) || CollUtil.isEmpty(usableFence)) {
|
|
|
|
|
- orderVO.setIsHighRiskArea(false);
|
|
|
|
|
- orderVO.setDistanceKm(null);
|
|
|
|
|
- orderVO.setFenceIntro(null);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- BigDecimal minDistanceKm = null;
|
|
|
|
|
- TGeoFence nearestFence = null;
|
|
|
|
|
- // 遍历可用围栏,计算最小距离围栏
|
|
|
|
|
- for (TGeoFence fence : usableFence) {
|
|
|
|
|
- BigDecimal distKm = DistanceUtil.formatDistanceInKilometers(userLat, userLon, fence.getLatitude(), fence.getLongitude());
|
|
|
|
|
- if (ObjectUtil.isNull(distKm)) {
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
- // 更新最近围栏
|
|
|
|
|
- if (ObjectUtil.isNull(minDistanceKm) || distKm.compareTo(minDistanceKm) < 0) {
|
|
|
|
|
- minDistanceKm = distKm;
|
|
|
|
|
- nearestFence = fence;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 赋值围栏相关信息
|
|
|
|
|
- if (ObjectUtil.isNull(minDistanceKm) || ObjectUtil.isNull(nearestFence)) {
|
|
|
|
|
- orderVO.setIsHighRiskArea(false);
|
|
|
|
|
- orderVO.setDistanceKm(null);
|
|
|
|
|
- orderVO.setFenceIntro(null);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- orderVO.setDistanceKm(minDistanceKm);
|
|
|
|
|
- // 判断是否在围栏半径内
|
|
|
|
|
- boolean isHighRisk = minDistanceKm.compareTo(nearestFence.getRadiusKm()) <= 0;
|
|
|
|
|
- orderVO.setIsHighRiskArea(isHighRisk);
|
|
|
|
|
- orderVO.setFenceIntro(nearestFence.getFenceIntro());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* 格式化预约时间范围
|
|
* 格式化预约时间范围
|
|
|
* 示例输出: "5月12日 16:00-18:00"
|
|
* 示例输出: "5月12日 16:00-18:00"
|