node实现接口资源批量管理(含excel数据导入及模板下载)

概述

该功能是基于BSPV1.0版本中资源管理的扩展,在资源管理中添加接口资源,方便在对角色授权时做到更加精确的控制。第一层级为系统,第二层级为系统模块,第三层级为接口

UI图

api资源

功能设计

Api资源表设计

  1. 考虑到系统中资源有限,在设计表时系统、模块、接口全部存储同一数据表,通过type进行区分,为方便构建系统模块接口间的关联关系,数据表中引入parentId。
@Entity({name: 'api_resource'})
export class ApiResource {
    @PrimaryGeneratedColumn()
    id: number;

    @Column({ length: 500 })
    name: string;

    @Column('text', {nullable: true})
    desc: string;

    @Column()
    value: string;

    @Column({comment: '1: 系统, 2: 模块, 3: 接口'})
    type: number;

    @Column()
    parentId: number;

    @Column({nullable: false})
    system: string;

    @Column({nullable: false})
    module: string;

    @Column()
    code: string;

    @Column({default: 0})
    isDelete: number;

    @Column({default: '', nullable: true })
    crateTime: string;

    @Column({default: '', nullable: true })
    updateTime: string;

    @Column({default: '', nullable: true })
    deleteTime: string;
}

基本CRDU

资源添加/编辑

    /**
     * 添加api资源
     * @param params
     */
    @Post('resource/add')
    public async createApiResource(@Body() params: CreateApiResourceDto): Promise {
        try {
            await this.apiResourceService.createApiResource(params);
            return new ResultData(MessageType.CREATE, null, true);
        } catch (e) {
            return new ResultData(MessageType.CREATE, null, false);
        }
    }

    /**
     * 更新api资源
     * @param params
     */
    @Post('resource/update')
    public async updateApiResource(@Body() params: UpdateApiResourceDto): Promise {
        try {
            await this.apiResourceService.updateApiResource(params);
            return new ResultData(MessageType.UPDATE, null, true);
        } catch (e) {
            return new ResultData(MessageType.UPDATE, null, false);
        }

    }
