添加订单相关能力

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;
}
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.MemberFuncItem;
/**
* 会员-功能权限配置 Mapper
*
* @author zk
*/
@Mapper
public interface MemberFuncItemMapper extends CommonMapper<MemberFuncItem> {
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.MemberOrder;
/**
* 会员订单 Mapper
*
* @author zk
*/
@Mapper
public interface MemberOrderMapper extends CommonMapper<MemberOrder> {
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.MemberProduct;
/**
* 会员商品 Mapper
*
* @author zk
*/
@Mapper
public interface MemberProductMapper extends CommonMapper<MemberProduct> {
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.MemberRouteItem;
/**
* 会员-菜单权限配置 Mapper
*
* @author zk
*/
@Mapper
public interface MemberRouteItemMapper extends CommonMapper<MemberRouteItem> {
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.MemberUser;
/**
* 用户会员状态 Mapper
*
* @author zk
*/
@Mapper
public interface MemberUserMapper extends CommonMapper<MemberUser> {
}
@@ -0,0 +1,14 @@
package org.jiayunet.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.jiayunet.pojo.po.PayWechatFlow;
/**
* 微信支付流水 Mapper
*
* @author zk
*/
@Mapper
public interface PayWechatFlowMapper extends CommonMapper<PayWechatFlow> {
}
@@ -0,0 +1,29 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 会员-功能权限配置表(bg_member_func_item
* <p>全局配置,定义会员包含哪些功能权限及次数限制</p>
*
* @author zk
*/
@Data
@TableName(value = "bg_member_func_item")
public class MemberFuncItem {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 功能权限编码,关联 bg_func_permission.func_code */
private String funcCode;
/** 0=不限次 1=限次 */
private Integer countLimit;
/** 发放次数,countLimit=1时有值 */
private Integer addCount;
}
@@ -0,0 +1,55 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.Instant;
/**
* 会员订单表(bg_member_order
* <p>记录用户购买会员商品的订单信息</p>
*
* @author zk
*/
@Data
@TableName(value = "bg_member_order")
public class MemberOrder {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 订单编号,"OP" + id */
private String orderNo;
/** 下单用户 */
private Long userId;
/** 关联 bg_member_product.id */
private Long productId;
/** 实付金额(分) */
private Integer payAmount;
/** 支付渠道 1=微信 2=支付宝 */
private Integer payChannel;
/** 订单状态 0=待支付 1=已支付 2=已退款 3=已关闭 */
private Integer status;
/** 支付时间 */
private Instant payTime;
/** 退款时间(预留) */
private Instant refundTime;
/** 退款金额(分,预留) */
private Integer refundAmount;
/** 下单时间 */
private Instant createTime;
/** 更新时间 */
private Instant updateTime;
}
@@ -0,0 +1,63 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.Instant;
/**
* 会员商品表(bg_member_product
* <p>定义会员套餐的名称、价格、时长等商业属性</p>
*
* @author zk
*/
@Data
@TableName(value = "bg_member_product")
public class MemberProduct {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 商品名称 */
private String productName;
/** 标签,如"限时优惠"、"最划算" */
private String tag;
/** 主推标识 0=否 1=是 */
private Integer isFeatured;
/** 购买按钮文字,如"立即开通" */
private String buyButtonText;
/** 实付价格(分) */
private Integer price;
/** 划线价(分) */
private Integer originalPrice;
/** 折算月价(分) */
private Integer monthlyPrice;
/** 有效天数 */
private Integer durationDays;
/** 排序,越小越靠前 */
private Integer sortOrder;
/** 状态 0=下架 1=上架 */
private Integer status;
/** 创建时间 */
private Instant createTime;
/** 更新时间 */
private Instant updateTime;
/** 逻辑删除 0=正常 非0=已删除 */
@TableLogic(value = "0", delval = "new()")
private Long isDelete;
}
@@ -0,0 +1,23 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 会员-菜单权限配置表(bg_member_route_item
* <p>全局配置,定义会员解锁哪些菜单</p>
*
* @author zk
*/
@Data
@TableName(value = "bg_member_route_item")
public class MemberRouteItem {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 路由菜单ID,关联 bg_route_menu.id */
private Long routeId;
}
@@ -0,0 +1,34 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.Instant;
/**
* 用户会员状态表(bg_member_user
* <p>记录用户的会员到期时间,用于展示会员状态</p>
*
* @author zk
*/
@Data
@TableName(value = "bg_member_user")
public class MemberUser {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 用户ID */
private Long userId;
/** 会员到期时间 */
private Instant expireTime;
/** 首次开通时间 */
private Instant createTime;
/** 最近续费时间 */
private Instant updateTime;
}
@@ -0,0 +1,49 @@
package org.jiayunet.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.Instant;
/**
* 微信支付流水表(bg_pay_wechat_flow
* <p>记录微信支付的下单和回调信息</p>
*
* @author zk
*/
@Data
@TableName(value = "bg_pay_wechat_flow")
public class PayWechatFlow {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/** 订单类型,member=会员订单,后续可扩展其他业务类型 */
private String orderType;
/** 商户订单号,对应微信 out_trade_no,关联业务订单表 */
private String orderNo;
/** 微信支付订单号,支付成功后由回调返回 */
private String transactionId;
/** 订单金额(分),对应微信 amount.total */
private Integer total;
/** 流水状态 0=待支付 1=已支付 2=已关闭 */
private Integer status;
/** 支付成功时间,系统收到成功回调的时间 */
private Instant successTime;
/** 回调解密后的原始JSON完整数据,用于问题排查 */
private String notifyData;
/** 记录创建时间(下单时写入) */
private Instant createTime;
/** 记录更新时间(回调时更新) */
private Instant updateTime;
}