diff --git a/src/modules/knowledge/controller/admin/book-category.ts b/src/modules/knowledge/controller/admin/book-category.ts new file mode 100644 index 0000000..6027bf5 --- /dev/null +++ b/src/modules/knowledge/controller/admin/book-category.ts @@ -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 {} diff --git a/src/modules/knowledge/controller/admin/book.ts b/src/modules/knowledge/controller/admin/book.ts new file mode 100644 index 0000000..165d8e6 --- /dev/null +++ b/src/modules/knowledge/controller/admin/book.ts @@ -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)); + } +} diff --git a/src/modules/knowledge/entity/book-category.ts b/src/modules/knowledge/entity/book-category.ts new file mode 100644 index 0000000..b1c26d8 --- /dev/null +++ b/src/modules/knowledge/entity/book-category.ts @@ -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; +} diff --git a/src/modules/knowledge/entity/book.ts b/src/modules/knowledge/entity/book.ts new file mode 100644 index 0000000..f5390b2 --- /dev/null +++ b/src/modules/knowledge/entity/book.ts @@ -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; +} diff --git a/src/modules/knowledge/service/book-category.ts b/src/modules/knowledge/service/book-category.ts new file mode 100644 index 0000000..ab33eff --- /dev/null +++ b/src/modules/knowledge/service/book-category.ts @@ -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; + + async add(params: any) { + const safeParams = escapeObject(params); + return super.add(safeParams); + } + + async update(params: any) { + const safeParams = escapeObject(params); + return super.update(safeParams); + } +} diff --git a/src/modules/knowledge/service/book.ts b/src/modules/knowledge/service/book.ts new file mode 100644 index 0000000..838dd1a --- /dev/null +++ b/src/modules/knowledge/service/book.ts @@ -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; + + @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 }; + } +}