task-283(后台迁移收尾/发布): admin-vue 后台工程入库 + Flask 后台整体退役
- 新后台 admin-frontend-vue 整工程入库(base=/admin-vue/、History 路由、Nginx 静态托管方案与 verify-dist 校验) - Java AdminConsoleController 改为入口重定向: /admin|/admin.html、/login|/login.html -> /admin-vue/; 删除 classpath:static 旧 admin.html/login.html 单页与 admin.js 副本 - Flask 后台整体退役: 删除 admin_api/auth/main 蓝图、web_source 页面、static 脚本与 admin 相关测试; app.py 收敛为仅注册 version_bp(/api/version、/api/version/latest, 供桌面端更新检查) - AdminApiGuardFilterTest 豁免样例路径 /admin.html -> /admin-vue/
This commit is contained in:
@@ -2,20 +2,21 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readSource } from './helpers.ts'
|
||||
|
||||
/** 对齐 admin.html:5298-5300(店铺密钥 序号/紫鸟账号名称) 与 5339-5348(店铺管理列序与列名)。 */
|
||||
/** 验收反馈:列表不再展示"序号/ID"列。对齐 admin.html:5298-5300(紫鸟账号名称) 与 5339-5348(店铺管理列序与列名)。 */
|
||||
|
||||
test('align_shop_keys_columns', () => {
|
||||
const page = readSource('src/pages/shop/ShopKeysPage.vue')
|
||||
assert.match(page, /label="序号"/, '首列改序号')
|
||||
assert.match(page, /type="index"/, '序号列按行号生成')
|
||||
assert.doesNotMatch(page, /label="序号"/, '不再展示序号列')
|
||||
assert.doesNotMatch(page, /type="index"/, '不用行号列')
|
||||
assert.doesNotMatch(page, /label="ID"/, '不展示 ID 列')
|
||||
assert.match(page, /label="备注名"/, '首业务列按备注名起')
|
||||
assert.match(page, /紫鸟账号名称/, '列名对齐「紫鸟账号名称」')
|
||||
assert.doesNotMatch(page, /label="ID"/, '去掉 ID 列')
|
||||
})
|
||||
|
||||
test('align_shop_manage_columns_order_and_labels', () => {
|
||||
const page = readSource('src/pages/shop/ShopManagePage.vue')
|
||||
assert.match(page, /label="序号"/, '首列改序号')
|
||||
assert.doesNotMatch(page, /label="ID"/, '去掉 ID 列')
|
||||
assert.doesNotMatch(page, /label="序号"/, '不再展示序号列')
|
||||
assert.doesNotMatch(page, /label="ID"/, '不展示 ID 列')
|
||||
// 列序:分组 → 店铺名 → 店铺商城名 → 自动化账号 → 账号 → 密码(对齐 admin.html:5339-5348)。
|
||||
const table = page.slice(page.indexOf('<el-table'))
|
||||
const orderMatch = /label="分组"[\s\S]*?label="店铺名"[\s\S]*?label="店铺商城名"[\s\S]*?label="自动化账号"[\s\S]*?label="账号"[\s\S]*?label="密码"/.exec(table)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { parseDuplicateConsole, parseLedgerPage, parseConsoleGroup, parseLedgerRow } from '../src/pages/tasks/duplicate-console-model.ts'
|
||||
import {
|
||||
indexShopGroups,
|
||||
dupWithinGroup,
|
||||
storeColumnsOf,
|
||||
} from '../src/pages/tasks/duplicate-console-logic.ts'
|
||||
|
||||
function occurrence(asin: string, shop: string, group: string, country: string, date: string) {
|
||||
return { asin, date, price: '', brand: 'B', shop_name: shop, group_name: group, country_codes: [country], country }
|
||||
}
|
||||
|
||||
/** 已解析模型形态(camel)的 occurrence,供纯逻辑派生入参。 */
|
||||
function camelOcc(asin: string, shop: string, group: string, country: string, date: string) {
|
||||
return { asin, date, price: '', brand: 'B', shopName: shop, groupName: group, countryCodes: [country], country }
|
||||
}
|
||||
|
||||
test('test_dup_console_model_parses_overview_dup_and_groups', () => {
|
||||
const consoleData = parseDuplicateConsole({
|
||||
success: true,
|
||||
data: {
|
||||
pending: false,
|
||||
scanned_at: '2026-09-04 03:10:00',
|
||||
summary: { shop_count: 4, asin_total: 6, record_total: 10, duplicate_asin_total: 3, duplicate_shop_count: 4, site_count: 3, asin_per_shop: 2.5, source: 'job' },
|
||||
shops: [
|
||||
{ shop_name: 'ShopA', group_name: 'GroupA', country_codes: ['UK', 'DE'], asin_count: 3, record_count: 3 },
|
||||
{ shop_name: 'ShopB', group_name: 'GroupA', country_codes: ['UK'], asin_count: 3, record_count: 3 },
|
||||
{ shop_name: 'ShopC', group_name: 'GroupC', country_codes: ['FR'], asin_count: 2, record_count: 2 },
|
||||
],
|
||||
dup: [
|
||||
{ asin: 'E0000001', shop_count: 3, record_count: 3, occurrences: [
|
||||
occurrence('E0000001', 'ShopA', 'GroupA', 'UK', '2026-08-29'),
|
||||
occurrence('E0000001', 'ShopB', 'GroupA', 'UK', '2026-08-30'),
|
||||
occurrence('E0000001', 'ShopC', 'GroupC', 'FR', '2026-08-31'),
|
||||
] },
|
||||
],
|
||||
groups: [{ name: 'GroupA', shop_count: 2, asin_unique: 4, record_count: 6, dup_count: 2 }],
|
||||
total_dup: 1,
|
||||
},
|
||||
})
|
||||
assert.equal(consoleData.overview.pending, false)
|
||||
assert.equal(consoleData.overview.summary?.asinTotal, 6)
|
||||
assert.equal(consoleData.overview.shops.length, 3)
|
||||
assert.equal(consoleData.totalDup, 1)
|
||||
assert.equal(consoleData.dup[0].asin, 'E0000001')
|
||||
assert.equal(consoleData.dup[0].occurrences.length, 3)
|
||||
const group = consoleData.groups[0]
|
||||
assert.deepEqual(group, { name: 'GroupA', shopCount: 2, asinUnique: 4, recordCount: 6, dupCount: 2 })
|
||||
})
|
||||
|
||||
test('test_dup_console_parse_group_ignores_missing_name', () => {
|
||||
assert.equal(parseConsoleGroup({ shop_count: 1 }), null)
|
||||
assert.ok(parseConsoleGroup({ name: 'G', shop_count: 2 }))
|
||||
})
|
||||
|
||||
test('test_dup_console_group_derivation_restricts_to_members', () => {
|
||||
const dupA = { asin: 'A', shopCount: 2, recordCount: 2, occurrences: [
|
||||
camelOcc('A', 'ShopA', 'GroupA', 'UK', '2026-08-01'),
|
||||
camelOcc('A', 'ShopB', 'GroupA', 'UK', '2026-08-02'),
|
||||
] }
|
||||
const dupE = { asin: 'E', shopCount: 3, recordCount: 3, occurrences: [
|
||||
camelOcc('E', 'ShopA', 'GroupA', 'UK', '2026-08-01'),
|
||||
camelOcc('E', 'ShopB', 'GroupA', 'UK', '2026-08-02'),
|
||||
camelOcc('E', 'ShopC', 'GroupC', 'FR', '2026-08-03'),
|
||||
] }
|
||||
const groups = indexShopGroups([
|
||||
{ shopName: 'ShopA', groupName: 'GroupA', countryCodes: [], asinCount: 0, recordCount: 0 },
|
||||
{ shopName: 'ShopB', groupName: 'GroupA', countryCodes: [], asinCount: 0, recordCount: 0 },
|
||||
{ shopName: 'ShopC', groupName: 'GroupC', countryCodes: [], asinCount: 0, recordCount: 0 },
|
||||
])
|
||||
assert.deepEqual(groups.members.GroupA, ['ShopA', 'ShopB'])
|
||||
assert.deepEqual(groups.shopGroupNames.ShopC, ['GroupC'])
|
||||
|
||||
const inGroupA = dupWithinGroup([dupA, dupE], groups.members.GroupA)
|
||||
assert.equal(inGroupA.length, 2)
|
||||
// E 组内只剩 ShopA/ShopB 两条记录。
|
||||
const e = inGroupA.find((item) => item.asin === 'E')
|
||||
assert.equal(e?.shopCount, 2)
|
||||
assert.equal(e?.recordCount, 2)
|
||||
// 单店成员组内不会命中(ShopD 不在数据里)。
|
||||
assert.deepEqual(dupWithinGroup([dupA, dupE], ['ShopD']), [])
|
||||
})
|
||||
|
||||
test('test_dup_console_store_columns_order_and_restriction', () => {
|
||||
const dupA = { asin: 'A', shopCount: 2, recordCount: 2, occurrences: [
|
||||
camelOcc('A', 'ShopB', 'GroupA', 'UK', '2026-08-01'),
|
||||
camelOcc('A', 'ShopA', 'GroupA', 'UK', '2026-08-02'),
|
||||
] }
|
||||
assert.deepEqual(storeColumnsOf([dupA]), ['ShopB', 'ShopA'])
|
||||
assert.deepEqual(storeColumnsOf([dupA], ['ShopA', 'ShopB', 'ShopC']), ['ShopB', 'ShopA'])
|
||||
// 空行 + 显式成员列:仍输出成员店铺。
|
||||
assert.deepEqual(storeColumnsOf([], ['ShopA', 'ShopB']), ['ShopA', 'ShopB'])
|
||||
})
|
||||
|
||||
test('test_dup_console_ledger_parse_rows_and_paging', () => {
|
||||
const page = parseLedgerPage({
|
||||
success: true,
|
||||
data: {
|
||||
pending: false,
|
||||
scanned_at: '2026-09-04 03:10:00',
|
||||
items: [
|
||||
{ asin: 'E0000001', brand: 'BrandE', store_count: 3, stores: ['ShopA', 'ShopB', 'ShopC'], groups: ['GroupA', 'GroupC'], countries: ['UK', 'FR'], record_count: 3, earliest: '2026-08-29 00:00:00', latest: '2026-08-31 00:00:00', occurrences: [occurrence('E0000001', 'ShopA', 'GroupA', 'UK', '2026-08-29')] },
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
},
|
||||
})
|
||||
assert.equal(page.pending, false)
|
||||
assert.equal(page.total, 1)
|
||||
assert.equal(page.pageSize, 200)
|
||||
const row = page.items[0]
|
||||
assert.equal(row.asin, 'E0000001')
|
||||
assert.equal(row.storeCount, 3)
|
||||
assert.equal(row.occurrences.length, 1)
|
||||
const single = parseLedgerRow({ asin: 'X0000001', store_count: 0, stores: [], groups: [], countries: [], record_count: 0, earliest: '', latest: '', occurrences: [] })
|
||||
assert.equal(single?.storeCount, 0)
|
||||
assert.equal(parseLedgerRow({}), null)
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
const BASE = process.env.AIIMAGE_LIVE_BASE || 'http://127.0.0.1:18080'
|
||||
|
||||
test('test_task_180_public_version_live_reachability', async () => {
|
||||
// 公开版本路径可达。
|
||||
const res = await fetch(`${BASE}/api/version`)
|
||||
assert.equal(res.status, 200)
|
||||
const body = (await res.json()) as Record<string, unknown>
|
||||
assert.ok('version' in body)
|
||||
assert.ok('desc' in body)
|
||||
assert.ok('url' in body)
|
||||
})
|
||||
|
||||
test('test_task_180_public_version_live_latest_contract', async () => {
|
||||
// latest 契约含 version/file_url(允许 null)。
|
||||
const res = await fetch(`${BASE}/api/version/latest`)
|
||||
assert.equal(res.status, 200)
|
||||
const body = (await res.json()) as Record<string, unknown>
|
||||
assert.ok('version' in body)
|
||||
assert.ok('file_url' in body)
|
||||
})
|
||||
|
||||
test('test_task_180_public_version_live_no_envelope', async () => {
|
||||
// 公开接口无 success 包装。
|
||||
const body = (await (await fetch(`${BASE}/api/version`)).json()) as Record<string, unknown>
|
||||
assert.equal('success' in body, false, '公开接口不包装 ApiResponse')
|
||||
assert.equal('data' in body, false)
|
||||
})
|
||||
|
||||
test('test_task_180_public_version_live_values_type', async () => {
|
||||
const body = (await (await fetch(`${BASE}/api/version`)).json()) as Record<string, unknown>
|
||||
for (const key of ['version', 'desc', 'url']) {
|
||||
const v = body[key]
|
||||
assert.ok(v === null || typeof v === 'string' || typeof v === 'number', key)
|
||||
}
|
||||
})
|
||||
|
||||
test('test_task_180_public_version_live_latest_keys_only', async () => {
|
||||
const body = (await (await fetch(`${BASE}/api/version/latest`)).json()) as Record<string, unknown>
|
||||
const keys = Object.keys(body).sort()
|
||||
assert.deepEqual(keys, ['file_url', 'version'], 'latest 仅含两个字段')
|
||||
})
|
||||
|
||||
test('test_task_180_public_version_live_download_url_preserved', async () => {
|
||||
const body = (await (await fetch(`${BASE}/api/version/latest`)).json()) as Record<string, unknown>
|
||||
if (body.file_url) assert.match(String(body.file_url), /^https?:\/\//)
|
||||
})
|
||||
@@ -13,7 +13,7 @@ import { adminPages } from '../src/router/routes.ts'
|
||||
|
||||
test('test_task_010_lazy_page_boundary_normal_primary_path', () => {
|
||||
// 正常主路径:所有注册页面都是懒加载器,可被路由异步边界包裹。
|
||||
assert.equal(adminPages.length, 3)
|
||||
assert.equal(adminPages.length, 16)
|
||||
for (const page of adminPages) {
|
||||
assert.equal(isLazyLoader(page.load), true, `页面 ${page.path} 必须是懒加载函数`)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ test('test_task_012_route_error_page_normal_repeated_operation_is_idempotent', (
|
||||
test('test_task_012_route_error_page_boundary_empty_input', () => {
|
||||
// 边界空值:错误页文件存在,且不进入业务路由注册表。
|
||||
assert.equal(existsSync(join(process.cwd(), NOT_FOUND)), true)
|
||||
assert.equal(adminPages.length, 3, '错误页不应计入业务路由')
|
||||
assert.equal(adminPages.length, 16, '错误页不应计入业务路由')
|
||||
})
|
||||
|
||||
test('test_task_012_route_error_page_boundary_single_item', () => {
|
||||
|
||||
@@ -75,6 +75,6 @@ test('test_task_016_shell_logout_interaction_dependency_failure_returns_actionab
|
||||
assert.match(layout, /runLogout/)
|
||||
const store = readSource('src/stores/admin-session.ts')
|
||||
assert.match(store, /\$reset\(\)/)
|
||||
assert.match(store, /location\.assign\('\/login'\)/)
|
||||
assert.match(store, /location\.assign\(joinAdminPath\('login'\)\)/)
|
||||
assert.match(layout, /LOGOUT_CONFIRM_MESSAGE/)
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ const store = readSource('src/stores/admin-session.ts')
|
||||
|
||||
test('test_task_029_menu_tree_init_normal_primary_path', () => {
|
||||
// 正常主路径:用户拉取成功后拉取菜单树并写入 store.menuTree。
|
||||
assert.match(store, /this\.menuTree\s*=\s*await fetchAdminMenuTree\(\)/)
|
||||
assert.match(store, /this\.menuTree\s*=\s*canonicalizeMenuTree\(await fetchAdminMenuTree\(\)\)/)
|
||||
})
|
||||
|
||||
test('test_task_029_menu_tree_init_normal_variant_input', () => {
|
||||
|
||||
@@ -59,7 +59,7 @@ test('test_task_036_logout_clear_state_dependency_failure_returns_actionable_mes
|
||||
const store = readSource('src/stores/admin-session.ts')
|
||||
assert.match(store, /async signOut\(\)/)
|
||||
assert.match(store, /\$reset\(\)/)
|
||||
assert.match(store, /location\.assign\('\/login'\)/)
|
||||
assert.match(store, /location\.assign\(joinAdminPath\('login'\)\)/)
|
||||
const session = readSource('src/api/session.ts')
|
||||
assert.match(session, /\/logout/, '登出走 auth 模块根端点 POST /logout')
|
||||
assert.equal(session.includes('/api/admin/logout'), false, '不应指向不存在的 /api/admin/logout')
|
||||
|
||||
@@ -12,7 +12,7 @@ test('test_task_005_topbar_title_model_normal_primary_path', () => {
|
||||
assert.equal(pageTitleOf('用户管理'), '用户管理')
|
||||
const vm = topbarUserOf({ id: 1, username: 'admin', role: 'super_admin' })
|
||||
assert.equal(vm.username, 'admin')
|
||||
assert.equal(vm.role, 'super_admin')
|
||||
assert.equal(vm.role, '超级管理员')
|
||||
assert.equal(vm.hasUser, true)
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ test('test_task_005_topbar_title_model_boundary_single_item', () => {
|
||||
assert.equal(pageTitleOf('首页'), '首页')
|
||||
const single = topbarUserOf({ id: 9, username: 'root', role: 'admin' })
|
||||
assert.equal(single.username, 'root')
|
||||
assert.equal(single.role, 'admin')
|
||||
assert.equal(single.role, '管理员')
|
||||
})
|
||||
|
||||
test('test_task_005_topbar_title_model_boundary_limit_or_missing_field', () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
test('test_task_008_domain_route_registry_normal_primary_path', () => {
|
||||
// 正常主路径:注册表首批 account 域页面登记为可消费路由记录。
|
||||
assert.equal(adminPages.length, 3)
|
||||
assert.equal(adminPages.length, 16)
|
||||
assert.equal(adminRouteRecords.length, adminPages.length)
|
||||
const first = adminRouteRecords[0]
|
||||
assert.equal(first.path, 'account/users')
|
||||
|
||||
Reference in New Issue
Block a user