|
|
@@ -0,0 +1,596 @@
|
|
|
+package com.xuekairui.payment.service;
|
|
|
+
|
|
|
+import cn.hutool.core.util.IdUtil;
|
|
|
+import com.alipay.api.AlipayClient;
|
|
|
+import com.alipay.api.domain.AlipayCommerceMerchantcardTemplateCreateModel;
|
|
|
+import com.alipay.api.domain.CardCycle;
|
|
|
+import com.alipay.api.domain.CardPeriodPrice;
|
|
|
+import com.alipay.api.domain.CardTemplateSale;
|
|
|
+import com.alipay.api.domain.CardTemplateUse;
|
|
|
+import com.alipay.api.domain.MerchantCardTemplate;
|
|
|
+import com.alipay.api.request.AlipayCommerceMerchantcardTemplateCreateRequest;
|
|
|
+import com.alipay.api.request.AlipayCommerceMerchantcardTemplateQueryRequest;
|
|
|
+import com.alipay.api.request.AlipayCommerceMerchantcardTemplatestatusModifyRequest;
|
|
|
+import com.alipay.api.response.AlipayCommerceMerchantcardTemplateCreateResponse;
|
|
|
+import com.alipay.api.response.AlipayCommerceMerchantcardTemplateQueryResponse;
|
|
|
+import com.alipay.api.response.AlipayCommerceMerchantcardTemplatestatusModifyResponse;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.xuekairui.common.BusinessException;
|
|
|
+import com.xuekairui.common.ErrorCode;
|
|
|
+import com.xuekairui.payment.config.PaymentProperties;
|
|
|
+import com.xuekairui.payment.entity.AlipayAnxinCard;
|
|
|
+import com.xuekairui.payment.entity.AlipayAnxinOrderRecord;
|
|
|
+import com.xuekairui.payment.entity.PaymentPlan;
|
|
|
+import com.xuekairui.payment.mapper.AlipayAnxinCardMapper;
|
|
|
+import com.xuekairui.payment.mapper.AlipayAnxinOrderRecordMapper;
|
|
|
+import com.xuekairui.user.enums.MembershipLevel;
|
|
|
+import com.xuekairui.user.service.MembershipService;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.ObjectProvider;
|
|
|
+import org.springframework.beans.factory.annotation.Qualifier;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 支付宝安心付服务(周期卡 - 小程序会员)
|
|
|
+ *
|
|
|
+ * <p>封装支付宝安心付「周期卡」的卡模板管理、扣款通知处理、会员续期流程。
|
|
|
+ * <ul>
|
|
|
+ * <li>卡模板管理:调用 alipay.commerce.merchantcard.template.create 创建周期卡模板,与 PaymentPlan 一一对应</li>
|
|
|
+ * <li>扣款通知:接收支付宝周期卡扣款异步通知,激活/续期会员</li>
|
|
|
+ * <li>卡变更通知:接收卡模板审核结果通知(通过/驳回/冻结/解冻)</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>支付宝安心付接入流程:</b>
|
|
|
+ * <ol>
|
|
|
+ * <li>在支付宝开放平台开通「安心付」产品</li>
|
|
|
+ * <li>调用 {@link #createCardTemplate} 为每个支付方案创建周期卡模板</li>
|
|
|
+ * <li>支付宝审核通过后,卡模板状态变为 EFFECTIVE</li>
|
|
|
+ * <li>用户通过支付宝小程序购买周期卡(小程序端调用 my.commerceMerchantCardOrder)</li>
|
|
|
+ * <li>支付宝按周期自动扣款,并通过回调通知服务端</li>
|
|
|
+ * <li>服务端收到扣款通知后激活/续期会员</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * @see <a href="https://opendocs.alipay.com/solution/0d3p55">安心付-先享周期卡</a>
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class AlipayAnxinPayService {
|
|
|
+
|
|
|
+ private final AlipayAnxinCardMapper anxinCardMapper;
|
|
|
+ private final AlipayAnxinOrderRecordMapper anxinOrderRecordMapper;
|
|
|
+ private final PaymentPlanService paymentPlanService;
|
|
|
+ private final PaymentProperties paymentProperties;
|
|
|
+ private final MembershipService membershipService;
|
|
|
+ private final ObjectProvider<AlipayClient> anxinAlipayClientProvider;
|
|
|
+
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ public AlipayAnxinPayService(
|
|
|
+ AlipayAnxinCardMapper anxinCardMapper,
|
|
|
+ AlipayAnxinOrderRecordMapper anxinOrderRecordMapper,
|
|
|
+ PaymentPlanService paymentPlanService,
|
|
|
+ PaymentProperties paymentProperties,
|
|
|
+ MembershipService membershipService,
|
|
|
+ @Qualifier("anxinAlipayClient") ObjectProvider<AlipayClient> anxinAlipayClientProvider) {
|
|
|
+ this.anxinCardMapper = anxinCardMapper;
|
|
|
+ this.anxinOrderRecordMapper = anxinOrderRecordMapper;
|
|
|
+ this.paymentPlanService = paymentPlanService;
|
|
|
+ this.paymentProperties = paymentProperties;
|
|
|
+ this.membershipService = membershipService;
|
|
|
+ this.anxinAlipayClientProvider = anxinAlipayClientProvider;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================== 卡模板管理 ========================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 为支付方案创建安心付周期卡模板
|
|
|
+ *
|
|
|
+ * <p>调用 alipay.commerce.merchantcard.template.create 接口创建卡模板,
|
|
|
+ * 创建后状态为 PENDING(待审核),支付宝审核通过后变为 EFFECTIVE。
|
|
|
+ *
|
|
|
+ * @param planId 支付方案ID
|
|
|
+ * @return 创建的卡模板记录
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public AlipayAnxinCard createCardTemplate(Long planId) {
|
|
|
+ PaymentPlan plan = paymentPlanService.getPlanById(planId);
|
|
|
+ if (plan.getPrice() == null || plan.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ throw new BusinessException(ErrorCode.PARAM_ERROR, "支付方案价格必须大于0");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否已创建过卡模板
|
|
|
+ AlipayAnxinCard existing = anxinCardMapper.selectByPlanId(planId);
|
|
|
+ if (existing != null && existing.getCardId() != null) {
|
|
|
+ throw new BusinessException(ErrorCode.PAY_ANXIN_CARD_CREATE_FAILED,
|
|
|
+ "该方案已创建安心付卡模板: cardId=" + existing.getCardId());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (paymentProperties.isMock()) {
|
|
|
+ return mockCreateCardTemplate(plan);
|
|
|
+ }
|
|
|
+
|
|
|
+ return doCreateCardTemplate(plan, existing);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询卡模板状态(从本地数据库)
|
|
|
+ */
|
|
|
+ public AlipayAnxinCard getCardByPlanId(Long planId) {
|
|
|
+ AlipayAnxinCard card = anxinCardMapper.selectByPlanId(planId);
|
|
|
+ if (card == null) {
|
|
|
+ throw new BusinessException(ErrorCode.PAY_ANXIN_CARD_NOT_FOUND);
|
|
|
+ }
|
|
|
+ return card;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询所有卡模板
|
|
|
+ */
|
|
|
+ public List<AlipayAnxinCard> listAllCards() {
|
|
|
+ return anxinCardMapper.selectList(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 同步卡模板状态(调用支付宝接口查询最新状态)
|
|
|
+ *
|
|
|
+ * @param cardId 支付宝卡模板ID
|
|
|
+ * @return 更新后的卡模板记录
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public AlipayAnxinCard syncCardStatus(String cardId) {
|
|
|
+ AlipayAnxinCard card = anxinCardMapper.selectByCardId(cardId);
|
|
|
+ if (card == null) {
|
|
|
+ throw new BusinessException(ErrorCode.PAY_ANXIN_CARD_NOT_FOUND);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (paymentProperties.isMock()) {
|
|
|
+ return card;
|
|
|
+ }
|
|
|
+
|
|
|
+ AlipayClient client = anxinAlipayClientProvider.getIfAvailable();
|
|
|
+ if (client == null) {
|
|
|
+ throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "安心付AlipayClient未初始化");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ AlipayCommerceMerchantcardTemplateQueryRequest request = new AlipayCommerceMerchantcardTemplateQueryRequest();
|
|
|
+ request.setBizContent("{\"card_id\":\"" + cardId + "\"}");
|
|
|
+
|
|
|
+ boolean publicKeyMode = isPublicKeyMode();
|
|
|
+ AlipayCommerceMerchantcardTemplateQueryResponse response = publicKeyMode
|
|
|
+ ? client.execute(request)
|
|
|
+ : client.certificateExecute(request);
|
|
|
+
|
|
|
+ if (response.isSuccess()) {
|
|
|
+ String status = response.getBody();
|
|
|
+ log.info("查询安心付卡模板状态: cardId={}, body={}", cardId, status);
|
|
|
+ // 解析状态并更新
|
|
|
+ updateCardStatusFromResponse(card, response);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查询安心付卡模板状态失败: cardId={}", cardId, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ return card;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 修改卡模板状态(上架/下架/冻结)
|
|
|
+ *
|
|
|
+ * @param cardId 支付宝卡模板ID
|
|
|
+ * @param action 操作类型:EFFECTIVE(上架)/ INVALID(下架)/ FROZEN(冻结)
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public void modifyCardStatus(String cardId, String action) {
|
|
|
+ AlipayAnxinCard card = anxinCardMapper.selectByCardId(cardId);
|
|
|
+ if (card == null) {
|
|
|
+ throw new BusinessException(ErrorCode.PAY_ANXIN_CARD_NOT_FOUND);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (paymentProperties.isMock()) {
|
|
|
+ card.setStatus(action);
|
|
|
+ anxinCardMapper.updateById(card);
|
|
|
+ log.info("【模拟】修改安心付卡模板状态: cardId={}, action={}", cardId, action);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ AlipayClient client = anxinAlipayClientProvider.getIfAvailable();
|
|
|
+ if (client == null) {
|
|
|
+ throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "安心付AlipayClient未初始化");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ AlipayCommerceMerchantcardTemplatestatusModifyRequest request =
|
|
|
+ new AlipayCommerceMerchantcardTemplatestatusModifyRequest();
|
|
|
+ request.setBizContent("{\"card_id\":\"" + cardId + "\",\"status\":\"" + action + "\"}");
|
|
|
+
|
|
|
+ boolean publicKeyMode = isPublicKeyMode();
|
|
|
+ AlipayCommerceMerchantcardTemplatestatusModifyResponse response = publicKeyMode
|
|
|
+ ? client.execute(request)
|
|
|
+ : client.certificateExecute(request);
|
|
|
+
|
|
|
+ if (response.isSuccess()) {
|
|
|
+ card.setStatus(action);
|
|
|
+ anxinCardMapper.updateById(card);
|
|
|
+ log.info("修改安心付卡模板状态成功: cardId={}, action={}", cardId, action);
|
|
|
+ } else {
|
|
|
+ log.error("修改安心付卡模板状态失败: cardId={}, subCode={}, subMsg={}",
|
|
|
+ cardId, response.getSubCode(), response.getSubMsg());
|
|
|
+ throw new BusinessException(ErrorCode.PAY_ANXIN_CARD_CREATE_FAILED,
|
|
|
+ "修改卡模板状态失败: " + response.getSubMsg());
|
|
|
+ }
|
|
|
+ } catch (BusinessException e) {
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("修改安心付卡模板状态异常: cardId={}", cardId, e);
|
|
|
+ throw new BusinessException(ErrorCode.PAY_ANXIN_CARD_CREATE_FAILED,
|
|
|
+ "修改卡模板状态异常: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================== 扣款通知处理 ========================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理安心付扣款通知(核销订单变更通知)
|
|
|
+ *
|
|
|
+ * <p>支付宝在每次周期扣款后发送此通知,通知内容包括:
|
|
|
+ * <ul>
|
|
|
+ * <li>card_id — 卡模板ID</li>
|
|
|
+ * <li>order_id — 售卖订单ID</li>
|
|
|
+ * <li>sub_order_id — 核销子订单ID(用于幂等)</li>
|
|
|
+ * <li>deduct_amount — 扣款金额</li>
|
|
|
+ * <li>deduct_status — 扣款状态(SUCCESS/FAIL)</li>
|
|
|
+ * <li>deduct_time — 扣款时间</li>
|
|
|
+ * <li>out_biz_no — 商家业务号(存储系统用户ID)</li>
|
|
|
+ * <li>user_id — 支付宝用户ID</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * @param params 通知参数
|
|
|
+ * @return 处理结果(success/fail)
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public String handleDeductNotify(Map<String, String> params) {
|
|
|
+ log.info("收到安心付扣款通知: {}", maskSensitiveParams(params));
|
|
|
+
|
|
|
+ String subOrderId = params.get("sub_order_id");
|
|
|
+ String orderId = params.get("order_id");
|
|
|
+ String cardId = params.get("card_id");
|
|
|
+ String deductStatus = params.get("deduct_status");
|
|
|
+ String deductAmount = params.get("deduct_amount");
|
|
|
+ String deductTime = params.get("deduct_time");
|
|
|
+ String outBizNo = params.get("out_biz_no");
|
|
|
+ String alipayUserId = params.get("user_id");
|
|
|
+
|
|
|
+ if (subOrderId == null) {
|
|
|
+ log.warn("安心付扣款通知缺少 sub_order_id");
|
|
|
+ return "fail";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 幂等:已处理过的扣款通知直接返回 success
|
|
|
+ AlipayAnxinOrderRecord existing = anxinOrderRecordMapper.selectBySubOrderId(subOrderId);
|
|
|
+ if (existing != null && existing.getMembershipActivated() == 1) {
|
|
|
+ log.info("安心付扣款通知已处理,跳过: subOrderId={}", subOrderId);
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找关联的卡模板和支付方案
|
|
|
+ AlipayAnxinCard card = cardId != null ? anxinCardMapper.selectByCardId(cardId) : null;
|
|
|
+ if (card == null) {
|
|
|
+ log.warn("安心付扣款通知未匹配到卡模板: cardId={}", cardId);
|
|
|
+ // 仍然返回 success 避免支付宝重复通知
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ PaymentPlan plan = paymentPlanService.getPlanById(card.getPlanId());
|
|
|
+
|
|
|
+ // 解析系统用户ID(通过 out_biz_no 传入的系统用户ID)
|
|
|
+ Long userId = parseUserIdFromOutBizNo(outBizNo);
|
|
|
+ if (userId == null) {
|
|
|
+ log.warn("安心付扣款通知无法匹配系统用户: outBizNo={}, alipayUserId={}", outBizNo, alipayUserId);
|
|
|
+ // 记录通知但不激活会员
|
|
|
+ saveOrderRecord(params, card, plan, null, deductStatus, deductAmount, deductTime);
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存扣款记录
|
|
|
+ AlipayAnxinOrderRecord record = saveOrderRecord(params, card, plan, userId, deductStatus, deductAmount, deductTime);
|
|
|
+
|
|
|
+ // 扣款成功时激活/续期会员
|
|
|
+ if ("SUCCESS".equalsIgnoreCase(deductStatus)) {
|
|
|
+ try {
|
|
|
+ activateMembership(userId, plan, record);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("安心付扣款后会员激活失败: userId={}, subOrderId={}", userId, subOrderId, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理安心付卡变更通知(卡模板审核结果通知)
|
|
|
+ *
|
|
|
+ * <p>支付宝在卡模板审核通过/驳回/冻结/解冻时发送此通知。
|
|
|
+ *
|
|
|
+ * @param params 通知参数
|
|
|
+ * @return 处理结果(success/fail)
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public String handleCardChangeNotify(Map<String, String> params) {
|
|
|
+ log.info("收到安心付卡变更通知: {}", params);
|
|
|
+
|
|
|
+ String cardId = params.get("card_id");
|
|
|
+ String changeType = params.get("change_type");
|
|
|
+ String status = params.get("status");
|
|
|
+
|
|
|
+ if (cardId == null) {
|
|
|
+ log.warn("安心付卡变更通知缺少 card_id");
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ AlipayAnxinCard card = anxinCardMapper.selectByCardId(cardId);
|
|
|
+ if (card == null) {
|
|
|
+ log.warn("安心付卡变更通知未匹配到卡模板: cardId={}", cardId);
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新卡模板状态
|
|
|
+ if (status != null && !status.isBlank()) {
|
|
|
+ card.setStatus(status);
|
|
|
+ }
|
|
|
+ anxinCardMapper.updateById(card);
|
|
|
+ log.info("安心付卡模板状态更新: cardId={}, changeType={}, status={}", cardId, changeType, status);
|
|
|
+
|
|
|
+ return "success";
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================== 内部方法 ========================
|
|
|
+
|
|
|
+ private AlipayAnxinCard doCreateCardTemplate(PaymentPlan plan, AlipayAnxinCard existing) {
|
|
|
+ PaymentProperties.AnxinAlipay ax = paymentProperties.getAnxinAlipay();
|
|
|
+ AlipayClient client = anxinAlipayClientProvider.getIfAvailable();
|
|
|
+ if (client == null) {
|
|
|
+ throw new BusinessException(ErrorCode.PAY_CHANNEL_NOT_CONFIGURED, "安心付AlipayClient未初始化");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ AlipayCommerceMerchantcardTemplateCreateRequest request =
|
|
|
+ new AlipayCommerceMerchantcardTemplateCreateRequest();
|
|
|
+
|
|
|
+ // 设置通知地址(优先使用安心付专用配置,回退到支付宝通用配置)
|
|
|
+ String notifyUrl = ax.getNotifyUrl() != null && !ax.getNotifyUrl().isBlank()
|
|
|
+ ? ax.getNotifyUrl() : paymentProperties.getAlipay().getNotifyUrl();
|
|
|
+ request.setNotifyUrl(notifyUrl);
|
|
|
+
|
|
|
+ // 构建卡模板对象
|
|
|
+ MerchantCardTemplate cardTemplate = new MerchantCardTemplate();
|
|
|
+ cardTemplate.setCardType("PERIOD_PAY");
|
|
|
+ cardTemplate.setCardTemplateName(plan.getPlanName());
|
|
|
+ cardTemplate.setOutCardId("plan_" + plan.getId());
|
|
|
+ cardTemplate.setNeedOrderAgreement(true);
|
|
|
+
|
|
|
+ // 设置小程序appId(安心付小程序应用ID)
|
|
|
+ if (ax.getAppId() != null && !ax.getAppId().isBlank()) {
|
|
|
+ cardTemplate.setCardTemplateAppId(ax.getAppId());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建使用信息(价格+周期)
|
|
|
+ CardTemplateUse useInfo = new CardTemplateUse();
|
|
|
+ useInfo.setPriceMode("PERIOD_PRICE");
|
|
|
+
|
|
|
+ // 周期价格列表(价格单位:分)
|
|
|
+ long priceInCents = plan.getPrice().multiply(new BigDecimal("100")).longValue();
|
|
|
+ CardPeriodPrice periodPrice = new CardPeriodPrice();
|
|
|
+ periodPrice.setPeriod(1L);
|
|
|
+ periodPrice.setSalePrice(priceInCents);
|
|
|
+ periodPrice.setOriginalPrice(priceInCents);
|
|
|
+ useInfo.setPeriodPriceList(List.of(periodPrice));
|
|
|
+
|
|
|
+ // 周期扣款信息
|
|
|
+ CardCycle cycleInfo = new CardCycle();
|
|
|
+ cycleInfo.setCycleType("DAY");
|
|
|
+ cycleInfo.setCycleValue(String.valueOf(plan.getDurationDays()));
|
|
|
+ cycleInfo.setCycleChargeType("PERIOD");
|
|
|
+ cycleInfo.setChargeNow(true);
|
|
|
+ useInfo.setCycleInfo(cycleInfo);
|
|
|
+
|
|
|
+ cardTemplate.setUseInfo(useInfo);
|
|
|
+
|
|
|
+ // 构建售卖信息
|
|
|
+ CardTemplateSale saleInfo = new CardTemplateSale();
|
|
|
+ cardTemplate.setSaleInfo(saleInfo);
|
|
|
+
|
|
|
+ AlipayCommerceMerchantcardTemplateCreateModel model = new AlipayCommerceMerchantcardTemplateCreateModel();
|
|
|
+ model.setCardTemplate(cardTemplate);
|
|
|
+
|
|
|
+ request.setBizModel(model);
|
|
|
+
|
|
|
+ boolean publicKeyMode = isPublicKeyMode();
|
|
|
+ AlipayCommerceMerchantcardTemplateCreateResponse response = publicKeyMode
|
|
|
+ ? client.execute(request)
|
|
|
+ : client.certificateExecute(request);
|
|
|
+
|
|
|
+ if (!response.isSuccess()) {
|
|
|
+ log.error("安心付卡模板创建失败: planId={}, code={}, msg={}, subCode={}, subMsg={}",
|
|
|
+ plan.getId(), response.getCode(), response.getMsg(),
|
|
|
+ response.getSubCode(), response.getSubMsg());
|
|
|
+ throw new BusinessException(ErrorCode.PAY_ANXIN_CARD_CREATE_FAILED,
|
|
|
+ "创建卡模板失败: " + response.getSubMsg());
|
|
|
+ }
|
|
|
+
|
|
|
+ String cardId = response.getCardTemplateId();
|
|
|
+ log.info("安心付卡模板创建成功: planId={}, cardId={}", plan.getId(), cardId);
|
|
|
+
|
|
|
+ // 保存或更新卡模板记录
|
|
|
+ AlipayAnxinCard card = existing != null ? existing : new AlipayAnxinCard();
|
|
|
+ card.setPlanId(plan.getId());
|
|
|
+ card.setCardId(cardId);
|
|
|
+ card.setCardName(plan.getPlanName());
|
|
|
+ card.setCardType("PERIOD_PAY");
|
|
|
+ card.setPeriodAmount(plan.getPrice());
|
|
|
+ card.setPeriodDays(plan.getDurationDays());
|
|
|
+ card.setStatus("PENDING");
|
|
|
+ card.setRawResponse(response.getBody());
|
|
|
+
|
|
|
+ if (existing != null) {
|
|
|
+ anxinCardMapper.updateById(card);
|
|
|
+ } else {
|
|
|
+ anxinCardMapper.insert(card);
|
|
|
+ }
|
|
|
+
|
|
|
+ return card;
|
|
|
+ } catch (BusinessException e) {
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("安心付卡模板创建异常: planId={}", plan.getId(), e);
|
|
|
+ throw new BusinessException(ErrorCode.PAY_ANXIN_CARD_CREATE_FAILED,
|
|
|
+ "创建卡模板异常: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private AlipayAnxinCard mockCreateCardTemplate(PaymentPlan plan) {
|
|
|
+ String mockCardId = "MOCK_CARD_" + IdUtil.getSnowflake(1, 1).nextIdStr();
|
|
|
+ log.info("【模拟】创建安心付卡模板: planId={}, cardId={}", plan.getId(), mockCardId);
|
|
|
+
|
|
|
+ AlipayAnxinCard card = AlipayAnxinCard.builder()
|
|
|
+ .planId(plan.getId())
|
|
|
+ .cardId(mockCardId)
|
|
|
+ .cardName(plan.getPlanName())
|
|
|
+ .cardType("PERIOD_PAY")
|
|
|
+ .periodAmount(plan.getPrice())
|
|
|
+ .periodDays(plan.getDurationDays())
|
|
|
+ .status("EFFECTIVE")
|
|
|
+ .appointmentUrl("alipays://platformapi/startapp?appId=mock&cardId=" + mockCardId)
|
|
|
+ .rawResponse("{\"mock\":true}")
|
|
|
+ .build();
|
|
|
+ anxinCardMapper.insert(card);
|
|
|
+ return card;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateCardStatusFromResponse(AlipayAnxinCard card, AlipayCommerceMerchantcardTemplateQueryResponse response) {
|
|
|
+ try {
|
|
|
+ String body = response.getBody();
|
|
|
+ if (body != null) {
|
|
|
+ Map<String, Object> map = objectMapper.readValue(body, Map.class);
|
|
|
+ Object status = map.get("status");
|
|
|
+ if (status != null) {
|
|
|
+ card.setStatus(status.toString());
|
|
|
+ anxinCardMapper.updateById(card);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析卡模板状态响应失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private AlipayAnxinOrderRecord saveOrderRecord(Map<String, String> params, AlipayAnxinCard card,
|
|
|
+ PaymentPlan plan, Long userId, String deductStatus,
|
|
|
+ String deductAmount, String deductTime) {
|
|
|
+ String subOrderId = params.get("sub_order_id");
|
|
|
+ String orderId = params.get("order_id");
|
|
|
+ String outBizNo = params.get("out_biz_no");
|
|
|
+ String alipayUserId = params.get("user_id");
|
|
|
+
|
|
|
+ // 幂等:如果已存在则更新
|
|
|
+ AlipayAnxinOrderRecord record = subOrderId != null
|
|
|
+ ? anxinOrderRecordMapper.selectBySubOrderId(subOrderId) : null;
|
|
|
+ if (record == null) {
|
|
|
+ record = new AlipayAnxinOrderRecord();
|
|
|
+ }
|
|
|
+
|
|
|
+ record.setUserId(userId);
|
|
|
+ record.setPlanId(plan.getId());
|
|
|
+ record.setCardId(card.getCardId());
|
|
|
+ record.setOrderId(orderId);
|
|
|
+ record.setSubOrderId(subOrderId);
|
|
|
+ record.setOutBizNo(outBizNo);
|
|
|
+ record.setAlipayUserId(alipayUserId);
|
|
|
+ record.setDeductStatus(deductStatus != null ? deductStatus.toUpperCase() : null);
|
|
|
+ record.setDeductAmount(deductAmount != null ? new BigDecimal(deductAmount) : null);
|
|
|
+ record.setDeductTime(parseDateTime(deductTime));
|
|
|
+ record.setRawParams(toJson(params));
|
|
|
+
|
|
|
+ if (record.getId() != null) {
|
|
|
+ anxinOrderRecordMapper.updateById(record);
|
|
|
+ } else {
|
|
|
+ anxinOrderRecordMapper.insert(record);
|
|
|
+ }
|
|
|
+
|
|
|
+ return record;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void activateMembership(Long userId, PaymentPlan plan, AlipayAnxinOrderRecord record) {
|
|
|
+ MembershipLevel level = MembershipLevel.fromCode(plan.getMembershipLevel());
|
|
|
+ if (level == null) {
|
|
|
+ log.warn("安心付扣款通知:无法识别会员等级: {}", plan.getMembershipLevel());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成支付订单号(用于会员激活记录)
|
|
|
+ String orderNo = "AX" + IdUtil.getSnowflake(1, 1).nextIdStr();
|
|
|
+ record.setOrderNo(orderNo);
|
|
|
+ record.setMembershipActivated(1);
|
|
|
+ anxinOrderRecordMapper.updateById(record);
|
|
|
+
|
|
|
+ // 激活/续期会员
|
|
|
+ membershipService.activateFromPayment(userId, level, plan.getDurationDays(), orderNo);
|
|
|
+ log.info("安心付扣款会员激活成功: userId={}, level={}, days={}, orderNo={}",
|
|
|
+ userId, level.getCode(), plan.getDurationDays(), orderNo);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long parseUserIdFromOutBizNo(String outBizNo) {
|
|
|
+ if (outBizNo == null || outBizNo.isBlank()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Long.parseLong(outBizNo);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("无法从 out_biz_no 解析用户ID: {}", outBizNo);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private LocalDateTime parseDateTime(String dateTimeStr) {
|
|
|
+ if (dateTimeStr == null || dateTimeStr.isBlank()) {
|
|
|
+ return LocalDateTime.now();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
|
|
+ } catch (Exception e) {
|
|
|
+ try {
|
|
|
+ return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
|
|
+ } catch (Exception e2) {
|
|
|
+ return LocalDateTime.now();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String toJson(Map<String, String> params) {
|
|
|
+ try {
|
|
|
+ return objectMapper.writeValueAsString(params);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return params.toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, String> maskSensitiveParams(Map<String, String> params) {
|
|
|
+ Map<String, String> masked = new HashMap<>(params);
|
|
|
+ if (masked.containsKey("sign")) {
|
|
|
+ masked.put("sign", "***");
|
|
|
+ }
|
|
|
+ return masked;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isPublicKeyMode() {
|
|
|
+ PaymentProperties.AnxinAlipay ax = paymentProperties.getAnxinAlipay();
|
|
|
+ return ax.getAlipayPublicKey() != null && !ax.getAlipayPublicKey().isBlank();
|
|
|
+ }
|
|
|
+}
|