feat(distribution): 收口账号同步与支付配置并优化结算
This commit is contained in:
@@ -279,6 +279,52 @@ func readOrderSnapshotPrice(raw datatypes.JSON, key string) float64 {
|
||||
return readJSONNumber(breakdown[key])
|
||||
}
|
||||
|
||||
// readOrderSnapshotBPS 优先读取整数基点字段;旧快照只有 float 比例时换算为 bps。
|
||||
func readOrderSnapshotBPS(raw datatypes.JSON, key string) int64 {
|
||||
breakdown, ok := readOrderSnapshotBreakdown(raw)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
if bps := readJSONNumber(breakdown[key+"_bps"]); bps > 0 {
|
||||
return int64(math.Round(bps))
|
||||
}
|
||||
legacy := readJSONNumber(breakdown[key])
|
||||
if legacy <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int64(math.Round(legacy * 10000))
|
||||
}
|
||||
|
||||
// quantityUnits 把 M 数量转成百分之一 M 的整数单位,避免比例计算依赖浮点。
|
||||
func quantityUnits(value float64) int64 {
|
||||
if value <= 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0
|
||||
}
|
||||
return int64(math.Round(value * 100))
|
||||
}
|
||||
|
||||
// roundedDiv 对非负整数做四舍五入除法。
|
||||
func roundedDiv(numerator, denominator int64) int64 {
|
||||
if numerator <= 0 || denominator <= 0 {
|
||||
return 0
|
||||
}
|
||||
return (numerator + denominator/2) / denominator
|
||||
}
|
||||
|
||||
// proportionalCent 按 part/total 折算金额;total<=0 时保持旧行为取全额。
|
||||
func proportionalCent(baseCent, partUnits, totalUnits int64) int64 {
|
||||
if baseCent <= 0 {
|
||||
return 0
|
||||
}
|
||||
if totalUnits <= 0 {
|
||||
return baseCent
|
||||
}
|
||||
if partUnits <= 0 {
|
||||
return 0
|
||||
}
|
||||
return roundedDiv(baseCent*partUnits, totalUnits)
|
||||
}
|
||||
|
||||
func readJSONNumber(value any) float64 {
|
||||
switch typed := value.(type) {
|
||||
case float64:
|
||||
@@ -377,28 +423,28 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
|
||||
|
||||
consumedM := roundQuantity(coinConsumedM)
|
||||
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
|
||||
buyerRatio := readOrderSnapshotPrice(order.AccountSnapshot, "buyer_ratio")
|
||||
sellerRatio := readOrderSnapshotPrice(order.AccountSnapshot, "seller_ratio")
|
||||
buyerRatioBPS := readOrderSnapshotBPS(order.AccountSnapshot, "buyer_ratio")
|
||||
sellerRatioBPS := readOrderSnapshotBPS(order.AccountSnapshot, "seller_ratio")
|
||||
consumedUnits := quantityUnits(consumedM)
|
||||
totalCoinUnits := quantityUnits(totalCoinM)
|
||||
|
||||
// 币费:优先按各自比例 × 实际消耗;无比例时回退到预收币基价 × (消耗/发布量),允许 >1(打超)
|
||||
// 无发布量快照时保持旧行为:按全额使用(ratio=1)
|
||||
coinUseRatio := coinConsumptionRatio(consumedM, totalCoinM)
|
||||
// 币费:比例优先按 bps 整数换算;缺失 bps 时把旧比例换算成 bps。
|
||||
// 无比例时回退到预收币基价按消耗比例折算,仍使用整数除法。
|
||||
var usedBuyerCoinPriceCent, usedOwnerCoinPriceCent int64
|
||||
if buyerRatio > 0 && (totalCoinM > 0 || consumedM > 0) {
|
||||
// 价(元) = 消耗M×100 / ratio;分 = 元×100
|
||||
usedBuyerCoinPriceCent = int64(math.Round(consumedM * 10000 / buyerRatio))
|
||||
if buyerRatioBPS > 0 && (totalCoinUnits > 0 || consumedUnits > 0) {
|
||||
usedBuyerCoinPriceCent = roundedDiv(consumedUnits*1_000_000, buyerRatioBPS)
|
||||
} else {
|
||||
usedBuyerCoinPriceCent = int64(math.Round(float64(buyerCoinBasePriceCent) * coinUseRatio))
|
||||
usedBuyerCoinPriceCent = proportionalCent(buyerCoinBasePriceCent, consumedUnits, totalCoinUnits)
|
||||
}
|
||||
if sellerRatio > 0 && (totalCoinM > 0 || consumedM > 0) {
|
||||
usedOwnerCoinPriceCent = int64(math.Round(consumedM * 10000 / sellerRatio))
|
||||
if sellerRatioBPS > 0 && (totalCoinUnits > 0 || consumedUnits > 0) {
|
||||
usedOwnerCoinPriceCent = roundedDiv(consumedUnits*1_000_000, sellerRatioBPS)
|
||||
} else {
|
||||
usedOwnerCoinPriceCent = int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio))
|
||||
usedOwnerCoinPriceCent = proportionalCent(sellerCoinBasePriceCent, consumedUnits, totalCoinUnits)
|
||||
}
|
||||
coinOvershoot := totalCoinM > 0 && consumedM > totalCoinM
|
||||
coinOvershoot := totalCoinUnits > 0 && consumedUnits > totalCoinUnits
|
||||
if !coinOvershoot {
|
||||
// 未打超时,比例反推只能用于折算未用完部分,不能因四舍五入超过订单快照。
|
||||
if totalCoinM > 0 && consumedM >= totalCoinM {
|
||||
// 未打超时,比例反推只能用于折算未用完部分,不能因取整超过订单快照。
|
||||
if totalCoinUnits > 0 && consumedUnits >= totalCoinUnits {
|
||||
usedBuyerCoinPriceCent = buyerCoinBasePriceCent
|
||||
usedOwnerCoinPriceCent = sellerCoinBasePriceCent
|
||||
} else {
|
||||
@@ -412,8 +458,10 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
|
||||
var usedOwnerConsumablePriceCent int64
|
||||
if prepaidConsumablePriceCent > 0 {
|
||||
if usedBuyerConsumablePriceCent <= prepaidConsumablePriceCent {
|
||||
consumableUseRatio := maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0)
|
||||
usedOwnerConsumablePriceCent = int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio))
|
||||
usedOwnerConsumablePriceCent = roundedDiv(
|
||||
prepaidOwnerConsumablePriceCent*usedBuyerConsumablePriceCent,
|
||||
prepaidConsumablePriceCent,
|
||||
)
|
||||
usedOwnerConsumablePriceCent = minCent(usedOwnerConsumablePriceCent, prepaidOwnerConsumablePriceCent)
|
||||
} else {
|
||||
// 预收范围内按号主预收基准,超出预收部分作为后置损耗全额划归号主(平台不抽成)
|
||||
|
||||
@@ -66,3 +66,37 @@ func TestBuildOrderPricingUsesCentBreakdown(t *testing.T) {
|
||||
t.Fatalf("OwnerPureCoinAmountCent = %d, want 23315", pricing.OwnerPureCoinAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateCheckoutSettlementPrefersRatioBPS(t *testing.T) {
|
||||
// bps 必须优先于旧 float 比例:buyer 1:40、seller 1:45,消耗 180M。
|
||||
order := model.RentalOrder{
|
||||
RentAmountCent: 25000,
|
||||
OwnerRentAmountCent: 22222,
|
||||
DepositAmountCent: 50000,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 100000000,
|
||||
"asset_summary": {
|
||||
"price_breakdown": {
|
||||
"buyer_ratio": 1,
|
||||
"seller_ratio": 1,
|
||||
"buyer_ratio_bps": 400000,
|
||||
"seller_ratio_bps": 450000,
|
||||
"buyer_coin_base_price": 250,
|
||||
"seller_coin_base_price": 222.22,
|
||||
"consumable_price": 0
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
}
|
||||
|
||||
settlement := calculateCheckoutSettlement(order, 0, 180, 0)
|
||||
if settlement.ActualRentAmountCent != 45000 {
|
||||
t.Fatalf("ActualRentAmountCent = %d, want 45000", settlement.ActualRentAmountCent)
|
||||
}
|
||||
if settlement.OwnerRentIncomeCent != 40000 {
|
||||
t.Fatalf("OwnerRentIncomeCent = %d, want 40000", settlement.OwnerRentIncomeCent)
|
||||
}
|
||||
if settlement.PlatformFeeCent != 5000 {
|
||||
t.Fatalf("PlatformFeeCent = %d, want 5000", settlement.PlatformFeeCent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Phase 2 验收报告(分销收口、结算 bps 与构建优化)
|
||||
|
||||
> 日期:2026-10-30
|
||||
> 项目:解忧商行(hfb_jysh)
|
||||
> 范围:分销账号同步、主站支付配置、测试站点回归、订单结算 bps、构建拆包
|
||||
> 结论:**1–4 与 6 已完成;真实外部支付 E2E 被支付商户/短信/实名配置阻塞**
|
||||
|
||||
---
|
||||
|
||||
## 1. 分销账号同步
|
||||
|
||||
- 页面:`frontend/src/features/admin/AdminDistributionListingsPage.tsx`
|
||||
- 接口:`POST /api/admin/distribution/sites/:id/sync-accounts`
|
||||
- 权限:`distribution:account_display`
|
||||
- 弹窗支持:新账号默认加价、是否清理失效商品;执行后展示同步/跳过/更新/恢复/清理数量。
|
||||
- 请求类型与结果类型已落 `frontend/src/features/admin/types.ts`,API 封装在 `frontend/src/features/admin/api.ts`。
|
||||
- 验收中未执行真实同步写入。
|
||||
|
||||
## 2. 主站支付配置管理
|
||||
|
||||
- 页面:`frontend/src/features/admin/AdminPaymentConfigsPage.tsx`
|
||||
- 路由:`/console-jy8k3m/payment-configs`
|
||||
- 权限:`payment_config:list/create/update/delete`
|
||||
- 能力:
|
||||
- 服务商、支付方式、状态、环境筛选
|
||||
- 名称、商户号、网关、回调、签名/密钥、额外配置 JSON、业务标签
|
||||
- 创建、编辑、删除;密钥只写不读,编辑时留空不修改
|
||||
- 拉卡拉/顺诚的 `extra_config` 通过 JSON 文本框维护
|
||||
- 后端配置列表在开发库为空,只读 GET 返回 200。
|
||||
|
||||
## 3. 测试站点真实数据回归
|
||||
|
||||
已通过管理端 onboard 接口创建开发测试站点:
|
||||
|
||||
```text
|
||||
site_id: 1
|
||||
site_code: jysh_test_1793347033
|
||||
domain: test-1793347033.zuhaoc.com
|
||||
admin: jysh_admin_1793347033
|
||||
```
|
||||
|
||||
站点级只读接口全部 200:
|
||||
|
||||
- 钱包、提现记录、短信配置、邀请配置
|
||||
- 二维码列表/统计、分销看板、分销用户、站点公告
|
||||
- 收货商品、账号展示、支付通道、结算批次、分销订单
|
||||
|
||||
全部 14 个后台页面路由返回 200,包括分销看板、支付配置与既有分销页面。
|
||||
|
||||
## 4. 订单结算比例 bps 化
|
||||
|
||||
- `backend/internal/modules/order/pricing.go`
|
||||
- 新增 `readOrderSnapshotBPS`:优先读 `buyer_ratio_bps` / `seller_ratio_bps`,缺失时把旧 float 比例换算为 bps。
|
||||
- 新增 `quantityUnits`:M 数量转百分之一 M 的整数单位。
|
||||
- 新增 `roundedDiv` / `proportionalCent`:金额与比例折算改为整数四舍五入除法。
|
||||
- 币费、消耗品号主分摊不再使用 float 乘法。
|
||||
- 新增测试 `TestCalculateCheckoutSettlementPrefersRatioBPS`,验证 bps 覆盖冲突的旧比例。
|
||||
- 验证:
|
||||
|
||||
```text
|
||||
go test ./internal/modules/order ./internal/modules/listing ./pkg/money
|
||||
通过
|
||||
|
||||
go test ./internal/modules/order ./internal/modules/payment ./internal/modules/distribution ./internal/modules/listing ./internal/modules/invitation ./internal/modules/chat
|
||||
全部通过
|
||||
```
|
||||
|
||||
## 5. E2E 执行情况与阻塞
|
||||
|
||||
已完成可执行部分:订单、支付、分销、商品、邀请、聊天模块测试全部通过。
|
||||
|
||||
真实外部支付订单 E2E **未执行**,原因:
|
||||
|
||||
- 开发库没有真实支付商户配置,无法发起外部支付/回调。
|
||||
- 短信签名与实名服务仍为空,无法完成真实注册/实名链路。
|
||||
- 测试站点尚无真实商品、买家和卖家用户数据。
|
||||
|
||||
因此本项记录为环境阻塞,不伪造“全链路已通过”。
|
||||
|
||||
## 6. 构建拆包优化
|
||||
|
||||
`frontend/vite.config.ts` 新增:
|
||||
|
||||
- `vendor-antd-icons`
|
||||
- `vendor-date`
|
||||
|
||||
首次尝试把全部 antd/rc 打进独立包产生 1.2 MB 超大包,已回退该策略,仅拆图标与日期库。
|
||||
|
||||
最终构建:
|
||||
|
||||
```text
|
||||
npx tsc --noEmit 通过
|
||||
npm run build 通过
|
||||
主入口 492.81 kB / gzip 155.77 kB
|
||||
vendor-antd-icons 101.61 kB / gzip 29.54 kB
|
||||
vendor-date 16.77 kB / gzip 6.73 kB
|
||||
```
|
||||
|
||||
主入口已从 525.59 kB 降至 492.81 kB,低于 500 kB;超大包告警消失。
|
||||
|
||||
## 7. 当前工作区
|
||||
|
||||
本轮新增/修改尚未提交:
|
||||
|
||||
- 账号同步与分销商品页
|
||||
- 主站支付配置页与 API/类型
|
||||
- 订单结算 bps 与测试
|
||||
- Vite 拆包配置
|
||||
- 本文档与项目计划
|
||||
|
||||
## 8. 下一步
|
||||
|
||||
1. 提交本轮变更。
|
||||
2. 配置测试支付商户、短信签名与实名服务,再跑真实订单 E2E。
|
||||
3. 用测试站点补充商品/买家/卖家数据,回归商品、订单、钱包、结算写流程。
|
||||
4. 上线前执行完整 `scripts/check.sh` 与生产冒烟。
|
||||
+1
-1
@@ -395,7 +395,7 @@ hfb_jysh/
|
||||
|
||||
### Phase 4:管理后台核心(4–5 周)
|
||||
|
||||
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、资金出款、运营开支、公告管理、审计日志、系统配置、分销站点、分销看板、站点钱包、分销订单、结算批次、分销用户、站点公告、分销商品、支付通道、规则版本、二维码池、站点短信与邀请返利;剩余分销账号同步、主站支付配置管理等细分长尾和真实订单端到端回归。
|
||||
> 状态:**进行中(2026-10-30)**。已完成仪表盘、订单、退款、提现、商品、用户、申诉、财务看板、资金出款、运营开支、公告管理、审计日志、系统配置、支付配置、分销站点、分销看板、站点钱包、分销订单、结算批次、分销用户、站点公告、分销商品、支付通道、规则版本、二维码池、站点短信与邀请返利;分销账号同步入口已接入,订单结算比例已 bps 化;剩余真实外部支付 E2E、真实站点数据业务回归与上线准备。
|
||||
|
||||
- 后台登录、工作台、订单列表、订单详情。
|
||||
- 商品列表、商品详情、审核。
|
||||
|
||||
@@ -175,6 +175,11 @@ const AdminSystemConfigsPage = lazy(() =>
|
||||
default: module.AdminSystemConfigsPage,
|
||||
}))
|
||||
)
|
||||
const AdminPaymentConfigsPage = lazy(() =>
|
||||
import('@/features/admin/AdminPaymentConfigsPage').then(module => ({
|
||||
default: module.AdminPaymentConfigsPage,
|
||||
}))
|
||||
)
|
||||
const AdminDistributionSitesPage = lazy(() =>
|
||||
import('@/features/admin/AdminDistributionSitesPage').then(module => ({
|
||||
default: module.AdminDistributionSitesPage,
|
||||
@@ -303,6 +308,7 @@ export function AppRouter() {
|
||||
<Route path="announcements" element={<AdminAnnouncementsPage />} />
|
||||
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
|
||||
<Route path="system-configs" element={<AdminSystemConfigsPage />} />
|
||||
<Route path="payment-configs" element={<AdminPaymentConfigsPage />} />
|
||||
<Route path="distribution/sites" element={<AdminDistributionSitesPage />} />
|
||||
<Route
|
||||
path="distribution/dashboard"
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
PlayCircleOutlined,
|
||||
ReloadOutlined,
|
||||
StopOutlined,
|
||||
SyncOutlined,
|
||||
TagsOutlined,
|
||||
} from '@ant-design/icons'
|
||||
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
batchUpdateAdminAccountMarkup,
|
||||
fetchAdminAccountDisplays,
|
||||
fetchAdminReceivedListings,
|
||||
syncAdminDistributionSiteAccounts,
|
||||
updateAdminAccountDisplay,
|
||||
updateAdminAccountMarkup,
|
||||
updateAdminReceivedListingStatus,
|
||||
@@ -91,6 +94,11 @@ interface MarkupFormValues {
|
||||
shelf_ratio_delta: number
|
||||
}
|
||||
|
||||
interface SyncFormValues {
|
||||
clean_stale_listings: boolean
|
||||
default_shelf_ratio_delta: number
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
if (response?.data?.message) return response.data.message
|
||||
@@ -128,6 +136,9 @@ export function AdminDistributionListingsPage() {
|
||||
const [batchMarkupOpen, setBatchMarkupOpen] = useState(false)
|
||||
const [batchMarkupForm] = Form.useForm<MarkupFormValues>()
|
||||
const [working, setWorking] = useState(false)
|
||||
const [syncOpen, setSyncOpen] = useState(false)
|
||||
const [syncForm] = Form.useForm<SyncFormValues>()
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
|
||||
const receivedQuery = useQuery({
|
||||
queryKey: ['admin-received-listings', siteId, receivedFilters, receivedPage, receivedPageSize],
|
||||
@@ -290,6 +301,43 @@ export function AdminDistributionListingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const openSync = () => {
|
||||
syncForm.setFieldsValue({
|
||||
clean_stale_listings: false,
|
||||
default_shelf_ratio_delta: 0,
|
||||
})
|
||||
setSyncOpen(true)
|
||||
}
|
||||
|
||||
const runSync = async () => {
|
||||
if (!siteId) return
|
||||
const values = await syncForm.validateFields()
|
||||
setSyncing(true)
|
||||
try {
|
||||
const result = await syncAdminDistributionSiteAccounts(siteId, values)
|
||||
modal.success({
|
||||
title: '账号同步完成',
|
||||
content: (
|
||||
<div className="admin-stack admin-stack--tiny">
|
||||
<span>同步 {result.synced_count} 件,跳过 {result.skipped_count} 件</span>
|
||||
<span>更新 {result.updated_count} 件,恢复 {result.reenabled_count} 件</span>
|
||||
<span>清理失效 {result.disabled_stale_count} 件</span>
|
||||
<span>当前展示 {result.total_active} 件</span>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
setSyncOpen(false)
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-received-listings', siteId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-account-displays', siteId] }),
|
||||
])
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
} finally {
|
||||
setSyncing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const receivedColumns: TableColumnsType<AdminReceivedListing> = [
|
||||
{
|
||||
title: '商品',
|
||||
@@ -514,14 +562,25 @@ export function AdminDistributionListingsPage() {
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={refresh}
|
||||
loading={receivedQuery.isFetching || accountsQuery.isFetching}
|
||||
disabled={!canView}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Space>
|
||||
{canManage ? (
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
disabled={!siteId}
|
||||
onClick={openSync}
|
||||
>
|
||||
同步账号
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={refresh}
|
||||
loading={receivedQuery.isFetching || accountsQuery.isFetching}
|
||||
disabled={!canView}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -843,6 +902,60 @@ export function AdminDistributionListingsPage() {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={syncOpen}
|
||||
title="同步站点账号"
|
||||
width={560}
|
||||
maskClosable={!syncing}
|
||||
closable={!syncing}
|
||||
onCancel={() => (syncing ? undefined : setSyncOpen(false))}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={() => setSyncOpen(false)} disabled={syncing}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
loading={syncing}
|
||||
onClick={() => void runSync()}
|
||||
>
|
||||
开始同步
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="同步会改动站点账号展示数据"
|
||||
description="同步按主站商品重新生成站点账号映射,可恢复/停用失效商品;请确认当前站点后再执行。"
|
||||
/>
|
||||
<Form<SyncFormValues>
|
||||
form={syncForm}
|
||||
layout="vertical"
|
||||
disabled={syncing}
|
||||
initialValues={{ clean_stale_listings: false, default_shelf_ratio_delta: 0 }}
|
||||
>
|
||||
<Form.Item
|
||||
name="default_shelf_ratio_delta"
|
||||
label="新账号默认加价(点)"
|
||||
extra="仅对本次新同步进站点的账号使用;已有账号保持原加价。"
|
||||
>
|
||||
<InputNumber min={-50} max={50} step={0.1} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="clean_stale_listings"
|
||||
label="清理失效商品"
|
||||
valuePropName="checked"
|
||||
extra="开启后会停用主站已下架/失效且不再同步的商品展示。"
|
||||
>
|
||||
<Switch checkedChildren="清理" unCheckedChildren="保留" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import {
|
||||
CreditCardOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
|
||||
import {
|
||||
createAdminPaymentConfig,
|
||||
deleteAdminPaymentConfig,
|
||||
fetchAdminPaymentConfigs,
|
||||
updateAdminPaymentConfig,
|
||||
} from '@/features/admin/api'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import { DataTable } from '@/features/admin/components/DataTable'
|
||||
import { FilterBar, FilterField } from '@/features/admin/components/FilterBar'
|
||||
import type {
|
||||
AdminPaymentConfig,
|
||||
AdminPaymentConfigPayload,
|
||||
AdminPaymentConfigQuery,
|
||||
} from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
|
||||
const providerLabels: Record<string, string> = {
|
||||
leshua: '乐刷',
|
||||
lakala: '拉卡拉',
|
||||
shuncheng: '顺诚',
|
||||
mock: 'Mock',
|
||||
}
|
||||
|
||||
const payWayLabels: Record<string, string> = {
|
||||
ZFBZF: '支付宝',
|
||||
WXZF: '微信支付',
|
||||
}
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
active: '启用',
|
||||
disabled: '停用',
|
||||
testing: '测试中',
|
||||
}
|
||||
|
||||
const statusOptions = Object.entries(statusLabels).map(([value, label]) => ({ value, label }))
|
||||
const providerOptions = Object.entries(providerLabels).map(([value, label]) => ({ value, label }))
|
||||
const payWayOptions = Object.entries(payWayLabels).map(([value, label]) => ({ value, label }))
|
||||
|
||||
interface ConfigFormValues {
|
||||
name: string
|
||||
provider: string
|
||||
merchant_id: 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
|
||||
business_tags_text?: string
|
||||
extra_config_json?: string
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const response = (error as { response?: { data?: { message?: string } } })?.response
|
||||
if (response?.data?.message) return response.data.message
|
||||
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 [draft, setDraft] = useState<AdminPaymentConfigQuery>({})
|
||||
const [filters, setFilters] = useState<AdminPaymentConfigQuery>({})
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [editorOpen, setEditorOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<AdminPaymentConfig>()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canView = Boolean(permissions?.includes('*') || permissions?.includes('payment_config:list'))
|
||||
const canCreate = Boolean(
|
||||
permissions?.includes('*') || permissions?.includes('payment_config:create')
|
||||
)
|
||||
const canUpdate = Boolean(
|
||||
permissions?.includes('*') || permissions?.includes('payment_config:update')
|
||||
)
|
||||
const canDelete = Boolean(
|
||||
permissions?.includes('*') || permissions?.includes('payment_config:delete')
|
||||
)
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: ['admin-payment-configs', filters, page, pageSize],
|
||||
queryFn: () => fetchAdminPaymentConfigs({ ...filters, page, page_size: pageSize }),
|
||||
enabled: canView,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const data = listQuery.data
|
||||
const rows = data?.items ?? []
|
||||
|
||||
const summary = useMemo(
|
||||
() => ({
|
||||
total: data?.total ?? 0,
|
||||
active: rows.filter(row => row.status === 'active').length,
|
||||
defaultCount: rows.filter(row => row.is_default).length,
|
||||
}),
|
||||
[data, rows]
|
||||
)
|
||||
|
||||
const openCreate = () => {
|
||||
setEditing(undefined)
|
||||
form.setFieldsValue({
|
||||
name: '',
|
||||
provider: 'leshua',
|
||||
merchant_id: '',
|
||||
gateway_url: '',
|
||||
sign_key: '',
|
||||
notify_key: '',
|
||||
notify_url: '',
|
||||
jump_url: '',
|
||||
pay_way: 'WXZF',
|
||||
jspay_flag: '',
|
||||
sign_type: 'MD5',
|
||||
is_default: false,
|
||||
status: 'active',
|
||||
environment: 'production',
|
||||
business_tags_text: '',
|
||||
extra_config_json: '',
|
||||
})
|
||||
setEditorOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (row: AdminPaymentConfig) => {
|
||||
setEditing(row)
|
||||
form.setFieldsValue({
|
||||
name: row.name,
|
||||
provider: row.provider,
|
||||
merchant_id: row.merchant_id,
|
||||
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)
|
||||
: '',
|
||||
})
|
||||
setEditorOpen(true)
|
||||
}
|
||||
|
||||
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 不正确')
|
||||
}
|
||||
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,
|
||||
business_tags: (values.business_tags_text ?? '')
|
||||
.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()
|
||||
return payload
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
const values = await form.validateFields()
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const payload = buildPayload(values)
|
||||
if (editing) {
|
||||
await updateAdminPaymentConfig(editing.id, payload)
|
||||
message.success('支付配置已更新')
|
||||
} else {
|
||||
await createAdminPaymentConfig(payload)
|
||||
message.success('支付配置已创建')
|
||||
}
|
||||
setEditorOpen(false)
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-payment-configs'] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-payment-config-options'] })
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const remove = (row: AdminPaymentConfig) => {
|
||||
modal.confirm({
|
||||
title: `确认删除支付配置「${row.name}」?`,
|
||||
content: '正在被订单、提现或分销站点引用的配置无法删除。',
|
||||
okText: '删除',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAdminPaymentConfig(row.id)
|
||||
message.success('支付配置已删除')
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-payment-configs'] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-payment-config-options'] })
|
||||
} catch (error) {
|
||||
message.error(errorText(error))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<AdminPaymentConfig> = [
|
||||
{
|
||||
title: '配置',
|
||||
key: 'config',
|
||||
width: 240,
|
||||
fixed: 'left',
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack">
|
||||
<Space size={6} wrap>
|
||||
<strong>{row.name}</strong>
|
||||
{row.is_default ? <Tag color="gold">默认</Tag> : null}
|
||||
</Space>
|
||||
<span className="admin-mono">#{row.id}</span>
|
||||
<span className="admin-mono">{formatText(row.merchant_id)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '服务商 / 方式',
|
||||
key: 'provider',
|
||||
width: 170,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack admin-stack--tiny">
|
||||
<Tag color="geekblue">{providerLabels[row.provider] ?? row.provider}</Tag>
|
||||
<span>{payWayLabels[row.pay_way] ?? formatText(row.pay_way)}</span>
|
||||
<span>{formatText(row.sign_type)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '环境 / 状态',
|
||||
key: 'status',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<div className="admin-stack admin-stack--tiny">
|
||||
<Tag color={row.status === 'active' ? 'success' : row.status === 'testing' ? 'gold' : 'default'}>
|
||||
{statusLabels[row.status] ?? row.status}
|
||||
</Tag>
|
||||
<span>{row.environment === 'production' ? '生产' : '沙箱'}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '引用 / 交易',
|
||||
key: 'usage',
|
||||
width: 170,
|
||||
align: 'right',
|
||||
render: (_, row) => (
|
||||
<div className="admin-amount">
|
||||
<strong>{row.total_transactions} 笔</strong>
|
||||
<span>引用 {row.reference_count}</span>
|
||||
<span>{formatMoney(row.total_amount_cent)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '回调地址',
|
||||
key: 'notify',
|
||||
width: 240,
|
||||
render: (_, row) => (
|
||||
<Typography.Text ellipsis={{ tooltip: row.notify_url }} style={{ maxWidth: 220 }}>
|
||||
{formatText(row.notify_url)}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '最近使用',
|
||||
dataIndex: 'last_used_at',
|
||||
width: 170,
|
||||
render: value => formatDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Space size={4} wrap>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
disabled={!canUpdate}
|
||||
onClick={() => openEdit(row)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
disabled={!canDelete}
|
||||
onClick={() => remove(row)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const applyFilters = () => {
|
||||
setFilters({
|
||||
provider: draft.provider,
|
||||
pay_way: draft.pay_way,
|
||||
status: draft.status,
|
||||
environment: draft.environment,
|
||||
})
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
setDraft({})
|
||||
setFilters({})
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<AdminPageHeader
|
||||
eyebrow="JIEYOU OPS · 支付"
|
||||
title="支付配置"
|
||||
description="维护主站支付商户与回调参数;密钥只写不读,编辑时留空表示不修改。"
|
||||
meta={
|
||||
<Space size={8} wrap>
|
||||
<Tag color="geekblue">共 {summary.total} 条</Tag>
|
||||
<Tag color="green">启用 {summary.active}</Tag>
|
||||
<Tag color="gold">默认 {summary.defaultCount}</Tag>
|
||||
{listQuery.isFetching ? <Tag color="processing">正在更新</Tag> : null}
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
{canCreate ? (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建配置
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={listQuery.isFetching}
|
||||
disabled={!canView}
|
||||
onClick={() => void listQuery.refetch()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{!canView ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="当前账号没有支付配置查看权限"
|
||||
description="需要 payment_config:list 权限。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FilterBar
|
||||
onSearch={applyFilters}
|
||||
onReset={
|
||||
Object.values(draft).some(Boolean) || Object.values(filters).some(Boolean)
|
||||
? resetFilters
|
||||
: undefined
|
||||
}
|
||||
loading={listQuery.isFetching}
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
{listQuery.isPending ? '加载中' : `当前 ${rows.length} 条`}
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<FilterField label="服务商" width={160}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.provider}
|
||||
placeholder="全部"
|
||||
options={providerOptions}
|
||||
onChange={value => setDraft(current => ({ ...current, provider: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="支付方式" width={160}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.pay_way}
|
||||
placeholder="全部"
|
||||
options={payWayOptions}
|
||||
onChange={value => setDraft(current => ({ ...current, pay_way: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="状态" width={140}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.status}
|
||||
placeholder="全部"
|
||||
options={statusOptions}
|
||||
onChange={value => setDraft(current => ({ ...current, status: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
<FilterField label="环境" width={140}>
|
||||
<Select
|
||||
allowClear
|
||||
value={draft.environment}
|
||||
placeholder="全部"
|
||||
options={[
|
||||
{ value: 'production', label: '生产' },
|
||||
{ value: 'sandbox', label: '沙箱' },
|
||||
]}
|
||||
onChange={value => setDraft(current => ({ ...current, environment: value }))}
|
||||
/>
|
||||
</FilterField>
|
||||
</FilterBar>
|
||||
|
||||
{listQuery.isError ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="支付配置没有加载出来"
|
||||
description={errorText(listQuery.error)}
|
||||
action={
|
||||
<Button size="small" onClick={() => void listQuery.refetch()}>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<DataTable<AdminPaymentConfig>
|
||||
title="主站支付配置"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
loading={listQuery.isFetching}
|
||||
total={data?.total ?? 0}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onPageChange={(nextPage, nextSize) => {
|
||||
setPage(nextPage)
|
||||
setPageSize(nextSize)
|
||||
}}
|
||||
emptyDescription={canView ? '还没有支付配置' : '当前账号没有查看权限'}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={editorOpen}
|
||||
title={editing ? `编辑支付配置 · ${editing.name}` : '新建支付配置'}
|
||||
width={820}
|
||||
maskClosable={!submitting}
|
||||
closable={!submitting}
|
||||
onCancel={() => (submitting ? undefined : setEditorOpen(false))}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={() => setEditorOpen(false)} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" loading={submitting} onClick={() => void save()}>
|
||||
保存配置
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="admin-table-alert"
|
||||
message="密钥只写不读"
|
||||
description={
|
||||
editing
|
||||
? 'SignKey / NotifyKey 留空表示保持原值;额外配置需提交完整 JSON。'
|
||||
: '乐刷/拉卡拉/顺诚需要完整密钥与回调地址;创建前请确认商户环境。'
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
</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' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select options={statusOptions} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="environment" label="环境">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'production', label: '生产' },
|
||||
{ value: 'sandbox', label: '沙箱' },
|
||||
]}
|
||||
/>
|
||||
</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":"..."}' />
|
||||
</Form.Item>
|
||||
<Typography.Paragraph type="secondary" className="admin-form-hint">
|
||||
<CreditCardOutlined /> 支付配置变更会记录审计日志;密钥不会回显。
|
||||
</Typography.Paragraph>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -46,9 +46,15 @@ import type {
|
||||
AdminOrderPage,
|
||||
AdminOrderQuery,
|
||||
AdminPaymentChannel,
|
||||
AdminPaymentConfig,
|
||||
AdminPaymentConfigOption,
|
||||
AdminPaymentConfigPage,
|
||||
AdminPaymentConfigPayload,
|
||||
AdminPaymentConfigQuery,
|
||||
AdminReceivedListingPage,
|
||||
AdminReceivedListingQuery,
|
||||
AdminSyncSiteAccountsPayload,
|
||||
AdminSyncSiteAccountsResult,
|
||||
AdminSystemConfig,
|
||||
AdminSystemConfigUpdatePayload,
|
||||
AdminUser,
|
||||
@@ -944,6 +950,53 @@ export async function fetchAdminPaymentConfigOptions() {
|
||||
return data.data.items ?? []
|
||||
}
|
||||
|
||||
export async function syncAdminDistributionSiteAccounts(
|
||||
siteId: number,
|
||||
payload: AdminSyncSiteAccountsPayload
|
||||
) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminSyncSiteAccountsResult>>(
|
||||
`/admin/distribution/sites/${siteId}/sync-accounts`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchAdminPaymentConfigs(query: AdminPaymentConfigQuery) {
|
||||
const { data } = await adminClient.get<ApiResponse<Partial<AdminPaymentConfigPage>>>(
|
||||
'/admin/payment-configs',
|
||||
{ params: compactParams(query) }
|
||||
)
|
||||
return {
|
||||
items: data.data.items ?? [],
|
||||
total: data.data.total ?? 0,
|
||||
page: data.data.page ?? query.page ?? 1,
|
||||
page_size: data.data.page_size ?? query.page_size ?? 20,
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAdminPaymentConfig(payload: AdminPaymentConfigPayload) {
|
||||
const { data } = await adminClient.post<ApiResponse<AdminPaymentConfig>>(
|
||||
'/admin/payment-configs',
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateAdminPaymentConfig(
|
||||
id: number,
|
||||
payload: Partial<AdminPaymentConfigPayload>
|
||||
) {
|
||||
const { data } = await adminClient.put<ApiResponse<AdminPaymentConfig>>(
|
||||
`/admin/payment-configs/${id}`,
|
||||
payload
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deleteAdminPaymentConfig(id: number) {
|
||||
await adminClient.delete(`/admin/payment-configs/${id}`)
|
||||
}
|
||||
|
||||
export async function fetchFinanceDisbursements(query: FinanceDisbursementQuery) {
|
||||
const { data } = await adminClient.get<ApiResponse<Partial<FinanceDisbursementPage>>>(
|
||||
'/admin/finance/disbursements',
|
||||
|
||||
@@ -1280,3 +1280,78 @@ export interface AdminDistributionAnalytics {
|
||||
total_order_user_count: number
|
||||
trend_days: AdminDistributionTrendPoint[]
|
||||
}
|
||||
|
||||
export interface AdminSyncSiteAccountsPayload {
|
||||
listing_ids?: number[]
|
||||
clean_stale_listings: boolean
|
||||
default_shelf_ratio_delta: number
|
||||
}
|
||||
|
||||
export interface AdminSyncSiteAccountsResult {
|
||||
synced_count: number
|
||||
skipped_count: number
|
||||
updated_count: number
|
||||
reenabled_count: number
|
||||
disabled_stale_count: number
|
||||
total_active: number
|
||||
}
|
||||
|
||||
/** 主站支付配置;密钥字段后端默认不返回。 */
|
||||
export interface AdminPaymentConfig {
|
||||
id: number
|
||||
name: string
|
||||
provider: string
|
||||
merchant_id: string
|
||||
gateway_url: string
|
||||
notify_url: string
|
||||
jump_url: string
|
||||
pay_way: string
|
||||
jspay_flag: string
|
||||
sign_type: string
|
||||
extra_config?: Record<string, unknown>
|
||||
is_default: boolean
|
||||
status: string
|
||||
environment: string
|
||||
business_tags: string[]
|
||||
reference_count: number
|
||||
total_transactions: number
|
||||
total_amount_cent: number
|
||||
last_used_at?: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AdminPaymentConfigPage {
|
||||
items: AdminPaymentConfig[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface AdminPaymentConfigQuery {
|
||||
provider?: string
|
||||
pay_way?: string
|
||||
status?: string
|
||||
environment?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface AdminPaymentConfigPayload {
|
||||
name: string
|
||||
provider: string
|
||||
merchant_id: 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
|
||||
extra_config?: Record<string, unknown>
|
||||
is_default?: boolean
|
||||
status?: string
|
||||
environment?: string
|
||||
business_tags?: string[]
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ const allNavGroups: NavGroup[] = [
|
||||
icon: <SettingOutlined />,
|
||||
children: [
|
||||
{ key: '/system-configs', label: '系统配置', permission: 'system_config:view' },
|
||||
{ key: '/payment-configs', label: '支付配置', permission: 'payment_config:list' },
|
||||
{ key: '/audit-logs', label: '审计日志', permission: 'audit_log:view' },
|
||||
],
|
||||
},
|
||||
@@ -168,6 +169,7 @@ const sectionMeta: Record<string, { title: string; description: string }> = {
|
||||
'/distribution/invitation': { title: '邀请返利', description: '配置分站邀请好友的返利比例。' },
|
||||
'/announcements': { title: '公告管理', description: '维护主站与分站公告。' },
|
||||
'/system-configs': { title: '系统配置', description: '维护首页、发布、订单与支付配置。' },
|
||||
'/payment-configs': { title: '支付配置', description: '维护主站支付商户、渠道与回调参数。' },
|
||||
'/audit-logs': { title: '审计日志', description: '查看高风险操作与变更记录。' },
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,12 @@ export default defineConfig({
|
||||
if (id.includes('@tanstack') || id.includes('/zustand/') || id.includes('/axios/')) {
|
||||
return 'vendor-data'
|
||||
}
|
||||
if (id.includes('/@ant-design/icons/')) {
|
||||
return 'vendor-antd-icons'
|
||||
}
|
||||
if (id.includes('/dayjs/')) {
|
||||
return 'vendor-date'
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user