first commit

This commit is contained in:
2026-09-21 19:21:35 +08:00
commit 6c944c4ffd
2733 changed files with 296956 additions and 0 deletions
@@ -0,0 +1,26 @@
package com.ruoyi.thirdparty.MaYi.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "mayi")
public class MYConfig {
private String apiKey;
private String gateway;
private String memberid;
private String notifyBaseUrl;
private String callBackUrl;
// (银行)通道编码
private String payBankCode;
// 下单接口
public static final String ApiPayAddOrder = "/Pay_AddOrder";
// 查询订单接口
public static final String ApiQueryTrans = "/Pay_Trade_query.html";
}
@@ -0,0 +1,90 @@
package com.ruoyi.thirdparty.MaYi.controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.mayi.PayNotifyRequest;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import com.ruoyi.thirdparty.MaYi.config.MYConfig;
import com.ruoyi.thirdparty.MaYi.service.MYService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
@Api(tags = "聚合支付")
@Slf4j
@RestController
@RequestMapping("api/mayi")
public class MYController extends BaseController {
@Autowired
private MYService myService;
@Autowired
private TtUserService userService;
// 下单
@ApiOperation("预下单")
@PostMapping("/ApiAddTrans")
@UpdateUserCache
public R ApiAddTrans(HttpServletRequest request, @RequestBody @Validated CreateOrderParam param) {
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
Integer userId = ((Long) checkLogin.getData()).intValue();
TtUser user = userService.getById(userId);
// 是否实名认证0未认证,1已认证'
if (user.getIsRealCheck().equals("0")) {
return R.realNameRequired("请实名认证后再进行充值!");
}
return myService.ApiAddTrans(param, user, request);
}
// 支付回调
@ApiOperation("支付回调")
// @PostMapping("/notify")
public String addTransNotify(@RequestBody PayNotifyRequest data) {
log.info("聚合支付支付回调 {}", JSONUtil.toJsonStr(data));
return myService.payNotify(data);
}
// 查询订单接口
// @PostMapping("/ApiQueryTrans")
// public String ApiQueryTrans(HttpServletRequest request, Map<String, String> param) {
// return "success";
// }
//
// // 查询余额接口
// @PostMapping("/ApiQueryBalancePHP")
// public String ApiQueryBalancePHP(HttpServletRequest request, Map<String, String> param) {
// return "success";
// }
public R checkLogin() {
Long userId;
try {
userId = getUserId();
if (ObjectUtil.isEmpty(userId)) return R.fail(401,"登录过期,请重新登录。");
return R.ok(userId);
} catch (Exception e) {
return R.fail("登录过期,请重新登录。");
}
}
}
@@ -0,0 +1,408 @@
package com.ruoyi.thirdparty.MaYi.service.Impl;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.*;
import com.ruoyi.admin.service.TtBonusService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.domain.common.constant.PayOrderStatus;
import com.ruoyi.domain.common.constant.PayType;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.dto.mayi.ApiPayAddOrderResponse;
import com.ruoyi.domain.dto.mayi.PayNotifyRequest;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtPromotionLevel;
import com.ruoyi.domain.entity.TtRechargeProd;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.MaYi.config.MYConfig;
import com.ruoyi.thirdparty.MaYi.service.MYService;
import com.ruoyi.thirdparty.MaYi.utils.signUtil;
import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Slf4j
@Service
public class MYServiceImpl implements MYService {
@Autowired
private MYConfig myConfig;
@Autowired
private TtOrderMapper orderMapper;
@Autowired
private Executor customThreadPoolExecutor;
@Autowired
private TtBonusService bonusService;
@Autowired
private TtUserService userService;
@Autowired
private TtUserMapper userMapper;
@Autowired
private TtRechargeProdMapper rechargeListMapper;
@Autowired
private TtUserBlendErcashMapper userBlendErcashMapper;
@Autowired
private TtRechargeProdMapper rechargeProdMapper;
@Autowired
private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService;
@Autowired
private ISysConfigService configService;
@Autowired
private TtPromotionLevelMapper ttPromotionLevelMapper;
@Autowired
private TtFirstRechargeMapper ttFirstRechargeMapper;
@Override
public R ApiAddTrans(@Validated CreateOrderParam param, TtUser user, HttpServletRequest request) {
// 查询商品信息
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper)
.eq(TtRechargeProd::getId, param.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
if (ObjectUtil.isEmpty(goods)) return R.fail("不存在的商品。");
if (goods.getPrice().compareTo(param.getGoodsPrice()) != 0) return R.fail("商品价格不一致。");
// 总价值
BigDecimal totalAmount = param.getGoodsPrice().multiply(new BigDecimal(param.getGoodsNum()));
// 构建完整参数
Map<String, String> map = buildBaseParam(new HashMap<>(), request);
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
String orderId = String.valueOf(snowflake.nextId());
map.put("pay_orderid", orderId);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String tranDateTime = dateFormat.format(new Date());
map.put("pay_applydate", tranDateTime);
map.put("pay_bankcode", myConfig.getPayBankCode());
// 回调地址
map.put("pay_notifyurl", myConfig.getNotifyBaseUrl());
// 同步跳转
map.put("pay_callbackurl", myConfig.getCallBackUrl() + "/api/mayi/notify");
map.put("pay_amount", totalAmount.toString());
// 签名
String sign = signUtil.getSign(map, myConfig.getApiKey());
map.put("pay_md5sign", sign);
map.put("pay_clientip", IpUtils.getIpAddr());
map.put("pay_productname", goods.getName());
map.put("type", "json");
// System.out.println(JSONUtil.toJsonStr(map));
// 发送请求
HttpRequest post = HttpUtil.createPost(myConfig.getGateway() + MYConfig.ApiPayAddOrder);
post.header("Content-Type","application/x-www-form-urlencoded");
post.formStr(map);
HttpResponse res = post.execute();
// 解析响应
ApiPayAddOrderResponse resBody = JSONUtil.toBean(res.body(), ApiPayAddOrderResponse.class);
if (!resBody.getStatus().equals("1")) {
return R.fail(resBody);
}
// 创建订单
TtOrder order = new TtOrder();
order.setOrderId(orderId);
order.setOutTradeNo(resBody.getPay_orderid());
order.setUserId(user.getUserId());
order.setType(PayType.JU_HE_ZHI_FU.getCode());
order.setGoodsId(param.getGoodsId());
order.setGoodsPrice(param.getGoodsPrice());
order.setGoodsNum(param.getGoodsNum());
order.setTotalAmount(totalAmount);
order.setSign(sign);
order.setStatus(PayOrderStatus.NO_PAY.getCode());
order.setPayUrl(resBody.getPayUrl());
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
orderMapper.insert(order);
return R.ok(order);
}
@Override
public String payNotify(PayNotifyRequest data) {
TtOrder order = null;
// TODO: 2024/4/12 最好再查询一下第三方平台的订单信息
try {
// 查询订单信息
order = new LambdaQueryChainWrapper<>(orderMapper)
.eq(TtOrder::getOrderId, data.getOrderid())
.eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode())
.one();
if (ObjectUtil.isEmpty(order)) {
log.warn("支付回调异常,不存在的有效订单。");
return "fail";
}
// 查询用户信息
TtUser user = new LambdaQueryChainWrapper<>(userMapper)
.eq(TtUser::getUserId, order.getUserId())
.eq(TtUser::getDelFlag, 0)
.one();
if (ObjectUtil.isEmpty(user)) {
log.warn("支付回调异常,不存在的有效用户。");
return "fail";
}
if (order.getStatus().equals(PayOrderStatus.PAY_COMPLE.getCode()) || order.getStatus().equals(PayOrderStatus.PAY_YET.getCode())) {
log.warn("重复的回调!该订单已完成。");
return "success";
}
// 查询商品信息
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeListMapper)
.eq(TtRechargeProd::getId, order.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
if (ObjectUtil.isEmpty(goods)) {
log.warn("支付回调异常,不存在的商品。");
return "fail";
}
// 账户结算
if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO);
if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO);
payNotifyAccounting(order, user, goods, order.getGoodsNum());
// 首充赠送
firstChargeGiftAmount(user, goods, order.getGoodsNum());
// 推广等级充值赠送
promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum());
// 推广充值返佣
promotionChargeCommission(user, goods, order.getGoodsNum());
// 更新订单
new LambdaUpdateChainWrapper<>(orderMapper)
.eq(TtOrder::getId, order.getId())
.set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode())
.set(TtOrder::getUpdateTime, new Date())
.update();
// 发送充值成功通知
rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(user.getUserId().toString(), order.getGoodsPrice());
return "success";
} catch (Exception e) {
e.printStackTrace();
log.warn("聚合支付,回调异常");
if (ObjectUtil.isNotNull(order)) {
new LambdaUpdateChainWrapper<>(orderMapper)
.eq(TtOrder::getId, order.getId())
.set(TtOrder::getStatus, PayOrderStatus.CALL_BACK_ERRO.getCode())
.set(TtOrder::getUpdateTime, new Date())
.update();
}
return "fail";
}
}
@Override
public R ApiQueryTrans(Map<String, String> param, TtUser user) {
return null;
}
@Override
public R ApiPropayTrans(Map<String, String> param, TtUser user) {
return null;
}
@Override
public R ApiQueryBalancePHP(Map<String, String> param, TtUser user) {
return null;
}
// 构建基本请求信息
private Map<String, String> buildBaseParam(Map<String, String> map, HttpServletRequest request) {
map.put("pay_memberid", myConfig.getMemberid());
return map;
}
// 账户结算
public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) {
// 加钱(充值不加积分:productA + productC 全部计入 account_amount)
// BigDecimal totalAmount = order.getTotalAmount();
BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber));
BigDecimal totalAmount = productAmount.add(productGift);
BigDecimal totalCredits = BigDecimal.ZERO;
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, user.getUserId())
.setSql("account_amount = account_amount + " + totalAmount.toString());
userService.update(userUpdate);
user = userService.getById(user.getUserId());
// 综合消费日志
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(user.getUserId())
.amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null)
.finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null)
.credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null)
.finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null)
.total(totalAmount.add(totalCredits)) // 收支合计
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.RECHARGE.getCode())
.remark(TtAccountRecordSource.RECHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
// 更新VIP等级
userService.updateUserVIPLevel(user.getUserId());
}
/**
* 首充赠送
*/
public void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
// 1.判断是否为首充
LambdaQueryWrapper<TtUserBlendErcash> wrapper = new LambdaQueryWrapper<>();
wrapper
.eq(TtUserBlendErcash::getSource, 1)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId());
List<TtUserBlendErcash> ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper);
if (ttUserBlendErcashes.size() > 0) {
return;
}
// 2.加钱
// 充值金额
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
// 查询赠送比例
BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount);
if (Objects.isNull(firstChargeAmountRatio)) {
return;
}
// 计算赠送金额
BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio);
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + giftAmount.toString());
userService.update(userUpdate);
// 3.记录
ttUser = userService.getById(ttUser.getUserId());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(ttUser.getUserId())
.amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null)
.finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null)
.total(giftAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.FIRST_CHARGE.getCode())
.remark(TtAccountRecordSource.FIRST_CHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
}
/**
* 推广等级充值赠送
*/
public void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
// 1.查询用户推广等级对应的赠送比例
TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel());
// 2.加钱
BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio();
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
BigDecimal giftAmount = totalAmount.multiply(amountRatio);
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + giftAmount.toString());
userService.update(userUpdate);
// 3.记录
ttUser = userService.getById(ttUser.getUserId());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(ttUser.getUserId())
.amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null)
.finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null)
.total(giftAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode())
.remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
}
/**
* 推广充值返佣
*/
private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
if (Objects.isNull(ttUser.getParentId())) {
return;
}
// 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结,
// 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount);
}
}
@@ -0,0 +1,35 @@
package com.ruoyi.thirdparty.MaYi.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.mayi.PayNotifyRequest;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtRechargeProd;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
public interface MYService {
R ApiAddTrans(CreateOrderParam param, TtUser user, HttpServletRequest request);
R ApiQueryTrans(Map<String,String> param, TtUser user);
R ApiPropayTrans(Map<String,String> param, TtUser user);
R ApiQueryBalancePHP(Map<String,String> param, TtUser user);
public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber);
String payNotify(PayNotifyRequest data);
/**
* �׳�����
*/
public void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber);
/**
* �ƹ�ȼ���ֵ����
*/
public void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber);
}
@@ -0,0 +1,47 @@
package com.ruoyi.thirdparty.MaYi.utils;
// zy返回值
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ZYHTTPRes {
public enum resCode{
success("0000"),
DOING("0001");
private String code;
resCode(String code){
this.code = code;
}
public String getCode(){
return this.code;
}
}
private String respCode;
//返回码说明
private String respDesc;
//返回码
private String payCode;
private String payDesc;
private String amt;
private String tranTime;
private String tranType;
//扩展信息
private String msgExt;
private String payUrl;
// 交易流水号
private String merReqNo;
//服务端响应流水号
private String serverRspNo;
private String sign;
}
@@ -0,0 +1,39 @@
package com.ruoyi.thirdparty.MaYi.utils;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.crypto.digest.Digester;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import java.security.MessageDigest;
import java.util.*;
public class signUtil {
public static String getSign(Map<String, String> map, String key) {
// 排序
List<Map.Entry<String, String>> entryList = new ArrayList<>(map.entrySet());
Collections.sort(entryList, new Comparator<Map.Entry<String, String>>() {
@Override
public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
String str = "";
for (int i = 0; i < entryList.size(); i++) {
// if (i == entryList.size()-1) {
// str = str + entryList.get(i).getKey() + "=" + entryList.get(i).getValue();
// continue;
// }
str = str + entryList.get(i).getKey() + "=" + entryList.get(i).getValue() + "&";
}
str = str + "key=" +key;
// MD5加密
Digester md5 = new Digester(DigestAlgorithm.MD5);
return md5.digestHex(str).toUpperCase();
}
}
@@ -0,0 +1,43 @@
package com.ruoyi.thirdparty.abkpay.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* abkpay 支付渠道配置,对应 application.yml 中 abkpay 节点。
* 仅代收(充值),代收下单/查单/余额分别拼接 baseUrl + /Pay_SG、/Pay_Query、/Pay_Querybalance。
*/
@Data
@ConfigurationProperties(prefix = "abkpay")
public class AbkPayProperties {
/** API 基础地址(代收),例如 https://t5556qpd.abkoiuhnbrqwkjngpay.xyz */
private String baseUrl;
/** 商户号(mchId),由平台提供 —— 部署时填写 */
private String mchId;
/** 商户密钥(secret),由平台提供 —— 部署时填写 */
private String secret;
/**
* 默认产品编码(wayCode),金额<800 时使用。
* 常规流量为 300<=金额<800 区间(<300 已由统一充值路由默认分配给 itpay)。
*/
private String wayCode;
/** 高额产品编码(wayCodeHigh),金额>=800 时使用;为空则统一使用 wayCode */
private String wayCodeHigh;
/**
* 客户端 IP(clientIp),下单必填。
* 一般填写平台已加白的服务器出口 IP(白名单 IP)。
*/
private String clientIp;
/** 支付回调通知地址(notifyUrl),只有传该值平台才发起回调 */
private String notifyUrl;
/** 前端支付完成跳转地址(returnUrl,可空) */
private String returnUrl;
}
@@ -0,0 +1,125 @@
package com.ruoyi.thirdparty.abkpay.controller;
import cn.hutool.json.JSONUtil;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.UserPermission;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.thirdparty.abkpay.model.AbkPayResult;
import com.ruoyi.thirdparty.abkpay.service.AbkPayPaymentService;
import com.ruoyi.thirdparty.chengduhipay.controller.OrderStatusRequest;
import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest;
import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* abkpay 支付渠道接口(仅代收)。
* 接口清单:
* pay —— 创建充值订单(一般由统一充值按权重路由调用)
* notify —— 接收平台异步回调(平台调用,需返回字符串 SUCCESS)
* orderStatus —— 查询本地订单支付状态(供前端轮询)
* queryPay —— 主动查询平台订单
*/
@Api(tags = "abkpay充值")
@Slf4j
@RestController
@RequestMapping("/api/abkpay")
public class AbkPayController {
@Autowired
private TtUserService ttUserService;
@Autowired
private AbkPayPaymentService abkPayPaymentService;
/**
* 创建充值订单(默认由统一充值路由调用,独立入口默认关闭)
*/
@ApiOperation("创建充值订单")
@UserPermission
@PostMapping("/pay")
public R<PayResponse> pay(@RequestBody @Validated PayRequest param) {
Long userId = SecurityUtils.getUserId();
TtUser ttUser = ttUserService.getById(userId);
if (ttUser == null) {
return R.fail("用户不存在");
}
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired("未实名认证");
}
return abkPayPaymentService.pay(userId, param);
}
/**
* 异步回调通知(application/json)。
* 平台订单成功或失败都会回调,商户须返回字符串 "SUCCESS"(大小写均可)。
* 处理失败(验签失败/订单不存在等)返回非 SUCCESS,促使平台重推。
*/
@ApiOperation("充值回调通知")
@Anonymous
@PostMapping("/notify")
@ResponseBody
public String notify(@RequestBody Map<String, Object> params) {
log.info("AbkPay notify params: {}", JSONUtil.toJsonStr(params));
R<Void> result = abkPayPaymentService.notifyCallback(toStringMap(params));
if (!R.isSuccess(result)) {
log.error("AbkPay notify 处理失败: {}", result.getMsg());
return "fail";
}
return "SUCCESS";
}
/**
* 查询本地订单支付状态(供前端轮询)
*/
@ApiOperation("查询本地订单支付状态")
@UserPermission
@PostMapping("/orderStatus")
public R<Map<String, Object>> orderStatus(@RequestBody @Validated OrderStatusRequest param) {
Long userId = SecurityUtils.getUserId();
return abkPayPaymentService.queryOrderStatus(userId, param.getOrderId());
}
/**
* 主动查询平台订单(仅允许查询本人订单)
*/
@ApiOperation("查询充值订单")
@UserPermission
@GetMapping("/queryPay")
public R<AbkPayResult> queryPay(@RequestParam String outTradeNo) {
Long userId = SecurityUtils.getUserId();
return abkPayPaymentService.queryPay(userId, outTradeNo);
}
/**
* 将 JSON 回调参数(含数字类型)统一转为字符串 Map,供签名校验使用。
*/
private Map<String, String> toStringMap(Map<String, Object> params) {
Map<String, String> result = new LinkedHashMap<>();
if (params == null) {
return result;
}
for (Map.Entry<String, Object> entry : params.entrySet()) {
if (entry.getValue() != null) {
result.put(entry.getKey(), String.valueOf(entry.getValue()));
}
}
return result;
}
}
@@ -0,0 +1,71 @@
package com.ruoyi.thirdparty.abkpay.model;
import lombok.Data;
/**
* abkpay 响应 data 业务数据(下单 / 查单 / 查余额共用,按接口取对应字段)。
* 金额单位:分(例如 10000 表示 100.00 元)。
*/
@Data
public class AbkPayData {
/** 商户号 */
private String mchId;
/** 通道类型 / 产品编码(查单返回) */
private String wayCode;
/** 支付订单号(平台单号) */
private String tradeNo;
/** 商户订单号 */
private String outTradeNo;
/** 通道订单号 */
private String originTradeNo;
/** 订单金额(单位:分) */
private String amount;
/** 商品标题 */
private String subject;
/** 商品描述 */
private String body;
/** 商户扩展参数 */
private String extParam;
/** 异步通知地址 */
private String notifyUrl;
/** 支付地址(下单返回,展示给付款用户) */
private String payUrl;
/** 订单过期时间(13 位时间戳) */
private String expiredTime;
/** 支付成功时间(13 位时间戳) */
private String successTime;
/** 下单时间(13 位时间戳) */
private String createTime;
/** 订单状态:0=待支付,1=支付成功,2=支付失败 */
private Integer state;
/** 通知状态:0=未通知,1=通知成功,2=通知失败 */
private Integer notifyState;
/** SDK 参数内容(部分通道返回) */
private String sdkData;
/** 商户余额 / 跑量(查余额返回,单位:元) */
private String balance;
/** 商户可用额度 / 剩余预付(查余额返回,单位:元) */
private String bond;
/** 商户总预付(查余额返回,单位:元) */
private String zbond;
}
@@ -0,0 +1,33 @@
package com.ruoyi.thirdparty.abkpay.model;
import lombok.Data;
/**
* abkpay 代收异步回调通知数据(对接文档回调参数,仅作字段参考)。
* 实际验签与处理以回调原始参数 Map 为准。金额单位:分。
*/
@Data
public class AbkPayNotifyData {
/** 商户号 */
private String mchId;
/** 支付订单号(平台单号) */
private String tradeNo;
/** 商户订单号 */
private String outTradeNo;
/** 通道订单号 */
private String originTradeNo;
/** 订单金额(单位:分) */
private String amount;
/** 商品标题 */
private String subject;
/** 商品描述 */
private String body;
/** 商户扩展参数(回调原样返回) */
private String extParam;
/** 订单状态:0=待支付,1=支付成功,2=支付失败 */
private String state;
/** 通知时间(13 位时间戳) */
private String notifyTime;
/** 签名 */
private String sign;
}
@@ -0,0 +1,19 @@
package com.ruoyi.thirdparty.abkpay.model;
import lombok.Data;
/**
* abkpay 统一响应对象(下单 / 查单 / 查余额通用)。
* code=0 表示网关成功,其它为失败;data 为业务数据。
*/
@Data
public class AbkPayResult {
/** 返回状态码:0=成功,其它失败 */
private Integer code;
/** 返回信息(失败原因,如:签名失败) */
private String message;
/** 对 data 内数据的签名,如 data 为空则不返回 */
private String sign;
/** 业务数据 */
private AbkPayData data;
}
@@ -0,0 +1,476 @@
package com.ruoyi.thirdparty.abkpay.service;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtOrderMapper;
import com.ruoyi.admin.mapper.TtRechargeProdMapper;
import com.ruoyi.admin.mapper.TtRechargeRecordMapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.domain.common.constant.PayOrderStatus;
import com.ruoyi.domain.common.constant.PayType;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtRechargeProd;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtRechargeRecord;
import com.ruoyi.thirdparty.abkpay.config.AbkPayProperties;
import com.ruoyi.thirdparty.abkpay.model.AbkPayResult;
import com.ruoyi.thirdparty.abkpay.utils.AbkPaySignUtil;
import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest;
import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse;
import com.ruoyi.thirdparty.common.service.PayFrequencyGuard;
import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* abkpay 支付渠道充值服务(仅代收)。
* 实现:创建订单(Pay_SG)、异步回调(notify)、订单查询(Pay_Query)、余额查询(Pay_Querybalance)、本地订单状态查询。
* 安全:回调只在 state==1 加币、校验回调金额与订单金额一致、DB条件CAS防重复加币、验签。
* 请求:POST application/json;金额单位:分。
*/
@Slf4j
@Service
@EnableConfigurationProperties(AbkPayProperties.class)
public class AbkPayPaymentService {
@Autowired
private AbkPayProperties properties;
@Autowired
private TtOrderMapper orderMapper;
@Autowired
private TtRechargeProdMapper rechargeProdMapper;
@Autowired
private TtUserMapper userMapper;
@Autowired
private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService;
@Autowired
private TtRechargeRecordMapper ttRechargeRecordMapper;
@Autowired
private TtUserService userService;
@Autowired
private TtUserBlendErcashMapper userBlendErcashMapper;
@Autowired
private PayFrequencyGuard payFrequencyGuard;
@Autowired
private PlatformTransactionManager transactionManager;
// ========================================================================
// 创建订单:Pay_SG
// ========================================================================
public R<PayResponse> pay(Long uid, PayRequest request) {
// 1. 查询商品
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper)
.eq(TtRechargeProd::getId, request.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
if (ObjectUtil.isEmpty(goods)) {
return R.fail("不存在的商品");
}
// 2. 生成商户单号(雪花ID,纯数字)
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
String orderId = String.valueOf(snowflake.nextId());
// 3. 计算金额(元)与提交金额(分)
BigDecimal totalAmount = goods.getPrice();
String amountFen = yuanToFen(totalAmount);
// subject 使用下单用户ID,便于渠道侧对账定位用户
String subject = String.valueOf(uid);
// 4. 组装参与签名的参数(wayCode 按金额分层:>=800 用 wayCodeHigh,否则用 wayCode)
Map<String, String> params = new LinkedHashMap<>();
params.put("mchId", properties.getMchId());
params.put("wayCode", selectWayCode(totalAmount));
params.put("subject", subject);
params.put("outTradeNo", orderId);
params.put("amount", amountFen);
if (StrUtil.isNotBlank(properties.getClientIp())) {
params.put("clientIp", properties.getClientIp());
}
params.put("notifyUrl", properties.getNotifyUrl());
if (StrUtil.isNotBlank(properties.getReturnUrl())) {
params.put("returnUrl", properties.getReturnUrl());
}
params.put("reqTime", String.valueOf(System.currentTimeMillis()));
// 5. 计算签名
String sign = AbkPaySignUtil.sign(params, properties.getSecret());
params.put("sign", sign);
// 6. 发起下单请求(application/json)
String createUrl = properties.getBaseUrl() + "/Pay_SG.html";
log.info("AbkPay createOrder url={}, params={}", createUrl, JSONUtil.toJsonStr(params));
try (HttpResponse response = HttpRequest.post(createUrl)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(params))
.timeout(15000)
.execute()) {
String body = response.body();
log.info("AbkPay createOrder response status={}, body={}", response.getStatus(), body);
AbkPayResult resBody = JSONUtil.toBean(body, AbkPayResult.class);
if (resBody.getCode() == null || resBody.getCode() != 0 || resBody.getData() == null) {
log.error("AbkPay 下单失败, code={}, message={}", resBody.getCode(), resBody.getMessage());
return R.fail(StrUtil.isNotBlank(resBody.getMessage()) ? resBody.getMessage() : "下单失败");
}
// 7. 保存订单
TtOrder order = new TtOrder();
order.setOrderId(orderId);
order.setOutTradeNo(resBody.getData().getTradeNo());
order.setUserId(Math.toIntExact(uid));
order.setType(PayType.ABK_PAY.getCode());
order.setGoodsId(request.getGoodsId());
order.setGoodsPrice(goods.getPrice());
order.setGoodsNum(1);
order.setTotalAmount(totalAmount);
order.setSign(sign);
order.setStatus(PayOrderStatus.NO_PAY.getCode());
order.setCreateTime(new Date());
orderMapper.insert(order);
// 8. 返回支付链接
PayResponse resp = new PayResponse();
resp.setPayUrl(resBody.getData().getPayUrl());
resp.setOrderId(orderId);
resp.setPlatformOrderId(resBody.getData().getTradeNo());
log.info("AbkPay 下单成功, orderId={}, tradeNo={}", orderId, resBody.getData().getTradeNo());
return R.ok(resp);
} catch (Exception e) {
log.error("AbkPay 下单异常", e);
return R.fail("支付通道异常,请稍后重试");
}
}
// ========================================================================
// 异步回调:notify
// ========================================================================
/**
* 处理 abkpay 异步回调。
* 防错加币:验签通过且 state==1(成功) 才结算;其它状态仅应答不加币。
* 防金额篡改:校验回调金额(分)与本地订单金额一致。
* 防重复加币:DB 条件CAS(NO_PAY -> PAY_COMPLE)取影响行数,仅抢占成功者加币,与结算同事务。
*/
public R<Void> notifyCallback(Map<String, String> params) {
log.info("AbkPay notify callback: {}", JSONUtil.toJsonStr(params));
// 1. 验签,失败直接拒绝,不加币
if (!AbkPaySignUtil.verify(params, properties.getSecret())) {
log.error("AbkPay 回调签名验证失败");
return R.fail("签名验证失败");
}
String outTradeNo = params.get("outTradeNo");
String state = params.get("state");
// 2. 只有明确的成功状态(state==1)才加币
if (!"1".equals(state)) {
log.warn("AbkPay 回调非成功状态,不加币, outTradeNo={}, state={}", outTradeNo, state);
return R.ok();
}
// 3. 查询本地订单(orderId == outTradeNo)
if (StrUtil.isBlank(outTradeNo)) {
log.error("AbkPay 回调缺少 outTradeNo");
return R.fail("回调数据不完整");
}
TtOrder order = new LambdaQueryChainWrapper<>(orderMapper)
.eq(TtOrder::getOrderId, outTradeNo)
.eq(TtOrder::getType, PayType.ABK_PAY.getCode())
.one();
if (order == null) {
log.error("AbkPay 回调订单不存在, outTradeNo={}", outTradeNo);
return R.fail("订单不存在");
}
// 3.1 校验回调金额(分)与本地订单金额一致,防止金额篡改;金额缺失直接拒绝
String callbackAmount = params.get("amount");
String orderAmountFen = yuanToFen(order.getTotalAmount());
if (StrUtil.isBlank(callbackAmount) || !callbackAmount.equals(orderAmountFen)) {
log.error("AbkPay 回调金额缺失或与订单金额不一致, outTradeNo={}, callback={}, order={}",
outTradeNo, callbackAmount, orderAmountFen);
return R.fail("回调金额与订单金额不一致");
}
// 4. DB条件CAS + 事务,保证同一成功回调重复推送只加一次币
Boolean credited = new TransactionTemplate(transactionManager).execute(txStatus -> {
LambdaUpdateWrapper<TtOrder> uw = new LambdaUpdateWrapper<>();
uw.eq(TtOrder::getId, order.getId())
.eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode())
.set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode())
.set(TtOrder::getUpdateTime, new Date());
int rows = orderMapper.update(null, uw);
if (rows <= 0) {
// 已被处理,幂等跳过
return Boolean.FALSE;
}
// 抢占成功,执行结算
TtUser user = new LambdaQueryChainWrapper<>(userMapper)
.eq(TtUser::getUserId, order.getUserId())
.eq(TtUser::getDelFlag, 0)
.one();
if (user == null) {
throw new IllegalStateException("AbkPay 回调用户不存在, userId=" + order.getUserId());
}
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper)
.eq(TtRechargeProd::getId, order.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
if (goods == null) {
throw new IllegalStateException("AbkPay 回调商品不存在, goodsId=" + order.getGoodsId());
}
if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO);
if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO);
payNotifyAccounting(order, user, goods, order.getGoodsNum());
return Boolean.TRUE;
});
if (Boolean.TRUE.equals(credited)) {
// 支付成功:清空该用户的未支付次数缓存
payFrequencyGuard.clearOnPaid(order.getUserId().longValue());
rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(
order.getUserId().toString(), order.getGoodsPrice());
log.info("AbkPay 回调处理成功, orderId={}", order.getOrderId());
} else {
log.info("AbkPay 回调幂等跳过(订单已处理), orderId={}", order.getOrderId());
}
return R.ok();
}
// ========================================================================
// 本地订单状态查询(供前端轮询)
// ========================================================================
public R<Map<String, Object>> queryOrderStatus(Long userId, String orderId) {
if (ObjectUtil.isEmpty(orderId)) {
return R.fail("订单号不能为空");
}
TtOrder order = new LambdaQueryChainWrapper<>(orderMapper)
.eq(TtOrder::getOrderId, orderId)
.one();
if (order == null) {
return R.fail("订单不存在");
}
if (!Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) {
return R.fail("无权查询该订单");
}
String status = order.getStatus();
String message;
if (PayOrderStatus.PAY_COMPLE.getCode().equals(status)) {
message = "充值成功";
} else if (PayOrderStatus.NO_PAY.getCode().equals(status)) {
message = "支付中";
} else if (PayOrderStatus.CANCEL.getCode().equals(status)) {
message = "订单已取消";
} else if (PayOrderStatus.CALL_BACK_ERRO.getCode().equals(status)) {
message = "回调处理异常";
} else {
message = "支付失败";
}
Map<String, Object> data = new HashMap<>();
data.put("status", status);
data.put("amount", order.getGoodsPrice());
data.put("message", message);
return R.ok(data);
}
// ========================================================================
// 订单查询:Pay_Query
// ========================================================================
public R<AbkPayResult> queryPay(Long userId, String outTradeNo) {
if (StrUtil.isBlank(outTradeNo)) {
return R.fail("订单号不能为空");
}
// 归属校验:只允许查询本人的 abkpay 订单
TtOrder order = new LambdaQueryChainWrapper<>(orderMapper)
.eq(TtOrder::getOrderId, outTradeNo)
.eq(TtOrder::getType, PayType.ABK_PAY.getCode())
.one();
if (order == null) {
return R.fail("订单不存在");
}
if (userId == null || !Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) {
return R.fail("无权查询该订单");
}
Map<String, String> params = new LinkedHashMap<>();
params.put("mchId", properties.getMchId());
params.put("outTradeNo", outTradeNo);
params.put("reqTime", String.valueOf(System.currentTimeMillis()));
String sign = AbkPaySignUtil.sign(params, properties.getSecret());
params.put("sign", sign);
String queryUrl = properties.getBaseUrl() + "/Pay_Query.html";
log.info("AbkPay queryOrder url={}, params={}", queryUrl, JSONUtil.toJsonStr(params));
try (HttpResponse response = HttpRequest.post(queryUrl)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(params))
.timeout(15000)
.execute()) {
String body = response.body();
log.info("AbkPay queryOrder response status={}, body={}", response.getStatus(), body);
AbkPayResult resBody = JSONUtil.toBean(body, AbkPayResult.class);
if (resBody.getCode() == null || resBody.getCode() != 0) {
return R.fail(StrUtil.isNotBlank(resBody.getMessage()) ? resBody.getMessage() : "查询失败");
}
return R.ok(resBody);
} catch (Exception e) {
log.error("AbkPay 查单异常", e);
return R.fail("查询失败");
}
}
// ========================================================================
// 余额查询:Pay_Querybalance
// ========================================================================
public R<String> getBalance() {
Map<String, String> params = new LinkedHashMap<>();
params.put("mchId", properties.getMchId());
params.put("reqTime", String.valueOf(System.currentTimeMillis()));
String sign = AbkPaySignUtil.sign(params, properties.getSecret());
params.put("sign", sign);
String balanceUrl = properties.getBaseUrl() + "/Pay_Querybalance.html";
log.info("AbkPay getBalance url={}, params={}", balanceUrl, JSONUtil.toJsonStr(params));
try (HttpResponse response = HttpRequest.post(balanceUrl)
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(params))
.timeout(15000)
.execute()) {
String body = response.body();
log.info("AbkPay getBalance response status={}, body={}", response.getStatus(), body);
AbkPayResult resBody = JSONUtil.toBean(body, AbkPayResult.class);
if (resBody.getCode() == null || resBody.getCode() != 0 || resBody.getData() == null) {
return R.fail(StrUtil.isNotBlank(resBody.getMessage()) ? resBody.getMessage() : "查询失败");
}
return R.ok(resBody.getData().getBalance());
} catch (Exception e) {
log.error("AbkPay 余额查询异常", e);
return R.fail("查询失败");
}
}
// ========================================================================
// 账户结算(与现有 hipay/richku/itpay 逻辑保持一致)
// ========================================================================
public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) {
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
BigDecimal oldAmount = user.getAccountAmount();
if (oldAmount == null) {
oldAmount = BigDecimal.ZERO;
}
BigDecimal newAmount = oldAmount.add(totalAmount);
// 更新用户余额
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, user.getUserId())
.setSql("account_amount = account_amount + " + totalAmount);
userService.update(userUpdate);
// 综合消费日志
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(user.getUserId())
.amount(totalAmount)
.finalAmount(newAmount)
.total(totalAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.RECHARGE.getCode())
.remark(TtAccountRecordSource.RECHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
// 充值记录
TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build();
ttRechargeRecord.setUserId(order.getUserId());
ttRechargeRecord.setParentId(user.getParentId());
ttRechargeRecord.setArrivalAmount(totalAmount);
ttRechargeRecord.setAmountActuallyPaid(totalAmount);
ttRechargeRecord.setFinallyPrice(newAmount);
ttRechargeRecord.setOrderId(order.getOrderId());
ttRechargeRecord.setOutTradeNo(order.getOutTradeNo());
ttRechargeRecord.setStatus("0");
ttRechargeRecord.setChannelType("1");
ttRechargeRecord.setCreateTime(DateUtils.getNowDate());
ttRechargeRecordMapper.insert(ttRechargeRecord);
// 更新VIP等级
userService.updateUserVIPLevel(user.getUserId());
}
// ========================================================================
// 工具方法
// ========================================================================
/**
* 按订单金额(元)选择产品编码:
* 金额>=800 且配置了 wayCodeHigh -> wayCodeHigh(如 8003);否则 -> 默认 wayCode(如 8012,覆盖 300~800)。
* 说明:<300 的订单由统一充值路由默认分配给 itpay,正常不会到达本渠道;
* 若 itpay 停用导致 <300 回落到本渠道,也用默认 wayCode 下单,由平台侧限额规则决定是否受理。
*/
private String selectWayCode(BigDecimal amount) {
if (StrUtil.isNotBlank(properties.getWayCodeHigh())
&& amount != null
&& amount.compareTo(new BigDecimal("800")) >= 0) {
return properties.getWayCodeHigh();
}
return properties.getWayCode();
}
/**
* 元转分(字符串整数)。例如 100.00 -> "10000"。
*/
private String yuanToFen(BigDecimal yuan) {
if (yuan == null) {
return "0";
}
return yuan.multiply(new BigDecimal(100)).setScale(0, RoundingMode.HALF_UP).toPlainString();
}
}
@@ -0,0 +1,71 @@
package com.ruoyi.thirdparty.abkpay.utils;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.TreeMap;
/**
* abkpay 签名工具。
* 签名算法(对接文档):所有非空且不为 "0" 的参数按参数名 ASCII 升序(字典序)->
* 拼成 key1=value1&key2=value2&... -> 末尾追加 &key={商户密钥} -> MD5 -> 32 位小写。
* 例:sign = md5( key1=value1&key2=value2...&key=商户密钥 )
*/
@Slf4j
public class AbkPaySignUtil {
/**
* 生成签名
*
* @param params 参与签名的参数(不包含 sign 本身)
* @param secret 商户密钥
* @return MD5 小写签名字符串
*/
public static String sign(Map<String, String> params, String secret) {
TreeMap<String, String> sorted = new TreeMap<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
String value = entry.getValue();
// 对接文档要求:值为空、空字符串或 "0" 时均不参与签名
if (value != null && !value.isEmpty() && !"0".equals(value)) {
sorted.put(entry.getKey(), value);
}
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sorted.entrySet()) {
if (sb.length() > 0) {
sb.append("&");
}
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
// 末尾追加商户密钥,格式为 &key={secret}
sb.append("&key=").append(secret);
String signStr = sb.toString();
// 注意:签名原文含商户密钥,禁止打印到日志
return DigestUtil.md5Hex(signStr).toLowerCase();
}
/**
* 验证回调签名(大小写不敏感)
*
* @param params 完整回调参数(包含 sign)
* @param secret 商户密钥
* @return 签名是否匹配
*/
public static boolean verify(Map<String, String> params, String secret) {
String receivedSign = params.get("sign");
if (receivedSign == null || receivedSign.isEmpty()) {
return false;
}
TreeMap<String, String> signParams = new TreeMap<>(params);
signParams.remove("sign");
String calculatedSign = sign(signParams, secret);
boolean valid = calculatedSign.equalsIgnoreCase(receivedSign);
if (!valid) {
log.error("AbkPay 签名验证失败, expected={}, actual={}", calculatedSign, receivedSign);
}
return valid;
}
}
@@ -0,0 +1,32 @@
package com.ruoyi.thirdparty.alipay.config;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
@EnableConfigurationProperties(value = AliProperties.class)
public class AliCilent {
@Bean
@Qualifier("RealNameAuthentication")
public AlipayClient RealNameAuthentication(AliProperties aliProperties) {
List<MerchantConfig> merchants = aliProperties.getMerchants();
if (merchants.isEmpty()) throw new IllegalArgumentException("No merchant configuration provided");
MerchantConfig merchantConfig = merchants.get(1);
return new DefaultAlipayClient(
aliProperties.getServerUrl(),
merchantConfig.getAppId(),
merchantConfig.getPrivateKey(),
aliProperties.getFormat(),
aliProperties.getCharset(),
merchantConfig.getAlipayPublicKey(),
aliProperties.getSignType()
);
}
}
@@ -0,0 +1,32 @@
package com.ruoyi.thirdparty.alipay.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@Data
@ConfigurationProperties(prefix = "alipay")
public class AliProperties {
private String serverUrl;
private String format;
private String charset;
private String signType;
private List<MerchantConfig> merchants;
private String notifyUrl;
private String realNameAuthenticationReturnUrl;
private String authenticationHost;
private String authenticationPath;
private String authenticationAppcode;
}
@@ -0,0 +1,14 @@
package com.ruoyi.thirdparty.alipay.config;
import lombok.Data;
@Data
public class MerchantConfig {
private String appId;
private String privateKey;
private String alipayPublicKey;
}
@@ -0,0 +1,55 @@
package com.ruoyi.thirdparty.alipay.controller;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.UserPermission;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import com.ruoyi.thirdparty.alipay.service.AliPayService;
import com.ruoyi.thirdparty.wechat.service.ApiTokenService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Api(tags = "支付宝")
@RestController
@RequestMapping("/api/alipay")
public class AliPayController {
private final ApiTokenService tokenService;
private final AliPayService aliPayService;
private final TtUserService ttUserService;
public AliPayController(ApiTokenService apiTokenService,
AliPayService aliPayService,
TtUserService ttUserService) {
this.tokenService = apiTokenService;
this.aliPayService = aliPayService;
this.ttUserService = ttUserService;
}
@ApiOperation("支付")
@UserPermission
@PostMapping(value = "/trade/page/pay")
public R pay(@RequestBody @Validated CreateOrderParam param) {
Long userId = SecurityUtils.getUserId();
TtUser ttUser = ttUserService.getById(userId);
// 是否实名认证(0未认证 1已认证)
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired();
}
String ip = IpUtils.getIpAddr();
return aliPayService.pay(param, ttUser, ip);
}
// @PostMapping(value = "/callBack")
public String callBack(@RequestParam Map<String, String> params) {
return aliPayService.callBack(params);
}
}
@@ -0,0 +1,14 @@
package com.ruoyi.thirdparty.alipay.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import java.util.Map;
public interface AliPayService {
R pay(CreateOrderParam param, TtUser ttUser, String ip);
String callBack(Map<String, String> params);
}
@@ -0,0 +1,14 @@
package com.ruoyi.thirdparty.alipay.service;
public interface RealNameAuthenticationService {
String authInitialize(String realName, String idNum);
String startCertify(String certifyId);
String queryCertifyResult(String certifyId);
String authentication(String realName, String idNum);
String authentication2(String realName, String idNum, String phoneNum);
}
@@ -0,0 +1,373 @@
package com.ruoyi.thirdparty.alipay.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.AlipayConstants;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.*;
import com.ruoyi.admin.service.TtBonusService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.common.constant.PayOrderStatus;
import com.ruoyi.domain.common.constant.PayType;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtPromotionLevel;
import com.ruoyi.domain.entity.TtRechargeProd;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.alipay.config.AliProperties;
import com.ruoyi.thirdparty.alipay.service.AliPayService;
import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Slf4j
@Service
public class AliPayServiceImpl implements AliPayService {
@Autowired
private TtRechargeProdMapper rechargeProdMapper;
@Autowired
private AliProperties aliProperties;
@Autowired
private TtOrderMapper orderMapper;
@Autowired
private TtUserBlendErcashMapper userBlendErcashMapper;
@Autowired
private TtBonusService bonusService;
@Autowired
private TtUserService userService;
@Autowired
private TtUserMapper userMapper;
@Autowired
private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService;
@Autowired
private TtRechargeProdMapper rechargeListMapper;
@Autowired
private ISysConfigService configService;
@Autowired
private TtPromotionLevelMapper ttPromotionLevelMapper;
@Autowired
private TtFirstRechargeMapper ttFirstRechargeMapper;
@Override
public R pay(CreateOrderParam param, TtUser ttUser, String ip) {
// 查询商品信息
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper)
.eq(TtRechargeProd::getId, param.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
if (ObjectUtil.isEmpty(goods)) {
return R.fail("不存在的商品。");
}
if (goods.getPrice().compareTo(param.getGoodsPrice()) != 0) {
return R.fail("商品价格不一致。");
}
// 计算总价值
BigDecimal totalAmount = param.getGoodsPrice().multiply(new BigDecimal(param.getGoodsNum()));
// 构建支付
String serverUrl = aliProperties.getServerUrl();
String appId = aliProperties.getMerchants().get(0).getAppId();
String privateKey = aliProperties.getMerchants().get(0).getPrivateKey();
String format = aliProperties.getFormat();
String charset = aliProperties.getCharset();
String alipayPublicKey = aliProperties.getMerchants().get(0).getAlipayPublicKey();
String signType = aliProperties.getSignType();
String notifyUrl = aliProperties.getNotifyUrl();
// 初始化SDK
AlipayClient alipayClient = new DefaultAlipayClient(serverUrl, appId, privateKey, format, charset, alipayPublicKey, signType);
// 构造请求参数以调用接口
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
// 设置通知地址
request.setNotifyUrl(notifyUrl);
// 设置订单标题
model.setSubject(goods.getName());
// 设置商户订单号
String orderId = String.valueOf(IdUtil.getSnowflake(1, 1).nextId());
model.setOutTradeNo(orderId);
// 设置订单总金额
model.setTotalAmount(totalAmount.toString());
request.setBizModel(model);
try {
AlipayTradePrecreateResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
// 创建订单
TtOrder order = new TtOrder();
order.setOrderId(orderId);
order.setOutTradeNo(response.getOutTradeNo());
order.setUserId(ttUser.getUserId());
order.setType(PayType.ZFB.getCode());
order.setGoodsId(param.getGoodsId());
order.setGoodsPrice(param.getGoodsPrice());
order.setGoodsNum(param.getGoodsNum());
order.setTotalAmount(totalAmount);
order.setStatus(PayOrderStatus.NO_PAY.getCode());
order.setCreateTime(new Date());
orderMapper.insert(order);
JSONObject jsonObject = JSON.parseObject(response.getBody());
return R.ok(jsonObject);
}
} catch (AlipayApiException e) {
e.printStackTrace();
}
return R.fail("支付失败");
}
@Override
public String callBack(Map<String, String> params) {
log.info("支付成功通知");
String result = "failure";
String alipayPublicKey = aliProperties.getMerchants().get(0).getAlipayPublicKey();
try {
// 异步通知校验
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayPublicKey,
AlipayConstants.CHARSET_UTF8,
AlipayConstants.SIGN_TYPE_RSA2);
if (!signVerified) {
log.error("支付成功,异步通知验签失败!");
return result;
}
// 进行二次校验
String tradeStatus = params.get("trade_status");
if (!"TRADE_SUCCESS".equals(tradeStatus)) {
log.error("交易失败");
return result;
}
// 处理业务
// 查询订单信息
String outTradeNo = params.get("out_trade_no");
TtOrder order = new LambdaQueryChainWrapper<>(orderMapper)
.eq(TtOrder::getOutTradeNo, outTradeNo)
.eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode())
.one();
// 防止重复通知
if (!PayOrderStatus.NO_PAY.getCode().equals(order.getStatus())) {
return result;
}
// 查询用户信息
TtUser user = new LambdaQueryChainWrapper<>(userMapper)
.eq(TtUser::getUserId, order.getUserId())
.eq(TtUser::getDelFlag, 0)
.one();
// 查询商品信息
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeListMapper)
.eq(TtRechargeProd::getId, order.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
// 账户结算
if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO);
if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO);
payNotifyAccounting(order, user, goods, order.getGoodsNum());
try {
// 首充赠送
firstChargeGiftAmount(user, goods, order.getGoodsNum());
// 推广等级充值赠送
promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum());
// 推广充值返佣
promotionChargeCommission(user, goods, order.getGoodsNum());
} catch (Exception e) {
e.printStackTrace();
}
// 更新订单
boolean update = new LambdaUpdateChainWrapper<>(orderMapper)
.eq(TtOrder::getId, order.getId())
.set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode())
.set(TtOrder::getUpdateTime, new Date())
.update();
if (update) {
result = "success";
}
// 发送充值成功通知
rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(order.getUserId().toString(), order.getGoodsPrice());
} catch (AlipayApiException e) {
e.printStackTrace();
}
return result;
}
// 账户结算
public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) {
// 加钱(充值不加积分:productA + productC 全部计入 account_amount)
// BigDecimal totalAmount = order.getTotalAmount();
BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber));
BigDecimal totalAmount = productAmount.add(productGift);
BigDecimal totalCredits = BigDecimal.ZERO;
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, user.getUserId())
.setSql("account_amount = account_amount + " + totalAmount.toString());
userService.update(userUpdate);
user = userService.getById(user.getUserId());
// 综合消费日志
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(user.getUserId())
.amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null)
.finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null)
.credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null)
.finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null)
.total(totalAmount.add(totalCredits)) // 收支合计
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.RECHARGE.getCode())
.remark(TtAccountRecordSource.RECHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
// 更新VIP等级
userService.updateUserVIPLevel(user.getUserId());
}
/**
* 首充赠送
*/
private void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
// 1.判断是否为首充
LambdaQueryWrapper<TtUserBlendErcash> wrapper = new LambdaQueryWrapper<>();
wrapper
.eq(TtUserBlendErcash::getSource, 1)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId());
List<TtUserBlendErcash> ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper);
if (ttUserBlendErcashes.size() > 0) {
return;
}
// 2.加钱
// 充值金额
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
// 查询赠送比例
BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount);
if (Objects.isNull(firstChargeAmountRatio)) {
return;
}
// 计算赠送金额
BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio);
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + giftAmount.toString());
userService.update(userUpdate);
// 3.记录
ttUser = userService.getById(ttUser.getUserId());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(ttUser.getUserId())
.amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null)
.finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null)
.total(giftAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.FIRST_CHARGE.getCode())
.remark(TtAccountRecordSource.FIRST_CHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
}
/**
* 推广等级充值赠送
*/
private void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
// 1.查询用户推广等级对应的赠送比例
TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel());
if (Objects.isNull(ttPromotionLevel))
return;
// 2.加钱
BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio();
if (Objects.isNull(amountRatio))
return;
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
BigDecimal giftAmount = totalAmount.multiply(amountRatio);
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + giftAmount.toString());
userService.update(userUpdate);
// 3.记录
ttUser = userService.getById(ttUser.getUserId());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(ttUser.getUserId())
.amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null)
.finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null)
.total(giftAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode())
.remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
}
/**
* 推广充值返佣
*/
private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
if (Objects.isNull(ttUser.getParentId())) {
return;
}
// 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结,
// 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount);
}
}
@@ -0,0 +1,225 @@
package com.ruoyi.thirdparty.alipay.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.request.AlipayUserCertifyOpenCertifyRequest;
import com.alipay.api.request.AlipayUserCertifyOpenInitializeRequest;
import com.alipay.api.request.AlipayUserCertifyOpenQueryRequest;
import com.alipay.api.response.AlipayUserCertifyOpenCertifyResponse;
import com.alipay.api.response.AlipayUserCertifyOpenInitializeResponse;
import com.alipay.api.response.AlipayUserCertifyOpenQueryResponse;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.thirdparty.alipay.config.AliProperties;
import com.ruoyi.thirdparty.alipay.service.RealNameAuthenticationService;
import com.ruoyi.thirdparty.apispace.config.ApiSpaceProperties;
import com.ruoyi.thirdparty.alipay.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Service
@Slf4j
@EnableConfigurationProperties(ApiSpaceProperties.class)
public class RealNameAuthenticationServiceImpl implements RealNameAuthenticationService {
@Qualifier("RealNameAuthentication")
private final AlipayClient RealNameAuthentication;
private final AliProperties aliProperties;
private final ApiSpaceProperties apiSpaceProperties;
@Autowired
private RestTemplate restTemplate;
public RealNameAuthenticationServiceImpl(AlipayClient realNameAuthentication,
AliProperties aliProperties,
ApiSpaceProperties apiSpaceProperties) {
RealNameAuthentication = realNameAuthentication;
this.aliProperties = aliProperties;
this.apiSpaceProperties = apiSpaceProperties;
}
@Override
public String authInitialize(String realName, String idNum) {
AlipayUserCertifyOpenInitializeRequest request = new AlipayUserCertifyOpenInitializeRequest();
JSONObject identityObj = new JSONObject();
identityObj.put("identity_type", "CERT_INFO");
identityObj.put("cert_type", "IDENTITY_CARD");
identityObj.put("cert_name", realName);
identityObj.put("cert_no", idNum);
JSONObject merchantConfigObj = new JSONObject();
try {
merchantConfigObj.put("return_url", aliProperties.getRealNameAuthenticationReturnUrl());
JSONObject bizContentObj = new JSONObject();
bizContentObj.put("outer_order_no", IdUtil.simpleUUID());
bizContentObj.put("biz_code", "FACE");
bizContentObj.put("identity_param", identityObj);
bizContentObj.put("merchant_config", merchantConfigObj);
request.setBizContent(bizContentObj.toString());
AlipayUserCertifyOpenInitializeResponse response = RealNameAuthentication.execute(request);
if (response.isSuccess()) {
return response.getCertifyId();
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public String startCertify(String certifyId) {
AlipayUserCertifyOpenCertifyRequest request = new AlipayUserCertifyOpenCertifyRequest();
JSONObject bizContentObj = new JSONObject();
bizContentObj.put("certify_id", certifyId);
request.setBizContent(bizContentObj.toString());
try {
AlipayUserCertifyOpenCertifyResponse response = RealNameAuthentication.pageExecute(request, "GET");
if (response.isSuccess()) {
return "alipays://platformapi/startapp?appId=20000067&url=" + URLEncoder.encode(response.getBody(), "UTF-8");
} else {
return null;
}
} catch (AlipayApiException | UnsupportedEncodingException e) {
return null;
}
}
@Override
public String queryCertifyResult(String certifyId) {
AlipayUserCertifyOpenQueryRequest request = new AlipayUserCertifyOpenQueryRequest();
JSONObject bizContentObj = new JSONObject();
bizContentObj.put("certify_id", certifyId);
request.setBizContent(bizContentObj.toString());
try {
AlipayUserCertifyOpenQueryResponse response = RealNameAuthentication.execute(request);
if (StrUtil.equals(response.getCode(), "40004")) {
return response.getSubMsg();
}
if (response.isSuccess()) {
String body = response.getBody();
log.info("返回的结构体:{}", body);
if (StringUtils.isNotBlank(body)) {
JSONObject jsonObject = JSONObject.parseObject(body);
JSONObject queryResponse = jsonObject.getJSONObject("alipay_user_certify_open_query_response");
if (StrUtil.equals(queryResponse.getString("code"), "10000") && StrUtil.equals(queryResponse.getString("passed"), "T")) {
return "";
} else return "认证失败,请重新认证!";
}
} else return "认证查询服务调用失败!";
} catch (AlipayApiException e) {
return "开始认证服务调用失败!";
}
return "认证失败,请重新认证!";
}
@Override
public String authentication(String realName, String idNum) {
log.info("realName is {}, idNum is {}", realName, idNum);
String method = "POST";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + aliProperties.getAuthenticationAppcode());
Map<String, String> querys = new HashMap<String, String>();
Map<String, String> bodys = new HashMap<String, String>();
bodys.put("idCardNo", idNum);
bodys.put("name", realName);
try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(aliProperties.getAuthenticationHost(), aliProperties.getAuthenticationPath(), method, headers, querys, bodys);
log.info("authentication response code is {}", response.getStatusLine().getStatusCode());
//获取response的body
String result = EntityUtils.toString(response.getEntity());
log.info("authentication result is {}", result);
JSONObject jsonObject = JSONObject.parseObject(result);
int code = jsonObject.getIntValue("code");
if (code == 400) {
return "身份证格式不正确!";
}
if (code != 200) {
return "认证失败,请重新认证!";
}
JSONObject data = jsonObject.getJSONObject("data");
int res = data.getIntValue("result");
if (res == 1) {
return "认证失败,请重新认证!";
}
} catch (Exception e) {
log.error("authentication is {}", e.getMessage(), e);
return "认证失败,请重新认证!";
}
return "";
}
@Override
public String authentication2(String realName, String idNum, String phoneNum) {
log.info("realName is {}, idNum is {}, phoneNum is {}", realName, idNum, phoneNum);
try {
HttpHeaders headers = new HttpHeaders();
headers.set("X-APISpace-Token", apiSpaceProperties.getCarriersAuthToken());
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("name", realName);
map.add("idNum", idNum);
map.add("mobile", phoneNum);
// 5. 组装请求实体
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
// 6. 发送 POST 请求并获取响应
ResponseEntity<String> response = restTemplate.postForEntity(apiSpaceProperties.getCarriersAuthUrl(),
request, String.class);
log.info("authentication response code is {}", response.getStatusCode());
log.info("authentication result is {}", response.getBody());
JSONObject jsonObject = JSONObject.parseObject(response.getBody());
if (jsonObject == null) {
return "认证失败,请重新认证!";
}
String code = jsonObject.getString("code");
if (!Objects.equals(code, "200000")) {
return "认证失败,请重新认证!";
}
JSONObject data = jsonObject.getJSONObject("data");
String res = data.getString("result");
if (!Objects.equals(res, "01")) {
return "认证失败,请重新认证!";
}
} catch (Exception e) {
log.error("authentication is {}", e.getMessage(), e);
return "认证失败,请重新认证!";
}
return "";
}
}
@@ -0,0 +1,311 @@
package com.ruoyi.thirdparty.alipay.utils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
Map<String, String> bodys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Put String
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Put stream
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (!StringUtils.isBlank(path)) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
sbQuery.append(query.getValue());
}
if (!StringUtils.isBlank(query.getKey())) {
sbQuery.append(query.getKey());
if (!StringUtils.isBlank(query.getValue())) {
sbQuery.append("=");
sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
private static HttpClient wrapClient(String host) {
HttpClient httpClient = new DefaultHttpClient();
if (host.startsWith("https://")) {
sslClient(httpClient);
}
return httpClient;
}
private static void sslClient(HttpClient httpClient) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] xcs, String str) {
}
public void checkServerTrusted(X509Certificate[] xcs, String str) {
}
};
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", 443, ssf));
} catch (KeyManagementException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}
@@ -0,0 +1,45 @@
package com.ruoyi.thirdparty.aliyunsms.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 阿里云短信服务配置
* 文档:https://help.aliyun.com/document_detail/101414.html
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "aliyun.sms")
public class AliyunSmsConfig {
/**
* 阿里云 AccessKey ID
*/
private String accessKeyId;
/**
* 阿里云 AccessKey Secret
*/
private String accessKeySecret;
/**
* 短信签名(已审核通过的签名名称)
*/
private String signName;
/**
* 短信模板 CODE(已审核通过的模板,且模板变量需为 ${code})
*/
private String templateCode;
/**
* 区域 ID,默认 cn-hangzhou
*/
private String regionId = "cn-hangzhou";
/**
* 服务端点 endpoint,默认 dysmsapi.aliyuncs.com
*/
private String endpoint = "dysmsapi.aliyuncs.com";
}
@@ -0,0 +1,30 @@
package com.ruoyi.thirdparty.aliyunsms.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.thirdparty.aliyunsms.service.AliyunSmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 阿里云短信测试接口(管理端使用,避免对外暴露)
*/
@RestController
@RequestMapping("/admin/aliyunsms")
public class AliyunSmsController extends BaseController {
@Autowired
private AliyunSmsService aliyunSmsService;
/**
* 测试发送验证码:使用配置中的默认签名+模板,模板变量为 ${code}
*/
@GetMapping("/sendVerifyCode")
public AjaxResult sendVerifyCode(@RequestParam String mobile, @RequestParam String code) {
String result = aliyunSmsService.sendVerifyCode(mobile, code);
return "0".equals(result) ? success("发送成功") : AjaxResult.error("发送失败:" + result);
}
}
@@ -0,0 +1,24 @@
package com.ruoyi.thirdparty.aliyunsms.service;
public interface AliyunSmsService {
/**
* 发送短信验证码(使用配置中的默认模板)
*
* @param mobile 手机号
* @param code 验证码
* @return "0" 表示成功;其它返回阿里云返回的 Code(如 isv.MOBILE_NUMBER_ILLEGAL)
*/
String sendVerifyCode(String mobile, String code);
/**
* 通用发送,可指定模板与模板参数 JSON
*
* @param mobile 手机号
* @param signName 签名(为空则使用配置默认)
* @param templateCode 模板 Code(为空则使用配置默认)
* @param templateParam 模板参数 JSON 字符串,例如 {"code":"1234"}
* @return "0" 表示成功;其它返回阿里云返回的 Code
*/
String sendSms(String mobile, String signName, String templateCode, String templateParam);
}
@@ -0,0 +1,86 @@
package com.ruoyi.thirdparty.aliyunsms.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.thirdparty.aliyunsms.config.AliyunSmsConfig;
import com.ruoyi.thirdparty.aliyunsms.service.AliyunSmsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 阿里云短信服务实现,基于 aliyun-java-sdk-core + dysmsapi 通用调用方式
*/
@Slf4j
@Service
public class AliyunSmsServiceImpl implements AliyunSmsService {
@Autowired
private AliyunSmsConfig aliyunSmsConfig;
@Override
public String sendVerifyCode(String mobile, String code) {
String templateParam = "{\"code\":\"" + code + "\"}";
return sendSms(mobile, aliyunSmsConfig.getSignName(), aliyunSmsConfig.getTemplateCode(), templateParam);
}
@Override
public String sendSms(String mobile, String signName, String templateCode, String templateParam) {
if (StringUtils.isEmpty(aliyunSmsConfig.getAccessKeyId()) || StringUtils.isEmpty(aliyunSmsConfig.getAccessKeySecret())) {
log.error("阿里云短信未配置 AccessKey,请检查 aliyun.sms 配置");
return "CONFIG_ERROR";
}
String useSign = StringUtils.isNotEmpty(signName) ? signName : aliyunSmsConfig.getSignName();
String useTemplate = StringUtils.isNotEmpty(templateCode) ? templateCode : aliyunSmsConfig.getTemplateCode();
try {
DefaultProfile profile = DefaultProfile.getProfile(
aliyunSmsConfig.getRegionId(),
aliyunSmsConfig.getAccessKeyId(),
aliyunSmsConfig.getAccessKeySecret()
);
IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain(aliyunSmsConfig.getEndpoint());
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", aliyunSmsConfig.getRegionId());
request.putQueryParameter("PhoneNumbers", mobile);
request.putQueryParameter("SignName", useSign);
request.putQueryParameter("TemplateCode", useTemplate);
if (StringUtils.isNotEmpty(templateParam)) {
request.putQueryParameter("TemplateParam", templateParam);
}
CommonResponse response = client.getCommonResponse(request);
String data = response.getData();
log.info("阿里云短信发送 mobile={}, sign={}, template={}, param={}, result={}",
mobile, useSign, useTemplate, templateParam, data);
if (StringUtils.isEmpty(data)) {
return "EMPTY_RESPONSE";
}
JSONObject json = JSONObject.parseObject(data);
String respCode = json.getString("Code");
if ("OK".equalsIgnoreCase(respCode)) {
return "0";
}
return respCode;
} catch (ClientException e) {
log.error("阿里云短信发送异常 mobile={}, errCode={}, errMsg={}", mobile, e.getErrCode(), e.getErrMsg(), e);
return e.getErrCode() == null ? "CLIENT_ERROR" : e.getErrCode();
} catch (Exception e) {
log.error("阿里云短信发送未知异常 mobile={}", mobile, e);
return "UNKNOWN_ERROR";
}
}
}
@@ -0,0 +1,20 @@
package com.ruoyi.thirdparty.apispace.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "apispace")
public class ApiSpaceProperties {
/**
* 运营商三要素实名认证 Token
*/
private String carriersAuthToken;
/**
* 运营商三要素实名认证 URL
*/
private String carriersAuthUrl;
}
@@ -0,0 +1,14 @@
package com.ruoyi.thirdparty.baidu.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "baidu")
public class BaiduConfig {
private String baseUrl;
private String token;
private String logidUrl;
}
@@ -0,0 +1,9 @@
package com.ruoyi.thirdparty.baidu.domain.body;
import lombok.Data;
@Data
public class BaiduConversionType {
private String logidUrl;
private Integer newType;
}
@@ -0,0 +1,11 @@
package com.ruoyi.thirdparty.baidu.domain.body;
import lombok.Data;
import java.util.List;
@Data
public class BaiduUploadConvertData {
private String token;
private List<BaiduConversionType> conversionTypes;
}
@@ -0,0 +1,6 @@
package com.ruoyi.thirdparty.baidu.service;
public interface BaiduService {
void upload(Integer userId, Integer newType, String bdVid);
}
@@ -0,0 +1,67 @@
package com.ruoyi.thirdparty.baidu.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.thirdparty.baidu.config.BaiduConfig;
import com.ruoyi.thirdparty.baidu.domain.body.BaiduConversionType;
import com.ruoyi.thirdparty.baidu.domain.body.BaiduUploadConvertData;
import com.ruoyi.thirdparty.baidu.service.BaiduService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Slf4j
@Service
public class BaiduServiceImpl implements BaiduService {
@Autowired
private BaiduConfig baiduConfig = new BaiduConfig();
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
//注册49
//购买10
@Override
public void upload(Integer userId, Integer newType, String bdVid) {
log.info("userId is {}, newType is {}, bdVid is {}", userId, newType, bdVid);
if (StringUtils.isBlank(bdVid) || newType == null || userId == null) {
return;
}
if (newType != 10 && newType != 49) {
return;
}
if (Objects.equals(10, newType)) {
LambdaQueryWrapper<TtUserBlendErcash> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(TtUserBlendErcash::getUserId, userId)
.eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode())
.eq(TtUserBlendErcash::getSource, TtAccountRecordSource.RECHARGE.getCode())
;
// 大于1非首次充值
List<TtUserBlendErcash> ercashes = ttUserBlendErcashMapper.selectList(queryWrapper);
if (ercashes.size() != 1) {
return;
}
}
// 构建请求头
Map<String, String> headers = new HashMap<>();
BaiduUploadConvertData data = new BaiduUploadConvertData();
data.setToken(baiduConfig.getToken());
BaiduConversionType type = new BaiduConversionType();
type.setNewType(newType);
type.setLogidUrl(baiduConfig.getLogidUrl() + bdVid);
data.setConversionTypes(Collections.singletonList(type));
String body = JSON.toJSONString(data);
String result = HttpUtils.sendPostJSONString(baiduConfig.getBaseUrl(), body, headers);
log.info("body is {}, result is {}", body, result);
}
}
@@ -0,0 +1,36 @@
package com.ruoyi.thirdparty.baiweipay.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "baiweipay")
public class BaiweiPayConfig {
/**
* 服务器URL
*/
private String serverUrl;
/**
* 商户ID
*/
private String pid;
/**
* 签名
*/
private String sign;
/**
* 通知URL
*/
private String notifyUrl;
/**
* 返回URL
*/
private String returnUrl;
}
@@ -0,0 +1,50 @@
package com.ruoyi.thirdparty.baiweipay.controller;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.UserPermission;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import com.ruoyi.thirdparty.baiweipay.service.BaiweiPayService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Api(tags = "百威支付")
@Slf4j
@RestController
@RequestMapping("api/baiweipay")
public class BaiweiPayController {
@Autowired
private TtUserService ttUserService;
@Autowired
private BaiweiPayService baiweiPayService;
@ApiOperation("支付")
@UserPermission
@PostMapping(value = "/pay")
public R pay(@RequestBody @Validated CreateOrderParam param) {
Long userId = SecurityUtils.getUserId();
TtUser ttUser = ttUserService.getById(userId);
// 是否实名认证(0未认证 1已认证)
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired();
}
String ip = IpUtils.getIpAddr();
return baiweiPayService.pay(param, ttUser, ip);
}
// @GetMapping("/notify")
public String notify(@RequestParam Map<String, String> params) {
return baiweiPayService.notify(params);
}
}
@@ -0,0 +1,19 @@
package com.ruoyi.thirdparty.baiweipay.domain;
import lombok.Data;
@Data
public class BaiweiPayResponse {
private String code;
private String msg;
private String tradeNo;
private String payurl;
private String qrcode;
private String urlscheme;
}
@@ -0,0 +1,14 @@
package com.ruoyi.thirdparty.baiweipay.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import java.util.Map;
public interface BaiweiPayService {
R pay(CreateOrderParam param, TtUser ttUser, String ip);
String notify(Map<String, String> params);
}
@@ -0,0 +1,403 @@
package com.ruoyi.thirdparty.baiweipay.service.impl;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.*;
import com.ruoyi.admin.service.TtBonusService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.domain.common.constant.PayOrderStatus;
import com.ruoyi.domain.common.constant.PayType;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtPromotionLevel;
import com.ruoyi.domain.entity.TtRechargeProd;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateOrderParam;
import com.ruoyi.domain.other.TtRechargeRecord;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.baiweipay.config.BaiweiPayConfig;
import com.ruoyi.thirdparty.baiweipay.service.BaiweiPayService;
import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService;
import com.ruoyi.thirdparty.baiweipay.domain.BaiweiPayResponse;
import com.ruoyi.thirdparty.baiweipay.util.BaiweiPayUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.*;
@Slf4j
@Service
public class BaiweiPayServiceImpl implements BaiweiPayService {
@Autowired
private TtRechargeProdMapper rechargeProdMapper;
@Autowired
private BaiweiPayConfig baiweiPayConfig;
@Autowired
private TtOrderMapper orderMapper;
@Autowired
private TtUserMapper userMapper;
@Autowired
private TtRechargeProdMapper rechargeListMapper;
@Autowired
private TtBonusService bonusService;
@Autowired
private TtUserService userService;
@Autowired
private TtUserBlendErcashMapper userBlendErcashMapper;
@Autowired
private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService;
@Autowired
private TtRechargeRecordMapper ttRechargeRecordMapper;
@Autowired
private ISysConfigService configService;
@Autowired
private TtPromotionLevelMapper ttPromotionLevelMapper;
@Autowired
private TtFirstRechargeMapper ttFirstRechargeMapper;
@Autowired
private RestTemplate restTemplate;
@Override
public R pay(CreateOrderParam param, TtUser ttUser, String ip) {
// 1.查询商品信息
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper)
.eq(TtRechargeProd::getId, param.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
if (ObjectUtil.isEmpty(goods)) {
return R.fail("不存在的商品");
}
if (goods.getPrice().compareTo(param.getGoodsPrice()) != 0) {
return R.fail("商品价格不一致");
}
// 计算总价值
BigDecimal totalAmount = param.getGoodsPrice().multiply(new BigDecimal(param.getGoodsNum()));
// 2.构建支付参数
Map<String, String> map = new HashMap<>();
map.put("pid", baiweiPayConfig.getPid());
map.put("type", "alipay");
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
String orderId = String.valueOf(snowflake.nextId());
map.put("out_trade_no", orderId);
map.put("notify_url", baiweiPayConfig.getNotifyUrl());
map.put("return_url", baiweiPayConfig.getReturnUrl());
map.put("name", goods.getName());
map.put("money", String.valueOf(totalAmount));
map.put("clientip", ip);
String key = baiweiPayConfig.getSign();
BaiweiPayUtil signUtil = new BaiweiPayUtil(key);
String sign = signUtil.getSign(map);
map.put("sign", sign);
map.put("sign_type", "MD5");
// 3.发送请求
// HttpRequest post = HttpUtil.createPost(baiweiPayConfig.getServerUrl());
// post.header("Content-Type","application/x-www-form-urlencoded");
// post.formStr(map);
// HttpResponse res = post.execute();
// 切换请求
// 获取服务器 URL
String url = baiweiPayConfig.getServerUrl();
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 构建表单数据
MultiValueMap<String, String> formParams = new LinkedMultiValueMap<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
formParams.add(entry.getKey(), entry.getValue());
}
// 构建请求实体
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formParams, headers);
// 发送 POST 请求
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
// 获取响应
String result = response.getBody();
// 4.解析响应
BaiweiPayResponse resBody = JSONUtil.toBean(result, BaiweiPayResponse.class);
if (!resBody.getCode().equals("1")) {
return R.fail(resBody.getMsg());
}
// 5.创建订单
TtOrder order = new TtOrder();
order.setOrderId(orderId);
order.setOutTradeNo(resBody.getTradeNo());
order.setUserId(ttUser.getUserId());
order.setType(PayType.GFHT_PAY.getCode());
order.setGoodsId(param.getGoodsId());
order.setGoodsPrice(param.getGoodsPrice());
order.setGoodsNum(param.getGoodsNum());
order.setTotalAmount(totalAmount);
order.setSign(sign);
order.setStatus(PayOrderStatus.NO_PAY.getCode());
order.setCreateTime(new Date());
orderMapper.insert(order);
return R.ok(resBody);
}
@Override
public String notify(Map<String, String> params) {
log.info("支付成功通知");
String result = "failure";
// 校验
String tradeStatus = params.get("trade_status");
if (!"TRADE_SUCCESS".equals(tradeStatus)) {
log.error("交易失败");
return result;
}
// 查询订单信息
String outTradeNo = params.get("trade_no");
TtOrder order = new LambdaQueryChainWrapper<>(orderMapper)
.eq(TtOrder::getOutTradeNo, outTradeNo)
.eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode())
.one();
// 防止重复通知
if (!PayOrderStatus.NO_PAY.getCode().equals(order.getStatus())) {
return result;
}
// 查询用户信息
TtUser user = new LambdaQueryChainWrapper<>(userMapper)
.eq(TtUser::getUserId, order.getUserId())
.eq(TtUser::getDelFlag, 0)
.one();
// 3.查询商品信息
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeListMapper)
.eq(TtRechargeProd::getId, order.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
// 4.账户结算
if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO);
if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO);
payNotifyAccounting(order, user, goods, order.getGoodsNum());
try {
// 首充赠送
firstChargeGiftAmount(user, goods, order.getGoodsNum());
// 推广等级充值赠送
promotionLevelChargeGiftAmount(user, goods, order.getGoodsNum());
// 推广充值返佣
promotionChargeCommission(user, goods, order.getGoodsNum());
} catch (Exception e) {
e.printStackTrace();
}
// 5.更新订单
boolean update = new LambdaUpdateChainWrapper<>(orderMapper)
.eq(TtOrder::getId, order.getId())
.set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode())
.set(TtOrder::getUpdateTime, new Date())
.update();
if (update) {
result = "success";
}
// 6.发送充值通知
rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(order.getUserId().toString(), order.getGoodsPrice());
return result;
}
// 账户结算
public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) {
// 加钱(充值不加积分:productA + productC 全部计入 account_amount)
// BigDecimal totalAmount = order.getTotalAmount();
BigDecimal productAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
BigDecimal productGift = goods.getProductC().multiply(new BigDecimal(goodsNumber));
BigDecimal totalAmount = productAmount.add(productGift);
BigDecimal totalCredits = BigDecimal.ZERO;
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, user.getUserId())
.setSql("account_amount = account_amount + " + totalAmount.toString());
userService.update(userUpdate);
user = userService.getById(user.getUserId());
// 综合消费日志
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(user.getUserId())
.amount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? totalAmount : null)
.finalAmount(totalAmount.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountAmount().add(totalAmount) : null)
.credits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? totalCredits : null)
.finalCredits(totalCredits.compareTo(BigDecimal.ZERO) > 0 ? user.getAccountCredits().add(totalCredits) : null)
.total(totalAmount.add(totalCredits)) // 收支合计
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.RECHARGE.getCode())
.remark(TtAccountRecordSource.RECHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
// 充值记录
TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build();
ttRechargeRecord.setUserId(order.getUserId());
ttRechargeRecord.setParentId(user.getParentId());
ttRechargeRecord.setArrivalAmount(totalAmount);
ttRechargeRecord.setAmountActuallyPaid(totalAmount);
ttRechargeRecord.setFinallyPrice(user.getAccountAmount());
ttRechargeRecord.setOrderId(order.getOrderId());
ttRechargeRecord.setOutTradeNo(order.getOutTradeNo());
ttRechargeRecord.setStatus("0");
ttRechargeRecord.setChannelType("1");
ttRechargeRecord.setCreateTime(DateUtils.getNowDate());
ttRechargeRecordMapper.insert(ttRechargeRecord);
// 更新VIP等级
userService.updateUserVIPLevel(user.getUserId());
}
/**
* 首充赠送
*/
private void firstChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
// 1.判断是否为首充
LambdaQueryWrapper<TtUserBlendErcash> wrapper = new LambdaQueryWrapper<>();
wrapper
.eq(TtUserBlendErcash::getSource, 1)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId());
List<TtUserBlendErcash> ttUserBlendErcashes = userBlendErcashMapper.selectList(wrapper);
if (ttUserBlendErcashes.size() > 0) {
return;
}
// 2.加钱
// 充值金额
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
// 查询赠送比例
BigDecimal firstChargeAmountRatio = ttFirstRechargeMapper.selectRatioByMinAmount(totalAmount);
if (Objects.isNull(firstChargeAmountRatio)) {
return;
}
// 计算赠送金额
BigDecimal giftAmount = totalAmount.multiply(firstChargeAmountRatio);
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + giftAmount.toString());
userService.update(userUpdate);
// 3.记录
ttUser = userService.getById(ttUser.getUserId());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(ttUser.getUserId())
.amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null)
.finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null)
.total(giftAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.FIRST_CHARGE.getCode())
.remark(TtAccountRecordSource.FIRST_CHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
}
/**
* 推广等级充值赠送
*/
@Async
public void promotionLevelChargeGiftAmount(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
// 1.查询用户推广等级对应的赠送比例
TtPromotionLevel ttPromotionLevel = ttPromotionLevelMapper.selectById(ttUser.getPromotionLevel());
if (Objects.isNull(ttPromotionLevel))
return;
// 2.加钱
BigDecimal amountRatio = ttPromotionLevel.getRechargeGiftRatio();
if (Objects.isNull(amountRatio))
return;
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
BigDecimal giftAmount = totalAmount.multiply(amountRatio);
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + giftAmount.toString());
userService.update(userUpdate);
// 3.记录
ttUser = userService.getById(ttUser.getUserId());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(ttUser.getUserId())
.amount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? giftAmount : null)
.finalAmount(giftAmount.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount().add(giftAmount) : null)
.total(giftAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getCode())
.remark(TtAccountRecordSource.PROMOTION_LEVEL_CHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
}
/**
* 推广充值返佣
*/
private void promotionChargeCommission(TtUser ttUser, TtRechargeProd goods, Integer goodsNumber) {
if (Objects.isNull(ttUser.getParentId())) {
return;
}
// 充值返佣统一走 TtBonusService:按各级上级 commissionRate 逐级向上实时返佣、招商周结,
// 写 source=13,与卡密充值(ApiRechargeServiceImpl→bonus)口径一致,避免使用推广等级费率造成口径不符。
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
bonusService.distributeRechargeCommission(ttUser.getUserId(), totalAmount);
}
}
@@ -0,0 +1,76 @@
package com.ruoyi.thirdparty.baiweipay.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.TreeMap;
public class BaiweiPayUtil {
private String key;
public BaiweiPayUtil(String key) {
this.key = key;
}
/**
* 生成MD5签名
*
* @param params 参数列表
* @return 生成的签名
*/
public String getSign(Map<String, String> params) {
// 排序参数
Map<String, String> sortedParams = new TreeMap<>(params);
sortedParams.remove("sign");
sortedParams.remove("sign_type");
// 构建签名字符串
StringBuilder signStr = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
signStr.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
if (signStr.length() > 0) {
signStr.setLength(signStr.length() - 1); // 移除最后一个&
}
// 添加商户密钥
signStr.append(this.key);
// 计算MD5签名
return md5(signStr.toString()).toLowerCase();
}
/**
* 计算MD5哈希值
*
* @param input 输入字符串
* @return MD5哈希值
*/
private static String md5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());
return bytesToHex(messageDigest);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not found", e);
}
}
/**
* 将字节数组转换为十六进制字符串
*
* @param bytes 字节数组
* @return 十六进制字符串
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
@@ -0,0 +1,30 @@
package com.ruoyi.thirdparty.chengduhipay.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 成都Hipay充值配置,对应application.yml中chengdu-hipay节点
*/
@Data
@ConfigurationProperties(prefix = "chengdu-hipay")
public class ChengduHipayProperties {
/** API基础地址,如 https://xahnob7yqke3.hipay.one */
private String baseUrl;
/** 商户编号,由API平台提供 */
private String recvId;
/** API密钥,由API平台提供 */
private String apiKey;
/** 充值通道编号,由API平台提供 */
private String payChannelId;
/** 支付回调通知地址 */
private String notifyUrl;
/** 前端支付完成跳转地址 */
private String returnUrl;
}
@@ -0,0 +1,139 @@
package com.ruoyi.thirdparty.chengduhipay.controller;
import cn.hutool.json.JSONUtil;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.UserPermission;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.thirdparty.chengduhipay.model.PaymentData;
import com.ruoyi.thirdparty.chengduhipay.service.ChengduHipayPaymentService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.security.MessageDigest;
import java.util.Map;
/**
* 成都Hipay充值接口,对应API文档V1.0.4
* 接口清单:
* POST /api/chengduhipay/pay —— 创建充值订单
* POST /api/chengduhipay/notify —— 接收支付回调通知(Hipay平台调用)
* GET /api/chengduhipay/queryPay —— 查询充值订单
* GET /api/chengduhipay/balance —— 查询商户余额
*/
@Api(tags = "成都Hipay充值")
@Slf4j
@RestController
@RequestMapping("/api/chengduhipay")
public class ChengduHipayController {
@Autowired
private TtUserService ttUserService;
@Autowired
private ChengduHipayPaymentService chengduHipayPaymentService;
/**
* 创建充值订单
* 对应API文档2.1节:POST /pay/pay/createpay
*/
@ApiOperation("创建充值订单")
@UserPermission
// @PostMapping("/pay")
public R pay(@RequestBody @Validated PayRequest param) {
Long userId = SecurityUtils.getUserId();
TtUser ttUser = ttUserService.getById(userId);
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired("未实名认证");
}
String ip = IpUtils.getIpAddr();
return chengduHipayPaymentService.pay(userId, param);
}
/**
* 充值回调通知
* 由Hipay平台在用户支付成功后调用,对应API文档2.2节
* 通用返回格式: {"code":1, "msg":"success", "data":"{...}"}
* data字段为JSON字符串,需要反序列化
*/
@ApiOperation("充值回调通知")
@Anonymous
// @PostMapping("/notify")
public String notify(@RequestBody Map<String, String> params) {
log.info("ChengduHipay notify params: {}", JSONUtil.toJsonStr(params));
chengduHipayPaymentService.notifyCallback(params);
return "success";
}
/**
* 查询本地订单支付状态(供前端调起支付后轮询使用)
* 前端轮询逻辑建议:每 2~3 秒调一次,最多 60 秒,遇到 success=true 即弹出“充值成功”并刷新余额。
* @param param 请求体,包含创建充值接口返回的本地订单号 orderId
*/
@ApiOperation("查询本地订单支付状态")
@UserPermission
@PostMapping("/orderStatus")
public R<Map<String, Object>> orderStatus(@RequestBody @Validated OrderStatusRequest param) {
Long userId = SecurityUtils.getUserId();
return chengduHipayPaymentService.queryOrderStatus(userId, param.getOrderId());
}
/**
* 查询充值订单
* 对应API文档2.3节:GET /pay/pay/getpay
* @param Id Hipay平台返回的订单唯一编号
*/
@ApiOperation("查询充值订单")
@GetMapping("/queryPay")
public R<PaymentData> queryPay(@RequestParam String Id) {
return chengduHipayPaymentService.getPay(Id);
}
/**
* 查询商户余额
* 对应API文档4节:GET /pay/pay/getbalance
*/
@ApiOperation("查询商户余额")
@GetMapping("/balance")
public R<String> balance() {
return chengduHipayPaymentService.getBalance();
}
public static void main(String[] args) throws Exception {
// 常量定义
final String RECV_ID = "e7c0f86eff4296c636b8d290300efd29";
final String API_KEY = "922297c5e1dc44c78e8b4afac7315b16";
final String ORDER_ID = "2060184782830178304";
final String AMOUNT = "100.00";
// 1. 计算 sign = md5("pay" + recvid + orderid + amount + apikey)
String signStr = "pay" + RECV_ID + ORDER_ID + AMOUNT + API_KEY;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] signDigest = md.digest(signStr.getBytes("UTF-8"));
StringBuilder signSb = new StringBuilder();
for (byte b : signDigest) {
signSb.append(String.format("%02x", b & 0xff));
}
String sign = signSb.toString();
// 2. 计算 resign = md5(sign + apikey)
String resignStr = sign + API_KEY;
byte[] resignDigest = md.digest(resignStr.getBytes("UTF-8"));
StringBuilder resignSb = new StringBuilder();
for (byte b : resignDigest) {
resignSb.append(String.format("%02x", b & 0xff));
}
String resign = resignSb.toString();
// 输出结果
System.out.println("sign = " + sign);
System.out.println("resign = " + resign);
}
}
@@ -0,0 +1,15 @@
package com.ruoyi.thirdparty.chengduhipay.controller;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 查询本地订单支付状态请求参数
*/
@Data
public class OrderStatusRequest {
@NotBlank(message = "订单号不能为空")
private String orderId;
}
@@ -0,0 +1,19 @@
package com.ruoyi.thirdparty.chengduhipay.controller;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 用户充值请求参数
*/
@Data
public class PayRequest {
@NotNull(message = "商品id不能为空")
private Integer goodsId;
/**
* 支付方式:1=支付宝,2=银联
*/
private Integer payType;
}
@@ -0,0 +1,18 @@
package com.ruoyi.thirdparty.chengduhipay.controller;
import lombok.Data;
/**
* 充值响应返回给前端的数据
*/
@Data
public class PayResponse {
/** 二维码地址 */
private String qrUrl;
/** H5支付页面地址 */
private String payUrl;
/** 平台订单号 */
private String orderId;
/** Hipay平台订单唯一编号,用于查询接口 */
private String platformOrderId;
}
@@ -0,0 +1,18 @@
package com.ruoyi.thirdparty.chengduhipay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单类型,对应API文档1.2.2节
*/
@AllArgsConstructor
@Getter
public enum OrderTypeEnum {
RECHARGE(1, "充值订单"),
WITHDRAW(3, "代付订单"),
;
private final int code;
private final String desc;
}
@@ -0,0 +1,20 @@
package com.ruoyi.thirdparty.chengduhipay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单状态,对应API文档1.2.3节
*/
@AllArgsConstructor
@Getter
public enum StateEnum {
CREATE(2, "已创建"),
EXCHANGE(5, "已转币"),
CANCEL(9, "已取消"),
ERROR(99, "错误"),
;
private final int code;
private final String desc;
}
@@ -0,0 +1,16 @@
package com.ruoyi.thirdparty.chengduhipay.model;
import lombok.Data;
/**
* 创建充值订单API响应,对应API文档通用返回对象+2.1.5节
*/
@Data
public class CreatePayResult {
/** 返回代码,1=成功,非1=错误 */
private int code;
/** 返回消息 */
private String msg;
/** 业务数据 */
private PaymentData data;
}
@@ -0,0 +1,44 @@
package com.ruoyi.thirdparty.chengduhipay.model;
import lombok.Data;
import java.math.BigDecimal;
/**
* 充值回调通知数据,对应API文档2.2.3节返回参数
*/
@Data
public class NotifyData {
/** 平台唯一标识ID */
private String id;
/** 商户订单号 */
private String orderid;
/** 订单类型:1=充值 3=代付 */
private int ordertype;
/** 订单状态:5=已转币才算支付成功 */
private int state;
/** 收币方编号 */
private String recvid;
/** 币数量 */
private BigDecimal amount;
/** 收币手续费 */
private BigDecimal recvcharge;
/** 创建日期 */
private String createtime;
/** 收币方昵称 */
private String recvnickname;
/** 收币方头像 */
private String recvheader;
/** 转币日期 */
private String transtime;
/** 前台跳转地址 */
private String returnurl;
/** 说明 */
private String note;
/** 系统备注 */
private String remark;
/** 签名 */
private String sign;
/** 验签字段 resign=md5(sign+apikey) */
private String resign;
}
@@ -0,0 +1,54 @@
package com.ruoyi.thirdparty.chengduhipay.model;
import lombok.Data;
import java.math.BigDecimal;
/**
* 充值订单数据详情,对应API文档2.1.4节返回参数
*/
@Data
public class PaymentData {
/** 平台唯一标识ID */
private String id;
/** 商户订单号 */
private String orderid;
/** 订单类型:1=充值 3=代付 */
private int ordertype;
/** 订单状态:2=已创建 5=已转币 9=已取消 99=错误 */
private int state;
/** 收币方编号 */
private String recvid;
/** 币数量 */
private BigDecimal amount;
/** 收币手续费 */
private BigDecimal recvcharge;
/** 创建日期 yyyy-MM-dd HH:mm:ss */
private String createtime;
/** 收币方昵称 */
private String recvnickname;
/** 收币方头像 */
private String recvheader;
/** 转币日期 yyyy-MM-dd HH:mm:ss */
private String transtime;
/** 二维码地址 */
private String qrurl;
/** 回调地址 */
private String notifyurl;
/** 二维码内容 */
private String qrcode;
/** 前台跳转地址 */
private String returnurl;
/** 说明(请求原样返回) */
private String note;
/** 系统备注 */
private String remark;
/** 验签字段 resign=md5(sign+apikey),创建订单时为空 */
private String resign;
/** 签名(请求签名原样返回) */
private String sign;
/** H5支付页面地址 */
private String payurl;
/** 收款银行信息JSON字符串(仅银行卡通道有数据) */
private String recvinfo;
}
@@ -0,0 +1,525 @@
package com.ruoyi.thirdparty.chengduhipay.service;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.TtOrderMapper;
import com.ruoyi.admin.mapper.TtRechargeProdMapper;
import com.ruoyi.admin.mapper.TtRechargeRecordMapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MoneyUtil;
import com.ruoyi.domain.common.constant.PayOrderStatus;
import com.ruoyi.domain.common.constant.PayType;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtRechargeProd;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtRechargeRecord;
import com.ruoyi.thirdparty.chengduhipay.config.ChengduHipayProperties;
import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest;
import com.ruoyi.thirdparty.chengduhipay.controller.PayResponse;
import com.ruoyi.thirdparty.chengduhipay.enums.StateEnum;
import com.ruoyi.thirdparty.chengduhipay.model.CreatePayResult;
import com.ruoyi.thirdparty.chengduhipay.model.NotifyData;
import com.ruoyi.thirdparty.chengduhipay.model.PaymentData;
import com.ruoyi.thirdparty.common.service.PayFrequencyGuard;
import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.DigestUtils;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.util.*;
/**
* 成都Hipay充值服务,严格按照API文档V1.0.4实现
* 实现接口:
* 2.1 创建充值接口 —— POST /pay/pay/createpay
* 2.2 充值回调接口 —— 验签+账户结算
* 2.3 查询充值单接口 —— GET /pay/pay/getpay
* 4 查询余额接口 —— GET /pay/pay/getbalance
*/
@Slf4j
@Service
@EnableConfigurationProperties(ChengduHipayProperties.class)
public class ChengduHipayPaymentService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private ChengduHipayProperties properties;
@Autowired
private TtOrderMapper orderMapper;
@Autowired
private TtRechargeProdMapper rechargeProdMapper;
@Autowired
private TtUserMapper userMapper;
@Autowired
private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService;
@Autowired
private TtRechargeRecordMapper ttRechargeRecordMapper;
@Autowired
private TtUserService userService;
@Autowired
private TtUserBlendErcashMapper userBlendErcashMapper;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private PayFrequencyGuard payFrequencyGuard;
// ========================================================================
// 渠道选择:根据支付方式 + 充值金额,选择手续费最低的渠道
// ========================================================================
/**
* 按费率从低到高返回支付渠道优先列表,当前金额覆盖的排前面,未覆盖的排后面作为降级。
*
* 渠道清单:
* ─── 支付宝(payType=1)───
* 637 支付宝超大 2000~10000 4.8%
* 612 支付宝大额 800~10000 5.5%
* 621 支付宝中额 500~5000 5.8%
* 610 支付宝小额 300~2000 6.0%
* 616 超级支付宝 100~10000 12%
* ─── 银联(payType=2)───
* 601 数字人民币 100~5000 0.7%
* 653 银联扫码 30~500 1.0%
*/
private List<String> selectPayChannelPriority(Integer payType, BigDecimal amount) {
int amt = amount.intValue();
// 1=支付宝
if (payType != null && payType == 1) {
List<String> preferred = new ArrayList<>();
List<String> fallback = new ArrayList<>();
if (amt >= 2000 && amt <= 10000) preferred.add("637"); else fallback.add("637");
if (amt >= 800 && amt <= 10000) preferred.add("612"); else fallback.add("612");
if (amt >= 500 && amt <= 5000) preferred.add("621"); else fallback.add("621");
if (amt >= 300 && amt <= 2000) preferred.add("610"); else fallback.add("610");
if (amt >= 100 && amt <= 10000) preferred.add("616"); else fallback.add("616");
preferred.addAll(fallback);
return preferred;
}
// 2=银联
if (payType != null && payType == 2) {
List<String> preferred = new ArrayList<>();
List<String> fallback = new ArrayList<>();
if (amt >= 100 && amt <= 5000) preferred.add("601"); else fallback.add("601");
if (amt >= 30 && amt <= 500) preferred.add("653"); else fallback.add("653");
preferred.addAll(fallback);
return preferred;
}
// 未指定 payType,回退到配置默认渠道
return List.of(properties.getPayChannelId());
}
// ========================================================================
// 2.1 创建充值接口
// ========================================================================
/**
* 创建充值订单,对应API文档2.1节
*
* @param uid 用户ID
* @param request 充值请求(商品ID、数量、支付通道)
* @return 包含二维码地址/H5支付页面的响应
*/
public R<PayResponse> pay(Long uid, PayRequest request) {
// 1. 查询商品信息
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper)
.eq(TtRechargeProd::getId, request.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
if (ObjectUtil.isEmpty(goods)) {
return R.fail("不存在的商品");
}
// 2. 生成订单号(雪花ID)
Snowflake snowflake = IdUtil.getSnowflake(1, 1);
String orderId = String.valueOf(snowflake.nextId());
// 3. 计算金额(一次只充一个商品,数量固定为 1)
BigDecimal totalAmount = goods.getPrice();
String amount = MoneyUtil.toStr(totalAmount);
// 4. MD5签名: sign=md5("pay"+recvid+orderid+amount+apikey)
String signText = "pay" + properties.getRecvId() + orderId + amount + properties.getApiKey();
String sign = DigestUtils.md5DigestAsHex(signText.getBytes(StandardCharsets.UTF_8));
// 5. 按费率从低到高依次尝试渠道,失败则降级到下一渠道
List<String> channelPriority = selectPayChannelPriority(request.getPayType(), totalAmount);
log.info("ChengduHipay payType={}, amount={}, channelPriority={}", request.getPayType(), amount, channelPriority);
CreatePayResult resBody = null;
String usedChannel = null;
for (String payChannelId : channelPriority) {
log.info("ChengduHipay 尝试渠道: {}", payChannelId);
// 6. 组装请求参数
Map<String, Object> data = new HashMap<>();
data.put("recvid", properties.getRecvId());
data.put("orderid", orderId);
data.put("amount", amount);
data.put("sign", sign);
data.put("paychannelid", payChannelId);
data.put("memuid", DigestUtils.md5DigestAsHex((uid + "").getBytes(StandardCharsets.UTF_8)));
data.put("notifyurl", properties.getNotifyUrl());
data.put("returnurl", properties.getReturnUrl());
data.put("note", "充值" + amount);
log.info("ChengduHipay createPay request channel={}: {}", payChannelId, JSONUtil.toJsonStr(data));
// 7. 调用Hipay创建充值接口
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(data, headers);
String createPayUrl = properties.getBaseUrl() + "/pay/pay/createpay";
ResponseEntity<String> response = restTemplate.exchange(createPayUrl, HttpMethod.POST, entity, String.class);
String result = response.getBody();
log.info("ChengduHipay createPay response channel={}: {}", payChannelId, result);
// 8. 解析响应
resBody = JSONUtil.toBean(result, CreatePayResult.class);
if (resBody.getCode() == 1) {
usedChannel = payChannelId;
break;
}
log.warn("渠道 {} 失败 code={} msg={},降级尝试下一渠道", payChannelId, resBody.getCode(), resBody.getMsg());
}
if (usedChannel == null || resBody == null || resBody.getCode() != 1) {
String errMsg = resBody != null ? resBody.getMsg() : "所有渠道均不可用";
log.error("创建充值订单失败,所有渠道均已尝试,最后msg={}", errMsg);
return R.fail(errMsg);
}
log.info("ChengduHipay 最终使用渠道: {}", usedChannel);
// 9. 保存订单到数据库
TtOrder order = new TtOrder();
order.setOrderId(orderId);
order.setOutTradeNo(resBody.getData().getOrderid());
order.setUserId(Math.toIntExact(uid));
order.setType(PayType.CHENGDU_HI_PAY.getCode());
order.setGoodsId(request.getGoodsId());
order.setGoodsPrice(goods.getPrice());
order.setGoodsNum(1);
order.setTotalAmount(totalAmount);
order.setSign(sign);
order.setStatus(PayOrderStatus.NO_PAY.getCode());
order.setCreateTime(new Date());
orderMapper.insert(order);
// 10. 返回支付链接
PayResponse resp = new PayResponse();
resp.setQrUrl(resBody.getData().getQrurl());
resp.setPayUrl(resBody.getData().getPayurl());
resp.setOrderId(orderId);
resp.setPlatformOrderId(resBody.getData().getId());
return R.ok(resp);
}
// ========================================================================
// 2.2 充值回调接口
// ========================================================================
/**
* 处理Hipay平台的充值回调通知,对应API文档2.2节
* 验签方式:resign == md5(sign + apikey)
* 成功标准:code==1 且 state==5(已转币)
*/
public R<Void> notifyCallback(Map<String, String> params) {
log.info("ChengduHipay notify callback: {}", JSONUtil.toJsonStr(params));
// 1. 检查返回码
String code = params.get("code");
if (!"1".equals(code)) {
log.error("回调返回失败,code={}, msg={}", code, params.get("msg"));
return R.fail("支付失败");
}
// 2. 解析data字段(data为JSON字符串,需要反序列化)
String dataStr = params.get("data");
if (ObjectUtil.isEmpty(dataStr)) {
log.error("回调data字段为空");
return R.fail("回调数据为空");
}
NotifyData resBody = JSONUtil.toBean(dataStr, NotifyData.class);
// 3. 订单状态判断:必须为5(已转币)才算支付成功
if (StateEnum.EXCHANGE.getCode() != resBody.getState()) {
log.error("交易状态非已转币,state={}", resBody.getState());
return R.fail("交易未完成");
}
// 4. 验签:resign == md5(sign + apikey)
String sign = resBody.getSign();
String resign = resBody.getResign();
String expectedResign = DigestUtils.md5DigestAsHex(
(sign + properties.getApiKey()).getBytes(StandardCharsets.UTF_8));
if (!expectedResign.equals(resign)) {
log.error("回调验签失败,expected={}, actual={}", expectedResign, resign);
return R.fail("签名验证失败");
}
// 5. 查询本地订单(按平台订单号查询,不带状态,用于取 id 做 CAS)
String outTradeNo = resBody.getOrderid();
TtOrder order = new LambdaQueryChainWrapper<>(orderMapper)
.eq(TtOrder::getOutTradeNo, outTradeNo)
.eq(TtOrder::getType, PayType.CHENGDU_HI_PAY.getCode())
.one();
if (order == null) {
log.error("订单不存在,outTradeNo={}", outTradeNo);
return R.fail("订单不存在");
}
// 6. DB条件CAS + 事务:NO_PAY->PAY_COMPLE 抢占成功者才结算,防成功回调重复推送双倍加币
Boolean credited = new TransactionTemplate(transactionManager).execute(txStatus -> {
LambdaUpdateWrapper<TtOrder> uw = new LambdaUpdateWrapper<>();
uw.eq(TtOrder::getId, order.getId())
.eq(TtOrder::getStatus, PayOrderStatus.NO_PAY.getCode())
.set(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode())
.set(TtOrder::getUpdateTime, new Date());
int rows = orderMapper.update(null, uw);
if (rows <= 0) {
return Boolean.FALSE;
}
TtUser user = new LambdaQueryChainWrapper<>(userMapper)
.eq(TtUser::getUserId, order.getUserId())
.eq(TtUser::getDelFlag, 0)
.one();
if (user == null) {
throw new IllegalStateException("ChengduHipay 回调用户不存在, userId=" + order.getUserId());
}
TtRechargeProd goods = new LambdaQueryChainWrapper<>(rechargeProdMapper)
.eq(TtRechargeProd::getId, order.getGoodsId())
.eq(TtRechargeProd::getStatus, 0)
.one();
if (goods == null) {
throw new IllegalStateException("ChengduHipay 回调商品不存在, goodsId=" + order.getGoodsId());
}
if (ObjectUtil.isNull(goods.getProductA())) goods.setProductA(BigDecimal.ZERO);
if (ObjectUtil.isNull(goods.getProductC())) goods.setProductC(BigDecimal.ZERO);
payNotifyAccounting(order, user, goods, order.getGoodsNum());
return Boolean.TRUE;
});
// 7. 发送充值成功通知(幂等:已处理则跳过)
if (Boolean.TRUE.equals(credited)) {
// 支付成功:清空该用户的未支付次数缓存
payFrequencyGuard.clearOnPaid(order.getUserId().longValue());
rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(
order.getUserId().toString(), order.getGoodsPrice());
log.info("ChengduHipay回调处理成功,orderId={}", order.getOrderId());
} else {
log.info("ChengduHipay回调幂等跳过(订单已处理),orderId={}", order.getOrderId());
}
return R.ok();
}
// ========================================================================
// 2.2.1 查询本地订单支付状态(供前端轮询使用)
// ========================================================================
/**
* 查询本地订单的支付状态,供前端在创建充值订单后轮询使用。
* 反映回调是否已落库结算完成。
* 使用场景:前端拿到支付二维码/H5链接后,每隔几秒轮询一次本接口,
* 状态映射:
* 0 支付中(NO_PAY)
* 4 充值成功(PAY_COMPLE)
* -1 / 2 / 3 充值失败(PAY_FAIL / CANCEL / CALL_BACK_ERRO)
*
* @param userId 当前登录用户ID
* @param orderId 本地订单号(创建充值时返回的 orderId)
*/
public R<Map<String, Object>> queryOrderStatus(Long userId, String orderId) {
if (ObjectUtil.isEmpty(orderId)) {
return R.fail("订单号不能为空");
}
TtOrder order = new LambdaQueryChainWrapper<>(orderMapper)
.eq(TtOrder::getOrderId, orderId)
.one();
if (order == null) {
return R.fail("订单不存在");
}
// 越权防护:仅允许查询自己的订单
if (!Integer.valueOf(Math.toIntExact(userId)).equals(order.getUserId())) {
return R.fail("无权查询该订单");
}
String status = order.getStatus();
String message;
if (PayOrderStatus.PAY_COMPLE.getCode().equals(status)) {
message = "充值成功";
} else if (PayOrderStatus.NO_PAY.getCode().equals(status)) {
message = "支付中";
} else if (PayOrderStatus.CANCEL.getCode().equals(status)) {
message = "订单已取消";
} else if (PayOrderStatus.CALL_BACK_ERRO.getCode().equals(status)) {
message = "回调处理异常";
} else {
message = "支付失败";
}
Map<String, Object> data = new HashMap<>();
data.put("status", status);
data.put("amount", order.getGoodsPrice());
data.put("message", message);
return R.ok(data);
}
// ========================================================================
// 2.3 查询充值单接口
// ========================================================================
/**
* 查询充值订单,对应API文档2.3节
* GET /pay/pay/getpay?id={平台订单唯一编号}
* @param Id Hipay平台返回的订单唯一编号
*/
public R<PaymentData> getPay(String Id) {
String url = properties.getBaseUrl() + "/pay/pay/getpay?id=" + Id;
log.info("ChengduHipay getPay request: {}", url);
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
String result = response.getBody();
log.info("ChengduHipay getPay response: {}", result);
CreatePayResult resBody = JSONUtil.toBean(result, CreatePayResult.class);
if (resBody.getCode() != 1) {
return R.fail(resBody.getMsg());
}
return R.ok(resBody.getData());
}
// ========================================================================
// 4 查询余额接口
// ========================================================================
/**
* 查询商户余额,对应API文档4节
* GET /pay/pay/getbalance?id={商户编号}&pass=md5(apikey)
* 返回格式: {"code":1,"data":"{\"balance\":168.8880}","msg":"success"}
*/
public R<String> getBalance() {
String pass = DigestUtils.md5DigestAsHex(
properties.getApiKey().getBytes(StandardCharsets.UTF_8));
String url = properties.getBaseUrl() + "/pay/pay/getbalance?id="
+ properties.getRecvId() + "&pass=" + pass;
log.info("ChengduHipay getBalance request: {}", url);
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
String result = response.getBody();
log.info("ChengduHipay getBalance response: {}", result);
// 余额接口data为JSON字符串: {"code":1,"data":"{\"balance\":168.8880}","msg":"success"}
JSONObject json = JSONUtil.parseObj(result);
if (json.getInt("code") != 1) {
return R.fail(json.getStr("msg"));
}
// data字段是JSON字符串,需要二次解析
String dataStr = json.getStr("data");
if (ObjectUtil.isEmpty(dataStr)) {
return R.ok(null);
}
JSONObject dataJson = JSONUtil.parseObj(dataStr);
return R.ok(dataJson.getStr("balance"));
}
// ========================================================================
// 账户结算(与现有hipay逻辑保持一致)
// ========================================================================
/**
* 支付成功后的账户结算:加余额、记录流水、更新VIP
*/
public void payNotifyAccounting(TtOrder order, TtUser user, TtRechargeProd goods, Integer goodsNumber) {
BigDecimal totalAmount = goods.getProductA().multiply(new BigDecimal(goodsNumber));
BigDecimal oldAmount = user.getAccountAmount();
if (oldAmount == null) {
oldAmount = BigDecimal.ZERO;
}
BigDecimal newAmount = oldAmount.add(totalAmount);
// 更新用户余额
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, user.getUserId())
.setSql("account_amount = account_amount + " + totalAmount);
userService.update(userUpdate);
// 综合消费日志
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(user.getUserId())
.amount(totalAmount)
.finalAmount(newAmount)
.total(totalAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.RECHARGE.getCode())
.remark(TtAccountRecordSource.RECHARGE.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
userBlendErcashMapper.insert(blendErcash);
// 充值记录
TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build();
ttRechargeRecord.setUserId(order.getUserId());
ttRechargeRecord.setParentId(user.getParentId());
ttRechargeRecord.setArrivalAmount(totalAmount);
ttRechargeRecord.setAmountActuallyPaid(totalAmount);
ttRechargeRecord.setFinallyPrice(newAmount);
ttRechargeRecord.setOrderId(order.getOrderId());
ttRechargeRecord.setOutTradeNo(order.getOutTradeNo());
ttRechargeRecord.setStatus("0");
ttRechargeRecord.setChannelType("1");
ttRechargeRecord.setCreateTime(DateUtils.getNowDate());
ttRechargeRecordMapper.insert(ttRechargeRecord);
// 更新VIP等级
userService.updateUserVIPLevel(user.getUserId());
}
}
@@ -0,0 +1,34 @@
package com.ruoyi.thirdparty.common.NetworkDataLoader;
import cn.hutool.core.util.HashUtil;
import com.ruoyi.common.core.domain.entity.SysDictData;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Arrays;
import java.util.List;
@Data
public abstract class BaseNetworkDataLoader {
public String name;
protected List<SysDictData> allEnum;
// 根据类型和标签获取code
protected String getCodeByNameOrHash(List<SysDictData> typeList, String nameOrHash){
for (SysDictData type : typeList){
if (type.getDictLabel().equals(nameOrHash)) return type.getDictValue();
if (type.getDictType().equals(nameOrHash)) return type.getDictValue();
}
return null;
}
protected Long createOrnamentId(String MarketHashName){
Long id = Long.valueOf(HashUtil.rsHash(MarketHashName));
if (id < 0) id = Math.abs(id);
return id;
}
}
@@ -0,0 +1,12 @@
package com.ruoyi.thirdparty.common.NetworkDataLoader;
import com.ruoyi.domain.entity.TtOrnament;
import java.util.List;
import java.util.Set;
public interface NetworkDataLoader {
Set<TtOrnament> load();
}
@@ -0,0 +1,157 @@
package com.ruoyi.thirdparty.common.NetworkDataLoader;
import cn.hutool.core.util.HashUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.domain.dto.zbt.OrnamentZBT;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.thirdparty.zbt.param.ProductListParams;
import com.ruoyi.thirdparty.zbt.param.SearchParams;
import com.ruoyi.thirdparty.zbt.result.ResultZbt;
import com.ruoyi.thirdparty.zbt.result.product.AvailableMarket;
import com.ruoyi.thirdparty.zbt.result.product.AvailableMarketList;
import com.ruoyi.thirdparty.zbt.result.product.SearchData;
import com.ruoyi.thirdparty.zbt.service.ZBTService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@AllArgsConstructor
// @Data
public class ZBTNetworkDataLoader extends BaseNetworkDataLoader implements NetworkDataLoader{
private ZBTService zbtService;
@Override
public Set<TtOrnament> load() {
Set<TtOrnament> res = new HashSet<>();
for (SysDictData type : allEnum){
// 根据指定的饰品类型加载数据
if (!type.getDictType().equals("ornaments_type")) continue;
log.info("正在加载{}类型的饰品资源~",type.getDictLabel());
Set<TtOrnament> data = ZBTLoadOrnamentByType(type);
res.addAll(data);
}
return res;
}
private Set<TtOrnament> ZBTLoadOrnamentByType(SysDictData type){
HashSet<TtOrnament> result = new HashSet<>();
// 分批请求api
int page = 1;
boolean stopFlag = false;
while (true) {
// 结束标记
if (stopFlag) break;
//1 请求平台数据
SearchParams param = SearchParams.builder()
.page(String.valueOf(page))
.category(type.getDictLabel())
.limit("200")
.build();
ResultZbt<SearchData> response = zbtService.search(param);
// 解析平台数据
SearchData data = response.getData();
if (!response.getSuccess() || ObjectUtil.isEmpty(data)) break;
List<OrnamentZBT> list = JSONUtil.toList(JSONUtil.toJsonStr(data.getList()), OrnamentZBT.class);
if (list.size() < 200) stopFlag = !stopFlag;
// 转换为通用类型
Set<TtOrnament> collect = list.stream().map(item -> {
// 生成本平台id
Long id = createOrnamentId(item.getMarketHashName());
TtOrnament ornament = TtOrnament.builder()
.id(id)
.zbtId(item.getItemId())
.marketHashName(item.getMarketHashName())
.name(item.getItemName())
.shortName(item.getShortName())
// 价格信息
.price(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getPrice() : null)
.quantity(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getQuantity() : null)
.imageUrl(item.getImageUrl())
.type(type.getDictValue())
.typeName(item.getTypeName())
.typeHashName(item.getType())
.quality(getCodeByNameOrHash(allEnum,item.getQuality()))
.qualityName(item.getQualityName())
.qualityHashName(item.getQuality())
.qualityColor(item.getQualityColor())
.rarity(getCodeByNameOrHash(allEnum,item.getRarity()))
.rarityName(item.getRarityName())
.rarityHashName(item.getRarity())
.rarityColor(item.getRarityColor())
.exterior(getCodeByNameOrHash(allEnum,item.getExterior()))
.exteriorHashName(item.getExterior())
.exteriorName(item.getExteriorName())
.updateTime(new Date())
.build();
// 补充价格信息
AvailableMarket sellInfo = apiSellInfo(String.valueOf(ornament.getZbtId()));
if (ObjectUtil.isNotEmpty(sellInfo)) {
ornament.setUsePrice(sellInfo.getCnyPrice());
}
return ornament;
}).collect(Collectors.toSet());
result.addAll(collect);
// log.info("加载【扎比特】{}条类型为{}的饰品数据。",page*collect.size(),typeHash);
System.out.print("\r加载【扎比特】"+page*collect.size()+"条类型为"+type.getDictLabel()+"的饰品数据。请耐心等待");
page++;
}
return result;
}
// 查询价格信息
private AvailableMarket apiSellInfo(String itemId){
ProductListParams p = ProductListParams.builder()
.itemId(itemId)
.page("1")
.limit("1")
.language("zh_CN")
.build();
ResultZbt<AvailableMarketList> zbtData = zbtService.productList(p);
if (!zbtData.getSuccess()) return null;
List<AvailableMarket> list = zbtData.getData().getList();
if (list.isEmpty()) return null;
return list.get(0);
}
}
@@ -0,0 +1,35 @@
package com.ruoyi.thirdparty.common.config;
import com.rabbitmq.client.Channel;
import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig;
import com.ruoyi.thirdparty.common.service.DeliverGoodsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Slf4j
public class DeliveryRabbitConsumer {
private final DeliverGoodsService deliverGoodsService;
public DeliveryRabbitConsumer(DeliverGoodsService deliverGoodsService) {
this.deliverGoodsService = deliverGoodsService;
}
// 自动提货业务
@RabbitListener(queues = DelayedQueueConfig.DELIVERY_QUEUE)
public void deliveryReceive(Message message, Channel channel) throws IOException {
Integer userId = Integer.valueOf(new String(message.getBody()));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
try {
deliverGoodsService.autoDelivery(userId);
} catch (Exception e) {
log.error(e.getMessage());
channel.basicRecover(false);
}
}
}
@@ -0,0 +1,51 @@
package com.ruoyi.thirdparty.common.config;
// 网络资源加载器配置
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.system.mapper.SysDictDataMapper;
import com.ruoyi.thirdparty.common.NetworkDataLoader.NetworkDataLoader;
import com.ruoyi.thirdparty.common.NetworkDataLoader.ZBTNetworkDataLoader;
import com.ruoyi.thirdparty.zbt.service.ZBTService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Configuration
public class NetworkDataLoaderConfig {
@Autowired
private ZBTService zbtService;
@Autowired
private SysDictDataMapper dictDataMapper;
@Bean
public List<NetworkDataLoader> networkDataLoaderList(){
// 加载必要的枚举数据
List<String> dictTypes = Arrays.asList(
"ornaments_type", "ornaments_type_name",
"ornaments_exterior", "ornaments_exterior_name",
"ornaments_quality", "ornaments_quality_name",
"ornaments_rarity", "ornaments_rarity_name");
List<SysDictData> enumList = dictDataMapper.selectDictDataByTypes(dictTypes);
ArrayList<NetworkDataLoader> networkDataLoaders = new ArrayList<>();
// zbt加载器
ZBTNetworkDataLoader zbtNetworkDataLoader = new ZBTNetworkDataLoader(zbtService);
zbtNetworkDataLoader.setAllEnum(enumList);
zbtNetworkDataLoader.setName("扎比特加载器");
networkDataLoaders.add(zbtNetworkDataLoader);
return networkDataLoaders;
}
}
@@ -0,0 +1,31 @@
package com.ruoyi.thirdparty.common.controller;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.thirdparty.common.service.ApiCommonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Api(tags = "第三方公共接口")
@RestController
@RequestMapping("/api/thirdparty/common")
public class ApiCommonController {
private final ApiCommonService commonService;
public ApiCommonController(ApiCommonService commonService) {
this.commonService = commonService;
}
@ApiOperation("生成二维码")
@PostMapping("/skipLinkToQRCode")
public R<Object> skipLinkToQRCode(HttpServletResponse response, @RequestParam("contents") String contents) throws IOException {
return commonService.writerPayImage(response, contents);
}
}
@@ -0,0 +1,84 @@
package com.ruoyi.thirdparty.common.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.admin.service.TtOrderService;
import com.ruoyi.admin.service.TtRechargeConfigService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.common.constant.PartyType;
import com.ruoyi.domain.common.constant.PayOrderStatus;
import com.ruoyi.domain.dto.recharge.RechargeConfigOrderVo;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtRechargeConfig;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Api(tags = "充值配置接口")
@RestController
@RequestMapping("/api/recharge/config")
public class ApiRechargeConfigController extends BaseController {
@Autowired
private TtRechargeConfigService ttRechargeConfigService;
@Autowired
private TtOrderService ttOrderService;
@ApiOperation("充值结果列表")
@PostMapping("/order")
public R<List<RechargeConfigOrderVo>> order() {
startPage();
LambdaQueryWrapper<TtOrder> wrapper = Wrappers.lambdaQuery();
wrapper.eq(TtOrder::getStatus, PayOrderStatus.PAY_COMPLE.getCode());
wrapper.eq(TtOrder::getUserId, getUserId());
wrapper.eq(TtOrder::getThirdParty, PartyType.INNER_PICTURE_PAY.getCode());
wrapper.orderByDesc(TtOrder::getCreateTime);
List<TtOrder> list = ttOrderService.list(wrapper);
if (CollectionUtils.isEmpty(list)) {
return R.ok(new ArrayList<>());
}
List<RechargeConfigOrderVo> vos = new ArrayList<>();
List<Integer> goodsIds = new ArrayList<>();
for (TtOrder ttOrder : list) {
RechargeConfigOrderVo vo = new RechargeConfigOrderVo();
BeanUtils.copyProperties(ttOrder, vo);
goodsIds.add(ttOrder.getGoodsId());
vos.add(vo);
}
LambdaQueryWrapper<TtRechargeConfig> wrapper1 = Wrappers.lambdaQuery();
wrapper1.in(TtRechargeConfig::getId, goodsIds);
List<TtRechargeConfig> configs = ttRechargeConfigService.list(wrapper1);
Map<Integer, TtRechargeConfig> configMap = configs.stream().collect(Collectors.toMap(TtRechargeConfig::getId, Function.identity()));
for (RechargeConfigOrderVo vo : vos) {
if (configMap.get(vo.getGoodsId()) != null) {
vo.setPicture(configMap.get(vo.getGoodsId()).getPicture());
}
}
return R.ok(vos);
}
@ApiOperation("充值列表")
@PostMapping("/list")
public R<List<TtRechargeConfig>> list() {
startPage();
LambdaQueryWrapper<TtRechargeConfig> wrapper = Wrappers.lambdaQuery();
wrapper.eq(TtRechargeConfig::getStatus, "0");
List<TtRechargeConfig> list = ttRechargeConfigService.list(wrapper);
return R.ok(list);
}
}
@@ -0,0 +1,246 @@
package com.ruoyi.thirdparty.common.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.admin.config.RedisConstants;
import com.ruoyi.admin.service.TtRechargeProdService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.annotation.UserPermission;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import cn.hutool.crypto.digest.DigestUtil;
import com.ruoyi.common.annotation.RateLimiter;
import com.ruoyi.common.enums.LimitType;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.entity.TtRechargeProd;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.chengduhipay.controller.PayRequest;
import com.ruoyi.thirdparty.chengduhipay.service.ChengduHipayPaymentService;
import com.ruoyi.thirdparty.common.model.RechargeCardPayRequest;
import com.ruoyi.thirdparty.richku.service.RichKuPaymentService;
import com.ruoyi.thirdparty.common.model.UnifiedPayRequest;
import com.ruoyi.thirdparty.common.service.ApiRechargeService;
import com.ruoyi.thirdparty.common.service.PayCompanyRouter;
import com.ruoyi.thirdparty.common.service.PayFrequencyGuard;
import com.ruoyi.thirdparty.abkpay.service.AbkPayPaymentService;
import com.ruoyi.thirdparty.itpay.service.ItPayPaymentService;
import com.ruoyi.thirdparty.snappay.service.SnapPayPaymentService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Api(tags = "充值")
@RestController
@RequestMapping("/api/recharge")
public class ApiRechargeController extends BaseController {
private final ISysConfigService sysConfigService;
private final TtUserService userService;
private final ApiRechargeService apiRechargeService;
private final TtRechargeProdService rechargeListService;
private final PayCompanyRouter payCompanyRouter;
private final ChengduHipayPaymentService chengduHipayPaymentService;
private final RichKuPaymentService richKuPaymentService;
private final ItPayPaymentService itPayPaymentService;
private final AbkPayPaymentService abkPayPaymentService;
private final SnapPayPaymentService snapPayPaymentService;
private final PayFrequencyGuard payFrequencyGuard;
private final RedisLock redisLock;
public ApiRechargeController(ISysConfigService sysConfigService,
TtUserService userService,
ApiRechargeService apiRechargeService,
TtRechargeProdService rechargeListService,
PayCompanyRouter payCompanyRouter,
ChengduHipayPaymentService chengduHipayPaymentService,
RichKuPaymentService richKuPaymentService,
ItPayPaymentService itPayPaymentService,
AbkPayPaymentService abkPayPaymentService,
SnapPayPaymentService snapPayPaymentService,
PayFrequencyGuard payFrequencyGuard,
RedisLock redisLock) {
this.redisLock = redisLock;
this.sysConfigService = sysConfigService;
this.userService = userService;
this.apiRechargeService = apiRechargeService;
this.rechargeListService = rechargeListService;
this.payCompanyRouter = payCompanyRouter;
this.chengduHipayPaymentService = chengduHipayPaymentService;
this.richKuPaymentService = richKuPaymentService;
this.itPayPaymentService = itPayPaymentService;
this.abkPayPaymentService = abkPayPaymentService;
this.snapPayPaymentService = snapPayPaymentService;
this.payFrequencyGuard = payFrequencyGuard;
}
@ApiOperation("卡支付")
@UpdateUserCache
@RateLimiter(time = 60, count = 30, limitType = LimitType.IP)
@PostMapping("/cardPay")
public R<Boolean> cardPay(@RequestBody RechargeCardPayRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
// 检查卡密充值维护开关
String cardRechargeMaintenance = sysConfigService.selectConfigByKey("cardRechargeMaintenance");
if ("1".equals(cardRechargeMaintenance)) {
return R.fail("卡密充值维护中......");
}
TtUser ttUser = userService.getById(getUserId());
// 是否实名认证(0未认证 1已认证)
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired();
}
// 控制层防重锁:按卡密内容(password)串行,锁包裹整个 @Transactional 事务,解锁晚于事务提交,
// 避免弱网重复提交导致同一卡密重复充值(锁粒度=单卡密,不影响其他卡密并发)
String lockKey = RedisConstants.CARD_PAY_LOCK + DigestUtil.sha256Hex(request.getPassword());
boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS);
if (!locked) {
return R.fail(false, "充值进行中,请勿重复操作");
}
try {
String msg = apiRechargeService.cardPay(request.getPassword(), ttUser);
if (StringUtils.isEmpty(msg)) {
return R.ok(true, "充値成功!");
} else {
return R.fail(false, msg);
}
} finally {
redisLock.unlock(lockKey);
}
}
@ApiOperation("充值列表")
// @UpdateUserCache
@Anonymous
@PostMapping("/list")
public R<List<Map<String, Object>>> list() {
startPage();
LambdaQueryWrapper<TtRechargeProd> wrapper = Wrappers.lambdaQuery();
wrapper.eq(TtRechargeProd::getStatus, "0");
List<TtRechargeProd> list = rechargeListService.list(wrapper);
List<Map<String, Object>> result = list.stream().map(prod -> {
Map<String, Object> map = new HashMap<>();
map.put("id", prod.getId());
map.put("name", prod.getName());
map.put("price", prod.getPrice());
map.put("status", prod.getStatus());
map.put("cardLink", prod.getCardLink());
map.put("picture", prod.getPicture());
map.put("transactionLink", prod.getTransactionLink());
// productA/productC 统一转字符串,拼接展示标签:有赠送时显示 "500+50 赠送",否则显示金币数
BigDecimal productA = prod.getProductA() != null ? prod.getProductA() : BigDecimal.ZERO;
BigDecimal productC = prod.getProductC() != null ? prod.getProductC() : BigDecimal.ZERO;
String productAStr = productA.stripTrailingZeros().toPlainString();
String productCStr = productC.stripTrailingZeros().toPlainString();
map.put("productA", productAStr);
map.put("productC", productCStr);
if (productC.compareTo(BigDecimal.ZERO) > 0) {
map.put("label", productAStr + "+" + productCStr + " 赠送");
} else {
map.put("label", productAStr);
}
return map;
}).collect(Collectors.toList());
return R.ok(result);
}
@ApiOperation("统一充值下单(按权重分配支付公司)")
@UserPermission
@PostMapping("/unifiedPay")
public R unifiedPay(@RequestBody UnifiedPayRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
// 检查充值渠道维护开关
String rechargeMaintenance = sysConfigService.selectConfigByKey("rechargeMaintenance");
if ("1".equals(rechargeMaintenance)) {
return R.fail("充值渠道维护中......");
}
Long userId = getUserId();
TtUser ttUser = userService.getById(userId);
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired("未实名认证");
}
// 频控:封禁期内直接拒绝(渠道无关,按 uid 统一生效)
if (payFrequencyGuard.isBanned(userId)) {
return R.fail(705,"频繁拉单未支付,请五分钟后重试");
}
// 路由逻辑:金额<300 且 itpay 已启用(权重>0) 默认走 itpay;>=300 按权重选择(如 abkpay,其内部再按金额选产品编码:300~800用8012,>=800用8003)
// 说明:把某渠道权重设为 0 即视为停用,不再强制路由到它;itpay 需保持权重>0,否则 <300 规则失效回落到权重选择
String company;
TtRechargeProd goods = rechargeListService.getById(request.getGoodsId());
if (goods == null) {
return R.fail("充值商品不存在");
}
if (snapPayPaymentService.isRoutable(goods.getPrice())) {
// 金额位于 SnapPay 限额区间(默认 1000~50000)且已启用:强制走 SnapPay;
// 若 SnapPay 停用(sys_config snappayEnabled=0 或未配置 merchantId),则回落到下方规则
company = "snappay";
} else if (goods.getPrice().compareTo(new BigDecimal("300")) < 0
&& payCompanyRouter.isEnabled("itpay")) {
company = "itpay";
} else {
company = payCompanyRouter.select(request.getPayType());
}
// 转换为对应公司的请求格式
PayRequest payReq = new PayRequest();
payReq.setGoodsId(request.getGoodsId());
payReq.setPayType(request.getPayType());
R result = dispatchPay(company, userId, payReq);
// SnapPay 走不了(下单失败/通道异常)时,自动按权重回落到其他渠道(如 abkpay)再试一次;
// 注:snappay 不在权重池内,回落不会再选中自己,不会死循环
if ("snappay".equals(company) && !R.isSuccess(result)) {
String fallback = payCompanyRouter.select(request.getPayType());
log.warn("SnapPay 下单失败,按权重回落到 {}, 原因: {}", fallback, result.getMsg());
company = fallback;
result = dispatchPay(fallback, userId, payReq);
}
// 下单成功后累计"拉单未支付"次数(支付成功回调会清零)
if (R.isSuccess(result)) {
payFrequencyGuard.recordUnpaidPull(userId);
return result;
}
// 统一下单失败错误码 706(涵盖所有支付渠道,具体失败原因见各渠道日志)
log.error("统一充值下单失败, company={}, msg={}", company, result.getMsg());
return R.fail(706, "创建订单失败");
}
/**
* 按支付公司名称分发下单请求
*/
private R dispatchPay(String company, Long userId, PayRequest payReq) {
return switch (company) {
case "chengduhipay" -> chengduHipayPaymentService.pay(userId, payReq);
case "richku" -> richKuPaymentService.pay(userId, payReq, null);
case "itpay" -> itPayPaymentService.pay(userId, payReq);
case "abkpay" -> abkPayPaymentService.pay(userId, payReq);
case "snappay" -> snapPayPaymentService.pay(userId, payReq);
default -> {
log.error("未知支付公司: {}", company);
yield R.fail("支付通道暂不可用");
}
};
}
}
@@ -0,0 +1,93 @@
package com.ruoyi.thirdparty.common.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.common.model.ApiSmsBody;
import com.ruoyi.thirdparty.common.model.ApiSmsNoSliderBody;
import com.ruoyi.thirdparty.common.service.ApiSmsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "短信")
@RestController
@RequestMapping("/api/sms")
public class ApiSmsController extends BaseController {
private final ISysConfigService sysConfigService;
private final ApiSmsService apiSmsService;
@Autowired
private RedisCache redisCache;
public ApiSmsController(ISysConfigService sysConfigService,
ApiSmsService apiSmsService) {
this.sysConfigService = sysConfigService;
this.apiSmsService = apiSmsService;
}
@ApiOperation("获取短信验证码")
@Anonymous
@PostMapping("/getVerifyCode")
public R<Object> getVerifyCode(@RequestBody ApiSmsBody smsBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String phoneNumber = smsBody.getPhoneNumber();
String captchaId = smsBody.getCaptchaId();
// 校验滑块验证码
if (StringUtils.isEmpty(captchaId)) {
return R.fail("请先完成滑块验证");
}
String verifiedKey = "captcha:verified:" + phoneNumber;
String storedCaptchaId = redisCache.getCacheObject(verifiedKey);
if (StringUtils.isEmpty(storedCaptchaId) || !storedCaptchaId.equals(captchaId)) {
return R.fail("滑块验证已过期或无效,请重新验证");
}
String msg = apiSmsService.getVerifyCode(smsBody);
if (StringUtils.isNotEmpty(msg)) {
// 短信发送失败(次数/余额不足),返回特定错误码
if (msg.contains("次数") || msg.contains("余额") || msg.contains("不足")) {
return R.fail(999, "短信发送次数不足");
}
return R.fail(msg);
}
return R.ok("获取验证码成功,请注意短信通知!");
}
@ApiOperation("获取短信验证码(免滑块)")
@Anonymous
@PostMapping("/getVerifyCodeNoSlider")
public R<Object> getVerifyCodeNoSlider(@RequestBody ApiSmsNoSliderBody body) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
// 不校验滑块,直接发送验证码;适配 service 层入参不含 captchaId
ApiSmsBody smsBody = new ApiSmsBody();
smsBody.setPhoneNumber(body.getPhoneNumber());
smsBody.setType(body.getType());
String msg = apiSmsService.getVerifyCode(smsBody);
if (StringUtils.isNotEmpty(msg)) {
if (msg.contains("次数") || msg.contains("余额") || msg.contains("不足")) {
return R.fail(999, "短信发送次数不足");
}
return R.fail(msg);
}
return R.ok("获取验证码成功,请注意短信通知!");
}
}
@@ -0,0 +1,82 @@
package com.ruoyi.thirdparty.common.controller;
import com.ruoyi.domain.dto.deliver.TradeBuyParam;
import com.ruoyi.domain.dto.deliver.TradeManualConfirmParam;
import com.ruoyi.domain.vo.AvailableMarketOrnamentVO;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.thirdparty.common.service.DeliverGoodsService;
import com.ruoyi.thirdparty.zbt.param.ProductListParams;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "管理端发货管理")
@RestController
@RequestMapping("/admin/deliverGoods")
public class DeliverGoodsController extends BaseController {
private final DeliverGoodsService deliverGoodsService;
public DeliverGoodsController(DeliverGoodsService deliverGoodsService) {
this.deliverGoodsService = deliverGoodsService;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class GetAvailableMarketListParam{
// private List<String> hashNameList;
private Integer partyType;
private List<Long> ornamentsId;
}
// 获取饰品在各个平台的在售信息
@ApiOperation("获取饰品在各个平台的在售信息")
@PostMapping("/getAvailableMarketList")
public R getAvailableMarketList(@RequestBody GetAvailableMarketListParam param) {
return deliverGoodsService.getAvailableMarketList(param);
}
// 根据饰品hashName获取所有平台的在售列表(弃用接口)
@ApiOperation("根据饰品hashName获取所有平台的在售列表(弃用接口)")
@GetMapping("/getAvailableMarketListByHashName/{marketHashName}")
public R<List<AvailableMarketOrnamentVO>> getAvailableMarketListByHashName(@PathVariable("marketHashName") String marketHashName,
ProductListParams productListParams) {
List<AvailableMarketOrnamentVO> list = deliverGoodsService.getAvailableMarketListByHashName(marketHashName, productListParams);
if (StringUtils.isNull(list)) return R.fail("获取饰品所有在售列表异常!");
return R.ok(list);
}
@ApiOperation("购买发货")
@PostMapping("/tradeBuy")
public R tradeBuy(@RequestBody TradeBuyParam param) {
return deliverGoodsService.tradeBuy(param);
}
@ApiOperation("手动确认发货")
@PostMapping("/tradeConfirm")
public R tradeConfirm(@RequestBody TradeManualConfirmParam param) {
return deliverGoodsService.tradeConfirm(param);
}
@ApiOperation("同步状态")
@GetMapping("/synchronousStatus")
public R<Boolean> synchronousStatus(String outTradeNo) {
String msg = deliverGoodsService.synchronousStatus(outTradeNo);
return StringUtils.isEmpty(msg) ? R.ok(true, "同步成功!") : R.fail(false, msg);
}
@ApiOperation("一键同步状态")
@GetMapping("/syncAllStatus")
public R syncAllStatus() {
int row = deliverGoodsService.syncAllStatus();
return row > 0 ? R.ok() : R.fail("一键同步失败");
}
}
@@ -0,0 +1,15 @@
package com.ruoyi.thirdparty.common.model;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class ApiSmsBody {
private String phoneNumber;
@ApiModelProperty("1注册 2登录 3更换手机号 4找回密码 5实名认证")
private String type;
@ApiModelProperty("滑块验证码ID")
private String captchaId;
@ApiModelProperty("上级邀请码(推广后台登录:手机号未注册时用它校验是否可自动注册为主播)")
private String parentInvitationCode;
}
@@ -0,0 +1,16 @@
package com.ruoyi.thirdparty.common.model;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 免滑块短信验证码请求体(不含 captchaId)
*/
@Data
public class ApiSmsNoSliderBody {
@ApiModelProperty("手机号")
private String phoneNumber;
@ApiModelProperty("1注册 2登录 3更换手机号 4找回密码 5实名认证")
private String type;
}
@@ -0,0 +1,9 @@
package com.ruoyi.thirdparty.common.model;
import lombok.Data;
@Data
public class RechargeCardPayRequest {
private String password; // 卡密
}
@@ -0,0 +1,20 @@
package com.ruoyi.thirdparty.common.model;
import io.swagger.annotations.ApiModelProperty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 统一充值请求,由后端按权重分配支付公司
*/
@Data
public class UnifiedPayRequest {
@NotNull(message = "商品id不能为空")
private Integer goodsId;
@ApiModelProperty("支付方式:1=支付宝,2=银联")
private Integer payType;
@ApiModelProperty("1:原有支付,2:图片支付")
private Integer payWay = 1;
}
@@ -0,0 +1,73 @@
package com.ruoyi.thirdparty.common.scheduled;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.admin.service.TtOrnamentService;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.thirdparty.common.NetworkDataLoader.BaseNetworkDataLoader;
import com.ruoyi.thirdparty.common.NetworkDataLoader.NetworkDataLoader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
@Configuration // 1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class ThirdPartyTask {
@Autowired
private TtOrnamentService ttOrnamentService;
@Value("${mkcsgo.startLoadOrnaments}")
private Boolean startLoadOrnaments;
// 资源加载器集合
@Autowired
private List<NetworkDataLoader> networkDataLoaderList;
// 定时加载发货平台的物品数据
//@Scheduled(cron = "0 50 23 * * ?")
// @Scheduled(cron = "0 6 17 * * ?")
public void networkDataLoad(){
log.info("是否加载网络ornaments资源{}", startLoadOrnaments);
if (!startLoadOrnaments) return;
// 1 加载各个平台数据
Set<TtOrnament> ttOrnaments = new HashSet<>();
for (NetworkDataLoader loader : networkDataLoaderList) {
log.info("【{}】开始加载网络资源~",((BaseNetworkDataLoader)loader).name);
Set<TtOrnament> load = loader.load();
ttOrnaments.addAll(load);
}
if (ObjectUtil.isEmpty(ttOrnaments) || ttOrnaments.isEmpty()) {
log.warn("加载饰品网络资源失败。");
return;
}
// 2 写db
log.info("db allData ing~~~");
for (TtOrnament item : ttOrnaments) {
try {
ttOrnamentService.saveOrUpdate(item);
} catch (DuplicateKeyException e) {
// log.info("过滤重复数据 【id】{}【hash】{}save。",item.getId(),item.getMarketHashName());
} catch (Exception e) {
e.printStackTrace();
log.info("保存数据异常。");
break;
}
}
log.info("加载饰品网络资源成功。");
}
}
@@ -0,0 +1,11 @@
package com.ruoyi.thirdparty.common.service;
import com.ruoyi.common.core.domain.R;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface ApiCommonService {
R<Object> writerPayImage(HttpServletResponse response, String contents) throws IOException;
}
@@ -0,0 +1,8 @@
package com.ruoyi.thirdparty.common.service;
import com.ruoyi.domain.entity.sys.TtUser;
public interface ApiRechargeService {
String cardPay(String password, TtUser ttUser);
}
@@ -0,0 +1,10 @@
package com.ruoyi.thirdparty.common.service;
import com.ruoyi.thirdparty.common.model.ApiSmsBody;
public interface ApiSmsService {
String getVerifyCode(ApiSmsBody smsBody);
String validateCaptcha(String code, String verifyKey);
}
@@ -0,0 +1,30 @@
package com.ruoyi.thirdparty.common.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.deliver.TradeBuyParam;
import com.ruoyi.domain.dto.deliver.TradeManualConfirmParam;
import com.ruoyi.domain.vo.AvailableMarketOrnamentVO;
import com.ruoyi.thirdparty.common.controller.DeliverGoodsController.GetAvailableMarketListParam;
import com.ruoyi.thirdparty.zbt.param.ProductListParams;
import java.util.List;
public interface DeliverGoodsService {
R getAvailableMarketList(Integer ornamentsId, Integer partyType);
R tradeBuy(TradeBuyParam param);
String synchronousStatus(String outTradeNo);
int syncAllStatus();
void autoDelivery(Integer userId);
List<AvailableMarketOrnamentVO> getAvailableMarketListByHashName(String marketHashName, ProductListParams productListParams);
R getAvailableMarketList(GetAvailableMarketListParam param);
R tradeConfirm(TradeManualConfirmParam param);
}
@@ -0,0 +1,107 @@
package com.ruoyi.thirdparty.common.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.ruoyi.domain.other.PayCompanyWeight;
import com.ruoyi.system.service.ISysConfigService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* 支付公司权重路由器,根据配置的权重比例随机选择支付公司。
* 配置格式: [{"name":"chengduhipay","weight":7},{"name":"hipay","weight":3}]
* 表示10次支付中,约7次到chengduhipay,3次到hipay。
*/
@Slf4j
@Component
public class PayCompanyRouter {
private final ISysConfigService sysConfigService;
public PayCompanyRouter(ISysConfigService sysConfigService) {
this.sysConfigService = sysConfigService;
}
/**
* 按权重随机选择一个支付公司(不区分渠道)
*/
public String select() {
return select(null);
}
/**
* 按权重随机选择一个支持指定渠道的支付公司
* @param payType 支付渠道:1=支付宝,2=银联。为 null 则不限制
*/
public String select(Integer payType) {
String configJson = sysConfigService.selectConfigByKey("payCompanyWeights");
if (StrUtil.isBlank(configJson)) {
log.warn("payCompanyWeights 配置为空,回退到 chengduhipay");
return "chengduhipay";
}
try {
JSONArray jsonArray = JSONUtil.parseArray(configJson);
List<PayCompanyWeight> list = jsonArray.toList(PayCompanyWeight.class);
// 过滤有效权重 + 渠道匹配(payType为空则不过滤渠道)
List<PayCompanyWeight> validList = list.stream()
.filter(w -> w.getWeight() > 0 && StrUtil.isNotBlank(w.getName()))
.filter(w -> w.getPayType() == null || payType == null || w.getPayType().equals(payType))
.toList();
if (validList.isEmpty()) {
log.warn("payCompanyWeights 无匹配 payType={} 的有效权重,回退到 chengduhipay", payType);
return "chengduhipay";
}
int totalWeight = validList.stream().mapToInt(PayCompanyWeight::getWeight).sum();
if (totalWeight <= 0) {
log.warn("payCompanyWeights 总权重为0,回退到 chengduhipay");
return "chengduhipay";
}
int random = ThreadLocalRandom.current().nextInt(totalWeight);
int cumulative = 0;
for (PayCompanyWeight w : validList) {
cumulative += w.getWeight();
if (random < cumulative) {
log.info("加权随机选中支付公司: {}, payType={}, 权重={}/{}, random={}",
w.getName(), w.getPayType(), w.getWeight(), totalWeight, random);
return w.getName();
}
}
return validList.get(0).getName();
} catch (Exception e) {
log.error("解析 payCompanyWeights 失败,回退到 chengduhipay: {}", e.getMessage());
return "chengduhipay";
}
}
/**
* 判断指定支付公司当前是否启用(在 payCompanyWeights 中且 weight>0)。
* 用于强制路由前的开关校验:权重设为 0 即视为停用。
*/
public boolean isEnabled(String name) {
if (StrUtil.isBlank(name)) {
return false;
}
String configJson = sysConfigService.selectConfigByKey("payCompanyWeights");
if (StrUtil.isBlank(configJson)) {
return false;
}
try {
List<PayCompanyWeight> list = JSONUtil.parseArray(configJson).toList(PayCompanyWeight.class);
return list.stream().anyMatch(w -> name.equals(w.getName()) && w.getWeight() > 0);
} catch (Exception e) {
log.error("解析 payCompanyWeights 失败: {}", e.getMessage());
return false;
}
}
}
@@ -0,0 +1,82 @@
package com.ruoyi.thirdparty.common.service;
import com.ruoyi.admin.config.RedisConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 支付拉单频控守卫(渠道无关,按 uid 统一生效)。
* 规则:x 分钟内 y 次拉单未支付则封禁 z 分钟。当前 x=5, y=3, z=10。
* 两个缓存:
* 次数缓存 PAY_PULL_UNPAID_COUNT+uid:TTL=x 分钟,值=累计未支付拉单次数;
* 封禁缓存 PAY_PULL_BAN+uid:值=z、TTL=z 分钟。
*/
@Slf4j
@Component
public class PayFrequencyGuard {
/** 次数缓存有效期 x(分钟) */
private static final long PULL_WINDOW_MINUTES = 5;
/** 触发封禁的未支付拉单次数阈值 y */
private static final long PULL_MAX_UNPAID = 3;
/** 封禁时长 z(分钟),同时作为封禁缓存的值 */
private static final long BAN_MINUTES = 10;
private static final DefaultRedisScript<Long> RECORD_UNPAID_SCRIPT;
static {
RECORD_UNPAID_SCRIPT = new DefaultRedisScript<>();
RECORD_UNPAID_SCRIPT.setScriptText(
"local count = redis.call('INCR', KEYS[1]) " +
"if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end " +
"if count >= tonumber(ARGV[2]) then " +
" redis.call('SET', KEYS[2], ARGV[3], 'EX', ARGV[4]) " +
" redis.call('DEL', KEYS[1]) " +
" return 1 " +
"end " +
"return 0"
);
RECORD_UNPAID_SCRIPT.setResultType(Long.class);
}
private final StringRedisTemplate stringRedisTemplate;
public PayFrequencyGuard(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/** 是否处于封禁期 */
public boolean isBanned(Long uid) {
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(RedisConstants.PAY_PULL_BAN + uid));
}
/**
* 记录一次"拉单未支付":次数缓存自增,达到阈值则写入封禁缓存并清空次数缓存。
* 次数缓存 TTL = x 分钟;封禁缓存 值=TTL=z 分钟。
*/
public void recordUnpaidPull(Long uid) {
String countKey = RedisConstants.PAY_PULL_UNPAID_COUNT + uid;
String banKey = RedisConstants.PAY_PULL_BAN + uid;
Long result = stringRedisTemplate.execute(
RECORD_UNPAID_SCRIPT,
List.of(countKey, banKey),
String.valueOf(PULL_WINDOW_MINUTES * 60),
String.valueOf(PULL_MAX_UNPAID),
String.valueOf(BAN_MINUTES),
String.valueOf(BAN_MINUTES * 60)
);
if (result != null && result == 1L) {
log.warn("支付频控触发封禁:uid={}, {}分钟内未支付拉单达{}次, 封禁{}分钟",
uid, PULL_WINDOW_MINUTES, PULL_MAX_UNPAID, BAN_MINUTES);
}
}
/** 支付成功后清空该用户的未支付次数缓存(支付了就不计入未支付) */
public void clearOnPaid(Long uid) {
stringRedisTemplate.delete(RedisConstants.PAY_PULL_UNPAID_COUNT + uid);
}
}
@@ -0,0 +1,14 @@
package com.ruoyi.thirdparty.common.service;
import java.math.BigDecimal;
/**
* 充值成功通知
*/
public interface RechargeSuccessfulNoticeService {
/**
* 发送充值成功通知
*/
void sendRechargeSuccessNotice(String userId, BigDecimal amount);
}
@@ -0,0 +1,42 @@
package com.ruoyi.thirdparty.common.service.impl;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.thirdparty.common.service.ApiCommonService;
import org.springframework.stereotype.Service;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class ApiCommonServiceImpl implements ApiCommonService {
@Override
public R<Object> writerPayImage(HttpServletResponse response, String contents) throws IOException {
ServletOutputStream out = response.getOutputStream();
try {
Map<EncodeHintType,Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
hints.put(EncodeHintType.MARGIN, 0);
BitMatrix bitMatrix = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE,300,300,hints);
MatrixToImageWriter.writeToStream(bitMatrix,"jpg",out);
return R.ok("success");
}catch (Exception e){
return R.fail("数据异常");
}finally {
if(out != null){
out.flush();
out.close();
}
}
}
}
@@ -0,0 +1,373 @@
package com.ruoyi.thirdparty.common.service.impl;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtBlankCardKeyMapper;
import com.ruoyi.admin.mapper.TtOrderMapper;
import com.ruoyi.admin.mapper.TtPromoCardKeyMapper;
import com.ruoyi.admin.mapper.TtRechargeCardMapper;
import com.ruoyi.admin.mapper.TtRechargeRecordMapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.service.TtBonusService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.PayOrderStatus;
import com.ruoyi.domain.common.constant.PayType;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.TtBlankCardKey;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtPromoCardKey;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtRechargeCard;
import com.ruoyi.domain.other.TtRechargeRecord;
import com.ruoyi.thirdparty.common.service.ApiRechargeService;
import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.Date;
@Service
@Slf4j
public class ApiRechargeServiceImpl implements ApiRechargeService {
private final TtBonusService bonusService;
private final TtUserService userService;
private final TtRechargeCardMapper rechargeCardMapper;
private final TtRechargeRecordMapper rechargeRecordMapper;
public ApiRechargeServiceImpl(TtBonusService bonusService,
TtUserService userService,
TtRechargeCardMapper rechargeCardMapper,
TtRechargeRecordMapper rechargeRecordMapper) {
this.bonusService = bonusService;
this.userService = userService;
this.rechargeCardMapper = rechargeCardMapper;
this.rechargeRecordMapper = rechargeRecordMapper;
}
@Autowired
private RechargeSuccessfulNoticeService rechargeSuccessfulNoticeService;
@Autowired
private TtUserBlendErcashMapper userBlendErcashMapper;
@Autowired
private TtOrderMapper orderMapper;
@Autowired
private TtPromoCardKeyMapper promoCardKeyMapper;
@Autowired
private TtBlankCardKeyMapper blankCardKeyMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public String cardPay(String password, TtUser ttUser) {
// 并发防护由控制层 CARD_PAY_LOCK 负责(按卡密内容加锁、包裹整个事务、解锁晚于提交);
// 此处仅保留数据库乐观锁(WHERE status=0 + 影响行数校验)作为并发双用兜底
long startTime = System.currentTimeMillis();
try {
TtRechargeCard ttRechargeCard = new LambdaQueryChainWrapper<>(rechargeCardMapper).eq(TtRechargeCard::getPassword, password)
.eq(TtRechargeCard::getStatus, "0").eq(TtRechargeCard::getDelFlag, "0").one();
// 如果后台卡密表找不到,依次查找空白卡密表、卡商推广平台卡密表
if (StringUtils.isNull(ttRechargeCard)) {
String blankResult = cardPayByBlankCardKey(password, ttUser);
if (blankResult != null) return blankResult;
return cardPayByPromoCardKey(password, ttUser);
}
// 原子占用:仅当 status=0 时置为 status=1,防止并发双用
LambdaUpdateWrapper<TtRechargeCard> cardUpdateWrapper = new LambdaUpdateWrapper<>();
cardUpdateWrapper.eq(TtRechargeCard::getId, ttRechargeCard.getId())
.eq(TtRechargeCard::getStatus, "0")
.set(TtRechargeCard::getStatus, "1")
.set(TtRechargeCard::getUseUserId, ttUser.getUserId())
.set(TtRechargeCard::getUseTime, DateUtils.getNowDate())
.set(TtRechargeCard::getUpdateBy, ttUser.getUserName())
.set(TtRechargeCard::getUpdateTime, DateUtils.getNowDate());
if (rechargeCardMapper.update(null, cardUpdateWrapper) > 0) {
BigDecimal totalAmount = ttRechargeCard.getPrice();
boolean isBlankCard = Integer.valueOf(1).equals(ttRechargeCard.getIsBlank());
boolean isBlankAccount = Integer.valueOf(1).equals(ttUser.getIsBlankAccount());
// 空白卡密仅允许白号使用
if (isBlankCard && !isBlankAccount) {
// 回滚卡密状态
ttRechargeCard.setStatus("0");
ttRechargeCard.setUseUserId(null);
ttRechargeCard.setUseTime(null);
rechargeCardMapper.updateById(ttRechargeCard);
return "该卡密仅限白号使用!";
}
// 白号只能使用空白卡密:命中普通卡密一律拒绝并回滚卡密状态
if (isBlankAccount && !isBlankCard) {
ttRechargeCard.setStatus("0");
ttRechargeCard.setUseUserId(null);
ttRechargeCard.setUseTime(null);
rechargeCardMapper.updateById(ttRechargeCard);
return "白号仅限使用空白卡密!";
}
// 1. 同步增加用户余额(不动积分),与支付宝/微信通道保持一致
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + totalAmount.toString());
userService.update(userUpdate);
// 2. 同步写入充值流水(updateUserVIPLevel 依赖 source=RECHARGE 流水累计金额,必须同步落库)
TtUser refreshUser = userService.getById(ttUser.getUserId());
Timestamp now = new Timestamp(System.currentTimeMillis());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(refreshUser.getUserId())
.amount(totalAmount)
.finalAmount(refreshUser.getAccountAmount())
.total(totalAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.RECHARGE.getCode())
.remark(TtAccountRecordSource.RECHARGE.getMsg())
.createTime(now)
.updateTime(now)
.build();
userBlendErcashMapper.insert(blendErcash);
// 3. 更新VIP等级,必须在充值流水同步落库后调用
userService.updateUserVIPLevel(ttUser.getUserId());
TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build();
ttRechargeRecord.setUserId(ttUser.getUserId());
ttRechargeRecord.setParentId(ttUser.getParentId());
ttRechargeRecord.setArrivalAmount(totalAmount);
ttRechargeRecord.setAmountActuallyPaid(totalAmount);
ttRechargeRecord.setFinallyPrice(refreshUser.getAccountAmount());
ttRechargeRecord.setOrderId(password);
ttRechargeRecord.setStatus("0");
ttRechargeRecord.setChannelType("0");
ttRechargeRecord.setCreateTime(DateUtils.getNowDate());
// 空白卡密标记:充值记录列表中会被过滤不显示
if (isBlankCard) {
ttRechargeRecord.setIsBlankCard(1);
}
// 实时计算返佣(白号跳过返佣,不触发上级佣金)
if (rechargeRecordMapper.insert(ttRechargeRecord) > 0 && !isBlankAccount) {
bonusService.bonus(ttUser.getUserId().intValue(), ttRechargeRecord.getId());
}
// 设置商户订单号
String orderId = String.valueOf(IdUtil.getSnowflake(1, 1).nextId());
// 创建订单
TtOrder order = new TtOrder();
order.setOrderId(orderId);
order.setOutTradeNo(password);
order.setUserId(ttUser.getUserId());
order.setType(PayType.MK_CARD.getCode());
order.setGoodsId(ttRechargeCard.getId());
order.setGoodsPrice(ttRechargeCard.getPrice());
order.setGoodsNum(1);
order.setTotalAmount(totalAmount);
order.setStatus(PayOrderStatus.PAY_COMPLE.getCode());
order.setCreateTime(new Date());
orderMapper.insert(order);
// 发送充值成功通知
// rechargeSuccessfulNoticeService.sendRechargeSuccessNotice(ttUser.getUserId().toString(), totalAmount);
return "";
} else {
log.warn("卡密充值竞态拦截: password={}, userId={}", password, ttUser.getUserId());
return "该卡密已被使用!";
}
} catch (Exception e) {
// 显式标记回滚:卡密占用与加余额/写流水/建订单必须原子,避免出现"卡密已消耗但钱未到账"
log.error("卡密充值异常回滚: password={}, userId={}", password, ttUser.getUserId(), e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return "数据异常!";
} finally {
// P1#9: 统一响应时间,缓解三表查询时序侧信道
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed < 50) {
try { Thread.sleep(50 - elapsed); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); }
}
}
}
/**
* 空白卡密兑换(tt_blank_card_key 表)
* 直接到账:status=1,仅加余额,不写流水/不更新VIP/不建订单
* @return null 表示未找到该卡密,继续查找其他表;非null 表示已处理
*/
private String cardPayByBlankCardKey(String password, TtUser ttUser) {
TtBlankCardKey blankCard = new LambdaQueryChainWrapper<>(blankCardKeyMapper)
.eq(TtBlankCardKey::getCardCode, password)
.eq(TtBlankCardKey::getStatus, 0)
.one();
if (blankCard == null) return null; // 未找到,继续查找其他表
// 空白卡密仅限白号使用:普通用户/主播/招商命中此卡密一律拒绝,且不消耗卡密(保留status=0,方便白号后续仍能正常使用)
if (!Integer.valueOf(1).equals(ttUser.getIsBlankAccount())) {
return "该卡密仅限白号使用!";
}
// 原子占用:仅当 status=0 时置为 status=1,防止并发双用
LambdaUpdateWrapper<TtBlankCardKey> blankUpdateWrapper = new LambdaUpdateWrapper<>();
blankUpdateWrapper.eq(TtBlankCardKey::getId, blankCard.getId())
.eq(TtBlankCardKey::getStatus, 0)
.set(TtBlankCardKey::getStatus, 1)
.set(TtBlankCardKey::getUsedById, ttUser.getUserId().longValue())
.set(TtBlankCardKey::getUsedByName, ttUser.getUserName())
.set(TtBlankCardKey::getUsedTime, DateUtils.getNowDate());
if (blankCardKeyMapper.update(null, blankUpdateWrapper) <= 0) {
log.warn("空白卡密竞态拦截: cardCode={}, userId={}", password, ttUser.getUserId());
return "该卡密已被使用!";
}
BigDecimal totalAmount = blankCard.getAmount();
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + totalAmount.toString());
userService.update(userUpdate);
// 写入余额流水与充值记录,供前端 cocos「余额记录/充值记录」展示;
// 后台通过 is_blank_account(余额流水)与 is_blank_card(充值记录)过滤,不体现白号数据。
// 白号不产生上级佣金,不触发返佣、不写订单、不更新VIP。
TtUser refreshUser = userService.getById(ttUser.getUserId());
Timestamp now = new Timestamp(System.currentTimeMillis());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(refreshUser.getUserId())
.amount(totalAmount)
.finalAmount(refreshUser.getAccountAmount())
.total(totalAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.RECHARGE.getCode())
.remark(TtAccountRecordSource.RECHARGE.getMsg())
.createTime(now)
.updateTime(now)
.build();
userBlendErcashMapper.insert(blendErcash);
TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build();
ttRechargeRecord.setUserId(ttUser.getUserId());
ttRechargeRecord.setParentId(ttUser.getParentId());
ttRechargeRecord.setArrivalAmount(totalAmount);
ttRechargeRecord.setAmountActuallyPaid(totalAmount);
ttRechargeRecord.setFinallyPrice(refreshUser.getAccountAmount());
ttRechargeRecord.setOrderId(password);
ttRechargeRecord.setStatus("0");
ttRechargeRecord.setChannelType("0");
// 空白卡密标记:后台充值记录列表按 is_blank_card 过滤不显示
ttRechargeRecord.setIsBlankCard(1);
ttRechargeRecord.setCreateTime(DateUtils.getNowDate());
rechargeRecordMapper.insert(ttRechargeRecord);
return "";
}
/**
* 卡商推广平台卡密兑换(promo_card_key 表)
* 玩家使用卡密直接到账:status=1(已使用),加余额+写流水+更新VIP
*/
private String cardPayByPromoCardKey(String password, TtUser ttUser) {
TtPromoCardKey promoCard = new LambdaQueryChainWrapper<>(promoCardKeyMapper)
.eq(TtPromoCardKey::getCardCode, password)
.eq(TtPromoCardKey::getStatus, 0)
.one();
if (promoCard == null) return "卡密不存在或已被使用!";
// 白号只能使用空白卡密:卡商卡密一律拒绝,不消费该卡密
if (Integer.valueOf(1).equals(ttUser.getIsBlankAccount())) {
return "白号仅限使用空白卡密!";
}
BigDecimal totalAmount = promoCard.getAmount();
// 1. 原子占用:仅当仍未使用(status=0)时置为已使用(status=1),与卡商销毁(status=0→2)在 status=0 上互斥竞争,
// 有且只有一方成功,杜绝"玩家刚好使用、卡商刚好销毁"的并发双花(旧 updateById 无 status 守卫会盲覆盖销毁态)
LambdaUpdateWrapper<TtPromoCardKey> useWrapper = new LambdaUpdateWrapper<>();
useWrapper.eq(TtPromoCardKey::getId, promoCard.getId())
.eq(TtPromoCardKey::getStatus, 0)
.set(TtPromoCardKey::getStatus, 1)
.set(TtPromoCardKey::getUsedById, ttUser.getUserId().longValue())
.set(TtPromoCardKey::getUsedByName, ttUser.getUserName())
.set(TtPromoCardKey::getUsedTime, DateUtils.getNowDate());
if (promoCardKeyMapper.update(null, useWrapper) <= 0) {
// 期间被销毁或被其他请求先用掉,未加余额,安全退出
return "卡密不存在或已被使用!";
}
// 2. 增加用户余额
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate.eq(TtUser::getUserId, ttUser.getUserId())
.setSql("account_amount = account_amount + " + totalAmount.toPlainString());
userService.update(userUpdate);
// 3. 写入充值流水
TtUser refreshUser = userService.getById(ttUser.getUserId());
Timestamp now = new Timestamp(System.currentTimeMillis());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(refreshUser.getUserId())
.amount(totalAmount)
.finalAmount(refreshUser.getAccountAmount())
.total(totalAmount)
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.RECHARGE.getCode())
.remark("卡密充值: " + promoCard.getCardCode())
.createTime(now)
.updateTime(now)
.build();
userBlendErcashMapper.insert(blendErcash);
// 4. 更新VIP等级
userService.updateUserVIPLevel(ttUser.getUserId());
// 5. 写入充值记录(与普通卡密/第三方支付通道保持一致,确保"充值记录"列表可见)
TtRechargeRecord ttRechargeRecord = TtRechargeRecord.builder().build();
ttRechargeRecord.setUserId(ttUser.getUserId());
ttRechargeRecord.setParentId(ttUser.getParentId());
ttRechargeRecord.setArrivalAmount(totalAmount);
ttRechargeRecord.setAmountActuallyPaid(totalAmount);
ttRechargeRecord.setFinallyPrice(refreshUser.getAccountAmount());
ttRechargeRecord.setOrderId(password);
ttRechargeRecord.setStatus("0");
ttRechargeRecord.setChannelType("0");
// 记录来源卡商ID(promo_card_key.creatorId 为卡商 userId)
if (promoCard.getCreatorId() != null) {
ttRechargeRecord.setCardMerchantId(promoCard.getCreatorId().intValue());
}
ttRechargeRecord.setCreateTime(DateUtils.getNowDate());
// 实时计算返佣,触发上级佣金
if (rechargeRecordMapper.insert(ttRechargeRecord) > 0) {
bonusService.bonus(ttUser.getUserId().intValue(), ttRechargeRecord.getId());
}
// 6. 写入支付订单,口径与后台卡密 cardPay 主分支完全一致(type=卡密、status=支付完成);
// 商品ID 固定 0:卡商卡密不在 tt_recharge_config 里,避免误命中同ID充值商品导致取错商品图
TtOrder order = TtOrder.builder()
.orderId(String.valueOf(IdUtil.getSnowflake(1, 1).nextId()))
.outTradeNo(password)
.userId(ttUser.getUserId())
.type(PayType.MK_CARD.getCode())
.goodsId(0)
.goodsPrice(totalAmount)
.goodsNum(1)
.totalAmount(totalAmount)
.status(PayOrderStatus.PAY_COMPLE.getCode())
.createTime(new Date())
.build();
orderMapper.insert(order);
return "";
}
}
@@ -0,0 +1,151 @@
package com.ruoyi.thirdparty.common.service.impl;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.thirdparty.aliyunsms.service.AliyunSmsService;
import com.ruoyi.thirdparty.common.model.ApiSmsBody;
import com.ruoyi.thirdparty.common.service.ApiSmsService;
import com.ruoyi.thirdparty.foursms.service.FourSmsService;
import com.ruoyi.thirdparty.note.service.YunXinNoteService;
import com.ruoyi.thirdparty.smsbao.service.SmsbaoService;
import com.ruoyi.thirdparty.winic.service.WinicService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class ApiSmsServiceImpl implements ApiSmsService {
private final RedisCache redisCache;
private final TtUserService userService;
private final YunXinNoteService yunXinNoteService;
public ApiSmsServiceImpl(RedisCache redisCache,
TtUserService userService,
YunXinNoteService yunXinNoteService) {
this.redisCache = redisCache;
this.userService = userService;
this.yunXinNoteService = yunXinNoteService;
}
@Autowired
private SmsbaoService smsbaoService;
@Autowired
private WinicService winicService;
@Autowired
private AliyunSmsService aliyunSmsService;
@Autowired
private FourSmsService fourSmsService;
@Value("${ruoyi.name}")
private String systemName;
/** 短信发送频率限制:10 分钟内最多 3 次 */
private static final int SMS_RATE_LIMIT_MAX_COUNT = 3;
private static final int SMS_RATE_LIMIT_MINUTES = 10;
@Override
public String getVerifyCode(ApiSmsBody smsBody) {
String phoneNumber = smsBody.getPhoneNumber();
String type = smsBody.getType();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY;
if (StringUtils.isEmpty(type)) {
return "类型不能为空";
}
if (StringUtils.isEmpty(phoneNumber)) {
return "手机号不能为空";
}
if (!Validator.isMobile(phoneNumber)) {
return "手机号格式错误,请检查手机号是否输入正确!";
}
// 频率限制:同一手机号 10 分钟内最多发送 3 次(不限类型,防止跨类型刷短信)
String rateLimitKey = "sms:rate_limit:" + phoneNumber;
Integer sendCount = redisCache.getCacheObject(rateLimitKey);
if (sendCount != null && sendCount >= SMS_RATE_LIMIT_MAX_COUNT) {
return "操作过于频繁,请" + SMS_RATE_LIMIT_MINUTES + "分钟后再试";
}
if ("1".equals(type) && !userService.checkPhoneUnique(TtUser.builder().phoneNumber(phoneNumber).build())) {
return "手机号'" + phoneNumber + "'已被注册!";
}
// type=2(登录)不再校验手机号是否已注册,因为 verificationCodeLoginOrRegister 接口支持未注册自动注册
if ("3".equals(type) || "4".equals(type)) {
TtUser ttUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper()).eq(TtUser::getPhoneNumber, phoneNumber).one();
if (StringUtils.isNull(ttUser)) return "该手机号未在本站注册!";
}
if ("1".equals(type)) {
verifyKey = verifyKey + "ApiRegister_" + phoneNumber;
} else if ("2".equals(type)) {
verifyKey = verifyKey + "ApiLogin_" + phoneNumber;
} else if ("3".equals(type)) {
verifyKey = verifyKey + "UpdatePhoneNumber_" + phoneNumber;
} else if ("4".equals(type)) {
verifyKey = verifyKey + "ApiForgetPassword_" + phoneNumber;
} else if ("5".equals(type)) {
verifyKey = verifyKey + "ApiRealNameAuth_" + phoneNumber;
}
String cacheCode = redisCache.getCacheObject(verifyKey);
if (StringUtils.isNotEmpty(cacheCode)) {
return "验证码已发送,请注意手机短信信息!";
}
String randomCode = RandomUtil.randomNumbers(4);
// 网易云信
// String code = yunXinNoteService.sendNote(phoneNumber, randomCode);
// if (StringUtils.isEmpty(code)) {
// return "发送短信验证码时出现异常!";
// }
//短信宝(备用渠道)
// String content = "【长沙赤焰网络】您的验证码为" + randomCode + ",该验证码1分钟内有效,请勿泄露于他人。";
// String code = smsbaoService.sendSms(phoneNumber, content);
// if (!"0".equals(code)) {
// return "发送短信验证码时出现异常!";
// }
// 切换为 4SMS(模板化发送,params.code 为验证码;返回 "0" 表示成功)
// String code = fourSmsService.sendVerifyCode(phoneNumber, randomCode);
// if (!"0".equals(code)) {
// return "发送短信验证码时出现异常!";
// }
// 阿里云短信(已停用,改为短信宝)
String code = aliyunSmsService.sendVerifyCode(phoneNumber, randomCode);
if (!"0".equals(code)) {
return "发送短信验证码时出现异常!";
}
// 切换为中正云通信
// String content = "【OG】您的验证码为" + randomCode + ",该验证码5分钟内有效,请勿泄露于他人。";
// String code = winicService.sendSms(phoneNumber, content);
// if (!"000".equals(code)) {
// return "发送短信验证码时出现异常!";
// }
// 更新发送频率计数(保留剩余 TTL,避免重置窗口)
if (sendCount == null) {
redisCache.setCacheObject(rateLimitKey, 1, SMS_RATE_LIMIT_MINUTES, TimeUnit.MINUTES);
} else {
long remainTtl = redisCache.getExpire(rateLimitKey);
redisCache.setCacheObject(rateLimitKey, sendCount + 1,
remainTtl > 0 ? (int) remainTtl : SMS_RATE_LIMIT_MINUTES * 60, TimeUnit.SECONDS);
}
redisCache.setCacheObject(verifyKey, randomCode, 5, TimeUnit.MINUTES);
return "";
}
@Override
public String validateCaptcha(String code, String verifyKey) {
verifyKey = CacheConstants.CAPTCHA_CODE_KEY + verifyKey;
String captcha = redisCache.getCacheObject(verifyKey);
if (!code.equalsIgnoreCase(captcha)) return "验证码错误";
redisCache.deleteObject(verifyKey);
return "success";
}
}
@@ -0,0 +1,713 @@
package com.ruoyi.thirdparty.common.service.impl;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.admin.mapper.*;
import com.ruoyi.admin.service.TtOrnamentService;
import com.ruoyi.admin.service.TtOrnamentYYService;
import com.ruoyi.admin.service.TtYyOrnamentsService;
import com.ruoyi.common.constant.ThirdParty.Platform;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.DeliveryOrderStatus;
import com.ruoyi.domain.common.constant.PartyType;
import com.ruoyi.domain.common.constant.TtboxRecordStatus;
import com.ruoyi.domain.dto.deliver.TradeBuyParam;
import com.ruoyi.domain.dto.deliver.TradeManualConfirmParam;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.entity.delivery.TtDeliveryRecord;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtYYOrnaments;
import com.ruoyi.domain.vo.AvailableMarketOrnamentVO;
import com.ruoyi.domain.vo.OnSaleOrnamentVO;
import com.ruoyi.domain.vo.queryTemplateSaleByCategoryDataVO;
import com.ruoyi.domain.vo.queryTemplateSaleByCategoryVO;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.common.controller.DeliverGoodsController.GetAvailableMarketListParam;
import com.ruoyi.thirdparty.common.service.DeliverGoodsService;
import com.ruoyi.thirdparty.cs340.domain.body.ByTemplateCreateOrder;
import com.ruoyi.thirdparty.cs340.domain.result.ByTemplateCreateOrderResult;
import com.ruoyi.thirdparty.cs340.domain.result.Cs340OrderStatusResult;
import com.ruoyi.thirdparty.cs340.domain.result.Cs340Result;
import com.ruoyi.thirdparty.cs340.service.Cs340Service;
import com.ruoyi.thirdparty.v5item.domain.body.CreateOrderBody;
import com.ruoyi.thirdparty.v5item.domain.result.CreateOrderResult;
import com.ruoyi.thirdparty.v5item.domain.result.QueryOrderStatusResult;
import com.ruoyi.thirdparty.v5item.domain.result.V5ItemResult;
import com.ruoyi.thirdparty.v5item.service.V5ItemService;
import com.ruoyi.thirdparty.yyyouping.service.yyyoupingService;
import com.ruoyi.thirdparty.yyyouping.utils.YYClient;
import com.ruoyi.thirdparty.yyyouping.utils.common.YYResult;
import com.ruoyi.thirdparty.zbt.param.*;
import com.ruoyi.thirdparty.zbt.result.ResultZbt;
import com.ruoyi.thirdparty.zbt.result.buy.OpenBuyResultDTO;
import com.ruoyi.thirdparty.zbt.result.order.OrderBuyDetailDTO;
import com.ruoyi.thirdparty.zbt.result.product.AvailableMarket;
import com.ruoyi.thirdparty.zbt.result.product.AvailableMarketList;
import com.ruoyi.thirdparty.zbt.service.ZBTService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.DeliveryOrderStatus.DELIVERY_BEFORE;
import static com.ruoyi.domain.common.constant.DeliveryOrderStatus.ORDER_CANCEL;
@Service
@Slf4j
public class DeliverGoodsServiceImpl implements DeliverGoodsService {
private final ISysConfigService configService;
private final ZBTService zbtService;
private final TtUserMapper userMapper;
private final TtOrnamentMapper ornamentsMapper;
private final TtBoxRecordsMapper boxRecordsMapper;
private final TtDeliveryRecordMapper deliveryRecordMapper;
//@Autowired
private YYClient yyClient;
@Autowired
private TtYyOrnamentsService ttYyOrnamentsService;
@Autowired
private TtOrnamentService ttOrnamentService;
@Autowired
private TtOrnamentYYService ttOrnamentYYService;
@Autowired
private TtOrnamentsYYMapper ttOrnamentsYYMapper;
@Autowired
private yyyoupingService yyyoupingService;
@Autowired
private V5ItemService v5ItemService;
@Autowired
private Cs340Service cs340Service;
public DeliverGoodsServiceImpl(ISysConfigService configService,
ZBTService zbtService,
TtUserMapper userMapper,
TtOrnamentMapper ornamentsMapper,
TtBoxRecordsMapper boxRecordsMapper,
TtDeliveryRecordMapper deliveryRecordMapper) {
this.configService = configService;
this.zbtService = zbtService;
this.userMapper = userMapper;
this.ornamentsMapper = ornamentsMapper;
this.boxRecordsMapper = boxRecordsMapper;
this.deliveryRecordMapper = deliveryRecordMapper;
}
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public R getAvailableMarketList(Integer ornamentsId,Integer partyType) {
// 从平台获取的数据集
List<AvailableMarket> resList = null;
if (partyType.equals(PartyType.ZBT.getCode())){
// 获取饰品信息
TtOrnament ornament = new LambdaQueryChainWrapper<>(ornamentsMapper)
.eq(TtOrnament::getId, ornamentsId)
.one();
// 构建查询参数
ProductListParams apiParm = ProductListParams.builder()
.itemId(String.valueOf(ornament.getId()))
.build();
// 查询接口
ResultZbt<AvailableMarketList> productList = zbtService.productList(apiParm);
if (!productList.getSuccess()) {
log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!");
return null;
}
// 过滤可用数据
AvailableMarketList availableMarketList = productList.getData();
resList = availableMarketList.getList();
}else if (partyType.equals(PartyType.YY_YOU_PING.getCode())){
// 获取饰品信息
TtOrnament ornament = new LambdaQueryChainWrapper<>(ornamentsMapper)
.eq(TtOrnament::getId, ornamentsId)
.one();
// 构建查询参数
ProductListParams apiParm = ProductListParams.builder()
.itemId(String.valueOf(ornament.getId()))
.build();
// 查询接口
ResultZbt<AvailableMarketList> productList = zbtService.productList(apiParm);
if (!productList.getSuccess()) {
log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!");
return null;
}
// 过滤可用数据
AvailableMarketList availableMarketList = productList.getData();
resList = availableMarketList.getList();
}
if (ObjectUtil.isEmpty(resList)) return R.fail("平台没有该物品在售信息。");
// 构建返回结果
List<OnSaleOrnamentVO> resultList = new ArrayList<>();
// 提取可用数据
for (AvailableMarket availableMarket : resList) {
OnSaleOrnamentVO data = OnSaleOrnamentVO.builder().build();
data.setId(availableMarket.getId());
data.setDelivery(availableMarket.getDelivery());
data.setItemName(availableMarket.getItemName());
data.setImageUrl(availableMarket.getImageUrl());
data.setCnyPrice(availableMarket.getCnyPrice());
data.setPrice(availableMarket.getPrice());
resultList.add(data);
}
return R.ok(resultList);
}
@Override
public R tradeBuy(TradeBuyParam param) {
// 获取提货订单数据
TtDeliveryRecord ttDeliveryRecord = deliveryRecordMapper.selectById(param.getDeliveryRecordId());
// 获取用户数据
TtUser ttUser = userMapper.selectById(ttDeliveryRecord.getUserId());
// 初始化数据
String transactionLink = ttUser.getTransactionLink();
if (StringUtils.isEmpty(transactionLink)) {
return R.fail("该用户未绑定Steam交易链接!");
}
String outTradeNo = ttDeliveryRecord.getOutTradeNo();
if (param.getPartyType().equals(PartyType.ZBT.getCode())){
// 构建购买参数
NormalBuyParamV2DTO normalBuyParamV2DTO = new NormalBuyParamV2DTO();
normalBuyParamV2DTO.setOutTradeNo(outTradeNo);
normalBuyParamV2DTO.setProductId(param.getProductId().toString());
normalBuyParamV2DTO.setTradeUrl(transactionLink);
// 购买
ResultZbt<OpenBuyResultDTO> tradeBuy = zbtService.tradeBuy(normalBuyParamV2DTO);
if (!tradeBuy.getSuccess()) {
return R.ok(tradeBuy.getErrorMsg());
}
// 获取结果数据
OpenBuyResultDTO data = tradeBuy.getData();
System.err.println("结果数据:" + data);
// 更新提货订单数据
ttDeliveryRecord.setBuyPrice(data.getBuyPrice());
ttDeliveryRecord.setThirdpartyDelivery(data.getDelivery());
ttDeliveryRecord.setOrderId(data.getOrderId());
ttDeliveryRecord.setStatus(DELIVERY_BEFORE.getCode()); // 发货订单状态 1待发货
ttDeliveryRecord.setMessage(DELIVERY_BEFORE.getMsg());
ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername());
ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate());
if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) {
return R.ok();
}
return R.fail("更新提货订单数据异常!");
}else if (param.getPartyType().equals(PartyType.V5ITEM.getCode())){
LambdaQueryWrapper<TtOrnament> ttOrnamentsQuery = new LambdaQueryWrapper<>();
ttOrnamentsQuery.eq(TtOrnament::getId, param.getProductId());
TtOrnament ttOrnament = ttOrnamentService.getOne(ttOrnamentsQuery);
CreateOrderBody createOrderBody = new CreateOrderBody();
createOrderBody.setMarketHashName(ttOrnament.getMarketHashName());
BigDecimal purchasePrice = param.getPrice(); //.multiply(new BigDecimal("2"));
createOrderBody.setPurchasePrice(purchasePrice);
createOrderBody.setTradeUrl(transactionLink);
createOrderBody.setMerchantOrderNo(outTradeNo);
V5ItemResult<CreateOrderResult> createOrderResult = v5ItemService.createOrder(createOrderBody);
if (createOrderResult.getCode() != 0) {
return R.fail(createOrderResult.getMsg());
}
// 更新提货订单数据
ttDeliveryRecord.setBuyPrice(createOrderResult.getData().getPayAmount());
ttDeliveryRecord.setThirdpartyDelivery(1);
ttDeliveryRecord.setOrderId(createOrderResult.getData().getOrderNo());
ttDeliveryRecord.setStatus(DELIVERY_BEFORE.getCode()); // 发货订单状态 1待发货
ttDeliveryRecord.setMessage(DELIVERY_BEFORE.getMsg());
ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername());
ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate());
if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) {
return R.ok();
}
return R.ok("正在开发");
} else if (Objects.equals(param.getPartyType(), PartyType.CS340.getCode())) {
LambdaQueryWrapper<TtOrnament> ttOrnamentsQuery = new LambdaQueryWrapper<>();
ttOrnamentsQuery.eq(TtOrnament::getId, param.getProductId());
TtOrnament ttOrnament = ttOrnamentService.getOne(ttOrnamentsQuery);
BigDecimal purchasePrice = param.getPrice();// .multiply(new BigDecimal("2"));
ByTemplateCreateOrder createOrderBody = new ByTemplateCreateOrder();
createOrderBody.setPurchasePrice(purchasePrice.toString());
createOrderBody.setTradeLinks(transactionLink);
createOrderBody.setMerchantOrderNo(outTradeNo);
createOrderBody.setCommodityHashName(ttOrnament.getMarketHashName());
Cs340Result<ByTemplateCreateOrderResult> createOrderResult = cs340Service.createOrder(createOrderBody);
if (createOrderResult.getCode() != 0) {
return R.fail(createOrderResult.getMsg());
}
// 更新提货订单数据
ttDeliveryRecord.setBuyPrice(createOrderResult.getData().getPayAmount());
ttDeliveryRecord.setThirdpartyDelivery(1);
ttDeliveryRecord.setOrderId(createOrderResult.getData().getOrderNo());
ttDeliveryRecord.setStatus(DELIVERY_BEFORE.getCode()); // 发货订单状态 1待发货
ttDeliveryRecord.setMessage(DELIVERY_BEFORE.getMsg());
ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername());
ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate());
if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) {
return R.ok();
}
return R.ok("正在开发");
} else if (param.getPartyType().equals(PartyType.YY_YOU_PING.getCode())){
return R.ok("暂未开放。");
} else {
return R.fail("非法的第三方平台代码");
}
}
@Override
@Transactional
public String synchronousStatus(String outTradeNo) {
// 获取提货数据
TtDeliveryRecord ttDeliveryRecord = new LambdaQueryChainWrapper<>(deliveryRecordMapper)
.eq(TtDeliveryRecord::getOutTradeNo, outTradeNo).one();
if (ttDeliveryRecord == null) {
return "未找到对应的提货记录";
}
if (DeliveryOrderStatus.ORDER_COMPLETE.getCode().equals(ttDeliveryRecord.getStatus())
|| DeliveryOrderStatus.ORDER_CANCEL.getCode().equals(ttDeliveryRecord.getStatus())) {
return "";
}
// 查询 CS340 订单状态
Cs340Result<Cs340OrderStatusResult> orderStatusResult = cs340Service.queryOrderStatus(ttDeliveryRecord.getOrderId());
if (orderStatusResult == null || orderStatusResult.getData() == null || orderStatusResult.getCode() != 0) {
return "查询购买订单详情失败";
}
// CS340 大状态:2失败 / 100创建中 / 120支付中 / 130异常挂起 / 140交货中 / 280已取消 / 340已完成 / 360结算中
Integer status = orderStatusResult.getData().getBigStatus();
ttDeliveryRecord.setMessage(orderStatusResult.getData().getStatusMsg());
ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername());
ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate());
if (Objects.equals(status, 2) || Objects.equals(status, 280)) {
// 失败 / 已取消 → 本地映射为「订单取消」。
// 业务语义:取消后饰品需同时出现在「提货背包」和「我的库存」两个 tab,供用户再次发起提货。
// 实现方式:保持 box_records.status=2 不动,仅将 delivery_record.status 改为 11。
// - 「我的库存」SQL: status IN (0,2) + NOT EXISTS (delivery.status!=11) → 命中(box=2, delivery=11 通过 NOT EXISTS)
// - 「提货背包」SQL: status = 2 + NOT EXISTS (delivery.status!=11) → 命中(box=2, delivery=11 通过 NOT EXISTS)
// 与 [TtDeliveryRecordServiceImpl.deliveryFail](后台「退回申请」)口径保持一致。
ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_CANCEL.getCode());
} else if (Objects.equals(status, 340)) {
// 已完成 → 本地置为「订单完成」,并将 box 标为已提货
ttDeliveryRecord.setStatus(DeliveryOrderStatus.ORDER_COMPLETE.getCode());
// 联动:订单完成后将对应 box_records 状态标为「已经提货」,不再展示在背包 / 提货背包,不可分解 / 转赠
markBoxAsDelivered(ttDeliveryRecord.getBoxRecordsId());
} else {
// 其他大状态(创建中 / 支付中 / 交货中 / 结算中 / 异常挂起)暂记为「已发货」,等待下次同步
ttDeliveryRecord.setStatus(DeliveryOrderStatus.DELIVERY_AFTER.getCode());
}
if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) return "";
return "同步提货订单数据异常!";
}
/**
* 将提货记录对应的 box_records 状态从 APPLY_DELIVERY(2) 走到 DELIVERY_YET(3)。
* 仅在当前状态确为「申请提货」时才写入,避免覆盖其他状态(如已被 admin 删除 / 已分解)。
*/
private void markBoxAsDelivered(Long boxRecordsId) {
if (boxRecordsId == null) {
return;
}
TtBoxRecords box = boxRecordsMapper.selectById(boxRecordsId);
if (box != null && TtboxRecordStatus.APPLY_DELIVERY.getCode().equals(box.getStatus())) {
box.setStatus(TtboxRecordStatus.DELIVERY_YET.getCode());
box.setUpdateTime(DateUtils.getNowDate());
boxRecordsMapper.updateById(box);
}
}
/**
* 保留V5历史同步订单状态的代码逻辑,后面再对接V5的话及时打开
*/
private String synchronousStatusForV5(String outTradeNo) {
// 获取提货数据
TtDeliveryRecord ttDeliveryRecord = new LambdaQueryChainWrapper<>(deliveryRecordMapper)
.eq(TtDeliveryRecord::getOutTradeNo, outTradeNo).one();
if (10 == ttDeliveryRecord.getStatus() || 11 == ttDeliveryRecord.getStatus()) {
return "";
}
// 构建查询参数
OrderBuyDetailParams orderBuyDetailParams = new OrderBuyDetailParams();
orderBuyDetailParams.setOutTradeNo(ttDeliveryRecord.getOutTradeNo());
QueryOrderStatusResult orderStatusResult = v5ItemService.queryOrderStatus(ttDeliveryRecord.getOutTradeNo(), ttDeliveryRecord.getOrderId());
if (orderStatusResult == null) {
return "查询购买订单详情V2失败";
}
// 获取结果数据
Integer status = orderStatusResult.getStatus();
if (Objects.equals(status, 4)){
ttDeliveryRecord.setStatus(status);
ttDeliveryRecord.setMessage(orderStatusResult.getStatusMsg());
ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername());
ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate());
// 将用户提货饰品返回用户背包
TtBoxRecords ttBoxRecords = new LambdaQueryChainWrapper<>(boxRecordsMapper)
.eq(TtBoxRecords::getId, ttDeliveryRecord.getBoxRecordsId())
.eq(TtBoxRecords::getHolderUserId, ttDeliveryRecord.getUserId())
.eq(TtBoxRecords::getOrnamentId, ttDeliveryRecord.getOrnamentId())
.eq(TtBoxRecords::getStatus, "2") // 状态 2已提取
.one();
if (StringUtils.isNull(ttBoxRecords)) {
return "未查询到该用户背包的饰品信息!";
}
ttBoxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode()); // 状态 0背包
ttBoxRecords.setUpdateTime(DateUtils.getNowDate());
boxRecordsMapper.updateById(ttBoxRecords);
if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) {
return "";
}
} else {
ttDeliveryRecord.setMessage(orderStatusResult.getStatusMsg());
// 发送发货成功通知
Map<String, String> noticeMap = new HashMap<>();
noticeMap.put("userId", ttDeliveryRecord.getUserId().toString());
noticeMap.put("title", "发货成功通知");
noticeMap.put("content", "发货成功,请注意查收。");
noticeMap.put("createTime", DateUtils.getTime());
rabbitTemplate.convertAndSend("notice_queue", noticeMap);
}
ttDeliveryRecord.setStatus(DeliveryOrderStatus.DELIVERY_AFTER.getCode());
ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername());
ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate());
if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) return "";
return "同步提货订单数据异常!";
}
@Override
public int syncAllStatus() {
List<TtDeliveryRecord> records = new LambdaQueryChainWrapper<>(deliveryRecordMapper)
.eq(TtDeliveryRecord::getStatus, 1)
.list();
for (TtDeliveryRecord record : records) {
String msg = synchronousStatus(record.getOutTradeNo());
}
return records.size();
}
@Override
public void autoDelivery(Integer userId) {
// 获取需要自动发货的数据
List<TtDeliveryRecord> deliveryRecordList = new LambdaQueryChainWrapper<>(deliveryRecordMapper)
.eq(TtDeliveryRecord::getUserId, userId)
.eq(TtDeliveryRecord::getDelivery, "2") // 网站发货模式(1人工发货 2自动发货 3主播号提取)
.eq(TtDeliveryRecord::getStatus, "0") // 发货订单状态(0发起提货 1待发货 3待收货 10订单完成 11订单取消)
.list();
if (StringUtils.isNull(deliveryRecordList) || deliveryRecordList.isEmpty()) {
log.info("{}号用户发起提货,无自动发货饰品,请在后台手动发货!", userId);
return;
}
// 饰品购买价格溢价率
String buyPricePremiumRateStr = configService.selectConfigByKey("buyPricePremiumRate");
BigDecimal buyPricePremiumRate = new BigDecimal(buyPricePremiumRateStr);
// ZBT平台币种汇率
String ZBTParitiesStr = configService.selectConfigByKey("ZBTParities");
BigDecimal ZBTParities = new BigDecimal(ZBTParitiesStr);
// 快速购买
for (TtDeliveryRecord ttDeliveryRecord : deliveryRecordList) {
// 获取用户信息
TtUser ttUser = userMapper.selectById(ttDeliveryRecord.getUserId());
// 获取饰品信息
TtOrnament ttOrnament = ornamentsMapper.selectById(ttDeliveryRecord.getOrnamentId());
// 计算可接受最高价
BigDecimal ornamentsPrice = ttDeliveryRecord.getOrnamentsPrice(); // 饰品价格
BigDecimal price = ornamentsPrice.divide(ZBTParities, 2, RoundingMode.HALF_UP).multiply(buyPricePremiumRate);
// 构建购买参数
QuickBuyParamV2DTO quickBuyParamV2DTO = new QuickBuyParamV2DTO();
// TODO: 2024/3/30 ttOrnament.getId()
quickBuyParamV2DTO.setItemId(String.valueOf(ttOrnament.getId()));
quickBuyParamV2DTO.setMaxPrice(price.toString()); // 购买a可以接受的最高价格(T币)
quickBuyParamV2DTO.setOutTradeNo(ttDeliveryRecord.getOutTradeNo());
quickBuyParamV2DTO.setTradeUrl(ttUser.getTransactionLink());
ResultZbt<OpenBuyResultDTO> tradeQuickBuy = zbtService.tradeQuickBuy(quickBuyParamV2DTO);
if (!tradeQuickBuy.getSuccess()) {
log.error("快速购买接口v2异常,订单号:{}", ttDeliveryRecord.getOutTradeNo());
continue;
}
// 获取结果数据
OpenBuyResultDTO data = tradeQuickBuy.getData();
// 更新提货订单数据
ttDeliveryRecord.setBuyPrice(data.getBuyPrice());
ttDeliveryRecord.setThirdpartyDelivery(data.getDelivery());
ttDeliveryRecord.setOrderId(data.getOrderId());
ttDeliveryRecord.setStatus(DELIVERY_BEFORE.getCode()); // 发货订单状态 1待发货
ttDeliveryRecord.setMessage("等待发货");
ttDeliveryRecord.setUpdateBy("自动发货");
ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate());
// 更新状态
if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) {
log.info("订单'{}'自动发货成功,等待卖家发货中......", ttDeliveryRecord.getOutTradeNo());
}
}
}
public List<AvailableMarketOrnamentVO> ZBTGetAvailable(String marketHashName, ProductListParams productListParams){
LambdaQueryWrapper<TtOrnament> ttOrnamentsQuery = new LambdaQueryWrapper<>();
ttOrnamentsQuery.eq(TtOrnament::getMarketHashName, marketHashName);
TtOrnament ornament = ttOrnamentService.getOne(ttOrnamentsQuery);
// zbt所有数据
productListParams.setItemId(String.valueOf(ornament.getId()));
ResultZbt<AvailableMarketList> availableZbt = zbtService.productList(productListParams);
if (!availableZbt.getSuccess()){
log.warn(availableZbt.getErrorMsg());
return null;
}
AvailableMarketList data = availableZbt.getData();
return data.getList().stream().map(item->{
return AvailableMarketOrnamentVO.builder()
.platform(Platform.ZBT.getCode())
.itemId(item.getItemId())
.delivery(item.getDelivery())
.itemName(item.getItemName())
.imageUrl(item.getImageUrl())
.cnyPrice(item.getCnyPrice())
.price(item.getPrice())
.build();
}).collect(Collectors.toList());
}
public List<AvailableMarketOrnamentVO> YYGetAvailable(String marketHashName, ProductListParams productListParams){
LambdaQueryWrapper<TtYYOrnaments> ttYYOrnamentsQuery = new LambdaQueryWrapper<>();
ttYYOrnamentsQuery.eq(TtYYOrnaments::getHashName,marketHashName);
TtYYOrnaments yyOrnament = ttYyOrnamentsService.getOne(ttYYOrnamentsQuery);
HashMap<String, Object> map = new HashMap<>();
JSONObject o = new JSONObject();
o.put("templateId",yyOrnament.getId());
log.info("obj="+o.toString());
map.put("requestList", ListUtil.of(o));
YYResult yyResult = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(map);
Object dataJson = yyResult.getData();
ObjectMapper objectMapper = new ObjectMapper();
queryTemplateSaleByCategoryDataVO dataVO;
try {
dataVO = objectMapper.readValue(JSON.toJSONString(dataJson), queryTemplateSaleByCategoryDataVO.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
List<queryTemplateSaleByCategoryVO> voList = dataVO.getSaleTemplateByCategoryResponseList();
List<AvailableMarketOrnamentVO> collect = voList.stream().map(item -> {
return AvailableMarketOrnamentVO.builder()
.platform(Platform.YY.getCode())
.templateId(item.getTemplateId())
.imageUrl(item.getIconUrl())
.build();
}).collect(Collectors.toList());
return null;
}
@Override
public List<AvailableMarketOrnamentVO> getAvailableMarketListByHashName(String marketHashName, ProductListParams productListParams) {
// zbt所有数据
List<AvailableMarketOrnamentVO> ZBTGetAvailable = ZBTGetAvailable(marketHashName, productListParams);
// yy所有数据
List<AvailableMarketOrnamentVO> YYGetAvailable = YYGetAvailable(marketHashName, productListParams);
// 合并所有数据
return null;
// // 获取饰品itemId
// TtOrnaments ornaments = new LambdaQueryChainWrapper<>(ornamentsMapper).eq(TtOrnaments::getId, ornamentsId).one();
// // 构建查询参数
// productListParams.setItemId(ornaments.getItemId());
// ResultZbt<AvailableMarketList> productList = zbtService.productList(productListParams); // 查询接口
// if (!productList.getSuccess()) {
// log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!");
// return null;
// }
// AvailableMarketList availableMarketList = productList.getData();
// // 查询到的数据
// List<AvailableMarket> list = availableMarketList.getList();
// // 构建返回结果
// List<AvailableMarketOrnamentsListDataVO> resultList = new ArrayList<>();
// // 提取可用数据
// for (AvailableMarket availableMarket : list) {
// AvailableMarketOrnamentsListDataVO data = AvailableMarketOrnamentsListDataVO.builder().build();
// data.setId(availableMarket.getId());
// data.setDelivery(availableMarket.getDelivery());
// data.setItemName(availableMarket.getItemName());
// data.setImageUrl(availableMarket.getImageUrl());
// data.setCnyPrice(availableMarket.getCnyPrice());
// data.setPrice(availableMarket.getPrice());
// resultList.add(data);
// }
// return resultList;
}
@Override
public R getAvailableMarketList(GetAvailableMarketListParam param) {
// 返回结果
List<OnSaleOrnamentVO> resultList = new ArrayList<>();
// 获取饰品信息(这里暂时不支持批量查询)
List<TtOrnament> list = new LambdaQueryChainWrapper<>(ornamentsMapper)
.in(TtOrnament::getId, param.getOrnamentsId().get(0))
.list();
if (param.getPartyType().equals(PartyType.ZBT.getCode())){
for (TtOrnament item: list){
if (ObjectUtil.isEmpty(item.getZbtId())) continue;
// 构建查询参数
ProductListParams apiParm = ProductListParams.builder()
.itemId(String.valueOf(item.getZbtId()))
.build();
// 查询接口
ResultZbt<AvailableMarketList> productList = zbtService.productList(apiParm);
if (!productList.getSuccess()) {
log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!");
return R.fail("调用ZBT获取某一个饰品的所有在售接口异常");
}
// 过滤可用数据
AvailableMarketList availableMarketList = productList.getData();
List<AvailableMarket> list1 = availableMarketList.getList();
if (ObjectUtil.isEmpty(list1)) {
return R.fail("平台没有该物品在售信息。");
}
// 提取可用数据
for (AvailableMarket availableMarket : list1) {
OnSaleOrnamentVO data = OnSaleOrnamentVO.builder().build();
data.setPartyType(PartyType.ZBT.getCode());
data.setId(availableMarket.getId());
data.setDelivery(availableMarket.getDelivery());
data.setItemName(availableMarket.getItemName());
data.setImageUrl(availableMarket.getImageUrl());
data.setCnyPrice(availableMarket.getCnyPrice());
data.setPrice(availableMarket.getPrice());
resultList.add(data);
}
}
} else if (param.getPartyType().equals(PartyType.YY_YOU_PING.getCode())){
// 构建查询参数
HashMap<String, Object> apiParam = new HashMap<>();
ArrayList<Map<String,String>> ids = new ArrayList<>();
for (TtOrnament item : list){
Map<String, String> i = MapUtil.builder("templateId", String.valueOf(item.getYyyoupingId())).build();
ids.add(i);
}
apiParam.put("requestList",ids);
// 查询接口
YYResult yyResult = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(apiParam);
if (!yyResult.isSuccess()) {
log.error("调用ZBT获取某一个饰品的所有在售接口异常,请检查代码是否正确!");
return R.fail("调用YY获取某一个饰品的所有在售接口异常");
}
// 过滤可用数据
List<String> data = JSONUtil.toList(JSON.toJSONString(yyResult.getData()), String.class);
resultList = data.stream().map(item->{
cn.hutool.json.JSONObject obj = JSONUtil.parseObj(item);
cn.hutool.json.JSONObject saleTemplateResponse = obj.get("saleTemplateResponse", cn.hutool.json.JSONObject.class);
cn.hutool.json.JSONObject saleCommodityResponse = obj.get("saleCommodityResponse", cn.hutool.json.JSONObject.class);
return OnSaleOrnamentVO.builder()
.partyType(PartyType.YY_YOU_PING.getCode())
.id(saleTemplateResponse.get("templateId",String.class))
.itemName(saleCommodityResponse.get("name",String.class))
.imageUrl(list.get(0).getImageUrl())
.price(saleCommodityResponse.get("minSellPrice",BigDecimal.class))
.build();
}).collect(Collectors.toList());
}
return R.ok(resultList);
}
@Override
public R tradeConfirm(TradeManualConfirmParam param) {
// 获取提货订单数据
TtDeliveryRecord ttDeliveryRecord = deliveryRecordMapper.selectById(param.getDeliveryRecordId());
// 更新提货订单数据
ttDeliveryRecord.setBuyPrice(param.getPrice());
ttDeliveryRecord.setThirdpartyDelivery(1);
ttDeliveryRecord.setOrderId(param.getOrderNo());
ttDeliveryRecord.setStatus(param.getStatus()); // 发货订单状态 1待发货
ttDeliveryRecord.setMessage(param.getMessage());
ttDeliveryRecord.setUpdateBy(SecurityUtils.getUsername());
ttDeliveryRecord.setUpdateTime(DateUtils.getNowDate());
if (deliveryRecordMapper.updateById(ttDeliveryRecord) > 0) {
return R.ok();
}
return R.fail("更新提货订单数据异常!");
}
}
@@ -0,0 +1,51 @@
package com.ruoyi.thirdparty.common.service.impl;
import com.ruoyi.domain.common.constant.MessageCategoryEnum;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.message.service.MessageService;
import com.ruoyi.thirdparty.common.service.RechargeSuccessfulNoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class RechargeSuccessfulNoticeServiceImpl implements RechargeSuccessfulNoticeService {
/*@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void sendRechargeSuccessNotice(String userId, BigDecimal amount) {
try {
Map<String, String> noticeMap = new HashMap<>();
noticeMap.put("userId", userId);
noticeMap.put("title", "充值到账通知");
noticeMap.put("content", "您充值" + amount + "已到账,请注意查收。");
noticeMap.put("createTime", DateUtils.getTime());
rabbitTemplate.convertAndSend("notice_queue", noticeMap);
} catch (Exception e) {
log.error("happen exception is {}", e.getMessage(), e);
}
}*/
@Autowired
private MessageService messageService;
@Override
public void sendRechargeSuccessNotice(String userId, BigDecimal amount) {
AsyncManager.me().run(() -> {
try {
String title = MessageCategoryEnum.SYS_RECHARGE.getTitle();
String content = "您充值" + amount + "金币已到账,请注意查收。";
messageService.sendNotice(Long.valueOf(userId), title, content, MessageCategoryEnum.SYS_RECHARGE.getCode());
} catch (Exception e) {
log.error("发送充值通知失败,userId={}, error={}", userId, e.getMessage(), e);
}
});
}
}
@@ -0,0 +1,606 @@
package com.ruoyi.thirdparty.common.task;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.math.MathUtil;
import cn.hutool.core.util.HashUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.ruoyi.admin.service.ConfigService;
import com.ruoyi.admin.service.TtOrnamentService;
import com.ruoyi.admin.service.TtOrnamentYYService;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.domain.common.constant.PartyType;
import com.ruoyi.domain.dto.yyyouping.OrnamentYY;
import com.ruoyi.domain.dto.zbt.OrnamentZBT;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.system.mapper.SysDictDataMapper;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.common.NetworkDataLoader.NetworkDataLoader;
import com.ruoyi.thirdparty.yyyouping.service.yyyoupingService;
import com.ruoyi.thirdparty.yyyouping.utils.common.YYResult;
import com.ruoyi.thirdparty.zbt.param.SearchParams;
import com.ruoyi.thirdparty.zbt.result.ResultZbt;
import com.ruoyi.thirdparty.zbt.result.product.SearchData;
import com.ruoyi.thirdparty.zbt.service.ZBTService;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import java.io.File;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.sql.SQLIntegrityConstraintViolationException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.PartyType.YY_YOU_PING;
import static com.ruoyi.domain.common.constant.PartyType.ZBT;
@Slf4j
// 项目启动加载所有饰品信息
//@Component
public class LoadAllOrnaments implements ApplicationRunner {
// @Value("${yy-youping.ornamentsFilePath}")
// private String YYOrnamentsFilePath;
//
// @Autowired
// private yyyoupingService yyyoupingService;
@Autowired
private TtOrnamentService ttOrnamentService;
@Value("${mkcsgo.startLoadOrnaments}")
private Boolean startLoadOrnaments;
// 资源加载器集合
@Autowired
private List<NetworkDataLoader> networkDataLoaderList;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("是否加载网络ornaments资源{}", startLoadOrnaments);
if (!startLoadOrnaments) return;
// 1 加载各个平台数据
Set<TtOrnament> ttOrnaments = new HashSet<>();
for (NetworkDataLoader loader : networkDataLoaderList) {
Set<TtOrnament> load = loader.load();
ttOrnaments.addAll(load);
}
if (ObjectUtil.isEmpty(ttOrnaments) || ttOrnaments.isEmpty()) {
log.warn("加载饰品网络资源失败。");
return;
}
// 2 写db
log.info("db allData ing~~~");
for (TtOrnament item : ttOrnaments) {
try {
ttOrnamentService.saveOrUpdate(item);
} catch (DuplicateKeyException e) {
// log.info("过滤重复数据 【id】{}【hash】{}save。",item.getId(),item.getMarketHashName());
} catch (Exception e) {
e.printStackTrace();
log.info("保存数据异常。");
break;
}
}
log.info("加载饰品网络资源成功。");
}
// 从平台获取数据
// private Set<TtOrnament> loadOrnamentFromNet(PartyType[] partyTypes) {
//
// // 加载必要的枚举数据
// List<String> dictTypes = Arrays.asList(
// "ornaments_type", "ornaments_type_name",
// "ornaments_exterior", "ornaments_exterior_name",
// "ornaments_quality", "ornaments_quality_name",
// "ornaments_rarity", "ornaments_rarity_name");
// List<SysDictData> enumList = dictDataMapper.selectDictDataByTypes(dictTypes);
//
// Set<TtOrnament> res = null;
// for (NetworkDataLoader loader:networkDataLoaderList){
// Set<TtOrnament> load = loader.load();
// res.addAll(load);
// }
//
// return res;
//
// //1 加载ZBT平台数据
//
// //... 加载...平台数据
//
// //2 合并所有数据
//
// // //1 从一个平台获取第一组基础数据
// // Set<TtOrnament> baseData = loadBaseData(partyTypes[1],enumList);
// // if (ObjectUtil.isEmpty(baseData) || baseData.isEmpty()) return baseData;
// //
// // //2 整合其他平台
// // return mergeOtherParty(baseData, partyTypes, enumList);
//
// }
//
// public Set<TtOrnament> mergeOtherParty(Set<TtOrnament> baseData,PartyType[] partyTypes ,List<SysDictData> enumList){
//
// // Integer f = 0;
// for (int i = 1; i < partyTypes.length; i++) {
// if (partyTypes[i].equals(YY_YOU_PING)) {
//
// YYResult yyResult = yyyoupingService.yyApiTemplateQuery(new HashMap<String, Object>());
// String fileUrl = (String) yyResult.getData();
// log.info(fileUrl);
//
// // 下载文件
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
// String format = dateFormat.format(System.currentTimeMillis());
// String fileName = YYOrnamentsFilePath + "yy_orn_" + format + ".txt";
//
// long size = HttpUtil.downloadFile(fileUrl, FileUtil.file(fileName));
// log.info("yy饰品信息下载成功,文件大小: " + size);
//
// // 解析json
// File file = FileUtil.file(fileName);
// JSONArray jsonArray = JSONUtil.readJSONArray(file, StandardCharsets.UTF_8);
// List<OrnamentYY> yyList = jsonArray.toList(OrnamentYY.class);
//
// // 转换为TtOrnament
// Set<TtOrnament> collect = yyList.stream().map(item -> {
//
// return TtOrnament.builder()
// .id(Long.valueOf(item.getHashName().hashCode()))
// .yyyoupingId(item.getId())
// .marketHashName(item.getHashName())
// .name(item.getName())
// .price(BigDecimal.TEN) //// TODO: 2024/4/1 补充价格信息
// .usePrice(BigDecimal.TEN)
// .updateTime(item.getUpdateTime())
// .build();
//
// }).collect(Collectors.toSet());
//
// baseData.addAll(collect);
//
// } else if (partyTypes[i].equals(ZBT)) {
//
// // 饰品类型枚举
// //List<SysDictData> typeList = dictDataMapper.selectDictDataByType("ornaments_type");
//
// for (SysDictData type : enumList) {
//
// if (!type.getDictType().equals("ornaments_type")) continue;
//
// Set<TtOrnament> ttOrnaments = ZBTLoadOrnamentByType(type.getDictLabel(),type,enumList);
//
// baseData.addAll(ttOrnaments);
//
// log.info("合并其他平台{}条数据。",ttOrnaments.size());
//
// }
//
// } else {
// log.warn("非法的平台类型代码,获取饰品数据失败。");
// }
// }
//
// // 过滤zbt没有的数据
// log.info("过滤数据");
// Set<TtOrnament> resData = baseData.stream().filter(item -> {
// return ObjectUtil.isNotEmpty(item.getZbtId());
// }).collect(Collectors.toSet());
//
// return resData;
//
// }
//
// // zbt 根据饰品类型获取数据
// private Set<TtOrnament> ZBTLoadOrnamentByType(String typeHash,SysDictData type,List<SysDictData> allEnum){
//
// HashSet<TtOrnament> result = new HashSet<>();
//
// // 分批请求api
// int page = 1;
// boolean stopFlag = false;
// while (true) {
//
// // 结束标记
// if (stopFlag) break;
//
// // 请求平台数据
// SearchParams param = SearchParams.builder()
// .page(String.valueOf(page))
// .category(typeHash)
// .limit("200")
// .build();
// ResultZbt<SearchData> response = zbtService.search(param);
// SearchData data = response.getData();
// if (!response.getSuccess() || ObjectUtil.isEmpty(data)) break;
//
// List<OrnamentZBT> list = JSONUtil.toList(JSONUtil.toJsonStr(data.getList()), OrnamentZBT.class);
//
// if (list.size() < 200) stopFlag = !stopFlag;
//
// // 转换为通用类型
// Set<TtOrnament> collect = list.stream().map(item -> {
//
// Long id = Long.valueOf(HashUtil.rsHash(item.getMarketHashName()));
// if (id < 0) id = Math.abs(id);
//
// TtOrnament ornament = TtOrnament.builder()
// .id(id)
// .zbtId(item.getItemId())
// .marketHashName(item.getMarketHashName())
// .name(item.getItemName())
// .shortName(item.getShortName())
//
// .price(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getPrice() : null)
// .usePrice(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getPrice() : null)
// .quantity(ObjectUtil.isNotEmpty(item.getPriceInfo()) ? item.getPriceInfo().getQuantity() : null)
//
// .imageUrl(item.getImageUrl())
//
// .type(type.getDictValue())
// .typeName(item.getTypeName())
//
// .quality(getCodeByNameOrHash(allEnum,item.getQuality()))
// .qualityName(item.getQualityName())
// .qualityColor(item.getQualityColor())
//
// .rarity(getCodeByNameOrHash(allEnum,item.getRarity()))
// .rarityName(item.getRarityName())
// .rarityColor(item.getRarityColor())
//
// .exterior(getCodeByNameOrHash(allEnum,item.getExterior()))
// .exteriorHashName(item.getExterior())
// .exteriorName(item.getExteriorName())
//
// .updateTime(item.getUpdateTime())
// .build();
//
// ornament.setUsePrice(ornament.getPrice());
//
// return ornament;
// }).collect(Collectors.toSet());
//
// result.addAll(collect);
//
// // log.info("加载【扎比特】{}条类型为{}的饰品数据。",page*collect.size(),typeHash);
// System.out.print("\r加载【扎比特】"+page*collect.size()+"条类型为"+typeHash+"的饰品数据。");
//
// page++;
// }
//
// return result;
//
// }
//
// // 从yy平台获取第一组基础数据
// private Set<TtOrnament> loadBaseData(PartyType partyType,List<SysDictData> allEnum) {
//
// Set<TtOrnament> baseData = null;
//
// if (partyType.equals(YY_YOU_PING)) {
// baseData = getBaseDataFromYY(allEnum);
// } else if (partyType.equals(ZBT)) {
//
// baseData = getBaseDataFromZBT(allEnum);
//
// } else {
// log.warn("非法的平台类型代码,获取饰品数据失败。");
// return baseData;
// }
//
// return baseData;
// }
//
// // 从ZBT获取基础数据
// public Set<TtOrnament> getBaseDataFromZBT(List<SysDictData> allEnum){
//
// Set<TtOrnament> baseData = null;
//
// YYResult yyResult = yyyoupingService.yyApiTemplateQuery(new HashMap<String, Object>());
// if (!yyResult.isSuccess()) {
// log.warn(yyResult.getMsg());
// return baseData;
// }
// String fileUrl = (String) yyResult.getData();
// log.info("yyFile=" + fileUrl);
//
// // 下载文件
// String fileName = YYOrnamentsFilePath + "yyOrn" + System.currentTimeMillis() + ".txt";
// System.out.println(fileName);
//
// long size = HttpUtil.downloadFile(fileUrl, new File(fileName));
// log.info("yy饰品信息下载成功,文件大小: " + size);
//
// // 解析json
// File file = FileUtil.file(fileName);
// JSONArray jsonArray = JSONUtil.readJSONArray(file, StandardCharsets.UTF_8);
// List<OrnamentYY> yyList = jsonArray.toList(OrnamentYY.class);
//
// log.info("补充详细信息~~~");
// // 转换为TtOrnament
// baseData = yyList.stream().map(item -> {
//
// Long id = Long.valueOf(HashUtil.rsHash(item.getHashName()));
// if (id < 0) {
// id = Math.abs(id);
// }
//
// TtOrnament ornament = TtOrnament.builder()
// .id(id)
// .yyyoupingId(item.getId())
// .marketHashName(item.getHashName())
//
// .type(getCodeByNameOrHash(allEnum,item.getTypeHashName()))
// .typeName(item.getTypeName())
// .typeHashName(item.getTypeHashName())
//
// .name(item.getName())
// .shortName(item.getName())
//
// .isPutaway("1")
// .isProprietaryProperty("1")
// .updateTime(item.getUpdateTime())
// .build();
//
// return ornament;
// }).collect(Collectors.toSet());
//
// // 分批补充 补充其他信息
// List<Map<String, String>> requestList = baseData.stream().map(item -> {
// return MapUtil.builder("templateId", String.valueOf(item.getYyyoupingId())).build();
// }).collect(Collectors.toList());
//
// int page = 0;
// int pageSize = 200;
// Boolean stopFlag = false;
// ArrayList<TtOrnament> ornamentsList = new ArrayList<>(baseData);
// while (true) {
//
// if (stopFlag) {
// System.out.println("-100% 补充完成✔");
// break;
// }
//
// BigDecimal p = new BigDecimal(page * pageSize)
// .divide(new BigDecimal(baseData.size()),2,BigDecimal.ROUND_DOWN);
// System.out.print("\r补充"+page * pageSize+"条基础记录"+p.multiply(new BigDecimal(100)).setScale(0)+"%");
//
//
// List<Map<String, String>> reqList = null;
// try {
// reqList = requestList.subList(page * pageSize, page * pageSize + pageSize); // 尽量大
// } catch (Exception e) {
// int lenth = requestList.size();
// reqList = requestList.subList(page * pageSize, lenth - 1); // 尽量大
// stopFlag = !stopFlag;
// }
//
// HashMap<String, Object> map = new HashMap<>();
// map.put("requestList", reqList);
//
// YYResult yyResult1 = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(map);
// List<JSONObject> list = JSONUtil.toList(JSONUtil.toJsonStr(yyResult1.getData()), JSONObject.class);
//
// for (JSONObject obj : list) {
//
// JSONObject saleTemplateResponse = JSONUtil.parseObj(obj.get("saleTemplateResponse"));
// JSONObject saleCommodityResponse = JSONUtil.parseObj(obj.get("saleCommodityResponse"));
//
// String templateId = String.valueOf((Integer) saleTemplateResponse.get("templateId"));
//
// for (int i = 0; i < ornamentsList.size(); i++) {
//
// TtOrnament ornament = ornamentsList.get(i);
// if (templateId.equals(String.valueOf(ornament.getYyyoupingId()))) {
//
// ornament.setPrice(new BigDecimal(saleCommodityResponse.get("minSellPrice").toString()));
// ornament.setUsePrice(new BigDecimal(saleCommodityResponse.get("minSellPrice").toString()));
//
// ornament.setImageUrl(saleTemplateResponse.get("iconUrl").toString());
//
// Object exteriorName = saleTemplateResponse.get("exteriorName");
// if (ObjectUtil.isNotNull(exteriorName)){
// ornament.setExterior(getCodeByNameOrHash(allEnum,exteriorName.toString()));
// ornament.setExteriorName(exteriorName.toString());
// }
//
// ornament.setQuality(getCodeByNameOrHash(allEnum,saleTemplateResponse.get("qualityName").toString()));
// ornament.setQualityName(saleTemplateResponse.get("qualityName").toString());
// // if (ObjectUtil.isEmpty(ornament.getQuality()) && ObjectUtil.isNotEmpty(ornament.getQualityName())){
// // System.out.println(ornament.getQuality());
// // System.out.println(ornament.getQualityName());
// // }
//
// ornament.setRarity(getCodeByNameOrHash(allEnum,saleTemplateResponse.get("rarityName").toString()));
// ornament.setRarityName(saleTemplateResponse.get("rarityName").toString());
//
// ornamentsList.set(i,ornament);
//
// break;
// }
// }
// }
//
// page++;
//
// }
//
// baseData = new HashSet<>(ornamentsList);
//
// return baseData;
// }
//
// // 从yy获取基础数据
// public Set<TtOrnament> getBaseDataFromYY(List<SysDictData> allEnum){
//
// Set<TtOrnament> baseData = null;
//
// YYResult yyResult = yyyoupingService.yyApiTemplateQuery(new HashMap<String, Object>());
// if (!yyResult.isSuccess()) {
// log.warn(yyResult.getMsg());
// return baseData;
// }
// String fileUrl = (String) yyResult.getData();
// log.info("yyFile=" + fileUrl);
//
// // 下载文件
// String fileName = YYOrnamentsFilePath + "yyOrn" + System.currentTimeMillis() + ".txt";
// System.out.println(fileName);
//
// long size = HttpUtil.downloadFile(fileUrl, new File(fileName));
// log.info("yy饰品信息下载成功,文件大小: " + size);
//
// // 解析json
// File file = FileUtil.file(fileName);
// JSONArray jsonArray = JSONUtil.readJSONArray(file, StandardCharsets.UTF_8);
// List<OrnamentYY> yyList = jsonArray.toList(OrnamentYY.class);
//
// log.info("补充详细信息~~~");
// // 转换为TtOrnament
// baseData = yyList.stream().map(item -> {
//
// Long id = Long.valueOf(HashUtil.rsHash(item.getHashName()));
// if (id < 0) {
// id = Math.abs(id);
// }
//
// TtOrnament ornament = TtOrnament.builder()
// .id(id)
// .yyyoupingId(item.getId())
// .marketHashName(item.getHashName())
//
// .type(getCodeByNameOrHash(allEnum,item.getTypeHashName()))
// .typeName(item.getTypeName())
// .typeHashName(item.getTypeHashName())
//
// .name(item.getName())
// .shortName(item.getName())
//
// .isPutaway("1")
// .isProprietaryProperty("1")
// .updateTime(item.getUpdateTime())
// .build();
//
// return ornament;
// }).collect(Collectors.toSet());
//
// // 分批补充 补充其他信息
// List<Map<String, String>> requestList = baseData.stream().map(item -> {
// return MapUtil.builder("templateId", String.valueOf(item.getYyyoupingId())).build();
// }).collect(Collectors.toList());
//
// int page = 0;
// int pageSize = 200;
// Boolean stopFlag = false;
// ArrayList<TtOrnament> ornamentsList = new ArrayList<>(baseData);
// while (true) {
//
// if (stopFlag) {
// System.out.println("-100% 补充完成✔");
// break;
// }
//
// BigDecimal p = new BigDecimal(page * pageSize)
// .divide(new BigDecimal(baseData.size()),2,BigDecimal.ROUND_DOWN);
// System.out.print("\r补充"+page * pageSize+"条基础记录"+p.multiply(new BigDecimal(100)).setScale(0)+"%");
//
//
// List<Map<String, String>> reqList = null;
// try {
// reqList = requestList.subList(page * pageSize, page * pageSize + pageSize); // 尽量大
// } catch (Exception e) {
// int lenth = requestList.size();
// reqList = requestList.subList(page * pageSize, lenth - 1); // 尽量大
// stopFlag = !stopFlag;
// }
//
// HashMap<String, Object> map = new HashMap<>();
// map.put("requestList", reqList);
//
// YYResult yyResult1 = yyyoupingService.yyApiBatchGetOnSaleCommodityInfo(map);
// List<JSONObject> list = JSONUtil.toList(JSONUtil.toJsonStr(yyResult1.getData()), JSONObject.class);
//
// for (JSONObject obj : list) {
//
// JSONObject saleTemplateResponse = JSONUtil.parseObj(obj.get("saleTemplateResponse"));
// JSONObject saleCommodityResponse = JSONUtil.parseObj(obj.get("saleCommodityResponse"));
//
// String templateId = String.valueOf((Integer) saleTemplateResponse.get("templateId"));
//
// for (int i = 0; i < ornamentsList.size(); i++) {
//
// TtOrnament ornament = ornamentsList.get(i);
// if (templateId.equals(String.valueOf(ornament.getYyyoupingId()))) {
//
// ornament.setPrice(new BigDecimal(saleCommodityResponse.get("minSellPrice").toString()));
// ornament.setUsePrice(new BigDecimal(saleCommodityResponse.get("minSellPrice").toString()));
//
// ornament.setImageUrl(saleTemplateResponse.get("iconUrl").toString());
//
// Object exteriorName = saleTemplateResponse.get("exteriorName");
// if (ObjectUtil.isNotNull(exteriorName)){
// ornament.setExterior(getCodeByNameOrHash(allEnum,exteriorName.toString()));
// ornament.setExteriorName(exteriorName.toString());
// }
//
// ornament.setQuality(getCodeByNameOrHash(allEnum,saleTemplateResponse.get("qualityName").toString()));
// ornament.setQualityName(saleTemplateResponse.get("qualityName").toString());
// // if (ObjectUtil.isEmpty(ornament.getQuality()) && ObjectUtil.isNotEmpty(ornament.getQualityName())){
// // System.out.println(ornament.getQuality());
// // System.out.println(ornament.getQualityName());
// // }
//
// ornament.setRarity(getCodeByNameOrHash(allEnum,saleTemplateResponse.get("rarityName").toString()));
// ornament.setRarityName(saleTemplateResponse.get("rarityName").toString());
//
// ornamentsList.set(i,ornament);
//
// break;
// }
// }
// }
//
// page++;
//
// }
//
// baseData = new HashSet<>(ornamentsList);
//
// return baseData;
// }
//
// public static void main(String[] args) {
// System.out.println("StatTrak™".equals(""));
// }
//
// // 根据类型和标签获取code
// private String getCodeByNameOrHash(List<SysDictData> typeList,String nameOrHash){
//
// for (SysDictData type : typeList){
// if (type.getDictLabel().equals(nameOrHash)) return type.getDictValue();
// if (type.getDictType().equals(nameOrHash)) return type.getDictValue();
// }
// return null;
// }
}
@@ -0,0 +1,22 @@
package com.ruoyi.thirdparty.cs340.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "cs340")
public class Cs340Config {
private String baseUrl;
private String appKey;
private String privateKey;
private String publicKey;
private String callbackPublicKey;
}
@@ -0,0 +1,28 @@
package com.ruoyi.thirdparty.cs340.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.thirdparty.cs340.domain.result.Cs340CallBackNotifyInfo;
import com.ruoyi.thirdparty.cs340.service.Cs340Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* cs2pifa 发货回调接口(对外暴露,供第三方平台回调)
*/
@RestController
@RequestMapping("/api/cs2pifa")
public class Cs2pifaCallbackController {
@Autowired
private Cs340Service cs340Service;
/**
* 接收 cs2pifa 平台发货回调通知
*/
@Anonymous
@PostMapping("/notify")
public AjaxResult notify(@RequestBody Cs340CallBackNotifyInfo notify) {
return cs340Service.notify(notify);
}
}
@@ -0,0 +1,77 @@
package com.ruoyi.thirdparty.cs340.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.thirdparty.cs340.domain.body.ByTemplateCreateOrder;
import com.ruoyi.thirdparty.cs340.domain.body.Cs340BatchQueryPricesBody;
import com.ruoyi.thirdparty.cs340.domain.body.Cs340CheckTradeUrlBody;
import com.ruoyi.thirdparty.cs340.domain.body.Cs340OnSaleInfoBody;
import com.ruoyi.thirdparty.cs340.domain.result.BatchGetOnSaleCommodityInfoResult;
import com.ruoyi.thirdparty.cs340.domain.result.Cs340CallBackNotifyInfo;
import com.ruoyi.thirdparty.cs340.service.Cs340Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/admin/cs340item")
public class Cs340ItemController extends BaseController {
@Autowired
private Cs340Service cs340Service;
@PostMapping("/checkTradeUrl")
public AjaxResult checkTradeUrl(@RequestBody Cs340CheckTradeUrlBody body) {
return success(cs340Service.checkTradeUrl(body.getTradeUrl()));
}
@GetMapping("/checkMerchantBalance")
public AjaxResult checkMerchantBalance() {
return success(cs340Service.checkMerchantBalance());
}
/**
* 查询库存
*/
@PostMapping("/queryOnSaleInfo")
public AjaxResult queryOnSaleInfo(@Validated @RequestBody Cs340OnSaleInfoBody onSaleInfoBody) {
BatchGetOnSaleCommodityInfoResult result = cs340Service.queryOnSaleInfo(onSaleInfoBody);
if (result == null || result.getSaleCommodityResponse() == null) {
return AjaxResult.error("cs2pifa平台未查询到该饰品的在售信息,请确认饰品名称(hashName)是否正确,或稍后再试");
}
return success(result);
}
@PostMapping("/createOrder")
public AjaxResult createOrder(@RequestBody ByTemplateCreateOrder createOrderBody) {
return success(cs340Service.createOrder(createOrderBody));
}
@PostMapping("/notify")
public AjaxResult notify(@RequestBody Cs340CallBackNotifyInfo notify) {
return cs340Service.notify(notify);
}
/**
* 批量实时查询三方最低在售价格
*/
@PostMapping("/batchQueryPrices")
public AjaxResult batchQueryPrices(@Validated @RequestBody Cs340BatchQueryPricesBody body) {
List<BatchGetOnSaleCommodityInfoResult> results = cs340Service.batchGetOnSaleCommodityInfoResult(body.getHashNames());
Map<String, String> priceMap = new HashMap<>();
for (BatchGetOnSaleCommodityInfoResult result : results) {
if (result.getSaleTemplateResponse() != null
&& result.getSaleCommodityResponse() != null
&& result.getSaleCommodityResponse().getMinSellPrice() != null) {
priceMap.put(
result.getSaleTemplateResponse().getTemplateHashName(),
result.getSaleCommodityResponse().getMinSellPrice());
}
}
return success(priceMap);
}
}
@@ -0,0 +1,27 @@
package com.ruoyi.thirdparty.cs340.domain.body;
import lombok.Data;
/**
* merchantOrderNo String 是 商户订单号 小于等于59位的字符串
* tradeLinks String 是 收货方的Steam交易链接
* commodityTemplateId String 是 商品模版ID
* 数据来源:查询模板ID templateId和hashName二选一必传递,同时传入以templateId为准,如传入不可为0
* commodityHashName String 是 模板hashname
* 数据来源:查询模板ID templateId和hashName二选一必传递,同时传入以templateId为准,如传入不可为空
* purchasePrice String 是 购买最高价 单位:元
* 不可为0
* fastShipping Integer 否 极速发货购买模式 0:优先购买极速发货;
* 1:只购买极速发货,该模式purchasePrice参数请使用fastShippingMinSellPrice作为价格,极速发货价格可通过批量查询在售商品价格或者批量查询在售商品详情接口获取,极速发货价格存在并且大于0可使用极速发货。
*/
@Data
public class ByTemplateCreateOrder {
private String merchantOrderNo;
private String tradeLinks;
private String commodityHashName;
private String purchasePrice;
}
@@ -0,0 +1,14 @@
package com.ruoyi.thirdparty.cs340.domain.body;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
@Data
public class Cs340BatchQueryPricesBody {
@NotEmpty(message = "hashNames不能为空")
private List<String> hashNames;
}
@@ -0,0 +1,9 @@
package com.ruoyi.thirdparty.cs340.domain.body;
import lombok.Data;
@Data
public class Cs340CheckTradeUrlBody {
private String tradeUrl;
}
@@ -0,0 +1,12 @@
package com.ruoyi.thirdparty.cs340.domain.body;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
@Data
public class Cs340OnSaleInfoBody {
@NotNull(message = "marketHashName不能为空")
private String marketHashName;
}
@@ -0,0 +1,16 @@
package com.ruoyi.thirdparty.cs340.domain.dto;
import lombok.Data;
import java.util.List;
@Data
public class BatchGetOnSaleCommodityInfoDto {
private String sign;
private String timestamp;
private String appKey;
List<CommodityInfoRequestListDto> requestList;
}
@@ -0,0 +1,38 @@
package com.ruoyi.thirdparty.cs340.domain.dto;
import lombok.Data;
/**
* merchantOrderNo String 是 商户订单号 小于等于59位的字符串
* tradeLinks String 是 收货方的Steam交易链接
* commodityHashName String 是 模板hashname&数据来源:查询模板ID templateId和hashName二选一必传递,同时传入以templateId为准,如传入不可为空
* purchasePrice String 是 购买最高价 单位:元 不可为0
* 请求示例
* {
* "sign":"4LL1O++Btn5JgZ6kHuJAN7dEG4cgLd0goo+Cb4wld0ygpU8lU+73mLxXYtN81ApAhWuw9zn+ENrtc2KcrL6XgHMSG8JC/5NjHCGO/3jDUjAac3YMh5vdQjsqhK1IoxdrOXM9QUyUj2kU9CA4eRey5wBx+BjbzHkSXXt6tOk9pDkeg56wuKR9L3g1BldcwBEacW2fU4pfEW9vpJC9QTCU/FAT0JXMTpYOaeIn+uf9eQRlRNguI1eUoWJAiGQgVQ/TlFlkdVqeE0pHp3Ntod2r+pkb4vCKrMDI6HW28XsM0uMupOfBUL8zRNFEOvLtswJduVo5r5Fd1ysHFcFywHOT0A==",
* "timestamp":"2023-01-17 17:59:00",
* "appKey":"123456",
* "merchantOrderNo": "20230112000",
* "tradeLinks": "https://steamcommunity.com/tradeoffer/new/?partner=123456&token=vP12QeQA",
* "commodityTemplateId": "100696",
* "commodityHashName": "",
* "purchasePrice": "0.08"
* }
*/
@Data
public class ByTemplateCreateOrderDto {
private String sign;
private String timestamp;
private String appKey;
private String merchantOrderNo;
private String tradeLinks;
private String commodityHashName;
private String purchasePrice;
}
@@ -0,0 +1,14 @@
package com.ruoyi.thirdparty.cs340.domain.dto;
import lombok.Data;
@Data
public class CheckMerchantBalanceDto {
private String sign;
private String timestamp;
private String appKey;
}
@@ -0,0 +1,16 @@
package com.ruoyi.thirdparty.cs340.domain.dto;
import lombok.Data;
@Data
public class CheckTradeUrlDto {
private String sign;
private String timestamp;
private String appKey;
private String tradeLinks;
}
@@ -0,0 +1,17 @@
package com.ruoyi.thirdparty.cs340.domain.dto;
import lombok.Data;
/**
* requestList Object[] 是 批量请求参数列表
* 数组数量:1-200
* templateId Integer 是 模板id
* 来源:查询模板ID templateId和hashName二选一必传递,如传入不可为0
* templateHashName String 是 模板hashName
* 来源:查询模板ID templateId和hashName二选一必传递,如传入不可为空
*/
@Data
public class CommodityInfoRequestListDto {
private Long templateId;
private String templateHashName;
}
@@ -0,0 +1,19 @@
package com.ruoyi.thirdparty.cs340.domain.dto;
import lombok.Data;
/**
* orderNo String 是 订单号 小于等于40位的数字字符串
* merchantNo String 是 商户订单号 数据来源:
* 指定模板购买
* 指定商品购买
* 注:orderNo和merchantNo二选一传递,同时传入以orderNo为准
* 小于等于59位的字符串
*/
@Data
public class Cs340OrderStatusDto {
private String sign;
private String timestamp;
private String appKey;
private String orderNo;
}
@@ -0,0 +1,29 @@
package com.ruoyi.thirdparty.cs340.domain.dto;
import lombok.Data;
/**
* typeId Integer 是 武器大类ID
* typeHashName String 是 武器大类hashName 如传入不可为空
* typeId和typeHashName同时传递时以typeId为准
* weaponId String 是 武器类型ID
* weaponHashName String 是 武器类型HashName 如传入不可为空
* weaponId和weaponHashName同时传递时以weaponId为准
* 武器大类和武器类型同时传递,以武器类型为准。
* priceRangeMinmun String 否 商品价格区间最小值 单位:元
* priceRangeMaxmun String 否 商品价格区间最大值 单位:元
* page String 否 页码 最大50
* pageSize String 否 每页显示数量 最大值200,若传入参数>200,则按200执行
*/
@Data
public class QueryTemplateSaleByCategoryDto {
private String sign;
private String timestamp;
private String appKey;
private Integer typeId;
private String typeHashName;
private String weaponId;
private String weaponHashName;
private String page;
private String pageSize;
}
@@ -0,0 +1,27 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
/**
* data Object[] 返回消息体。
* saleTemplateResponse Object
* templateId String 模板id。
* templateHashName String 模板hashName。
* iconUrl String 模板图片链接。
* exteriorName String 外观名称。
* rarityName String 品质。
* qualityName String 类别。
* saleCommodityResponse Object
* minSellPrice String 在售最低价(单位:元)。
* fastShippingMinSellPrice String 极速发货在售最低价(单位:元)。
* referencePrice String 模板参考价(单位:元)。
* sellNum String 在售数量。
*/
@Data
public class BatchGetOnSaleCommodityInfoResult {
private SaleTemplateResponse saleTemplateResponse;
private SaleCommodityResponse saleCommodityResponse;
}
@@ -0,0 +1,37 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
import java.math.BigDecimal;
/**
* 参数 类型 说明
* data Object 返回消息体。
* merchantOrderNo String 商户订单号。
* orderNo String 订单单号。
* payAmount number 实际支付金额。
* orderStatus Integer 交易状态:1,成功;0,失败。
* 示例:
* {
* "code": 0,
* "msg": "成功",
* "timestamp": 1673943192928,
* "data": {
* "merchantOrderNo": "20230112102",
* "orderNo": "2023011710002994746",
* "payAmount": 0.1,
* "orderStatus": 0
* }
* }
*/
@Data
public class ByTemplateCreateOrderResult {
private String merchantOrderNo;
private String orderNo;
private BigDecimal payAmount;
private Integer orderStatus;
}
@@ -0,0 +1,45 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
/**
* orderNo String 订单号。
* orderType Integer 预留字段
* orderSubType Integer 预留字段
* shippingMode Integer 发货模式:0,卖家直发;1,极速发货
* tradeOfferId long 报价ID
* tradeOfferLinks String 报价链接
* buyerUserId Integer 购买用户编号。
* orderStatus Integer 订单大状态。
* orderSubStatus Integer 订单小状态。
* failCode Integer 订单失败原因编号。
* failReason String 订单失败原因提示信息。
* merchantOrderNo String 第三方商户单号。
* notifyType Integer 通知类型(0:创建订单,1:等待发货,2:等待收货,3:购买成功,4:订单取消)。
* notifyDesc String 通知类型描述(创建订单,等待发货,等待收货,购买成功,订单取消)。
*
* 通知类型 orderStatus orderSubStatus
* 创建订单 100-创建订单
* 等待发货 140-交货中 1101 待发送报价
* 等待收货 1103 待确认报价
* 购买成功 340-已完成
* 订单取消 280-已取消
* 订单购买失败 2-购买失败
*/
@Data
public class Cs340CallBackInfo {
private String orderNo;
private Integer orderType;
private Integer orderSubType;
private Integer shippingMode;
private Long tradeOfferId;
private String tradeOfferLinks;
private Integer buyerUserId;
private Integer orderStatus;
private Integer orderSubStatus;
private Integer failCode;
private String failReason;
private String merchantOrderNo;
private Integer notifyType;
private String notifyDesc;
}
@@ -0,0 +1,10 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
@Data
public class Cs340CallBackNotifyInfo {
private String messageNo;
private String callBackInfo;
private String sign;
}
@@ -0,0 +1,9 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
@Data
public class Cs340CallBackResult {
private String messageNo;
private boolean flag;
}
@@ -0,0 +1,13 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
@Data
public class Cs340CheckTradeUrlResult {
private String steamId;
private Integer status;
private String msg;
}
@@ -0,0 +1,13 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class Cs340MerchantBalanceResult {
private BigDecimal amount;
private Integer userId;
}
@@ -0,0 +1,43 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
/**
* data object 返回消息体。
* orderNumber String 订单编号。
* shippingMode Integer 发货模式:0,卖家直发;1,极速发货
* tradeOfferId Long 报价ID
* tradeOfferLinks String 报价链接(不是每笔订单都存在)
* statusMsg String 订单状态。
* bigStatus Integer 订单大状态。
* bigStatusMsg String 订单大状态提示信息。
* smallStatus Integer 订单小状态。
* smallStatusMsg String 订单小状态提示信息。
* failCode Integer 订单失败原因编号,仅记录失败原因具体以bigStatus状态为准
* failReason String 订单失败原因提示信息。
*
* 大状态bigStatus说明
* 数值 说明
* 2 失败
* 100 创建中。
* 120 支付中。
* 130 异常挂起。
* 140 交货中。
* 360 结算中。
* 340 已完成。
* 280 已取消。
*/
@Data
public class Cs340OrderStatusResult {
private String orderNumber;
private Integer shippingMode;
private Long tradeOfferId;
private String tradeOfferLinks;
private String statusMsg;
private Integer bigStatus;
private String bigStatusMsg;
private Integer smallStatus;
private String smallStatusMsg;
private Integer failCode;
private String failReason;
}
@@ -0,0 +1,15 @@
package com.ruoyi.thirdparty.cs340.domain.result;
import lombok.Data;
@Data
public class Cs340Result<T> {
private Integer code;
private String msg;
private Long timestamp;
private T data;
}

Some files were not shown because too many files have changed in this diff Show More