fix(product-family): auto-merge SKUs after manual tag edits; fix session cookie path
问题2(SKU 无法自动合并,GBIU017 案例)四处叠加根因修复: - 人工标签旧词「热转印」被封闭词表静默踢出合并矩阵 → CRAFT_TAG_ALIASES 别名归一为「烫画」(矩阵归因与 CUSTOM 标签同路径生效) - updateTags 缺任一定价维度组即整链掉出矩阵且人工接管后永不恢复 → 缺啥补啥(按链接名派生补齐,人工勾选值不动,响应带 filledDimensionTags) - autoGroup 只建新族从不并入已有族(产生 USIU005-2 类碎片)→ 并入已有 autoManaged 同款族优先,无匹配才新建;同款仅人工锁定族则跳过并报告 - 名称回退分组键含物流备注,包邮/不包邮永不同组 → 新增族语义键 familyNameKey(国家+品名+SKU),api/admin 两侧同构,合并默认勾选随之修复 附加: - updateTags 后未归族链接自动并入匹配族(attachToMatchingFamily,响应带 attachedFamilyId) - 整理新增碎片族合并 consolidateFragments:纯碎片族并入带商品族并删除 (保公开 goodId=族ID 稳定),带商品/覆盖价/人工锁定进人工复审报告 - admin:保存标签提示补齐明细,整理完成消息含并入/碎片合并/待人工数 问题1(后台频繁 Unauthorized 掉线): - refresh cookie path '/auth' 与代理前缀(/api、/v2-api)不匹配导致浏览器 永远带不上 refresh cookie → 两 cookie path 统一为 '/' - v2 构建基址带尾斜杠 + 手工拼接产生 /v2-api//auth/refresh 双斜杠 404 → request.ts 规范拼接 测试:api jest 187/187、admin vitest 22/22、双侧 tsc 0 错误; public.service 夹具改为自包含(不依赖共享库既有数据)。
This commit is contained in:
@@ -44,6 +44,10 @@ export const productFamiliesApi = {
|
||||
linksUpdated: number
|
||||
goodsUpdated: number
|
||||
familiesCreated: number
|
||||
familiesMerged: number
|
||||
familiesSkipped: number
|
||||
fragmentsConsolidated: number
|
||||
fragmentsManualReview: number
|
||||
familiesRecomputed: number
|
||||
}>('/product-families/organize')
|
||||
},
|
||||
|
||||
@@ -36,8 +36,11 @@ request.interceptors.response.use(
|
||||
) {
|
||||
config._retried = true
|
||||
try {
|
||||
// 规范拼接:VITE_API_BASE 可能带尾斜杠(如 /v2-api/),直接相加会产生
|
||||
// /v2-api//auth/refresh 双斜杠导致 404
|
||||
const base = (import.meta.env.VITE_API_BASE || '/api').replace(/\/+$/, '')
|
||||
await axios.post(
|
||||
`${import.meta.env.VITE_API_BASE || '/api'}/auth/refresh`,
|
||||
`${base}/auth/refresh`,
|
||||
{},
|
||||
{ withCredentials: true },
|
||||
)
|
||||
|
||||
@@ -414,6 +414,10 @@ export interface OriginGoodTagInfo {
|
||||
export interface OriginGoodTagsResult {
|
||||
tagsManual: boolean
|
||||
tags: OriginGoodTagInfo[]
|
||||
/** 人工勾选缺定价维度时按链接名自动补齐的标签名(缺啥补啥) */
|
||||
filledDimensionTags?: string[]
|
||||
/** 保存时链接无族且自动并入成功的目标族 id */
|
||||
attachedFamilyId?: string
|
||||
}
|
||||
|
||||
export interface ProductFamilyMember {
|
||||
|
||||
@@ -10,11 +10,17 @@ describe('sameFamily', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('任一方 familyId 为空 → 回退名称 3 段规则', () => {
|
||||
it('任一方 familyId 为空 → 回退族语义键(国家+品名+SKU,物流/工艺不分族)', () => {
|
||||
const a = node(null, '美国(包邮)T恤-DG001-单面印花')
|
||||
expect(sameFamily(a, node(null, '美国(包邮)T恤-DG001-单面印花-某仓'))).toBe(true)
|
||||
expect(sameFamily(a, node('12', '美国(包邮)T恤-DG001-单面印花'))).toBe(true)
|
||||
expect(sameFamily(a, node(null, '美国(不包邮)T恤-DG001-单面印花'))).toBe(false)
|
||||
// 物流/工艺是矩阵维度而非分族维度:包邮 vs 不包邮、单面 vs 双面 同款同族
|
||||
expect(sameFamily(a, node(null, '美国(不包邮)T恤-DG001-单面印花'))).toBe(true)
|
||||
expect(sameFamily(a, node(null, '美国(不包邮)T恤-DG001-双面印花-美西一仓'))).toBe(true)
|
||||
// 不同款号不同族
|
||||
expect(sameFamily(a, node(null, '美国(包邮)T恤-DG002-单面印花'))).toBe(false)
|
||||
// 不同国家不同族
|
||||
expect(sameFamily(a, node(null, '英国(包邮)T恤-DG001-单面印花'))).toBe(false)
|
||||
})
|
||||
|
||||
it('familyId 不同 → false(即使名称同段)', () => {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { sameOriginGroup } from './origin-name'
|
||||
import { familyNameKey } from './origin-name'
|
||||
|
||||
/**
|
||||
* 两个原产品节点是否属于同一产品族:
|
||||
* 优先用服务端返回的 familyId 精确匹配;无族归属时回退名称 3 段规则
|
||||
* (老数据/未回填场景兜底)。
|
||||
* 优先用服务端返回的 familyId 精确匹配;无族归属时回退族语义键
|
||||
* (国家+品名+SKU —— 物流/工艺是矩阵维度不分族;老数据/未回填场景兜底)。
|
||||
*/
|
||||
export function sameFamily(
|
||||
a: { familyId?: string | null; goodName?: string | null },
|
||||
b: { familyId?: string | null; goodName?: string | null },
|
||||
): boolean {
|
||||
if (a.familyId && b.familyId) return a.familyId === b.familyId
|
||||
return sameOriginGroup(a.goodName, b.goodName)
|
||||
return familyNameKey(a.goodName) !== '' && familyNameKey(a.goodName) === familyNameKey(b.goodName)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,30 @@ export function sameOriginGroup(
|
||||
return ka !== '' && ka === truncateToProcess(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 族语义分组键(与 api 端 familyNameKey 同构):第 1 段剥离(物流备注)括号,
|
||||
* 保留 国家+品名+SKU,丢工艺/仓库段 —— 物流/工艺是价格矩阵维度而非分族维度。
|
||||
*/
|
||||
export function familyNameKey(name: string | null | undefined): string {
|
||||
if (!name) return '';
|
||||
const segs = name.split('-').map((s) => s.trim());
|
||||
const head = segs[0] ?? '';
|
||||
if (!head) return '';
|
||||
const normalized = head.replace(/\(/g, '(').replace(/\)/g, ')');
|
||||
const openAt = normalized.indexOf('(');
|
||||
let stripped = normalized;
|
||||
if (openAt >= 0) {
|
||||
const closeAt = normalized.indexOf(')', openAt);
|
||||
if (closeAt > openAt) {
|
||||
stripped = normalized.slice(0, openAt) + normalized.slice(closeAt + 1);
|
||||
}
|
||||
}
|
||||
stripped = stripped.trim();
|
||||
const sku = segs[1] ?? '';
|
||||
if (!stripped) return sku || '';
|
||||
return sku ? `${stripped}-${sku}` : stripped;
|
||||
}
|
||||
|
||||
/** 链接名结构化解析结果:品名 + 型号(SKU 代码) */
|
||||
export interface ParsedLinkName {
|
||||
productName: string;
|
||||
|
||||
@@ -1024,7 +1024,7 @@ async function onOrganize() {
|
||||
try {
|
||||
const r = await productFamiliesApi.organize()
|
||||
ElMessage.success(
|
||||
`整理完成:解析 ${r.labelsParsed} 条 / 标签更新 ${r.linksUpdated} 链接 / 新建族 ${r.familiesCreated} / 重算 ${r.familiesRecomputed} 族`,
|
||||
`整理完成:解析 ${r.labelsParsed} 条 / 标签更新 ${r.linksUpdated} 链接 / 新建族 ${r.familiesCreated} / 并入 ${r.familiesMerged} 链接 / 碎片合并 ${r.fragmentsConsolidated ?? 0} 族${r.fragmentsManualReview ? `(待人工合并 ${r.fragmentsManualReview} 族)` : ''} / 重算 ${r.familiesRecomputed} 族`,
|
||||
)
|
||||
await loadAll()
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { productFamiliesApi } from '@/api/product-families'
|
||||
import { countriesApi } from '@/api/countries'
|
||||
import { syncApi } from '@/api/sync'
|
||||
import { cleanLinkName, deriveLinkTagNames, linkDims } from '@/utils/origin-name'
|
||||
import type { OriginGoodTagsResult } from '@/types'
|
||||
import { findCategoryPath, buildCascader } from '@/utils/category-tree'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -364,8 +365,9 @@ async function loadEditFamily() {
|
||||
async function saveMemberTags(row: { id: string }) {
|
||||
memberTagSaving.value = row.id
|
||||
try {
|
||||
await originGoodsApi.updateTags(row.id, memberTagEdits.value[row.id] ?? [])
|
||||
ElMessage.success('标签已保存')
|
||||
const res = await originGoodsApi.updateTags(row.id, memberTagEdits.value[row.id] ?? [])
|
||||
const filled = (res as OriginGoodTagsResult | undefined)?.filledDimensionTags ?? []
|
||||
ElMessage.success(filled.length ? `标签已保存(缺维度已自动补齐:${filled.join('、')})` : '标签已保存')
|
||||
await loadEditFamily()
|
||||
emit('changed')
|
||||
} catch (e: any) {
|
||||
|
||||
Reference in New Issue
Block a user