feat(#78): add manual SYB sync action
This commit is contained in:
@@ -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 商品导入
|
||||
|
||||
@@ -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 } }
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user