Explorar o código

feat(applet): 新增微信小程序订单支付功能

- 新增获取当前登录用户信息接口
- 新增创建订单接口,支持微信支付参数构建
- 新增订单支付状态查询接口
- 新增微信支付回调处理逻辑
- 新增订单支付成功后状态更新机制
- 新增订单超时未支付和积分异步增加预留逻辑
- 新增用户支付订单信息管理后台接口
- 新增订单实体类、表单对象、视图对象及相关映射配置
- 新增系统常量状态定义
- 修改用户信息字段 wechat 为 openid
- 完善用户信息服务实现,使用 userId 替代手机号查询
- 新增档位下单表单对象和支付表单对象
- 新增订单编号生成规则和金额转换工具方法
- 新增可重入锁防止微信重复回调处理
- 新增微信支付 V3 版本工具类集成
- 新增支付订单数据库表结构及 MyBatis 映射文件
- 新增订单分页查询、新增、编辑、删除管理功能
- 新增订单信息对象转换器配置
- 新增订单管理权限控制注解配置
wzq hai 1 día
pai
achega
85352fbb02
Modificáronse 23 ficheiros con 1724 adicións e 20 borrados
  1. 81 0
      src/main/java/com/zsElectric/boot/business/controller/UserOrderInfoController.java
  2. 14 0
      src/main/java/com/zsElectric/boot/business/controller/applet/AppletHomeController.java
  3. 79 2
      src/main/java/com/zsElectric/boot/business/controller/applet/AppletOrderController.java
  4. 20 0
      src/main/java/com/zsElectric/boot/business/converter/UserOrderInfoConverter.java
  5. 28 0
      src/main/java/com/zsElectric/boot/business/mapper/UserOrderInfoMapper.java
  6. 1 1
      src/main/java/com/zsElectric/boot/business/model/entity/UserInfo.java
  7. 92 0
      src/main/java/com/zsElectric/boot/business/model/entity/UserOrderInfo.java
  8. 1 1
      src/main/java/com/zsElectric/boot/business/model/form/UserInfoForm.java
  9. 93 0
      src/main/java/com/zsElectric/boot/business/model/form/UserOrderInfoForm.java
  10. 22 0
      src/main/java/com/zsElectric/boot/business/model/form/applet/LevelOrderForm.java
  11. 27 0
      src/main/java/com/zsElectric/boot/business/model/form/applet/UserPayForm.java
  12. 21 0
      src/main/java/com/zsElectric/boot/business/model/query/UserOrderInfoQuery.java
  13. 1 1
      src/main/java/com/zsElectric/boot/business/model/vo/UserInfoVO.java
  14. 65 0
      src/main/java/com/zsElectric/boot/business/model/vo/UserOrderInfoVO.java
  15. 4 0
      src/main/java/com/zsElectric/boot/business/model/vo/applet/AppletUserInfoVO.java
  16. 71 0
      src/main/java/com/zsElectric/boot/business/service/UserOrderInfoService.java
  17. 7 10
      src/main/java/com/zsElectric/boot/business/service/impl/UserInfoServiceImpl.java
  18. 369 0
      src/main/java/com/zsElectric/boot/business/service/impl/UserOrderInfoServiceImpl.java
  19. 11 0
      src/main/java/com/zsElectric/boot/common/constant/SystemConstants.java
  20. 675 0
      src/main/java/com/zsElectric/boot/core/pay/WXPayUtility.java
  21. 5 0
      src/main/java/com/zsElectric/boot/core/pay/WechatPayV3Utils.java
  22. 5 5
      src/main/java/com/zsElectric/boot/system/service/impl/UserServiceImpl.java
  23. 32 0
      src/main/resources/mapper/business/UserOrderInfoMapper.xml

+ 81 - 0
src/main/java/com/zsElectric/boot/business/controller/UserOrderInfoController.java

@@ -0,0 +1,81 @@
+package com.zsElectric.boot.business.controller;
+
+import com.zsElectric.boot.business.service.UserOrderInfoService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.zsElectric.boot.business.model.form.UserOrderInfoForm;
+import com.zsElectric.boot.business.model.query.UserOrderInfoQuery;
+import com.zsElectric.boot.business.model.vo.UserOrderInfoVO;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.zsElectric.boot.core.web.PageResult;
+import com.zsElectric.boot.core.web.Result;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import jakarta.validation.Valid;
+
+/**
+ * 用户支付订单信息前端控制层
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+@Tag(name = "用户支付订单信息接口")
+@RestController
+@RequestMapping("/api/v1/user-order-info")
+@RequiredArgsConstructor
+public class UserOrderInfoController {
+
+    private final UserOrderInfoService userOrderInfoService;
+
+    @Operation(summary = "用户支付订单信息分页列表")
+    @GetMapping("/page")
+    @PreAuthorize("@ss.hasPerm('business:user-order-info:query')")
+    public PageResult<UserOrderInfoVO> getUserOrderInfoPage(UserOrderInfoQuery queryParams) {
+        IPage<UserOrderInfoVO> result = userOrderInfoService.getUserOrderInfoPage(queryParams);
+        return PageResult.success(result);
+    }
+
+    @Operation(summary = "新增用户支付订单信息")
+    @PostMapping
+    @PreAuthorize("@ss.hasPerm('business:user-order-info:add')")
+    public Result<Void> saveUserOrderInfo(@RequestBody @Valid UserOrderInfoForm formData) {
+        boolean result = userOrderInfoService.saveUserOrderInfo(formData);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "获取用户支付订单信息表单数据")
+    @GetMapping("/{id}/form")
+    @PreAuthorize("@ss.hasPerm('business:user-order-info:edit')")
+    public Result<UserOrderInfoForm> getUserOrderInfoForm(
+            @Parameter(description = "用户支付订单信息ID") @PathVariable Long id
+    ) {
+        UserOrderInfoForm formData = userOrderInfoService.getUserOrderInfoFormData(id);
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "修改用户支付订单信息")
+    @PutMapping(value = "/{id}")
+    @PreAuthorize("@ss.hasPerm('business:user-order-info:edit')")
+    public Result<Void> updateUserOrderInfo(
+            @Parameter(description = "用户支付订单信息ID") @PathVariable Long id,
+            @RequestBody @Validated UserOrderInfoForm formData
+    ) {
+        boolean result = userOrderInfoService.updateUserOrderInfo(id, formData);
+        return Result.judge(result);
+    }
+
+    @Operation(summary = "删除用户支付订单信息")
+    @DeleteMapping("/{ids}")
+    @PreAuthorize("@ss.hasPerm('business:user-order-info:delete')")
+    public Result<Void> deleteUserOrderInfos(
+            @Parameter(description = "用户支付订单信息ID,多个以英文逗号(,)分割") @PathVariable String ids
+    ) {
+        boolean result = userOrderInfoService.deleteUserOrderInfos(ids);
+        return Result.judge(result);
+    }
+}

+ 14 - 0
src/main/java/com/zsElectric/boot/business/controller/applet/AppletHomeController.java

@@ -1,5 +1,9 @@
 package com.zsElectric.boot.business.controller.applet;
 
+import com.zsElectric.boot.business.model.vo.UserInfoVO;
+import com.zsElectric.boot.business.service.UserInfoService;
+import com.zsElectric.boot.core.web.Result;
+import io.swagger.v3.oas.annotations.Operation;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.zsElectric.boot.business.model.query.StationInfoQuery;
 import com.zsElectric.boot.business.model.vo.StationInfoVO;
@@ -8,6 +12,7 @@ import com.zsElectric.boot.core.web.PageResult;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -19,6 +24,15 @@ import org.springframework.web.bind.annotation.RestController;
 @RequiredArgsConstructor
 public class AppletHomeController {
 
+    private final UserInfoService userInfoService;
+
+    @Operation(summary = "微信小程序获取当前登录信息")
+    @GetMapping("/getUserInfo")
+    public Result<UserInfoVO> getUserInfo() {
+        UserInfoVO currentUserInfo = userInfoService.getCurrentUserInfo();
+        return Result.success(currentUserInfo);
+    }
+
     private final AppletHomeService appletHomeService;
 
     /**

+ 79 - 2
src/main/java/com/zsElectric/boot/business/controller/applet/AppletOrderController.java

@@ -1,18 +1,95 @@
 package com.zsElectric.boot.business.controller.applet;
 
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.zsElectric.boot.business.model.entity.RechargeLevel;
+import com.zsElectric.boot.business.model.form.applet.LevelOrderForm;
+import com.zsElectric.boot.business.model.form.applet.UserPayForm;
 import com.zsElectric.boot.business.service.RechargeLevelService;
+import com.zsElectric.boot.business.service.UserOrderInfoService;
+import com.zsElectric.boot.common.annotation.RepeatSubmit;
+import com.zsElectric.boot.common.constant.SystemConstants;
+import com.zsElectric.boot.core.web.Result;
+import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
 
 @Tag(name = "订单相关接口")
+@Slf4j
 @RestController
 @RequestMapping("/applet/v1/order")
 @RequiredArgsConstructor
 public class AppletOrderController {
 
     private final RechargeLevelService rechargeLevelService;
+    private final UserOrderInfoService userOrderInfoService;
+
+    /**
+     * 获取充电档位
+     *
+     * @return
+     */
+    @Operation(summary = "获取充电档位")
+    @GetMapping("/getReChargeLevel")
+    public Result<List<RechargeLevel>> getReChargeLevel() {
+        List<RechargeLevel> list = rechargeLevelService.list(Wrappers.lambdaQuery(RechargeLevel.class).eq(RechargeLevel::getStatus, SystemConstants.STATUS_ONE));
+        return Result.success(list);
+    }
+
+    /**
+     * 创建订单
+     *
+     * @param levelOrderForm
+     * @return
+     */
+    @Operation(summary = "创建订单")
+    @PostMapping("/createOrder")
+    public Result<UserPayForm> createOrder(@RequestBody LevelOrderForm levelOrderForm) {
+        UserPayForm payForm = userOrderInfoService.createOrder(levelOrderForm);
+        return Result.success(payForm);
+    }
+
+    /**
+     * 订单-支付
+     *
+     * @param orderId
+     * @return
+     */
+    @Operation(summary = "订单-支付")
+    @PutMapping("/payOrder/{orderId}")
+    public Result<UserPayForm> payOrder(@PathVariable("orderId") String orderId) {
+        return Result.success(userOrderInfoService.payOrder(orderId));
+    }
 
