feat: 完善北大课程相关的内容

This commit is contained in:
luch1994 2024-08-26 11:31:54 +08:00 committed by dayou
parent 2162f3cffd
commit d08f1c71e5
91 changed files with 1663 additions and 2369 deletions

3
.gitignore vendored
View File

@ -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
# 默认的上传文件夹

View File

@ -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 {

View File

@ -1,5 +0,0 @@
{
"dependencies": {
"node-cron": "^3.0.3"
}
}

View File

@ -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
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log

View File

@ -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",

View File

@ -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();
});
}

View File

@ -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<string>('XIAOJU_SURVEY_LOGGER_FILENAME'),
});
}

View File

@ -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, // 白名单校验错误

View File

@ -7,6 +7,8 @@ export enum RECORD_STATUS {
REMOVED = 'removed', // 删除
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
COMOPUTETING = 'computing', // 计算中
FINISHED = 'finished', // 已完成
ERROR = 'error', // 错误
}
// 历史类型

View File

@ -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<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
const sessionIdKey = this.reflector.get<string>(
'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<string[]>(
'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('没有权限');
}
}

View File

@ -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';

View File

@ -60,7 +60,6 @@ export interface DataItem {
rangeConfig?: any;
starStyle?: string;
innerType?: string;
deleteRecover?: boolean;
quotaNoDisplay?: boolean;
}

View File

@ -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' });
}
}

View File

@ -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,
};

View File

@ -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');

View File

@ -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,
},
);
});

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -18,6 +18,8 @@ export class SurveyHistory extends BaseEntity {
operator: {
username: string;
_id: string;
sessionId: string;
};
@Column('string')
sessionId: string;
}

View File

@ -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 || '用户凭证检测失败');
}
}
}

View File

@ -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,
},
};
}
}

View File

@ -37,12 +37,8 @@ export class AuthService {
}
async expiredCheck(token: string) {
let decoded;
try {
decoded = verify(
token,
this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),
);
verify(token, this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'));
} catch (err) {
return true;
}

View File

@ -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,

View File

@ -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}`;
}
}

View File

@ -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 {}

View File

@ -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<T>(callback: () => Promise<T>): Promise<T> {
// 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();
}
}
}

View File

@ -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 {}

View File

@ -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<Lock> {
return this.redlock.acquire([resource], ttl);
}
async unlockResource(lock: Lock): Promise<void> {
await lock.release();
}
}

View File

@ -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>(CollaboratorController);
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
logger = module.get<Logger>(Logger);
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
userService = module.get<UserService>(UserService);
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
workspaceMemberServie = module.get<WorkspaceMemberService>(
@ -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);

View File

@ -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<Collaborator>;
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<MongoRepository<Collaborator>>(
getRepositoryToken(Collaborator),
);
logger = module.get<Logger>(Logger);
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
});
describe('create', () => {

View File

@ -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>(
XiaojuSurveyPluginManager,
);
logger = module.get<Logger>(Logger);
logger = module.get<XiaojuSurveyLogger>(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);
});
});

View File

@ -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,

View File

@ -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,

View File

@ -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);
}

View File

@ -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;

View File

@ -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<string, any> = {};
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<string, any> = {
...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,
};
}
}

View File

@ -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,
};
}
}

View File

@ -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,

View File

@ -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,
});
}
}
}

View File

@ -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,
};
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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<Collaborator>,
private readonly logger: Logger,
private readonly logger: XiaojuSurveyLogger,
) {}
async create({ surveyId, userId, permissions }) {

View File

@ -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<any> = [];
private static isExecuting: boolean = false;
constructor(
@InjectRepository(DownloadTask)
private readonly downloadTaskRepository: MongoRepository<DownloadTask>,
private readonly responseSchemaService: ResponseSchemaService,
@InjectRepository(SurveyResponse)
private readonly surveyResponseRepository: MongoRepository<SurveyResponse>,
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}`,
);
}
}
}

View File

@ -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<void> => {
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,
});
}
}

View File

@ -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<Session>,
) {}
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,
},
},
),
]);
}
}

View File

@ -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<SurveyResponse>,
@InjectRepository(SurveyDownload)
private readonly SurveyDownloadRepository: MongoRepository<SurveyDownload>,
@InjectRepository(SurveyMeta)
private readonly SurveyDmetaRepository: MongoRepository<SurveyMeta>,
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: '删除成功',
},
};
}
}

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -62,7 +62,6 @@
"quota": "0"
}
],
"deleteRecover": false,
"quotaNoDisplay": false
}
]

View File

@ -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),
);
});

View File

@ -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,
) {}

View File

@ -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 || [],

View File

@ -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,

View File

@ -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(),

View File

@ -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(
`参数错误: 请联系管理员`,

View File

@ -2,10 +2,71 @@
<router-view></router-view>
</template>
<script>
export default {
name: 'App'
<script setup lang="ts">
import { watch } from 'vue'
import { get as _get } from 'lodash-es'
import { useUserStore } from '@/management/stores/user'
import { useRouter } from 'vue-router'
import { ElMessageBox, ElMessage, type Action } from 'element-plus'
// axios
import axios from 'axios'
const userStore = useUserStore()
const router = useRouter()
let timer: any
const showConfirmBox = () => {
ElMessageBox.alert('登录状态已失效,请重新登陆。', '提示', {
confirmButtonText: '确认',
showClose: false,
callback: (action: Action) => {
if (action === 'confirm') {
userStore.logout();
router.replace({ name: 'login' });
}
}
});
}
const checkAuth = async () => {
try {
const token = _get(userStore, 'userInfo.token')
const res = await axios({
url: '/api/user/getUserInfo',
headers: {
Authorization: `Bearer ${token}`
}
})
if (res.data.code !== 200) {
showConfirmBox();
} else {
timer = setTimeout(() => {
checkAuth()
}, 30 * 60 * 1000);
}
} catch (error) {
const e = error as any
ElMessage.error(e.message)
}
}
watch(() => userStore.hasLogined, (hasLogined) => {
if (hasLogined) {
timer = setTimeout(() => {
checkAuth()
}, 30 * 60 * 1000);
} else {
clearTimeout(timer);
}
})
</script>
<style lang="scss">

View File

@ -16,12 +16,4 @@ export const getStatisticList = (data) => {
}
})
}
//问卷下载
export const downloadSurvey = ({ surveyId, isDesensitive }) => {
return axios.get('/survey/surveyDownload/download', {
params: {
surveyId,
isDesensitive
}
})
}

View File

