|
|
@@ -11,6 +11,9 @@ import com.ylx.common.core.domain.model.WxLoginUser;
|
|
|
import com.ylx.common.exception.ServiceException;
|
|
|
import com.ylx.common.utils.DateUtils;
|
|
|
import com.ylx.common.utils.SecurityUtils;
|
|
|
+import com.ylx.common.weixinPay.enums.WxPayTypeEnum;
|
|
|
+import com.ylx.common.weixinPay.service.WxPayV3Service;
|
|
|
+import com.ylx.giftCard.domain.GiftCardOrder;
|
|
|
import com.ylx.massage.domain.MaTechnician;
|
|
|
import com.ylx.massage.domain.TAddress;
|
|
|
import com.ylx.massage.domain.TWxUser;
|
|
|
@@ -42,6 +45,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.LocalTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
@@ -72,6 +76,8 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
|
|
|
private ShoppingFundsDetailService shoppingFundsDetailService;
|
|
|
@Resource
|
|
|
private CouponService couponService;
|
|
|
+ @Resource
|
|
|
+ private WxPayV3Service wxPayV3Service;
|
|
|
|
|
|
@Override
|
|
|
public TOrder addOrder(TOrder order) {
|
|
|
@@ -205,156 +211,83 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
|
|
|
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
- public void submitOrder(OrderSubmitDTO dto) {
|
|
|
-
|
|
|
- TOrder order = new TOrder();
|
|
|
- String orderNo = orderNumberGenerator.generateNextOrderNumber("");
|
|
|
- order.setOrderNo(orderNo);
|
|
|
+ public Map<String, Object> submitOrder(OrderSubmitDTO dto) {
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
|
|
|
- // 1. 获取当前用户
|
|
|
+ // 1. 获取并校验当前用户
|
|
|
WxLoginUser wxLoginUser = SecurityUtils.getWxLoginUser();
|
|
|
if (ObjectUtil.isNull(wxLoginUser)) {
|
|
|
throw new ServiceException("用户未登录");
|
|
|
}
|
|
|
-
|
|
|
Long userId = Long.parseLong(wxLoginUser.getId());
|
|
|
+ String openId = wxLoginUser.getCOpenid();
|
|
|
|
|
|
- order.setUserId(userId);
|
|
|
-
|
|
|
- // 2. 获取项目信息
|
|
|
+ // 2. 获取并校验项目信息
|
|
|
Project project = this.projectService.getById(dto.getProjectId());
|
|
|
if (ObjectUtil.isNull(project)) {
|
|
|
throw new ServiceException("项目不存在");
|
|
|
}
|
|
|
|
|
|
- order.setProjectId(dto.getProjectId());
|
|
|
- order.setProjectType(project.getType());
|
|
|
- order.setProjectName(project.getTitle());
|
|
|
- order.setProjectCover(project.getCover());
|
|
|
- order.setHighlight(project.getHighlight());
|
|
|
- order.setAppointmentStartTime(dto.getAppointmentStartTime());
|
|
|
- order.setProjectDuration(project.getStandardDuration());
|
|
|
-
|
|
|
- // 3. 获取商户信息
|
|
|
+ // 3. 获取并校验商户信息
|
|
|
MaTechnician maTechnician = this.maTechnicianService.getById(dto.getMerchantId());
|
|
|
if (ObjectUtil.isNull(maTechnician)) {
|
|
|
throw new ServiceException("商户不存在");
|
|
|
}
|
|
|
|
|
|
- List<TAddress> merchantAddresslist = this.addressService.list(new LambdaQueryWrapper<TAddress>()
|
|
|
- .eq(TAddress::getMerchantId, dto.getMerchantId())
|
|
|
- .eq(TAddress::getUserType, 2)
|
|
|
- .eq(TAddress::getIsDelete, 0));
|
|
|
- if (CollUtil.isEmpty(merchantAddresslist)) {
|
|
|
- throw new ServiceException("商户地址不存在");
|
|
|
- }
|
|
|
-
|
|
|
- order.setMerchantId(dto.getMerchantId());
|
|
|
- order.setMerchantType(maTechnician.getTechType());
|
|
|
- order.setMerchantNickName(maTechnician.getTeNickName());
|
|
|
- order.setMerchantAvatar(maTechnician.getTeAvatar());
|
|
|
-
|
|
|
// 4. 获取联系人信息
|
|
|
TAddress address = this.addressService.getById(dto.getAddressId());
|
|
|
if (ObjectUtil.isNull(address)) {
|
|
|
throw new ServiceException("客户联系地址不存在");
|
|
|
}
|
|
|
|
|
|
- order.setContactPersonName(address.getUserName());
|
|
|
- order.setContactPhoneNumber(address.getPhone());
|
|
|
- order.setContactAddressInfo(address.getDetailAddress());
|
|
|
-
|
|
|
- // 5. 设置价格信息
|
|
|
- LocalDateTime appointmentStartTime = dto.getAppointmentStartTime();
|
|
|
+ // 5. 获取商户地址信息
|
|
|
+ List<TAddress> merchantAddressList = this.addressService.list(new LambdaQueryWrapper<TAddress>()
|
|
|
+ .eq(TAddress::getMerchantId, dto.getMerchantId())
|
|
|
+ .eq(TAddress::getUserType, 2)
|
|
|
+ .eq(TAddress::getIsDelete, 0));
|
|
|
+ if (CollUtil.isEmpty(merchantAddressList)) {
|
|
|
+ throw new ServiceException("商户地址不存在");
|
|
|
+ }
|
|
|
|
|
|
- // 优惠券优惠
|
|
|
+ // 6. 计算价格与优惠券核销 (核心优化:先算钱、扣券,再落库)
|
|
|
+ BigDecimal basePrice = Optional.ofNullable(project.getPrice()).orElse(BigDecimal.ZERO);
|
|
|
+ BigDecimal trafficFee = Optional.ofNullable(dto.getTrafficFee()).orElse(BigDecimal.ZERO);
|
|
|
BigDecimal couponDiscount = BigDecimal.ZERO;
|
|
|
- order.setCouponId(dto.getCouponId());
|
|
|
+
|
|
|
if (ObjectUtil.isNotNull(dto.getCouponId())) {
|
|
|
- couponDiscount = this.couponService.calculateDiscountAmount(dto.getCouponId(), wxLoginUser.getCOpenid(), project.getPrice());
|
|
|
+ // 计算抵扣金额
|
|
|
+ couponDiscount = this.couponService.calculateDiscountAmount(dto.getCouponId(), openId, basePrice);
|
|
|
+ // 立即核销优惠券,保证与订单在同一事务中
|
|
|
+ this.couponService.useCoupon(dto.getCouponId(), openId, null, 1);
|
|
|
}
|
|
|
- // 车费
|
|
|
- BigDecimal trafficFee = Optional.ofNullable(dto.getTrafficFee()).orElse(BigDecimal.ZERO);
|
|
|
- // 商品原价
|
|
|
- BigDecimal price = Optional.ofNullable(project.getPrice()).orElse(BigDecimal.ZERO);
|
|
|
|
|
|
// 实付金额 = 商品原价 - 优惠券优惠 + 车费
|
|
|
- BigDecimal finalAmount = price.subtract(couponDiscount).add(trafficFee);
|
|
|
-
|
|
|
- order.setBasePrice(price);
|
|
|
- order.setCouponDiscount(couponDiscount);
|
|
|
- order.setTrafficFee(trafficFee);
|
|
|
- order.setFinalAmount(finalAmount);
|
|
|
+ BigDecimal finalAmount = basePrice.subtract(couponDiscount).add(trafficFee)
|
|
|
+ .setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
|
- order.setCreateTime(DateUtils.getNowDate());
|
|
|
- order.setAppointmentStartTime(appointmentStartTime);
|
|
|
- order.setStatus(0);
|
|
|
- order.setExecStatus(0);
|
|
|
- order.setDispatchedStatus(0);
|
|
|
-
|
|
|
- // 6.经纬度信息
|
|
|
- order.setUserLatitude(new BigDecimal(address.getLatitude()));
|
|
|
- order.setUserLongitude(new BigDecimal(address.getLongitude()));
|
|
|
- //取默认真实地址(type=1)
|
|
|
- Optional<TAddress> realAddrOpt = merchantAddresslist.stream()
|
|
|
- .filter(a -> a.getType() == 1)
|
|
|
- .findFirst();
|
|
|
- //取默认虚拟地址(type=2)
|
|
|
- Optional<TAddress> virtualAddrOpt = merchantAddresslist.stream()
|
|
|
- .filter(a -> a.getType() == 2)
|
|
|
- .findFirst();
|
|
|
-
|
|
|
- realAddrOpt.ifPresent(addr -> {
|
|
|
- order.setMerchantLongitude(new BigDecimal(addr.getLongitude()));
|
|
|
- order.setMerchantLatitude(new BigDecimal(addr.getLatitude()));
|
|
|
- });
|
|
|
- virtualAddrOpt.ifPresent(addr -> {
|
|
|
- order.setVirtualLongitude(new BigDecimal(addr.getLongitude()));
|
|
|
- order.setVirtualLatitude(new BigDecimal(addr.getLatitude()));
|
|
|
- });
|
|
|
-
|
|
|
- // 7. 判断支付方式
|
|
|
- Integer paymentMethod = dto.getPaymentMethod();
|
|
|
- order.setPaymentMethod(paymentMethod);
|
|
|
- // 余额支付
|
|
|
- if (ObjectUtil.equals(PaymentMethodEnum.BALANCE.getCode(), paymentMethod)) {
|
|
|
- order.setStatus(OrderStatusEnum.PENDING_DISPATCH.getCode());
|
|
|
- order.setPaidTime(LocalDateTime.now());
|
|
|
- } else {
|
|
|
- order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode());
|
|
|
- }
|
|
|
+ // 7. 组装订单对象
|
|
|
+ TOrder order = buildOrder(dto, project, maTechnician, address, merchantAddressList,
|
|
|
+ basePrice, trafficFee, couponDiscount, finalAmount, userId);
|
|
|
|
|
|
+ // 8. 保存订单
|
|
|
boolean saveResult = this.save(order);
|
|
|
if (!saveResult) {
|
|
|
throw new ServiceException("添加订单失败");
|
|
|
}
|
|
|
|
|
|
- // 8. 处理用户余额数据
|
|
|
- if (ObjectUtil.equals(PaymentMethodEnum.BALANCE.getCode(), paymentMethod)) {
|
|
|
-
|
|
|
- // 处理用户余额
|
|
|
- TWxUser user = this.wxUserService.getById(userId);
|
|
|
- BigDecimal newBalance = user.getdBalance().add(finalAmount);
|
|
|
- this.wxUserService.lambdaUpdate()
|
|
|
- .set(TWxUser::getdBalance, newBalance)
|
|
|
- .eq(TWxUser::getId, user.getId())
|
|
|
- .update();
|
|
|
-
|
|
|
- // 记录购物金明细
|
|
|
- ShoppingFundsDetailAddDto shoppingFundsDetailAddDto = new ShoppingFundsDetailAddDto();
|
|
|
- shoppingFundsDetailAddDto.setUserId(userId.toString());
|
|
|
- shoppingFundsDetailAddDto.setAmount(finalAmount);
|
|
|
- shoppingFundsDetailAddDto.setOrderNo(orderNo);
|
|
|
- shoppingFundsDetailAddDto.setExpenseType(ShoppingFundsExpenseTypeEnum.CONSUMPTION.getCode());
|
|
|
- shoppingFundsDetailAddDto.setBalance(newBalance);
|
|
|
- shoppingFundsDetailService.addShoppingFundsDetail(shoppingFundsDetailAddDto);
|
|
|
-
|
|
|
- }
|
|
|
- // 9. 判断有没有使用优惠券
|
|
|
- if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
- this.couponService.useCoupon(dto.getCouponId(), wxLoginUser.getCOpenid(), order.getId(), 1);
|
|
|
+ // 9. 更新优惠券关联的真实订单ID (因为刚才核销时订单还没生成,这里补上)
|
|
|
+ if (ObjectUtil.isNotNull(dto.getCouponId())) {
|
|
|
+ this.couponService.useCoupon(dto.getCouponId(), openId, order.getId(), 1);
|
|
|
}
|
|
|
|
|
|
+ // 10. 处理余额支付逻辑
|
|
|
+ if (ObjectUtil.equals(PaymentMethodEnum.BALANCE.getCode(), dto.getPaymentMethod())) {
|
|
|
+ handleBalancePayment(userId, finalAmount, order.getOrderNo());
|
|
|
+ map.put("orderId", order.getId());
|
|
|
+ return map;
|
|
|
+ } else {
|
|
|
+ return createWxPayOrder(order, wxLoginUser);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -470,4 +403,110 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
|
|
|
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());
|
|
|
+ order.setProjectDuration(project.getStandardDuration());
|
|
|
+ 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.setBasePrice(basePrice);
|
|
|
+ order.setCouponDiscount(couponDiscount);
|
|
|
+ order.setTrafficFee(trafficFee);
|
|
|
+ order.setFinalAmount(finalAmount);
|
|
|
+ order.setCouponId(dto.getCouponId());
|
|
|
+ order.setCreateTime(DateUtils.getNowDate());
|
|
|
+ order.setAppointmentStartTime(dto.getAppointmentStartTime());
|
|
|
+ order.setStatus(0);
|
|
|
+ order.setExecStatus(0);
|
|
|
+ order.setDispatchedStatus(0);
|
|
|
+ order.setPaymentMethod(dto.getPaymentMethod());
|
|
|
+
|
|
|
+ // 经纬度安全赋值
|
|
|
+ order.setUserLatitude(new BigDecimal(address.getLatitude()));
|
|
|
+ order.setUserLongitude(new BigDecimal(address.getLongitude()));
|
|
|
+
|
|
|
+ merchantAddressList.stream().filter(a -> a.getType() == 1).findFirst().ifPresent(addr -> {
|
|
|
+ order.setMerchantLongitude(new BigDecimal(addr.getLongitude()));
|
|
|
+ order.setMerchantLatitude(new BigDecimal(addr.getLatitude()));
|
|
|
+ });
|
|
|
+ merchantAddressList.stream().filter(a -> a.getType() == 2).findFirst().ifPresent(addr -> {
|
|
|
+ order.setVirtualLongitude(new BigDecimal(addr.getLongitude()));
|
|
|
+ order.setVirtualLatitude(new BigDecimal(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, String orderNo) {
|
|
|
+ // 1. 数据库层面原子扣减余额,并校验余额是否充足
|
|
|
+ boolean deductSuccess = this.wxUserService.lambdaUpdate()
|
|
|
+ .setSql("d_balance = d_balance - " + finalAmount)
|
|
|
+ .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(orderNo);
|
|
|
+ detailDto.setExpenseType(ShoppingFundsExpenseTypeEnum.CONSUMPTION.getCode());
|
|
|
+ detailDto.setBalance(user.getdBalance());
|
|
|
+ this.shoppingFundsDetailService.addShoppingFundsDetail(detailDto);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建微信支付订单(事务外执行,减少事务时长)
|
|
|
+ */
|
|
|
+ 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("支付服务异常,请稍后重试");
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|