+    /**
+     * 支付回调
+     *
+     * @param request
+     * @return
+     */
+    @Operation(summary = "支付回调")
+    @RequestMapping("/wechatPayNotify")
+    public Map<String, String> wechatPayNotify(HttpServletRequest request, HttpServletResponse response) throws InterruptedException {
+        log.info("--------------------------------------------支付回调");
+        return userOrderInfoService.wechatPayNotify(request, response);
+    }
 
+    /**
+     * 订单支付是否成功查询
+     *
+     * @param orderNo
+     * @return
+     */
+    @Operation(summary = "订单支付是否成功查询")
+    @GetMapping("/orderQuery/{orderNo}")
+    public Result<String> orderQuery(@PathVariable("orderNo") String orderNo) throws IOException {
+        return Result.success(userOrderInfoService.orderQuery(orderNo));
+    }
 }

+ 20 - 0
src/main/java/com/zsElectric/boot/business/converter/UserOrderInfoConverter.java

@@ -0,0 +1,20 @@
+package com.zsElectric.boot.business.converter;
+
+import org.mapstruct.Mapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zsElectric.boot.business.model.entity.UserOrderInfo;
+import com.zsElectric.boot.business.model.form.UserOrderInfoForm;
+
+/**
+ * 用户支付订单信息对象转换器
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+@Mapper(componentModel = "spring")
+public interface UserOrderInfoConverter {
+
+    UserOrderInfoForm toForm(UserOrderInfo entity);
+
+    UserOrderInfo toEntity(UserOrderInfoForm formData);
+}

+ 28 - 0
src/main/java/com/zsElectric/boot/business/mapper/UserOrderInfoMapper.java

@@ -0,0 +1,28 @@
+package com.zsElectric.boot.business.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zsElectric.boot.business.model.entity.UserOrderInfo;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zsElectric.boot.business.model.query.UserOrderInfoQuery;
+import com.zsElectric.boot.business.model.vo.UserOrderInfoVO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 用户支付订单信息Mapper接口
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+@Mapper
+public interface UserOrderInfoMapper extends BaseMapper<UserOrderInfo> {
+
+    /**
+     * 获取用户支付订单信息分页数据
+     *
+     * @param page        分页对象
+     * @param queryParams 查询参数
+     * @return {@link Page<UserOrderInfoVO>} 用户支付订单信息分页列表
+     */
+    Page<UserOrderInfoVO> getUserOrderInfoPage(Page<UserOrderInfoVO> page, UserOrderInfoQuery queryParams);
+
+}

+ 1 - 1
src/main/java/com/zsElectric/boot/business/model/entity/UserInfo.java

@@ -36,7 +36,7 @@ public class UserInfo extends BaseEntity {
     /**
      * 微信openid
      */
-    private String wechat;
+    private String openid;
     /**
      * 积分
      */

+ 92 - 0
src/main/java/com/zsElectric/boot/business/model/entity/UserOrderInfo.java

@@ -0,0 +1,92 @@
+package com.zsElectric.boot.business.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.Version;
+import com.zsElectric.boot.common.base.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 用户支付订单信息实体对象
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+@Getter
+@Setter
+@TableName("c_user_order_info")
+public class UserOrderInfo extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+    /**
+     * 微信openId
+     */
+    private String openid;
+    /**
+     * 订单编号
+     */
+    private String orderNo;
+    /**
+     * 订单金额
+     */
+    private BigDecimal orderMoney;
+    /**
+     * 订单剩余金额
+     */
+    private BigDecimal lastMoney;
+    /**
+     * 支付时间
+     */
+    private LocalDateTime payTime;
+    /**
+     * 支付档ID
+     */
+    private Long levelId;
+    /**
+     * 第三方支付单号
+     */
+    private String outTradeNo;
+    /**
+     * 订单状态 1 待支付  2 已支付 3 已取消 4 已退款
+     */
+    private Integer orderStatus;
+    /**
+     * 订单类型 1 微信 2 第三方
+     */
+    private Integer orderType;
+    /**
+     * 微信支付流水号
+     */
+    private String transactionId;
+    /**
+     * 退款金额
+     */
+    private BigDecimal refundMoney;
+    /**
+     * 退款时间
+     */
+    private LocalDateTime refundTime;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 乐观锁
+     */
+    @Version
+    private Integer version;
+    /**
+     * 逻辑删除(0-未删除 1-已删除)
+     */
+    @TableLogic
+    private Integer isDeleted;
+}

+ 1 - 1
src/main/java/com/zsElectric/boot/business/model/form/UserInfoForm.java

