feat(distribution): 优化一键同步主站商品功能(并发防重、分批入库、失效商品联动下架与初始加价)

- 后端优化:
  - 引入 Redis + 内存 sync.Map 双重互斥锁,避免并发/连击导致唯一索引冲突 (ErrSyncInProgress)
  - 使用 CreateInBatches(500) 进行大数据量批量插入,杜绝超长 SQL 与锁表超时
  - 增加 CleanStaleListings 支持,自动排查并联动下架主站非在售的货架商品,清理历史幽灵数据
  - 增加 DefaultShelfRatioDelta 支持,新同步商品直接继承初始加价比例,免除二次手动改价
- 前端优化:
  - distribution-admin 货架管理新增一键同步配置弹窗(可勾选自动下架失效商品、配置初始加价)
  - 同步完成后展示结构化统计弹窗(新增、保持、联动下架、主站在售总量)
  - 前后端类型定义与 API 客户端对齐升级
- 测试覆盖:
  - 补充 TestSyncSiteAccountsOptimizations 单元测试与 handler 集成测试
  - 全工程测试与门禁 check.sh 100% 通过
This commit is contained in:
yml2213
2026-09-11 12:47:59 +08:00
parent ef91b24bf2
commit 9ae6d5eaea
11 changed files with 363 additions and 38 deletions
+10 -2
View File
@@ -222,9 +222,17 @@ export interface AccountDisplay {
source_channel?: string
}
export interface SyncSiteAccountsRequest {
listing_ids?: number[]
clean_stale_listings?: boolean
default_shelf_ratio_delta?: number
}
export interface SyncSiteAccountsResult {
synced_count: number
skipped_count: number
updated_count?: number
disabled_stale_count?: number
total_active: number
}
@@ -405,8 +413,8 @@ export async function updateAccountDisplay(id: number, displayStatus: string): P
return extract(resp)
}
export async function syncSiteAccounts(siteID: number): Promise<SyncSiteAccountsResult> {
const resp = await api.post<ApiResponse<SyncSiteAccountsResult>>(`/admin/distribution/sites/${siteID}/sync-accounts`)
export async function syncSiteAccounts(siteID: number, req?: SyncSiteAccountsRequest): Promise<SyncSiteAccountsResult> {
const resp = await api.post<ApiResponse<SyncSiteAccountsResult>>(`/admin/distribution/sites/${siteID}/sync-accounts`, req ?? {})
return extract(resp)
}
+155 -8
View File
@@ -18,6 +18,8 @@ import {
Radio,
Row,
Col,
Checkbox,
Statistic,
} from 'antd'
import {
SyncOutlined,
@@ -42,6 +44,7 @@ import {
getMe,
type AccountDisplay,
type Site,
type SyncSiteAccountsResult,
} from '../api'
import { useMember } from '../MemberContext'
@@ -199,18 +202,32 @@ export default function Accounts() {
void init()
}, [member])
async function handleSync() {
// 同步主站商品配置与结果弹窗状态
const [syncModalVisible, setSyncModalVisible] = useState(false)
const [syncCleanStale, setSyncCleanStale] = useState(true)
const [syncDefaultRatioDelta, setSyncDefaultRatioDelta] = useState<number>(0)
const [syncResultModalVisible, setSyncResultModalVisible] = useState(false)
const [syncResult, setSyncResult] = useState<SyncSiteAccountsResult | null>(null)
function handleOpenSyncModal() {
if (!siteID) {
message.warning('无法获取当前分销站点信息')
return
}
setSyncModalVisible(true)
}
async function handleExecuteSync() {
if (!siteID) return
setSyncing(true)
try {
const res = await syncSiteAccounts(siteID)
message.success(
`同步完成!成功同步新增商品 ${res.synced_count} 个,已在货架 ${res.skipped_count} 个(主站共享池在售共 ${res.total_active} 个)`,
5,
)
const res = await syncSiteAccounts(siteID, {
clean_stale_listings: syncCleanStale,
default_shelf_ratio_delta: Math.max(0, syncDefaultRatioDelta),
})
setSyncResult(res)
setSyncModalVisible(false)
setSyncResultModalVisible(true)
await load(siteID)
} catch (error: unknown) {
const respMessage = (error as { response?: { data?: { message?: string } } })?.response?.data?.message
@@ -578,7 +595,7 @@ export default function Accounts() {
type="primary"
icon={<SyncOutlined spin={syncing} />}
loading={syncing}
onClick={handleSync}
onClick={handleOpenSyncModal}
style={{ background: '#1677ff' }}
>
一键同步主站商品
@@ -728,7 +745,7 @@ export default function Accounts() {
type="primary"
icon={<SyncOutlined spin={syncing} />}
loading={syncing}
onClick={handleSync}
onClick={handleOpenSyncModal}
>
一键同步主站商品
</Button>
@@ -1243,6 +1260,136 @@ export default function Accounts() {
)}
</Space>
</Modal>
{/* 同步配置弹窗 */}
<Modal
title={
<Space>
<SyncOutlined style={{ color: '#1677ff' }} />
<span>一键同步主站商品设置</span>
</Space>
}
open={syncModalVisible}
onCancel={() => !syncing && setSyncModalVisible(false)}
onOk={handleExecuteSync}
confirmLoading={syncing}
okText="立即开始同步"
cancelText="取消"
width={540}
>
<Space direction="vertical" size="middle" style={{ width: '100%', marginTop: 12 }}>
<Alert
type="info"
showIcon
message="货架铺货说明"
description="系统将自动拉取主站及全网审核通过的在售精品账号,极速批量补充到本分站货架,已在货架的商品不会重复生成。"
/>
<Card size="small" style={{ background: '#f8fafc', borderColor: '#e2e8f0' }}>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div>
<Checkbox
checked={syncCleanStale}
onChange={(e) => setSyncCleanStale(e.target.checked)}
>
<Typography.Text strong>自动下架主站已失效/已出租商品</Typography.Text>
</Checkbox>
<div style={{ paddingLeft: 24, fontSize: 12, color: '#64748b', marginTop: 4 }}>
推荐勾选。同步时自动将主站已下架、已出租或被删除的商品标记为已下架,避免历史幽灵商品滞留货架。
</div>
</div>
<Divider style={{ margin: '8px 0' }} />
<div>
<Typography.Text strong>新同步商品初始加价比例(下浮比例点数)</Typography.Text>
<div style={{ marginTop: 6, display: 'flex', alignItems: 'center', gap: 12 }}>
<InputNumber
min={0}
max={50}
step={0.5}
value={syncDefaultRatioDelta}
onChange={(val) => setSyncDefaultRatioDelta(val ?? 0)}
style={{ width: 160 }}
addonAfter="点比例"
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{syncDefaultRatioDelta === 0
? '保持主站原价上架'
: `预计标价上浮 ${(syncDefaultRatioDelta * 1.1).toFixed(1)}% 左右,溢价利润 100% 归本站`}
</Typography.Text>
</div>
<div style={{ fontSize: 12, color: '#94a3b8', marginTop: 6 }}>
仅对本次<strong>新增入货架</strong>的商品生效,已在货架的商品保持其原有加价不变。
</div>
</div>
</Space>
</Card>
</Space>
</Modal>
{/* 同步结果展示弹窗 */}
<Modal
title={
<Space>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<span>主站商品同步完成</span>
</Space>
}
open={syncResultModalVisible}
onCancel={() => setSyncResultModalVisible(false)}
footer={[
<Button key="ok" type="primary" onClick={() => setSyncResultModalVisible(false)}>
我知道了
</Button>,
]}
width={500}
>
<div style={{ padding: '16px 0' }}>
<Row gutter={[16, 16]}>
<Col span={12}>
<Card size="small" style={{ background: '#f6ffed', borderColor: '#b7eb8f', textAlign: 'center' }}>
<Statistic
title="新增上架商品"
value={syncResult?.synced_count ?? 0}
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
suffix="个"
/>
</Card>
</Col>
<Col span={12}>
<Card size="small" style={{ background: '#f0f5ff', borderColor: '#adc6ff', textAlign: 'center' }}>
<Statistic
title="已在货架保持"
value={syncResult?.skipped_count ?? 0}
valueStyle={{ color: '#1677ff', fontWeight: 'bold' }}
suffix="个"
/>
</Card>
</Col>
<Col span={12}>
<Card size="small" style={{ background: '#fffbe6', borderColor: '#ffe58f', textAlign: 'center' }}>
<Statistic
title="联动下架失效商品"
value={syncResult?.disabled_stale_count ?? 0}
valueStyle={{ color: '#faad14', fontWeight: 'bold' }}
suffix="个"
/>
</Card>
</Col>
<Col span={12}>
<Card size="small" style={{ background: '#fafafa', borderColor: '#d9d9d9', textAlign: 'center' }}>
<Statistic
title="主站共享池在售"
value={syncResult?.total_active ?? 0}
valueStyle={{ color: '#595959', fontWeight: 'bold' }}
suffix="个"
/>
</Card>
</Col>
</Row>
</div>
</Modal>
</Space>
)
}