From d08f1c71e521607c200ec0756d3c02c974154ee0 Mon Sep 17 00:00:00 2001 From: luch1994 <1097650398@qq.com> Date: Mon, 26 Aug 2024 11:31:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=8C=97=E5=A4=A7?= =?UTF-8?q?=E8=AF=BE=E7=A8=8B=E7=9B=B8=E5=85=B3=E7=9A=84=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + nginx/nginx.conf | 3 + package.json | 5 - server/.env | 16 +- server/package.json | 8 +- server/scripts/run-local.ts | 13 +- server/src/app.module.ts | 10 +- server/src/enums/exceptionCode.ts | 1 + server/src/enums/index.ts | 2 + server/src/guards/session.guard.ts | 94 +++++ server/src/guards/survey.guard.ts | 1 - server/src/interfaces/survey.ts | 1 - server/src/logger/index.ts | 25 +- server/src/logger/logger.provider.ts | 6 +- server/src/logger/util.ts | 4 +- .../src/middlewares/logRequest.middleware.ts | 8 +- server/src/models/captcha.entity.ts | 3 +- server/src/models/clientEncrypt.entity.ts | 3 +- server/src/models/downloadTask.entity.ts | 34 ++ server/src/models/session.entity.ts | 15 + server/src/models/surveyDownload.entity.ts | 46 --- server/src/models/surveyHistory.entity.ts | 4 +- .../auth/controllers/auth.controller.ts | 24 +- .../auth/controllers/user.controller.ts | 21 +- .../src/modules/auth/services/auth.service.ts | 6 +- .../src/modules/file/services/file.service.ts | 7 +- .../services/uploadHandlers/local.handler.ts | 13 +- server/src/modules/mutex/mutex.module.ts | 9 - .../mutex/services/mutexService.service.ts | 28 -- server/src/modules/redis/redis.module.ts | 9 + server/src/modules/redis/redis.service.ts | 32 ++ .../__test/collaborator.controller.spec.ts | 34 +- .../__test/collaborator.service.spec.ts | 8 +- .../__test/dataStatistic.controller.spec.ts | 18 +- .../__test/surveyHistory.controller.spec.ts | 6 +- .../__test/surveyHistory.service.spec.ts | 8 +- .../controllers/collaborator.controller.ts | 28 +- .../controllers/dataStatistic.controller.ts | 11 +- .../controllers/downloadTask.controller.ts | 187 +++++++++ .../survey/controllers/session.controller.ts | 84 ++++ .../survey/controllers/survey.controller.ts | 45 ++- .../controllers/surveyDownload.controller.ts | 219 ----------- .../controllers/surveyHistory.controller.ts | 56 +-- .../controllers/surveyMeta.controller.ts | 14 +- .../modules/survey/dto/downloadTask.dto.ts | 51 +++ .../src/modules/survey/dto/getdownload.dto.ts | 43 --- .../survey/services/collaborator.service.ts | 4 +- .../survey/services/downloadTask.service.ts | 282 ++++++++++++++ .../survey/services/message.service.ts | 87 ----- .../survey/services/session.service.ts | 79 ++++ .../survey/services/surveyDownload.service.ts | 365 ------------------ .../survey/services/surveyHistory.service.ts | 29 +- server/src/modules/survey/survey.module.ts | 33 +- .../surveyTemplate/survey/normal.json | 1 - .../__test/surveyResponse.controller.spec.ts | 19 +- .../controllers/responseSchema.controller.ts | 4 +- .../controllers/surveyResponse.controller.ts | 40 +- .../surveyResponse/surveyResponse.module.ts | 10 +- .../_test/workspace.controller.spec.ts | 4 +- .../controllers/workspace.controller.ts | 10 +- web/src/management/App.vue | 67 +++- web/src/management/api/analysis.js | 10 +- web/src/management/api/auth.js | 4 + web/src/management/api/download.js | 34 -- web/src/management/api/downloadTask.js | 29 ++ web/src/management/api/survey.js | 22 +- web/src/management/config/questionConfig.js | 1 - .../pages/analysis/pages/DataTablePage.vue | 140 ++++--- .../download/components/DownloadList.vue | 273 ------------- .../TaskList.vue} | 44 +-- .../components/DownloadTaskList.vue | 200 ++++++++++ web/src/management/pages/edit/index.vue | 57 +-- .../modules/contentModule/PublishPanel.vue | 69 +--- .../edit/modules/contentModule/SavePanel.vue | 92 ++--- web/src/management/pages/list/index.vue | 4 +- web/src/management/router/index.ts | 4 +- web/src/management/store/download/index.js | 54 --- web/src/management/store/edit/state.js | 63 --- web/src/management/store/index.js | 21 - web/src/management/stores/edit.ts | 39 +- .../questions/widgets/CheckboxModule/meta.js | 6 +- .../questions/widgets/RadioModule/meta.js | 13 +- .../materials/setters/widgets/QuotaConfig.vue | 14 - web/src/render/components/QuestionWrapper.vue | 2 +- web/src/render/pages/RenderPage.vue | 12 +- web/src/render/store/actions.js | 364 ----------------- web/src/render/store/mutations.js | 68 ---- web/src/render/store/state.js | 16 - web/src/render/stores/question.js | 6 - web/src/render/stores/survey.js | 67 ++-- web/vite.config.ts | 4 + 91 files changed, 1663 insertions(+), 2369 deletions(-) delete mode 100644 package.json create mode 100644 server/src/guards/session.guard.ts create mode 100644 server/src/models/downloadTask.entity.ts create mode 100644 server/src/models/session.entity.ts delete mode 100644 server/src/models/surveyDownload.entity.ts delete mode 100644 server/src/modules/mutex/mutex.module.ts delete mode 100644 server/src/modules/mutex/services/mutexService.service.ts create mode 100644 server/src/modules/redis/redis.module.ts create mode 100644 server/src/modules/redis/redis.service.ts create mode 100644 server/src/modules/survey/controllers/downloadTask.controller.ts create mode 100644 server/src/modules/survey/controllers/session.controller.ts delete mode 100644 server/src/modules/survey/controllers/surveyDownload.controller.ts create mode 100644 server/src/modules/survey/dto/downloadTask.dto.ts delete mode 100644 server/src/modules/survey/dto/getdownload.dto.ts create mode 100644 server/src/modules/survey/services/downloadTask.service.ts delete mode 100644 server/src/modules/survey/services/message.service.ts create mode 100644 server/src/modules/survey/services/session.service.ts delete mode 100644 server/src/modules/survey/services/surveyDownload.service.ts delete mode 100644 web/src/management/api/download.js create mode 100644 web/src/management/api/downloadTask.js delete mode 100644 web/src/management/pages/download/components/DownloadList.vue rename web/src/management/pages/{download/SurveyDownloadPage.vue => downloadTask/TaskList.vue} (67%) create mode 100644 web/src/management/pages/downloadTask/components/DownloadTaskList.vue delete mode 100644 web/src/management/store/download/index.js delete mode 100644 web/src/management/store/edit/state.js delete mode 100644 web/src/management/store/index.js delete mode 100644 web/src/render/store/actions.js delete mode 100644 web/src/render/store/mutations.js delete mode 100644 web/src/render/store/state.js diff --git a/.gitignore b/.gitignore index e0d3138e..7c79d23c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules dist package-lock.json +yarn.lock # local env files .env.local @@ -25,6 +26,8 @@ pnpm-debug.log* *.sw? .history + +exportfile components.d.ts # 默认的上传文件夹 diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 30e5d7fc..529d57e5 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -52,6 +52,9 @@ http { proxy_pass http://127.0.0.1:3000; } + location /exportfile { + proxy_pass http://127.0.0.1:3000; + } # 静态文件的默认存储文件夹 # 文件夹的配置在 server/src/modules/file/config/index.ts SERVER_LOCAL_CONFIG.FILE_KEY_PREFIX location /userUpload { diff --git a/package.json b/package.json deleted file mode 100644 index 1bb6abe9..00000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "node-cron": "^3.0.3" - } -} diff --git a/server/.env b/server/.env index c039a348..8651c097 100644 --- a/server/.env +++ b/server/.env @@ -1,12 +1,18 @@ -XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey -XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017 -XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin +XIAOJU_SURVEY_MONGO_DB_NAME= # xiaojuSurvey +XIAOJU_SURVEY_MONGO_URL= # mongodb://localhost:27017 # 建议设置强密码 +XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin +XIAOJU_SURVEY_REDIS_HOST= +XIAOJU_SURVEY_REDIS_PORT= +XIAOJU_SURVEY_REDIS_USERNAME= +XIAOJU_SURVEY_REDIS_PASSWORD= +XIAOJU_SURVEY_REDIS_DB= -XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey + +XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY= # dataAesEncryptSecretKey XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret XIAOJU_SURVEY_JWT_EXPIRES_IN=8h -XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log \ No newline at end of file +XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log diff --git a/server/package.json b/server/package.json index 7f2bbe69..5222380a 100644 --- a/server/package.json +++ b/server/package.json @@ -27,11 +27,11 @@ "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.1", "ali-oss": "^6.20.0", - "async-mutex": "^0.5.0", - "cheerio": "^1.0.0-rc.12", + "cheerio": "1.0.0-rc.12", "crypto-js": "^4.2.0", "dotenv": "^16.3.2", "fs-extra": "^11.2.0", + "ioredis": "^5.4.1", "joi": "^17.11.0", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", @@ -40,10 +40,11 @@ "moment": "^2.30.1", "mongodb": "^5.9.2", "nanoid": "^3.3.7", - "node-cron": "^3.0.3", "node-fetch": "^2.7.0", "node-forge": "^1.3.1", + "node-xlsx": "^0.24.0", "qiniu": "^7.11.1", + "redlock": "^5.0.0-beta.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "svg-captcha": "^1.4.0", @@ -72,6 +73,7 @@ "jest": "^29.5.0", "mongodb-memory-server": "^9.1.4", "prettier": "^3.0.0", + "redis-memory-server": "^0.11.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", diff --git a/server/scripts/run-local.ts b/server/scripts/run-local.ts index 963d3bea..9e45ed64 100644 --- a/server/scripts/run-local.ts +++ b/server/scripts/run-local.ts @@ -1,5 +1,6 @@ import { MongoMemoryServer } from 'mongodb-memory-server'; import { spawn } from 'child_process'; +import { RedisMemoryServer } from 'redis-memory-server'; async function startServerAndRunScript() { // 启动 MongoDB 内存服务器 @@ -8,12 +9,19 @@ async function startServerAndRunScript() { console.log('MongoDB Memory Server started:', mongoUri); + const redisServer = new RedisMemoryServer(); + const redisHost = await redisServer.getHost(); + const redisPort = await redisServer.getPort(); + // 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量 const tsnode = spawn( 'cross-env', [ `XIAOJU_SURVEY_MONGO_URL=${mongoUri}`, + `XIAOJU_SURVEY_REDIS_HOST=${redisHost}`, + `XIAOJU_SURVEY_REDIS_PORT=${redisPort}`, 'NODE_ENV=development', + 'SERVER_ENV=local', 'npm', 'run', 'start:dev', @@ -31,9 +39,10 @@ async function startServerAndRunScript() { console.error(data); }); - tsnode.on('close', (code) => { + tsnode.on('close', async (code) => { console.log(`Nodemon process exited with code ${code}`); - mongod.stop(); // 停止 MongoDB 内存服务器 + await mongod.stop(); // 停止 MongoDB 内存服务器 + await redisServer.stop(); }); } diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 68675073..bbc87911 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -40,8 +40,9 @@ import { LoggerProvider } from './logger/logger.provider'; import { PluginManagerProvider } from './securityPlugin/pluginManager.provider'; import { LogRequestMiddleware } from './middlewares/logRequest.middleware'; import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager'; -import { Logger } from './logger'; -import { SurveyDownload } from './models/surveyDownload.entity'; +import { XiaojuSurveyLogger } from './logger'; +import { DownloadTask } from './models/downloadTask.entity'; +import { Session } from './models/session.entity'; @Module({ imports: [ @@ -82,7 +83,8 @@ import { SurveyDownload } from './models/surveyDownload.entity'; Workspace, WorkspaceMember, Collaborator, - SurveyDownload, + DownloadTask, + Session, ], }; }, @@ -130,7 +132,7 @@ export class AppModule { ), new SurveyUtilPlugin(), ); - Logger.init({ + XiaojuSurveyLogger.init({ filename: this.configService.get('XIAOJU_SURVEY_LOGGER_FILENAME'), }); } diff --git a/server/src/enums/exceptionCode.ts b/server/src/enums/exceptionCode.ts index 85cffaa7..192abb9d 100644 --- a/server/src/enums/exceptionCode.ts +++ b/server/src/enums/exceptionCode.ts @@ -11,6 +11,7 @@ export enum EXCEPTION_CODE { SURVEY_TYPE_ERROR = 3003, // 问卷类型错误 SURVEY_NOT_FOUND = 3004, // 问卷不存在 SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容 + SURVEY_SAVE_CONFLICT = 3006, // 问卷冲突 CAPTCHA_INCORRECT = 4001, // 验证码不正确 WHITELIST_ERROR = 4002, // 白名单校验错误 diff --git a/server/src/enums/index.ts b/server/src/enums/index.ts index 6a7eb74a..897a4b90 100644 --- a/server/src/enums/index.ts +++ b/server/src/enums/index.ts @@ -7,6 +7,8 @@ export enum RECORD_STATUS { REMOVED = 'removed', // 删除 FORCE_REMOVED = 'forceRemoved', // 从回收站删除 COMOPUTETING = 'computing', // 计算中 + FINISHED = 'finished', // 已完成 + ERROR = 'error', // 错误 } // 历史类型 diff --git a/server/src/guards/session.guard.ts b/server/src/guards/session.guard.ts new file mode 100644 index 00000000..041f97d8 --- /dev/null +++ b/server/src/guards/session.guard.ts @@ -0,0 +1,94 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { get } from 'lodash'; +import { NoPermissionException } from 'src/exceptions/noPermissionException'; +import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; +import { SessionService } from 'src/modules/survey/services/session.service'; +import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; +import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; +import { CollaboratorService } from 'src/modules/survey/services/collaborator.service'; + +@Injectable() +export class SessionGuard implements CanActivate { + constructor( + private reflector: Reflector, + private readonly sessionService: SessionService, + private readonly surveyMetaService: SurveyMetaService, + private readonly workspaceMemberService: WorkspaceMemberService, + private readonly collaboratorService: CollaboratorService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const user = request.user; + const sessionIdKey = this.reflector.get( + 'sessionId', + context.getHandler(), + ); + + const sessionId = get(request, sessionIdKey); + + if (!sessionId) { + throw new NoPermissionException('没有权限'); + } + + const saveSession = await this.sessionService.findOne(sessionId); + + request.saveSession = saveSession; + + const surveyId = saveSession.surveyId; + + const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId }); + + if (!surveyMeta) { + throw new SurveyNotFoundException('问卷不存在'); + } + + request.surveyMeta = surveyMeta; + + // 兼容老的问卷没有ownerId + if ( + surveyMeta.ownerId === user._id.toString() || + surveyMeta.owner === user.username + ) { + // 问卷的owner,可以访问和操作问卷 + return true; + } + + if (surveyMeta.workspaceId) { + const memberInfo = await this.workspaceMemberService.findOne({ + workspaceId: surveyMeta.workspaceId, + userId: user._id.toString(), + }); + if (!memberInfo) { + throw new NoPermissionException('没有权限'); + } + return true; + } + + const permissions = this.reflector.get( + 'surveyPermission', + context.getHandler(), + ); + + if (!Array.isArray(permissions) || permissions.length === 0) { + throw new NoPermissionException('没有权限'); + } + + const info = await this.collaboratorService.getCollaborator({ + surveyId, + userId: user._id.toString(), + }); + + if (!info) { + throw new NoPermissionException('没有权限'); + } + request.collaborator = info; + if ( + permissions.some((permission) => info.permissions.includes(permission)) + ) { + return true; + } + throw new NoPermissionException('没有权限'); + } +} diff --git a/server/src/guards/survey.guard.ts b/server/src/guards/survey.guard.ts index de904edb..ab49526b 100644 --- a/server/src/guards/survey.guard.ts +++ b/server/src/guards/survey.guard.ts @@ -3,7 +3,6 @@ import { Reflector } from '@nestjs/core'; import { get } from 'lodash'; import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; - import { CollaboratorService } from 'src/modules/survey/services/collaborator.service'; import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; diff --git a/server/src/interfaces/survey.ts b/server/src/interfaces/survey.ts index d683dc7b..4ba7d49c 100644 --- a/server/src/interfaces/survey.ts +++ b/server/src/interfaces/survey.ts @@ -60,7 +60,6 @@ export interface DataItem { rangeConfig?: any; starStyle?: string; innerType?: string; - deleteRecover?: boolean; quotaNoDisplay?: boolean; } diff --git a/server/src/logger/index.ts b/server/src/logger/index.ts index f1892b71..a933fe93 100644 --- a/server/src/logger/index.ts +++ b/server/src/logger/index.ts @@ -1,15 +1,15 @@ import * as log4js from 'log4js'; import moment from 'moment'; -import { Request } from 'express'; +import { Injectable, Scope } from '@nestjs/common'; const log4jsLogger = log4js.getLogger(); -export class Logger { +@Injectable({ scope: Scope.REQUEST }) +export class XiaojuSurveyLogger { private static inited = false; - - constructor() {} + private traceId: string; static init(config: { filename: string }) { - if (this.inited) { + if (XiaojuSurveyLogger.inited) { return; } log4js.configure({ @@ -30,25 +30,28 @@ export class Logger { default: { appenders: ['app'], level: 'trace' }, }, }); + XiaojuSurveyLogger.inited = true; } - _log(message, options: { dltag?: string; level: string; req?: Request }) { + _log(message, options: { dltag?: string; level: string }) { const datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS'); const level = options?.level; const dltag = options?.dltag ? `${options.dltag}||` : ''; - const traceIdStr = options?.req?.['traceId'] - ? `traceid=${options?.req?.['traceId']}||` - : ''; + const traceIdStr = this.traceId ? `traceid=${this.traceId}||` : ''; return log4jsLogger[level]( `[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`, ); } - info(message, options?: { dltag?: string; req?: Request }) { + setTraceId(traceId: string) { + this.traceId = traceId; + } + + info(message, options?: { dltag?: string }) { return this._log(message, { ...options, level: 'info' }); } - error(message, options: { dltag?: string; req?: Request }) { + error(message, options?: { dltag?: string }) { return this._log(message, { ...options, level: 'error' }); } } diff --git a/server/src/logger/logger.provider.ts b/server/src/logger/logger.provider.ts index 2a298dd4..cf7c7bbe 100644 --- a/server/src/logger/logger.provider.ts +++ b/server/src/logger/logger.provider.ts @@ -1,8 +1,8 @@ import { Provider } from '@nestjs/common'; -import { Logger } from './index'; +import { XiaojuSurveyLogger } from './index'; export const LoggerProvider: Provider = { - provide: Logger, - useClass: Logger, + provide: XiaojuSurveyLogger, + useClass: XiaojuSurveyLogger, }; diff --git a/server/src/logger/util.ts b/server/src/logger/util.ts index ada62779..4ffc3c1b 100644 --- a/server/src/logger/util.ts +++ b/server/src/logger/util.ts @@ -10,9 +10,9 @@ const getCountStr = () => { export const genTraceId = ({ ip }) => { // ip转16位 + 当前时间戳(毫秒级)+自增序列(1000开始自增到9000)+ 当前进程id的后5位 - ip = ip.replace('::ffff:', ''); + ip = ip.replace('::ffff:', '').replace('::1', ''); let ipArr; - if (ip.indexOf(':') > 0) { + if (ip.indexOf(':') >= 0) { ipArr = ip.split(':').map((segment) => { // 将IPv6每个段转为16位,并补0到长度为4 return parseInt(segment, 16).toString(16).padStart(4, '0'); diff --git a/server/src/middlewares/logRequest.middleware.ts b/server/src/middlewares/logRequest.middleware.ts index ade62496..d906b479 100644 --- a/server/src/middlewares/logRequest.middleware.ts +++ b/server/src/middlewares/logRequest.middleware.ts @@ -1,26 +1,25 @@ // logger.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; -import { Logger } from '../logger/index'; // 替换为你实际的logger路径 +import { XiaojuSurveyLogger } from '../logger/index'; // 替换为你实际的logger路径 import { genTraceId } from '../logger/util'; @Injectable() export class LogRequestMiddleware implements NestMiddleware { - constructor(private readonly logger: Logger) {} + constructor(private readonly logger: XiaojuSurveyLogger) {} use(req: Request, res: Response, next: NextFunction) { const { method, originalUrl, ip } = req; const userAgent = req.get('user-agent') || ''; const startTime = Date.now(); const traceId = genTraceId({ ip }); - req['traceId'] = traceId; + this.logger.setTraceId(traceId); const query = JSON.stringify(req.query); const body = JSON.stringify(req.body); this.logger.info( `method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`, { dltag: 'request_in', - req, }, ); @@ -30,7 +29,6 @@ export class LogRequestMiddleware implements NestMiddleware { `status=${res.statusCode.toString()}||duration=${duration}ms`, { dltag: 'request_out', - req, }, ); }); diff --git a/server/src/models/captcha.entity.ts b/server/src/models/captcha.entity.ts index 55e1c45d..6bebba66 100644 --- a/server/src/models/captcha.entity.ts +++ b/server/src/models/captcha.entity.ts @@ -5,8 +5,7 @@ import { BaseEntity } from './base.entity'; @Entity({ name: 'captcha' }) export class Captcha extends BaseEntity { @Index({ - expireAfterSeconds: - new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000, + expireAfterSeconds: 3600, }) @ObjectIdColumn() _id: ObjectId; diff --git a/server/src/models/clientEncrypt.entity.ts b/server/src/models/clientEncrypt.entity.ts index eaaebdbc..5f952afe 100644 --- a/server/src/models/clientEncrypt.entity.ts +++ b/server/src/models/clientEncrypt.entity.ts @@ -6,8 +6,7 @@ import { BaseEntity } from './base.entity'; @Entity({ name: 'clientEncrypt' }) export class ClientEncrypt extends BaseEntity { @Index({ - expireAfterSeconds: - new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000, + expireAfterSeconds: 3600, }) @ObjectIdColumn() _id: ObjectId; diff --git a/server/src/models/downloadTask.entity.ts b/server/src/models/downloadTask.entity.ts new file mode 100644 index 00000000..90bb7f0b --- /dev/null +++ b/server/src/models/downloadTask.entity.ts @@ -0,0 +1,34 @@ +import { Entity, Column } from 'typeorm'; +import { BaseEntity } from './base.entity'; + +@Entity({ name: 'downloadTask' }) +export class DownloadTask extends BaseEntity { + @Column() + surveyId: string; + + @Column() + surveyPath: string; + + // 文件路径 + @Column() + url: string; + + // 文件key + @Column() + fileKey: string; + + // 任务创建人 + @Column() + ownerId: string; + + // 文件名 + @Column() + filename: string; + + // 文件大小 + @Column() + fileSize: string; + + @Column() + params: string; +} diff --git a/server/src/models/session.entity.ts b/server/src/models/session.entity.ts new file mode 100644 index 00000000..c7e3f485 --- /dev/null +++ b/server/src/models/session.entity.ts @@ -0,0 +1,15 @@ +import { Entity, Column, Index, ObjectIdColumn } from 'typeorm'; +import { ObjectId } from 'mongodb'; +import { BaseEntity } from './base.entity'; + +@Entity({ name: 'session' }) +export class Session extends BaseEntity { + @Index({ + expireAfterSeconds: 3600, + }) + @ObjectIdColumn() + _id: ObjectId; + + @Column() + surveyId: string; +} diff --git a/server/src/models/surveyDownload.entity.ts b/server/src/models/surveyDownload.entity.ts deleted file mode 100644 index 96e09b87..00000000 --- a/server/src/models/surveyDownload.entity.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Entity, Column, BeforeInsert, AfterLoad } from 'typeorm'; -import pluginManager from '../securityPlugin/pluginManager'; -import { BaseEntity } from './base.entity'; - -@Entity({ name: 'surveyDownload' }) -export class SurveyDownload extends BaseEntity { - @Column() - pageId: string; - - @Column() - surveyPath: string; - - @Column() - title: string; - - @Column() - filePath: string; - - @Column() - onwer: string; - - @Column() - filename: string; - - @Column() - fileSize: string; - - @Column() - fileType: string; - - // @Column() - // ownerId: string; - - @Column() - downloadTime: string; - - @BeforeInsert() - async onDataInsert() { - return await pluginManager.triggerHook('beforeResponseDataCreate', this); - } - - @AfterLoad() - async onDataLoaded() { - return await pluginManager.triggerHook('afterResponseDataReaded', this); - } -} diff --git a/server/src/models/surveyHistory.entity.ts b/server/src/models/surveyHistory.entity.ts index f0f2ba5d..4d573c4b 100644 --- a/server/src/models/surveyHistory.entity.ts +++ b/server/src/models/surveyHistory.entity.ts @@ -18,6 +18,8 @@ export class SurveyHistory extends BaseEntity { operator: { username: string; _id: string; - sessionId: string; }; + + @Column('string') + sessionId: string; } diff --git a/server/src/modules/auth/controllers/auth.controller.ts b/server/src/modules/auth/controllers/auth.controller.ts index b7647440..9d5d6d08 100644 --- a/server/src/modules/auth/controllers/auth.controller.ts +++ b/server/src/modules/auth/controllers/auth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Get, Body, HttpCode, Req, UnauthorizedException } from '@nestjs/common'; +import { Controller, Post, Body, HttpCode } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { UserService } from '../services/user.service'; import { CaptchaService } from '../services/captcha.service'; @@ -7,7 +7,6 @@ import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { create } from 'svg-captcha'; import { ApiTags } from '@nestjs/swagger'; -import { Request } from 'express'; @ApiTags('auth') @Controller('/api/auth') export class AuthController { @@ -163,25 +162,4 @@ export class AuthController { }, }; } - - @Get('/statuscheck') - @HttpCode(200) - async checkStatus(@Req() request: Request) { - const token = request.headers.authorization?.split(' ')[1]; - if (!token) { - throw new UnauthorizedException('请登录'); - } - try { - const expired = await this.authService.expiredCheck(token); - return { - code: 200, - data: { - expired: expired - }, - }; - } catch (error) { - throw new UnauthorizedException(error?.message || '用户凭证检测失败'); - } - } - } diff --git a/server/src/modules/auth/controllers/user.controller.ts b/server/src/modules/auth/controllers/user.controller.ts index c7e74359..3757a209 100644 --- a/server/src/modules/auth/controllers/user.controller.ts +++ b/server/src/modules/auth/controllers/user.controller.ts @@ -1,4 +1,11 @@ -import { Controller, Get, Query, HttpCode, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Query, + HttpCode, + UseGuards, + Request, +} from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { Authentication } from 'src/guards/authentication.guard'; @@ -43,4 +50,16 @@ export class UserController { }), }; } + + @UseGuards(Authentication) + @Get('/getUserInfo') + async getUserInfo(@Request() req) { + return { + code: 200, + data: { + userId: req.user._id.toString(), + username: req.user.username, + }, + }; + } } diff --git a/server/src/modules/auth/services/auth.service.ts b/server/src/modules/auth/services/auth.service.ts index 1dd257dc..1573ec56 100644 --- a/server/src/modules/auth/services/auth.service.ts +++ b/server/src/modules/auth/services/auth.service.ts @@ -37,12 +37,8 @@ export class AuthService { } async expiredCheck(token: string) { - let decoded; try { - decoded = verify( - token, - this.configService.get('XIAOJU_SURVEY_JWT_SECRET'), - ); + verify(token, this.configService.get('XIAOJU_SURVEY_JWT_SECRET')); } catch (err) { return true; } diff --git a/server/src/modules/file/services/file.service.ts b/server/src/modules/file/services/file.service.ts index fb89ae1b..6cace9fa 100644 --- a/server/src/modules/file/services/file.service.ts +++ b/server/src/modules/file/services/file.service.ts @@ -14,13 +14,18 @@ export class FileService { configKey, file, pathPrefix, + keepOriginFilename, }: { configKey: string; file: Express.Multer.File; pathPrefix: string; + keepOriginFilename?: boolean; }) { const handler = this.getHandler(configKey); - const { key } = await handler.upload(file, { pathPrefix }); + const { key } = await handler.upload(file, { + pathPrefix, + keepOriginFilename, + }); const url = await handler.getUrl(key); return { key, diff --git a/server/src/modules/file/services/uploadHandlers/local.handler.ts b/server/src/modules/file/services/uploadHandlers/local.handler.ts index 122e7dcb..83a95c5b 100644 --- a/server/src/modules/file/services/uploadHandlers/local.handler.ts +++ b/server/src/modules/file/services/uploadHandlers/local.handler.ts @@ -12,9 +12,14 @@ export class LocalHandler implements FileUploadHandler { async upload( file: Express.Multer.File, - options?: { pathPrefix?: string }, + options?: { pathPrefix?: string; keepOriginFilename?: boolean }, ): Promise<{ key: string }> { - const filename = await generateUniqueFilename(file.originalname); + let filename; + if (options?.keepOriginFilename) { + filename = file.originalname; + } else { + filename = await generateUniqueFilename(file.originalname); + } const filePath = join( options?.pathPrefix ? options?.pathPrefix : '', filename, @@ -35,6 +40,10 @@ export class LocalHandler implements FileUploadHandler { } getUrl(key: string): string { + if (process.env.SERVER_ENV === 'local') { + const port = process.env.PORT || 3000; + return `http://localhost:${port}/${key}`; + } return `/${key}`; } } diff --git a/server/src/modules/mutex/mutex.module.ts b/server/src/modules/mutex/mutex.module.ts deleted file mode 100644 index 4a6b13e5..00000000 --- a/server/src/modules/mutex/mutex.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Global, Module } from '@nestjs/common'; -import { MutexService } from './services/mutexService.service'; - -@Global() -@Module({ - providers: [MutexService], - exports: [MutexService], -}) -export class MutexModule {} diff --git a/server/src/modules/mutex/services/mutexService.service.ts b/server/src/modules/mutex/services/mutexService.service.ts deleted file mode 100644 index 6702532e..00000000 --- a/server/src/modules/mutex/services/mutexService.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Mutex } from 'async-mutex'; -import { HttpException } from 'src/exceptions/httpException'; -import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; - -@Injectable() -export class MutexService { - private mutex = new Mutex(); - - async runLocked(callback: () => Promise): Promise { - // acquire lock - const release = await this.mutex.acquire(); - try { - return await callback(); - } catch (error) { - if (error instanceof HttpException) { - throw new HttpException( - error.message, - EXCEPTION_CODE.RESPONSE_OVER_LIMIT, - ); - } else { - throw error; - } - } finally { - release(); - } - } -} diff --git a/server/src/modules/redis/redis.module.ts b/server/src/modules/redis/redis.module.ts new file mode 100644 index 00000000..c3b28359 --- /dev/null +++ b/server/src/modules/redis/redis.module.ts @@ -0,0 +1,9 @@ +// src/redis/redis.module.ts +import { Module } from '@nestjs/common'; +import { RedisService } from './redis.service'; + +@Module({ + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/server/src/modules/redis/redis.service.ts b/server/src/modules/redis/redis.service.ts new file mode 100644 index 00000000..84f0227c --- /dev/null +++ b/server/src/modules/redis/redis.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { Redis } from 'ioredis'; +import Redlock, { Lock } from 'redlock'; + +@Injectable() +export class RedisService { + private readonly redisClient: Redis; + private readonly redlock: Redlock; + + constructor() { + this.redisClient = new Redis({ + host: process.env.XIAOJU_SURVEY_REDIS_HOST, + port: parseInt(process.env.XIAOJU_SURVEY_REDIS_PORT), + password: process.env.XIAOJU_SURVEY_REDIS_PASSWORD || undefined, + username: process.env.XIAOJU_SURVEY_REDIS_USERNAME || undefined, + db: parseInt(process.env.XIAOJU_SURVEY_REDIS_DB) || 0, + }); + this.redlock = new Redlock([this.redisClient], { + retryCount: 10, + retryDelay: 200, // ms + retryJitter: 200, // ms + }); + } + + async lockResource(resource: string, ttl: number): Promise { + return this.redlock.acquire([resource], ttl); + } + + async unlockResource(lock: Lock): Promise { + await lock.release(); + } +} diff --git a/server/src/modules/survey/__test/collaborator.controller.spec.ts b/server/src/modules/survey/__test/collaborator.controller.spec.ts index 0119aaa3..2d9f458a 100644 --- a/server/src/modules/survey/__test/collaborator.controller.spec.ts +++ b/server/src/modules/survey/__test/collaborator.controller.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CollaboratorController } from '../controllers/collaborator.controller'; import { CollaboratorService } from '../services/collaborator.service'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { HttpException } from 'src/exceptions/httpException'; import { CreateCollaboratorDto } from '../dto/createCollaborator.dto'; import { Collaborator } from 'src/models/collaborator.entity'; @@ -25,7 +25,7 @@ jest.mock('src/guards/workspace.guard'); describe('CollaboratorController', () => { let controller: CollaboratorController; let collaboratorService: CollaboratorService; - let logger: Logger; + let logger: XiaojuSurveyLogger; let userService: UserService; let surveyMetaService: SurveyMetaService; let workspaceMemberServie: WorkspaceMemberService; @@ -50,7 +50,7 @@ describe('CollaboratorController', () => { }, }, { - provide: Logger, + provide: XiaojuSurveyLogger, useValue: { error: jest.fn(), info: jest.fn(), @@ -84,7 +84,7 @@ describe('CollaboratorController', () => { controller = module.get(CollaboratorController); collaboratorService = module.get(CollaboratorService); - logger = module.get(Logger); + logger = module.get(XiaojuSurveyLogger); userService = module.get(UserService); surveyMetaService = module.get(SurveyMetaService); workspaceMemberServie = module.get( @@ -191,7 +191,6 @@ describe('CollaboratorController', () => { describe('getSurveyCollaboratorList', () => { it('should return collaborator list', async () => { const query = { surveyId: 'surveyId' }; - const req = { user: { _id: 'userId' } }; const result = [ { _id: 'collaboratorId', userId: 'userId', username: '' }, ]; @@ -202,7 +201,7 @@ describe('CollaboratorController', () => { jest.spyOn(userService, 'getUserListByIds').mockResolvedValueOnce([]); - const response = await controller.getSurveyCollaboratorList(query, req); + const response = await controller.getSurveyCollaboratorList(query); expect(response).toEqual({ code: 200, @@ -214,11 +213,10 @@ describe('CollaboratorController', () => { const query: GetSurveyCollaboratorListDto = { surveyId: '', }; - const req = { user: { _id: 'userId' } }; - await expect( - controller.getSurveyCollaboratorList(query, req), - ).rejects.toThrow(HttpException); + await expect(controller.getSurveyCollaboratorList(query)).rejects.toThrow( + HttpException, + ); expect(logger.error).toHaveBeenCalledTimes(1); }); }); @@ -230,14 +228,13 @@ describe('CollaboratorController', () => { userId: 'userId', permissions: ['read'], }; - const req = { user: { _id: 'userId' } }; const result = { _id: 'userId', permissions: ['read'] }; jest .spyOn(collaboratorService, 'changeUserPermission') .mockResolvedValue(result); - const response = await controller.changeUserPermission(reqBody, req); + const response = await controller.changeUserPermission(reqBody); expect(response).toEqual({ code: 200, @@ -251,11 +248,10 @@ describe('CollaboratorController', () => { userId: '', permissions: ['surveyManage'], }; - const req = { user: { _id: 'userId' } }; - await expect( - controller.changeUserPermission(reqBody, req), - ).rejects.toThrow(HttpException); + await expect(controller.changeUserPermission(reqBody)).rejects.toThrow( + HttpException, + ); expect(logger.error).toHaveBeenCalledTimes(1); }); }); @@ -263,14 +259,13 @@ describe('CollaboratorController', () => { describe('deleteCollaborator', () => { it('should delete collaborator successfully', async () => { const query = { surveyId: 'surveyId', userId: 'userId' }; - const req = { user: { _id: 'userId' } }; const result = { acknowledged: true, deletedCount: 1 }; jest .spyOn(collaboratorService, 'deleteCollaborator') .mockResolvedValue(result); - const response = await controller.deleteCollaborator(query, req); + const response = await controller.deleteCollaborator(query); expect(response).toEqual({ code: 200, @@ -280,9 +275,8 @@ describe('CollaboratorController', () => { it('should throw an exception if validation fails', async () => { const query = { surveyId: '', userId: '' }; - const req = { user: { _id: 'userId' } }; - await expect(controller.deleteCollaborator(query, req)).rejects.toThrow( + await expect(controller.deleteCollaborator(query)).rejects.toThrow( HttpException, ); expect(logger.error).toHaveBeenCalledTimes(1); diff --git a/server/src/modules/survey/__test/collaborator.service.spec.ts b/server/src/modules/survey/__test/collaborator.service.spec.ts index 34a1157e..f276cf9a 100644 --- a/server/src/modules/survey/__test/collaborator.service.spec.ts +++ b/server/src/modules/survey/__test/collaborator.service.spec.ts @@ -3,13 +3,13 @@ import { CollaboratorService } from '../services/collaborator.service'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Collaborator } from 'src/models/collaborator.entity'; import { MongoRepository } from 'typeorm'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { InsertManyResult, ObjectId } from 'mongodb'; describe('CollaboratorService', () => { let service: CollaboratorService; let repository: MongoRepository; - let logger: Logger; + let logger: XiaojuSurveyLogger; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -20,7 +20,7 @@ describe('CollaboratorService', () => { useClass: MongoRepository, }, { - provide: Logger, + provide: XiaojuSurveyLogger, useValue: { info: jest.fn(), }, @@ -32,7 +32,7 @@ describe('CollaboratorService', () => { repository = module.get>( getRepositoryToken(Collaborator), ); - logger = module.get(Logger); + logger = module.get(XiaojuSurveyLogger); }); describe('create', () => { diff --git a/server/src/modules/survey/__test/dataStatistic.controller.spec.ts b/server/src/modules/survey/__test/dataStatistic.controller.spec.ts index 5b9b5d82..9d40ced0 100644 --- a/server/src/modules/survey/__test/dataStatistic.controller.spec.ts +++ b/server/src/modules/survey/__test/dataStatistic.controller.spec.ts @@ -9,7 +9,7 @@ import { ResponseSchemaService } from '../../surveyResponse/services/responseSch import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider'; import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { UserService } from 'src/modules/auth/services/user.service'; import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin'; @@ -28,7 +28,7 @@ describe('DataStatisticController', () => { let dataStatisticService: DataStatisticService; let responseSchemaService: ResponseSchemaService; let pluginManager: XiaojuSurveyPluginManager; - let logger: Logger; + let logger: XiaojuSurveyLogger; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -56,7 +56,7 @@ describe('DataStatisticController', () => { })), }, { - provide: Logger, + provide: XiaojuSurveyLogger, useValue: { error: jest.fn(), }, @@ -73,7 +73,7 @@ describe('DataStatisticController', () => { pluginManager = module.get( XiaojuSurveyPluginManager, ); - logger = module.get(Logger); + logger = module.get(XiaojuSurveyLogger); pluginManager.registerPlugin( new ResponseSecurityPlugin('dataAesEncryptSecretKey'), @@ -123,7 +123,7 @@ describe('DataStatisticController', () => { .spyOn(dataStatisticService, 'getDataTable') .mockResolvedValueOnce(mockDataTable); - const result = await controller.data(mockRequest.query, mockRequest); + const result = await controller.data(mockRequest.query); expect(result).toEqual({ code: 200, @@ -169,7 +169,7 @@ describe('DataStatisticController', () => { .spyOn(dataStatisticService, 'getDataTable') .mockResolvedValueOnce(mockDataTable); - const result = await controller.data(mockRequest.query, mockRequest); + const result = await controller.data(mockRequest.query); expect(result).toEqual({ code: 200, @@ -187,9 +187,9 @@ describe('DataStatisticController', () => { }, }; - await expect( - controller.data(mockRequest.query, mockRequest), - ).rejects.toThrow(HttpException); + await expect(controller.data(mockRequest.query)).rejects.toThrow( + HttpException, + ); expect(logger.error).toHaveBeenCalledTimes(1); }); }); diff --git a/server/src/modules/survey/__test/surveyHistory.controller.spec.ts b/server/src/modules/survey/__test/surveyHistory.controller.spec.ts index cffd5a76..54a975fd 100644 --- a/server/src/modules/survey/__test/surveyHistory.controller.spec.ts +++ b/server/src/modules/survey/__test/surveyHistory.controller.spec.ts @@ -7,7 +7,7 @@ import { SurveyMetaService } from '../services/surveyMeta.service'; import { UserService } from 'src/modules/auth/services/user.service'; import { AuthService } from 'src/modules/auth/services/auth.service'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; jest.mock('src/guards/authentication.guard'); jest.mock('src/guards/survey.guard'); @@ -49,7 +49,7 @@ describe('SurveyHistoryController', () => { useClass: jest.fn().mockImplementation(() => ({})), }, { - provide: Logger, + provide: XiaojuSurveyLogger, useValue: { info: jest.fn(), error: jest.fn(), @@ -66,7 +66,7 @@ describe('SurveyHistoryController', () => { it('should return history list when query is valid', async () => { const queryInfo = { surveyId: 'survey123', historyType: 'published' }; - await controller.getList(queryInfo, {}); + await controller.getList(queryInfo); expect(surveyHistoryService.getHistoryList).toHaveBeenCalledWith({ surveyId: queryInfo.surveyId, diff --git a/server/src/modules/survey/__test/surveyHistory.service.spec.ts b/server/src/modules/survey/__test/surveyHistory.service.spec.ts index f226cfc0..07532feb 100644 --- a/server/src/modules/survey/__test/surveyHistory.service.spec.ts +++ b/server/src/modules/survey/__test/surveyHistory.service.spec.ts @@ -78,7 +78,13 @@ describe('SurveyHistoryService', () => { .spyOn(repository, 'save') .mockResolvedValueOnce({} as SurveyHistory); - await service.addHistory({ surveyId, schema, type, user }); + await service.addHistory({ + surveyId, + schema, + type, + user, + sessionId: '', + }); expect(spyCreate).toHaveBeenCalledWith({ pageId: surveyId, diff --git a/server/src/modules/survey/controllers/collaborator.controller.ts b/server/src/modules/survey/controllers/collaborator.controller.ts index 08c97068..0545aeb1 100644 --- a/server/src/modules/survey/controllers/collaborator.controller.ts +++ b/server/src/modules/survey/controllers/collaborator.controller.ts @@ -20,7 +20,7 @@ import { SURVEY_PERMISSION, SURVEY_PERMISSION_DESCRIPTION, } from 'src/enums/surveyPermission'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; import { CollaboratorService } from '../services/collaborator.service'; @@ -40,7 +40,7 @@ import { SurveyMetaService } from '../services/surveyMeta.service'; export class CollaboratorController { constructor( private readonly collaboratorService: CollaboratorService, - private readonly logger: Logger, + private readonly logger: XiaojuSurveyLogger, private readonly userService: UserService, private readonly surveyMetaService: SurveyMetaService, private readonly workspaceMemberServie: WorkspaceMemberService, @@ -69,7 +69,7 @@ export class CollaboratorController { ) { const { error, value } = CreateCollaboratorDto.validate(reqBody); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException( '系统错误,请联系管理员', EXCEPTION_CODE.PARAMETER_ERROR, @@ -124,7 +124,7 @@ export class CollaboratorController { ) { const { error, value } = BatchSaveCollaboratorDto.validate(reqBody); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException( '系统错误,请联系管理员', EXCEPTION_CODE.PARAMETER_ERROR, @@ -184,7 +184,7 @@ export class CollaboratorController { neIdList: collaboratorIdList, userIdList: newCollaboratorUserIdList, }); - this.logger.info('batchDelete:' + JSON.stringify(delRes), { req }); + this.logger.info('batchDelete:' + JSON.stringify(delRes)); if (Array.isArray(newCollaborator) && newCollaborator.length > 0) { const insertRes = await this.collaboratorService.batchCreate({ surveyId: value.surveyId, @@ -208,7 +208,7 @@ export class CollaboratorController { const delRes = await this.collaboratorService.batchDeleteBySurveyId( value.surveyId, ); - this.logger.info(JSON.stringify(delRes), { req }); + this.logger.info(JSON.stringify(delRes)); } return { @@ -225,11 +225,10 @@ export class CollaboratorController { ]) async getSurveyCollaboratorList( @Query() query: GetSurveyCollaboratorListDto, - @Request() req, ) { const { error, value } = GetSurveyCollaboratorListDto.validate(query); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } @@ -263,17 +262,14 @@ export class CollaboratorController { @SetMetadata('surveyPermission', [ SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE, ]) - async changeUserPermission( - @Body() reqBody: ChangeUserPermissionDto, - @Request() req, - ) { + async changeUserPermission(@Body() reqBody: ChangeUserPermissionDto) { const { error, value } = Joi.object({ surveyId: Joi.string(), userId: Joi.string(), permissions: Joi.array().items(Joi.string().required()), }).validate(reqBody); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } @@ -292,13 +288,13 @@ export class CollaboratorController { @SetMetadata('surveyPermission', [ SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE, ]) - async deleteCollaborator(@Query() query, @Request() req) { + async deleteCollaborator(@Query() query) { const { error, value } = Joi.object({ surveyId: Joi.string(), userId: Joi.string(), }).validate(query); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } @@ -319,7 +315,7 @@ export class CollaboratorController { const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId }); if (!surveyMeta) { - this.logger.error(`问卷不存在: ${surveyId}`, { req }); + this.logger.error(`问卷不存在: ${surveyId}`); throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND); } diff --git a/server/src/modules/survey/controllers/dataStatistic.controller.ts b/server/src/modules/survey/controllers/dataStatistic.controller.ts index 752bc68a..9a396b6c 100644 --- a/server/src/modules/survey/controllers/dataStatistic.controller.ts +++ b/server/src/modules/survey/controllers/dataStatistic.controller.ts @@ -5,7 +5,6 @@ import { HttpCode, UseGuards, SetMetadata, - Request, } from '@nestjs/common'; import * as Joi from 'joi'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; @@ -17,13 +16,12 @@ import { Authentication } from 'src/guards/authentication.guard'; import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; import { SurveyGuard } from 'src/guards/survey.guard'; import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { AggregationStatisDto } from '../dto/aggregationStatis.dto'; import { handleAggretionData } from '../utils'; import { QUESTION_TYPE } from 'src/enums/question'; -import { SurveyDownloadService } from '../services/surveyDownload.service'; @ApiTags('survey') @ApiBearerAuth() @@ -33,9 +31,7 @@ export class DataStatisticController { private readonly responseSchemaService: ResponseSchemaService, private readonly dataStatisticService: DataStatisticService, private readonly pluginManager: XiaojuSurveyPluginManager, - private readonly logger: Logger, - // - private readonly surveyDownloadService: SurveyDownloadService, + private readonly logger: XiaojuSurveyLogger, ) {} @Get('/dataTable') @@ -47,7 +43,6 @@ export class DataStatisticController { async data( @Query() queryInfo, - @Request() req, ) { const { value, error } = await Joi.object({ surveyId: Joi.string().required(), @@ -56,7 +51,7 @@ export class DataStatisticController { pageSize: Joi.number().default(10), }).validate(queryInfo); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } const { surveyId, isDesensitive, page, pageSize } = value; diff --git a/server/src/modules/survey/controllers/downloadTask.controller.ts b/server/src/modules/survey/controllers/downloadTask.controller.ts new file mode 100644 index 00000000..f9001112 --- /dev/null +++ b/server/src/modules/survey/controllers/downloadTask.controller.ts @@ -0,0 +1,187 @@ +import { + Controller, + Get, + Query, + HttpCode, + UseGuards, + SetMetadata, + Request, + Post, + Body, + // Response, +} from '@nestjs/common'; +import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; + +import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; + +import { Authentication } from 'src/guards/authentication.guard'; +import { SurveyGuard } from 'src/guards/survey.guard'; +import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; +import { XiaojuSurveyLogger } from 'src/logger'; +import { HttpException } from 'src/exceptions/httpException'; +import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; +//后添加 +import { DownloadTaskService } from '../services/downloadTask.service'; +import { + GetDownloadTaskDto, + CreateDownloadDto, + GetDownloadTaskListDto, + DeleteDownloadTaskDto, +} from '../dto/downloadTask.dto'; +import moment from 'moment'; +import { NoPermissionException } from 'src/exceptions/noPermissionException'; + +@ApiTags('downloadTask') +@ApiBearerAuth() +@Controller('/api/downloadTask') +export class DownloadTaskController { + constructor( + private readonly responseSchemaService: ResponseSchemaService, + private readonly downloadTaskService: DownloadTaskService, + private readonly logger: XiaojuSurveyLogger, + ) {} + + @Post('/createTask') + @HttpCode(200) + @UseGuards(SurveyGuard) + @SetMetadata('surveyId', 'body.surveyId') + @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE]) + @UseGuards(Authentication) + async createTask( + @Body() + reqBody: CreateDownloadDto, + @Request() req, + ) { + const { value, error } = CreateDownloadDto.validate(reqBody); + if (error) { + this.logger.error(error.message); + throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); + } + const { surveyId, isDesensitive } = value; + const responseSchema = + await this.responseSchemaService.getResponseSchemaByPageId(surveyId); + const id = await this.downloadTaskService.createDownloadTask({ + surveyId, + responseSchema, + operatorId: req.user._id.toString(), + params: { isDesensitive }, + }); + this.downloadTaskService.processDownloadTask({ taskId: id }); + return { + code: 200, + data: { taskId: id }, + }; + } + + @Get('/getDownloadTaskList') + @HttpCode(200) + @UseGuards(Authentication) + async downloadList( + @Query() + queryInfo: GetDownloadTaskListDto, + ) { + 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 { total, list } = await this.downloadTaskService.getDownloadTaskList({ + ownerId, + pageIndex, + pageSize, + }); + return { + code: 200, + data: { + total: total, + list: list.map((data) => { + const item: Record = {}; + item.taskId = data._id.toString(); + item.curStatus = data.curStatus; + item.filename = data.filename; + item.url = data.url; + const fmt = 'YYYY-MM-DD HH:mm:ss'; + const units = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + let unitIndex = 0; + let size = Number(data.fileSize); + if (isNaN(size)) { + item.fileSize = data.fileSize; + } else { + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + item.fileSize = `${size.toFixed()} ${units[unitIndex]}`; + } + item.createDate = moment(Number(data.createDate)).format(fmt); + return item; + }), + }, + }; + } + + @Get('/getDownloadTask') + @HttpCode(200) + @UseGuards(Authentication) + async getDownloadTask(@Query() query: GetDownloadTaskDto, @Request() req) { + const { value, error } = GetDownloadTaskDto.validate(query); + if (error) { + throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); + } + + const taskInfo = await this.downloadTaskService.getDownloadTaskById({ + taskId: value.taskId, + }); + + if (!taskInfo) { + throw new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR); + } + + if (taskInfo.ownerId !== req.user._id.toString()) { + throw new NoPermissionException('没有权限'); + } + const res: Record = { + ...taskInfo, + }; + res.taskId = taskInfo._id.toString(); + delete res._id; + + return { + code: 200, + data: res, + }; + } + + @Post('/deleteDownloadTask') + @HttpCode(200) + @UseGuards(Authentication) + async deleteFileByName(@Body() body: DeleteDownloadTaskDto, @Request() req) { + const { value, error } = DeleteDownloadTaskDto.validate(body); + if (error) { + throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); + } + const { taskId } = value; + + const taskInfo = await this.downloadTaskService.getDownloadTaskById({ + taskId, + }); + + if (!taskInfo) { + throw new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR); + } + + if (taskInfo.ownerId !== req.user._id.toString()) { + throw new NoPermissionException('没有权限'); + } + + const delRes = await this.downloadTaskService.deleteDownloadTask({ + taskId, + }); + + return { + code: 200, + data: delRes.modifiedCount === 1, + }; + } +} diff --git a/server/src/modules/survey/controllers/session.controller.ts b/server/src/modules/survey/controllers/session.controller.ts new file mode 100644 index 00000000..4b4455e4 --- /dev/null +++ b/server/src/modules/survey/controllers/session.controller.ts @@ -0,0 +1,84 @@ +import { + Controller, + Post, + Body, + HttpCode, + UseGuards, + SetMetadata, + Request, +} from '@nestjs/common'; +import * as Joi from 'joi'; +import { ApiTags } from '@nestjs/swagger'; + +import { SessionService } from '../services/session.service'; + +import { Authentication } from 'src/guards/authentication.guard'; +import { SurveyGuard } from 'src/guards/survey.guard'; +import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; +import { XiaojuSurveyLogger } from 'src/logger'; +import { HttpException } from 'src/exceptions/httpException'; +import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; +import { SessionGuard } from 'src/guards/session.guard'; + +@ApiTags('survey') +@Controller('/api/session') +export class SessionController { + constructor( + private readonly sessionService: SessionService, + private readonly logger: XiaojuSurveyLogger, + ) {} + + @Post('/create') + @HttpCode(200) + @UseGuards(SurveyGuard) + @SetMetadata('surveyId', 'body.surveyId') + @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE]) + @UseGuards(Authentication) + async create( + @Body() + reqBody: { + surveyId: string; + }, + ) { + const { value, error } = Joi.object({ + surveyId: Joi.string().required(), + }).validate(reqBody); + + if (error) { + this.logger.error(error.message); + throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); + } + + const surveyId = value.surveyId; + const session = await this.sessionService.create({ surveyId }); + + return { + code: 200, + data: { + sessionId: session._id.toString(), + }, + }; + } + + @Post('/seize') + @HttpCode(200) + @UseGuards(SessionGuard) + @SetMetadata('sessionId', 'body.sessionId') + @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE]) + @UseGuards(Authentication) + async seize( + @Request() + req, + ) { + const saveSession = req.saveSession; + + await this.sessionService.updateSessionToEditing({ + sessionId: saveSession._id.toString(), + surveyId: saveSession.surveyId, + }); + + return { + code: 200, + }; + } +} diff --git a/server/src/modules/survey/controllers/survey.controller.ts b/server/src/modules/survey/controllers/survey.controller.ts index 911a3ce2..e97b21ca 100644 --- a/server/src/modules/survey/controllers/survey.controller.ts +++ b/server/src/modules/survey/controllers/survey.controller.ts @@ -26,12 +26,13 @@ import { Authentication } from 'src/guards/authentication.guard'; import { HISTORY_TYPE } from 'src/enums'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { SurveyGuard } from 'src/guards/survey.guard'; import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; 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'; @ApiTags('survey') @@ -43,8 +44,9 @@ export class SurveyController { private readonly responseSchemaService: ResponseSchemaService, private readonly contentSecurityService: ContentSecurityService, private readonly surveyHistoryService: SurveyHistoryService, - private readonly logger: Logger, + private readonly logger: XiaojuSurveyLogger, private readonly counterService: CounterService, + private readonly sessionService: SessionService, ) {} @Get('/getBannerData') @@ -73,9 +75,7 @@ export class SurveyController { ) { const { error, value } = CreateSurveyDto.validate(reqBody); if (error) { - this.logger.error(`createSurvey_parameter error: ${error.message}`, { - req, - }); + this.logger.error(`createSurvey_parameter error: ${error.message}`); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); } @@ -134,12 +134,29 @@ export class SurveyController { sessionId: Joi.string().required(), }).validate(surveyInfo); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } - const username = req.user.username; - const surveyId = value.surveyId; const sessionId = value.sessionId; + const surveyId = value.surveyId; + const latestEditingOne = await this.sessionService.findLatestEditingOne({ + surveyId, + }); + + 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, + ); + } + } + await this.sessionService.updateSessionToEditing({ sessionId, surveyId }); + + const username = req.user.username; + const configData = value.configData; await this.surveyConfService.saveSurveyConf({ surveyId, @@ -153,7 +170,6 @@ export class SurveyController { _id: req.user._id.toString(), username, }, - sessionId: sessionId, }); return { code: 200, @@ -202,7 +218,7 @@ export class SurveyController { }).validate(queryInfo); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } @@ -245,15 +261,13 @@ export class SurveyController { queryInfo: { surveyPath: string; }, - @Request() - req, ) { const { value, error } = Joi.object({ surveyId: Joi.string().required(), }).validate({ surveyId: queryInfo.surveyPath }); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } const surveyId = value.surveyId; @@ -284,15 +298,13 @@ export class SurveyController { ) { const { value, error } = Joi.object({ surveyId: Joi.string().required(), - sessionId: Joi.string().required(), }).validate(surveyInfo); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } const username = req.user.username; const surveyId = value.surveyId; - const sessionId = value.sessionId; const surveyMeta = req.surveyMeta; const surveyConf = await this.surveyConfService.getSurveyConfBySurveyId(surveyId); @@ -332,7 +344,6 @@ export class SurveyController { _id: req.user._id.toString(), username, }, - sessionId: sessionId, }); return { code: 200, diff --git a/server/src/modules/survey/controllers/surveyDownload.controller.ts b/server/src/modules/survey/controllers/surveyDownload.controller.ts deleted file mode 100644 index d03b8b1c..00000000 --- a/server/src/modules/survey/controllers/surveyDownload.controller.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { - Controller, - Get, - Query, - HttpCode, - UseGuards, - SetMetadata, - Request, - Res, - // Response, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; - -import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; - -import { Authentication } from 'src/guards/authentication.guard'; -import { SurveyGuard } from 'src/guards/survey.guard'; -import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; -import { Logger } from 'src/logger'; -import { HttpException } from 'src/exceptions/httpException'; -import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; -//后添加 -import { SurveyDownloadService } from '../services/surveyDownload.service'; -import { - DownloadFileByNameDto, - GetDownloadDto, - GetDownloadListDto, -} from '../dto/getdownload.dto'; -import { join } from 'path'; -import * as util from 'util'; -import * as fs from 'fs'; -import { Response } from 'express'; -import moment from 'moment'; -import { MessageService } from '../services/message.service'; - -@ApiTags('survey') -@ApiBearerAuth() -@Controller('/api/survey/surveyDownload') -export class SurveyDownloadController { - constructor( - private readonly responseSchemaService: ResponseSchemaService, - private readonly surveyDownloadService: SurveyDownloadService, - private readonly logger: Logger, - private readonly messageService: MessageService, - ) {} - - @Get('/download') - @HttpCode(200) - @UseGuards(SurveyGuard) - @SetMetadata('surveyId', 'query.surveyId') - @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE]) - @UseGuards(Authentication) - async download( - @Query() - queryInfo: GetDownloadDto, - @Request() req, - ) { - const { value, error } = GetDownloadDto.validate(queryInfo); - if (error) { - this.logger.error(error.message, { req }); - throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); - } - const { surveyId, isDesensitive } = value; - const responseSchema = - await this.responseSchemaService.getResponseSchemaByPageId(surveyId); - const id = await this.surveyDownloadService.createDownload({ - surveyId, - responseSchema, - }); - this.messageService.addMessage({ - responseSchema, - surveyId, - isDesensitive, - id, - }); - return { - code: 200, - data: { message: '正在生成下载文件,请稍后查看' }, - }; - } - @Get('/getdownloadList') - @HttpCode(200) - @UseGuards(SurveyGuard) - @SetMetadata('surveyId', 'query.surveyId') - @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE]) - @UseGuards(Authentication) - async downloadList( - @Query() - queryInfo: GetDownloadListDto, - @Request() req, - ) { - const { value, error } = GetDownloadListDto.validate(queryInfo); - if (error) { - this.logger.error(error.message, { req }); - throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); - } - const { ownerId, page, pageSize } = value; - const { total, listBody } = - await this.surveyDownloadService.getDownloadList({ - ownerId, - page, - pageSize, - }); - return { - code: 200, - data: { - total: total, - listBody: listBody.map((data) => { - const fmt = 'YYYY-MM-DD HH:mm:ss'; - const units = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - let unitIndex = 0; - let size = Number(data.fileSize); - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - data.downloadTime = moment(Number(data.downloadTime)).format(fmt); - data.fileSize = `${size.toFixed()} ${units[unitIndex]}`; - return data; - }), - }, - }; - } - - @Get('/getdownloadfileByName') - // @HttpCode(200) - // @UseGuards(SurveyGuard) - // @SetMetadata('surveyId', 'query.surveyId') - // @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE]) - // @UseGuards(Authentication) - async getDownloadfileByName( - @Query() queryInfo: DownloadFileByNameDto, - @Res() res: Response, - ) { - const { value, error } = DownloadFileByNameDto.validate(queryInfo); - if (error) { - throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); - } - - const { owner, fileName } = value; - const rootDir = process.cwd(); // 获取当前工作目录 - const filePath = join(rootDir, 'download', owner, fileName); - - // 使用 util.promisify 将 fs.access 转换为返回 Promise 的函数 - const access = util.promisify(fs.access); - try { - console.log('检查文件路径:', filePath); - await access(filePath, fs.constants.F_OK); - - // 文件存在,设置响应头并流式传输文件 - res.setHeader('Content-Type', 'application/octet-stream'); - console.log('文件存在,设置响应头'); - const encodedFileName = encodeURIComponent(fileName); - const contentDisposition = `attachment; filename="${encodedFileName}"; filename*=UTF-8''${encodedFileName}`; - res.setHeader('Content-Disposition', contentDisposition); - console.log('设置响应头成功,文件名:', encodedFileName); - - const fileStream = fs.createReadStream(filePath); - console.log('创建文件流成功'); - fileStream.pipe(res); - - fileStream.on('end', () => { - console.log('文件传输完成'); - }); - - fileStream.on('error', (streamErr) => { - console.error('文件流错误:', streamErr); - res.status(500).send('文件传输中出现错误'); - }); - } catch (err) { - console.error('文件不存在:', filePath); - res.status(404).send('文件不存在'); - } - } - - @Get('/deletefileByName') - @HttpCode(200) - @UseGuards(SurveyGuard) - @SetMetadata('surveyId', 'query.surveyId') - @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE]) - @UseGuards(Authentication) - async deleteFileByName( - @Query() queryInfo: DownloadFileByNameDto, - @Res() res: Response, - ) { - const { value, error } = DownloadFileByNameDto.validate(queryInfo); - if (error) { - throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); - } - const { owner, fileName } = value; - - try { - const result = await this.surveyDownloadService.deleteDownloadFile({ - owner, - fileName, - }); - - // 根据 deleteDownloadFile 的返回值执行不同操作 - if (result === 0) { - return res.status(404).json({ - code: 404, - message: '文件状态已删除或文件不存在', - }); - } - - return res.status(200).json({ - code: 200, - message: '文件删除成功', - data: {}, - }); - } catch (error) { - return res.status(500).json({ - code: 500, - message: '删除文件时出错', - error: error.message, - }); - } - } -} diff --git a/server/src/modules/survey/controllers/surveyHistory.controller.ts b/server/src/modules/survey/controllers/surveyHistory.controller.ts index 38b8733b..53c999c1 100644 --- a/server/src/modules/survey/controllers/surveyHistory.controller.ts +++ b/server/src/modules/survey/controllers/surveyHistory.controller.ts @@ -5,7 +5,6 @@ import { HttpCode, UseGuards, SetMetadata, - Request, } from '@nestjs/common'; import * as Joi from 'joi'; import { ApiTags } from '@nestjs/swagger'; @@ -15,7 +14,7 @@ import { SurveyHistoryService } from '../services/surveyHistory.service'; import { Authentication } from 'src/guards/authentication.guard'; import { SurveyGuard } from 'src/guards/survey.guard'; import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; @ApiTags('survey') @@ -23,7 +22,7 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; export class SurveyHistoryController { constructor( private readonly surveyHistoryService: SurveyHistoryService, - private readonly logger: Logger, + private readonly logger: XiaojuSurveyLogger, ) {} @Get('/getList') @@ -42,7 +41,6 @@ export class SurveyHistoryController { surveyId: string; historyType: string; }, - @Request() req, ) { const { value, error } = Joi.object({ surveyId: Joi.string().required(), @@ -50,7 +48,7 @@ export class SurveyHistoryController { }).validate(queryInfo); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } @@ -65,52 +63,4 @@ export class SurveyHistoryController { data, }; } - - - @Get('/getConflictList') - @HttpCode(200) - @UseGuards(SurveyGuard) - @SetMetadata('surveyId', 'query.surveyId') - @SetMetadata('surveyPermission', [ - SURVEY_PERMISSION.SURVEY_CONF_MANAGE, - SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE, - SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE, - ]) - @UseGuards(Authentication) - async getConflictList( - @Query() - queryInfo: { - surveyId: string; - historyType: string; - sessionId: string; - }, - @Request() req, - ) { - const { value, error } = Joi.object({ - surveyId: Joi.string().required(), - historyType: Joi.string().required(), - sessionId: Joi.string().required(), - }).validate(queryInfo); - - if (error) { - this.logger.error(error.message, { req }); - throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); - } - - const surveyId = value.surveyId; - const historyType = value.historyType; - const sessionId = value.sessionId; - - const data = await this.surveyHistoryService.getConflictList({ - surveyId, - historyType, - sessionId, - }); - - return { - code: 200, - data, - }; - } - } diff --git a/server/src/modules/survey/controllers/surveyMeta.controller.ts b/server/src/modules/survey/controllers/surveyMeta.controller.ts index 3b965222..f9839a6b 100644 --- a/server/src/modules/survey/controllers/surveyMeta.controller.ts +++ b/server/src/modules/survey/controllers/surveyMeta.controller.ts @@ -19,7 +19,7 @@ import { getFilter, getOrder } from 'src/utils/surveyUtil'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { Authentication } from 'src/guards/authentication.guard'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { SurveyGuard } from 'src/guards/survey.guard'; import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; import { WorkspaceGuard } from 'src/guards/workspace.guard'; @@ -33,7 +33,7 @@ import { CollaboratorService } from '../services/collaborator.service'; export class SurveyMetaController { constructor( private readonly surveyMetaService: SurveyMetaService, - private readonly logger: Logger, + private readonly logger: XiaojuSurveyLogger, private readonly collaboratorService: CollaboratorService, ) {} @@ -51,9 +51,7 @@ export class SurveyMetaController { }).validate(reqBody, { allowUnknown: true }); if (error) { - this.logger.error(`updateMeta_parameter error: ${error.message}`, { - req, - }); + this.logger.error(`updateMeta_parameter error: ${error.message}`); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); } const survey = req.surveyMeta; @@ -81,7 +79,7 @@ export class SurveyMetaController { ) { const { value, error } = GetSurveyListDto.validate(queryInfo); if (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR); } const { curPage, pageSize, workspaceId } = value; @@ -91,14 +89,14 @@ export class SurveyMetaController { try { filter = getFilter(JSON.parse(decodeURIComponent(value.filter))); } catch (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); } } if (value.order) { try { order = order = getOrder(JSON.parse(decodeURIComponent(value.order))); } catch (error) { - this.logger.error(error.message, { req }); + this.logger.error(error.message); } } const userId = req.user._id.toString(); diff --git a/server/src/modules/survey/dto/downloadTask.dto.ts b/server/src/modules/survey/dto/downloadTask.dto.ts new file mode 100644 index 00000000..c5f11e3a --- /dev/null +++ b/server/src/modules/survey/dto/downloadTask.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty } from '@nestjs/swagger'; +import Joi from 'joi'; + +export class CreateDownloadDto { + @ApiProperty({ description: '问卷id', required: true }) + surveyId: string; + @ApiProperty({ description: '是否脱敏', required: false }) + isDesensitive: boolean; + + static validate(data) { + return Joi.object({ + surveyId: Joi.string().required(), + isDesensitive: Joi.boolean().allow(null).default(false), + }).validate(data); + } +} +export class GetDownloadTaskListDto { + @ApiProperty({ description: '当前页', required: false }) + pageIndex: number; + @ApiProperty({ description: '一页大小', required: false }) + pageSize: number; + + static validate(data) { + return Joi.object({ + pageIndex: Joi.number().default(1), + pageSize: Joi.number().default(20), + }).validate(data); + } +} + +export class GetDownloadTaskDto { + @ApiProperty({ description: '任务id', required: true }) + taskId: string; + + static validate(data) { + return Joi.object({ + taskId: Joi.string().required(), + }).validate(data); + } +} + +export class DeleteDownloadTaskDto { + @ApiProperty({ description: '任务id', required: true }) + taskId: string; + + static validate(data) { + return Joi.object({ + taskId: Joi.string().required(), + }).validate(data); + } +} diff --git a/server/src/modules/survey/dto/getdownload.dto.ts b/server/src/modules/survey/dto/getdownload.dto.ts deleted file mode 100644 index 5f56009f..00000000 --- a/server/src/modules/survey/dto/getdownload.dto.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import Joi from 'joi'; - -export class GetDownloadDto { - @ApiProperty({ description: '问卷id', required: true }) - surveyId: string; - @ApiProperty({ description: '是否脱密', required: true }) - isDesensitive: boolean; - - static validate(data) { - return Joi.object({ - surveyId: Joi.string().required(), - isDesensitive: Joi.boolean().default(true), // 默认true就是需要脱敏 - }).validate(data); - } -} -export class GetDownloadListDto { - @ApiProperty({ description: '拥有者id', required: true }) - ownerId: string; - @ApiProperty({ description: '当前页', required: false }) - page: number; - @ApiProperty({ description: '一页大小', required: false }) - pageSize: number; - - static validate(data) { - return Joi.object({ - ownerId: Joi.string().required(), - page: Joi.number().default(1), - pageSize: Joi.number().default(20), - }).validate(data); - } -} -export class DownloadFileByNameDto { - @ApiProperty({ description: '文件名', required: true }) - fileName: string; - owner: string; - static validate(data) { - return Joi.object({ - fileName: Joi.string().required(), - owner: Joi.string().required(), - }).validate(data); - } -} diff --git a/server/src/modules/survey/services/collaborator.service.ts b/server/src/modules/survey/services/collaborator.service.ts index cfd6c628..59d70dd8 100644 --- a/server/src/modules/survey/services/collaborator.service.ts +++ b/server/src/modules/survey/services/collaborator.service.ts @@ -3,14 +3,14 @@ import { Collaborator } from 'src/models/collaborator.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { MongoRepository } from 'typeorm'; import { ObjectId } from 'mongodb'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; @Injectable() export class CollaboratorService { constructor( @InjectRepository(Collaborator) private readonly collaboratorRepository: MongoRepository, - private readonly logger: Logger, + private readonly logger: XiaojuSurveyLogger, ) {} async create({ surveyId, userId, permissions }) { diff --git a/server/src/modules/survey/services/downloadTask.service.ts b/server/src/modules/survey/services/downloadTask.service.ts new file mode 100644 index 00000000..2a3e0bba --- /dev/null +++ b/server/src/modules/survey/services/downloadTask.service.ts @@ -0,0 +1,282 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { MongoRepository } from 'typeorm'; +import { ResponseSchema } from 'src/models/responseSchema.entity'; +import { DownloadTask } from 'src/models/downloadTask.entity'; +import { RECORD_STATUS } from 'src/enums'; +import { ObjectId } from 'mongodb'; +import { ResponseSchemaService } from 'src/modules/surveyResponse/services/responseScheme.service'; +import { SurveyResponse } from 'src/models/surveyResponse.entity'; +import { DataStatisticService } from './dataStatistic.service'; +import xlsx from 'node-xlsx'; +import { load } from 'cheerio'; +import { get } from 'lodash'; +import { FileService } from 'src/modules/file/services/file.service'; +import { XiaojuSurveyLogger } from 'src/logger'; + +@Injectable() +export class DownloadTaskService { + private static taskList: Array = []; + private static isExecuting: boolean = false; + + constructor( + @InjectRepository(DownloadTask) + private readonly downloadTaskRepository: MongoRepository, + private readonly responseSchemaService: ResponseSchemaService, + @InjectRepository(SurveyResponse) + private readonly surveyResponseRepository: MongoRepository, + private readonly dataStatisticService: DataStatisticService, + private readonly fileService: FileService, + private readonly logger: XiaojuSurveyLogger, + ) {} + + async createDownloadTask({ + surveyId, + responseSchema, + operatorId, + params, + }: { + surveyId: string; + responseSchema: ResponseSchema; + operatorId: string; + params: any; + }) { + const downloadTask = this.downloadTaskRepository.create({ + surveyId, + surveyPath: responseSchema.surveyPath, + fileSize: '计算中', + ownerId: operatorId, + params: { + ...params, + title: responseSchema.title, + }, + }); + await this.downloadTaskRepository.save(downloadTask); + return downloadTask._id.toString(); + } + + async getDownloadTaskList({ + ownerId, + pageIndex, + pageSize, + }: { + ownerId: string; + pageIndex: number; + pageSize: number; + }) { + const where = { + onwer: ownerId, + 'curStatus.status': { + $ne: RECORD_STATUS.REMOVED, + }, + }; + const [surveyDownloadList, total] = + await this.downloadTaskRepository.findAndCount({ + where, + take: pageSize, + skip: (pageIndex - 1) * pageSize, + order: { + createDate: -1, + }, + }); + return { + total, + list: surveyDownloadList, + }; + } + + async getDownloadTaskById({ taskId }) { + const res = await this.downloadTaskRepository.find({ + where: { + _id: new ObjectId(taskId), + }, + }); + if (Array.isArray(res) && res.length > 0) { + return res[0]; + } + return null; + } + + async deleteDownloadTask({ taskId }: { taskId: string }) { + const curStatus = { + status: RECORD_STATUS.REMOVED, + date: Date.now(), + }; + return this.downloadTaskRepository.updateOne( + { + _id: new ObjectId(taskId), + 'curStatus.status': { + $ne: RECORD_STATUS.REMOVED, + }, + }, + { + $set: { + curStatus, + }, + $push: { + statusList: curStatus as never, + }, + }, + ); + } + + processDownloadTask({ taskId }) { + DownloadTaskService.taskList.push(taskId); + if (!DownloadTaskService.isExecuting) { + this.executeTask(); + DownloadTaskService.isExecuting = true; + } + } + + private async executeTask() { + try { + for (const taskId of DownloadTaskService.taskList) { + const taskInfo = await this.getDownloadTaskById({ taskId }); + if (!taskInfo || taskInfo.curStatus.status === RECORD_STATUS.REMOVED) { + // 不存在或者已删除的,不处理 + continue; + } + await this.handleDownloadTask({ taskInfo }); + } + } finally { + DownloadTaskService.isExecuting = false; + } + } + + private async handleDownloadTask({ taskInfo }) { + try { + // 更新任务状态为计算中 + const updateRes = await this.downloadTaskRepository.updateOne( + { + _id: taskInfo._id, + }, + { + $set: { + curStatus: { + status: RECORD_STATUS.COMOPUTETING, + date: Date.now(), + }, + }, + }, + ); + + this.logger.info(JSON.stringify(updateRes)); + + // 开始计算任务 + const surveyId = taskInfo.surveyId; + const responseSchema = + await this.responseSchemaService.getResponseSchemaByPageId(surveyId); + const where = { + pageId: surveyId, + 'curStatus.status': { + $ne: 'removed', + }, + }; + const total = await this.surveyResponseRepository.count(where); + const pageSize = 200; + const pageTotal = Math.ceil(total / pageSize); + const xlsxHead = []; + const xlsxBody = []; + for (let pageIndex = 1; pageIndex <= pageTotal; pageIndex++) { + const { listHead, listBody } = + await this.dataStatisticService.getDataTable({ + surveyId, + pageNum: pageIndex, + pageSize, + responseSchema, + }); + if (xlsxHead.length === 0) { + for (const item of listHead) { + const $ = load(item.title); + const text = $.text(); + xlsxHead.push(text); + } + } + for (const bodyItem of listBody) { + const bodyData = []; + for (const headItem of listHead) { + const field = headItem.field; + const val = get(bodyItem, field, ''); + const $ = load(val); + const text = $.text(); + bodyData.push(text); + } + xlsxBody.push(bodyData); + } + } + const xlsxData = [xlsxHead, ...xlsxBody]; + const buffer = await xlsx.build([ + { 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, + encoding: '7bit', + mimetype: 'application/octet-stream', + filename: originalname, + size: buffer.length, + buffer: buffer, + stream: null, + destination: null, + path: '', + }; + const { url, key } = await this.fileService.upload({ + configKey: 'SERVER_LOCAL_CONFIG', + file, + pathPrefix: 'exportfile', + keepOriginFilename: true, + }); + + const curStatus = { + status: RECORD_STATUS.FINISHED, + date: Date.now(), + }; + + // 更新计算结果 + const updateFinishRes = await this.downloadTaskRepository.updateOne( + { + _id: taskInfo._id, + }, + { + $set: { + curStatus, + url, + filename: originalname, + fileKey: key, + fileSize: buffer.length, + }, + $push: { + statusList: curStatus as never, + }, + }, + ); + this.logger.info(JSON.stringify(updateFinishRes)); + } catch (error) { + const curStatus = { + status: RECORD_STATUS.ERROR, + date: Date.now(), + }; + await this.downloadTaskRepository.updateOne( + { + _id: taskInfo._id, + }, + { + $set: { + curStatus, + }, + $push: { + statusList: curStatus as never, + }, + }, + ); + this.logger.error( + `导出文件失败 taskId: ${taskInfo._id.toString()}, surveyId: ${taskInfo.surveyId}, message: ${error.message}`, + ); + } + } +} diff --git a/server/src/modules/survey/services/message.service.ts b/server/src/modules/survey/services/message.service.ts deleted file mode 100644 index ad18bf36..00000000 --- a/server/src/modules/survey/services/message.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { EventEmitter } from 'events'; -import { SurveyDownloadService } from './surveyDownload.service'; -import { Inject, Injectable } from '@nestjs/common'; -import { ResponseSchema } from 'src/models/responseSchema.entity'; - -interface QueueItem { - surveyId: string; - responseSchema: ResponseSchema; - isDesensitive: boolean; - id: object; -} - -@Injectable() -export class MessageService extends EventEmitter { - private queue: QueueItem[]; - private concurrency: number; - private processing: number; - - constructor( - @Inject('NumberToken') concurrency: number, - private readonly surveyDownloadService: SurveyDownloadService, - ) { - super(); - this.queue = []; - this.concurrency = concurrency; - this.processing = 0; - this.on('messageAdded', this.processMessages); - } - - public addMessage({ - surveyId, - responseSchema, - isDesensitive, - id, - }: { - surveyId: string; - responseSchema: ResponseSchema; - isDesensitive: boolean; - id: object; - }) { - const message = { - surveyId, - responseSchema, - isDesensitive, - id, - }; - this.queue.push(message); - this.emit('messageAdded'); - } - - private processMessages = async (): Promise => { - if (this.processing >= this.concurrency || this.queue.length === 0) { - return; - } - - const messagesToProcess = Math.min( - this.queue.length, - this.concurrency - this.processing, - ); - const messages = this.queue.splice(0, messagesToProcess); - - this.processing += messagesToProcess; - - await Promise.all( - messages.map(async (message) => { - console.log(`开始计算: ${message}`); - await this.handleMessage(message); - this.emit('messageProcessed', message); - }), - ); - - this.processing -= messagesToProcess; - if (this.queue.length > 0) { - setImmediate(() => this.processMessages()); - } - }; - - async handleMessage(message: QueueItem) { - const { surveyId, responseSchema, isDesensitive, id } = message; - await this.surveyDownloadService.getDownloadPath({ - responseSchema, - surveyId, - isDesensitive, - id, - }); - } -} diff --git a/server/src/modules/survey/services/session.service.ts b/server/src/modules/survey/services/session.service.ts new file mode 100644 index 00000000..f6a78134 --- /dev/null +++ b/server/src/modules/survey/services/session.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { MongoRepository } from 'typeorm'; +import { Session } from 'src/models/session.entity'; +import { ObjectId } from 'mongodb'; +import { RECORD_STATUS } from 'src/enums'; + +@Injectable() +export class SessionService { + constructor( + @InjectRepository(Session) + private readonly sessionRepository: MongoRepository, + ) {} + + create({ surveyId }) { + const session = this.sessionRepository.create({ + surveyId, + }); + return this.sessionRepository.save(session); + } + + findOne(sessionId) { + return this.sessionRepository.findOne({ + where: { + _id: new ObjectId(sessionId), + }, + }); + } + + findLatestEditingOne({ surveyId }) { + return this.sessionRepository.findOne({ + where: { + surveyId, + 'curStatus.status': { + $ne: RECORD_STATUS.NEW, + }, + }, + }); + } + + updateSessionToEditing({ sessionId, surveyId }) { + const now = Date.now(); + const editingStatus = { + status: RECORD_STATUS.EDITING, + date: now, + }; + const newStatus = { + status: RECORD_STATUS.NEW, + date: now, + }; + return Promise.all([ + this.sessionRepository.updateOne( + { + _id: new ObjectId(sessionId), + }, + { + $set: { + curStatus: editingStatus, + updateDate: now, + }, + }, + ), + this.sessionRepository.updateMany( + { + surveyId, + _id: { + $ne: new ObjectId(sessionId), + }, + }, + { + $set: { + curStatus: newStatus, + updateDate: now, + }, + }, + ), + ]); + } +} diff --git a/server/src/modules/survey/services/surveyDownload.service.ts b/server/src/modules/survey/services/surveyDownload.service.ts deleted file mode 100644 index 9e0ccc3c..00000000 --- a/server/src/modules/survey/services/surveyDownload.service.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { MongoRepository } from 'typeorm'; -import { SurveyResponse } from 'src/models/surveyResponse.entity'; - -import moment from 'moment'; -import { keyBy } from 'lodash'; -import { DataItem } from 'src/interfaces/survey'; -import { ResponseSchema } from 'src/models/responseSchema.entity'; -import { getListHeadByDataList } from '../utils'; -//后添加 -import { promises } from 'fs'; -import { join } from 'path'; -import { SurveyDownload } from 'src/models/surveyDownload.entity'; -import { SurveyMeta } from 'src/models/surveyMeta.entity'; -import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; -import { RECORD_STATUS } from 'src/enums'; -import * as cron from 'node-cron'; -import fs from 'fs'; -import path from 'path'; - -@Injectable() -export class SurveyDownloadService implements OnModuleInit { - private radioType = ['radio-star', 'radio-nps']; - - constructor( - @InjectRepository(SurveyResponse) - private readonly surveyResponseRepository: MongoRepository, - @InjectRepository(SurveyDownload) - private readonly SurveyDownloadRepository: MongoRepository, - @InjectRepository(SurveyMeta) - private readonly SurveyDmetaRepository: MongoRepository, - private readonly pluginManager: XiaojuSurveyPluginManager, - ) {} - //初始化一个自动删除过期文件的方法 - async onModuleInit() { - cron.schedule('0 0 * * *', async () => { - try { - const files = await this.SurveyDownloadRepository.find({ - where: { - 'curStatus.status': { - $ne: RECORD_STATUS.REMOVED, - }, - }, - }); - const now = Date.now(); - - for (const file of files) { - if (!file.downloadTime || !file.filePath) { - continue; - } - - const fileSaveDate = Number(file.downloadTime); - const diffDays = (now - fileSaveDate) / (1000 * 60 * 60 * 24); - - if (diffDays > 10) { - this.deleteDownloadFile({ - owner: file.onwer, - fileName: file.filename, - }); - } - } - } catch (err) { - console.error('删除文件错误', err); - } - }); - } - - async createDownload({ - surveyId, - responseSchema, - }: { - surveyId: string; - responseSchema: ResponseSchema; - }) { - const [surveyMeta] = await this.SurveyDmetaRepository.find({ - where: { - surveyPath: responseSchema.surveyPath, - }, - }); - const newSurveyDownload = this.SurveyDownloadRepository.create({ - pageId: surveyId, - surveyPath: responseSchema.surveyPath, - title: responseSchema.title, - fileSize: '计算中', - downloadTime: String(Date.now()), - onwer: surveyMeta.owner, - }); - newSurveyDownload.curStatus = { - status: RECORD_STATUS.COMOPUTETING, - date: Date.now(), - }; - return (await this.SurveyDownloadRepository.save(newSurveyDownload))._id; - } - - private formatHead(listHead = []) { - const head = []; - - listHead.forEach((headItem) => { - head.push({ - field: headItem.field, - title: headItem.title, - }); - - if (headItem.othersCode?.length) { - headItem.othersCode.forEach((item) => { - head.push({ - field: item.code, - title: `${headItem.title}-${item.option}`, - }); - }); - } - }); - - return head; - } - async getDownloadPath({ - surveyId, - responseSchema, - isDesensitive, - id, - }: { - surveyId: string; - responseSchema: ResponseSchema; - isDesensitive: boolean; - id: object; - }) { - const dataList = responseSchema?.code?.dataConf?.dataList || []; - const Head = getListHeadByDataList(dataList); - const listHead = this.formatHead(Head); - const dataListMap = keyBy(dataList, 'field'); - const where = { - pageId: surveyId, - 'curStatus.status': { - $ne: 'removed', - }, - }; - const [surveyResponseList] = - await this.surveyResponseRepository.findAndCount({ - where, - order: { - createDate: -1, - }, - }); - const [surveyMeta] = await this.SurveyDmetaRepository.find({ - where: { - surveyPath: responseSchema.surveyPath, - }, - }); - const listBody = surveyResponseList.map((submitedData) => { - const data = submitedData.data; - const dataKeys = Object.keys(data); - - for (const itemKey of dataKeys) { - if (typeof itemKey !== 'string') { - continue; - } - if (itemKey.indexOf('data') !== 0) { - continue; - } - // 获取题目id - const itemConfigKey = itemKey.split('_')[0]; - // 获取题目 - const itemConfig: DataItem = dataListMap[itemConfigKey]; - // 题目删除会出现,数据列表报错 - if (!itemConfig) { - continue; - } - // 处理选项的更多输入框 - if ( - this.radioType.includes(itemConfig.type) && - !data[`${itemConfigKey}_custom`] - ) { - data[`${itemConfigKey}_custom`] = - data[`${itemConfigKey}_${data[itemConfigKey]}`]; - } - // 将选项id还原成选项文案 - if ( - Array.isArray(itemConfig.options) && - itemConfig.options.length > 0 - ) { - const optionTextMap = keyBy(itemConfig.options, 'hash'); - data[itemKey] = Array.isArray(data[itemKey]) - ? data[itemKey] - .map((item) => optionTextMap[item]?.text || item) - .join(',') - : optionTextMap[data[itemKey]]?.text || data[itemKey]; - } - } - return { - ...data, - diffTime: (submitedData.diffTime / 1000).toFixed(2), - createDate: moment(submitedData.createDate).format( - 'YYYY-MM-DD HH:mm:ss', - ), - }; - }); - if (isDesensitive) { - // 脱敏 - listBody.forEach((item) => { - this.pluginManager.triggerHook('desensitiveData', item); - }); - } - - let titlesCsv = - listHead - .map((question) => `"${question.title.replace(/<[^>]*>/g, '')}"`) - .join(',') + '\n'; - // 获取工作区根目录的路径 - const rootDir = process.cwd(); - const timestamp = Date.now(); - - const filePath = join( - rootDir, - 'download', - `${surveyMeta.owner}`, - `${surveyMeta.title}_${timestamp}.csv`, - ); - const dirPath = path.dirname(filePath); - fs.mkdirSync(dirPath, { recursive: true }); - listBody.forEach((row) => { - const rowValues = listHead.map((head) => { - const value = row[head.field]; - if (typeof value === 'string') { - // 处理字符串中的特殊字符 - return `"${value.replace(/"/g, '""').replace(/<[^>]*>/g, '')}"`; - } - return `"${value}"`; // 其他类型的值(数字、布尔等)直接转换为字符串 - }); - titlesCsv += rowValues.join(',') + '\n'; - }); - const BOM = '\uFEFF'; - let size = 0; - const newSurveyDownload = await this.SurveyDownloadRepository.findOne({ - where: { - _id: id, - }, - }); - fs.writeFile(filePath, BOM + titlesCsv, { encoding: 'utf8' }, (err) => { - if (err) { - console.error('保存文件时出错:', err); - } else { - console.log('文件已保存:', filePath); - fs.stat(filePath, (err, stats) => { - if (err) { - console.error('获取文件大小时出错:', err); - } else { - console.log('文件大小:', stats.size); - size = stats.size; - const filename = `${surveyMeta.title}_${timestamp}.csv`; - const fileType = 'csv'; - (newSurveyDownload.pageId = surveyId), - (newSurveyDownload.surveyPath = responseSchema.surveyPath), - (newSurveyDownload.title = responseSchema.title), - (newSurveyDownload.filePath = filePath), - (newSurveyDownload.filename = filename), - (newSurveyDownload.fileType = fileType), - (newSurveyDownload.fileSize = String(size)), - (newSurveyDownload.downloadTime = String(Date.now())), - (newSurveyDownload.onwer = surveyMeta.owner); - newSurveyDownload.curStatus = { - status: RECORD_STATUS.NEW, - date: Date.now(), - }; - - this.SurveyDownloadRepository.save(newSurveyDownload); - } - }); - } - }); - - return { - filePath, - }; - } - - async getDownloadList({ - ownerId, - page, - pageSize, - }: { - ownerId: string; - page: number; - pageSize: number; - }) { - const where = { - onwer: ownerId, - 'curStatus.status': { - $ne: RECORD_STATUS.REMOVED, - }, - }; - const [surveyDownloadList, total] = - await this.SurveyDownloadRepository.findAndCount({ - where, - take: pageSize, - skip: (page - 1) * pageSize, - order: { - createDate: -1, - }, - }); - const listBody = surveyDownloadList.map((data) => { - return { - _id: data._id, - filename: data.filename, - fileType: data.fileType, - fileSize: data.fileSize, - downloadTime: data.downloadTime, - curStatus: data.curStatus.status, - owner: data.onwer, - }; - }); - return { - total, - listBody, - }; - } - async test({}: { fileName: string }) { - return null; - } - - async deleteDownloadFile({ - owner, - fileName, - }: { - owner: string; - fileName: string; - }) { - const where = { - filename: fileName, - }; - - const [surveyDownloadList] = await this.SurveyDownloadRepository.find({ - where, - }); - if (surveyDownloadList.curStatus.status === RECORD_STATUS.REMOVED) { - return 0; - } - - const newStatusInfo = { - status: RECORD_STATUS.REMOVED, - date: Date.now(), - }; - surveyDownloadList.curStatus = newStatusInfo; - // if (Array.isArray(survey.statusList)) { - // survey.statusList.push(newStatusInfo); - // } else { - // survey.statusList = [newStatusInfo]; - // } - const rootDir = process.cwd(); // 获取当前工作目录 - const filePath = join(rootDir, 'download', owner, fileName); - try { - await promises.unlink(filePath); - console.log(`File at ${filePath} has been successfully deleted.`); - } catch (error) { - console.error(`Failed to delete file at ${filePath}:`, error); - } - await this.SurveyDownloadRepository.save(surveyDownloadList); - return { - code: 200, - data: { - message: '删除成功', - }, - }; - } -} diff --git a/server/src/modules/survey/services/surveyHistory.service.ts b/server/src/modules/survey/services/surveyHistory.service.ts index 47ac13e0..0b512faf 100644 --- a/server/src/modules/survey/services/surveyHistory.service.ts +++ b/server/src/modules/survey/services/surveyHistory.service.ts @@ -17,9 +17,8 @@ export class SurveyHistoryService { schema: SurveySchemaInterface; type: HISTORY_TYPE; user: any; - sessionId: string; }) { - const { surveyId, schema, type, user, sessionId } = params; + const { surveyId, schema, type, user } = params; const newHistory = this.surveyHistory.create({ pageId: surveyId, type, @@ -27,7 +26,6 @@ export class SurveyHistoryService { operator: { _id: user._id.toString(), username: user.username, - sessionId: sessionId, }, }); return this.surveyHistory.save(newHistory); @@ -52,29 +50,4 @@ export class SurveyHistoryService { select: ['createDate', 'operator', 'type', '_id'], }); } - - async getConflictList({ - surveyId, - historyType, - sessionId, - }: { - surveyId: string; - historyType: HISTORY_TYPE; - sessionId: string; - }) { - const result = await this.surveyHistory.find({ - where: { - pageId: surveyId, - type: historyType, - // 排除掉sessionid相同的历史,这些保存不构成冲突 - 'operator.sessionId': { $ne: sessionId }, - }, - order: { createDate: 'DESC' }, - take: 1, - select: ['createDate', 'operator', 'type', '_id'], - }); - - return result; - } - } diff --git a/server/src/modules/survey/survey.module.ts b/server/src/modules/survey/survey.module.ts index 67b0f5ef..5cd48d89 100644 --- a/server/src/modules/survey/survey.module.ts +++ b/server/src/modules/survey/survey.module.ts @@ -7,6 +7,7 @@ import { LoggerProvider } from 'src/logger/logger.provider'; import { SurveyResponseModule } from '../surveyResponse/surveyResponse.module'; import { AuthModule } from '../auth/auth.module'; import { WorkspaceModule } from '../workspace/workspace.module'; +import { FileModule } from '../file/file.module'; import { DataStatisticController } from './controllers/dataStatistic.controller'; import { SurveyController } from './controllers/survey.controller'; @@ -14,6 +15,8 @@ import { SurveyHistoryController } from './controllers/surveyHistory.controller' import { SurveyMetaController } from './controllers/surveyMeta.controller'; import { SurveyUIController } from './controllers/surveyUI.controller'; import { CollaboratorController } from './controllers/collaborator.controller'; +import { DownloadTaskController } from './controllers/downloadTask.controller'; +import { SessionController } from './controllers/session.controller'; import { SurveyConf } from 'src/models/surveyConf.entity'; import { SurveyHistory } from 'src/models/surveyHistory.entity'; @@ -21,6 +24,8 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { Word } from 'src/models/word.entity'; import { Collaborator } from 'src/models/collaborator.entity'; +import { DownloadTask } from 'src/models/downloadTask.entity'; + import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider'; import { DataStatisticService } from './services/dataStatistic.service'; import { SurveyConfService } from './services/surveyConf.service'; @@ -30,11 +35,10 @@ import { ContentSecurityService } from './services/contentSecurity.service'; import { CollaboratorService } from './services/collaborator.service'; import { Counter } from 'src/models/counter.entity'; import { CounterService } from '../surveyResponse/services/counter.service'; -//后添加 -import { SurveyDownload } from 'src/models/surveyDownload.entity'; -import { SurveyDownloadService } from './services/surveyDownload.service'; -import { SurveyDownloadController } from './controllers/surveyDownload.controller'; -import { MessageService } from './services/message.service'; +import { FileService } from '../file/services/file.service'; +import { DownloadTaskService } from './services/downloadTask.service'; +import { SessionService } from './services/session.service'; +import { Session } from 'src/models/session.entity'; @Module({ imports: [ @@ -46,13 +50,14 @@ import { MessageService } from './services/message.service'; Word, Collaborator, Counter, - //后添加 - SurveyDownload, + DownloadTask, + Session, ]), ConfigModule, SurveyResponseModule, AuthModule, WorkspaceModule, + FileModule, ], controllers: [ DataStatisticController, @@ -61,8 +66,8 @@ import { MessageService } from './services/message.service'; SurveyMetaController, SurveyUIController, CollaboratorController, - //后添加 - SurveyDownloadController, + DownloadTaskController, + SessionController, ], providers: [ DataStatisticService, @@ -74,13 +79,9 @@ import { MessageService } from './services/message.service'; CollaboratorService, LoggerProvider, CounterService, - //后添加 - SurveyDownloadService, - MessageService, - { - provide: 'NumberToken', // 使用一个唯一的标识符 - useValue: 10, // 假设这是你想提供的值 - }, + DownloadTaskService, + FileService, + SessionService, ], }) export class SurveyModule {} diff --git a/server/src/modules/survey/template/surveyTemplate/survey/normal.json b/server/src/modules/survey/template/surveyTemplate/survey/normal.json index b666c597..77f0685a 100644 --- a/server/src/modules/survey/template/surveyTemplate/survey/normal.json +++ b/server/src/modules/survey/template/surveyTemplate/survey/normal.json @@ -62,7 +62,6 @@ "quota": "0" } ], - "deleteRecover": false, "quotaNoDisplay": false } ] diff --git a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts index b7b1a6eb..6533d6c7 100644 --- a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts +++ b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts @@ -20,7 +20,7 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi import { RECORD_STATUS } from 'src/enums'; import { SurveyResponse } from 'src/models/surveyResponse.entity'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { ResponseSchema } from 'src/models/responseSchema.entity'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { UserService } from 'src/modules/auth/services/user.service'; @@ -122,7 +122,7 @@ describe('SurveyResponseController', () => { }, }, { - provide: Logger, + provide: XiaojuSurveyLogger, useValue: { error: jest.fn(), info: jest.fn(), @@ -220,7 +220,8 @@ describe('SurveyResponseController', () => { jest .spyOn(clientEncryptService, 'deleteEncryptInfo') .mockResolvedValueOnce(undefined); - const result = await controller.createResponse(reqBody, {}); + + const result = await controller.createResponse(reqBody); expect(result).toEqual({ code: 200, msg: '提交成功' }); expect( @@ -267,7 +268,7 @@ describe('SurveyResponseController', () => { .spyOn(responseSchemaService, 'getResponseSchemaByPath') .mockResolvedValueOnce(null); - await expect(controller.createResponse(reqBody, {})).rejects.toThrow( + await expect(controller.createResponse(reqBody)).rejects.toThrow( SurveyNotFoundException, ); }); @@ -276,7 +277,7 @@ describe('SurveyResponseController', () => { const reqBody = cloneDeep(mockSubmitData); delete reqBody.sign; - await expect(controller.createResponse(reqBody, {})).rejects.toThrow( + await expect(controller.createResponse(reqBody)).rejects.toThrow( HttpException, ); @@ -289,7 +290,7 @@ describe('SurveyResponseController', () => { const reqBody = cloneDeep(mockDecryptErrorBody); reqBody.sign = 'mock sign'; - await expect(controller.createResponse(reqBody, {})).rejects.toThrow( + await expect(controller.createResponse(reqBody)).rejects.toThrow( HttpException, ); @@ -305,7 +306,7 @@ describe('SurveyResponseController', () => { .spyOn(responseSchemaService, 'getResponseSchemaByPath') .mockResolvedValueOnce(mockResponseSchema); - await expect(controller.createResponse(reqBody, {})).rejects.toThrow( + await expect(controller.createResponse(reqBody)).rejects.toThrow( HttpException, ); }); @@ -317,7 +318,7 @@ describe('SurveyResponseController', () => { .spyOn(responseSchemaService, 'getResponseSchemaByPath') .mockResolvedValueOnce(mockResponseSchema); - await expect(controller.createResponse(reqBody, {})).rejects.toThrow( + await expect(controller.createResponse(reqBody)).rejects.toThrow( HttpException, ); }); @@ -343,7 +344,7 @@ describe('SurveyResponseController', () => { }, } as ResponseSchema); - await expect(controller.createResponse(reqBody, {})).rejects.toThrow( + await expect(controller.createResponse(reqBody)).rejects.toThrow( new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR), ); }); diff --git a/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts b/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts index 8df092bf..2640eefd 100644 --- a/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts +++ b/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts @@ -13,7 +13,7 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { RECORD_STATUS } from 'src/enums'; import { ApiTags } from '@nestjs/swagger'; import Joi from 'joi'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; import { WhitelistType } from 'src/interfaces/survey'; import { UserService } from 'src/modules/auth/services/user.service'; @@ -24,7 +24,7 @@ import { WorkspaceMemberService } from 'src/modules/workspace/services/workspace export class ResponseSchemaController { constructor( private readonly responseSchemaService: ResponseSchemaService, - private readonly logger: Logger, + private readonly logger: XiaojuSurveyLogger, private readonly userService: UserService, private readonly workspaceMemberService: WorkspaceMemberService, ) {} diff --git a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts index 336314a5..fc96e7b1 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Body, HttpCode, Request } from '@nestjs/common'; +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'; @@ -10,15 +10,15 @@ import { ResponseSchemaService } from '../services/responseScheme.service'; import { SurveyResponseService } from '../services/surveyResponse.service'; import { ClientEncryptService } from '../services/clientEncrypt.service'; import { MessagePushingTaskService } from '../../message/services/messagePushingTask.service'; +import { RedisService } from 'src/modules/redis/redis.service'; import moment from 'moment'; import * as Joi from 'joi'; import * as forge from 'node-forge'; import { ApiTags } from '@nestjs/swagger'; -import { MutexService } from 'src/modules/mutex/services/mutexService.service'; import { CounterService } from '../services/counter.service'; -import { Logger } from 'src/logger'; +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'; @@ -31,16 +31,16 @@ export class SurveyResponseController { private readonly surveyResponseService: SurveyResponseService, private readonly clientEncryptService: ClientEncryptService, private readonly messagePushingTaskService: MessagePushingTaskService, - private readonly mutexService: MutexService, private readonly counterService: CounterService, - private readonly logger: Logger, + private readonly logger: XiaojuSurveyLogger, + private readonly redisService: RedisService, private readonly userService: UserService, private readonly workspaceMemberService: WorkspaceMemberService, ) {} @Post('/createResponse') @HttpCode(200) - async createResponse(@Body() reqBody, @Request() req) { + async createResponse(@Body() reqBody) { // 检查签名 checkSign(reqBody); // 校验参数 @@ -56,9 +56,7 @@ export class SurveyResponseController { }).validate(reqBody, { allowUnknown: true }); if (error) { - this.logger.error(`updateMeta_parameter error: ${error.message}`, { - req, - }); + this.logger.error(`updateMeta_parameter error: ${error.message}`); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); } @@ -223,8 +221,11 @@ export class SurveyResponseController { return pre; }, {}); - //选项配额校验 - await this.mutexService.runLocked(async () => { + const surveyId = responseSchema.pageId; + const lockKey = `locks:optionSelectedCount:${surveyId}`; + const lock = await this.redisService.lockResource(lockKey, 1000); + this.logger.info(`lockKey: ${lockKey}`); + try { for (const field in decryptedData) { const value = decryptedData[field]; const values = Array.isArray(value) ? value : [value]; @@ -240,13 +241,11 @@ export class SurveyResponseController { const option = optionTextAndId[field].find( (opt) => opt['hash'] === val, ); - if ( - option['quota'] != 0 && - option['quota'] <= optionCountData[val] - ) { + const quota = parseInt(option['quota']); + if (quota !== 0 && quota <= optionCountData[val]) { const item = dataList.find((item) => item['field'] === field); throw new HttpException( - `${item['title']}中的${option['text']}所选人数已达到上限,请重新选择`, + `【${item['title']}】中的【${option['text']}】所选人数已达到上限,请重新选择`, EXCEPTION_CODE.RESPONSE_OVER_LIMIT, ); } @@ -275,7 +274,13 @@ export class SurveyResponseController { optionCountData['total']++; } } - }); + } catch (error) { + this.logger.error(error.message); + throw error; + } finally { + await this.redisService.unlockResource(lock); + this.logger.info(`unlockResource: ${lockKey}`); + } // 入库 const surveyResponse = @@ -288,7 +293,6 @@ export class SurveyResponseController { optionTextAndId, }); - const surveyId = responseSchema.pageId; const sendData = getPushingData({ surveyResponse, questionList: responseSchema?.code?.dataConf?.dataList || [], diff --git a/server/src/modules/surveyResponse/surveyResponse.module.ts b/server/src/modules/surveyResponse/surveyResponse.module.ts index 912e4294..335b23dd 100644 --- a/server/src/modules/surveyResponse/surveyResponse.module.ts +++ b/server/src/modules/surveyResponse/surveyResponse.module.ts @@ -1,16 +1,18 @@ import { Module } from '@nestjs/common'; import { MessageModule } from '../message/message.module'; +import { RedisModule } from '../redis/redis.module'; import { ResponseSchemaService } from './services/responseScheme.service'; import { SurveyResponseService } from './services/surveyResponse.service'; import { CounterService } from './services/counter.service'; import { ClientEncryptService } from './services/clientEncrypt.service'; +import { RedisService } from '../redis/redis.service'; import { ResponseSchema } from 'src/models/responseSchema.entity'; import { Counter } from 'src/models/counter.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { ClientEncrypt } from 'src/models/clientEncrypt.entity'; -import { Logger } from 'src/logger'; +import { LoggerProvider } from 'src/logger/logger.provider'; import { ClientEncryptController } from './controllers/clientEncrpt.controller'; import { CounterController } from './controllers/counter.controller'; @@ -22,7 +24,6 @@ import { WorkspaceModule } from '../workspace/workspace.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; -import { MutexModule } from '../mutex/mutex.module'; @Module({ imports: [ @@ -34,9 +35,9 @@ import { MutexModule } from '../mutex/mutex.module'; ]), ConfigModule, MessageModule, + RedisModule, AuthModule, WorkspaceModule, - MutexModule, ], controllers: [ ClientEncryptController, @@ -50,7 +51,8 @@ import { MutexModule } from '../mutex/mutex.module'; SurveyResponseService, CounterService, ClientEncryptService, - Logger, + LoggerProvider, + RedisService, ], exports: [ ResponseSchemaService, diff --git a/server/src/modules/workspace/_test/workspace.controller.spec.ts b/server/src/modules/workspace/_test/workspace.controller.spec.ts index 73d84ab3..58f168f3 100644 --- a/server/src/modules/workspace/_test/workspace.controller.spec.ts +++ b/server/src/modules/workspace/_test/workspace.controller.spec.ts @@ -10,7 +10,7 @@ import { Workspace } from 'src/models/workspace.entity'; import { WorkspaceMember } from 'src/models/workspaceMember.entity'; import { UserService } from 'src/modules/auth/services/user.service'; import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { User } from 'src/models/user.entity'; jest.mock('src/guards/authentication.guard'); @@ -65,7 +65,7 @@ describe('WorkspaceController', () => { }, }, { - provide: Logger, + provide: XiaojuSurveyLogger, useValue: { info: jest.fn(), error: jest.fn(), diff --git a/server/src/modules/workspace/controllers/workspace.controller.ts b/server/src/modules/workspace/controllers/workspace.controller.ts index 75da6319..d74c9878 100644 --- a/server/src/modules/workspace/controllers/workspace.controller.ts +++ b/server/src/modules/workspace/controllers/workspace.controller.ts @@ -31,7 +31,7 @@ import { import { splitMembers } from '../utils/splitMember'; import { UserService } from 'src/modules/auth/services/user.service'; import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; -import { Logger } from 'src/logger'; +import { XiaojuSurveyLogger } from 'src/logger'; import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto'; import { WorkspaceMember } from 'src/models/workspaceMember.entity'; import { Workspace } from 'src/models/workspace.entity'; @@ -46,7 +46,7 @@ export class WorkspaceController { private readonly workspaceMemberService: WorkspaceMemberService, private readonly userService: UserService, private readonly surveyMetaService: SurveyMetaService, - private readonly logger: Logger, + private readonly logger: XiaojuSurveyLogger, ) {} @Get('getRoleList') @@ -64,10 +64,7 @@ export class WorkspaceController { async create(@Body() workspace: CreateWorkspaceDto, @Request() req) { const { value, error } = CreateWorkspaceDto.validate(workspace); if (error) { - this.logger.error( - `CreateWorkspaceDto validate failed: ${error.message}`, - { req }, - ); + this.logger.error(`CreateWorkspaceDto validate failed: ${error.message}`); throw new HttpException( `参数错误: 请联系管理员`, EXCEPTION_CODE.PARAMETER_ERROR, @@ -137,7 +134,6 @@ export class WorkspaceController { if (error) { this.logger.error( `GetWorkspaceListDto validate failed: ${error.message}`, - { req }, ); throw new HttpException( `参数错误: 请联系管理员`, diff --git a/web/src/management/App.vue b/web/src/management/App.vue index cc283ad9..bd20b829 100644 --- a/web/src/management/App.vue +++ b/web/src/management/App.vue @@ -2,10 +2,71 @@ - diff --git a/web/src/management/pages/download/components/DownloadList.vue b/web/src/management/pages/download/components/DownloadList.vue deleted file mode 100644 index a13a3531..00000000 --- a/web/src/management/pages/download/components/DownloadList.vue +++ /dev/null @@ -1,273 +0,0 @@ - - - - - diff --git a/web/src/management/pages/download/SurveyDownloadPage.vue b/web/src/management/pages/downloadTask/TaskList.vue similarity index 67% rename from web/src/management/pages/download/SurveyDownloadPage.vue rename to web/src/management/pages/downloadTask/TaskList.vue index 8a2d8b1e..7a7cd3b9 100644 --- a/web/src/management/pages/download/SurveyDownloadPage.vue +++ b/web/src/management/pages/downloadTask/TaskList.vue @@ -5,7 +5,7 @@ logo 问卷列表 - 下载页面 + 下载中心
- +
@@ -122,7 +97,6 @@ const activeIndex = ref('2') padding: 20px; display: flex; justify-content: center; - height: 100%; width: 100%; /* 确保容器宽度为100% */ } } diff --git a/web/src/management/pages/downloadTask/components/DownloadTaskList.vue b/web/src/management/pages/downloadTask/components/DownloadTaskList.vue new file mode 100644 index 00000000..da653e0d --- /dev/null +++ b/web/src/management/pages/downloadTask/components/DownloadTaskList.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/web/src/management/pages/edit/index.vue b/web/src/management/pages/edit/index.vue index 523b5adb..6fb1bb64 100644 --- a/web/src/management/pages/edit/index.vue +++ b/web/src/management/pages/edit/index.vue @@ -14,70 +14,29 @@