1
zj
2026-03-02 de8bbf1c24fd449137a522ec4c7e8f04e3dcd748
1
6 files modified
5 files added
438 ■■■■■ changed files
src/main/java/com/nq/common/interceptor/ApiUserAuthorityInterceptor.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/controller/protol/UserController.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/service/impl/PayServiceImpl.java 154 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/service/impl/SiteInfoServiceImpl.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/service/impl/UserWithdrawServiceImpl.java 128 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/utils/AesEncryptUtil.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/vo/pay/PayOutMD5Util.java 31 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/vo/pay/PayoutCallbackData.java 21 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/vo/pay/PayoutCallbackRequest.java 19 ●●●●● patch | view | raw | blame | history
src/main/java/com/nq/vo/pay/PayoutResponseVO.java 28 ●●●●● patch | view | raw | blame | history
src/main/resources/application.properties 2 ●●● patch | view | raw | blame | history
src/main/java/com/nq/common/interceptor/ApiUserAuthorityInterceptor.java
@@ -87,6 +87,9 @@
        if ("/user/payoutCallback.do".equals(url)) {//代付回调
            return true;
        }
        if ("/user/payoutCallbackTwo.do".equals(url)) {//代付回调
            return true;
        }
        User currentUser = getCurrentUser(httpServletRequest);
        GoogleTranslateUtil googleTranslateUtil = new GoogleTranslateUtil();
        String lang = httpServletRequest.getHeader("lang");