@@ -40,7 +40,7 @@ public class UserInfoForm implements Serializable {
 
     @Schema(description = "微信openid")
     @Size(max=128, message="微信openid长度不能超过128个字符")
-    private String wechat;
+    private String openid;
 
     @Schema(description = "积分")
     private BigDecimal integralNum;

+ 93 - 0
src/main/java/com/zsElectric/boot/business/model/form/UserOrderInfoForm.java

@@ -0,0 +1,93 @@
+package com.zsElectric.boot.business.model.form;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import jakarta.validation.constraints.*;
+
+/**
+ * 用户支付订单信息表单对象
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+@Getter
+@Setter
+@Schema(description = "用户支付订单信息表单对象")
+public class UserOrderInfoForm implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "主键ID")
+    @NotNull(message = "主键ID不能为空")
+    private Long id;
+
+    @Schema(description = "用户ID")
+    private Long userId;
+
+    @Schema(description = "用户openId")
+    private String openid;
+
+    @Schema(description = "订单编号")
+    @Size(max = 128, message = "订单编号长度不能超过128个字符")
+    private String orderNo;
+
+    @Schema(description = "订单金额")
+    private BigDecimal orderMoney;
+
+    @Schema(description = "订单剩余金额")
+    private BigDecimal lastMoney;
+
+    @Schema(description = "支付时间")
+    @Size(max = 20, message = "支付时间长度不能超过20个字符")
+    private LocalDateTime payTime;
+
+    @Schema(description = "支付档ID")
+    private Long levelId;
+
+    @Schema(description = "第三方支付单号")
+    @Size(max = 50, message = "第三方支付单号长度不能超过50个字符")
+    private String outTradeNo;
+
+    @Schema(description = "订单状态 1 待支付  2 已支付 3 已取消 4 已退款")
+    private Integer orderStatus;
+
+    @Schema(description = "订单类型 1 微信 2 第三方")
+    private Integer orderType;
+
+    @Schema(description = "微信支付流水号")
+    private String transactionId;
+
+    @Schema(description = "退款金额")
+    private BigDecimal refundMoney;
+
+    @Schema(description = "退款时间")
+    @Size(max = 20, message = "退款时间长度不能超过20个字符")
+    private LocalDateTime refundTime;
+
+    @Schema(description = "备注")
+    @Size(max = 255, message = "备注长度不能超过255个字符")
+    private String remark;
+
+    @Schema(description = "乐观锁")
+    private Integer version;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新时间")
+    private LocalDateTime updateTime;
+
+    @Schema(description = "逻辑删除(0-未删除 1-已删除)")
+    private Integer isDeleted;
+
+
+}

+ 22 - 0
src/main/java/com/zsElectric/boot/business/model/form/applet/LevelOrderForm.java

@@ -0,0 +1,22 @@
+package com.zsElectric.boot.business.model.form.applet;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Getter
+@Setter
+@Schema(description = "档位下单表单对象")
+public class LevelOrderForm implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "档位ID")
+    @NotNull(message = "请选择充值档位")
+    private Long levelId;
+}

+ 27 - 0
src/main/java/com/zsElectric/boot/business/model/form/applet/UserPayForm.java

@@ -0,0 +1,27 @@
+package com.zsElectric.boot.business.model.form.applet;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.Map;
+
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@Schema(description = "支付表单对象")
+public class UserPayForm implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "订单ID")
+    private Long orderId;
+
+    @Schema(description = "订单号")
+    private String orderNo;
+
+    @Schema(description = "JsApi参数")
+    private Map<String,Object> params;
+}

+ 21 - 0
src/main/java/com/zsElectric/boot/business/model/query/UserOrderInfoQuery.java

@@ -0,0 +1,21 @@
+package com.zsElectric.boot.business.model.query;
+
+import com.zsElectric.boot.common.base.BasePageQuery;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * 用户支付订单信息分页查询对象
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+@Schema(description = "用户支付订单信息查询对象")
+@Getter
+@Setter
+public class UserOrderInfoQuery extends BasePageQuery {
+
+}

+ 1 - 1
src/main/java/com/zsElectric/boot/business/model/vo/UserInfoVO.java

@@ -32,7 +32,7 @@ public class UserInfoVO implements Serializable {
     @Schema(description = "手机号")
     private String phone;
     @Schema(description = "微信openid")
-    private String wechat;
+    private String openid;
     @Schema(description = "积分")
     private BigDecimal integralNum;
     @Schema(description = "所属第三方(无则为自营)")

+ 65 - 0
src/main/java/com/zsElectric/boot/business/model/vo/UserOrderInfoVO.java

@@ -0,0 +1,65 @@
+package com.zsElectric.boot.business.model.vo;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+
+/**
+ * 用户支付订单信息视图对象
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+@Getter
+@Setter
+@Schema(description = "用户支付订单信息视图对象")
+public class UserOrderInfoVO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "主键ID")
+    private Long id;
+    @Schema(description = "用户ID")
+    private Long userId;
+    @Schema(description = "用户openId")
+    private String openid;
+    @Schema(description = "订单编号")
+    private String orderNo;
+    @Schema(description = "订单金额")
+    private BigDecimal orderMoney;
+    @Schema(description = "订单剩余金额")
+    private BigDecimal lastMoney;
+    @Schema(description = "支付时间")
+    private LocalDateTime payTime;
+    @Schema(description = "支付档ID")
+    private Long levelId;
+    @Schema(description = "第三方支付单号")
+    private String outTradeNo;
+    @Schema(description = "订单状态 1 待支付  2 已支付 3 已取消 4 已退款")
+    private Integer orderStatus;
+    @Schema(description = "订单类型 1 微信 2 第三方")
+    private Integer orderType;
+    @Schema(description = "微信支付流水号")
+    private String transactionId;
+    @Schema(description = "退款金额")
+    private BigDecimal refundMoney;
+    @Schema(description = "退款时间")
+    private LocalDateTime refundTime;
+    @Schema(description = "备注")
+    private String remark;
+    @Schema(description = "乐观锁")
+    private Integer version;
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+    @Schema(description = "更新时间")
+    private LocalDateTime updateTime;
+    @Schema(description = "逻辑删除(0-未删除 1-已删除)")
+    private Integer isDeleted;
+}

+ 4 - 0
src/main/java/com/zsElectric/boot/business/model/vo/applet/AppletUserInfoVO.java

@@ -0,0 +1,4 @@
+package com.zsElectric.boot.business.model.vo.applet;
+
+public class AppletUserInfoVO {
+}

+ 71 - 0
src/main/java/com/zsElectric/boot/business/service/UserOrderInfoService.java

@@ -0,0 +1,71 @@
+package com.zsElectric.boot.business.service;
+
+import com.zsElectric.boot.business.model.entity.UserOrderInfo;
+import com.zsElectric.boot.business.model.form.UserOrderInfoForm;
+import com.zsElectric.boot.business.model.form.applet.LevelOrderForm;
+import com.zsElectric.boot.business.model.form.applet.UserPayForm;
+import com.zsElectric.boot.business.model.query.UserOrderInfoQuery;
+import com.zsElectric.boot.business.model.vo.UserOrderInfoVO;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.util.Map;
+
+/**
+ * 用户支付订单信息服务类
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+public interface UserOrderInfoService extends IService<UserOrderInfo> {
+
+    /**
+     * 用户支付订单信息分页列表
+     *
+     * @return {@link IPage<UserOrderInfoVO>} 用户支付订单信息分页列表
+     */
+    IPage<UserOrderInfoVO> getUserOrderInfoPage(UserOrderInfoQuery queryParams);
+
+    /**
+     * 获取用户支付订单信息表单数据
+     *
+     * @param id 用户支付订单信息ID
+     * @return 用户支付订单信息表单数据
+     */
+    UserOrderInfoForm getUserOrderInfoFormData(Long id);
+
+    /**
+     * 新增用户支付订单信息
+     *
+     * @param formData 用户支付订单信息表单对象
+     * @return 是否新增成功
+     */
+    boolean saveUserOrderInfo(UserOrderInfoForm formData);
+
+    /**
+     * 修改用户支付订单信息
+     *
+     * @param id       用户支付订单信息ID
+     * @param formData 用户支付订单信息表单对象
+     * @return 是否修改成功
+     */
+    boolean updateUserOrderInfo(Long id, UserOrderInfoForm formData);
+
+    /**
+     * 删除用户支付订单信息
+     *
+     * @param ids 用户支付订单信息ID,多个以英文逗号(,)分割
+     * @return 是否删除成功
+     */
+    boolean deleteUserOrderInfos(String ids);
+
+    UserPayForm createOrder(LevelOrderForm levelOrderForm);
+
+    UserPayForm payOrder(String orderId);
+
+    String orderQuery(String orderNo);
+
+    Map<String, String> wechatPayNotify(HttpServletRequest request, HttpServletResponse response);
+}

