修改图片预览、json导出
This commit is contained in:
+208
-22
@@ -3,11 +3,15 @@ import path from 'node:path';
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
|
||||||
const API_BASE = process.env.CADLABEL_API_BASE ?? 'http://127.0.0.1:8891/api';
|
const API_BASE = process.env.CADLABEL_API_BASE ?? 'http://127.0.0.1:8891/api';
|
||||||
const SOURCE_ROOT = path.resolve(process.cwd(), process.env.CADLABEL_SOURCE_ROOT ?? '../output_json 2');
|
const DEFAULT_SOURCE_ROOT = fs.existsSync(path.resolve(process.cwd(), '../json_data/original'))
|
||||||
|
? '../json_data/original'
|
||||||
|
: '../output_json 2';
|
||||||
|
const SOURCE_ROOT = path.resolve(process.cwd(), process.env.CADLABEL_SOURCE_ROOT ?? DEFAULT_SOURCE_ROOT);
|
||||||
const APPLY = process.argv.includes('--apply');
|
const APPLY = process.argv.includes('--apply');
|
||||||
const FORCE = process.argv.includes('--force');
|
const FORCE = process.argv.includes('--force');
|
||||||
const onlyModel = valueAfter('--model');
|
const onlyModel = valueAfter('--model');
|
||||||
const MARKER = '[AI批量分析:v1]';
|
const MARKER = '[AI批量分析:v1]';
|
||||||
|
const API_TIMEOUT_MS = Number(process.env.CADLABEL_API_TIMEOUT_MS ?? 120_000);
|
||||||
|
|
||||||
const CLASS_DEFINITIONS = [
|
const CLASS_DEFINITIONS = [
|
||||||
{
|
{
|
||||||
@@ -141,7 +145,7 @@ async function api(relativePath, init) {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(init?.headers ?? {})
|
...(init?.headers ?? {})
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(30_000)
|
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
if (!response.ok || payload.code !== 'OK') {
|
if (!response.ok || payload.code !== 'OK') {
|
||||||
@@ -154,8 +158,78 @@ function post(relativePath, body) {
|
|||||||
return api(relativePath, { method: 'POST', body: JSON.stringify(body) });
|
return api(relativePath, { method: 'POST', body: JSON.stringify(body) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function classFor(modelName) {
|
function classFor(modelName, geometry) {
|
||||||
return CLASS_DEFINITIONS.find((item) => item.ids.includes(modelName)) ?? {
|
const configured = CLASS_DEFINITIONS.find((item) => item.ids.includes(modelName));
|
||||||
|
if (configured) return configured;
|
||||||
|
if (modelName.includes('方板+均布圆周孔')) {
|
||||||
|
return {
|
||||||
|
label: '圆周孔方形安装板',
|
||||||
|
scene: '方形安装基面上的圆周均布紧固、定位和载荷分散',
|
||||||
|
primaryFunction: '方板承载与圆周孔系连接'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (modelName.includes('方板+四角孔')) {
|
||||||
|
return {
|
||||||
|
label: '多孔方形安装板',
|
||||||
|
scene: '方形安装基面上的四角紧固、中心定位和附加接口连接',
|
||||||
|
primaryFunction: '方板承载与多点安装'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (modelName.includes('方台')) {
|
||||||
|
return {
|
||||||
|
label: geometry.interfaceCount ? '多孔方形安装台' : '方形承载台',
|
||||||
|
scene: '小型机械组件的平面承载、垫高、定位和紧固安装',
|
||||||
|
primaryFunction: '方台承载与安装定位'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (modelName.includes('圆盘+均布孔')) {
|
||||||
|
return {
|
||||||
|
label: '圆周孔圆形安装盘',
|
||||||
|
scene: '圆形安装面上的周向均布紧固和同轴定位',
|
||||||
|
primaryFunction: '圆盘承载与圆周孔系连接'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (modelName.includes('圆盘')) {
|
||||||
|
return {
|
||||||
|
label: '圆形承载盘',
|
||||||
|
scene: '圆形端面的承载、间隔和同轴安装',
|
||||||
|
primaryFunction: '圆盘承载与端面定位'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const sequenceNumber = /^\d+$/.test(modelName) ? Number(modelName) : null;
|
||||||
|
if (sequenceNumber !== null && sequenceNumber >= 40 && sequenceNumber <= 51) {
|
||||||
|
return {
|
||||||
|
label: '多孔圆形法兰盘',
|
||||||
|
scene: '中心接口与周向孔系组合的法兰连接和同轴定位',
|
||||||
|
primaryFunction: '圆形法兰连接与孔系定位'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sequenceNumber !== null && sequenceNumber >= 52 && sequenceNumber <= 58) {
|
||||||
|
return {
|
||||||
|
label: '双耳异形连接板',
|
||||||
|
scene: '双圆头或长圆形接口之间的跨距连接、铰接和定位',
|
||||||
|
primaryFunction: '双接口跨距连接与载荷传递'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sequenceNumber !== null && sequenceNumber >= 59 && sequenceNumber <= 66) {
|
||||||
|
return {
|
||||||
|
label: '多孔异形安装板',
|
||||||
|
scene: '多规格孔位的组合安装、定位和多点紧固',
|
||||||
|
primaryFunction: '异形板承载与多孔连接'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (sequenceNumber !== null && sequenceNumber >= 67) {
|
||||||
|
return {
|
||||||
|
label: '复合孔系圆形法兰盘',
|
||||||
|
scene: '多圈、多规格孔系的法兰连接、精密定位和装配适配',
|
||||||
|
primaryFunction: '复合孔系法兰连接'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return geometry.outerCircular ? {
|
||||||
|
label: '多孔圆形安装盘',
|
||||||
|
scene: '圆形安装面上的承载、周向紧固和同轴定位',
|
||||||
|
primaryFunction: '圆盘承载与多孔连接'
|
||||||
|
} : {
|
||||||
label: '机械安装连接件',
|
label: '机械安装连接件',
|
||||||
scene: '机械组件的承载、定位和紧固连接',
|
scene: '机械组件的承载、定位和紧固连接',
|
||||||
primaryFunction: '机械承载与装配连接'
|
primaryFunction: '机械承载与装配连接'
|
||||||
@@ -177,6 +251,54 @@ function boundingBox(source) {
|
|||||||
return dimensions.map(formatNumber);
|
return dimensions.map(formatNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function geometryAnalysis(source) {
|
||||||
|
const box = source.validation_hints?.part_box_m;
|
||||||
|
const size = Array.isArray(box) && box.length === 6
|
||||||
|
? [box[3] - box[0], box[4] - box[1], box[5] - box[2]].map((value) => value * 1000)
|
||||||
|
: [];
|
||||||
|
const maxSpan = size.length ? Math.max(...size) : 0;
|
||||||
|
const faces = (source.features ?? []).flatMap((feature) => feature.owned_faces ?? []);
|
||||||
|
const cylinders = faces
|
||||||
|
.filter((face) => face.surface?.is_cylinder && Array.isArray(face.surface.cylinder_params))
|
||||||
|
.map((face) => {
|
||||||
|
const params = face.surface.cylinder_params;
|
||||||
|
const center = params.slice(0, 3).map((value) => value * 1000);
|
||||||
|
const axis = params.slice(3, 6);
|
||||||
|
const axisIndex = axis.reduce(
|
||||||
|
(best, value, index) => Math.abs(value) > Math.abs(axis[best] ?? 0) ? index : best,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const radialDistance = Math.hypot(...center.filter((_, index) => index !== axisIndex));
|
||||||
|
return { radius: Math.abs(params[6] * 1000), center, radialDistance };
|
||||||
|
});
|
||||||
|
const outerCircular = cylinders.some((item) =>
|
||||||
|
maxSpan > 0 && item.radius >= maxSpan * 0.4 && item.radialDistance <= maxSpan * 0.08
|
||||||
|
);
|
||||||
|
const interfaces = cylinders.filter((item) =>
|
||||||
|
maxSpan > 0 && item.radius <= maxSpan * 0.2
|
||||||
|
);
|
||||||
|
const uniqueInterfaces = new Map();
|
||||||
|
for (const item of interfaces) {
|
||||||
|
const key = `${formatNumber(item.radius)}:${item.center.map((value) => formatNumber(value)).join(',')}`;
|
||||||
|
uniqueInterfaces.set(key, item);
|
||||||
|
}
|
||||||
|
const diameterGroups = new Map();
|
||||||
|
for (const item of uniqueInterfaces.values()) {
|
||||||
|
const diameter = formatNumber(item.radius * 2);
|
||||||
|
diameterGroups.set(diameter, (diameterGroups.get(diameter) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
const interfaceSummary = [...diameterGroups.entries()]
|
||||||
|
.sort((left, right) => Number(right[0]) - Number(left[0]))
|
||||||
|
.map(([diameter, count]) => `${count}ר${diameter} mm`)
|
||||||
|
.join('、');
|
||||||
|
return {
|
||||||
|
size,
|
||||||
|
outerCircular,
|
||||||
|
interfaceCount: uniqueInterfaces.size,
|
||||||
|
interfaceSummary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function formatNumber(value) {
|
function formatNumber(value) {
|
||||||
if (!Number.isFinite(value)) return String(value);
|
if (!Number.isFinite(value)) return String(value);
|
||||||
return Number(value.toFixed(Math.abs(value) < 10 ? 3 : 2)).toString();
|
return Number(value.toFixed(Math.abs(value) < 10 ? 3 : 2)).toString();
|
||||||
@@ -240,17 +362,22 @@ function operationIntent(step) {
|
|||||||
return `执行建模步骤“${step.name}”,在前序几何基础上形成该步骤对应的有效几何。`;
|
return `执行建模步骤“${step.name}”,在前序几何基础上形成该步骤对应的有效几何。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepAnnotation(modelName, step, feature) {
|
function stepAnnotation(modelName, step, feature, geometry) {
|
||||||
const evidence = dimensionEvidence(feature, step, modelName);
|
const evidence = dimensionEvidence(feature, step, modelName);
|
||||||
const faceCount = feature?.owned_faces?.length ?? 0;
|
const faceCount = feature?.owned_faces?.length ?? 0;
|
||||||
const group = GROUP_DEFINITIONS[stepGroup(step)];
|
const group = GROUP_DEFINITIONS[stepGroup(step)];
|
||||||
|
const compoundProfile = step.type === 'extrude_add' && geometry.interfaceCount > 0
|
||||||
|
? `该复合草图一次拉伸同时形成主体外轮廓与 ${geometry.interfaceCount} 个圆形内轮廓。`
|
||||||
|
: '';
|
||||||
return {
|
return {
|
||||||
modelingDescription: `${operationIntent(step)}该步骤在本零件中归入“${group.name}”。`,
|
modelingDescription: `${operationIntent(step)}${compoundProfile}该步骤在本零件中归入“${group.name}”。`,
|
||||||
supplementDescription: [
|
supplementDescription: [
|
||||||
`重建顺序第 ${step.order} 步,绑定源特征 ${step.sourceFeatureId}。`,
|
`重建顺序第 ${step.order} 步,绑定源特征 ${step.sourceFeatureId}。`,
|
||||||
|
geometry.size.length ? `最终几何包络约为 ${geometry.size.map(formatNumber).join(' × ')} mm。` : null,
|
||||||
|
compoundProfile ? `圆形接口证据:${geometry.interfaceSummary}。` : null,
|
||||||
evidence.length ? `关键参数证据:${evidence.join(';')}。` : '源特征未提供可直接解释的标量尺寸,语义依据操作类型与几何结果确定。',
|
evidence.length ? `关键参数证据:${evidence.join(';')}。` : '源特征未提供可直接解释的标量尺寸,语义依据操作类型与几何结果确定。',
|
||||||
faceCount ? `源特征记录 ${faceCount} 个归属面,可用于后续拓扑复核。` : '源特征未记录独占面,需结合前后步骤或最终拓扑复核。'
|
faceCount ? `源特征记录 ${faceCount} 个归属面,可用于后续拓扑复核。` : '源特征未记录独占面,需结合前后步骤或最终拓扑复核。'
|
||||||
].join(''),
|
].filter(Boolean).join(''),
|
||||||
remark: `${MARKER} 参数来自 SolidWorks 提取结果;角度、阵列计数及孔向导深度可能采用插件编码或构造深度,不应在未核对原模型前作为公差或有效加工深度。`
|
remark: `${MARKER} 参数来自 SolidWorks 提取结果;角度、阵列计数及孔向导深度可能采用插件编码或构造深度,不应在未核对原模型前作为公差或有效加工深度。`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -279,9 +406,21 @@ function structurePayload(groupKey, steps, classInfo) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function geometryInterfaceStructurePayload(step, geometry, classInfo) {
|
||||||
|
return {
|
||||||
|
structureName: '复合草图孔系与圆弧接口',
|
||||||
|
structureType: '孔系/圆弧轮廓结构',
|
||||||
|
purpose: '在主体成形步骤中同步建立圆形内轮廓,用于紧固、定位、穿轴或装配避让。',
|
||||||
|
solution: `由建模步骤 ${step.order}.${step.name} 的复合草图一次拉伸形成;检测到 ${geometry.interfaceCount} 个圆形接口,直径分组为 ${geometry.interfaceSummary}。`,
|
||||||
|
reason: `复合草图将主体外轮廓与接口轮廓置于同一成形步骤,可保持相对位置一致;该判断同时结合“${classInfo.label}”的典型用途。`,
|
||||||
|
remark: `${MARKER} 圆形接口依据源特征归属面的圆柱几何识别;孔的用途、配合等级与加工公差需结合装配对象复核。`,
|
||||||
|
historyIds: [step.historyId]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function functionPayloads(classInfo, structures) {
|
function functionPayloads(classInfo, structures) {
|
||||||
const allIds = structures.map((item) => item.id);
|
const allIds = structures.map((item) => item.id);
|
||||||
const interfaceStructures = structures.filter((item) => ['cut', 'hole', 'thread', 'counterbore', 'pattern'].includes(item.groupKey));
|
const interfaceStructures = structures.filter((item) => ['cut', 'hole', 'thread', 'counterbore', 'pattern', 'geometry_interface'].includes(item.groupKey));
|
||||||
const edgeStructures = structures.filter((item) => item.groupKey === 'edge');
|
const edgeStructures = structures.filter((item) => item.groupKey === 'edge');
|
||||||
const payloads = [{
|
const payloads = [{
|
||||||
functionName: classInfo.primaryFunction,
|
functionName: classInfo.primaryFunction,
|
||||||
@@ -311,12 +450,15 @@ function functionPayloads(classInfo, structures) {
|
|||||||
return payloads;
|
return payloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelPayload(model, source, histories, classInfo) {
|
function modelPayload(model, source, histories, classInfo, geometry) {
|
||||||
const box = boundingBox(source);
|
const box = boundingBox(source);
|
||||||
const boxText = box ? `零件几何包络约为 ${box.join(' × ')} mm。` : '源数据未提供可解析的零件包络。';
|
const boxText = box ? `零件几何包络约为 ${box.join(' × ')} mm。` : '源数据未提供可解析的零件包络。';
|
||||||
|
const interfaceText = geometry.interfaceCount > 0
|
||||||
|
? `复合草图形成 ${geometry.interfaceCount} 个圆形接口,直径分组为 ${geometry.interfaceSummary}。`
|
||||||
|
: '未从源特征归属面中识别出独立的小直径圆形接口。';
|
||||||
const sequence = histories.map((step) => `${step.order}.${step.name}(${step.type})`).join(';');
|
const sequence = histories.map((step) => `${step.order}.${step.name}(${step.type})`).join(';');
|
||||||
const hasRevolve = histories.some((step) => step.type.includes('revolve'));
|
const hasRevolve = histories.some((step) => step.type.includes('revolve'));
|
||||||
const hasHole = histories.some((step) => step.type === 'hole');
|
const hasHole = histories.some((step) => step.type === 'hole') || geometry.interfaceCount > 0;
|
||||||
const manufacturing = [
|
const manufacturing = [
|
||||||
hasRevolve ? '回转表面车削或车铣复合加工' : 'CNC 铣削或轮廓加工',
|
hasRevolve ? '回转表面车削或车铣复合加工' : 'CNC 铣削或轮廓加工',
|
||||||
hasHole ? '钻孔/扩孔/攻丝等孔系加工' : null,
|
hasHole ? '钻孔/扩孔/攻丝等孔系加工' : null,
|
||||||
@@ -326,11 +468,27 @@ function modelPayload(model, source, histories, classInfo) {
|
|||||||
applicationScenario: classInfo.scene,
|
applicationScenario: classInfo.scene,
|
||||||
material: 'SolidWorks 源模型未指定材料;数据集建议铝合金或钢制测试件,需按载荷与环境确认',
|
material: 'SolidWorks 源模型未指定材料;数据集建议铝合金或钢制测试件,需按载荷与环境确认',
|
||||||
manufacturingMethod: `${manufacturing}(依据建模特征推定,待工艺确认)`,
|
manufacturingMethod: `${manufacturing}(依据建模特征推定,待工艺确认)`,
|
||||||
userRequirement: `设计一件${classInfo.label},用于${classInfo.scene}。${boxText}需保留建模历史中定义的主体、孔槽、连接接口和边缘处理,并保证各接口的相对位置与重建顺序一致。`,
|
userRequirement: `设计一件${classInfo.label},用于${classInfo.scene}。${boxText}${interfaceText}需保留建模历史中定义的主体、孔槽、连接接口和边缘处理,并保证各接口的相对位置与重建顺序一致。`,
|
||||||
designDescription: `${model.modelName} 被识别为${classInfo.label}。${boxText}有效建模序列为:${sequence}。主体加料特征建立承载包络,切除与孔向导特征形成装配接口,阵列保证重复结构一致,倒角/圆角改善边缘装配性。材料、载荷、配合公差和制造基准未由源 JSON 完整给出,相关结论均需工程复核。`
|
designDescription: `${model.modelName} 被识别为${classInfo.label}。${boxText}${interfaceText}有效建模序列为:${sequence}。主体加料特征建立承载包络,独立切除、孔向导或复合草图内轮廓形成装配接口,阵列保证重复结构一致,倒角/圆角改善边缘装配性。材料、载荷、配合公差和制造基准未由源 JSON 完整给出,相关结论均需工程复核。`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasText(value) {
|
||||||
|
return typeof value === 'string' && value.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCompleteAnnotation(exportData, historyCount) {
|
||||||
|
const annotatedCount = (exportData.featureIndex ?? []).filter((item) =>
|
||||||
|
hasText(item.annotation?.modelingIntent)
|
||||||
|
&& hasText(item.annotation?.description)
|
||||||
|
&& hasText(item.annotation?.notes)
|
||||||
|
).length;
|
||||||
|
return annotatedCount === historyCount
|
||||||
|
&& (exportData.structures?.length ?? 0) > 0
|
||||||
|
&& (exportData.functions?.length ?? 0) > 0
|
||||||
|
&& (exportData.unassignedHistoryIds?.length ?? 0) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
function hasUnmanagedAnnotation(exportData) {
|
function hasUnmanagedAnnotation(exportData) {
|
||||||
const annotations = exportData.featureIndex ?? [];
|
const annotations = exportData.featureIndex ?? [];
|
||||||
const existingNotes = annotations.map((item) => item.annotation?.notes).filter(Boolean);
|
const existingNotes = annotations.map((item) => item.annotation?.notes).filter(Boolean);
|
||||||
@@ -366,12 +524,16 @@ async function annotateModel(model) {
|
|||||||
api(`/cad-models/${model.id}/export/annotation`)
|
api(`/cad-models/${model.id}/export/annotation`)
|
||||||
]);
|
]);
|
||||||
if (!histories.length) return { model: model.modelName, status: 'skipped:no_histories' };
|
if (!histories.length) return { model: model.modelName, status: 'skipped:no_histories' };
|
||||||
|
if (!FORCE && isCompleteAnnotation(exportData, histories.length)) {
|
||||||
|
return { model: model.modelName, status: 'skipped:already_complete' };
|
||||||
|
}
|
||||||
if (!FORCE && hasUnmanagedAnnotation(exportData)) {
|
if (!FORCE && hasUnmanagedAnnotation(exportData)) {
|
||||||
return { model: model.modelName, status: 'skipped:existing_manual_annotation' };
|
return { model: model.modelName, status: 'skipped:existing_manual_annotation' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = sourceFor(model.modelName);
|
const source = sourceFor(model.modelName);
|
||||||
const classInfo = classFor(model.modelName);
|
const geometry = geometryAnalysis(source);
|
||||||
|
const classInfo = classFor(model.modelName, geometry);
|
||||||
const featuresById = new Map((source.features ?? []).map((feature) => [feature.id, feature]));
|
const featuresById = new Map((source.features ?? []).map((feature) => [feature.id, feature]));
|
||||||
const normalizedHistories = histories.map((step) => ({
|
const normalizedHistories = histories.map((step) => ({
|
||||||
historyId: step.id,
|
historyId: step.id,
|
||||||
@@ -381,15 +543,22 @@ async function annotateModel(model) {
|
|||||||
type: step.operationType
|
type: step.operationType
|
||||||
}));
|
}));
|
||||||
const groups = groupSteps(normalizedHistories);
|
const groups = groupSteps(normalizedHistories);
|
||||||
const modelUpdate = modelPayload(model, source, normalizedHistories, classInfo);
|
const geometryStep = geometry.interfaceCount > 0
|
||||||
|
? normalizedHistories.find((step) => step.type === 'extrude_add')
|
||||||
|
: null;
|
||||||
|
const modelUpdate = modelPayload(model, source, normalizedHistories, classInfo, geometry);
|
||||||
|
const dryRunStructures = [...groups.keys()].map((groupKey, index) => ({ id: index + 1, groupKey }));
|
||||||
|
if (geometryStep) dryRunStructures.push({ id: dryRunStructures.length + 1, groupKey: 'geometry_interface' });
|
||||||
|
|
||||||
if (!APPLY) {
|
if (!APPLY) {
|
||||||
return {
|
return {
|
||||||
model: model.modelName,
|
model: model.modelName,
|
||||||
status: 'dry-run',
|
status: 'dry-run',
|
||||||
histories: histories.length,
|
histories: histories.length,
|
||||||
structures: groups.size,
|
structures: dryRunStructures.length,
|
||||||
functions: functionPayloads(classInfo, [...groups.keys()].map((groupKey, index) => ({ id: index + 1, groupKey }))).length
|
functions: functionPayloads(classInfo, dryRunStructures).length,
|
||||||
|
circularInterfaces: geometry.interfaceCount,
|
||||||
|
interfaceSummary: geometry.interfaceSummary || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +566,7 @@ async function annotateModel(model) {
|
|||||||
for (const step of normalizedHistories) {
|
for (const step of normalizedHistories) {
|
||||||
await post(
|
await post(
|
||||||
`/cad-models/${model.id}/histories/${step.historyId}/annotation`,
|
`/cad-models/${model.id}/histories/${step.historyId}/annotation`,
|
||||||
stepAnnotation(model.modelName, step, featuresById.get(step.sourceFeatureId))
|
stepAnnotation(model.modelName, step, featuresById.get(step.sourceFeatureId), geometry)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +576,14 @@ async function annotateModel(model) {
|
|||||||
const saved = await ensureStructure(model.id, structurePayload(groupKey, steps, classInfo), existingStructures);
|
const saved = await ensureStructure(model.id, structurePayload(groupKey, steps, classInfo), existingStructures);
|
||||||
structures.push({ ...saved, groupKey });
|
structures.push({ ...saved, groupKey });
|
||||||
}
|
}
|
||||||
|
if (geometryStep) {
|
||||||
|
const saved = await ensureStructure(
|
||||||
|
model.id,
|
||||||
|
geometryInterfaceStructurePayload(geometryStep, geometry, classInfo),
|
||||||
|
existingStructures
|
||||||
|
);
|
||||||
|
structures.push({ ...saved, groupKey: 'geometry_interface' });
|
||||||
|
}
|
||||||
|
|
||||||
const existingFunctions = new Map((exportData.functions ?? []).map((item) => [item.name, { id: item.id }]));
|
const existingFunctions = new Map((exportData.functions ?? []).map((item) => [item.name, { id: item.id }]));
|
||||||
for (const payload of functionPayloads(classInfo, structures)) {
|
for (const payload of functionPayloads(classInfo, structures)) {
|
||||||
@@ -414,9 +591,13 @@ async function annotateModel(model) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const verified = await api(`/cad-models/${model.id}/export/annotation`);
|
const verified = await api(`/cad-models/${model.id}/export/annotation`);
|
||||||
const annotatedCount = (verified.featureIndex ?? []).filter((item) => item.annotation?.modelingIntent).length;
|
const annotatedCount = (verified.featureIndex ?? []).filter((item) =>
|
||||||
if (annotatedCount !== histories.length || verified.unassignedHistoryIds?.length) {
|
hasText(item.annotation?.modelingIntent)
|
||||||
throw new Error(`${model.modelName} 校验失败: annotated=${annotatedCount}/${histories.length}, unassigned=${verified.unassignedHistoryIds}`);
|
&& hasText(item.annotation?.description)
|
||||||
|
&& hasText(item.annotation?.notes)
|
||||||
|
).length;
|
||||||
|
if (!isCompleteAnnotation(verified, histories.length)) {
|
||||||
|
throw new Error(`${model.modelName} 校验失败: annotated=${annotatedCount}/${histories.length}, structures=${verified.structures?.length ?? 0}, functions=${verified.functions?.length ?? 0}, unassigned=${verified.unassignedHistoryIds}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
model: model.modelName,
|
model: model.modelName,
|
||||||
@@ -429,8 +610,13 @@ async function annotateModel(model) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const page = await api('/cad-models?page=1&size=100');
|
const modelsByPage = [];
|
||||||
let models = [...page.items].sort((left, right) => left.modelName.localeCompare(right.modelName));
|
for (let pageNumber = 1; ; pageNumber += 1) {
|
||||||
|
const page = await api(`/cad-models?page=${pageNumber}&size=100`);
|
||||||
|
modelsByPage.push(...page.items);
|
||||||
|
if (modelsByPage.length >= page.total || page.items.length === 0) break;
|
||||||
|
}
|
||||||
|
let models = modelsByPage.sort((left, right) => left.modelName.localeCompare(right.modelName));
|
||||||
if (onlyModel) models = models.filter((model) => model.modelName === onlyModel);
|
if (onlyModel) models = models.filter((model) => model.modelName === onlyModel);
|
||||||
if (!models.length) throw new Error(onlyModel ? `未找到模型 ${onlyModel}` : '数据库中没有模型');
|
if (!models.length) throw new Error(onlyModel ? `未找到模型 ${onlyModel}` : '数据库中没有模型');
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ public class JsonDatasetImportProperties {
|
|||||||
/** 是否由 Java 导入器上传对象;外部已上传时关闭。 */
|
/** 是否由 Java 导入器上传对象;外部已上传时关闭。 */
|
||||||
private boolean uploadEnabled = true;
|
private boolean uploadEnabled = true;
|
||||||
|
|
||||||
|
/** 是否清空现有标注数据;关闭时仅允许追加不存在的新模型。 */
|
||||||
|
private boolean replaceExisting = true;
|
||||||
|
|
||||||
/** 导入模型归属的分类 ID。 */
|
/** 导入模型归属的分类 ID。 */
|
||||||
private long categoryId = 1L;
|
private long categoryId = 1L;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public record CadModelResponse(
|
|||||||
String sourceSwPath,
|
String sourceSwPath,
|
||||||
String featureTreeJsonPath,
|
String featureTreeJsonPath,
|
||||||
String viewPath,
|
String viewPath,
|
||||||
|
String thumbnailPath,
|
||||||
String applicationScenario,
|
String applicationScenario,
|
||||||
String material,
|
String material,
|
||||||
String manufacturingMethod,
|
String manufacturingMethod,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public record UpdateCadModelRequest(
|
|||||||
@Size(max = 1024) String sourceSwPath,
|
@Size(max = 1024) String sourceSwPath,
|
||||||
@Size(max = 1024) String featureTreeJsonPath,
|
@Size(max = 1024) String featureTreeJsonPath,
|
||||||
@Size(max = 1024) String viewPath,
|
@Size(max = 1024) String viewPath,
|
||||||
|
@Size(max = 1024) String thumbnailPath,
|
||||||
@Size(max = 255) String applicationScenario,
|
@Size(max = 255) String applicationScenario,
|
||||||
@Size(max = 255) String material,
|
@Size(max = 255) String material,
|
||||||
@Size(max = 255) String manufacturingMethod,
|
@Size(max = 255) String manufacturingMethod,
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ public class CadModel {
|
|||||||
/** 模型视图资源路径。 */
|
/** 模型视图资源路径。 */
|
||||||
private String viewPath;
|
private String viewPath;
|
||||||
|
|
||||||
|
/** 模型预览图路径。 */
|
||||||
|
private String thumbnailPath;
|
||||||
|
|
||||||
/** 应用场景。 */
|
/** 应用场景。 */
|
||||||
private String applicationScenario;
|
private String applicationScenario;
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显式启用后,全量替换标注后台中的模型数据。
|
* 显式启用后,将处理完成的 JSON 数据集导入标注后台。
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@@ -33,13 +33,14 @@ public class JsonDatasetImportRunner implements ApplicationRunner {
|
|||||||
}
|
}
|
||||||
Path sourceDirectory = Path.of(properties.getSourceDirectory()).toAbsolutePath().normalize();
|
Path sourceDirectory = Path.of(properties.getSourceDirectory()).toAbsolutePath().normalize();
|
||||||
Path artifactsDirectory = Path.of(properties.getArtifactsDirectory()).toAbsolutePath().normalize();
|
Path artifactsDirectory = Path.of(properties.getArtifactsDirectory()).toAbsolutePath().normalize();
|
||||||
log.warn("Starting destructive CAD JSON dataset import. sourceDirectory={}, artifactsDirectory={}, categoryId={}",
|
log.warn("Starting CAD JSON dataset import. sourceDirectory={}, artifactsDirectory={}, replaceExisting={}, categoryId={}",
|
||||||
sourceDirectory, artifactsDirectory, properties.getCategoryId());
|
sourceDirectory, artifactsDirectory, properties.isReplaceExisting(), properties.getCategoryId());
|
||||||
JsonDatasetImportService.ImportSummary summary = jsonDatasetImportService.replaceDataset(
|
JsonDatasetImportService.ImportSummary summary = jsonDatasetImportService.importDataset(
|
||||||
sourceDirectory,
|
sourceDirectory,
|
||||||
artifactsDirectory,
|
artifactsDirectory,
|
||||||
properties.getCosPrefix(),
|
properties.getCosPrefix(),
|
||||||
properties.isUploadEnabled(),
|
properties.isUploadEnabled(),
|
||||||
|
properties.isReplaceExisting(),
|
||||||
properties.getCategoryId()
|
properties.getCategoryId()
|
||||||
);
|
);
|
||||||
log.info("CAD JSON dataset import finished. modelCount={}, historyCount={}",
|
log.info("CAD JSON dataset import finished. modelCount={}, historyCount={}",
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package com.cadlabel.service;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 SolidWorks 特征树 JSON 数据集替换导入标注后台。
|
* 将 SolidWorks 特征树 JSON 数据集导入标注后台。
|
||||||
*/
|
*/
|
||||||
public interface JsonDatasetImportService {
|
public interface JsonDatasetImportService {
|
||||||
|
|
||||||
ImportSummary replaceDataset(Path sourceDirectory, Path artifactsDirectory, String cosPrefix,
|
ImportSummary importDataset(Path sourceDirectory, Path artifactsDirectory, String cosPrefix,
|
||||||
boolean uploadEnabled, long categoryId);
|
boolean uploadEnabled, boolean replaceExisting, long categoryId);
|
||||||
|
|
||||||
record ImportSummary(int modelCount, int historyCount) {
|
record ImportSummary(int modelCount, int historyCount) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ public class CadModelServiceImpl implements CadModelService {
|
|||||||
validateMaxLength("sourceSwPath", request.sourceSwPath(), 1024);
|
validateMaxLength("sourceSwPath", request.sourceSwPath(), 1024);
|
||||||
validateMaxLength("featureTreeJsonPath", request.featureTreeJsonPath(), 1024);
|
validateMaxLength("featureTreeJsonPath", request.featureTreeJsonPath(), 1024);
|
||||||
validateMaxLength("viewPath", request.viewPath(), 1024);
|
validateMaxLength("viewPath", request.viewPath(), 1024);
|
||||||
|
validateMaxLength("thumbnailPath", request.thumbnailPath(), 1024);
|
||||||
validateMaxLength("applicationScenario", request.applicationScenario(), 255);
|
validateMaxLength("applicationScenario", request.applicationScenario(), 255);
|
||||||
validateMaxLength("material", request.material(), 255);
|
validateMaxLength("material", request.material(), 255);
|
||||||
validateMaxLength("manufacturingMethod", request.manufacturingMethod(), 255);
|
validateMaxLength("manufacturingMethod", request.manufacturingMethod(), 255);
|
||||||
@@ -229,6 +230,9 @@ public class CadModelServiceImpl implements CadModelService {
|
|||||||
if (request.viewPath() != null) {
|
if (request.viewPath() != null) {
|
||||||
model.setViewPath(request.viewPath());
|
model.setViewPath(request.viewPath());
|
||||||
}
|
}
|
||||||
|
if (request.thumbnailPath() != null) {
|
||||||
|
model.setThumbnailPath(request.thumbnailPath());
|
||||||
|
}
|
||||||
if (request.applicationScenario() != null) {
|
if (request.applicationScenario() != null) {
|
||||||
model.setApplicationScenario(request.applicationScenario());
|
model.setApplicationScenario(request.applicationScenario());
|
||||||
}
|
}
|
||||||
@@ -266,6 +270,7 @@ public class CadModelServiceImpl implements CadModelService {
|
|||||||
addField(fields, "sourceSwPath", request.sourceSwPath());
|
addField(fields, "sourceSwPath", request.sourceSwPath());
|
||||||
addField(fields, "featureTreeJsonPath", request.featureTreeJsonPath());
|
addField(fields, "featureTreeJsonPath", request.featureTreeJsonPath());
|
||||||
addField(fields, "viewPath", request.viewPath());
|
addField(fields, "viewPath", request.viewPath());
|
||||||
|
addField(fields, "thumbnailPath", request.thumbnailPath());
|
||||||
addField(fields, "applicationScenario", request.applicationScenario());
|
addField(fields, "applicationScenario", request.applicationScenario());
|
||||||
addField(fields, "material", request.material());
|
addField(fields, "material", request.material());
|
||||||
addField(fields, "manufacturingMethod", request.manufacturingMethod());
|
addField(fields, "manufacturingMethod", request.manufacturingMethod());
|
||||||
@@ -314,6 +319,7 @@ public class CadModelServiceImpl implements CadModelService {
|
|||||||
model.getSourceSwPath(),
|
model.getSourceSwPath(),
|
||||||
model.getFeatureTreeJsonPath(),
|
model.getFeatureTreeJsonPath(),
|
||||||
model.getViewPath(),
|
model.getViewPath(),
|
||||||
|
model.getThumbnailPath(),
|
||||||
model.getApplicationScenario(),
|
model.getApplicationScenario(),
|
||||||
model.getMaterial(),
|
model.getMaterial(),
|
||||||
model.getManufacturingMethod(),
|
model.getManufacturingMethod(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.cadlabel.service.impl;
|
package com.cadlabel.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
import com.cadlabel.entity.CadModel;
|
import com.cadlabel.entity.CadModel;
|
||||||
import com.cadlabel.entity.CadModelHistory;
|
import com.cadlabel.entity.CadModelHistory;
|
||||||
import com.cadlabel.mapper.CadModelHistoryMapper;
|
import com.cadlabel.mapper.CadModelHistoryMapper;
|
||||||
@@ -29,7 +30,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/** 将 Node 后端完成重建的 SolidWorks 数据集上传 COS 并替换远程标注数据。 */
|
/** 将 Node 后端完成重建的 SolidWorks 数据集上传对象存储并导入远程标注数据。 */
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -47,8 +48,8 @@ public class SolidWorksJsonDatasetImportService implements JsonDatasetImportServ
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ImportSummary replaceDataset(Path sourceDirectory, Path artifactsDirectory, String cosPrefix,
|
public ImportSummary importDataset(Path sourceDirectory, Path artifactsDirectory, String cosPrefix,
|
||||||
boolean uploadEnabled, long categoryId) {
|
boolean uploadEnabled, boolean replaceExisting, long categoryId) {
|
||||||
if (categoryId <= 0) {
|
if (categoryId <= 0) {
|
||||||
throw new IllegalArgumentException("categoryId 必须为正数");
|
throw new IllegalArgumentException("categoryId 必须为正数");
|
||||||
}
|
}
|
||||||
@@ -57,6 +58,9 @@ public class SolidWorksJsonDatasetImportService implements JsonDatasetImportServ
|
|||||||
if (models.size() != sourceFiles.size()) {
|
if (models.size() != sourceFiles.size()) {
|
||||||
throw new IllegalArgumentException("JSON 与完整产物数量不一致: json=" + sourceFiles.size() + ", artifacts=" + models.size());
|
throw new IllegalArgumentException("JSON 与完整产物数量不一致: json=" + sourceFiles.size() + ", artifacts=" + models.size());
|
||||||
}
|
}
|
||||||
|
if (!replaceExisting) {
|
||||||
|
ensureModelsDoNotExist(models);
|
||||||
|
}
|
||||||
|
|
||||||
if (uploadEnabled) {
|
if (uploadEnabled) {
|
||||||
for (ImportedModel model : models) {
|
for (ImportedModel model : models) {
|
||||||
@@ -64,7 +68,9 @@ public class SolidWorksJsonDatasetImportService implements JsonDatasetImportServ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearExistingAnnotationData();
|
if (replaceExisting) {
|
||||||
|
clearExistingAnnotationData();
|
||||||
|
}
|
||||||
int historyCount = 0;
|
int historyCount = 0;
|
||||||
for (ImportedModel importedModel : models) {
|
for (ImportedModel importedModel : models) {
|
||||||
CadModel model = createModel(importedModel, cosPrefix, categoryId);
|
CadModel model = createModel(importedModel, cosPrefix, categoryId);
|
||||||
@@ -121,7 +127,10 @@ public class SolidWorksJsonDatasetImportService implements JsonDatasetImportServ
|
|||||||
Path versionRoot = manifestPath.getParent().getParent();
|
Path versionRoot = manifestPath.getParent().getParent();
|
||||||
JsonNode sourceRoot = readJson(versionRoot.resolve("source.feature-tree.json"));
|
JsonNode sourceRoot = readJson(versionRoot.resolve("source.feature-tree.json"));
|
||||||
String partName = requireText(text(sourceRoot, "part_name"), "转换产物缺少 part_name: " + versionRoot);
|
String partName = requireText(text(sourceRoot, "part_name"), "转换产物缺少 part_name: " + versionRoot);
|
||||||
if (!sourceFiles.containsKey(partName) || !partNames.add(partName)) {
|
if (!sourceFiles.containsKey(partName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!partNames.add(partName)) {
|
||||||
throw new IllegalArgumentException("转换产物无法唯一匹配 JSON: " + partName);
|
throw new IllegalArgumentException("转换产物无法唯一匹配 JSON: " + partName);
|
||||||
}
|
}
|
||||||
requireFiles(versionRoot, "source-json-rebuilt.step", "scene.glb", "topology.json", "thumbnail.svg",
|
requireFiles(versionRoot, "source-json-rebuilt.step", "scene.glb", "topology.json", "thumbnail.svg",
|
||||||
@@ -156,6 +165,20 @@ public class SolidWorksJsonDatasetImportService implements JsonDatasetImportServ
|
|||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureModelsDoNotExist(List<ImportedModel> models) {
|
||||||
|
Set<String> modelNames = models.stream().map(ImportedModel::partName).collect(java.util.stream.Collectors.toSet());
|
||||||
|
Set<String> sourceFilenames = models.stream()
|
||||||
|
.map(model -> model.partName() + ".SLDPRT")
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
Long count = cadModelMapper.selectCount(Wrappers.lambdaQuery(CadModel.class)
|
||||||
|
.in(CadModel::getModelName, modelNames)
|
||||||
|
.or()
|
||||||
|
.in(CadModel::getSourceSwFilename, sourceFilenames));
|
||||||
|
if (count != null && count > 0) {
|
||||||
|
throw new IllegalArgumentException("追加导入包含远程已存在模型,拒绝写入: duplicateCount=" + count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void uploadModelAssets(ImportedModel model, String cosPrefix) {
|
private void uploadModelAssets(ImportedModel model, String cosPrefix) {
|
||||||
List<Path> files;
|
List<Path> files;
|
||||||
try (Stream<Path> paths = Files.walk(model.versionRoot())) {
|
try (Stream<Path> paths = Files.walk(model.versionRoot())) {
|
||||||
@@ -194,6 +217,7 @@ public class SolidWorksJsonDatasetImportService implements JsonDatasetImportServ
|
|||||||
model.setSourceSwPath(trimTo(objectKey(cosPrefix, source, "source-json-rebuilt.step"), 1024));
|
model.setSourceSwPath(trimTo(objectKey(cosPrefix, source, "source-json-rebuilt.step"), 1024));
|
||||||
model.setFeatureTreeJsonPath(trimTo(objectKey(cosPrefix, source, "source.feature-tree.json"), 1024));
|
model.setFeatureTreeJsonPath(trimTo(objectKey(cosPrefix, source, "source.feature-tree.json"), 1024));
|
||||||
model.setViewPath(trimTo(objectKey(cosPrefix, source, "scene.glb"), 1024));
|
model.setViewPath(trimTo(objectKey(cosPrefix, source, "scene.glb"), 1024));
|
||||||
|
model.setThumbnailPath(trimTo(objectKey(cosPrefix, source, "thumbnail.svg"), 1024));
|
||||||
model.setApplicationScenario("");
|
model.setApplicationScenario("");
|
||||||
model.setMaterial("");
|
model.setMaterial("");
|
||||||
model.setManufacturingMethod("");
|
model.setManufacturingMethod("");
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ app:
|
|||||||
artifacts-directory: ${APP_JSON_DATASET_IMPORT_ARTIFACTS_DIRECTORY:}
|
artifacts-directory: ${APP_JSON_DATASET_IMPORT_ARTIFACTS_DIRECTORY:}
|
||||||
cos-prefix: ${APP_JSON_DATASET_IMPORT_COS_PREFIX:datasets/solidworks-rebuild-v1}
|
cos-prefix: ${APP_JSON_DATASET_IMPORT_COS_PREFIX:datasets/solidworks-rebuild-v1}
|
||||||
upload-enabled: ${APP_JSON_DATASET_IMPORT_UPLOAD_ENABLED:true}
|
upload-enabled: ${APP_JSON_DATASET_IMPORT_UPLOAD_ENABLED:true}
|
||||||
|
replace-existing: ${APP_JSON_DATASET_IMPORT_REPLACE_EXISTING:true}
|
||||||
category-id: ${APP_JSON_DATASET_IMPORT_CATEGORY_ID:1}
|
category-id: ${APP_JSON_DATASET_IMPORT_CATEGORY_ID:1}
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ class CadModelControllerTest {
|
|||||||
.andExpect(jsonPath("$.data.size").value(5))
|
.andExpect(jsonPath("$.data.size").value(5))
|
||||||
.andExpect(jsonPath("$.data.total").value(11))
|
.andExpect(jsonPath("$.data.total").value(11))
|
||||||
.andExpect(jsonPath("$.data.items[0].id").value(1))
|
.andExpect(jsonPath("$.data.items[0].id").value(1))
|
||||||
.andExpect(jsonPath("$.data.items[0].modelName").value("轴承座"));
|
.andExpect(jsonPath("$.data.items[0].modelName").value("轴承座"))
|
||||||
|
.andExpect(jsonPath("$.data.items[0].thumbnailPath").value("thumbnails/015133.svg"));
|
||||||
|
|
||||||
verify(cadModelService).listModels(new CadModelListQuery(2, 5, 7L, "success", "轴承"));
|
verify(cadModelService).listModels(new CadModelListQuery(2, 5, 7L, "success", "轴承"));
|
||||||
}
|
}
|
||||||
@@ -74,6 +75,7 @@ class CadModelControllerTest {
|
|||||||
.andExpect(jsonPath("$.code").value("OK"))
|
.andExpect(jsonPath("$.code").value("OK"))
|
||||||
.andExpect(jsonPath("$.data.id").value(1))
|
.andExpect(jsonPath("$.data.id").value(1))
|
||||||
.andExpect(jsonPath("$.data.modelName").value("连接板"))
|
.andExpect(jsonPath("$.data.modelName").value("连接板"))
|
||||||
|
.andExpect(jsonPath("$.data.thumbnailPath").value("thumbnails/015133.svg"))
|
||||||
.andExpect(jsonPath("$.data.createdAt").value("2026-07-08 10:00:00"));
|
.andExpect(jsonPath("$.data.createdAt").value("2026-07-08 10:00:00"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ class CadModelControllerTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -174,6 +177,7 @@ class CadModelControllerTest {
|
|||||||
"sw/015133.sldprt",
|
"sw/015133.sldprt",
|
||||||
"features/015133.json",
|
"features/015133.json",
|
||||||
"views/015133",
|
"views/015133",
|
||||||
|
"thumbnails/015133.svg",
|
||||||
"工业夹具",
|
"工业夹具",
|
||||||
"铝合金",
|
"铝合金",
|
||||||
"CNC",
|
"CNC",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class CadAnnotationServiceIntegrationTest {
|
|||||||
source_sw_path VARCHAR(1024) NOT NULL,
|
source_sw_path VARCHAR(1024) NOT NULL,
|
||||||
feature_tree_json_path VARCHAR(1024) NOT NULL DEFAULT '',
|
feature_tree_json_path VARCHAR(1024) NOT NULL DEFAULT '',
|
||||||
view_path VARCHAR(1024) NOT NULL DEFAULT '',
|
view_path VARCHAR(1024) NOT NULL DEFAULT '',
|
||||||
|
thumbnail_path VARCHAR(1024) NOT NULL DEFAULT '',
|
||||||
application_scenario VARCHAR(255) NOT NULL DEFAULT '',
|
application_scenario VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
material VARCHAR(255) NOT NULL DEFAULT '',
|
material VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
manufacturing_method VARCHAR(255) NOT NULL DEFAULT '',
|
manufacturing_method VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class CadModelServiceIntegrationTest {
|
|||||||
source_sw_path VARCHAR(1024) NOT NULL,
|
source_sw_path VARCHAR(1024) NOT NULL,
|
||||||
feature_tree_json_path VARCHAR(1024) NOT NULL DEFAULT '',
|
feature_tree_json_path VARCHAR(1024) NOT NULL DEFAULT '',
|
||||||
view_path VARCHAR(1024) NOT NULL DEFAULT '',
|
view_path VARCHAR(1024) NOT NULL DEFAULT '',
|
||||||
|
thumbnail_path VARCHAR(1024) NOT NULL DEFAULT '',
|
||||||
application_scenario VARCHAR(255) NOT NULL DEFAULT '',
|
application_scenario VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
material VARCHAR(255) NOT NULL DEFAULT '',
|
material VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
manufacturing_method VARCHAR(255) NOT NULL DEFAULT '',
|
manufacturing_method VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
@@ -160,6 +161,7 @@ class CadModelServiceIntegrationTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
"partial_failed",
|
"partial_failed",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -194,6 +196,7 @@ class CadModelServiceIntegrationTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
"done",
|
"done",
|
||||||
null
|
null
|
||||||
)))
|
)))
|
||||||
@@ -214,6 +217,7 @@ class CadModelServiceIntegrationTest {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
)))
|
)))
|
||||||
.isInstanceOf(BizException.class)
|
.isInstanceOf(BizException.class)
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ class SolidWorksJsonDatasetImportServiceTest {
|
|||||||
return 1;
|
return 1;
|
||||||
}).when(cadModelMapper).insert(any(CadModel.class));
|
}).when(cadModelMapper).insert(any(CadModel.class));
|
||||||
|
|
||||||
JsonDatasetImportService.ImportSummary summary = service.replaceDataset(
|
JsonDatasetImportService.ImportSummary summary = service.importDataset(
|
||||||
sourceDirectory, artifactsDirectory, "dataset-test", true, 7L);
|
sourceDirectory, artifactsDirectory, "dataset-test", true, true, 7L);
|
||||||
|
|
||||||
assertThat(summary).isEqualTo(new JsonDatasetImportService.ImportSummary(1, 2));
|
assertThat(summary).isEqualTo(new JsonDatasetImportService.ImportSummary(1, 2));
|
||||||
ArgumentCaptor<CadModel> modelCaptor = ArgumentCaptor.forClass(CadModel.class);
|
ArgumentCaptor<CadModel> modelCaptor = ArgumentCaptor.forClass(CadModel.class);
|
||||||
@@ -115,6 +115,7 @@ class SolidWorksJsonDatasetImportServiceTest {
|
|||||||
assertThat(modelCaptor.getValue().getFeatureTreeJsonPath()).isEqualTo("dataset-test/demo/source.feature-tree.json");
|
assertThat(modelCaptor.getValue().getFeatureTreeJsonPath()).isEqualTo("dataset-test/demo/source.feature-tree.json");
|
||||||
assertThat(modelCaptor.getValue().getSourceSwPath()).isEqualTo("dataset-test/demo/source-json-rebuilt.step");
|
assertThat(modelCaptor.getValue().getSourceSwPath()).isEqualTo("dataset-test/demo/source-json-rebuilt.step");
|
||||||
assertThat(modelCaptor.getValue().getViewPath()).isEqualTo("dataset-test/demo/scene.glb");
|
assertThat(modelCaptor.getValue().getViewPath()).isEqualTo("dataset-test/demo/scene.glb");
|
||||||
|
assertThat(modelCaptor.getValue().getThumbnailPath()).isEqualTo("dataset-test/demo/thumbnail.svg");
|
||||||
|
|
||||||
ArgumentCaptor<CadModelHistory> historyCaptor = ArgumentCaptor.forClass(CadModelHistory.class);
|
ArgumentCaptor<CadModelHistory> historyCaptor = ArgumentCaptor.forClass(CadModelHistory.class);
|
||||||
verify(cadModelHistoryMapper, org.mockito.Mockito.times(2)).insert(historyCaptor.capture());
|
verify(cadModelHistoryMapper, org.mockito.Mockito.times(2)).insert(historyCaptor.capture());
|
||||||
|
|||||||
Reference in New Issue
Block a user