From c566f987f563ddcf82be4a651a151efc272c3ef8 Mon Sep 17 00:00:00 2001 From: dqz Date: Tue, 11 Mar 2025 08:59:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=A2=E5=8D=95=E5=92=8C?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E7=BD=91=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/OrderController.java | 24 +- .../api/controller/PaymentController.java | 87 +++++++ .../agileboot/common/utils/OpenSignUtil.java | 63 +++++ .../shop/order/OrderApplicationService.java | 156 ++++++++++++ .../order/command/SubmitOrderCommand.java | 13 + .../domain/shop/order/db/ShopOrderEntity.java | 74 ++++++ .../shop/order/db/ShopOrderGoodsEntity.java | 65 +++++ .../shop/order/db/ShopOrderGoodsMapper.java | 15 ++ .../shop/order/db/ShopOrderGoodsService.java | 15 ++ .../order/db/ShopOrderGoodsServiceImpl.java | 17 ++ .../domain/shop/order/db/ShopOrderMapper.java | 15 ++ .../shop/order/db/ShopOrderService.java | 15 ++ .../shop/order/db/ShopOrderServiceImpl.java | 17 ++ .../shop/order/dto/CreateOrderResult.java | 12 + .../domain/shop/order/dto/ShopOrderDTO.java | 36 +++ .../shop/order/model/OrderGoodsModel.java | 49 ++++ .../order/model/OrderGoodsModelFactory.java | 25 ++ .../domain/shop/order/model/OrderModel.java | 71 ++++++ .../shop/order/model/OrderModelFactory.java | 35 +++ .../order/query/SearchShopOrderQuery.java | 34 +++ .../payment/PaymentApplicationService.java | 67 ++++++ .../payment/dto/PaymentCallbackRequest.java | 30 +++ .../payment/dto/WxJsApiPreCreateRequest.java | 27 +++ .../payment/dto/WxJsApiPreCreateResponse.java | 15 ++ .../mapper/shop/ShopOrderGoodsMapper.xml | 5 + .../resources/mapper/shop/ShopOrderMapper.xml | 5 + .../mybatisplus/CodeGenerator.java | 2 +- sql/20250308.sql | 42 ++++ 支付网关.md | 222 ++++++++++++++++++ 29 files changed, 1244 insertions(+), 9 deletions(-) create mode 100644 agileboot-api/src/main/java/com/agileboot/api/controller/PaymentController.java create mode 100644 agileboot-common/src/main/java/com/agileboot/common/utils/OpenSignUtil.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/command/SubmitOrderCommand.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderEntity.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsEntity.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsMapper.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsService.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsServiceImpl.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderMapper.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderService.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderServiceImpl.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/CreateOrderResult.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/ShopOrderDTO.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderGoodsModel.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderGoodsModelFactory.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderModel.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderModelFactory.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/order/query/SearchShopOrderQuery.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/PaymentApplicationService.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/PaymentCallbackRequest.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/WxJsApiPreCreateRequest.java create mode 100644 agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/WxJsApiPreCreateResponse.java create mode 100644 agileboot-domain/src/main/resources/mapper/shop/ShopOrderGoodsMapper.xml create mode 100644 agileboot-domain/src/main/resources/mapper/shop/ShopOrderMapper.xml create mode 100644 sql/20250308.sql create mode 100644 支付网关.md diff --git a/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java b/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java index 5cecaab..ecb28ab 100644 --- a/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java +++ b/agileboot-api/src/main/java/com/agileboot/api/controller/OrderController.java @@ -1,25 +1,33 @@ package com.agileboot.api.controller; 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.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 */ +@RequiredArgsConstructor @RestController @RequestMapping("/api/order") public class OrderController extends BaseController { - /** - * 访问首页,提示语 - */ - @RequestMapping("/") - public String index() { - return "暂无订单"; + private final OrderApplicationService orderApplicationService; + + // 新增提交订单接口 + @PostMapping("/submit") + public ResponseDTO submitOrder(@Validated @RequestBody SubmitOrderCommand command) { + CreateOrderResult result = orderApplicationService.createOrder(command); + return ResponseDTO.ok(result); } - - } diff --git a/agileboot-api/src/main/java/com/agileboot/api/controller/PaymentController.java b/agileboot-api/src/main/java/com/agileboot/api/controller/PaymentController.java new file mode 100644 index 0000000..569693f --- /dev/null +++ b/agileboot-api/src/main/java/com/agileboot/api/controller/PaymentController.java @@ -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 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) { + // 实现订单状态更新和幂等性校验 + } +} diff --git a/agileboot-common/src/main/java/com/agileboot/common/utils/OpenSignUtil.java b/agileboot-common/src/main/java/com/agileboot/common/utils/OpenSignUtil.java new file mode 100644 index 0000000..7189e16 --- /dev/null +++ b/agileboot-common/src/main/java/com/agileboot/common/utils/OpenSignUtil.java @@ -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 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 getKVList(Map sortedParams) { + return sortedParams.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .collect(Collectors.toList()); + } + + public static String openSign(String appKey, Map params) { + if(MapUtils.isEmpty(params)) { + params = new HashMap(1); + } + Map sortedParams = new TreeMap<>(params); + List 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; + } + } + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java new file mode 100644 index 0000000..c780371 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/OrderApplicationService.java @@ -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 getOrderList(SearchShopOrderQuery<> query) { + Page page = orderService.page(query.toPage(), query.toQueryWrapper()); + List 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 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 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 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 command) { + for (Long orderId : command.getIds()) { + OrderModel model = orderModelFactory.loadById(orderId); + model.deleteById(); + } + } +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/command/SubmitOrderCommand.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/command/SubmitOrderCommand.java new file mode 100644 index 0000000..d317001 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/command/SubmitOrderCommand.java @@ -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 goodsList; +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderEntity.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderEntity.java new file mode 100644 index 0000000..049d69e --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderEntity.java @@ -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; + +/** + *

+ * 商品订单表 + *

+ * + * @author valarchie + * @since 2025-03-10 + */ +@Getter +@Setter +@TableName("shop_order") +@ApiModel(value = "ShopOrderEntity对象", description = "商品订单表") +public class ShopOrderEntity extends BaseEntity { + + 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; + } + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsEntity.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsEntity.java new file mode 100644 index 0000000..5afab56 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsEntity.java @@ -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; + +/** + *

+ * 订单商品明细表 + *

+ * + * @author valarchie + * @since 2025-03-10 + */ +@Getter +@Setter +@TableName("shop_order_goods") +@ApiModel(value = "ShopOrderGoodsEntity对象", description = "订单商品明细表") +public class ShopOrderGoodsEntity extends BaseEntity { + + 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; + } + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsMapper.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsMapper.java new file mode 100644 index 0000000..2f4854b --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsMapper.java @@ -0,0 +1,15 @@ +package com.agileboot.domain.shop.order.db; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 订单商品明细表 Mapper 接口 + *

+ * + * @author valarchie + * @since 2025-03-10 + */ +public interface ShopOrderGoodsMapper extends BaseMapper { + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsService.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsService.java new file mode 100644 index 0000000..b7a935f --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsService.java @@ -0,0 +1,15 @@ +package com.agileboot.domain.shop.order.db; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 订单商品明细表 服务类 + *

+ * + * @author valarchie + * @since 2025-03-10 + */ +public interface ShopOrderGoodsService extends IService { + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsServiceImpl.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsServiceImpl.java new file mode 100644 index 0000000..59d0582 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderGoodsServiceImpl.java @@ -0,0 +1,17 @@ +package com.agileboot.domain.shop.order.db; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 订单商品明细表 服务实现类 + *

+ * + * @author valarchie + * @since 2025-03-10 + */ +@Service +public class ShopOrderGoodsServiceImpl extends ServiceImpl implements ShopOrderGoodsService { + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderMapper.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderMapper.java new file mode 100644 index 0000000..cf45af9 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderMapper.java @@ -0,0 +1,15 @@ +package com.agileboot.domain.shop.order.db; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + *

+ * 商品订单表 Mapper 接口 + *

+ * + * @author valarchie + * @since 2025-03-10 + */ +public interface ShopOrderMapper extends BaseMapper { + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderService.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderService.java new file mode 100644 index 0000000..c4363f5 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderService.java @@ -0,0 +1,15 @@ +package com.agileboot.domain.shop.order.db; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + *

+ * 商品订单表 服务类 + *

+ * + * @author valarchie + * @since 2025-03-10 + */ +public interface ShopOrderService extends IService { + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderServiceImpl.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderServiceImpl.java new file mode 100644 index 0000000..fc724c4 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/db/ShopOrderServiceImpl.java @@ -0,0 +1,17 @@ +package com.agileboot.domain.shop.order.db; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 商品订单表 服务实现类 + *

+ * + * @author valarchie + * @since 2025-03-10 + */ +@Service +public class ShopOrderServiceImpl extends ServiceImpl implements ShopOrderService { + +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/CreateOrderResult.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/CreateOrderResult.java new file mode 100644 index 0000000..c2f37d3 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/CreateOrderResult.java @@ -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; +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/ShopOrderDTO.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/ShopOrderDTO.java new file mode 100644 index 0000000..8a82691 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/dto/ShopOrderDTO.java @@ -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 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; +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderGoodsModel.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderGoodsModel.java new file mode 100644 index 0000000..83d3f86 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderGoodsModel.java @@ -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("商品库存不足"); + } + } +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderGoodsModelFactory.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderGoodsModelFactory.java new file mode 100644 index 0000000..04c4834 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderGoodsModelFactory.java @@ -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); + } +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderModel.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderModel.java new file mode 100644 index 0000000..6246f15 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderModel.java @@ -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 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); + } +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderModelFactory.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderModelFactory.java new file mode 100644 index 0000000..4509602 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/model/OrderModelFactory.java @@ -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); + } +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/query/SearchShopOrderQuery.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/query/SearchShopOrderQuery.java new file mode 100644 index 0000000..95b3fae --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/order/query/SearchShopOrderQuery.java @@ -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 extends AbstractPageQuery { + + private String orderNumber; + private Integer status; + private Integer payStatus; + private Date startTime; + private Date endTime; + + @Override + public QueryWrapper addQueryCondition() { + QueryWrapper 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; + } +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/PaymentApplicationService.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/PaymentApplicationService.java new file mode 100644 index 0000000..78f6f04 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/PaymentApplicationService.java @@ -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; + } +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/PaymentCallbackRequest.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/PaymentCallbackRequest.java new file mode 100644 index 0000000..fff5739 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/PaymentCallbackRequest.java @@ -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; + } +} diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/WxJsApiPreCreateRequest.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/WxJsApiPreCreateRequest.java new file mode 100644 index 0000000..a153356 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/WxJsApiPreCreateRequest.java @@ -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; // 选填 +} \ No newline at end of file diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/WxJsApiPreCreateResponse.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/WxJsApiPreCreateResponse.java new file mode 100644 index 0000000..91613d0 --- /dev/null +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/payment/dto/WxJsApiPreCreateResponse.java @@ -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; +} \ No newline at end of file diff --git a/agileboot-domain/src/main/resources/mapper/shop/ShopOrderGoodsMapper.xml b/agileboot-domain/src/main/resources/mapper/shop/ShopOrderGoodsMapper.xml new file mode 100644 index 0000000..b44edfb --- /dev/null +++ b/agileboot-domain/src/main/resources/mapper/shop/ShopOrderGoodsMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/agileboot-domain/src/main/resources/mapper/shop/ShopOrderMapper.xml b/agileboot-domain/src/main/resources/mapper/shop/ShopOrderMapper.xml new file mode 100644 index 0000000..a9fd925 --- /dev/null +++ b/agileboot-domain/src/main/resources/mapper/shop/ShopOrderMapper.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/agileboot-infrastructure/src/main/java/com/agileboot/infrastructure/mybatisplus/CodeGenerator.java b/agileboot-infrastructure/src/main/java/com/agileboot/infrastructure/mybatisplus/CodeGenerator.java index db98377..5da0de6 100644 --- a/agileboot-infrastructure/src/main/java/com/agileboot/infrastructure/mybatisplus/CodeGenerator.java +++ b/agileboot-infrastructure/src/main/java/com/agileboot/infrastructure/mybatisplus/CodeGenerator.java @@ -61,7 +61,7 @@ public class CodeGenerator { //生成的类 放在orm子模块下的/target/generated-code目录底下 .module("/agileboot-orm/target/generated-code") .parentPackage("com.agileboot") - .tableName("shop_category") + .tableName("shop_order_goods") // 决定是否继承基类 .isExtendsFromBaseEntity(true) .build(); diff --git a/sql/20250308.sql b/sql/20250308.sql new file mode 100644 index 0000000..139fcda --- /dev/null +++ b/sql/20250308.sql @@ -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='订单商品明细表'; \ No newline at end of file diff --git a/支付网关.md b/支付网关.md new file mode 100644 index 0000000..b469f6d --- /dev/null +++ b/支付网关.md @@ -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×tamp=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 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 params) { + if(MapUtils.isEmpty(params)) { + params = new HashMap(1); + } + Map sortedParams = new TreeMap<>(params); + List 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**