origin/apps/server/src/models/transform/transform.service.ts

542 lines
18 KiB
TypeScript
Executable File
Raw 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.

import { Injectable, Logger } from '@nestjs/common';
import * as ExcelJS from 'exceljs';
import { TransformMethodSchema, db, Prisma, Staff } from '@nice/common';
import dayjs from 'dayjs';
import * as argon2 from 'argon2';
import { TaxonomyService } from '@server/models/taxonomy/taxonomy.service';
import { uploadFile } from '@server/utils/tool';
import { DepartmentService } from '../department/department.service';
import { StaffService } from '../staff/staff.service';
import { z, ZodError } from 'zod';
import { deleteByPattern } from '@server/utils/redis/utils';
class TreeNode {
value: string;
children: TreeNode[];
constructor(value: string) {
this.value = value;
this.children = [];
}
addChild(childValue: string): TreeNode {
let newChild = undefined;
if (this.children.findIndex((child) => child.value === childValue) === -1) {
newChild = new TreeNode(childValue);
this.children.push(newChild);
}
return this.children.find((child) => child.value === childValue);
}
}
@Injectable()
export class TransformService {
constructor(
private readonly departmentService: DepartmentService,
private readonly staffService: StaffService,
private readonly taxonomyService: TaxonomyService,
) {}
private readonly logger = new Logger(TransformService.name);
excelDateToISO(excelDate: number) {
// 设置 Excel 序列号的起点
const startDate = dayjs('1899-12-31');
// 加上 Excel 中的天数注意必须减去2因为 Excel 错误地把1900年当作闰年
const date = startDate.add(excelDate, 'day');
// 转换为 ISO 字符串
return date.toDate();
}
async getDepts(domainId: string, cellStr: string) {
const pattern = /[\s、,.。;\n]+/;
const depts: string[] = [];
if (pattern.test(cellStr)) {
const deptNames = cellStr.split(pattern);
for (const name of deptNames) {
const dept = await this.departmentService.findInDomain(domainId, name);
if (dept) depts.push(dept.id);
}
} else {
const dept = await this.departmentService.findInDomain(domainId, cellStr);
if (dept) depts.push(dept.id);
}
if (depts.length === 0) {
this.logger.error(`未找到单位:${cellStr}`);
}
return depts;
}
async getStaffs(deptIds: string[], cellStr: string) {
const staffs: string[] = [];
const pattern = /[\s、,.。;\n]+/;
const allStaffsArrays = await Promise.all(
deptIds.map((deptId) => this.staffService.findByDept({ deptId })),
);
const combinedStaffs = allStaffsArrays.reduce(
(acc, curr) => acc.concat(curr),
[],
);
if (pattern.test(cellStr)) {
const staffNames = cellStr.split(pattern);
for (const name of staffNames) {
if (
combinedStaffs.map((staff, index) => staff?.showname).includes(name)
) {
const staffWithName = combinedStaffs.find(
(staff) => staff?.showname === name,
);
if (staffWithName) {
// 将该员工的 ID 添加到 staffIds 数组中
staffs.push(staffWithName.id);
}
}
// if (staff) staffs.push(staff.staffId);
}
} else {
// const staff = await this.lanxin.getStaffsByDepartment(deptIds);
// if (staff) staffs.push(staff.staffId);
if (
combinedStaffs.map((staff, index) => staff?.showname).includes(cellStr)
) {
const staffWithName = combinedStaffs.find(
(staff) => staff?.showname === cellStr,
);
if (staffWithName) {
// 将该员工的 ID 添加到 staffIds 数组中
staffs.push(staffWithName.id);
}
}
}
if (staffs.length === 0) {
this.logger.error(`未找到人员:${cellStr}`);
}
return staffs;
}
buildTree(data: string[][]): TreeNode {
const root = new TreeNode('root');
try {
for (const path of data) {
let currentNode = root;
for (const value of path) {
currentNode = currentNode.addChild(value);
}
}
return root;
} catch (error) {
console.error(error);
}
}
async generateTreeFromFile(file: Buffer): Promise<{ tree: TreeNode }> {
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(file);
const worksheet = workbook.getWorksheet(1);
const data: string[][] = [];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber > 1) {
// Skip header row if any
try {
const rowData: string[] = (row.values as string[])
.slice(2)
.map((cell) => (cell || '').toString());
data.push(rowData.map((value) => value.trim()));
} catch (error) {
throw new Error(`发生报错!位于第${rowNumber}行,错误: ${error}`);
}
}
});
// Fill forward values
for (let i = 1; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
if (!data[i][j]) data[i][j] = data[i - 1][j];
}
}
return { tree: this.buildTree(data) };
}
printTree(node: TreeNode, level: number = 0): void {
const indent = ' '.repeat(level);
for (const child of node.children) {
this.printTree(child, level + 1);
}
}
swapKeyValue<T extends Record<string, string>>(
input: T,
): { [K in T[keyof T]]: Extract<keyof T, string> } {
const result: Partial<{ [K in T[keyof T]]: Extract<keyof T, string> }> = {};
for (const key in input) {
if (Object.prototype.hasOwnProperty.call(input, key)) {
const value = input[key];
result[value] = key;
}
}
return result as { [K in T[keyof T]]: Extract<keyof T, string> };
}
isEmptyRow(row: any) {
return row.every((cell: any) => {
return !cell || cell.toString().trim() === '';
});
}
async importStaffs(data: z.infer<typeof TransformMethodSchema.importStaffs>) {
const { base64, domainId } = data;
this.logger.log('开始');
const buffer = Buffer.from(base64, 'base64');
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(buffer);
const importsStaffMethodSchema = z.object({
name: z.string(),
phoneNumber: z.string().regex(/^\d+$/), // Assuming phone numbers should be numeric
deptName: z.string(),
});
const worksheet = workbook.getWorksheet(1); // Assuming the data is in the first sheet
const staffs: { name: string; phoneNumber: string; deptName: string }[] =
[];
worksheet.eachRow((row, rowNumber) => {
if (rowNumber > 1) {
// Assuming the first row is headers
const name = row.getCell(1).value as string;
const phoneNumber = row.getCell(2).value.toString() as string;
const deptName = row.getCell(3).value as string;
try {
importsStaffMethodSchema.parse({ name, phoneNumber, deptName });
staffs.push({ name, phoneNumber, deptName });
} catch (error) {
throw new Error(`发生报错!位于第${rowNumber}行,错误: ${error}`);
}
}
});
// 获取所有唯一的部门名称
const uniqueDeptNames = [...new Set(staffs.map((staff) => staff.deptName))];
// 获取所有部门名称对应的部门ID
const deptIdsMap = await this.departmentService.getDeptIdsByNames(
uniqueDeptNames,
domainId,
);
const count = await db.staff.count();
const hashedPassword = await argon2.hash('123456');
// 为员工数据添加部门ID
const staffsToCreate = staffs.map((staff, index) => ({
showname: staff.name,
username: staff.phoneNumber,
phoneNumber: staff.phoneNumber,
password: hashedPassword,
deptId: deptIdsMap[staff.deptName],
domainId,
order: index + count,
}));
// 批量创建员工数据
const createdStaffs = await db.staff.createMany({
data: staffsToCreate,
});
await deleteByPattern('row-*');
return createdStaffs;
}
async importTerms(
staff: Staff,
data: z.infer<typeof TransformMethodSchema.importTerms>,
) {
const { base64, domainId, taxonomyId, parentId } = data;
this.logger.log('开始');
await db.$transaction(async (tx) => {
const buffer = Buffer.from(base64, 'base64');
const { tree: root } = await this.generateTreeFromFile(buffer);
this.printTree(root);
const termsData: Prisma.TermCreateManyInput[] = [];
const termAncestriesData: Prisma.TermAncestryCreateManyInput[] = [];
if (!taxonomyId) {
throw new Error('未指定分类!');
}
this.logger.log('存在taxonomyId');
const taxonomy = await tx.taxonomy.findUnique({
where: { id: taxonomyId },
});
if (!taxonomy) {
throw new Error('未找到对应分类');
}
const count = await tx.term.count({ where: { taxonomyId: taxonomyId } });
let termIndex = 0;
this.logger.log(count);
const gatherTermsData = async (nodes: TreeNode[], depth = 0) => {
let currentIndex = 0;
for (const node of nodes) {
const termData = {
name: node.value,
taxonomyId: taxonomyId,
domainId: domainId,
createdBy: staff.id,
order: count + termIndex + 1,
};
termsData.push(termData);
termIndex++;
// Debug: Log term data preparation
await gatherTermsData(node.children, depth + 1);
currentIndex++;
}
};
await gatherTermsData(root.children);
let createdTerms: { id: string; name: string }[] = [];
try {
createdTerms = await tx.term.createManyAndReturn({
data: termsData,
select: { id: true, name: true },
});
// Debug: Log created terms
} catch (error) {
console.error('创建Terms报错:', error);
throw new Error('创建失败');
}
const termsUpdate = [];
const gatherAncestryData = (
nodes: TreeNode[],
ancestors: string[] = parentId ? [null, parentId] : [null],
depth = 0,
) => {
let currentIndex = 0;
for (const node of nodes) {
// if (depth !== 0) {
const dept = createdTerms.find((dept) => dept.name === node.value);
if (dept) {
termsUpdate.push({
where: { id: dept.id },
data: { parentId: ancestors[ancestors.length - 1] },
});
for (let i = 0; i < ancestors.length; i++) {
const ancestryData = {
ancestorId: ancestors[i],
descendantId: dept.id,
relDepth: depth - i + 1,
};
termAncestriesData.push(ancestryData);
}
const newAncestors = [...ancestors, dept.id];
gatherAncestryData(node.children, newAncestors, depth + 1);
}
currentIndex++;
}
// console.log(`depth:${depth}`);
// for (const node of nodes) {
// if (depth !== 0) {
// const term = createdTerms.find((term) => term.name === node.value);
// if (term) {
// termsUpdate.push({
// where: { id: term.id },
// data: { parentId: ancestors[ancestors.length - 1] },
// });
// for (let i = 0; i < ancestors.length; i++) {
// const ancestryData = {
// ancestorId: ancestors[i],
// descendantId: term.id,
// relDepth: depth - i,
// };
// termAncestriesData.push(ancestryData);
// console.log(`准备好的闭包表数据ATermAncestryData:`, ancestryData);
// }
// const newAncestors = [...ancestors, term.id];
// gatherAncestryData(node.children, newAncestors, depth + 1);
// }
// } else {
// gatherAncestryData(
// node.children,
// [createdTerms.find((term) => term.name === node.value).id],
// depth + 1,
// );
// }
// currentIndex++;
// }
};
gatherAncestryData(root.children);
this.logger.log('准备好闭包表数据 Ancestries Data:', termAncestriesData);
try {
const updatePromises = termsUpdate.map((update) =>
tx.term.update(update),
);
await Promise.all(updatePromises);
await tx.termAncestry.createMany({ data: termAncestriesData });
const allTerm = await tx.term.findMany({
where: {
id: {
in: createdTerms.map((termt) => termt.id),
},
},
select: {
id: true,
children: {
where: { deletedAt: null },
select: { id: true, deletedAt: true },
},
},
});
for (const term of allTerm) {
await tx.term.update({
where: {
id: term.id,
},
data: {
hasChildren: term.children.length > 0,
},
});
}
await deleteByPattern('row-*');
return { count: createdTerms.length };
} catch (error) {
console.error('Error 更新Term或者创建Terms闭包表失败:', error);
throw new Error('更新术语信息或者创建术语闭包表失败');
}
});
//prisma的特性create之后填入了对应id需要做一次这个查询才会填入相应值
const termAncestries = await db.termAncestry.findMany({
include: {
ancestor: true,
descendant: true,
},
});
}
async importDepts(
staff: Staff,
data: z.infer<typeof TransformMethodSchema.importDepts>,
) {
const { base64, domainId, parentId } = data;
// this.logger.log('开始', parentId);
const buffer = Buffer.from(base64, 'base64');
await db.$transaction(async (tx) => {
const { tree: root } = await this.generateTreeFromFile(buffer);
this.printTree(root);
const deptsData: Prisma.DepartmentCreateManyInput[] = [];
const deptAncestriesData: Prisma.DeptAncestryCreateManyInput[] = [];
const count = await tx.department.count({ where: {} });
let deptIndex = 0;
// this.logger.log(count);
const gatherDeptsData = async (
nodes: TreeNode[],
depth = 0,
dept?: string,
) => {
let currentIndex = 0;
for (const node of nodes) {
const deptData = {
name: node.value,
// taxonomyId: taxonomyId,
domainId: domainId,
// createdBy: staff.id,
order: count + deptIndex + 1,
};
deptsData.push(deptData);
deptIndex++;
// Debug: Log term data preparation
await gatherDeptsData(node.children, depth + 1);
currentIndex++;
}
};
await gatherDeptsData(root.children);
let createdDepts: { id: string; name: string }[] = [];
try {
createdDepts = await tx.department.createManyAndReturn({
data: deptsData,
select: { id: true, name: true },
});
// Debug: Log created terms
} catch (error) {
console.error('创建Depts报错:', error);
throw new Error('创建失败');
}
const deptsUpdate = [];
const gatherAncestryData = (
nodes: TreeNode[],
ancestors: string[] = parentId ? [null, parentId] : [null],
depth = 0,
) => {
let currentIndex = 0;
for (const node of nodes) {
// if (depth !== 0) {
const dept = createdDepts.find((dept) => dept.name === node.value);
if (dept) {
deptsUpdate.push({
where: { id: dept.id },
data: { parentId: ancestors[ancestors.length - 1] },
});
for (let i = 0; i < ancestors.length; i++) {
const ancestryData = {
ancestorId: ancestors[i],
descendantId: dept.id,
relDepth: depth - i + 1,
};
deptAncestriesData.push(ancestryData);
}
const newAncestors = [...ancestors, dept.id];
gatherAncestryData(node.children, newAncestors, depth + 1);
}
currentIndex++;
}
};
gatherAncestryData(root?.children);
this.logger.log('准备好闭包表数据 Ancestries Data:', deptAncestriesData);
try {
const updatePromises = deptsUpdate.map((update) =>
tx.department.update(update),
);
await Promise.all(updatePromises);
await tx.deptAncestry.createMany({ data: deptAncestriesData });
const allDept = await tx.department.findMany({
where: {
id: {
in: createdDepts.map((dept) => dept.id),
},
},
select: {
id: true,
children: {
where: { deletedAt: null },
select: { id: true, deletedAt: true },
},
},
});
for (const dept of allDept) {
await tx.department.update({
where: {
id: dept.id,
},
data: {
hasChildren: dept.children.length > 0,
},
});
}
await deleteByPattern('row-*');
return { count: createdDepts.length };
} catch (error) {
console.error('Error 更新Dept或者创建Depts闭包表失败:', error);
throw new Error('更新单位信息或者创建单位闭包表失败');
}
});
//prisma的特性create之后填入了对应id需要做一次这个查询才会填入相应值
// const deptAncestries = db.deptAncestry.findMany({
// include: {
// ancestor: true,
// descendant: true,
// },
// });
}
}