feat(#78): add manual SYB sync action

This commit is contained in:
QiuSW
2026-08-24 17:05:16 +08:00
parent 3261561940
commit 5c9b75fc53
3 changed files with 71 additions and 7 deletions
+3 -3
View File
@@ -2,8 +2,8 @@
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
wiki_page: Business-Rules-and-Glossary
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
wiki_revision: 8e5668f30853e7afa2c03186ff882458a5d15d9c
synchronized_at: 2026-08-24T08:45:50Z
wiki_revision: 3d2a64c6638229ff95a8ac5a5680bb41f0dcb91d
synchronized_at: 2026-08-24T09:01:14Z
<!-- gitea-wiki-mirror:end -->
# 业务规则与术语
@@ -191,7 +191,7 @@ synchronized_at: 2026-08-24T08:45:50Z
- 同一时刻只允许一个 SYB 同步任务运行。上一次仍在运行时,本次定时触发直接跳过,不排队、不并发,也不立即重试;等待下一小时再次触发。
- 定时任务失败时明确记录失败原因,不自动重试。服务重启后由既有启动恢复逻辑处理遗留的运行中记录。
- SYB 商品页不再提供“导入”和“同步记录”快捷按钮,也不查询、展示或轮询同步状态;后台同步成功、失败或中断均不在商品页弹出消息,商品数据由用户主动查询或刷新获取。
- 独立“SYB 同步记录”页面是同步结果的唯一 Web 展示位置,保留运行状态、进度、数量、失败原因和店铺统计;go-admin 定时任务中的 `GoAutoSYBHourlySync` 提供“执行记录”入口。手动导入 API 继续保留。
- 独立“SYB 同步记录”页面是同步结果的唯一 Web 展示位置,保留运行状态、进度、数量、失败原因和店铺统计;go-admin 定时任务中的 `GoAutoSYBHourlySync` 提供“执行记录”入口。管理员可在同步记录页人工发起覆盖昨天和今天的同步,采购员只读;人工与定时同步共用导入服务、执行记录和互斥,不排队、不并发,也不立即重试。
## cmautobuy 商品导入
+32 -3
View File
@@ -3,7 +3,14 @@
<template #wrapper>
<el-card class="page-card" shadow="never">
<div class="page-heading">
<div><h1>SYB 同步记录</h1><p>查看每次 SYB 同步的进度、结果和按店铺统计。自动同步失败后,系统会在下个调度周期再次尝试。</p></div>
<div><h1>SYB 同步记录</h1><p>查看每次 SYB 同步的进度、结果和按店铺统计。管理员可以立即同步今天和昨天的数据。</p></div>
<el-button
v-if="isAdmin"
type="primary"
:loading="manualSyncStarting"
:disabled="manualSyncStarting || hasRunningSync"
@click="startManualSync"
>{{ hasRunningSync ? '同步进行中' : '立即同步' }}</el-button>
</div>
<el-form :model="query" :inline="true" class="search-form" @submit.prevent="search">
<el-form-item label="状态"><el-select v-model="query.status" clearable placeholder="全部状态" style="width:150px"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
@@ -61,14 +68,15 @@
<script>
import { Search, RefreshLeft } from '@element-plus/icons-vue'
import { listSybSyncRuns, getSybSyncRun } from '@/api/goauto/syb-products'
import { ElMessage } from 'element-plus'
import { listSybSyncRuns, getSybSyncRun, importSybProducts } from '@/api/goauto/syb-products'
export default {
name: 'GoAutoSybSyncRuns',
setup() { return { Search, RefreshLeft } },
data() {
return {
loading: false, loadError: '', items: [], total: 0, dateRange: null, pollTimer: null,
loading: false, loadError: '', items: [], total: 0, dateRange: null, pollTimer: null, manualSyncStarting: false,
query: { page: 1, pageSize: 20, status: '', dateFrom: '', dateTo: '' },
detail: { open: false, loading: false, item: null },
statusOptions: [
@@ -77,6 +85,10 @@ export default {
]
}
},
computed: {
isAdmin() { return (this.$store.getters.roles || []).includes('admin') },
hasRunningSync() { return this.items.some(item => item.status === 'running') }
},
created() { this.load().then(() => { const id = Number(this.$route.query.runId); if (id > 0) this.openDetail(id) }) },
beforeUnmount() { this.stopPolling() },
methods: {
@@ -92,6 +104,23 @@ export default {
},
search() { this.query.page = 1; [this.query.dateFrom, this.query.dateTo] = this.dateRange || ['', '']; this.load() },
reset() { this.dateRange = null; this.query = { page: 1, pageSize: 20, status: '', dateFrom: '', dateTo: '' }; this.load() },
formatLocalDate(value) {
const pad = number => String(number).padStart(2, '0')
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`
},
async startManualSync() {
if (this.manualSyncStarting || this.hasRunningSync) return
const today = new Date()
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
this.manualSyncStarting = true
try {
await importSybProducts({ dateFrom: this.formatLocalDate(yesterday), dateTo: this.formatLocalDate(today) })
ElMessage.success('已开始同步今天和昨天')
this.dateRange = null
this.query = { page: 1, pageSize: 20, status: '', dateFrom: '', dateTo: '' }
await this.load()
} finally { this.manualSyncStarting = false }
},
async openDetail(id) { this.detail = { open: true, loading: true, item: null }; try { const r = await getSybSyncRun(id); this.detail.item = r.data.item } finally { this.detail.loading = false } },
startPolling() { if (!this.pollTimer) this.pollTimer = window.setInterval(() => { this.load(); if (this.detail.open && this.detail.item?.status === 'running') this.openDetail(this.detail.item.id) }, 3000) },
stopPolling() { if (this.pollTimer) { window.clearInterval(this.pollTimer); this.pollTimer = null } }
+36 -1
View File
@@ -17,7 +17,7 @@ test('同步记录展示失败原因和按店铺统计', async ({ page, context
await page.goto('http://localhost:9527/#/syb-sync-runs/index');
await expect(page.getByRole('heading', { name: 'SYB 同步记录' })).toBeVisible();
await expect(page.getByText('自动同步失败后,系统会在下个调度周期再次尝试。', { exact: false })).toBeVisible();
await expect(page.getByRole('button', { name: '立即同步' })).toHaveCount(0);
await expect(page.getByText('2026-08-18 至 2026-08-20')).toBeVisible();
await page.getByRole('button', { name: '详情' }).click();
await expect(page.getByText('第 3 天读取失败,等待下个调度周期重试')).toBeVisible();
@@ -26,6 +26,41 @@ test('同步记录展示失败原因和按店铺统计', async ({ page, context
await expect(page.getByText('系统定时同步', { exact: true }).first()).toBeVisible();
});
test('管理员可立即同步今天和昨天,受理后按钮进入运行中状态', async ({ page, context }) => {
await authenticate(context);
let importBody: any = null;
let importAccepted = false;
await page.route('**/api/**', async route => {
const url = new URL(route.request().url());
if (url.pathname.startsWith('/src/api/')) return route.continue();
if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] } } });
if (url.pathname.endsWith('/api/admin/v1/syb-products/import') && route.request().method() === 'POST') {
importBody = route.request().postDataJSON();
await new Promise(resolve => setTimeout(resolve, 200));
importAccepted = true;
return route.fulfill({ status: 202, json: { code: 200, data: { runId: 88, status: 'running' } } });
}
if (url.pathname.endsWith('/api/admin/v1/syb-products/sync-runs')) {
const items = importAccepted ? [{ id: 88, dateFrom: importBody.dateFrom, dateTo: importBody.dateTo, status: 'running', progressPercent: 0, daysProcessed: 0, daysTotal: 2, orderCount: 0, detailCount: 0, created: 0, updated: 0, operatorName: '管理员', startedAt: '2026-08-24T08:00:00Z' }] : [];
return route.fulfill({ json: { code: 200, data: { items, total: items.length, page: 1, pageSize: 20 } } });
}
return route.fulfill({ json: { code: 200, data: [] } });
});
await page.goto('http://localhost:9527/#/syb-sync-runs/index');
const button = page.getByRole('button', { name: '立即同步' });
await expect(button).toBeVisible();
await button.click();
await expect(button).toBeDisabled();
await expect(page.getByText('已开始同步今天和昨天')).toBeVisible();
await expect(page.getByRole('button', { name: '同步进行中' })).toBeDisabled();
const now = new Date();
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const formatDate = (value: Date) => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
expect(importBody).toEqual({ dateFrom: formatDate(yesterday), dateTo: formatDate(now) });
});
test('SYB 定时任务可进入执行记录,其他任务不显示该入口', async ({ page, context }) => {
await authenticate(context);
await page.route('**/api/**', async route => {