289 lines
11 KiB
Markdown
289 lines
11 KiB
Markdown
|
|
# 订单服务开发文档 - Part 2(Service实现 + Controller)
|
|||
|
|
|
|||
|
|
## 四、Service 实现
|
|||
|
|
|
|||
|
|
### OrderServiceImpl.java
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
package com.openclaw.service.impl;
|
|||
|
|
|
|||
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||
|
|
import com.openclaw.constant.ErrorCode;
|
|||
|
|
import com.openclaw.dto.*;
|
|||
|
|
import com.openclaw.entity.*;
|
|||
|
|
import com.openclaw.exception.BusinessException;
|
|||
|
|
import com.openclaw.repository.*;
|
|||
|
|
import com.openclaw.service.*;
|
|||
|
|
import com.openclaw.util.IdGenerator;
|
|||
|
|
import com.openclaw.vo.*;
|
|||
|
|
import lombok.RequiredArgsConstructor;
|
|||
|
|
import org.springframework.stereotype.Service;
|
|||
|
|
import org.springframework.transaction.annotation.Transactional;
|
|||
|
|
import java.math.BigDecimal;
|
|||
|
|
import java.time.LocalDateTime;
|
|||
|
|
import java.util.ArrayList;
|
|||
|
|
import java.util.List;
|
|||
|
|
|
|||
|
|
@Service
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class OrderServiceImpl implements OrderService {
|
|||
|
|
|
|||
|
|
private final OrderRepository orderRepo;
|
|||
|
|
private final OrderItemRepository itemRepo;
|
|||
|
|
private final OrderRefundRepository refundRepo;
|
|||
|
|
private final SkillRepository skillRepo;
|
|||
|
|
private final SkillDownloadRepository downloadRepo;
|
|||
|
|
private final PointsService pointsService;
|
|||
|
|
private final IdGenerator idGenerator;
|
|||
|
|
|
|||
|
|
private static final BigDecimal POINTS_RATE = new BigDecimal("0.01"); // 1积分=0.01元
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
@Transactional
|
|||
|
|
public OrderVO createOrder(Long userId, OrderCreateDTO dto) {
|
|||
|
|
List<Skill> skills = new ArrayList<>();
|
|||
|
|
BigDecimal total = BigDecimal.ZERO;
|
|||
|
|
|
|||
|
|
for (Long skillId : dto.getSkillIds()) {
|
|||
|
|
Skill skill = skillRepo.selectById(skillId);
|
|||
|
|
if (skill == null || !"approved".equals(skill.getStatus()))
|
|||
|
|
throw new BusinessException(ErrorCode.SKILL_NOT_FOUND);
|
|||
|
|
if (downloadRepo.existsByUserIdAndSkillId(userId, skillId))
|
|||
|
|
throw new BusinessException(ErrorCode.SKILL_ALREADY_OWNED);
|
|||
|
|
skills.add(skill);
|
|||
|
|
total = total.add(Boolean.TRUE.equals(skill.getIsFree()) ? BigDecimal.ZERO : skill.getPrice());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 积分抵扣计算
|
|||
|
|
int pts = dto.getPointsToUse() == null ? 0 : dto.getPointsToUse();
|
|||
|
|
BigDecimal ptsDed = BigDecimal.ZERO;
|
|||
|
|
if (pts > 0) {
|
|||
|
|
if (!pointsService.hasEnoughPoints(userId, pts))
|
|||
|
|
throw new BusinessException(ErrorCode.POINTS_NOT_ENOUGH);
|
|||
|
|
ptsDed = new BigDecimal(pts).multiply(POINTS_RATE).min(total);
|
|||
|
|
}
|
|||
|
|
BigDecimal cash = total.subtract(ptsDed);
|
|||
|
|
|
|||
|
|
// 创建订单主记录
|
|||
|
|
Order order = new Order();
|
|||
|
|
order.setOrderNo(idGenerator.generateOrderNo());
|
|||
|
|
order.setUserId(userId);
|
|||
|
|
order.setTotalAmount(total);
|
|||
|
|
order.setCashAmount(cash);
|
|||
|
|
order.setPointsUsed(pts);
|
|||
|
|
order.setPointsDeductAmount(ptsDed);
|
|||
|
|
order.setStatus("pending");
|
|||
|
|
order.setPaymentMethod(dto.getPaymentMethod());
|
|||
|
|
order.setExpiredAt(LocalDateTime.now().plusMinutes(30));
|
|||
|
|
orderRepo.insert(order);
|
|||
|
|
|
|||
|
|
// 创建订单项(快照商品信息)
|
|||
|
|
for (Skill s : skills) {
|
|||
|
|
OrderItem item = new OrderItem();
|
|||
|
|
item.setOrderId(order.getId());
|
|||
|
|
item.setSkillId(s.getId());
|
|||
|
|
item.setSkillName(s.getName());
|
|||
|
|
item.setSkillCover(s.getCoverImageUrl());
|
|||
|
|
BigDecimal price = Boolean.TRUE.equals(s.getIsFree()) ? BigDecimal.ZERO : s.getPrice();
|
|||
|
|
item.setUnitPrice(price);
|
|||
|
|
item.setQuantity(1);
|
|||
|
|
item.setTotalPrice(price);
|
|||
|
|
itemRepo.insert(item);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 冻结积分
|
|||
|
|
if (pts > 0) pointsService.freezePoints(userId, pts, order.getId());
|
|||
|
|
|
|||
|
|
// 纯免费/纯积分支付直接完成,无需拉起支付
|
|||
|
|
if (cash.compareTo(BigDecimal.ZERO) == 0) {
|
|||
|
|
handlePaySuccess(order.getOrderNo(), null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return buildOrderVO(order);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public OrderVO getOrderDetail(Long orderId, Long userId) {
|
|||
|
|
Order order = orderRepo.selectById(orderId);
|
|||
|
|
if (order == null || !order.getUserId().equals(userId))
|
|||
|
|
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
|
|||
|
|
return buildOrderVO(order);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public IPage<OrderVO> listOrders(Long userId, String status, int pageNum, int pageSize) {
|
|||
|
|
IPage<Order> page = orderRepo.selectPage(
|
|||
|
|
new Page<>(pageNum, pageSize),
|
|||
|
|
new LambdaQueryWrapper<Order>()
|
|||
|
|
.eq(Order::getUserId, userId)
|
|||
|
|
.eq(status != null, Order::getStatus, status)
|
|||
|
|
.orderByDesc(Order::getCreatedAt));
|
|||
|
|
return page.convert(this::buildOrderVO);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
@Transactional
|
|||
|
|
public void cancelOrder(Long orderId, Long userId, String reason) {
|
|||
|
|
Order order = orderRepo.selectById(orderId);
|
|||
|
|
if (order == null || !order.getUserId().equals(userId))
|
|||
|
|
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
|
|||
|
|
if (!"pending".equals(order.getStatus()))
|
|||
|
|
throw new BusinessException(ErrorCode.ORDER_STATUS_ERROR);
|
|||
|
|
order.setStatus("cancelled");
|
|||
|
|
order.setCancelReason(reason);
|
|||
|
|
orderRepo.updateById(order);
|
|||
|
|
// 解冻积分
|
|||
|
|
if (order.getPointsUsed() != null && order.getPointsUsed() > 0)
|
|||
|
|
pointsService.unfreezePoints(userId, order.getPointsUsed(), orderId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
@Transactional
|
|||
|
|
public void handlePaySuccess(String orderNo, String transactionId) {
|
|||
|
|
Order order = orderRepo.findByOrderNo(orderNo);
|
|||
|
|
if (order == null) throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
|
|||
|
|
if ("paid".equals(order.getStatus())) return; // 幂等处理
|
|||
|
|
|
|||
|
|
order.setStatus("paid");
|
|||
|
|
order.setPaidAt(LocalDateTime.now());
|
|||
|
|
orderRepo.updateById(order);
|
|||
|
|
|
|||
|
|
// 消耗冻结积分(正式扣减)
|
|||
|
|
if (order.getPointsUsed() != null && order.getPointsUsed() > 0)
|
|||
|
|
pointsService.consumePoints(order.getUserId(), order.getPointsUsed(), order.getId(), "order");
|
|||
|
|
|
|||
|
|
// 授权Skill访问权限
|
|||
|
|
String dlType = (order.getPointsUsed() != null && order.getPointsUsed() > 0
|
|||
|
|
&& order.getCashAmount().compareTo(BigDecimal.ZERO) == 0) ? "points" : "paid";
|
|||
|
|
itemRepo.findByOrderId(order.getId()).forEach(item ->
|
|||
|
|
downloadRepo.grantAccess(order.getUserId(), item.getSkillId(), order.getId(), dlType));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
@Transactional
|
|||
|
|
public void applyRefund(Long orderId, Long userId, RefundApplyDTO dto) {
|
|||
|
|
Order order = orderRepo.selectById(orderId);
|
|||
|
|
if (order == null || !order.getUserId().equals(userId))
|
|||
|
|
throw new BusinessException(ErrorCode.ORDER_NOT_FOUND);
|
|||
|
|
if (!"paid".equals(order.getStatus()) && !"completed".equals(order.getStatus()))
|
|||
|
|
throw new BusinessException(ErrorCode.ORDER_STATUS_ERROR);
|
|||
|
|
order.setStatus("refunding");
|
|||
|
|
orderRepo.updateById(order);
|
|||
|
|
|
|||
|
|
OrderRefund refund = new OrderRefund();
|
|||
|
|
refund.setOrderId(orderId);
|
|||
|
|
refund.setRefundNo(idGenerator.generateRefundNo());
|
|||
|
|
refund.setRefundAmount(order.getCashAmount());
|
|||
|
|
refund.setRefundPoints(order.getPointsUsed());
|
|||
|
|
refund.setReason(dto.getReason());
|
|||
|
|
if (dto.getImages() != null) refund.setImages(dto.getImages().toString());
|
|||
|
|
refund.setStatus("pending");
|
|||
|
|
refundRepo.insert(refund);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private OrderVO buildOrderVO(Order order) {
|
|||
|
|
OrderVO vo = new OrderVO();
|
|||
|
|
vo.setId(order.getId());
|
|||
|
|
vo.setOrderNo(order.getOrderNo());
|
|||
|
|
vo.setTotalAmount(order.getTotalAmount());
|
|||
|
|
vo.setCashAmount(order.getCashAmount());
|
|||
|
|
vo.setPointsUsed(order.getPointsUsed());
|
|||
|
|
vo.setPointsDeductAmount(order.getPointsDeductAmount());
|
|||
|
|
vo.setStatus(order.getStatus());
|
|||
|
|
vo.setStatusLabel(switch (order.getStatus()) {
|
|||
|
|
case "pending" -> "待支付";
|
|||
|
|
case "paid" -> "已支付";
|
|||
|
|
case "completed" -> "已完成";
|
|||
|
|
case "cancelled" -> "已取消";
|
|||
|
|
case "refunding" -> "退款中";
|
|||
|
|
case "refunded" -> "已退款";
|
|||
|
|
default -> order.getStatus();
|
|||
|
|
});
|
|||
|
|
vo.setPaymentMethod(order.getPaymentMethod());
|
|||
|
|
vo.setCreatedAt(order.getCreatedAt());
|
|||
|
|
vo.setPaidAt(order.getPaidAt());
|
|||
|
|
vo.setItems(itemRepo.findByOrderId(order.getId()).stream().map(i -> {
|
|||
|
|
OrderItemVO iv = new OrderItemVO();
|
|||
|
|
iv.setSkillId(i.getSkillId());
|
|||
|
|
iv.setSkillName(i.getSkillName());
|
|||
|
|
iv.setSkillCover(i.getSkillCover());
|
|||
|
|
iv.setUnitPrice(i.getUnitPrice());
|
|||
|
|
iv.setQuantity(i.getQuantity());
|
|||
|
|
iv.setTotalPrice(i.getTotalPrice());
|
|||
|
|
return iv;
|
|||
|
|
}).toList());
|
|||
|
|
return vo;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 五、Controller
|
|||
|
|
|
|||
|
|
### OrderController.java
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
package com.openclaw.controller;
|
|||
|
|
|
|||
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|||
|
|
import com.openclaw.common.Result;
|
|||
|
|
import com.openclaw.dto.*;
|
|||
|
|
import com.openclaw.service.OrderService;
|
|||
|
|
import com.openclaw.util.UserContext;
|
|||
|
|
import com.openclaw.vo.*;
|
|||
|
|
import jakarta.validation.Valid;
|
|||
|
|
import lombok.RequiredArgsConstructor;
|
|||
|
|
import org.springframework.web.bind.annotation.*;
|
|||
|
|
|
|||
|
|
@RestController
|
|||
|
|
@RequestMapping("/api/v1/orders")
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class OrderController {
|
|||
|
|
|
|||
|
|
private final OrderService orderService;
|
|||
|
|
|
|||
|
|
/** 创建订单 */
|
|||
|
|
@PostMapping
|
|||
|
|
public Result<OrderVO> createOrder(@Valid @RequestBody OrderCreateDTO dto) {
|
|||
|
|
return Result.ok(orderService.createOrder(UserContext.getUserId(), dto));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 订单详情 */
|
|||
|
|
@GetMapping("/{id}")
|
|||
|
|
public Result<OrderVO> getDetail(@PathVariable Long id) {
|
|||
|
|
return Result.ok(orderService.getOrderDetail(id, UserContext.getUserId()));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 订单列表 */
|
|||
|
|
@GetMapping
|
|||
|
|
public Result<IPage<OrderVO>> listOrders(
|
|||
|
|
@RequestParam(required = false) String status,
|
|||
|
|
@RequestParam(defaultValue = "1") int pageNum,
|
|||
|
|
@RequestParam(defaultValue = "10") int pageSize) {
|
|||
|
|
return Result.ok(orderService.listOrders(UserContext.getUserId(), status, pageNum, pageSize));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 取消订单 */
|
|||
|
|
@PutMapping("/{id}/cancel")
|
|||
|
|
public Result<Void> cancelOrder(
|
|||
|
|
@PathVariable Long id,
|
|||
|
|
@RequestParam(required = false) String reason) {
|
|||
|
|
orderService.cancelOrder(id, UserContext.getUserId(), reason);
|
|||
|
|
return Result.ok();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 申请退款 */
|
|||
|
|
@PostMapping("/{id}/refund")
|
|||
|
|
public Result<Void> applyRefund(
|
|||
|
|
@PathVariable Long id,
|
|||
|
|
@Valid @RequestBody RefundApplyDTO dto) {
|
|||
|
|
orderService.applyRefund(id, UserContext.getUserId(), dto);
|
|||
|
|
return Result.ok();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
**文档版本**:v1.0 | **创建日期**:2026-03-16
|