diff --git a/src/modules/knowledge/service/film.ts b/src/modules/knowledge/service/film.ts new file mode 100644 index 0000000..2c048aa --- /dev/null +++ b/src/modules/knowledge/service/film.ts @@ -0,0 +1,127 @@ +import { KnowledgeFilmEntity } from './../entity/film'; +import { Inject, Provide } from '@midwayjs/core'; +import { BaseService } from '@cool-midway/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { escapeObject } from '../utils/xss'; + +/** + * 知识库模块-电影信息 + */ +@Provide() +export class KnowledgeFilmService extends BaseService { + @InjectEntityModel(KnowledgeFilmEntity) + knowledgeFilmEntity: Repository; + + @Inject() + ctx; + + /** + * 新增电影 + * - 业务层查重:检查同名电影是否已存在 + * - 对用户输入进行 XSS 过滤,防止存储型 XSS 攻击 + * - 排除 posters 字段(JSON数组,不需要转义) + * @param params 电影数据 + */ + async add(params: any) { + // 业务层查重:检查同名电影是否已存在 + if (params.name) { + const exist = await this.knowledgeFilmEntity.findOne({ + where: { name: params.name } + }); + if (exist) { + throw new Error(`电影"${params.name}"已存在,请勿重复添加`); + } + } + // XSS 过滤 - 排除 posters 字段(JSON 数组) + const safeParams = escapeObject(params, ['posters']); + return super.add(safeParams); + } + + /** + * 更新电影 + * - 对用户输入进行 XSS 过滤,防止存储型 XSS 攻击 + * - 排除 posters 字段(JSON数组,不需要转义) + * @param params 电影数据 + */ + async update(params: any) { + // XSS 过滤 - 排除 posters 字段(JSON 数组) + const safeParams = escapeObject(params, ['posters']); + return super.update(safeParams); + } + + /** + * API: importFilms - 批量导入电影数据 + * - 业务层查重:检查同名电影是否已存在,已存在则跳过 + * - 将 Excel 导入的数据逐条插入数据库 + * - 跳过名称为空的记录 + * - 对文本字段进行 XSS 过滤 + * @param films 电影数据数组(来自 Excel 解析) + * @returns 导入结果统计 + */ + async importFilms(films: any[]) { + const successList: string[] = []; + const failList: { name: string; error: string }[] = []; + const skipList: string[] = []; + + for (const film of films) { + // 跳过名称为空的记录 + if (!film.name) { + failList.push({ name: film.name || '(空)', error: '名称不能为空' }); + continue; + } + + // 业务层查重:检查同名电影是否已存在 + const exist = await this.knowledgeFilmEntity.findOne({ + where: { name: film.name } + }); + if (exist) { + skipList.push(film.name); + continue; + } + + try { + // 映射 Excel 列名到数据库字段 + const filmData = { + name: film.name || '', + director: film.director || film['导演'] || film['导演/作者'] || '', + year: film.year || film['年份'] || null, + country: film.country || film['国家'] || '', + language: film.language || film['语言'] || '', + mainCharacters: film.mainCharacters || film['主要人物'] || '', + synopsis: film.synopsis || film['内容简介'] || '', + backgroundStory: film.backgroundStory || film['背景故事'] || '', + posters: film.posters || film['海报链接'] + ? [film.posters || film['海报链接']] + : [], + categoryId: film.categoryId ?? film['分类ID'] ?? film['分类'] ?? 45, + quality: film.quality || film['质量'] || film['质量评级'] || 'C', + watched: film.watched || film['是否已看'] || false, + doubanRating: film.doubanRating || film['豆瓣评分'] || null, + tmdbRating: film.tmdbRating || film['TMDB评分'] || null, + link: film.link || film['链接'] || '', + honors: film.honors || film['荣誉'] || '', + highlights: film.highlights || film['亮点所在'] || '', + whyWorthWatching: film.whyWorthWatching || film['为什么值得一看'] || '' + }; + + // XSS 过滤 + const safeData = escapeObject(filmData, ['posters']); + await this.knowledgeFilmEntity.insert(safeData); + successList.push(filmData.name); + } catch (e: any) { + failList.push({ name: film.name, error: e.message || '导入失败' }); + } + } + + return { + total: films.length, + success: successList.length, + fail: failList.length, + skip: skipList.length, + successList, + failList, + skipList + }; + } +}