+ 7 - 10
src/main/java/com/zsElectric/boot/business/service/impl/UserInfoServiceImpl.java

@@ -1,5 +1,7 @@
 package com.zsElectric.boot.business.service.impl;
 
+import cn.hutool.core.util.ObjectUtil;
+import com.zsElectric.boot.security.util.SecurityUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -113,23 +115,18 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
     @Override
     public UserInfoVO getCurrentUserInfo() {
         // 从 Spring Security 上下文获取当前登录用户的用户名(即手机号)
-        String mobile = com.zsElectric.boot.security.util.SecurityUtils.getUsername();
+        Long userId = SecurityUtils.getUserId();
         
-        log.info("获取当前用户信息,手机号: {}", mobile);
+        log.info("获取当前用户信息,userId: {}", userId);
         
-        if (StrUtil.isBlank(mobile)) {
+        if (ObjectUtil.isNull(userId)) {
             log.warn("未获取到当前登录用户信息");
             return null;
         }
-        
-        // 根据手机号查询 UserInfo
-        UserInfo userInfo = this.getOne(
-                new LambdaQueryWrapper<UserInfo>()
-                        .eq(UserInfo::getPhone, mobile)
-        );
+
+        UserInfo userInfo = this.getById(userId);
         
         if (userInfo == null) {
-            log.warn("未找到手机号 {} 对应的用户信息", mobile);
             return null;
         }
         

+ 369 - 0
src/main/java/com/zsElectric/boot/business/service/impl/UserOrderInfoServiceImpl.java

@@ -0,0 +1,369 @@
+package com.zsElectric.boot.business.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.google.gson.JsonObject;
+import com.zsElectric.boot.business.mapper.RechargeLevelMapper;
+import com.zsElectric.boot.business.model.entity.RechargeLevel;
+import com.zsElectric.boot.business.model.form.applet.LevelOrderForm;
+import com.zsElectric.boot.business.model.form.applet.UserPayForm;
+import com.zsElectric.boot.business.model.vo.UserInfoVO;
+import com.zsElectric.boot.business.service.RechargeLevelService;
+import com.zsElectric.boot.business.service.UserInfoService;
+import com.zsElectric.boot.common.constant.SystemConstants;
+import com.zsElectric.boot.core.pay.WXPayUtility;
+import com.zsElectric.boot.core.pay.WechatConstants;
+import com.zsElectric.boot.core.pay.WechatPayV3Utils;
+import com.zsElectric.boot.core.pay.WechatUrlConstants;
+import com.zsElectric.boot.security.util.SecurityUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zsElectric.boot.business.mapper.UserOrderInfoMapper;
+import com.zsElectric.boot.business.service.UserOrderInfoService;
+import com.zsElectric.boot.business.model.entity.UserOrderInfo;
+import com.zsElectric.boot.business.model.form.UserOrderInfoForm;
+import com.zsElectric.boot.business.model.query.UserOrderInfoQuery;
+import com.zsElectric.boot.business.model.vo.UserOrderInfoVO;
+import com.zsElectric.boot.business.converter.UserOrderInfoConverter;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * 用户支付订单信息服务实现类
+ *
+ * @author zsElectric
+ * @since 2025-12-16 16:25
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UserOrderInfoServiceImpl extends ServiceImpl<UserOrderInfoMapper, UserOrderInfo> implements UserOrderInfoService {
+
+    private final UserOrderInfoConverter userOrderInfoConverter;
+
+    private final UserInfoService userInfoService;
+
+    private final RechargeLevelMapper rechargeLevelMapper;
+
+    private final WechatPayV3Utils wechatPayV3Utils;
+
+    // 声明一个可重入锁
+    private final ReentrantLock lock = new ReentrantLock();
+
+    /**
+     * 获取用户支付订单信息分页列表
+     *
+     * @param queryParams 查询参数
+     * @return {@link IPage<UserOrderInfoVO>} 用户支付订单信息分页列表
+     */
+    @Override
+    public IPage<UserOrderInfoVO> getUserOrderInfoPage(UserOrderInfoQuery queryParams) {
+        Page<UserOrderInfoVO> pageVO = this.baseMapper.getUserOrderInfoPage(
+                new Page<>(queryParams.getPageNum(), queryParams.getPageSize()),
+                queryParams
+        );
+        return pageVO;
+    }
+
+    /**
+     * 获取用户支付订单信息表单数据
+     *
+     * @param id 用户支付订单信息ID
+     * @return 用户支付订单信息表单数据
+     */
+    @Override
+    public UserOrderInfoForm getUserOrderInfoFormData(Long id) {
+        UserOrderInfo entity = this.getById(id);
+        return userOrderInfoConverter.toForm(entity);
+    }
+
+    /**
+     * 新增用户支付订单信息
+     *
+     * @param formData 用户支付订单信息表单对象
+     * @return 是否新增成功
+     */
+    @Override
+    public boolean saveUserOrderInfo(UserOrderInfoForm formData) {
+        UserOrderInfo entity = userOrderInfoConverter.toEntity(formData);
+        return this.save(entity);
+    }
+
+    /**
+     * 更新用户支付订单信息
+     *
+     * @param id       用户支付订单信息ID
+     * @param formData 用户支付订单信息表单对象
+     * @return 是否修改成功
+     */
+    @Override
+    public boolean updateUserOrderInfo(Long id, UserOrderInfoForm formData) {
+        UserOrderInfo entity = userOrderInfoConverter.toEntity(formData);
+        return this.updateById(entity);
+    }
+
+    /**
+     * 删除用户支付订单信息
+     *
+     * @param ids 用户支付订单信息ID,多个以英文逗号(,)分割
+     * @return 是否删除成功
+     */
+    @Override
+    public boolean deleteUserOrderInfos(String ids) {
+        Assert.isTrue(StrUtil.isNotBlank(ids), "删除的用户支付订单信息数据为空");
+        // 逻辑删除
+        List<Long> idList = Arrays.stream(ids.split(","))
+                .map(Long::parseLong)
+                .toList();
+        return this.removeByIds(idList);
+    }
+
+    @Override
+    public UserPayForm createOrder(LevelOrderForm levelOrderForm) {
+        Long userId = SecurityUtils.getUserId();
+        String userOpenId = userInfoService.getCurrentUserInfo().getOpenid();
+        String orderNo = createOrderNo("SP",userId);
+        //创建订单
+        UserOrderInfo orderInfo = new UserOrderInfo();
+        orderInfo.setUserId(userId);
+        orderInfo.setOrderNo(orderNo);
+        orderInfo.setLevelId(levelOrderForm.getLevelId());
+        orderInfo.setOpenid(userOpenId);
+
+        this.save(orderInfo);
+
+        //构建支付表单返回给前端支撑JsApi支付调用
+        UserPayForm payForm = new UserPayForm();
+        payForm.setOrderId(orderInfo.getId()).setOrderNo(orderNo);
+
+        //查询档位
+        RechargeLevel level = rechargeLevelMapper.selectById(levelOrderForm.getLevelId());
+
+        Map<String, Object> result = payment(userOpenId,orderNo,level.getMoney());
+        payForm.setParams(result);
+        return payForm;
+    }
+
+    @Override
+    public UserPayForm payOrder(String orderId) {
+        UserOrderInfo orderInfo = this.getById(orderId);
+        //构建支付表单
+        UserPayForm payForm = new UserPayForm();
+        payForm.setOrderId(orderInfo.getId()).setOrderNo(orderInfo.getOrderNo());
+
+        //查询档位
+        RechargeLevel level = rechargeLevelMapper.selectById(orderInfo.getLevelId());
+
+        Map<String, Object> result = payment(orderInfo.getOpenid(),orderInfo.getOrderNo(),level.getMoney());
+        payForm.setParams(result);
+        return payForm;
+    }
+
+    @Override
+    public String orderQuery(String orderNo) {
+        //查询订单
+        UserOrderInfo orderInfo = this.getById(Wrappers.<UserOrderInfo>lambdaQuery().eq(UserOrderInfo::getOrderNo, orderNo).last("limit 1"));
+        if (ObjectUtil.isEmpty(orderInfo)) {
+            throw new RuntimeException("当前订单不存在");
+        }
+        //null代表查询失败 SUCCESS-成功 USERPAYING和ACCEPT为中间态 其他为支付失败
+        JsonObject res = orderQueryByOutTradeNo(orderNo, WechatConstants.WECHAT_MCH_ID);
+        String s = res == null ? null : res.get("trade_state").getAsString();
+//        String s = "SUCCESS";
+        if ("SUCCESS".equals(s)) {
+            if (ObjectUtil.isNotEmpty(orderInfo) && Objects.equals(orderInfo.getOrderStatus(), SystemConstants.STATUS_ONE)) {
+                orderInfo.setOrderStatus(SystemConstants.STATUS_TWO);
+                orderInfo.setPayTime(LocalDateTime.now());
+                orderInfo.setOutTradeNo(res.get("transaction_id").getAsString());
+                this.updateById(orderInfo);
+            }
+            return "100001";//支付成功
+        }
+        if (s == null) {
+            //查询订单
+            return "100002";//查询失败
+        }
+        if ("USERPAYING".equals(s) || "ACCEPT".equals(s)) {
+            //查询订单
+            return "100003";//查询中
+        }
+        return "100004";//支付失败
+    }
+
+    @Override
+    public Map<String, String> wechatPayNotify(HttpServletRequest request, HttpServletResponse response) {
+
+        Map<String, String> result = new HashMap<>(2);
+        //验签及解析返回数据
+        JsonObject res = wechatPayV3Utils.getCallbackData(request);
+        if (res == null) {
+            result.put("code", "FAIL");
+            result.put("message", "失败");
+            return result;
+        }
+        log.info("最终拿到的微信支付通知数据:" + res);
+        String orderNo = res.get("out_trade_no").getAsString();
+        if (lock.tryLock()) {
+            // 处理支付成功后的业务 例如 将订单状态修改为已支付 具体参数键值可参考文档 注意!!! 微信可能会多次发送重复的通知 因此要判断业务是否已经处理过了 避免重复处理
+            try {
+                //查询订单,判断是否已修改为已支付状态
+                UserOrderInfo orderInfo = this.getOne(Wrappers.<UserOrderInfo>lambdaQuery().eq(UserOrderInfo::getOrderNo, orderNo).last("limit 1"));
+
+                if (ObjectUtil.isNotEmpty(orderInfo)) {
+                    if (Objects.equals(orderInfo.getOrderStatus(), SystemConstants.STATUS_TWO)) {
+                        result.put("code", "SUCCESS");
+                        result.put("message", "OK");
+                        return result;
+                    }
+                    if (Objects.equals(orderInfo.getOrderStatus(), SystemConstants.STATUS_ONE)) {
+                        orderInfo.setOrderStatus(SystemConstants.STATUS_TWO);
+                        orderInfo.setOrderType(SystemConstants.STATUS_ONE);
+                        orderInfo.setTransactionId(res.get("transaction_id").getAsString());
+                        orderInfo.setPayTime(LocalDateTime.now());
+//                        orderInfo.setPayTime(dealDateFormat(res.getString("success_time")));
+
+                        this.updateById(orderInfo);
+                        //todo 发送延迟消息 15分钟超时未支付
+
+                        //todo 异步增加积分
+                    }
+                }
+                result.put("code", "SUCCESS");
+                result.put("message", "OK");
+                return result;
+            } catch (Exception e) {
+                log.error("微信支付回调异常:" + e.getMessage());
+                result.put("code", "FAIL");
+                result.put("message", "失败");
+                return result;
+            }finally {
+                lock.unlock();
+            }
+        }else {
+            result.put("code", "FAIL");
+            result.put("message", "失败");
+            return result;
+        }
+    }
+
+    /**
+     * 通过商户订单号查询订单在微信侧支付状态
+     *
+     * @param out_trade_no 发起支付时创建的商户订单号
+     * @return null代表查询失败 SUCCESS-成功 USERPAYING和ACCEPT为中间态 其他为支付失败
+     */
+    public JsonObject orderQueryByOutTradeNo(String out_trade_no, String subMchId) {
+        String url = WechatUrlConstants.PAY_V3_QUERY_OUT;
+        url = url.replace("{out_trade_no}", WXPayUtility.urlEncode(out_trade_no));
+        Map<String, Object> args = new HashMap<>();
+        args.put("sub_mchid", subMchId);
+        url = url + "?" + WXPayUtility.urlEncode(args);
+        return wechatPayV3Utils.sendGet(url);
+    }
+
+    /**
+     * 构建支付表单返回给前端支撑JsApi支付调用
+     * @param openId
+     * @param orderNo
+     * @param amount
+     * @return
+     */
+    private Map<String, Object> payment(String openId, String orderNo,BigDecimal amount) {
+        //15分钟超时限制
+        Calendar calendar = Calendar.getInstance();
+        calendar.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE) + 15);
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
+        //构建微信支付参数
+        Map<String, Object> params = new HashMap<>();
+        params.put("appid", WechatConstants.WECHAT_MP_APPID); //小程序appid
+        params.put("mchid", WechatConstants.WECHAT_MCH_ID); //商户号
+        params.put("description", "订单业务"); //商品描述
+        params.put("out_trade_no", orderNo); //商户订单号
+        params.put("time_expire", sdf.format(calendar.getTime())); //交易结束时间 选填 时间到了之后将不能再支付 遵循rfc3339标准格式
+        params.put("attach", orderNo); //附加数据 选填
+        // 在查询API和支付通知中原样返回 可作为自定义参数使用
+        params.put("notify_url", WechatUrlConstants.PAY_V3_NOTIFY); //支付结果异步通知接口
+
+        //订单金额信息
+        Map<String, Object> amount_json = new HashMap<>();
+        //支付金额 单位:分
+//        amount_json.put("total", Integer.parseInt(amount_fee(Double.valueOf("0.1"))));测试用例
+        amount_json.put("total", Integer.parseInt(amount_fee(amount)));
+        params.put("amount", amount_json);
+
+        //支付者信息
+        Map<String, Object> payer = new HashMap<>();
+        //用户在小程序侧的openid
+        payer.put("openid", openId);
+        params.put("payer", payer);
+
+        return params;
+    }
+
+    /**
+     * 创建商户订单号
+     * 要求 32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一
+     * 组成 两位前缀 + 17位时间戳 + 9位id补零 + 4位随机数 合计32位
+     *
+     * @param head 例如 商品-SP 退款-TK 等等
+     * @param id   用户id
+     * @return
+     */
+    public String createOrderNo(String head, Long id) {
+        StringBuilder uid = new StringBuilder(id.toString());
+        Date date = new Date();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
+        int length = uid.length();
+        for (int i = 0; i < 9 - length; i++) {
+            uid.insert(0, "0");
+        }
+        return head + sdf.format(date) + uid + (int) ((Math.random() * 9 + 1) * 1000);
+    }
+
+    /**
+     * 金额元转分字符串
+     *
+     * @param cny 元
+     * @return
+     */
+    public String amount_fee(BigDecimal cny) {
+        BigDecimal b2 = new BigDecimal("100");
+        return cny.multiply(b2).setScale(0, RoundingMode.DOWN).toString();
+    }
+
+    public static String amount_fee(double price) {
+        DecimalFormat df = new DecimalFormat("#.00");
+        price = Double.valueOf(df.format(price));
+        int money = (int) (price * 100);
+        return money + "";
+    }
+
+    /**
+     * 分转元,转换为bigDecimal在转成double
+     *
+     * @return
+     */
+    public static double changeF2Y3(int price) {
+        return BigDecimal.valueOf(Long.valueOf(price)).divide(new BigDecimal("100")).doubleValue();
+    }
+
+}

