Files
number/后端架构设计/07-订单服务开发文档-part2.md

289 lines
11 KiB
Markdown
Raw Normal View History

2026-03-17 12:09:43 +08:00
# 订单服务开发文档 - 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