feat: 完善北大课程相关的内容
This commit is contained in:
parent
2162f3cffd
commit
d08f1c71e5
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||
|
||||
# 默认的上传文件夹
|
||||
|
@ -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 {
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"node-cron": "^3.0.3"
|
||||
}
|
||||
}
|
16
server/.env
16
server/.env
@ -1,12 +1,18 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME= # xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL= # mongodb://localhost:27017 # 建议设置强密码
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin
|
||||
|
||||
XIAOJU_SURVEY_REDIS_HOST=
|
||||
XIAOJU_SURVEY_REDIS_PORT=
|
||||
XIAOJU_SURVEY_REDIS_USERNAME=
|
||||
XIAOJU_SURVEY_REDIS_PASSWORD=
|
||||
XIAOJU_SURVEY_REDIS_DB=
|
||||
|
||||
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY= # dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
|
||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||
XIAOJU_SURVEY_JWT_EXPIRES_IN=8h
|
||||
|
||||
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log
|
||||
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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'),
|
||||
});
|
||||
}
|
||||
|
@ -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, // 白名单校验错误
|
||||
|
||||
|
@ -7,6 +7,8 @@ export enum RECORD_STATUS {
|
||||
REMOVED = 'removed', // 删除
|
||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
||||
COMOPUTETING = 'computing', // 计算中
|
||||
FINISHED = 'finished', // 已完成
|
||||
ERROR = 'error', // 错误
|
||||
}
|
||||
|
||||
// 历史类型
|
||||
|
94
server/src/guards/session.guard.ts
Normal file
94
server/src/guards/session.guard.ts
Normal 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('没有权限');
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -60,7 +60,6 @@ export interface DataItem {
|
||||
rangeConfig?: any;
|
||||
starStyle?: string;
|
||||
innerType?: string;
|
||||
deleteRecover?: boolean;
|
||||
quotaNoDisplay?: boolean;
|
||||
}
|
||||
|
||||
|
@ -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' });
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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');
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
34
server/src/models/downloadTask.entity.ts
Normal file
34
server/src/models/downloadTask.entity.ts
Normal 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;
|
||||
}
|
15
server/src/models/session.entity.ts
Normal file
15
server/src/models/session.entity.ts
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ export class SurveyHistory extends BaseEntity {
|
||||
operator: {
|
||||
username: string;
|
||||
_id: string;
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
@Column('string')
|
||||
sessionId: string;
|
||||
}
|
||||
|
@ -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 || '用户凭证检测失败');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
9
server/src/modules/redis/redis.module.ts
Normal file
9
server/src/modules/redis/redis.module.ts
Normal 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 {}
|
32
server/src/modules/redis/redis.service.ts
Normal file
32
server/src/modules/redis/redis.service.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
187
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
187
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
84
server/src/modules/survey/controllers/session.controller.ts
Normal file
84
server/src/modules/survey/controllers/session.controller.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
51
server/src/modules/survey/dto/downloadTask.dto.ts
Normal file
51
server/src/modules/survey/dto/downloadTask.dto.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 }) {
|
||||
|
282
server/src/modules/survey/services/downloadTask.service.ts
Normal file
282
server/src/modules/survey/services/downloadTask.service.ts
Normal 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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
79
server/src/modules/survey/services/session.service.ts
Normal file
79
server/src/modules/survey/services/session.service.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
@ -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: '删除成功',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -62,7 +62,6 @@
|
||||
"quota": "0"
|
||||
}
|
||||
],
|
||||
"deleteRecover": false,
|
||||
"quotaNoDisplay": false
|
||||
}
|
||||
]
|
||||
|
@ -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),
|
||||
);
|
||||
});
|
||||
|
@ -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,
|
||||
) {}
|
||||
|
@ -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 || [],
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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(
|
||||
`参数错误: 请联系管理员`,
|
||||
|
@ -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">
|
||||
|
@ -16,12 +16,4 @@ export const getStatisticList = (data) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
//问卷下载
|
||||
export const downloadSurvey = ({ surveyId, isDesensitive }) => {
|
||||
return axios.get('/survey/surveyDownload/download', {
|
||||
params: {
|
||||
surveyId,
|
||||
isDesensitive
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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')
|
||||
}
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
29
web/src/management/api/downloadTask.js
Normal file
29
web/src/management/api/downloadTask.js
Normal 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,
|
||||
})
|
||||
}
|
@ -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 })
|
||||
}
|
@ -77,6 +77,5 @@ export const defaultQuestionConfig = {
|
||||
value: 500
|
||||
}
|
||||
},
|
||||
deleteRecover: false,
|
||||
quotaNoDisplay: false
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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% */
|
||||
}
|
||||
}
|
@ -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>
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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: {}
|
||||
}
|
@ -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: ''
|
||||
}
|
@ -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
|
||||
}
|
||||
})
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 }
|
||||
|
@ -145,7 +145,7 @@ const handleChange = (data) => {
|
||||
}
|
||||
// 处理选项配额
|
||||
if (props.moduleConfig.type === NORMAL_CHOICES) {
|
||||
store.dispatch('changeQuota', data)
|
||||
questionStore.updateQuotaData(data)
|
||||
}
|
||||
// 断点续答的的数据缓存
|
||||
localStorageBack()
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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: {}
|
||||
}
|
@ -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({})
|
||||
|
@ -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({
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user