Administrator
2024-07-15 740de92b22fe7e4742bd0f46e3f57debf8370f2f
2024-07-15 朱宝民 农户修改接口、补卡是否已退款判断条件
3个文件已修改
10个文件已添加
1676 ■■■■■ 已修改文件
pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/cardOperate/CardOperateCtrl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/client/ClientCtrl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/result/WechatResultCode.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/util/AesUtil.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/util/HmacSha256.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/util/PayHelper.java 508 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/virtualCard/VirtualCardCtrl.java 269 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/virtualCard/VirtualCardSv.java 352 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/PayInfo.java 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/dto/Refund.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/dto/RefundRequest.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/dto/RefundResponse.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/dto/ToRefund.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/cardOperate/CardOperateCtrl.java
@@ -229,7 +229,7 @@
        // 如果传入了退还金额,需要判断老卡(被挂失的水卡)挂失时是否已经退款,无论退款多少都提示用户挂失时已退款
        if(reissueAmount != null && reissueAmount > 0) {
            Double tradeAmount = cardOperateSv.getTradeAmountByCardNo(cardNum);
            if(tradeAmount != null) {
            if(tradeAmount != null && tradeAmount > 0) {
                return BaseResponseUtils.buildErrorMsg(SellResultCode.THE_FEE_CANNOT_BE_REFUNDED.getMessage());
            }
        }
pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/client/ClientCtrl.java
@@ -253,7 +253,8 @@
        String villageName = map_districts.get("villageName").toString();
        // 生成8位行政区划编码,生成农户编号用
        String district8 = countyNum + townNum + villageNum;
        //String district8 = countyNum + townNum + villageNum;
        String district8 = String.format("%02d", Integer.parseInt(countyNum))  + String.format("%03d", Integer.parseInt(townNum)) + String.format("%03d", Integer.parseInt(villageNum));
        // 生成农户编号
        String clientNum = generateClientNum(district8);
        // 生成12位5级行政区划编码串及名称串
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/result/WechatResultCode.java
@@ -29,7 +29,6 @@
     */
    RTU_NOT_EXIST(20001, "阀控器不存在"),
    //RTU_ADDR_CANNOT_BE_NULL(20002, "阀控器地址不能为空");
    CLIENT_CARD_NOT_EXIST(30001, "水卡不存在"),
    /**
@@ -45,7 +44,19 @@
    VALIDATION_TIMEOUT(20004, "验证超时"),
    PHONE_NUMBER_IS_ERROR(20004, "手机号错误,非注册农户"),
    INVALID_CODE(20004, "无效的临时登录凭证"),
    LOGIN_FAIL(20004, "登录失败");
    LOGIN_FAIL(20004, "登录失败"),
    /**
     * 虚拟卡
     */
    ABNORMAL(10001, "退款异常"),
    PROCESSING(10001, "退款处理中"),
    VC_OPEN_ACCOUNT_FAIL(90002, "虚拟卡账户注册失败"),
    CARD_NUMBER_OVERRUN(10002, "水卡编号已满"),
    VIRTUAL_CARD_NOT_EXIST(90006, "虚拟卡账户不存在"),
    RECHARGE_NOT_EXIST(90006, "充值记录不存在"),
    RECHARGE_FAIL(90006, "充值失败"),
    NO_ACCOUNT(40001, "您指定的虚拟卡未注册");
    private final Integer code;
    private final String message;
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/util/AesUtil.java
New file
@@ -0,0 +1,50 @@
package com.dy.pipIrrWechat.util;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:18
 * @LastEditTime 2024-07-15 10:18
 * @Description
 */
public class AesUtil {
    static final int KEY_LENGTH_BYTE = 32;
    static final int TAG_LENGTH_BIT = 128;
    /**
     * 解密
     * @param apiV3Key apiV3密钥
     * @param associatedData 附加数据
     * @param nonce 随机串
     * @param ciphertext 数据密文
     * @return 解密后字符串
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public static String decryptToString(byte[] apiV3Key, byte[] associatedData, byte[] nonce, String ciphertext) throws GeneralSecurityException, IOException {
        try {
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec key = new SecretKeySpec(apiV3Key, "AES");
            GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
            cipher.init(Cipher.DECRYPT_MODE, key, spec);
            cipher.updateAAD(associatedData);
            return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new IllegalStateException(e);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new IllegalArgumentException(e);
        }
    }
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/util/HmacSha256.java
New file
@@ -0,0 +1,43 @@
package com.dy.pipIrrWechat.util;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:19
 * @LastEditTime 2024-07-15 10:19
 * @Description
 */
public class HmacSha256 {
    public static String getSignature(String secretKey, String data) throws NoSuchAlgorithmException, InvalidKeyException {
        // 创建密钥对象
        byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
        // 创建Mac对象并初始化
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(secretKeySpec);
        // 将待加密的数据转换为字节数组
        byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
        // 使用密钥对数据进行加密
        byte[] encryptedBytes = mac.doFinal(dataBytes);
        // 将加密后的结果转换为十六进制字符串
        StringBuilder hexString = new StringBuilder();
        for (byte b : encryptedBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        String hmacSha256 = hexString.toString();
        return hmacSha256;
    }
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/util/PayHelper.java
New file
@@ -0,0 +1,508 @@
package com.dy.pipIrrWechat.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.dy.common.webUtil.BaseResponse;
import com.dy.common.webUtil.BaseResponseUtils;
import com.dy.pipIrrGlobal.pojoSe.SeVirtualCard;
import com.dy.pipIrrGlobal.voSe.VoOrders;
import com.dy.pipIrrWechat.result.WechatResultCode;
import com.dy.pipIrrWechat.virtualCard.VirtualCardSv;
import com.dy.pipIrrWechat.wechatpay.PayInfo;
import com.dy.pipIrrWechat.wechatpay.dto.Refund;
import com.dy.pipIrrWechat.wechatpay.dto.RefundRequest;
import com.dy.pipIrrWechat.wechatpay.dto.RefundResponse;
import com.dy.pipIrrWechat.wechatpay.dto.ToRefund;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import javax.crypto.NoSuchPaddingException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:11
 * @LastEditTime 2024-07-15 10:11
 * @Description
 */
@Component
@RequiredArgsConstructor
public class PayHelper {
    private final VirtualCardSv virtualCardSv;
    private final RestTemplateUtil restTemplateUtil;
    private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    private String checkSessionUrl = PayInfo.checkSessionUrl;
    private String tokenUrl = PayInfo.tokenUrl;
    private String resetUserSessionKeyUrl = PayInfo.resetUserSessionKeyUrl;
    private String notifyUrl = PayInfo.notifyUrl;
    private String schema = PayInfo.schema;
    private String privateCertFileName = PayInfo.privateCertFileName;
    private String refundUrl = PayInfo.refundUrl;
    // 平台证书公钥
    public Map<String, Certificate> CERTIFICATE_MAP = new HashMap();
    /**
     * 获取32位随机字符串
     * @return 随机串
     */
    public String generateRandomString() {
        Random random = new Random();
        StringBuilder sb = new StringBuilder(32);
        for (int i = 0; i < 32; i++) {
            int index = random.nextInt(CHARACTERS.length());
            sb.append(CHARACTERS.charAt(index));
        }
        return sb.toString();
    }
    /**
     * 获取商户证书私钥对象
     * @param filename 私钥文件路径
     * @return 私钥对象
     * @throws IOException
     */
    public PrivateKey getPrivateKey(String filename) throws IOException {
        String content = new String(Files.readAllBytes(Paths.get(filename)), "utf-8");
        try {
            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")
                    .replace("-----END PRIVATE KEY-----", "")
                    .replaceAll("\\s+", "");
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("当前Java环境不支持RSA", e);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("无效的密钥格式");
        }
    }
    /**
     * 检验登录态
     * @param appid 小程序 appId
     * @param secret 小程序 appSecret
     * @param openid 用户唯一标识符
     * @param sessionKey 会话密钥
     * @return
     * @throws IOException
     */
    public JSONObject checkSessionKey(String appid, String secret, String openid, String sessionKey) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        String accessToken = "";
        Integer expiresIn = 0;
        String signature = HmacSha256.getSignature(sessionKey, "");
        String sigMethod = "hmac_sha256";
        JSONObject job_token = getAccessToken(appid, secret);
        if(job_token != null) {
            accessToken = job_token.getString("access_token");
            expiresIn = job_token.getInteger("expires_in");
        }
        Map<String, Object> queryParams = new HashMap<>();
        queryParams.put("access_token", accessToken);
        queryParams.put("openid", openid);
        queryParams.put("signature", signature);
        queryParams.put("sig_method", sigMethod);
        Map<String, String> headerParams = new HashMap<>();
        JSONObject result = restTemplateUtil.get(checkSessionUrl, queryParams, headerParams);
        return result;
    }
    /**
     * 重置登录态
     * @param appid 小程序 appId
     * @param secret 小程序 appSecret
     * @param openid 用户唯一标识符
     * @param sessionKey 会话密钥
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws IOException
     */
    public JSONObject resetUserSessionKey(String appid, String secret, String openid, String sessionKey) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
        String accessToken = "";
        Integer expiresIn = 0;
        String signature = HmacSha256.getSignature(sessionKey, "");
        String sigMethod = "hmac_sha256";
        JSONObject job_token = getAccessToken(appid, secret);
        if(job_token != null) {
            accessToken = job_token.getString("access_token");
            expiresIn = job_token.getInteger("expires_in");
        }
        Map<String, Object> queryParams = new HashMap<>();
        queryParams.put("access_token", accessToken);
        queryParams.put("openid", openid);
        queryParams.put("signature", signature);
        queryParams.put("sig_method", sigMethod);
        Map<String, String> headerParams = new HashMap<>();
        JSONObject result = restTemplateUtil.get(resetUserSessionKeyUrl, queryParams, headerParams);
        return result;
    }
    /**
     * 获取接口调用凭据
     * @param appid 小程序 appId
     * @param secret 小程序 appSecret
     * @return 凭据及凭据有效时间
     * @throws IOException
     */
    public JSONObject getAccessToken(String appid, String secret) throws IOException {
        Map<String, Object> queryParams = new HashMap<>();
        queryParams.put("grant_type", "client_credential");
        queryParams.put("appid", appid);
        queryParams.put("secret", secret);
        Map<String, String> headerParams = new HashMap<>();
        JSONObject job_result = restTemplateUtil.get(tokenUrl, queryParams, headerParams);
        return job_result;
    }
    /**
     * 构造签名串_下单
     * @param method HTTP请求方法
     * @param url URL
     * @param timestamp 时间戳
     * @param nonceStr 随机串
     * @param body 报文主题
     * @return 签名串
     */
    public String buildMessage_order(String method, String url, long timestamp, String nonceStr, String body) {
        return method + "\n"
                + url + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + body + "\n";
    }
    /**
     * 构造签名串_再次下单
     * @param appid 小程序唯一标识
     * @param timestamp 时间戳
     * @param nonceStr 随机串
     * @param pkg package
     * @return 签名串
     */
    public String buildMessage_signAgain(String appid, String timestamp, String nonceStr, String pkg) {
        return appid + "\n"
                + timestamp + "\n"
                + nonceStr + "\n"
                + pkg + "\n";
    }
    /**
     * 签名
     * @param message 被签名信息
     * @param certFileName 私钥证书文件路径
     * @return signature签名值,签名信息中的一项,参与生成签名信息
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws SignatureException
     * @throws IOException
     */
    public String sign(byte[] message, String certFileName) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException {
        Signature sign = Signature.getInstance("SHA256withRSA");
        sign.initSign(getPrivateKey(certFileName));
        sign.update(message);
        return Base64.getEncoder().encodeToString(sign.sign());
    }
    /**
     * 获取签名信息
     * @param method
     * @param url
     * @param body
     * @return 签名信息,HTTP头中的签名信息
     * HTTP头:Authorization: 认证类型 签名信息
     * 认证类型,WECHATPAY2-SHA256-RSA2048
     */
    public String getToken(String method, String url, String body, String nonceStr, Long timestamp, String certFileName) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException, NoSuchPaddingException {
        String message = buildMessage_order(method, url, timestamp, nonceStr, body);
        String signature = sign(message.getBytes("utf-8"), certFileName);
        return "mchid=\"" + PayInfo.mchid + "\","
                + "nonce_str=\"" + nonceStr + "\","
                + "timestamp=\"" + timestamp + "\","
                + "serial_no=\"" + PayInfo.serial_no + "\","
                + "signature=\"" + signature + "\"";
    }
    /**
     * 构造验造签名串
     * @param wechatpayTimestamp 请求头中返回的时间戳
     * @param wechatpayNonce 请求头中返回的随机串
     * @param boey 请求返回的body
     * @return signatureStr构造的验签名串
     */
    public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String boey) {
        String signatureStr = wechatpayTimestamp + "\n"
                + wechatpayNonce + "\n"
                + boey + "\n";
        return signatureStr;
    }
    /**
     * 重新下载证书
     */
    public void refreshCertificate() throws GeneralSecurityException, IOException {
        String method = "GET";
        String httpUrl = "/v3/certificates";
        String nonceStr = generateRandomString();
        Long timestamp = System.currentTimeMillis() / 1000;
        String header = PayInfo.schema + " " + getToken(method, httpUrl, "", nonceStr, timestamp, PayInfo.privateCertFileName);
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", header);
        headers.put("Accept", "application/json");
        //headers.put("User-Agent", "https://zh.wikipedia.org/wiki/User_agent");
        JSONObject job_result = restTemplateUtil.getHeaders(PayInfo.certificates,null, headers);
        JSONObject job_headers = job_result.getJSONObject("headers");
        String wechatpayNonce = job_headers.getJSONArray("Wechatpay-Nonce").getString(0);
        String wechatpaySerial = job_headers.getJSONArray("Wechatpay-Serial").getString(0);
        String signature_h = job_headers.getJSONArray("Wechatpay-Signature").getString(0);
        String signatureType_h = job_headers.getJSONArray("Wechatpay-Signature-Type").getString(0);
        String wechatpayTimestamp = job_headers.getJSONArray("Wechatpay-Timestamp").getString(0);
        JSONObject job_body = job_result.getJSONObject("body");
        if(job_body != null) {
            JSONArray array = job_body.getJSONArray("data");
            if(array != null && array.size() > 0) {
                for(int i = 0; i < array.size(); i++) {
                    JSONObject job_data = array.getJSONObject(i);
                    String certificateSerial = job_data.getString("serial_no");
                    String effective_time  = job_data.getString("effective_time");
                    String expire_time  = job_data.getString("expire_time");
                    JSONObject job_certificate = job_data.getJSONObject("encrypt_certificate");
                    String algorithm = job_certificate.getString("algorithm");
                    String nonce  = job_certificate.getString("nonce");
                    String associated_data  = job_certificate.getString("associated_data");
                    String ciphertext  = job_certificate.getString("ciphertext");
                    //对证书密文进行解密得到平台证书公钥
                    String publicKey = AesUtil.decryptToString(PayInfo.key.getBytes("utf-8"), associated_data.getBytes("utf-8"), nonce.getBytes("utf-8"), ciphertext);
                    // 将平台公钥字符串转成Certificate对象
                    final CertificateFactory cf = CertificateFactory.getInstance("X509");
                    ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
                    Certificate certificate = null;
                    try {
                        certificate = cf.generateCertificate(inputStream);
                    } catch (CertificateException e) {
                        e.printStackTrace();
                    }
                    // 响应头证书序号与响应体证书序列号一致,且时间差小于5分钟时才将证书存储map
                    Long timeDiff = (System.currentTimeMillis() / 1000 - Long.parseLong(wechatpayTimestamp))/60;
                    if(wechatpaySerial.equals(certificateSerial) && timeDiff <= 5) {
                        // 证书放入MAP
                        CERTIFICATE_MAP.put(certificateSerial, certificate);
                    }
                }
            }
        }
    }
    /**
     * 使用微信平台证书进行响应验签
     * @param wechatpaySerial 来自响应头的微信平台证书序列号
     * @param signatureStr 构造的验签名串
     * @param wechatpaySignature 来自响应头的微信平台签名
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     * @throws SignatureException
     */
    public Boolean responseSignVerify(String wechatpaySerial, String signatureStr, String wechatpaySignature) throws GeneralSecurityException, IOException {
        if(CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) {
            CERTIFICATE_MAP.clear();
            refreshCertificate();
        }
        Certificate certificate = (Certificate)CERTIFICATE_MAP.get(wechatpaySerial);
        if(certificate == null) {
            return false;
        }
        // 获取公钥
        PublicKey publicKey = certificate.getPublicKey();
        // 初始化SHA256withRSA前面器
        Signature signature = Signature.getInstance("SHA256withRSA");
        // 用微信平台公钥对前面器进行初始化
        signature.initVerify(certificate);
        // 将构造的验签名串更新到签名器中
        signature.update(signatureStr.getBytes(StandardCharsets.UTF_8));
        // 请求头中微信服务器返回的签名用Base64解码,使用签名器进行验证
        boolean valid = signature.verify(Base64.getDecoder().decode(wechatpaySignature));
        return valid;
    }
    /**
     * 获取待退款对象列表
     *      待退款对象包含订单号和可退款金额
     *      订单对象包含订单号、充值金额、充值完成时间
     *      1. 根据虚拟卡号到虚拟卡表中取出该卡余额
     *      2. 根据虚拟卡号到充值表取出订单对象列表
     * @param virtualId
     * @param refundAmount
     * @return
     */
    public List<ToRefund> getToRefunds(Long virtualId, Integer refundAmount) {
        ToRefund toRefund = new ToRefund();
        List<ToRefund> list = new ArrayList<>();
        Double money = 0d;
        // 根据虚拟卡号获取当前虚拟卡余额
        SeVirtualCard seVirtualCard = virtualCardSv.selectVirtuCardById(virtualId);
        if(seVirtualCard != null) {
            money = seVirtualCard.getMoney();
        }
        // 要退金额大于该卡余额,返回空列表
        if(refundAmount > money) {
            return list;
        }
        // 根据虚拟卡号获取订单列表(仅限充值成功的)
        List<VoOrders> list_Orders = virtualCardSv.selectOrders(virtualId);
        // 遍历订单列表,获取
        if(list_Orders != null && list_Orders.size() > 0) {
            JSONArray array_Orders = (JSONArray) JSON.toJSON(list_Orders);
            for(int i = 0; i < array_Orders.size(); i++) {
                JSONObject job_order = array_Orders.getJSONObject(i);
                String orderNumber = job_order.getString("orderNumber");
                Integer rechargeAmount = job_order.getInteger("rechargeAmount");
                Date rechargeTime = job_order.getDate("rechargeTime");
                // 计算充值至今时间差(分钟)
                Long timestamp_Recharge = rechargeTime.getTime() / 1000;
                Long timestamp_Current = System.currentTimeMillis() / 1000;
                Long timeDiff_Minute = (timestamp_Current - timestamp_Recharge)/60;
                // 获取该订单已退款笔数
                Integer refundCount = 0;
                List<Integer> list_RefundAmount = virtualCardSv.selectRefundAmount(orderNumber);
                if(list_RefundAmount != null && list_RefundAmount.size() > 0) {
                    refundCount = list_RefundAmount.size();
                }
                // 充值至今未超过一年且该订单退款总次数未超过50次
                if(timeDiff_Minute/(365*24*60) >= 1 && (refundCount + 1) > 50)
                    return list;
                /**
                 * 1. 如果要退金额小于当前订单的充值金额,要退金额即为应退金额并返回
                 * 2. 如果要推金额大于当前订单充值金额,当前订单充值金额即为应退金额
                 *      a. 生成应退款对象
                 *      b. 计算新的余额
                 *      c. 金蒜新的要退款金额
                 *      d. 如果要退金额大于0,遍历下一个订单
                 */
                if(refundAmount <= rechargeAmount) {
                    toRefund = new ToRefund();
                    toRefund.setOrderNumber(orderNumber);
                    toRefund.setRefundAmount(refundAmount);
                    list.add(toRefund);
                    // 计算新的余额和新的要退金额
                    money = money - refundAmount;
                    refundAmount = refundAmount - refundAmount;
                    return list;
                }else {
                    toRefund = new ToRefund();
                    toRefund.setOrderNumber(orderNumber);
                    toRefund.setRefundAmount(rechargeAmount);
                    list.add(toRefund);
                    // 计算新的余额和新的要退金额
                    money = money - rechargeAmount;
                    refundAmount = refundAmount - rechargeAmount;
                    if(refundAmount > 0) {
                        continue;
                    }else {
                        return list;
                    }
                }
            }
        }
        return list;
    }
    /**
     * 退款申请,调用微信支付退款申请接口
     * @param po 退款请求对象,包含订单号、退款单号、退款金额
     * @return
     * @throws NoSuchPaddingException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @throws IOException
     * @throws SignatureException
     * @throws InvalidKeyException
     */
    public BaseResponse<Boolean> refunds(Refund po) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException {
        String tradeNo = po.getTradeNo();
        String refundNo = po.getRefundNo();
        Integer refund = po.getRefund();
        // 生成body
        Integer total = virtualCardSv.getRechargeAmountByOrderNumber(tradeNo);
        RefundRequest.Amount amount = new RefundRequest.Amount();
        amount.setRefund(refund);
        amount.setTotal(total);
        amount.setCurrency("CNY");
        RefundRequest refundRequest = new RefundRequest();
        refundRequest.setOut_trade_no(tradeNo);
        refundRequest.setOut_refund_no(refundNo);
        refundRequest.setNotify_url(notifyUrl);
        refundRequest.setAmount(amount);
        // 生成header
        String nonceStr = generateRandomString();
        Long timestamp = System.currentTimeMillis() / 1000;
        String method = "POST";
        String httpUrl = "/v3/refund/domestic/refunds";
        String body = JSONObject.toJSONString(refundRequest);
        String header = schema + " " + getToken(method, httpUrl, body, nonceStr, timestamp, privateCertFileName);
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", header);
        headers.put("Accept", "application/json");
        headers.put("Content-Type", "application/json");
        JSONObject job_refundResponse = restTemplateUtil.post(PayInfo.refundUrl, body, headers);
        RefundResponse refundResponse = JSON.parseObject(job_refundResponse.toJSONString(), RefundResponse.class);
        String status = refundResponse.getStatus();
        if(status != null && status.equals("SUCCESS")) {
            // 退款申请已受理
            return BaseResponseUtils.buildSuccess(true) ;
        } else if(status != null && status.equals("PROCESSING")) {
            // 退款处理中
            return BaseResponseUtils.buildFail(WechatResultCode.PROCESSING.getMessage());
        } else {
            // 退款异常
            return BaseResponseUtils.buildError(WechatResultCode.ABNORMAL.getMessage());
        }
    }
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/virtualCard/VirtualCardCtrl.java
New file
@@ -0,0 +1,269 @@
package com.dy.pipIrrWechat.virtualCard;
import com.dy.common.aop.SsoAop;
import com.dy.common.webUtil.BaseResponse;
import com.dy.common.webUtil.BaseResponseUtils;
import com.dy.common.webUtil.QueryResultVo;
import com.dy.pipIrrGlobal.pojoSe.SeVirtualCard;
import com.dy.pipIrrGlobal.voSe.VoVcRecharge;
import com.dy.pipIrrGlobal.voSe.VoVirtualCard;
import com.dy.pipIrrWechat.util.PayHelper;
import com.dy.pipIrrWechat.virtualCard.enums.LastOperateENUM;
import com.dy.pipIrrWechat.result.WechatResultCode;
import com.dy.pipIrrWechat.virtualCard.dto.DtoRegist;
import com.dy.pipIrrWechat.virtualCard.dto.DtoVcRecharge;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 9:55
 * @LastEditTime 2024-07-15 9:55
 * @Description
 */
@Slf4j
@Tag(name = "虚拟卡管理", description = "虚拟卡管理")
@RestController
@RequestMapping(path="virtual_card")
@RequiredArgsConstructor
@Validated
public class VirtualCardCtrl {
    private final VirtualCardSv virtualCardSv;
    private final PayHelper payHelper;
    /**
     * 获取农户全部虚拟卡
     * @return
     */
    @GetMapping(path = "/get")
    @SsoAop()
    public BaseResponse<List<VoVirtualCard>> getVCs(Long clientId){
        try {
            List<VoVirtualCard> res = virtualCardSv.getVCs(clientId);
            return BaseResponseUtils.buildSuccess(res);
        } catch (Exception e) {
            log.error("获取支付方式记录异常", e);
            return BaseResponseUtils.buildException(e.getMessage()) ;
        }
    }
    /**
     * 根据虚拟卡ID获取虚拟卡对象
     * @param vcId
     * @return
     */
    @GetMapping(path = "/getVcById")
    @SsoAop()
    public BaseResponse<VoVirtualCard> getVcById(@RequestParam Long vcId){
        try {
            return BaseResponseUtils.buildSuccess(virtualCardSv.getVcById(vcId));
        } catch (Exception e) {
            log.error("获取支付方式记录异常", e);
            return BaseResponseUtils.buildException(e.getMessage()) ;
        }
    }
    /**
     * 虚拟卡账号注册
     * @param po
     * @param bindingResult
     * @return
     */
    @PostMapping(path = "add_vc")
    @SsoAop()
    public BaseResponse<Boolean> addVC(@RequestBody @Valid DtoRegist po, BindingResult bindingResult){
        if(bindingResult != null && bindingResult.hasErrors()){
            return BaseResponseUtils.buildFail(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
        }
        Long clientId = po.getClientId();
        // 获取5级行政区划串areaCode
        String areaCode = String.valueOf(virtualCardSv.getAreaCodeById(clientId));
        /**
         * 根据行政区划串(areaCode)在虚拟卡表中针对虚拟卡编号(vcNum)进行模糊查询
         * 如果5位顺序号已经达到最大值,提示用户联系系统管理员
         * 如果5位顺序号未达到最大值,则加1
         * cardNum为新的卡号
         */
        String vcNum = Optional.ofNullable(virtualCardSv.getVcCardNumOfMax(areaCode)).orElse("");
        if(vcNum != null && vcNum.trim().length() > 0) {
            Integer number = Integer.parseInt(vcNum.substring(12));
            number = number + 1;
            if(number > 65535) {
                return BaseResponseUtils.buildFail(WechatResultCode.CARD_NUMBER_OVERRUN.getMessage());
            }
            vcNum = vcNum.substring(0, 12) + String.format("%05d", number);
        } else {
            vcNum = areaCode + "00001";
        }
        SeVirtualCard seVirtualCard = new SeVirtualCard();
        seVirtualCard.setVcNum(Long.parseLong(vcNum));
        seVirtualCard.setClientId(clientId);
        seVirtualCard.setMoney(0d);
        seVirtualCard.setLastOperate(LastOperateENUM.OPEN_ACCOUNT.getCode());
        seVirtualCard.setLastOperateTime(new Date());
        seVirtualCard.setInUse((byte) 0);
        seVirtualCard.setCreateTime(new Date());
        Long rec = virtualCardSv.insertVirtualCard(seVirtualCard);
        if(rec == null) {
            return BaseResponseUtils.buildFail(WechatResultCode.VC_OPEN_ACCOUNT_FAIL.getMessage());
        }
        return BaseResponseUtils.buildSuccess(true) ;
    }
    /**
     * 用户申请退款
     * @param po
     * @param bindingResult
     * @return
     */
    //@Operation(summary = "虚拟卡申请退款", description = "虚拟卡申请退款")
    //@ApiResponses(value = {
    //        @ApiResponse(
    //                responseCode = ResultCodeMsg.RsCode.SUCCESS_CODE,
    //                description = "操作结果:true:成功,false:失败(BaseResponse.content)",
    //                content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
    //                        schema = @Schema(implementation = Boolean.class))}
    //        )
    //})
    //@PostMapping(path = "add_refund", consumes = MediaType.APPLICATION_JSON_VALUE)
    //@Transactional(rollbackFor = Exception.class)
    //@SsoAop()
    //public BaseResponse<Boolean> addRefund(@RequestBody @Valid DtoRefund po, BindingResult bindingResult){
    //    if(bindingResult != null && bindingResult.hasErrors()){
    //        return BaseResponseUtils.buildFail(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
    //    }
    //
    //    Long virtualId = po.getVirtualId();
    //    Integer refundAmount = po.getRefundAmount();
    //
    //    // 根据虚拟卡ID获取虚拟卡对象
    //    SeVirtualCard seVirtualCard = virtualCardSv.selectVirtuCardById(virtualId);
    //    if(seVirtualCard == null) {
    //        return BaseResponseUtils.buildFail(WechatResultCode.VIRTUAL_CARD_NOT_EXIST.getMessage());
    //    }
    //    Long clientId = seVirtualCard.getClientId();
    //    Double money = seVirtualCard.getMoney();
    //
    //    // 验证退款金额是否大于余额
    //    if(refundAmount > money) {
    //        return BaseResponseUtils.buildFail(WechatResultCode.REFUND_AMOUNT_CANNOT_GREATER_THAN_MONEY.getMessage());
    //    }
    //
    //    // 计算消费后余额
    //    Double afterRefund = money - refundAmount;
    //
    //    SeVcRefund seVcRefund = new SeVcRefund();
    //    seVcRefund.setVcId(virtualId);
    //    seVcRefund.setClientId(clientId);
    //    seVcRefund.setMoney(money);
    //    seVcRefund.setRefundAmount(refundAmount);
    //    seVcRefund.setAfterRefund(afterRefund);
    //    seVcRefund.setApplicationTime(new Date());
    //    seVcRefund.setRefundStatus(RefundStateENUM.TO_AUDIT.getCode());
    //
    //    Long rec = virtualCardSv.addRefund(seVcRefund);
    //    if(rec == 0) {
    //        return BaseResponseUtils.buildFail(WechatResultCode.APPLICATION_REFUND_FAIL.getMessage());
    //    }
    //    return BaseResponseUtils.buildSuccess(true) ;
    //}
    /**
     * 审核退款申请
     * @param po
     * @param bindingResult
     * @return
     */
    //@Operation(summary = "审核退款申请", description = "审核退款申请")
    //@ApiResponses(value = {
    //        @ApiResponse(
    //                responseCode = ResultCodeMsg.RsCode.SUCCESS_CODE,
    //                description = "操作结果:true:成功,false:失败(BaseResponse.content)",
    //                content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
    //                        schema = @Schema(implementation = Boolean.class))}
    //        )
    //})
    //@PostMapping(path = "audit_refund", consumes = MediaType.APPLICATION_JSON_VALUE)
    //@Transactional(rollbackFor = Exception.class)
    //@SsoAop()
    //public BaseResponse<Boolean> auditRefund(@RequestBody @Valid DtoAudit po, BindingResult bindingResult) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, IOException, SignatureException, InvalidKeyException {
    //    if(bindingResult != null && bindingResult.hasErrors()){
    //        return BaseResponseUtils.buildFail(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
    //    }
    //
    //    // 根据退款ID获取退款对象,并更新审核人、审核时间、审核备注、退款状态字段
    //    SeVcRefund seVcRefund = virtualCardSv.selectRefundByRefundId(po.getRefundId());
    //    Long virtualId = seVcRefund.getVcId();
    //    Integer refundAmount = seVcRefund.getRefundAmount();
    //    seVcRefund.setAuditor(po.getAuditor());
    //    seVcRefund.setAuditTime(new Date());
    //    seVcRefund.setRemarks(po.getRemarks());
    //    seVcRefund.setRefundStatus(RefundStateENUM.TO_REFUND.getCode());
    //    Integer rec = virtualCardSv.updateRefund(seVcRefund);
    //    if(rec == 0) {
    //        return BaseResponseUtils.buildFail(WechatResultCode.AUDIT_REFUND_FAIL.getMessage());
    //    }
    //
    //    // 完成审核后获取待退款订单列表
    //    List<ToRefund> list_ToRefund = payHelper.getToRefunds(virtualId, refundAmount);
    //    if(list_ToRefund == null || list_ToRefund.size() <=0)
    //        return BaseResponseUtils.buildFail(WechatResultCode.NOT_SUFFICIENT_FUNDS.getMessage());
    //
    //    //遍历待退款列表
    //    JSONArray array_ToRefund = (JSONArray) JSON.toJSON(list_ToRefund);
    //    for(int i = 0; i < array_ToRefund.size(); i++) {
    //        JSONObject job_ToRefund = array_ToRefund.getJSONObject(i);
    //        String orderNumber_ToRefund = job_ToRefund.getString("orderNumber");
    //        Integer refundAmount_ToRefund = job_ToRefund.getInteger("refundAmount");
    //
    //        // 生成退款分项记录
    //        SeVcRefundItem seVcRefundItem = new SeVcRefundItem();
    //        seVcRefundItem.setRefundId(po.getRefundId());
    //        seVcRefundItem.setOrderNumber(orderNumber_ToRefund);
    //        String refundNumber = virtualCardSv.generateRefundNumber(orderNumber_ToRefund);
    //        seVcRefundItem.setRefundNumber(refundNumber);
    //        seVcRefundItem.setRefundAmount(refundAmount_ToRefund);
    //        seVcRefundItem.setCreateTime(new Date());
    //        seVcRefundItem.setRefundStatus(RefundItemStateENUM.NO_REFUND.getCode());
    //        Long refundItemId = virtualCardSv.addRefundItem(seVcRefundItem);
    //
    //        // 调用微信退款申请接口
    //        Refund refund = new Refund();
    //        refund.setTradeNo(orderNumber_ToRefund);
    //        refund.setRefundNo(refundNumber);
    //        refund.setRefund(refundAmount_ToRefund);
    //        BaseResponse rep = payHelper.refunds(refund);
    //    }
    //
    //    return BaseResponseUtils.buildSuccess(true) ;
    //}
    /**
     * 获取虚拟卡充值记录
     * @return
     */
    @GetMapping(path = "/getVcRechargeRecords")
    @SsoAop()
    public BaseResponse<QueryResultVo<List<VoVcRecharge>>> getVcRechargeRecords(DtoVcRecharge dtoVcRecharge){
        try {
            QueryResultVo<List<VoVcRecharge>> res = virtualCardSv.getVcRechargeRecords(dtoVcRecharge);
            return BaseResponseUtils.buildSuccess(res);
        } catch (Exception e) {
            log.error("获取虚拟卡充值记录异常", e);
            return BaseResponseUtils.buildException(e.getMessage()) ;
        }
    }
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/virtualCard/VirtualCardSv.java
New file
@@ -0,0 +1,352 @@
package com.dy.pipIrrWechat.virtualCard;
import com.dy.common.webUtil.BaseResponse;
import com.dy.common.webUtil.BaseResponseUtils;
import com.dy.common.webUtil.QueryResultVo;
import com.dy.pipIrrGlobal.daoSe.*;
import com.dy.pipIrrGlobal.pojoSe.SeVcRecharge;
import com.dy.pipIrrGlobal.pojoSe.SeVcRefund;
import com.dy.pipIrrGlobal.pojoSe.SeVcRefundItem;
import com.dy.pipIrrGlobal.pojoSe.SeVirtualCard;
import com.dy.pipIrrGlobal.voSe.VoOrders;
import com.dy.pipIrrGlobal.voSe.VoVcRecharge;
import com.dy.pipIrrGlobal.voSe.VoVirtualCard;
import com.dy.pipIrrWechat.virtualCard.dto.DtoVcRecharge;
import com.dy.pipIrrWechat.virtualCard.dto.DtoVirtualCard;
import com.dy.pipIrrWechat.virtualCard.enums.LastOperateENUM;
import com.dy.pipIrrWechat.virtualCard.enums.OrderStateENUM;
import com.dy.pipIrrWechat.result.WechatResultCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.utils.PojoUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 9:39
 * @LastEditTime 2024-07-15 9:39
 * @Description
 */
@Slf4j
@Service
public class VirtualCardSv {
    @Autowired
    private SeVirtualCardMapper seVirtualCardMapper;
    @Autowired
    private SeVcRechargeMapper seVcRechargeMapper;
    @Autowired
    private SeVcRefundMapper seVcRefundMapper;
    @Autowired
    private SeVcRefundItemMapper seVcRefundItemMapper;
    @Autowired
    private SeClientMapper seClientMapper;
    /**
     * 根据农户ID获取5级行政区划代码,注册虚拟卡使用
     * @param clientId 农户ID
     * @return 5级行政区划代码
     */
    public Long getAreaCodeById(Long clientId) {
        return seClientMapper.getAreaCodeById(clientId);
    }
    /**
     * 获取农户全部虚拟卡
     * @return
     */
    public List<VoVirtualCard> getVCs(Long clientId) {
        return seVirtualCardMapper.getVCs(clientId);
    }
    /**
     * 根据虚拟卡ID获取虚拟卡对象
     * @param vcId
     * @return
     */
    public VoVirtualCard getVcById(Long vcId) {
        return seVirtualCardMapper.getVcById(vcId);
    }
    /**
     * 注册虚拟卡
     * @param po
     * @return
     */
    public Long insertVirtualCard(SeVirtualCard po) {
        seVirtualCardMapper.insert(po);
        return po.getId();
    }
    /**
     * 根据行政区划串模糊查询虚拟卡编号,注册虚拟卡使用
     * @param areaCode
     * @return
     */
    String getVcCardNumOfMax(String areaCode) {
        return seVirtualCardMapper.getVcCardNumOfMax(areaCode);
    }
    /** 废弃
     * 验证农户是否拥有指定名称的虚拟卡
     * @param po
     * @return
     */
    //public Integer getRecordCountByName(DtoRegist po) {
    //    return seVirtualCardMapper.getRecordCountByName(po.getClientId(), po.getVcName());
    //}
    /**
     * 修改虚拟卡
     * 充值、消费、申请退款、审核退款时需要修改虚拟卡的:余额、最后操作、最后操作时间
     * @param po
     * @return
     */
    public Integer updateVirtualCard(SeVirtualCard po) {
        return seVirtualCardMapper.updateByPrimaryKeySelective(po);
    }
    /**
     * 根据虚拟卡编号获取虚拟卡对象
     * @param virtualId
     * @return
     */
    public SeVirtualCard selectVirtuCardById(Long virtualId) {
        return seVirtualCardMapper.selectByPrimaryKey(virtualId);
    }
    /**
     * 添加虚拟卡充值记录
     * JSAPI下单后生成部分充值记录
     * @param po
     * @return
     */
    public BaseResponse<Boolean> insertVCRecharge(DtoVirtualCard po) {
        String orderNumber = po.getOrderNumber();
        Long virtualId = po.getVirtualId();
        Long clientId = po.getClientId();
        Integer rechargeAmount = po.getRechargeAmount();
        // 验证该虚拟卡账户是否存在并取出当前账户余额
        SeVirtualCard seVirtualCard = seVirtualCardMapper.selectByPrimaryKey(virtualId);
        if(seVirtualCard == null) {
            return BaseResponseUtils.buildFail(WechatResultCode.NO_ACCOUNT.getMessage());
        }
        Double money = seVirtualCard.getMoney();
        // 添加充值记录
        SeVcRecharge seVcRecharge = new SeVcRecharge();
        seVcRecharge.setVcId(virtualId);
        seVcRecharge.setClientId(clientId);
        seVcRecharge.setMoney(money);
        seVcRecharge.setOrderNumber(orderNumber);
        seVcRecharge.setRechargeAmount(rechargeAmount);
        seVcRecharge.setOrderTime(new Date());
        seVcRecharge.setOrderState(OrderStateENUM.NON_PAYMENT.getCode());
        Integer rec = seVcRechargeMapper.insert(seVcRecharge);
        if(rec == null) {
            return BaseResponseUtils.buildFail(WechatResultCode.RECHARGE_FAIL.getMessage());
        }
        return BaseResponseUtils.buildSuccess(true) ;
    }
    /**
     * 根据订单号获取虚拟卡充值对象
     * @param orderNumber
     * @return
     */
    public SeVcRecharge getVCRechargeByorderNumber(String orderNumber) {
        return seVcRechargeMapper.getVCRechargeByorderNumber(orderNumber);
    }
    /**
     * 修改虚拟卡充值记录
     * 微信支付通知后:
     *      1. 更新充值表:充值后余额、支付完成时间、订单状态
     *      2. 更新虚拟卡表:账户余额、最后操作、最后操作时间
     * @param orderNumber 订单编号
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public BaseResponse<Boolean> updateVCRecharge(String orderNumber, Date rechargeTime) {
        SeVcRecharge seVcRecharge = seVcRechargeMapper.getVCRechargeByorderNumber(orderNumber);
        if(seVcRecharge == null) {
            return BaseResponseUtils.buildFail(WechatResultCode.RECHARGE_NOT_EXIST.getMessage());
        }
        Long virtualId = seVcRecharge.getVcId();
        Double money = seVcRecharge.getMoney();
        Integer rechargeAmount = seVcRecharge.getRechargeAmount();
        Double afterRrecharge = money + rechargeAmount;
        seVcRecharge.setAfterRecharge(afterRrecharge);
        seVcRecharge.setRechargeTime(rechargeTime);
        seVcRecharge.setOrderState(OrderStateENUM.PAID.getCode());
        Integer rec = seVcRechargeMapper.updateByPrimaryKeySelective(seVcRecharge);
        if(rec == null) {
            return BaseResponseUtils.buildFail(WechatResultCode.RECHARGE_FAIL.getMessage());
        }
        SeVirtualCard seVirtualCard = seVirtualCardMapper.selectByPrimaryKey(virtualId);
        if(seVirtualCard == null) {
            return BaseResponseUtils.buildFail(WechatResultCode.VIRTUAL_CARD_NOT_EXIST.getMessage());
        }
        seVirtualCard.setMoney(afterRrecharge);
        seVirtualCard.setLastOperate(LastOperateENUM.RECHARGE.getCode());
        seVirtualCard.setLastOperateTime(new Date());
        Integer rec2 = seVirtualCardMapper.updateByPrimaryKeySelective(seVirtualCard);
        if(rec2 == null) {
            return BaseResponseUtils.buildFail(WechatResultCode.RECHARGE_FAIL.getMessage());
        }
        return BaseResponseUtils.buildSuccess(true) ;
    }
    /**
     * 修改虚拟卡充值记录(废弃)
     * 微信小程序支付通知后修改:余额、充值后余额、充值完成时间
     * @param po
     * @return
     */
    public Integer updateVCRecharge(SeVcRecharge po) {
        return seVcRechargeMapper.updateByPrimaryKeySelective(po);
    }
    /**
     * 根据虚拟卡号获取订单列表
     * @param virtualId
     * @return
     */
    public List<VoOrders> selectOrders(Long virtualId) {
        List<VoOrders> rsVo = seVcRechargeMapper.getOrders(virtualId);
        return rsVo ;
    }
    /**
     * 根据退款ID获取退款对象
     * @param refundId
     * @return
     */
    public SeVcRefund selectRefundByRefundId(Long refundId) {
        return seVcRefundMapper.selectByPrimaryKey(refundId);
    }
    /**
     * 添加退款申请
     * @param po
     * @return
     */
    public Long addRefund(SeVcRefund po) {
        seVcRefundMapper.insert(po);
        return po.getId();
    }
    /**
     * 修改退款记录
     * @param po
     * @return
     */
    public Integer updateRefund(SeVcRefund po) {
        return seVcRefundMapper.updateByPrimaryKeySelective(po);
    }
    /**
     * 根据订单号获取其各笔退款金额
     * @param orderNumber
     * @return
     */
    public List<Integer> selectRefundAmount(String orderNumber) {
        List<Integer> rsVo = seVcRefundMapper.getRefundAmount(orderNumber);
        return rsVo;
    }
    /**
     * 添加退款分项
     * @param po
     * @return
     */
    public Long addRefundItem(SeVcRefundItem po) {
        seVcRefundItemMapper.insert(po);
        return po.getRefundId();
    }
    /**
     * 编辑退款分项
     * @param po
     * @return
     */
    public Integer updateRefundItem(SeVcRefundItem po) {
        return seVcRefundItemMapper.updateByPrimaryKeySelective(po);
    }
    /**
     * 根据订单号生成退款单号
     * @param orderNumber
     * @return
     */
    public String generateRefundNumber(String orderNumber) {
        String refundNumber = seVcRefundItemMapper.getLastRefundNumber(orderNumber);
        if(refundNumber == null) {
            refundNumber = orderNumber + "01";
            return refundNumber;
        }
        String a = String.format("%02d", (Integer.parseInt(refundNumber.substring(29,30).trim()) + 1));
        return  a;
    }
    /**
     * 根据订单号获取充值金额,调用退款申请接口使用
     * @param orderNumber
     * @return
     */
    public Integer getRechargeAmountByOrderNumber(String orderNumber) {
        return seVcRechargeMapper.getRechargeAmountByOrderNumber(orderNumber);
    }
    /**
     * 根据退款通知接口返回的退款单号反查退款ID,查询该退款ID下未退款记录数量
     * @param refundNumber
     * @return
     */
    public Integer getNoRefundedCount(String refundNumber) {
        return seVcRefundItemMapper.getNoRefundedCount(refundNumber);
    }
    /**
     * 根据退款单号获取退款ID,退款通知后更新退款表所需
     * @param refundNumber
     * @return
     */
    public Long getRefundIdByRefundNumber(String refundNumber) {
        return seVcRefundItemMapper.getRefundIdByRefundNumber(refundNumber);
    }
    /**
     * 获取虚拟卡充值记录
     * @param dtoVcRecharge
     * @return
     */
    public QueryResultVo<List<VoVcRecharge>> getVcRechargeRecords(DtoVcRecharge dtoVcRecharge){
        Map<String, Object> params = (Map<String, Object>) PojoUtils.generalize(dtoVcRecharge);
        Long itemTotal = seVirtualCardMapper.getRechargeRecordCount(params);
        QueryResultVo<List<VoVcRecharge>> rsVo = new QueryResultVo<>();
        rsVo.pageSize = dtoVcRecharge.pageSize;
        rsVo.pageCurr = dtoVcRecharge.pageCurr;
        rsVo.calculateAndSet(itemTotal, params);
        rsVo.obj = seVirtualCardMapper.getVcRechargeRecords(params);
        return rsVo;
    }
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/PayInfo.java
New file
@@ -0,0 +1,176 @@
package com.dy.pipIrrWechat.wechatpay;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:26
 * @LastEditTime 2024-07-15 10:26
 * @Description
 */
public class PayInfo {
    /**
     * 小程序登录API
     */
    public static String loginUrl = "https://api.weixin.qq.com/sns/jscode2session";
    /**
     * 检验登录态
     */
    public static String checkSessionUrl = "https://api.weixin.qq.com/wxa/checksession";
    /**
     * 重置登录态
     */
    public static String resetUserSessionKeyUrl = "https://api.weixin.qq.com/wxa/resetusersessionkey";
    /**
     * 获取接口调用凭据
     */
    public static String tokenUrl = "https://api.weixin.qq.com/cgi-bin/token";
    /**
     * 统一下单API
     */
    //public static String orderUrl = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    public static String orderUrl = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
    /**
     * 平台证书下载URL
     */
    public static String certificates = "https://api.mch.weixin.qq.com/v3/certificates";
    /*
     * 支付结果通知API
     */
    public static String notifyUrl = "https://44978f7456.imdo.co/sell/payment/orderNotify";
    /*
     * 查询订单API
     */
    public static String queryUrl = "https://api.mch.weixin.qq.com/pay/orderquery";
    /**
     * 申请退款API
     */
    public static String refundUrl = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
    /*
     * 退款通知API
     */
    public static String refundNotifyUrl = "https://www.muxiaobao.com/wxpay/pay.action";
    /*
     * 退款查询API
     */
    public static String refundQueryUrl = "https://api.mch.weixin.qq.com/pay/refundquery";
    /*
     * 小程序唯一标识
     */
    //public static String appid = "wxbc2b6a00dd904ead";
    public static String appid = "wxf773810cd5643196";
    /*
     * 小程序的 app secret
     */
    //public static String secret = "796ffe3e9921f756db0499e80d6ed0cd";
    public static String secret = "080d4f947095551e988cfe9338e27f15";
    /*
     * 小程序的授权类型,登录凭证校验使用
     */
    public static String grantType = "authorization_code";
    /*
     * 商户号(微信支付分配的商户号)
     */
    public static String mchid = "1640721520";
    /*
     * 商户平台设置的密钥key
     */
    public static String key = "DaYuJieShuiYanJiuYuan20230412ABC";
    /**
     * 商户API证书序列号
     */
    public static String serial_no = "52D65AA66405C738670377F467178F4C950E1606";
    /*
     * 终端IP,调用微信支付API的机器IP
     */
    public static String addrIp = "47.104.211.89";
    /*
     * 随机字符串,长度要求在32位以内
     */
    //public static String nonceStr = PayHelper.generateRandomString();
    /*
     * 时间戳 从1970年1月1日00:00:00至今的秒数,即当前的时间
     */
    //public static Long timeStamp = PayHelper.getTimeStamp();
    /*
     * 交易类型,小程序取值JSAPI
     */
    public static String tradeType = "JSAPI";
    /*
     * 签名类型
     */
    //public static String signType = "MD5";
    public static String signType = "RSA";
    /*
     * 商品描述 商品简单描述,该字段请按照规范传递
     */
    //public static String body = "大禹研究院-水费";
    public static String description = "大禹研究院-水费";
    /*
     * 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
     */
    public static String attach = "天津";
    /*
     * 签名,参与签名参数:appid、attach、mch_id、nonce_str、body、out_trade_no、total_fee、spbill_create_ip、notify_url、trade_type、openid
     */
    public String sign = "";
    /**
     * HTTP头认证类型
     */
    public static String schema = "WECHATPAY2-SHA256-RSA2048";
    /**
     * 私钥文件路径
     */
    public static String privateCertFileName = "C:\\webchat\\apiclient_key.pem";
    public static String publicCertFileName = "C:\\webchat\\wxp_cert.pem";
    /*
     * 微信订单号,优先使用
     */
    public static String transactionid = "";
    /*
     * 商户系统内部订单号
     */
    public static String out_trade_no = "";
    /*
     * 商户退款单号
     */
    public static String out_refund_no = "";
    /*
     * 退款金额
     */
    public static Float refundfee;
    /*
     * 订单金额
     */
    public static Float totalfee;
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/dto/Refund.java
New file
@@ -0,0 +1,39 @@
package com.dy.pipIrrWechat.wechatpay.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:28
 * @LastEditTime 2024-07-15 10:28
 * @Description
 */
@Data
@Schema(name = "退款请求对象")
public class Refund {
    public static final long serialVersionUID = 202403011607001L;
    /**
     * 商户订单号
     */
    @Schema(description = "商户订单号", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @NotBlank(message = "商户订单号不能为空")
    private String tradeNo;
    /**
     * 退款单号
     */
    @Schema(description = "退款单号", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private String refundNo;
    /**
     * 退款金额
     */
    @Schema(description = "退款金额", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @NotNull(message = "退款金额不能为空")
    private Integer refund;
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/dto/RefundRequest.java
New file
@@ -0,0 +1,70 @@
package com.dy.pipIrrWechat.wechatpay.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:29
 * @LastEditTime 2024-07-15 10:29
 * @Description
 */
@Data
@Schema(name = "退款请求对象")
public class RefundRequest {
    public static final long serialVersionUID = 202403011540001L;
    /**
     * 商户订单号,下单时的订单号
     */
    @Schema(description = "商户订单号", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @NotBlank(message = "商户订单号不能为空")
    private String out_trade_no;
    /**
     * 商户退款单号,订单号前加前缀“R”
     */
    @Schema(description = "商户退款单号", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @NotBlank(message = "商户退款单号不能为空")
    private String out_refund_no;
    /**
     * 退款结果回调url,refundUrl
     */
    @Schema(description = "退款结果回调url", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @NotBlank(message = "退款结果回调url不能为空")
    private String notify_url;
    /**
     * 金额信息
     */
    @Schema(description = "金额信息", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private RefundRequest.Amount amount;
    @Data
    public static class Amount {
        /**
         * 退款金额
         */
        @Schema(description = "退款金额", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
        @NotNull(message = "退款金额不能为空")
        private Integer refund;
        /**
         * 原订单金额,根据订单号查询
         */
        @Schema(description = "原订单金额", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
        @NotNull(message = "原订单金额不能为空")
        private Integer total;
        /**
         * 退款币种,固定为“CNY”
         */
        @Schema(description = "退款币种", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
        @NotBlank(message = "退款币种不能为空")
        private String currency;
    }
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/dto/RefundResponse.java
New file
@@ -0,0 +1,114 @@
package com.dy.pipIrrWechat.wechatpay.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:30
 * @LastEditTime 2024-07-15 10:30
 * @Description
 */
@Data
@Schema(name = "退款申请返回对象")
public class RefundResponse {
    public static final long serialVersionUID = 202403011431001L;
    /**
     * 微信支付退款号
     */
    private String refund_id;
    /**
     * 商户退款单号
     */
    private String out_refund_no;
    /**
     * 微信支付订单号
     */
    private String transaction_id;
    /**
     * 商户订单号
     */
    private String out_trade_no;
    /**
     * 退款渠道
     */
    private String channel;
    /**
     * 退款入账账户
     */
    private String user_received_account;
    /**
     * 退款成功时间
     */
    private String success_time;
    /**
     * 退款创建时间
     */
    private String create_time;
    /**
     * 退款状态
     */
    private String status;
    /**
     * 金额信息
     */
    private RefundResponse.Amount amount;
    @Data
    private static class Amount {
        /**
         * 订单总金额
         */
        private Integer total;
        /**
         * 退款金额
         */
        private Integer refund;
        /**
         * 用户支付金额
         */
        private Integer payer_total;
        /**
         * 用户退款金额
         */
        private Integer payer_refund;
        /**
         * 应结退款金额
         */
        private Integer settlement_refund;
        /**
         * 应结订单金额
         */
        private Integer settlement_total;
        /**
         * 优惠退款金额
         */
        private Integer discount_refund;
        /**
         * 退款币种
         */
        private String currency;
    }
}
pipIrr-platform/pipIrr-web/pipIrr-web-wechat/src/main/java/com/dy/pipIrrWechat/wechatpay/dto/ToRefund.java
New file
@@ -0,0 +1,35 @@
package com.dy.pipIrrWechat.wechatpay.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
/**
 * @author ZhuBaoMin
 * @date 2024-07-15 10:32
 * @LastEditTime 2024-07-15 10:32
 * @Description
 */
@Data
@Schema(name = "待退款对象")
public class ToRefund {
    public static final long serialVersionUID = 202403072144001L;
    /**
     * 订单号
     */
    @Schema(description = "订单号", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @NotBlank(message = "订单号不能为空")
    private String orderNumber;
    /**
     * 退款金额
     */
    @Schema(description = "退款金额", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @NotNull(message = "退款金额不能为空")
    @Positive(message = "退款金额必须为大于0的整数")
    private Integer refundAmount;
}