添加订单相关能力

This commit is contained in:
zk
2026-05-14 20:45:03 +08:00
parent bc9694fbec
commit 9312c07f21
21 changed files with 877 additions and 0 deletions
@@ -0,0 +1,49 @@
package org.jiayunet.controller;
import lombok.AllArgsConstructor;
import org.jiayunet.pojo.dto.memberProduct.CreateOrderDto;
import org.jiayunet.pojo.dto.memberProduct.OrderDetailDto;
import org.jiayunet.pojo.param.memberProduct.CreateOrderParam;
import org.jiayunet.pojo.po.MemberProduct;
import org.jiayunet.service.MemberProductService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 会员商品接口
*
* @author zk
*/
@RestController
@RequestMapping("/member/product")
@AllArgsConstructor
public class MemberProductController {
private final MemberProductService memberProductService;
/**
* 商品列表
*/
@GetMapping("/list")
public List<MemberProduct> list() {
return memberProductService.list();
}
/**
* 创建订单
*/
@PostMapping("/createOrder")
public CreateOrderDto createOrder(@Validated @RequestBody CreateOrderParam param) {
return memberProductService.createOrder(param.getProductId(), param.getPayChannel());
}
/**
* 查询订单详情
*/
@GetMapping("/orderDetail")
public OrderDetailDto orderDetail(@RequestParam Long orderId) {
return memberProductService.orderDetail(orderId);
}
}
@@ -0,0 +1,39 @@
package org.jiayunet.controller;
import lombok.AllArgsConstructor;
import org.jiayunet.pojo.dto.memberStatus.FuncStockDto;
import org.jiayunet.pojo.dto.memberStatus.MemberStatusDto;
import org.jiayunet.service.MemberStatusService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 会员状态接口
*
* @author zk
*/
@RestController
@RequestMapping("/member")
@AllArgsConstructor
public class MemberStatusController {
private final MemberStatusService memberStatusService;
/**
* 查询会员状态
*/
@GetMapping("/status")
public MemberStatusDto status() {
return memberStatusService.status();
}
/**
* 查询功能权限库存
*/
@GetMapping("/funcStock")
public FuncStockDto funcStock(@RequestParam String funcCode) {
return memberStatusService.funcStock(funcCode);
}
}
@@ -0,0 +1,18 @@
package org.jiayunet.pojo.dto.memberProduct;
import lombok.Data;
/**
* 创建订单返回
*
* @author zk
*/
@Data
public class CreateOrderDto {
/** 订单ID,用于轮询状态 */
private Long orderId;
/** 微信支付二维码链接 */
private String codeUrl;
}
@@ -0,0 +1,38 @@
package org.jiayunet.pojo.dto.memberProduct;
import lombok.Data;
import java.time.Instant;
/**
* 订单详情返回
*
* @author zk
*/
@Data
public class OrderDetailDto {
/** 订单ID */
private Long orderId;
/** 订单编号 */
private String orderNo;
/** 商品名称 */
private String productName;
/** 实付金额(分) */
private Integer payAmount;
/** 订单状态 0=待支付 1=已支付 2=已退款 3=已关闭 */
private Integer status;
/** 支付渠道 1=微信 2=支付宝 */
private Integer payChannel;
/** 支付时间 */
private Instant payTime;
/** 下单时间 */
private Instant createTime;
}
@@ -0,0 +1,41 @@
package org.jiayunet.pojo.dto.memberStatus;
import lombok.Data;
import java.time.Instant;
/**
* 功能权限库存返回
*
* @author zk
*/
@Data
public class FuncStockDto {
/** 功能编码 */
private String funcCode;
/** 功能名称 */
private String funcName;
/** 每日免费次数 */
private Integer dailyFreeCount;
/** 今日已用次数 */
private Integer todayUsed;
/** 今日免费剩余次数 */
private Integer freeRemain;
/** 0=不限次 1=限次 */
private Integer countLimit;
/** 付费剩余次数,countLimit=0时为null */
private Integer remainCount;
/** 0=不限时 1=限时 */
private Integer timeLimit;
/** 权限过期时间,timeLimit=0时为null */
private Instant expireTime;
}
@@ -0,0 +1,26 @@
package org.jiayunet.pojo.dto.memberStatus;
import lombok.Data;
import java.time.Instant;
/**
* 会员状态返回
*
* @author zk
*/
@Data
public class MemberStatusDto {
/** 是否是会员 */
private Boolean isMember;
/** 到期时间 */
private Instant expireTime;
/** 首次开通时间 */
private Instant createTime;
/** 最近续费时间 */
private Instant updateTime;
}
@@ -0,0 +1,22 @@
package org.jiayunet.pojo.param.memberProduct;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 创建会员订单入参
*
* @author zk
*/
@Data
public class CreateOrderParam {
/** 商品ID */
@NotNull(message = "商品ID不能为空")
private Long productId;
/** 支付渠道 1=微信 2=支付宝 */
@NotNull(message = "支付渠道不能为空")
private Integer payChannel;
}
@@ -0,0 +1,176 @@
package org.jiayunet.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.wechat.pay.java.service.payments.nativepay.model.Amount;
import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.mapper.MemberOrderMapper;
import org.jiayunet.mapper.MemberProductMapper;
import org.jiayunet.mapper.PayWechatFlowMapper;
import org.jiayunet.pojo.dto.memberProduct.CreateOrderDto;
import org.jiayunet.pojo.dto.memberProduct.OrderDetailDto;
import org.jiayunet.pojo.po.MemberOrder;
import org.jiayunet.pojo.po.MemberProduct;
import org.jiayunet.pojo.po.PayWechatFlow;
import org.jiayunet.tool.UserSecurityTool;
import org.jiayunet.wxPay.WxNativePayAbility;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import java.util.List;
/**
* 会员商品服务
* <p>依赖:WxNativePayAbility(微信Native支付下单)</p>
* <p>使用表:bg_member_product(查询商品)、bg_member_order(创建/查询订单)、bg_pay_wechat_flow(创建支付流水)</p>
*
* @author zk
*/
@Service
@Slf4j
public class MemberProductService {
@Autowired
private MemberProductMapper memberProductMapper;
@Autowired
private MemberOrderMapper memberOrderMapper;
@Autowired
private PayWechatFlowMapper payWechatFlowMapper;
@Autowired
private WxNativePayAbility wxNativePayAbility;
@Value("${wx_pay.merchant_id:}")
private String merchantId;
@Value("${app.wx.app_id:}")
private String appId;
/**
* 商品列表
* <p>查询上架商品,按sort_order排序</p>
*/
public List<MemberProduct> list() {
return memberProductMapper.selectList(
new LambdaQueryWrapper<MemberProduct>()
.eq(MemberProduct::getStatus, 1)
.orderByAsc(MemberProduct::getSortOrder)
);
}
/**
* 创建订单
* <p>1. 校验商品存在且上架 2. 创建订单 3. 生成orderNo 4. 创建支付流水 5. 调支付渠道下单拿codeUrl</p>
*/
@Transactional(rollbackFor = Exception.class)
public CreateOrderDto createOrder(Long productId, Integer payChannel) {
Long userId = UserSecurityTool.getUserId();
// 1. 查商品,校验存在且上架
MemberProduct product = memberProductMapper.selectById(productId);
if (product == null || product.getStatus() != 1) {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "商品不存在或已下架");
}
// 2. 创建订单
MemberOrder order = new MemberOrder();
order.setUserId(userId);
order.setProductId(productId);
order.setPayAmount(product.getPrice());
order.setPayChannel(payChannel);
order.setStatus(0);
memberOrderMapper.insert(order);
// 3. 生成orderNo
String orderNo = "OP" + order.getId();
order.setOrderNo(orderNo);
memberOrderMapper.update(null, new LambdaUpdateWrapper<MemberOrder>()
.eq(MemberOrder::getId, order.getId())
.set(MemberOrder::getOrderNo, orderNo));
// 4. 调支付渠道下单,拿二维码URL
String codeUrl;
if (payChannel == 1) {
// 微信Native支付
codeUrl = prepayWechat(orderNo, product);
} else {
// TODO 支付宝当面付
codeUrl = prepayAlipay(orderNo, product);
}
// 5. 返回
CreateOrderDto dto = new CreateOrderDto();
dto.setOrderId(order.getId());
dto.setCodeUrl(codeUrl);
return dto;
}
/**
* 微信Native支付下单
*/
private String prepayWechat(String orderNo, MemberProduct product) {
// 创建微信支付流水
PayWechatFlow flow = new PayWechatFlow();
flow.setOrderType("member");
flow.setOrderNo(orderNo);
flow.setTotal(product.getPrice());
flow.setStatus(0);
payWechatFlowMapper.insert(flow);
// 调微信Native下单
PrepayRequest request = new PrepayRequest();
request.setAppid(appId);
request.setMchid(merchantId);
request.setDescription(product.getProductName());
request.setOutTradeNo(orderNo);
Amount amount = new Amount();
amount.setTotal(product.getPrice());
request.setAmount(amount);
return wxNativePayAbility.prepay(request);
}
/**
* 支付宝当面付下单
* TODO 接入支付宝当面付SDK,返回二维码URL
*/
private String prepayAlipay(String orderNo, MemberProduct product) {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "支付宝支付暂未开放");
}
/**
* 查询订单详情
* <p>查询订单信息,关联商品名称</p>
*/
public OrderDetailDto orderDetail(Long orderId) {
Long userId = UserSecurityTool.getUserId();
MemberOrder order = memberOrderMapper.selectOne(
new LambdaQueryWrapper<MemberOrder>()
.eq(MemberOrder::getId, orderId)
.eq(MemberOrder::getUserId, userId)
);
Assert.notNull(order, "订单不存在");
MemberProduct product = memberProductMapper.selectById(order.getProductId());
OrderDetailDto dto = new OrderDetailDto();
dto.setOrderId(order.getId());
dto.setOrderNo(order.getOrderNo());
dto.setProductName(product != null ? product.getProductName() : null);
dto.setPayAmount(order.getPayAmount());
dto.setStatus(order.getStatus());
dto.setPayChannel(order.getPayChannel());
dto.setPayTime(order.getPayTime());
dto.setCreateTime(order.getCreateTime());
return dto;
}
}
@@ -0,0 +1,131 @@
package org.jiayunet.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.jiayunet.exception.BusinessException;
import org.jiayunet.exception.BusinessExpCodeEnum;
import org.jiayunet.mapper.FuncPermissionMapper;
import org.jiayunet.mapper.MemberUserMapper;
import org.jiayunet.mapper.UserFuncPermissionStockMapper;
import org.jiayunet.mapper.UserFuncUsageLogMapper;
import org.jiayunet.pojo.dto.memberStatus.FuncStockDto;
import org.jiayunet.pojo.dto.memberStatus.MemberStatusDto;
import org.jiayunet.pojo.po.FuncPermission;
import org.jiayunet.pojo.po.MemberUser;
import org.jiayunet.pojo.po.UserFuncPermissionStock;
import org.jiayunet.pojo.po.UserFuncUsageLog;
import org.jiayunet.tool.UserSecurityTool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
/**
* 会员状态查询服务
* <p>依赖:FuncPermissionMapper、UserFuncPermissionStockMapper、UserFuncUsageLogMapper、MemberUserMapper</p>
* <p>使用表:bg_member_user(查询会员状态)、bg_func_permission(查询功能定义)、bg_user_func_permission_stock(查询库存)、bg_user_func_usage_log(统计今日使用)</p>
*
* @author zk
*/
@Service
@Slf4j
public class MemberStatusService {
@Autowired
private MemberUserMapper memberUserMapper;
@Autowired
private FuncPermissionMapper funcPermissionMapper;
@Autowired
private UserFuncPermissionStockMapper userFuncPermissionStockMapper;
@Autowired
private UserFuncUsageLogMapper userFuncUsageLogMapper;
/**
* 查询会员状态
* <p>查询当前用户的会员记录,判断是否在有效期内</p>
*/
public MemberStatusDto status() {
Long userId = UserSecurityTool.getUserId();
MemberUser memberUser = memberUserMapper.selectOne(
new LambdaQueryWrapper<MemberUser>().eq(MemberUser::getUserId, userId)
);
MemberStatusDto dto = new MemberStatusDto();
if (memberUser == null) {
dto.setIsMember(false);
return dto;
}
dto.setIsMember(memberUser.getExpireTime().isAfter(Instant.now()));
dto.setExpireTime(memberUser.getExpireTime());
dto.setCreateTime(memberUser.getCreateTime());
dto.setUpdateTime(memberUser.getUpdateTime());
return dto;
}
/**
* 查询功能权限库存
* <p>1. 查功能定义 2. 统计今日已用次数 3. 查付费库存 4. 组装返回</p>
*/
public FuncStockDto funcStock(String funcCode) {
Long userId = UserSecurityTool.getUserId();
// 1. 查功能定义
FuncPermission funcPermission = funcPermissionMapper.selectOne(
new LambdaQueryWrapper<FuncPermission>()
.eq(FuncPermission::getFuncCode, funcCode)
.eq(FuncPermission::getStatus, 1)
);
if (funcPermission == null) {
throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "功能不存在或未启用");
}
// 2. 统计今日已用次数
Instant todayStart = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant();
Long todayUsed = userFuncUsageLogMapper.selectCount(
new LambdaQueryWrapper<UserFuncUsageLog>()
.eq(UserFuncUsageLog::getUserId, userId)
.eq(UserFuncUsageLog::getFuncCode, funcCode)
.ge(UserFuncUsageLog::getCreateTime, todayStart)
);
// 3. 查付费库存
UserFuncPermissionStock stock = userFuncPermissionStockMapper.selectOne(
new LambdaQueryWrapper<UserFuncPermissionStock>()
.eq(UserFuncPermissionStock::getUserId, userId)
.eq(UserFuncPermissionStock::getFuncCode, funcCode)
);
// 4. 组装返回
int dailyFreeCount = funcPermission.getDailyFreeCount() == null ? 0 : funcPermission.getDailyFreeCount();
int used = todayUsed.intValue();
int freeRemain = Math.max(0, dailyFreeCount - used);
FuncStockDto dto = new FuncStockDto();
dto.setFuncCode(funcCode);
dto.setFuncName(funcPermission.getFuncName());
dto.setDailyFreeCount(dailyFreeCount);
dto.setTodayUsed(used);
dto.setFreeRemain(freeRemain);
if (stock != null) {
dto.setCountLimit(stock.getCountLimit());
dto.setRemainCount(stock.getRemainCount());
dto.setTimeLimit(stock.getTimeLimit());
dto.setExpireTime(stock.getExpireTime());
} else {
dto.setCountLimit(null);
dto.setRemainCount(null);
dto.setTimeLimit(null);
dto.setExpireTime(null);
}
return dto;
}
}