xuhy
15 小时以前 59ac31065740db24b7a242e0cbfeb5a4806da7a2
AI对接,微信小程序支付
1个文件已删除
14个文件已修改
27个文件已添加
2881 ■■■■■ 已修改文件
generator/src/test/java/com/xizang/CodeGeneratorTests.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TSysConfigController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TSysInspectionController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TSysOtherConfigController.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/TaskUtil.java 108 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/H5AICallbackController.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/TSysInspectionController.java 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application-prod.yml 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application-test.yml 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TSysOtherConfigMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TSysPayRecordMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/TSysConfig.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/TSysInspection.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/TSysOtherConfig.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/model/TSysPayRecord.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/TSysOtherConfigService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/TSysPayRecordService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TSysOtherConfigServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TSysPayRecordServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/util/AIUtil.java 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/util/H5AIUtil.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java 400 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TSysInspectionMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TSysOtherConfigMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
generator/src/test/java/com/xizang/CodeGeneratorTests.java
@@ -144,7 +144,7 @@
//         strategy.setTablePrefix(pc.getModuleName() + "");
//        strategy.setLikeTable(new LikeTable("room"));
        //strategy.setLikeTable(new LikeTable("member"));
        strategy.setLikeTable(new LikeTable("t_sys_ai_config"));// 生成表名
        strategy.setLikeTable(new LikeTable("t_sys_pay_record"));// 生成表名
//        strategy.setLikeTable(new LikeTable("t_hotel"));// 生成表名
//        strategy.setLikeTable(new LikeTable("t_scan_message"));// 生成表名
//        strategy.setNotLikeTable(new LikeTable("hotel_info"));// 不生成表名
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TSysConfigController.java
@@ -17,13 +17,13 @@
/**
 * <p>
 * 系统配置-协议管理-其他设置 前端控制器
 * 系统配置-协议管理 前端控制器
 * </p>
 *
 * @author xiaochen
 * @since 2025-08-20
 */