+ 11 - 0
src/main/java/com/zsElectric/boot/common/constant/SystemConstants.java

@@ -29,4 +29,15 @@ public interface SystemConstants {
      */
     String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
 
+    /**
+     * 状态
+     */
+    Integer STATUS_ZERO = 0;
+
+    Integer STATUS_ONE = 1;
+
+    Integer STATUS_TWO = 2;
+
+    Integer STATUS_THREE = 3;
+
 }

+ 675 - 0
src/main/java/com/zsElectric/boot/core/pay/WXPayUtility.java

@@ -0,0 +1,675 @@
+package com.zsElectric.boot.core.pay;
+
+import com.google.gson.*;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import okhttp3.Headers;
+import okhttp3.Response;
+import okio.BufferedSource;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Objects;
+
+public class WXPayUtility {
+    private static final Gson gson = new GsonBuilder()
+            .disableHtmlEscaping()
+            .addSerializationExclusionStrategy(new ExclusionStrategy() {
+                @Override
+                public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+                    final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+                    return expose != null && !expose.serialize();
+                }
+
+                @Override
+                public boolean shouldSkipClass(Class<?> aClass) {
+                    return false;
+                }
+            })
+            .addDeserializationExclusionStrategy(new ExclusionStrategy() {
+                @Override
+                public boolean shouldSkipField(FieldAttributes fieldAttributes) {
+                    final Expose expose = fieldAttributes.getAnnotation(Expose.class);
+                    return expose != null && !expose.deserialize();
+                }
+
+                @Override
+                public boolean shouldSkipClass(Class<?> aClass) {
+                    return false;
+                }
+            })
+            .create();
+    private static final char[] SYMBOLS =
+            "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
+    private static final SecureRandom random = new SecureRandom();
+
+    /**
+     * 将 Object 转换为 JSON 字符串
+     */
+    public static String toJson(Object object) {
+        return gson.toJson(object);
+    }
+
+    /**
+     * 将 JSON 字符串解析为特定类型的实例
+     */
+    public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
+        return gson.fromJson(json, classOfT);
+    }
+
+    /**
+     * 从公私钥文件路径中读取文件内容
+     *
+     * @param keyPath 文件路径
+     * @return 文件内容
+     */
+    private static String readKeyStringFromPath(String keyPath) {
+        try {
+            return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    /**
+     * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
+     *
+     * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
+     * @return PrivateKey 对象
+     */
+    public static PrivateKey loadPrivateKeyFromString(String keyString) {
+        try {
+            keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
+                    .replace("-----END PRIVATE KEY-----", "")
+                    .replaceAll("\\s+", "");
+            return KeyFactory.getInstance("RSA").generatePrivate(
+                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(e);
+        } catch (InvalidKeySpecException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * 从 PKCS#8 格式的私钥文件中加载私钥
+     *
+     * @param keyPath 私钥文件路径
+     * @return PrivateKey 对象
+     */
+    public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
+        return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
+    }
+
+    /**
+     * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
+     *
+     * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
+     * @return PublicKey 对象
+     */
+    public static PublicKey loadPublicKeyFromString(String keyString) {
+        try {
+            keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
+                    .replace("-----END PUBLIC KEY-----", "")
+                    .replaceAll("\\s+", "");
+            return KeyFactory.getInstance("RSA").generatePublic(
+                    new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException(e);
+        } catch (InvalidKeySpecException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * 从 PKCS#8 格式的公钥文件中加载公钥
+     *
+     * @param keyPath 公钥文件路径
+     * @return PublicKey 对象
+     */
+    public static PublicKey loadPublicKeyFromPath(String keyPath) {
+        return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
+    }
+
+    /**
+     * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
+     */
+    public static String createNonce(int length) {
+        char[] buf = new char[length];
+        for (int i = 0; i < length; ++i) {
+            buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
+        }
+        return new String(buf);
+    }
+
+    /**
+     * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
+     *
+     * @param publicKey 加密用公钥对象
+     * @param plaintext 待加密明文
+     * @return 加密后密文
+     */
+    public static String encrypt(PublicKey publicKey, String plaintext) {
+        final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
+
+        try {
+            Cipher cipher = Cipher.getInstance(transformation);
+            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
+            return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
+        } catch (BadPaddingException | IllegalBlockSizeException e) {
+            throw new IllegalArgumentException("Plaintext is too long", e);
+        }
+    }
+
+    public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce,
+                                        byte[] ciphertext) {
+        final String transformation = "AES/GCM/NoPadding";
+        final String algorithm = "AES";
+        final int tagLengthBit = 128;
+
+        try {
+            Cipher cipher = Cipher.getInstance(transformation);
+            cipher.init(
+                    Cipher.DECRYPT_MODE,
+                    new SecretKeySpec(key, algorithm),
+                    new GCMParameterSpec(tagLengthBit, nonce));
+            if (associatedData != null) {
+                cipher.updateAAD(associatedData);
+            }
+            return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
+        } catch (InvalidKeyException
+                 | InvalidAlgorithmParameterException
+                 | BadPaddingException
+                 | IllegalBlockSizeException
+                 | NoSuchAlgorithmException
+                 | NoSuchPaddingException e) {
+            throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed",
+                    transformation), e);
+        }
+    }
+
+    /**
+     * 使用私钥按照指定算法进行签名
+     *
+     * @param message    待签名串
+     * @param algorithm  签名算法,如 SHA256withRSA
+     * @param privateKey 签名用私钥对象
+     * @return 签名结果
+     */
+    public static String sign(String message, String algorithm, PrivateKey privateKey) {
+        byte[] sign;
+        try {
+            Signature signature = Signature.getInstance(algorithm);
+            signature.initSign(privateKey);
+            signature.update(message.getBytes(StandardCharsets.UTF_8));
+            sign = signature.sign();
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
+        } catch (SignatureException e) {
+            throw new RuntimeException("An error occurred during the sign process.", e);
+        }
+        return Base64.getEncoder().encodeToString(sign);
+    }
+
+    /**
+     * 使用公钥按照特定算法验证签名
+     *
+     * @param message   待签名串
+     * @param signature 待验证的签名内容
+     * @param algorithm 签名算法,如:SHA256withRSA
+     * @param publicKey 验签用公钥对象
+     * @return 签名验证是否通过
+     */
+    public static boolean verify(String message, String signature, String algorithm,
+                                 PublicKey publicKey) {
+        try {
+            Signature sign = Signature.getInstance(algorithm);
+            sign.initVerify(publicKey);
+            sign.update(message.getBytes(StandardCharsets.UTF_8));
+            return sign.verify(Base64.getDecoder().decode(signature));
+        } catch (SignatureException e) {
+            return false;
+        } catch (InvalidKeyException e) {
+            throw new IllegalArgumentException("verify uses an illegal publickey.", e);
+        } catch (NoSuchAlgorithmException e) {
+            throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3请求签名规则构造 Authorization 签名
+     *
+     * @param mchid               商户号
+     * @param certificateSerialNo 商户API证书序列号
+     * @param privateKey          商户API证书私钥
+     * @param method              请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
+     * @param uri                 请求接口的URL
+     * @param body                请求接口的Body
+     * @return 构造好的微信支付APIv3 Authorization 头
+     */
+    public static String buildAuthorization(String mchid, String certificateSerialNo,
+                                            PrivateKey privateKey,
+                                            String method, String uri, String body) {
+        String nonce = createNonce(32);
+        long timestamp = Instant.now().getEpochSecond();
+
+        String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
+                body == null ? "" : body);
+
+        String signature = sign(message, "SHA256withRSA", privateKey);
+
+        return String.format(
+                "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
+                        "timestamp=\"%d\",serial_no=\"%s\"",
+                mchid, nonce, signature, timestamp, certificateSerialNo);
+    }
+
+    /**
+     * 对参数进行 URL 编码
+     *
+     * @param content 参数内容
+     * @return 编码后的内容
+     */
+    public static String urlEncode(String content) {
+        try {
+            return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 对参数Map进行 URL 编码,生成 QueryString
+     *
+     * @param params Query参数Map
+     * @return QueryString
+     */
+    public static String urlEncode(Map<String, Object> params) {
+        if (params == null || params.isEmpty()) {
+            return "";
+        }
+
+        int index = 0;
+        StringBuilder result = new StringBuilder();
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
+            if (entry.getValue() == null) {
+                continue;
+            }
+            result.append(entry.getKey())
+                    .append("=")
+                    .append(urlEncode(entry.getValue().toString()));
+            index++;
+            if (index < params.size()) {
+                result.append("&");
+            }
+        }
+        return result.toString();
+    }
+
+    /**
+     * 从应答中提取 Body
+     *
+     * @param response HTTP 请求应答对象
+     * @return 应答中的Body内容,Body为空时返回空字符串
+     */
+    public static String extractBody(Response response) {
+        if (response.body() == null) {
+            return "";
+        }
+
+        try {
+            BufferedSource source = response.body().source();
+            return source.readUtf8();
+        } catch (IOException e) {
+            throw new RuntimeException(String.format("An error occurred during reading response body. " +
+                    "Status: %d", response.code()), e);
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
+     *
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey   微信支付公钥对象
+     * @param headers              微信支付应答 Header 列表
+     * @param body                 微信支付应答 Body
+     */
+    public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
+                                        Headers headers,
+                                        String body) {
+        String timestamp = headers.get("Wechatpay-Timestamp");
+        String requestId = headers.get("Request-ID");
+        try {
+            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+            // 拒绝过期请求
+            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+                throw new IllegalArgumentException(
+                        String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]",
+                                timestamp, requestId));
+            }
+        } catch (DateTimeException | NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]",
+                            timestamp, requestId));
+        }
+        String serialNumber = headers.get("Wechatpay-Serial");
+        if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " +
+                            "%s", wechatpayPublicKeyId, serialNumber));
+        }
+
+        String signature = headers.get("Wechatpay-Signature");
+        String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+                body == null ? "" : body);
+
+        boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+        if (!success) {
+            throw new IllegalArgumentException(
+                    String.format("Validate response failed,the WechatPay signature is incorrect.%n"
+                                    + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
+                            headers.get("Request-ID"), headers, body));
+        }
+    }
+
+    /**
+     * 根据微信支付APIv3通知验签规则对通知签名进行验证,验证不通过时抛出异常
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey 微信支付公钥对象
+     * @param headers 微信支付通知 Header 列表
+     * @param body 微信支付通知 Body
+     */
+    public static void validateNotification(String wechatpayPublicKeyId,
+                                            PublicKey wechatpayPublicKey, Headers headers,
+                                            String body) {
+        String timestamp = headers.get("Wechatpay-Timestamp");
+        try {
+            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
+            // 拒绝过期请求
+            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
+                throw new IllegalArgumentException(
+                        String.format("Validate notification failed, timestamp[%s] is expired", timestamp));
+            }
+        } catch (DateTimeException | NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, timestamp[%s] is invalid", timestamp));
+        }
+        String serialNumber = headers.get("Wechatpay-Serial");
+        if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " +
+                                    "Remote: %s",
+                            wechatpayPublicKeyId,
+                            serialNumber));
+        }
+
+        String signature = headers.get("Wechatpay-Signature");
+        String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
+                body == null ? "" : body);
+
+        boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
+        if (!success) {
+            throw new IllegalArgumentException(
+                    String.format("Validate notification failed, WechatPay signature is incorrect.\n"
+                                    + "responseHeader[%s]\tresponseBody[%.1024s]",
+                            headers, body));
+        }
+    }
+
+    /**
+     * 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常
+     * @param apiv3Key 商户的 APIv3 Key
+     * @param wechatpayPublicKeyId 微信支付公钥ID
+     * @param wechatpayPublicKey   微信支付公钥对象
+     * @param headers              微信支付应答 Header 列表
+     * @param body                 微信支付应答 Body
+     * @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问
+     */
+    public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId,
+                                                 PublicKey wechatpayPublicKey, Headers headers,
+                                                 String body) {
+        validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body);
+        Notification notification = gson.fromJson(body, Notification.class);
+        notification.decrypt(apiv3Key);
+        return notification;
+    }
+
+    /**
+     * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
+     */
+    public static class ApiException extends RuntimeException {
+        private static final long serialVersionUID = 2261086748874802175L;
+
+        private final int statusCode;
+        private final String body;
+        private final Headers headers;
+        private final String errorCode;
+        private final String errorMessage;
+
+        public ApiException(int statusCode, String body, Headers headers) {
+            super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode,
+                    body, headers));
+            this.statusCode = statusCode;
+            this.body = body;
+            this.headers = headers;
+
+            if (body != null && !body.isEmpty()) {
+                JsonElement code;
+                JsonElement message;
+
+                try {
+                    JsonObject jsonObject = gson.fromJson(body, JsonObject.class);
+                    code = jsonObject.get("code");
+                    message = jsonObject.get("message");
+                } catch (JsonSyntaxException ignored) {
+                    code = null;
+                    message = null;
+                }
+                this.errorCode = code == null ? null : code.getAsString();
+                this.errorMessage = message == null ? null : message.getAsString();
+            } else {
+                this.errorCode = null;
+                this.errorMessage = null;
+            }
+        }
+
+        /**
+         * 获取 HTTP 应答状态码
+         */
+        public int getStatusCode() {
+            return statusCode;
+        }
+
+        /**
+         * 获取 HTTP 应答包体内容
+         */
+        public String getBody() {
+            return body;
+        }
+
+        /**
+         * 获取 HTTP 应答 Header
+         */
+        public Headers getHeaders() {
+            return headers;
+        }
+
+        /**
+         * 获取 错误码 (错误应答中的 code 字段)
+         */
+        public String getErrorCode() {
+            return errorCode;
+        }
+
+        /**
+         * 获取 错误消息 (错误应答中的 message 字段)
+         */
+        public String getErrorMessage() {
+            return errorMessage;
+        }
+    }
+
+    public static class Notification {
+        @SerializedName("id")
+        private String id;
+        @SerializedName("create_time")
+        private String createTime;
+        @SerializedName("event_type")
+        private String eventType;
+        @SerializedName("resource_type")
+        private String resourceType;
+        @SerializedName("summary")
+        private String summary;
+        @SerializedName("resource")
+        private Resource resource;
+        private String plaintext;
+
+        public String getId() {
+            return id;
+        }
+
+        public String getCreateTime() {
+            return createTime;
+        }
+
+        public String getEventType() {
+            return eventType;
+        }
+
+        public String getResourceType() {
+            return resourceType;
+        }
+
+        public String getSummary() {
+            return summary;
+        }
+
+        public Resource getResource() {
+            return resource;
+        }
+
+        /**
+         * 获取解密后的业务数据(JSON字符串,需要自行解析)
+         */
+        public String getPlaintext() {
+            return plaintext;
+        }
+
+        private void validate() {
+            if (resource == null) {
+                throw new IllegalArgumentException("Missing required field `resource` in notification");
+            }
+            resource.validate();
+        }
+
+        /**
+         * 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。
+         * 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public
+         * @param apiv3Key 商户APIv3 Key
+         */
+        private void decrypt(String apiv3Key) {
+            validate();
+
+            plaintext = aesAeadDecrypt(
+                    apiv3Key.getBytes(StandardCharsets.UTF_8),
+                    resource.associatedData.getBytes(StandardCharsets.UTF_8),
+                    resource.nonce.getBytes(StandardCharsets.UTF_8),
+                    Base64.getDecoder().decode(resource.ciphertext)
+            );
+        }
+
+        public static class Resource {
+            @SerializedName("algorithm")
+            private String algorithm;
+
+            @SerializedName("ciphertext")
+            private String ciphertext;
+
+            @SerializedName("associated_data")
+            private String associatedData;
+
+            @SerializedName("nonce")
+            private String nonce;
+
+            @SerializedName("original_type")
+            private String originalType;
+
+            public String getAlgorithm() {
+                return algorithm;
+            }
+
+            public String getCiphertext() {
+                return ciphertext;
+            }
+
+            public String getAssociatedData() {
+                return associatedData;
+            }
+
+            public String getNonce() {
+                return nonce;
+            }
+
+            public String getOriginalType() {
+                return originalType;
+            }
+
+            private void validate() {
+                if (algorithm == null || algorithm.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `algorithm` in Notification" +
+                            ".Resource");
+                }
+                if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) {
+                    throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " +
+                            "Notification.Resource", algorithm));
+                }
+
+                if (ciphertext == null || ciphertext.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" +
+                            ".Resource");
+                }
+
+                if (associatedData == null || associatedData.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `associatedData` in " +
+                            "Notification.Resource");
+                }
+
+                if (nonce == null || nonce.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `nonce` in Notification" +
+                            ".Resource");
+                }
+
+                if (originalType == null || originalType.isEmpty()) {
+                    throw new IllegalArgumentException("Missing required field `originalType` in " +
+                            "Notification.Resource");
+                }
+            }
+        }
+    }
+}

