feat: 新增书籍及书籍分类管理模块(entity/controller/service)

This commit is contained in:
Jony
2026-07-02 23:53:10 +08:00
parent 893b5316f3
commit f0fff0b113
6 changed files with 280 additions and 0 deletions
@@ -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;
}
+45
View File
@@ -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);
}
}
+160
View File
@@ -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 };
}
}