src/main/java/com/nq/controller/protol/UserController.java
@@ -21,6 +21,7 @@
import javax.servlet.http.HttpSession;
import com.nq.utils.translate.GoogleTranslateUtil;
import com.nq.vo.pay.PayoutCallbackRequest;
import com.nq.vo.stock.UserStockSubscribeAddIn;
import org.apache.ibatis.annotations.Property;
import org.slf4j.Logger;
@@ -516,4 +517,13 @@
    public void payoutCallback(@RequestBody PayoutCallbackVo callbackVo, HttpServletResponse response) throws IOException {
        payService.payoutCallback(callbackVo, response);
    }
    /**
     * 代付回调接口
     */
    @PostMapping({"payoutCallbackTwo.do"})
    public Map<String, Object> payoutCallbackTwo(@RequestBody PayoutCallbackRequest callbackVo, HttpServletResponse response) throws IOException {
        return payService.payoutCallbackTwo(callbackVo, response);
    }
}
src/main/java/com/nq/service/impl/PayServiceImpl.java
@@ -41,6 +41,9 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.nq.vo.pay.PayOutMD5Util;
import com.nq.vo.pay.PayoutCallbackData;
import com.nq.vo.pay.PayoutCallbackRequest;
import net.sf.json.JSON;
import net.sf.json.JSONObject;
import okhttp3.*;
@@ -121,7 +124,8 @@
        }else if(type == 2){//支付2
            return getPaymentZero(tradeAmount, uipReqRul, user,request);
        } else if(type == 3){//支付2
            return getPaymentTwo(tradeAmount, uipReqRul, user,request);
//            return getPaymentTwo(tradeAmount, uipReqRul, user,request);
            return ServerResponse.createByErrorMsg("未开启,请选择其他支付方式",request);
        }else if(type == 1){
//            return getPaymentThree(tradeAmount, threeUrl, user,request);
            return getPaymentOne(tradeAmount, reqUrl,user,request);
@@ -131,7 +135,7 @@
    private ServerResponse getPaymentZero(String tradeAmount, String uipReqRul, User user, HttpServletRequest request) throws Exception {
        //int
        String url = "https://gateway.kings-pays.com/gateway/payout/init";//正式地址
        String url = "https://gateway.kings-pays.com/gateway/payment/init";//正式地址
        String merchantKey = "qqaC1DH/LeR9iPvm";//商户key 需替换
        String aesKey = "ge6vK40fHNZPFJ4p";//商户aesKey 需替换
        String aesIv = "6gJoHTEE1i2O3ovE";//商户aesIv 需替换
@@ -154,6 +158,7 @@
        Headers headers = new Headers.Builder().add("merchant_key", merchantKey).build();//merchant_key需替换
        //请求
        String resp = doPost(url, requestObj.toString(), headers);
        log.info("代收返回:"+resp);
        Gson gson = new Gson();
        PaymentResponseZero paymentResponse = gson.fromJson(resp, PaymentResponseZero.class);
        if(paymentResponse.getCode() != 0 && !paymentResponse.getMsg().equals("success")){
@@ -1423,7 +1428,7 @@
                transferResponseService.updateById(transferResponse);
                
                userWithdraw.setWithStatus(2); // 失败
                userWithdraw.setWithMsg("代付失败:" + vo.getMsg());
                userWithdraw.setWithMsg("Withdrawal failed:" + vo.getMsg());
                userWithdraw.setTransTime(new Date());
                userWithdrawMapper.updateByPrimaryKeySelective(userWithdraw);
                
@@ -1482,4 +1487,147 @@
            pw.close();
        }
    }
    public Map<String, Object> payoutCallbackTwo(PayoutCallbackRequest request, HttpServletResponse response) {
        String merchantKey = "qqaC1DH/LeR9iPvm";//商户key 需替换
        String aesKey = "ge6vK40fHNZPFJ4p";//商户aesKey 需替换
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);   // 固定返回 200
        try {
            PayoutCallbackData data = request.getData();
            String signature = request.getSignature_n();
            // 1. 签名验证
            String signStr = merchantKey
                    + nullToEmpty(data.getMessage())
                    + nullToEmpty(data.getAmount())
                    + nullToEmpty(data.getStatus())
                    + nullToEmpty(data.getMerchantOrderNo())
                    + nullToEmpty(data.getOrderNo())
                    + aesKey;
            String calculatedSign = PayOutMD5Util.md5(signStr).toLowerCase();
            if (!calculatedSign.equals(signature)) {
                log.error("代付回调签名验证失败,merchantOrderNo={},本地签名={},回调签名={}",
                        data.getMerchantOrderNo(), calculatedSign, signature);
                // 签名失败不处理业务,但依旧返回 200
                return result;
            }
            // 2. 查询本地代付记录
            TransferResponse transfer = transferResponseService.getOne(
                    new LambdaQueryWrapper<TransferResponse>()
                            .eq(TransferResponse::getMerTransferId, data.getMerchantOrderNo())
                            .last("limit 1")
            );
            if (transfer == null) {
                log.error("代付回调未找到对应记录,merchantOrderNo={}", data.getMerchantOrderNo());
                return result;
            }
            // 3. 防止重复回调
            if (transfer.getCallbackState() != 0) {
                log.info("代付回调已处理过,merchantOrderNo={},状态={}", data.getMerchantOrderNo(), transfer.getCallbackState());
                return result;
            }
            // 4. 查询提现记录
            UserWithdraw withdraw = userWithdrawMapper.selectByPrimaryKey(transfer.getWithId());
            if (withdraw == null) {
                log.error("代付回调未找到提现记录,withId={}", transfer.getWithId());
                return result;
            }
            // 5. 根据状态处理
            String status = data.getStatus();
            if ("SUCCESS".equals(status)) {
                handleSuccess(transfer, withdraw, data, signature);
            } else if ("FAILURE".equals(status)) {
                handleFailure(transfer, withdraw, data, signature);
            } else {
                log.info("代付回调未知状态:{},暂不处理,merchantOrderNo={}", status, data.getMerchantOrderNo());
            }
        } catch (Exception e) {
            log.error("代付回调处理异常", e);
        }
        return result;
    }
    /**
     * 处理成功回调
     */
    private void handleSuccess(TransferResponse transfer, UserWithdraw withdraw,
                               PayoutCallbackData data, String signature) {
        // 更新代付记录
        transfer.setTradeResult(1);          // 成功
        transfer.setCallbackState(1);        // 已处理成功
        transfer.setRespCode("SUCCESS");
        transfer.setSignType("MD5");
        transfer.setSign(signature);
        transfer.setUpdatedAt(new Date());
        transferResponseService.updateById(transfer);
        // 更新提现记录
        withdraw.setWithStatus(1);            // 成功
        withdraw.setWithMsg("代付成功");
        withdraw.setTransTime(new Date());
        userWithdrawMapper.updateByPrimaryKeySelective(withdraw);
        // 扣除冻结资金(提现时已冻结,此处只需将冻结减少,可用余额不变)
        UserAssets assets = iUserAssetsServices.assetsByTypeAndUserId("IN", withdraw.getUserId());
        if (assets != null) {
            assets.setFreezeMoney(assets.getFreezeMoney().subtract(withdraw.getWithAmt()));
            userAssetsMapper.updateById(assets);
        }
        log.info("代付成功处理完成,商户订单号={},平台订单号={}", data.getMerchantOrderNo(), data.getOrderNo());
    }
    /**
     * 处理失败回调
     */
    private void handleFailure(TransferResponse transfer, UserWithdraw withdraw,
                               PayoutCallbackData data, String signature) {
        // 更新代付记录
        transfer.setTradeResult(2);          // 失败
        transfer.setCallbackState(2);        // 已处理失败
        transfer.setRespCode("FAIL");
        transfer.setErrorMsg(data.getMessage());
        transfer.setSignType("MD5");
        transfer.setSign(signature);
        transfer.setUpdatedAt(new Date());
        transferResponseService.updateById(transfer);
        // 更新提现记录
        withdraw.setWithStatus(2);            // 失败
        withdraw.setWithMsg("Withdrawal failed:" + data.getMessage());
        withdraw.setTransTime(new Date());
        userWithdrawMapper.updateByPrimaryKeySelective(withdraw);
        // 返还资金:解冻并增加可用余额
        UserAssets assets = iUserAssetsServices.assetsByTypeAndUserId("IN", withdraw.getUserId());
        if (assets != null) {
            assets.setAvailableBalance(assets.getAvailableBalance().add(withdraw.getWithAmt()));
            assets.setFreezeMoney(assets.getFreezeMoney().subtract(withdraw.getWithAmt()));
            userAssetsMapper.updateById(assets);
        }
        log.info("代付失败处理完成,商户订单号={},平台订单号={},失败原因={}",
                data.getMerchantOrderNo(), data.getOrderNo(), data.getMessage());
    }
    /**
     * 将 null 转为空字符串,防止拼接 NPE
     */
    private String nullToEmpty(String str) {
        return str == null ? "" : str;
    }
}
src/main/java/com/nq/service/impl/SiteInfoServiceImpl.java
@@ -125,13 +125,15 @@
                        } else if (agentUser.getId().equals(56)) {
                            siteInfo.setOnlineService("https://t.me/Greenback8");
                        } else if (agentUser.getId().equals(57)) {
                            siteInfo.setOnlineService(""); // 代理账户138073(ID:57)客服号链接为空
                            siteInfo.setOnlineService("https://t.me/GreenbackCapital"); // 代理账户138073(ID:57)客服号链接为空
                        } else if (agentUser.getId().equals(58)) {
                            siteInfo.setOnlineService("https://t.me/Greenback9");
                        } else if (agentUser.getId().equals(59)) {
                            siteInfo.setOnlineService("https://t.me/GC855858");
                        } else if (agentUser.getId().equals(61)) {
                            siteInfo.setOnlineService("https://t.me/Duro_Capital2");
                            siteInfo.setOnlineService("https://t.me/Greenback2");
                        } else if (agentUser.getId().equals(62)) {
                            siteInfo.setOnlineService("https://t.me/Greenback_Capital79");
                        }
                    }
