添加订单和支付网关逻辑

This commit is contained in:
dqz 2025-03-11 08:59:40 +08:00
parent cc7280f375
commit c566f987f5
29 changed files with 1244 additions and 9 deletions

View File

@ -1,25 +1,33 @@
package com.agileboot.api.controller; package com.agileboot.api.controller;
import com.agileboot.common.core.base.BaseController; import com.agileboot.common.core.base.BaseController;
import com.agileboot.domain.shop.order.dto.CreateOrderResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.domain.shop.order.OrderApplicationService;
import com.agileboot.domain.shop.order.command.SubmitOrderCommand;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/** /**
* 调度日志操作处理 * 调度日志操作处理
* *
* @author valarchie * @author valarchie
*/ */
@RequiredArgsConstructor
@RestController @RestController
@RequestMapping("/api/order") @RequestMapping("/api/order")
public class OrderController extends BaseController { public class OrderController extends BaseController {
/** private final OrderApplicationService orderApplicationService;
* 访问首页提示语
*/
@RequestMapping("/")
public String index() {
return "暂无订单";
}
// 新增提交订单接口
@PostMapping("/submit")
public ResponseDTO<CreateOrderResult> submitOrder(@Validated @RequestBody SubmitOrderCommand command) {
CreateOrderResult result = orderApplicationService.createOrder(command);
return ResponseDTO.ok(result);
}
} }

View File

@ -0,0 +1,87 @@
package com.agileboot.api.controller;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.net.URLDecoder;
import cn.hutool.json.JSONUtil;
import com.agileboot.common.core.dto.ResponseDTO;
import com.agileboot.common.utils.OpenSignUtil;
import com.agileboot.domain.shop.payment.dto.PaymentCallbackRequest;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/payment")
public class PaymentController {
// 新增回调接口
@PostMapping("/callback")
public String paymentCallback(HttpServletRequest request, @RequestBody String requestBody) {
try {
// 1. 参数解析
PaymentCallbackRequest callbackReq = parseCallbackRequest(requestBody);
PaymentCallbackRequest.BizContent bizContent = parseBizContent(callbackReq.getBiz_content());
// 2. 签名验证需要根据biz_id获取对应的appKey
String appKey = getAppKeyByBizId(callbackReq.getBiz_id()); // 需要实现根据biz_id获取appKey的逻辑
boolean signValid = OpenSignUtil.checkOpenSign(appKey, callbackReq.getSign(), requestBody);
if (!signValid) {
log.error("签名验证失败:{}", requestBody);
return "fail";
}
// 3. 业务处理需要实现幂等性校验
handlePaymentSuccess(
bizContent.getBiz_order_id(),
bizContent.getTotal_amount(),
bizContent.getTrade_id()
);
return "success";
} catch (Exception e) {
log.error("支付回调处理失败", e);
return "fail";
}
}
private PaymentCallbackRequest parseCallbackRequest(String requestBody) {
// 实现将URL参数解析为PaymentCallbackRequest
// 示例实现需要根据实际参数格式调整
Map<String, String> paramMap = new HashMap<>();
for (String param : requestBody.split("&")) {
String[] pair = param.split("=");
if (pair.length == 2) {
paramMap.put(pair[0], URLDecoder.decode(pair[1], StandardCharsets.UTF_8));
}
}
return BeanUtil.toBean(paramMap, PaymentCallbackRequest.class);
}
private PaymentCallbackRequest.BizContent parseBizContent(String bizContent) {
// 实现biz_content的JSON解析
return JSONUtil.toBean(bizContent, PaymentCallbackRequest.BizContent.class);
}
// 需要实现的方法根据业务需求补充
private String getAppKeyByBizId(String bizId) {
// 根据biz_id从数据库或配置获取对应的appKey
return "wxshop202503081132";
}
private void handlePaymentSuccess(String bizOrderId, Integer amount, String tradeId) {
// 实现订单状态更新和幂等性校验
}
}

View File

