TRechargeServiceImpl.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. package com.ylx.massage.service.impl;
  2. import cn.hutool.core.date.DatePattern;
  3. import cn.hutool.core.date.DateUtil;
  4. import cn.hutool.core.util.StrUtil;
  5. import cn.hutool.json.JSONObject;
  6. import cn.hutool.json.JSONUtil;
  7. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  8. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  9. import com.ijpay.core.IJPayHttpResponse;
  10. import com.ijpay.core.enums.RequestMethodEnum;
  11. import com.ijpay.core.kit.PayKit;
  12. import com.ijpay.core.kit.WxPayKit;
  13. import com.ijpay.core.utils.DateTimeZoneUtil;
  14. import com.ijpay.wxpay.WxPayApi;
  15. import com.ijpay.wxpay.enums.WxDomainEnum;
  16. import com.ijpay.wxpay.enums.v3.BasePayApiEnum;
  17. import com.ijpay.wxpay.model.CloseOrderModel;
  18. import com.ijpay.wxpay.model.v3.*;
  19. import com.ylx.common.config.WechatAccountConfig;
  20. import com.ylx.common.config.WxPayConfig;
  21. import com.ylx.common.core.domain.R;
  22. import com.ylx.common.exception.ServiceException;
  23. import com.ylx.common.utils.StringUtils;
  24. import com.ylx.massage.domain.TConsumptionLog;
  25. import com.ylx.massage.domain.TRecharge;
  26. import com.ylx.massage.domain.TWxUser;
  27. import com.ylx.massage.enums.BillTypeEnum;
  28. import com.ylx.massage.mapper.TRechargeMapper;
  29. import com.ylx.massage.service.TConsumptionLogService;
  30. import com.ylx.massage.service.TRechargeService;
  31. import com.ylx.massage.service.TWxUserService;
  32. import com.ylx.massage.utils.OrderNumberGenerator;
  33. import lombok.extern.slf4j.Slf4j;
  34. import org.springframework.beans.factory.annotation.Autowired;
  35. import org.springframework.stereotype.Service;
  36. import org.springframework.transaction.annotation.Transactional;
  37. import javax.annotation.Resource;
  38. import java.math.BigDecimal;
  39. import java.security.cert.X509Certificate;
  40. import java.time.LocalDateTime;
  41. import java.time.ZoneId;
  42. import java.util.ArrayList;
  43. import java.util.Date;
  44. import java.util.List;
  45. import java.util.Map;
  46. import static com.ylx.common.constant.HttpStatus.SUCCESS;
  47. /**
  48. * 充值记录表 服务实现类
  49. */
  50. @Service
  51. @Slf4j
  52. public class TRechargeServiceImpl extends ServiceImpl<TRechargeMapper, TRecharge> implements TRechargeService {
  53. @Resource
  54. private TWxUserService wxUserService;
  55. @Resource
  56. private OrderNumberGenerator generator;
  57. @Resource
  58. private TConsumptionLogService consumptionLogService;
  59. @Autowired
  60. private WxPayConfig wxPayProperties;
  61. @Autowired
  62. private WechatAccountConfig wechatAccountConfig;
  63. @Autowired
  64. private TRechargeMapper rechargeMapper;
  65. String serialNo;
  66. @Override
  67. @Transactional(rollbackFor = Exception.class)
  68. public TRecharge recharge(TRecharge recharge) {
  69. //增加充值记录
  70. if (StringUtils.isBlank(recharge.getcOpenId())) {
  71. throw new ServiceException("openId不能为空");
  72. }
  73. if (null == recharge.getdMoney() || recharge.getdMoney().compareTo(BigDecimal.ZERO) <= 0) {
  74. throw new ServiceException("充值金额不能为空");
  75. }
  76. // LambdaQueryWrapper<TRecharge> queryWrapper = new LambdaQueryWrapper<>();
  77. // queryWrapper.eq(TRecharge::getcOpenId, recharge.getcOpenId()).eq(TRecharge::getPayStatus, -1).eq(TRecharge::getdMoney, recharge.getdMoney());
  78. // TRecharge one = getOne(queryWrapper);
  79. // if (one != null) {
  80. // log.info("未支付的充值记录,{}",one.getRechargeNo());
  81. // return one;
  82. // }
  83. recharge.setRechargeNo(generator.generateNextOrderNumber(OrderNumberGenerator.KEY_PREFIX_RECHAR));
  84. recharge.setDtCreateTime(LocalDateTime.now());
  85. save(recharge);
  86. //todo 调用微信支付成功后增加账户金额
  87. return recharge;
  88. }
  89. @Override
  90. @Transactional(rollbackFor = Exception.class)
  91. public TRecharge increaseAmount(String rechargeNo) {
  92. //增加个人账户金额
  93. TRecharge recharge = getByRechargeNo(rechargeNo);
  94. if(recharge == null){
  95. log.error("未支付的充值记录不存在,单号:{}",rechargeNo);
  96. return null;
  97. }
  98. //修改充值状态
  99. recharge.setPayStatus(1);
  100. this.updateById(recharge);
  101. //增加个人账户金额
  102. TWxUser user = wxUserService.getByOpenId(recharge.getcOpenId());
  103. user.setdBalance(user.getdBalance().add(recharge.getdMoney()));
  104. wxUserService.updateById(user);
  105. //增加消费记录
  106. TConsumptionLog tConsumptionLog = new TConsumptionLog();
  107. tConsumptionLog.setAmount(recharge.getdMoney());
  108. tConsumptionLog.setBillNo(rechargeNo);
  109. tConsumptionLog.setOpenId(recharge.getcOpenId());
  110. tConsumptionLog.setBillType(BillTypeEnum.RECHARGE.getCode());
  111. tConsumptionLog.setNote("微信充值");
  112. consumptionLogService.save(tConsumptionLog);
  113. log.info("微信充值成功,充值单号:{},充值金额:{},充值OpenId:{}",rechargeNo,recharge.getdMoney(),user.getcOpenid());
  114. return recharge;
  115. }
  116. @Override
  117. public R getPay(String setOutTradeNo, BigDecimal amount, String openId, String description, String attach) throws Exception {
  118. String timeExpire = DateTimeZoneUtil.dateToTimeZone(System.currentTimeMillis() + 1000 * 60 * 3);
  119. UnifiedOrderModel unifiedOrderModel = new UnifiedOrderModel()
  120. // .setAppid(wxPayProperties.getAppId())
  121. .setAppid(wechatAccountConfig.getMpAppId())
  122. .setMchid(wxPayProperties.getMchId())
  123. //商品描述
  124. .setDescription(description)
  125. //订单号
  126. .setOut_trade_no(setOutTradeNo)
  127. //交易结束时间
  128. .setTime_expire(timeExpire)
  129. //附加数据
  130. .setAttach(attach)
  131. //通知地址异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http
  132. //示例值:https://www.weixin.qq.com/wxpay/pay.php
  133. .setNotify_url(wxPayProperties.getNotifyUrl())
  134. //支付金额以分为单位
  135. .setAmount(new Amount().setTotal(amount.multiply(new BigDecimal(100)).intValue()))
  136. //交易人
  137. .setPayer(new Payer().setOpenid(openId));
  138. log.info("统一下单参数 {}", JSONUtil.toJsonStr(unifiedOrderModel));
  139. IJPayHttpResponse response = WxPayApi.v3(
  140. RequestMethodEnum.POST,
  141. WxDomainEnum.CHINA.toString(),
  142. BasePayApiEnum.JS_API_PAY.toString(),
  143. wxPayProperties.getMchId(),
  144. getSerialNumber(),
  145. null,
  146. wxPayProperties.getCertKeyPath(),
  147. JSONUtil.toJsonStr(unifiedOrderModel)
  148. );
  149. log.info("统一下单响应 {}", response);
  150. // 根据证书序列号查询对应的证书来验证签名结果
  151. boolean verifySignature = WxPayKit.verifySignature(response, wxPayProperties.getPlatFormPath());
  152. log.info("verifySignature: {}", verifySignature);
  153. if (response.getStatus() == SUCCESS && verifySignature) {
  154. String body = response.getBody();
  155. JSONObject jsonObject = JSONUtil.parseObj(body);
  156. String prepayId = jsonObject.getStr("prepay_id");
  157. Map<String, String> map = WxPayKit.jsApiCreateSign(wechatAccountConfig.getMpAppId(), prepayId, wxPayProperties.getCertKeyPath());
  158. log.info("唤起支付参数:{}", map);
  159. return R.ok(JSONUtil.toJsonStr(map));
  160. }
  161. return R.ok(JSONUtil.toJsonStr(response));
  162. }
  163. @Override
  164. public String refund(String outRefundNo, String transactionId, String outTradeNo, BigDecimal amount) {
  165. try {
  166. int i = amount.multiply(new BigDecimal(100)).intValue();
  167. log.info("商户退款单号: {}", outRefundNo);
  168. //退款商品
  169. List<RefundGoodsDetail> list = new ArrayList<>();
  170. RefundGoodsDetail refundGoodsDetail = new RefundGoodsDetail()
  171. //商户侧商品编码
  172. .setMerchant_goods_id("123")
  173. //商品名称
  174. .setGoods_name("IJPay 测试")
  175. //商品单价
  176. .setUnit_price(1)
  177. //商品退款金额
  178. .setRefund_amount(1)
  179. //商品退货数量
  180. .setRefund_quantity(1);
  181. list.add(refundGoodsDetail);
  182. RefundModel refundModel = new RefundModel()
  183. //商户退款单号
  184. .setOut_refund_no(outRefundNo)
  185. //退款原因
  186. //.setReason("IJPay 测试退款")
  187. //回调地址
  188. .setNotify_url(wxPayProperties.getRefundNotify())
  189. //金额信息
  190. .setAmount(new RefundAmount().setRefund(i).setTotal(i).setCurrency("CNY"));
  191. //退款商品
  192. //.setGoods_detail(list);
  193. //商户订单号
  194. if (StrUtil.isNotEmpty(transactionId)) {
  195. refundModel.setTransaction_id(transactionId);
  196. }
  197. if (StrUtil.isNotEmpty(outTradeNo)) {
  198. refundModel.setOut_trade_no(outTradeNo);
  199. }
  200. log.info("退款参数 {}", JSONUtil.toJsonStr(refundModel));
  201. IJPayHttpResponse response = WxPayApi.v3(
  202. RequestMethodEnum.POST,
  203. WxDomainEnum.CHINA.toString(),
  204. BasePayApiEnum.REFUND.toString(),
  205. wxPayProperties.getMchId(),
  206. getSerialNumber(),
  207. null,
  208. wxPayProperties.getCertKeyPath(),
  209. JSONUtil.toJsonStr(refundModel)
  210. );
  211. // 根据证书序列号查询对应的证书来验证签名结果
  212. boolean verifySignature = WxPayKit.verifySignature(response, wxPayProperties.getPlatFormPath());
  213. log.info("verifySignature: {}", verifySignature);
  214. log.info("退款响应 {}", response);
  215. if (verifySignature) {
  216. return response.getBody();
  217. }
  218. } catch (Exception e) {
  219. log.error("系统异常", e);
  220. return e.getMessage();
  221. }
  222. return null;
  223. }
  224. @Override
  225. public BigDecimal getRecharge(String jsid, Date startDate, Date endDate) {
  226. return rechargeMapper.getRecharge(jsid, startDate, endDate);
  227. }
  228. @Override
  229. public boolean closeWeChatOrder(String outTradeNo) {
  230. try {
  231. // 关闭订单API路径:/v3/pay/transactions/out-trade-no/{out_trade_no}/close
  232. String closeOrderApi = String.format("/v3/pay/transactions/out-trade-no/%s/close", outTradeNo);
  233. log.info("关闭微信订单,订单号:{},API路径:{}", outTradeNo, closeOrderApi);
  234. CloseOrderModel closeOrderModel = new CloseOrderModel();
  235. closeOrderModel.setMch_id(wxPayProperties.getMchId());
  236. IJPayHttpResponse response = WxPayApi.v3(
  237. RequestMethodEnum.POST,
  238. WxDomainEnum.CHINA.toString(),
  239. closeOrderApi,
  240. wxPayProperties.getMchId(),
  241. getSerialNumber(),
  242. null,
  243. wxPayProperties.getCertKeyPath(),
  244. JSONUtil.toJsonStr(closeOrderModel)
  245. );
  246. log.info("关闭微信订单响应:{},状态码:{}", response.getBody(), response.getStatus());
  247. // 200表示成功
  248. if (response.getStatus() == SUCCESS) {
  249. return true;
  250. }
  251. // 微信侧无订单时返回ORDERNOTEXIST错误码(订单已支付或从未创建),视为关闭成功
  252. if (response.getStatus() == 400 && response.getBody() != null && response.getBody().contains("ORDERNOTEXIST")) {
  253. log.info("微信侧无此订单,无需关闭,订单号:{}", outTradeNo);
  254. return true;
  255. }
  256. return false;
  257. } catch (Exception e) {
  258. log.error("关闭微信订单异常,订单号:{},错误:{}", outTradeNo, e.getMessage(), e);
  259. // 微信关单失败不影响后续流程,忽略错误
  260. return true;
  261. }
  262. }
  263. @Override
  264. public int countSuccessRecharges(String openId, Date rechargeQueryTime) {
  265. // 统计用户在指定时间起(含)之后成功的充值次数
  266. // 支付状态为 1 表示已支付(充值成功)
  267. if (StrUtil.isBlank(openId)) {
  268. return 0;
  269. }
  270. LambdaQueryWrapper<TRecharge> queryWrapper = new LambdaQueryWrapper<>();
  271. queryWrapper.eq(TRecharge::getcOpenId, openId)
  272. .eq(TRecharge::getPayStatus, 1);
  273. // rechargeQueryTime 为 null 表示不限制起点(永久活动/全量统计)
  274. if (rechargeQueryTime != null) {
  275. LocalDateTime start = LocalDateTime.ofInstant(rechargeQueryTime.toInstant(), ZoneId.systemDefault());
  276. queryWrapper.ge(TRecharge::getDtCreateTime, start);
  277. }
  278. long cnt = this.count(queryWrapper);
  279. return cnt > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) cnt;
  280. }
  281. /**
  282. * 获取证书序列号
  283. * @return String 证书序列号
  284. */
  285. private String getSerialNumber() {
  286. if (StrUtil.isEmpty(serialNo)) {
  287. // 获取证书序列号
  288. X509Certificate certificate = PayKit.getCertificate(wxPayProperties.getCertPath());
  289. if (null != certificate) {
  290. serialNo = certificate.getSerialNumber().toString(16).toUpperCase();
  291. // 提前两天检查证书是否有效
  292. boolean isValid = PayKit.checkCertificateIsValid(certificate, wxPayProperties.getMchId(), -2);
  293. log.info("证书是否可用 {} 证书有效期为 {}", isValid, DateUtil.format(certificate.getNotAfter(), DatePattern.NORM_DATETIME_PATTERN));
  294. }
  295. // System.out.println("输出证书信息:\n" + certificate.toString());
  296. // // 输出关键信息,截取部分并进行标记
  297. // System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16));
  298. // System.out.println("版本号:" + certificate.getVersion());
  299. // System.out.println("签发者:" + certificate.getIssuerDN());
  300. // System.out.println("有效起始日期:" + certificate.getNotBefore());
  301. // System.out.println("有效终止日期:" + certificate.getNotAfter());
  302. // System.out.println("主体名:" + certificate.getSubjectDN());
  303. // System.out.println("签名算法:" + certificate.getSigAlgName());
  304. // System.out.println("签名:" + certificate.getSignature().toString());
  305. }
  306. System.out.println("serialNo:" + serialNo);
  307. return serialNo;
  308. }
  309. private TRecharge getByRechargeNo(String rechargeNo) {
  310. return getOne(new LambdaQueryWrapper<TRecharge>().eq(TRecharge::getRechargeNo, rechargeNo).eq(TRecharge::getPayStatus, -1));
  311. }
  312. }