diff --git a/frontend/src/features/admin/AdminPaymentConfigsPage.tsx b/frontend/src/features/admin/AdminPaymentConfigsPage.tsx index d773062..eb6872d 100644 --- a/frontend/src/features/admin/AdminPaymentConfigsPage.tsx +++ b/frontend/src/features/admin/AdminPaymentConfigsPage.tsx @@ -4,11 +4,14 @@ import { Alert, App, Button, + Collapse, Col, Form, Input, + InputNumber, Modal, Row, + Segmented, Select, Space, Switch, @@ -63,23 +66,120 @@ const statusOptions = Object.entries(statusLabels).map(([value, label]) => ({ va const providerOptions = Object.entries(providerLabels).map(([value, label]) => ({ value, label })) const payWayOptions = Object.entries(payWayLabels).map(([value, label]) => ({ value, label })) +const defaultGateways: Record = { + leshua: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi', + mock: '', +} + +const lakalaGateways: Record = { + production: 'https://s2.lakala.com', + sandbox: 'https://test.wsmsd.cn/sit', +} + +const shunchengGateways: Record = { + production: 'http://jyzxapi.ydxnhb.com', + sandbox: 'http://jyzxapi.ydxnhb.com', +} + +const defaultNotifyPaths: Record = { + leshua: '/api/payments/leshua/notify', + lakala: '/api/payments/lakala/notify', + shuncheng: '/api/payments/shuncheng/notify', + mock: '', +} + +const lakalaPayModeByPayWay: Record = { + ZFBZF: 'ALIPAY', + WXZF: 'WECHAT', +} + +function defaultGateway(provider: string, environment = 'production') { + if (provider === 'lakala') { + return lakalaGateways[environment] ?? lakalaGateways.production + } + if (provider === 'shuncheng') { + return shunchengGateways[environment] ?? shunchengGateways.production + } + return defaultGateways[provider] ?? '' +} + +function defaultNotifyURL(provider: string) { + const path = defaultNotifyPaths[provider] + return path ? `${window.location.origin}${path}` : '' +} + +function defaultJumpURL(provider: string) { + return provider === 'mock' ? '' : `${window.location.origin}/orders` +} + +function defaultSignType(provider: string) { + return provider === 'lakala' ? 'SHA256withRSA' : 'MD5' +} + +function defaultExtraConfig(provider: string): Record { + if (provider === 'lakala') { + return { + app_id: '', + serial_no: '', + term_no: '', + lakala_cert: '', + pay_mode: 'ALIPAY', + order_expire_minutes: 15, + } + } + if (provider === 'shuncheng') { + return { + secret_id: '', + shop_no: '', + refund_type: '', + token_refresh_margin_seconds: 300, + } + } + return {} +} + +function extraFormDefaults(provider: string) { + const extra = defaultExtraConfig(provider) + return { + extra_app_id: String(extra.app_id ?? ''), + extra_serial_no: String(extra.serial_no ?? ''), + extra_term_no: String(extra.term_no ?? ''), + extra_lakala_cert: String(extra.lakala_cert ?? ''), + extra_pay_mode: String(extra.pay_mode ?? ''), + extra_order_expire_minutes: Number(extra.order_expire_minutes ?? 15), + extra_secret_id: String(extra.secret_id ?? ''), + extra_shop_no: String(extra.shop_no ?? ''), + extra_refund_type: String(extra.refund_type ?? ''), + extra_token_refresh_margin_seconds: Number(extra.token_refresh_margin_seconds ?? 300), + } +} + interface ConfigFormValues { name: string provider: string merchant_id: string + pay_way: string gateway_url?: string sign_key?: string notify_key?: string notify_url?: string - jump_url?: string - pay_way?: string - jspay_flag?: string - sign_type?: string is_default: boolean status: string environment: string + jump_url?: string + jspay_flag?: string + sign_type?: string business_tags_text?: string - extra_config_json?: string + extra_app_id?: string + extra_serial_no?: string + extra_term_no?: string + extra_lakala_cert?: string + extra_pay_mode?: string + extra_order_expire_minutes?: number + extra_secret_id?: string + extra_shop_no?: string + extra_refund_type?: string + extra_token_refresh_margin_seconds?: number } function errorText(error: unknown) { @@ -88,21 +188,16 @@ function errorText(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试' } -function parseExtraConfig(text?: string) { - const trimmed = text?.trim() - if (!trimmed) return undefined - const parsed = JSON.parse(trimmed) - if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') { - throw new Error('额外配置必须是 JSON 对象') - } - return parsed as Record -} - export function AdminPaymentConfigsPage() { const { message, modal } = App.useApp() const queryClient = useQueryClient() const [form] = Form.useForm() const providerValue = Form.useWatch('provider', form) + const isMock = providerValue === 'mock' + const isLakala = providerValue === 'lakala' + const isShuncheng = providerValue === 'shuncheng' + const signKeyLabel = isLakala ? '商户私钥' : isShuncheng ? 'SecretKey' : '签名密钥' + const notifyKeyLabel = isLakala ? '通知证书' : '通知密钥' const [draft, setDraft] = useState({}) const [filters, setFilters] = useState({}) const [page, setPage] = useState(1) @@ -149,65 +244,114 @@ export function AdminPaymentConfigsPage() { name: '', provider: 'leshua', merchant_id: '', - gateway_url: '', + pay_way: 'ZFBZF', + gateway_url: defaultGateway('leshua', 'production'), sign_key: '', notify_key: '', - notify_url: '', - jump_url: '', - pay_way: 'WXZF', - jspay_flag: '', - sign_type: 'MD5', + notify_url: defaultNotifyURL('leshua'), + jump_url: defaultJumpURL('leshua'), + sign_type: defaultSignType('leshua'), + jspay_flag: '2', is_default: false, status: 'active', environment: 'production', business_tags_text: '', - extra_config_json: '', + ...extraFormDefaults('leshua'), }) setEditorOpen(true) } const openEdit = (row: AdminPaymentConfig) => { setEditing(row) + const extra = { ...defaultExtraConfig(row.provider), ...(row.extra_config ?? {}) } form.setFieldsValue({ name: row.name, provider: row.provider, merchant_id: row.merchant_id, + pay_way: row.pay_way, gateway_url: row.gateway_url, sign_key: '', notify_key: '', notify_url: row.notify_url, jump_url: row.jump_url, - pay_way: row.pay_way, jspay_flag: row.jspay_flag, sign_type: row.sign_type, is_default: row.is_default, status: row.status, environment: row.environment, business_tags_text: (row.business_tags ?? []).join(', '), - extra_config_json: row.extra_config ? JSON.stringify(row.extra_config, null, 2) : '', + extra_app_id: String(extra.app_id ?? ''), + extra_serial_no: String(extra.serial_no ?? ''), + extra_term_no: String(extra.term_no ?? ''), + extra_lakala_cert: String(extra.lakala_cert ?? ''), + extra_pay_mode: String(extra.pay_mode ?? ''), + extra_order_expire_minutes: Number(extra.order_expire_minutes ?? 15), + extra_secret_id: String(extra.secret_id ?? ''), + extra_shop_no: String(extra.shop_no ?? ''), + extra_refund_type: String(extra.refund_type ?? ''), + extra_token_refresh_margin_seconds: Number(extra.token_refresh_margin_seconds ?? 300), }) setEditorOpen(true) } + const handleProviderChange = (provider: string) => { + if (editing) return + const environment = (form.getFieldValue('environment') as string) || 'production' + form.setFieldsValue({ + gateway_url: defaultGateway(provider, environment), + notify_url: defaultNotifyURL(provider), + jump_url: defaultJumpURL(provider), + sign_type: defaultSignType(provider), + jspay_flag: '2', + ...extraFormDefaults(provider), + }) + } + + const handlePayWayChange = (payWay: string | number) => { + if (isLakala) { + form.setFieldsValue({ extra_pay_mode: lakalaPayModeByPayWay[String(payWay)] ?? 'AUTO' }) + } + } + + const handleEnvironmentChange = (environment: string) => { + const gateway = form.getFieldValue('gateway_url') as string | undefined + if (isLakala && (!gateway || Object.values(lakalaGateways).includes(gateway))) { + form.setFieldsValue({ gateway_url: defaultGateway('lakala', environment) }) + return + } + if (isShuncheng && (!gateway || Object.values(shunchengGateways).includes(gateway))) { + form.setFieldsValue({ gateway_url: defaultGateway('shuncheng', environment) }) + } + } + const buildPayload = (values: ConfigFormValues): AdminPaymentConfigPayload => { let extraConfig: Record | undefined - try { - extraConfig = parseExtraConfig(values.extra_config_json) - } catch (error) { - throw new Error(error instanceof Error ? error.message : '额外配置 JSON 不正确', { - cause: error, - }) + if (values.provider === 'lakala') { + const extra: Record = { + app_id: values.extra_app_id?.trim() ?? '', + serial_no: values.extra_serial_no?.trim() ?? '', + term_no: values.extra_term_no?.trim() ?? '', + pay_mode: values.extra_pay_mode || 'AUTO', + order_expire_minutes: values.extra_order_expire_minutes ?? 15, + } + if (values.extra_lakala_cert?.trim()) extra.lakala_cert = values.extra_lakala_cert.trim() + extraConfig = extra + } else if (values.provider === 'shuncheng') { + const extra: Record = { + secret_id: values.extra_secret_id?.trim() ?? '', + token_refresh_margin_seconds: values.extra_token_refresh_margin_seconds ?? 300, + } + if (values.extra_shop_no?.trim()) extra.shop_no = values.extra_shop_no.trim() + if (values.extra_refund_type !== undefined && values.extra_refund_type !== '') { + extra.refund_type = values.extra_refund_type + } + extraConfig = extra } const payload: AdminPaymentConfigPayload = { name: values.name.trim(), provider: values.provider, merchant_id: values.merchant_id.trim(), - gateway_url: values.gateway_url?.trim() || '', - notify_url: values.notify_url?.trim() || '', - jump_url: values.jump_url?.trim() || '', pay_way: values.pay_way, - jspay_flag: values.jspay_flag?.trim() || '', - sign_type: values.sign_type, is_default: values.is_default, status: values.status, environment: values.environment, @@ -215,10 +359,17 @@ export function AdminPaymentConfigsPage() { .split(',') .map(item => item.trim()) .filter(Boolean), - extra_config: extraConfig, } - if (values.sign_key?.trim()) payload.sign_key = values.sign_key.trim() - if (values.notify_key?.trim()) payload.notify_key = values.notify_key.trim() + if (values.provider !== 'mock') { + payload.gateway_url = values.gateway_url?.trim() ?? '' + payload.notify_url = values.notify_url?.trim() ?? '' + payload.jump_url = values.jump_url?.trim() ?? '' + payload.jspay_flag = values.jspay_flag?.trim() ?? '' + payload.sign_type = values.sign_type + payload.extra_config = extraConfig + if (values.sign_key?.trim()) payload.sign_key = values.sign_key.trim() + if (values.notify_key?.trim()) payload.notify_key = values.notify_key.trim() + } return payload } @@ -556,69 +707,178 @@ export function AdminPaymentConfigsPage() { form={form} layout="vertical" disabled={submitting}> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + handlePayWayChange(value)} + /> + + + + + {!isMock ? ( + + + + ) : null} + + {isLakala ? ( + + + + + + + + + + + + + + + + + + ) : null} + + {isShuncheng ? ( + + + + ) : null} + + {!isMock ? ( + + + + {isLakala ? ( + + ) : ( + + )} + + + + + {isLakala ? ( + + ) : ( + + )} + + + + ) : null} + + {!isMock ? ( + + + + ) : null} + + + + + + - - - - - - - - + - - - - + + + + + + + {isLakala ? ( + <> + + + + + + + + + + + + + +