From 57ad860b68d0f83e0c3f59acbbfa07b38755321c Mon Sep 17 00:00:00 2001 From: luch1994 <1097650398@qq.com> Date: Thu, 12 Sep 2024 17:58:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E9=AA=8C=E6=94=B6?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/.env | 4 +- server/.gitignore | 4 +- server/src/models/session.entity.ts | 3 + .../controllers/downloadTask.controller.ts | 5 +- .../survey/controllers/session.controller.ts | 7 +- .../survey/controllers/survey.controller.ts | 27 ++++--- .../survey/services/downloadTask.service.ts | 14 ++-- .../survey/services/session.service.ts | 3 +- .../controllers/surveyResponse.controller.ts | 74 +++++++++++-------- .../services/counter.service.ts | 21 ------ server/src/utils/xss.ts | 42 +++++------ .../pages/analysis/components/DataTable.vue | 13 +++- .../pages/analysis/pages/DataTablePage.vue | 19 ++++- .../pages/downloadTask/TaskList.vue | 4 +- .../components/DownloadTaskList.vue | 72 +++++++++++------- .../modules/contentModule/PublishPanel.vue | 50 ++++++++++--- .../edit/modules/contentModule/SavePanel.vue | 12 +-- web/src/management/pages/list/index.vue | 2 +- web/src/management/router/index.ts | 5 +- 19 files changed, 235 insertions(+), 146 deletions(-) diff --git a/server/.env b/server/.env index 3e4e9e70..b2286da4 100644 --- a/server/.env +++ b/server/.env @@ -1,5 +1,5 @@ -XIAOJU_SURVEY_MONGO_DB_NAME= xiaojuSurvey -XIAOJU_SURVEY_MONGO_URL= # mongodb://localhost:27017 # 建议设置强密码 +XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey +XIAOJU_SURVEY_MONGO_URL= # mongodb://127.0.0.1:27017 # 建议设置强密码 XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin XIAOJU_SURVEY_REDIS_HOST= diff --git a/server/.gitignore b/server/.gitignore index 6309f8c1..93515c51 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -38,4 +38,6 @@ yarn.lock !.vscode/launch.json !.vscode/extensions.json -tmp \ No newline at end of file +tmp +exportfile +userUpload \ No newline at end of file diff --git a/server/src/models/session.entity.ts b/server/src/models/session.entity.ts index c7e3f485..bee82cc1 100644 --- a/server/src/models/session.entity.ts +++ b/server/src/models/session.entity.ts @@ -12,4 +12,7 @@ export class Session extends BaseEntity { @Column() surveyId: string; + + @Column() + userId: string; } diff --git a/server/src/modules/survey/controllers/downloadTask.controller.ts b/server/src/modules/survey/controllers/downloadTask.controller.ts index f9001112..23ac42cb 100644 --- a/server/src/modules/survey/controllers/downloadTask.controller.ts +++ b/server/src/modules/survey/controllers/downloadTask.controller.ts @@ -79,15 +79,16 @@ export class DownloadTaskController { async downloadList( @Query() queryInfo: GetDownloadTaskListDto, + @Request() req, ) { const { value, error } = GetDownloadTaskListDto.validate(queryInfo); if (error) { this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } - const { ownerId, pageIndex, pageSize } = value; + const { pageIndex, pageSize } = value; const { total, list } = await this.downloadTaskService.getDownloadTaskList({ - ownerId, + ownerId: req.user._id.toString(), pageIndex, pageSize, }); diff --git a/server/src/modules/survey/controllers/session.controller.ts b/server/src/modules/survey/controllers/session.controller.ts index 4b4455e4..33b87a46 100644 --- a/server/src/modules/survey/controllers/session.controller.ts +++ b/server/src/modules/survey/controllers/session.controller.ts @@ -39,6 +39,8 @@ export class SessionController { reqBody: { surveyId: string; }, + @Request() + req, ) { const { value, error } = Joi.object({ surveyId: Joi.string().required(), @@ -50,7 +52,10 @@ export class SessionController { } const surveyId = value.surveyId; - const session = await this.sessionService.create({ surveyId }); + const session = await this.sessionService.create({ + surveyId, + userId: req.user._id.toString(), + }); return { code: 200, diff --git a/server/src/modules/survey/controllers/survey.controller.ts b/server/src/modules/survey/controllers/survey.controller.ts index e97b21ca..f68cc456 100644 --- a/server/src/modules/survey/controllers/survey.controller.ts +++ b/server/src/modules/survey/controllers/survey.controller.ts @@ -34,6 +34,7 @@ import { WorkspaceGuard } from 'src/guards/workspace.guard'; import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace'; import { SessionService } from '../services/session.service'; import { MemberType, WhitelistType } from 'src/interfaces/survey'; +import { UserService } from 'src/modules/auth/services/user.service'; @ApiTags('survey') @Controller('/api/survey') @@ -47,6 +48,7 @@ export class SurveyController { private readonly logger: XiaojuSurveyLogger, private readonly counterService: CounterService, private readonly sessionService: SessionService, + private readonly userService: UserService, ) {} @Get('/getBannerData') @@ -146,11 +148,21 @@ export class SurveyController { if (latestEditingOne && latestEditingOne._id.toString() !== sessionId) { const curSession = await this.sessionService.findOne(sessionId); if (curSession.createDate <= latestEditingOne.updateDate) { - // 在当前用户打开之后,有人保存过了 - throw new HttpException( - '当前问卷已在其它页面开启编辑', - EXCEPTION_CODE.SURVEY_SAVE_CONFLICT, - ); + // 在当前用户打开之后,被其他页面保存过了 + const isSameOperator = + latestEditingOne.userId === req.user._id.toString(); + let preOperator; + if (!isSameOperator) { + preOperator = await this.userService.getUserById( + latestEditingOne.userId, + ); + } + return { + code: EXCEPTION_CODE.SURVEY_SAVE_CONFLICT, + errmsg: isSameOperator + ? '当前问卷已在其它页面开启编辑,刷新以获取最新内容' + : `当前问卷已由 ${preOperator.username} 编辑,刷新以获取最新内容`, + }; } } await this.sessionService.updateSessionToEditing({ sessionId, surveyId }); @@ -331,11 +343,6 @@ export class SurveyController { pageId: surveyId, }); - await this.counterService.createCounters({ - surveyPath: surveyMeta.surveyPath, - dataList: surveyConf.code.dataConf.dataList, - }); - await this.surveyHistoryService.addHistory({ surveyId, schema: surveyConf.code, diff --git a/server/src/modules/survey/services/downloadTask.service.ts b/server/src/modules/survey/services/downloadTask.service.ts index 2a3e0bba..7f276075 100644 --- a/server/src/modules/survey/services/downloadTask.service.ts +++ b/server/src/modules/survey/services/downloadTask.service.ts @@ -13,6 +13,7 @@ import { load } from 'cheerio'; import { get } from 'lodash'; import { FileService } from 'src/modules/file/services/file.service'; import { XiaojuSurveyLogger } from 'src/logger'; +import moment from 'moment'; @Injectable() export class DownloadTaskService { @@ -41,6 +42,7 @@ export class DownloadTaskService { operatorId: string; params: any; }) { + const filename = `${responseSchema.title}-${params.isDesensitive ? '脱敏' : '原'}回收数据-${moment().format('YYYYMMDDHHmmss')}.xlsx`; const downloadTask = this.downloadTaskRepository.create({ surveyId, surveyPath: responseSchema.surveyPath, @@ -50,6 +52,7 @@ export class DownloadTaskService { ...params, title: responseSchema.title, }, + filename, }); await this.downloadTaskRepository.save(downloadTask); return downloadTask._id.toString(); @@ -65,7 +68,7 @@ export class DownloadTaskService { pageSize: number; }) { const where = { - onwer: ownerId, + ownerId, 'curStatus.status': { $ne: RECORD_STATUS.REMOVED, }, @@ -209,16 +212,12 @@ export class DownloadTaskService { { name: 'sheet1', data: xlsxData, options: {} }, ]); - const isDesensitive = taskInfo.params?.isDesensitive; - - const originalname = `${taskInfo.params.title}-${isDesensitive ? '脱敏' : '原'}回收数据.xlsx`; - const file: Express.Multer.File = { fieldname: 'file', - originalname: originalname, + originalname: taskInfo.filename, encoding: '7bit', mimetype: 'application/octet-stream', - filename: originalname, + filename: taskInfo.filename, size: buffer.length, buffer: buffer, stream: null, @@ -246,7 +245,6 @@ export class DownloadTaskService { $set: { curStatus, url, - filename: originalname, fileKey: key, fileSize: buffer.length, }, diff --git a/server/src/modules/survey/services/session.service.ts b/server/src/modules/survey/services/session.service.ts index f6a78134..e70fd3da 100644 --- a/server/src/modules/survey/services/session.service.ts +++ b/server/src/modules/survey/services/session.service.ts @@ -12,9 +12,10 @@ export class SessionService { private readonly sessionRepository: MongoRepository, ) {} - create({ surveyId }) { + create({ surveyId, userId }) { const session = this.sessionRepository.create({ surveyId, + userId, }); return this.sessionRepository.save(session); } diff --git a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts index fb2bcf04..160f9366 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts @@ -2,7 +2,6 @@ import { Controller, Post, Body, HttpCode } from '@nestjs/common'; import { HttpException } from 'src/exceptions/httpException'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; import { checkSign } from 'src/utils/checkSign'; -import { cleanRichTextWithMediaTag } from 'src/utils/xss' import { ENCRYPT_TYPE } from 'src/enums/encrypt'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { getPushingData } from 'src/utils/messagePushing'; @@ -23,6 +22,14 @@ import { XiaojuSurveyLogger } from 'src/logger'; import { WhitelistType } from 'src/interfaces/survey'; import { UserService } from 'src/modules/auth/services/user.service'; import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; +import { QUESTION_TYPE } from 'src/enums/question'; + +const optionQuestionType: Array = [ + QUESTION_TYPE.RADIO, + QUESTION_TYPE.CHECKBOX, + QUESTION_TYPE.BINARY_CHOICE, + QUESTION_TYPE.VOTE, +]; @ApiTags('surveyResponse') @Controller('/api/surveyResponse') @@ -207,6 +214,7 @@ export class SurveyResponseController { const optionTextAndId = dataList .filter((questionItem) => { return ( + optionQuestionType.includes(questionItem.type) && Array.isArray(questionItem.options) && questionItem.options.length > 0 && decryptedData[questionItem.field] @@ -222,20 +230,23 @@ export class SurveyResponseController { return pre; }, {}); + // 使用redis作为锁,校验选项配额 const surveyId = responseSchema.pageId; const lockKey = `locks:optionSelectedCount:${surveyId}`; const lock = await this.redisService.lockResource(lockKey, 1000); this.logger.info(`lockKey: ${lockKey}`); try { + const successParams = []; for (const field in decryptedData) { const value = decryptedData[field]; const values = Array.isArray(value) ? value : [value]; if (field in optionTextAndId) { - const optionCountData = await this.counterService.get({ - key: field, - surveyPath, - type: 'option', - }); + const optionCountData = + (await this.counterService.get({ + key: field, + surveyPath, + type: 'option', + })) || {}; //遍历选项hash值 for (const val of values) { @@ -243,38 +254,41 @@ export class SurveyResponseController { (opt) => opt['hash'] === val, ); const quota = parseInt(option['quota']); - if (quota !== 0 && quota <= optionCountData[val]) { - const item = dataList.find((item) => item['field'] === field); - throw new HttpException( - `【${cleanRichTextWithMediaTag(item['title'])}】中的【${cleanRichTextWithMediaTag(option['text'])}】所选人数已达到上限,请重新选择`, - EXCEPTION_CODE.RESPONSE_OVER_LIMIT, - ); + if ( + quota && + optionCountData?.[val] && + quota <= optionCountData[val] + ) { + return { + code: EXCEPTION_CODE.RESPONSE_OVER_LIMIT, + data: { + field, + optionHash: option.hash, + }, + }; + } + if (!optionCountData[val]) { + optionCountData[val] = 0; } + optionCountData[val]++; } - } - } - - for (const field in decryptedData) { - const value = decryptedData[field]; - const values = Array.isArray(value) ? value : [value]; - if (field in optionTextAndId) { - const optionCountData = await this.counterService.get({ + if (!optionCountData['total']) { + optionCountData['total'] = 1; + } else { + optionCountData['total']++; + } + successParams.push({ key: field, surveyPath, type: 'option', + data: optionCountData, }); - for (const val of values) { - optionCountData[val]++; - this.counterService.set({ - key: field, - surveyPath, - type: 'option', - data: optionCountData, - }); - } - optionCountData['total']++; } } + // 校验通过后统一更新 + await Promise.all( + successParams.map((item) => this.counterService.set(item)), + ); } catch (error) { this.logger.error(error.message); throw error; diff --git a/server/src/modules/surveyResponse/services/counter.service.ts b/server/src/modules/surveyResponse/services/counter.service.ts index 9036dae8..0b72ab82 100644 --- a/server/src/modules/surveyResponse/services/counter.service.ts +++ b/server/src/modules/surveyResponse/services/counter.service.ts @@ -65,25 +65,4 @@ export class CounterService { return pre; }, {}); } - - async createCounters({ surveyPath, dataList }) { - const optionList = dataList.filter((questionItem) => { - return ( - Array.isArray(questionItem.options) && questionItem.options.length > 0 - ); - }); - optionList.forEach((option) => { - const data = {}; - option.options.forEach((option) => { - data[option.hash] = 0; - }); - data['total'] = 0; - this.set({ - surveyPath, - key: option.field, - type: 'option', - data: data, - }); - }); - } } diff --git a/server/src/utils/xss.ts b/server/src/utils/xss.ts index 7da9f2bf..b7ea7ee5 100644 --- a/server/src/utils/xss.ts +++ b/server/src/utils/xss.ts @@ -1,53 +1,53 @@ -import xss from 'xss' +import xss from 'xss'; const myxss = new (xss as any).FilterXSS({ onIgnoreTagAttr(tag, name, value) { if (name === 'style' || name === 'class') { - return `${name}="${value}"` + return `${name}="${value}"`; } - return undefined + return undefined; }, onIgnoreTag(tag, html) { // 过滤为空,否则不过滤为空 - var re1 = new RegExp('<.+?>', 'g') + const re1 = new RegExp('<.+?>', 'g'); if (re1.test(html)) { - return '' + return ''; } else { - return html + return html; } - } -}) + }, +}); export const cleanRichTextWithMediaTag = (text) => { if (!text) { - return text === 0 ? 0 : '' + return text === 0 ? 0 : ''; } const html = transformHtmlTag(text) .replace(//g, '[图片]') - .replace(//g, '[视频]') - const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, '') + .replace(//g, '[视频]'); + const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, ''); - return content -} + return content; +}; export function escapeHtml(html) { - return html.replace(//g, '>') + return html.replace(//g, '>'); } export const transformHtmlTag = (html) => { - if (!html) return '' - if (typeof html !== 'string') return html + '' + if (!html) return ''; + if (typeof html !== 'string') return html + ''; return html .replace(html ? /&(?!#?\w+;)/g : /&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") - .replace(/\\\n/g, '\\n') + .replace(/\\\n/g, '\\n'); //.replace(/ /g, "") -} +}; -const filterXSSClone = myxss.process.bind(myxss) +const filterXSSClone = myxss.process.bind(myxss); -export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html)) +export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html)); -export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html)) +export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html)); diff --git a/web/src/management/pages/analysis/components/DataTable.vue b/web/src/management/pages/analysis/components/DataTable.vue index e6835d64..437315a9 100644 --- a/web/src/management/pages/analysis/components/DataTable.vue +++ b/web/src/management/pages/analysis/components/DataTable.vue @@ -60,6 +60,7 @@ -