@ -7,3 +7,7 @@ export const register = (data) => {
export const login = (data) => {
return axios.post('/auth/login', data)
}
export const getUserInfo = () => {
return axios.get('/user/getUserInfo')
}

View File

@ -1,34 +0,0 @@
import axios from './base'
//问卷列表
export const getDownloadList = ({ ownerId, page, pageSize }) => {
return axios.get('/survey/surveyDownload/getdownloadList', {
params: {
ownerId,
page,
pageSize
}
})
}
//问卷下载
export const getDownloadFileByName = (fileName) => {
return axios
.get('/survey/surveyDownload/getdownloadfileByName', {
params: {
fileName
},
responseType: 'blob'
})
.then((res) => {
return res
})
}
//问卷删除
export const deleteDownloadFile = (owner, fileName) => {
return axios.get('/survey/surveyDownload/deletefileByName', {
params: {
owner,
fileName
}
})
}

View File

@ -0,0 +1,29 @@
import axios from './base'
export const createDownloadSurveyResponseTask = ({ surveyId, isDesensitive }) => {
return axios.post('/downloadTask/createTask', {
surveyId,
isDesensitive
})
}
export const getDownloadTask = taskId => {
return axios.get('/downloadTask/getDownloadTask', { params: { taskId } })
}
export const getDownloadTaskList = ({ pageIndex, pageSize }) => {
return axios.get('/downloadTask/getDownloadTaskList', {
params: {
pageIndex,
pageSize
}
})
}
//问卷删除
export const deleteDownloadTask = (taskId) => {
return axios.post('/downloadTask/deleteDownloadTask', {
taskId,
})
}

View File

@ -24,9 +24,9 @@ export const saveSurvey = ({ surveyId, configData, sessionId }) => {
return axios.post('/survey/updateConf', { surveyId, configData, sessionId })
}
export const publishSurvey = ({ surveyId, sessionId }) => {
export const publishSurvey = ({ surveyId }) => {
return axios.post('/survey/publishSurvey', {
surveyId, sessionId
surveyId
})
}
@ -43,16 +43,6 @@ export const getSurveyHistory = ({ surveyId, historyType }) => {
})
}
export const getConflictHistory = ({ surveyId, historyType, sessionId }) => {
return axios.get('/surveyHisotry/getConflictList', {
params: {
surveyId,
historyType,
sessionId
}
})
}
export const deleteSurvey = (surveyId) => {
return axios.post('/survey/deleteSurvey', {
surveyId
@ -62,3 +52,11 @@ export const deleteSurvey = (surveyId) => {
export const updateSurvey = (data) => {
return axios.post('/survey/updateMeta', data)
}
export const getSessionId = ({ surveyId }) => {
return axios.post('/session/create', { surveyId })
}
export const seizeSession = ({ sessionId }) => {
return axios.post('/session/seize', { sessionId })
}

View File

@ -77,6 +77,5 @@ export const defaultQuestionConfig = {
value: 500
}
},
deleteRecover: false,
quotaNoDisplay: false
}

View File

@ -2,22 +2,14 @@
<div class="data-table-page">
<template v-if="tableData.total">
<div class="menus">
<el-button type="primary" :loading="isDownloading" @click="onDownload">导出全部数据</el-button>
<el-switch
class="desensitive-switch"
:model-value="isShowOriginData"
active-text="是否展示原数据"
@input="onIsShowOriginChange"
>
</el-switch>
<div style="display: flex; justify-content: flex-end">
<el-switch
:model-value="isDownloadDesensitive"
active-text="是否下载脱敏数据"
@input="onisDownloadDesensitive"
style="margin-right: 20px"
>
</el-switch>
<el-button type="primary" @click="onDownload">导出数据</el-button>
</div>
</div>
</template>
@ -35,18 +27,42 @@
<div v-else>
<EmptyIndex :data="noDataConfig" />
</div>
<el-dialog
v-model="downloadDialogVisible"
title="导出确认"
width="500"
>
<el-form :model="downloadForm">
<el-form-item label="导出内容">
<el-radio-group v-model="downloadForm.isDesensitive">
<el-radio :value="true">脱敏数据</el-radio>
<el-radio value="Venue">原回收数据</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="downloadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmDownload()">
确认
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { reactive, toRefs, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import EmptyIndex from '@/management/components/EmptyIndex.vue'
import { getRecycleList, downloadSurvey } from '@/management/api/analysis'
import { getRecycleList } from '@/management/api/analysis'
import { noDataConfig } from '@/management/config/analysisConfig'
import DataTable from '../components/DataTable.vue'
import { createDownloadSurveyResponseTask, getDownloadTask } from '@/management/api/downloadTask'
const dataTableState = reactive({
mainTableLoading: false,
@ -58,13 +74,17 @@ const dataTableState = reactive({
currentPage: 1,
isShowOriginData: false,
tmpIsShowOriginData: false,
isDownloadDesensitive: true
isDownloading: false,
downloadDialogVisible: false,
downloadForm: {
isDesensitive: true,
},
})
const { mainTableLoading, tableData, isShowOriginData, isDownloadDesensitive } = toRefs(dataTableState)
const { mainTableLoading, tableData, isShowOriginData, downloadDialogVisible, isDownloading } = toRefs(dataTableState)
const downloadForm = dataTableState.downloadForm
const route = useRoute()
const router = useRouter()
const formatHead = (listHead) => {
const head = []
@ -131,54 +151,60 @@ onMounted(() => {
init()
})
const onDownload = async () => {
try {
await ElMessageBox.confirm('是否确认下载?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch (error) {
console.log('取消下载')
return
}
exportData()
gotoDownloadList()
dataTableState.downloadDialogVisible = true
}
const gotoDownloadList = async () => {
try {
await ElMessageBox.confirm('计算中,是否前往下载中心?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
} catch (error) {
console.log('取消跳转')
const confirmDownload = async () => {
if (isDownloading.value) {
return
}
router.push('/survey/download')
}
const onisDownloadDesensitive = async () => {
if (dataTableState.isDownloadDesensitive) {
dataTableState.isDownloadDesensitive = false
} else {
dataTableState.isDownloadDesensitive = true
}
}
const exportData = async () => {
try {
const res = await downloadSurvey({
surveyId: String(route.params.id),
isDesensitive: dataTableState.isDownloadDesensitive
})
if (res.code === 200) {
ElMessage.success('下载成功')
isDownloading.value = true
const createRes = await createDownloadSurveyResponseTask({ surveyId: route.params.id, isDesensitive: downloadForm.isDesensitive })
dataTableState.downloadDialogVisible = false
if (createRes.code === 200) {
try {
const taskInfo = await checkIsTaskFinished(createRes.data.taskId)
console.log(taskInfo)
if (taskInfo.url) {
window.open(taskInfo.url)
ElMessage.success("导出成功")
}
} catch (error) {
ElMessage.error('导出失败,请重试')
}
} else {
ElMessage.error('导出失败,请重试')
}
} catch (error) {
ElMessage.error('下载失败')
ElMessage.error(error.message)
ElMessage.error('导出失败,请重试')
} finally {
isDownloading.value = false
}
}
const checkIsTaskFinished = (taskId) => {
return new Promise((resolve, reject) => {
const run = () => {
getDownloadTask(taskId).then(res => {
if (res.code === 200 && res.data) {
const status = res.data.curStatus.status
if (status === 'new' || status === 'computing') {
setTimeout(() => {
run()
}, 5000)
} else {
resolve(res.data)
}
} else {
reject("导出失败");
}
})
}
run()
})
}
@ -204,4 +230,8 @@ const exportData = async () => {
.data-list {
margin-bottom: 20px;
}
.desensitive-switch {
float: right;
}
</style>

View File

@ -1,273 +0,0 @@
<template>
<div class="list-wrapper" v-if="total">
<el-table
v-if="total"
ref="multipleListTable"
class="list-table"
:data="dataList"
empty-text="暂无数据"
row-key="_id"
header-row-class-name="tableview-header"
row-class-name="tableview-row"
cell-class-name="tableview-cell"
style="width: 100%"
v-loading="loading"
>
<el-table-column
v-for="field in fieldList"
:key="field.key"
:prop="field.key"
:label="field.title"
:width="field.width"
class-name="link"
>
</el-table-column>
<el-table-column label="操作" width="200">
<template v-slot="{ row }">
<el-button type="text" size="small" @click="handleDownload(row)"> 下载 </el-button>
<el-button type="text" size="small" @click="openDeleteDialog(row)"> 删除 </el-button>
<el-dialog v-model="centerDialogVisible" title="Warning" width="500" align-center>
<span>确认删除文件吗</span>
<template #footer>
<div class="dialog-footer">
<el-button @click="centerDialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="confirmDelete"> 确认 </el-button>
</div>
</template>
</el-dialog>
</template>
</el-table-column>
</el-table>
<div class="small-text">文件有效期为十天过期或删除将从下载页面移除请及时下载.</div>
<div class="list-pagination" v-if="total">
<el-pagination
background
layout="prev, pager, next"
:total="total"
@current-change="handleCurrentChange"
>
</el-pagination>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useStore } from 'vuex'
import { get, map } from 'lodash-es'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import 'element-plus/theme-chalk/src/message-box.scss'
import moment from 'moment'
//
import 'moment/locale/zh-cn'
//
moment.locale('zh-cn')
import { deleteDownloadFile } from '@/management/api/download'
import axios from 'axios'
interface DownloadItem {
downloadTime: number //
[key: string]: any //
}
const store = useStore()
const props = defineProps({
loading: {
type: Boolean,
default: false
},
data: {
type: Array,
default: () => []
},
total: {
type: Number,
default: 0
}
})
const centerDialogVisible = ref(false)
//
const handleDownload = async (row: any) => {
if (row.curStatus == 'removed') {
ElMessage.error('文件已删除')
return
}
const fileName = row.filename
const owner = row.owner
axios({
method: 'get',
url:
'/api/survey/surveyDownload/getdownloadfileByName?fileName=' + fileName + '&owner=' + owner,
responseType: 'blob' // Blob
})
.then((response: { data: BlobPart }) => {
const blob = new Blob([response.data])
const blobUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = fileName
//
document.body.appendChild(link)
//
link.click()
//
window.URL.revokeObjectURL(blobUrl)
})
.catch((error: any) => {
console.error('下载文件时出错:', error)
})
// try {
// //
// const response = await getDownloadFileByName(fileName);
// console.log('Response from server:', response);
// // MIME
// let mimeType = '';
// if (fileName.endsWith('.csv')) {
// mimeType = 'text/csv; charset=utf-8'; // UTF-8
// } else if (fileName.endsWith('.xls')) {
// mimeType = 'application/vnd.ms-excel';
// } else if (fileName.endsWith('.xlsx')) {
// mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
// } else {
// throw new Error('');
// }
// const blob = new Blob(response.data, { type: mimeType });
// console.log('Blob:', blob); // Check the Blob object
// const url = window.URL.createObjectURL(blob);
// const link = document.createElement('a');
// link.href = url;
// link.download = fileName;
// link.click();
// window.URL.revokeObjectURL(url);
// if (link.parentNode)
// link.parentNode.removeChild(link);
// } catch (error) {
// console.error(':', error);
// }
}
//
const openDeleteDialog = (row: any) => {
centerDialogVisible.value = true
store.dispatch('download/setRow', row)
}
const handleDelete = async (row: any, callback: { (): void; (): void }) => {
try {
console.log('Delete file:', row.filename)
const fileName = row.filename
const owner = row.owner
await deleteDownloadFile(owner, fileName)
row.curStatus = 'removed'
if (callback) {
callback()
}
} catch (error) {
console.error('删除文件时出错:', error)
}
}
//
const confirmDelete = () => {
handleDelete(store.state.download.currentRow, () => {
centerDialogVisible.value = false
})
}
const fields = ['filename', 'fileType', 'fileSize', 'downloadTime', 'curStatus']
const total = computed(() => {
return props.total
})
const data = computed(() => {
return props.data
})
const dataList = computed(() => {
return (data.value as DownloadItem[]).map((item: DownloadItem) => {
if (typeof item === 'object' && item !== null) {
return {
...item
}
}
})
})
const fieldList = computed(() => {
return map(fields, (f) => {
return get(downloadListConfig, f)
})
})
const downloadListConfig = {
filename: {
title: '文件名称',
key: 'filename',
width: 340,
tip: true
},
fileType: {
title: '格式',
key: 'fileType',
width: 200,
tip: true
},
fileSize: {
title: '预估大小',
key: 'fileSize',
width: 140
},
downloadTime: {
title: '下载时间',
key: 'downloadTime',
width: 240
},
curStatus: {
title: '状态',
key: 'curStatus',
comp: 'StateModule'
}
}
const handleCurrentChange = (val: number) => {
const params = {
pageSize: 15,
page: val,
ownerId: store.state.user.userInfo.username
}
store.dispatch('download/getDownloadList', params)
}
</script>
<style lang="scss" scoped>
.question-list-root {
height: 100%;
background-color: #f6f7f9;
.list-wrapper {
padding: 10px 20px;
background: #fff;
.list-table {
min-height: 620px;
.cell {
text-align: center;
}
}
.small-text {
color: red;
}
.list-pagination {
margin-top: 20px;
:deep(.el-pagination) {
display: flex;
justify-content: flex-end;
}
}
}
}
</style>

View File

@ -5,7 +5,7 @@
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
<el-menu-item index="1" @click="handleSurvey">问卷列表</el-menu-item>
<el-menu-item index="2">下载页面</el-menu-item>
<el-menu-item index="2">下载中心</el-menu-item>
</el-menu>
</div>
<div class="login-info">
@ -15,56 +15,31 @@
</div>
</div>
<div class="table-container">
<DownloadList
:loading="loading"
:data="surveyList"
:total="surveyTotal"
@reflush="fetchSurveyList"
></DownloadList>
<DownloadTaskList></DownloadTaskList>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useStore } from 'vuex'
import { ref, computed } from 'vue'
import { useUserStore } from '@/management/stores/user'
import { useRouter } from 'vue-router'
import DownloadList from './components/DownloadList.vue'
import DownloadTaskList from './components/DownloadTaskList.vue'
const store = useStore()
const userStore = useUserStore()
const router = useRouter()
const userInfo = computed(() => {
return store.state.user.userInfo
})
const surveyList = computed(() => {
return store.state.download.surveyList
})
const surveyTotal = computed(() => {
return store.state.download.surveyTotal
return userStore.userInfo
})
const handleSurvey = () => {
router.push('/survey')
}
const handleLogout = () => {
store.dispatch('user/logout')
userStore.logout()
router.replace({ name: 'login' })
}
const loading = ref(false)
onMounted(() => {
fetchSurveyList()
})
const fetchSurveyList = async (params?: any) => {
if (!params) {
params = {
pageSize: 15,
curPage: 1
}
}
;(params.ownerId = store.state.user.userInfo.username), (loading.value = true)
await store.dispatch('download/getDownloadList', params)
loading.value = false
}
const activeIndex = ref('2')
</script>
@ -122,7 +97,6 @@ const activeIndex = ref('2')
padding: 20px;
display: flex;
justify-content: center;
height: 100%;
width: 100%; /* 确保容器宽度为100% */
}
}

View File

@ -0,0 +1,200 @@
<template>
<div v-loading="loading" class="list-wrapper" v-if="total">
<el-table
v-if="total"
ref="multipleListTable"
class="list-table"
:data="dataList"
empty-text="暂无数据"
row-key="_id"
header-row-class-name="tableview-header"
row-class-name="tableview-row"
cell-class-name="tableview-cell"
style="width: 100%"
v-loading="loading"
>
<el-table-column
v-for="field in fieldList"
:key="field.key"
:prop="field.key"
:label="field.title"
:width="field.width"
class-name="link"
:formatter="field.formatter"
>
</el-table-column>
<el-table-column label="操作" width="200">
<template v-slot="{ row }">
<el-button size="small" @click="handleDownload(row)"> 下载 </el-button>
<el-button type="primary" size="small" @click="openDeleteDialog(row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<div class="list-pagination" v-if="total">
<el-pagination
background
layout="prev, pager, next"
:total="total"
:size="pageSize"
@current-change="handleCurrentChange"
>
</el-pagination>
</div>
<el-dialog v-model="centerDialogVisible" title="" width="500" align-center>
<span>确认删除下载记录吗</span>
<template #footer>
<div class="dialog-footer">
<el-button @click="centerDialogVisible = false"> 取消 </el-button>
<el-button type="primary" @click="confirmDelete"> 确认 </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { get, map } from 'lodash-es'
import { ElMessage } from 'element-plus'
import { deleteDownloadTask, getDownloadTaskList } from '@/management/api/downloadTask'
import { CODE_MAP } from '@/management/api/base'
import 'element-plus/theme-chalk/src/message.scss'
import 'element-plus/theme-chalk/src/message-box.scss'
import moment from 'moment'
//
import 'moment/locale/zh-cn'
//
moment.locale('zh-cn')
const loading = ref(false)
const pageSize = ref(15)
const total = ref(0)
const dataList = reactive([])
onMounted(() => {
getList({ pageIndex: 1 })
})
const getList = async ({ pageIndex }: { pageIndex: number }) => {
if (!pageIndex) {
pageIndex = 1
}
const params = {
pageSize: pageSize.value,
pageIndex,
}
const res: Record<string, any> = await getDownloadTaskList(params)
if (res.code === CODE_MAP.SUCCESS) {
total.value = res.data.total
dataList.values = res.data.list
}
loading.value = false
}
const statusTextMap: Record<string, string> = {
new: '排队中',
computing: '计算中',
finished: '已完成',
removed: '已删除',
};
const centerDialogVisible = ref(false)
let currentDelRow: Record<string, any> = {}
//
const handleDownload = async (row: any) => {
if (row.curStatus.status === 'removed') {
ElMessage.error('文件已删除')
return
}
if (row.url) {
window.open(row.url)
}
}
//
const openDeleteDialog = (row: any) => {
centerDialogVisible.value = true
currentDelRow = row
}
//
const confirmDelete = async () => {
try {
await deleteDownloadTask(currentDelRow.taskId)
await getList({ pageIndex: 1 })
} catch (error) {
ElMessage.error("删除失败,请刷新重试")
}
centerDialogVisible.value = false
}
const fields = ['filename', 'fileSize', 'createDate', 'curStatus']
const fieldList = computed(() => {
return map(fields, (f) => {
return get(downloadListConfig, f)
})
})
const downloadListConfig = {
filename: {
title: '文件名称',
key: 'filename',
width: 340,
tip: true
},
fileSize: {
title: '预估大小',
key: 'fileSize',
width: 140
},
createDate: {
title: '下载时间',
key: 'createDate',
width: 240
},
curStatus: {
title: '状态',
key: 'curStatus.status',
formatter(row: Record<string, any>, column: Record<string, any>) {
console.log({
row,
column,
})
return statusTextMap[get(row, column.rawColumnKey)]
}
}
}
const handleCurrentChange = (val: number) => {
getList({ pageIndex: val })
}
</script>
<style lang="scss" scoped>
.question-list-root {
height: 100%;
background-color: #f6f7f9;
.list-wrapper {
padding: 10px 20px;
background: #fff;
.list-table {
.cell {
text-align: center;
}
}
.small-text {
color: red;
}
.list-pagination {
margin-top: 20px;
:deep(.el-pagination) {
display: flex;
justify-content: flex-end;
}
}
}
}
</style>

View File

@ -14,70 +14,29 @@
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, onUnmounted } from 'vue'
import { onMounted } from 'vue'
import { useEditStore } from '@/management/stores/edit'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { Action } from 'element-plus'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import LeftMenu from '@/management/components/LeftMenu.vue'
import CommonTemplate from './components/CommonTemplate.vue'
import Navbar from './components/ModuleNavbar.vue'
import axios from '../../api/base'
import { initShowLogicEngine } from '@/management/hooks/useShowLogicEngine'
const editStore = useEditStore()
const { init, setSurveyId } = editStore
const { init, setSurveyId, initSessionId } = editStore
const router = useRouter()
const route = useRoute()
const authCheckInterval = ref<any>(null)
const showConfirmBox = () => {
ElMessageBox.alert('登录状态已失效,请重新登陆。', '提示', {
confirmButtonText: '确认',
showClose: false,
callback: (action: Action) => {
if (action === 'confirm') {
axios.get('/auth/statuscheck')
.then((response) => {
if (response.data.expired) {
store.dispatch('user/logout').then(() => {
router.replace({name: 'login'}); //
})
} else {
location.reload(); //
}
})
.catch((error) => {
console.log("error: " + error);
store.dispatch('user/logout').then(() => {
router.replace({name: 'login'});
})
});
}
}
});
}
const checkAuth = () => {
axios.get('/auth/statuscheck').then((response) => {
if (response.data.expired) {
clearInterval(authCheckInterval.value);
authCheckInterval.value = null
showConfirmBox();
}
}).catch((error) => {
console.log("erro:" + error)
});
}
onMounted(async () => {
setSurveyId(route.params.id as string)
const surveyId = route.params.id as string
setSurveyId(surveyId)
try {
await init()
// 30
authCheckInterval.value = setInterval(() => checkAuth(), 1000);
} catch (err: any) {
ElMessage.error(err.message)
@ -86,10 +45,6 @@ onMounted(async () => {
}, 1000)
}
})
onUnmounted(() => {
clearInterval(authCheckInterval.value);
authCheckInterval.value = null
})
</script>
<style lang="scss" scoped>
.edit-index {

View File

@ -6,14 +6,12 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useEditStore } from '@/management/stores/edit'
import { useUserStore } from '@/management/stores/user'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, type Action } from 'element-plus'
import { ElMessage, } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { publishSurvey, saveSurvey, getConflictHistory } from '@/management/api/survey'
import { publishSurvey, saveSurvey } from '@/management/api/survey'
import buildData from './buildData'
import { storeToRefs } from 'pinia'
interface Props {
updateLogicConf: any
@ -24,12 +22,12 @@ const props = defineProps<Props>()
const isPublishing = ref<boolean>(false)
const editStore = useEditStore()
const { schema, getSchemaFromRemote } = editStore
const userStore = useUserStore()
const router = useRouter()
const { getSchemaFromRemote } = editStore
const { schema, sessionId } = storeToRefs(editStore)
const saveData = computed(() => {
return buildData(schema, sessionStorage.getItem('sessionUUID'))
return buildData(schema.value, sessionId.value)
})
const router = useRouter()
const validate = () => {
let checked = true
@ -51,55 +49,19 @@ const validate = () => {
}
}
const checkConflict = async (surveyid:string) => {
try {
const dailyHis = await getConflictHistory({surveyId: surveyid, historyType: 'dailyHis', sessionId: sessionStorage.getItem('sessionUUID')})
if (dailyHis.data.length > 0) {
const lastHis = dailyHis.data.at(0)
if (Date.now() - lastHis.createDate > 2 * 60 * 1000) {
return [false, '']
}
return [true, lastHis.operator.username]
}
} catch (error) {
console.log(error)
}
return [false, '']
}
const onSave = async () => {
let res
if (!saveData.value.sessionId) {
ElMessage.error('未获取到sessionId')
return null
}
if (!saveData.value.surveyId) {
ElMessage.error('未获取到问卷id')
return null
}
//
const [isconflict, conflictName] = await checkConflict(saveData.value.surveyId)
if(isconflict) {
if (conflictName == userStore.userInfo.username) {
ElMessageBox.alert('当前问卷已在其它页面开启编辑,刷新以获取最新内容。', '提示', {
confirmButtonText: '确认',
callback: (action: Action) => {
if (action === 'confirm') {
getSchemaFromRemote()
}
}
});
} else {
ElMessageBox.alert(`当前问卷2分钟内由${conflictName}编辑,刷新以获取最新内容。`, '提示', {
confirmButtonText: '确认',
callback: (action: Action) => {
if (action === 'confirm') {
getSchemaFromRemote()
}
}
});
}
return null
} else {
//
res = await saveSurvey(saveData.value)
}
const res: Record<string, any> = await saveSurvey(saveData.value)
return res
}
const handlePublish = async () => {
@ -124,8 +86,9 @@ const handlePublish = async () => {
}
if(saveRes && saveRes?.code !== 200) {
ElMessage.error(`保存失败 ${saveRes.errmsg}`)
return
}
const publishRes: any = await publishSurvey({ surveyId: saveData.value.surveyId, sessionId: sessionStorage.getItem('sessionUUID') })
const publishRes: any = await publishSurvey({ surveyId: saveData.value.surveyId })
if (publishRes.code === 200) {
ElMessage.success('发布成功')
getSchemaFromRemote()

View File

@ -14,18 +14,15 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, watch, onMounted } from 'vue'
import { ref, computed, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useEditStore } from '@/management/stores/edit'
import { nanoid } from 'nanoid'
import { get as _get } from 'lodash-es'
import { ElMessage, ElMessageBox, type Action } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { saveSurvey } from '@/management/api/survey'
import { saveSurvey, seizeSession } from '@/management/api/survey'
import buildData from './buildData'
import { getConflictHistory } from '@/management/api/survey'
interface Props {
updateLogicConf: any
@ -47,8 +44,8 @@ const saveText = computed(
)
const editStore = useEditStore()
const { schemaUpdateTime } = storeToRefs(editStore)
const { schema } = editStore
const { schemaUpdateTime, schema, sessionId } = storeToRefs(editStore)
const validate = () => {
let checked = true
@ -70,67 +67,33 @@ const validate = () => {
msg
}
}
onMounted(() => {
if (!sessionStorage.getItem('sessionUUID')) {
sessionStorage.setItem('sessionUUID', nanoid());
}
})
const checkConflict = async (surveyid: string) => {
try {
const dailyHis = await getConflictHistory({surveyId: surveyid, historyType: 'dailyHis', sessionId: sessionStorage.getItem('sessionUUID')})
if (dailyHis.data.length > 0) {
const lastHis = dailyHis.data.at(0)
if (Date.now() - lastHis.createDate > 2 * 60 * 1000) {
return [false, '']
}
return [true, lastHis.operator.username]
}
}catch (error) {
console.log(error)
}
return [false, '']
}
const onSave = async () => {
let res
const saveData = buildData(store.state.edit.schema, sessionStorage.getItem('sessionUUID'))
const saveData = buildData(schema.value, sessionId.value);
if (!saveData.sessionId) {
ElMessage.error('sessionId有误')
return null
}
if (!saveData.surveyId) {
ElMessage.error('未获取到问卷id')
return null
}
//
const [isconflict, conflictName] = await checkConflict(saveData.surveyId)
if(isconflict) {
if (conflictName == store.state.user.userInfo.username) {
ElMessageBox.alert('当前问卷已在其它页面开启编辑,刷新以获取最新内容。', '提示', {
confirmButtonText: '确认',
callback: (action: Action) => {
if (action === 'confirm') {
store.dispatch('edit/getSchemaFromRemote')
}
}
});
} else {
ElMessageBox.alert(`当前问卷2分钟内由${conflictName}编辑,刷新以获取最新内容。`, '提示', {
confirmButtonText: '确认',
callback: (action: Action) => {
if (action === 'confirm') {
store.dispatch('edit/getSchemaFromRemote')
}
}
});
}
return null
} else {
//
res = await saveSurvey(saveData)
}
const res: Record<string, any> = await saveSurvey(saveData)
return res
}
const seize = async () => {
const seizeRes: Record<string, any> = await seizeSession({ sessionId })
if (seizeRes.code === 200) {
location.reload();
} else {
ElMessage.error('获取权限失败,请重试')
}
}
const timerHandle = ref<NodeJS.Timeout | number | null>(null)
const triggerAutoSave = () => {
if (autoSaveStatus.value === 'saving') {
@ -171,17 +134,17 @@ const handleSave = async () => {
return
}
isSaving.value = true
isShowAutoSave.value = false
//
const { checked, msg } = validate()
if (!checked) {
isSaving.value = false
ElMessage.error(msg)
return
}
isSaving.value = true
try {
const res: any = await onSave()
if(!res) {
@ -189,11 +152,16 @@ const handleSave = async () => {
}
if (res.code === 200) {
ElMessage.success('保存成功')
}
if(res.code !== 200) {
} else if (res.code === 3006) {
ElMessageBox.alert('当前问卷已在其它页面开启编辑,点击“抢占”以获取保存权限。', '提示', {
confirmButtonText: '抢占',
callback: () => {
seize();
}
});
} else {
ElMessage.error(res.errmsg)
}
} catch (error) {
ElMessage.error('保存问卷失败')
} finally {

View File

@ -5,7 +5,7 @@
<img class="logo-img" src="/imgs/Logo.webp" alt="logo" />
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
<el-menu-item index="1">问卷列表</el-menu-item>
<el-menu-item index="2" @click="handleDownload">下载页面</el-menu-item>
<el-menu-item index="2" @click="handleDownload">下载中心</el-menu-item>
</el-menu>
</div>
<div class="login-info">
@ -187,7 +187,7 @@ const handleLogout = () => {
}
//
const handleDownload = () => {
router.push('/survey/download')
router.push('/survey/downloadTask/')
}
</script>

View File

@ -27,9 +27,9 @@ const routes: RouteRecordRaw[] = [
}
},
{
path: '/survey/download/',
path: '/survey/downloadTask/',
name: 'download',
component: () => import('../pages/download/SurveyDownloadPage.vue'),
component: () => import('../pages/downloadTask/TaskList.vue'),
meta: {
needLogin: true
}

View File

@ -1,54 +0,0 @@
import 'element-plus/theme-chalk/src/message.scss'
import { getDownloadList, deleteDownloadFile } from '@/management/api/download'
export default {
namespaced: true,
state: {
surveyList: [],
surveyTotal: 0,
currentRow: []
},
mutations: {
setSurveyList(state, list) {
state.surveyList = list
},
setSurveyTotal(state, total) {
state.surveyTotal = total
},
setCurrentRow(state, row) {
state.currentRow = row
}
},
actions: {
async setRow({ commit }, payload) {
commit('setCurrentRow', payload)
},
async getDownloadList({ commit }, payload) {
let params = {
ownerId: payload.ownerId,
page: payload.page ? payload.page : 1,
pageSize: payload.pageSize ? payload.pageSize : 15
}
try {
const { data } = await getDownloadList(params)
console.log(data)
commit('setSurveyList', data.listBody)
commit('setSurveyTotal', data.total)
} catch (e) {
console.error(e)
}
},
async DownloadFileByName(payload) {
let params = {
fileName: payload.fileName
}
try {
const { data } = await deleteDownloadFile(params)
console.log(data)
} catch (e) {
console.error(e)
}
}
},
getters: {}
}

View File

@ -1,63 +0,0 @@
export default {
currentEditOne: null,
currentEditStatus: 'Success',
schemaUpdateTime: Date.now(),
surveyId: '', // url上取的surveyId
schema: {
metaData: null,
bannerConf: {
titleConfig: {
mainTitle: '<h3 style="text-align: center">欢迎填写问卷</h3>',
subTitle: `<p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style="color: rgb(204, 0, 0)">期待您的参与!</span></p>`,
applyTitle: ''
},
bannerConfig: {
bgImage: '',
bgImageAllowJump: false,
bgImageJumpLink: '',
videoLink: '',
postImg: ''
}
},
bottomConf: {
logoImage: '',
logoImageWidth: '28%'
},
skinConf: {
backgroundConf: {
color: '#fff'
},
themeConf: {
color: '#ffa600'
},
contentConf: {
opacity: 100
}
},
baseConf: {
begTime: '',
endTime: '',
language: 'chinese',
showVoteProcess: 'allow',
tLimit: 0,
answerBegTime: '',
answerEndTime: '',
answerLimitTime: 0,
breakAnswer: false,
backAnswer: false
},
submitConf: {
submitTitle: '',
msgContent: {},
confirmAgain: {
is_again: true
},
link: ''
},
questionDataList: [],
logicConf: {
showLogicConf: []
}
},
downloadPath: ''
}

View File

@ -1,21 +0,0 @@
import { createStore } from 'vuex'
import edit from './edit'
import user from './user'
import list from './list'
import actions from './actions'
import mutations from './mutations'
import state from './state'
import download from './download'
export default createStore({
state,
getters: {},
mutations,
actions,
modules: {
edit,
user,
list,
download
}
})

View File

@ -10,7 +10,7 @@ import { QUESTION_TYPE } from '@/common/typeEnum'
import { getQuestionByType } from '@/management/utils/index'
import { filterQuestionPreviewData } from '@/management/utils/index'
import { getSurveyById } from '@/management/api/survey'
import { getSurveyById, getSessionId } from '@/management/api/survey'
import { getNewField } from '@/management/utils'
import submitFormConfig from '@/management/pages/edit/setterConfig/submitConfig'
@ -93,6 +93,7 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
})
const { showLogicEngine, initShowLogicEngine, jumpLogicEngine, initJumpLogicEngine } =
useLogicEngine(schema)
function initSchema({ metaData, codeData }: { metaData: any; codeData: any }) {
schema.metaData = metaData
schema.bannerConf = _merge({}, schema.bannerConf, codeData.bannerConf)
@ -151,7 +152,7 @@ function useInitializeSchema(surveyId: Ref<string>, initializeSchemaCallBack: ()
initSchema,
getSchemaFromRemote,
showLogicEngine,
jumpLogicEngine
jumpLogicEngine,
}
}
@ -477,21 +478,48 @@ function useLogicEngine(schema: any) {
initJumpLogicEngine
}
}
type IBannerItem = {
name: string
key: string
list: Array<Object>
}
type IBannerList = Record<string, IBannerItem>
export const useEditStore = defineStore('edit', () => {
const surveyId = ref('')
const bannerList: Ref<IBannerList> = ref({})
const cooperPermissions = ref(Object.values(SurveyPermissions))
const schemaUpdateTime = ref(Date.now())
function setSurveyId(id: string) {
surveyId.value = id
}
const { schema, initSchema, getSchemaFromRemote, showLogicEngine, jumpLogicEngine } =
useInitializeSchema(surveyId, () => {
editGlobalBaseConf.initCounts()
})
const sessionId = ref('')
async function initSessionId() {
const sessionIdKey = `${surveyId.value}_sessionId`;
const localSessionId = sessionStorage.getItem(sessionIdKey)
if (localSessionId) {
sessionId.value = localSessionId
} else {
const res: Record<string, any> = await getSessionId({ surveyId: surveyId.value })
if (res.code === 200) {
sessionId.value = res.data.sessionId
sessionStorage.setItem(sessionIdKey, sessionId.value)
}
}
}
const questionDataList = toRef(schema, 'questionDataList')
const editGlobalBaseConf = useEditGlobalBaseConf(questionDataList, updateTime)
@ -499,9 +527,6 @@ export const useEditStore = defineStore('edit', () => {
schema.questionDataList = data
}
function setSurveyId(id: string) {
surveyId.value = id
}
const fetchBannerData = async () => {
const res: any = await getBannerData()
@ -509,6 +534,7 @@ export const useEditStore = defineStore('edit', () => {
bannerList.value = res.data
}
}
const fetchCooperPermissions = async (id: string) => {
const res: any = await getCollaboratorPermissions(id)
if (res.code === CODE_MAP.SUCCESS) {
@ -531,6 +557,7 @@ export const useEditStore = defineStore('edit', () => {
const { metaData } = schema
if (!metaData || (metaData as any)?._id !== surveyId.value) {
await getSchemaFromRemote()
await initSessionId()
}
currentEditOne.value = null
currentEditStatus.value = 'Success'
@ -641,7 +668,9 @@ export const useEditStore = defineStore('edit', () => {
return {
editGlobalBaseConf,
surveyId,
sessionId,
setSurveyId,
initSessionId,
bannerList,
fetchBannerData,
cooperPermissions,

View File

@ -119,15 +119,11 @@ const meta = {
},
type: 'QuotaConfig',
// 输出转换
valueSetter({ options, deleteRecover, quotaNoDisplay}) {
valueSetter({ options, quotaNoDisplay}) {
return [{
key: 'options',
value: options
},
{
key: 'deleteRecover',
value: deleteRecover
},
{
key: 'quotaNoDisplay',
value: quotaNoDisplay

View File

@ -72,13 +72,6 @@ const meta = {
}
]
},
// deleteRecover
{
name: 'deleteRecover',
propType: Boolean,
description: '删除后恢复选项配额',
defaultValue: false
},
{
name: 'quotaNoDisplay',
propType: Boolean,
@ -107,15 +100,11 @@ const meta = {
},
type: 'QuotaConfig',
// 输出转换
valueSetter({ options, deleteRecover, quotaNoDisplay}) {
valueSetter({ options, quotaNoDisplay}) {
return [{
key: 'options',
value: options
},
{
key: 'deleteRecover',
value: deleteRecover
},
{
key: 'quotaNoDisplay',
value: quotaNoDisplay

View File

@ -44,17 +44,6 @@
</el-table-column>
</el-table>
<div></div>
<div>
<el-checkbox v-model="deleteRecoverValue" label="删除后恢复选项配额"> </el-checkbox>
<el-tooltip
class="tooltip"
effect="dark"
placement="right"
content="勾选后,把收集到的数据项删除或者设置为无效回收即可恢复选项配额。"
>
<i-ep-questionFilled class="icon-tip" />
</el-tooltip>
</div>
<div>
<el-checkbox v-model="quotaNoDisplayValue" label="不展示配额剩余数量"> </el-checkbox>
<el-tooltip
@ -89,7 +78,6 @@ const emit = defineEmits(['form-change'])
const dialogVisible = ref(false)
const moduleConfig = ref(props.moduleConfig)
const optionData = ref(props.moduleConfig.options)
const deleteRecoverValue = ref(moduleConfig.value.deleteRecover)
const quotaNoDisplayValue = ref(moduleConfig.value.quotaNoDisplay)
const openQuotaConfig = () => {
@ -107,7 +95,6 @@ const confirm = () => {
handleQuotaChange()
emit(FORM_CHANGE_EVENT_KEY, {
options: optionData.value,
deleteRecover: deleteRecoverValue.value,
quotaNoDisplay: quotaNoDisplayValue.value
})
}
@ -145,7 +132,6 @@ watch(
(val) => {
moduleConfig.value = val
optionData.value = val.options
deleteRecoverValue.value = val.deleteRecover
quotaNoDisplayValue.value = val.quotaNoDisplay
},
{ immediate: true, deep: true }

View File

@ -145,7 +145,7 @@ const handleChange = (data) => {
}
//
if (props.moduleConfig.type === NORMAL_CHOICES) {
store.dispatch('changeQuota', data)
questionStore.updateQuotaData(data)
}
//
localStorageBack()

View File

@ -92,10 +92,10 @@ const normalizationRequestBody = () => {
localStorage.removeItem(surveyPath.value + "_questionData")
localStorage.removeItem("isSubmit")
//
var formData = Object.assign({}, surveyStore.formValues)
for(const key in formData){
formData[key] = encodeURIComponent(formData[key])
}
var formData : Record<string, any> = Object.assign({}, surveyStore.formValues)
for(const key in formData){
formData[key] = encodeURIComponent(formData[key])
}
localStorage.setItem(surveyPath.value + "_questionData", JSON.stringify(formData))
localStorage.setItem('isSubmit', JSON.stringify(true))
@ -122,7 +122,6 @@ const submitSurver = async () => {
}
try {
const params = normalizationRequestBody()
console.log(params)
const res: any = await submitForm(params)
if (res.code === 200) {
router.replace({ name: 'successPage' })
@ -130,6 +129,9 @@ const submitSurver = async () => {
alert({
title: res.errmsg || '提交失败'
})
if (res.code === 9003) {
questionStore.initQuotaMap()
}
}
} catch (error) {
console.log(error)

View File

@ -1,364 +0,0 @@
import moment from 'moment'
// 引入中文
import 'moment/locale/zh-cn'
// 设置中文
moment.locale('zh-cn')
import adapter from '../adapter'
import { queryVote, getEncryptInfo } from '@/render/api/survey'
import state from './state'
import useCommandComponent from '../hooks/useCommandComponent'
import BackAnswerDialog from '../components/BackAnswerDialog.vue'
import { NORMAL_CHOICES } from '@/common/typeEnum.ts'
/**
* CODE_MAP不从management引入在dev阶段会导致B端 router被加载进而导致C端路由被添加 baseUrl: /management
*/
const CODE_MAP = {
SUCCESS: 200,
ERROR: 500,
NO_AUTH: 403
}
const VOTE_INFO_KEY = 'voteinfo'
const QUOTA_INFO_KEY = 'limitinfo'
import router from '../router'
const confirm = useCommandComponent(BackAnswerDialog)
export default {
// 初始化
init(
{ commit, dispatch },
{ bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }
) {
commit('setEnterTime')
const { begTime, endTime, answerBegTime, answerEndTime, breakAnswer, backAnswer} = baseConf
const { msgContent } = submitConf
const now = Date.now()
if (now < new Date(begTime).getTime()) {
router.push({ name: 'errorPage' })
commit('setErrorInfo', {
errorType: 'overTime',
errorMsg: `<p>问卷未到开始填写时间,暂时无法进行填写<p/>
<p>开始时间为: ${begTime}</p>`
})
return
} else if (now > new Date(endTime).getTime()) {
router.push({ name: 'errorPage' })
commit('setErrorInfo', {
errorType: 'overTime',
errorMsg: msgContent.msg_9001 || '您来晚了,感谢支持问卷~'
})
return
} else if (answerBegTime && answerEndTime) {
const momentNow = moment()
const todayStr = momentNow.format('yyyy-MM-DD')
const momentStartTime = moment(`${todayStr} ${answerBegTime}`)
const momentEndTime = moment(`${todayStr} ${answerEndTime}`)
if (momentNow.isBefore(momentStartTime) || momentNow.isAfter(momentEndTime)) {
router.push({ name: 'errorPage' })
commit('setErrorInfo', {
errorType: 'overTime',
errorMsg: `<p>不在答题时间范围内,暂时无法进行填写<p/>
<p>答题时间为: ${answerBegTime} ~ ${answerEndTime}</p>`
})
return
}
}
router.push({ name: 'renderPage' })
//回填,断点续填
const localData = JSON.parse(localStorage.getItem(state.surveyPath + "_questionData"))
//数据解密
for(const key in localData){
localData[key] = decodeURIComponent(localData[key])
}
const isSubmit = JSON.parse(localStorage.getItem('isSubmit'))
if(localData) {
if(isSubmit){
if(!backAnswer) {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} else {
confirm({
title: "您之前已提交过问卷,是否要回填?",
onConfirm: async () => {
try {
loadFormData({ commit, dispatch }, {bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }, localData)
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
},
onCancel: async() => {
try {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
}
})
}
} else{
if(!breakAnswer) {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} else {
confirm({
title: "您之前已填写部分内容, 是否要继续填写?",
onConfirm: async () => {
try {
loadFormData({ commit, dispatch }, {bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }, localData)
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
},
onCancel: async() => {
try {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
}
})
}
}
} else {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
}
},
// 用户输入或者选择后,更新表单数据
changeData({ commit }, data) {
commit('changeFormData', data)
},
// 初始化投票题的数据
async initVoteData({ state, commit }) {
const questionData = state.questionData
const surveyPath = state.surveyPath
const fieldList = []
for (const field in questionData) {
const { type } = questionData[field]
if (/vote/.test(type)) {
fieldList.push(field)
}
}
if (fieldList.length <= 0) {
return
}
try {
localStorage.removeItem(VOTE_INFO_KEY)
const voteRes = await queryVote({
surveyPath,
fieldList: fieldList.join(',')
})
if (voteRes.code === 200) {
localStorage.setItem(
VOTE_INFO_KEY,
JSON.stringify({
...voteRes.data
})
)
commit('setVoteMap', voteRes.data)
}
} catch (error) {
console.log(error)
}
},
updateVoteData({ state, commit }, data) {
const { key: questionKey, value: questionVal } = data
// 更新前获取接口缓存在localStorage中的数据
const localData = localStorage.getItem(VOTE_INFO_KEY)
const voteinfo = JSON.parse(localData)
const currentQuestion = state.questionData[questionKey]
const options = currentQuestion.options
const voteTotal = voteinfo?.[questionKey]?.total || 0
let totalPayload = {
questionKey,
voteKey: 'total',
voteValue: voteTotal
}
options.forEach((option) => {
const optionhash = option.hash
const voteCount = voteinfo?.[questionKey]?.[optionhash] || 0
// 如果选中值包含该选项对应voteCount 和 voteTotal + 1
if (
Array.isArray(questionVal) ? questionVal.includes(optionhash) : questionVal === optionhash
) {
const countPayload = {
questionKey,
voteKey: optionhash,
voteValue: voteCount + 1
}
totalPayload.voteValue += 1
commit('updateVoteMapByKey', countPayload)
} else {
const countPayload = {
questionKey,
voteKey: optionhash,
voteValue: voteCount
}
commit('updateVoteMapByKey', countPayload)
}
commit('updateVoteMapByKey', totalPayload)
})
},
async getEncryptInfo({ commit }) {
try {
const res = await getEncryptInfo()
if (res.code === CODE_MAP.SUCCESS) {
commit('setEncryptInfo', res.data)
}
} catch (error) {
console.log(error)
}
},
async initQuotaMap({ state, commit }) {
const questionData = state.questionData
const surveyPath = state.surveyPath
const fieldList = Object.keys(questionData).filter(field => {
if (NORMAL_CHOICES.includes(questionData[field].type)) {
return questionData[field].options.some(option => option.quota > 0)
}
})
// 如果不存在则不请求选项上限接口
if (fieldList.length <= 0) {
return
}
try {
localStorage.removeItem(QUOTA_INFO_KEY)
const quotaRes = await queryVote({
surveyPath,
fieldList: fieldList.join(',')
})
if (quotaRes.code === 200) {
localStorage.setItem(
QUOTA_INFO_KEY,
JSON.stringify({
...quotaRes.data
})
)
Object.keys(quotaRes.data).forEach(field => {
Object.keys(quotaRes.data[field]).forEach((optionHash) => {
commit('updateQuotaMapByKey', { questionKey: field, optionKey: optionHash, data: quotaRes.data[field][optionHash] })
})
})
}
} catch (error) {
console.log(error)
}
},
// 题目选中时更新选项配额
changeQuota({ state, commit }, data) {
const { key: questionKey, value: questionVal } = data
// 更新前获取接口缓存在localStorage中的数据
const localData = localStorage.getItem(QUOTA_INFO_KEY)
const quotaMap = JSON.parse(localData)
// const quotaMap = state.quotaMap
const currentQuestion = state.questionData[questionKey]
const options = currentQuestion.options
options.forEach((option) => {
const optionhash = option.hash
const selectCount = quotaMap?.[questionKey]?.[optionhash].selectCount || 0
// 如果选中值包含该选项,对应 voteCount 和 voteTotal + 1
if (
Array.isArray(questionVal) ? questionVal.includes(optionhash) : questionVal === optionhash
) {
const countPayload = {
questionKey,
optionKey: optionhash,
selectCount: selectCount + 1
}
commit('updateQuotaMapByKey', countPayload)
} else {
const countPayload = {
questionKey,
optionKey: optionhash,
selectCount: selectCount
}
commit('updateQuotaMapByKey', countPayload)
}
})
}
}
// 加载上次填写过的数据到问卷页
function loadFormData({ commit, dispatch }, {bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }, formData) {
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf
})
console.log("formdata", formData)
for(const key in formData){
formValues[key] = formData[key]
console.log("formValues",formValues)
}
// 将数据设置到state上
commit('assignState', {
questionData,
questionSeq,
rules,
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
formValues
})
// 获取已投票数据
dispatch('initVoteData')
// 获取选项上线选中数据
dispatch('initQuotaMap')
}
// 加载空白页面
function clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }) {
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf
})
// 将数据设置到state上
commit('assignState', {
questionData,
questionSeq,
rules,
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
formValues
})
// 获取已投票数据
dispatch('initVoteData')
// dispatch('initQuotaMap')
}

View File

@ -1,68 +0,0 @@
import { forEach, set } from 'lodash-es'
export default {
// 将数据设置到state上
assignState(state, data) {
forEach(data, (value, key) => {
state[key] = value
})
},
setQuestionData(state, data) {
state.questionData = data
},
setErrorInfo(state, { errorType, errorMsg }) {
state.errorInfo = {
errorType,
errorMsg
}
},
changeFormData(state, data) {
let { key, value } = data
set(state, `formValues.${key}`, value)
//数据加密
var formData = Object.assign({}, state.formValues);
for(const key in formData){
formData[key] = encodeURIComponent(formData[key])
}
//浏览器存储
localStorage.removeItem(state.surveyPath + "_questionData")
localStorage.setItem(state.surveyPath + "_questionData", JSON.stringify(formData))
localStorage.setItem('isSubmit', JSON.stringify(false))
},
changeSelectMoreData(state, data) {
const { key, value, field } = data
set(state, `questionData.${field}.othersValue.${key}`, value)
},
setEnterTime(state) {
state.enterTime = Date.now()
},
setSurveyPath(state, data) {
state.surveyPath = data
},
setVoteMap(state, data) {
state.voteMap = data
},
updateVoteMapByKey(state, data) {
const { questionKey, voteKey, voteValue } = data
// 兼容为空的情况
if (!state.voteMap[questionKey]) {
state.voteMap[questionKey] = {}
}
state.voteMap[questionKey][voteKey] = voteValue
},
setQuestionSeq(state, data) {
state.questionSeq = data
},
setEncryptInfo(state, data) {
state.encryptInfo = data
},
updateQuotaMapByKey(state, { questionKey, optionKey, data }) {
// 兼容为空的情况
if (!state.quotaMap[questionKey]) {
state.quotaMap[questionKey] = {}
}
state.quotaMap[questionKey][optionKey] = data
}
}

View File

@ -1,16 +0,0 @@
import { isMobile } from '../utils/index'
export default {
surveyPath: '',
questionData: null,
isMobile: isMobile(),
errorInfo: {
errorType: '',
errorMsg: ''
},
enterTime: null,
questionSeq: [], // 题目的顺序,因为可能会有分页的情况,所以是一个二维数组[[qid1, qid2], [qid3,qid4]]
voteMap: {},
encryptInfo: null,
quotaMap: {}
}

View File

@ -8,12 +8,6 @@ import { QUESTION_TYPE, NORMAL_CHOICES } from '@/common/typeEnum'
const VOTE_INFO_KEY = 'voteinfo'
const QUOTA_INFO_KEY = 'limitinfo'
import useCommandComponent from '../hooks/useCommandComponent'
import BackAnswerDialog from '../components/BackAnswerDialog.vue'
const confirm = useCommandComponent(BackAnswerDialog)
// 投票进度逻辑聚合
const usevVoteMap = (questionData) => {
const voteMap = ref({})

View File

@ -12,12 +12,16 @@ import moment from 'moment'
// 引入中文
import 'moment/locale/zh-cn'
// 设置中文
moment.locale('zh-cn')
import adapter from '../adapter'
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
// import { jumpLogicRule } from '@/common/logicEngine/jumpLogicRule'
import useCommandComponent from '../hooks/useCommandComponent'
import BackAnswerDialog from '../components/BackAnswerDialog.vue'
const confirm = useCommandComponent(BackAnswerDialog)
moment.locale('zh-cn')
/**
* CODE_MAP不从management引入在dev阶段会导致B端 router被加载进而导致C端路由被添加 baseUrl: /management
*/
@ -158,55 +162,37 @@ export const useSurveyStore = defineStore('survey', () => {
}
// 加载上次填写过的数据到问卷页
function loadFormData({bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }, formData) {
function loadFormData(params, formData) {
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf
bannerConf: params.bannerConf,
baseConf: params.baseConf,
bottomConf: params.bottomConf,
dataConf: params.dataConf,
skinConf: params.skinConf,
submitConf: params.submitConf,
})
for(const key in formData){
formValues[key] = formData[key]
}
// 将数据设置到state上
commit('assignState', {
questionData,
questionSeq,
rules,
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
formValues
})
// 获取已投票数据
dispatch('initVoteData')
// 获取选项上线选中数据
dispatch('initQuotaMap')
// todo: 建议通过questionStore提供setqueationdata方法修改属性否则不好跟踪变化
questionStore.questionData = questionData
questionStore.questionSeq = questionSeq
// 将数据设置到state上
rules.value = rules
bannerConf.value = option.bannerConf
baseConf.value = option.baseConf
bottomConf.value = option.bottomConf
dataConf.value = option.dataConf
skinConf.value = option.skinConf
submitConf.value = option.submitConf
formValues.value = _formValues
bannerConf.value = params.bannerConf
baseConf.value = params.baseConf
bottomConf.value = params.bottomConf
dataConf.value = params.dataConf
skinConf.value = params.skinConf
submitConf.value = params.submitConf
formValues.value = formValues
whiteData.value = option.whiteData
pageConf.value = option.pageConf
whiteData.value = params.whiteData
pageConf.value = params.pageConf
// 获取已投票数据
questionStore.initVoteData()
@ -214,12 +200,15 @@ export const useSurveyStore = defineStore('survey', () => {
}
const initSurvey = (option) => {
setEnterTime()
if (!canFillQuestionnaire(option.baseConf, option.submitConf)) {
return
}
const { breakAnswer } = option.baseConf
const localData = JSON.parse(localStorage.getItem(surveyPath.value + "_questionData"))
for(const key in localData){
localData[key] = decodeURIComponent(localData[key])
@ -244,7 +233,7 @@ export const useSurveyStore = defineStore('survey', () => {
},
onCancel: async() => {
try {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
clearFormData({ bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} catch (error) {
console.log(error)
} finally {
@ -254,7 +243,7 @@ export const useSurveyStore = defineStore('survey', () => {
})
}
} else {
if(!option.baseConf.breakAnswer) {
if(!breakAnswer) {
clearFormData(option)
} else {
confirm({

View File

@ -121,6 +121,10 @@ export default defineConfig({
target: 'http://127.0.0.1:3000',
changeOrigin: true
},
'/exportfile': {
target: 'http://127.0.0.1:3000',
changeOrigin: true
},
// 静态文件的默认存储文件夹
'/userUpload': {
target: 'http://127.0.0.1:3000',