service层
    /**
     * 添加api资源
     * @param params
     */
    public async createApiResource(params: CreateApiResourceDto): Promise {
        try {
            return await this.apiResourceRepository
                .createQueryBuilder('r')
                .insert()
                .into(ApiResource)
                .values([{
                    name: params.name,
                    code: params.code,
                    type: params.type,
                    system: params.system,
                    isDelete: 0,
                    module: params.module,
                    crateTime: formatDate(),
                    value: params.value ? params.value : '',
                    desc: params.desc,
                    parentId: params.parentId }])
                .execute();
        } catch (e) {
            throw new ApiException('操作失败', ApiErrorCode.ROLE_LIST_FAILED, 200);
        }

    }

    /**
     * 更新api资源
     * @param params
     */
    public async updateApiResource(params: UpdateApiResourceDto): Promise {
        try {
            return await this.apiResourceRepository
                .createQueryBuilder('r')
                .update(ApiResource)
                .set({
                    name: params.name,
                    code: params.code,
                    type: params.type,
                    system: params.system,
                    isDelete: 0,
                    module: params.module,
                    updateTime: formatDate(),
                    value: params.value ? params.value : '',
                    desc: params.desc,
                    parentId: params.parentId })
                .where('id = :id', { id: params.id })
                .execute();
        } catch (e) {
            throw new ApiException('操作失败', ApiErrorCode.ROLE_LIST_FAILED, 200);
        }

excel模板下载

  1. 客户端请求文件时,服务端查询模板文件,如存在模板文件则直接通过文件流(res.type('application/vnd.openxmlformats'))的形式返回前端,如果文件不存在则通过调用excel工具函数生成文件最后返回给前端。
    /**
     * 下载模板
     * @param res
     */
    @Get('template/download')
    public async downloadExcel(@Res() res: Response): Promise {
        try {
            const filePath = join(__dirname, './apiResourceTemplate.xlsx');
            if (!existsSync(filePath)) {
                await this.createExcel();
            }
            res.type('application/vnd.openxmlformats');
            res.attachment('接口资源导入模板.xlsx');
            res.send(readFileSync(filePath));
        } catch (e) {
            return new ResultData(MessageType.FILEERROR,  false);
        }

    }
    /**
     * 创建excel
     */
    public async createExcel(): Promise {
        const params = {
            rows: this.apiResourceService.getRowDatas(), // 要导出的数据
            columns: this.apiResourceService.getColumnDatas(), // 列头信息
            sheetName: '导出示例', // 工作簿名称
            filePath: join(__dirname, './apiResourceTemplate.xlsx'),
        };
        return await this.apiResourceService.createExcel(params);
    }
  1. 生成excel文件核心代码如下
/**
 * 导出excel文件
 * @param {Array} columns  列头信息
 * @param {Array} rows  行数据
 * @param {String} sheetName  工作表名称
 * @param {path} savePath  文件保存路径
 * @param {path} style 设置每行高度
 */
export const exportExcel = async (columns, rows, sheetName, savePath, style = { row: { height: 32 } }) => {
    try {
        const workbook = new Excel.Workbook();
        const sheet = workbook.addWorksheet(sheetName);
        // 设置表头
        sheet.columns = columns.map(column => {
            return { header: column.name, width: column.size, key: column.key };
        });

        // 设置列的样式,对齐方式、自动换行等样式
        for (let i = 0; i < columns.length; i++) {
            const column = columns[i];
            if (!_.get(column, 'alignment', '')) {
                continue;
            }
            sheet.getColumn(column.key).alignment = column.alignment;
        }

        // 设置表头单元格样式与对齐方式
        const rowHeader = sheet.getRow(1);
        for (let i = 1; i <= sheet.columns.length; i++) {
            rowHeader.getCell(i).font = { name: 'Arial Black', size: 12, bold: false };
            rowHeader.getCell(i).alignment = {
                wrapText: false,
                horizontal: 'center',
            };
        }

        // 填充数据
        for (let index = 0; index < rows.length; index++) {
            const row = rows[index];
            const data = setRowVaules(columns, row, index, sheet, workbook);
            sheet.addRow(data);
            sheet.getRow(index + 2).height = style.row.height;
        }

        await workbook.xlsx.writeFile(savePath);
    } catch (error) {
        console.log(error);
    }
};
  1. 关于前端下载文件

前端下载文件多种方式均可实现,第一种通过请求静态文件方式获取下载(直接通过url地址请求)代码如下;第二种通过ajax异步请求数据,再转化为Blob类型下载,该方案需要后端配合将返回数据格式设置为流( res.type('application/vnd.openxmlformats');)代码如下。

a. 第一种直接请求静态资源

http://img1.gtimg.com/chinanba/pics/hv1/124/191/2324/151166929.jpg

b. 异步模拟a标签点击

export const $getFile =  (url: any, params: any, server: any =  'wbw') => {
    axios.defaults.baseURL = getBaseUrl(server);
    return new Promise((resolve, reject) => {
        // 下载文件流必须将responseType设置为arraybuffer
        axios.get(url, { params, responseType: 'arraybuffer', }).then((res: any) => {
            resolve(res); // 返回请求成功的数据 data
        }).catch((err: any) => {
            reject(err);
        });
    });
};
$getFile('apiResource/template/download', {})
                    .then(data => {
                        // 将数据流转化为Blob
                        const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
                        let a = document.createElement('a')
                        // a.href = baseURL +  '/apiResource/template/download'
                        a.href = url
                        a.download = '资源导入模板'
                        a.click()
                    });

系统中全部接口会走网关进行用户身份认证,第一种方案无法通过手动方式写入headers故抛弃,选用第二种异步请求文件流的方式进行模板下载。

前端请求文件流后进行下载是必须将responseType设置为arraybuffer,否则下载的文件存在乱码,非常重要。

excel数据批量导入资源

  1. controller层通过FileInterceptor中间拿到二进制数据流格式的文件,然后通过excel工具r解析数据,解析数据源格式必须设置为buffer。
    /**
     * 资源数据导入
     * @param res
     */
    @Post('template/import')
    @UseInterceptors(FileInterceptor('file'))
    public async importExcel(@UploadedFile() file): Promise {
        try {
            // excel中列对应的字段映射
            const column = this.apiResourceService.getColumnDatas();
            const list = await this.apiResourceService.importExcel(column, file.buffer, true, 'buffer');
            return new ResultData(MessageType.GETLIST,  {data: [], count: list.length}, true);
        } catch (e) {
            return new ResultData(MessageType.FILEERROR,  false);
        }

    }
     /**
     * 设置 excel 列头信息
     * name 列名
     * type 类型
     * key 对应数据的 key
     * size 大小
     */
    public getColumnDatas() {
        return [
            {
                name: '接口名称',
                type: 'String',
                key: 'name',
                size: 20,
                index: 1,
            },
            {
                name: '编码',
                type: 'String',
                key: 'code',
                size: 20,
                index: 2,
            },
            {
                name: '所属系统',
                type: 'String',
                key: 'system',
                size: 20,
                index: 3,
            },
            {
                name: '所属模块',
                type: 'String',
                key: 'module',
                size: 20,
                index: 4,
            },
            {
                name: '属性值',
                type: 'String',
                key: 'value',
                size: 20,
                index: 5,
            },
            {
                name: '描述',
                type: 'String',
                key: 'desc',
                size: 20,
                index: 6,
            },
        ];
    }
  1. service层进行数据的过滤,将有效数据进行持久化。
    /**
     * 导入资源数据
     */
    public async importExcel(column, file, hasHeader, type) {
        const list: ApiResource[] = await importExcel(column, file, hasHeader, type);
        let modulesName: string[] = [];
        const moduleMap = new Map();
        const afterList: ApiResource[] = [];
        const apiResourceList: ApiResource[] = list.map((item: ApiResource, index: number) => {
            modulesName.push(item.module);
            return {
                ...item,
                crateTime: formatDate(),
                isDelete: 0,
                type: 3,
            };
        });
        modulesName = Array.from(new Set(modulesName));
        for (let i = 0; i < modulesName.length; i++) {
            const currentModule = await this.apiResourceRepository.findOne({code: modulesName[i]});
            if (currentModule) {
                moduleMap.set(modulesName[i], currentModule);
            }
        }
        apiResourceList.forEach((item: ApiResource) => {
            item.parentId = moduleMap.get(item.module) ?  moduleMap.get(item.module).id : null;
            item.system = moduleMap.get(item.module) ?  moduleMap.get(item.module).system : null;
            if (item.parentId && item.system) {
                afterList.push(item);
            }
        });
        try {
            await this.apiResourceRepository
                .createQueryBuilder('r')
                .insert()
                .into(ApiResource)
                .values(afterList)
                .execute();
            return afterList;
        } catch (e) {
            console.log(e)
            throw new ApiException('操作失败', ApiErrorCode.AUTHORITY_DELETE_FILED, 200);
        }
    }

结言

  1. 资源管理中加入接口资源是为更加精确控制角色权限而设计的,后期在网管层面中会有有对应体现

原文地址:http://blog.canyuegongzi.xyz/

资源

github(前端)
github(后端)

在线地址BSPv1.1

你可能感兴趣的:(nest,node.js,excel,typescript)