TRechargeServiceImpl.java 15 KB

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