@ -0,0 +1,63 @@
package com.agileboot.common.utils;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.stream.Collectors;
import static cn.hutool.crypto.SecureUtil.md5;
@Slf4j
public class OpenSignUtil {
/**
* @param appKey 支付网关提供的业务秘钥与biz_id是配对一起提供的
* @param sign 支付网关回调参数里的sign字段值
* @param reqBody 支付网关回调的原始请求body字符串
*/
public static boolean checkOpenSign(String appKey, String sign, String reqBody) {
String[] fields = reqBody.split("&");
Map<String, String> fieldMap = new HashMap<>();
for (String field : fields) {
String[] pair = field.split("=");
if (pair.length != 2) {
continue;
}
// sign字段不参与校验空字段不校验
if (!"sign".equals(pair[0]) && StringUtils.isNotBlank(pair[1])) {
fieldMap.put(pair[0], pair[1]);
}
}
String generateSign = openSign(appKey, fieldMap);
return Objects.equals(generateSign, sign);
}
private static List<String> getKVList(Map<String, String> sortedParams) {
return sortedParams.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.toList());
}
public static String openSign(String appKey, Map<String, String> params) {
if(MapUtils.isEmpty(params)) {
params = new HashMap<String, String>(1);
}
Map<String, String> sortedParams = new TreeMap<>(params);
List<String> kvPairList = getKVList(sortedParams);
String sourceText = StringUtils.join(kvPairList, "&");
sourceText = sourceText + "&app_key=" + appKey;
log.info("sourceText:{}", sourceText);
try {
return md5(Arrays.toString(sourceText.getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
return null;
}
}
}

View File

@ -0,0 +1,156 @@
package com.agileboot.domain.shop.order;
import cn.hutool.core.bean.BeanUtil;
import com.agileboot.common.core.page.PageDTO;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.common.command.BulkOperationCommand;
import com.agileboot.domain.shop.goods.db.ShopGoodsEntity;
import com.agileboot.domain.shop.goods.db.ShopGoodsService;
import com.agileboot.domain.shop.order.command.SubmitOrderCommand;
import com.agileboot.domain.shop.order.db.ShopOrderEntity;
import com.agileboot.domain.shop.order.db.ShopOrderService;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsService;
import com.agileboot.domain.shop.order.dto.CreateOrderResult;
import com.agileboot.domain.shop.order.dto.ShopOrderDTO;
import com.agileboot.domain.shop.order.model.OrderModel;
import com.agileboot.domain.shop.order.model.OrderModelFactory;
import com.agileboot.domain.shop.order.model.OrderGoodsModel;
import com.agileboot.domain.shop.order.model.OrderGoodsModelFactory;
import com.agileboot.domain.shop.order.query.SearchShopOrderQuery;
import com.agileboot.domain.shop.payment.PaymentApplicationService;
import com.agileboot.domain.shop.payment.dto.WxJsApiPreCreateRequest;
import com.agileboot.domain.shop.payment.dto.WxJsApiPreCreateResponse;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class OrderApplicationService {
private final ShopOrderService orderService;
private final ShopOrderGoodsService orderGoodsService;
private final ShopGoodsService goodsService;
private final OrderModelFactory orderModelFactory;
private final OrderGoodsModelFactory orderGoodsModelFactory;
private final PaymentApplicationService paymentApplicationService;
/*public PageDTO<ShopOrderDTO> getOrderList(SearchShopOrderQuery<> query) {
Page<ShopOrderEntity> page = orderService.page(query.toPage(), query.toQueryWrapper());
List<ShopOrderDTO> dtoList = page.getRecords().stream().map(ShopOrderDTO::new).collect(Collectors.toList());
return new PageDTO<>(dtoList, page.getTotal());
}*/
@Transactional
public CreateOrderResult createOrder(SubmitOrderCommand command) {
ShopOrderEntity order = command.getOrder();
List<ShopOrderGoodsEntity> goodsList = command.getGoodsList();
OrderModel orderModel = orderModelFactory.create();
BeanUtil.copyProperties(order, orderModel);
orderModel.generateOrderNumber();
orderModel.insert();
processOrderGoods(orderModel, goodsList);
// 新增支付接口调用
WxJsApiPreCreateRequest paymentRequest = buildPaymentRequest(orderModel);
WxJsApiPreCreateResponse paymentResponse = paymentApplicationService.callJsApiPreCreate(paymentRequest);
return new CreateOrderResult(orderModel.getOrderId(), paymentResponse);
}
private WxJsApiPreCreateRequest buildPaymentRequest(OrderModel orderModel) {
WxJsApiPreCreateRequest request = new WxJsApiPreCreateRequest();
request.setIp("222.218.10.217");
request.setOpenid(orderModel.getOpenid()); //
request.setBiz_order_id(String.valueOf(orderModel.getOrderId())); // 使用订单唯一编号
// 金额转换元转分建议增加精度处理
request.setPay_amount(orderModel.getTotalAmount().toPlainString());
request.setTitle("商品订单支付");
request.setNotify_url("http://wxshop.ab98.cn/shop-back-end/api/payment/callback");
request.setBiz_id("wxshop");
request.setUcid(orderModel.getUcid());
request.setExtra("");
return request;
}
private void processOrderGoods(OrderModel orderModel, List<ShopOrderGoodsEntity> orderGoodsList) {
BigDecimal totalAmount = BigDecimal.ZERO;
for (ShopOrderGoodsEntity goods : orderGoodsList) {
OrderGoodsModel goodsModel = orderGoodsModelFactory.create(goods);
// 设置订单ID关联
goodsModel.setOrderId(orderModel.getOrderId());
// 计算商品金额并验证库存
goodsModel.calculateTotal();
goodsModel.validateQuantity();
// 保存订单商品
goodsModel.insert();
// 扣减库存
deductGoodsStock(goodsModel.getGoodsId(), goodsModel.getQuantity());
totalAmount = totalAmount.add(goodsModel.getTotalAmount());
}
// 更新订单总金额
orderModel.setTotalAmount(totalAmount);
orderModel.updateById();
}
private void deductGoodsStock(Long goodsId, Integer quantity) {
ShopGoodsEntity goods = goodsService.getById(goodsId);
if (goods == null || goods.getStock() < quantity) {
throw new ApiException(ErrorCode.FAILED, "商品库存不足");
}
goods.setStock(goods.getStock() - quantity);
goodsService.updateById(goods);
}
@Transactional
public void cancelOrder(Long orderId) {
OrderModel orderModel = orderModelFactory.loadById(orderId);
// 状态校验
orderModel.validateStatusTransition(5); // 5代表已取消
// 恢复库存
List<ShopOrderGoodsEntity> goodsList = orderGoodsService.lambdaQuery()
.eq(ShopOrderGoodsEntity::getOrderId, orderId)
.list();
goodsList.forEach(goods -> restoreGoodsStock(goods.getGoodsId(), goods.getQuantity()));
// 更新订单状态
orderModel.setStatus(5);
orderModel.updateById();
}
private void restoreGoodsStock(Long goodsId, Integer quantity) {
ShopGoodsEntity goods = goodsService.getById(goodsId);
if (goods != null) {
goods.setStock(goods.getStock() + quantity);
goodsService.updateById(goods);
}
}
public void deleteOrders(BulkOperationCommand<Long> command) {
for (Long orderId : command.getIds()) {
OrderModel model = orderModelFactory.loadById(orderId);
model.deleteById();
}
}
}

View File

@ -0,0 +1,13 @@
package com.agileboot.domain.shop.order.command;
import com.agileboot.domain.shop.order.db.ShopOrderEntity;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity;
import java.util.List;
import lombok.Data;
@Data
public class SubmitOrderCommand {
private String openid;
private ShopOrderEntity order;
private List<ShopOrderGoodsEntity> goodsList;
}

View File

@ -0,0 +1,74 @@
package com.agileboot.domain.shop.order.db;
import com.agileboot.common.core.base.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 商品订单表
* </p>
*
* @author valarchie
* @since 2025-03-10
*/
@Getter
@Setter
@TableName("shop_order")
@ApiModel(value = "ShopOrderEntity对象", description = "商品订单表")
public class ShopOrderEntity extends BaseEntity<ShopOrderEntity> {
private static final long serialVersionUID = 1L;
@ApiModelProperty("订单唯一ID")
@TableId(value = "order_id", type = IdType.AUTO)
private Long orderId;
@ApiModelProperty("ucid")
@TableField("ucid")
private String ucid;
@ApiModelProperty("openid")
@TableField("openid")
private String openid;
@ApiModelProperty("支付网关交易id")
@TableField("trade_id")
private String tradeId;
@ApiModelProperty("订单总金额")
@TableField("total_amount")
private BigDecimal totalAmount;
@ApiModelProperty("订单状态1待付款 2已付款 3已发货 4已完成 5已取消")
@TableField("`status`")
private Integer status;
@ApiModelProperty("支付状态1未支付 2已支付 3退款中 4已退款")
@TableField("pay_status")
private Integer payStatus;
@ApiModelProperty("支付方式")
@TableField("payment_method")
private String paymentMethod;
@ApiModelProperty("支付时间")
@TableField("pay_time")
private Date payTime;
@Override
public Serializable pkVal() {
return this.orderId;
}
}

View File

@ -0,0 +1,65 @@
package com.agileboot.domain.shop.order.db;
import com.agileboot.common.core.base.BaseEntity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.math.BigDecimal;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
* 订单商品明细表
* </p>
*
* @author valarchie
* @since 2025-03-10
*/
@Getter
@Setter
@TableName("shop_order_goods")
@ApiModel(value = "ShopOrderGoodsEntity对象", description = "订单商品明细表")
public class ShopOrderGoodsEntity extends BaseEntity<ShopOrderGoodsEntity> {
private static final long serialVersionUID = 1L;
@ApiModelProperty("订单商品唯一ID")
@TableId(value = "order_goods_id", type = IdType.AUTO)
private Long orderGoodsId;
@ApiModelProperty("关联订单ID")
@TableField("order_id")
private Long orderId;
@ApiModelProperty("关联商品ID")
@TableField("goods_id")
private Long goodsId;
@ApiModelProperty("购买数量")
@TableField("quantity")
private Integer quantity;
@ApiModelProperty("购买时单价")
@TableField("price")
private BigDecimal price;
@ApiModelProperty("商品总金额")
@TableField("total_amount")
private BigDecimal totalAmount;
@ApiModelProperty("商品状态1正常 2已退货 3已换货")
@TableField("`status`")
private Integer status;
@Override
public Serializable pkVal() {
return this.orderGoodsId;
}
}

View File

@ -0,0 +1,15 @@
package com.agileboot.domain.shop.order.db;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 订单商品明细表 Mapper 接口
* </p>
*
* @author valarchie
* @since 2025-03-10
*/
public interface ShopOrderGoodsMapper extends BaseMapper<ShopOrderGoodsEntity> {
}

View File

@ -0,0 +1,15 @@
package com.agileboot.domain.shop.order.db;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 订单商品明细表 服务类
* </p>
*
* @author valarchie
* @since 2025-03-10
*/
public interface ShopOrderGoodsService extends IService<ShopOrderGoodsEntity> {
}

View File

@ -0,0 +1,17 @@
package com.agileboot.domain.shop.order.db;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 订单商品明细表 服务实现类
* </p>
*
* @author valarchie
* @since 2025-03-10
*/
@Service
public class ShopOrderGoodsServiceImpl extends ServiceImpl<ShopOrderGoodsMapper, ShopOrderGoodsEntity> implements ShopOrderGoodsService {
}

View File

@ -0,0 +1,15 @@
package com.agileboot.domain.shop.order.db;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 商品订单表 Mapper 接口
* </p>
*
* @author valarchie
* @since 2025-03-10
*/
public interface ShopOrderMapper extends BaseMapper<ShopOrderEntity> {
}

View File

@ -0,0 +1,15 @@
package com.agileboot.domain.shop.order.db;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 商品订单表 服务类
* </p>
*
* @author valarchie
* @since 2025-03-10
*/
public interface ShopOrderService extends IService<ShopOrderEntity> {
}

View File

@ -0,0 +1,17 @@
package com.agileboot.domain.shop.order.db;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 商品订单表 服务实现类
* </p>
*
* @author valarchie
* @since 2025-03-10
*/
@Service
public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrderEntity> implements ShopOrderService {
}

View File

@ -0,0 +1,12 @@
package com.agileboot.domain.shop.order.dto;
import com.agileboot.domain.shop.payment.dto.WxJsApiPreCreateResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class CreateOrderResult {
private Long orderId;
private WxJsApiPreCreateResponse paymentInfo;
}

View File

@ -0,0 +1,36 @@
package com.agileboot.domain.shop.order.dto;
import cn.hutool.core.bean.BeanUtil;
import com.agileboot.domain.shop.order.db.ShopOrderEntity;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
@Data
public class ShopOrderDTO {
public ShopOrderDTO(ShopOrderEntity entity) {
if (entity != null) {
BeanUtil.copyProperties(entity, this);
// 如果需要关联查询如商品明细可参考以下方式
/*
List<ShopOrderGoodsEntity> goodsList = orderGoodsService.lambdaQuery()
.eq(ShopOrderGoodsEntity::getOrderId, entity.getOrderId())
.list();
this.goodsList = goodsList.stream().map(ShopOrderGoodsDTO::new).collect(Collectors.toList());
*/
}
}
private Long orderId;
private String ucid;
private String openid;
private String tradeId;
private BigDecimal totalAmount;
private Integer status;
private Integer payStatus;
private String paymentMethod;
private Date payTime;
private Date createTime;
}

View File

@ -0,0 +1,49 @@
package com.agileboot.domain.shop.order.model;
import cn.hutool.core.bean.BeanUtil;
import com.agileboot.domain.shop.goods.db.ShopGoodsEntity;
import com.agileboot.domain.shop.goods.db.ShopGoodsService;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsService;
import java.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class OrderGoodsModel extends ShopOrderGoodsEntity {
private final ShopOrderGoodsService orderGoodsService;
private final ShopGoodsService goodsService;
public OrderGoodsModel(ShopOrderGoodsEntity entity,
ShopOrderGoodsService orderGoodsService,
ShopGoodsService goodsService) {
this(orderGoodsService, goodsService);
if (entity != null) {
BeanUtil.copyProperties(entity, this);
}
}
public OrderGoodsModel(ShopOrderGoodsService orderGoodsService,
ShopGoodsService goodsService) {
this.orderGoodsService = orderGoodsService;
this.goodsService = goodsService;
}
public void calculateTotal() {
ShopGoodsEntity goods = goodsService.getById(getGoodsId());
if (goods != null) {
BigDecimal price = goods.getPrice();
this.setPrice(price);
this.setTotalAmount(price.multiply(BigDecimal.valueOf(getQuantity())));
}
}
public void validateQuantity() {
ShopGoodsEntity goods = goodsService.getById(getGoodsId());
if (goods != null && getQuantity() > goods.getStock()) {
throw new RuntimeException("商品库存不足");
}
}
}

View File

@ -0,0 +1,25 @@
package com.agileboot.domain.shop.order.model;
import com.agileboot.domain.shop.goods.db.ShopGoodsService;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsEntity;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsService;
public class OrderGoodsModelFactory {
private final ShopOrderGoodsService orderGoodsService;
private final ShopGoodsService goodsService;
public OrderGoodsModelFactory(ShopOrderGoodsService orderGoodsService,
ShopGoodsService goodsService) {
this.orderGoodsService = orderGoodsService;
this.goodsService = goodsService;
}
public OrderGoodsModel create(ShopOrderGoodsEntity entity) {
return new OrderGoodsModel(entity, orderGoodsService, goodsService);
}
public OrderGoodsModel create() {
return new OrderGoodsModel(orderGoodsService, goodsService);
}
}

View File

@ -0,0 +1,71 @@
package com.agileboot.domain.shop.order.model;
import cn.hutool.core.bean.BeanUtil;
import com.agileboot.common.config.AgileBootConfig;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.shop.order.db.ShopOrderEntity;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsService;
import com.agileboot.domain.shop.order.db.ShopOrderService;
import java.math.BigDecimal;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class OrderModel extends ShopOrderEntity {
private ShopOrderService orderService;
private ShopOrderGoodsService orderGoodsService;
public OrderModel(ShopOrderEntity entity, ShopOrderService orderService,
ShopOrderGoodsService orderGoodsService) {
this(orderService, orderGoodsService);
if (entity != null) {
BeanUtil.copyProperties(entity, this);
}
}
public OrderModel(ShopOrderService orderService, ShopOrderGoodsService orderGoodsService) {
this.orderService = orderService;
this.orderGoodsService = orderGoodsService;
}
public void calculateTotalAmount(List<Long> goodsIds) {
BigDecimal total = goodsIds.stream()
.map(goodsId -> {
// 这里需要实现获取商品价格和计算逻辑
return BigDecimal.ZERO; // 示例返回值
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
this.setTotalAmount(total);
}
public void validateStatusTransition(Integer newStatus) {
Integer currentStatus = this.getStatus();
// 实现状态机校验逻辑
if (currentStatus == 5 && newStatus != 1) {
throw new ApiException(ErrorCode.FAILED, "已取消订单不可修改状态");
}
}
@Override
public boolean updateById() {
if (AgileBootConfig.isDemoEnabled() && isSpecialOrder()) {
throw new ApiException(ErrorCode.FAILED);
}
return super.updateById();
}
private boolean isSpecialOrder() {
// 示例逻辑判断是否为特殊订单
return this.getTotalAmount().compareTo(new BigDecimal("10000")) > 0;
}
public void generateOrderNumber() {
// 实现订单号生成逻辑示例时间戳+随机数
String orderNo = "OD" + System.currentTimeMillis() + (int)(Math.random()*1000);
this.setUcid(orderNo);
}
}

View File

@ -0,0 +1,35 @@
package com.agileboot.domain.shop.order.model;
import com.agileboot.common.exception.ApiException;
import com.agileboot.common.exception.error.ErrorCode;
import com.agileboot.domain.shop.order.db.ShopOrderEntity;
import com.agileboot.domain.shop.order.db.ShopOrderGoodsService;
import com.agileboot.domain.shop.order.db.ShopOrderService;
public class OrderModelFactory {
private final ShopOrderService orderService;
private final ShopOrderGoodsService orderGoodsService;
public OrderModelFactory(ShopOrderService orderService,
ShopOrderGoodsService orderGoodsService) {
this.orderService = orderService;
this.orderGoodsService = orderGoodsService;
}
public OrderModel create(ShopOrderEntity entity) {
return new OrderModel(entity, orderService, orderGoodsService);
}
public OrderModel create() {
return new OrderModel(orderService, orderGoodsService);
}
public OrderModel loadById(Long orderId) {
ShopOrderEntity entity = orderService.getById(orderId);
if (entity == null) {
throw new ApiException(ErrorCode.Business.COMMON_OBJECT_NOT_FOUND, orderId, "订单");
}
return new OrderModel(entity, orderService, orderGoodsService);
}
}

View File

@ -0,0 +1,34 @@
package com.agileboot.domain.shop.order.query;
import cn.hutool.core.util.StrUtil;
import com.agileboot.common.core.page.AbstractPageQuery;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.Date;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class SearchShopOrderQuery<T> extends AbstractPageQuery<T> {
private String orderNumber;
private Integer status;
private Integer payStatus;
private Date startTime;
private Date endTime;
@Override
public QueryWrapper<T> addQueryCondition() {
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
queryWrapper
.like(StrUtil.isNotEmpty(orderNumber), "order_number", orderNumber)
.eq(status != null, "status", status)
.eq(payStatus != null, "pay_status", payStatus)
.between(startTime != null && endTime != null, "create_time", startTime, endTime)
.eq("deleted", 0)
.orderByDesc("create_time");
return queryWrapper;
}
}

View File

@ -0,0 +1,67 @@
package com.agileboot.domain.shop.payment;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.agileboot.domain.shop.payment.dto.WxJsApiPreCreateRequest;
import com.agileboot.domain.shop.payment.dto.WxJsApiPreCreateResponse;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class PaymentApplicationService {
public WxJsApiPreCreateResponse callJsApiPreCreate(WxJsApiPreCreateRequest request) {
String gatewayUrl = "http://111.59.237.29:7890/open/trade/wx/jsapi/precreate";
try {
String jsonBody = JSONUtil.toJsonStr(request);
// 使用try-with-resources自动关闭连接
try (HttpResponse httpResponse = HttpUtil.createPost(gatewayUrl)
.contentType(ContentType.JSON.getValue())
.body(jsonBody)
.timeout(5000)
.execute()) {
// 获取HTTP状态码和响应体
int status = httpResponse.getStatus();
String result = httpResponse.body();
// 先校验HTTP状态码
if (status != HttpStatus.HTTP_OK) {
throw new RuntimeException("支付网关返回异常状态码:" + status);
}
// ... 保持原有的JSON解析逻辑 ...
if (!JSONUtil.isTypeJSONObject(result)) {
throw new RuntimeException("支付网关返回非JSON格式响应" + result);
}
WxJsApiPreCreateResponse response = JSONUtil.toBean(result, WxJsApiPreCreateResponse.class);
if (response.getAppId() != null) {
return response;
}
PaymentGatewayError error = JSONUtil.toBean(result, PaymentGatewayError.class);
throw new RuntimeException("支付网关业务错误:" + error.getMessage());
}
} catch (Exception e) {
throw new RuntimeException("支付网关调用失败:" + e.getLocalizedMessage(), e);
}
}
// 添加错误响应处理类
@Data
private static class PaymentGatewayError {
private String timestamp;
private Integer status;
private String error;
private String message;
private String path;
}
}

View File

@ -0,0 +1,30 @@
package com.agileboot.domain.shop.payment.dto;
import lombok.Data;
import java.util.Map;
@Data
public class PaymentCallbackRequest {
private String client_id;
private String biz_id;
private String nonce_str;
private String sign;
private String sign_type;
private String version;
private String timestamp;
private String biz_content; // 需要二次解析的字段
// biz_content解析后的实体
@Data
public static class BizContent {
private String uid;
private String trade_id;
private Integer total_amount;
private String trade_pay_time;
private String trade_status;
private Integer pay_type;
private String callback_content;
private String title;
private String biz_order_id;
}
}

View File

@ -0,0 +1,27 @@
package com.agileboot.domain.shop.payment.dto;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;
@Data
public class WxJsApiPreCreateRequest {
private String ip;
@NotBlank(message = "openid不能为空")
private String openid;
private String biz_order_id;
private String pay_amount;
private String title;
private String notify_url;
private String biz_id;
private String ucid; // 选填
private String extra; // 选填
}

View File

@ -0,0 +1,15 @@
package com.agileboot.domain.shop.payment.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class WxJsApiPreCreateResponse {
private String appId;
private String timeStamp;
private String nonceStr;
@JsonProperty("package")
private String packageValue;
private String signType;
private String paySign;
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.agileboot.domain.shop.order.db.ShopOrderGoodsMapper">
</mapper>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.agileboot.domain.shop.order.db.ShopOrderMapper">
</mapper>

View File

@ -61,7 +61,7 @@ public class CodeGenerator {
//生成的类 放在orm子模块下的/target/generated-code目录底下 //生成的类 放在orm子模块下的/target/generated-code目录底下
.module("/agileboot-orm/target/generated-code") .module("/agileboot-orm/target/generated-code")
.parentPackage("com.agileboot") .parentPackage("com.agileboot")
.tableName("shop_category") .tableName("shop_order_goods")
// 决定是否继承基类 // 决定是否继承基类
.isExtendsFromBaseEntity(true) .isExtendsFromBaseEntity(true)
.build(); .build();

42
sql/20250308.sql Normal file
View File

@ -0,0 +1,42 @@
CREATE TABLE `shop_order` (
`order_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单唯一ID',
`ucid` VARCHAR(32) DEFAULT NULL COMMENT 'ucid',
`openid` VARCHAR(32) NOT NULL COMMENT 'openid',
`trade_id` VARCHAR(32) NOT NULL COMMENT '支付网关交易id',
`total_amount` DECIMAL(15,2) NOT NULL COMMENT '订单总金额',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '订单状态1待付款 2已付款 3已发货 4已完成 5已取消',
`pay_status` TINYINT NOT NULL DEFAULT 1 COMMENT '支付状态1未支付 2已支付 3退款中 4已退款',
`payment_method` VARCHAR(32) DEFAULT NULL COMMENT '支付方式',
`pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
`creator_id` BIGINT NOT NULL DEFAULT 0 COMMENT '创建者ID',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater_id` BIGINT NOT NULL DEFAULT 0 COMMENT '更新者ID',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志0存在 1删除',
PRIMARY KEY (`order_id`),
KEY `idx_status` (`status`),
KEY `idx_pay_status` (`pay_status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品订单表';
CREATE TABLE `shop_order_goods` (
`order_goods_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单商品唯一ID',
`order_id` BIGINT NOT NULL COMMENT '关联订单ID',
`goods_id` BIGINT NOT NULL COMMENT '关联商品ID',
`quantity` INT NOT NULL DEFAULT 1 COMMENT '购买数量',
`price` DECIMAL(15,2) NOT NULL COMMENT '购买时单价',
`total_amount` DECIMAL(15,2) GENERATED ALWAYS AS (quantity * price) STORED COMMENT '商品总金额',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '商品状态1正常 2已退货 3已换货',
`creator_id` BIGINT NOT NULL DEFAULT 0 COMMENT '创建者ID',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater_id` BIGINT NOT NULL DEFAULT 0 COMMENT '更新者ID',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '删除标志0存在 1删除',
PRIMARY KEY (`order_goods_id`),
KEY `idx_order` (`order_id`),
KEY `idx_goods` (`goods_id`),
KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`),
CONSTRAINT `fk_order_goods_order` FOREIGN KEY (`order_id`) REFERENCES `shop_order` (`order_id`),
CONSTRAINT `fk_order_goods_goods` FOREIGN KEY (`goods_id`) REFERENCES `shop_goods` (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单商品明细表';

222
支付网关.md Normal file
View File

@ -0,0 +1,222 @@
# 全局公共参数
**全局Header参数**
| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 |
| --- | --- | ---- | ---- | ---- |
| 暂无参数 |
**全局Query参数**
| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 |
| --- | --- | ---- | ---- | ---- |
| 暂无参数 |
**全局Body参数**
| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 |
| --- | --- | ---- | ---- | ---- |
| 暂无参数 |
**全局认证方式**
> 无需认证
# 状态码说明
| 状态码 | 中文描述 |
| --- | ---- |
| 暂无参数 |
# 微信支付
> 创建人: 达民
> 更新人: 达民
> 创建时间: 2025-03-08 08:44:12
> 更新时间: 2025-03-08 08:44:12
```text
暂无描述
```
**目录Header参数**
| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 |
| --- | --- | ---- | ---- | ---- |
| 暂无参数 |
**目录Query参数**
| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 |
| --- | --- | ---- | ---- | ---- |
| 暂无参数 |
**目录Body参数**
| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 |
| --- | --- | ---- | ---- | ---- |
| 暂无参数 |
**目录认证信息**
> 继承父级
**Query**
## JS API支付
> 创建人: 达民
> 更新人: 达民
> 创建时间: 2025-03-08 08:49:16
> 更新时间: 2025-03-08 15:08:38
**在微信内置浏览器里打开目标应用页面(如微信公众号、点击聊天窗口的链接打开页面),根据需要调起微信支付.
1、openid的获取参考https://segmentfault.com/a/1190000013392838 。也可以自己找微信官方文档。
2、如何调起jsapi支付参考文档https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
3、支付回调。支付网关根据请求支付传递的notify_url将支付结果通知到业务方
回调参数**
| 参数 | 含义 |
| --- | --- |
| client_id | 客户端ID |
| biz_id | 支付网关为业务方分配的ID |
| nonce_str | 参与签名的混淆字段 |
| sign | 签名校验字段值 |
| sign_type | 签名类型一般是MD5 |
| version | 支付网关版本号 |
| timestamp | 回调时的时间戳,毫秒 |
| biz_content | 订单的相关数据以key=value组合并以&拼接在一起的字符串其中value未做urlencode处理 |
| 以上参数按照key=value的形式并对参数值做urlencode& 拼接多个key=value形成一个字符串以POST的方式调用notify_url时将该字符串提交给业务方。 | |
**其中biz_content包含了以下参数**
| 参数 | 含义 |
| --- | --- |
| uid | 应用方为人员分配的记录ID应用方在请求接口时以ucid传递的参数值 |
| trade_id | 支付网关为本次支付分配的记录ID |
| total_amount | 本次支付涉及的金额,单位:分 |
| trade_pay_time | 支付成功的时间格式yyyy-MM-dd HH:mm:ss |
| trade_status | 支付支付状态成功时值为SUCCESS |
| pay_type | 支付方式jsapi接口调起的支付本字段值固定为113 |
| callback_content | 微信回调给支付网关的原始xml数据参考https://pay.weixin.qq.com/doc/v2/partner/4011936644 |
| title | 业务方调接口时传递的title字段值 |
| biz_order_id | 业务方支付订单的订单号 |
**举个例子:
biz_content=%7B%22uid%22%3A%22102579%22%2C%22trade_id%22%3A%221501822040%22%2C%22total_amount%22%3A83601%2C%22trade_pay_time%22%3A%222025-03-08%2008%3A48%3A35%22%2C%22trade_status%22%3A%22SUCCESS%22%2C%22pay_type%22%3A%22113%22%2C%22callback_content%22%3A%22%3Cxml%3E%E7%95%A5%3C%2Fxml%3E%22%2C%22title%22%3A%22%E4%BA%91%E9%98%9F%E9%95%BF%E5%8C%BB%E8%8D%AF%E8%BF%9E%E9%94%81%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F%22%2C%22biz_order_id%22%3A%2221-364535%22%7D&nonce_str=11961acffe074c7da556a18e8549027f&sign=5ec91e8d3f4feb4b3c273e5512a7d7c3&biz_id=ydrug&sign_type=MD5&version=1.0&client_id=yeshan&timestamp=1741394916159,只有通过了签名校验才应该能被业务方接受并进行业务的下一步处理。业务方应该根据回调数据里的相关参数biz_order_id找回相应的订单以进行下一步的业务处理。注意同一个支付订单的支付结果支付网关可能会多次回调通知业务方所以业务方要做好幂等处理。签名校验算法如下**
```java
/**
* @param appKey 支付网关提供的业务秘钥与biz_id是配对一起提供的
* @param sign 支付网关回调参数里的sign字段值
* @param reqBody 支付网关回调的原始请求body字符串
*/
public static boolean checkOpenSign(String appKey, String sign, String reqBody) {
String[] fields = reqBody.split("&");
Map<String, String> fieldMap = new HashMap<>();
for (String field : fields) {
String[] pair = field.split("=");
if (pair.length != 2) {
continue;
}
// sign字段不参与校验空字段不校验
if (!"sign".equals(pair[0]) && StringUtils.isNotBlank(pair[1])) {
fieldMap.put(pair[0], pair[1]);
}
}
String generateSign = openSign(appKey, fieldMap);
return Objects.equals(generateSign, sign);
}
public static String openSign(String appKey, Map<String, String> params) {
if(MapUtils.isEmpty(params)) {
params = new HashMap<String, String>(1);
}
Map<String, String> sortedParams = new TreeMap<>(params);
List<String> kvPairList = getKVList(sortedParams);
String sourceText = StringUtils.join(kvPairList, "&");
sourceText = sourceText + "&app_key=" + appKey;
LogUtils.NORMAL.info("sourceText:" + sourceText);
try {
return md5(sourceText.getBytes("utf-8"));
} catch (Exception e) {
return null;
}
}
```
**接口状态**
> 开发中
**接口URL**
> http://localhost:7045/open/trade/wx/jsapi/precreate
| 环境 | URL |
| --- | --- |
**请求方式**
> POST
**Content-Type**
> json
**请求Body参数**
```javascript
{
"ucid": "123", //应用方为人员分配的记录ID以方便后续排查问题。选填
"ip": "127.0.0.1", //调用方应用所在服务器的ip。必填
"openid": "o_xsewew131SGS", //微信用户的openid参考【设计】模块的【详细说明】。必填
"biz_order_id": "1234536", //应用方为本次支付订单定义的订单ID务必确保在业务应用内唯一。必填
"pay_amount": 1, //支付金额,单位:分。必填
"title": "测试", //用户在微信支付界面看到订单标题,也就是商品描述。必填
"notify_url": "http://...", //业务方接收支付结果的异步回调地址。必填
"biz_id": "abc", //支付网关为业务方分配的业务应用ID。必填
"extra": "342rewrs" //业务方期望支付网关回调时透传的数据。选填
}
```
**认证方式**
> 继承父级
**响应示例**
* 成功(200)
```javascript
//成功使用以下参数调起jsapi支付。
{
"appId": "",
"timeStamp": "",
"nonceStr": "",
"package": "",
"signType": "",
"paySign": ""
}
```
* 失败(200)
```javascript
{
"timestamp": "2025-03-08T01:55:14.480+0000",
"status": 500,
"error": "Internal Server Error",
"message": "第三方返回失败, errorMsg = 第三方返回失败, errorMsg = 无效的openid",
"path": "/open/trade/wx/jsapi/precreate"
}
```
**Query**