[Feature]: 升级状态相关功能 (#438)

* fix: 解决readme的冲突

* feat: 修改补充单测 (#437)
This commit is contained in:
luch 2024-09-30 19:18:55 +08:00 committed by GitHub
parent 2bc11c6dfe
commit 0ce78b62a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
125 changed files with 2500 additions and 1214 deletions

22
DATA_COLLECTION.md Normal file
View File

@ -0,0 +1,22 @@
# Important Disclosure reXIAOJUSURVEY Data Collection
XIAOJUSURVEY is open-source software developed and maintained by XIAOJUSURVEY Team and available at https://github.com/didi/xiaoju-survey.
We hereby state the purpose and reason for collecting data.
## Purpose of data collection
Data collected is used to help improve XIAOJUSURVEY for all users. It is important that our team understands the usage patterns as soon as possible, so we can best decide how to design future features and prioritize current work.
## Types of data collected
XIAOJUSURVEY just collects data about version's information. The data collected is subsequently reported to the XIAOJUSURVEY's backend services.
All data collected will be used exclusively by the XIAOJUSURVEY team for analytical purposes only. The data will be neither accessible nor sold to any third party.
## Sensitive data
XIAOJUSURVEY will never collect and/or report sensitive information, such as private keys, API keys, or passwords.
## How do I opt-in to or opt-out of data sharing?
See [docs](https://xiaojusurvey.didi.cn/docs/next/community/%E6%95%B0%E6%8D%AE%E4%B8%8A%E6%8A%A5%E5%A3%B0%E6%98%8E) for information on configuring this functionality.

View File

@ -222,4 +222,4 @@ npm run serve
## CHANGELOG ## CHANGELOG
关注重大变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48) 关注重大项目变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)

View File

@ -15,7 +15,7 @@ services:
- xiaoju-survey - xiaoju-survey
xiaoju-survey: xiaoju-survey:
image: "xiaojusurvey/xiaoju-survey:1.1.6-slim" # 最新版本https://hub.docker.com/r/xiaojusurvey/xiaoju-survey/tags image: "xiaojusurvey/xiaoju-survey:1.2.0-slim" # 最新版本https://hub.docker.com/r/xiaojusurvey/xiaoju-survey/tags
container_name: xiaoju-survey container_name: xiaoju-survey
restart: always restart: always
ports: ports:

View File

@ -1,5 +1,5 @@
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
XIAOJU_SURVEY_MONGO_URL=mongodb://127.0.0.1:27017 XIAOJU_SURVEY_MONGO_URL=
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
XIAOJU_SURVEY_REDIS_HOST= XIAOJU_SURVEY_REDIS_HOST=

View File

@ -0,0 +1,20 @@
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
XIAOJU_SURVEY_MONGO_URL=mongodb://127.0.0.1:27017
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
XIAOJU_SURVEY_REDIS_HOST=
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_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_REPORT=true

View File

@ -1,7 +1,7 @@
{ {
"name": "server", "name": "xiaoju-survey-server",
"version": "0.0.1", "version": "1.3.0",
"description": "", "description": "XIAOJUSURVEY的server端",
"author": "", "author": "",
"scripts": { "scripts": {
"build": "nest build", "build": "nest build",
@ -22,6 +22,7 @@
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1", "@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/microservices": "^10.4.4",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@nestjs/serve-static": "^4.0.0", "@nestjs/serve-static": "^4.0.0",
"@nestjs/swagger": "^7.3.0", "@nestjs/swagger": "^7.3.0",
@ -75,7 +76,7 @@
"prettier": "^3.0.0", "prettier": "^3.0.0",
"redis-memory-server": "^0.11.0", "redis-memory-server": "^0.11.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^6.3.3", "supertest": "^7.0.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"ts-loader": "^9.4.3", "ts-loader": "^9.4.3",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
@ -94,7 +95,9 @@
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
}, },
"collectCoverageFrom": [ "collectCoverageFrom": [
"**/*.(t|j)s" "**/*.(t|j)s",
"!**/*.module.ts",
"!**/upgrade.*.ts"
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node", "testEnvironment": "node",

View File

@ -0,0 +1,62 @@
import fs, { promises as fsa } from 'fs-extra';
import path from 'path';
import fetch from 'node-fetch';
interface PackageJson {
type?: string;
name?: string;
version?: string;
description?: string;
id?: string;
msg?: string;
}
const getId = () => {
const id = new Date().getTime().toString();
process.env.XIAOJU_SURVEY_REPORT_ID = id;
return id;
};
const readData = async (directory: string): Promise<PackageJson | null> => {
const packageJsonPath = path.join(directory, 'package.json');
const id = process.env.XIAOJU_SURVEY_REPORT_ID || getId();
try {
if (!fs.existsSync(directory)) {
return {
type: 'server',
name: '',
version: '',
description: '',
id,
msg: '文件不存在',
};
}
const data = await fsa.readFile(packageJsonPath, 'utf8').catch((e) => e);
const { name, version, description } = JSON.parse(data) as PackageJson;
return { type: 'server', name, version, description, id };
} catch (error) {
return error;
}
};
(async (): Promise<void> => {
if (
process.env.NODE_ENV === 'development' &&
!process.env.XIAOJU_SURVEY_REPORT
) {
return;
}
const res = await readData(path.join(process.cwd()));
// 上报
fetch('https://xiaojusurveysrc.didi.cn/reportSourceData', {
method: 'POST',
headers: {
Accept: 'application/json, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify(res),
}).catch(() => {});
})();

View File

@ -14,6 +14,7 @@ import { AuthModule } from './modules/auth/auth.module';
import { MessageModule } from './modules/message/message.module'; import { MessageModule } from './modules/message/message.module';
import { FileModule } from './modules/file/file.module'; import { FileModule } from './modules/file/file.module';
import { WorkspaceModule } from './modules/workspace/workspace.module'; import { WorkspaceModule } from './modules/workspace/workspace.module';
import { UpgradeModule } from './modules/upgrade/upgrade.module';
import { join } from 'path'; import { join } from 'path';
@ -35,18 +36,21 @@ import { MessagePushingLog } from './models/messagePushingLog.entity';
import { WorkspaceMember } from './models/workspaceMember.entity'; import { WorkspaceMember } from './models/workspaceMember.entity';
import { Workspace } from './models/workspace.entity'; import { Workspace } from './models/workspace.entity';
import { Collaborator } from './models/collaborator.entity'; import { Collaborator } from './models/collaborator.entity';
import { DownloadTask } from './models/downloadTask.entity';
import { Session } from './models/session.entity';
import { LoggerProvider } from './logger/logger.provider'; import { LoggerProvider } from './logger/logger.provider';
import { PluginManagerProvider } from './securityPlugin/pluginManager.provider'; import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
import { LogRequestMiddleware } from './middlewares/logRequest.middleware'; import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
import { PluginManager } from './securityPlugin/pluginManager'; import { PluginManager } from './securityPlugin/pluginManager';
import { Logger } from './logger'; import { Logger } from './logger';
import { DownloadTask } from './models/downloadTask.entity';
import { Session } from './models/session.entity';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({}), ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV}`, // 根据 NODE_ENV 动态加载对应的 .env 文件
isGlobal: true, // 使配置模块在应用的任何地方可用
}),
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
@ -104,6 +108,7 @@ import { Session } from './models/session.entity';
MessageModule, MessageModule,
FileModule, FileModule,
WorkspaceModule, WorkspaceModule,
UpgradeModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [

View File

@ -0,0 +1,6 @@
export enum DOWNLOAD_TASK_STATUS {
WAITING = 'waiting', // 排队中
COMPUTING = 'computing', // 计算中
SUCCEED = 'succeed', // 导出成功
FAILED = 'failed', // 导出失败
}

View File

@ -21,6 +21,7 @@ export enum EXCEPTION_CODE {
RESPONSE_OVER_LIMIT = 9003, // 超出限制 RESPONSE_OVER_LIMIT = 9003, // 超出限制
RESPONSE_SCHEMA_REMOVED = 9004, // 问卷已删除 RESPONSE_SCHEMA_REMOVED = 9004, // 问卷已删除
RESPONSE_DATA_DECRYPT_ERROR = 9005, // 问卷已删除 RESPONSE_DATA_DECRYPT_ERROR = 9005, // 问卷已删除
RESPONSE_PAUSING = 9006, // 问卷已暂停
UPLOAD_FILE_ERROR = 5001, // 上传文件错误 UPLOAD_FILE_ERROR = 5001, // 上传文件错误
} }

View File

@ -1,14 +1,15 @@
// 状态类型 // 状态类型
export enum RECORD_STATUS { export enum RECORD_STATUS {
NEW = 'new', // 新建 NEW = 'new', // 新建 | 未发布
EDITING = 'editing', // 编辑
PAUSING = 'pausing', // 暂停
PUBLISHED = 'published', // 发布 PUBLISHED = 'published', // 发布
REMOVED = 'removed', // 删除 EDITING = 'editing', // 编辑
FORCE_REMOVED = 'forceRemoved', // 从回收站删除 FINISHED = 'finished', // 已结束
COMPUTING = 'computing', // 计算中 REMOVED = 'removed',
FINISHED = 'finished', // 已完成 }
ERROR = 'error', // 错误
export const enum RECORD_SUB_STATUS {
DEFAULT = '', // 默认
PAUSING = 'pausing', // 暂停
} }
// 历史类型 // 历史类型

View File

@ -0,0 +1,4 @@
export enum SESSION_STATUS {
ACTIVATED = 'activated',
DEACTIVATED = 'deactivated',
}

View File

@ -24,6 +24,7 @@ export class SessionGuard implements CanActivate {
} }
const sessionInfo = await this.sessionService.findOne(sessionId); const sessionInfo = await this.sessionService.findOne(sessionId);
request.sessionInfo = sessionInfo; request.sessionInfo = sessionInfo;
request.surveyId = sessionInfo.surveyId;
return true; return true;
} }
} }

View File

@ -112,7 +112,7 @@ export enum MemberType {
} }
export interface BaseConf { export interface BaseConf {
begTime: string; beginTime: string;
endTime: string; endTime: string;
answerBegTime: string; answerBegTime: string;
answerEndTime: string; answerEndTime: string;

View File

@ -1,12 +1,15 @@
import * as log4js from 'log4js'; import * as log4js from 'log4js';
import moment from 'moment'; import moment from 'moment';
import { Injectable, Scope } from '@nestjs/common'; import { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT, RequestContext } from '@nestjs/microservices';
const log4jsLogger = log4js.getLogger(); const log4jsLogger = log4js.getLogger();
@Injectable({ scope: Scope.REQUEST }) @Injectable({ scope: Scope.REQUEST })
export class Logger { export class Logger {
private static inited = false; private static inited = false;
private traceId: string;
constructor(@Inject(CONTEXT) private readonly ctx: RequestContext) {}
static init(config: { filename: string }) { static init(config: { filename: string }) {
if (Logger.inited) { if (Logger.inited) {
@ -37,16 +40,14 @@ export class Logger {
const datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS'); const datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS');
const level = options?.level; const level = options?.level;
const dltag = options?.dltag ? `${options.dltag}||` : ''; const dltag = options?.dltag ? `${options.dltag}||` : '';
const traceIdStr = this.traceId ? `traceid=${this.traceId}||` : ''; const traceIdStr = this.ctx?.['traceId']
? `traceid=${this.ctx?.['traceId']}||`
: '';
return log4jsLogger[level]( return log4jsLogger[level](
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`, `[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,
); );
} }
setTraceId(traceId: string) {
this.traceId = traceId;
}
info(message, options?: { dltag?: string }) { info(message, options?: { dltag?: string }) {
return this._log(message, { ...options, level: 'info' }); return this._log(message, { ...options, level: 'info' });
} }

View File

@ -20,7 +20,9 @@ export const genTraceId = ({ ip }) => {
} else { } else {
ipArr = ip ipArr = ip
.split('.') .split('.')
.map((item) => parseInt(item).toString(16).padStart(2, '0')); .map((item) =>
item ? parseInt(item).toString(16).padStart(2, '0') : '',
);
} }
return `${ipArr.join('')}${Date.now().toString()}${getCountStr()}${process.pid.toString().slice(-5)}`; return `${ipArr.join('')}${Date.now().toString()}${getCountStr()}${process.pid.toString().slice(-5)}`;

View File

@ -1,6 +1,7 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import 'scripts/run-report';
async function bootstrap() { async function bootstrap() {
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;

View File

@ -12,7 +12,7 @@ export class LogRequestMiddleware implements NestMiddleware {
const userAgent = req.get('user-agent') || ''; const userAgent = req.get('user-agent') || '';
const startTime = Date.now(); const startTime = Date.now();
const traceId = genTraceId({ ip }); const traceId = genTraceId({ ip });
this.logger.setTraceId(traceId); req['traceId'] = traceId;
const query = JSON.stringify(req.query); const query = JSON.stringify(req.query);
const body = JSON.stringify(req.body); const body = JSON.stringify(req.body);
this.logger.info( this.logger.info(

View File

@ -1,30 +0,0 @@
import { BaseEntity } from '../base.entity';
import { RECORD_STATUS } from 'src/enums';
describe('BaseEntity', () => {
let baseEntity: BaseEntity;
beforeEach(() => {
baseEntity = new BaseEntity();
});
it('should initialize default info before insert', () => {
const now = Date.now();
baseEntity.initDefaultInfo();
expect(baseEntity.curStatus.status).toBe(RECORD_STATUS.NEW);
expect(baseEntity.curStatus.date).toBeCloseTo(now, -3);
expect(baseEntity.statusList).toHaveLength(1);
expect(baseEntity.statusList[0].status).toBe(RECORD_STATUS.NEW);
expect(baseEntity.statusList[0].date).toBeCloseTo(now, -3);
expect(baseEntity.createDate).toBeCloseTo(now, -3);
expect(baseEntity.updateDate).toBeCloseTo(now, -3);
});
it('should update updateDate before update', () => {
const now = Date.now();
baseEntity.onUpdate();
expect(baseEntity.updateDate).toBeCloseTo(now, -3);
});
});

View File

@ -0,0 +1,57 @@
import { SurveyMeta } from '../surveyMeta.entity';
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
// 模拟日期
const mockDateNow = Date.now();
describe('SurveyMeta Entity', () => {
let surveyMeta: SurveyMeta;
// 在每个测试之前,初始化 SurveyMeta 实例
beforeEach(() => {
surveyMeta = new SurveyMeta();
// 模拟 Date.now() 返回固定的时间
jest.spyOn(Date, 'now').mockReturnValue(mockDateNow);
});
afterEach(() => {
jest.restoreAllMocks(); // 每次测试后还原所有 mock
});
it('should set default curStatus and subStatus on insert when they are not provided', () => {
surveyMeta.initDefaultInfo();
// 验证 curStatus 是否被初始化为默认值
expect(surveyMeta.curStatus).toEqual({
status: RECORD_STATUS.NEW,
date: mockDateNow,
});
// 验证 statusList 是否包含 curStatus
expect(surveyMeta.statusList).toEqual([
{
status: RECORD_STATUS.NEW,
date: mockDateNow,
},
]);
// 验证 subStatus 是否被初始化为默认值
expect(surveyMeta.subStatus).toEqual({
status: RECORD_SUB_STATUS.DEFAULT,
date: mockDateNow,
});
});
it('should initialize statusList if curStatus is provided but statusList is empty', () => {
surveyMeta.curStatus = null;
surveyMeta.initDefaultInfo();
expect(surveyMeta.statusList).toEqual([
{
status: RECORD_STATUS.NEW,
date: expect.any(Number),
},
]);
});
});

View File

@ -1,43 +1,13 @@
import { Column, ObjectIdColumn, BeforeInsert, BeforeUpdate } from 'typeorm'; import { ObjectIdColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from '../enums';
export class BaseEntity { export class BaseEntity {
@ObjectIdColumn() @ObjectIdColumn()
_id: ObjectId; _id: ObjectId;
@Column() @CreateDateColumn({ type: 'timestamp', precision: 3 })
curStatus: { createdAt: Date;
status: RECORD_STATUS;
date: number;
};
@Column() @UpdateDateColumn({ type: 'timestamp', precision: 3 })
statusList: Array<{ updatedAt: Date;
status: RECORD_STATUS;
date: number;
}>;
@Column()
createDate: number;
@Column()
updateDate: number;
@BeforeInsert()
initDefaultInfo() {
const now = Date.now();
if (!this.curStatus) {
const curStatus = { status: RECORD_STATUS.NEW, date: now };
this.curStatus = curStatus;
this.statusList = [curStatus];
}
this.createDate = now;
this.updateDate = now;
}
@BeforeUpdate()
onUpdate() {
this.updateDate = Date.now();
}
} }

View File

@ -11,4 +11,16 @@ export class Collaborator extends BaseEntity {
@Column('jsonb') @Column('jsonb')
permissions: Array<string>; permissions: Array<string>;
@Column()
creator: string;
@Column()
creatorId: string;
@Column()
operator: string;
@Column()
operatorId: string;
} }

View File

@ -1,5 +1,6 @@
import { Entity, Column } from 'typeorm'; import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity'; import { BaseEntity } from './base.entity';
import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus';
@Entity({ name: 'downloadTask' }) @Entity({ name: 'downloadTask' })
export class DownloadTask extends BaseEntity { export class DownloadTask extends BaseEntity {
@ -35,4 +36,13 @@ export class DownloadTask extends BaseEntity {
@Column() @Column()
params: string; params: string;
@Column()
isDeleted: boolean;
@Column()
deletedAt: Date;
@Column()
status: DOWNLOAD_TASK_STATUS;
} }

View File

@ -27,4 +27,16 @@ export class MessagePushingTask extends BaseEntity {
@Column() @Column()
ownerId: string; ownerId: string;
@Column()
isDeleted: boolean;
@Column()
deletedAt: Date;
@Column()
operator: string;
@Column()
operatorId: string;
} }

View File

@ -1,6 +1,7 @@
import { Entity, Column } from 'typeorm'; import { Entity, Column } from 'typeorm';
import { SurveySchemaInterface } from '../interfaces/survey'; import { SurveySchemaInterface } from '../interfaces/survey';
import { BaseEntity } from './base.entity'; import { BaseEntity } from './base.entity';
import { RECORD_STATUS, RECORD_SUB_STATUS } from '../enums';
@Entity({ name: 'surveyPublish' }) @Entity({ name: 'surveyPublish' })
export class ResponseSchema extends BaseEntity { export class ResponseSchema extends BaseEntity {
@ -15,4 +16,19 @@ export class ResponseSchema extends BaseEntity {
@Column() @Column()
pageId: string; pageId: string;
@Column()
curStatus: {
status: RECORD_STATUS;
date: number;
};
@Column()
subStatus: {
status: RECORD_SUB_STATUS;
date: number;
};
@Column()
isDeleted: boolean;
} }

View File

@ -1,6 +1,7 @@
import { Entity, Column, Index, ObjectIdColumn } from 'typeorm'; import { Entity, Column, Index, ObjectIdColumn } from 'typeorm';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { BaseEntity } from './base.entity'; import { BaseEntity } from './base.entity';
import { SESSION_STATUS } from 'src/enums/surveySessionStatus';
@Entity({ name: 'session' }) @Entity({ name: 'session' })
export class Session extends BaseEntity { export class Session extends BaseEntity {
@ -15,4 +16,7 @@ export class Session extends BaseEntity {
@Column() @Column()
userId: string; userId: string;
@Column()
status: SESSION_STATUS;
} }

View File

@ -19,4 +19,7 @@ export class SurveyHistory extends BaseEntity {
username: string; username: string;
_id: string; _id: string;
}; };
@Column('string')
sessionId: string;
} }

View File

@ -1,5 +1,6 @@
import { Entity, Column } from 'typeorm'; import { Entity, Column, BeforeInsert } from 'typeorm';
import { BaseEntity } from './base.entity'; import { BaseEntity } from './base.entity';
import { RECORD_STATUS, RECORD_SUB_STATUS } from '../enums';
@Entity({ name: 'surveyMeta' }) @Entity({ name: 'surveyMeta' })
export class SurveyMeta extends BaseEntity { export class SurveyMeta extends BaseEntity {
@ -18,6 +19,9 @@ export class SurveyMeta extends BaseEntity {
@Column() @Column()
creator: string; creator: string;
@Column()
creatorId: string;
@Column() @Column()
owner: string; owner: string;
@ -32,4 +36,48 @@ export class SurveyMeta extends BaseEntity {
@Column() @Column()
workspaceId: string; workspaceId: string;
@Column()
curStatus: {
status: RECORD_STATUS;
date: number;
};
@Column()
subStatus: {
status: RECORD_SUB_STATUS;
date: number;
};
@Column()
statusList: Array<{
status: RECORD_STATUS | RECORD_SUB_STATUS;
date: number;
}>;
@Column()
operator: string;
@Column()
operatorId: string;
@Column()
isDeleted: boolean;
@Column()
deletedAt: Date;
@BeforeInsert()
initDefaultInfo() {
const now = Date.now();
if (!this.curStatus) {
const curStatus = { status: RECORD_STATUS.NEW, date: now };
this.curStatus = curStatus;
this.statusList = [curStatus];
}
if (!this.subStatus) {
const subStatus = { status: RECORD_SUB_STATUS.DEFAULT, date: now };
this.subStatus = subStatus;
}
}
} }

View File

@ -3,12 +3,33 @@ import { BaseEntity } from './base.entity';
@Entity({ name: 'workspace' }) @Entity({ name: 'workspace' })
export class Workspace extends BaseEntity { export class Workspace extends BaseEntity {
@Column()
creatorId: string;
@Column()
creator: string;
@Column() @Column()
ownerId: string; ownerId: string;
@Column()
owner: string;
@Column() @Column()
name: string; name: string;
@Column() @Column()
description: string; description: string;
@Column()
operator: string;
@Column()
operatorId: string;
@Column()
isDeleted: boolean;
@Column()
deletedAt: Date;
} }

View File

@ -11,4 +11,16 @@ export class WorkspaceMember extends BaseEntity {
@Column() @Column()
role: string; role: string;
@Column()
creator: string;
@Column()
creatorId: string;
@Column()
operator: string;
@Column()
operatorId: string;
} }

View File

@ -5,7 +5,6 @@ import { UserService } from '../services/user.service';
import { User } from 'src/models/user.entity'; import { User } from 'src/models/user.entity';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { hash256 } from 'src/utils/hash256'; import { hash256 } from 'src/utils/hash256';
import { RECORD_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
describe('UserService', () => { describe('UserService', () => {
@ -145,7 +144,6 @@ describe('UserService', () => {
expect(userRepository.findOne).toHaveBeenCalledWith({ expect(userRepository.findOne).toHaveBeenCalledWith({
where: { where: {
username: username, username: username,
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
}, },
}); });
expect(user).toEqual(userInfo); expect(user).toEqual(userInfo);
@ -163,7 +161,6 @@ describe('UserService', () => {
expect(findOneSpy).toHaveBeenCalledWith({ expect(findOneSpy).toHaveBeenCalledWith({
where: { where: {
username: username, username: username,
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
}, },
}); });
expect(user).toBe(null); expect(user).toBe(null);
@ -184,7 +181,6 @@ describe('UserService', () => {
expect(userRepository.findOne).toHaveBeenCalledWith({ expect(userRepository.findOne).toHaveBeenCalledWith({
where: { where: {
_id: new ObjectId(id), _id: new ObjectId(id),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
}, },
}); });
expect(user).toEqual(userInfo); expect(user).toEqual(userInfo);
@ -202,7 +198,6 @@ describe('UserService', () => {
expect(findOneSpy).toHaveBeenCalledWith({ expect(findOneSpy).toHaveBeenCalledWith({
where: { where: {
_id: new ObjectId(id), _id: new ObjectId(id),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
}, },
}); });
expect(user).toBe(null); expect(user).toBe(null);
@ -211,8 +206,8 @@ describe('UserService', () => {
it('should return a list of users by username', async () => { it('should return a list of users by username', async () => {
const username = 'test'; const username = 'test';
const userList = [ const userList = [
{ _id: new ObjectId(), username: 'testUser1', createDate: new Date() }, { _id: new ObjectId(), username: 'testUser1', createdAt: new Date() },
{ _id: new ObjectId(), username: 'testUser2', createDate: new Date() }, { _id: new ObjectId(), username: 'testUser2', createdAt: new Date() },
]; ];
jest jest
@ -228,11 +223,10 @@ describe('UserService', () => {
expect(userRepository.find).toHaveBeenCalledWith({ expect(userRepository.find).toHaveBeenCalledWith({
where: { where: {
username: new RegExp(username), username: new RegExp(username),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
}, },
skip: 0, skip: 0,
take: 10, take: 10,
select: ['_id', 'username', 'createDate'], select: ['_id', 'username', 'createdAt'],
}); });
expect(result).toEqual(userList); expect(result).toEqual(userList);
}); });
@ -243,12 +237,12 @@ describe('UserService', () => {
{ {
_id: new ObjectId(idList[0]), _id: new ObjectId(idList[0]),
username: 'testUser1', username: 'testUser1',
createDate: new Date(), createdAt: new Date(),
}, },
{ {
_id: new ObjectId(idList[1]), _id: new ObjectId(idList[1]),
username: 'testUser2', username: 'testUser2',
createDate: new Date(), createdAt: new Date(),
}, },
]; ];
@ -263,9 +257,8 @@ describe('UserService', () => {
_id: { _id: {
$in: idList.map((id) => new ObjectId(id)), $in: idList.map((id) => new ObjectId(id)),
}, },
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
}, },
select: ['_id', 'username', 'createDate'], select: ['_id', 'username', 'createdAt'],
}); });
expect(result).toEqual(userList); expect(result).toEqual(userList);
}); });

View File

@ -5,7 +5,6 @@ import { User } from 'src/models/user.entity';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { hash256 } from 'src/utils/hash256'; import { hash256 } from 'src/utils/hash256';
import { RECORD_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
@Injectable() @Injectable()
@ -53,9 +52,6 @@ export class UserService {
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { where: {
username: username, username: username,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}, },
}); });
@ -66,9 +62,6 @@ export class UserService {
const user = await this.userRepository.findOne({ const user = await this.userRepository.findOne({
where: { where: {
_id: new ObjectId(id), _id: new ObjectId(id),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}, },
}); });
@ -79,13 +72,10 @@ export class UserService {
const list = await this.userRepository.find({ const list = await this.userRepository.find({
where: { where: {
username: new RegExp(username), username: new RegExp(username),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}, },
skip, skip,
take, take,
select: ['_id', 'username', 'createDate'], select: ['_id', 'username', 'createdAt'],
}); });
return list; return list;
} }
@ -96,11 +86,8 @@ export class UserService {
_id: { _id: {
$in: idList.map((item) => new ObjectId(item)), $in: idList.map((item) => new ObjectId(item)),
}, },
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
}, },
}, select: ['_id', 'username', 'createdAt'],
select: ['_id', 'username', 'createDate'],
}); });
return list; return list;
} }

View File

@ -8,7 +8,6 @@ import {
MESSAGE_PUSHING_TYPE, MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK, MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing'; } from 'src/enums/messagePushing';
import { RECORD_STATUS } from 'src/enums';
describe('MessagePushingTaskDto', () => { describe('MessagePushingTaskDto', () => {
let dto: MessagePushingTaskDto; let dto: MessagePushingTaskDto;
@ -34,9 +33,9 @@ describe('MessagePushingTaskDto', () => {
}); });
it('should have a type', () => { it('should have a type', () => {
dto.type = MESSAGE_PUSHING_TYPE.HTTP; // Set your desired type here dto.type = MESSAGE_PUSHING_TYPE.HTTP;
expect(dto.type).toBeDefined(); expect(dto.type).toBeDefined();
expect(dto.type).toEqual(MESSAGE_PUSHING_TYPE.HTTP); // Adjust based on your enum expect(dto.type).toEqual(MESSAGE_PUSHING_TYPE.HTTP);
}); });
it('should have a push address', () => { it('should have a push address', () => {
@ -46,13 +45,13 @@ describe('MessagePushingTaskDto', () => {
}); });
it('should have a trigger hook', () => { it('should have a trigger hook', () => {
dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED; // Set your desired hook here dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED;
expect(dto.triggerHook).toBeDefined(); expect(dto.triggerHook).toBeDefined();
expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED); // Adjust based on your enum expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED);
}); });
it('should have an array of surveys', () => { it('should have an array of surveys', () => {
dto.surveys = ['survey1', 'survey2']; // Set your desired surveys here dto.surveys = ['survey1', 'survey2'];
expect(dto.surveys).toBeDefined(); expect(dto.surveys).toBeDefined();
expect(dto.surveys).toEqual(['survey1', 'survey2']); expect(dto.surveys).toEqual(['survey1', 'survey2']);
}); });
@ -62,13 +61,6 @@ describe('MessagePushingTaskDto', () => {
expect(dto.owner).toBeDefined(); expect(dto.owner).toBeDefined();
expect(dto.owner).toBe('test_owner'); expect(dto.owner).toBe('test_owner');
}); });
it('should have current status', () => {
dto.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
expect(dto.curStatus).toBeDefined();
expect(dto.curStatus.status).toEqual(RECORD_STATUS.NEW);
expect(dto.curStatus.date).toBeDefined();
});
}); });
describe('CodeDto', () => { describe('CodeDto', () => {

View File

@ -10,7 +10,6 @@ import { MessagePushingLogService } from '../services/messagePushingLog.service'
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto'; import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto'; import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
import { RECORD_STATUS } from 'src/enums';
import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing'; import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing';
import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing'; import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity'; import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
@ -118,10 +117,12 @@ describe('MessagePushingTaskService', () => {
expect(result).toEqual(tasks); expect(result).toEqual(tasks);
expect(repository.find).toHaveBeenCalledWith({ expect(repository.find).toHaveBeenCalledWith({
where: { where: {
isDeleted: {
$ne: true,
},
ownerId: mockOwnerId, ownerId: mockOwnerId,
surveys: { $all: [surveyId] }, surveys: { $all: [surveyId] },
triggerHook: hook, triggerHook: hook,
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
}, },
}); });
}); });
@ -146,10 +147,19 @@ describe('MessagePushingTaskService', () => {
where: { where: {
ownerId: mockOwnerId, ownerId: mockOwnerId,
_id: new ObjectId(taskId), _id: new ObjectId(taskId),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED }, isDeleted: {
$ne: true,
},
}, },
}); });
}); });
it('should throw an error when message pushing task is not found', async () => {
const taskId = '65afc62904d5db18534c0f78';
jest.spyOn(repository, 'findOne').mockResolvedValue(null); // 模拟未找到任务
const mockOwnerId = '66028642292c50f8b71a9eee';
await expect(service.findOne({ id: taskId, ownerId: mockOwnerId }));
});
}); });
describe('update', () => { describe('update', () => {
@ -161,10 +171,6 @@ describe('MessagePushingTaskService', () => {
pushAddress: 'http://update.example.com', pushAddress: 'http://update.example.com',
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED, triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
surveys: ['new survey id'], surveys: ['new survey id'],
curStatus: {
status: RECORD_STATUS.EDITING,
date: Date.now(),
},
}; };
const existingTask = new MessagePushingTask(); const existingTask = new MessagePushingTask();
existingTask._id = new ObjectId(taskId); existingTask._id = new ObjectId(taskId);
@ -190,6 +196,20 @@ describe('MessagePushingTaskService', () => {
}); });
expect(repository.save).toHaveBeenCalledWith(updatedTask); expect(repository.save).toHaveBeenCalledWith(updatedTask);
}); });
it('should throw an error if the task to be updated is not found', async () => {
const taskId = '65afc62904d5db18534c0f78';
const updateDto: UpdateMessagePushingTaskDto = { name: 'Updated Task' };
jest.spyOn(repository, 'findOne').mockResolvedValue(null); // 模拟任务未找到
const mockOwnerId = '66028642292c50f8b71a9eee';
await expect(
service.update({
ownerId: mockOwnerId,
id: taskId,
updateData: updateDto,
}),
).rejects.toThrow(`Message pushing task with id ${taskId} not found`);
});
}); });
describe('remove', () => { describe('remove', () => {
@ -197,36 +217,52 @@ describe('MessagePushingTaskService', () => {
const taskId = '65afc62904d5db18534c0f78'; const taskId = '65afc62904d5db18534c0f78';
const updateResult = { modifiedCount: 1 }; const updateResult = { modifiedCount: 1 };
const mockOwnerId = '66028642292c50f8b71a9eee'; const mockOperatorId = '66028642292c50f8b71a9eee';
const mockOperator = 'mockOperator';
jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult); jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult);
const result = await service.remove({ const result = await service.remove({
id: taskId, id: taskId,
ownerId: mockOwnerId, operatorId: mockOperatorId,
operator: mockOperator,
}); });
expect(result).toEqual(updateResult); expect(result).toEqual(updateResult);
expect(repository.updateOne).toHaveBeenCalledWith( expect(repository.updateOne).toHaveBeenCalledWith(
{ {
ownerId: mockOwnerId,
_id: new ObjectId(taskId), _id: new ObjectId(taskId),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
}, },
{ {
$set: { $set: {
curStatus: { isDeleted: true,
status: RECORD_STATUS.REMOVED, operatorId: mockOperatorId,
date: expect.any(Number), operator: mockOperator,
}, deletedAt: expect.any(Date),
},
$push: {
statusList: {
status: RECORD_STATUS.REMOVED,
date: expect.any(Number),
}, },
}, },
);
});
it('should throw an error if the task to be removed is not found', async () => {
const taskId = '65afc62904d5db18534c0f78';
jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({ modifiedCount: 0 }); // 模拟删除失败
const mockOperatorId = '66028642292c50f8b71a9eee';
const mockOperator = 'mockOperator';
const result = await service.remove({
id: taskId,
operatorId: mockOperatorId,
operator: mockOperator,
});
expect(result.modifiedCount).toBe(0);
expect(repository.updateOne).toHaveBeenCalledWith(
{
_id: new ObjectId(taskId),
}, },
expect.any(Object),
); );
}); });
}); });
@ -258,7 +294,34 @@ describe('MessagePushingTaskService', () => {
$push: { $push: {
surveys: surveyId, surveys: surveyId,
}, },
$set: {
updatedAt: expect.any(Date),
}, },
},
);
});
it('should not add the surveyId if it already exists in the task', async () => {
const taskId = '65afc62904d5db18534c0f78';
const surveyId = '65af380475b64545e5277dd9';
const mockOwnerId = '66028642292c50f8b71a9eee';
jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({ modifiedCount: 0 }); // 模拟重复添加
const result = await service.surveyAuthorizeTask({
taskId,
surveyId,
ownerId: mockOwnerId,
});
expect(result.modifiedCount).toBe(0);
expect(repository.updateOne).toHaveBeenCalledWith(
{
_id: new ObjectId(taskId),
surveys: { $nin: [surveyId] }, // 确保只有不包含时才插入
ownerId: mockOwnerId,
},
expect.any(Object),
); );
}); });
}); });

View File

@ -37,9 +37,4 @@ describe('UpdateMessagePushingTaskDto', () => {
dto.surveys = null; dto.surveys = null;
expect(dto.surveys).toBeNull(); expect(dto.surveys).toBeNull();
}); });
it('should have a nullable curStatus', () => {
dto.curStatus = null;
expect(dto.curStatus).toBeNull();
});
}); });

View File

@ -150,8 +150,9 @@ export class MessagePushingTaskController {
async remove(@Request() req, @Param('id') id: string) { async remove(@Request() req, @Param('id') id: string) {
const userId = req.user._id; const userId = req.user._id;
const res = await this.messagePushingTaskService.remove({ const res = await this.messagePushingTaskService.remove({
ownerId: userId,
id, id,
operator: req.user.username,
operatorId: userId,
}); });
return { return {
code: 200, code: 200,

View File

@ -4,7 +4,6 @@ import {
MESSAGE_PUSHING_TYPE, MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK, MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing'; } from 'src/enums/messagePushing';
import { RECORD_STATUS } from 'src/enums';
export class MessagePushingTaskDto { export class MessagePushingTaskDto {
@ApiProperty({ description: '任务id' }) @ApiProperty({ description: '任务id' })
@ -27,12 +26,6 @@ export class MessagePushingTaskDto {
@ApiProperty({ description: '所有者' }) @ApiProperty({ description: '所有者' })
owner: string; owner: string;
@ApiProperty({ description: '任务状态', required: false })
curStatus?: {
status: RECORD_STATUS;
date: number;
};
} }
export class CodeDto { export class CodeDto {

View File

@ -1,5 +1,4 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { RECORD_STATUS } from 'src/enums';
import { import {
MESSAGE_PUSHING_TYPE, MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK, MESSAGE_PUSHING_HOOK,
@ -20,10 +19,4 @@ export class UpdateMessagePushingTaskDto {
@ApiProperty({ description: '绑定的问卷id', required: false }) @ApiProperty({ description: '绑定的问卷id', required: false })
surveys?: string[]; surveys?: string[];
@ApiProperty({ description: '任务状态', required: false })
curStatus?: {
status: RECORD_STATUS;
date: number;
};
} }

View File

@ -6,7 +6,6 @@ import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto'; import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto'; import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing'; import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing';
import { MessagePushingLogService } from './messagePushingLog.service'; import { MessagePushingLogService } from './messagePushingLog.service';
import { httpPost } from 'src/utils/request'; import { httpPost } from 'src/utils/request';
@ -44,8 +43,8 @@ export class MessagePushingTaskService {
ownerId?: string; ownerId?: string;
}): Promise<MessagePushingTask[]> { }): Promise<MessagePushingTask[]> {
const where: Record<string, any> = { const where: Record<string, any> = {
'curStatus.status': { isDeleted: {
$ne: RECORD_STATUS.REMOVED, $ne: true,
}, },
}; };
if (surveyId) { if (surveyId) {
@ -64,19 +63,19 @@ export class MessagePushingTaskService {
}); });
} }
async findOne({ findOne({
id, id,
ownerId, ownerId,
}: { }: {
id: string; id: string;
ownerId: string; ownerId: string;
}): Promise<MessagePushingTask> { }): Promise<MessagePushingTask> {
return await this.messagePushingTaskRepository.findOne({ return this.messagePushingTaskRepository.findOne({
where: { where: {
ownerId, ownerId,
_id: new ObjectId(id), _id: new ObjectId(id),
'curStatus.status': { isDeleted: {
$ne: RECORD_STATUS.REMOVED, $ne: true,
}, },
}, },
}); });
@ -104,25 +103,25 @@ export class MessagePushingTaskService {
return await this.messagePushingTaskRepository.save(updatedTask); return await this.messagePushingTaskRepository.save(updatedTask);
} }
async remove({ id, ownerId }: { id: string; ownerId: string }) { async remove({
const curStatus = { id,
status: RECORD_STATUS.REMOVED, operator,
date: Date.now(), operatorId,
}; }: {
id: string;
operator: string;
operatorId: string;
}) {
return this.messagePushingTaskRepository.updateOne( return this.messagePushingTaskRepository.updateOne(
{ {
ownerId,
_id: new ObjectId(id), _id: new ObjectId(id),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}, },
{ {
$set: { $set: {
curStatus, isDeleted: true,
}, operator,
$push: { operatorId,
statusList: curStatus as never, deletedAt: new Date(),
}, },
}, },
); );
@ -147,6 +146,9 @@ export class MessagePushingTaskService {
$push: { $push: {
surveys: surveyId as never, surveys: surveyId as never,
}, },
$set: {
updatedAt: new Date(),
},
}, },
); );
} }

View File

@ -75,6 +75,25 @@ describe('CollaboratorService', () => {
}); });
}); });
describe('deleteCollaborator', () => {
it('should delete a collaborator by userId and surveyId', async () => {
const deleteOneSpy = jest
.spyOn(repository, 'deleteOne')
.mockResolvedValue({ acknowledged: true, deletedCount: 1 });
const result = await service.deleteCollaborator({
userId: '1',
surveyId: '1',
});
expect(deleteOneSpy).toHaveBeenCalledWith({
userId: '1',
surveyId: '1',
});
expect(result).toEqual({ acknowledged: true, deletedCount: 1 });
});
});
describe('batchCreate', () => { describe('batchCreate', () => {
it('should batch create collaborators', async () => { it('should batch create collaborators', async () => {
const insertManySpy = jest const insertManySpy = jest
@ -86,15 +105,193 @@ describe('CollaboratorService', () => {
const result = await service.batchCreate({ const result = await service.batchCreate({
surveyId: '1', surveyId: '1',
collaboratorList: [{ userId: '1', permissions: [] }], collaboratorList: [{ userId: '1', permissions: [] }],
creator: 'testCreator',
creatorId: 'testCreatorId',
}); });
expect(insertManySpy).toHaveBeenCalledWith([ expect(insertManySpy).toHaveBeenCalledWith([
{ surveyId: '1', userId: '1', permissions: [] }, {
userId: '1',
permissions: [],
surveyId: '1',
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
creator: 'testCreator',
creatorId: 'testCreatorId',
},
]); ]);
expect(result).toEqual({ insertedCount: 1 }); expect(result).toEqual({ insertedCount: 1 });
}); });
}); });
describe('changeUserPermission', () => {
it("should update a user's permissions", async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const result = await service.changeUserPermission({
userId: '1',
surveyId: '1',
permission: 'read',
operator: 'testOperator',
operatorId: 'testOperatorId',
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
surveyId: '1',
userId: '1',
},
{
$set: {
permission: 'read',
operator: 'testOperator',
operatorId: 'testOperatorId',
updatedAt: expect.any(Date),
},
},
);
expect(result).toEqual({});
});
});
describe('batchDelete', () => {
it('should batch delete collaborators', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const collaboratorId = new ObjectId().toString();
const result = await service.batchDelete({
surveyId: '1',
idList: [collaboratorId],
});
const expectedQuery = {
surveyId: '1',
$or: [
{
_id: {
$in: [new ObjectId(collaboratorId)],
},
},
],
};
expect(logger.info).toHaveBeenCalledWith(JSON.stringify(expectedQuery));
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual(mockResult);
});
it('should batch delete collaborators by idList and userIdList', async () => {
const collaboratorId = new ObjectId().toString();
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue({ acknowledged: true, deletedCount: 2 });
const result = await service.batchDelete({
idList: [collaboratorId],
userIdList: ['user1', 'user2'],
surveyId: '1',
});
const expectedQuery = {
surveyId: '1',
$or: [
{
userId: {
$in: ['user1', 'user2'],
},
},
{
_id: {
$in: [new ObjectId(collaboratorId)],
},
},
],
};
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual({ acknowledged: true, deletedCount: 2 });
});
it('should handle batch delete with neIdList only', async () => {
const neCollaboratorId = new ObjectId().toString();
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue({ acknowledged: true, deletedCount: 1 });
const result = await service.batchDelete({
neIdList: [neCollaboratorId],
surveyId: '1',
});
const expectedQuery = {
surveyId: '1',
$or: [
{
_id: {
$nin: [new ObjectId(neCollaboratorId)],
},
},
],
};
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual({ acknowledged: true, deletedCount: 1 });
});
});
describe('batchDeleteBySurveyId', () => {
it('should batch delete collaborators by survey id', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const surveyId = new ObjectId().toString();
const result = await service.batchDeleteBySurveyId(surveyId);
expect(deleteManySpy).toHaveBeenCalledWith({
surveyId,
});
expect(result).toEqual(mockResult);
});
});
describe('updateById', () => {
it('should update collaborator by id', async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const collaboratorId = new ObjectId().toString();
const result = await service.updateById({
collaboratorId,
permissions: [],
operator: 'testOperator',
operatorId: 'testOperatorId',
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
_id: new ObjectId(collaboratorId),
},
{
$set: {
permissions: [],
operator: 'testOperator',
operatorId: 'testOperatorId',
updatedAt: expect.any(Date),
},
},
);
expect(result).toEqual({});
});
});
describe('getSurveyCollaboratorList', () => { describe('getSurveyCollaboratorList', () => {
it('should return a list of collaborators for a survey', async () => { it('should return a list of collaborators for a survey', async () => {
const collaboratorId = new ObjectId().toString(); const collaboratorId = new ObjectId().toString();
@ -121,38 +318,6 @@ describe('CollaboratorService', () => {
}); });
}); });
describe('getCollaboratorListByIds', () => {
it('should return a list of collaborators by ids', async () => {
const collaboratorId = new ObjectId().toString();
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
{
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
},
] as Collaborator[]);
const result = await service.getCollaboratorListByIds({
idList: [collaboratorId],
});
expect(findSpy).toHaveBeenCalledWith({
_id: {
$in: [new ObjectId(collaboratorId)],
},
});
expect(result).toEqual([
{
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
},
]);
});
});
describe('getCollaborator', () => { describe('getCollaborator', () => {
it('should return a collaborator', async () => { it('should return a collaborator', async () => {
const collaboratorId = new ObjectId().toString(); const collaboratorId = new ObjectId().toString();
@ -183,127 +348,6 @@ describe('CollaboratorService', () => {
}); });
}); });
describe('changeUserPermission', () => {
it("should update a user's permissions", async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const result = await service.changeUserPermission({
userId: '1',
surveyId: '1',
permission: 'read',
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
surveyId: '1',
userId: '1',
},
{
$set: {
permission: 'read',
},
},
);
expect(result).toEqual({});
});
});
describe('deleteCollaborator', () => {
it('should delete a collaborator', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteOneSpy = jest
.spyOn(repository, 'deleteOne')
.mockResolvedValue(mockResult);
const result = await service.deleteCollaborator({
userId: '1',
surveyId: '1',
});
expect(deleteOneSpy).toHaveBeenCalledWith({
userId: '1',
surveyId: '1',
});
expect(result).toEqual(mockResult);
});
});
describe('batchDelete', () => {
it('should batch delete collaborators', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const collaboratorId = new ObjectId().toString();
const result = await service.batchDelete({
surveyId: '1',
idList: [collaboratorId],
});
const expectedQuery = {
surveyId: '1',
$or: [
{
_id: {
$in: [new ObjectId(collaboratorId)],
},
},
],
};
expect(logger.info).toHaveBeenCalledWith(JSON.stringify(expectedQuery));
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual(mockResult);
});
});
describe('batchDeleteBySurveyId', () => {
it('should batch delete collaborators by survey id', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const surveyId = new ObjectId().toString();
const result = await service.batchDeleteBySurveyId(surveyId);
expect(deleteManySpy).toHaveBeenCalledWith({
surveyId,
});
expect(result).toEqual(mockResult);
});
});
describe('updateById', () => {
it('should update collaborator by id', async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const collaboratorId = new ObjectId().toString();
const result = await service.updateById({
collaboratorId,
permissions: [],
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
_id: new ObjectId(collaboratorId),
},
{
$set: {
permissions: [],
},
},
);
expect(result).toEqual({});
});
});
describe('getCollaboratorListByUserId', () => { describe('getCollaboratorListByUserId', () => {
it('should return a list of collaborators by user id', async () => { it('should return a list of collaborators by user id', async () => {
const userId = new ObjectId().toString(); const userId = new ObjectId().toString();
@ -327,5 +371,48 @@ describe('CollaboratorService', () => {
{ _id: '1', surveyId: '1', userId, permissions: [] }, { _id: '1', surveyId: '1', userId, permissions: [] },
]); ]);
}); });
it('should return a list of collaborators by their IDs', async () => {
const collaboratorId1 = new ObjectId().toString();
const collaboratorId2 = new ObjectId().toString();
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
{
_id: new ObjectId(collaboratorId1),
surveyId: '1',
userId: 'user1',
permissions: [],
},
{
_id: new ObjectId(collaboratorId2),
surveyId: '2',
userId: 'user2',
permissions: [],
},
] as Collaborator[]);
const result = await service.getCollaboratorListByIds({
idList: [collaboratorId1, collaboratorId2],
});
expect(findSpy).toHaveBeenCalledWith({
_id: {
$in: [new ObjectId(collaboratorId1), new ObjectId(collaboratorId2)],
},
});
expect(result).toEqual([
{
_id: new ObjectId(collaboratorId1),
surveyId: '1',
userId: 'user1',
permissions: [],
},
{
_id: new ObjectId(collaboratorId2),
surveyId: '2',
userId: 'user2',
permissions: [],
},
]);
});
}); });
}); });

View File

@ -109,8 +109,8 @@ describe('DataStatisticController', () => {
}, },
], ],
listBody: [ listBody: [
{ diffTime: '0.5', createDate: '2024-02-11' }, { diffTime: '0.5', createdAt: '2024-02-11' },
{ diffTime: '0.5', createDate: '2024-02-11' }, { diffTime: '0.5', createdAt: '2024-02-11' },
], ],
}; };
@ -155,8 +155,8 @@ describe('DataStatisticController', () => {
}, },
], ],
listBody: [ listBody: [
{ diffTime: '0.5', createDate: '2024-02-11', data123: '15200000000' }, { diffTime: '0.5', createdAt: '2024-02-11', data123: '15200000000' },
{ diffTime: '0.5', createDate: '2024-02-11', data123: '13800000000' }, { diffTime: '0.5', createdAt: '2024-02-11', data123: '13800000000' },
], ],
}; };
@ -212,8 +212,8 @@ describe('DataStatisticController', () => {
date: 1717158851823, date: 1717158851823,
}, },
], ],
createDate: 1717158851823, createdAt: 1717158851823,
updateDate: 1717159136025, updatedAt: 1717159136025,
title: '问卷调研', title: '问卷调研',
surveyPath: 'ZdGNzTTR', surveyPath: 'ZdGNzTTR',
code: { code: {
@ -233,7 +233,7 @@ describe('DataStatisticController', () => {
}, },
}, },
baseConf: { baseConf: {
begTime: '2024-05-31 20:31:36', beginTime: '2024-05-31 20:31:36',
endTime: '2034-05-31 20:31:36', endTime: '2034-05-31 20:31:36',
language: 'chinese', language: 'chinese',
showVoteProcess: 'allow', showVoteProcess: 'allow',

View File

@ -151,8 +151,8 @@ describe('DataStatisticService', () => {
date: 1710340863123.0, date: 1710340863123.0,
}, },
], ],
createDate: 1710340863123.0, createdAt: 1710340863123.0,
updateDate: 1710340863123.0, updatedAt: 1710340863123.0,
}, },
] as unknown as Array<SurveyResponse>; ] as unknown as Array<SurveyResponse>;
@ -196,7 +196,7 @@ describe('DataStatisticService', () => {
data413: expect.any(Number), data413: expect.any(Number),
data863: expect.any(String), data863: expect.any(String),
diffTime: expect.any(String), diffTime: expect.any(String),
createDate: expect.any(String), createdAt: expect.any(String),
}), }),
]), ]),
}); });
@ -273,8 +273,8 @@ describe('DataStatisticService', () => {
date: 1710400232161.0, date: 1710400232161.0,
}, },
], ],
createDate: 1710400232161.0, createdAt: 1710400232161.0,
updateDate: 1710400232161.0, updatedAt: 1710400232161.0,
}, },
] as unknown as Array<SurveyResponse>; ] as unknown as Array<SurveyResponse>;
@ -295,7 +295,7 @@ describe('DataStatisticService', () => {
expect(result.listBody).toEqual( expect(result.listBody).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
createDate: expect.any(String), createdAt: expect.any(String),
data405: expect.any(String), data405: expect.any(String),
data450: expect.any(String), data450: expect.any(String),
data458: expect.any(String), data458: expect.any(String),

View File

@ -145,7 +145,7 @@ describe('DownloadTaskController', () => {
filename: 'mockFile.csv', filename: 'mockFile.csv',
url: 'http://mock-url.com', url: 'http://mock-url.com',
fileSize: 1024, fileSize: 1024,
createDate: Date.now(), createdAt: Date.now(),
}, },
], ],
}; };
@ -219,10 +219,13 @@ describe('DownloadTaskController', () => {
describe('deleteFileByName', () => { describe('deleteFileByName', () => {
it('should delete a download task successfully', async () => { it('should delete a download task successfully', async () => {
const mockBody = { taskId: 'mockTaskId' }; const mockBody = { taskId: 'mockTaskId' };
const mockReq = { user: { _id: 'mockUserId' } }; const mockUserId = new ObjectId();
const mockReq = {
user: { _id: mockUserId, username: 'mockUsername' },
};
const mockTaskInfo: any = { const mockTaskInfo: any = {
_id: new ObjectId(), _id: new ObjectId(),
creatorId: 'mockUserId', creatorId: mockUserId.toString(),
}; };
const mockDelRes = { modifiedCount: 1 }; const mockDelRes = { modifiedCount: 1 };
@ -237,6 +240,8 @@ describe('DownloadTaskController', () => {
expect(downloadTaskService.deleteDownloadTask).toHaveBeenCalledWith({ expect(downloadTaskService.deleteDownloadTask).toHaveBeenCalledWith({
taskId: mockBody.taskId, taskId: mockBody.taskId,
operator: mockReq.user.username,
operatorId: mockReq.user._id.toString(),
}); });
expect(result).toEqual({ code: 200, data: true }); expect(result).toEqual({ code: 200, data: true });
}); });

View File

@ -9,7 +9,7 @@ import { DataStatisticService } from '../services/dataStatistic.service';
import { FileService } from 'src/modules/file/services/file.service'; import { FileService } from 'src/modules/file/services/file.service';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums'; import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus';
describe('DownloadTaskService', () => { describe('DownloadTaskService', () => {
let service: DownloadTaskService; let service: DownloadTaskService;
@ -93,6 +93,7 @@ describe('DownloadTaskService', () => {
title: mockParams.responseSchema.title, title: mockParams.responseSchema.title,
}, },
filename: expect.any(String), filename: expect.any(String),
status: DOWNLOAD_TASK_STATUS.WAITING,
}); });
expect(downloadTaskRepository.save).toHaveBeenCalled(); expect(downloadTaskRepository.save).toHaveBeenCalled();
expect(result).toEqual(mockTaskId); expect(result).toEqual(mockTaskId);
@ -118,11 +119,11 @@ describe('DownloadTaskService', () => {
expect(downloadTaskRepository.findAndCount).toHaveBeenCalledWith({ expect(downloadTaskRepository.findAndCount).toHaveBeenCalledWith({
where: { where: {
creatorId: mockCreatorId, creatorId: mockCreatorId,
'curStatus.status': { $ne: RECORD_STATUS.REMOVED }, isDeleted: { $ne: true },
}, },
take: 10, take: 10,
skip: 0, skip: 0,
order: { createDate: -1 }, order: { createdAt: -1 },
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -161,32 +162,84 @@ describe('DownloadTaskService', () => {
}); });
describe('deleteDownloadTask', () => { describe('deleteDownloadTask', () => {
it('should update task status to REMOVED', async () => { it('should mark task as deleted and set deletedAt', async () => {
const mockTaskId = new ObjectId().toString(); const mockTaskId = new ObjectId().toString();
const mockOperator = 'operatorName';
const mockOperatorId = 'operatorId1';
const mockUpdateResult = { matchedCount: 1 }; const mockUpdateResult = { matchedCount: 1 };
jest jest
.spyOn(downloadTaskRepository, 'updateOne') .spyOn(downloadTaskRepository, 'updateOne')
.mockResolvedValue(mockUpdateResult as any); .mockResolvedValue(mockUpdateResult as any);
const result = await service.deleteDownloadTask({ taskId: mockTaskId }); const result = await service.deleteDownloadTask({
taskId: mockTaskId,
operator: mockOperator,
operatorId: mockOperatorId,
});
expect(downloadTaskRepository.updateOne).toHaveBeenCalledWith( expect(downloadTaskRepository.updateOne).toHaveBeenCalledWith(
{ { _id: new ObjectId(mockTaskId) },
_id: new ObjectId(mockTaskId),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
},
{ {
$set: { $set: {
curStatus: { isDeleted: true,
status: RECORD_STATUS.REMOVED, operator: mockOperator,
date: expect.any(Number), operatorId: mockOperatorId,
deletedAt: expect.any(Date),
}, },
}, },
$push: { statusList: expect.any(Object) },
},
); );
expect(result).toEqual(mockUpdateResult); expect(result).toEqual(mockUpdateResult);
}); });
}); });
describe('processDownloadTask', () => {
it('should push task to queue and execute if not executing', async () => {
const mockTaskId = new ObjectId().toString();
jest.spyOn(service, 'executeTask').mockImplementation(jest.fn());
service.processDownloadTask({ taskId: mockTaskId });
expect(DownloadTaskService.taskList).toContain(mockTaskId);
expect(service.executeTask).toHaveBeenCalled();
});
it('should handle already executing case', async () => {
const mockTaskId = new ObjectId().toString();
DownloadTaskService.isExecuting = true;
jest.spyOn(service, 'executeTask').mockImplementation(jest.fn());
service.processDownloadTask({ taskId: mockTaskId });
expect(DownloadTaskService.taskList).toContain(mockTaskId);
expect(service.executeTask).not.toHaveBeenCalled();
});
});
describe('executeTask', () => {
it('should process and execute tasks in queue', async () => {
const mockTaskId = new ObjectId().toString();
DownloadTaskService.taskList.push(mockTaskId);
jest.spyOn(service, 'getDownloadTaskById').mockResolvedValue({
_id: new ObjectId(mockTaskId),
isDeleted: false,
} as any);
jest.spyOn(service, 'handleDownloadTask').mockResolvedValue(undefined);
await service.executeTask();
expect(service.getDownloadTaskById).toHaveBeenCalledWith({
taskId: mockTaskId,
});
expect(service.handleDownloadTask).toHaveBeenCalled();
});
it('should stop executing when queue is empty', async () => {
DownloadTaskService.taskList = [];
await service.executeTask();
expect(DownloadTaskService.isExecuting).toBe(false);
});
});
}); });

View File

@ -14,8 +14,8 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
date: 1710399368439, date: 1710399368439,
}, },
], ],
createDate: 1710399368440, createdAt: 1710399368440,
updateDate: 1710399368440, updatedAt: 1710399368440,
title: '加密全流程', title: '加密全流程',
surveyPath: 'EBzdmnSp', surveyPath: 'EBzdmnSp',
code: { code: {
@ -32,7 +32,7 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
}, },
}, },
baseConf: { baseConf: {
begTime: '2024-03-14 14:54:41', beginTime: '2024-03-14 14:54:41',
endTime: '2034-03-14 14:54:41', endTime: '2034-03-14 14:54:41',
language: 'chinese', language: 'chinese',
tLimit: 0, tLimit: 0,
@ -295,7 +295,7 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
}, },
}, },
pageId: '65f29f3192862d6a9067ad1c', pageId: '65f29f3192862d6a9067ad1c',
} as ResponseSchema; } as unknown as ResponseSchema;
export const mockResponseSchema: ResponseSchema = { export const mockResponseSchema: ResponseSchema = {
_id: new ObjectId('65b0d46e04d5db18534c0f7c'), _id: new ObjectId('65b0d46e04d5db18534c0f7c'),
@ -326,7 +326,7 @@ export const mockResponseSchema: ResponseSchema = {
}, },
}, },
baseConf: { baseConf: {
begTime: '2024-01-23 21:59:05', beginTime: '2024-01-23 21:59:05',
endTime: '2034-01-23 21:59:05', endTime: '2034-01-23 21:59:05',
language: 'chinese', language: 'chinese',
tLimit: 0, tLimit: 0,
@ -654,6 +654,6 @@ export const mockResponseSchema: ResponseSchema = {
}, },
}, },
pageId: '65afc62904d5db18534c0f78', pageId: '65afc62904d5db18534c0f78',
createDate: 1710340841289, createdAt: 1710340841289,
updateDate: 1710340841289.0, updatedAt: 1710340841289.0,
} as ResponseSchema; } as unknown as ResponseSchema;

View File

@ -0,0 +1,144 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MongoRepository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { SessionService } from '../services/session.service';
import { Session } from 'src/models/session.entity';
import { ObjectId } from 'mongodb';
import { SESSION_STATUS } from 'src/enums/surveySessionStatus';
describe('SessionService', () => {
let service: SessionService;
let repository: MongoRepository<Session>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SessionService,
{
provide: getRepositoryToken(Session),
useClass: MongoRepository,
},
],
}).compile();
service = module.get<SessionService>(SessionService);
repository = module.get<MongoRepository<Session>>(
getRepositoryToken(Session),
);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should create and save a new session', async () => {
const mockSession = {
surveyId: 'survey123',
userId: 'user123',
status: SESSION_STATUS.DEACTIVATED,
};
const createdSession: any = { ...mockSession, _id: new ObjectId() };
jest.spyOn(repository, 'create').mockReturnValue(createdSession);
jest.spyOn(repository, 'save').mockResolvedValue(createdSession);
const result = await service.create({
surveyId: mockSession.surveyId,
userId: mockSession.userId,
});
expect(result).toEqual(createdSession);
expect(repository.create).toHaveBeenCalledWith(mockSession);
expect(repository.save).toHaveBeenCalledWith(createdSession);
});
});
describe('findOne', () => {
it('should find a session by id', async () => {
const sessionId = '65afc62904d5db18534c0f78';
const foundSession = {
_id: new ObjectId(sessionId),
surveyId: 'survey123',
userId: 'user123',
status: SESSION_STATUS.ACTIVATED,
};
jest
.spyOn(repository, 'findOne')
.mockResolvedValue(foundSession as Session);
const result = await service.findOne(sessionId);
expect(result).toEqual(foundSession);
expect(repository.findOne).toHaveBeenCalledWith({
where: { _id: new ObjectId(sessionId) },
});
});
});
describe('findLatestEditingOne', () => {
it('should find the latest editing session for a survey', async () => {
const surveyId = 'survey123';
const latestSession = {
_id: new ObjectId(),
surveyId: surveyId,
userId: 'user123',
status: SESSION_STATUS.ACTIVATED,
};
jest
.spyOn(repository, 'findOne')
.mockResolvedValue(latestSession as Session);
const result = await service.findLatestEditingOne({ surveyId });
expect(result).toEqual(latestSession);
expect(repository.findOne).toHaveBeenCalledWith({
where: {
surveyId,
status: SESSION_STATUS.ACTIVATED,
},
});
});
});
describe('updateSessionToEditing', () => {
it('should update a session to editing and deactivate other sessions', async () => {
const sessionId = '65afc62904d5db18534c0f78';
const surveyId = 'survey123';
const updateResult: any = { affected: 1 };
const updateManyResult = { modifiedCount: 1 };
jest.spyOn(repository, 'update').mockResolvedValue(updateResult);
jest.spyOn(repository, 'updateMany').mockResolvedValue(updateManyResult);
const result = await service.updateSessionToEditing({
sessionId,
surveyId,
});
expect(result).toEqual([updateResult, updateManyResult]);
expect(repository.update).toHaveBeenCalledWith(
{ _id: new ObjectId(sessionId) },
{
status: SESSION_STATUS.ACTIVATED,
updatedAt: expect.any(Date),
},
);
expect(repository.updateMany).toHaveBeenCalledWith(
{
surveyId,
_id: { $ne: new ObjectId(sessionId) },
},
{
$set: {
status: SESSION_STATUS.DEACTIVATED,
updatedAt: expect.any(Date),
},
},
);
});
});
});

View File

@ -5,13 +5,12 @@ import { SurveyConfService } from '../services/surveyConf.service';
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
import { ContentSecurityService } from '../services/contentSecurity.service'; import { ContentSecurityService } from '../services/contentSecurity.service';
import { SurveyHistoryService } from '../services/surveyHistory.service'; import { SurveyHistoryService } from '../services/surveyHistory.service';
import { CounterService } from '../../surveyResponse/services/counter.service';
import { SessionService } from '../services/session.service'; import { SessionService } from '../services/session.service';
import { UserService } from '../../auth/services/user.service'; import { UserService } from '../../auth/services/user.service';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { SurveyConf } from 'src/models/surveyConf.entity';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { HttpException } from 'src/exceptions/httpException';
import { Authentication } from 'src/guards/authentication.guard';
jest.mock('../services/surveyMeta.service'); jest.mock('../services/surveyMeta.service');
jest.mock('../services/surveyConf.service'); jest.mock('../services/surveyConf.service');
@ -19,9 +18,7 @@ jest.mock('../../surveyResponse/services/responseScheme.service');
jest.mock('../services/contentSecurity.service'); jest.mock('../services/contentSecurity.service');
jest.mock('../services/surveyHistory.service'); jest.mock('../services/surveyHistory.service');
jest.mock('../services/session.service'); jest.mock('../services/session.service');
jest.mock('../../surveyResponse/services/counter.service');
jest.mock('../../auth/services/user.service'); jest.mock('../../auth/services/user.service');
jest.mock('src/guards/authentication.guard'); jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard'); jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard'); jest.mock('src/guards/workspace.guard');
@ -31,28 +28,17 @@ describe('SurveyController', () => {
let surveyMetaService: SurveyMetaService; let surveyMetaService: SurveyMetaService;
let surveyConfService: SurveyConfService; let surveyConfService: SurveyConfService;
let responseSchemaService: ResponseSchemaService; let responseSchemaService: ResponseSchemaService;
let surveyHistoryService: SurveyHistoryService;
let sessionService: SessionService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [SurveyController], controllers: [SurveyController],
providers: [ providers: [
SurveyMetaService, SurveyMetaService,
{ SurveyConfService,
provide: SurveyConfService,
useValue: {
getSurveyConfBySurveyId: jest.fn(),
getSurveyContentByCode: jest.fn(),
createSurveyConf: jest.fn(),
saveSurveyConf: jest.fn(),
},
},
ResponseSchemaService, ResponseSchemaService,
ContentSecurityService, ContentSecurityService,
SurveyHistoryService, SurveyHistoryService,
SessionService, SessionService,
CounterService,
UserService, UserService,
{ {
provide: Logger, provide: Logger,
@ -61,6 +47,12 @@ describe('SurveyController', () => {
info: jest.fn(), info: jest.fn(),
}, },
}, },
{
provide: Authentication,
useClass: jest.fn().mockImplementation(() => ({
canActivate: () => true,
})),
},
], ],
}).compile(); }).compile();
@ -70,9 +62,6 @@ describe('SurveyController', () => {
responseSchemaService = module.get<ResponseSchemaService>( responseSchemaService = module.get<ResponseSchemaService>(
ResponseSchemaService, ResponseSchemaService,
); );
surveyHistoryService =
module.get<SurveyHistoryService>(SurveyHistoryService);
sessionService = module.get<SessionService>(SessionService);
}); });
describe('getBannerData', () => { describe('getBannerData', () => {
@ -94,12 +83,11 @@ describe('SurveyController', () => {
const newId = new ObjectId(); const newId = new ObjectId();
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({ jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
_id: newId, _id: newId,
} as SurveyMeta); } as any);
jest.spyOn(surveyConfService, 'createSurveyConf').mockResolvedValue({ jest.spyOn(surveyConfService, 'createSurveyConf').mockResolvedValue({
_id: new ObjectId(), _id: new ObjectId(),
pageId: newId.toString(), } as any);
} as SurveyConf);
const result = await controller.createSurvey(surveyInfo, { const result = await controller.createSurvey(surveyInfo, {
user: { username: 'testUser', _id: new ObjectId() }, user: { username: 'testUser', _id: new ObjectId() },
@ -113,13 +101,15 @@ describe('SurveyController', () => {
}); });
}); });
it('should throw an error if validation fails', async () => {
const surveyInfo = {}; // Invalid data
await expect(
controller.createSurvey(surveyInfo as any, { user: {} }),
).rejects.toThrow(HttpException);
});
it('should create a new survey by copy', async () => { it('should create a new survey by copy', async () => {
const existsSurveyId = new ObjectId(); const existsSurveyId = new ObjectId();
const existsSurveyMeta = {
_id: existsSurveyId,
surveyType: 'exam',
owner: 'testUser',
} as SurveyMeta;
const params = { const params = {
surveyType: 'normal', surveyType: 'normal',
remark: '问卷调研', remark: '问卷调研',
@ -128,15 +118,15 @@ describe('SurveyController', () => {
createFrom: existsSurveyId.toString(), createFrom: existsSurveyId.toString(),
}; };
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
_id: new ObjectId(),
} as SurveyMeta);
const request = { const request = {
user: { username: 'testUser', _id: new ObjectId() }, user: { username: 'testUser', _id: new ObjectId() },
surveyMeta: existsSurveyMeta, surveyMeta: { _id: existsSurveyId, surveyType: 'exam' },
}; };
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
_id: new ObjectId(),
} as any);
const result = await controller.createSurvey(params, request); const result = await controller.createSurvey(params, request);
expect(result?.data?.id).toBeDefined(); expect(result?.data?.id).toBeDefined();
}); });
@ -145,69 +135,30 @@ describe('SurveyController', () => {
describe('updateConf', () => { describe('updateConf', () => {
it('should update survey configuration', async () => { it('should update survey configuration', async () => {
const surveyId = new ObjectId(); const surveyId = new ObjectId();
const surveyMeta = {
_id: surveyId,
surveyType: 'exam',
owner: 'testUser',
} as SurveyMeta;
jest
.spyOn(surveyConfService, 'saveSurveyConf')
.mockResolvedValue(undefined);
jest
.spyOn(surveyHistoryService, 'addHistory')
.mockResolvedValue(undefined);
jest
.spyOn(sessionService, 'findLatestEditingOne')
.mockResolvedValue(null);
jest
.spyOn(sessionService, 'updateSessionToEditing')
.mockResolvedValue(undefined);
const reqBody = { const reqBody = {
surveyId: surveyId.toString(), surveyId: surveyId.toString(),
configData: { configData: {
bannerConf: { /* ... your config data here ... */
titleConfig: {},
bannerConfig: {},
},
baseConf: {
begTime: '2024-01-23 21:59:05',
endTime: '2034-01-23 21:59:05',
},
bottomConf: { logoImage: '/imgs/Logo.webp', logoImageWidth: '60%' },
skinConf: {
skinColor: '#4a4c5b',
inputBgColor: '#ffffff',
backgroundConf: {
color: '#fff',
type: 'color',
image: '',
},
themeConf: {
color: '#ffa600',
},
contentConf: {
opacity: 100,
},
},
submitConf: {},
dataConf: {
dataList: [],
},
}, },
sessionId: 'mock-session-id', sessionId: 'mock-session-id',
}; };
const result = await controller.updateConf(reqBody, { const result = await controller.updateConf(reqBody, {
user: { username: 'testUser', _id: 'testUserId' }, user: { username: 'testUser', _id: 'testUserId' },
surveyMeta, surveyMeta: { _id: surveyId },
}); });
expect(result).toEqual({ expect(result).toEqual({
code: 200, code: 200,
}); });
}); });
it('should throw an error if validation fails', async () => {
const reqBody = {}; // Invalid data
await expect(
controller.updateConf(reqBody, { user: {} }),
).rejects.toThrow(HttpException);
});
}); });
describe('deleteSurvey', () => { describe('deleteSurvey', () => {
@ -217,7 +168,7 @@ describe('SurveyController', () => {
_id: surveyId, _id: surveyId,
surveyType: 'exam', surveyType: 'exam',
owner: 'testUser', owner: 'testUser',
} as SurveyMeta; };
jest jest
.spyOn(surveyMetaService, 'deleteSurveyMeta') .spyOn(surveyMetaService, 'deleteSurveyMeta')
@ -227,13 +178,10 @@ describe('SurveyController', () => {
.mockResolvedValue(undefined); .mockResolvedValue(undefined);
const result = await controller.deleteSurvey({ const result = await controller.deleteSurvey({
user: { username: 'testUser' },
surveyMeta, surveyMeta,
user: { username: 'testUser', _id: new ObjectId() },
}); });
expect(result).toEqual({ code: 200 });
expect(result).toEqual({
code: 200,
});
}); });
}); });
@ -244,23 +192,19 @@ describe('SurveyController', () => {
_id: surveyId, _id: surveyId,
surveyType: 'exam', surveyType: 'exam',
owner: 'testUser', owner: 'testUser',
} as SurveyMeta; };
jest jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId') .spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue({ .mockResolvedValue({} as any);
_id: new ObjectId(),
pageId: surveyId.toString(),
} as SurveyConf);
const request = {
user: { username: 'testUser', _id: new ObjectId() },
surveyMeta,
};
const result = await controller.getSurvey( const result = await controller.getSurvey(
{ surveyId: surveyId.toString() }, { surveyId: surveyId.toString() },
request, {
surveyMeta,
user: { username: 'testUser', _id: new ObjectId() },
},
); );
expect(result?.data?.surveyMetaRes).toBeDefined(); expect(result?.data?.surveyMetaRes).toBeDefined();
expect(result?.data?.surveyConfRes).toBeDefined(); expect(result?.data?.surveyConfRes).toBeDefined();
}); });
@ -273,28 +217,77 @@ describe('SurveyController', () => {
_id: surveyId, _id: surveyId,
surveyType: 'exam', surveyType: 'exam',
owner: 'testUser', owner: 'testUser',
} as SurveyMeta; isDeleted: false,
};
jest jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId') .spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue({ .mockResolvedValue({
_id: new ObjectId(),
pageId: surveyId.toString(),
code: {}, code: {},
} as SurveyConf); } as any);
jest jest
.spyOn(surveyConfService, 'getSurveyContentByCode') .spyOn(surveyConfService, 'getSurveyContentByCode')
.mockResolvedValue({ text: '' }); .mockResolvedValue({ text: '' });
jest
.spyOn(surveyMetaService, 'publishSurveyMeta')
.mockResolvedValue(undefined);
const result = await controller.publishSurvey( const result = await controller.publishSurvey(
{ surveyId: surveyId.toString() }, { surveyId: surveyId.toString() },
{ { surveyMeta, user: { username: 'testUser', _id: new ObjectId() } },
user: { username: 'testUser', _id: new ObjectId() },
surveyMeta,
},
); );
expect(result.code).toBe(200); expect(result.code).toBe(200);
}); });
it('should throw an error if the survey is deleted', async () => {
const surveyId = new ObjectId();
const surveyMeta = { _id: surveyId, isDeleted: true };
await expect(
controller.publishSurvey(
{ surveyId: surveyId.toString() },
{ surveyMeta, user: { username: 'testUser' } },
),
).rejects.toThrow(HttpException);
});
});
// New tests for additional methods
describe('pausingSurvey', () => {
it('should pause the survey successfully', async () => {
const surveyMeta = { surveyPath: 'some/path' };
jest
.spyOn(surveyMetaService, 'pausingSurveyMeta')
.mockResolvedValue(undefined);
jest
.spyOn(responseSchemaService, 'pausingResponseSchema')
.mockResolvedValue(undefined);
const result = await controller.pausingSurvey({
surveyMeta,
user: { username: 'testUser' },
});
expect(result.code).toBe(200);
});
});
describe('getPreviewSchema', () => {
it('should get the preview schema successfully', async () => {
const surveyId = new ObjectId();
jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue({} as any);
jest.spyOn(surveyMetaService, 'getSurveyById').mockResolvedValue({
title: 'Test Survey',
surveyPath: 'some/path',
} as any);
const result = await controller.getPreviewSchema({
surveyPath: surveyId.toString(),
});
expect(result.code).toBe(200);
});
}); });
}); });

View File

@ -42,7 +42,7 @@ describe('SurveyHistoryService', () => {
msgContent: undefined, msgContent: undefined,
}, },
baseConf: { baseConf: {
begTime: '', beginTime: '',
endTime: '', endTime: '',
answerBegTime: '', answerBegTime: '',
answerEndTime: '', answerEndTime: '',
@ -121,9 +121,9 @@ describe('SurveyHistoryService', () => {
}, },
take: 100, take: 100,
order: { order: {
createDate: -1, createdAt: -1,
}, },
select: ['createDate', 'operator', 'type', '_id'], select: ['createdAt', 'operator', 'type', '_id'],
}); });
}); });
}); });

View File

@ -59,18 +59,25 @@ describe('SurveyMetaController', () => {
remark: '', remark: '',
}; };
const req = { const mockUser = {
user: {
username: 'test-user', username: 'test-user',
}, _id: new ObjectId(),
};
const req = {
user: mockUser,
surveyMeta: survey, surveyMeta: survey,
}; };
const result = await controller.updateMeta(reqBody, req); const result = await controller.updateMeta(reqBody, req);
expect(surveyMetaService.editSurveyMeta).toHaveBeenCalledWith({ expect(surveyMetaService.editSurveyMeta).toHaveBeenCalledWith({
operator: mockUser.username,
operatorId: mockUser._id.toString(),
survey: {
title: reqBody.title, title: reqBody.title,
remark: reqBody.remark, remark: reqBody.remark,
},
}); });
expect(result).toEqual({ code: 200 }); expect(result).toEqual({ code: 200 });
@ -116,11 +123,14 @@ describe('SurveyMetaController', () => {
data: [ data: [
{ {
_id: new ObjectId(), _id: new ObjectId(),
createDate: date, createdAt: date,
updateDate: date, updatedAt: date,
curStatus: { curStatus: {
date: date, date: date,
}, },
subStatus: {
date: date,
},
surveyType: 'normal', surveyType: 'normal',
}, },
], ],
@ -135,10 +145,7 @@ describe('SurveyMetaController', () => {
count: 10, count: 10,
data: expect.arrayContaining([ data: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
createDate: expect.stringMatching( createdAt: expect.stringMatching(
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
),
updateDate: expect.stringMatching(
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
), ),
curStatus: expect.objectContaining({ curStatus: expect.objectContaining({
@ -146,6 +153,11 @@ describe('SurveyMetaController', () => {
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
), ),
}), }),
subStatus: expect.objectContaining({
date: expect.stringMatching(
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
),
}),
surveyType: 'normal', surveyType: 'normal',
}), }),
]), ]),
@ -178,7 +190,7 @@ describe('SurveyMetaController', () => {
condition: [{ field: 'surveyType', value: 'normal' }], condition: [{ field: 'surveyType', value: 'normal' }],
}, },
]), ]),
order: JSON.stringify([{ field: 'createDate', value: -1 }]), order: JSON.stringify([{ field: 'createdAt', value: -1 }]),
}; };
const userId = new ObjectId().toString(); const userId = new ObjectId().toString();
const req = { const req = {
@ -198,7 +210,7 @@ describe('SurveyMetaController', () => {
surveyIdList: [], surveyIdList: [],
userId, userId,
filter: { surveyType: 'normal', title: { $regex: 'hahah' } }, filter: { surveyType: 'normal', title: { $regex: 'hahah' } },
order: { createDate: -1 }, order: { createdAt: -1 },
workspaceId: undefined, workspaceId: undefined,
}); });
}); });

View File

@ -2,12 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing';
import { SurveyMetaService } from '../services/surveyMeta.service'; import { SurveyMetaService } from '../services/surveyMeta.service';
import { MongoRepository } from 'typeorm'; import { MongoRepository } from 'typeorm';
import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
import { PluginManager } from 'src/securityPlugin/pluginManager'; import { PluginManager } from 'src/securityPlugin/pluginManager';
import { RECORD_STATUS } from 'src/enums';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { SurveyUtilPlugin } from 'src/securityPlugin/surveyUtilPlugin'; import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
describe('SurveyMetaService', () => { describe('SurveyMetaService', () => {
@ -26,10 +24,11 @@ describe('SurveyMetaService', () => {
count: jest.fn(), count: jest.fn(),
create: jest.fn(), create: jest.fn(),
save: jest.fn(), save: jest.fn(),
updateOne: jest.fn(),
findAndCount: jest.fn(), findAndCount: jest.fn(),
}, },
}, },
PluginManagerProvider, PluginManager,
], ],
}).compile(); }).compile();
@ -38,18 +37,18 @@ describe('SurveyMetaService', () => {
getRepositoryToken(SurveyMeta), getRepositoryToken(SurveyMeta),
); );
pluginManager = module.get<PluginManager>(PluginManager); pluginManager = module.get<PluginManager>(PluginManager);
pluginManager.registerPlugin(new SurveyUtilPlugin());
}); });
describe('getNewSurveyPath', () => { describe('getNewSurveyPath', () => {
it('should generate a new survey path', async () => { it('should generate a new survey path', async () => {
jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(1); jest.spyOn(pluginManager, 'triggerHook').mockResolvedValueOnce('path1');
jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(0); jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(0);
const surveyPath = await service.getNewSurveyPath(); const surveyPath = await service.getNewSurveyPath();
expect(typeof surveyPath).toBe('string'); expect(surveyPath).toBe('path1');
expect(surveyRepository.count).toHaveBeenCalledTimes(2); expect(pluginManager.triggerHook).toHaveBeenCalledTimes(1);
expect(surveyRepository.count).toHaveBeenCalledTimes(1);
}); });
}); });
@ -63,14 +62,11 @@ describe('SurveyMetaService', () => {
userId: new ObjectId().toString(), userId: new ObjectId().toString(),
createMethod: '', createMethod: '',
createFrom: '', createFrom: '',
workspaceId: 'workspace1',
}; };
const newSurvey = new SurveyMeta(); const newSurvey = new SurveyMeta();
const mockedSurveyPath = 'mockedSurveyPath'; jest.spyOn(service, 'getNewSurveyPath').mockResolvedValue('path1');
jest
.spyOn(service, 'getNewSurveyPath')
.mockResolvedValue(mockedSurveyPath);
jest jest
.spyOn(surveyRepository, 'create') .spyOn(surveyRepository, 'create')
.mockImplementation(() => newSurvey); .mockImplementation(() => newSurvey);
@ -82,86 +78,118 @@ describe('SurveyMetaService', () => {
title: params.title, title: params.title,
remark: params.remark, remark: params.remark,
surveyType: params.surveyType, surveyType: params.surveyType,
surveyPath: mockedSurveyPath, surveyPath: 'path1',
creator: params.username, creator: params.username,
ownerId: params.userId, creatorId: params.userId,
owner: params.username, owner: params.username,
ownerId: params.userId,
createMethod: params.createMethod, createMethod: params.createMethod,
createFrom: params.createFrom, createFrom: params.createFrom,
workspaceId: params.workspaceId,
}); });
expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey); expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey);
expect(result).toEqual(newSurvey); expect(result).toEqual(newSurvey);
}); });
}); });
describe('editSurveyMeta', () => { describe('pausingSurveyMeta', () => {
it('should edit a survey meta and return it if in NEW or EDITING status', async () => { it('should throw an exception if survey is in NEW status', async () => {
const survey = new SurveyMeta();
survey.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
await expect(service.pausingSurveyMeta(survey)).rejects.toThrow(
HttpException,
);
});
it('should pause a survey and update subStatus', async () => {
const survey = new SurveyMeta(); const survey = new SurveyMeta();
survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() }; survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() };
survey.statusList = []; survey.statusList = [];
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey); jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
const result = await service.editSurveyMeta(survey); const result = await service.pausingSurveyMeta(survey);
expect(survey.curStatus.status).toEqual(RECORD_STATUS.EDITING); expect(survey.subStatus.status).toBe(RECORD_SUB_STATUS.PAUSING);
expect(survey.statusList.length).toBe(1); expect(survey.statusList.length).toBe(1);
expect(survey.statusList[0].status).toEqual(RECORD_STATUS.EDITING); expect(survey.statusList[0].status).toBe(RECORD_SUB_STATUS.PAUSING);
expect(surveyRepository.save).toHaveBeenCalledWith(survey);
expect(result).toEqual(survey);
});
});
describe('editSurveyMeta', () => {
it('should edit a survey meta and return it', async () => {
const survey = new SurveyMeta();
survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() };
survey.statusList = [];
const operator = 'editor';
const operatorId = 'editorId';
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
const result = await service.editSurveyMeta({
survey,
operator,
operatorId,
});
expect(survey.curStatus.status).toBe(RECORD_STATUS.EDITING);
expect(survey.statusList.length).toBe(1);
expect(survey.statusList[0].status).toBe(RECORD_STATUS.EDITING);
expect(survey.operator).toBe(operator);
expect(survey.operatorId).toBe(operatorId);
expect(surveyRepository.save).toHaveBeenCalledWith(survey); expect(surveyRepository.save).toHaveBeenCalledWith(survey);
expect(result).toEqual(survey); expect(result).toEqual(survey);
}); });
}); });
describe('deleteSurveyMeta', () => { describe('deleteSurveyMeta', () => {
it('should delete survey meta and update status', async () => { it('should mark a survey as deleted', async () => {
// 准备假的SurveyMeta对象 const surveyId = new ObjectId().toString();
const survey = new SurveyMeta(); const operator = 'deleter';
survey.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() }; const operatorId = 'deleterId';
survey.statusList = [];
// 模拟save方法 jest.spyOn(surveyRepository, 'updateOne').mockResolvedValue({
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey); matchedCount: 1,
modifiedCount: 1,
// 调用要测试的方法 acknowledged: true,
const result = await service.deleteSurveyMeta(survey);
// 验证结果
expect(result).toBe(survey);
expect(survey.curStatus.status).toBe(RECORD_STATUS.REMOVED);
expect(survey.statusList.length).toBe(1);
expect(survey.statusList[0].status).toBe(RECORD_STATUS.REMOVED);
expect(surveyRepository.save).toHaveBeenCalledTimes(1);
expect(surveyRepository.save).toHaveBeenCalledWith(survey);
}); });
it('should throw exception when survey is already removed', async () => { const result = await service.deleteSurveyMeta({
// 准备假的SurveyMeta对象其状态已设置为REMOVED surveyId,
const survey = new SurveyMeta(); operator,
survey.curStatus = { status: RECORD_STATUS.REMOVED, date: Date.now() }; operatorId,
});
// 调用要测试的方法并期待异常 expect(surveyRepository.updateOne).toHaveBeenCalledWith(
await expect(service.deleteSurveyMeta(survey)).rejects.toThrow( { _id: new ObjectId(surveyId) },
HttpException, {
$set: {
isDeleted: true,
operator,
operatorId,
deletedAt: expect.any(Date),
},
},
); );
expect(result.matchedCount).toBe(1);
// 验证save方法没有被调用
expect(surveyRepository.save).not.toHaveBeenCalled();
}); });
}); });
describe('getSurveyMetaList', () => { describe('getSurveyMetaList', () => {
it('should return a list of survey metadata', async () => { it('should return a list of survey metadata', async () => {
// 准备模拟数据
const mockData = [ const mockData = [
{ _id: 1, title: 'Survey 1' }, { _id: 1, title: 'Survey 1' },
{ _id: 2, title: 'Survey 2' },
] as unknown as Array<SurveyMeta>; ] as unknown as Array<SurveyMeta>;
const mockCount = 2; const mockCount = 1;
jest jest
.spyOn(surveyRepository, 'findAndCount') .spyOn(surveyRepository, 'findAndCount')
.mockResolvedValue([mockData, mockCount]); .mockResolvedValue([mockData, mockCount]);
// 调用方法并检查返回值
const condition = { const condition = {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
@ -170,40 +198,47 @@ describe('SurveyMetaService', () => {
filter: {}, filter: {},
order: {}, order: {},
}; };
const result = await service.getSurveyMetaList(condition); const result = await service.getSurveyMetaList(condition);
// 验证返回值
expect(result).toEqual({ data: mockData, count: mockCount }); expect(result).toEqual({ data: mockData, count: mockCount });
// 验证repository方法被正确调用
expect(surveyRepository.findAndCount).toHaveBeenCalledTimes(1); expect(surveyRepository.findAndCount).toHaveBeenCalledTimes(1);
}); });
}); });
describe('publishSurveyMeta', () => { describe('publishSurveyMeta', () => {
it('should publish a survey meta and add status to statusList', async () => { it('should publish a survey and update curStatus', async () => {
// 准备模拟数据 const surveyMeta = new SurveyMeta();
const surveyMeta = { surveyMeta.statusList = [];
id: 1,
title: 'Test Survey',
statusList: [],
} as unknown as SurveyMeta;
const savedSurveyMeta = {
...surveyMeta,
curStatus: {
status: RECORD_STATUS.PUBLISHED,
date: expect.any(Number),
},
} as unknown as SurveyMeta;
jest.spyOn(surveyRepository, 'save').mockResolvedValue(savedSurveyMeta); jest.spyOn(surveyRepository, 'save').mockResolvedValue(surveyMeta);
// 调用方法并检查返回值
const result = await service.publishSurveyMeta({ surveyMeta }); const result = await service.publishSurveyMeta({ surveyMeta });
// 验证返回值 expect(surveyMeta.curStatus.status).toBe(RECORD_STATUS.PUBLISHED);
expect(result).toEqual(savedSurveyMeta); expect(surveyMeta.statusList.length).toBe(1);
// 验证repository方法被正确调用 expect(surveyMeta.statusList[0].status).toBe(RECORD_STATUS.PUBLISHED);
expect(surveyRepository.save).toHaveBeenCalledWith(savedSurveyMeta); expect(surveyRepository.save).toHaveBeenCalledWith(surveyMeta);
expect(result).toEqual(surveyMeta);
});
});
describe('countSurveyMetaByWorkspaceId', () => {
it('should return the count of surveys in a workspace', async () => {
const workspaceId = 'workspace1';
const mockCount = 5;
jest.spyOn(surveyRepository, 'count').mockResolvedValue(mockCount);
const result = await service.countSurveyMetaByWorkspaceId({
workspaceId,
});
expect(result).toBe(mockCount);
expect(surveyRepository.count).toHaveBeenCalledWith({
workspaceId,
isDeleted: { $ne: true },
});
}); });
}); });
}); });

View File

@ -185,10 +185,14 @@ export class CollaboratorController {
userIdList: newCollaboratorUserIdList, userIdList: newCollaboratorUserIdList,
}); });
this.logger.info('batchDelete:' + JSON.stringify(delRes)); this.logger.info('batchDelete:' + JSON.stringify(delRes));
const username = req.user.username;
const userId = req.user._id.toString();
if (Array.isArray(newCollaborator) && newCollaborator.length > 0) { if (Array.isArray(newCollaborator) && newCollaborator.length > 0) {
const insertRes = await this.collaboratorService.batchCreate({ const insertRes = await this.collaboratorService.batchCreate({
surveyId: value.surveyId, surveyId: value.surveyId,
collaboratorList: newCollaborator, collaboratorList: newCollaborator,
creator: username,
creatorId: userId,
}); });
this.logger.info(`${JSON.stringify(insertRes)}`); this.logger.info(`${JSON.stringify(insertRes)}`);
} }
@ -198,6 +202,8 @@ export class CollaboratorController {
this.collaboratorService.updateById({ this.collaboratorService.updateById({
collaboratorId: item._id, collaboratorId: item._id,
permissions: item.permissions, permissions: item.permissions,
operator: username,
operatorId: userId,
}), }),
), ),
); );

View File

@ -98,7 +98,7 @@ export class DownloadTaskController {
list: list.map((data) => { list: list.map((data) => {
const item: Record<string, any> = {}; const item: Record<string, any> = {};
item.taskId = data._id.toString(); item.taskId = data._id.toString();
item.curStatus = data.curStatus; item.status = data.status;
item.filename = data.filename; item.filename = data.filename;
item.url = data.url; item.url = data.url;
const fmt = 'YYYY-MM-DD HH:mm:ss'; const fmt = 'YYYY-MM-DD HH:mm:ss';
@ -114,7 +114,7 @@ export class DownloadTaskController {
} }
item.fileSize = `${size.toFixed()} ${units[unitIndex]}`; item.fileSize = `${size.toFixed()} ${units[unitIndex]}`;
} }
item.createDate = moment(Number(data.createDate)).format(fmt); item.createdAt = moment(data.createdAt).format(fmt);
return item; return item;
}), }),
}, },
@ -177,6 +177,8 @@ export class DownloadTaskController {
const delRes = await this.downloadTaskService.deleteDownloadTask({ const delRes = await this.downloadTaskService.deleteDownloadTask({
taskId, taskId,
operator: req.user.username,
operatorId: req.user._id.toString(),
}); });
return { return {

View File

@ -67,9 +67,9 @@ export class SessionController {
@Post('/seize') @Post('/seize')
@HttpCode(200) @HttpCode(200)
@UseGuards(SurveyGuard) @UseGuards(SessionGuard, SurveyGuard)
@UseGuards(SessionGuard)
@SetMetadata('sessionId', 'body.sessionId') @SetMetadata('sessionId', 'body.sessionId')
@SetMetadata('surveyId', 'surveyId')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE]) @SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
@UseGuards(Authentication) @UseGuards(Authentication)
async seize( async seize(

View File

@ -144,7 +144,7 @@ export class SurveyController {
if (latestEditingOne && latestEditingOne._id.toString() !== sessionId) { if (latestEditingOne && latestEditingOne._id.toString() !== sessionId) {
const curSession = await this.sessionService.findOne(sessionId); const curSession = await this.sessionService.findOne(sessionId);
if (curSession.createDate <= latestEditingOne.updateDate) { if (curSession.createdAt <= latestEditingOne.updatedAt) {
// 在当前用户打开之后,被其他页面保存过了 // 在当前用户打开之后,被其他页面保存过了
const isSameOperator = const isSameOperator =
latestEditingOne.userId === req.user._id.toString(); latestEditingOne.userId === req.user._id.toString();
@ -194,11 +194,38 @@ export class SurveyController {
async deleteSurvey(@Request() req) { async deleteSurvey(@Request() req) {
const surveyMeta = req.surveyMeta; const surveyMeta = req.surveyMeta;
await this.surveyMetaService.deleteSurveyMeta(surveyMeta); const delMetaRes = await this.surveyMetaService.deleteSurveyMeta({
surveyId: surveyMeta._id.toString(),
operator: req.user.username,
operatorId: req.user._id.toString(),
});
const delResponseRes =
await this.responseSchemaService.deleteResponseSchema({ await this.responseSchemaService.deleteResponseSchema({
surveyPath: surveyMeta.surveyPath, surveyPath: surveyMeta.surveyPath,
}); });
this.logger.info(JSON.stringify(delMetaRes));
this.logger.info(JSON.stringify(delResponseRes));
return {
code: 200,
};
}
@HttpCode(200)
@Post('/pausingSurvey')
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
@UseGuards(Authentication)
async pausingSurvey(@Request() req) {
const surveyMeta = req.surveyMeta;
await this.surveyMetaService.pausingSurveyMeta(surveyMeta);
await this.responseSchemaService.pausingResponseSchema({
surveyPath: surveyMeta.surveyPath,
});
return { return {
code: 200, code: 200,
}; };
@ -305,6 +332,12 @@ export class SurveyController {
const username = req.user.username; const username = req.user.username;
const surveyId = value.surveyId; const surveyId = value.surveyId;
const surveyMeta = req.surveyMeta; const surveyMeta = req.surveyMeta;
if (surveyMeta.isDeleted) {
throw new HttpException(
'问卷已删除,无法发布',
EXCEPTION_CODE.SURVEY_NOT_FOUND,
);
}
const surveyConf = const surveyConf =
await this.surveyConfService.getSurveyConfBySurveyId(surveyId); await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
@ -330,7 +363,8 @@ export class SurveyController {
pageId: surveyId, pageId: surveyId,
}); });
await this.surveyHistoryService.addHistory({ // 添加发布历史可以异步添加
this.surveyHistoryService.addHistory({
surveyId, surveyId,
schema: surveyConf.code, schema: surveyConf.code,
type: HISTORY_TYPE.PUBLISH_HIS, type: HISTORY_TYPE.PUBLISH_HIS,

View File

@ -18,7 +18,7 @@ import { Logger } from 'src/logger';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
@ApiTags('survey') @ApiTags('survey')
@Controller('/api/surveyHisotry') @Controller('/api/surveyHistory')
export class SurveyHistoryController { export class SurveyHistoryController {
constructor( constructor(
private readonly surveyHistoryService: SurveyHistoryService, private readonly surveyHistoryService: SurveyHistoryService,

View File

@ -58,7 +58,11 @@ export class SurveyMetaController {
survey.title = value.title; survey.title = value.title;
survey.remark = value.remark; survey.remark = value.remark;
await this.surveyMetaService.editSurveyMeta(survey); await this.surveyMetaService.editSurveyMeta({
survey,
operator: req.user.username,
operatorId: req.user._id.toString(),
});
return { return {
code: 200, code: 200,
@ -127,9 +131,10 @@ export class SurveyMetaController {
if (!item.surveyType) { if (!item.surveyType) {
item.surveyType = item.questionType || 'normal'; item.surveyType = item.questionType || 'normal';
} }
item.createDate = moment(item.createDate).format(fmt); item.createdAt = moment(item.createdAt).format(fmt);
item.updateDate = moment(item.updateDate).format(fmt);
item.curStatus.date = moment(item.curStatus.date).format(fmt); item.curStatus.date = moment(item.curStatus.date).format(fmt);
item.subStatus.date = moment(item.subStatus.date).format(fmt);
item.updatedAt = moment(item.updatedAt).format(fmt);
const surveyId = item._id.toString(); const surveyId = item._id.toString();
if (cooperSurveyIdMap[surveyId]) { if (cooperSurveyIdMap[surveyId]) {
item.isCollaborated = true; item.isCollaborated = true;

View File

@ -22,12 +22,17 @@ export class CollaboratorService {
return this.collaboratorRepository.save(collaborator); return this.collaboratorRepository.save(collaborator);
} }
async batchCreate({ surveyId, collaboratorList }) { async batchCreate({ surveyId, collaboratorList, creator, creatorId }) {
const now = new Date();
const res = await this.collaboratorRepository.insertMany( const res = await this.collaboratorRepository.insertMany(
collaboratorList.map((item) => { collaboratorList.map((item) => {
return { return {
...item, ...item,
surveyId, surveyId,
createdAt: now,
updatedAt: now,
creator,
creatorId,
}; };
}), }),
); );
@ -60,7 +65,13 @@ export class CollaboratorService {
return info; return info;
} }
async changeUserPermission({ userId, surveyId, permission }) { async changeUserPermission({
userId,
surveyId,
permission,
operator,
operatorId,
}) {
const updateRes = await this.collaboratorRepository.updateOne( const updateRes = await this.collaboratorRepository.updateOne(
{ {
surveyId, surveyId,
@ -69,6 +80,9 @@ export class CollaboratorService {
{ {
$set: { $set: {
permission, permission,
operator,
operatorId,
updatedAt: new Date(),
}, },
}, },
); );
@ -134,7 +148,7 @@ export class CollaboratorService {
return delRes; return delRes;
} }
updateById({ collaboratorId, permissions }) { updateById({ collaboratorId, permissions, operator, operatorId }) {
return this.collaboratorRepository.updateOne( return this.collaboratorRepository.updateOne(
{ {
_id: new ObjectId(collaboratorId), _id: new ObjectId(collaboratorId),
@ -142,6 +156,9 @@ export class CollaboratorService {
{ {
$set: { $set: {
permissions, permissions,
operator,
operatorId,
updatedAt: new Date(),
}, },
}, },
); );

View File

@ -34,8 +34,8 @@ export class DataStatisticService {
const dataListMap = keyBy(dataList, 'field'); const dataListMap = keyBy(dataList, 'field');
const where = { const where = {
pageId: surveyId, pageId: surveyId,
'curStatus.status': { isDeleted: {
$ne: 'removed', $ne: true,
}, },
}; };
const [surveyResponseList, total] = const [surveyResponseList, total] =
@ -44,7 +44,7 @@ export class DataStatisticService {
take: pageSize, take: pageSize,
skip: (pageNum - 1) * pageSize, skip: (pageNum - 1) * pageSize,
order: { order: {
createDate: -1, createdAt: -1,
}, },
}); });
@ -90,10 +90,10 @@ export class DataStatisticService {
} }
return { return {
...data, ...data,
diffTime: (submitedData.diffTime / 1000).toFixed(2), diffTime: submitedData.diffTime
createDate: moment(submitedData.createDate).format( ? (submitedData.diffTime / 1000).toFixed(2)
'YYYY-MM-DD HH:mm:ss', : '0',
), createdAt: moment(submitedData.createdAt).format('YYYY-MM-DD HH:mm:ss'),
}; };
}); });
return { return {
@ -124,8 +124,8 @@ export class DataStatisticService {
{ {
$match: { $match: {
pageId: surveyId, pageId: surveyId,
'curStatus.status': { isDeleted: {
$ne: 'removed', $ne: true,
}, },
}, },
}, },

View File

@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm'; import { MongoRepository } from 'typeorm';
import { ResponseSchema } from 'src/models/responseSchema.entity'; import { ResponseSchema } from 'src/models/responseSchema.entity';
import { DownloadTask } from 'src/models/downloadTask.entity'; import { DownloadTask } from 'src/models/downloadTask.entity';
import { RECORD_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { ResponseSchemaService } from 'src/modules/surveyResponse/services/responseScheme.service'; import { ResponseSchemaService } from 'src/modules/surveyResponse/services/responseScheme.service';
import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity';
@ -14,6 +13,7 @@ import { get } from 'lodash';
import { FileService } from 'src/modules/file/services/file.service'; import { FileService } from 'src/modules/file/services/file.service';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import moment from 'moment'; import moment from 'moment';
import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus';
@Injectable() @Injectable()
export class DownloadTaskService { export class DownloadTaskService {
@ -56,6 +56,7 @@ export class DownloadTaskService {
title: responseSchema.title, title: responseSchema.title,
}, },
filename, filename,
status: DOWNLOAD_TASK_STATUS.WAITING,
}); });
await this.downloadTaskRepository.save(downloadTask); await this.downloadTaskRepository.save(downloadTask);
return downloadTask._id.toString(); return downloadTask._id.toString();
@ -72,8 +73,8 @@ export class DownloadTaskService {
}) { }) {
const where = { const where = {
creatorId, creatorId,
'curStatus.status': { isDeleted: {
$ne: RECORD_STATUS.REMOVED, $ne: true,
}, },
}; };
const [surveyDownloadList, total] = const [surveyDownloadList, total] =
@ -82,7 +83,7 @@ export class DownloadTaskService {
take: pageSize, take: pageSize,
skip: (pageIndex - 1) * pageSize, skip: (pageIndex - 1) * pageSize,
order: { order: {
createDate: -1, createdAt: -1,
}, },
}); });
return { return {
@ -103,24 +104,25 @@ export class DownloadTaskService {
return null; return null;
} }
async deleteDownloadTask({ taskId }: { taskId: string }) { async deleteDownloadTask({
const curStatus = { taskId,
status: RECORD_STATUS.REMOVED, operator,
date: Date.now(), operatorId,
}; }: {
taskId: string;
operator: string;
operatorId: string;
}) {
return this.downloadTaskRepository.updateOne( return this.downloadTaskRepository.updateOne(
{ {
_id: new ObjectId(taskId), _id: new ObjectId(taskId),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}, },
{ {
$set: { $set: {
curStatus, isDeleted: true,
}, operator,
$push: { operatorId,
statusList: curStatus as never, deletedAt: new Date(),
}, },
}, },
); );
@ -140,7 +142,7 @@ export class DownloadTaskService {
const taskId = DownloadTaskService.taskList.shift(); const taskId = DownloadTaskService.taskList.shift();
this.logger.info(`handle taskId: ${taskId}`); this.logger.info(`handle taskId: ${taskId}`);
const taskInfo = await this.getDownloadTaskById({ taskId }); const taskInfo = await this.getDownloadTaskById({ taskId });
if (!taskInfo || taskInfo.curStatus.status === RECORD_STATUS.REMOVED) { if (!taskInfo || taskInfo.isDeleted) {
// 不存在或者已删除的,不处理 // 不存在或者已删除的,不处理
continue; continue;
} }
@ -151,7 +153,7 @@ export class DownloadTaskService {
} }
} }
private async handleDownloadTask({ taskInfo }) { async handleDownloadTask({ taskInfo }) {
try { try {
// 更新任务状态为计算中 // 更新任务状态为计算中
const updateRes = await this.downloadTaskRepository.updateOne( const updateRes = await this.downloadTaskRepository.updateOne(
@ -160,10 +162,8 @@ export class DownloadTaskService {
}, },
{ {
$set: { $set: {
curStatus: { status: DOWNLOAD_TASK_STATUS.COMPUTING,
status: RECORD_STATUS.COMPUTING, updatedAt: new Date(),
date: Date.now(),
},
}, },
}, },
); );
@ -176,9 +176,6 @@ export class DownloadTaskService {
await this.responseSchemaService.getResponseSchemaByPageId(surveyId); await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
const where = { const where = {
pageId: surveyId, pageId: surveyId,
'curStatus.status': {
$ne: 'removed',
},
}; };
const total = await this.surveyResponseRepository.count(where); const total = await this.surveyResponseRepository.count(where);
const pageSize = 200; const pageSize = 200;
@ -205,9 +202,13 @@ export class DownloadTaskService {
for (const headItem of listHead) { for (const headItem of listHead) {
const field = headItem.field; const field = headItem.field;
const val = get(bodyItem, field, ''); const val = get(bodyItem, field, '');
if (typeof val === 'string') {
const $ = load(val); const $ = load(val);
const text = $.text(); const text = $.text();
bodyData.push(text); bodyData.push(text);
} else {
bodyData.push(val);
}
} }
xlsxBody.push(bodyData); xlsxBody.push(bodyData);
} }
@ -236,11 +237,6 @@ export class DownloadTaskService {
filename: taskInfo.filename, filename: taskInfo.filename,
}); });
const curStatus = {
status: RECORD_STATUS.FINISHED,
date: Date.now(),
};
// 更新计算结果 // 更新计算结果
const updateFinishRes = await this.downloadTaskRepository.updateOne( const updateFinishRes = await this.downloadTaskRepository.updateOne(
{ {
@ -248,32 +244,24 @@ export class DownloadTaskService {
}, },
{ {
$set: { $set: {
curStatus, status: DOWNLOAD_TASK_STATUS.SUCCEED,
url, url,
fileKey: key, fileKey: key,
fileSize: buffer.length, fileSize: buffer.length,
}, updatedAt: new Date(),
$push: {
statusList: curStatus as never,
}, },
}, },
); );
this.logger.info(JSON.stringify(updateFinishRes)); this.logger.info(JSON.stringify(updateFinishRes));
} catch (error) { } catch (error) {
const curStatus = {
status: RECORD_STATUS.ERROR,
date: Date.now(),
};
await this.downloadTaskRepository.updateOne( await this.downloadTaskRepository.updateOne(
{ {
_id: taskInfo._id, _id: taskInfo._id,
}, },
{ {
$set: { $set: {
curStatus, status: DOWNLOAD_TASK_STATUS.FAILED,
}, updatedAt: new Date(),
$push: {
statusList: curStatus as never,
}, },
}, },
); );

View File

@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm'; import { MongoRepository } from 'typeorm';
import { Session } from 'src/models/session.entity'; import { Session } from 'src/models/session.entity';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums'; import { SESSION_STATUS } from 'src/enums/surveySessionStatus';
@Injectable() @Injectable()
export class SessionService { export class SessionService {
@ -16,6 +16,7 @@ export class SessionService {
const session = this.sessionRepository.create({ const session = this.sessionRepository.create({
surveyId, surveyId,
userId, userId,
status: SESSION_STATUS.DEACTIVATED,
}); });
return this.sessionRepository.save(session); return this.sessionRepository.save(session);
} }
@ -32,33 +33,20 @@ export class SessionService {
return this.sessionRepository.findOne({ return this.sessionRepository.findOne({
where: { where: {
surveyId, surveyId,
'curStatus.status': { status: SESSION_STATUS.ACTIVATED,
$ne: RECORD_STATUS.NEW,
},
}, },
}); });
} }
updateSessionToEditing({ sessionId, surveyId }) { 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([ return Promise.all([
this.sessionRepository.updateOne( this.sessionRepository.update(
{ {
_id: new ObjectId(sessionId), _id: new ObjectId(sessionId),
}, },
{ {
$set: { status: SESSION_STATUS.ACTIVATED,
curStatus: editingStatus, updatedAt: new Date(),
updateDate: now,
},
}, },
), ),
this.sessionRepository.updateMany( this.sessionRepository.updateMany(
@ -70,8 +58,8 @@ export class SessionService {
}, },
{ {
$set: { $set: {
curStatus: newStatus, status: SESSION_STATUS.DEACTIVATED,
updateDate: now, updatedAt: new Date(),
}, },
}, },
), ),

View File

@ -45,9 +45,9 @@ export class SurveyHistoryService {
}, },
take: 100, take: 100,
order: { order: {
createDate: -1, createdAt: -1,
}, },
select: ['createDate', 'operator', 'type', '_id'], select: ['createdAt', 'operator', 'type', '_id'],
}); });
} }
} }

View File

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository, FindOptionsOrder } from 'typeorm'; import { MongoRepository, FindOptionsOrder } from 'typeorm';
import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { RECORD_STATUS } from 'src/enums'; import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
@ -65,6 +65,7 @@ export class SurveyMetaService {
surveyType: surveyType, surveyType: surveyType,
surveyPath, surveyPath,
creator: username, creator: username,
creatorId: userId,
owner: username, owner: username,
ownerId: userId, ownerId: userId,
createMethod, createMethod,
@ -75,11 +76,36 @@ export class SurveyMetaService {
return await this.surveyRepository.save(newSurvey); return await this.surveyRepository.save(newSurvey);
} }
async editSurveyMeta(survey: SurveyMeta) { async pausingSurveyMeta(survey: SurveyMeta) {
if ( if (survey?.curStatus?.status === RECORD_STATUS.NEW) {
survey.curStatus.status !== RECORD_STATUS.NEW && throw new HttpException(
survey.curStatus.status !== RECORD_STATUS.EDITING '问卷不能暂停',
) { EXCEPTION_CODE.SURVEY_STATUS_TRANSFORM_ERROR,
);
}
const subCurStatus = {
status: RECORD_SUB_STATUS.PAUSING,
date: Date.now(),
};
survey.subStatus = subCurStatus;
if (Array.isArray(survey.statusList)) {
survey.statusList.push(subCurStatus);
} else {
survey.statusList = [subCurStatus];
}
return this.surveyRepository.save(survey);
}
async editSurveyMeta({
survey,
operator,
operatorId,
}: {
survey: SurveyMeta;
operator: string;
operatorId: string;
}) {
if (survey?.curStatus?.status !== RECORD_STATUS.EDITING) {
const newStatus = { const newStatus = {
status: RECORD_STATUS.EDITING, status: RECORD_STATUS.EDITING,
date: Date.now(), date: Date.now(),
@ -87,28 +113,27 @@ export class SurveyMetaService {
survey.curStatus = newStatus; survey.curStatus = newStatus;
survey.statusList.push(newStatus); survey.statusList.push(newStatus);
} }
survey.updatedAt = new Date();
survey.operator = operator;
survey.operatorId = operatorId;
return this.surveyRepository.save(survey); return this.surveyRepository.save(survey);
} }
async deleteSurveyMeta(survey: SurveyMeta) { async deleteSurveyMeta({ surveyId, operator, operatorId }) {
if (survey.curStatus.status === RECORD_STATUS.REMOVED) { return this.surveyRepository.updateOne(
throw new HttpException( {
'问卷已删除,不能重复删除', _id: new ObjectId(surveyId),
EXCEPTION_CODE.SURVEY_STATUS_TRANSFORM_ERROR, },
{
$set: {
isDeleted: true,
operator,
operatorId,
deletedAt: new Date(),
},
},
); );
} }
const newStatusInfo = {
status: RECORD_STATUS.REMOVED,
date: Date.now(),
};
survey.curStatus = newStatusInfo;
if (Array.isArray(survey.statusList)) {
survey.statusList.push(newStatusInfo);
} else {
survey.statusList = [newStatusInfo];
}
return this.surveyRepository.save(survey);
}
async getSurveyMetaList(condition: { async getSurveyMetaList(condition: {
pageNum: number; pageNum: number;
@ -125,14 +150,16 @@ export class SurveyMetaService {
const skip = (pageNum - 1) * pageSize; const skip = (pageNum - 1) * pageSize;
try { try {
const query: Record<string, any> = Object.assign( const query: Record<string, any> = Object.assign(
{},
{ {
'curStatus.status': { isDeleted: {
$ne: 'removed', $ne: true,
}, },
}, },
condition.filter, condition.filter,
); );
if (condition.filter['curStatus.status']) {
query['subStatus.status'] = RECORD_SUB_STATUS.DEFAULT;
}
if (workspaceId) { if (workspaceId) {
query.workspaceId = workspaceId; query.workspaceId = workspaceId;
} else { } else {
@ -160,9 +187,8 @@ export class SurveyMetaService {
condition.order && Object.keys(condition.order).length > 0 condition.order && Object.keys(condition.order).length > 0
? (condition.order as FindOptionsOrder<SurveyMeta>) ? (condition.order as FindOptionsOrder<SurveyMeta>)
: ({ : ({
createDate: -1, createdAt: -1,
} as FindOptionsOrder<SurveyMeta>); } as FindOptionsOrder<SurveyMeta>);
const [data, count] = await this.surveyRepository.findAndCount({ const [data, count] = await this.surveyRepository.findAndCount({
where: query, where: query,
skip, skip,
@ -181,6 +207,10 @@ export class SurveyMetaService {
date: Date.now(), date: Date.now(),
}; };
surveyMeta.curStatus = curStatus; surveyMeta.curStatus = curStatus;
surveyMeta.subStatus = {
status: RECORD_SUB_STATUS.DEFAULT,
date: Date.now(),
};
if (Array.isArray(surveyMeta.statusList)) { if (Array.isArray(surveyMeta.statusList)) {
surveyMeta.statusList.push(curStatus); surveyMeta.statusList.push(curStatus);
} else { } else {
@ -192,8 +222,8 @@ export class SurveyMetaService {
async countSurveyMetaByWorkspaceId({ workspaceId }) { async countSurveyMetaByWorkspaceId({ workspaceId }) {
const total = await this.surveyRepository.count({ const total = await this.surveyRepository.count({
workspaceId, workspaceId,
'curStatus.status': { isDeleted: {
$ne: RECORD_STATUS.REMOVED, $ne: true,
}, },
}); });
return total; return total;

View File

@ -32,7 +32,7 @@
"logoImageWidth": "60%" "logoImageWidth": "60%"
}, },
"baseConf": { "baseConf": {
"begTime": "2024-01-01 00:00:00", "beginTime": "2024-01-01 00:00:00",
"endTime": "2034-01-01 00:00:00", "endTime": "2034-01-01 00:00:00",
"tLimit": 0, "tLimit": 0,
"language": "chinese", "language": "chinese",

View File

@ -22,7 +22,7 @@ export async function getSchemaBySurveyType(surveyType: string) {
} }
const code = Object.assign({}, templateBase, codeData); const code = Object.assign({}, templateBase, codeData);
const nowMoment = moment(); const nowMoment = moment();
code.baseConf.begTime = nowMoment.format('YYYY-MM-DD HH:mm:ss'); code.baseConf.beginTime = nowMoment.format('YYYY-MM-DD HH:mm:ss');
code.baseConf.endTime = nowMoment code.baseConf.endTime = nowMoment
.add(10, 'years') .add(10, 'years')
.format('YYYY-MM-DD HH:mm:ss'); .format('YYYY-MM-DD HH:mm:ss');
@ -63,7 +63,7 @@ export function getListHeadByDataList(dataList) {
type: QUESTION_TYPE.TEXT, type: QUESTION_TYPE.TEXT,
}); });
listHead.push({ listHead.push({
field: 'createDate', field: 'createdAt',
title: '提交时间', title: '提交时间',
type: QUESTION_TYPE.TEXT, type: QUESTION_TYPE.TEXT,
}); });

View File

@ -3,7 +3,6 @@ import { MongoRepository } from 'typeorm';
import { ClientEncryptService } from '../services/clientEncrypt.service'; import { ClientEncryptService } from '../services/clientEncrypt.service';
import { ClientEncrypt } from 'src/models/clientEncrypt.entity'; import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
import { ENCRYPT_TYPE } from 'src/enums/encrypt'; import { ENCRYPT_TYPE } from 'src/enums/encrypt';
import { RECORD_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
@ -22,6 +21,7 @@ describe('ClientEncryptService', () => {
save: jest.fn(), save: jest.fn(),
findOne: jest.fn(), findOne: jest.fn(),
updateOne: jest.fn(), updateOne: jest.fn(),
deleteOne: jest.fn(),
}, },
}, },
], ],
@ -88,9 +88,6 @@ describe('ClientEncryptService', () => {
expect(repository.findOne).toHaveBeenCalledWith({ expect(repository.findOne).toHaveBeenCalledWith({
where: { where: {
_id: new ObjectId(id), _id: new ObjectId(id),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}, },
}); });
expect(result).toEqual(encryptInfo); expect(result).toEqual(encryptInfo);
@ -109,11 +106,13 @@ describe('ClientEncryptService', () => {
describe('deleteEncryptInfo', () => { describe('deleteEncryptInfo', () => {
it('should delete encrypt info by id', async () => { it('should delete encrypt info by id', async () => {
const id = new ObjectId().toHexString(); const id = new ObjectId().toHexString();
const updateResult = { matchedCount: 1, modifiedCount: 1 }; const deleteResult = { matchedCount: 1, modifiedCount: 1 };
jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult); jest
.spyOn(repository, 'deleteOne')
.mockResolvedValue(deleteResult as any);
const result = await service.deleteEncryptInfo(id); const result = await service.deleteEncryptInfo(id);
expect(result).toEqual(updateResult); expect(result).toEqual(deleteResult);
}); });
}); });
}); });

View File

@ -50,6 +50,7 @@ describe('CounterService', () => {
surveyPath: 'testPath', surveyPath: 'testPath',
type: 'testType', type: 'testType',
data, data,
updatedAt: expect.any(Date),
}, },
}, },
{ upsert: true }, { upsert: true },

View File

@ -1,5 +1,5 @@
import { ResponseSchema } from 'src/models/responseSchema.entity'; import { ResponseSchema } from 'src/models/responseSchema.entity';
import { RECORD_STATUS } from 'src/enums'; import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
export const mockResponseSchema: ResponseSchema = { export const mockResponseSchema: ResponseSchema = {
@ -8,14 +8,18 @@ export const mockResponseSchema: ResponseSchema = {
status: RECORD_STATUS.PUBLISHED, status: RECORD_STATUS.PUBLISHED,
date: 1710399368439, date: 1710399368439,
}, },
subStatus: {
status: RECORD_SUB_STATUS.DEFAULT,
date: 1710399368439,
},
statusList: [ statusList: [
{ {
status: RECORD_STATUS.PUBLISHED, status: RECORD_STATUS.PUBLISHED,
date: 1710399368439, date: 1710399368439,
}, },
], ],
createDate: 1710399368440, createdAt: 1710399368440,
updateDate: 1710399368440, updatedAt: 1710399368440,
title: '加密全流程', title: '加密全流程',
surveyPath: 'EBzdmnSp', surveyPath: 'EBzdmnSp',
code: { code: {
@ -32,7 +36,7 @@ export const mockResponseSchema: ResponseSchema = {
}, },
}, },
baseConf: { baseConf: {
begTime: '2024-03-14 14:54:41', beginTime: '2024-03-14 14:54:41',
endTime: '2034-03-14 14:54:41', endTime: '2034-03-14 14:54:41',
language: 'chinese', language: 'chinese',
tLimit: 10, tLimit: 10,
@ -247,4 +251,4 @@ export const mockResponseSchema: ResponseSchema = {
}, },
}, },
pageId: '65f29f3192862d6a9067ad1c', pageId: '65f29f3192862d6a9067ad1c',
} as ResponseSchema; } as unknown as ResponseSchema;

View File

@ -3,27 +3,28 @@ import { ResponseSchemaController } from '../controllers/responseSchema.controll
import { ResponseSchemaService } from '../services/responseScheme.service'; import { ResponseSchemaService } from '../services/responseScheme.service';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { RECORD_STATUS } from 'src/enums'; import { RECORD_SUB_STATUS } from 'src/enums';
import { ResponseSchema } from 'src/models/responseSchema.entity'; import { ResponseSchema } from 'src/models/responseSchema.entity';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { UserService } from 'src/modules/auth/services/user.service'; import { UserService } from 'src/modules/auth/services/user.service';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { AuthService } from 'src/modules/auth/services/auth.service';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
jest.mock('../services/responseScheme.service'); jest.mock('../services/responseScheme.service');
jest.mock('src/modules/auth/services/user.service');
jest.mock('src/modules/workspace/services/workspaceMember.service');
describe('ResponseSchemaController', () => { describe('ResponseSchemaController', () => {
let controller: ResponseSchemaController; let controller: ResponseSchemaController;
let responseSchemaService: ResponseSchemaService; let responseSchemaService: ResponseSchemaService;
let userService: UserService;
let workspaceMemberService: WorkspaceMemberService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [ResponseSchemaController], controllers: [ResponseSchemaController],
providers: [ providers: [
ResponseSchemaService, ResponseSchemaService,
AuthService,
{ {
provide: Logger, provide: Logger,
useValue: { useValue: {
@ -42,12 +43,6 @@ describe('ResponseSchemaController', () => {
findAllByUserId: jest.fn(), findAllByUserId: jest.fn(),
}, },
}, },
{
provide: AuthService,
useValue: {
create: jest.fn(),
},
},
{ {
provide: Logger, provide: Logger,
useValue: { useValue: {
@ -61,6 +56,10 @@ describe('ResponseSchemaController', () => {
responseSchemaService = module.get<ResponseSchemaService>( responseSchemaService = module.get<ResponseSchemaService>(
ResponseSchemaService, ResponseSchemaService,
); );
userService = module.get<UserService>(UserService);
workspaceMemberService = module.get<WorkspaceMemberService>(
WorkspaceMemberService,
);
}); });
describe('getSchema', () => { describe('getSchema', () => {
@ -68,12 +67,20 @@ describe('ResponseSchemaController', () => {
const mockQueryInfo = { surveyPath: 'validSurveyPath' }; const mockQueryInfo = { surveyPath: 'validSurveyPath' };
const mockResponseSchema = { const mockResponseSchema = {
surveyPath: 'testSurveyPath', surveyPath: 'testSurveyPath',
curStatus: { status: RECORD_STATUS.PUBLISHED, date: Date.now() }, curStatus: { status: 'published', date: Date.now() },
subStatus: { status: RECORD_SUB_STATUS.DEFAULT, date: Date.now() },
code: {
baseConf: {
passwordSwitch: false,
password: null,
whitelist: [],
},
},
} as ResponseSchema; } as ResponseSchema;
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(Promise.resolve(mockResponseSchema)); .mockResolvedValue(mockResponseSchema);
const result = await controller.getSchema(mockQueryInfo); const result = await controller.getSchema(mockQueryInfo);
@ -97,153 +104,180 @@ describe('ResponseSchemaController', () => {
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({ .mockResolvedValue({
curStatus: { status: RECORD_STATUS.REMOVED }, isDeleted: true,
} as ResponseSchema); } as ResponseSchema);
await expect(controller.getSchema(mockQueryInfo)).rejects.toThrow( await expect(controller.getSchema(mockQueryInfo)).rejects.toThrow(
new HttpException('问卷已删除', EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED), new HttpException(
'问卷不存在或已删除',
EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED,
),
); );
}); });
it('whitelistValidate should throw SurveyNotFoundException when survey is removed', async () => { it('should throw HttpException with RESPONSE_PAUSING code when survey is paused', async () => {
const surveyPath = ''; const mockQueryInfo = { surveyPath: 'pausedSurveyPath' };
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: { status: 'published' },
subStatus: { status: RECORD_SUB_STATUS.PAUSING },
} as ResponseSchema);
await expect(controller.getSchema(mockQueryInfo)).rejects.toThrow(
new HttpException('该问卷已暂停回收', EXCEPTION_CODE.RESPONSE_PAUSING),
);
});
});
describe('whitelistValidate', () => {
it('should throw HttpException when parameters are invalid', async () => {
const surveyPath = 'testSurveyPath';
const body = { password: 1 };
await expect(
controller.whitelistValidate(surveyPath, body),
).rejects.toThrow(HttpException);
});
it('should throw SurveyNotFoundException when survey is removed', async () => {
const surveyPath = 'removedSurveyPath';
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(null); .mockResolvedValue(null);
await expect( await expect(
controller.whitelistValidate(surveyPath, { controller.whitelistValidate(surveyPath, { password: '123456' }),
password: '123456',
}),
).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交')); ).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交'));
}); });
it('whitelistValidate should throw WHITELIST_ERROR code when password is incorrect', async () => { it('should throw HttpException when password is incorrect', async () => {
const surveyPath = ''; const surveyPath = 'testSurveyPath';
jest const mockSchema = {
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: {
status: 'published',
},
code: { code: {
baseConf: { baseConf: {
passwordSwitch: true, passwordSwitch: true,
password: '123456', password: '123456',
}, },
}, },
} as ResponseSchema); };
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any);
await expect( await expect(
controller.whitelistValidate(surveyPath, { controller.whitelistValidate(surveyPath, { password: 'wrongPassword' }),
password: '123457',
}),
).rejects.toThrow( ).rejects.toThrow(
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), new HttpException('密码验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
); );
}); });
it('whitelistValidate should be successfully', async () => { it('should validate successfully when password is correct', async () => {
const surveyPath = 'test'; const surveyPath = 'testSurveyPath';
jest const mockSchema = {
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: {
status: 'published',
},
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
},
},
} as ResponseSchema);
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
}),
).resolves.toEqual({ code: 200, data: null });
});
it('whitelistValidate should throw WHITELIST_ERROR code when mobile or email is incorrect', async () => {
const surveyPath = '';
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: {
status: 'published',
},
code: { code: {
baseConf: { baseConf: {
passwordSwitch: true, passwordSwitch: true,
password: '123456', password: '123456',
whitelistType: 'CUSTOM', whitelistType: 'CUSTOM',
memberType: 'MOBILE', whitelist: ['allowed@example.com'],
whitelist: ['13500000000'],
}, },
}, },
} as ResponseSchema); };
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any);
const result = await controller.whitelistValidate(surveyPath, {
password: '123456',
whitelist: 'allowed@example.com',
});
expect(result).toEqual({ code: 200, data: null });
});
it('should throw HttpException when whitelist value is not in CUSTOM whitelist', async () => {
const surveyPath = 'testSurveyPath';
const mockSchema = {
code: {
baseConf: {
whitelistType: 'CUSTOM',
whitelist: ['allowed@example.com'],
},
},
};
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any);
await expect( await expect(
controller.whitelistValidate(surveyPath, { controller.whitelistValidate(surveyPath, {
password: '123456', password: '123456',
whitelist: '13500000001', whitelist: 'notAllowed@example.com',
}), }),
).rejects.toThrow( ).rejects.toThrow(
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
); );
}); });
it('whitelistValidate should throw WHITELIST_ERROR code when member is incorrect', async () => { it('should throw HttpException when user is not found in MEMBER whitelist', async () => {
const surveyPath = ''; const surveyPath = 'testSurveyPath';
jest const mockSchema = {
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: {
status: 'published',
},
code: { code: {
baseConf: { baseConf: {
passwordSwitch: true,
password: '123456',
whitelistType: 'MEMBER', whitelistType: 'MEMBER',
whitelist: ['Jack'], whitelist: [],
}, },
}, },
} as ResponseSchema); };
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any);
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(null);
await expect( await expect(
controller.whitelistValidate(surveyPath, { controller.whitelistValidate(surveyPath, {
password: '123456', password: '123456',
whitelist: 'James', whitelist: 'nonExistentUser',
}),
).rejects.toThrow(
new HttpException('名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
);
});
it('should throw HttpException when user is not a workspace member', async () => {
const surveyPath = 'testSurveyPath';
const mockSchema = {
code: {
baseConf: {
whitelistType: 'MEMBER',
whitelist: [],
},
},
};
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any);
jest
.spyOn(userService, 'getUserByUsername')
.mockResolvedValue({ _id: new Object(), username: '' } as any);
jest
.spyOn(workspaceMemberService, 'findAllByUserId')
.mockResolvedValue([]);
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
whitelist: 'testUser',
}), }),
).rejects.toThrow( ).rejects.toThrow(
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
); );
}); });
}); });
it('whitelistValidate should return verifyId successfully', async () => {
const surveyPath = '';
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: {
status: 'published',
},
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
whitelistType: 'CUSTOM',
memberType: 'MOBILE',
whitelist: ['13500000000'],
},
},
} as ResponseSchema);
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
whitelist: '13500000000',
}),
).resolves.toEqual({ code: 200, data: null });
});
}); });

View File

@ -20,6 +20,7 @@ describe('ResponseSchemaService', () => {
findOne: jest.fn().mockResolvedValue(mockResponseSchema), findOne: jest.fn().mockResolvedValue(mockResponseSchema),
create: jest.fn(), create: jest.fn(),
save: jest.fn(), save: jest.fn(),
updateOne: jest.fn(),
}, },
}, },
], ],
@ -120,22 +121,24 @@ describe('ResponseSchemaService', () => {
describe('deleteResponseSchema', () => { describe('deleteResponseSchema', () => {
it('should delete response schema by survey path', async () => { it('should delete response schema by survey path', async () => {
jest jest
.spyOn(responseSchemaRepository, 'findOne') .spyOn(responseSchemaRepository, 'updateOne')
.mockResolvedValueOnce(cloneDeep(mockResponseSchema)); .mockResolvedValueOnce(cloneDeep(mockResponseSchema));
jest
.spyOn(responseSchemaRepository, 'save')
.mockResolvedValueOnce(undefined);
await service.deleteResponseSchema({ await service.deleteResponseSchema({
surveyPath: mockResponseSchema.surveyPath, surveyPath: mockResponseSchema.surveyPath,
}); });
expect(responseSchemaRepository.findOne).toHaveBeenCalledWith({ expect(responseSchemaRepository.updateOne).toHaveBeenCalledWith(
where: { {
surveyPath: mockResponseSchema.surveyPath, surveyPath: mockResponseSchema.surveyPath,
}, },
}); {
expect(responseSchemaRepository.save).toHaveBeenCalledTimes(1); $set: {
isDeleted: true,
updatedAt: new Date(),
},
},
);
}); });
}); });
}); });

View File

@ -18,7 +18,7 @@ import { HttpException } from 'src/exceptions/httpException';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin'; import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
import { RECORD_STATUS } from 'src/enums'; import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { ResponseSchema } from 'src/models/responseSchema.entity'; import { ResponseSchema } from 'src/models/responseSchema.entity';
@ -71,8 +71,8 @@ const mockClientEncryptInfo = {
date: 1710399425273.0, date: 1710399425273.0,
}, },
], ],
createDate: 1710399425273.0, createdAt: 1710399425273.0,
updateDate: 1710399425273.0, updatedAt: 1710399425273.0,
}; };
describe('SurveyResponseController', () => { describe('SurveyResponseController', () => {
@ -178,7 +178,7 @@ describe('SurveyResponseController', () => {
.mockResolvedValueOnce({ .mockResolvedValueOnce({
_id: new ObjectId('65fc2dd77f4520858046e129'), _id: new ObjectId('65fc2dd77f4520858046e129'),
clientTime: 1711025112552, clientTime: 1711025112552,
createDate: 1711025113146, createdAt: 1711025113146,
curStatus: { curStatus: {
status: RECORD_STATUS.NEW, status: RECORD_STATUS.NEW,
date: 1711025113146, date: 1711025113146,
@ -212,7 +212,7 @@ describe('SurveyResponseController', () => {
], ],
surveyPath: 'EBzdmnSp', surveyPath: 'EBzdmnSp',
updateDate: 1711025113146, updatedAt: 1711025113146,
secretKeys: [], secretKeys: [],
} as unknown as SurveyResponse); } as unknown as SurveyResponse);
jest jest
@ -334,6 +334,9 @@ describe('SurveyResponseController', () => {
curStatus: { curStatus: {
status: RECORD_STATUS.PUBLISHED, status: RECORD_STATUS.PUBLISHED,
}, },
subStatus: {
status: RECORD_SUB_STATUS.DEFAULT,
},
code: { code: {
baseConf: { baseConf: {
passwordSwitch: true, passwordSwitch: true,

View File

@ -18,6 +18,7 @@ describe('SurveyResponseService', () => {
create: jest.fn(), create: jest.fn(),
save: jest.fn(), save: jest.fn(),
count: jest.fn(), count: jest.fn(),
find: jest.fn(),
}, },
}, },
], ],
@ -69,16 +70,12 @@ describe('SurveyResponseService', () => {
it('should get the total survey response count by path', async () => { it('should get the total survey response count by path', async () => {
const surveyPath = 'testPath'; const surveyPath = 'testPath';
const count = 10; const count = 10;
jest.spyOn(surveyResponseRepository, 'count').mockResolvedValue(count); jest
.spyOn(surveyResponseRepository, 'find')
.mockResolvedValue(new Array(10));
const result = await service.getSurveyResponseTotalByPath(surveyPath); const result = await service.getSurveyResponseTotalByPath(surveyPath);
expect(result).toEqual(count); expect(result).toEqual(count);
expect(surveyResponseRepository.count).toHaveBeenCalledWith({
where: {
surveyPath,
'curStatus.status': { $ne: 'removed' },
},
});
}); });
}); });

View File

@ -10,7 +10,7 @@ import {
import { ResponseSchemaService } from '../services/responseScheme.service'; import { ResponseSchemaService } from '../services/responseScheme.service';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { RECORD_STATUS } from 'src/enums'; import { RECORD_SUB_STATUS } from 'src/enums';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import Joi from 'joi'; import Joi from 'joi';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
@ -44,16 +44,20 @@ export class ResponseSchemaController {
await this.responseSchemaService.getResponseSchemaByPath( await this.responseSchemaService.getResponseSchemaByPath(
queryInfo.surveyPath, queryInfo.surveyPath,
); );
if ( if (!responseSchema || responseSchema.isDeleted) {
!responseSchema ||
responseSchema.curStatus.status === RECORD_STATUS.REMOVED
) {
throw new HttpException( throw new HttpException(
'问卷已删除', '问卷不存在或已删除',
EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED, EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED,
); );
} }
if (responseSchema.subStatus.status === RECORD_SUB_STATUS.PAUSING) {
throw new HttpException(
'该问卷已暂停回收',
EXCEPTION_CODE.RESPONSE_PAUSING,
);
}
// 去掉C端的敏感字段 // 去掉C端的敏感字段
if (responseSchema.code?.baseConf) { if (responseSchema.code?.baseConf) {
responseSchema.code.baseConf.password = null; responseSchema.code.baseConf.password = null;
@ -82,7 +86,7 @@ export class ResponseSchemaController {
// 问卷信息 // 问卷信息
const schema = const schema =
await this.responseSchemaService.getResponseSchemaByPath(surveyPath); await this.responseSchemaService.getResponseSchemaByPath(surveyPath);
if (!schema || schema.curStatus.status === 'removed') { if (!schema || schema.isDeleted) {
throw new SurveyNotFoundException('该问卷不存在,无法提交'); throw new SurveyNotFoundException('该问卷不存在,无法提交');
} }

View File

@ -5,6 +5,7 @@ import { checkSign } from 'src/utils/checkSign';
import { ENCRYPT_TYPE } from 'src/enums/encrypt'; import { ENCRYPT_TYPE } from 'src/enums/encrypt';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { getPushingData } from 'src/utils/messagePushing'; import { getPushingData } from 'src/utils/messagePushing';
import { RECORD_SUB_STATUS } from 'src/enums';
import { ResponseSchemaService } from '../services/responseScheme.service'; import { ResponseSchemaService } from '../services/responseScheme.service';
import { SurveyResponseService } from '../services/surveyResponse.service'; import { SurveyResponseService } from '../services/surveyResponse.service';
@ -82,9 +83,15 @@ export class SurveyResponseController {
// 查询schema // 查询schema
const responseSchema = const responseSchema =
await this.responseSchemaService.getResponseSchemaByPath(surveyPath); await this.responseSchemaService.getResponseSchemaByPath(surveyPath);
if (!responseSchema || responseSchema.curStatus.status === 'removed') { if (!responseSchema || responseSchema.isDeleted) {
throw new SurveyNotFoundException('该问卷不存在,无法提交'); throw new SurveyNotFoundException('该问卷不存在,无法提交');
} }
if (responseSchema?.subStatus?.status === RECORD_SUB_STATUS.PAUSING) {
throw new HttpException(
'该问卷已暂停,无法提交',
EXCEPTION_CODE.RESPONSE_PAUSING,
);
}
// 白名单的verifyId校验 // 白名单的verifyId校验
const baseConf = responseSchema.code.baseConf; const baseConf = responseSchema.code.baseConf;
@ -132,12 +139,12 @@ export class SurveyResponseController {
const now = Date.now(); const now = Date.now();
// 提交时间限制 // 提交时间限制
const begTime = responseSchema.code?.baseConf?.begTime || 0; const beginTime = responseSchema.code?.baseConf?.beginTime || 0;
const endTime = responseSchema?.code?.baseConf?.endTime || 0; const endTime = responseSchema?.code?.baseConf?.endTime || 0;
if (begTime && endTime) { if (beginTime && endTime) {
const begTimeStamp = new Date(begTime).getTime(); const beginTimeStamp = new Date(beginTime).getTime();
const endTimeStamp = new Date(endTime).getTime(); const endTimeStamp = new Date(endTime).getTime();
if (now < begTimeStamp || now > endTimeStamp) { if (now < beginTimeStamp || now > endTimeStamp) {
throw new HttpException( throw new HttpException(
'不在答题有效期内', '不在答题有效期内',
EXCEPTION_CODE.RESPONSE_CURRENT_TIME_NOT_ALLOW, EXCEPTION_CODE.RESPONSE_CURRENT_TIME_NOT_ALLOW,

View File

@ -4,7 +4,6 @@ import { MongoRepository } from 'typeorm';
import { ClientEncrypt } from 'src/models/clientEncrypt.entity'; import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
import { ENCRYPT_TYPE } from 'src/enums/encrypt'; import { ENCRYPT_TYPE } from 'src/enums/encrypt';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
@Injectable() @Injectable()
export class ClientEncryptService { export class ClientEncryptService {
@ -38,26 +37,13 @@ export class ClientEncryptService {
return this.clientEncryptRepository.findOne({ return this.clientEncryptRepository.findOne({
where: { where: {
_id: new ObjectId(id), _id: new ObjectId(id),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}, },
}); });
} }
deleteEncryptInfo(id: string) { deleteEncryptInfo(id: string) {
return this.clientEncryptRepository.updateOne( return this.clientEncryptRepository.deleteOne({
{
_id: new ObjectId(id), _id: new ObjectId(id),
}, });
{
$set: {
curStatus: {
status: RECORD_STATUS.REMOVED,
date: Date.now(),
},
},
},
);
} }
} }

View File

@ -33,6 +33,7 @@ export class CounterService {
surveyPath, surveyPath,
type, type,
data, data,
updatedAt: new Date(),
}, },
}, },
{ {

View File

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm'; import { MongoRepository } from 'typeorm';
import { ResponseSchema } from 'src/models/responseSchema.entity'; import { ResponseSchema } from 'src/models/responseSchema.entity';
import { RECORD_STATUS } from 'src/enums'; import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
@Injectable() @Injectable()
export class ResponseSchemaService { export class ResponseSchemaService {
@ -23,19 +23,27 @@ export class ResponseSchemaService {
status: RECORD_STATUS.PUBLISHED, status: RECORD_STATUS.PUBLISHED,
date: Date.now(), date: Date.now(),
}; };
clientSurvey.subStatus = {
status: RECORD_SUB_STATUS.DEFAULT,
date: Date.now(),
};
return this.responseSchemaRepository.save(clientSurvey); return this.responseSchemaRepository.save(clientSurvey);
} else { } else {
const curStatus = { const curStatus = {
status: RECORD_STATUS.PUBLISHED, status: RECORD_STATUS.PUBLISHED,
date: Date.now(), date: Date.now(),
}; };
const subStatus = {
status: RECORD_SUB_STATUS.DEFAULT,
date: Date.now(),
};
const newClientSurvey = this.responseSchemaRepository.create({ const newClientSurvey = this.responseSchemaRepository.create({
title, title,
surveyPath, surveyPath,
code, code,
pageId, pageId,
curStatus, curStatus,
statusList: [curStatus], subStatus,
}); });
return this.responseSchemaRepository.save(newClientSurvey); return this.responseSchemaRepository.save(newClientSurvey);
} }
@ -53,22 +61,32 @@ export class ResponseSchemaService {
}); });
} }
async deleteResponseSchema({ surveyPath }) { async pausingResponseSchema({ surveyPath }) {
const responseSchema = await this.responseSchemaRepository.findOne({ const responseSchema = await this.responseSchemaRepository.findOne({
where: { surveyPath }, where: { surveyPath },
}); });
if (responseSchema) { if (responseSchema) {
const newStatus = { const subStatus = {
status: RECORD_STATUS.PUBLISHED, status: RECORD_SUB_STATUS.PAUSING,
date: Date.now(), date: Date.now(),
}; };
responseSchema.curStatus = newStatus; responseSchema.subStatus = subStatus;
if (Array.isArray(responseSchema.statusList)) { responseSchema.curStatus.status = RECORD_STATUS.PUBLISHED;
responseSchema.statusList.push(newStatus);
} else {
responseSchema.statusList = [newStatus];
}
return this.responseSchemaRepository.save(responseSchema); return this.responseSchemaRepository.save(responseSchema);
} }
} }
async deleteResponseSchema({ surveyPath }) {
return this.responseSchemaRepository.updateOne(
{
surveyPath,
},
{
$set: {
isDeleted: true,
updatedAt: new Date(),
},
},
);
}
} }

View File

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm'; import { MongoRepository } from 'typeorm';
import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { RECORD_STATUS } from 'src/enums';
@Injectable() @Injectable()
export class SurveyResponseService { export class SurveyResponseService {
constructor( constructor(
@ -39,9 +39,6 @@ export class SurveyResponseService {
const data = await this.surveyResponseRepository.find({ const data = await this.surveyResponseRepository.find({
where: { where: {
surveyPath, surveyPath,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}, },
}); });
return (data || []).length; return (data || []).length;

View File

@ -0,0 +1,21 @@
import { Controller, Get, HttpCode, Request } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { UpgradeService } from '../services/upgrade.service';
@ApiTags('survey')
@Controller('/api/upgrade')
export class UpgradeController {
constructor(private readonly upgradeService: UpgradeService) {}
@Get('/upgradeFeatureStatus')
@HttpCode(200)
async upgradeSubStatus(@Request() req) {
this.upgradeService.upgradeFeatureStatus();
return {
code: 200,
data: {
traceId: req.traceId,
},
};
}
}

View File

@ -0,0 +1,210 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { ResponseSchema } from 'src/models/responseSchema.entity';
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
import { Workspace } from 'src/models/workspace.entity';
import { Collaborator } from 'src/models/collaborator.entity';
import { Counter } from 'src/models/counter.entity';
import { DownloadTask } from 'src/models/downloadTask.entity';
import { MessagePushingLog } from 'src/models/messagePushingLog.entity';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { Session } from 'src/models/session.entity';
import { SurveyConf } from 'src/models/surveyConf.entity';
import { User } from 'src/models/user.entity';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { SESSION_STATUS } from 'src/enums/surveySessionStatus';
import { Logger } from 'src/logger';
@Injectable()
export class UpgradeService {
constructor(
private readonly logger: Logger,
@InjectRepository(Collaborator)
private readonly collaboratorRepository: MongoRepository<Collaborator>,
@InjectRepository(Counter)
private readonly counterRepository: MongoRepository<Counter>,
@InjectRepository(DownloadTask)
private readonly downloadTaskRepository: MongoRepository<DownloadTask>,
@InjectRepository(MessagePushingLog)
private readonly messagePushingLogRepository: MongoRepository<MessagePushingLog>,
@InjectRepository(MessagePushingTask)
private readonly messagePushingTaskRepository: MongoRepository<MessagePushingTask>,
@InjectRepository(ResponseSchema)
private readonly responseSchemaRepository: MongoRepository<ResponseSchema>,
@InjectRepository(Session)
private readonly sessionRepository: MongoRepository<Session>,
@InjectRepository(SurveyConf)
private readonly surveyConfRepository: MongoRepository<SurveyConf>,
@InjectRepository(SurveyMeta)
private readonly surveyMetaRepository: MongoRepository<SurveyMeta>,
@InjectRepository(User)
private readonly userRepository: MongoRepository<User>,
@InjectRepository(Workspace)
private readonly workspaceRepository: MongoRepository<Workspace>,
@InjectRepository(WorkspaceMember)
private readonly workspaceMemberRepository: MongoRepository<WorkspaceMember>,
) {}
async upgradeFeatureStatus() {
const repositories = [
this.collaboratorRepository,
this.counterRepository,
this.downloadTaskRepository,
this.messagePushingLogRepository,
this.messagePushingTaskRepository,
this.responseSchemaRepository,
this.sessionRepository,
this.surveyConfRepository,
this.surveyMetaRepository,
this.userRepository,
this.workspaceRepository,
this.workspaceMemberRepository,
];
const handleCreatedAtAndUpdatedAt = (doc) => {
if (!doc.createdAt) {
if (doc.createDate) {
doc.createdAt = new Date(doc.createDate);
delete doc.createDate;
} else {
doc.createdAt = new Date();
}
}
if (!doc.updatedAt) {
if (doc.updateDate) {
doc.updatedAt = new Date(doc.updateDate);
delete doc.updateDate;
} else {
doc.updatedAt = new Date();
}
}
};
const handleDelStatus = (doc) => {
// 已删除的字段升级
if (doc?.curStatus?.status === 'removed') {
delete doc.curStatus;
doc.isDeleted = true;
doc.deletedAt = new Date(doc.updatedAt);
}
};
const handleSubStatus = (doc) => {
// 编辑中字段升级
if (
!doc?.subStatus &&
(doc?.curStatus?.status == RECORD_STATUS.PUBLISHED ||
doc?.curStatus?.status == RECORD_STATUS.NEW ||
doc?.curStatus?.status === RECORD_STATUS.EDITING)
) {
const subStatus = {
status: RECORD_SUB_STATUS.DEFAULT,
date: doc.curStatus.date,
};
doc.subStatus = subStatus;
}
};
const handleBegTime = (doc) => {
if (!doc?.baseConf?.beginTime && doc?.baseConf?.begTime) {
doc.baseConf.beginTime = doc.baseConf.begTime;
delete doc.baseConf.begTime;
}
};
const handleSessionStatus = (doc) => {
if (!doc.status && doc.curStatus) {
if (doc?.curStatus?.id && doc?.curStatus?.id === 'editing') {
doc.status = SESSION_STATUS.ACTIVATED;
} else {
doc.status = SESSION_STATUS.DEACTIVATED;
}
delete doc.curStatus;
}
};
const handleCreatorId = async (doc) => {
if (!doc.ownerId && doc.owner) {
const userInfo = await this.userRepository.findOne({
where: {
username: doc.owner,
},
});
if (userInfo && userInfo._id) {
doc.ownerId = userInfo._id.toString();
}
}
if (doc.ownerId && doc.owner && !doc.creatorId) {
doc.creatorId = doc.ownerId;
doc.creator = doc.owner;
}
};
const save = async ({ doc, repository }) => {
const entity = repository.create(doc);
await repository.updateOne(
{
_id: entity._id,
},
{
$set: entity,
},
);
// this.logger.info(JSON.stringify(updateRes));
};
this.logger.info(`upgrading...`);
for (const repository of repositories) {
const name =
typeof repository.target === 'function'
? repository.target.name
: typeof repository.target === 'string'
? repository.target
: '';
const cursor = repository.createCursor();
this.logger.info(`upgrading ${name}`);
while (await cursor.hasNext()) {
try {
const doc = await cursor.next();
// 把createDate和updateDate升级成createdAt和updatedAt
handleCreatedAtAndUpdatedAt(doc);
if (
repository === this.surveyMetaRepository ||
repository === this.responseSchemaRepository
) {
// 新增subStatus字段
handleSubStatus(doc);
}
if (
repository === this.surveyMetaRepository ||
repository === this.downloadTaskRepository ||
repository === this.messagePushingTaskRepository ||
repository === this.workspaceRepository ||
repository === this.responseSchemaRepository
) {
// 新增isDeleted等相关字段
handleDelStatus(doc);
}
// 同步sessionStatus到新定义的字段
if (repository === this.sessionRepository) {
handleSessionStatus(doc);
}
// 同步begTime更新成beginTime
if (repository === this.surveyConfRepository) {
handleBegTime(doc);
}
// 同步ownerId到creatorId
if (repository === this.surveyMetaRepository) {
await handleCreatorId(doc);
}
await save({ repository, doc });
} catch (error) {
this.logger.error(`upgrade ${name} error ${error.message}`);
}
}
this.logger.info(`finish upgrade ${name}`);
}
this.logger.info(`upgrad finished...`);
}
}

View File

@ -0,0 +1,51 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { UpgradeService } from './services/upgrade.service';
import { Collaborator } from 'src/models/collaborator.entity';
import { Counter } from 'src/models/counter.entity';
import { DownloadTask } from 'src/models/downloadTask.entity';
import { MessagePushingLog } from 'src/models/messagePushingLog.entity';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { ResponseSchema } from 'src/models/responseSchema.entity';
import { Session } from 'src/models/session.entity';
import { SurveyConf } from 'src/models/surveyConf.entity';
import { SurveyHistory } from 'src/models/surveyHistory.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { User } from 'src/models/user.entity';
import { Workspace } from 'src/models/workspace.entity';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { UpgradeController } from './controllers/upgrade.controller';
import { AuthModule } from '../auth/auth.module';
import { Logger } from 'src/logger';
@Module({
imports: [
TypeOrmModule.forFeature([
Collaborator,
Counter,
DownloadTask,
MessagePushingLog,
MessagePushingTask,
ResponseSchema,
Session,
SurveyConf,
SurveyHistory,
SurveyMeta,
SurveyResponse,
User,
Workspace,
WorkspaceMember,
]),
ConfigModule,
AuthModule,
],
controllers: [UpgradeController],
providers: [UpgradeService, Logger],
exports: [UpgradeService],
})
export class UpgradeModule {}

View File

@ -12,6 +12,7 @@ import { UserService } from 'src/modules/auth/services/user.service';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { User } from 'src/models/user.entity'; import { User } from 'src/models/user.entity';
import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto';
jest.mock('src/guards/authentication.guard'); jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard'); jest.mock('src/guards/survey.guard');
@ -22,6 +23,7 @@ describe('WorkspaceController', () => {
let workspaceService: WorkspaceService; let workspaceService: WorkspaceService;
let workspaceMemberService: WorkspaceMemberService; let workspaceMemberService: WorkspaceMemberService;
let userService: UserService; let userService: UserService;
let logger: Logger;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@ -31,7 +33,6 @@ describe('WorkspaceController', () => {
provide: WorkspaceService, provide: WorkspaceService,
useValue: { useValue: {
create: jest.fn(), create: jest.fn(),
findAllById: jest.fn(),
findAllByIdWithPagination: jest.fn(), findAllByIdWithPagination: jest.fn(),
update: jest.fn(), update: jest.fn(),
delete: jest.fn(), delete: jest.fn(),
@ -47,7 +48,6 @@ describe('WorkspaceController', () => {
batchUpdate: jest.fn(), batchUpdate: jest.fn(),
batchDelete: jest.fn(), batchDelete: jest.fn(),
countByWorkspaceId: jest.fn(), countByWorkspaceId: jest.fn(),
batchSearchByWorkspace: jest.fn(),
}, },
}, },
{ {
@ -80,24 +80,26 @@ describe('WorkspaceController', () => {
WorkspaceMemberService, WorkspaceMemberService,
); );
userService = module.get<UserService>(UserService); userService = module.get<UserService>(UserService);
logger = module.get<Logger>(Logger);
}); });
describe('create', () => { describe('create', () => {
it('should create a workspace and return workspaceId', async () => { it('should create a workspace and return workspaceId', async () => {
const mockUserId = new ObjectId(),
mockUsername = 'username';
const createWorkspaceDto: CreateWorkspaceDto = { const createWorkspaceDto: CreateWorkspaceDto = {
name: 'Test Workspace', name: 'Test Workspace',
description: 'Test Description', description: 'Test Description',
members: [{ userId: 'userId1', role: WORKSPACE_ROLE.USER }], members: [{ userId: mockUserId.toString(), role: WORKSPACE_ROLE.USER }],
}; };
const req = { user: { _id: new ObjectId() } }; const req = { user: { _id: new ObjectId(), username: 'testuser' } };
const createdWorkspace = { _id: new ObjectId() }; const createdWorkspace = { _id: new ObjectId() };
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([ jest
{ .spyOn(userService, 'getUserListByIds')
_id: 'userId1', .mockResolvedValue([
}, { _id: mockUserId, username: mockUsername },
] as unknown as Array<User>); ] as unknown as Array<User>);
jest jest
.spyOn(workspaceService, 'create') .spyOn(workspaceService, 'create')
.mockResolvedValue(createdWorkspace as Workspace); .mockResolvedValue(createdWorkspace as Workspace);
@ -113,6 +115,7 @@ describe('WorkspaceController', () => {
expect(workspaceService.create).toHaveBeenCalledWith({ expect(workspaceService.create).toHaveBeenCalledWith({
name: createWorkspaceDto.name, name: createWorkspaceDto.name,
description: createWorkspaceDto.description, description: createWorkspaceDto.description,
owner: req.user.username,
ownerId: req.user._id.toString(), ownerId: req.user._id.toString(),
}); });
expect(workspaceMemberService.create).toHaveBeenCalledWith({ expect(workspaceMemberService.create).toHaveBeenCalledWith({
@ -123,31 +126,31 @@ describe('WorkspaceController', () => {
expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({ expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({
workspaceId: createdWorkspace._id.toString(), workspaceId: createdWorkspace._id.toString(),
members: createWorkspaceDto.members, members: createWorkspaceDto.members,
creator: req.user.username,
creatorId: req.user._id.toString(),
}); });
}); });
it('should throw an exception if validation fails', async () => { it('should throw an exception if validation fails', async () => {
const createWorkspaceDto: CreateWorkspaceDto = { const createWorkspaceDto = { name: '', members: [] };
name: '',
members: [],
};
const req = { user: { _id: new ObjectId() } }; const req = { user: { _id: new ObjectId() } };
await expect(controller.create(createWorkspaceDto, req)).rejects.toThrow( await expect(controller.create(createWorkspaceDto, req)).rejects.toThrow(
HttpException, HttpException,
); );
expect(logger.error).toHaveBeenCalledTimes(1);
}); });
}); });
describe('findAll', () => { describe('findAll', () => {
it('should return a list of workspaces for the user', async () => { it('should return a list of workspaces for the user', async () => {
const req = { user: { _id: new ObjectId() } }; const req = { user: { _id: new ObjectId() } };
const queryInfo: GetWorkspaceListDto = { curPage: 1, pageSize: 10 };
const memberList = [{ workspaceId: new ObjectId().toString() }]; const memberList = [{ workspaceId: new ObjectId().toString() }];
const workspaces = [{ _id: new ObjectId(), name: 'Test Workspace' }]; const workspaces = [{ _id: new ObjectId(), name: 'Test Workspace' }];
jest jest
.spyOn(workspaceMemberService, 'findAllByUserId') .spyOn(workspaceMemberService, 'findAllByUserId')
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>); .mockResolvedValue(memberList as Array<WorkspaceMember>);
jest jest
.spyOn(workspaceService, 'findAllByIdWithPagination') .spyOn(workspaceService, 'findAllByIdWithPagination')
@ -156,12 +159,11 @@ describe('WorkspaceController', () => {
count: workspaces.length, count: workspaces.length,
}); });
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([]); jest
.spyOn(userService, 'getUserListByIds')
.mockResolvedValue([{ _id: new ObjectId() }] as unknown as Array<User>);
const result = await controller.findAll(req, { const result = await controller.findAll(req, queryInfo);
curPage: 1,
pageSize: 10,
});
expect(result.code).toEqual(200); expect(result.code).toEqual(200);
expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({ expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({
@ -174,6 +176,18 @@ describe('WorkspaceController', () => {
name: undefined, name: undefined,
}); });
}); });
it('should throw an exception if validation fails', async () => {
const req = { user: { _id: new ObjectId() } };
const queryInfo: GetWorkspaceListDto = {
curPage: 'not_a_number',
pageSize: 10,
} as any;
await expect(controller.findAll(req, queryInfo)).rejects.toThrow(
HttpException,
);
});
}); });
describe('update', () => { describe('update', () => {
@ -185,11 +199,9 @@ describe('WorkspaceController', () => {
adminMembers: [], adminMembers: [],
userMembers: [], userMembers: [],
}; };
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([ jest
{ .spyOn(userService, 'getUserListByIds')
_id: userId, .mockResolvedValue([{ _id: userId }] as Array<User>);
},
] as Array<User>);
const updateDto = { const updateDto = {
name: 'Updated Workspace', name: 'Updated Workspace',
members: [ members: [
@ -203,72 +215,41 @@ describe('WorkspaceController', () => {
jest.spyOn(workspaceService, 'update').mockResolvedValue(updateResult); jest.spyOn(workspaceService, 'update').mockResolvedValue(updateResult);
jest.spyOn(workspaceMemberService, 'batchCreate').mockResolvedValue(null); jest.spyOn(workspaceMemberService, 'batchCreate').mockResolvedValue(null);
jest.spyOn(workspaceMemberService, 'batchUpdate').mockResolvedValue(null); jest.spyOn(workspaceMemberService, 'batchUpdate').mockResolvedValue(null);
jest.spyOn(workspaceMemberService, 'batchDelete').mockResolvedValue(null);
const result = await controller.update(id, updateDto); const result = await controller.update(id, updateDto, {
user: { username: 'testuser', _id: new ObjectId() },
expect(result).toEqual({
code: 200,
}); });
expect(workspaceService.update).toHaveBeenCalledWith(id, {
name: updateDto.name, expect(result).toEqual({ code: 200 });
expect(workspaceService.update).toHaveBeenCalledWith({
id,
workspace: { name: updateDto.name },
operator: 'testuser',
operatorId: expect.any(String),
}); });
expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({ expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({
workspaceId: id, workspaceId: id,
members: members.newMembers, members: members.newMembers,
creator: 'testuser',
creatorId: expect.any(String),
}); });
expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({ expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({
idList: members.adminMembers, idList: members.adminMembers,
role: WORKSPACE_ROLE.ADMIN, role: WORKSPACE_ROLE.ADMIN,
operator: 'testuser',
operatorId: expect.any(String),
}); });
expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({ expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({
idList: members.userMembers, idList: members.userMembers,
role: WORKSPACE_ROLE.USER, role: WORKSPACE_ROLE.USER,
operator: 'testuser',
operatorId: expect.any(String),
}); });
expect(workspaceMemberService.batchDelete).toHaveBeenCalledWith({
idList: [],
neIdList: [],
}); });
}); });
describe('delete', () => {
it('should delete a workspace', async () => {
const id = 'workspaceId';
jest.spyOn(workspaceService, 'delete').mockResolvedValue(null);
const result = await controller.delete(id);
expect(result).toEqual({ code: 200 });
expect(workspaceService.delete).toHaveBeenCalledWith(id);
});
});
describe('getWorkspaceAndMember', () => {
it('should return a list of workspaces and members for the user', async () => {
const req = { user: { _id: new ObjectId() } };
const workspaceId = new ObjectId();
const userId = new ObjectId();
const memberList = [{ workspaceId, userId: userId }];
const workspaces = [{ _id: workspaceId, name: 'Test Workspace' }];
const userList = [{ _id: userId, username: 'Test User' }];
jest
.spyOn(workspaceService, 'findAllByUserId')
.mockResolvedValue(workspaces as Array<Workspace>);
jest
.spyOn(workspaceMemberService, 'batchSearchByWorkspace')
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
jest
.spyOn(userService, 'getUserListByIds')
.mockResolvedValue(userList as User[]);
const result = await controller.getWorkspaceAndMember(req);
expect(result.code).toEqual(200);
expect(workspaceService.findAllByUserId).toHaveBeenCalledWith(
req.user._id.toString(),
);
expect(
workspaceMemberService.batchSearchByWorkspace,
).toHaveBeenCalledWith(workspaces.map((item) => item._id.toString()));
});
}); });
}); });

View File

@ -45,6 +45,7 @@ describe('WorkspaceService', () => {
const workspace = { const workspace = {
name: 'Test Workspace', name: 'Test Workspace',
description: 'Description', description: 'Description',
owner: 'Test Owner', // 添加 owner 属性
ownerId: 'ownerId', ownerId: 'ownerId',
}; };
const createdWorkspace = { ...workspace, _id: new ObjectId() }; const createdWorkspace = { ...workspace, _id: new ObjectId() };
@ -59,7 +60,11 @@ describe('WorkspaceService', () => {
const result = await service.create(workspace); const result = await service.create(workspace);
expect(result).toEqual(createdWorkspace); expect(result).toEqual(createdWorkspace);
expect(workspaceRepository.create).toHaveBeenCalledWith(workspace); expect(workspaceRepository.create).toHaveBeenCalledWith({
...workspace,
creator: workspace.owner,
creatorId: workspace.ownerId,
});
expect(workspaceRepository.save).toHaveBeenCalledWith(createdWorkspace); expect(workspaceRepository.save).toHaveBeenCalledWith(createdWorkspace);
}); });
}); });
@ -90,24 +95,35 @@ describe('WorkspaceService', () => {
it('should update a workspace', async () => { it('should update a workspace', async () => {
const workspaceId = 'workspaceId'; const workspaceId = 'workspaceId';
const updateData = { name: 'Updated Workspace' }; const updateData = { name: 'Updated Workspace' };
const operator = 'Test Operator';
const operatorId = 'operatorId';
jest jest
.spyOn(workspaceRepository, 'update') .spyOn(workspaceRepository, 'update')
.mockResolvedValue({ affected: 1 } as any); .mockResolvedValue({ affected: 1 } as any);
const result = await service.update(workspaceId, updateData); const result = await service.update({
id: workspaceId,
workspace: updateData,
operator,
operatorId,
});
expect(result).toEqual({ affected: 1 }); expect(result).toEqual({ affected: 1 });
expect(workspaceRepository.update).toHaveBeenCalledWith( expect(workspaceRepository.update).toHaveBeenCalledWith(workspaceId, {
workspaceId, ...updateData,
updateData, updatedAt: expect.any(Date),
); operator,
operatorId,
});
}); });
}); });
describe('delete', () => { describe('delete', () => {
it('should delete a workspace and update related surveyMeta', async () => { it('should delete a workspace and update related surveyMeta', async () => {
const workspaceId = new ObjectId().toString(); const workspaceId = new ObjectId().toString();
const operator = 'Test Operator';
const operatorId = 'operatorId';
jest jest
.spyOn(workspaceRepository, 'updateOne') .spyOn(workspaceRepository, 'updateOne')
@ -116,11 +132,17 @@ describe('WorkspaceService', () => {
.spyOn(surveyMetaRepository, 'updateMany') .spyOn(surveyMetaRepository, 'updateMany')
.mockResolvedValue({ modifiedCount: 1 } as any); .mockResolvedValue({ modifiedCount: 1 } as any);
await service.delete(workspaceId); const result = await service.delete(workspaceId, {
operator,
operatorId,
});
expect(workspaceRepository.updateOne).toHaveBeenCalledTimes(1); expect(workspaceRepository.updateOne).toHaveBeenCalledTimes(1);
expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1); expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1);
expect(result).toEqual({
workspaceRes: { modifiedCount: 1 },
surveyRes: { modifiedCount: 1 },
});
}); });
}); });
@ -139,9 +161,43 @@ describe('WorkspaceService', () => {
.spyOn(workspaceRepository, 'find') .spyOn(workspaceRepository, 'find')
.mockResolvedValue(workspaces as any); .mockResolvedValue(workspaces as any);
const result = await service.findAllByUserId(''); const result = await service.findAllByUserId('userId');
expect(result).toEqual(workspaces); expect(result).toEqual(workspaces);
expect(workspaceRepository.find).toHaveBeenCalledTimes(1); expect(workspaceRepository.find).toHaveBeenCalledTimes(1);
}); });
}); });
describe('findAllByIdWithPagination', () => {
it('should return paginated workspaces', async () => {
const workspaceIdList = [
new ObjectId().toString(),
new ObjectId().toString(),
];
const page = 1;
const limit = 10;
const workspaces = [
{ _id: workspaceIdList[0], name: 'Workspace 1' },
{ _id: workspaceIdList[1], name: 'Workspace 2' },
];
jest
.spyOn(workspaceRepository, 'findAndCount')
.mockResolvedValue([workspaces, workspaces.length] as any);
const result = await service.findAllByIdWithPagination({
workspaceIdList,
page,
limit,
});
expect(result).toEqual({ list: workspaces, count: workspaces.length });
expect(workspaceRepository.findAndCount).toHaveBeenCalledWith({
where: expect.any(Object),
skip: 0,
take: limit,
order: { createdAt: -1 },
});
});
});
}); });

View File

@ -115,11 +115,16 @@ describe('WorkspaceMemberController', () => {
}; };
const updateResult = { modifiedCount: 1 }; const updateResult = { modifiedCount: 1 };
// Mock request object
const req = {
user: { username: 'admin', _id: 'operatorId' },
};
jest jest
.spyOn(workspaceMemberService, 'updateRole') .spyOn(workspaceMemberService, 'updateRole')
.mockResolvedValue(updateResult); .mockResolvedValue(updateResult);
const result = await controller.updateRole(updateDto); const result = await controller.updateRole(updateDto, req);
expect(result).toEqual({ expect(result).toEqual({
code: 200, code: 200,
@ -128,9 +133,11 @@ describe('WorkspaceMemberController', () => {
}, },
}); });
expect(workspaceMemberService.updateRole).toHaveBeenCalledWith({ expect(workspaceMemberService.updateRole).toHaveBeenCalledWith({
role: updateDto.role,
workspaceId: updateDto.workspaceId, workspaceId: updateDto.workspaceId,
userId: updateDto.userId, userId: updateDto.userId,
role: updateDto.role, operator: req.user.username,
operatorId: req.user._id.toString(),
}); });
}); });
@ -140,8 +147,11 @@ describe('WorkspaceMemberController', () => {
userId: '', userId: '',
role: '', role: '',
}; };
const req = {
user: { username: 'admin', _id: 'operatorId' },
};
await expect(controller.updateRole(updateDto)).rejects.toThrow( await expect(controller.updateRole(updateDto, req)).rejects.toThrow(
HttpException, HttpException,
); );
}); });

View File

@ -58,23 +58,53 @@ describe('WorkspaceMemberService', () => {
{ userId: 'userId1', role: 'admin' }, { userId: 'userId1', role: 'admin' },
{ userId: 'userId2', role: 'user' }, { userId: 'userId2', role: 'user' },
]; ];
const dataToInsert = members.map((item) => ({ ...item, workspaceId })); const creator = 'creatorName';
const creatorId = 'creatorId';
const now = new Date();
const dataToInsert = members.map((item) => ({
...item,
workspaceId,
createdAt: now,
updatedAt: now,
creator,
creatorId,
}));
jest jest.spyOn(repository, 'insertMany').mockResolvedValueOnce({
.spyOn(repository, 'insertMany') insertedCount: members.length,
.mockResolvedValueOnce({ insertedCount: members.length } as any); } as any);
const result = await service.batchCreate({ workspaceId, members }); const result = await service.batchCreate({
workspaceId,
members,
creator,
creatorId,
});
expect(result).toEqual({ insertedCount: members.length }); expect(result).toEqual({ insertedCount: members.length });
expect(repository.insertMany).toHaveBeenCalledWith(dataToInsert); expect(repository.insertMany).toHaveBeenCalledWith(
dataToInsert.map((item) => {
return {
...item,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
};
}),
);
}); });
it('should return insertedCount 0 if no members to insert', async () => { it('should return insertedCount 0 if no members to insert', async () => {
const workspaceId = new ObjectId().toString(); const workspaceId = new ObjectId().toString();
const members = []; const members = [];
const creator = 'creatorName';
const creatorId = 'creatorId';
const result = await service.batchCreate({ workspaceId, members }); const result = await service.batchCreate({
workspaceId,
members,
creator,
creatorId,
});
expect(result).toEqual({ insertedCount: 0 }); expect(result).toEqual({ insertedCount: 0 });
}); });
@ -84,12 +114,19 @@ describe('WorkspaceMemberService', () => {
it('should batch update workspace members roles', async () => { it('should batch update workspace members roles', async () => {
const idList = [new ObjectId().toString(), new ObjectId().toString()]; const idList = [new ObjectId().toString(), new ObjectId().toString()];
const role = 'user'; const role = 'user';
const operator = 'operatorName';
const operatorId = 'operatorId';
jest jest
.spyOn(repository, 'updateMany') .spyOn(repository, 'updateMany')
.mockResolvedValue({ modifiedCount: idList.length } as any); .mockResolvedValue({ modifiedCount: idList.length } as any);
const result = await service.batchUpdate({ idList, role }); const result = await service.batchUpdate({
idList,
role,
operator,
operatorId,
});
expect(result).toEqual({ modifiedCount: idList.length }); expect(result).toEqual({ modifiedCount: idList.length });
}); });
@ -97,8 +134,15 @@ describe('WorkspaceMemberService', () => {
it('should return modifiedCount 0 if no ids to update', async () => { it('should return modifiedCount 0 if no ids to update', async () => {
const idList = []; const idList = [];
const role = 'user'; const role = 'user';
const operator = 'operatorName';
const operatorId = 'operatorId';
const result = await service.batchUpdate({ idList, role }); const result = await service.batchUpdate({
idList,
role,
operator,
operatorId,
});
expect(result).toEqual({ modifiedCount: 0 }); expect(result).toEqual({ modifiedCount: 0 });
}); });
@ -160,17 +204,25 @@ describe('WorkspaceMemberService', () => {
const workspaceId = 'workspaceId'; const workspaceId = 'workspaceId';
const userId = 'userId'; const userId = 'userId';
const role = 'admin'; const role = 'admin';
const operator = 'operatorName';
const operatorId = 'operatorId';
jest jest
.spyOn(repository, 'updateOne') .spyOn(repository, 'updateOne')
.mockResolvedValue({ modifiedCount: 1 } as any); .mockResolvedValue({ modifiedCount: 1 } as any);
const result = await service.updateRole({ workspaceId, userId, role }); const result = await service.updateRole({
workspaceId,
userId,
role,
operator,
operatorId,
});
expect(result).toEqual({ modifiedCount: 1 }); expect(result).toEqual({ modifiedCount: 1 });
expect(repository.updateOne).toHaveBeenCalledWith( expect(repository.updateOne).toHaveBeenCalledWith(
{ workspaceId, userId }, { workspaceId, userId },
{ $set: { role } }, { $set: { role, operator, operatorId, updatedAt: expect.any(Date) } },
); );
}); });
}); });

View File

@ -100,10 +100,12 @@ export class WorkspaceController {
} }
} }
const userId = req.user._id.toString(); const userId = req.user._id.toString();
const username = req.user.username;
// 插入空间表 // 插入空间表
const retWorkspace = await this.workspaceService.create({ const retWorkspace = await this.workspaceService.create({
name: value.name, name: value.name,
description: value.description, description: value.description,
owner: username,
ownerId: userId, ownerId: userId,
}); });
const workspaceId = retWorkspace._id.toString(); const workspaceId = retWorkspace._id.toString();
@ -117,6 +119,8 @@ export class WorkspaceController {
await this.workspaceMemberService.batchCreate({ await this.workspaceMemberService.batchCreate({
workspaceId, workspaceId,
members: value.members, members: value.members,
creator: username,
creatorId: userId,
}); });
} }
return { return {
@ -206,7 +210,7 @@ export class WorkspaceController {
const ownerInfo = userInfoMap?.[item.ownerId] || {}; const ownerInfo = userInfoMap?.[item.ownerId] || {};
return { return {
...item, ...item,
createDate: moment(item.createDate).format('YYYY-MM-DD HH:mm:ss'), createdAt: moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss'),
owner: ownerInfo.username, owner: ownerInfo.username,
currentUserId: curWorkspaceInfo.userId, currentUserId: curWorkspaceInfo.userId,
currentUserRole: curWorkspaceInfo.role, currentUserRole: curWorkspaceInfo.role,
@ -268,13 +272,24 @@ export class WorkspaceController {
@UseGuards(WorkspaceGuard) @UseGuards(WorkspaceGuard)
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_WORKSPACE]) @SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_WORKSPACE])
@SetMetadata('workspaceId', 'params.id') @SetMetadata('workspaceId', 'params.id')
async update(@Param('id') id: string, @Body() workspace: CreateWorkspaceDto) { async update(
@Param('id') id: string,
@Body() workspace: CreateWorkspaceDto,
@Request() req,
) {
const members = workspace.members; const members = workspace.members;
if (!Array.isArray(members) || members.length === 0) { if (!Array.isArray(members) || members.length === 0) {
throw new HttpException('成员不能为空', EXCEPTION_CODE.PARAMETER_ERROR); throw new HttpException('成员不能为空', EXCEPTION_CODE.PARAMETER_ERROR);
} }
delete workspace.members; delete workspace.members;
const updateRes = await this.workspaceService.update(id, workspace); const operator = req.user.username,
operatorId = req.user._id.toString();
const updateRes = await this.workspaceService.update({
id,
workspace,
operator,
operatorId,
});
this.logger.info(`updateRes: ${JSON.stringify(updateRes)}`); this.logger.info(`updateRes: ${JSON.stringify(updateRes)}`);
const { newMembers, adminMembers, userMembers } = splitMembers(members); const { newMembers, adminMembers, userMembers } = splitMembers(members);
if ( if (
@ -320,14 +335,20 @@ export class WorkspaceController {
this.workspaceMemberService.batchCreate({ this.workspaceMemberService.batchCreate({
workspaceId: id, workspaceId: id,
members: newMembers, members: newMembers,
creator: operator,
creatorId: operatorId,
}), }),
this.workspaceMemberService.batchUpdate({ this.workspaceMemberService.batchUpdate({
idList: adminMembers, idList: adminMembers,
role: WORKSPACE_ROLE.ADMIN, role: WORKSPACE_ROLE.ADMIN,
operator,
operatorId,
}), }),
this.workspaceMemberService.batchUpdate({ this.workspaceMemberService.batchUpdate({
idList: userMembers, idList: userMembers,
role: WORKSPACE_ROLE.USER, role: WORKSPACE_ROLE.USER,
operator,
operatorId,
}), }),
]); ]);
this.logger.info(`updateRes: ${JSON.stringify(res)}`); this.logger.info(`updateRes: ${JSON.stringify(res)}`);
@ -341,8 +362,13 @@ export class WorkspaceController {
@UseGuards(WorkspaceGuard) @UseGuards(WorkspaceGuard)
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_WORKSPACE]) @SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_WORKSPACE])
@SetMetadata('workspaceId', 'params.id') @SetMetadata('workspaceId', 'params.id')
async delete(@Param('id') id: string) { async delete(@Param('id') id: string, @Request() req) {
const res = await this.workspaceService.delete(id); const operator = req.user.username,
operatorId = req.user._id.toString();
const res = await this.workspaceService.delete(id, {
operator,
operatorId,
});
this.logger.info(`res: ${JSON.stringify(res)}`); this.logger.info(`res: ${JSON.stringify(res)}`);
return { return {
code: 200, code: 200,

View File

@ -77,7 +77,10 @@ export class WorkspaceMemberController {
@Post('updateRole') @Post('updateRole')
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_MEMBER]) @SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_MEMBER])
@SetMetadata('workspaceId', 'body.workspaceId') @SetMetadata('workspaceId', 'body.workspaceId')
async updateRole(@Body() updateDto: UpdateWorkspaceMemberDto) { async updateRole(
@Body() updateDto: UpdateWorkspaceMemberDto,
@Request() req,
) {
const { error, value } = UpdateWorkspaceMemberDto.validate(updateDto); const { error, value } = UpdateWorkspaceMemberDto.validate(updateDto);
if (error) { if (error) {
throw new HttpException( throw new HttpException(
@ -85,10 +88,14 @@ export class WorkspaceMemberController {
EXCEPTION_CODE.PARAMETER_ERROR, EXCEPTION_CODE.PARAMETER_ERROR,
); );
} }
const operator = req.user.username,
operatorId = req.user._id.toString();
const updateRes = await this.workspaceMemberService.updateRole({ const updateRes = await this.workspaceMemberService.updateRole({
role: value.role, role: value.role,
workspaceId: value.workspaceId, workspaceId: value.workspaceId,
userId: value.userId, userId: value.userId,
operator,
operatorId,
}); });
return { return {
code: 200, code: 200,

View File

@ -6,7 +6,6 @@ import { Workspace } from 'src/models/workspace.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
interface FindAllByIdWithPaginationParams { interface FindAllByIdWithPaginationParams {
workspaceIdList: string[]; workspaceIdList: string[];
@ -31,10 +30,13 @@ export class WorkspaceService {
async create(workspace: { async create(workspace: {
name: string; name: string;
description: string; description: string;
owner: string;
ownerId: string; ownerId: string;
}): Promise<Workspace> { }): Promise<Workspace> {
const newWorkspace = this.workspaceRepository.create({ const newWorkspace = this.workspaceRepository.create({
...workspace, ...workspace,
creatorId: workspace.ownerId,
creator: workspace.owner,
}); });
return this.workspaceRepository.save(newWorkspace); return this.workspaceRepository.save(newWorkspace);
} }
@ -56,8 +58,8 @@ export class WorkspaceService {
_id: { _id: {
$in: workspaceIdList.map((item) => new ObjectId(item)), $in: workspaceIdList.map((item) => new ObjectId(item)),
}, },
'curStatus.status': { isDeleted: {
$ne: RECORD_STATUS.REMOVED, $ne: true,
}, },
}; };
@ -68,11 +70,11 @@ export class WorkspaceService {
}, },
select: [ select: [
'_id', '_id',
'curStatus',
'name', 'name',
'description', 'description',
'ownerId', 'ownerId',
'createDate', 'creatorId',
'createdAt',
], ],
}); });
} }
@ -91,8 +93,8 @@ export class WorkspaceService {
_id: { _id: {
$in: workspaceIdList.map((m) => new ObjectId(m)), $in: workspaceIdList.map((m) => new ObjectId(m)),
}, },
'curStatus.status': { isDeleted: {
$ne: RECORD_STATUS.REMOVED, $ne: true,
}, },
}; };
if (name) { if (name) {
@ -103,31 +105,40 @@ export class WorkspaceService {
skip, skip,
take: limit, take: limit,
order: { order: {
createDate: -1, createdAt: -1,
}, },
}); });
return { list: data, count }; return { list: data, count };
} }
update(id: string, workspace: Partial<Workspace>) { update({
id,
workspace,
operator,
operatorId,
}: {
id: string;
workspace: Partial<Workspace>;
operator: string;
operatorId: string;
}) {
workspace.updatedAt = new Date();
workspace.operator = operator;
workspace.operatorId = operatorId;
return this.workspaceRepository.update(id, workspace); return this.workspaceRepository.update(id, workspace);
} }
async delete(id: string) { async delete(id: string, { operator, operatorId }) {
const newStatus = {
status: RECORD_STATUS.REMOVED,
date: Date.now(),
};
const workspaceRes = await this.workspaceRepository.updateOne( const workspaceRes = await this.workspaceRepository.updateOne(
{ {
_id: new ObjectId(id), _id: new ObjectId(id),
}, },
{ {
$set: { $set: {
curStatus: newStatus, isDeleted: true,
}, deletedAt: new Date(),
$push: { operator,
statusList: newStatus as never, operatorId,
}, },
}, },
); );
@ -137,10 +148,10 @@ export class WorkspaceService {
}, },
{ {
$set: { $set: {
curStatus: newStatus, isDeleted: true,
}, deletedAt: new Date(),
$push: { operator,
statusList: newStatus as never, operatorId,
}, },
}, },
); );
@ -155,8 +166,8 @@ export class WorkspaceService {
return await this.workspaceRepository.find({ return await this.workspaceRepository.find({
where: { where: {
ownerId: userId, ownerId: userId,
'curStatus.status': { isDeleted: {
$ne: RECORD_STATUS.REMOVED, $ne: true,
}, },
}, },
order: { order: {
@ -168,7 +179,7 @@ export class WorkspaceService {
'name', 'name',
'description', 'description',
'ownerId', 'ownerId',
'createDate', 'createdAt',
], ],
}); });
} }

View File

@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm'; import { MongoRepository } from 'typeorm';
import { WorkspaceMember } from 'src/models/workspaceMember.entity'; import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
@Injectable() @Injectable()
export class WorkspaceMemberService { export class WorkspaceMemberService {
@ -24,25 +23,44 @@ export class WorkspaceMemberService {
async batchCreate({ async batchCreate({
workspaceId, workspaceId,
members, members,
creator,
creatorId,
}: { }: {
workspaceId: string; workspaceId: string;
members: Array<{ userId: string; role: string }>; members: Array<{ userId: string; role: string }>;
creator: string;
creatorId: string;
}) { }) {
if (members.length === 0) { if (members.length === 0) {
return { return {
insertedCount: 0, insertedCount: 0,
}; };
} }
const now = new Date();
const dataToInsert = members.map((item) => { const dataToInsert = members.map((item) => {
return { return {
...item, ...item,
workspaceId, workspaceId,
createdAt: now,
updatedAt: now,
creator,
creatorId,
}; };
}); });
return this.workspaceMemberRepository.insertMany(dataToInsert); return this.workspaceMemberRepository.insertMany(dataToInsert);
} }
async batchUpdate({ idList, role }: { idList: Array<string>; role: string }) { async batchUpdate({
idList,
role,
operator,
operatorId,
}: {
idList: Array<string>;
role: string;
operator: string;
operatorId: string;
}) {
if (idList.length === 0) { if (idList.length === 0) {
return { return {
modifiedCount: 0, modifiedCount: 0,
@ -57,6 +75,9 @@ export class WorkspaceMemberService {
{ {
$set: { $set: {
role, role,
operator,
operatorId,
updatedAt: new Date(),
}, },
}, },
); );
@ -94,11 +115,8 @@ export class WorkspaceMemberService {
return this.workspaceMemberRepository.find({ return this.workspaceMemberRepository.find({
where: { where: {
workspaceId, workspaceId,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
}, },
}, select: ['_id', 'createdAt', 'curStatus', 'role', 'userId'],
select: ['_id', 'createDate', 'curStatus', 'role', 'userId'],
}); });
} }
@ -111,7 +129,7 @@ export class WorkspaceMemberService {
}); });
} }
async updateRole({ workspaceId, userId, role }) { async updateRole({ workspaceId, userId, role, operator, operatorId }) {
return this.workspaceMemberRepository.updateOne( return this.workspaceMemberRepository.updateOne(
{ {
workspaceId, workspaceId,
@ -120,6 +138,9 @@ export class WorkspaceMemberService {
{ {
$set: { $set: {
role, role,
operator,
operatorId,
updatedAt: new Date(),
}, },
}, },
); );
@ -135,9 +156,6 @@ export class WorkspaceMemberService {
async countByWorkspaceId({ workspaceId }) { async countByWorkspaceId({ workspaceId }) {
return this.workspaceMemberRepository.count({ return this.workspaceMemberRepository.count({
workspaceId, workspaceId,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
}); });
} }

View File

@ -1,5 +1,5 @@
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { getPushingData } from './messagePushing'; import { getPushingData } from '../messagePushing';
import { RECORD_STATUS } from 'src/enums'; import { RECORD_STATUS } from 'src/enums';
describe('getPushingData', () => { describe('getPushingData', () => {

View File

@ -20,6 +20,7 @@ export function getFilter(filterList: Array<FilterItem>) {
'remark', 'remark',
'surveyType', 'surveyType',
'curStatus.status', 'curStatus.status',
'subStatus.status',
]; ];
return filterList.reduce( return filterList.reduce(
(preItem, curItem) => { (preItem, curItem) => {
@ -61,7 +62,7 @@ export function getFilter(filterList: Array<FilterItem>) {
} }
export function getOrder(order: Array<OrderItem>) { export function getOrder(order: Array<OrderItem>) {
const allowOrderFields = ['createDate', 'updateDate', 'curStatus.date']; const allowOrderFields = ['createdAt', 'updatedAt', 'curStatus.date'];
const orderList = order.filter((orderItem) => const orderList = order.filter((orderItem) =>
allowOrderFields.includes(orderItem.field), allowOrderFields.includes(orderItem.field),

View File

@ -1,7 +1,7 @@
{ {
"name": "web", "name": "xiaoju-survey-web",
"version": "0.1.0", "version": "1.3.0",
"private": true, "description": "XIAOJUSURVEY的web端包含B端和C端应用",
"type": "module", "type": "module",
"scripts": { "scripts": {
"serve": "npm run dev", "serve": "npm run dev",
@ -41,6 +41,7 @@
"@iconify-json/ep": "^1.1.15", "@iconify-json/ep": "^1.1.15",
"@rushstack/eslint-patch": "^1.10.2", "@rushstack/eslint-patch": "^1.10.2",
"@tsconfig/node20": "^20.1.2", "@tsconfig/node20": "^20.1.2",
"@types/fs-extra": "^11.0.4",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
@ -52,6 +53,7 @@
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"eslint": "^8.49.0", "eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.17.0",
"fs-extra": "^11.2.0",
"husky": "^9.0.11", "husky": "^9.0.11",
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"prettier": "^3.0.3", "prettier": "^3.0.3",

47
web/report.ts Normal file
View File

@ -0,0 +1,47 @@
import fs from 'fs-extra'
const fsa = fs.promises
process.env.XIAOJU_SURVEY_REPORT = 'true'
const readData = async (pkg: string) => {
const id = new Date().getTime().toString()
try {
if (!fs.existsSync(pkg)) {
return {
type: 'web',
name: '',
version: '',
description: '',
id,
msg: '文件不存在'
}
}
const data = await fsa.readFile(pkg, 'utf8').catch((e) => e)
const { name, version, description } = JSON.parse(data)
return { type: 'web', name, version, description, id }
} catch (error) {
return error
}
}
const report = async () => {
if (!process.env.XIAOJU_SURVEY_REPORT) {
return
}
const res = await readData('./package.json')
// 上报
fetch &&
fetch('https://xiaojusurveysrc.didi.cn/reportSourceData', {
method: 'POST',
headers: {
Accept: 'application/json, */*',
'Content-Type': 'application/json'
},
body: JSON.stringify(res)
}).catch(() => {})
}
report()

View File

@ -16,3 +16,4 @@ export const getStatisticList = (data) => {
} }
}) })
} }

View File

@ -35,7 +35,7 @@ export const createSurvey = (data) => {
} }
export const getSurveyHistory = ({ surveyId, historyType }) => { export const getSurveyHistory = ({ surveyId, historyType }) => {
return axios.get('/surveyHisotry/getList', { return axios.get('/surveyHistory/getList', {
params: { params: {
surveyId, surveyId,
historyType historyType
@ -53,6 +53,16 @@ export const updateSurvey = (data) => {
return axios.post('/survey/updateMeta', data) return axios.post('/survey/updateMeta', data)
} }
export const pausingSurvey= (surveyId) => {
return axios.post('/survey/pausingSurvey', {
surveyId
})
}
export const upgradeSubStatus = () => {
return axios.get('/upgrade/subStatus')
}
export const getSessionId = ({ surveyId }) => { export const getSessionId = ({ surveyId }) => {
return axios.post('/session/create', { surveyId }) return axios.post('/session/create', { surveyId })
} }

View File

@ -27,9 +27,9 @@ export const spaceListConfig = {
key: 'owner', key: 'owner',
width: 150 width: 150
}, },
createDate: { createdAt: {
title: '创建时间', title: '创建时间',
key: 'createDate', key: 'createdAt',
minWidth: 200 minWidth: 200
} }
} }
@ -64,14 +64,14 @@ export const fieldConfig = {
key: 'owner', key: 'owner',
width: 140 width: 140
}, },
updateDate: { updatedAt: {
title: '更新时间', title: '更新时间',
key: 'curStatus.date', key: 'updatedAt',
minWidth: 200 minWidth: 200
}, },
createDate: { createdAt: {
title: '创建时间', title: '创建时间',
key: 'createDate', key: 'createdAt',
minWidth: 200 minWidth: 200
} }
} }
@ -103,14 +103,37 @@ export const noDownloadTaskConfig = {
img: '/imgs/icons/list-empty.webp' img: '/imgs/icons/list-empty.webp'
} }
export const statusMaps = { export const curStatus = {
new: '未发布', new: {
editing: '修改中', value: 'new',
published: '已发布', label: '未发布'
removed: '已删除', },
pausing: '' published: {
value: 'published',
label: '已发布'
},
editing: {
label: '修改中',
value: 'editing'
},
} }
// 子状态
export const subStatus = {
pausing: {
label: '暂停中',
value: 'pausing'
}
}
export const statusMaps = {
...Object.fromEntries(Object.keys(curStatus).map(key => ([key, curStatus[key].label]))),
...Object.fromEntries(Object.keys(subStatus).map(key => ([key, subStatus[key].label])))
}
export const curStatusKey = 'curStatus.status';
export const subStatusKey = 'subStatus.status';
// 问卷类型 // 问卷类型
export const surveyTypeSelect = { export const surveyTypeSelect = {
label: '问卷类型', label: '问卷类型',
@ -151,29 +174,21 @@ export const curStatusSelect = {
value: '', value: '',
label: '全部状态' label: '全部状态'
}, },
{ curStatus.new,
value: 'new', curStatus.published,
label: '未发布' curStatus.editing,
}, subStatus.pausing
{
value: 'published',
label: '已发布'
},
{
value: 'editing',
label: '修改中'
}
], ],
default: '' default: ''
} }
export const selectOptionsDict = Object.freeze({ export const selectOptionsDict = Object.freeze({
surveyType: surveyTypeSelect, surveyType: surveyTypeSelect,
'curStatus.status': curStatusSelect status: curStatusSelect,
}) })
export const buttonOptionsDict = Object.freeze({ export const buttonOptionsDict = Object.freeze({
'curStatus.date': { 'updatedAt': {
label: '更新时间', label: '更新时间',
icons: [ icons: [
{ {
@ -194,7 +209,7 @@ export const buttonOptionsDict = Object.freeze({
} }
] ]
}, },
createDate: { createdAt: {
label: '创建时间', label: '创建时间',
icons: [ icons: [
{ {

View File

@ -80,7 +80,7 @@ const popoverContent = ref('')
const getContent = (content) => { const getContent = (content) => {
if (Array.isArray(content)) { if (Array.isArray(content)) {
return content.map((item) => getContent(item)).join(',') return content.map(item => getContent(item)).join(',');
} }
if (content === null || content === undefined) { if (content === null || content === undefined) {
return '' return ''

View File

@ -194,7 +194,7 @@ const checkIsTaskFinished = (taskId) => {
const run = () => { const run = () => {
getDownloadTask(taskId).then((res) => { getDownloadTask(taskId).then((res) => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const status = res.data.curStatus.status const status = res.data.status
if (status === 'new' || status === 'computing') { if (status === 'new' || status === 'computing') {
setTimeout(() => { setTimeout(() => {
run() run()

View File

@ -26,7 +26,7 @@
<el-table-column label="操作" width="200"> <el-table-column label="操作" width="200">
<template v-slot="{ row }"> <template v-slot="{ row }">
<span <span
v-if="row.curStatus?.status === 'finished'" v-if="row?.status === 'succeed'"
class="text-btn download-btn" class="text-btn download-btn"
@click="handleDownload(row)" @click="handleDownload(row)"
> >
@ -44,7 +44,7 @@
background background
layout="prev, pager, next" layout="prev, pager, next"
:total="total" :total="total"
small size="small"
:page-size="pageSize" :page-size="pageSize"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >
@ -90,21 +90,19 @@ const getList = async ({ pageIndex }: { pageIndex: number }) => {
} }
const statusTextMap: Record<string, string> = { const statusTextMap: Record<string, string> = {
new: '排队中', waiting: '排队中',
computing: '计算中', computing: '计算中',
finished: '已完成', succeed: '已完成',
removed: '已删除' failed: '导出失败',
} }
let currentDelRow: Record<string, any> = {} let currentDelRow: Record<string, any> = {}
// //
const handleDownload = async (row: any) => { const handleDownload = async (row: any) => {
if (row.curStatus.status === 'removed') {
ElMessage.error('文件已删除')
return
}
if (row.url) { if (row.url) {
window.open(row.url) window.open(row.url)
} else {
ElMessageBox.alert('文件不存在')
} }
} }
// //
@ -137,7 +135,7 @@ const confirmDelete = async () => {
} }
} }
const fields = ['filename', 'fileSize', 'createDate', 'curStatus'] const fields = ['filename', 'fileSize', 'createdAt', 'status']
const fieldList = computed(() => { const fieldList = computed(() => {
return map(fields, (f) => { return map(fields, (f) => {
@ -157,14 +155,14 @@ const downloadListConfig = {
key: 'fileSize', key: 'fileSize',
width: 140 width: 140
}, },
createDate: { createdAt: {
title: '下载时间', title: '下载时间',
key: 'createDate', key: 'createdAt',
width: 240 width: 240
}, },
curStatus: { status: {
title: '状态', title: '状态',
key: 'curStatus.status', key: 'status',
formatter(row: Record<string, any>, column: Record<string, any>) { formatter(row: Record<string, any>, column: Record<string, any>) {
return statusTextMap[get(row, column.rawColumnKey)] return statusTextMap[get(row, column.rawColumnKey)]
} }

View File

@ -24,6 +24,7 @@ import LeftMenu from '@/management/components/LeftMenu.vue'
import CommonTemplate from './components/CommonTemplate.vue' import CommonTemplate from './components/CommonTemplate.vue'
import Navbar from './components/ModuleNavbar.vue' import Navbar from './components/ModuleNavbar.vue'
const editStore = useEditStore() const editStore = useEditStore()
const { init, setSurveyId, schema } = editStore const { init, setSurveyId, schema } = editStore

View File

@ -34,7 +34,7 @@ import { getSurveyHistory } from '@/management/api/survey'
const getItemData = (item: any) => ({ const getItemData = (item: any) => ({
operator: item?.operator?.username || '未知用户', operator: item?.operator?.username || '未知用户',
time: moment(item.createDate).format('YYYY-MM-DD HH:mm:ss') time: moment(item.createdAt).format('YYYY-MM-DD HH:mm:ss')
}) })
const dailyList = ref<Array<any>>([]) const dailyList = ref<Array<any>>([])

Some files were not shown because too many files have changed in this diff Show More