@Api(tags = "系统配置-协议管理-其他设置")
@Api(tags = "系统配置-协议管理")
@RestController
@RequestMapping("/t-sys-config")
public class TSysConfigController {
@@ -33,19 +33,19 @@
        this.sysConfigService = sysConfigService;
    }
    /**
     * 修改系统配置-协议管理-其他设置
     * 修改系统配置-协议管理
     */
    @Log(title = "系统配置-协议管理-其他设置-修改系统配置-协议管理-其他设置管理", businessType = BusinessType.UPDATE)
    @ApiOperation(value = "修改系统配置-协议管理-其他设置管理")
    @Log(title = "系统配置-协议管理-修改系统配置-协议管理管理", businessType = BusinessType.UPDATE)
    @ApiOperation(value = "修改系统配置-协议管理管理")
    @PostMapping(value = "/update")
    public R<Boolean> update(@Validated @RequestBody TSysConfig dto) {
        return R.ok(sysConfigService.updateById(dto));
    }
    /**
     * 查看系统配置-协议管理-其他设置详情
     * 查看系统配置-协议管理详情
     */
    @ApiOperation(value = "查看系统配置-协议管理-其他设置详情",notes = "配置类型 1=用户协议 2=隐私协议 3=其他设置")
    @ApiOperation(value = "查看系统配置-协议管理详情",notes = "配置类型 1=用户协议 2=隐私协议")
    @GetMapping(value = "/getDetailById")
    public R<TSysConfig> getDetailById(@RequestParam Integer configType) {
        TSysConfig sysConfig = sysConfigService.getOne(Wrappers.<TSysConfig>lambdaQuery().eq(TSysConfig::getConfigType, configType)
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TSysInspectionController.java
@@ -59,6 +59,7 @@
    @ApiOperation(value = "添加用户检测信息管理")
    @PostMapping(value = "/add")
    public R<Boolean> add(@Validated @RequestBody TSysInspection dto) {
        dto.setIsPay(1);
        return R.ok(sysInspectionService.save(dto));
    }
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TSysOtherConfigController.java
New file
@@ -0,0 +1,52 @@
package com.ruoyi.web.controller.api;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.model.TSysOtherConfig;
import com.ruoyi.system.service.TSysOtherConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
 * <p>
 * 其他设置 前端控制器
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
@Api(tags = "其他设置")
@RestController
@RequestMapping("/t-sys-other-config")
public class TSysOtherConfigController {
    private final TSysOtherConfigService sysOtherConfigService;
    @Autowired
    public TSysOtherConfigController(TSysOtherConfigService sysOtherConfigService) {
        this.sysOtherConfigService = sysOtherConfigService;
    }
    /**
     * 修改其他设置
     */
    @Log(title = "其他设置-修改其他设置管理", businessType = BusinessType.UPDATE)
    @ApiOperation(value = "修改其他设置管理")
    @PostMapping(value = "/update")
    public R<Boolean> update(@Validated @RequestBody TSysOtherConfig dto) {
        return R.ok(sysOtherConfigService.updateById(dto));
    }
    /**
     * 查看其他设置详情
     */
    @ApiOperation(value = "查看其他设置详情")
    @GetMapping(value = "/getDetailById")
    public R<TSysOtherConfig> getDetailById() {
        TSysOtherConfig sysConfig = sysOtherConfigService.getById(1);
        return R.ok(sysConfig);
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/TaskUtil.java
@@ -1,83 +1,25 @@
//package com.ruoyi.web.controller.task;
//
//
//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
//import com.ruoyi.common.utils.SmsUtil;
//import com.ruoyi.system.mapper.TBillMapper;
//import com.ruoyi.system.model.TBill;
//import com.ruoyi.system.model.TContract;
//import com.ruoyi.system.model.TContractRentType;
//import com.ruoyi.system.service.TBillService;
//import com.ruoyi.system.service.TContractRentTypeService;
//import com.ruoyi.system.service.TContractService;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.scheduling.annotation.Scheduled;
//import org.springframework.stereotype.Component;
//
//import javax.annotation.Resource;
//import java.math.BigDecimal;
//import java.time.LocalDate;
//import java.time.LocalDateTime;
//import java.time.LocalTime;
//import java.time.ZoneId;
//import java.time.temporal.ChronoUnit;
//import java.time.temporal.TemporalAdjusters;
//import java.util.ArrayList;
//import java.util.Date;
//import java.util.List;
//import java.util.Random;
//import java.util.stream.Collectors;
//
///**
// * @author zhibing.pu
// * @date 2023/7/11 8:39
// */
//@Component
//public class TaskUtil {
//    @Autowired
//    private TContractService contractService;
//    @Autowired
//    private TBillMapper billMapper;
//    // 用于更新违约金账单
//    // 每分钟执行一次的定时任务
//
//    @Scheduled(cron = "0 * * * * ?")
//    public void dayOfProportionBill() {
//        try {
//            // 查询所有未缴费账单
//            List<TBill> list = billMapper.selectList(new LambdaQueryWrapper<TBill>().eq(TBill::getPayFeesStatus, 1)
//                    .le(TBill::getPayableFeesTime,LocalDate.now()));
//            for (TBill tBill : list) {
//                tBill.setPayFeesStatus("4");
//                TContract contract = contractService.getById(tBill.getContractId());
//                LocalDate payableFeesTime = tBill.getPayableFeesTime();
//                // 将LocalDate转化为LocalDateTime
//                LocalDateTime payableFeesTime1 = LocalDateTime.of(payableFeesTime, LocalTime.of(0, 0, 0));
//                LocalDateTime now = LocalDateTime.now();
//                // 计算两个时间相差多少个小时
//                long hours = ChronoUnit.HOURS.between(payableFeesTime1, now);
//                long l = hours / 24;
//                if (l>=3){
//                    // 违约金比例
//                    BigDecimal proportion = contract.getProportion();
//                    // 按每天 待缴费金额 * XX% 增加违约金费用
//                    if (tBill.getOutstandingMoney().compareTo(new BigDecimal("0"))==0){
//                        tBill.setPayFeesStatus("3");
//                        billMapper.updateById(tBill);
//                        continue;
//                    }
//                    BigDecimal money = tBill.getOutstandingMoney().multiply(new BigDecimal(100).add(proportion)).divide(new BigDecimal(100),2, BigDecimal.ROUND_DOWN);
//                    tBill.setOverDays((int) l);
//                    tBill.setPayableFeesPenalty((tBill.getPayableFeesPenalty()!=null?tBill.getPayableFeesPenalty():BigDecimal.ZERO).add(money));
//                    tBill.setOutstandingMoney(money);
//                    billMapper.updateById(tBill);
//
//                }
//            }
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
//    }
//
//
//}
package com.ruoyi.web.controller.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
 * @author xiaochen
 * @date 2023/7/11 8:39
 */
@Component
public class TaskUtil {
    @Scheduled(cron = "0 0 0 * * ?")
    public void dayOfProportionBill() {
        try {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/H5AICallbackController.java
@@ -1,13 +1,16 @@
package com.ruoyi.web.controller.api;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.redis.service.RedisService;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.model.TSysAiConfig;
import com.ruoyi.system.model.TSysInspection;
import com.ruoyi.system.service.TSysAiConfigService;
import com.ruoyi.system.service.TSysAppUserService;
import com.ruoyi.system.service.TSysInspectionService;
import com.ruoyi.system.utils.util.AesSimpleUtil;
import com.ruoyi.system.utils.util.RsaSimpleUtil;
import com.ruoyi.system.utils.wx.model.WeixinProperties;
@@ -36,6 +39,8 @@
    @Autowired
    private TSysAiConfigService sysAiConfigService;
    @Autowired
    private TSysInspectionService sysInspectionService;
    @PostMapping("/reportReturn")
    public String reportReturn(String encryptedJson, String signEncryptedJson) throws Exception {
@@ -47,6 +52,7 @@
        // 1. 先解密
        String source = AesSimpleUtil.decrypt(encryptedJson, aesKey);
        System.out.println("AI问诊回调数据=====:"+ source);
        JSONObject jsonObject = JSONObject.parseObject(source);
        // 2. 再验证签名
@@ -58,6 +64,66 @@
            return "error";
        }
        // 3. 验证成功,则仅仅业务保存并返回success字符串
        /**
         * {
         *   "returnType": 1,
         *   "name": "张三",
         *   "phone": "13344445555",
         *   "email": null,
         *   "sex": 1,
         *   "age": 42,
         *   "time": "2023-03-08 08:23:13",
         *   "pdf": "https://labelsys-images.oss-cn-hangzhou.aliyuncs.com/tongueFile/check/2023/03/87d76220-085e-4d2e-9fd7-9f5a01da1940.pdf",
         *   "healthIndex": 100.0,
         *   "constitutionNames": "气虚",
         *   "symptomName": "胃气虚证",
         *   "thirdId": "13344445555",
         *   "tongueFeature": "正常人舌色,舌质淡红润泽。舌苔呈现白色。",
         *   "faceFeature": "面部颜色暗沉。"
         * }
         */
        Integer returnType = jsonObject.getInteger("returnType");
        String name = jsonObject.getString("name");
        String phone = jsonObject.getString("phone");
        String email = jsonObject.getString("email");
        Integer sex = jsonObject.getInteger("sex");
        Integer age = jsonObject.getInteger("age");
        String time = jsonObject.getString("time");
        String pdf = jsonObject.getString("pdf");
        Double healthIndex = jsonObject.getDouble("healthIndex");
        String constitutionNames = jsonObject.getString("constitutionNames");
        String symptomName = jsonObject.getString("symptomName");
        String thirdId = jsonObject.getString("thirdId");
        String tongueFeature = jsonObject.getString("tongueFeature");
        String faceFeature = jsonObject.getString("faceFeature");
        if(returnType == 2){
            log.info("用户["+phone+"]检测失败");
        }
        long count = sysInspectionService.count(Wrappers.lambdaQuery(TSysInspection.class)
                .eq(TSysInspection::getPersonPhone, phone)
                .eq(TSysInspection::getCheckTime, time));
        if (count == 0) {
            TSysInspection sysInspection = new TSysInspection();
            sysInspection.setPersonName(name);
            sysInspection.setPersonPhone(phone);
            sysInspection.setPersonSex(sex);
            sysInspection.setPersonAge(age);
            sysInspection.setCheckTime(time);
            sysInspection.setPdfUrl(pdf);
            sysInspection.setHealthIndex(healthIndex);
            sysInspection.setConstitutionNames(constitutionNames);
            sysInspection.setSymptomName(symptomName);
            sysInspection.setTongueFeature(tongueFeature);
            sysInspection.setFaceFeature(faceFeature);
            sysInspection.setAppUserId(thirdId);
            sysInspection.setInspectionType(2);
            sysInspection.setIsPay(0);
            sysInspectionService.save(sysInspection);
        }
        return "success";
    }
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/TSysInspectionController.java
@@ -1,23 +1,24 @@
package com.ruoyi.web.controller.api;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.basic.PageInfo;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.model.TSysAppUser;
import com.ruoyi.system.model.TSysInspection;
import com.ruoyi.system.query.TSysInspectionQuery;
import com.ruoyi.system.service.TSysAppUserService;
import com.ruoyi.system.service.TSysInspectionService;
import com.ruoyi.system.utils.util.AIUtil;
import com.ruoyi.system.vo.TSysInspectionVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Objects;
/**
 * <p>
@@ -34,10 +35,14 @@
    private final TSysInspectionService sysInspectionService;
    private final TokenService tokenService;
    private final TSysAppUserService sysAppUserService;
    private final AIUtil aiUtil;
    @Autowired
    public TSysInspectionController(TSysInspectionService sysInspectionService, TokenService tokenService) {
    public TSysInspectionController(TSysInspectionService sysInspectionService, TokenService tokenService, TSysAppUserService sysAppUserService, AIUtil aiUtil) {
        this.sysInspectionService = sysInspectionService;
        this.tokenService = tokenService;
        this.sysAppUserService = sysAppUserService;
        this.aiUtil = aiUtil;
    }
    /**
@@ -59,5 +64,27 @@
        return R.ok(sysInspectionService.getById(id));
    }
    /**
     * 用户检测跳转时单点登录
     */
    @ApiOperation(value = "用户检测跳转时单点登录")
    @PostMapping(value = "/ssoLogin")
    public R<String> ssoLogin() {
        String userId = tokenService.getLoginUserApplet().getUserId();
        TSysAppUser sysAppUser = sysAppUserService.getById(userId);
        if(StringUtils.isEmpty(sysAppUser.getNickName()) || Objects.isNull(sysAppUser.getSex()) || Objects.isNull(sysAppUser.getBirthTime())){
            return R.fail("请先完善用户信息");
        }
        int code = aiUtil.ssoLogin().getCode();
        if(code != 200){
            return R.fail("单点登录失败");
        }
        code = aiUtil.initUser(userId, sysAppUser.getNickName(), sysAppUser.getSex(), DateUtils.localDateTimeToString(sysAppUser.getBirthTime())).getCode();
        if(code != 200){
            return R.fail("初始化用户失败");
        }
        return R.ok();
    }
}
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/WxPayController.java
New file
@@ -0,0 +1,209 @@
package com.ruoyi.web.controller.api;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.core.type.TypeReference;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.CodeGenerateUtils;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.model.TSysAppUser;
import com.ruoyi.system.model.TSysInspection;
import com.ruoyi.system.model.TSysOtherConfig;
import com.ruoyi.system.model.TSysPayRecord;
import com.ruoyi.system.service.TSysAppUserService;
import com.ruoyi.system.service.TSysInspectionService;
import com.ruoyi.system.service.TSysOtherConfigService;
import com.ruoyi.system.service.TSysPayRecordService;
import com.ruoyi.system.wxPay.enums.RefundEnum;
import com.ruoyi.system.wxPay.enums.TradeStateEnum;
import com.ruoyi.system.wxPay.model.WeixinPayProperties;
import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
import com.ruoyi.system.wxPay.utils.WxTimeUtils;
import com.ruoyi.system.wxPay.utils.WxV3Pay;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Objects;
/**
 * 微信相关接口
 */
@Slf4j
@RestController
@CrossOrigin
@RequestMapping("/wx/")
@Api(tags = {"微信支付相关接口"})
public class WxPayController {
    @Autowired
    private WxV3Pay wxV3Pay;
    @Autowired
    private TokenService tokenService;
    @Autowired
    private TSysAppUserService sysAppUserService;
    @Autowired
    private TSysOtherConfigService sysOtherConfigService;
    @Autowired
    private TSysPayRecordService sysPayRecordService;
    @Autowired
    private WeixinPayProperties weixinPayProperties;
    @Autowired
    private TSysInspectionService sysInspectionService;
    /**
     * 按实际修改
     */
    @ApiOperation("订单支付")
    @PostMapping("orderPay")
    public R<Map<String, Object>> orderPay(@RequestParam(value = "inspectionId") String inspectionId) {
        TSysOtherConfig sysOtherConfig = sysOtherConfigService.getById(1);
        if (Objects.isNull(sysOtherConfig.getAiPrice())) {
            return R.fail("系统未配置AI支付价格");
        }
        String userId = tokenService.getLoginUserApplet().getUserId();
        TSysAppUser sysAppUser = sysAppUserService.getById(userId);
        // 价格
        Integer totalPrice = sysOtherConfig.getAiPrice().multiply(new BigDecimal(100)).intValue();
        // 生成订单号
        String orderNo = CodeGenerateUtils.generateOrderSn();
        // 查询用户信息 用户openid
        String openId = sysAppUser.getOpenId();
        // 存储支付记录
        sysPayRecordService.saveData(orderNo, userId, sysOtherConfig.getAiPrice(), 1);
        // 调用支付方法
        Map<String, Object> result = wxV3Pay.jsApi(orderNo, totalPrice, openId, weixinPayProperties.getV3().getNotifyPayUrl(),"AI检测报告支付");
        log.info("支付参数:{}", result);
        return R.ok(result);
    }
    /**
     * 微信v3支付-订单退款
     *
     * @return
     */
    @ApiOperation("订单退款")
    @PostMapping(value = "refund-order")
    public AjaxResult<String> refundOrder() {
         Map<String, Object> result = wxV3Pay.refund(new WxPaymentRefundModel());
         log.info("退款结果:{}", result);
        // 微信支付退款单号
        String refund_id = result.get("refund_id").toString();
        // 商户退款单号
        String out_refund_no = result.get("out_refund_no").toString();
        // 微信支付订单号
        String transaction_id = result.get("transaction_id").toString();
        // 商户订单号 tradeNo
        String out_trade_no = result.get("out_trade_no").toString();
        // 退款成功时间
        String success_time = Objects.nonNull(result.get("success_time")) ? result.get("success_time").toString() : null;
        // 退款状态 RefundEnum
        String status = result.get("status").toString();
        // TODO 退款业务处理
        return AjaxResult.success();
    }
    /**
     * 支付回调
     */
    @PostMapping("pay/notify")
    @ApiOperation("订单回调")
    public void payNotify(HttpServletRequest request) throws Exception {
        try {
            Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {});
            log.info("支付回调:{}", params);
            // 商户订单号
            String tradeNo = params.get("out_trade_no").toString();
            // 交易状态
            String trade_state = params.get("trade_state").toString();
            // 交易状态描述
            String trade_state_desc = params.get("trade_state_desc").toString();
            // 微信支付订单号
            String transaction_id = params.get("transaction_id").toString();
            // 支付完成时间
            // 时间不对的话,可以调用  WxTimeUtils.toRfc3339Date(success_time)转换一下
            String success_time = params.get("success_time").toString();
            // 附加数据
            Integer attach = Integer.parseInt(params.get("attach").toString());
            // 查询订单
            TSysPayRecord sysPayRecord = sysPayRecordService.getOne(Wrappers.lambdaQuery(TSysPayRecord.class)
                    .eq(TSysPayRecord::getOrderNo, tradeNo).last("LIMIT 1"));
            // 处理订单
            if (trade_state.equals(TradeStateEnum.SUCCESS.name())) {
                log.info("回调成功");
                // 订单号查询订单
                sysPayRecord.setPayState(2);
                sysPayRecord.setPayTime(WxTimeUtils.dateToLocalDateTime(WxTimeUtils.toRfc3339Date(success_time)));
                sysPayRecord.setTransactionId(transaction_id);
                sysPayRecordService.updateById(sysPayRecord);
                // 处理检测报告为可见
                TSysInspection sysInspection = sysInspectionService.getById(sysPayRecord.getInspectionId());
                sysInspection.setIsPay(1);
                sysInspectionService.updateById(sysInspection);
                wxV3Pay.ack();
            }
            wxV3Pay.ack();
        } catch (Exception e) {
            log.error("支付回调异常:{}", e, e);
            wxV3Pay.ack(false, e.getMessage());
        }
    }
    /**
     * 支付回调成功后
     */
    @PostMapping("pay/ack")
    public void ack(){
        try {
            wxV3Pay.ack();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 退款回调
     */
    @PostMapping("refund/notify")
    public void refundNotify(HttpServletRequest request) throws IOException {
        try {
            Map<String, Object> params = wxV3Pay.verifyNotify(request, new TypeReference<Map<String, Object>>() {
            });
            // 商户订单号
            String out_trade_no = params.get("out_trade_no").toString();
            // 商户退款单号
            String out_refund_no = params.get("out_refund_no").toString();
            // 微信支付订单号
            String transaction_id = params.get("transaction_id").toString();
            // 微信支付退款单号
            String refund_id = params.get("refund_id").toString();
            // 退款状态
            String tradeState = params.get("refund_status").toString();
            // 退款成功时间
            // 时间不对的话,可以调用  WxTimeUtils.toRfc3339Date(success_time)转换一下
            String success_time = params.get("success_time").toString();
            if (tradeState.equals(RefundEnum.SUCCESS.name())) {
                wxV3Pay.ack();
            } else {
                wxV3Pay.ack(false, "不是成功的退款状态");
            }
        } catch (Exception e) {
            e.printStackTrace();
            wxV3Pay.ack(false, e.getMessage());
        }
    }
}
ruoyi-applet/src/main/resources/application-prod.yml
@@ -211,23 +211,25 @@
#OSS及短信配置
code:
  config:
    templateCodeTest: "SMS_154950909"
    signNameTest: "阿里云短信测试"
    accessKeyId: LTAI5tAdba8HtT1C6UqtSxBt
    accessKeySecret: 0SRb6XGkciQDPWn2rYqbJtq2qRMDY8
    signName: "四川金达通信工程"
    templateCode: "SMS_293985284"
cos:
  client:
    accessKey: AKIDCF5EF2c0DE1e5JK8r4EGJF4mNsMgp26x
    secretKey: lLl184rUyFOOE0d5KNGC3kmfNsCWk4GU
    bucket: xzgt-1305134071
    bucketAddr: ap-chengdu
    rootSrc: https://xzgt-1305134071.cos.ap-chengdu.myqcloud.com
    location: /xizang
sms:
  enable: true
  appId: 1400957506
  secretid: AKIDCF5EF2c0DE1e5JK8r4EGJF4mNsMgp26x
  secretkey: lLl184rUyFOOE0d5KNGC3kmfNsCWk4GU
  sign: 畅云出行
    accessKeyId: LTAI5t99NH2Wwoq48ho72u8M
    accessKeySecret: wAGHKUEBrrTgDcbfjnBKTEt5858Ru5
    signName: "海诊通"
payment:
  wx:
    # 微信appid
    appId: wxa17e8d1331e50934
    # 微信商户号
    mchId: 1721757915
    # 秘钥
    secretId: 79c234527fd3b6553679d52be5e29b19
    v3:
      # 加签串
      apiKey: V7mKp9qL2Rs4jU6tX8wZ0bC3eF5hN1yD4gA
      # 加签证书地址
      privateKeyPath: D:/app/cert/weixin/apiclient_key.pem
      # 证书序列号
      mchSerialNo: 52C3F27D42CB31E70F93C2E9A3FF4F0BD845EA6B
      # 支付成功回调地址
      notifyPayUrl: http://221.182.45.100/wx/pay/notify
      # 支付退款回调地址
      notifyRefundUrl: http://221.182.45.100/wx/refund/notify
ruoyi-applet/src/main/resources/application-test.yml
@@ -197,11 +197,29 @@
    prefix: https://xzgt.test.591taxi.cn:${server.port}${server.servlet.context-path}
wx:
  conf:
    appId: wxe91f1af7638aa5dd
    appId: wxa17e8d1331e50934
    secretId: a787e1a462715604e0c9528b6d8960d1
sms:
  enable: true
  appId: 1400957506
  secretid: AKIDCF5EF2c0DE1e5JK8r4EGJF4mNsMgp26x
  secretkey: lLl184rUyFOOE0d5KNGC3kmfNsCWk4GU
  sign: 畅云出行
code:
  config:
    accessKeyId: LTAI5t99NH2Wwoq48ho72u8M
    accessKeySecret: wAGHKUEBrrTgDcbfjnBKTEt5858Ru5
    signName: "海诊通"
payment:
  wx:
    # 微信appid
    appId: wxa17e8d1331e50934
    # 微信商户号
    mchId: 1721757915
    # 秘钥
    secretId: 79c234527fd3b6553679d52be5e29b19
    v3:
      # 加签串
      apiKey: V7mKp9qL2Rs4jU6tX8wZ0bC3eF5hN1yD4gA
      # 加签证书地址
      privateKeyPath: D:/app/cert/weixin/apiclient_key.pem
      # 证书序列号
      mchSerialNo: 52C3F27D42CB31E70F93C2E9A3FF4F0BD845EA6B
      # 支付成功回调地址
      notifyPayUrl: http://221.182.45.100/wx/pay/notify
      # 支付退款回调地址
      notifyRefundUrl: http://221.182.45.100/wx/refund/notify
ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java
@@ -189,6 +189,79 @@
        return result.toString();
    }
    /**
     * 向指定 URL 发送POST方法的请求
     *
     * @param url 发送请求的 URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @return 所代表远程资源的响应结果
     */
    public static String sendPost(String url, String param,String token)
    {
        PrintWriter out = null;
        BufferedReader in = null;
        StringBuilder result = new StringBuilder();
        try
        {
            log.info("sendPost - {}", url);
            URL realUrl = new URL(url);
            URLConnection conn = realUrl.openConnection();
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            conn.setRequestProperty("Accept-Charset", "utf-8");
            conn.setRequestProperty("contentType", "utf-8");
            conn.setRequestProperty("Authorization","bearer "+ token);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            out = new PrintWriter(conn.getOutputStream());
            out.print(param);
            out.flush();
            in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
            String line;
            while ((line = in.readLine()) != null)
            {
                result.append(line);
            }
            log.info("recv - {}", result);
        }
        catch (ConnectException e)
        {
            log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e);
        }
        catch (SocketTimeoutException e)
        {
            log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e);
        }
        catch (IOException e)
        {
            log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e);
        }
        catch (Exception e)
        {
            log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e);
        }
        finally
        {
            try
            {
                if (out != null)
                {
                    out.close();
                }
                if (in != null)
                {
                    in.close();
                }
            }
            catch (IOException ex)
            {
                log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
            }
        }
        return result.toString();
    }
    public static String sendSSLPost(String url, String param)
    {
        StringBuilder result = new StringBuilder();
ruoyi-system/pom.xml
@@ -94,6 +94,11 @@
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.wechatpay-apiv3</groupId>
            <artifactId>wechatpay-apache-httpclient</artifactId>
            <version>0.4.3</version>
        </dependency>
    </dependencies>
</project>
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TSysOtherConfigMapper.java
New file
@@ -0,0 +1,16 @@
package com.ruoyi.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.system.model.TSysOtherConfig;
/**
 * <p>
 * 其他设置 Mapper 接口
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
public interface TSysOtherConfigMapper extends BaseMapper<TSysOtherConfig> {
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TSysPayRecordMapper.java
New file
@@ -0,0 +1,16 @@
package com.ruoyi.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.system.model.TSysPayRecord;
/**
 * <p>
 * 微信支付记录 Mapper 接口
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
public interface TSysPayRecordMapper extends BaseMapper<TSysPayRecord> {
}
ruoyi-system/src/main/java/com/ruoyi/system/model/TSysConfig.java
@@ -12,7 +12,7 @@
/**
 * <p>
 * 系统配置-协议管理-其他设置
 * 系统配置-协议管理
 * </p>
 *
 * @author xiaochen
@@ -21,7 +21,7 @@
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_sys_config")
@ApiModel(value="TSysConfig对象", description="系统配置-协议管理-其他设置")
@ApiModel(value="TSysConfig对象", description="系统配置-协议管理")
public class TSysConfig extends BaseModel {
    private static final long serialVersionUID = 1L;
@@ -29,7 +29,7 @@
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;
    @ApiModelProperty(value = "配置类型 1=用户协议 2=隐私协议 3=其他设置")
    @ApiModelProperty(value = "配置类型 1=用户协议 2=隐私协议")
    @TableField("config_type")
    private Integer configType;
ruoyi-system/src/main/java/com/ruoyi/system/model/TSysInspection.java
@@ -109,6 +109,10 @@
    @TableField("tongue_feature")
    private String tongueFeature;
    @ApiModelProperty(value = "面象特征分析")
    @TableField("face_feature")
    private String faceFeature;
    @ApiModelProperty(value = "风险疾病名称")
    @TableField("disease_risks")
    private String diseaseRisks;
ruoyi-system/src/main/java/com/ruoyi/system/model/TSysOtherConfig.java
New file
@@ -0,0 +1,59 @@
package com.ruoyi.system.model;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * 其他设置
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_sys_other_config")
@ApiModel(value="TSysOtherConfig对象", description="其他设置")
public class TSysOtherConfig implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;
    @ApiModelProperty(value = "采购满多少元获得1积分")
    @TableField("procurement_conditions")
    private Double procurementConditions;
    @ApiModelProperty(value = "积分有效期")
    @TableField("points_expiration")
    private Integer pointsExpiration;
    @ApiModelProperty(value = "有效期单位 1=天 2=月(31) 3=年(365)")
    @TableField("expire_unit")
    private Integer expireUnit;
    @ApiModelProperty(value = "诊所每天可发送短信数")
    @TableField("msg_count")
    private Integer msgCount;
    @ApiModelProperty(value = "ai检测费用")
    @TableField("ai_price")
    private BigDecimal aiPrice;
    @ApiModelProperty(value = "自动收货时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField("delivery_time")
    private LocalDateTime deliveryTime;
}
ruoyi-system/src/main/java/com/ruoyi/system/model/TSysPayRecord.java
New file
@@ -0,0 +1,66 @@
package com.ruoyi.system.model;
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 com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * 微信支付记录
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_sys_pay_record")
@ApiModel(value="TSysPayRecord对象", description="微信支付记录")
public class TSysPayRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;
    @ApiModelProperty(value = "用户id")
    @TableField("app_user_id")
    private String appUserId;
    @ApiModelProperty(value = "检测id")
    @TableField("inspection_id")
    private String inspectionId;
    @ApiModelProperty(value = "支付价格")
    @TableField("pay_amount")
    private BigDecimal payAmount;
    @ApiModelProperty(value = "支付状态 1=待支付 2=已支付")
    @TableField("pay_state")
    private Integer payState;
    @ApiModelProperty(value = "商户流水号")
    @TableField("order_no")
    private String orderNo;
    @ApiModelProperty(value = "支付流水号")
    @TableField("transaction_id")
    private String transactionId;
    @ApiModelProperty(value = "支付时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField("pay_time")
    private LocalDateTime payTime;
}
ruoyi-system/src/main/java/com/ruoyi/system/service/TSysOtherConfigService.java
New file
@@ -0,0 +1,16 @@
package com.ruoyi.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.system.model.TSysOtherConfig;
/**
 * <p>
 * 其他设置 服务类
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
public interface TSysOtherConfigService extends IService<TSysOtherConfig> {
}
ruoyi-system/src/main/java/com/ruoyi/system/service/TSysPayRecordService.java
New file
@@ -0,0 +1,26 @@
package com.ruoyi.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.system.model.TSysPayRecord;
import java.math.BigDecimal;
/**
 * <p>
 * 微信支付记录 服务类
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
public interface TSysPayRecordService extends IService<TSysPayRecord> {
    /**
     * 保存数据
     * @param orderNo 支付单号
     * @param userId 用户id
     * @param aiPrice ai价格
     * @param state 支付状态 1=待支付 2=已支付
     */
    void saveData(String orderNo, String userId, BigDecimal aiPrice, Integer state);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TSysOtherConfigServiceImpl.java
New file
@@ -0,0 +1,20 @@
package com.ruoyi.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.system.mapper.TSysOtherConfigMapper;
import com.ruoyi.system.model.TSysOtherConfig;
import com.ruoyi.system.service.TSysOtherConfigService;
import org.springframework.stereotype.Service;
/**
 * <p>
 * 其他设置 服务实现类
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
@Service
public class TSysOtherConfigServiceImpl extends ServiceImpl<TSysOtherConfigMapper, TSysOtherConfig> implements TSysOtherConfigService {
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TSysPayRecordServiceImpl.java
New file
@@ -0,0 +1,31 @@
package com.ruoyi.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.system.mapper.TSysPayRecordMapper;
import com.ruoyi.system.model.TSysPayRecord;
import com.ruoyi.system.service.TSysPayRecordService;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
 * <p>
 * 微信支付记录 服务实现类
 * </p>
 *
 * @author xiaochen
 * @since 2025-09-18
 */
@Service
public class TSysPayRecordServiceImpl extends ServiceImpl<TSysPayRecordMapper, TSysPayRecord> implements TSysPayRecordService {
    @Override
    public void saveData(String orderNo, String userId, BigDecimal aiPrice, Integer state) {
        TSysPayRecord sysPayRecord = new TSysPayRecord();
        sysPayRecord.setOrderNo(orderNo);
        sysPayRecord.setAppUserId(userId);
        sysPayRecord.setPayAmount(aiPrice);
        sysPayRecord.setPayState(state);
        this.save(sysPayRecord);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/util/AIUtil.java
New file
@@ -0,0 +1,116 @@
package com.ruoyi.system.utils.util;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.system.model.TSysAiConfig;
import com.ruoyi.system.service.TSysAiConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class AIUtil {
    @Autowired
    private TSysAiConfigService sysAiConfigService;
    @Autowired
    private RedisCache redisCache;
    /**
     * 获取企业应用access_token
     */
    public static final String ACCESS_TOKEN_URL = "https://www.ai-tongue.com/backend/auth/invoker/pwd/signin";
    /**
     * 会员单点登录
     */
    public static final String SSO_URL = "https://www.ai-tongue.com/h5/sso";
    /**
     * 初始化会员信息
     */
    public static final String INIT_USER_URL = "https://www.ai-tongue.com/backend/check/i/secret/thirdUser/init";
    /**
     * 获取企业应用access_token
     * @return
     */
    public String getAccessToken() {
        String accessToken = redisCache.getCacheObject(Constants.H5AI_ACCESS_TOKEN);
        if(StringUtils.hasLength(accessToken)){
            return accessToken;
        }
        TSysAiConfig sysAiConfig = sysAiConfigService.getById(1);
        String result = HttpUtils.sendPost(ACCESS_TOKEN_URL, "devid="+sysAiConfig.getDevId()+"&devsecret="+sysAiConfig.getDevSecret());
        log.info("获取access_token:{}", result);
        JSONObject object = JSONObject.parseObject(result);
        if(object.getInteger("code") != 0){
            throw new ServiceException(object.getString("msg"));
        }
        JSONObject data = object.getJSONObject("data");
        accessToken = data.getString("access_token");
        Integer expiresIn = data.getInteger("expires_in");
        redisCache.setCacheObject(Constants.H5AI_ACCESS_TOKEN, accessToken, expiresIn-20, TimeUnit.SECONDS);
        return accessToken;
    }
    /**
     * 会员单点登录
     * @return
     */
    public R ssoLogin() {
        String accessToken = redisCache.getCacheObject(Constants.H5AI_ACCESS_TOKEN);
        if(!StringUtils.hasLength(accessToken)){
            accessToken = getAccessToken();
        }
        TSysAiConfig sysAiConfig = sysAiConfigService.getById(1);
        String result = HttpUtils.sendGet(SSO_URL, "access_token="+accessToken
                +"&encryptedThirdId="+sysAiConfig.getRsaPublicKey()
                +"&signEncryptedThirdId="+sysAiConfig.getDevRsaPublicKey()
                +"&capture=all&diseaseCode=C00.D00");
        log.info("会员单点登录结果:{}", result);
        JSONObject object = JSONObject.parseObject(result);
        if(object.getInteger("code") != 0){
            throw new ServiceException(object.getString("msg"));
        }
        return R.ok();
    }
    /**
     * 会员创建接口
     * @param nickName
     * @param sex
     * @param birthday
     * @return
     */
    public R initUser(String userId,String nickName, Integer sex, String birthday) {
        String accessToken = redisCache.getCacheObject(Constants.H5AI_ACCESS_TOKEN);
        if(!StringUtils.hasLength(accessToken)){
            accessToken = getAccessToken();
        }
        TSysAiConfig sysAiConfig = sysAiConfigService.getById(1);
        String result = HttpUtils.sendPost(INIT_USER_URL, "devId="+sysAiConfig.getDevId()
                +"&thirdId="+userId
                +"&signEncryptedThirdId="+sysAiConfig.getDevRsaPublicKey()
                +"&thirdName="+nickName
                +"&sex="+sex
                +"&birthday="+birthday,accessToken);
        log.info("会员创建初始化结果:{}", result);
        JSONObject object = JSONObject.parseObject(result);
        if(object.getInteger("code") != 0){
            throw new ServiceException(object.getString("msg"));
        }
        return R.ok();
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/utils/util/H5AIUtil.java
File was deleted
ruoyi-system/src/main/java/com/ruoyi/system/utils/wx/tools/WxUtils.java
@@ -11,14 +11,17 @@
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.AlgorithmParameters;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
import java.util.Random;
/**
 * @Description 获取用户信息工具类
@@ -27,7 +30,12 @@
 */
@Slf4j
public class WxUtils {
    /**
     * 随机字符
     */
    private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final Random RANDOM = new SecureRandom();
    /**
     * 微信小程序API 用户数据的解密
     *
@@ -95,20 +103,36 @@
     * @return
     */
    public static String streamBodyByReceive(HttpServletRequest request) throws IOException {
        log.info("微信异步回调地址:{}", request.getRequestURL());
        StringBuffer buffer = new StringBuffer();
        InputStream inputStream = request.getInputStream();
        InputStreamReader reader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(reader);
        String body = null;
        while ((body = bufferedReader.readLine()) != null) {
            buffer.append(body);
        BufferedReader reader = null;
        StringBuffer sb = new StringBuffer();
        try {
            ServletInputStream stream = request.getInputStream();
            // 获取响应
            reader = new BufferedReader(new InputStreamReader(stream));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            throw new RuntimeException("读取微信支付接口数据流出现异常!");
        } finally {
            reader.close();
            WxUtils.info(sb.toString());
        }
        String data = buffer.toString();
        reader.close();
        inputStream.close();
        log.info("微信异步回调数据:{}", data);
        return data;
        return sb.toString();
    }
    /**
     * 获取随机字符串 Nonce Str
     *
     * @return String 随机字符串
     */
    public static String generateNonceStr() {
        char[] nonceChars = new char[32];
        for (int index = 0; index < nonceChars.length; ++index) {
            nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
        }
        return new String(nonceChars);
    }
    /**
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/config/WxConfig.java
New file
@@ -0,0 +1,32 @@
package com.ruoyi.system.wxPay.config;
import com.ruoyi.system.utils.wx.model.WeixinProperties;
import com.ruoyi.system.wxPay.model.WeixinPayProperties;
import com.ruoyi.system.wxPay.utils.WxV3Pay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * 项目中需继承此类
 *
 * @author lihen
 */
@Configuration
public class WxConfig {
    private final WeixinPayProperties weixinPayProperties;
    @Autowired
    public WxConfig(WeixinPayProperties weixinPayProperties) {
        this.weixinPayProperties = weixinPayProperties;
    }
    @Bean
    @ConditionalOnMissingBean(name = "wxV3Pay")
    public WxV3Pay wxSpV3Pay() {
        return new WxV3Pay(weixinPayProperties);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/RefundEnum.java
New file
@@ -0,0 +1,53 @@
package com.ruoyi.system.wxPay.enums;
import lombok.Getter;
import java.util.stream.Stream;
/**
 * @author xiaochen
 * @ClassName ProfitSharingEnum
 * @Description
 * @date 2021-11-21 11:15
 */
public enum RefundEnum {
    /**
     * 退款成功
     */
    SUCCESS("SUCCESS", "退款成功"),
    /**
     * 退款关闭
     */
    CLOSED("CLOSED", "退款关闭"),
    /**
     * 退款处理中
     */
    PROCESSING("PROCESSING", "退款处理中"),
    /**
     * 退款异常
     */
    ABNORMAL("ABNORMAL", "退款异常"),
    ;
    @Getter
    private String code;
    @Getter
    private String desc;
    RefundEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    /**
     * 通过交易类型执行具体的交易方法
     *
     * @param code
     * @return
     */
    public static RefundEnum fromValue(String code) {
        return Stream.of(RefundEnum.values()).filter(fileType ->
                fileType.getCode().toLowerCase().equals(code.toLowerCase())
        ).findFirst().orElse(null);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/enums/TradeStateEnum.java
New file
@@ -0,0 +1,41 @@
package com.ruoyi.system.wxPay.enums;
import lombok.Getter;
import java.util.stream.Stream;
/**
 * @author xiaochen
 * @ClassName AliTradeStateEnum
 * @Description
 * @date 2022-01-07 11:56
 */
public enum TradeStateEnum {
    SUCCESS("支付成功"),
    RETURN("已分账回退"),
    FAIL("已失败"),
    PROCESSING("处理中"),
    FINISHED("分账完成"),
    REFUND("转入退款"),
    PAYERROR("支付失败(其他原因,如银行返回失败)"),
    USERPAYING("用户支付中"),
    CLOSED("已关闭"),
    NOTPAY("未支付"),
    UNKNOWN("未知"),
    DONE("服务订单完成"),
    // ...
    ;
    @Getter
    private String desc;
    TradeStateEnum(String desc) {
        this.desc = desc;
    }
    public static TradeStateEnum tradeState(String code) {
        return Stream.of(TradeStateEnum.values()).filter(fileType ->
                fileType.name().equals(code)
        ).findFirst().orElse(UNKNOWN);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/exception/WxException.java
New file
@@ -0,0 +1,55 @@
package com.ruoyi.system.wxPay.exception;
/**
 * @author lihen
 */
public class WxException extends RuntimeException {
    private final static int OK = 0;
    private final static int ValidateSignatureError = -40001;
    private final static int ParseXmlError = -40002;
    public final static int ComputeSignatureError = -40003;
    private final static int IllegalAesKey = -40004;
    private final static int ValidateAppidError = -40005;
    private final static int EncryptAESError = -40006;
    private final static int DecryptAESError = -40007;
    private final static int IllegalBuffer = -40008;
    private int code;
    private static String getMessage(int code) {
        switch (code) {
            case ValidateSignatureError:
                return "签名验证错误";
            case ParseXmlError:
                return "xml解析失败";
            case ComputeSignatureError:
                return "sha加密生成签名失败";
            case IllegalAesKey:
                return "SymmetricKey非法";
            case ValidateAppidError:
                return "appid校验失败";
            case EncryptAESError:
                return "aes加密失败";
            case DecryptAESError:
                return "aes解密失败";
            case IllegalBuffer:
                return "解密后得到的buffer非法";
            default:
                return null;
        }
    }
    public int getCode() {
        return code;
    }
    public WxException(int code) {
        super(getMessage(code));
        this.code = code;
    }
    public WxException(String message) {
        super(message);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/V3.java
New file
@@ -0,0 +1,77 @@
package com.ruoyi.system.wxPay.model;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.util.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
 * @author xiaochen
 * @ClassName V3
 * @Description
 */
@Slf4j
@Data
public class V3 {
    /**
     * 获取 API 密钥
     *
     * @return API密钥
     */
    private String apiKey;
    /**
     * 秘钥路径,apiclient_key.pem
     */
    private String privateKeyPath;
    /**
     * 商户证书序列号
     */
    private String  mchSerialNo;
    /**
     * 支付回调地址
     *
     * @return
     */
    private String notifyPayUrl;
    /**
     * 退款回调地址
     *
     * @return
     */
    private String notifyRefundUrl;
    /**
     * 退款回调地址
     */
    private String notifyTravelRefundUrl;
    public InputStream getPrivateKeyStream() {
        // 需要证书释放
        byte[] certData;
//        InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(this.privateKeyPath);
        InputStream certStream = null;
        try {
            certStream = new FileInputStream(this.privateKeyPath);
            certData = IOUtils.toByteArray(certStream);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("私钥文件未找到");
        }finally {
            if(null != certStream){
                try {
                    certStream.close();
                } catch (IOException e) {
                    log.error("私钥流关闭异常");
                }
            }
        }
        return new ByteArrayInputStream(certData);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WeixinPayProperties.java
New file
@@ -0,0 +1,113 @@
package com.ruoyi.system.wxPay.model;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.stereotype.Component;
/**
 * @author xiaochen
 * @ClassName WeixinProperties
 * @Description
 */
@ToString
@Component
@ConfigurationProperties(prefix = "payment.wx")
public class WeixinPayProperties {
    /**
     * 默认开启
     */
    private boolean enabled = true;
    /**
     * 获取 App ID
     *
     * @return App ID
     */
    private String appId;
    /**
     * 获取 Mch ID
     *
     * @return Mch ID
     */
    private String mchId;
    /**
     * 获取 secret ID
     *
     * @return secret ID
     */
    private String secretId;
    /**
     * 回调地址
     */
    private String callBackUrl;
    public String getSecretId() {
        return secretId;
    }
    public void setSecretId(String secretId) {
        this.secretId = secretId;
    }
    /**
     * v3
     */
    @NestedConfigurationProperty
    private V3 v3;
    public boolean isEnabled() {
        return enabled;
    }
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
    public V3 getV3() {
        return v3;
    }
    public void setV3(V3 v3) {
        this.v3 = v3;
    }
    /**
     * HTTP(S) 连接超时时间,单位毫秒
     *
     */
    public int getHttpConnectTimeoutMs() {
        return 6 * 1000;
    }
    /**
     * HTTP(S) 读数据超时时间,单位毫秒
     */
    public int getHttpReadTimeoutMs() {
        return 8 * 1000;
    }
    public String getAppId() {
        return appId;
    }
    public void setAppId(String appId) {
        this.appId = appId;
    }
    public String getMchId() {
        return mchId;
    }
    public void setMchId(String mchId) {
        this.mchId = mchId;
    }
    public String getCallBackUrl() {
        return callBackUrl;
    }
    public void setCallBackUrl(String callBackUrl) {
        this.callBackUrl = callBackUrl;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxCloseOrderModel.java
New file
@@ -0,0 +1,20 @@
package com.ruoyi.system.wxPay.model;
import lombok.*;
/**
 * @author xiaochen
 * @ClassName WxPaymentRefundModel
 * @Description
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class WxCloseOrderModel {
    private String mchid;
    private String out_trade_no;
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentInfoModel.java
New file
@@ -0,0 +1,201 @@
package com.ruoyi.system.wxPay.model;
import lombok.*;
import java.util.List;
/**
 * @author xiaochen
 * @ClassName WxPaymentInfoModel
 * @Description
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class WxPaymentInfoModel {
    /**
     * 合单商户appid
     */
    private String combine_appid;
    /**
     * 合单商户号
     */
    private String combine_mchid;
    /**
     * 合单商户订单号
     */
    private String combine_out_trade_no;
    /**
     * 合单--子单信息
     */
    private List<SubOrders> sub_orders;
    /**
     * 合单--支付者
     */
    private CombinePayerInfo combine_payer_info;
    private String appid;
    private String sp_appid;
    private String mchid;
    private String sp_mchid;
    private String sub_appid;
    private String sub_mchid;
    private String description;
    private String out_trade_no;
    private String time_expire;
    private String attach;
    private String notify_url;
    private String goods_tag;
    private SettleInfo settle_info;
    private Amount amount;
    private Payer payer;
    private Detail detail;
    private SceneInfo scene_info;
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class SettleInfo {
        private Boolean profit_sharing;
        private Integer subsidy_amount;
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class Amount {
        private Integer total;
        /**
         * 合单支付时需要
         */
        private Integer total_amount;
        @Builder.Default
        private String currency = "CNY";
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class Payer {
        private String openid;
        private String sp_openid;
        private String sub_openid;
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class Detail {
        private int cost_price;
        private String invoice_id;
        private List<GoodsDetail> goods_detail;
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        @Getter
        @Setter
        @ToString
        public static class GoodsDetail {
            private String merchant_goods_id;
            private String wechatpay_goods_id;
            private String goods_name;
            private int quantity;
            private int unit_price;
        }
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class SceneInfo {
        private String payer_client_ip;
        private String device_id;
        private StoreInfo store_info;
        private H5Info h5_info;
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        @Getter
        @Setter
        @ToString
        public static class StoreInfo {
            private String id;
            private String name;
            private String area_code;
            private String address;
        }
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        @Getter
        @Setter
        @ToString
        public static class H5Info {
            private String type;
            private String app_name;
            private String app_url;
            private String bundle_id;
            private String package_name;
        }
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class SubOrders {
        private String out_trade_no;
        private Amount amount;
        private String mchid;
        private String sub_mchid;
        private String attach;
        private String description;
        private String goods_tag;
        private SettleInfo settle_info;
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class CombinePayerInfo {
        private String openid;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/model/WxPaymentRefundModel.java
New file
@@ -0,0 +1,84 @@
package com.ruoyi.system.wxPay.model;
import lombok.*;
import java.util.List;
/**
 * @author xiaochen
 * @ClassName WxPaymentRefundModel
 * @Description
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class WxPaymentRefundModel {
    /**
     * 子商户,二级商户号
     */
    private String sub_mchid;
    /**
     * 电商平台APPID
     */
    private String sp_appid;
    private String transaction_id;
    private String out_trade_no;
    /**
     * 商户退款单号
     */
    private String out_refund_no;
    /**
     * 退款原因
     */
    private String reason;
    private String notify_url;
    /**
     * 资金账户,否
     */
    private String funds_account;
    /**
     * 退款金额信息
     */
    private RefundAmount amount;
    /**
     * 退款商品
     */
    private List<RefundGoodsDetail> goods_detail;
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class RefundAmount {
        /**
         * 原订单金额
         */
        private int total;
        @Builder.Default
        private String currency = "CNY";
        /**
         * 退款金额
         */
        private int refund;
    }
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Getter
    @Setter
    @ToString
    public static class RefundGoodsDetail {
        private String merchant_goods_id;
        private String wechatpay_goods_id;
        private String goods_name;
        private int unit_price;
        private int refund_amount;
        private int refund_quantity;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/AppletUserDecodeData.java
New file
@@ -0,0 +1,52 @@
package com.ruoyi.system.wxPay.pojo;
import lombok.Data;
/**
 * @author xiaochen
 * @ClassName AppletUserDecodeData
 * @Description
 * 用户主体信息部分
 * {
 *     "openId": "OPENID",
 *     "nickName": "NICKNAME",
 *     "gender": GENDER,
 *     "city": "CITY",
 *     "province": "PROVINCE",
 *     "country": "COUNTRY",
 *     "avatarUrl": "AVATARURL",
 *     "unionId": "UNIONID",
 *     "watermark":
 *     {
 *         "appid":"APPID",
 *         "timestamp":TIMESTAMP
 *     }
 * }
 * 电话部分
 * {
 *     "phoneNumber": "13580006666",
 *     "purePhoneNumber": "13580006666",
 *     "countryCode": "86",
 *     "watermark":
 *     {
 *         "appid":"APPID",
 *         "timestamp": TIMESTAMP
 *     }
 * }
 *
 */
@Data
public class AppletUserDecodeData {
    private String openId;
    private String unionId;
    private String nickName;
    private int gender;
    private String city;
    private String province;
    private String country;
    private String avatarUrl;
    private Watermark watermark;
    private String phoneNumber;
    private String purePhoneNumber;
    private String countryCode;
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/pojo/Watermark.java
New file
@@ -0,0 +1,9 @@
package com.ruoyi.system.wxPay.pojo;
import lombok.Data;
@Data
public class Watermark {
    private String appid;
    private String timestamp;
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/resp/NotifyV3PayDecodeRespBody.java
New file
@@ -0,0 +1,222 @@
package com.ruoyi.system.wxPay.resp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
 * @author xiaochen
 * @ClassName FacilV3PayNotifyRespBody
 * @Description
 */
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class NotifyV3PayDecodeRespBody implements Serializable {
    // 合单--开始
    private String combine_appid;
    private String combine_mchid;
    private String combine_out_trade_no;
    private List<SubOrders> sub_orders;
    // 合单--结束
    /**
     * 服务商应用ID
     */
    private String sp_appid;
    /**
     * 服务商户号
     */
    private String sp_mchid;
    /**
     * 商户号
     */
    private String mchid;
    /**
     * 子商户应用ID
     */
    private String sub_appid;
    /**
     * 子商户号
     */
    private String sub_mchid;
    /**
     * 商户订单号
     */
    private String out_trade_no;
    /**
     * 交易状态描述
     */
    private String trade_state_desc;
    /**
     * 交易类型,枚举值:
     * JSAPI:公众号支付
     * NATIVE:扫码支付
     * APP:APP支付
     * MICROPAY:付款码支付
     * MWEB:H5支付
     * FACEPAY:刷脸支付
     */
    private String trade_type;
    /**
     * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
     */
    private String attach;
    /**
     * 微信支付订单号
     */
    private String transaction_id;
    /**
     * 交易状态,枚举值:
     * SUCCESS:支付成功
     * REFUND:转入退款
     * NOTPAY:未支付
     * CLOSED:已关闭
     * REVOKED:已撤销(付款码支付)
     * USERPAYING:用户支付中(付款码支付)
     * PAYERROR:支付失败(其他原因,如银行返回失败)
     */
    private String trade_state;
    /**
     * 银行类型,采用字符串类型的银行标识。银行标识请参考《银行类型对照表》
     * https://pay.weixin.qq.com/wiki/doc/apiv3_partner/terms_definition/chapter1_1_3.shtml#part-6
     */
    private String bank_type;
    /**
     * 支付完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,
     * YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,
     * TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。
     * 例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒。
     * 示例值:2018-06-08T10:34:56+08:00
     */
    private String success_time;
    /**
     * 支付者信息
     */
    private Payer payer;
    /**
     * 支付者
     */
    private Payer combine_payer_info;
    /**
     * 订单金额信息
     */
    private Amount amount;
    /**
     * 场景信息
     */
    private SceneInfo scene_info;
    /**
     * 优惠功能,享受优惠时返回该字段
     */
    private List<PromotionDetail> promotion_detail;
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class Amount implements Serializable{
        /**
         * 用户支付金额
         */
        private int payer_total;
        /**
         * 总金额
         */
        private int total;
        /**
         * 标价金额
         */
        private int total_amount;
        /**
         * 现金支付金额
         */
        private int payer_amount;
        private String currency;
        private String payer_currency;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class GoodsDetail implements Serializable{
        private String goods_id;
        private int quantity;
        private int unit_price;
        private int discount_amount;
        private String goods_remark;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class Payer implements Serializable{
        private String openid;
        private String sp_openid;
        private String sub_openid;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class PromotionDetail implements Serializable{
        private String coupon_id;
        private String name;
        private String scope;
        private String type;
        private int amount;
        private String stock_id;
        private int wechatpay_contribute;
        private int merchant_contribute;
        private int other_contribute;
        private String currency;
        private List<GoodsDetail> goods_detail;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class SceneInfo implements Serializable{
        /**
         * 商户端设备号
         */
        private String device_id;
    }
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    public static class SubOrders implements Serializable{
        private String mchid;
        private String trade_type;
        private String trade_state;
        private String trade_state_desc;
        private String bank_type;
        private String attach;
        private String success_time;
        private String transaction_id;
        private String out_trade_no;
        private String sub_mchid;
        private Amount amount;
        /**
         * 优惠功能,享受优惠时返回该字段
         */
        private List<PromotionDetail> promotion_detail;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxAbstractPay.java
New file
@@ -0,0 +1,400 @@
package com.ruoyi.system.wxPay.utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.ruoyi.system.utils.wx.tools.WebUtils;
import com.ruoyi.system.utils.wx.tools.WxJsonUtils;
import com.ruoyi.system.utils.wx.tools.WxUtils;
import com.ruoyi.system.wxPay.model.WxCloseOrderModel;
import com.ruoyi.system.wxPay.model.WxPaymentInfoModel;
import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
import com.ruoyi.system.wxPay.resp.NotifyV3PayDecodeRespBody;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.*;
/**
 * @author xiaochen
 * @ClassName WxWifiV3Pay
 * @Description
 * @date 2021-11-13 21:10
 */
@Slf4j
public abstract class WxAbstractPay {
    /**
     * 请求成功相应码
     */
    private static final int STATUS_CODE = 200;
    /**
     * 请求成功相应码
     */
    private static final int OTHER_STATUS_CODE = 204;
    /**
     * 请求根地址
     */
    private static final String HOST = "https://api.mch.weixin.qq.com";
    private static RuntimeException parameterError(String message, Object... args) {
        message = String.format(message, args);
        return new IllegalArgumentException("parameter error: " + message);
    }
    /**
     * 封装基础数据
     *
     * @param requestBody
     * @param notifyUrl
     * @return
     */
    protected String buildBaseParam(WxPaymentInfoModel requestBody, String notifyUrl) {
        // 封装基础数据
        requestBody.setNotify_url(notifyUrl + requestBody.getNotify_url());
        String reqBody = WxJsonUtils.toJsonString(requestBody);
        return reqBody;
    }
    /**
     * 微信调起支付参数
     * 返回参数如有不理解 请访问微信官方文档
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml
     *
     * @param prepayId 微信下单返回的prepay_id
     * @param appId    应用ID(appid)
     * @return 当前调起支付所需的参数
     * @throws Exception
     */
    protected Map<String, Object> wxTuneUp(PrivateKeySigner privateKeySigner, String appId, String prepayId) {
        String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
        String nonceStr = WxUtils.generateNonceStr();
        String packageStr = "prepay_id=" + prepayId;
        //加载签名
        String signStr = Stream.of(appId, timeStamp, nonceStr, packageStr).collect(Collectors.joining("\n", "", "\n"));
        String packageSign = privateKeySigner.sign(signStr.getBytes(StandardCharsets.UTF_8)).getSign();
        Map<String, Object> map = new HashMap<>(6);
        map.put("appId", appId);
        map.put("timeStamp", timeStamp);
        map.put("nonceStr", nonceStr);
        map.put("package", packageStr);
        map.put("signType", "RSA");
        map.put("paySign", packageSign);
        return map;
    }
    /**
     * 构建方法请求
     *
     * @param uri
     * @param socketTimeout
     * @param connectTimeout
     * @return
     */
    protected HttpGet requestGet(String uri, int socketTimeout, int connectTimeout) {
        //请求URL
        HttpGet httpGet = new HttpGet(HOST + uri);
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
                .setConnectTimeout(connectTimeout).build();
        httpGet.setConfig(requestConfig);
        httpGet.setHeader("Content-type", "application/json");
        httpGet.setHeader("Accept", "application/json");
        return httpGet;
    }
    /**
     * 构建方法请求
     *
     * @param uri
     * @param socketTimeout
     * @param connectTimeout
     * @param reqdata
     * @return
     */
    protected HttpPost requestPost(String uri, int socketTimeout, int connectTimeout, String reqdata) {
        //请求URL
        HttpPost httpPost = new HttpPost(HOST + uri);
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout)
                .setConnectTimeout(connectTimeout).build();
        httpPost.setConfig(requestConfig);
        StringEntity entity = new StringEntity(reqdata, StandardCharsets.UTF_8);
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        return httpPost;
    }
    public abstract <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception;
    /**
     * 接收回调
     *
     * @param request
     * @return
     * @throws Exception
     */
    public <T> T verifyNotify(HttpServletRequest request, Verifier verifier, String apiKey, TypeReference<T> valueTypeRef) throws Exception {
        String body = WxUtils.streamBodyByReceive(request);
        String requestId = request.getHeader(REQUEST_ID);
        String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP};
        String value;
        // 验签必须参数检验
        for (String headerName : headers) {
            value = request.getHeader(headerName);
            if (value == null || "".equals(value)) {
                throw parameterError("empty [%s], request-id=[%s]", headerName, requestId);
            }
        }
        String serial = request.getHeader(WECHAT_PAY_SERIAL);
        String signature = request.getHeader(WECHAT_PAY_SIGNATURE);
        String timestamp = request.getHeader(WECHAT_PAY_TIMESTAMP);
        String nonce = request.getHeader(WECHAT_PAY_NONCE);
        // 构建request,传入必要参数
        NotificationRequest notificationRequest = new NotificationRequest.Builder().withSerialNumber(serial)
                .withNonce(nonce)
                .withTimestamp(timestamp)
                .withSignature(signature)
                .withBody(body)
                .build();
        NotificationHandler handler = new NotificationHandler(verifier, apiKey.getBytes(StandardCharsets.UTF_8));
        // 验签和解析请求体
        Notification notification = handler.parse(notificationRequest);
        assert notification != null;
        T respBody = WxJsonUtils.parseObject(notification.getDecryptData(), valueTypeRef);
        return respBody;
    }
    /**
     * 订单查询
     *
     * @param httpClient
     * @param socketTimeout
     * @param connectTimeout
     * @param url
     * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
     * @author xiaochen
     * @date 2021-12-20 17:12
     */
    protected NotifyV3PayDecodeRespBody query(CloseableHttpClient httpClient, int socketTimeout, int connectTimeout, String url) {
        //请求URL
        HttpGet httpGet = requestGet(
                url
                , socketTimeout
                , connectTimeout);
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
            int statusCode = response.getStatusLine().getStatusCode();
            String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            if (WxUtils.getLogger().isDebugEnabled()) {
                WxUtils.debug("请求成功:{}", respBodyStr);
            }
            if(404 == statusCode){
               return null;
            }
            // 成功相应
            if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode ) {
                NotifyV3PayDecodeRespBody body = WxJsonUtils.parseObject(respBodyStr, NotifyV3PayDecodeRespBody.class);
                return body;
            } else {
                WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
                throw new RuntimeException(respBodyStr);
            }
        } catch (ConnectTimeoutException e) {
            e.printStackTrace();
            throw new RuntimeException("接口超时");
        } catch (SocketTimeoutException e) {
            e.printStackTrace();
            throw new RuntimeException("读取接口数据超时");
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问");
        } finally {
            // 关闭响应
            try {
                if (response != null) {
                    //关闭结果集
                    response.getEntity().getContent().close();
                    response.close();
                }
            } catch (IOException e) {
                throw new RuntimeException("关闭流异常");
            }
        }
    }
    /**
     * 子级实现
     *
     * @param out_trade_no
     * @return
     */
    public abstract NotifyV3PayDecodeRespBody query(String out_trade_no);
    /**
     * 订单退款
     *
     * @param refundModel
     * @return
     */
    public abstract Map<String, Object> refund(WxPaymentRefundModel refundModel);
    public abstract String close(String out_trade_no);
    /**
     * 订单退款
     *
     * @param httpClient
     * @param uri
     * @param httpReadTimeoutMs
     * @param httpConnectTimeoutMs
     * @param refundModel
     * @return
     */
    public Map<String, Object> refund(CloseableHttpClient httpClient,
                                      String uri,
                                      int httpReadTimeoutMs,
                                      int httpConnectTimeoutMs,
                                      WxPaymentRefundModel refundModel) {
        String reqBody = WxJsonUtils.toJsonString(refundModel);
        //请求URL
        HttpEntityEnclosingRequestBase httpPost = requestPost(
                uri
                , httpReadTimeoutMs
                , httpConnectTimeoutMs, reqBody);
        String repBody = result(httpClient, httpPost);
        Map<String, Object> body = WxJsonUtils.parseObject(repBody, Map.class);
        return body;
    }
    /**
     * 请求结果
     *
     * @param request
     * @return
     */
    protected String result(CloseableHttpClient httpClient, HttpRequestBase request) {
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(request);
            int statusCode = response.getStatusLine().getStatusCode();
            String respBodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            if (WxUtils.getLogger().isDebugEnabled()) {
                WxUtils.debug("请求成功:{}", respBodyStr);
            }
            // 成功相应
            if (STATUS_CODE == statusCode || OTHER_STATUS_CODE == statusCode) {
                return respBodyStr;
            } else {
                WxUtils.error("failed,resp code = {},return body = {}", statusCode, respBodyStr);
                throw new RuntimeException(respBodyStr);
            }
        } catch (ConnectTimeoutException e) {
            e.printStackTrace();
            throw new RuntimeException("接口超时");
        } catch (SocketTimeoutException e) {
            e.printStackTrace();
            throw new RuntimeException("读取接口数据超时");
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("接口请求失败,请尝试检查网络环境或请求接口是否能正常访问");
        } finally {
            // 关闭响应
            try {
                if (response != null) {
                    //关闭结果集
                    response.getEntity().getContent().close();
                    response.close();
                }
            } catch (IOException e) {
                throw new RuntimeException("关闭流异常");
            }
        }
    }
    /**
     * 微信结果确认应答
     * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
     *
     * @param
     * @throws IOException
     */
    public void ack() throws IOException {
        ack(true, null);
    }
    /**
     * 微信结果确认应答
     * 支付通知http应答码为200或204才会当作正常接收,当回调处理异常时,应答的HTTP状态码应为500,或者4xx。
     *
     * @param
     * @throws IOException
     */
    public void ack(boolean ackSucc, String erroMsg) throws IOException {
        HttpServletResponse response = WebUtils.response();
        PrintWriter writer = response.getWriter();
        if (ackSucc) {
            log.info("响应微信回调成功!");
            response.setStatus(200);
            writer.write("{\"code\": \"SUCCESS\",\"message\": \"成功\"}");
        } else {
            log.info("响应微信回调失败:{}!", erroMsg);
            response.setStatus(500);
            writer.write("{\"code\": \"FAIL\",\"message\": " + (StringUtils.hasLength(erroMsg) ? erroMsg : "业务处理失败") + "}");
        }
        // 关闭流
        if (Objects.nonNull(writer)) {
            writer.close();
        }
    }
    /**
     * 关闭订单
     * @param httpClient
     * @param uri
     * @param httpReadTimeoutMs
     * @param httpConnectTimeoutMs
     * @param closeModel
     * @return
     */
    public String close(CloseableHttpClient httpClient,
                                      String uri,
                                      int httpReadTimeoutMs,
                                      int httpConnectTimeoutMs,
                                     WxCloseOrderModel closeModel) {
        String reqBody = WxJsonUtils.toJsonString(closeModel);
        //请求URL
        HttpEntityEnclosingRequestBase httpPost = requestPost(
                uri
                , httpReadTimeoutMs
                , httpConnectTimeoutMs, reqBody);
        String repBody = result(httpClient, httpPost);
        return repBody;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxTimeUtils.java
New file
@@ -0,0 +1,164 @@
package com.ruoyi.system.wxPay.utils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.util.StringUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Objects;
import java.util.TimeZone;
/**
 * @author xiaochen
 * @ClassName WxTimeUtils
 * @Description
 * @date 2021-12-16 16:07
 */
public class WxTimeUtils {
    /**
     * 系统默认时区
     */
    private static final ZoneId ZONE = ZoneId.systemDefault();
    /**
     * yyyy-MM-dd'T'HH:mm:ssxxx 比如:2020-05-23T17:06:30+08:00 0时区时末尾 为+00:00
     */
    public static final DateTimeFormatter YYYY_MM_DD_T_HH_MM_SS_XXX_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssxxx");
    /**
     * yyyy-MM-dd HH:mm:ss 比如:2020-05-23 17:06:30
     */
    public static final DateTimeFormatter YYYY_MM_DD_HH_MM_SS_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZONE);
    /**
     * 时间转 TimeZone
     *
     * @param date
     * @return
     * @throws Exception
     */
    public static String dateToTimeZone(Date date) throws Exception {
        String time;
        if (date == null) {
            throw new Exception("date is not null");
        }
        ZonedDateTime zonedDateTime = toZonedDateTime(date);
        time = format(zonedDateTime, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
        return time;
    }
    /**
     * Date转ZonedDateTime,时区为系统默认时区
     *
     * @param date Date
     * @return ZonedDateTime
     */
    public static ZonedDateTime toZonedDateTime(Date date) {
        Objects.requireNonNull(date, "date");
        return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault());
    }
    /**
     * 根据 formatter格式化 zonedDateTime
     *
     * @param zonedDateTime ZonedDateTime
     * @param formatter     DateTimeFormatter
     * @return String
     */
    public static String format(ZonedDateTime zonedDateTime, DateTimeFormatter formatter) {
        Objects.requireNonNull(zonedDateTime, "zonedDateTime");
        Objects.requireNonNull(formatter, "formatter");
        return zonedDateTime.format(formatter);
    }
    /**
     * TimeZone 时间转标准时间
     *
     * @param date
     * @return
     * @throws Exception
     */
    public static String toTimeZoneStr(String date) {
        String time;
        if (!StringUtils.hasLength(date)) {
            throw new RuntimeException("str is not null");
        }
        ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
        if (zonedDateTime == null) {
            throw new RuntimeException("str to zonedDateTime fail");
        }
        time = zonedDateTime.format(YYYY_MM_DD_HH_MM_SS_FMT);
        return time;
    }
    /**
     * 转date
     *
     * @param date
     * @return
     * @throws Exception
     */
    public static Date toDate(String date) {
        String time;
        if (!StringUtils.hasLength(date)) {
            throw new RuntimeException("str is not null");
        }
        ZonedDateTime zonedDateTime = parseToZonedDateTime(date, YYYY_MM_DD_T_HH_MM_SS_XXX_FMT);
        if (zonedDateTime == null) {
            throw new RuntimeException("str to zonedDateTime fail");
        }
        return Date.from(zonedDateTime.toInstant());
    }
    /**
     * str --> Date
     *
     * @param date
     * @return java.util.Date
     * @author xiaochen
     * @date 2022-01-20 18:20
     */
    public static Date toRfc3339Date(String date) {
        DateTime dt2 = new DateTime(date);
        return dt2.toDate();
    }
    /**
     * 将 Date 转为 LocalDateTime
     *
     * @param date
     * @return java.time.LocalDateTime;
     */
    public static LocalDateTime dateToLocalDateTime(Date date) {
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    }
    /**
     * str --> Date
     *
     * @param date
     * @return java.util.Date
     * @author xiaochen
     * @date 2022-01-20 18:20
     */
    public static String toRfc3339Str(Date date) {
        DateTime dt1 = new DateTime(new Date(), DateTimeZone.forTimeZone(TimeZone.getTimeZone("Asia/Shanghai")));
        return dt1.toString();
    }
    /**
     * 根据 formatter解析为 ZonedDateTime
     *
     * @param text      待解析字符串
     * @param formatter DateTimeFormatter
     * @return ZonedDateTime
     */
    public static ZonedDateTime parseToZonedDateTime(String text, DateTimeFormatter formatter) {
        return ZonedDateTime.parse(text, formatter);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/wxPay/utils/WxV3Pay.java
New file
@@ -0,0 +1,212 @@
package com.ruoyi.system.wxPay.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.system.utils.wx.tools.WxUtils;
import com.ruoyi.system.wxPay.model.WeixinPayProperties;
import com.ruoyi.system.wxPay.model.WxCloseOrderModel;
import com.ruoyi.system.wxPay.model.WxPaymentInfoModel;
import com.ruoyi.system.wxPay.model.WxPaymentRefundModel;
import com.ruoyi.system.wxPay.resp.NotifyV3PayDecodeRespBody;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.impl.client.CloseableHttpClient;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.Map;
/**
 * @author xiaochen
 * @ClassName WxWifiV3Pay
 * @Description
 * @date 2021-11-13 21:10
 */
@Slf4j
public class WxV3Pay extends WxAbstractPay {
    @Getter
    private WeixinPayProperties config;
    @Getter
    private Verifier verifier;
    private WechatPayHttpClientBuilder builder;
    @Getter
    private CloseableHttpClient httpClient;
    private PrivateKeySigner privateKeySigner;
    private WechatPay2Validator validator;
    @Getter
    private PrivateKey privateKey;
    /**
     * 初始化
     *
     * @param config
     */
    public WxV3Pay(WeixinPayProperties config) {
        // 检查v3支付配置信息
        if (WxUtils.getLogger().isDebugEnabled()) {
            WxUtils.debug("开始检查v3支付配置信息....");
        }
        try {
            this.config = config;
            checkWxConfig();
            this.privateKey = PemUtil.loadPrivateKey(config.getV3().getPrivateKeyStream());
            this.privateKeySigner = new PrivateKeySigner(config.getV3().getMchSerialNo(), privateKey);
            // 获取证书管理器实例
            CertificatesManager certificatesManager = CertificatesManager.getInstance();
            // 向证书管理器增加需要自动更新平台证书的商户信息
            certificatesManager.putMerchant(config.getMchId(), new WechatPay2Credentials(config.getMchId(),
                    this.privateKeySigner), config.getV3().getApiKey().getBytes(StandardCharsets.UTF_8));
            // 从证书管理器中获取verifier
            this.verifier = certificatesManager.getVerifier(config.getMchId());
            this.validator = new WechatPay2Validator(verifier);
            this.builder = WechatPayHttpClientBuilder.create()
                    .withMerchant(config.getMchId(), config.getV3().getMchSerialNo(), this.privateKey)
                    .withValidator(this.validator);
            this.httpClient = this.builder.build();
        } catch (Exception e) {
            // 打印异常信息
            e.printStackTrace();
            WxUtils.warn("检查v3支付配置信息出现错误,直连商户商户号:{},{}", config.getMchId(), e.getMessage());
            return;
        }
        if (WxUtils.getLogger().isDebugEnabled()) {
            WxUtils.debug("检查v3支付配置信息完成,未出现异常....");
        }
    }
    /**
     * 检查支付配置信息
     *
     * @throws Exception
     */
    private void checkWxConfig() throws Exception {
        if (this.config == null) {
            throw new Exception("config is null");
        }
        if (config.getMchId() == null || config.getMchId().trim().length() == 0) {
            throw new Exception("MchID in config is empty");
        }
        if (config.getV3().getMchSerialNo() == null) {
            throw new Exception("mchSerialNo in config is empty");
        }
        if (config.getV3().getPrivateKeyStream() == null) {
            throw new Exception("cert stream in config is empty");
        }
        if (this.config.getHttpConnectTimeoutMs() < 10) {
            throw new Exception("http connect timeout is too small");
        }
        if (this.config.getHttpReadTimeoutMs() < 10) {
            throw new Exception("http read timeout is too small");
        }
    }
    /**
     * jsApi下单
     *
     * @param tradeNo     订单号
     * @param amount      金额 分
     * @param openid      openid
     * @param description 订单描述
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @author xiaochen
     * @date 2022-03-22 12:47
     */
    public Map<String, Object> jsApi(String tradeNo, Integer amount, String openid, String notify_url, String description) {
        WxPaymentInfoModel requestBody = WxPaymentInfoModel.builder()
                .mchid(this.config.getMchId())
                .appid(this.config.getAppId())
                .description(description)
                .out_trade_no(tradeNo)
                .notify_url(notify_url)
//                .attach("")
                .amount(WxPaymentInfoModel.Amount.builder().total(amount).build())
                .payer(WxPaymentInfoModel.Payer.builder().openid(openid).build())
                // 分不分账
//                .settle_info(WxPaymentInfoModel.SettleInfo.builder().profit_sharing(true).build())
                .build();
        // 封装基础数据
        String reqBody = buildBaseParam(requestBody
                , this.config.getV3().getNotifyPayUrl());
        //请求URL
        HttpEntityEnclosingRequestBase httpPost = requestPost(
                "/v3/pay/transactions/jsapi"
                , this.config.getHttpReadTimeoutMs()
                , this.config.getHttpConnectTimeoutMs()
                , reqBody);
        String repBody = result(httpClient, httpPost);
        ObjectMapper om = new ObjectMapper();
        try {
            JsonNode rootNode = om.readTree(repBody);
            String prepayId = rootNode.path("prepay_id").asText();
            return wxTuneUp(this.privateKeySigner, requestBody.getAppid(), prepayId);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("获取支付数据错误!");
        }
    }
    /**
     * 接收回调
     *
     * @param request
     * @return
     * @throws Exception
     */
    @Override
    public <T> T verifyNotify(HttpServletRequest request, TypeReference<T> valueTypeRef) throws Exception {
        return verifyNotify(request, this.verifier, this.config.getV3().getApiKey(), valueTypeRef);
    }
    /**
     * 订单查询
     *
     * @param out_trade_no
     * @return com.abl.biz.center.payment.wx.v3.NotifyV3PayDecodeRespBody
     * @author xiaochen
     * @date 2021-12-20 16:47
     */
    @Override
    public NotifyV3PayDecodeRespBody query(String out_trade_no) {
        String url =
                String.format("/v3/pay/transactions/out-trade-no/%s", out_trade_no) + String.format("?mchid=%s", this.getConfig().getMchId());
        return query(this.httpClient, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), url);
    }
    /**
     * 退款
     *
     * @param refundModel
     * @return java.util.Map<java.lang.String, java.lang.Object>
     * @author xiaochen
     */
    @Override
    public Map<String, Object> refund(WxPaymentRefundModel refundModel) {
        refundModel.setNotify_url(this.config.getV3().getNotifyRefundUrl() + refundModel.getNotify_url());
        return refund(this.httpClient, "/v3/refund/domestic/refunds", this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), refundModel);
    }
    /**
     * 关闭订单
     * @param out_trade_no
     * @return
     */
    @Override
    public String close(String out_trade_no) {
        String uri = String.format("/v3/pay/transactions/out-trade-no/%s/close", out_trade_no);
        WxCloseOrderModel wxCloseOrderModel = new WxCloseOrderModel();
        wxCloseOrderModel.setMchid(this.config.getMchId());
        return close(this.httpClient, uri, this.config.getHttpReadTimeoutMs(), this.config.getHttpConnectTimeoutMs(), wxCloseOrderModel);
    }
}
ruoyi-system/src/main/resources/mapper/system/TSysInspectionMapper.xml
@@ -25,6 +25,7 @@
        <result column="constitution_names" property="constitutionNames" />
        <result column="symptom_name" property="symptomName" />
        <result column="tongue_feature" property="tongueFeature" />
        <result column="face_feature" property="faceFeature" />
        <result column="disease_risks" property="diseaseRisks" />
        <result column="is_pay" property="isPay" />
        <result column="create_time" property="createTime" />
@@ -38,7 +39,7 @@
    <sql id="Base_Column_List">
        id, app_user_id,clinic_id, inspection_info, team_code, team_name, device_code, device_name, person_name,
          person_sex, person_age, person_phone, person_height, person_weight, check_time, pdf_url, health_index,
          constitution_names, symptom_name, tongue_feature, disease_risks,is_pay, create_time, update_time,
          constitution_names, symptom_name, tongue_feature,face_feature, disease_risks,is_pay, create_time, update_time,
          create_by, update_by, disabled, create_id
    </sql>
    <select id="pageList" resultType="com.ruoyi.system.vo.TSysInspectionVO">
ruoyi-system/src/main/resources/mapper/system/TSysOtherConfigMapper.xml
New file
@@ -0,0 +1,27 @@
<?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.ruoyi.system.mapper.TSysOtherConfigMapper">
    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.system.model.TSysOtherConfig">
        <id column="id" property="id" />
        <result column="procurement_conditions" property="procurementConditions" />
        <result column="points_expiration" property="pointsExpiration" />
        <result column="expira_unit" property="expiraUnit" />
        <result column="msg_count" property="msgCount" />
        <result column="ai_price" property="aiPrice" />
        <result column="delivery_time" property="deliveryTime" />
        <result column="create_time" property="createTime" />
        <result column="update_time" property="updateTime" />
        <result column="create_by" property="createBy" />
        <result column="update_by" property="updateBy" />
        <result column="disabled" property="disabled" />
        <result column="create_id" property="createId" />
    </resultMap>
    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, procurement_conditions, points_expiration, expira_unit, msg_count, ai_price, delivery_time, create_time, update_time, create_by, update_by, disabled, create_id
    </sql>
</mapper>