Compare commits
3 Commits
a091132a70
...
fcd4ad3d1e
| Author | SHA1 | Date | |
|---|---|---|---|
| fcd4ad3d1e | |||
| f0fff0b113 | |||
| 893b5316f3 |
@@ -35,7 +35,16 @@ export default {
|
|||||||
// 文件上传
|
// 文件上传
|
||||||
upload: {
|
upload: {
|
||||||
fileSize: '200mb',
|
fileSize: '200mb',
|
||||||
whitelist: null,
|
whitelist: [
|
||||||
|
// 图片文件
|
||||||
|
'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp',
|
||||||
|
// 文档文件
|
||||||
|
'.pdf', '.doc', '.docx',
|
||||||
|
// 表格文件
|
||||||
|
'.xls', '.xlsx',
|
||||||
|
// 文本文件
|
||||||
|
'.txt', '.csv'
|
||||||
|
],
|
||||||
},
|
},
|
||||||
// 缓存 可切换成其他缓存如:redis http://www.midwayjs.org/docs/extensions/caching
|
// 缓存 可切换成其他缓存如:redis http://www.midwayjs.org/docs/extensions/caching
|
||||||
cacheManager: {
|
cacheManager: {
|
||||||
|
|||||||
+56
-1
@@ -1,2 +1,57 @@
|
|||||||
// 自动生成的文件,请勿手动修改
|
// 自动生成的文件,请勿手动修改
|
||||||
export const entities = [];
|
import * as entity0 from './modules/user/entity/wx';
|
||||||
|
import * as entity1 from './modules/user/entity/info';
|
||||||
|
import * as entity2 from './modules/user/entity/address';
|
||||||
|
import * as entity3 from './modules/task/entity/log';
|
||||||
|
import * as entity4 from './modules/task/entity/info';
|
||||||
|
import * as entity5 from './modules/space/entity/type';
|
||||||
|
import * as entity6 from './modules/space/entity/info';
|
||||||
|
import * as entity7 from './modules/recycle/entity/data';
|
||||||
|
import * as entity8 from './modules/plugin/entity/info';
|
||||||
|
import * as entity9 from './modules/knowledge/entity/film';
|
||||||
|
import * as entity10 from './modules/knowledge/entity/film-category';
|
||||||
|
import * as entity11 from './modules/knowledge/entity/book';
|
||||||
|
import * as entity12 from './modules/knowledge/entity/book-category';
|
||||||
|
import * as entity13 from './modules/dict/entity/type';
|
||||||
|
import * as entity14 from './modules/dict/entity/info';
|
||||||
|
import * as entity15 from './modules/demo/entity/goods';
|
||||||
|
import * as entity16 from './modules/base/entity/base';
|
||||||
|
import * as entity17 from './modules/base/entity/sys/user_role';
|
||||||
|
import * as entity18 from './modules/base/entity/sys/user';
|
||||||
|
import * as entity19 from './modules/base/entity/sys/role_menu';
|
||||||
|
import * as entity20 from './modules/base/entity/sys/role_department';
|
||||||
|
import * as entity21 from './modules/base/entity/sys/role';
|
||||||
|
import * as entity22 from './modules/base/entity/sys/param';
|
||||||
|
import * as entity23 from './modules/base/entity/sys/menu';
|
||||||
|
import * as entity24 from './modules/base/entity/sys/log';
|
||||||
|
import * as entity25 from './modules/base/entity/sys/department';
|
||||||
|
import * as entity26 from './modules/base/entity/sys/conf';
|
||||||
|
export const entities = [
|
||||||
|
...Object.values(entity0),
|
||||||
|
...Object.values(entity1),
|
||||||
|
...Object.values(entity2),
|
||||||
|
...Object.values(entity3),
|
||||||
|
...Object.values(entity4),
|
||||||
|
...Object.values(entity5),
|
||||||
|
...Object.values(entity6),
|
||||||
|
...Object.values(entity7),
|
||||||
|
...Object.values(entity8),
|
||||||
|
...Object.values(entity9),
|
||||||
|
...Object.values(entity10),
|
||||||
|
...Object.values(entity11),
|
||||||
|
...Object.values(entity12),
|
||||||
|
...Object.values(entity13),
|
||||||
|
...Object.values(entity14),
|
||||||
|
...Object.values(entity15),
|
||||||
|
...Object.values(entity16),
|
||||||
|
...Object.values(entity17),
|
||||||
|
...Object.values(entity18),
|
||||||
|
...Object.values(entity19),
|
||||||
|
...Object.values(entity20),
|
||||||
|
...Object.values(entity21),
|
||||||
|
...Object.values(entity22),
|
||||||
|
...Object.values(entity23),
|
||||||
|
...Object.values(entity24),
|
||||||
|
...Object.values(entity25),
|
||||||
|
...Object.values(entity26),
|
||||||
|
];
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export class BaseAuthorityMiddleware
|
|||||||
let statusCode = 200;
|
let statusCode = 200;
|
||||||
let { url } = ctx;
|
let { url } = ctx;
|
||||||
url = url.replace(this.prefix, '').split('?')[0];
|
url = url.replace(this.prefix, '').split('?')[0];
|
||||||
const token = ctx.get('Authorization');
|
const token = ctx.get('Authorization')?.replace('Bearer ', '');
|
||||||
const adminUrl = '/admin/';
|
const adminUrl = '/admin/';
|
||||||
// 路由地址为 admin前缀的 需要权限校验
|
// 路由地址为 admin前缀的 需要权限校验
|
||||||
if (_.startsWith(url, adminUrl)) {
|
if (_.startsWith(url, adminUrl)) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -142,6 +142,81 @@
|
|||||||
"childMenus": []
|
"childMenus": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "书籍管理",
|
||||||
|
"router": null,
|
||||||
|
"perms": null,
|
||||||
|
"type": 0,
|
||||||
|
"icon": "icon-goods",
|
||||||
|
"orderNum": 2,
|
||||||
|
"viewPath": null,
|
||||||
|
"keepAlive": true,
|
||||||
|
"isShow": true,
|
||||||
|
"childMenus": [
|
||||||
|
{
|
||||||
|
"name": "书籍列表",
|
||||||
|
"router": "/knowledge/book",
|
||||||
|
"perms": null,
|
||||||
|
"type": 1,
|
||||||
|
"icon": "icon-menu",
|
||||||
|
"orderNum": 1,
|
||||||
|
"viewPath": "modules/knowledge/views/book/index.vue",
|
||||||
|
"keepAlive": true,
|
||||||
|
"isShow": true,
|
||||||
|
"childMenus": [
|
||||||
|
{
|
||||||
|
"name": "新增",
|
||||||
|
"router": null,
|
||||||
|
"perms": "knowledge:book:add",
|
||||||
|
"type": 2,
|
||||||
|
"icon": null,
|
||||||
|
"orderNum": 1,
|
||||||
|
"viewPath": null,
|
||||||
|
"keepAlive": false,
|
||||||
|
"isShow": true,
|
||||||
|
"childMenus": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "删除",
|
||||||
|
"router": null,
|
||||||
|
"perms": "knowledge:book:delete",
|
||||||
|
"type": 2,
|
||||||
|
"icon": null,
|
||||||
|
"orderNum": 2,
|
||||||
|
"viewPath": null,
|
||||||
|
"keepAlive": false,
|
||||||
|
"isShow": true,
|
||||||
|
"childMenus": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "修改",
|
||||||
|
"router": null,
|
||||||
|
"perms": "knowledge:book:info,knowledge:book:update",
|
||||||
|
"type": 2,
|
||||||
|
"icon": null,
|
||||||
|
"orderNum": 3,
|
||||||
|
"viewPath": null,
|
||||||
|
"keepAlive": false,
|
||||||
|
"isShow": true,
|
||||||
|
"childMenus": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "查询",
|
||||||
|
"router": null,
|
||||||
|
"perms": "knowledge:book:page,knowledge:book:list,knowledge:book:info",
|
||||||
|
"type": 2,
|
||||||
|
"icon": null,
|
||||||
|
"orderNum": 4,
|
||||||
|
"viewPath": null,
|
||||||
|
"keepAlive": false,
|
||||||
|
"isShow": true,
|
||||||
|
"childMenus": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,14 @@ import { Inject, Provide } from '@midwayjs/core';
|
|||||||
import { BaseService } from '@cool-midway/core';
|
import { BaseService } from '@cool-midway/core';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { escapeObject } from '../utils/xss';
|
import {
|
||||||
|
escapeObject,
|
||||||
|
filterImageUrls,
|
||||||
|
truncateString,
|
||||||
|
validateYear,
|
||||||
|
validateRating,
|
||||||
|
validateCategoryId
|
||||||
|
} from '../utils/xss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库模块-电影信息
|
* 知识库模块-电影信息
|
||||||
@@ -16,14 +23,99 @@ export class KnowledgeFilmService extends BaseService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
ctx;
|
ctx;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询
|
||||||
|
* - 重写自 BaseService.page,绕开 BaseSqliteService.fieldEq 注释掉 sqlParams.push 的 bug
|
||||||
|
* (该 bug 会导致 quality/categoryId 等精确匹配字段的查询参数未传递,typeorm 默认 0,触发 SQLITE_MISMATCH)
|
||||||
|
* - 同时处理 watched 字段(前端布尔/字符串 → 数字 1/0)的类型转换
|
||||||
|
* @param query 查询条件
|
||||||
|
* @param option 查询配置
|
||||||
|
*/
|
||||||
|
async page(query: any, option?: any) {
|
||||||
|
// 1. watched 类型转换:布尔/字符串 → 数字 1/0
|
||||||
|
if (query.watched !== undefined && query.watched !== null && query.watched !== '') {
|
||||||
|
query.watched = query.watched === 'true' || query.watched === true ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 字符串数字 → 数字
|
||||||
|
if (query.categoryId !== undefined && query.categoryId !== null && query.categoryId !== '') {
|
||||||
|
if (typeof query.categoryId === 'string') {
|
||||||
|
const num = parseInt(query.categoryId, 10);
|
||||||
|
if (!isNaN(num)) {
|
||||||
|
query.categoryId = num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 3. 构造 QueryBuilder,手动管理所有字段,避免走 fieldEq 有 bug 的路径
|
||||||
|
const qb = this.knowledgeFilmEntity.createQueryBuilder('a');
|
||||||
|
|
||||||
|
// quality 精确匹配
|
||||||
|
if (query.quality !== undefined && query.quality !== null && query.quality !== '') {
|
||||||
|
qb.andWhere('a.quality = :quality', { quality: String(query.quality) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// categoryId 精确匹配
|
||||||
|
if (query.categoryId !== undefined && query.categoryId !== null && query.categoryId !== '') {
|
||||||
|
qb.andWhere('a.categoryId = :categoryId', { categoryId: query.categoryId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// watched 精确匹配
|
||||||
|
if (query.watched !== undefined && query.watched !== null && query.watched !== '') {
|
||||||
|
qb.andWhere('a.watched = :watched', { watched: query.watched });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键字搜索(name、director、mainCharacters 三个字段)
|
||||||
|
if (keyWord) {
|
||||||
|
const like = `%${keyWord}%`;
|
||||||
|
qb.andWhere(
|
||||||
|
`(a.name LIKE :kw OR a.director LIKE :kw OR a.mainCharacters 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);
|
||||||
|
|
||||||
|
// 4. 执行查询
|
||||||
|
const [list, total] = await qb.getManyAndCount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
pagination: {
|
||||||
|
page: pageNum,
|
||||||
|
size: pageSize,
|
||||||
|
total: Number(total)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增电影
|
* 新增电影
|
||||||
* - 业务层查重:检查同名电影是否已存在
|
* - 业务层查重:检查同名电影是否已存在
|
||||||
* - 对用户输入进行 XSS 过滤,防止存储型 XSS 攻击
|
* - 对用户输入进行 XSS 过滤,防止存储型 XSS 攻击
|
||||||
* - 排除 posters 字段(JSON数组,不需要转义)
|
* - 对 posters 字段进行 URL 白名单校验,只允许 http/https 协议
|
||||||
* @param params 电影数据
|
* @param params 电影数据
|
||||||
*/
|
*/
|
||||||
async add(params: any) {
|
async add(params: any) {
|
||||||
|
// 字段长度截断,防止超出数据库字段限制
|
||||||
|
if (params.name) params.name = truncateString(params.name, 200);
|
||||||
|
if (params.director) params.director = truncateString(params.director, 200);
|
||||||
|
if (params.country) params.country = truncateString(params.country, 100);
|
||||||
|
if (params.language) params.language = truncateString(params.language, 100);
|
||||||
|
if (params.quality) params.quality = truncateString(params.quality, 10);
|
||||||
|
if (params.link) params.link = truncateString(params.link, 500);
|
||||||
|
|
||||||
// 业务层查重:检查同名电影是否已存在
|
// 业务层查重:检查同名电影是否已存在
|
||||||
if (params.name) {
|
if (params.name) {
|
||||||
const exist = await this.knowledgeFilmEntity.findOne({
|
const exist = await this.knowledgeFilmEntity.findOne({
|
||||||
@@ -33,6 +125,10 @@ export class KnowledgeFilmService extends BaseService {
|
|||||||
throw new Error(`电影"${params.name}"已存在,请勿重复添加`);
|
throw new Error(`电影"${params.name}"已存在,请勿重复添加`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// URL 白名单过滤 posters 字段
|
||||||
|
if (params.posters) {
|
||||||
|
params.posters = filterImageUrls(params.posters);
|
||||||
|
}
|
||||||
// XSS 过滤 - 排除 posters 字段(JSON 数组)
|
// XSS 过滤 - 排除 posters 字段(JSON 数组)
|
||||||
const safeParams = escapeObject(params, ['posters']);
|
const safeParams = escapeObject(params, ['posters']);
|
||||||
return super.add(safeParams);
|
return super.add(safeParams);
|
||||||
@@ -41,10 +137,22 @@ export class KnowledgeFilmService extends BaseService {
|
|||||||
/**
|
/**
|
||||||
* 更新电影
|
* 更新电影
|
||||||
* - 对用户输入进行 XSS 过滤,防止存储型 XSS 攻击
|
* - 对用户输入进行 XSS 过滤,防止存储型 XSS 攻击
|
||||||
* - 排除 posters 字段(JSON数组,不需要转义)
|
* - 对 posters 字段进行 URL 白名单校验,只允许 http/https 协议
|
||||||
* @param params 电影数据
|
* @param params 电影数据
|
||||||
*/
|
*/
|
||||||
async update(params: any) {
|
async update(params: any) {
|
||||||
|
// 字段长度截断,防止超出数据库字段限制
|
||||||
|
if (params.name) params.name = truncateString(params.name, 200);
|
||||||
|
if (params.director) params.director = truncateString(params.director, 200);
|
||||||
|
if (params.country) params.country = truncateString(params.country, 100);
|
||||||
|
if (params.language) params.language = truncateString(params.language, 100);
|
||||||
|
if (params.quality) params.quality = truncateString(params.quality, 10);
|
||||||
|
if (params.link) params.link = truncateString(params.link, 500);
|
||||||
|
|
||||||
|
// URL 白名单过滤 posters 字段
|
||||||
|
if (params.posters) {
|
||||||
|
params.posters = filterImageUrls(params.posters);
|
||||||
|
}
|
||||||
// XSS 过滤 - 排除 posters 字段(JSON 数组)
|
// XSS 过滤 - 排除 posters 字段(JSON 数组)
|
||||||
const safeParams = escapeObject(params, ['posters']);
|
const safeParams = escapeObject(params, ['posters']);
|
||||||
return super.update(safeParams);
|
return super.update(safeParams);
|
||||||
@@ -81,25 +189,25 @@ export class KnowledgeFilmService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 映射 Excel 列名到数据库字段
|
// 映射 Excel 列名到数据库字段,并进行字段校验与截断
|
||||||
const filmData = {
|
const filmData = {
|
||||||
name: film.name || '',
|
name: truncateString(film.name || '', 200),
|
||||||
director: film.director || film['导演'] || film['导演/作者'] || '',
|
director: truncateString(film.director || film['导演'] || film['导演/作者'] || '', 200),
|
||||||
year: film.year || film['年份'] || null,
|
year: validateYear(film.year || film['年份']),
|
||||||
country: film.country || film['国家'] || '',
|
country: truncateString(film.country || film['国家'] || '', 100),
|
||||||
language: film.language || film['语言'] || '',
|
language: truncateString(film.language || film['语言'] || '', 100),
|
||||||
mainCharacters: film.mainCharacters || film['主要人物'] || '',
|
mainCharacters: film.mainCharacters || film['主要人物'] || '',
|
||||||
synopsis: film.synopsis || film['内容简介'] || '',
|
synopsis: film.synopsis || film['内容简介'] || '',
|
||||||
backgroundStory: film.backgroundStory || film['背景故事'] || '',
|
backgroundStory: film.backgroundStory || film['背景故事'] || '',
|
||||||
posters: film.posters || film['海报链接']
|
posters: filterImageUrls(film.posters || film['海报链接']
|
||||||
? [film.posters || film['海报链接']]
|
? [film.posters || film['海报链接']]
|
||||||
: [],
|
: []),
|
||||||
categoryId: film.categoryId ?? film['分类ID'] ?? film['分类'] ?? 45,
|
categoryId: validateCategoryId(film.categoryId ?? film['分类ID'] ?? film['分类'], 45),
|
||||||
quality: film.quality || film['质量'] || film['质量评级'] || 'C',
|
quality: truncateString(film.quality || film['质量'] || film['质量评级'] || 'C', 10),
|
||||||
watched: film.watched || film['是否已看'] || false,
|
watched: film.watched || film['是否已看'] || false,
|
||||||
doubanRating: film.doubanRating || film['豆瓣评分'] || null,
|
doubanRating: validateRating(film.doubanRating || film['豆瓣评分']),
|
||||||
tmdbRating: film.tmdbRating || film['TMDB评分'] || null,
|
tmdbRating: validateRating(film.tmdbRating || film['TMDB评分']),
|
||||||
link: film.link || film['链接'] || '',
|
link: truncateString(film.link || film['链接'] || '', 500),
|
||||||
honors: film.honors || film['荣誉'] || '',
|
honors: film.honors || film['荣誉'] || '',
|
||||||
highlights: film.highlights || film['亮点所在'] || '',
|
highlights: film.highlights || film['亮点所在'] || '',
|
||||||
whyWorthWatching: film.whyWorthWatching || film['为什么值得一看'] || ''
|
whyWorthWatching: film.whyWorthWatching || film['为什么值得一看'] || ''
|
||||||
|
|||||||
@@ -21,6 +21,112 @@ export function escapeHtml(str: string): string {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证图片 URL 是否安全
|
||||||
|
* 只允许 http:// 和 https:// 协议,防止 javascript: 等恶意协议
|
||||||
|
* @param url 图片 URL
|
||||||
|
* @returns 是否安全
|
||||||
|
*/
|
||||||
|
export function isValidImageUrl(url: string): boolean {
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 只允许 http:// 和 https:// 协议
|
||||||
|
return url.startsWith('http://') || url.startsWith('https://');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤图片 URL 数组,移除不安全的 URL
|
||||||
|
* @param urls 图片 URL 数组
|
||||||
|
* @returns 安全的 URL 数组
|
||||||
|
*/
|
||||||
|
export function filterImageUrls(urls: string[] | any): string[] {
|
||||||
|
if (!Array.isArray(urls)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return urls.filter(url => isValidImageUrl(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截断字符串到指定长度,防止超出数据库字段长度限制
|
||||||
|
* @param str 输入字符串
|
||||||
|
* @param maxLength 最大长度
|
||||||
|
* @returns 截断后的字符串
|
||||||
|
*/
|
||||||
|
export function truncateString(str: string, maxLength: number): string {
|
||||||
|
if (!str || typeof str !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return str.length > maxLength ? str.substring(0, maxLength) : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证并规范化年份字段
|
||||||
|
* 只允许 1800-当前年份+10 范围内的合理年份
|
||||||
|
* @param year 输入年份
|
||||||
|
* @returns 有效的年份数字或 null
|
||||||
|
*/
|
||||||
|
export function validateYear(year: any): number | null {
|
||||||
|
if (year === null || year === undefined || year === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const num = parseInt(String(year), 10);
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
if (num < 1800 || num > currentYear + 10) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证评分字段(豆瓣评分、TMDB评分)
|
||||||
|
* 只允许 0-10 范围内的数字,精度到小数点后1位
|
||||||
|
* @param rating 输入评分
|
||||||
|
* @returns 有效的评分数字或 null
|
||||||
|
*/
|
||||||
|
export function validateRating(rating: any): number | null {
|
||||||
|
if (rating === null || rating === undefined || rating === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 解析字符串格式的评分,如 "TMDB 7.4" / "豆瓣 8.0"
|
||||||
|
if (typeof rating === 'string') {
|
||||||
|
const match = rating.match(/(\d+\.?\d*)/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
rating = parseFloat(match[1]);
|
||||||
|
}
|
||||||
|
const num = parseFloat(String(rating));
|
||||||
|
if (isNaN(num)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (num < 0 || num > 10) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.round(num * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证分类ID
|
||||||
|
* 必须是正整数
|
||||||
|
* @param categoryId 输入分类ID
|
||||||
|
* @param defaultValue 默认值
|
||||||
|
* @returns 有效的分类ID
|
||||||
|
*/
|
||||||
|
export function validateCategoryId(categoryId: any, defaultValue: number | null = null): number | null {
|
||||||
|
if (categoryId === null || categoryId === undefined || categoryId === '') {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
const num = parseInt(String(categoryId), 10);
|
||||||
|
if (isNaN(num) || num <= 0) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 递归转义对象中的所有字符串字段
|
* 递归转义对象中的所有字符串字段
|
||||||
* @param obj 输入对象
|
* @param obj 输入对象
|
||||||
|
|||||||
Reference in New Issue
Block a user