PaymentMethodGateService.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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.domain.PosStore;
  12. import com.ruoyi.system.mapper.InfoBankCardMapper;
  13. import com.ruoyi.system.mapper.InfoUserMapper;
  14. import com.ruoyi.system.mapper.PaymentMethodConfigMapper;
  15. import com.ruoyi.system.mapper.PosStoreLinePayMapper;
  16. import com.ruoyi.system.mapper.PosStoreOmgMapper;
  17. import com.ruoyi.system.mapper.PosStoreMapper;
  18. import org.springframework.stereotype.Service;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.HashMap;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Set;
  25. /**
  26. * 支付方式闸门服务(031 可配置支付方式)——全系统唯一的支付方式可用性判定入口。
  27. *
  28. * <p>三层闸门:某支付方式可用 = 平台开关(方式×维度,缺行=开)
  29. * ∩ 收款方选择(普通商家/骑手在 info_user,摊位在 pos_store;NULL=全部)
  30. * ∩ 就绪度(门店凭证或对应归属的启用银行卡)。
  31. * 所有下单入口(用户餐饮下单、商家建单、闪送下单)与 bankInfo 必须统一调用
  32. * {@link #assertUsable(String, GateScope, Long, Long)},禁止在各自入口重复实现判定逻辑,
  33. * 保证闸门规则变化只改本类、全部入口行为一致(spec FR-004)。</p>
  34. *
  35. * <p>刻意不加缓存:开关表最多 4×2 行的小表索引查询,直查天然满足"保存后下一次查询即生效"
  36. * (SC-001),避免缓存失效复杂度。</p>
  37. */
  38. @Service
  39. public class PaymentMethodGateService {
  40. /** 支付方式组代码:到付(payType=1)。 */
  41. public static final String GROUP_COD = "COD";
  42. /** 支付方式组代码:信用卡+Apple Pay 同组(payType=2、5,OMG 渠道同开同关)。 */
  43. public static final String GROUP_CARD_OMG = "CARD_OMG";
  44. /** 支付方式组代码:LINE Pay(payType=3)。 */
  45. public static final String GROUP_LINE_PAY = "LINE_PAY";
  46. /** 支付方式组代码:线下转账(payType=6)。 */
  47. public static final String GROUP_OFFLINE = "OFFLINE_TRANSFER";
  48. /** 支付方式组代码:现金(payType=4);平台开关管控用户自取/堂食口径,叠加商家勾选;无就绪度要求(031 US7/FR-006)。 */
  49. public static final String GROUP_CASH = "CASH";
  50. /** 现金 payType=4:原 assertUsable 路径(商家建单/闪送缺省现金)直接放行;用户单经 assertUsableForOrder 按订单类型管控。 */
  51. public static final String PAY_TYPE_CASH = "4";
  52. /** payType 数值 → 组代码;不在表内的值视为非法(现金"4"仅走专门分支,不进映射)。 */
  53. private static final Map<String, String> PAY_TYPE_TO_GROUP = Map.of(
  54. "1", GROUP_COD,
  55. "2", GROUP_CARD_OMG,
  56. "3", GROUP_LINE_PAY,
  57. "5", GROUP_CARD_OMG,
  58. "6", GROUP_OFFLINE);
  59. /** 各维度可管控的组集合;RIDER_FLASH 无线上卡支付(闪送资金直达骑手)且不含现金(缺省支付不经勾选)。 */
  60. private static final Map<GateScope, List<String>> SCOPE_GROUPS = Map.of(
  61. GateScope.MERCHANT, List.of(GROUP_COD, GROUP_CARD_OMG, GROUP_LINE_PAY, GROUP_OFFLINE, GROUP_CASH),
  62. GateScope.RIDER_FLASH, List.of(GROUP_COD, GROUP_LINE_PAY, GROUP_OFFLINE));
  63. /** 闸门维度:商家餐饮单(用户向商家付款)与骑手闪送(用户直达骑手)独立开关。 */
  64. public enum GateScope {
  65. MERCHANT, RIDER_FLASH
  66. }
  67. /**
  68. * 供结算页渲染的一项可选支付方式。
  69. * CARD_OMG 组按 payType 展开为 2(信用卡)、5(Apple Pay)两项,组内 ready 一致。
  70. */
  71. public static class PayMethodOption {
  72. /** 支付方式组代码。 */
  73. private final String methodCode;
  74. /** 对外 payType 数值("1"/"2"/"3"/"5"/"6")。 */
  75. private final String payType;
  76. /** 就绪度:false 表示当前不可实际使用(如线下转账无启用卡),前端应置灰或不渲染。 */
  77. private final boolean ready;
  78. public PayMethodOption(String methodCode, String payType, boolean ready) {
  79. this.methodCode = methodCode;
  80. this.payType = payType;
  81. this.ready = ready;
  82. }
  83. public String getMethodCode() { return methodCode; }
  84. public String getPayType() { return payType; }
  85. public boolean isReady() { return ready; }
  86. }
  87. private final PaymentMethodConfigMapper configMapper;
  88. private final InfoUserMapper userMapper;
  89. private final InfoBankCardMapper bankCardMapper;
  90. private final PosStoreOmgMapper omgMapper;
  91. private final PosStoreLinePayMapper linePayMapper;
  92. private final PosStoreMapper storeMapper;
  93. @org.springframework.beans.factory.annotation.Autowired
  94. private IInfoRiderLinePayService riderLinePayService;
  95. public PaymentMethodGateService(PaymentMethodConfigMapper configMapper,
  96. InfoUserMapper userMapper,
  97. InfoBankCardMapper bankCardMapper,
  98. PosStoreOmgMapper omgMapper,
  99. PosStoreLinePayMapper linePayMapper,
  100. PosStoreMapper storeMapper) {
  101. this.configMapper = configMapper;
  102. this.userMapper = userMapper;
  103. this.bankCardMapper = bankCardMapper;
  104. this.omgMapper = omgMapper;
  105. this.linePayMapper = linePayMapper;
  106. this.storeMapper = storeMapper;
  107. }
  108. /**
  109. * 校验支付方式可用,不可用抛出国际化异常;三下单入口与 bankInfo 统一调用(唯一入口)。
  110. *
  111. * @param payType 支付方式数值(与订单 payType 同域)
  112. * @param scope 闸门维度
  113. * @param storeId 门店 ID(MERCHANT 维度线上渠道就绪度用:pos_store_omg/pos_store_line_pay 为门店级凭证)
  114. * @param payeeUserId 收款方用户 ID(普通商家主账号;摊位传夜市主账号也不会继承其支付设置;闪送下单时无收款方传 null)
  115. */
  116. public void assertUsable(String payType, GateScope scope, Long storeId, Long payeeUserId) {
  117. if (!isUsable(payType, scope, storeId, payeeUserId)) {
  118. throw new ServiceException(MessageUtils.message("pay.method.not.available"));
  119. }
  120. }
  121. /**
  122. * 带订单类型的校验重载(031 US7):用户餐饮下单统一调用。
  123. * 现金在此口径下按订单类型与商家勾选管控:自取/堂食查商家选择,外送拒绝,
  124. * orderType 缺失(商家建单/闪送缺省现金)与原 {@link #assertUsable} 口径一致放行。
  125. *
  126. * @param orderType 订单类型(PosOrder.type:0外送 1自取 2堂食);无类型上下文传 null
  127. */
  128. public void assertUsableForOrder(String payType, GateScope scope, Long storeId, Long payeeUserId, Long orderType) {
  129. if (!isUsableForOrder(payType, scope, storeId, payeeUserId, orderType)) {
  130. throw new ServiceException(MessageUtils.message("pay.method.not.available"));
  131. }
  132. }
  133. /** 现金订单类型口径的单一判定;非现金支付方式与原 isUsable 完全同路径。 */
  134. private boolean isUsableForOrder(String payType, GateScope scope, Long storeId, Long payeeUserId, Long orderType) {
  135. if (PAY_TYPE_CASH.equals(payType)) {
  136. if (orderType == null || scope == GateScope.RIDER_FLASH) return true;
  137. if (orderType != 1L && orderType != 2L) return false;
  138. if (platformDisabledMap(scope).containsKey(GROUP_CASH)) return false;
  139. Set<String> selected = selectedGroups(scope, storeId, payeeUserId);
  140. return selected == null || selected.contains(GROUP_CASH);
  141. }
  142. return isUsable(payType, scope, storeId, payeeUserId);
  143. }
  144. /**
  145. * 列出某维度对收款方当前可用的支付方式(结算页可选项、骑手端可选集共用)。
  146. * 返回平台开关 ∩ 收款方选择并附就绪度;已知骑手还须具备对应收款配置。
  147. * 骑手未定(用户下单)仅服从平台开关,接单接口再校验具体收款人。
  148. */
  149. public List<PayMethodOption> listAvailable(GateScope scope, Long storeId, Long payeeUserId) {
  150. return listAvailable(scope, storeId, payeeUserId, null);
  151. }
  152. /**
  153. * orderType 感知重载(031 US7):MERCHANT 维度且 orderType∈{1自取,2堂食} 时结果含现金项
  154. * (仍受商家勾选约束);其余 orderType 或原签名口径不含现金,老客户端零变化。
  155. */
  156. public List<PayMethodOption> listAvailable(GateScope scope, Long storeId, Long payeeUserId, Long orderType) {
  157. Map<String, Boolean> disabled = platformDisabledMap(scope);
  158. Set<String> selected = selectedGroups(scope, storeId, payeeUserId);
  159. boolean cashVisible = scope == GateScope.MERCHANT && orderType != null
  160. && (orderType == 1L || orderType == 2L);
  161. List<PayMethodOption> result = new ArrayList<>();
  162. for (String group : SCOPE_GROUPS.get(scope)) {
  163. if (GROUP_CASH.equals(group) && !cashVisible) continue;
  164. if (disabled.containsKey(group)) continue;
  165. if (selected != null && !selected.contains(group)) continue;
  166. boolean ready = isReady(group, scope, storeId, payeeUserId);
  167. for (String payType : payTypesOf(group)) {
  168. result.add(new PayMethodOption(group, payType, ready));
  169. }
  170. }
  171. return result;
  172. }
  173. /** 单一判定实现:assertUsable 与调用方需要的布尔语义共用,保证两口径永不分裂。 */
  174. private boolean isUsable(String payType, GateScope scope, Long storeId, Long payeeUserId) {
  175. // 现金仅商家建单场景使用,明确不纳入管控(spec FR-006)。
  176. if (PAY_TYPE_CASH.equals(payType)) return true;
  177. String group = PAY_TYPE_TO_GROUP.get(payType);
  178. if (group == null || !SCOPE_GROUPS.get(scope).contains(group)) return false;
  179. if (platformDisabledMap(scope).containsKey(group)) return false;
  180. Set<String> selected = selectedGroups(scope, storeId, payeeUserId);
  181. if (selected != null && !selected.contains(group)) return false;
  182. return isReady(group, scope, storeId, payeeUserId);
  183. }
  184. /** 平台已关闭的组集合(该维度);缺行=开,仅 enabled=0 的行进集合。 */
  185. private Map<String, Boolean> platformDisabledMap(GateScope scope) {
  186. List<PaymentMethodConfig> rows = configMapper.selectList(
  187. new QueryWrapper<PaymentMethodConfig>().eq("scope", scope.name()));
  188. Map<String, Boolean> disabled = new HashMap<>();
  189. for (PaymentMethodConfig row : rows) {
  190. if (Integer.valueOf(0).equals(row.getEnabled())) {
  191. disabled.put(row.getMethodCode(), Boolean.TRUE);
  192. }
  193. }
  194. return disabled;
  195. }
  196. /** 收款方接受的组集合;摊位按 storeId 读,普通商家/骑手按用户读;NULL/空=全部接受。 */
  197. private Set<String> selectedGroups(GateScope scope, Long storeId, Long payeeUserId) {
  198. String csv;
  199. PosStore store = scope == GateScope.MERCHANT && storeId != null ? storeMapper.selectById(storeId) : null;
  200. if (store != null && Integer.valueOf(1).equals(store.getIsStall())) {
  201. // 摊位的 pos_store.user_id 是夜市主,不能读取其个人支付选择。
  202. csv = store.getPayMethods();
  203. } else {
  204. if (payeeUserId == null) return null;
  205. InfoUser user = userMapper.selectById(payeeUserId);
  206. if (user == null) return null;
  207. csv = scope == GateScope.MERCHANT ? user.getPayMethods() : user.getFlashPayMethods();
  208. }
  209. if (StringUtils.isEmpty(csv) || csv.trim().isEmpty()) return null;
  210. Set<String> groups = new java.util.HashSet<>();
  211. for (String part : csv.split(",")) {
  212. String value = part.trim();
  213. if (!value.isEmpty()) groups.add(value);
  214. }
  215. return groups.isEmpty() ? null : groups;
  216. }
  217. /**
  218. * 商家支付设置页可选集:平台开放全集(不按商家已有选择过滤——页面本身用于修改选择),
  219. * 就绪度按名下门店聚合——线上渠道任一门店有启用凭证即算就绪,线下转账看主账号启用卡。
  220. * 摊位设置页传 ownerUserId=null、唯一 storeId,只读取该摊位的银行卡;普通商家保持主账号归属。
  221. * 供 GET /system/merchantPayMethods 使用;结算页口径见 {@link #listAvailable}。
  222. */
  223. public List<PayMethodOption> listAvailableForMerchantSettings(Long ownerUserId, List<Long> storeIds) {
  224. Map<String, Boolean> disabled = platformDisabledMap(GateScope.MERCHANT);
  225. List<PayMethodOption> result = new ArrayList<>();
  226. for (String group : SCOPE_GROUPS.get(GateScope.MERCHANT)) {
  227. if (disabled.containsKey(group)) continue;
  228. boolean ready;
  229. if (GROUP_CARD_OMG.equals(group) || GROUP_LINE_PAY.equals(group)) {
  230. ready = false;
  231. if (storeIds != null) {
  232. for (Long storeId : storeIds) {
  233. if (storeCredentialReady(group, storeId)) { ready = true; break; }
  234. }
  235. }
  236. } else if (GROUP_OFFLINE.equals(group)) {
  237. if (ownerUserId == null && storeIds != null && storeIds.size() == 1) {
  238. // 摊位设置页传唯一 storeId;卡归属摊位而非夜市主账号。
  239. ready = bankCardMapper.selectCount(new QueryWrapper<InfoBankCard>()
  240. .eq("store_id", storeIds.get(0)).eq("is_active", 1)) > 0;
  241. } else {
  242. ready = ownerUserId != null && bankCardMapper.selectCount(
  243. new QueryWrapper<InfoBankCard>().eq("user_id", ownerUserId).eq("is_active", 1)) > 0;
  244. }
  245. } else {
  246. ready = true;
  247. }
  248. for (String payType : payTypesOf(group)) {
  249. result.add(new PayMethodOption(group, payType, ready));
  250. }
  251. }
  252. return result;
  253. }
  254. /**
  255. * 商家设置页独立的只读说明;不扩展共用 PayMethodOption,以保持用户结算和骑手接口原样。
  256. * 普通商家和摊位读取同一份平台说明,商家保存支付选择时不能改写此配置。
  257. */
  258. public Map<String, String> listMerchantRemarks() {
  259. Map<String, String> remarks = new HashMap<>();
  260. for (PaymentMethodConfig row : configMapper.selectList(new QueryWrapper<PaymentMethodConfig>()
  261. .eq("scope", GateScope.MERCHANT.name()))) {
  262. if (GateScope.MERCHANT.name().equals(row.getScope())
  263. && SCOPE_GROUPS.get(GateScope.MERCHANT).contains(row.getMethodCode())
  264. && StringUtils.isNotBlank(row.getRemark())) {
  265. remarks.put(row.getMethodCode(), row.getRemark());
  266. }
  267. }
  268. return remarks;
  269. }
  270. /**
  271. * 就绪度判定:MERCHANT 维度线上渠道看门店级凭证、线下转账按普通商家或摊位归属看启用卡;
  272. * RIDER_FLASH 维度线上收款要求启用的骑手凭证,线下转账要求启用卡。
  273. */
  274. private boolean isReady(String group, GateScope scope, Long storeId, Long payeeUserId) {
  275. if (group.equals(GROUP_COD) || group.equals(GROUP_CASH)) return true;
  276. if (scope == GateScope.RIDER_FLASH) {
  277. // 待抢列表和接单使用同一闸门,不让无 LINE Pay 收款资格的骑手抢线上单。
  278. if (GROUP_LINE_PAY.equals(group) && payeeUserId != null) {
  279. return riderLinePayService.getEnabledCurrent(payeeUserId) != null;
  280. }
  281. // 线下转账:骑手身份已知(抢单池/骑手端可选集)要求启用卡;用户下单骑手未定(null)放行
  282. return !GROUP_OFFLINE.equals(group) || payeeUserId == null || bankCardMapper.selectCount(
  283. new QueryWrapper<InfoBankCard>().eq("user_id", payeeUserId).eq("is_active", 1)) > 0;
  284. }
  285. switch (group) {
  286. case GROUP_CARD_OMG:
  287. case GROUP_LINE_PAY:
  288. return storeId != null && storeCredentialReady(group, storeId);
  289. case GROUP_OFFLINE:
  290. PosStore store = storeId == null ? null : storeMapper.selectById(storeId);
  291. if (store != null && Integer.valueOf(1).equals(store.getIsStall())) {
  292. return bankCardMapper.selectCount(new QueryWrapper<InfoBankCard>()
  293. .eq("store_id", storeId).eq("is_active", 1)) > 0;
  294. }
  295. return payeeUserId != null && bankCardMapper.selectCount(
  296. new QueryWrapper<InfoBankCard>().eq("user_id", payeeUserId).eq("is_active", 1)) > 0;
  297. default:
  298. return false;
  299. }
  300. }
  301. /** 门店级线上渠道凭证就绪:存在 is_enabled=1 的 OMG/LINE Pay 配置行。 */
  302. private boolean storeCredentialReady(String group, Long storeId) {
  303. if (GROUP_CARD_OMG.equals(group)) {
  304. return omgMapper.selectCount(
  305. new QueryWrapper<PosStoreOmg>().eq("store_id", storeId).eq("is_enabled", 1)) > 0;
  306. }
  307. return linePayMapper.selectCount(
  308. new QueryWrapper<PosStoreLinePay>().eq("store_id", storeId).eq("is_enabled", 1)) > 0;
  309. }
  310. /** 组代码 → 对外 payType 展开顺序(保持稳定展示顺序);供抢单过滤等调用方展开。 */
  311. public static List<String> payTypesOf(String group) {
  312. switch (group) {
  313. case GROUP_COD: return List.of("1");
  314. case GROUP_CARD_OMG: return List.of("2", "5");
  315. case GROUP_LINE_PAY: return List.of("3");
  316. case GROUP_OFFLINE: return List.of("6");
  317. case GROUP_CASH: return List.of(PAY_TYPE_CASH);
  318. default: return List.of();
  319. }
  320. }
  321. /** 全部合法组代码(平台设置页与保存校验用)。 */
  322. public static List<String> allGroups() {
  323. return Arrays.asList(GROUP_COD, GROUP_CARD_OMG, GROUP_LINE_PAY, GROUP_OFFLINE);
  324. }
  325. /** 维度合法组集合(保存收款方选择时校验值域用)。 */
  326. public static List<String> groupsOf(GateScope scope) {
  327. return SCOPE_GROUPS.get(scope);
  328. }
  329. /** 平台开关矩阵的组集合:与维度组一致(现金进商家维度矩阵,标注仅自取/堂食;骑手维度本就无现金)。 */
  330. public static List<String> platformGroupsOf(GateScope scope) {
  331. return groupsOf(scope);
  332. }
  333. /** payType → 组代码映射查询(保存/展示场景用);未知返回 null。 */
  334. public static String groupOfPayType(String payType) {
  335. return PAY_TYPE_TO_GROUP.get(payType);
  336. }
  337. }