+ 5 - 0
src/main/java/com/zsElectric/boot/core/pay/WechatPayV3Utils.java

@@ -1,5 +1,6 @@
 package com.zsElectric.boot.core.pay;
 
+import com.aliyun.oss.ServiceException;
 import com.google.gson.Gson;
 import com.google.gson.JsonObject;
 import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
@@ -14,6 +15,7 @@ import com.zsElectric.boot.common.util.StringUtils;
 import jakarta.annotation.PostConstruct;
 import jakarta.servlet.http.HttpServletRequest;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
@@ -28,6 +30,9 @@ import java.io.FileInputStream;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.security.PrivateKey;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 @Slf4j
 @Component

+ 5 - 5
src/main/java/com/zsElectric/boot/system/service/impl/UserServiceImpl.java

@@ -329,16 +329,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
         if (existingUserInfo != null) {
             log.info("手机号 {} 对应的UserInfo已存在,ID: {}", mobile, existingUserInfo.getId());
             // 如果存在用户但没绑定openId,且提供了openId,则绑定openId
-            if (StrUtil.isNotBlank(openId) && StrUtil.isBlank(existingUserInfo.getWechat())) {
+            if (StrUtil.isNotBlank(openId) && StrUtil.isBlank(existingUserInfo.getOpenid())) {
                 log.info("为UserInfo {} 绑定 OpenID", existingUserInfo.getId());
-                existingUserInfo.setWechat(openId);
+                existingUserInfo.setOpenid(openId);
                 existingUserInfo.setUpdateTime(LocalDateTime.now());
                 return userInfoService.updateById(existingUserInfo);
             }
             // 如果提供了不同的openId,则更新
-            else if (StrUtil.isNotBlank(openId) && !openId.equals(existingUserInfo.getWechat())) {
+            else if (StrUtil.isNotBlank(openId) && !openId.equals(existingUserInfo.getOpenid())) {
                 log.info("更新UserInfo {} 的 OpenID", existingUserInfo.getId());
-                existingUserInfo.setWechat(openId);
+                existingUserInfo.setOpenid(openId);
                 existingUserInfo.setUpdateTime(LocalDateTime.now());
                 return userInfoService.updateById(existingUserInfo);
             }
@@ -350,7 +350,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
         log.info("创建新UserInfo,手机号: {}, OpenID: {}", mobile, openId);
         com.zsElectric.boot.business.model.entity.UserInfo newUserInfo = new com.zsElectric.boot.business.model.entity.UserInfo();
         newUserInfo.setPhone(mobile);
-        newUserInfo.setWechat(openId); // openId 可以为 null
+        newUserInfo.setOpenid(openId); // openId 可以为 null
         newUserInfo.setNickName("微信用户_" + mobile.substring(mobile.length() - 4)); // 使用手机号后4位作为昵称
         newUserInfo.setIntegralNum(BigDecimal.ZERO); // 初始积分为0
         newUserInfo.setCreateTime(LocalDateTime.now());

+ 32 - 0
src/main/resources/mapper/business/UserOrderInfoMapper.xml

@@ -0,0 +1,32 @@
+<?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.zsElectric.boot.business.mapper.UserOrderInfoMapper">
+
+    <!-- 获取用户支付订单信息分页列表 -->
+    <select id="getUserOrderInfoPage" resultType="com.zsElectric.boot.business.model.vo.UserOrderInfoVO">
+        SELECT
+        id,
+        user_id,
+        order_no,
+        order_money,
+        last_money,
+        pay_time,
+        level_id,
+        out_trade_no,
+        order_status,
+        order_type,
+        app_id,
+        refund_money,
+        refund_time,
+        remark,
+        version,
+        create_time,
+        update_time,
+        is_deleted
+        FROM
+        c_user_order_info
+        <where>
+        </where>
+    </select>
+
+</mapper>