| rencaika_merchant/docs/项目接口对接说明.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rencaika_merchant/pages/payment-capability/payment-capability.uvue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| rencaika_merchant/services/settlement.uts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
rencaika_merchant/docs/项目接口对接说明.md
@@ -113,7 +113,7 @@ | 结算明细导出 | `/api/merchant/settlement-statistics/export` | GET | 状态、日期范围 | Excel 文件 | 已接入 | | 商户进件状态 | `/api/merchant/onboarding/status` | GET | 当前门店会话 | `unsubmitted`、`pending`、`approved`、`rejected`、驳回原因、六项阶段、两项完成条件核实 | 已接入 | | 提交商户进件 | `/api/merchant/onboarding` | POST | 主体类型、企业/个人资料、联系人、幂等键 | 进件单 ID、审核状态、提交时间 | 已接入 | | 收款能力自查 | `/api/merchant/payment-readiness` | GET | 当前门店会话 | `ready`(能否收款)、`reason`(与下单被拒同一句文案)、`verifiedAt`(最近核实时间)、`nextVerifyAt`、`verifyAvailable` | 已接入 | | 收款能力自查 | `/api/merchant/payment-readiness` | GET | 当前门店会话 | `ready`(能否收款)、`reason`(与下单被拒同一句文案)、`verifiedAt`(最近核实时间)、`nextVerifyAt`、`verifyAvailable`、`retryAfterSeconds`(冷却剩余秒数,客户端据此做倒计时) | 已接入 | | 手动核实收款能力 | `/api/merchant/payment-readiness/verify` | POST | 当前门店会话 | 同上,另加 `refreshed`(本次是否真的查了通道);服务端有冷却,冷却期内直接返回当前快照 | 已接入 | | 提现概览 | `/api/merchant/withdrawals/summary` | GET | 当前门店会话 | 可提现金额、冻结金额,单位分 | 已接入 | | 提现记录 | `/api/merchant/withdrawals` | GET | 当前门店会话、分页 | 提现记录(状态只展示处理中/已到账/提现失败)、分页信息 | 已接入 | rencaika_merchant/pages/payment-capability/payment-capability.uvue
@@ -6,7 +6,7 @@ </view> <scroll-view scroll-y="true" class="result-content"> <page-feedback v-if="loadState != 'success'" :state="loadState" loading-text="收款能力加载中" error-text="收款能力加载失败,点击重试" @retry="loadReadiness"></page-feedback> <page-feedback v-if="loadState != 'success'" :state="loadState" loading-text="收款能力加载中" error-text="收款能力加载失败,点击重试" @retry="retryLoad"></page-feedback> <view v-else> <view class="status-hero"> <view class="status-icon-wrap"><image class="status-icon" :src="statusIcon" mode="aspectFit"></image></view> @@ -40,9 +40,12 @@ import { hasActiveSession } from '../../services/session.uts' import { loadPaymentReadiness, verifyPaymentReadiness, MerchantPaymentReadiness } from '../../services/settlement.uts' const readiness = ref<MerchantPaymentReadiness>({ ready: false, reason: '', verifiedAt: '', nextVerifyAt: '', verifyAvailable: false, refreshed: false }) const readiness = ref<MerchantPaymentReadiness>({ ready: false, reason: '', verifiedAt: '', nextVerifyAt: '', verifyAvailable: false, retryAfterSeconds: 0, refreshed: false }) const loadState = ref('loading') const syncing = ref(false) // 冷却剩余秒数由服务端给,客户端只做倒计时——不自己解析数据库时间戳字符串。 const cooldownLeft = ref(0) let cooldownTimer : number | null = null /** 通道回的是数据库时间戳(形如 2026-09-17 12:34:56.0),去掉毫秒再展示。 */ const displayTime = (value: string): string => { @@ -54,45 +57,84 @@ const verifiedText = computed((): string => readiness.value.verifiedAt.length > 0 ? displayTime(readiness.value.verifiedAt) : '尚未核实') // 冷却中才显示「下次可核实」:能点的时候显示这行只会让人以为还有别的限制。 const nextVerifyText = computed((): string => readiness.value.verifyAvailable ? '' : displayTime(readiness.value.nextVerifyAt)) const countdownText = computed((): string => { const total = cooldownLeft.value const minutes = Math.floor(total / 60) const seconds = total % 60 const padded = seconds < 10 ? '0' + seconds : '' + seconds return minutes > 0 ? '' + minutes + ':' + padded : padded + ' 秒' }) const verifyDisabled = computed((): boolean => syncing.value || !readiness.value.verifyAvailable) const verifyButtonLabel = computed((): string => { if (syncing.value) return '核实中...' return readiness.value.verifyAvailable ? '立即核实' : '稍后可再次核实' if (readiness.value.verifyAvailable) return '立即核实' // 两种「不能点」要分开说:冷却中会自己恢复,进件没通过则不是等一会儿就有用。 return cooldownLeft.value > 0 ? countdownText.value + '后可再次核实' : '暂不可核实' }) // 还没通过进件、或通道商户状态异常时用「等待核实」的图标,而不是打一个红叉—— // 这两种情况都不是商户做错了什么。 const statusIcon = computed((): string => readiness.value.ready ? '/static/icons/lucide/badge-check-orange.svg' : '/static/icons/lucide/clock-3.svg') const statusTitle = computed((): string => readiness.value.ready ? '现在可以收款' : '暂时无法收款') const stopCooldownTimer = () => { if (cooldownTimer != null) { clearInterval(cooldownTimer) cooldownTimer = null } } // 冷却结束就地恢复按钮,不必让商户退出页面再进来;到点顺手重新拉一次,拿到的就是最新判定。 const startCooldownTimer = () => { stopCooldownTimer() if (cooldownLeft.value <= 0) return cooldownTimer = setInterval(() => { if (cooldownLeft.value <= 1) { cooldownLeft.value = 0 stopCooldownTimer() loadReadiness(false) return } cooldownLeft.value = cooldownLeft.value - 1 }, 1000) } const applyReadiness = (item: MerchantPaymentReadiness) => { readiness.value = item cooldownLeft.value = item.retryAfterSeconds startCooldownTimer() loadState.value = 'success' } const loadReadiness = () => { // 冷却到点的自动刷新走 showLoading=false:内容已经是对的,切一次 loading 只会让整屏闪一下。 const loadReadiness = (showLoading: boolean) => { if (!hasActiveSession()) { loadState.value = 'error' return } loadState.value = 'loading' if (showLoading) loadState.value = 'loading' loadPaymentReadiness((item: MerchantPaymentReadiness) => { applyReadiness(item) }, (error: any) => { if (!showLoading) return loadState.value = 'error' uni.showToast({ title: error.message != null ? error.message : '收款能力加载失败', icon: 'none' }) }) } const retryLoad = () => loadReadiness(true) const verifyNow = () => { if (verifyDisabled.value) { if (!readiness.value.verifyAvailable) uni.showToast({ title: '刚核实过,请稍后再试', icon: 'none' }) if (syncing.value) return if (!readiness.value.verifyAvailable) { // 冷却中说「刚核实过」;没有冷却却仍不可核实,那原因只能来自判定本身(例如进件未通过)。 const title = cooldownLeft.value > 0 ? '刚核实过,请稍后再试' : readiness.value.reason.length > 0 ? readiness.value.reason : '当前无法核实收款能力' uni.showToast({ title: title, icon: 'none' }) return } syncing.value = true verifyPaymentReadiness((item: MerchantPaymentReadiness) => { syncing.value = false applyReadiness(item) uni.showToast({ title: item.refreshed ? '已重新核实通道状态' : '刚核实过,这是最近一次的结果', icon: 'none' }) uni.showToast({ title: item.refreshed ? '已重新核实通道状态' : '未重新查询通道,显示最近一次结果', icon: 'none' }) }, (error: any) => { syncing.value = false uni.showToast({ title: error.message != null ? error.message : '核实失败,请稍后重试', icon: 'none' }) @@ -104,7 +146,15 @@ } onShow(() => { loadReadiness() loadReadiness(true) }) onHide(() => { stopCooldownTimer() }) onUnload(() => { stopCooldownTimer() }) </script> rencaika_merchant/services/settlement.uts
@@ -8,13 +8,13 @@ export type SettlementBankCard = { id: string, bankName: string, branchName: string, cardMasked: string, status: string, reason: string, createdAt: string } export type SettlementPage<T> = { records: Array<T>, page: number, size: number, total: number } /** 收款能力自查:判定与下单闸门同源同文案,只返回派生结果。 */ export type MerchantPaymentReadiness = { ready: boolean, reason: string, verifiedAt: string, nextVerifyAt: string, verifyAvailable: boolean, refreshed: boolean } export type MerchantPaymentReadiness = { ready: boolean, reason: string, verifiedAt: string, nextVerifyAt: string, verifyAvailable: boolean, retryAfterSeconds: number, refreshed: boolean } const stringValue = (value: any): string => value == null ? '' : '' + value const numberValue = (value: any): number => value == null ? 0 : Number(value) /** retryable:通道侧配置已明确失败,商户可以重新提交;缺省按 false,宁可保守。 */ const onboarding = (value: any): SettlementOnboarding => ({ id: stringValue(value != null ? value.id : ''), applicationNo: stringValue(value != null ? value.applicationNo : ''), status: stringValue(value != null ? value.status : 'unsubmitted'), reason: stringValue(value != null ? value.reason : ''), retryable: value != null && value.retryable == true, submittedAt: stringValue(value != null ? value.submittedAt : ''), refreshable: value != null && value.refreshable == true, lastSyncedAt: stringValue(value != null ? value.lastSyncedAt : ''), syncError: stringValue(value != null ? value.syncError : ''), stages: (value != null && value.stages instanceof Array ? value.stages : []).map((stage: any): OnboardingStage => ({ key: stringValue(stage.key), label: stringValue(stage.label), status: stringValue(stage.status), reason: stringValue(stage.reason), qrCode: stringValue(stage.qrCode) })), completionChecks: (value != null && value.completionChecks instanceof Array ? value.completionChecks : []).map((check: any): OnboardingCheck => ({ key: stringValue(check.key), label: stringValue(check.label), status: stringValue(check.status), reason: stringValue(check.reason) })) }) const paymentReadiness = (value: any): MerchantPaymentReadiness => ({ ready: value != null && value.ready == true, reason: stringValue(value != null ? value.reason : ''), verifiedAt: stringValue(value != null ? value.verifiedAt : ''), nextVerifyAt: stringValue(value != null ? value.nextVerifyAt : ''), verifyAvailable: value != null && value.verifyAvailable == true, refreshed: value != null && value.refreshed == true }) const paymentReadiness = (value: any): MerchantPaymentReadiness => ({ ready: value != null && value.ready == true, reason: stringValue(value != null ? value.reason : ''), verifiedAt: stringValue(value != null ? value.verifiedAt : ''), nextVerifyAt: stringValue(value != null ? value.nextVerifyAt : ''), verifyAvailable: value != null && value.verifyAvailable == true, retryAfterSeconds: value != null ? numberValue(value.retryAfterSeconds) : 0, refreshed: value != null && value.refreshed == true }) export const refreshOnboardingProgress = (retry: boolean, success: (value: SettlementOnboarding) => void, error: (value: HttpError) => void): (() => void) => request<any>({ path: '/api/merchant/onboarding/refresh?retry=' + (retry ? 'true' : 'false'), method: 'POST' }, (data: any) => success(onboarding(data)), error) // 微信实名认证:业务开通确认生效后由商户主动推进的下一步。未决申请不会重复提交, // retry 只对「通道已明确驳回」有意义。