| | |
| | | |
| | | @Service |
| | | public class MerchantSettlementService { |
| | | private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MerchantSettlementService.class); |
| | | private static final Map<String, String> MCC_BY_MERCHANT_TYPE = Map.ofEntries( |
| | | Map.entry("food", "5812"), Map.entry("scenic", "7991"), Map.entry("hotel", "7011"), Map.entry("apartment", "7011"), |
| | | Map.entry("cinema", "7832"), Map.entry("fuel", "5541"), Map.entry("parking", "7523"), Map.entry("carWash", "7542"), |
| | |
| | | private MerchantOnboardingProgressService onboardingProgressService; |
| | | @org.springframework.beans.factory.annotation.Autowired |
| | | private MerchantRealnameService realnameService; |
| | | @org.springframework.beans.factory.annotation.Autowired |
| | | private com.talentcard.server.business.payment.huifu.HuifuOnboardingProgressGateway onboardingQueryGateway; |
| | | |
| | | public Map<String, Object> refreshOnboarding(MerchantPrincipal principal, boolean retry) { |
| | | store(principal); |
| | | List<Map<String,Object>> records=rows("select * from merchant_onboarding_application where merchant_id=? and store_id=? and initial_bank_card_id is not null order by id desc limit 1",principal.merchantId(),principal.storeId()); |
| | | if(records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND,"Onboarding application not found"); |
| | | return onboardingProgressService.refresh(((Number)records.get(0).get("id")).longValue(),retry); |
| | | return refreshProgress(((Number)records.get(0).get("id")).longValue(),retry); |
| | | } |
| | | |
| | | /** |
| | | * 刷新分阶段进度:**先推进已满足前置条件的开通步骤,再查询回报结果**。 |
| | | * |
| | | * <p>分阶段模型下这条路径是商户与后台推进开通的唯一入口。渠道侧的微信商户配置与 |
| | | * 业务开通原本只由进件审核回调触发,而分阶段申请的回调走的是进度刷新——不在这里补上 |
| | | * 推进,新商户的微信开通就永远不会被发起。{@code continuePostOnboardingProvisioning} |
| | | * 自身按状态判断是否还需要做,重复调用无害。 |
| | | */ |
| | | public Map<String, Object> refreshProgress(Long applicationId, boolean retry) { |
| | | List<Map<String,Object>> records=rows("select * from merchant_onboarding_application where id=? and initial_bank_card_id is not null",applicationId); |
| | | if(records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND,"Onboarding application not found"); |
| | | String applicationNo=clean(records.get(0).get("applicationNo")); |
| | | if (!applicationNo.isBlank()) continuePostOnboardingProvisioning(applicationNo); |
| | | return onboardingProgressService.refresh(applicationId,retry); |
| | | } |
| | | |
| | | /** 微信实名认证:商户主动推进的下一步。未决申请不会重复提交,见 MerchantRealnameService。 */ |
| | |
| | | if (huifuId.isBlank() || "rejected".equals(clean(application.get("status")))) return; |
| | | |
| | | String wechatStatus = clean(application.get("wechatConfigStatus")); |
| | | // 本地状态可能与通道脱节(通道侧已经配好、本地还是 not_started)。微信商户配置是**添加**接口、 |
| | | // 成功后不支持修改删除(每种参数最多 5 组),配置已在通道生效时再提一次会留下重复配置。 |
| | | if (needsProvisioningStep(wechatStatus) && wechatConfigEffective(huifuId)) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_message=? where id=?", "通道已有配置,按已配置收口", application.get("id")); |
| | | wechatStatus = "approved"; |
| | | } |
| | | if (wechatStatus.isBlank() || "not_started".equals(wechatStatus) || "failed".equals(wechatStatus)) { |
| | | String requestNo = clean(application.get("wechatConfigRequestNo")); |
| | | if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "WC"); |
| | |
| | | |
| | | application = rows("select * from merchant_onboarding_application where id=? limit 1", application.get("id")).get(0); |
| | | String businessStatus = clean(application.get("businessOpenStatus")); |
| | | // 同理:通道侧已经开通微信业务时不再重新提交,否则会为同一商户开出第二份业务开通申请。 |
| | | if (needsProvisioningStep(businessStatus) && wechatBusinessEffective(huifuId)) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='approved', business_open_response_message=? where id=?", "通道已开通微信业务,按已开通收口", application.get("id")); |
| | | businessStatus = "approved"; |
| | | } |
| | | if (businessStatus.isBlank() || "not_started".equals(businessStatus) || "failed".equals(businessStatus)) { |
| | | String requestNo = clean(application.get("businessOpenRequestNo")); |
| | | if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "BO"); |
| | |
| | | * <p>这个异步通知只给申请单状态、不给授权状态,所以 {@code APPLYMENT_STATE_PASSED} 只能算 |
| | | * processing——授权是否完成要等查询到 {@code authorize_stat=1} 才能定,不能凭这一步就写 approved。 |
| | | */ |
| | | private static boolean needsProvisioningStep(String status) { |
| | | return status.isBlank() || "not_started".equals(status) || "failed".equals(status); |
| | | } |
| | | |
| | | /** |
| | | * 微信小程序支付配置是否已经在通道生效:人才端 AppID 是否绑定在 {@code fee_type=01} 的配置项下。 |
| | | * 与分阶段进度用的是同一份证据。查询失败按「未生效」处理——宁可多提一次也不要漏配。 |
| | | */ |
| | | private boolean wechatConfigEffective(String huifuId) { |
| | | try { |
| | | Map<String, Object> config = onboardingQueryGateway.queryWechatConfig(huifuId); |
| | | if (config == null || !"00000000".equals(clean(config.get("resp_code")))) return false; |
| | | String appId = huifuPaymentProperties.getMiniProgramAppId().trim(); |
| | | for (Map<String, Object> entry : providerList(config.get("atu_config_bo_list"))) { |
| | | if (!"01".equals(clean(entry.get("fee_type")))) continue; |
| | | for (Map<String, Object> item : providerList(entry.get("wx_config_list"))) { |
| | | if ("1".equals(clean(item.get("config_type"))) && appId.equals(clean(item.get("config_value")))) return true; |
| | | } |
| | | } |
| | | return false; |
| | | } catch (RuntimeException exception) { |
| | | log.warn("Huifu merchant WeChat config probe failed: huifuId={}", huifuId, exception); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 微信业务是否已经在通道开通:商户详情的 {@code qry_wx_conf_list} 里出现线上三场景之一即视为已开通。 |
| | | * 业务开通是申请型接口,重复提交会给同一商户开出第二份申请,所以提交前必须先查。 |
| | | */ |
| | | private boolean wechatBusinessEffective(String huifuId) { |
| | | try { |
| | | Map<String, Object> detail = onboardingQueryGateway.queryCapabilities(huifuId); |
| | | if (detail == null || !"00000000".equals(clean(detail.get("resp_code")))) return false; |
| | | return providerList(detail.get("qry_wx_conf_list")).stream() |
| | | .anyMatch(item -> List.of("4", "5", "12").contains(clean(item.get("pay_scene")))); |
| | | } catch (RuntimeException exception) { |
| | | log.warn("Huifu merchant business open probe failed: huifuId={}", huifuId, exception); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** 通道返回的列表字段可能是 JSON 字符串也可能是已解析的数组,两种都要能吃。 */ |
| | | @SuppressWarnings("unchecked") |
| | | private static List<Map<String, Object>> providerList(Object raw) { |
| | | if (raw == null) return List.of(); |
| | | Object parsed = raw instanceof String value ? (value.isBlank() ? null : com.alibaba.fastjson.JSON.parse(value)) : raw; |
| | | if (!(parsed instanceof List<?> list)) return List.of(); |
| | | return list.stream().filter(Map.class::isInstance).map(item -> (Map<String, Object>) item).toList(); |
| | | } |
| | | |
| | | private static String realnameStatus(String applymentStat) { |
| | | return switch (applymentStat) { |
| | | case "APPLYMENT_STATE_REJECTED" -> "failed"; |