Files
biz-api-cloud-zax/scripts/gen-customer-recipe-sql.js
2026-07-17 17:29:36 +08:00

218 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const XLSX = require('xlsx');
const files = [
{ file: '大荤.xlsx', cat: '大荤' },
{ file: '小荤.xlsx', cat: '小荤' },
{ file: '素菜.xlsx', cat: '素菜' },
];
const TRAIL_RE = /^(.+?)\s*(\d+(?:\.\d+)?)\s*(斤|只|包|瓶|盒|袋|根)$/;
const LEAD_NUM_RE = /^(\d+(?:\.\d+)?)\s*(斤|只|条|包|瓶|盒)?(.+)$/;
const LEAD_CN_RE = /^([一二三四五六七八九十两]+)\s*(条|斤|只|包)?(.+)$/;
const CN_NUM = { 一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9, 十: 10 };
function escapeSql(s) {
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "''");
}
function cleanName(raw) {
return String(raw || '')
.replace(/(/g, '(')
.replace(/)/g, ')')
.replace(/\s+/g, ' ')
.trim()
.replace(/\(斤\)/g, '')
.replace(/\(盒\)/g, '')
.replace(/\(块\)/g, '')
.trim();
}
function parseMaterialCell(nameRaw, qtyRaw) {
let name = String(nameRaw || '').trim();
if (!name) return null;
let qty = qtyRaw === '' || qtyRaw === null || qtyRaw === undefined ? null : qtyRaw;
let unitHint = null;
if (typeof qty === 'string') {
const qm = String(qty).trim().match(/^(\d+(?:\.\d+)?)\s*(斤|只|包|瓶|盒|袋|个)?$/);
if (qm) {
qty = Number(qm[1]);
if (qm[2]) unitHint = qm[2];
} else if (String(qty).includes('包')) {
const m = String(qty).match(/(\d+)/);
qty = m ? Number(m[1]) : null;
unitHint = '包';
} else {
qty = null;
}
}
let m = name.match(TRAIL_RE);
if (m) {
return {
name: cleanName(m[1]),
quantity: qty != null ? Number(qty) : Number(m[2]),
unit: m[3],
};
}
if (/^\d/.test(name)) {
m = name.match(LEAD_NUM_RE);
if (m && m[3] && /[\u4e00-\u9fa5]/.test(m[3])) {
// 1.5条肉 -> 条肉
const matName = m[2] === '条' ? cleanName(m[2] + m[3]) : cleanName(m[3]);
return {
name: matName,
quantity: qty != null ? Number(qty) : Number(m[1]),
unit: m[2] === '条' ? '条' : m[2] || '斤',
};
}
}
m = name.match(LEAD_CN_RE);
if (m && m[3] && CN_NUM[m[1]]) {
// 两条肉 -> 条肉
const matName = m[2] === '条' ? cleanName(m[2] + m[3]) : cleanName(m[3]);
return {
name: matName,
quantity: qty != null ? Number(qty) : CN_NUM[m[1]],
unit: m[2] || '条',
};
}
// 黑木耳0
name = name.replace(/([\u4e00-\u9fa5])0+$/u, '$1').trim();
const n = cleanName(name);
if (!n) return null;
let unit = unitHint || '斤';
if ((n === '鸡蛋' || n === '鹌鹑蛋') && qty != null && Number(qty) >= 1) unit = '个';
return {
name: n,
quantity: qty != null && !Number.isNaN(Number(qty)) ? Number(qty) : null,
unit,
};
}
const materialMap = new Map();
const recipes = [];
let matSeq = 904000001;
let recipeSeq = 905000001;
let linkSeq = 906000001;
function getOrCreateMaterial(parsed) {
const key = parsed.name;
if (!materialMap.has(key)) {
materialMap.set(key, { id: matSeq++, name: key, unit: parsed.unit || '斤' });
} else {
const cur = materialMap.get(key);
if (cur.unit === '斤' && parsed.unit && parsed.unit !== '斤') cur.unit = parsed.unit;
}
return materialMap.get(key);
}
for (const f of files) {
const wb = XLSX.readFile('C:/Users/Administrator/Desktop/' + f.file);
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1, defval: '' });
for (let i = 1; i < rows.length; i++) {
const r = rows[i];
const dishName = String(r[1] || '').trim();
if (!dishName) continue;
const mats = [];
for (let c = 2; c <= 10; c += 2) {
const parsed = parseMaterialCell(r[c], r[c + 1]);
if (!parsed || !parsed.name) continue;
const mat = getOrCreateMaterial(parsed);
mats.push({
materialId: mat.id,
name: mat.name,
quantity: parsed.quantity,
sort: mats.length,
});
}
recipes.push({
id: recipeSeq++,
name: dishName,
category: f.cat,
materials: mats,
});
}
}
const out = [];
out.push('-- =============================================');
out.push('-- 客户食材+菜谱导入(大荤/小荤/素菜 Excel)');
out.push('-- 生成时间: ' + new Date().toISOString());
out.push('-- ID 段: 食材 904xxxxxx / 菜谱 905xxxxxx / 明细 906xxxxxx');
out.push('-- 请确认与库中已有 ID 不冲突;tenant_id=1;material_type=2(线上食材)');
out.push('-- 用量:按客户原表“克重”数值写入(业务习惯多为斤),单位写入食材 unit');
out.push('-- 菜谱 remark = 分类(大荤/小荤/素菜);无食材的菜谱仍导入菜名');
out.push('-- =============================================');
out.push('');
out.push('-- ---------- 1. 食材(' + materialMap.size + ' 种) ----------');
out.push('INSERT INTO `canteen_erp_material`');
out.push('(`id`, `tenant_id`, `dept_id`, `name`, `unit`, `material_type`, `nutrition_tags`, `del_flag`, `create_by`, `create_time`)');
out.push('VALUES');
const mats = [...materialMap.values()].sort((a, b) => a.id - b.id);
mats.forEach((m, idx) => {
const comma = idx === mats.length - 1 ? ';' : ',';
out.push(`(${m.id}, 1, NULL, '${escapeSql(m.name)}', '${escapeSql(m.unit)}', 2, NULL, '0', 'admin', NOW())${comma}`);
});
out.push('');
out.push('-- ---------- 2. 排菜菜谱(' + recipes.length + ' 道) ----------');
out.push('INSERT INTO `canteen_menu_recipe`');
out.push('(`id`, `tenant_id`, `dept_id`, `merchant_id`, `name`, `remark`, `status`, `del_flag`, `create_by`, `create_time`)');
out.push('VALUES');
recipes.forEach((r, idx) => {
const comma = idx === recipes.length - 1 ? ';' : ',';
out.push(`(${r.id}, 1, NULL, NULL, '${escapeSql(r.name)}', '${escapeSql(r.category)}', 0, '0', 'admin', NOW())${comma}`);
});
out.push('');
out.push('-- ---------- 3. 菜谱食材明细 ----------');
const links = [];
for (const r of recipes) {
for (const m of r.materials) {
links.push({
id: linkSeq++,
recipeId: r.id,
materialId: m.materialId,
quantity: m.quantity,
sort: m.sort,
comment: `${r.category}-${r.name}-${m.name}`,
});
}
}
out.push('INSERT INTO `canteen_menu_recipe_material` (`id`, `recipe_id`, `material_id`, `quantity`, `sort_order`, `create_time`) VALUES');
links.forEach((l, idx) => {
let qtySql = 'NULL';
if (l.quantity != null && !Number.isNaN(l.quantity)) {
qtySql = Number.isInteger(l.quantity)
? l.quantity.toFixed(2)
: String(Math.round(l.quantity * 1000) / 1000);
}
const comma = idx === links.length - 1 ? ';' : ',';
out.push(`(${l.id}, ${l.recipeId}, ${l.materialId}, ${qtySql}, ${l.sort}, NOW())${comma} -- ${escapeSql(l.comment)}`);
});
out.push('');
out.push('-- 统计: 食材 ' + mats.length + ' / 菜谱 ' + recipes.length + ' / 明细 ' + links.length);
out.push('-- 按分类: ' + files.map((f) => f.cat + '=' + recipes.filter((r) => r.category === f.cat).length).join(', '));
const path = 'C:/workcode/java/biz-api-cloud-zax/db/1030pjcloudx_canteen_customer_recipe_import.sql';
fs.writeFileSync(path, out.join('\n'), 'utf8');
console.log('written', path);
console.log('materials', mats.length, 'recipes', recipes.length, 'links', links.length);
console.log(
'empty-material recipes:',
recipes.filter((r) => r.materials.length === 0).map((r) => r.category + ':' + r.name).join(', '),
);
console.log('sample materials:', mats.slice(0, 20).map((m) => m.name + '(' + m.unit + ')').join(', '));