diff --git a/backend/package.json b/backend/package.json index af06813..9795466 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,6 +7,8 @@ "dev": "nodemon --exec ts-node src/index.ts", "build": "tsc", "start": "node dist/index.js", + "seed:demo": "node scripts/seedDemoData.js", + "repair:mojibake": "node scripts/repairMojibake.js", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio" diff --git a/backend/scripts/repairMojibake.js b/backend/scripts/repairMojibake.js new file mode 100644 index 0000000..3e73857 --- /dev/null +++ b/backend/scripts/repairMojibake.js @@ -0,0 +1,70 @@ +let PrismaClient + +try { + ;({ PrismaClient } = require('@prisma/client')) +} catch { + ;({ PrismaClient } = require('/app/node_modules/@prisma/client')) +} + +const prisma = new PrismaClient() + +const TARGETS = [ + { table: 'site_config', field: 'site_name' }, + { table: '`user`', field: 'name' } +] + +const mojibakePattern = '[À-ÿ€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ]' +const chinesePattern = '[一-龥]' + +const buildFixedExpr = (field) => + `CONVERT(CAST(CONVERT(${field} USING latin1) AS BINARY) USING utf8mb4) COLLATE utf8mb4_unicode_ci` + +async function repairTarget({ table, field }) { + const fixedExpr = buildFixedExpr(field) + const countSql = ` + SELECT COUNT(*) AS count + FROM ${table} + WHERE ${field} REGEXP '${mojibakePattern}' + AND ${fixedExpr} REGEXP '${chinesePattern}' + AND ${field} <> ${fixedExpr} + ` + + const [countRow] = await prisma.$queryRawUnsafe(countSql) + const count = Number(countRow?.count || 0) + + if (!count) { + console.log(`${table}.${field}: 无需修复`) + return 0 + } + + const updateSql = ` + UPDATE ${table} + SET ${field} = ${fixedExpr} + WHERE ${field} REGEXP '${mojibakePattern}' + AND ${fixedExpr} REGEXP '${chinesePattern}' + AND ${field} <> ${fixedExpr} + ` + + await prisma.$executeRawUnsafe(updateSql) + console.log(`${table}.${field}: 已修复 ${count} 条`) + return count +} + +async function main() { + let updated = 0 + + for (const target of TARGETS) { + updated += await repairTarget(target) + } + + console.log(`完成:共修复 ${updated} 条乱码数据`) +} + +main() + .catch((error) => { + console.error('修复乱码失败:', error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/backend/scripts/seedDemoData.js b/backend/scripts/seedDemoData.js new file mode 100644 index 0000000..06a75a5 --- /dev/null +++ b/backend/scripts/seedDemoData.js @@ -0,0 +1,755 @@ +let PrismaClient + +try { + ;({ PrismaClient } = require('@prisma/client')) +} catch { + ;({ PrismaClient } = require('/app/node_modules/@prisma/client')) +} + +const prisma = new PrismaClient() +const TEST_TAG = '[测试数据]' + +const locationSeeds = [ + { key: 'hq', name: '总部机房', code: 'HQ01', description: `${TEST_TAG} 总部核心设备区` }, + { key: 'office', name: '行政办公室', code: 'OF01', description: `${TEST_TAG} 行政办公区` }, + { key: 'service', name: '客服办公区', code: 'CS01', description: `${TEST_TAG} 客服与运营办公区` }, + { key: 'warehouse', name: '仓储维修间', code: 'WH01', description: `${TEST_TAG} 仓储和维修测试区` } +] + +const departmentNameMap = { + management: '总经理室', + opsA: '运营中心A', + service: '客服中心', + warehouse: '仓储中心', + finance: '财务部' +} + +const personDeptKeywordMap = { + manager: '总经理室', + ops: '运营中心A', + service: '客服中心', + warehouse: '仓储中心', + finance: '财务' +} + +const dayMs = 24 * 60 * 60 * 1000 +const dateOffset = (days) => new Date(Date.now() + days * dayMs) + +const normalizeShortName = (value) => { + const normalized = String(value || '').trim().toUpperCase() + return normalized.replace(/[^A-Z0-9]/g, '') +} + +const buildEquipmentCode = (shortName, locationId, sequence) => + `${normalizeShortName(shortName)}-${String(locationId).padStart(4, '0')}-${String(sequence).padStart(5, '0')}` + +const calculateValues = (unitPrice, totalQuantity, remainingQuantity) => ({ + total_value: Number(unitPrice) * Number(totalQuantity), + borrowed_value: Number(unitPrice) * (Number(totalQuantity) - Number(remainingQuantity)), + available_value: Number(unitPrice) * Number(remainingQuantity) +}) + +const ensureLocation = async (seed) => { + const existing = + (await prisma.storageLocation.findFirst({ where: { code: seed.code } })) || + (await prisma.storageLocation.findFirst({ where: { name: seed.name } })) + + if (existing) { + return prisma.storageLocation.update({ + where: { id: existing.id }, + data: { + name: seed.name, + code: seed.code, + status: 'active', + description: seed.description + } + }) + } + + return prisma.storageLocation.create({ + data: { + name: seed.name, + code: seed.code, + status: 'active', + description: seed.description + } + }) +} + +const ensureEquipment = async (seed) => { + const values = calculateValues(seed.unit_price, seed.total_quantity, seed.remaining_quantity) + const existing = await prisma.equipment.findUnique({ + where: { equipment_code: seed.equipment_code } + }) + + const data = { + equipment_code: seed.equipment_code, + equipment_name: seed.equipment_name, + equipment_model: seed.equipment_model, + equipment_type: seed.equipment_type, + code_generation_mode: 'manual', + status: seed.status, + total_quantity: seed.total_quantity, + remaining_quantity: seed.remaining_quantity, + unit_price: seed.unit_price, + total_value: values.total_value, + borrowed_value: values.borrowed_value, + available_value: values.available_value, + total_value_with_children: seed.total_value_with_children || values.total_value, + description: seed.description, + purchase_date: seed.purchase_date, + supplier: seed.supplier, + storage_location_id: seed.storage_location_id, + department_id: seed.department_id, + asset_type_id: seed.asset_type_id, + parent_id: seed.parent_id || null, + mount_status: seed.mount_status || 'none' + } + + if (existing) { + return prisma.equipment.update({ + where: { id: existing.id }, + data + }) + } + + return prisma.equipment.create({ data }) +} + +const ensureBorrowRecord = async (seed) => { + const existing = await prisma.borrowRecord.findFirst({ + where: { + equipment_code: seed.equipment_code, + person_id: seed.person_id, + status: seed.status, + notes: { contains: TEST_TAG } + } + }) + + if (existing) { + return existing + } + + return prisma.borrowRecord.create({ + data: seed + }) +} + +const ensureMaintenanceRecord = async (seed) => { + const existing = await prisma.maintenanceRecord.findFirst({ + where: { + equipment_code: seed.equipment_code, + issue_description: seed.issue_description + } + }) + + if (existing) { + return prisma.maintenanceRecord.update({ + where: { id: existing.id }, + data: seed + }) + } + + return prisma.maintenanceRecord.create({ data: seed }) +} + +const ensureDisposalRecord = async (seed) => { + const existing = await prisma.disposalRecord.findFirst({ + where: { + equipment_code: seed.equipment_code, + disposal_reason: seed.disposal_reason + } + }) + + if (existing) { + return prisma.disposalRecord.update({ + where: { id: existing.id }, + data: seed + }) + } + + return prisma.disposalRecord.create({ data: seed }) +} + +const ensureMountHistory = async (seed) => { + const existing = await prisma.mountHistory.findFirst({ + where: { + child_id: seed.child_id, + parent_id: seed.parent_id, + operation_type: seed.operation_type, + notes: seed.notes + } + }) + + if (existing) { + return existing + } + + return prisma.mountHistory.create({ data: seed }) +} + +const ensureOperationLog = async (seed) => { + const existing = await prisma.operationLog.findFirst({ + where: { + operation_type: seed.operation_type, + equipment_code: seed.equipment_code || null, + operation_details: seed.operation_details + } + }) + + if (existing) { + return existing + } + + return prisma.operationLog.create({ data: seed }) +} + +async function main() { + const admin = await prisma.user.findFirst({ + where: { username: 'admin' }, + select: { id: true, username: true, name: true } + }) + + if (!admin) { + throw new Error('未找到 admin 用户,无法写入测试数据') + } + + const assetTypeNormalization = [ + { id: 1, name: '主机', short_name: 'PC' }, + { id: 2, name: 'cpu', short_name: 'CPU' }, + { id: 3, name: '其他外设', short_name: 'QT' }, + { id: 4, name: '内存条', short_name: 'NCT' }, + { id: 5, name: '显卡', short_name: 'XK' }, + { id: 6, name: '显示器', short_name: 'XSQ' }, + { id: 7, name: '硬盘', short_name: 'YP' }, + { id: 8, name: '笔记本', short_name: 'PC' } + ] + + for (const item of assetTypeNormalization) { + await prisma.assetType.update({ + where: { id: item.id }, + data: { + name: item.name, + short_name: item.short_name, + status: 'active' + } + }) + } + + const assetTypes = await prisma.assetType.findMany({ + select: { id: true, name: true, short_name: true, can_be_parent: true, can_be_child: true } + }) + const assetTypeMap = new Map(assetTypes.map((item) => [item.name, item])) + + const requiredTypes = ['主机', 'cpu', '其他外设', '内存条', '显卡', '显示器', '硬盘', '笔记本'] + for (const typeName of requiredTypes) { + if (!assetTypeMap.has(typeName)) { + throw new Error(`缺少资产类型:${typeName}`) + } + } + + await prisma.assetType.updateMany({ + where: { name: { in: ['主机', '笔记本'] } }, + data: { can_be_parent: true } + }) + await prisma.assetType.updateMany({ + where: { name: { in: ['cpu', '其他外设', '内存条', '显卡', '硬盘'] } }, + data: { can_be_child: true } + }) + + const locationMap = new Map() + for (const seed of locationSeeds) { + const location = await ensureLocation(seed) + locationMap.set(seed.key, location) + } + + const departmentMap = new Map() + for (const [key, name] of Object.entries(departmentNameMap)) { + const department = await prisma.department.findFirst({ + where: { status: 'active', name } + }) + if (!department) { + throw new Error(`未找到启用部门:${name}`) + } + departmentMap.set(key, department) + } + + const personMap = new Map() + for (const [key, keyword] of Object.entries(personDeptKeywordMap)) { + const person = await prisma.person.findFirst({ + where: { + status: 'active', + department: { contains: keyword } + }, + orderBy: { id: 'asc' } + }) + + if (!person) { + throw new Error(`未找到可用测试人员,部门关键字:${keyword}`) + } + personMap.set(key, person) + } + + const siteConfigCount = await prisma.siteConfig.count() + if (siteConfigCount === 0) { + await prisma.siteConfig.create({ + data: { + site_name: '简尚固定资产管理系统', + theme_color: '#2563eb', + updated_by: admin.id + } + }) + } + + const code = (typeName, locationKey, sequence) => + buildEquipmentCode( + assetTypeMap.get(typeName).short_name, + locationMap.get(locationKey).id, + sequence + ) + + const equipmentSeeds = [ + { + equipment_code: code('主机', 'hq', 1), + equipment_name: '研发部测试主机', + equipment_model: 'ThinkCentre M900', + equipment_type: '主机', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 5899, + description: `${TEST_TAG} 用于二维码打印、挂载测试的主机`, + purchase_date: new Date('2025-01-15'), + supplier: '联想授权渠道', + storage_location_id: locationMap.get('hq').id, + department_id: departmentMap.get('opsA').id, + asset_type_id: assetTypeMap.get('主机').id + }, + { + equipment_code: code('主机', 'service', 1), + equipment_name: '客服前台主机', + equipment_model: 'Dell OptiPlex 7090', + equipment_type: '主机', + status: '已借出', + total_quantity: 1, + remaining_quantity: 0, + unit_price: 4680, + description: `${TEST_TAG} 当前借出状态演示`, + purchase_date: new Date('2025-02-20'), + supplier: '戴尔企业采购', + storage_location_id: locationMap.get('service').id, + department_id: departmentMap.get('service').id, + asset_type_id: assetTypeMap.get('主机').id + }, + { + equipment_code: code('笔记本', 'office', 1), + equipment_name: '管理层备用笔记本', + equipment_model: 'ThinkPad T14', + equipment_type: '笔记本', + status: '已借出', + total_quantity: 1, + remaining_quantity: 0, + unit_price: 6999, + description: `${TEST_TAG} 长期借用演示`, + purchase_date: new Date('2025-03-18'), + supplier: '联想官方商城', + storage_location_id: locationMap.get('office').id, + department_id: departmentMap.get('management').id, + asset_type_id: assetTypeMap.get('笔记本').id + }, + { + equipment_code: code('笔记本', 'office', 2), + equipment_name: '财务外勤笔记本', + equipment_model: 'HP ProBook 440', + equipment_type: '笔记本', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 5299, + description: `${TEST_TAG} 可借用笔记本`, + purchase_date: new Date('2024-12-01'), + supplier: '惠普商用渠道', + storage_location_id: locationMap.get('office').id, + department_id: departmentMap.get('finance').id, + asset_type_id: assetTypeMap.get('笔记本').id + }, + { + equipment_code: code('显示器', 'service', 1), + equipment_name: '客服工位显示器', + equipment_model: 'AOC 24B2XH', + equipment_type: '显示器', + status: '维护中', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 899, + description: `${TEST_TAG} 维修流程演示`, + purchase_date: new Date('2024-11-06'), + supplier: 'AOC 官方旗舰店', + storage_location_id: locationMap.get('service').id, + department_id: departmentMap.get('service').id, + asset_type_id: assetTypeMap.get('显示器').id + }, + { + equipment_code: code('显示器', 'office', 1), + equipment_name: '财务双屏显示器', + equipment_model: 'Dell P2422H', + equipment_type: '显示器', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 1199, + description: `${TEST_TAG} 正常在库显示器`, + purchase_date: new Date('2025-01-08'), + supplier: '戴尔企业采购', + storage_location_id: locationMap.get('office').id, + department_id: departmentMap.get('finance').id, + asset_type_id: assetTypeMap.get('显示器').id + }, + { + equipment_code: code('其他外设', 'warehouse', 1), + equipment_name: '标签打印机', + equipment_model: 'Xprinter XP-H500E', + equipment_type: '其他外设', + status: '待报废', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 899, + description: `${TEST_TAG} 报废流程演示设备`, + purchase_date: new Date('2023-08-05'), + supplier: '芯烨官方店', + storage_location_id: locationMap.get('warehouse').id, + department_id: departmentMap.get('warehouse').id, + asset_type_id: assetTypeMap.get('其他外设').id + }, + { + equipment_code: code('其他外设', 'office', 1), + equipment_name: '财务扫描枪', + equipment_model: 'Honeywell 1470g', + equipment_type: '其他外设', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 560, + description: `${TEST_TAG} 已完成维修历史演示`, + purchase_date: new Date('2024-06-11'), + supplier: '霍尼韦尔代理商', + storage_location_id: locationMap.get('office').id, + department_id: departmentMap.get('finance').id, + asset_type_id: assetTypeMap.get('其他外设').id + }, + { + equipment_code: code('硬盘', 'warehouse', 1), + equipment_name: '淘汰机械硬盘', + equipment_model: 'Seagate 1TB HDD', + equipment_type: '硬盘', + status: '已报废', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 260, + description: `${TEST_TAG} 已完成报废演示设备`, + purchase_date: new Date('2022-02-10'), + supplier: '希捷经销商', + storage_location_id: locationMap.get('warehouse').id, + department_id: departmentMap.get('warehouse').id, + asset_type_id: assetTypeMap.get('硬盘').id + }, + { + equipment_code: code('cpu', 'hq', 1), + equipment_name: '主机CPU', + equipment_model: 'Intel i5-12400', + equipment_type: 'cpu', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 1380, + description: `${TEST_TAG} 已挂载到研发部测试主机`, + purchase_date: new Date('2025-01-15'), + supplier: '英特尔代理', + storage_location_id: locationMap.get('hq').id, + department_id: departmentMap.get('opsA').id, + asset_type_id: assetTypeMap.get('cpu').id, + mount_status: 'mounted' + }, + { + equipment_code: code('内存条', 'hq', 1), + equipment_name: '主机内存', + equipment_model: 'Kingston DDR4 16GB', + equipment_type: '内存条', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 320, + description: `${TEST_TAG} 已挂载到研发部测试主机`, + purchase_date: new Date('2025-01-15'), + supplier: '金士顿经销商', + storage_location_id: locationMap.get('hq').id, + department_id: departmentMap.get('opsA').id, + asset_type_id: assetTypeMap.get('内存条').id, + mount_status: 'mounted' + }, + { + equipment_code: code('显卡', 'hq', 1), + equipment_name: '主机显卡', + equipment_model: 'NVIDIA T400', + equipment_type: '显卡', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 980, + description: `${TEST_TAG} 已挂载到研发部测试主机`, + purchase_date: new Date('2025-01-15'), + supplier: '英伟达代理', + storage_location_id: locationMap.get('hq').id, + department_id: departmentMap.get('opsA').id, + asset_type_id: assetTypeMap.get('显卡').id, + mount_status: 'mounted' + }, + { + equipment_code: code('硬盘', 'hq', 1), + equipment_name: '主机固态硬盘', + equipment_model: 'Samsung PM9A1 1TB', + equipment_type: '硬盘', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 699, + description: `${TEST_TAG} 已挂载到研发部测试主机`, + purchase_date: new Date('2025-01-15'), + supplier: '三星渠道', + storage_location_id: locationMap.get('hq').id, + department_id: departmentMap.get('opsA').id, + asset_type_id: assetTypeMap.get('硬盘').id, + mount_status: 'mounted' + }, + { + equipment_code: code('其他外设', 'service', 2), + equipment_name: '客服键鼠套装', + equipment_model: 'Logitech MK270', + equipment_type: '其他外设', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 149, + description: `${TEST_TAG} 已完成借还历史演示`, + purchase_date: new Date('2025-02-01'), + supplier: '罗技旗舰店', + storage_location_id: locationMap.get('service').id, + department_id: departmentMap.get('service').id, + asset_type_id: assetTypeMap.get('其他外设').id + }, + { + equipment_code: code('其他外设', 'office', 2), + equipment_name: '会议摄像头', + equipment_model: 'Logitech C930e', + equipment_type: '其他外设', + status: '可用', + total_quantity: 1, + remaining_quantity: 1, + unit_price: 799, + description: `${TEST_TAG} 正常在库会议设备`, + purchase_date: new Date('2024-10-09'), + supplier: '罗技企业采购', + storage_location_id: locationMap.get('office').id, + department_id: departmentMap.get('management').id, + asset_type_id: assetTypeMap.get('其他外设').id + } + ] + + const equipmentMap = new Map() + for (const seed of equipmentSeeds) { + const equipment = await ensureEquipment(seed) + equipmentMap.set(seed.equipment_code, equipment) + } + + const parentHost = equipmentMap.get(code('主机', 'hq', 1)) + const mountedChildren = [ + equipmentMap.get(code('cpu', 'hq', 1)), + equipmentMap.get(code('内存条', 'hq', 1)), + equipmentMap.get(code('显卡', 'hq', 1)), + equipmentMap.get(code('硬盘', 'hq', 1)) + ] + + for (const child of mountedChildren) { + await prisma.equipment.update({ + where: { id: child.id }, + data: { + parent_id: parentHost.id, + mount_status: 'mounted' + } + }) + + await ensureMountHistory({ + child_id: child.id, + parent_id: parentHost.id, + operation_type: 'mount', + operated_by: admin.id, + notes: `${TEST_TAG} 初始化挂载` + }) + } + + const childValue = mountedChildren.reduce((sum, item) => sum + Number(item.unit_price), 0) + await prisma.equipment.update({ + where: { id: parentHost.id }, + data: { + total_value_with_children: Number(parentHost.unit_price) + childValue + } + }) + + const borrowedHost = equipmentMap.get(code('主机', 'service', 1)) + const borrowedLaptop = equipmentMap.get(code('笔记本', 'office', 1)) + const historicalPeripheral = equipmentMap.get(code('其他外设', 'service', 2)) + const maintenanceMonitor = equipmentMap.get(code('显示器', 'service', 1)) + const disposedDisk = equipmentMap.get(code('硬盘', 'warehouse', 1)) + const disposalPrinter = equipmentMap.get(code('其他外设', 'warehouse', 1)) + const maintainedScanner = equipmentMap.get(code('其他外设', 'office', 1)) + + await ensureBorrowRecord({ + equipment_id: borrowedHost.id, + equipment_code: borrowedHost.equipment_code, + person_id: personMap.get('service').id, + person_name: personMap.get('service').real_name, + person_nickname: personMap.get('service').nickname, + person_identifier: personMap.get('service').id_number, + borrow_time: dateOffset(-12), + expected_return_time: dateOffset(3), + is_long_term: false, + return_time: null, + status: '已借出', + return_condition: '正常', + notes: `${TEST_TAG} 客服工位借用中` + }) + + await ensureBorrowRecord({ + equipment_id: borrowedLaptop.id, + equipment_code: borrowedLaptop.equipment_code, + person_id: personMap.get('manager').id, + person_name: personMap.get('manager').real_name, + person_nickname: personMap.get('manager').nickname, + person_identifier: personMap.get('manager').id_number, + borrow_time: dateOffset(-25), + expected_return_time: null, + is_long_term: true, + return_time: null, + status: '已借出', + return_condition: '正常', + notes: `${TEST_TAG} 管理层长期领用` + }) + + await ensureBorrowRecord({ + equipment_id: historicalPeripheral.id, + equipment_code: historicalPeripheral.equipment_code, + person_id: personMap.get('ops').id, + person_name: personMap.get('ops').real_name, + person_nickname: personMap.get('ops').nickname, + person_identifier: personMap.get('ops').id_number, + borrow_time: dateOffset(-40), + expected_return_time: dateOffset(-33), + is_long_term: false, + return_time: dateOffset(-31), + status: '已归还', + return_condition: '正常', + notes: `${TEST_TAG} 已归还历史记录` + }) + + await ensureMaintenanceRecord({ + equipment_id: maintenanceMonitor.id, + equipment_code: maintenanceMonitor.equipment_code, + issue_description: `${TEST_TAG} 屏幕闪烁,间歇性黑屏`, + repair_vendor: '常州本地维保点', + estimated_cost: 180, + actual_cost: null, + start_date: dateOffset(-2), + expected_end_date: dateOffset(5), + actual_end_date: null, + status: 'in_progress', + repair_result: null, + created_by: admin.id + }) + + await ensureMaintenanceRecord({ + equipment_id: maintainedScanner.id, + equipment_code: maintainedScanner.equipment_code, + issue_description: `${TEST_TAG} 扫描头识别异常`, + repair_vendor: '苏州售后中心', + estimated_cost: 120, + actual_cost: 100, + start_date: dateOffset(-18), + expected_end_date: dateOffset(-12), + actual_end_date: dateOffset(-13), + status: 'completed', + repair_result: '更换扫描头后恢复正常', + created_by: admin.id + }) + + await ensureDisposalRecord({ + equipment_id: disposalPrinter.id, + equipment_code: disposalPrinter.equipment_code, + disposal_reason: `${TEST_TAG} 主板老化,维修成本高于残值`, + disposal_method: '报废', + estimated_value: 80, + actual_value: null, + status: 'pending', + applied_by: admin.id, + applied_at: dateOffset(-1), + approved_by: null, + approved_at: null, + completed_at: null, + notes: `${TEST_TAG} 待审批报废单` + }) + + await ensureDisposalRecord({ + equipment_id: disposedDisk.id, + equipment_code: disposedDisk.equipment_code, + disposal_reason: `${TEST_TAG} 盘体异响且坏道严重`, + disposal_method: '报废', + estimated_value: 20, + actual_value: 0, + status: 'completed', + applied_by: admin.id, + applied_at: dateOffset(-30), + approved_by: admin.id, + approved_at: dateOffset(-28), + completed_at: dateOffset(-25), + notes: `${TEST_TAG} 已完成报废处置` + }) + + await ensureOperationLog({ + user_id: admin.id, + username: admin.username, + operation_type: '初始化测试数据', + equipment_code: null, + equipment_id: null, + operation_details: `${TEST_TAG} 写入演示资产、借用、维修、报废数据`, + ip_address: null + }) + + await ensureOperationLog({ + user_id: admin.id, + username: admin.username, + operation_type: '初始化挂载关系', + equipment_code: parentHost.equipment_code, + equipment_id: parentHost.id, + operation_details: `${TEST_TAG} 为主机挂载 CPU/内存/显卡/硬盘`, + ip_address: null + }) + + const summary = { + storage_locations: await prisma.storageLocation.count(), + equipments: await prisma.equipment.count(), + borrow_records: await prisma.borrowRecord.count(), + maintenance_records: await prisma.maintenanceRecord.count(), + disposal_records: await prisma.disposalRecord.count(), + mounted_children: await prisma.equipment.count({ where: { parent_id: parentHost.id } }) + } + + console.log(JSON.stringify(summary, null, 2)) +} + +main() + .catch((error) => { + console.error(error) + process.exitCode = 1 + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/backend/src/services/hrSyncService.ts b/backend/src/services/hrSyncService.ts index 887de48..12391d6 100644 --- a/backend/src/services/hrSyncService.ts +++ b/backend/src/services/hrSyncService.ts @@ -190,6 +190,14 @@ const getSourceDepartments = async (pool: any): Promise => { })) } +const parseSourceDeptIds = (value: string | null) => + (value || '') + .split('|') + .map((item) => item.trim()) + .filter(Boolean) + .map((item) => Number(item)) + .filter((item) => !Number.isNaN(item)) + const getSourcePersons = async (pool: any): Promise => { const result = await pool .request() @@ -251,6 +259,35 @@ const getSourcePersons = async (pool: any): Promise => { .filter((item: SourcePerson) => isFormalEmployee(item)) } +const filterRelevantDepartments = ( + allDepartments: SourceDepartment[], + sourcePersons: SourcePerson[] +): SourceDepartment[] => { + const relevantIds = new Set() + const departmentMap = new Map(allDepartments.map((item) => [item.sourceGroupId, item])) + + for (const person of sourcePersons) { + if (person.sourceMainDepartmentId) { + relevantIds.add(person.sourceMainDepartmentId) + } + + for (const deptId of parseSourceDeptIds(person.sourceDeptIds)) { + relevantIds.add(deptId) + } + } + + const scopedIds = new Set(relevantIds) + for (const deptId of relevantIds) { + let current = departmentMap.get(deptId) + while (current?.sourceParentGroupId) { + scopedIds.add(current.sourceParentGroupId) + current = departmentMap.get(current.sourceParentGroupId) + } + } + + return allDepartments.filter((item) => scopedIds.has(item.sourceGroupId)) +} + const syncDepartments = async (sourceDepartments: SourceDepartment[]) => { const existing = await prisma.department.findMany({ where: { source_system: SOURCE_SYSTEM }, @@ -454,14 +491,16 @@ const writeSyncLog = async (result: HrSyncResult, operator = 'system-sync') => { export const previewHrSync = async () => { const pool = await sql.connect(getSqlServerConfig()) try { - const [departments, persons] = await Promise.all([ + const [allDepartments, persons] = await Promise.all([ getSourceDepartments(pool), getSourcePersons(pool) ]) + const departments = filterRelevantDepartments(allDepartments, persons) return { departments: { total: departments.length, + raw_total: allDepartments.length, sample: departments.slice(0, 10) }, persons: { @@ -477,10 +516,11 @@ export const previewHrSync = async () => { export const runHrSync = async (operator = 'system-sync') => { const pool = await sql.connect(getSqlServerConfig()) try { - const [departments, persons] = await Promise.all([ + const [allDepartments, persons] = await Promise.all([ getSourceDepartments(pool), getSourcePersons(pool) ]) + const departments = filterRelevantDepartments(allDepartments, persons) const departmentResult = await syncDepartments(departments) const personResult = await syncPersons(persons) diff --git a/frontend/nginx.conf b/frontend/nginx.conf index a52ca15..640ea6e 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,6 +1,7 @@ server { listen 80; server_name localhost; + charset utf-8; root /usr/share/nginx/html; index index.html; diff --git a/frontend/src/views/settings/components/DepartmentManagement.vue b/frontend/src/views/settings/components/DepartmentManagement.vue index 6dfc4e2..5f00076 100644 --- a/frontend/src/views/settings/components/DepartmentManagement.vue +++ b/frontend/src/views/settings/components/DepartmentManagement.vue @@ -1,7 +1,10 @@ diff --git a/frontend/src/views/settings/components/UserManagement.vue b/frontend/src/views/settings/components/UserManagement.vue index 133c388..eaca98f 100644 --- a/frontend/src/views/settings/components/UserManagement.vue +++ b/frontend/src/views/settings/components/UserManagement.vue @@ -1,7 +1,10 @@