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
|