src/main/java/com/nq/service/impl/UserWithdrawServiceImpl.java
@@ -1,6 +1,7 @@
package com.nq.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -33,6 +34,7 @@
import javax.servlet.http.HttpServletResponse;
import com.nq.utils.timeutil.TimeUtil;
import okhttp3.*;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@@ -395,7 +397,10 @@
                    return ServerResponse.createBySuccessMsg("操作成功!");
                }
            }else if(state == 1){//走代付
                return getObjectServerResponseOne(withId, request, response, userWithdraw, user, userAssets);
                return getObjectServerResponseTwo(withId, request, response, userWithdraw, user, userAssets);
            }else if(state == 2){//走代付
                return ServerResponse.createByErrorMsg("代付通道关闭");
//                return getObjectServerResponseOne(withId, request, response, userWithdraw, user, userAssets);
            }else{
                return ServerResponse.createByErrorMsg("请选择对应的操作!");
            }
@@ -406,6 +411,127 @@
        return ServerResponse.createBySuccessMsg("操作失败!");
    }
    //代付二
    private ServerResponse getObjectServerResponseTwo(Integer withId, HttpServletRequest request, HttpServletResponse response, UserWithdraw userWithdraw, User user, UserAssets userAssets) throws Exception {
        String payoutUrl = "https://gateway.kings-pays.com/gateway/payout/init";//正式地址
        String merchantKey = "qqaC1DH/LeR9iPvm";//商户key 需替换
        String aesKey = "ge6vK40fHNZPFJ4p";//商户aesKey 需替换
        String aesIv = "6gJoHTEE1i2O3ovE";//商户aesIv 需替换
        // 1. 生成商户订单号
        String merchantOrderNo = generatePayoutOrderId(withId);
        // 2. 构建加密前的业务参数
        JSONObject dataObj = new JSONObject();
        dataObj.put("amount", userWithdraw.getWithAmt().intValue()); // 注意金额单位(示例中为整数)
        dataObj.put("transferType", "BANK_TRANSFER");
        dataObj.put("beneficiaryName", user.getNickName());
        dataObj.put("beneficiaryEmail", "null@gmail.com");
        dataObj.put("beneficiaryPhoneNo", user.getPhone());
        dataObj.put("beneficiaryAccount", userWithdraw.getBankNo());
        dataObj.put("beneficiaryIFSC", userWithdraw.getBankAddress());
        dataObj.put("merchantOrderNo", merchantOrderNo);
        dataObj.put("notifyUrl", "https://api.greenbackcaps.top/user/payoutCallbackTwo.do");
        // 3. AES 加密
        String encryptedData = AesEncryptUtil.encrypt(dataObj.toJSONString(), aesKey, aesIv);
        JSONObject requestObj = new JSONObject();
        requestObj.put("data", encryptedData);
        // 4. 设置请求头
        Headers headers = new Headers.Builder()
                .add("merchant_key", merchantKey)
                .build();
        // 5. 发送 HTTP 请求
        log.info("代付请求参数:{}", requestObj.toJSONString());
        String respStr = doPost(payoutUrl, requestObj.toJSONString(), headers);
        log.info("代付响应原始数据:{}", respStr);
        // 6. 解析响应(使用 Jackson 或 fastjson,这里以 fastjson 为例)
        JSONObject respJson = JSONObject.parseObject(respStr);
        int code = respJson.getIntValue("code");
        boolean success = respJson.getBooleanValue("success");
        String msg = respJson.getString("msg");
        // 6.1 接口调用失败(code != 0 或 success = false)
        if (code != 0 || !success) {
            handleFailure(userAssets, userWithdraw, "代付请求失败:" + msg);
            return ServerResponse.createByErrorMsg("代付请求失败:" + msg, request);
        }
        // 6.2 获取 data 部分
        JSONObject data = respJson.getJSONObject("data");
        String status = data.getString("status");       // ACCEPT / FAILURE / PROCESSING
        String failMsg = data.getString("message");     // 失败时的具体原因
        // 7. 根据 status 判断业务是否成功
        if ("ACCEPT".equals(status)) {
            // 代付订单被接受(不一定最终成功,需等待回调)
            String platformOrderNo = data.getString("orderNo");
            // 保存代付记录
            saveTransferRecord(merchantOrderNo, platformOrderNo, userWithdraw.getWithAmt(), user.getId(), withId);
            // 更新提现记录为“已提交”
            userWithdraw.setWithStatus(4); // 4:已提交
            userWithdraw.setTransTime(new Date());
            userWithdrawMapper.updateByPrimaryKeySelective(userWithdraw);
            log.info("代付下单成功,商户订单号:{}", merchantOrderNo);
            return ServerResponse.createBySuccessMsg("代付申请已提交,请等待处理");
        } else {
            // 业务失败(如 FAILURE)
            String errorMsg = (failMsg != null && !failMsg.isEmpty()) ? failMsg : msg;
            handleFailure(userAssets, userWithdraw, "Withdrawal failed:" + errorMsg);
            return ServerResponse.createByErrorMsg("Withdrawal failed:" + errorMsg, request);
        }
    }
    public static String doPost(String url, String data, Headers headers) throws IOException {
        OkHttpClient customClient = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .headers(headers)
                .post(RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), data))
                .build();
        Response response = customClient.newCall(request).execute();
        String resp = response.body().string();
        return resp;
    }
    private void handleFailure(UserAssets userAssets, UserWithdraw userWithdraw, String errorMsg) {
        userAssets.setAvailableBalance(userAssets.getAvailableBalance().add(userWithdraw.getWithAmt()));
        userAssets.setFreezeMoney(userAssets.getFreezeMoney().subtract(userWithdraw.getWithAmt()));
        userAssetsMapper.updateById(userAssets);
        userWithdraw.setWithStatus(2); // 2:失败
        userWithdraw.setWithMsg(errorMsg);
        userWithdraw.setTransTime(new Date());
        userWithdrawMapper.updateByPrimaryKeySelective(userWithdraw);
    }
    private void saveTransferRecord(String merchantOrderNo, String platformOrderNo, BigDecimal amount,
                                    Integer userId, Integer withId) {
        TransferResponse record = new TransferResponse();
        record.setMerTransferId(merchantOrderNo);
        record.setTradeNo(platformOrderNo);
        record.setTransferAmount(amount);
        record.setTradeResult(0);       // 0:已下单
        record.setCallbackState(0);     // 0:未处理
        record.setRespCode("SUCCESS");
        record.setSignType("AES");
        record.setApplyDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        record.setUserId(userId);
        record.setWithId(withId);
        record.setCreatedAt(new Date());
        record.setUpdatedAt(new Date());
        transferResponseService.save(record);
    }
    public int deleteByUserId(Integer userId) {
src/main/java/com/nq/utils/AesEncryptUtil.java
New file
@@ -0,0 +1,36 @@
package com.nq.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
 * @program: dabaogp
 * @description:
 * @create: 2026-03-02 15:11
 **/
public class AesEncryptUtil {
    public static String encrypt(String data, String key, String iv) throws Exception {
        // 直接使用字符串的 UTF-8 字节作为密钥和 IV(需确保长度为16)
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
        byte[] ivBytes = iv.getBytes(StandardCharsets.UTF_8);
        // 验证长度(可选)
        if (keyBytes.length != 16 || ivBytes.length != 16) {
            throw new IllegalArgumentException("Key and IV must be 16 bytes long (16 ASCII characters)");
        }
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encrypted);
    }
}
src/main/java/com/nq/vo/pay/PayOutMD5Util.java
New file
@@ -0,0 +1,31 @@
package com.nq.vo.pay;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
 * @program: dabaogp
 * @description:
 * @create: 2026-03-02 15:18
 **/
