KKSU
2025-02-05 2caf98df09bd38006ca5e590fa2418588bf0a351
feat(pay): 添加 FIUU支付功能

- 新增 FIUUController 处理支付请求和回调
- 添加 FIUUInitPayRequest 模型类
- 实现支付参数生成和回调验证逻辑
- 更新 WebMvcConfigure 添加支付回调路径
3 files added
1 files modified
416 ■■■■■ changed files
src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java 1 ●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/common/utils/ValidateEntityUtils.java 280 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/pay/controller/FIUUController.java 106 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/pay/model/FIUUInitPayRequest.java 29 ●●●●● patch | view | raw | blame | history
src/main/java/cc/mrbird/febs/common/configure/WebMvcConfigure.java
@@ -33,6 +33,7 @@
        registration.excludePathPatterns("/api/xcxPay/wxpayCallback");
        registration.excludePathPatterns("/api/xcxPay/rechargeCallBack");
        registration.excludePathPatterns("/api/xcxPay/fapiaoCallBack");
        registration.excludePathPatterns("/api/fuPay/callback");
        // 添加Swagger UI相关路径
        registration.excludePathPatterns("/api/swagger-ui.html");
src/main/java/cc/mrbird/febs/common/utils/ValidateEntityUtils.java
New file
@@ -0,0 +1,280 @@
package cc.mrbird.febs.common.utils;
import cc.mrbird.febs.common.exception.FebsException;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
 * 实体验证工具类
 */
