355 lines
13 KiB
TypeScript
355 lines
13 KiB
TypeScript
import { Provide } from '@midwayjs/core';
|
||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||
import { Repository, Like } from 'typeorm';
|
||
import { Dict } from '../entity/dict.entity';
|
||
import { DictItem } from '../entity/dict_item.entity';
|
||
import { CreateDictDTO, UpdateDictDTO } from '../dto/dict.dto';
|
||
import { CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
||
import * as xlsx from 'xlsx';
|
||
import * as fs from 'fs';
|
||
import { BatchOperationResultDTO } from '../dto/api.dto';
|
||
|
||
// 定义 Excel 行数据的类型接口
|
||
interface ExcelRow {
|
||
name: string;
|
||
title: string;
|
||
titleCN?: string;
|
||
value?: string;
|
||
image?: string;
|
||
shortName?: string;
|
||
sort?: number;
|
||
}
|
||
|
||
@Provide()
|
||
export class DictService {
|
||
|
||
@InjectEntityModel(Dict)
|
||
dictModel: Repository<Dict>;
|
||
|
||
@InjectEntityModel(DictItem)
|
||
dictItemModel: Repository<DictItem>;
|
||
|
||
// 格式化名称为 kebab-case
|
||
private formatName(name: string): string {
|
||
// 只替换空格和下划线
|
||
return String(name).replace(/[_\s]+/g, '-').toLowerCase();
|
||
}
|
||
|
||
// 生成并返回字典的XLSX模板
|
||
getDictXLSXTemplate() {
|
||
// 定义表头
|
||
const headers = ['name', 'title'];
|
||
// 创建一个新的工作表
|
||
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
||
// 创建一个新的工作簿
|
||
const wb = xlsx.utils.book_new();
|
||
// 将工作表添加到工作簿
|
||
xlsx.utils.book_append_sheet(wb, ws, 'Dicts');
|
||
// 将工作簿写入缓冲区
|
||
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||
}
|
||
|
||
// 从XLSX文件导入字典
|
||
async importDictsFromTable(bufferOrPath: Buffer | string) {
|
||
// 判断传入的是 Buffer 还是文件路径字符串
|
||
let buffer: Buffer;
|
||
if (typeof bufferOrPath === 'string') {
|
||
// 如果是文件路径,读取文件内容
|
||
buffer = fs.readFileSync(bufferOrPath);
|
||
} else {
|
||
// 如果是 Buffer,直接使用
|
||
buffer = bufferOrPath;
|
||
}
|
||
|
||
// 读取缓冲区中的工作簿
|
||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||
// 获取第一个工作表的名称
|
||
const wsname = wb.SheetNames[0];
|
||
// 获取第一个工作表
|
||
const ws = wb.Sheets[wsname];
|
||
// 将工作表转换为JSON对象数组,xlsx会自动将第一行作为表头
|
||
const data = xlsx.utils.sheet_to_json(ws) as { name: string; title: string }[];
|
||
// 创建要保存的字典实体数组
|
||
const dicts = data.map((row: { name: string; title: string }) => {
|
||
const dict = new Dict();
|
||
dict.name = this.formatName(row.name);
|
||
dict.title = row.title;
|
||
return dict;
|
||
});
|
||
// 保存字典实体数组到数据库
|
||
await this.dictModel.save(dicts);
|
||
// 返回成功导入的记录数
|
||
return { success: true, count: dicts.length };
|
||
}
|
||
|
||
// 生成并返回字典项的XLSX模板
|
||
getDictItemXLSXTemplate() {
|
||
const headers = ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'];
|
||
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
||
const wb = xlsx.utils.book_new();
|
||
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
||
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||
}
|
||
|
||
// 从XLSX文件导入字典项
|
||
async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> {
|
||
if (!dictId) {
|
||
throw new Error("引入失败, 请输入字典 ID")
|
||
}
|
||
|
||
const dict = await this.dictModel.findOneBy({ id: dictId });
|
||
if (!dict) {
|
||
throw new Error('指定的字典不存在');
|
||
}
|
||
|
||
// 判断传入的是 Buffer 还是文件路径字符串
|
||
let buffer: Buffer;
|
||
if (typeof bufferOrPath === 'string') {
|
||
// 如果是文件路径,读取文件内容
|
||
buffer = fs.readFileSync(bufferOrPath);
|
||
} else {
|
||
// 如果是 Buffer,直接使用
|
||
buffer = bufferOrPath;
|
||
}
|
||
|
||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||
const wsname = wb.SheetNames[0];
|
||
const ws = wb.Sheets[wsname];
|
||
// 使用默认的header解析方式,xlsx会自动将第一行作为表头
|
||
const data = xlsx.utils.sheet_to_json(ws) as ExcelRow[];
|
||
|
||
// 使用 upsertDictItem 方法逐个处理,存在则更新,不存在则创建
|
||
const createdItems = [];
|
||
const updatedItems = [];
|
||
const errors = [];
|
||
|
||
for (const row of data) {
|
||
try {
|
||
const result = await this.upsertDictItem(dictId, {
|
||
name: row.name,
|
||
title: row.title,
|
||
titleCN: row.titleCN,
|
||
value: row.value,
|
||
image: row.image,
|
||
shortName: row.shortName,
|
||
sort: row.sort || 0,
|
||
});
|
||
if (result.action === 'created') {
|
||
createdItems.push(result.item);
|
||
} else {
|
||
updatedItems.push(result.item);
|
||
}
|
||
} catch (error) {
|
||
// 记录错误信息
|
||
errors.push({
|
||
identifier: row.name || 'unknown',
|
||
error: error instanceof Error ? error.message : String(error)
|
||
});
|
||
}
|
||
}
|
||
|
||
const processed = createdItems.length + updatedItems.length;
|
||
|
||
return {
|
||
total: data.length,
|
||
processed: processed,
|
||
updated: updatedItems.length,
|
||
created: createdItems.length,
|
||
errors: errors
|
||
};
|
||
}
|
||
getDict(where: { name?: string; id?: number; }, relations: string[]) {
|
||
if (!where.name && !where.id) {
|
||
throw new Error('必须提供 name 或 id');
|
||
}
|
||
return this.dictModel.findOne({ where, relations });
|
||
}
|
||
// 获取字典列表,支持按标题搜索
|
||
async getDicts(options: { title?: string; name?: string; }) {
|
||
const where = {
|
||
title: options.title ? Like(`%${options.title}%`) : undefined,
|
||
name: options.name ? Like(`%${options.name}%`) : undefined,
|
||
}
|
||
return this.dictModel.find({ where });
|
||
}
|
||
|
||
// 创建新字典
|
||
async createDict(createDictDTO: CreateDictDTO) {
|
||
const dict = new Dict();
|
||
dict.name = this.formatName(createDictDTO.name);
|
||
dict.title = createDictDTO.title;
|
||
return this.dictModel.save(dict);
|
||
}
|
||
|
||
// 更新字典
|
||
async updateDict(id: number, updateDictDTO: UpdateDictDTO) {
|
||
if (updateDictDTO.name) {
|
||
updateDictDTO.name = this.formatName(updateDictDTO.name);
|
||
}
|
||
await this.dictModel.update(id, updateDictDTO);
|
||
return this.dictModel.findOneBy({ id });
|
||
}
|
||
|
||
// 删除字典及其所有字典项
|
||
async deleteDict(id: number) {
|
||
// 首先删除该字典下的所有字典项
|
||
await this.dictItemModel.delete({ dict: { id } });
|
||
// 然后删除字典本身
|
||
const result = await this.dictModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
// 获取字典项列表,支持按 dictId 过滤
|
||
async getDictItems(params: { dictId?: number; name?: string; title?: string; }) {
|
||
const { dictId, name, title } = params;
|
||
const where: any = {};
|
||
|
||
if (dictId) {
|
||
where.dict = { id: dictId };
|
||
}
|
||
if (name) {
|
||
where.name = Like(`%${name}%`);
|
||
}
|
||
if (title) {
|
||
where.title = Like(`%${title}%`);
|
||
}
|
||
|
||
// 如果提供了 dictId,则只返回该字典下的项
|
||
if (params.dictId) {
|
||
return this.dictItemModel.find({ where, relations: ['dict'] });
|
||
}
|
||
// 否则,返回所有字典项
|
||
return this.dictItemModel.find({ relations: ['dict'] });
|
||
}
|
||
|
||
// 创建新字典项
|
||
async createDictItem(createDictItemDTO: CreateDictItemDTO) {
|
||
const dict = await this.dictModel.findOneBy({ id: createDictItemDTO.dictId });
|
||
if (!dict) {
|
||
throw new Error(`创建新字典项,指定的字典ID为${createDictItemDTO.dictId},但不存在`);
|
||
}
|
||
const item = new DictItem();
|
||
item.name = this.formatName(createDictItemDTO.name);
|
||
item.title = createDictItemDTO.title;
|
||
item.titleCN = createDictItemDTO.titleCN; // 保存中文名称
|
||
item.image = createDictItemDTO.image;
|
||
item.shortName = createDictItemDTO.shortName;
|
||
item.dict = dict;
|
||
return this.dictItemModel.save(item);
|
||
}
|
||
|
||
// 更新或创建字典项 (Upsert)
|
||
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
|
||
async upsertDictItem(dictId: number, itemData: {
|
||
name: string;
|
||
title: string;
|
||
titleCN?: string;
|
||
value?: string;
|
||
image?: string;
|
||
shortName?: string;
|
||
sort?: number;
|
||
}) {
|
||
// 格式化 name
|
||
const formattedName = this.formatName(itemData.name);
|
||
|
||
// 查找是否已存在该字典项(根据 name 和 dictId)
|
||
const existingItem = await this.dictItemModel.findOne({
|
||
where: {
|
||
name: formattedName,
|
||
dict: { id: dictId }
|
||
}
|
||
});
|
||
|
||
if (existingItem) {
|
||
// 如果存在,则更新
|
||
existingItem.title = itemData.title;
|
||
existingItem.titleCN = itemData.titleCN;
|
||
existingItem.value = itemData.value;
|
||
existingItem.image = itemData.image;
|
||
existingItem.shortName = itemData.shortName;
|
||
existingItem.sort = itemData.sort || 0;
|
||
const savedItem = await this.dictItemModel.save(existingItem);
|
||
return { item: savedItem, action: 'updated' };
|
||
} else {
|
||
// 如果不存在,则创建新的
|
||
const dict = await this.dictModel.findOneBy({ id: dictId });
|
||
if (!dict) {
|
||
throw new Error(`指定的字典ID为${dictId},但不存在`);
|
||
}
|
||
const item = new DictItem();
|
||
item.name = formattedName;
|
||
item.title = itemData.title;
|
||
item.titleCN = itemData.titleCN;
|
||
item.value = itemData.value;
|
||
item.image = itemData.image;
|
||
item.shortName = itemData.shortName;
|
||
item.sort = itemData.sort || 0;
|
||
item.dict = dict;
|
||
const savedItem = await this.dictItemModel.save(item);
|
||
return { item: savedItem, action: 'created' };
|
||
}
|
||
}
|
||
|
||
// 更新字典项
|
||
async updateDictItem(id: number, updateDictItemDTO: UpdateDictItemDTO) {
|
||
if (updateDictItemDTO.name) {
|
||
updateDictItemDTO.name = this.formatName(updateDictItemDTO.name);
|
||
}
|
||
await this.dictItemModel.update(id, updateDictItemDTO);
|
||
return this.dictItemModel.findOneBy({ id });
|
||
}
|
||
|
||
// 删除字典项
|
||
async deleteDictItem(id: number) {
|
||
const result = await this.dictItemModel.delete(id);
|
||
return result.affected > 0;
|
||
}
|
||
|
||
// 根据字典名称获取字典项列表
|
||
async getDictItemsByDictName(dictName: string) {
|
||
// 查找字典
|
||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||
// 如果字典不存在,则返回空数组
|
||
if (!dict) {
|
||
return [];
|
||
}
|
||
// 返回该字典下的所有字典项
|
||
return this.dictItemModel.find({ where: { dict: { id: dict.id } } });
|
||
}
|
||
|
||
// 导出字典项为 XLSX 文件
|
||
async exportDictItemsToXLSX(dictId: number) {
|
||
// 查找字典
|
||
const dict = await this.dictModel.findOneBy({ id: dictId });
|
||
// 如果字典不存在,则抛出错误
|
||
if (!dict) {
|
||
throw new Error('指定的字典不存在');
|
||
}
|
||
// 获取该字典下的所有字典项
|
||
const items = await this.dictItemModel.find({
|
||
where: { dict: { id: dictId } },
|
||
order: { sort: 'ASC', id: 'DESC' },
|
||
});
|
||
// 定义表头
|
||
const headers = ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'];
|
||
// 将字典项转换为二维数组
|
||
const data = items.map((item) => [
|
||
item.name,
|
||
item.title,
|
||
item.titleCN || '',
|
||
item.value || '',
|
||
item.sort,
|
||
item.image || '',
|
||
item.shortName || '',
|
||
]);
|
||
// 创建工作表
|
||
const ws = xlsx.utils.aoa_to_sheet([headers, ...data]);
|
||
// 创建工作簿
|
||
const wb = xlsx.utils.book_new();
|
||
// 将工作表添加到工作簿
|
||
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
||
// 将工作簿写入缓冲区
|
||
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||
}
|
||
}
|