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

11 KiB
Raw Blame History

订单服务开发文档 - Part 2Service实现 + Controller

四、Service 实现

OrderServiceImpl.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

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