PaymentMethodGateService.java 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. package com.ruoyi.system.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  3. import com.ruoyi.common.exception.ServiceException;
  4. import com.ruoyi.common.utils.MessageUtils;
  5. import com.ruoyi.common.utils.StringUtils;
  6. import com.ruoyi.system.domain.InfoBankCard;
  7. import com.ruoyi.system.domain.InfoUser;
  8. import com.ruoyi.system.domain.PaymentMethodConfig;
  9. import com.ruoyi.system.domain.PosStoreLinePay;
  10. import com.ruoyi.system.domain.PosStoreOmg;
  11. import com.ruoyi.system.mapper.InfoBankCardMapper;
  12. import com.ruoyi.system.mapper.InfoUserMapper;
  13. import com.ruoyi.system.mapper.PaymentMethodConfigMapper;
  14. import com.ruoyi.system.mapper.PosStoreLinePayMapper;
  15. import com.ruoyi.system.mapper.PosStoreOmgMapper;
  16. import org.springframework.stereotype.Service;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.HashMap;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.Set;
  23. /**
  24. * 支付方式闸门服务(031 可配置支付方式)——全系统唯一的支付方式可用性判定入口。
  25. *
  26. * <p>三层闸门:某支付方式可用 = 平台开关(方式×维度,缺行=开)
  27. * ∩ 收款方选择(info_user 选择列,NULL=全部)∩ 就绪度(门店凭证或启用银行卡)。
  28. * 所有下单入口(用户餐饮下单、商家建单、闪送下单)与 bankInfo 必须统一调用
  29. * {@link #assertUsable(String, GateScope, Long, Long)},禁止在各自入口重复实现判定逻辑,
  30. * 保证闸门规则变化只改本类、全部入口行为一致(spec FR-004)。</p>
  31. *
  32. * <p>刻意不加缓存:开关表最多 4×2 行的小表索引查询,直查天然满足"保存后下一次查询即生效"
  33. * (SC-001),避免缓存失效复杂度。</p>
  34. */
  35. @Service
  36. public class PaymentMethodGateService {
  37. /** 支付方式组代码:到付(payType=1)。 */
  38. public static final String GROUP_COD = "COD";
  39. /** 支付方式组代码:信用卡+Apple Pay 同组(payType=2、5,OMG 渠道同开同关)。 */
  40. public static final String GROUP_CARD_OMG = "CARD_OMG";
  41. /** 支付方式组代码:LINE Pay(payType=3)。 */
  42. public static final String GROUP_LINE_PAY = "LINE_PAY";
  43. /** 支付方式组代码:线下转账(payType=6)。 */
  44. public static final String GROUP_OFFLINE = "OFFLINE_TRANSFER";
  45. /** 现金 payType=4:仅商家建单场景,明确不纳入闸门管控,直接放行(spec FR-006)。 */
  46. public static final String PAY_TYPE_CASH = "4";
  47. /** payType 数值 → 组代码;不在表内的值视为非法。 */
  48. private static final Map<String, String> PAY_TYPE_TO_GROUP = Map.of(
  49. "1", GROUP_COD,
  50. "2", GROUP_CARD_OMG,
  51. "3", GROUP_LINE_PAY,
  52. "5", GROUP_CARD_OMG,
  53. "6", GROUP_OFFLINE);
  54. /** 各维度可管控的组集合;RIDER_FLASH 无线上卡支付(闪送资金直达骑手)。 */
  55. private static final Map<GateScope, List<String>> SCOPE_GROUPS = Map.of(
  56. GateScope.MERCHANT, List.of(GROUP_COD, GROUP_CARD_OMG, GROUP_LINE_PAY, GROUP_OFFLINE),
  57. GateScope.RIDER_FLASH, List.of(GROUP_COD, GROUP_LINE_PAY, GROUP_OFFLINE));
  58. /** 闸门维度:商家餐饮单(用户向商家付款)与骑手闪送(用户直达骑手)独立开关。 */
  59. public enum GateScope {
  60. MERCHANT, RIDER_FLASH
  61. }
  62. /**
  63. * 供结算页渲染的一项可选支付方式。
  64. * CARD_OMG 组按 payType 展开为 2(信用卡)、5(Apple Pay)两项,组内 ready 一致。
  65. */
  66. public static class PayMethodOption {
  67. /** 支付方式组代码。 */
  68. private final String methodCode;
  69. /** 对外 payType 数值("1"/"2"/"3"/"5"/"6")。 */
  70. private final String payType;
  71. /** 就绪度:false 表示当前不可实际使用(如线下转账无启用卡),前端应置灰或不渲染。 */
  72. private final boolean ready;
  73. public PayMethodOption(String methodCode, String payType, boolean ready) {
  74. this.methodCode = methodCode;
  75. this.payType = payType;
  76. this.ready = ready;
  77. }
  78. public String getMethodCode() { return methodCode; }
  79. public String getPayType() { return payType; }
  80. public boolean isReady() { return ready; }
  81. }
  82. private final PaymentMethodConfigMapper configMapper;
  83. private final InfoUserMapper userMapper;
  84. private final InfoBankCardMapper bankCardMapper;
  85. private final PosStoreOmgMapper omgMapper;
  86. private final PosStoreLinePayMapper linePayMapper;
  87. public PaymentMethodGateService(PaymentMethodConfigMapper configMapper,
  88. InfoUserMapper userMapper,
  89. InfoBankCardMapper bankCardMapper,
  90. PosStoreOmgMapper omgMapper,
  91. PosStoreLinePayMapper linePayMapper) {
  92. this.configMapper = configMapper;
  93. this.userMapper = userMapper;
  94. this.bankCardMapper = bankCardMapper;
  95. this.omgMapper = omgMapper;
  96. this.linePayMapper = linePayMapper;
  97. }
  98. /**
  99. * 校验支付方式可用,不可用抛出国际化异常;三下单入口与 bankInfo 统一调用(唯一入口)。
  100. *
  101. * @param payType 支付方式数值(与订单 payType 同域)
  102. * @param scope 闸门维度
  103. * @param storeId 门店 ID(MERCHANT 维度线上渠道就绪度用:pos_store_omg/pos_store_line_pay 为门店级凭证)
  104. * @param payeeUserId 收款方用户 ID(商家主账号;闪送下单时无收款方传 null,跳过选择与就绪度层)
  105. */
  106. public void assertUsable(String payType, GateScope scope, Long storeId, Long payeeUserId) {
  107. if (!isUsable(payType, scope, storeId, payeeUserId)) {
  108. throw new ServiceException(MessageUtils.message("pay.method.not.available"));
  109. }
  110. }
  111. /**
  112. * 列出某维度对收款方当前可用的支付方式(结算页可选项、骑手端可选集共用)。
  113. * 返回平台开关 ∩ 收款方选择的全部项并附就绪度;RIDER_FLASH 维度就绪度恒 true
  114. * (收款方未定或即骑手本人,线下卡是否就绪由骑手自选方式时自行把握,抢单过滤兜底)。
  115. */
  116. public List<PayMethodOption> listAvailable(GateScope scope, Long storeId, Long payeeUserId) {
  117. Map<String, Boolean> disabled = platformDisabledMap(scope);
  118. Set<String> selected = selectedGroups(scope, payeeUserId);
  119. List<PayMethodOption> result = new ArrayList<>();
  120. for (String group : SCOPE_GROUPS.get(scope)) {
  121. if (disabled.containsKey(group)) continue;
  122. if (selected != null && !selected.contains(group)) continue;
  123. boolean ready = isReady(group, scope, storeId, payeeUserId);
  124. for (String payType : payTypesOf(group)) {
  125. result.add(new PayMethodOption(group, payType, ready));
  126. }
  127. }
  128. return result;
  129. }
  130. /** 单一判定实现:assertUsable 与调用方需要的布尔语义共用,保证两口径永不分裂。 */
  131. private boolean isUsable(String payType, GateScope scope, Long storeId, Long payeeUserId) {
  132. // 现金仅商家建单场景使用,明确不纳入管控(spec FR-006)。
  133. if (PAY_TYPE_CASH.equals(payType)) return true;
  134. String group = PAY_TYPE_TO_GROUP.get(payType);
  135. if (group == null || !SCOPE_GROUPS.get(scope).contains(group)) return false;
  136. if (platformDisabledMap(scope).containsKey(group)) return false;
  137. Set<String> selected = selectedGroups(scope, payeeUserId);
  138. if (selected != null && !selected.contains(group)) return false;
  139. return isReady(group, scope, storeId, payeeUserId);
  140. }
  141. /** 平台已关闭的组集合(该维度);缺行=开,仅 enabled=0 的行进集合。 */
  142. private Map<String, Boolean> platformDisabledMap(GateScope scope) {
  143. List<PaymentMethodConfig> rows = configMapper.selectList(
  144. new QueryWrapper<PaymentMethodConfig>().eq("scope", scope.name()));
  145. Map<String, Boolean> disabled = new HashMap<>();
  146. for (PaymentMethodConfig row : rows) {
  147. if (Integer.valueOf(0).equals(row.getEnabled())) {
  148. disabled.put(row.getMethodCode(), Boolean.TRUE);
  149. }
  150. }
  151. return disabled;
  152. }
  153. /** 收款方接受的组集合;null/空选择返回 null 表示"全部接受"(存量零行为变化)。 */
  154. private Set<String> selectedGroups(GateScope scope, Long payeeUserId) {
  155. if (payeeUserId == null) return null;
  156. InfoUser user = userMapper.selectById(payeeUserId);
  157. if (user == null) return null;
  158. String csv = scope == GateScope.MERCHANT ? user.getPayMethods() : user.getFlashPayMethods();
  159. if (StringUtils.isEmpty(csv) || csv.trim().isEmpty()) return null;
  160. Set<String> groups = new java.util.HashSet<>();
  161. for (String part : csv.split(",")) {
  162. String value = part.trim();
  163. if (!value.isEmpty()) groups.add(value);
  164. }
  165. return groups.isEmpty() ? null : groups;
  166. }
  167. /**
  168. * 就绪度判定:MERCHANT 维度线上渠道看门店级凭证、线下转账看商家启用卡;
  169. * RIDER_FLASH 维度恒满足(收款方未定,由骑手选择层与抢单过滤兜底)。
  170. */
  171. private boolean isReady(String group, GateScope scope, Long storeId, Long payeeUserId) {
  172. if (group.equals(GROUP_COD)) return true;
  173. if (scope == GateScope.RIDER_FLASH) return true;
  174. switch (group) {
  175. case GROUP_CARD_OMG:
  176. return storeId != null && omgMapper.selectCount(
  177. new QueryWrapper<PosStoreOmg>().eq("store_id", storeId).eq("is_enabled", 1)) > 0;
  178. case GROUP_LINE_PAY:
  179. return storeId != null && linePayMapper.selectCount(
  180. new QueryWrapper<PosStoreLinePay>().eq("store_id", storeId).eq("is_enabled", 1)) > 0;
  181. case GROUP_OFFLINE:
  182. return payeeUserId != null && bankCardMapper.selectCount(
  183. new QueryWrapper<InfoBankCard>().eq("user_id", payeeUserId).eq("is_active", 1)) > 0;
  184. default:
  185. return false;
  186. }
  187. }
  188. /** 组代码 → 对外 payType 展开顺序(保持稳定展示顺序)。 */
  189. private List<String> payTypesOf(String group) {
  190. switch (group) {
  191. case GROUP_COD: return List.of("1");
  192. case GROUP_CARD_OMG: return List.of("2", "5");
  193. case GROUP_LINE_PAY: return List.of("3");
  194. case GROUP_OFFLINE: return List.of("6");
  195. default: return List.of();
  196. }
  197. }
  198. /** 全部合法组代码(平台设置页与保存校验用)。 */
  199. public static List<String> allGroups() {
  200. return Arrays.asList(GROUP_COD, GROUP_CARD_OMG, GROUP_LINE_PAY, GROUP_OFFLINE);
  201. }
  202. /** 维度合法组集合(保存收款方选择时校验值域用)。 */
  203. public static List<String> groupsOf(GateScope scope) {
  204. return SCOPE_GROUPS.get(scope);
  205. }
  206. /** payType → 组代码映射查询(保存/展示场景用);未知返回 null。 */
  207. public static String groupOfPayType(String payType) {
  208. return PAY_TYPE_TO_GROUP.get(payType);
  209. }
  210. }