public class PayOutMD5Util {
    public static String md5(String source) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(source.getBytes("UTF-8"));
            return bytesToHex(bytes);
        } catch (NoSuchAlgorithmException | java.io.UnsupportedEncodingException e) {
            throw new RuntimeException("MD5加密失败", e);
        }
    }
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
src/main/java/com/nq/vo/pay/PayoutCallbackData.java
New file
@@ -0,0 +1,21 @@
package com.nq.vo.pay;
import lombok.Data;
/**
 * @program: dabaogp
 * @description:
 * @create: 2026-03-02 15:28
 **/
@Data
public class PayoutCallbackData {
    private String amount;           // 支出金额(字符串,如 "1500.00")
    private String orderNo;           // 平台订单号
    private String message;           // 失败原因或成功信息
    private String type;              // 类型,固定为 "payout"
    private String merchantOrderNo;   // 商户订单号
    private String processedTime;     // 成功时间(仅成功时返回)
    private String utr;               // UTR 码
    private String processAmount;     // 手续费
    private String status;            // 订单状态:SUCCESS / FAILURE
}
src/main/java/com/nq/vo/pay/PayoutCallbackRequest.java
New file
@@ -0,0 +1,19 @@
package com.nq.vo.pay;
/**
 * @program: dabaogp
 * @description:
 * @create: 2026-03-02 15:17
 **/
