feat: 新增书籍及书籍分类管理模块(entity/controller/service)
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import { Provide } from '@midwayjs/core';
|
||||||
|
import { CoolController, BaseController } from '@cool-midway/core';
|
||||||
|
import { KnowledgeBookCategoryEntity } from '../../entity/book-category';
|
||||||
|
import { KnowledgeBookCategoryService } from '../../service/book-category';
|
||||||
|
|
||||||
|
@CoolController({
|
||||||
|
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
|
||||||
|
entity: KnowledgeBookCategoryEntity,
|
||||||
|
service: KnowledgeBookCategoryService,
|
||||||
|
pageQueryOp: {
|
||||||
|
keyWordLikeFields: ['a.name'],
|
||||||
|
fieldEq: ['a.status'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class AdminKnowledgeBookCategoryController extends BaseController {}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Body, Post, Provide } from '@midwayjs/core';
|
||||||
|
import { CoolController, BaseController } from '@cool-midway/core';
|
||||||
|
import { KnowledgeBookEntity } from '../../entity/book';
|
||||||
|
import { KnowledgeBookService } from '../../service/book';
|
||||||
|
|
||||||
|
@CoolController({
|
||||||
|
api: ['add', 'delete', 'update', 'info', 'list', 'page'],
|
||||||
|
entity: KnowledgeBookEntity,
|
||||||
|
service: KnowledgeBookService,
|
||||||
|
pageQueryOp: {
|
||||||
|
keyWordLikeFields: ['a.name', 'a.author'],
|
||||||
|
fieldEq: ['a.quality', 'a.priority', 'a.categoryId'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class AdminKnowledgeBookController extends BaseController {
|
||||||
|
@Post('/import', { summary: '批量导入书籍' })
|
||||||
|
async importBooks(@Body() books: any[]) {
|
||||||
|
return this.ok(await this.service.importBooks(books));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { BaseEntity } from '../../base/entity/base';
|
||||||
|
import { Column, Entity, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('knowledge_book_category')
|
||||||
|
export class KnowledgeBookCategoryEntity extends BaseEntity {
|
||||||
|
@Index()
|
||||||
|
@Column({ comment: '分类名称', length: 100 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ comment: '排序', default: 0 })
|
||||||
|
sort: number;
|
||||||
|
|
||||||
|
@Column({ comment: '状态', dict: ['禁用', '启用'], default: 1 })
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
@Column({ comment: '备注', nullable: true })
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { BaseEntity } from '../../base/entity/base';
|
||||||
|
import { Column, Entity, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('knowledge_book')
|
||||||
|
export class KnowledgeBookEntity extends BaseEntity {
|
||||||
|
@Index()
|
||||||
|
@Column({ comment: '名称', length: 200 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ comment: '原名', nullable: true })
|
||||||
|
originalName: string;
|
||||||
|
|
||||||
|
@Column({ comment: '作者', nullable: true })
|
||||||
|
author: string;
|
||||||
|
|
||||||
|
@Column({ comment: '年份', nullable: true })
|
||||||
|
year: number;
|
||||||
|
|
||||||
|
@Column({ comment: '国家', nullable: true })
|
||||||
|
country: string;
|
||||||
|
|
||||||
|
@Column({ comment: '内容简介', type: 'text', nullable: true })
|
||||||
|
synopsis: string;
|
||||||
|
|
||||||
|
@Column({ comment: '背景故事', type: 'text', nullable: true })
|
||||||
|
backgroundStory: string;
|
||||||
|
|
||||||
|
@Column({ comment: '豆瓣评分', type: 'decimal', precision: 3, scale: 1, nullable: true })
|
||||||
|
doubanRating: number;
|
||||||
|
|
||||||
|
@Column({ comment: '优先级', nullable: true })
|
||||||
|
priority: string;
|
||||||
|
|
||||||
|
@Column({ comment: '分类ID', nullable: true })
|
||||||
|
categoryId: number;
|
||||||
|
|
||||||
|
@Column({ comment: '质量评级', nullable: true })
|
||||||
|
quality: string;
|
||||||
|
|
||||||
|
@Column({ comment: '封面', type: 'json', nullable: true })
|
||||||
|
cover: string[];
|
||||||
|
|
||||||
|
@Column({ comment: '标签', length: 500, nullable: true })
|
||||||
|
tags: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Provide } from '@midwayjs/core';
|
||||||
|
import { BaseService } from '@cool-midway/core';
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { escapeObject } from '../utils/xss';
|
||||||
|
import { KnowledgeBookCategoryEntity } from '../entity/book-category';
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
export class KnowledgeBookCategoryService extends BaseService {
|
||||||
|
@InjectEntityModel(KnowledgeBookCategoryEntity)
|
||||||
|
knowledgeBookCategoryEntity: Repository<KnowledgeBookCategoryEntity>;
|
||||||
|
|
||||||
|
async add(params: any) {
|
||||||
|
const safeParams = escapeObject(params);
|
||||||
|
return super.add(safeParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(params: any) {
|
||||||
|
const safeParams = escapeObject(params);
|
||||||
|
return super.update(safeParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
|
import { BaseService } from '@cool-midway/core';
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import {
|
||||||
|
escapeObject,
|
||||||
|
filterImageUrls,
|
||||||
|
truncateString,
|
||||||
|
validateYear,
|
||||||
|
validateRating,
|
||||||
|
validateCategoryId,
|
||||||
|
} from '../utils/xss';
|
||||||
|
import { KnowledgeBookEntity } from '../entity/book';
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
export class KnowledgeBookService extends BaseService {
|
||||||
|
@InjectEntityModel(KnowledgeBookEntity)
|
||||||
|
knowledgeBookEntity: Repository<KnowledgeBookEntity>;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
ctx;
|
||||||
|
|
||||||
|
async page(query: any, option?: any) {
|
||||||
|
const { page: pageNo = 1, size = 20, keyWord = '', order = 'id', sort = 'desc' } = query;
|
||||||
|
const pageNum = Math.max(1, parseInt(String(pageNo), 10) || 1);
|
||||||
|
const pageSize = Math.max(1, parseInt(String(size), 10) || 20);
|
||||||
|
const offset = (pageNum - 1) * pageSize;
|
||||||
|
|
||||||
|
const qb = this.knowledgeBookEntity.createQueryBuilder('a');
|
||||||
|
|
||||||
|
if (query.quality !== undefined && query.quality !== null && query.quality !== '') {
|
||||||
|
qb.andWhere('a.quality = :quality', { quality: String(query.quality) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.priority !== undefined && query.priority !== null && query.priority !== '') {
|
||||||
|
qb.andWhere('a.priority = :priority', { priority: String(query.priority) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.categoryId !== undefined && query.categoryId !== null && query.categoryId !== '') {
|
||||||
|
const catId = typeof query.categoryId === 'string' ? parseInt(query.categoryId, 10) : query.categoryId;
|
||||||
|
if (!isNaN(catId)) {
|
||||||
|
qb.andWhere('a.categoryId = :categoryId', { categoryId: catId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyWord) {
|
||||||
|
const like = `%${keyWord}%`;
|
||||||
|
qb.andWhere('(a.name LIKE :kw OR a.author LIKE :kw)', { kw: like });
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedSort = ['asc', 'desc'].includes(String(sort).toLowerCase()) ? String(sort).toUpperCase() : 'DESC';
|
||||||
|
const orderField = ['id', 'createTime', 'updateTime', 'name', 'year'].includes(order) ? order : 'id';
|
||||||
|
qb.orderBy(`a.${orderField}`, allowedSort as 'ASC' | 'DESC');
|
||||||
|
|
||||||
|
qb.skip(offset).take(pageSize);
|
||||||
|
|
||||||
|
const [list, total] = await qb.getManyAndCount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
pagination: {
|
||||||
|
page: pageNum,
|
||||||
|
size: pageSize,
|
||||||
|
total: Number(total),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(params: any) {
|
||||||
|
if (params.name) params.name = truncateString(params.name, 200);
|
||||||
|
if (params.author) params.author = truncateString(params.author, 100);
|
||||||
|
if (params.country) params.country = truncateString(params.country, 100);
|
||||||
|
if (params.quality) params.quality = truncateString(params.quality, 10);
|
||||||
|
if (params.priority) params.priority = truncateString(params.priority, 10);
|
||||||
|
|
||||||
|
if (params.name) {
|
||||||
|
const exist = await this.knowledgeBookEntity.findOne({
|
||||||
|
where: { name: params.name },
|
||||||
|
});
|
||||||
|
if (exist) {
|
||||||
|
throw new Error(`书籍"${params.name}"已存在,请勿重复添加`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.cover) {
|
||||||
|
params.cover = filterImageUrls(params.cover);
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeParams = escapeObject(params, ['cover']);
|
||||||
|
return super.add(safeParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(params: any) {
|
||||||
|
if (params.name) params.name = truncateString(params.name, 200);
|
||||||
|
if (params.author) params.author = truncateString(params.author, 100);
|
||||||
|
if (params.country) params.country = truncateString(params.country, 100);
|
||||||
|
if (params.quality) params.quality = truncateString(params.quality, 10);
|
||||||
|
if (params.priority) params.priority = truncateString(params.priority, 10);
|
||||||
|
|
||||||
|
if (params.cover) {
|
||||||
|
params.cover = filterImageUrls(params.cover);
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeParams = escapeObject(params, ['cover']);
|
||||||
|
return super.update(safeParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
async importBooks(books: any[]) {
|
||||||
|
let success = 0;
|
||||||
|
let fail = 0;
|
||||||
|
let skip = 0;
|
||||||
|
const successList = [];
|
||||||
|
const failList = [];
|
||||||
|
const skipList = [];
|
||||||
|
|
||||||
|
for (const book of books) {
|
||||||
|
if (!book.name) {
|
||||||
|
fail++;
|
||||||
|
failList.push({ name: book.name || '未知', reason: '缺少名称' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exist = await this.knowledgeBookEntity.findOne({
|
||||||
|
where: { name: book.name },
|
||||||
|
});
|
||||||
|
if (exist) {
|
||||||
|
skip++;
|
||||||
|
skipList.push({ name: book.name, reason: '已存在' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bookData = {
|
||||||
|
name: truncateString(book.name || '', 200),
|
||||||
|
originalName: book.originalName || book['原名'] || '',
|
||||||
|
author: truncateString(book.author || book['作者'] || '', 100),
|
||||||
|
year: validateYear(book.year || book['年份']),
|
||||||
|
country: truncateString(book.country || book['国家'] || '', 100),
|
||||||
|
synopsis: book.synopsis || book['内容简介'] || '',
|
||||||
|
backgroundStory: book.backgroundStory || book['背景故事'] || '',
|
||||||
|
cover: filterImageUrls(book.cover || book['封面链接'] ? [book.cover || book['封面链接']] : []),
|
||||||
|
categoryId: validateCategoryId(book.categoryId ?? book['分类ID'] ?? book['分类']),
|
||||||
|
quality: truncateString(book.quality || book['质量'] || book['质量评级'] || 'C', 10),
|
||||||
|
priority: truncateString(book.priority || book['优先级'] || 'P3', 10),
|
||||||
|
doubanRating: validateRating(book.doubanRating || book['豆瓣评分']),
|
||||||
|
tags: book.tags || book['标签'] || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.add(bookData);
|
||||||
|
success++;
|
||||||
|
successList.push({ name: book.name });
|
||||||
|
} catch (e) {
|
||||||
|
fail++;
|
||||||
|
failList.push({ name: book.name, reason: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total: books.length, success, fail, skip, successList, failList, skipList };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user