package com.tcm.training.admin.trade.service; import cn.idev.excel.FastExcel; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tcm.training.admin.common.DateTimes; import com.tcm.training.admin.common.PageResponse; import com.tcm.training.admin.common.PhoneMasker; import com.tcm.training.admin.trade.entity.PaymentFlow; import com.tcm.training.admin.trade.excel.PaymentFlowExcelRow; import com.tcm.training.admin.trade.excel.RefundFlowExcelRow; import com.tcm.training.admin.trade.mapper.PaymentFlowMapper; import com.tcm.training.admin.trade.row.PaymentFlowRow; import com.tcm.training.admin.trade.row.PaymentFlowSyncRow; import com.tcm.training.admin.trade.vo.PaymentFlowListItemVO; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.math.BigDecimal; import java.net.URLEncoder; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Service public class PaymentFlowAppService { private static final String BUSINESS_TYPE_COURSE_PURCHASE = "COURSE_PURCHASE"; private static final String BUSINESS_TYPE_MEMBER_PURCHASE = "MEMBER_PURCHASE"; private static final String FLOW_TYPE_PAYMENT = "PAYMENT"; private static final String FLOW_TYPE_REFUND = "REFUND"; private static final String TRADE_STATUS_SUCCESS = "SUCCESS"; private static final String TRADE_STATUS_REFUNDED = "REFUNDED"; private static final String SOURCE_TYPE_COURSE_PAYMENT = "COURSE_PAYMENT"; private static final String SOURCE_TYPE_COURSE_REFUND = "COURSE_REFUND"; private static final String SOURCE_TYPE_MEMBER_PAYMENT = "MEMBER_PAYMENT"; private static final String SOURCE_TYPE_MEMBER_REFUND = "MEMBER_REFUND"; private static final String PAYMENT_METHOD_WECHAT = "WECHAT"; private static final String PAYMENT_METHOD_ALIPAY = "ALIPAY"; private final PaymentFlowMapper paymentFlowMapper; public PaymentFlowAppService(PaymentFlowMapper paymentFlowMapper) { this.paymentFlowMapper = paymentFlowMapper; } public PageResponse pagePaymentFlows(int page, int pageSize, String flowNo, String businessOrderNo, String userPhone, String productKeyword, String businessType, String paymentMethod, LocalDateTime tradeFrom, LocalDateTime tradeTo) { syncFlows(); IPage result = queryFlowPage(page, pageSize, flowNo, businessOrderNo, userPhone, productKeyword, businessType, FLOW_TYPE_PAYMENT, paymentMethod, TRADE_STATUS_SUCCESS, tradeFrom, tradeTo); return PageResponse.of(toList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize()); } public PageResponse pageRefundFlows(int page, int pageSize, String flowNo, String businessOrderNo, String userPhone, String productKeyword, String businessType, String paymentMethod, LocalDateTime tradeFrom, LocalDateTime tradeTo) { syncFlows(); IPage result = queryFlowPage(page, pageSize, flowNo, businessOrderNo, userPhone, productKeyword, businessType, FLOW_TYPE_REFUND, paymentMethod, TRADE_STATUS_REFUNDED, tradeFrom, tradeTo); return PageResponse.of(toList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize()); } public void exportPaymentFlows(HttpServletResponse response, String flowNo, String businessOrderNo, String userPhone, String productKeyword, String businessType, String paymentMethod, LocalDateTime tradeFrom, LocalDateTime tradeTo) throws IOException { syncFlows(); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String filename = URLEncoder.encode("支付流水", "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx"); List rows = queryFlowList(flowNo, businessOrderNo, userPhone, productKeyword, businessType, FLOW_TYPE_PAYMENT, paymentMethod, TRADE_STATUS_SUCCESS, tradeFrom, tradeTo); List exports = new ArrayList(); for (int i = 0; i < rows.size(); i++) { PaymentFlowRow row = rows.get(i); PaymentFlowExcelRow export = new PaymentFlowExcelRow(); export.setSerialNo(i + 1); export.setBusinessOrderNo(defaultText(row.getBusinessOrderNo())); export.setFlowNo(defaultText(row.getFlowNo())); export.setUserNickname(defaultText(row.getUserNickname())); export.setUserPhone(PhoneMasker.mask(row.getUserPhone())); export.setBusinessType(orderSourceText(row.getBusinessType())); export.setProductNameSnapshot(defaultText(row.getProductNameSnapshot())); export.setPaymentMethod(paymentMethodText(row.getPaymentMethod())); export.setTransactionAmount(formatAmount(row.getTransactionAmount())); export.setTradeTime(DateTimes.format(row.getTradeTime())); exports.add(export); } FastExcel.write(response.getOutputStream(), PaymentFlowExcelRow.class) .sheet("支付流水") .doWrite(exports); } public void exportRefundFlows(HttpServletResponse response, String flowNo, String businessOrderNo, String userPhone, String productKeyword, String businessType, String paymentMethod, LocalDateTime tradeFrom, LocalDateTime tradeTo) throws IOException { syncFlows(); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String filename = URLEncoder.encode("退费流水", "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx"); List rows = queryFlowList(flowNo, businessOrderNo, userPhone, productKeyword, businessType, FLOW_TYPE_REFUND, paymentMethod, TRADE_STATUS_REFUNDED, tradeFrom, tradeTo); List exports = new ArrayList(); for (int i = 0; i < rows.size(); i++) { PaymentFlowRow row = rows.get(i); RefundFlowExcelRow export = new RefundFlowExcelRow(); export.setSerialNo(i + 1); export.setBusinessOrderNo(defaultText(row.getBusinessOrderNo())); export.setFlowNo(defaultText(row.getFlowNo())); export.setUserNickname(defaultText(row.getUserNickname())); export.setUserPhone(PhoneMasker.mask(row.getUserPhone())); export.setBusinessType(orderSourceText(row.getBusinessType())); export.setProductNameSnapshot(defaultText(row.getProductNameSnapshot())); export.setPaymentMethod(paymentMethodText(row.getPaymentMethod())); export.setTransactionAmount(formatAmount(row.getTransactionAmount())); export.setTradeTime(DateTimes.format(row.getTradeTime())); exports.add(export); } FastExcel.write(response.getOutputStream(), RefundFlowExcelRow.class) .sheet("退费流水") .doWrite(exports); } public void syncFlows() { syncMissingFlows(paymentFlowMapper.selectMissingCoursePaymentRows(), BUSINESS_TYPE_COURSE_PURCHASE, FLOW_TYPE_PAYMENT, TRADE_STATUS_SUCCESS, SOURCE_TYPE_COURSE_PAYMENT); syncMissingFlows(paymentFlowMapper.selectMissingCourseRefundRows(), BUSINESS_TYPE_COURSE_PURCHASE, FLOW_TYPE_REFUND, TRADE_STATUS_REFUNDED, SOURCE_TYPE_COURSE_REFUND); syncMissingFlows(paymentFlowMapper.selectMissingMemberPaymentRows(), BUSINESS_TYPE_MEMBER_PURCHASE, FLOW_TYPE_PAYMENT, TRADE_STATUS_SUCCESS, SOURCE_TYPE_MEMBER_PAYMENT); syncMissingFlows(paymentFlowMapper.selectMissingMemberRefundRows(), BUSINESS_TYPE_MEMBER_PURCHASE, FLOW_TYPE_REFUND, TRADE_STATUS_REFUNDED, SOURCE_TYPE_MEMBER_REFUND); } private void syncMissingFlows(List rows, String businessType, String flowType, String tradeStatus, String sourceType) { for (PaymentFlowSyncRow row : rows) { PaymentFlow flow = new PaymentFlow(); flow.setFlowNo(buildFlowNo(businessType, flowType, row.getBusinessOrderNo())); flow.setBusinessType(businessType); flow.setBusinessOrderId(row.getBusinessOrderId()); flow.setBusinessOrderNo(row.getBusinessOrderNo()); flow.setFlowType(flowType); flow.setTradeStatus(tradeStatus); flow.setAppUserId(row.getAppUserId()); flow.setUserNickname(row.getUserNickname()); flow.setUserPhone(row.getUserPhone()); flow.setProductNameSnapshot(row.getProductNameSnapshot()); flow.setPaymentMethod(row.getPaymentMethod()); flow.setTransactionAmount(defaultAmount(row.getTransactionAmount())); flow.setRemark(trimToNull(row.getRemark())); flow.setTradeTime(row.getTradeTime()); flow.setSourceType(sourceType); flow.setSourceId(row.getSourceId()); try { paymentFlowMapper.insert(flow); } catch (DuplicateKeyException ignored) { } } } private IPage queryFlowPage(int page, int pageSize, String flowNo, String businessOrderNo, String userPhone, String productKeyword, String businessType, String flowType, String paymentMethod, String tradeStatus, LocalDateTime tradeFrom, LocalDateTime tradeTo) { return paymentFlowMapper.selectPaymentFlowPage( new Page(page, pageSize), trimToNull(flowNo), trimToNull(businessOrderNo), trimToNull(userPhone), trimToNull(productKeyword), trimToNull(businessType), flowType, trimToNull(paymentMethod), tradeStatus, tradeFrom, tradeTo ); } private List queryFlowList(String flowNo, String businessOrderNo, String userPhone, String productKeyword, String businessType, String flowType, String paymentMethod, String tradeStatus, LocalDateTime tradeFrom, LocalDateTime tradeTo) { return paymentFlowMapper.selectPaymentFlowList( trimToNull(flowNo), trimToNull(businessOrderNo), trimToNull(userPhone), trimToNull(productKeyword), trimToNull(businessType), flowType, trimToNull(paymentMethod), tradeStatus, tradeFrom, tradeTo ); } private List toList(List rows) { List list = new ArrayList(); for (PaymentFlowRow row : rows) { PaymentFlowListItemVO item = new PaymentFlowListItemVO(); item.setId(row.getId()); item.setFlowNo(row.getFlowNo()); item.setBusinessType(row.getBusinessType()); item.setBusinessOrderId(row.getBusinessOrderId()); item.setBusinessOrderNo(row.getBusinessOrderNo()); item.setFlowType(row.getFlowType()); item.setTradeStatus(row.getTradeStatus()); item.setUserNickname(row.getUserNickname()); item.setUserPhone(PhoneMasker.mask(row.getUserPhone())); item.setProductNameSnapshot(row.getProductNameSnapshot()); item.setPaymentMethod(row.getPaymentMethod()); item.setTransactionAmount(row.getTransactionAmount()); item.setThirdPartyNo(row.getThirdPartyNo()); item.setRemark(row.getRemark()); item.setTradeTime(row.getTradeTime()); list.add(item); } return list; } private String buildFlowNo(String businessType, String flowType, String businessOrderNo) { String businessPrefix = BUSINESS_TYPE_COURSE_PURCHASE.equals(businessType) ? "COURSE" : "MEMBER"; String flowPrefix = FLOW_TYPE_PAYMENT.equals(flowType) ? "PAY" : "REFUND"; return flowPrefix + "-" + businessPrefix + "-" + businessOrderNo; } private BigDecimal defaultAmount(BigDecimal amount) { return amount == null ? BigDecimal.ZERO : amount; } private String trimToNull(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } private String defaultText(String value) { return value == null || value.trim().isEmpty() ? "-" : value.trim(); } private String formatAmount(BigDecimal value) { BigDecimal amount = value == null ? BigDecimal.ZERO : value; return amount.stripTrailingZeros().toPlainString(); } private String orderSourceText(String value) { if (BUSINESS_TYPE_COURSE_PURCHASE.equals(value)) { return "养生课程"; } if (BUSINESS_TYPE_MEMBER_PURCHASE.equals(value)) { return "会员"; } return "-"; } public String paymentMethodText(String value) { if (PAYMENT_METHOD_WECHAT.equals(value)) { return "微信支付"; } if (PAYMENT_METHOD_ALIPAY.equals(value)) { return "支付宝支付"; } return "-"; } }