import lombok.Data;
/**
 * 代付回调请求体
 */
@Data
public class PayoutCallbackRequest {
    private PayoutCallbackData data;
    private String signature_n;      // 签名
}
src/main/java/com/nq/vo/pay/PayoutResponseVO.java
New file
@@ -0,0 +1,28 @@
package com.nq.vo.pay;
import lombok.Data;
import java.math.BigDecimal;
/**
 * @program: dabaogp
 * @description:
 * @create: 2026-03-02 15:02
 **/
@Data
public class PayoutResponseVO {
    private int code;          // 0 表示接口调用成功
    private String msg;        // 提示信息
    private PayoutData data;   // 业务数据
    private boolean success;   // 是否成功(与 code 对应)
    @Data
    public static class PayoutData {
        private String orderNo;          // 平台订单号
        private String merchantOrderNo;  // 商户订单号
        private String status;           // 订单状态(ACCEPT/FAILURE/PROCESSING 等)
        private BigDecimal amount;       // 金额
        private String message;          // 失败原因(当 status=FAILURE 时存在)
    }
}
src/main/resources/application.properties
@@ -50,7 +50,7 @@
JS_IN_HTTP_API = http://api-in-3-socket.js-stock.top
JS_IN_HTTP_URL = http://api-in-pro.js-stock.top/
JS_IN_WS_URL = ws://api-in-pro-ws.js-stock.top
JS_IN_KEY = 5CXdBCcKHgWHZifECepM
JS_IN_KEY = xKChgi47AP1NMwMeYI3c
US_HTTP_API = http://api-us.js-stock.top/
US_WS_URL = ws://ws-us.js-stock.top