package com.nova.sankuai.service.impl; import cn.hutool.core.util.BooleanUtil; import cn.hutool.crypto.asymmetric.KeyType; import cn.hutool.crypto.asymmetric.RSA; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayConstants; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.request.AlipayTradeWapPayRequest; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.nova.sankuai.domain.dto.*; import com.nova.sankuai.domain.entity.*; import com.nova.sankuai.domain.vo.AlipayOrderInfoVO; import com.nova.sankuai.infra.config.AlipayConfigProperties; import com.nova.sankuai.infra.config.CommonException; import com.nova.sankuai.infra.mapper.*; import com.nova.sankuai.infra.utils.MessageSendUtil; import com.nova.sankuai.security.UserDetail; import com.nova.sankuai.service.AlipayOrderInfoConverter; import com.nova.sankuai.service.IAlipayPlatformService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @Slf4j @Service public class AlipayPlatformServiceImpl implements IAlipayPlatformService { /* * 参考文档:https://opendocs.alipay.com/open/203/105285 */ private final SysAlipayAccountMapper alipayAccountMapper; private final SysAlipayAccountUsageMapper alipayAccountUsageMapper; private final AlipayExcessTextmsgReceiverMapper alipayExcessTextmsgReceiverMapper; private final AlipayExcessTextmsgSendRecordMapper alipayExcessTextmsgSendRecordMapper; private final AlipayChannelRecordMapper alipayChannelRecordMapper; private final AlipayOrderInfoMapper alipayOrderInfoMapper; private final AlipayRefundRecordMapper alipayRefundRecordMapper; private final AlipayOrderInfoConverter converter; private final AlipayConfigProperties alipayConf; private final ObjectMapper objectMapper; private final RSA rsa; private final VipCardOrderMapper vipCardOrderMapper; private final CustomerUserMapper userMapper; private final Lock decisionLock = new ReentrantLock(); private final Lock alipayExcessRemindLock = new ReentrantLock(); private final Lock alipayExcessPreventionLock = new ReentrantLock(); private final ExecutorService exec = Executors.newFixedThreadPool(5); private final MessageDigest digest; private final MessageSendUtil messageSendUtil; public AlipayPlatformServiceImpl(SysAlipayAccountMapper alipayAccountMapper, SysAlipayAccountUsageMapper alipayAccountUsageMapper, AlipayExcessTextmsgReceiverMapper alipayExcessTextmsgReceiverMapper, AlipayExcessTextmsgSendRecordMapper alipayExcessTextmsgSendRecordMapper, AlipayChannelRecordMapper alipayChannelRecordMapper, AlipayOrderInfoMapper alipayOrderInfoMapper, AlipayRefundRecordMapper alipayRefundRecordMapper, AlipayOrderInfoConverter converter, AlipayConfigProperties alipayConf, ObjectMapper objectMapper, MessageSendUtil messageSendUtil, @Value("${loginrsa.key-pair.public}") String publicKey, @Value("${loginrsa.key-pair.private}") String privateKey, VipCardOrderMapper vipCardOrderMapper, CustomerUserMapper userMapper) throws NoSuchAlgorithmException { this.alipayAccountMapper = alipayAccountMapper; this.alipayAccountUsageMapper = alipayAccountUsageMapper; this.alipayExcessTextmsgReceiverMapper = alipayExcessTextmsgReceiverMapper; this.alipayExcessTextmsgSendRecordMapper = alipayExcessTextmsgSendRecordMapper; this.alipayChannelRecordMapper = alipayChannelRecordMapper; this.alipayOrderInfoMapper = alipayOrderInfoMapper; this.alipayRefundRecordMapper = alipayRefundRecordMapper; this.converter = converter; this.alipayConf = alipayConf; this.vipCardOrderMapper = vipCardOrderMapper; this.userMapper = userMapper; this.rsa = new RSA(privateKey, publicKey); this.messageSendUtil = messageSendUtil; digest = MessageDigest.getInstance("MD5"); this.objectMapper = objectMapper; } @Override public String orderCreate(AlipayOrderCreateDTO createDTO) { String orderId = createDTO.getChannelId() + "_" + createDTO.getOrderId(); // 渠道编码检查 AlipayChannelRecord channel = checkChannelID(createDTO.getChannelId()); // 当日支付宝可交易金额是否超额 long usageId = accountDecision(orderId, createDTO.getTotalAmount()); SysAlipayAccount account = alipayAccountMapper.selectByUsageId(usageId); try { // 支付宝订单生成 String form = generateAlipayForm(account, orderId, createDTO.getTotalAmount().toPlainString(), createDTO.getOrderSubject(), createDTO.getReturnUrl(), alipayConf.getPlatformNotifyUrl()); // 生成成功后记录订单信息 AlipayOrderInfo order = new AlipayOrderInfo(); order.setId(IdWorker.getId()); order.setChannelId(createDTO.getChannelId()); order.setOrderId(createDTO.getOrderId()); order.setOrderSubject(createDTO.getOrderSubject()); order.setOrderBody(createDTO.getOrderBody()); order.setGmtCreate(new Date()); order.setTotalAmount(createDTO.getTotalAmount()); order.setOrderStatus("WAIT_BUYER_PAY"); order.setAppId(account.getAppId()); order.setNotifyUrl(createDTO.getNotifyUrl() != null ? createDTO.getNotifyUrl() : ""); // 渠道当日是否限额 int insert = 0, syncLimit = 30, times = 0; Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String from = sdf.format(date) + " 00:00:00"; String to = sdf.format(date) + " 23:59:59"; while (insert == 0) { if (times >= syncLimit) { throw new CommonException("订单生成错误! 并发异常"); } times++; sleepWhile(); int vers = checkChannelTradingLimit(channel, createDTO.getTotalAmount()); if (vers == -1) { // 执行正常插入 order.setRecordVers(0); insert = alipayOrderInfoMapper.insertSelective(order); } else { // 执行版本号插入 order.setRecordVers(vers); insert = alipayOrderInfoMapper.insertSelectiveWithVers(order, from, to); } } return form; } catch (Exception e) { // 订单生成失败 cancelAlipayOrder(orderId); alipayAccountUsageMapper.deleteById(usageId); throw new CommonException("支付宝订单生成失败!reason=" + e.getMessage()); } } private void cancelAlipayOrder(String outTradeNo) { } private void sleepWhile() { try { TimeUnit.MILLISECONDS.sleep(10L); } catch (InterruptedException ignored) { } } private int checkChannelTradingLimit(AlipayChannelRecord channel, BigDecimal amount) { BigDecimal dailyLimit = channel.getDailyLimit(); Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String from = sdf.format(date) + " 00:00:00"; String to = sdf.format(date) + " 23:59:59"; int count = alipayOrderInfoMapper.checkTodayHasTradeOrder(channel.getChannelId(), from, to); if (count == 0) { // 当日还没有任何一笔交易 if (dailyLimit.subtract(amount).doubleValue() < 0) { throw new CommonException("渠道交易超额"); } return -1; } String channelId = channel.getChannelId(); AlipayChannelDailyLimitDTO limit = alipayOrderInfoMapper.checkChannelIdTradingLimit(channelId, from, to, dailyLimit); if (limit == null) { // 目标渠道当日达到限额 throw new CommonException("渠道已限额"); } if (dailyLimit.subtract(limit.getTotal()).subtract(amount).doubleValue() < 0) { throw new CommonException("渠道交易超额"); } return limit.getVers(); } @Override public String generateAlipayForm(SysAlipayAccount account, String orderId, String amount, String subject, String returnURL, String notifyURL) throws AlipayApiException { DefaultAlipayClient client = new DefaultAlipayClient( alipayConf.getUrl(), account.getAppId(), account.getPrivateKey(), "json", AlipayConstants.CHARSET_UTF8, account.getPublicKey(), "RSA2"); AlipayTradeWapPayRequest alipayReq = new AlipayTradeWapPayRequest(); alipayReq.setReturnUrl(returnURL); alipayReq.setNotifyUrl(notifyURL); alipayReq.setBizContent(new JSONObject() {{ put("out_trade_no", orderId); put("total_amount", amount); put("subject", subject); put("product_code", "QUICK_WAP_WAY"); }}.toJSONString()); return client.pageExecute(alipayReq).getBody(); } private AlipayChannelRecord checkChannelID(String channelId) { AlipayChannelRecord usingChannel = alipayChannelRecordMapper.selectByChannelIdAndStatus(channelId, "using"); if (usingChannel == null) { throw new CommonException("渠道ID无效"); } return usingChannel; } private final Map msgSendFlag = new ConcurrentHashMap<>(4); @Override public long accountDecision(String orderId, BigDecimal amount) { try { if (!decisionLock.tryLock(10L, TimeUnit.SECONDS)) { throw new RuntimeException("支付宝账户决定 - 获取锁超时"); } LocalDate today = LocalDate.now(); SysAlipayAccountUsage usage = new SysAlipayAccountUsage(); usage.setId(IdWorker.getId()); usage.setOrderId(orderId); usage.setCreateDate(today); usage.setSingleAmount(amount); usage.setStatus("init"); usage.setCreateDatetime(LocalDateTime.now()); int insert = alipayAccountUsageMapper.insertWithSelect(usage); if (insert == 0) { if (!today.equals(msgSendFlag.get("满额提醒"))) { // 没提醒过,或已经是第二天 msgSendFlag.put("满额提醒", today); log.info("alipay->进入满额提醒"); alipayExcessRemind(); } throw new CommonException("所有支付宝账户已限额,请联系管理员"); } else { // 如果没有超额,也可能是管理员手工重置了账户可用余额 msgSendFlag.remove("满额提醒"); alipayExcessPrevention(); return usage.getId(); } } catch (InterruptedException e) { throw new RuntimeException("支付宝账户决定 - 获取锁中断", e); } finally { decisionLock.unlock(); } } @Override public void handleNotify(Map map) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { String outTradeNo = nil(map.get("out_trade_no"), it -> it[0]); int splitIndex = outTradeNo.indexOf("_"); String channelId = outTradeNo.substring(0, splitIndex); String orderId = outTradeNo.substring(splitIndex + 1); AlipayOrderInfo order = new AlipayOrderInfo(); order.setAlipaySerial(nil(map.get("trade_no"), it -> it[0])); // 支付宝流水号 trade_no order.setChannelId(channelId); order.setOrderId(orderId); order.setGmtPayment(nil(map.get("gmt_create"), it -> sdf.parse(it[0]))); order.setGmtRefund(nil(map.get("gmt_refund"), it -> sdf.parse(it[0]))); order.setGmtClose(nil(map.get("gmt_close"), it -> sdf.parse(it[0]))); order.setBuyerId(nil(map.get("buyer_id"), it -> it[0])); order.setSellerId(nil(map.get("seller_id"), it -> it[0])); order.setReceiptAmount(newBigDecimal(nil(map.get("receipt_amount"), it -> it[0]))); order.setPointAmount(newBigDecimal(nil(map.get("invoice_amount"), it -> it[0]))); order.setBuyerPayAmount(newBigDecimal(nil(map.get("buyer_pay_amount"), it -> it[0]))); order.setInvoiceAmount(newBigDecimal(nil(map.get("point_amount"), it -> it[0]))); order.setRefundFee(newBigDecimal(nil(map.get("refund_fee"), it -> it[0]))); order.setOrderStatus(nil(map.get("trade_status"), it -> it[0])); if ("TRADE_CLOSED".equals(order.getOrderStatus())) { // 关闭订单 alipayAccountUsageMapper.delete(Wrappers.lambdaUpdate() .eq(SysAlipayAccountUsage::getOrderId, outTradeNo)); } else { alipayAccountUsageMapper.updateStatus(outTradeNo); } alipayOrderInfoMapper.updateByOrderSelective(order); AlipayOrderQueryDTO condition = new AlipayOrderQueryDTO(); condition.setChannelId(channelId); condition.setOrderId(orderId); AlipayOrderInfo orderResult = alipayOrderInfoMapper.selectByChannelIdAndOrderId(condition); String notifyUrl = orderResult.getNotifyUrl(); if (StringUtils.isNotBlank(notifyUrl)) { ArrayList errors = new ArrayList<>(); for (String url : notifyUrl.split(",")) { HttpResponse resp = HttpRequest.post(url) .contentType(MediaType.APPLICATION_JSON_VALUE) .body(toJsonString(sign(toJsonString(converter.toView(orderResult))))) .execute(); if (!resp.isOk()) { errors.add(String.format("[%s] resp is not ok: %s", url, resp)); } else if (!"ok".equals(resp.body())) { errors.add(String.format("[%s] resp is not ok: %s", url, resp.body())); } } if (errors.size() > 0) { throw new CommonException("回调地址中存在返回错误:" + errors); } } } catch (Exception e) { log.error("AlipayPlatform回调异常! error = " + e.getMessage() + " map = " + JSON.toJSONString(map), e); map.forEach((k, v) -> log.error("alipay callback param: k = " + k + ", v = " + Arrays.toString(v))); throw new RuntimeException(e); } } @FunctionalInterface interface MyFunc { R apply(T in) throws Exception; } private OUT nil(IN in, MyFunc fun) throws Exception { if (in == null) { return null; } return fun.apply(in); } private BigDecimal newBigDecimal(String amount) { if (amount == null) { return null; } return new BigDecimal(amount); } @Override public T decode(String encodedString, Class type) { try { if (BooleanUtil.isTrue(alipayConf.getPlatformEncryptFlag())) { byte[] content = rsa.decrypt(Base64.getDecoder().decode(encodedString), KeyType.PrivateKey); return objectMapper.readValue(new String(content), type); } return objectMapper.readValue(encodedString, type); } catch (Exception e) { throw new CommonException("解密失败", e); } } @Override public String encode(String inst) { try { if (BooleanUtil.isTrue(alipayConf.getPlatformEncryptFlag())) { byte[] bytes = rsa.encrypt(inst.getBytes(StandardCharsets.UTF_8), KeyType.PrivateKey); return Base64.getEncoder().encodeToString(bytes); } return inst; } catch (Exception e) { throw new RuntimeException("加密失败", e); } } @Override public AlipayOrderInfoVO orderQuery(AlipayOrderQueryDTO queryDTO) { AlipayOrderInfo orderInst = alipayOrderInfoMapper.selectByChannelIdAndOrderId(queryDTO); if (orderInst == null) { throw new CommonException("订单查询失败! 订单[" + queryDTO.getOrderId() + "]不存在"); } return converter.toView(orderInst); } @Override public String toJsonString(Object obj) { if (obj instanceof String) { return ((String) obj); } try { return objectMapper.writeValueAsString(obj); } catch (JsonProcessingException e) { String msg = "ObjectMapper 序列化出错: " + e.getMessage(); log.error(msg); throw new CommonException(msg); } } @Override public Map sign(String content) { String sign = Base64.getEncoder().encodeToString(digest.digest(content.getBytes(StandardCharsets.UTF_8))); Map map = new HashMap<>(); map.put("sign", sign); map.put("value", encode(content)); return map; } @Override public void transferAccount(AlipayTransferAccountDTO param) { } @Override public void refund(AlipayRefundDTO refundDTO) { // 保存退款记录 AlipayRefundRecord record = converter.toEntity(refundDTO); record.setRefundReqTime(new Date()); alipayRefundRecordMapper.insertSelective(record); } @Override public IPage refundList(AlipayRefundQueryDTO queryDTO) { Page page = new Page<>(queryDTO.getPageNo(), queryDTO.getPageSize()); return alipayRefundRecordMapper.selectPage(page, queryDTO); } @Override public void refundFinish(AlipayRefundFinishDTO finishDTO) { AlipayRefundRecord condition = new AlipayRefundRecord(); condition.setId(finishDTO.getRefundId()); condition.setRefundStatus(finishDTO.getRefundStatus()); condition.setDealUserId(finishDTO.getDealUserId()); condition.setDealTime(finishDTO.getDealFinishTime()); condition.setDealDesc(finishDTO.getDealDesc()); alipayRefundRecordMapper.updateByPrimaryKeySelective(condition); } @Override public BigDecimal refundAmountCalculate(UserDetail user) { Date now = new Date(); List prices = new ArrayList<>(); // 会员服务 if (userMapper.selectVipDateInTimeCount(user.getId(), now) > 0) { prices.add(vipCardOrderMapper.findLatestEndOrderPriceByUserIdAndVipTypes(user.getId(), Collections.singletonList(0))); } // 审批加速包 if (userMapper.selectQueueVipCount(user.getId()) > 0) { prices.add(vipCardOrderMapper.findLatestEndOrderPriceByUserIdAndVipTypes(user.getId(), Collections.singletonList(1))); } // 贷超会员 if (userMapper.selectLoanVipDateInTimeCount(user.getId(), now) > 0) { prices.add(vipCardOrderMapper.findLatestEndOrderPriceByUserIdAndVipTypes(user.getId(), Collections.singletonList(2))); } BigDecimal unitPrice = new BigDecimal(prices.stream().filter(Objects::nonNull).reduce(0, (Integer::sum))); return unitPrice.divide(BigDecimal.valueOf(100L), 2, RoundingMode.DOWN); } /** * 满额提醒 */ private void alipayExcessRemind() { try { LocalDate today = LocalDate.now(); LambdaQueryWrapper query = Wrappers.lambdaQuery() .eq(AlipayExcessTextmsgSendRecord::getSendDate, today) .eq(AlipayExcessTextmsgSendRecord::getSendType, 1); Integer count = alipayExcessTextmsgSendRecordMapper.selectCount(query); if (count == 0) { log.info("alipay->今日未发送满额提醒"); try { if (!alipayExcessRemindLock.tryLock(10L, TimeUnit.SECONDS)) { throw new CommonException("支付宝限额提醒 - 获取锁超时"); } count = alipayExcessTextmsgSendRecordMapper.selectCount(query); if (count == 0) { List phones = alipayExcessTextmsgReceiverMapper.selectList( Wrappers.lambdaQuery().eq(AlipayExcessTextmsgReceiver::getStatus, 1)) .stream() .map(AlipayExcessTextmsgReceiver::getPhone) .collect(Collectors.toList()); String content = "【三快分期】系统已达到支付限额,无法继续接受订单,请立即切换支付渠道"; phones.forEach(phone -> { AlipayExcessTextmsgSendRecord record = new AlipayExcessTextmsgSendRecord(); record.setReceiverPhone(phone); record.setTextContent(content); record.setSendDate(today); record.setSendDatetime(LocalDateTime.now()); record.setSendType(1); alipayExcessTextmsgSendRecordMapper.insert(record); messageSendUtil.sendMessage(phone, content); log.info("alipay->号码{}已发送短信", phone); }); } } catch (InterruptedException e) { throw new CommonException("支付宝限额提醒 - 获取锁中断", e); } finally { alipayExcessRemindLock.unlock(); } } } catch (RuntimeException e) { log.info("alipay->满额提醒异常:{}:{}", e.getClass(), e.getMessage()); throw e; } } /** * 满额预警 */ private void alipayExcessPrevention() { String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); BigDecimal todayTotal = alipayAccountUsageMapper.selectSumAmountInDay(today); BigDecimal total = alipayAccountMapper.selectTotalAmount(); BigDecimal divide = todayTotal.divide(total, 3, RoundingMode.HALF_UP); if (divide.doubleValue() < 0.9) { // 未达到预警阈值 msgSendFlag.remove("满额预警"); return; } LocalDate todayDate = LocalDate.now(); if (todayDate.equals(msgSendFlag.get("满额预警"))) { return; } msgSendFlag.put("满额预警", todayDate); LambdaQueryWrapper query = Wrappers.lambdaQuery() .eq(AlipayExcessTextmsgSendRecord::getSendDate, todayDate) .eq(AlipayExcessTextmsgSendRecord::getSendType, 2); Integer count = alipayExcessTextmsgSendRecordMapper.selectCount(query); if (count == 0) { try { if (!alipayExcessPreventionLock.tryLock(10L, TimeUnit.SECONDS)) { throw new CommonException("支付宝限额提醒 - 获取锁超时"); } count = alipayExcessTextmsgSendRecordMapper.selectCount(query); if (count == 0) { BigDecimal rest = total.subtract(todayTotal); DecimalFormat decimalFormat = new DecimalFormat(); decimalFormat.applyPattern("0.00"); String content = String.format("【三快分期】系统支付限额即将达到上限,请注意切换支付渠道,已用%s,剩余%s", decimalFormat.format(todayTotal.doubleValue()), decimalFormat.format(rest.doubleValue())); List phones = alipayExcessTextmsgReceiverMapper.selectList( Wrappers.lambdaQuery() .eq(AlipayExcessTextmsgReceiver::getStatus, 1)) .stream() .map(AlipayExcessTextmsgReceiver::getPhone) .collect(Collectors.toList()); phones.forEach(phone -> { AlipayExcessTextmsgSendRecord record = new AlipayExcessTextmsgSendRecord(); record.setReceiverPhone(phone); record.setTextContent(content); record.setSendDate(todayDate); record.setSendDatetime(LocalDateTime.now()); record.setSendType(2); alipayExcessTextmsgSendRecordMapper.insert(record); }); // 避免网络原因造成占用锁过长时间 exec.execute(() -> phones.forEach(e -> messageSendUtil.sendMessage(e, content))); } } catch (InterruptedException e) { throw new CommonException("支付宝限额提醒 - 获取锁中断", e); } finally { alipayExcessPreventionLock.unlock(); } } } @Override public T decodeByPublicKey(String encodedString, Class type) { try { if (BooleanUtil.isTrue(alipayConf.getPlatformEncryptFlag())) { byte[] content = rsa.decrypt(Base64.getDecoder().decode(encodedString), KeyType.PublicKey); return objectMapper.readValue(new String(content), type); } return objectMapper.readValue(encodedString, type); } catch (Exception e) { throw new CommonException("解密失败", e); } } }