public class ValidateEntityUtils {
    /**
     * 确保指定列的有效性,并返回相应的实体对象
     *      selectOne方法
     *      此方法用于验证数据库中某列的值是否存在,并返回包含该值的实体对象
     *      如果指定的值不存在或为null,将抛出异常
     *
     * @param valueToCheck 需要验证的值
     * @param columnExtractor 列值提取器,用于指定需要验证的列
     * @param queryWrapperExtractor 查询包装器提取器,用于执行数据库查询
     * @param errMsg 错误消息格式字符串
     * @param columnExtractors 可变参数(varargs)查询字段,可选,如果没有就是查询全部
     * @param <T> 实体类类型
     * @param <R> 列值类型
     * @param <V> 需要验证的值的类型
     * @return 返回包含指定列值的实体对象
     * @throws IllegalArgumentException 如果需要验证的值为null
     * @throws FebsException 如果查询结果为空或列值为null,或查询过程中发生异常
     */
    public static <T, R, V> T ensureColumnReturnEntity(
            V valueToCheck,
            SFunction<T, R> columnExtractor,
            SFunction<LambdaQueryWrapper<T>, T> queryWrapperExtractor,
            String errMsg,
            SFunction<T, R>... columnExtractors) {
        // 检查输入参数是否为null
        if (valueToCheck == null) {
            throw new IllegalArgumentException("The value to check cannot be null while ensureColumnReturnEntity column");
        }
        if (columnExtractor == null || queryWrapperExtractor == null) {
            throw new IllegalArgumentException("Column extractor and query wrapper extractor cannot be null while ensureColumnReturnEntity column");
        }
        T entity = null;
        try {
            // 创建LambdaQueryWrapper并配置查询条件
            LambdaQueryWrapper<T> wrapper = Wrappers.lambdaQuery();
            if (columnExtractors != null && columnExtractors.length > 0) {
                wrapper.select(columnExtractors);
            }
            wrapper.eq(columnExtractor, valueToCheck)
                    .last("limit 1");
            // 执行查询并获取结果实体
            entity = queryWrapperExtractor.apply(wrapper);
            // 如果查询结果为空,则抛出异常
            if (entity == null) {
                throw new FebsException(StrUtil.format(errMsg, valueToCheck));
            }
            // 提取查询结果中的列值
            R columnValue = columnExtractor.apply(entity);
            // 如果列值为null,则抛出异常
            if (columnValue == null) {
                throw new FebsException(StrUtil.format(errMsg, valueToCheck));
            }
        } catch (FebsException e) {
            // 记录异常日志
            throw e;
        } catch (Exception e) {
            // 记录异常日志
            throw new FebsException(StrUtil.format("An error occurred while ensuring column return entity: {}", valueToCheck));
        }
        // 返回查询到的实体类对象
        return entity;
    }
    /**
     * 确保指定列的有效性,并返回相应的实体对象列表
     *      selectList方法
     *      此方法用于验证数据库中某列的值是否存在,并返回包含该值的实体对象列表
     *      如果指定的值不存在或为null,将抛出异常
     *
     * @param valueToCheck 需要验证的值
     * @param columnExtractor 列值提取器,用于指定需要验证的列
     * @param queryWrapperExtractor 查询包装器提取器,用于执行数据库查询
     * @param errMsg 错误消息格式字符串
     * @param columnExtractors 可变参数(varargs)查询字段,可选,如果没有就是查询全部
     * @param <T> 实体类类型
     * @param <R> 列值类型
     * @param <V> 需要验证的值的类型
     * @return 返回包含指定列值的实体对象列表
     * @throws IllegalArgumentException 如果需要验证的值为null
     * @throws FebsException 如果查询结果为空或列值为null,或查询过程中发生异常
     */
    public static <T, R, V> List<T> ensureColumnReturnEntityList(
            V valueToCheck,
            SFunction<T, R> columnExtractor,
            SFunction<LambdaQueryWrapper<T>, List<T>> queryWrapperExtractor,
            String errMsg,
            SFunction<T, R>... columnExtractors) {
        // 检查需要验证的值是否为null
        if (valueToCheck == null) {
            throw new IllegalArgumentException("The value to check cannot be null while ensureColumnReturnEntityList column");
        }
        List<T> entities = new ArrayList<>();
        try {
            // 创建LambdaQueryWrapper并配置查询条件
            LambdaQueryWrapper<T> wrapper = Wrappers.lambdaQuery();
            if (columnExtractors != null && columnExtractors.length > 0) {
                wrapper.select(columnExtractors);
            }
            wrapper.eq(columnExtractor, valueToCheck);
            // 执行查询并获取结果实体列表
            entities = queryWrapperExtractor.apply(wrapper);
        } catch (Exception e) {
            // 记录异常日志
            throw new FebsException(StrUtil.format("An error occurred while ensureColumnReturnEntityList column: {}", valueToCheck));
        }
        // 如果查询结果为空,则抛出异常
        if (entities == null || entities.isEmpty()) {
            throw new FebsException(StrUtil.format(errMsg, valueToCheck));
        }
        // 返回查询到的实体类对象列表
        return entities;
    }
    /**
     * 确保指定列的值在数据库中是存在的
     *      selectOne方法
     *      该方法通过查询数据库来验证给定的列值是否存在如果不存在,则抛出异常
     *
     * @param valueToCheck 需要验证的列值
     * @param columnExtractor 用于提取实体类中列值的函数式接口
     * @param queryWrapperExtractor 用于构建查询条件并返回实体类的函数式接口
     * @param errMsg 当列值无效时抛出的异常消息格式字符串
     * @throws IllegalArgumentException 如果需要验证的值为null
     * @throws FebsException 如果在数据库中找不到指定的列值或列值为null,或者在查询过程中发生异常
     */
    public static <T, R, V> void ensureColumnValid(
            V valueToCheck,
            SFunction<T, R> columnExtractor,
            SFunction<LambdaQueryWrapper<T>, T> queryWrapperExtractor,
            String errMsg) {
        // 检查需要验证的值是否为null
        if (valueToCheck == null) {
            throw new IllegalArgumentException("The value to check cannot be null while ensureColumnValid column");
        }
        try {
            // 创建LambdaQueryWrapper并配置查询条件
            LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
            wrapper.select(columnExtractor)
                    .eq(columnExtractor, valueToCheck)
                    .last("limit 1");
            // 执行查询并获取结果实体
            T entity = queryWrapperExtractor.apply(wrapper);
            // 如果查询结果为空,则抛出异常
            if (entity == null) {
                throw new FebsException(StrUtil.format(errMsg, valueToCheck));
            }
            // 提取查询结果中的列值
            R columnValue = columnExtractor.apply(entity);
            // 如果列值为null,则抛出异常
            if (columnValue == null) {
                throw new FebsException(StrUtil.format(errMsg, valueToCheck));
            }
        } catch (Exception e) {
            // 记录异常日志
            throw new FebsException(e.getMessage());
        }
    }
    /**
     * 确保指定值在数据库中是唯一的
     *      selectCount方法
     *      该方法通过查询数据库来验证给定的列值是否已经存在,如果存在,则抛出异常,以确保数据的唯一性
     *
     * @param valueToCheck 需要检查的值
     * @param columnExtractor 用于提取实体类字段的函数式接口
     * @param countWrapperExtractor 用于获取查询条件包装器中记录数的函数式接口
     * @param errMsg 错误消息模板,当值不唯一时使用
     * @param <T> 实体类类型
     * @param <R> 字段类型
     * @param <V> 需要检查的值的类型
     * @throws IllegalArgumentException 如果需要检查的值为null,则抛出此异常
     * @throws FebsException 如果值已存在或在检查过程中发生错误,则抛出此异常
     */
    public static <T, R, V> void ensureUnique(
            V valueToCheck,
            SFunction<T, R> columnExtractor,
            SFunction<LambdaQueryWrapper<T>, Integer> countWrapperExtractor,
            String errMsg) {
        // 检查输入值是否为null,如果为null,则抛出IllegalArgumentException异常
        if (valueToCheck == null) {
            throw new IllegalArgumentException("The value to check cannot be null while ensureUnique column");
        }
        if (columnExtractor == null) {
            throw new IllegalArgumentException("The columnExtractor cannot be null while ensureUnique column");
        }
        if (countWrapperExtractor == null) {
            throw new IllegalArgumentException("The countWrapperExtractor cannot be null while ensureUnique column");
        }
        int count = 0;
        try {
            // 创建LambdaQueryWrapper对象,用于构建查询条件
            LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
            // 添加等于条件,检查的字段=需要检查的值
            wrapper.eq(columnExtractor, valueToCheck);
            // 执行查询并获取结果数量
            count = countWrapperExtractor.apply(wrapper);
        } catch (Exception e) {
            // 记录异常日志
            throw new FebsException(StrUtil.format("An error occurred while ensureUnique column: {}", valueToCheck));
        }
        // 如果结果数量大于0,说明值已存在,抛出FebsException异常
        if (count > 0) {
            throw new FebsException(StrUtil.format(errMsg, valueToCheck));
        }
    }
    /**
     * 确保两个参数相等,如果不相等则抛出异常
     *
     * @param value1 第一个参数
     * @param value2 第二个参数
     * @param errMsg 当两个参数不相等时抛出的异常消息格式字符串
     * @throws FebsException 如果两个参数不相等
     */
    public static <T> void ensureEqual(
            T value1,
            T value2,
            String errMsg) {
        // 使用 Objects.equals 处理 null 值,避免显式 null 检查
        if (!Objects.equals(value1, value2)) {
            // 延迟字符串格式化,只在需要抛出异常时进行
            throw new FebsException(StrUtil.format(errMsg, value1, value2));
        }
    }
    /**
     * 确保两个参数不相等,如果相等则抛出异常
     *
     * @param value1 第一个参数
     * @param value2 第二个参数
     * @param errMsg 当两个参数相等时抛出的异常消息格式字符串
     * @throws FebsException 如果两个参数相等
     */
    public static <T> void ensureNotEqual(
            T value1,
            T value2,
            String errMsg) {
        // 使用 Objects.equals 处理 null 值,避免显式 null 检查
        if (Objects.equals(value1, value2)) {
            // 延迟字符串格式化,只在需要抛出异常时进行
            throw new FebsException(StrUtil.format(errMsg, value1, value2));
        }
    }
}
src/main/java/cc/mrbird/febs/pay/controller/FIUUController.java
New file
@@ -0,0 +1,106 @@
package cc.mrbird.febs.pay.controller;
import cc.mrbird.febs.common.entity.FebsResponse;
import cc.mrbird.febs.common.enumerates.OrderDeliveryStateEnum;
import cc.mrbird.febs.common.enumerates.OrderStatusEnum;
import cc.mrbird.febs.common.exception.FebsException;
import cc.mrbird.febs.common.utils.ValidateEntityUtils;
import cc.mrbird.febs.mall.entity.MallOrderInfo;
import cc.mrbird.febs.mall.mapper.MallOrderInfoMapper;
import cc.mrbird.febs.pay.model.FIUUInitPayRequest;
import cn.hutool.core.date.DateUtil;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping(value = "/api/fuPay")
public class FIUUController {
    @Resource
    private MallOrderInfoMapper mallOrderInfoMapper;
    @ApiOperation(value = "初始化FIUU支付信息", notes = "初始化FIUU支付信息")
    @PostMapping("/initPayment")
    public FebsResponse initPayment(@RequestBody FIUUInitPayRequest orderRequest) {
        Long orderId = orderRequest.getOrderId();
        MallOrderInfo mallOrderInfo = ValidateEntityUtils.ensureColumnReturnEntity(orderId, MallOrderInfo::getId, mallOrderInfoMapper::selectOne, "订单不存在");
        ValidateEntityUtils.ensureEqual(mallOrderInfo.getPayResult(), "1", "订单已支付");
        String amount = mallOrderInfo.getAmount().toString();
        try {
            String merchantId = "e2umart01";
            String verifyKey = "4e3a4ed58e62ddbfacf41f6d5ec56bf2";
            String returnUrl = "https://www.mye2u.com/api/fuPay/callback"; // 支付结果回调地址
            // 生成 vcode(MD5(amount + merchantId + orderId + verifyKey))
            String vcode = DigestUtils.md5Hex(
                    amount +
                            merchantId +
                            orderRequest.getOrderId() +
                            verifyKey
            );
            // 返回支付参数
            Map<String, String> params = new HashMap<>();
            params.put("merchant_id", merchantId);
            params.put("orderid", String.valueOf(orderId));
            params.put("amount", amount);
            params.put("bill_name", orderRequest.getBuyerName());
            params.put("bill_email", orderRequest.getBuyerEmail());
            params.put("bill_mobile", orderRequest.getBuyerMobile());
            params.put("bill_desc", mallOrderInfo.getOrderNo());
            params.put("currency", "MYR"); // 默认 MYR
            params.put("vcode", vcode);
            params.put("returnurl", returnUrl);
            return new FebsResponse().success().data(params);
        } catch (Exception e) {
            return new FebsResponse().fail().message("支付参数校验失败");
        }
    }
    // Java 回调接口
    @PostMapping("/callback")
    public FebsResponse handlePaymentCallback(@RequestParam Map<String, String> params) {
        try {
            String secretKey = "59c709fc18978a6a83b87f05d37cecbf";
            String tranID = params.get("tranID");
            String orderId = params.get("orderid");
            String status = params.get("status");
            String domain = params.get("domain");
            String amount = params.get("amount");
            String currency = params.get("currency");
            String paydate = params.get("paydate");
            String skey = params.get("skey");
            // 计算 skey 验证
            String preSkey = DigestUtils.md5Hex(tranID + orderId + status + domain + amount + currency);
            String calculatedSkey = DigestUtils.md5Hex(paydate + domain + preSkey + secretKey);
            if (!calculatedSkey.equals(skey)) {
                throw new FebsException("订单回调失败,---"+orderId);
            }
            MallOrderInfo mallOrderInfo = ValidateEntityUtils.ensureColumnReturnEntity(orderId, MallOrderInfo::getId, mallOrderInfoMapper::selectOne, "订单不存在");
            ValidateEntityUtils.ensureEqual(mallOrderInfo.getPayResult(), "1", "订单已支付");
            ValidateEntityUtils.ensureEqual(mallOrderInfo.getAmount().toString(), amount, "订单金额异常");
            // 更新订单状态
            if ("00".equals(status)) {
                mallOrderInfo.setStatus(OrderStatusEnum.WAIT_SHIPPING.getValue());
                mallOrderInfo.setPayResult("1");
                mallOrderInfo.setPayTime(DateUtil.parseDateTime(paydate));
                mallOrderInfo.setDeliveryState(OrderDeliveryStateEnum.DELIVERY_WAIT.getValue());
                mallOrderInfo.setPayOrderNo(tranID);
                mallOrderInfoMapper.updateById(mallOrderInfo);
            }
            return new FebsResponse().success().message("OK");
        } catch (Exception e) {
            return new FebsResponse().fail().message("Internal Error");
        }
    }
}
src/main/java/cc/mrbird/febs/pay/model/FIUUInitPayRequest.java
New file
@@ -0,0 +1,29 @@
package cc.mrbird.febs.pay.model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
@ApiModel(value = "FIUUInitPayRequest", description = "初始化支付信息")
public class FIUUInitPayRequest {
    @NotNull(message = "参数不能为空")
    @ApiModelProperty(value = "订单ID")
    private Long orderId;
    @NotNull(message = "参数不能为空")
    @ApiModelProperty(value = "买家姓名")
    private String buyerName;
    @NotNull(message = "参数不能为空")
    @ApiModelProperty(value = "买家邮箱")
    private String buyerEmail;
    @NotNull(message = "参数不能为空")
    @ApiModelProperty(value = "买家手机号")
    private String buyerMobile;
}