Files
number/后端架构设计/07-订单服务开发文档-part2.md
2026-03-17 12:09:43 +08:00

289 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 订单服务开发文档 - Part 2Service实现 + 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