feat(admin): 支付配置表单按服务商分字段并收敛高级配置

This commit is contained in:
yml2213
2026-11-10 17:15:55 +08:00
parent 9e029a519f
commit 58d9f50bac
@@ -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<string, string> = {
leshua: 'https://paygate.leshuazf.com/cgi-bin/lepos_pay_gateway.cgi',
mock: '',
}
const lakalaGateways: Record<string, string> = {
production: 'https://s2.lakala.com',
sandbox: 'https://test.wsmsd.cn/sit',
}
const shunchengGateways: Record<string, string> = {
production: 'http://jyzxapi.ydxnhb.com',
sandbox: 'http://jyzxapi.ydxnhb.com',
}
const defaultNotifyPaths: Record<string, string> = {
leshua: '/api/payments/leshua/notify',
lakala: '/api/payments/lakala/notify',
shuncheng: '/api/payments/shuncheng/notify',
mock: '',
}
const lakalaPayModeByPayWay: Record<string, string> = {
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<string, unknown> {
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<string, unknown>
}
export function AdminPaymentConfigsPage() {
const { message, modal } = App.useApp()
const queryClient = useQueryClient()
const [form] = Form.useForm<ConfigFormValues>()
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<AdminPaymentConfigQuery>({})
const [filters, setFilters] = useState<AdminPaymentConfigQuery>({})
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<string, unknown> | 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<string, unknown> = {
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<string, unknown> = {
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<ConfigFormValues> form={form} layout="vertical" disabled={submitting}>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="name" label="配置名称" rules={[{ required: true }]}>
<Input maxLength={128} />
<Form.Item
name="name"
label="配置名称"
rules={[{ required: true, message: '请输入配置名称' }]}
>
<Input maxLength={128} placeholder="如:乐刷生产商户1" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="provider" label="服务商" rules={[{ required: true }]}>
<Select options={providerOptions} disabled={Boolean(editing)} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="merchant_id" label="商户号" rules={[{ required: true }]}>
<Input maxLength={128} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="pay_way" label="支付方式">
<Select allowClear options={payWayOptions} />
</Form.Item>
</Col>
</Row>
<Form.Item name="gateway_url" label="网关地址">
<Input maxLength={512} />
</Form.Item>
<Form.Item name="notify_url" label="回调地址">
<Input maxLength={512} />
</Form.Item>
<Form.Item name="jump_url" label="跳转地址">
<Input maxLength={512} />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="sign_key"
label="SignKey"
extra={editing ? '留空不修改' : '按服务商要求填写'}
>
<Input.Password autoComplete="new-password" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="notify_key"
label="NotifyKey"
extra={editing ? '留空不修改' : '按服务商要求填写'}
>
<Input.Password autoComplete="new-password" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="sign_type" label="签名类型">
<Select
allowClear
options={[
{ value: 'MD5', label: 'MD5' },
{ value: 'SHA256withRSA', label: 'SHA256withRSA' },
]}
options={providerOptions}
disabled={Boolean(editing)}
onChange={handleProviderChange}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="merchant_id"
label="商户号"
rules={[{ required: true, message: '请输入商户号' }]}
>
<Input maxLength={128} placeholder="渠道商户号" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="pay_way" label="支付方式" rules={[{ required: true }]}>
<Segmented
options={[
{ label: '支付宝', value: 'ZFBZF' },
{ label: '微信', value: 'WXZF' },
]}
onChange={value => handlePayWayChange(value)}
/>
</Form.Item>
</Col>
</Row>
{!isMock ? (
<Form.Item
name="gateway_url"
label="网关地址"
rules={[{ required: true, message: '请输入支付网关地址' }]}
>
<Input maxLength={512} />
</Form.Item>
) : null}
{isLakala ? (
<Row gutter={16}>
<Col span={8}>
<Form.Item
name="extra_app_id"
label="App ID"
rules={[{ required: true, message: '请输入拉卡拉开放平台 App ID' }]}
>
<Input maxLength={128} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="extra_serial_no"
label="证书序列号"
rules={[{ required: true, message: '请输入商户证书序列号' }]}
>
<Input maxLength={128} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="extra_term_no"
label="终端号"
rules={[{ required: true, message: '请输入拉卡拉终端号' }]}
>
<Input maxLength={64} />
</Form.Item>
</Col>
</Row>
) : null}
{isShuncheng ? (
<Form.Item
name="extra_secret_id"
label="Secret ID"
rules={[{ required: true, message: '请输入顺成 Secret ID' }]}
>
<Input maxLength={128} />
</Form.Item>
) : null}
{!isMock ? (
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="sign_key"
label={signKeyLabel}
rules={
editing
? undefined
: [
{
required: true,
message: isLakala ? '请输入商户私钥 PEM' : '请输入签名密钥',
},
]
}
extra={editing ? '留空不修改' : undefined}
>
{isLakala ? (
<Input.TextArea
rows={5}
placeholder="请输入商户私钥 PEM"
autoComplete="new-password"
/>
) : (
<Input.Password placeholder="请输入签名密钥" autoComplete="new-password" />
)}
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="notify_key"
label={notifyKeyLabel}
rules={
editing
? undefined
: [
{
required: true,
message: isLakala ? '请输入通知验签证书 PEM' : '请输入通知密钥',
},
]
}
extra={editing ? '留空不修改' : undefined}
>
{isLakala ? (
<Input.TextArea
rows={5}
placeholder="请输入拉卡拉通知验签证书 PEM"
autoComplete="new-password"
/>
) : (
<Input.Password placeholder="请输入通知密钥" autoComplete="new-password" />
)}
</Form.Item>
</Col>
</Row>
) : null}
{!isMock ? (
<Form.Item
name="notify_url"
label="回调地址"
rules={[{ required: true, message: '请输入异步通知回调地址' }]}
>
<Input maxLength={512} />
</Form.Item>
) : null}
<Form.Item
name="is_default"
label="是否默认"
valuePropName="checked"
extra="同一支付方式只保留一个启用默认配置"
>
<Switch checkedChildren="默认" unCheckedChildren="普通" />
</Form.Item>
<Row gutter={16}>
<Col span={8}>
<Form.Item name="status" label="状态">
<Select options={statusOptions} />
@@ -631,35 +891,113 @@ export function AdminPaymentConfigsPage() {
{ value: 'production', label: '生产' },
{ value: 'sandbox', label: '沙箱' },
]}
onChange={handleEnvironmentChange}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="jspay_flag" label="JSAPI 标识">
<Input maxLength={64} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="is_default" label="默认配置" valuePropName="checked">
<Switch checkedChildren="默认" unCheckedChildren="普通" />
</Form.Item>
</Col>
</Row>
<Form.Item name="business_tags_text" label="业务标签" extra="逗号分隔">
<Input maxLength={255} />
</Form.Item>
<Form.Item
name="extra_config_json"
label="额外配置 JSON"
extra="拉卡拉需 app_id / serial_no / term_no;顺诚需 secret_id"
>
<Input.TextArea
rows={5}
placeholder='{"app_id":"...","serial_no":"...","term_no":"..."}'
/>
<Input maxLength={255} placeholder="逗号分隔" />
</Form.Item>
<Collapse
ghost
items={[
{
key: 'advanced',
label: '高级配置',
children: (
<>
<Form.Item name="jump_url" label="跳转地址">
<Input maxLength={512} placeholder="支付完成跳转地址" />
</Form.Item>
{isLakala ? (
<>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="extra_pay_mode" label="拉卡拉模式">
<Select
options={[
{ value: 'ALIPAY', label: '支付宝' },
{ value: 'WECHAT', label: '微信' },
{ value: 'AUTO', label: '不限制,由收银台选择' },
{ value: 'UQRCODEPAY', label: '银联' },
]}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="extra_order_expire_minutes" label="有效期分钟">
<InputNumber min={1} max={1440} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
<Form.Item
name="extra_lakala_cert"
label="平台证书"
extra="拉卡拉平台响应验签证书 PEM,可与通知证书一致"
>
<Input.TextArea rows={4} />
</Form.Item>
</>
) : null}
{isShuncheng ? (
<Row gutter={16}>
<Col span={8}>
<Form.Item name="extra_shop_no" label="门店编号">
<Input maxLength={64} placeholder="可选,顺成商户门店编号" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="extra_refund_type" label="退款出资">
<Select
options={[
{ value: '', label: '顺成默认' },
{ value: '0', label: '当日交易抵扣' },
{ value: '1', label: '优先交易抵扣,不足走余额' },
{ value: '2', label: '仅商户余额' },
]}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="extra_token_refresh_margin_seconds" label="Token 刷新秒">
<InputNumber min={60} max={1800} style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
) : null}
<Row gutter={16}>
<Col span={12}>
<Form.Item name="jspay_flag" label="支付形态">
<Select
allowClear
options={[
{ value: '1', label: 'H5 / JSAPI' },
{ value: '2', label: '简易支付 / 收银台' },
]}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="sign_type" label="签名类型">
<Select
allowClear
options={[
{ value: 'MD5', label: 'MD5' },
{ value: 'SHA256withRSA', label: 'SHA256withRSA' },
]}
/>
</Form.Item>
</Col>
</Row>
</>
),
},
]}
/>
<Typography.Paragraph type="secondary" className="admin-form-hint">
<CreditCardOutlined /> 支付配置变更会记录审计日志;密钥不会回显。
</Typography.Paragraph>