package com.ruoyi.system.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.InfoBankCard; import com.ruoyi.system.domain.InfoUser; import com.ruoyi.system.domain.PaymentMethodConfig; import com.ruoyi.system.domain.PosStoreLinePay; import com.ruoyi.system.domain.PosStoreOmg; import com.ruoyi.system.mapper.InfoBankCardMapper; import com.ruoyi.system.mapper.InfoUserMapper; import com.ruoyi.system.mapper.PaymentMethodConfigMapper; import com.ruoyi.system.mapper.PosStoreLinePayMapper; import com.ruoyi.system.mapper.PosStoreOmgMapper; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * 支付方式闸门服务(031 可配置支付方式)——全系统唯一的支付方式可用性判定入口。 * *

三层闸门:某支付方式可用 = 平台开关(方式×维度,缺行=开) * ∩ 收款方选择(info_user 选择列,NULL=全部)∩ 就绪度(门店凭证或启用银行卡)。 * 所有下单入口(用户餐饮下单、商家建单、闪送下单)与 bankInfo 必须统一调用 * {@link #assertUsable(String, GateScope, Long, Long)},禁止在各自入口重复实现判定逻辑, * 保证闸门规则变化只改本类、全部入口行为一致(spec FR-004)。

* *

刻意不加缓存:开关表最多 4×2 行的小表索引查询,直查天然满足"保存后下一次查询即生效" * (SC-001),避免缓存失效复杂度。

*/ @Service public class PaymentMethodGateService { /** 支付方式组代码:到付(payType=1)。 */ public static final String GROUP_COD = "COD"; /** 支付方式组代码:信用卡+Apple Pay 同组(payType=2、5,OMG 渠道同开同关)。 */ public static final String GROUP_CARD_OMG = "CARD_OMG"; /** 支付方式组代码:LINE Pay(payType=3)。 */ public static final String GROUP_LINE_PAY = "LINE_PAY"; /** 支付方式组代码:线下转账(payType=6)。 */ public static final String GROUP_OFFLINE = "OFFLINE_TRANSFER"; /** 现金 payType=4:仅商家建单场景,明确不纳入闸门管控,直接放行(spec FR-006)。 */ public static final String PAY_TYPE_CASH = "4"; /** payType 数值 → 组代码;不在表内的值视为非法。 */ private static final Map PAY_TYPE_TO_GROUP = Map.of( "1", GROUP_COD, "2", GROUP_CARD_OMG, "3", GROUP_LINE_PAY, "5", GROUP_CARD_OMG, "6", GROUP_OFFLINE); /** 各维度可管控的组集合;RIDER_FLASH 无线上卡支付(闪送资金直达骑手)。 */ private static final Map> SCOPE_GROUPS = Map.of( GateScope.MERCHANT, List.of(GROUP_COD, GROUP_CARD_OMG, GROUP_LINE_PAY, GROUP_OFFLINE), GateScope.RIDER_FLASH, List.of(GROUP_COD, GROUP_LINE_PAY, GROUP_OFFLINE)); /** 闸门维度:商家餐饮单(用户向商家付款)与骑手闪送(用户直达骑手)独立开关。 */ public enum GateScope { MERCHANT, RIDER_FLASH } /** * 供结算页渲染的一项可选支付方式。 * CARD_OMG 组按 payType 展开为 2(信用卡)、5(Apple Pay)两项,组内 ready 一致。 */ public static class PayMethodOption { /** 支付方式组代码。 */ private final String methodCode; /** 对外 payType 数值("1"/"2"/"3"/"5"/"6")。 */ private final String payType; /** 就绪度:false 表示当前不可实际使用(如线下转账无启用卡),前端应置灰或不渲染。 */ private final boolean ready; public PayMethodOption(String methodCode, String payType, boolean ready) { this.methodCode = methodCode; this.payType = payType; this.ready = ready; } public String getMethodCode() { return methodCode; } public String getPayType() { return payType; } public boolean isReady() { return ready; } } private final PaymentMethodConfigMapper configMapper; private final InfoUserMapper userMapper; private final InfoBankCardMapper bankCardMapper; private final PosStoreOmgMapper omgMapper; private final PosStoreLinePayMapper linePayMapper; public PaymentMethodGateService(PaymentMethodConfigMapper configMapper, InfoUserMapper userMapper, InfoBankCardMapper bankCardMapper, PosStoreOmgMapper omgMapper, PosStoreLinePayMapper linePayMapper) { this.configMapper = configMapper; this.userMapper = userMapper; this.bankCardMapper = bankCardMapper; this.omgMapper = omgMapper; this.linePayMapper = linePayMapper; } /** * 校验支付方式可用,不可用抛出国际化异常;三下单入口与 bankInfo 统一调用(唯一入口)。 * * @param payType 支付方式数值(与订单 payType 同域) * @param scope 闸门维度 * @param storeId 门店 ID(MERCHANT 维度线上渠道就绪度用:pos_store_omg/pos_store_line_pay 为门店级凭证) * @param payeeUserId 收款方用户 ID(商家主账号;闪送下单时无收款方传 null,跳过选择与就绪度层) */ public void assertUsable(String payType, GateScope scope, Long storeId, Long payeeUserId) { if (!isUsable(payType, scope, storeId, payeeUserId)) { throw new ServiceException(MessageUtils.message("pay.method.not.available")); } } /** * 列出某维度对收款方当前可用的支付方式(结算页可选项、骑手端可选集共用)。 * 返回平台开关 ∩ 收款方选择的全部项并附就绪度;RIDER_FLASH 维度就绪度恒 true * (收款方未定或即骑手本人,线下卡是否就绪由骑手自选方式时自行把握,抢单过滤兜底)。 */ public List listAvailable(GateScope scope, Long storeId, Long payeeUserId) { Map disabled = platformDisabledMap(scope); Set selected = selectedGroups(scope, payeeUserId); List result = new ArrayList<>(); for (String group : SCOPE_GROUPS.get(scope)) { if (disabled.containsKey(group)) continue; if (selected != null && !selected.contains(group)) continue; boolean ready = isReady(group, scope, storeId, payeeUserId); for (String payType : payTypesOf(group)) { result.add(new PayMethodOption(group, payType, ready)); } } return result; } /** 单一判定实现:assertUsable 与调用方需要的布尔语义共用,保证两口径永不分裂。 */ private boolean isUsable(String payType, GateScope scope, Long storeId, Long payeeUserId) { // 现金仅商家建单场景使用,明确不纳入管控(spec FR-006)。 if (PAY_TYPE_CASH.equals(payType)) return true; String group = PAY_TYPE_TO_GROUP.get(payType); if (group == null || !SCOPE_GROUPS.get(scope).contains(group)) return false; if (platformDisabledMap(scope).containsKey(group)) return false; Set selected = selectedGroups(scope, payeeUserId); if (selected != null && !selected.contains(group)) return false; return isReady(group, scope, storeId, payeeUserId); } /** 平台已关闭的组集合(该维度);缺行=开,仅 enabled=0 的行进集合。 */ private Map platformDisabledMap(GateScope scope) { List rows = configMapper.selectList( new QueryWrapper().eq("scope", scope.name())); Map disabled = new HashMap<>(); for (PaymentMethodConfig row : rows) { if (Integer.valueOf(0).equals(row.getEnabled())) { disabled.put(row.getMethodCode(), Boolean.TRUE); } } return disabled; } /** 收款方接受的组集合;null/空选择返回 null 表示"全部接受"(存量零行为变化)。 */ private Set selectedGroups(GateScope scope, Long payeeUserId) { if (payeeUserId == null) return null; InfoUser user = userMapper.selectById(payeeUserId); if (user == null) return null; String csv = scope == GateScope.MERCHANT ? user.getPayMethods() : user.getFlashPayMethods(); if (StringUtils.isEmpty(csv) || csv.trim().isEmpty()) return null; Set groups = new java.util.HashSet<>(); for (String part : csv.split(",")) { String value = part.trim(); if (!value.isEmpty()) groups.add(value); } return groups.isEmpty() ? null : groups; } /** * 就绪度判定:MERCHANT 维度线上渠道看门店级凭证、线下转账看商家启用卡; * RIDER_FLASH 维度恒满足(收款方未定,由骑手选择层与抢单过滤兜底)。 */ private boolean isReady(String group, GateScope scope, Long storeId, Long payeeUserId) { if (group.equals(GROUP_COD)) return true; if (scope == GateScope.RIDER_FLASH) return true; switch (group) { case GROUP_CARD_OMG: return storeId != null && omgMapper.selectCount( new QueryWrapper().eq("store_id", storeId).eq("is_enabled", 1)) > 0; case GROUP_LINE_PAY: return storeId != null && linePayMapper.selectCount( new QueryWrapper().eq("store_id", storeId).eq("is_enabled", 1)) > 0; case GROUP_OFFLINE: return payeeUserId != null && bankCardMapper.selectCount( new QueryWrapper().eq("user_id", payeeUserId).eq("is_active", 1)) > 0; default: return false; } } /** 组代码 → 对外 payType 展开顺序(保持稳定展示顺序)。 */ private List payTypesOf(String group) { switch (group) { case GROUP_COD: return List.of("1"); case GROUP_CARD_OMG: return List.of("2", "5"); case GROUP_LINE_PAY: return List.of("3"); case GROUP_OFFLINE: return List.of("6"); default: return List.of(); } } /** 全部合法组代码(平台设置页与保存校验用)。 */ public static List allGroups() { return Arrays.asList(GROUP_COD, GROUP_CARD_OMG, GROUP_LINE_PAY, GROUP_OFFLINE); } /** 维度合法组集合(保存收款方选择时校验值域用)。 */ public static List groupsOf(GateScope scope) { return SCOPE_GROUPS.get(scope); } /** payType → 组代码映射查询(保存/展示场景用);未知返回 null。 */ public static String groupOfPayType(String payType) { return PAY_TYPE_TO_GROUP.get(payType); } }