feat: 新增数据推送功能 (#86)

This commit is contained in:
luch 2024-03-28 21:37:59 +08:00 committed by sudoooooo
parent b3b5fa9fac
commit 746bece538
50 changed files with 2017 additions and 75 deletions

View File

@ -24,6 +24,7 @@
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@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/typeorm": "^10.0.1", "@nestjs/typeorm": "^10.0.1",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
@ -35,6 +36,7 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"mongodb": "^5.9.2", "mongodb": "^5.9.2",
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"node-fetch": "^2.7.0",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@ -87,4 +89,4 @@
"^src/(.*)$": "<rootDir>/$1" "^src/(.*)$": "<rootDir>/$1"
} }
} }
} }

View File

@ -27,6 +27,8 @@ import { Counter } from './models/counter.entity';
import { SurveyResponse } from './models/surveyResponse.entity'; import { SurveyResponse } from './models/surveyResponse.entity';
import { ClientEncrypt } from './models/clientEncrypt.entity'; import { ClientEncrypt } from './models/clientEncrypt.entity';
import { Word } from './models/word.entity'; import { Word } from './models/word.entity';
import { MessagePushingTask } from './models/messagePushingTask.entity';
import { MessagePushingLog } from './models/messagePushingLog.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';
@ -69,6 +71,8 @@ import { Logger } from './logger';
ResponseSchema, ResponseSchema,
ClientEncrypt, ClientEncrypt,
Word, Word,
MessagePushingTask,
MessagePushingLog,
], ],
}; };
}, },

View File

@ -0,0 +1,7 @@
export enum MESSAGE_PUSHING_TYPE {
HTTP = 'http',
}
export enum MESSAGE_PUSHING_HOOK {
RESPONSE_INSERTED = 'response_inserted',
}

View File

@ -1,13 +1,12 @@
import * as log4js from 'log4js'; import * as log4js from 'log4js';
import moment from 'moment'; import moment from 'moment';
import { REQUEST } from '@nestjs/core'; import { Request } from 'express';
import { Inject, Request } from '@nestjs/common';
const log4jsLogger = log4js.getLogger(); const log4jsLogger = log4js.getLogger();
export class Logger { export class Logger {
private static inited = false; private static inited = false;
constructor(@Inject(REQUEST) private req: Request) {} constructor() {}
static init(config: { filename: string }) { static init(config: { filename: string }) {
if (this.inited) { if (this.inited) {
@ -33,23 +32,23 @@ export class Logger {
}); });
} }
_log(message, options: { dltag?: string; level: string }) { _log(message, options: { dltag?: string; level: string; req?: Request }) {
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.req['traceId'] const traceIdStr = options?.req['traceId']
? `traceid=${this.req['traceId']}||` ? `traceid=${options?.req['traceId']}||`
: ''; : '';
return log4jsLogger[level]( return log4jsLogger[level](
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`, `[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,
); );
} }
info(message, options = { dltag: '' }) { info(message, options?: { dltag?: string; req?: Request }) {
return this._log(message, { ...options, level: 'info' }); return this._log(message, { ...options, level: 'info' });
} }
error(message, options = { dltag: '' }) { error(message, options: { dltag?: string; req?: Request }) {
return this._log(message, { ...options, level: 'error' }); return this._log(message, { ...options, level: 'error' });
} }
} }

View File

@ -1,9 +1,26 @@
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';
async function bootstrap() { async function bootstrap() {
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('XIAOJU SURVEY')
.setDescription('')
.setVersion('1.0')
.addTag('auth')
.addTag('survey')
.addTag('surveyResponse')
.addTag('messagePushingTasks')
.addTag('ui')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('swagger', app, document);
await app.listen(PORT); await app.listen(PORT);
console.log(`server is running at: http://127.0.0.1:${PORT}`); console.log(`server is running at: http://127.0.0.1:${PORT}`);
} }

View File

@ -20,6 +20,7 @@ export class LogRequestMiddleware implements NestMiddleware {
`method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`, `method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`,
{ {
dltag: 'request_in', dltag: 'request_in',
req,
}, },
); );
@ -29,6 +30,7 @@ export class LogRequestMiddleware implements NestMiddleware {
`status=${res.statusCode.toString()}||duration=${duration}ms`, `status=${res.statusCode.toString()}||duration=${duration}ms`,
{ {
dltag: 'request_out', dltag: 'request_out',
req,
}, },
); );
}); });

View File

@ -25,6 +25,6 @@ describe('BaseEntity', () => {
const now = Date.now(); const now = Date.now();
baseEntity.onUpdate(); baseEntity.onUpdate();
expect(baseEntity.updateDate).toBeCloseTo(now, -3); // Check if date is close to current time expect(baseEntity.updateDate).toBeCloseTo(now, -3);
}); });
}); });

View File

@ -0,0 +1,17 @@
import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity';
@Entity({ name: 'messagePushingLog' })
export class MessagePushingLog extends BaseEntity {
@Column()
taskId: string;
@Column('jsonb')
request: Record<string, any>;
@Column('jsonb')
response: Record<string, any>;
@Column()
status: number; // http状态码
}

View File

@ -0,0 +1,30 @@
import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity';
import {
MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing';
@Entity({ name: 'messagePushingTask' })
export class MessagePushingTask extends BaseEntity {
@Column()
name: string;
@Column()
type: MESSAGE_PUSHING_TYPE;
@Column()
pushAddress: string; // 如果是http推送则是http的链接
@Column()
triggerHook: MESSAGE_PUSHING_HOOK;
@Column('jsonb')
surveys: Array<string>;
@Column()
creatorId: string;
@Column()
ownerId: string;
}

View File

@ -82,8 +82,6 @@ describe('CaptchaService', () => {
expect(captchaRepository.delete).toHaveBeenCalledWith(mockCaptchaId); expect(captchaRepository.delete).toHaveBeenCalledWith(mockCaptchaId);
}); });
// Add more test cases for different scenarios
}); });
describe('checkCaptchaIsCorrect', () => { describe('checkCaptchaIsCorrect', () => {

View File

@ -1,14 +1,18 @@
import { Controller, Post, Body, HttpCode } from '@nestjs/common'; import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { UserService } from '../services/user.service';
import { CaptchaService } from '../services/captcha.service'; // 假设你的验证码服务在这里
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { UserService } from '../services/user.service';
<<<<<<< Updated upstream
import { CaptchaService } from '../services/captcha.service'; // 假设你的验证码服务在这里
=======
import { CaptchaService } from '../services/captcha.service';
import { ConfigService } from '@nestjs/config';
>>>>>>> Stashed changes
import { AuthService } from '../services/auth.service'; import { AuthService } from '../services/auth.service';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { create } from 'svg-captcha';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { create } from 'svg-captcha';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('auth')
@Controller('/api/auth') @Controller('/api/auth')
export class AuthController { export class AuthController {
constructor( constructor(

View File

@ -0,0 +1,47 @@
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
import {
MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing';
describe('CreateMessagePushingTaskDto', () => {
let dto: CreateMessagePushingTaskDto;
beforeEach(() => {
dto = new CreateMessagePushingTaskDto();
});
it('should be defined', () => {
expect(dto).toBeDefined();
});
it('should have a name', () => {
dto.name = '';
expect(dto.name).toBeDefined();
expect(dto.name).toBe('');
});
it('should have a valid type', () => {
dto.type = MESSAGE_PUSHING_TYPE.HTTP;
expect(dto.type).toBeDefined();
expect(dto.type).toEqual(MESSAGE_PUSHING_TYPE.HTTP);
});
it('should have a push address', () => {
dto.pushAddress = '';
expect(dto.pushAddress).toBeDefined();
expect(dto.pushAddress).toBe('');
});
it('should have a valid trigger hook', () => {
dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED;
expect(dto.triggerHook).toBeDefined();
expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED);
});
it('should have an array of surveys', () => {
dto.surveys = ['survey1', 'survey2'];
expect(dto.surveys).toBeDefined();
expect(dto.surveys).toEqual(['survey1', 'survey2']);
});
});

View File

@ -1,17 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { ObjectId } from 'mongodb';
import { DataStatisticController } from '../controllers/dataStatistic.controller'; import { DataStatisticController } from '../controllers/dataStatistic.controller';
import { DataStatisticService } from '../services/dataStatistic.service'; import { DataStatisticService } from '../services/dataStatistic.service';
import { SurveyMetaService } from '../services/surveyMeta.service'; import { SurveyMetaService } from '../services/surveyMeta.service';
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider'; import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
import { Authtication } from 'src/guards/authtication'; import { Authtication } from 'src/guards/authtication';
import { UserService } from 'src/modules/auth/services/user.service'; import { UserService } from 'src/modules/auth/services/user.service';
import { ConfigService } from '@nestjs/config';
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin'; import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
import { ObjectId } from 'mongodb';
jest.mock('../services/dataStatistic.service'); jest.mock('../services/dataStatistic.service');
jest.mock('../services/surveyMeta.service'); jest.mock('../services/surveyMeta.service');
jest.mock('../../surveyResponse/services/responseScheme.service'); jest.mock('../../surveyResponse/services/responseScheme.service');

View File

@ -0,0 +1,228 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { MessagePushingTaskController } from '../controllers/messagePushingTask.controller';
import { MessagePushingTaskService } from '../services/messagePushingTask.service';
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
import { QueryMessagePushingTaskListDto } from '../dto/queryMessagePushingTaskList.dto';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import {
MESSAGE_PUSHING_HOOK,
MESSAGE_PUSHING_TYPE,
} from 'src/enums/messagePushing';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { Authtication } from 'src/guards/authtication';
import { UserService } from 'src/modules/auth/services/user.service';
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
import { ObjectId } from 'mongodb';
describe('MessagePushingTaskController', () => {
let controller: MessagePushingTaskController;
let service: MessagePushingTaskService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MessagePushingTaskController],
providers: [
ConfigService,
{
provide: MessagePushingTaskService,
useValue: {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
surveyAuthorizeTask: jest.fn(),
},
},
{
provide: Authtication,
useClass: jest.fn().mockImplementation(() => ({
canActivate: () => true,
})),
},
{
provide: UserService,
useClass: jest.fn().mockImplementation(() => ({
getUserByUsername() {
return {};
},
})),
},
],
}).compile();
controller = module.get<MessagePushingTaskController>(
MessagePushingTaskController,
);
service = module.get<MessagePushingTaskService>(MessagePushingTaskService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should create a message pushing task', async () => {
const createDto: CreateMessagePushingTaskDto = {
name: 'test name',
type: MESSAGE_PUSHING_TYPE.HTTP,
pushAddress: 'http://example.com',
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
surveys: [],
};
const req = {
user: {
_id: '66028642292c50f8b71a9eee',
},
};
const mockTask = {
_id: new ObjectId(),
name: 'test name',
type: MESSAGE_PUSHING_TYPE.HTTP,
pushAddress: 'http://example.com',
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
surveys: [],
} as MessagePushingTask;
jest.spyOn(service, 'create').mockResolvedValueOnce(mockTask);
const result = await controller.create(req, createDto);
expect(service.create).toHaveBeenCalledWith({
...createDto,
ownerId: req.user._id,
});
expect(result).toEqual({
code: 200,
data: { taskId: mockTask._id.toString() },
});
});
});
describe('findAll', () => {
it('should find all message pushing tasks by surveyId and triggerHook', async () => {
const queryDto: QueryMessagePushingTaskListDto = {
surveyId: '65f29f3192862d6a9067ad1c',
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
};
const mockList = [];
const req = {
user: {
_id: '66028642292c50f8b71a9eee',
},
};
jest.spyOn(service, 'findAll').mockResolvedValueOnce(mockList);
const result = await controller.findAll(req, queryDto);
expect(service.findAll).toHaveBeenCalledWith({
surveyId: queryDto.surveyId,
hook: queryDto.triggerHook,
ownerId: req.user._id,
});
expect(result).toEqual({ code: 200, data: mockList });
});
it('should throw HttpException if surveyId or triggerHook is missing', async () => {
const queryDto = {
surveyId: '',
triggerHook: '',
};
const req = {
user: {
_id: '',
},
};
await expect(
controller.findAll(req, queryDto as QueryMessagePushingTaskListDto),
).rejects.toThrow(
new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR),
);
expect(service.findAll).toHaveBeenCalledTimes(0);
});
});
describe('findOne', () => {
it('should find one message pushing task by ID', async () => {
const req = {
user: {
_id: '66028642292c50f8b71a9eee',
},
};
const mockTask = {} as MessagePushingTask; // create mock data for your test
jest.spyOn(service, 'findOne').mockResolvedValueOnce(mockTask);
const mockTaskId = '65afc62904d5db18534c0f78';
const result = await controller.findOne(req, mockTaskId);
expect(service.findOne).toHaveBeenCalledWith({
ownerId: req.user._id,
id: mockTaskId,
});
expect(result).toEqual({ code: 200, data: mockTask });
});
});
describe('update', () => {
it('should update a message pushing task by ID', async () => {
const updateDto: UpdateMessagePushingTaskDto = {
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
};
const mockTask = {}; // create mock data for your test
jest
.spyOn(service, 'update')
.mockResolvedValueOnce(mockTask as MessagePushingTask);
const mockTaskId = '65afc62904d5db18534c0f78';
const req = {
user: {
_id: '66028642292c50f8b71a9eee',
},
};
const result = await controller.update(req, mockTaskId, updateDto);
expect(service.update).toHaveBeenCalledWith({
id: mockTaskId,
ownerId: req.user._id,
updateData: updateDto,
});
expect(result).toEqual({ code: 200, data: mockTask });
});
});
describe('remove', () => {
it('should remove a message pushing task by ID', async () => {
const mockResponse = { modifiedCount: 1 };
const req = {
user: {
_id: '66028642292c50f8b71a9eee',
},
};
const mockTaskId = '65afc62904d5db18534c0f78';
jest.spyOn(service, 'remove').mockResolvedValueOnce(mockResponse);
const result = await controller.remove(req, mockTaskId);
expect(result).toEqual({ code: 200, data: true });
});
});
describe('surveyAuthorizeTask', () => {
it('should authorize a survey for a task', async () => {
const mockResponse = { modifiedCount: 1 };
const req = {
user: {
_id: '66028642292c50f8b71a9eee',
},
};
jest
.spyOn(service, 'surveyAuthorizeTask')
.mockResolvedValueOnce(mockResponse);
const mockTaskId = '65afc62904d5db18534c0f78';
const mockSurveyId = '65f29f3192862d6a9067ad1c';
const result = await controller.surveyAuthorizeTask(
req,
mockTaskId,
mockSurveyId,
);
expect(result).toEqual({ code: 200, data: true });
});
});
});

View File

@ -0,0 +1,141 @@
import {
MessagePushingTaskDto,
CodeDto,
MessagePushingTaskSucceedResponseDto,
MessagePushingTaskListSucceedResponse,
} from '../dto/messagePushingTask.dto';
import {
MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing';
import { RECORD_STATUS } from 'src/enums';
describe('MessagePushingTaskDto', () => {
let dto: MessagePushingTaskDto;
beforeEach(() => {
dto = new MessagePushingTaskDto();
});
it('should be defined', () => {
expect(dto).toBeDefined();
});
it('should have an id', () => {
dto._id = 'test_id';
expect(dto._id).toBeDefined();
expect(dto._id).toBe('test_id');
});
it('should have a name', () => {
dto.name = 'test_name';
expect(dto.name).toBeDefined();
expect(dto.name).toBe('test_name');
});
it('should have a type', () => {
dto.type = MESSAGE_PUSHING_TYPE.HTTP; // Set your desired type here
expect(dto.type).toBeDefined();
expect(dto.type).toEqual(MESSAGE_PUSHING_TYPE.HTTP); // Adjust based on your enum
});
it('should have a push address', () => {
dto.pushAddress = 'test_address';
expect(dto.pushAddress).toBeDefined();
expect(dto.pushAddress).toBe('test_address');
});
it('should have a trigger hook', () => {
dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED; // Set your desired hook here
expect(dto.triggerHook).toBeDefined();
expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED); // Adjust based on your enum
});
it('should have an array of surveys', () => {
dto.surveys = ['survey1', 'survey2']; // Set your desired surveys here
expect(dto.surveys).toBeDefined();
expect(dto.surveys).toEqual(['survey1', 'survey2']);
});
it('should have an owner', () => {
dto.owner = 'test_owner';
expect(dto.owner).toBeDefined();
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', () => {
let dto: CodeDto;
beforeEach(() => {
dto = new CodeDto();
});
it('should be defined', () => {
expect(dto).toBeDefined();
});
it('should have a code', () => {
dto.code = 200;
expect(dto.code).toBeDefined();
expect(dto.code).toBe(200);
});
});
describe('MessagePushingTaskSucceedResponseDto', () => {
let dto: MessagePushingTaskSucceedResponseDto;
beforeEach(() => {
dto = new MessagePushingTaskSucceedResponseDto();
});
it('should be defined', () => {
expect(dto).toBeDefined();
});
it('should have a code', () => {
dto.code = 200;
expect(dto.code).toBeDefined();
expect(dto.code).toBe(200);
});
it('should have data', () => {
const taskDto = new MessagePushingTaskDto();
dto.data = taskDto;
expect(dto.data).toBeDefined();
expect(dto.data).toBeInstanceOf(MessagePushingTaskDto);
});
});
describe('MessagePushingTaskListSucceedResponse', () => {
let dto: MessagePushingTaskListSucceedResponse;
beforeEach(() => {
dto = new MessagePushingTaskListSucceedResponse();
});
it('should be defined', () => {
expect(dto).toBeDefined();
});
it('should have a code', () => {
dto.code = 200;
expect(dto.code).toBeDefined();
expect(dto.code).toBe(200);
});
it('should have data', () => {
const taskDto = new MessagePushingTaskDto();
dto.data = [taskDto];
expect(dto.data).toBeDefined();
expect(dto.data).toBeInstanceOf(Array);
expect(dto.data[0]).toBeInstanceOf(MessagePushingTaskDto);
});
});

View File

@ -0,0 +1,255 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MessagePushingTaskService } from '../services/messagePushingTask.service';
import { MongoRepository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing';
import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
describe('MessagePushingTaskService', () => {
let service: MessagePushingTaskService;
let repository: MongoRepository<MessagePushingTask>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MessagePushingTaskService,
{
provide: getRepositoryToken(MessagePushingTask),
useClass: MongoRepository,
},
],
}).compile();
service = module.get<MessagePushingTaskService>(MessagePushingTaskService);
repository = module.get<MongoRepository<MessagePushingTask>>(
getRepositoryToken(MessagePushingTask),
);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new message pushing task', async () => {
const createDto: CreateMessagePushingTaskDto = {
name: 'Test Task',
type: MESSAGE_PUSHING_TYPE.HTTP,
pushAddress: 'http://example.com',
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
surveys: ['surveyId1', 'surveyId2'],
};
const savedTask = new MessagePushingTask();
savedTask.name = 'Test Task';
savedTask.type = MESSAGE_PUSHING_TYPE.HTTP;
savedTask.pushAddress = 'http://example.com';
savedTask.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED;
savedTask.surveys = ['surveyId1', 'surveyId2'];
const mockOwnerId = '66028642292c50f8b71a9eee';
jest.spyOn(repository, 'create').mockReturnValue(savedTask);
jest.spyOn(repository, 'save').mockResolvedValue(savedTask);
const result = await service.create({
...createDto,
ownerId: mockOwnerId,
});
expect(result).toEqual(savedTask);
expect(repository.create).toHaveBeenCalledWith({
...createDto,
ownerId: mockOwnerId,
});
expect(repository.save).toHaveBeenCalledWith(savedTask);
});
});
describe('findAll', () => {
it('should find message pushing tasks by survey id and trigger hook', async () => {
const surveyId = 'surveyId';
const hook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED;
const tasks = [
{
_id: new ObjectId(),
name: 'Task 1',
type: MESSAGE_PUSHING_TYPE.HTTP,
pushAddress: '',
},
{
_id: new ObjectId(),
name: 'Task 2',
type: MESSAGE_PUSHING_TYPE.HTTP,
pushAddress: '',
},
];
jest
.spyOn(repository, 'find')
.mockResolvedValue(tasks as MessagePushingTask[]);
const mockOwnerId = '66028642292c50f8b71a9eee';
const result = await service.findAll({
surveyId,
hook,
ownerId: mockOwnerId,
});
expect(result).toEqual(tasks);
expect(repository.find).toHaveBeenCalledWith({
where: {
ownerId: mockOwnerId,
surveys: { $all: [surveyId] },
triggerHook: hook,
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
},
});
});
});
describe('findOne', () => {
it('should find a message pushing task by id', async () => {
const taskId = '65afc62904d5db18534c0f78';
const task = { _id: new ObjectId(), name: 'Test Task' };
jest
.spyOn(repository, 'findOne')
.mockResolvedValue(task as MessagePushingTask);
const mockOwnerId = '66028642292c50f8b71a9eee';
const result = await service.findOne({
id: taskId,
ownerId: mockOwnerId,
});
expect(result).toEqual(task);
expect(repository.findOne).toHaveBeenCalledWith({
where: {
ownerId: mockOwnerId,
_id: new ObjectId(taskId),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
},
});
});
});
describe('update', () => {
it('should update a message pushing task', async () => {
const taskId = '65afc62904d5db18534c0f78';
const updateDto: UpdateMessagePushingTaskDto = {
name: 'Updated Task',
type: MESSAGE_PUSHING_TYPE.HTTP,
pushAddress: 'http://update.example.com',
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
surveys: ['new survey id'],
curStatus: {
status: RECORD_STATUS.EDITING,
date: Date.now(),
},
};
const existingTask = new MessagePushingTask();
existingTask._id = new ObjectId(taskId);
existingTask.name = 'Original Task';
const updatedTask = Object.assign({}, existingTask, updateDto);
const mockOwnerId = '66028642292c50f8b71a9eee';
jest.spyOn(repository, 'findOne').mockResolvedValue(existingTask);
jest.spyOn(repository, 'save').mockResolvedValue(updatedTask);
const result = await service.update({
ownerId: mockOwnerId,
id: taskId,
updateData: updateDto,
});
expect(result).toEqual(updatedTask);
expect(repository.findOne).toHaveBeenCalledWith({
where: {
ownerId: mockOwnerId,
_id: new ObjectId(taskId),
},
});
expect(repository.save).toHaveBeenCalledWith(updatedTask);
});
});
describe('remove', () => {
it('should remove a message pushing task', async () => {
const taskId = '65afc62904d5db18534c0f78';
const updateResult = { modifiedCount: 1 };
const mockOwnerId = '66028642292c50f8b71a9eee';
jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult);
const result = await service.remove({
id: taskId,
ownerId: mockOwnerId,
});
expect(result).toEqual(updateResult);
expect(repository.updateOne).toHaveBeenCalledWith(
{
ownerId: mockOwnerId,
_id: new ObjectId(taskId),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
},
{
$set: {
curStatus: {
status: RECORD_STATUS.REMOVED,
date: expect.any(Number),
},
},
$push: {
statusList: {
status: RECORD_STATUS.REMOVED,
date: expect.any(Number),
},
},
},
);
});
});
describe('surveyAuthorizeTask', () => {
it('should authorize a survey for a task', async () => {
const taskId = '65afc62904d5db18534c0f78';
const surveyId = '65af380475b64545e5277dd9';
const mockOwnerId = '66028642292c50f8b71a9eee';
const updateResult = { modifiedCount: 1 };
jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult);
const result = await service.surveyAuthorizeTask({
taskId,
surveyId,
ownerId: mockOwnerId,
});
expect(result).toEqual(updateResult);
expect(repository.updateOne).toHaveBeenCalledWith(
{
ownerId: mockOwnerId,
_id: new ObjectId(taskId),
surveys: { $nin: [surveyId] },
},
{
$push: {
surveys: surveyId,
},
},
);
});
});
});

View File

@ -0,0 +1,26 @@
import { QueryMessagePushingTaskListDto } from '../dto/queryMessagePushingTaskList.dto';
import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
describe('QueryMessagePushingTaskListDto', () => {
let dto: QueryMessagePushingTaskListDto;
beforeEach(() => {
dto = new QueryMessagePushingTaskListDto();
});
it('should be defined', () => {
expect(dto).toBeDefined();
});
it('should have a surveyId', () => {
dto.surveyId = 'surveyId';
expect(dto.surveyId).toBeDefined();
expect(dto.surveyId).toBe('surveyId');
});
it('should have a triggerHook', () => {
dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED; // Set your desired hook here
expect(dto.triggerHook).toBeDefined();
expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED); // Adjust based on your enum
});
});

View File

@ -10,8 +10,8 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { SurveyConf } from 'src/models/surveyConf.entity'; import { SurveyConf } from 'src/models/surveyConf.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 { LoggerProvider } from 'src/logger/logger.provider';
// Mock the services
jest.mock('../services/surveyMeta.service'); jest.mock('../services/surveyMeta.service');
jest.mock('../services/surveyConf.service'); jest.mock('../services/surveyConf.service');
jest.mock('../../surveyResponse/services/responseScheme.service'); jest.mock('../../surveyResponse/services/responseScheme.service');
@ -37,6 +37,7 @@ describe('SurveyController', () => {
ResponseSchemaService, ResponseSchemaService,
ContentSecurityService, ContentSecurityService,
SurveyHistoryService, SurveyHistoryService,
LoggerProvider,
], ],
}).compile(); }).compile();

View File

@ -87,7 +87,6 @@ describe('SurveyConfService', () => {
code: schema, code: schema,
} as unknown as SurveyConf); } as unknown as SurveyConf);
// 调用待测试的方法
await service.saveSurveyConf({ surveyId, schema }); await service.saveSurveyConf({ surveyId, schema });
// 验证save方法被调用了一次并且传入了正确的参数 // 验证save方法被调用了一次并且传入了正确的参数
@ -120,7 +119,6 @@ describe('SurveyConfService', () => {
expect(surveyConfRepository.save).not.toHaveBeenCalled(); expect(surveyConfRepository.save).not.toHaveBeenCalled();
}); });
// getSurveyContentByCode方法的单元测试
it('should get survey content by code', async () => { it('should get survey content by code', async () => {
// 准备参数和模拟数据 // 准备参数和模拟数据
const schema = { const schema = {

View File

@ -115,6 +115,10 @@ describe('SurveyHistoryService', () => {
type: historyType, type: historyType,
}, },
take: 100, take: 100,
order: {
createDate: -1,
},
select: ['createDate', 'operator', 'type', '_id'],
}); });
}); });
}); });

View File

@ -2,8 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing';
import { SurveyMetaController } from '../controllers/surveyMeta.controller'; import { SurveyMetaController } from '../controllers/surveyMeta.controller';
import { SurveyMetaService } from '../services/surveyMeta.service'; import { SurveyMetaService } from '../services/surveyMeta.service';
import { Authtication } from 'src/guards/authtication'; import { Authtication } from 'src/guards/authtication';
import * as Joi from 'joi';
import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { LoggerProvider } from 'src/logger/logger.provider';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
describe('SurveyMetaController', () => { describe('SurveyMetaController', () => {
let controller: SurveyMetaController; let controller: SurveyMetaController;
@ -23,6 +25,7 @@ describe('SurveyMetaController', () => {
.mockResolvedValue({ count: 0, data: [] }), .mockResolvedValue({ count: 0, data: [] }),
}, },
}, },
LoggerProvider,
], ],
}) })
.overrideGuard(Authtication) .overrideGuard(Authtication)
@ -74,9 +77,7 @@ describe('SurveyMetaController', () => {
}); });
it('should validate request body with Joi', async () => { it('should validate request body with Joi', async () => {
const reqBody = { const reqBody = {};
// Missing title and surveyId
};
const req = { const req = {
user: { user: {
username: 'test-user', username: 'test-user',
@ -86,8 +87,8 @@ describe('SurveyMetaController', () => {
try { try {
await controller.updateMeta(reqBody, req); await controller.updateMeta(reqBody, req);
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(Joi.ValidationError); expect(error).toBeInstanceOf(HttpException);
expect(error.details[0].message).toMatch('"title" is required'); expect(error.code).toBe(EXCEPTION_CODE.PARAMETER_ERROR);
} }
expect(surveyMetaService.checkSurveyAccess).not.toHaveBeenCalled(); expect(surveyMetaService.checkSurveyAccess).not.toHaveBeenCalled();

View File

@ -0,0 +1,45 @@
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
describe('UpdateMessagePushingTaskDto', () => {
let dto: UpdateMessagePushingTaskDto;
beforeEach(() => {
dto = new UpdateMessagePushingTaskDto();
});
it('should be defined', () => {
expect(dto).toBeDefined();
});
it('should have a nullable name', () => {
dto.name = null;
expect(dto.name).toBeNull();
});
it('should have a nullable type', () => {
dto.type = null;
expect(dto.type).toBeNull();
});
it('should have a nullable push address', () => {
dto.pushAddress = null;
expect(dto.pushAddress).toBeNull();
});
it('should have a triggerHook', () => {
dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED;
expect(dto.triggerHook).toBeDefined();
expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED);
});
it('should have a nullable array of surveys', () => {
dto.surveys = null;
expect(dto.surveys).toBeNull();
});
it('should have a nullable curStatus', () => {
dto.curStatus = null;
expect(dto.curStatus).toBeNull();
});
});

View File

@ -12,9 +12,11 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { ApiTags } from '@nestjs/swagger';
import { Authtication } from 'src/guards/authtication'; import { Authtication } from 'src/guards/authtication';
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
@ApiTags('survey')
@Controller('/api/survey/dataStatistic') @Controller('/api/survey/dataStatistic')
export class DataStatisticController { export class DataStatisticController {
constructor( constructor(

View File

@ -0,0 +1,170 @@
import {
Controller,
Get,
Post,
Body,
Param,
Put,
Delete,
Query,
UseGuards,
Request,
} from '@nestjs/common';
import { ApiTags, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { MessagePushingTaskService } from '../services/messagePushingTask.service';
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
import {
MessagePushingTaskSucceedResponseDto,
MessagePushingTaskListSucceedResponse,
CodeDto,
TaskIdDto,
} from '../dto/messagePushingTask.dto';
import { QueryMessagePushingTaskListDto } from '../dto/queryMessagePushingTaskList.dto';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { Authtication } from 'src/guards/authtication';
@UseGuards(Authtication)
@ApiBearerAuth()
@ApiTags('messagePushingTasks')
@Controller('/api/messagePushingTasks')
export class MessagePushingTaskController {
constructor(
private readonly messagePushingTaskService: MessagePushingTaskService,
) {}
@ApiResponse({
description: '创建的推送任务',
status: 200,
type: TaskIdDto,
})
@Post('')
async create(
@Request()
req,
@Body() createMessagePushingTaskDto: CreateMessagePushingTaskDto,
) {
const userId = req.user._id;
const messagePushingTask = await this.messagePushingTaskService.create({
...createMessagePushingTaskDto,
ownerId: userId,
});
return {
code: 200,
data: {
taskId: messagePushingTask._id.toString(),
},
};
}
@ApiResponse({
description: '推送任务列表',
status: 200,
type: MessagePushingTaskListSucceedResponse,
})
@Get('')
async findAll(
@Request()
req,
@Query() query: QueryMessagePushingTaskListDto,
) {
const userId = req.user._id;
if (!query?.surveyId && !query?.triggerHook && !userId) {
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const list = await this.messagePushingTaskService.findAll({
surveyId: query.surveyId,
hook: query.triggerHook,
ownerId: userId,
});
return {
code: 200,
data: list,
};
}
@ApiResponse({
description: '查询到的推送任务',
status: 200,
type: MessagePushingTaskSucceedResponseDto,
})
@Get(':id')
async findOne(@Request() req, @Param('id') id: string) {
const userId = req.user._id;
const task = await this.messagePushingTaskService.findOne({
ownerId: userId,
id,
});
return {
code: 200,
data: task,
};
}
@ApiResponse({
description: '更新结果',
status: 200,
type: MessagePushingTaskSucceedResponseDto,
})
@Put(':id')
async update(
@Request() req,
@Param('id') id: string,
@Body() updateMessagePushingTaskDto: UpdateMessagePushingTaskDto,
) {
const userId = req.user._id;
const newTask = await this.messagePushingTaskService.update({
id,
ownerId: userId,
updateData: updateMessagePushingTaskDto,
});
return {
code: 200,
data: newTask,
};
}
@ApiResponse({
description: '删除结果',
status: 200,
type: CodeDto,
})
@Delete(':id')
async remove(@Request() req, @Param('id') id: string) {
const userId = req.user._id;
const res = await this.messagePushingTaskService.remove({
ownerId: userId,
id,
});
return {
code: 200,
data: res.modifiedCount === 1,
};
}
@ApiResponse({
description: '给任务绑定新问卷',
status: 200,
})
@Post(':taskId/surveys/:surveyId')
async surveyAuthorizeTask(
@Request() req,
@Param('taskId') taskId: string,
@Param('surveyId') surveyId: string,
) {
const userId = req.user._id;
const res = await this.messagePushingTaskService.surveyAuthorizeTask({
taskId,
surveyId,
ownerId: userId,
});
return {
code: 200,
data: res.modifiedCount === 1,
};
}
}

View File

@ -16,12 +16,16 @@ import { ContentSecurityService } from '../services/contentSecurity.service';
import { SurveyHistoryService } from '../services/surveyHistory.service'; import { SurveyHistoryService } from '../services/surveyHistory.service';
import BannerData from '../template/banner/index.json'; import BannerData from '../template/banner/index.json';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { ApiTags } from '@nestjs/swagger';
import { Authtication } from 'src/guards/authtication'; import { Authtication } from 'src/guards/authtication';
import { HISTORY_TYPE } from 'src/enums'; import { HISTORY_TYPE } from 'src/enums';
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 { Logger } from 'src/logger';
@ApiTags('survey')
@Controller('/api/survey') @Controller('/api/survey')
export class SurveyController { export class SurveyController {
constructor( constructor(
@ -30,6 +34,7 @@ export class SurveyController {
private readonly responseSchemaService: ResponseSchemaService, private readonly responseSchemaService: ResponseSchemaService,
private readonly contentSecurityService: ContentSecurityService, private readonly contentSecurityService: ContentSecurityService,
private readonly surveyHistoryService: SurveyHistoryService, private readonly surveyHistoryService: SurveyHistoryService,
private readonly logger: Logger,
) {} ) {}
@Get('/getBannerData') @Get('/getBannerData')
@ -68,6 +73,9 @@ export class SurveyController {
}), }),
}).validateAsync(reqBody); }).validateAsync(reqBody);
} catch (error) { } catch (error) {
this.logger.error(`createSurvey_parameter error: ${error.message}`, {
req,
});
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
} }

View File

@ -11,8 +11,10 @@ import { SurveyHistoryService } from '../services/surveyHistory.service';
import { SurveyMetaService } from '../services/surveyMeta.service'; import { SurveyMetaService } from '../services/surveyMeta.service';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { ApiTags } from '@nestjs/swagger';
import { Authtication } from 'src/guards/authtication'; import { Authtication } from 'src/guards/authtication';
@ApiTags('survey')
@Controller('/api/surveyHisotry') @Controller('/api/surveyHisotry')
export class SurveyHistoryController { export class SurveyHistoryController {
constructor( constructor(

View File

@ -10,17 +10,23 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import * as Joi from 'joi'; import * as Joi from 'joi';
import moment from 'moment'; import moment from 'moment';
import { ApiTags } from '@nestjs/swagger';
import { getFilter, getOrder } from 'src/utils/surveyUtil'; import { getFilter, getOrder } from 'src/utils/surveyUtil';
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 { Authtication } from 'src/guards/authtication'; import { Authtication } from 'src/guards/authtication';
import { Logger } from 'src/logger';
import { SurveyMetaService } from '../services/surveyMeta.service'; import { SurveyMetaService } from '../services/surveyMeta.service';
@ApiTags('survey')
@Controller('/api/survey') @Controller('/api/survey')
export class SurveyMetaController { export class SurveyMetaController {
constructor(private readonly surveyMetaService: SurveyMetaService) {} constructor(
private readonly surveyMetaService: SurveyMetaService,
private readonly logger: Logger,
) {}
@UseGuards(Authtication) @UseGuards(Authtication)
@Post('/updateMeta') @Post('/updateMeta')
@ -34,6 +40,9 @@ export class SurveyMetaController {
surveyId: Joi.string().required(), surveyId: Joi.string().required(),
}).validateAsync(reqBody, { allowUnknown: true }); }).validateAsync(reqBody, { allowUnknown: true });
} catch (error) { } catch (error) {
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
req,
});
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
} }

View File

@ -1,7 +1,8 @@
import { Controller, Get, Res } from '@nestjs/common'; import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express'; import { Response } from 'express';
import { join } from 'path'; import { join } from 'path';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('ui')
@Controller() @Controller()
export class SurveyUIController { export class SurveyUIController {
constructor() {} constructor() {}

View File

@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import {
MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing';
export class CreateMessagePushingTaskDto {
@ApiProperty({ description: '任务名称' })
name: string;
@ApiProperty({ description: '任务类型' })
type: MESSAGE_PUSHING_TYPE;
@ApiProperty({ description: '推送的http链接' })
pushAddress: string;
@ApiProperty({ description: '触发时机' })
triggerHook: MESSAGE_PUSHING_HOOK;
@ApiProperty({ description: '包含问卷id' })
surveys?: string[];
}

View File

@ -0,0 +1,62 @@
import { ApiProperty } from '@nestjs/swagger';
import {
MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing';
import { RECORD_STATUS } from 'src/enums';
export class MessagePushingTaskDto {
@ApiProperty({ description: '任务id' })
_id: string;
@ApiProperty({ description: '任务名称' })
name: string;
@ApiProperty({ description: '任务类型' })
type: MESSAGE_PUSHING_TYPE;
@ApiProperty({ description: '推送的http链接' })
pushAddress: string;
@ApiProperty({ description: '触发时机' })
triggerHook: MESSAGE_PUSHING_HOOK;
@ApiProperty({ description: '包含问卷id' })
surveys: string[];
@ApiProperty({ description: '所有者' })
owner: string;
@ApiProperty({ description: '任务状态', required: false })
curStatus?: {
status: RECORD_STATUS;
date: number;
};
}
export class CodeDto {
@ApiProperty({ description: '状态码', default: 200 })
code: number;
}
export class TaskIdDto {
@ApiProperty({ description: '任务id' })
taskId: string;
}
export class MessagePushingTaskSucceedResponseDto {
@ApiProperty({ description: '状态码', default: 200 })
code: number;
@ApiProperty({ description: '任务详情' })
data: MessagePushingTaskDto;
}
export class MessagePushingTaskListSucceedResponse {
@ApiProperty({ description: '状态码', default: 200 })
code: number;
@ApiProperty({ description: '任务详情' })
data: [MessagePushingTaskDto];
}

View File

@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
export class QueryMessagePushingTaskListDto {
@ApiProperty({ description: '问卷id', required: false })
surveyId?: string;
@ApiProperty({ description: 'hook名称', required: false })
triggerHook?: MESSAGE_PUSHING_HOOK;
}

View File

@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { RECORD_STATUS } from 'src/enums';
import {
MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing';
export class UpdateMessagePushingTaskDto {
@ApiProperty({ description: '任务名称', required: false })
name?: string;
@ApiProperty({ description: '任务类型' })
type?: MESSAGE_PUSHING_TYPE;
@ApiProperty({ description: '推送的http链接' })
pushAddress?: string;
@ApiProperty({ description: '触发时机' })
triggerHook?: MESSAGE_PUSHING_HOOK;
@ApiProperty({ description: '绑定的问卷id', required: false })
surveys?: string[];
@ApiProperty({ description: '任务状态', required: false })
curStatus?: {
status: RECORD_STATUS;
date: number;
};
}

View File

@ -0,0 +1,149 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
@Injectable()
export class MessagePushingTaskService {
constructor(
@InjectRepository(MessagePushingTask)
private readonly messagePushingTaskRepository: MongoRepository<MessagePushingTask>,
) {}
async create(
createMessagePushingTaskDto: CreateMessagePushingTaskDto & {
ownerId: string;
},
): Promise<MessagePushingTask> {
const createdTask = this.messagePushingTaskRepository.create(
createMessagePushingTaskDto,
);
createdTask.creatorId = createdTask.ownerId;
if (!createdTask.surveys) {
createdTask.surveys = [];
}
return await this.messagePushingTaskRepository.save(createdTask);
}
async findAll({
surveyId,
hook,
ownerId,
}: {
surveyId?: string;
hook?: MESSAGE_PUSHING_HOOK;
ownerId?: string;
}): Promise<MessagePushingTask[]> {
const where: Record<string, any> = {
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
};
if (surveyId) {
where.surveys = {
$all: [surveyId],
};
}
if (hook) {
where.triggerHook = hook;
}
if (ownerId) {
where.ownerId = ownerId;
}
return await this.messagePushingTaskRepository.find({
where,
});
}
async findOne({
id,
ownerId,
}: {
id: string;
ownerId: string;
}): Promise<MessagePushingTask> {
return await this.messagePushingTaskRepository.findOne({
where: {
ownerId,
_id: new ObjectId(id),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
});
}
async update({
ownerId,
id,
updateData,
}: {
ownerId: string;
id: string;
updateData: UpdateMessagePushingTaskDto;
}): Promise<MessagePushingTask> {
const existingTask = await this.messagePushingTaskRepository.findOne({
where: {
ownerId,
_id: new ObjectId(id),
},
});
if (!existingTask) {
throw new Error(`Message pushing task with id ${id} not found.`);
}
const updatedTask = Object.assign(existingTask, updateData);
return await this.messagePushingTaskRepository.save(updatedTask);
}
async remove({ id, ownerId }: { id: string; ownerId: string }) {
const curStatus = {
status: RECORD_STATUS.REMOVED,
date: Date.now(),
};
return this.messagePushingTaskRepository.updateOne(
{
ownerId,
_id: new ObjectId(id),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
{
$set: {
curStatus,
},
$push: {
statusList: curStatus as never,
},
},
);
}
async surveyAuthorizeTask({
taskId,
surveyId,
ownerId,
}: {
taskId: string;
surveyId: string;
ownerId: string;
}) {
return this.messagePushingTaskRepository.updateOne(
{
_id: new ObjectId(taskId),
surveys: { $nin: [surveyId] },
ownerId,
},
{
$push: {
surveys: surveyId as never,
},
},
);
}
}

View File

@ -47,6 +47,7 @@ export class SurveyHistoryService {
order: { order: {
createDate: -1, createDate: -1,
}, },
select: ['createDate', 'operator', 'type', '_id'],
}); });
} }
} }

View File

@ -2,6 +2,8 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggerProvider } from 'src/logger/logger.provider';
import { SurveyResponseModule } from '../surveyResponse/surveyResponse.module'; import { SurveyResponseModule } from '../surveyResponse/surveyResponse.module';
import { AuthModule } from '../auth/auth.module'; import { AuthModule } from '../auth/auth.module';
@ -10,18 +12,21 @@ import { SurveyController } from './controllers/survey.controller';
import { SurveyHistoryController } from './controllers/surveyHistory.controller'; import { SurveyHistoryController } from './controllers/surveyHistory.controller';
import { SurveyMetaController } from './controllers/surveyMeta.controller'; import { SurveyMetaController } from './controllers/surveyMeta.controller';
import { SurveyUIController } from './controllers/surveyUI.controller'; import { SurveyUIController } from './controllers/surveyUI.controller';
import { MessagePushingTaskController } from './controllers/messagePushingTask.controller';
import { SurveyConf } from 'src/models/surveyConf.entity'; import { SurveyConf } from 'src/models/surveyConf.entity';
import { SurveyHistory } from 'src/models/surveyHistory.entity'; import { SurveyHistory } from 'src/models/surveyHistory.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { Word } from 'src/models/word.entity'; import { Word } from 'src/models/word.entity';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { DataStatisticService } from './services/dataStatistic.service'; import { DataStatisticService } from './services/dataStatistic.service';
import { SurveyConfService } from './services/surveyConf.service'; import { SurveyConfService } from './services/surveyConf.service';
import { SurveyHistoryService } from './services/surveyHistory.service'; import { SurveyHistoryService } from './services/surveyHistory.service';
import { SurveyMetaService } from './services/surveyMeta.service'; import { SurveyMetaService } from './services/surveyMeta.service';
import { ContentSecurityService } from './services/contentSecurity.service'; import { ContentSecurityService } from './services/contentSecurity.service';
import { MessagePushingTaskService } from './services/messagePushingTask.service';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider'; import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
@ -33,6 +38,7 @@ import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider
SurveyHistory, SurveyHistory,
SurveyResponse, SurveyResponse,
Word, Word,
MessagePushingTask,
]), ]),
ConfigModule, ConfigModule,
SurveyResponseModule, SurveyResponseModule,
@ -44,6 +50,7 @@ import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider
SurveyHistoryController, SurveyHistoryController,
SurveyMetaController, SurveyMetaController,
SurveyUIController, SurveyUIController,
MessagePushingTaskController,
], ],
providers: [ providers: [
DataStatisticService, DataStatisticService,
@ -52,6 +59,8 @@ import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider
SurveyMetaService, SurveyMetaService,
PluginManagerProvider, PluginManagerProvider,
ContentSecurityService, ContentSecurityService,
MessagePushingTaskService,
LoggerProvider,
], ],
}) })
export class SurveyModule {} export class SurveyModule {}

View File

@ -113,7 +113,6 @@
"type": "radio-star", "type": "radio-star",
"title": "标题2", "title": "标题2",
"answer": "", "answer": "",
"options": [],
"textRange": { "textRange": {
"min": { "min": {
"placeholder": "0", "placeholder": "0",

View File

@ -27,24 +27,6 @@
"checked": false, "checked": false,
"minNum": "", "minNum": "",
"maxNum": "", "maxNum": "",
"options": [
{
"text": "选项1",
"imageUrl": "",
"others": false,
"mustOthers": false,
"othersKey": "",
"placeholderDesc": ""
},
{
"text": "选项2",
"imageUrl": "",
"others": false,
"mustOthers": false,
"othersKey": "",
"placeholderDesc": ""
}
],
"star": 5, "star": 5,
"nps": { "nps": {
"leftText": "极不满意", "leftText": "极不满意",

View File

@ -0,0 +1,110 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MessagePushingLogService } from '../services/messagePushingLog.service';
import { MongoRepository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { MessagePushingLog } from 'src/models/messagePushingLog.entity';
import { ObjectId } from 'mongodb';
describe('MessagePushingLogService', () => {
let service: MessagePushingLogService;
let repository: MongoRepository<MessagePushingLog>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MessagePushingLogService,
{
provide: getRepositoryToken(MessagePushingLog),
useClass: MongoRepository,
},
],
}).compile();
service = module.get<MessagePushingLogService>(MessagePushingLogService);
repository = module.get<MongoRepository<MessagePushingLog>>(
getRepositoryToken(MessagePushingLog),
);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('createPushingLog', () => {
it('should create a message pushing log', async () => {
const taskId = '65afc62904d5db18534c0f78';
const request = { reqKey: 'value' };
const response = { resKey: 'value' };
const status = 200;
const createdLog = new MessagePushingLog();
createdLog.taskId = taskId;
createdLog.request = request;
createdLog.response = response;
createdLog.status = status;
jest.spyOn(repository, 'create').mockReturnValue(createdLog);
jest.spyOn(repository, 'save').mockResolvedValue(createdLog);
const result = await service.createPushingLog({
taskId,
request,
response,
status,
});
expect(result).toEqual(createdLog);
expect(repository.create).toHaveBeenCalledWith({
taskId,
request,
response,
status,
});
expect(repository.save).toHaveBeenCalledWith(createdLog);
});
});
describe('findAllByTaskId', () => {
it('should find all message pushing logs by task id', async () => {
const taskId = '65afc62904d5db18534c0f78';
const logs = [{ taskId, request: {}, response: {}, status: 200 }];
jest
.spyOn(repository, 'find')
.mockResolvedValue(logs as MessagePushingLog[]);
const result = await service.findAllByTaskId(taskId);
expect(result).toEqual(logs);
expect(repository.find).toHaveBeenCalledWith({ where: { taskId } });
});
});
describe('findOne', () => {
it('should find one message pushing log by id', async () => {
const logId = '65af380475b64545e5277dd9';
const log = {
_id: new ObjectId(logId),
taskId: '65afc62904d5db18534c0f78',
request: {},
response: {},
status: 200,
};
jest
.spyOn(repository, 'findOne')
.mockResolvedValue(log as MessagePushingLog);
const result = await service.findOne(logId);
expect(result).toEqual(log);
expect(repository.findOne).toHaveBeenCalledWith({
where: { _id: new ObjectId(logId) },
});
});
});
});

View File

@ -1,17 +1,27 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { ObjectId } from 'mongodb';
import { cloneDeep } from 'lodash';
import { mockResponseSchema } from './mockResponseSchema';
import { SurveyResponseController } from '../controllers/surveyResponse.controller'; import { SurveyResponseController } from '../controllers/surveyResponse.controller';
import { ResponseSchemaService } from '../services/responseScheme.service'; import { ResponseSchemaService } from '../services/responseScheme.service';
import { CounterService } from '../services/counter.service'; import { CounterService } from '../services/counter.service';
import { SurveyResponseService } from '../services/surveyResponse.service'; import { SurveyResponseService } from '../services/surveyResponse.service';
import { ClientEncryptService } from '../services/clientEncrypt.service'; import { ClientEncryptService } from '../services/clientEncrypt.service';
import { mockResponseSchema } from './mockResponseSchema'; import { MessagePushingTaskService } from '../../survey/services/messagePushingTask.service';
import { MessagePushingLogService } from '../services/messagePushingLog.service';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider'; import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
import { ObjectId } from 'mongodb';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { cloneDeep } from 'lodash';
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin'; import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing';
import { RECORD_STATUS } from 'src/enums';
import { SurveyResponse } from 'src/models/surveyResponse.entity';
const mockDecryptErrorBody = { const mockDecryptErrorBody = {
surveyPath: 'EBzdmnSp', surveyPath: 'EBzdmnSp',
@ -65,9 +75,10 @@ const mockClientEncryptInfo = {
describe('SurveyResponseController', () => { describe('SurveyResponseController', () => {
let controller: SurveyResponseController; let controller: SurveyResponseController;
let responseSchemaService: ResponseSchemaService; let responseSchemaService: ResponseSchemaService;
// let counterService: CounterService;
let surveyResponseService: SurveyResponseService; let surveyResponseService: SurveyResponseService;
let clientEncryptService: ClientEncryptService; let clientEncryptService: ClientEncryptService;
let messagePushingTaskService: MessagePushingTaskService;
let messagePushingLogService: MessagePushingLogService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@ -102,6 +113,18 @@ describe('SurveyResponseController', () => {
.mockResolvedValue(mockClientEncryptInfo), .mockResolvedValue(mockClientEncryptInfo),
}, },
}, },
{
provide: MessagePushingTaskService,
useValue: {
findAll: jest.fn(),
},
},
{
provide: MessagePushingLogService,
useValue: {
createPushingLog: jest.fn(),
},
},
PluginManagerProvider, PluginManagerProvider,
], ],
}).compile(); }).compile();
@ -110,13 +133,20 @@ describe('SurveyResponseController', () => {
responseSchemaService = module.get<ResponseSchemaService>( responseSchemaService = module.get<ResponseSchemaService>(
ResponseSchemaService, ResponseSchemaService,
); );
// counterService = module.get<CounterService>(CounterService);
surveyResponseService = module.get<SurveyResponseService>( surveyResponseService = module.get<SurveyResponseService>(
SurveyResponseService, SurveyResponseService,
); );
clientEncryptService = clientEncryptService =
module.get<ClientEncryptService>(ClientEncryptService); module.get<ClientEncryptService>(ClientEncryptService);
messagePushingTaskService = module.get<MessagePushingTaskService>(
MessagePushingTaskService,
);
messagePushingLogService = module.get<MessagePushingLogService>(
MessagePushingLogService,
);
const pluginManager = module.get<XiaojuSurveyPluginManager>( const pluginManager = module.get<XiaojuSurveyPluginManager>(
XiaojuSurveyPluginManager, XiaojuSurveyPluginManager,
); );
@ -141,10 +171,52 @@ describe('SurveyResponseController', () => {
.mockResolvedValueOnce(0); .mockResolvedValueOnce(0);
jest jest
.spyOn(surveyResponseService, 'createSurveyResponse') .spyOn(surveyResponseService, 'createSurveyResponse')
.mockResolvedValueOnce(undefined); .mockResolvedValueOnce({
_id: new ObjectId('65fc2dd77f4520858046e129'),
clientTime: 1711025112552,
createDate: 1711025113146,
curStatus: {
status: RECORD_STATUS.NEW,
date: 1711025113146,
},
difTime: 30518,
data: {
data458: '15000000000',
data515: '115019',
data450: '450111000000000000',
data405: '浙江省杭州市西湖区xxx',
data770: '123456@qq.com',
},
optionTextAndId: {
data515: [
{
hash: '115019',
text: '<p>男</p>',
},
{
hash: '115020',
text: '<p>女</p>',
},
],
},
pageId: '65f29f3192862d6a9067ad1c',
statusList: [
{
status: RECORD_STATUS.NEW,
date: 1711025113146,
},
],
surveyPath: 'EBzdmnSp',
updateDate: 1711025113146,
secretKeys: [],
} as unknown as SurveyResponse);
jest jest
.spyOn(clientEncryptService, 'deleteEncryptInfo') .spyOn(clientEncryptService, 'deleteEncryptInfo')
.mockResolvedValueOnce(undefined); .mockResolvedValueOnce(undefined);
jest
.spyOn(controller, 'sendSurveyResponseMessage')
.mockReturnValueOnce(undefined);
const result = await controller.createResponse(reqBody); const result = await controller.createResponse(reqBody);
@ -166,7 +238,7 @@ describe('SurveyResponseController', () => {
}, },
clientTime: reqBody.clientTime, clientTime: reqBody.clientTime,
difTime: reqBody.difTime, difTime: reqBody.difTime,
surveyId: mockResponseSchema.pageId, // mock response schema 的 pageId surveyId: mockResponseSchema.pageId,
optionTextAndId: { optionTextAndId: {
data515: [ data515: [
{ {
@ -180,6 +252,7 @@ describe('SurveyResponseController', () => {
], ],
}, },
}); });
expect(clientEncryptService.deleteEncryptInfo).toHaveBeenCalledWith( expect(clientEncryptService.deleteEncryptInfo).toHaveBeenCalledWith(
reqBody.sessionId, reqBody.sessionId,
); );
@ -247,4 +320,101 @@ describe('SurveyResponseController', () => {
); );
}); });
}); });
describe('sendSurveyResponseMessage', () => {
it('should send survey response message', async () => {
const sendData = {
surveyId: '65f29f3192862d6a9067ad1c',
surveyPath: 'EBzdmnSp',
surveyResponseId: '65fc2dd77f4520858046e129',
data: [
{
questionId: 'data458',
title: '<p>您的手机号</p>',
valueType: 'text',
alias: '',
value: ['15000000000'],
},
{
questionId: 'data515',
title: '<p>您的性别</p>',
valueType: 'option',
alias: '',
value: [
{
alias: '',
id: '115019',
text: '<p>男</p>',
},
],
},
{
questionId: 'data450',
title: '<p>身份证</p>',
valueType: 'text',
alias: '',
value: ['450111000000000000'],
},
{
questionId: 'data405',
title: '<p>地址</p>',
valueType: 'text',
alias: '',
value: ['浙江省杭州市西湖区xxx'],
},
{
questionId: 'data770',
title: '<p>邮箱</p>',
valueType: 'text',
alias: '',
value: ['123456@qq.com'],
},
],
};
const mockTasks = [
{
_id: new ObjectId('65fc31dbfd09a5d0619c3b74'),
name: 'Task 1',
type: MESSAGE_PUSHING_TYPE.HTTP,
pushAddress: 'success_url',
},
{
_id: new ObjectId('65fc31dbfd09a5d0619c3b75'),
name: 'Task 2',
type: MESSAGE_PUSHING_TYPE.HTTP,
pushAddress: 'fail_url',
},
] as MessagePushingTask[];
jest
.spyOn(messagePushingTaskService, 'findAll')
.mockReturnValue(Promise.resolve(mockTasks));
const mockFetch = jest.fn().mockImplementation((url) => {
if (url === 'success_url') {
return {
json: () => {
return Promise.resolve({ code: 200, msg: '提交成功' });
},
status: 200,
};
} else {
return {
data: 'failed',
status: 501,
};
}
});
jest.mock('node-fetch', () => jest.fn().mockImplementation(mockFetch));
await controller.sendSurveyResponseMessage({
sendData,
surveyId: mockResponseSchema.pageId,
});
expect(messagePushingTaskService.findAll).toHaveBeenCalled();
expect(messagePushingLogService.createPushingLog).toHaveBeenCalled();
});
});
}); });

View File

@ -21,10 +21,9 @@ describe('SurveyResponseUIController', () => {
}); });
it('should render the survey response with the correct path', () => { it('should render the survey response with the correct path', () => {
const surveyPath = 'some-survey-path';
const expectedFilePath = join(process.cwd(), 'public', 'render.html'); const expectedFilePath = join(process.cwd(), 'public', 'render.html');
controller.render(surveyPath, res); controller.render(res);
expect(res.sendFile).toHaveBeenCalledWith(expectedFilePath); expect(res.sendFile).toHaveBeenCalledWith(expectedFilePath);
}); });

View File

@ -3,7 +3,9 @@ import { ConfigService } from '@nestjs/config';
import { ClientEncryptService } from '../services/clientEncrypt.service'; import { ClientEncryptService } from '../services/clientEncrypt.service';
import * as forge from 'node-forge'; import * as forge from 'node-forge';
import { ENCRYPT_TYPE } from 'src/enums/encrypt'; import { ENCRYPT_TYPE } from 'src/enums/encrypt';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('surveyResponse')
@Controller('/api/clientEncrypt') @Controller('/api/clientEncrypt')
export class ClientEncryptController { export class ClientEncryptController {
constructor( constructor(

View File

@ -2,7 +2,9 @@ import { Controller, Get, HttpCode, Query } from '@nestjs/common';
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 { CounterService } from '../services/counter.service'; import { CounterService } from '../services/counter.service';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('surveyResponse')
@Controller('/api/counter') @Controller('/api/counter')
export class CounterController { export class CounterController {
constructor(private readonly counterService: CounterService) {} constructor(private readonly counterService: CounterService) {}

View File

@ -3,7 +3,9 @@ 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_STATUS } from 'src/enums';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('surveyResponse')
@Controller('/api/responseSchema') @Controller('/api/responseSchema')
export class ResponseSchemaController { export class ResponseSchemaController {
constructor(private readonly responseSchemaService: ResponseSchemaService) {} constructor(private readonly responseSchemaService: ResponseSchemaService) {}

View File

@ -2,16 +2,28 @@ import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { checkSign } from 'src/utils/checkSign'; import { checkSign } from 'src/utils/checkSign';
import * as Joi from 'joi'; import { ENCRYPT_TYPE } from 'src/enums/encrypt';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import {
MESSAGE_PUSHING_HOOK,
MESSAGE_PUSHING_TYPE,
} from 'src/enums/messagePushing';
import { getPushingData } from 'src/utils/messagePushing';
import { ResponseSchemaService } from '../services/responseScheme.service'; import { ResponseSchemaService } from '../services/responseScheme.service';
import { CounterService } from '../services/counter.service'; import { CounterService } from '../services/counter.service';
import moment from 'moment';
import { SurveyResponseService } from '../services/surveyResponse.service'; import { SurveyResponseService } from '../services/surveyResponse.service';
import { ClientEncryptService } from '../services/clientEncrypt.service'; import { ClientEncryptService } from '../services/clientEncrypt.service';
import { ENCRYPT_TYPE } from 'src/enums/encrypt'; import { MessagePushingTaskService } from '../../survey/services/messagePushingTask.service';
import * as forge from 'node-forge'; import { MessagePushingLogService } from '../services/messagePushingLog.service';
import moment from 'moment';
import * as Joi from 'joi';
import * as forge from 'node-forge';
import { ApiTags } from '@nestjs/swagger';
import fetch from 'node-fetch';
@ApiTags('surveyResponse')
@Controller('/api/surveyResponse') @Controller('/api/surveyResponse')
export class SurveyResponseController { export class SurveyResponseController {
constructor( constructor(
@ -19,6 +31,8 @@ export class SurveyResponseController {
private readonly counterService: CounterService, private readonly counterService: CounterService,
private readonly surveyResponseService: SurveyResponseService, private readonly surveyResponseService: SurveyResponseService,
private readonly clientEncryptService: ClientEncryptService, private readonly clientEncryptService: ClientEncryptService,
private readonly messagePushingTaskService: MessagePushingTaskService,
private readonly messagePushingLogService: MessagePushingLogService,
) {} ) {}
@Post('/createResponse') @Post('/createResponse')
@ -174,13 +188,27 @@ export class SurveyResponseController {
} }
// 入库 // 入库
await this.surveyResponseService.createSurveyResponse({ const surveyResponse =
surveyPath: validationResult.surveyPath, await this.surveyResponseService.createSurveyResponse({
data: decryptedData, surveyPath: validationResult.surveyPath,
clientTime, data: decryptedData,
difTime, clientTime,
difTime,
surveyId: responseSchema.pageId,
optionTextAndId,
});
const sendData = getPushingData({
surveyResponse,
questionList: responseSchema?.code?.dataConf?.dataList || [],
surveyId: responseSchema.pageId,
surveyPath: responseSchema.surveyPath,
});
// 数据异步推送
this.sendSurveyResponseMessage({
sendData,
surveyId: responseSchema.pageId, surveyId: responseSchema.pageId,
optionTextAndId,
}); });
// 入库成功后,要把密钥删掉,防止被重复使用 // 入库成功后,要把密钥删掉,防止被重复使用
@ -191,4 +219,53 @@ export class SurveyResponseController {
msg: '提交成功', msg: '提交成功',
}; };
} }
async sendSurveyResponseMessage({ sendData, surveyId }) {
try {
// 数据推送
const messagePushingTasks = await this.messagePushingTaskService.findAll({
surveyId,
hook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
});
if (
Array.isArray(messagePushingTasks) &&
messagePushingTasks.length > 0
) {
for (const task of messagePushingTasks) {
switch (task.type) {
case MESSAGE_PUSHING_TYPE.HTTP: {
try {
const res = await fetch(task.pushAddress, {
method: 'POST',
headers: {
Accept: 'application/json, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify(sendData),
});
const response = await res.json();
await this.messagePushingLogService.createPushingLog({
taskId: task._id.toString(),
request: sendData,
response: response,
status: res.status,
});
} catch (error) {
await this.messagePushingLogService.createPushingLog({
taskId: task._id.toString(),
request: sendData,
response: error.data || error.message,
status: error.status || 500,
});
}
break;
}
default:
break;
}
}
}
} catch (error) {}
}
} }

View File

@ -1,13 +1,14 @@
import { Controller, Get, Param, Res } from '@nestjs/common'; import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express'; import { Response } from 'express';
import { join } from 'path'; import { join } from 'path';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('ui')
@Controller() @Controller()
export class SurveyResponseUIController { export class SurveyResponseUIController {
constructor() {} constructor() {}
@Get('/render/:surveyPath') @Get('/render/:path*')
render(@Param('surveyPath') surveyPath: string, @Res() res: Response) { render(@Res() res: Response) {
res.sendFile(join(process.cwd(), 'public', 'render.html')); res.sendFile(join(process.cwd(), 'public', 'render.html'));
} }
} }

View File

@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { MessagePushingLog } from 'src/models/messagePushingLog.entity';
import { ObjectId } from 'mongodb';
@Injectable()
export class MessagePushingLogService {
constructor(
@InjectRepository(MessagePushingLog)
private readonly messagePushingLogRepository: MongoRepository<MessagePushingLog>,
) {}
async createPushingLog({
taskId,
request,
response,
status,
}): Promise<MessagePushingLog> {
const createdLog = this.messagePushingLogRepository.create({
taskId,
request,
response,
status,
});
return await this.messagePushingLogRepository.save(createdLog);
}
async findAllByTaskId(taskId: string): Promise<MessagePushingLog[]> {
return await this.messagePushingLogRepository.find({
where: {
taskId,
},
});
}
async findOne(id: string): Promise<MessagePushingLog> {
return await this.messagePushingLogRepository.findOne({
where: {
_id: new ObjectId(id),
},
});
}
}

View File

@ -28,7 +28,10 @@ export class SurveyResponseService {
}); });
// 提交问卷 // 提交问卷
return await this.surveyResponseRepository.save(newSubmitData); const res = await this.surveyResponseRepository.save(newSubmitData);
// res是加密后的数据需要手动调用loaded才会触发解密
res.onDataLoaded();
return res;
} }
async getSurveyResponseTotalByPath(surveyPath: string) { async getSurveyResponseTotalByPath(surveyPath: string) {

View File

@ -4,11 +4,15 @@ import { ResponseSchemaService } from './services/responseScheme.service';
import { SurveyResponseService } from './services/surveyResponse.service'; import { SurveyResponseService } from './services/surveyResponse.service';
import { CounterService } from './services/counter.service'; import { CounterService } from './services/counter.service';
import { ClientEncryptService } from './services/clientEncrypt.service'; import { ClientEncryptService } from './services/clientEncrypt.service';
import { MessagePushingTaskService } from '../survey/services/messagePushingTask.service';
import { MessagePushingLogService } from './services/messagePushingLog.service';
import { ResponseSchema } from 'src/models/responseSchema.entity'; import { ResponseSchema } from 'src/models/responseSchema.entity';
import { Counter } from 'src/models/counter.entity'; import { Counter } from 'src/models/counter.entity';
import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { ClientEncrypt } from 'src/models/clientEncrypt.entity'; import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { MessagePushingLog } from 'src/models/messagePushingLog.entity';
import { ClientEncryptController } from './controllers/clientEncrpt.controller'; import { ClientEncryptController } from './controllers/clientEncrpt.controller';
import { CounterController } from './controllers/counter.controller'; import { CounterController } from './controllers/counter.controller';
@ -26,6 +30,8 @@ import { ConfigModule } from '@nestjs/config';
Counter, Counter,
SurveyResponse, SurveyResponse,
ClientEncrypt, ClientEncrypt,
MessagePushingTask,
MessagePushingLog,
]), ]),
ConfigModule, ConfigModule,
], ],
@ -41,6 +47,8 @@ import { ConfigModule } from '@nestjs/config';
SurveyResponseService, SurveyResponseService,
CounterService, CounterService,
ClientEncryptService, ClientEncryptService,
MessagePushingTaskService,
MessagePushingLogService,
], ],
exports: [ exports: [
ResponseSchemaService, ResponseSchemaService,

View File

@ -0,0 +1,155 @@
import { ObjectId } from 'mongodb';
import { getPushingData } from './messagePushing';
import { RECORD_STATUS } from 'src/enums';
describe('getPushingData', () => {
it('should combine survey response data with response schema correctly', () => {
const surveyResponse = {
_id: new ObjectId('65fc2dd77f4520858046e129'),
clientTime: 1711025112552,
createDate: 1711025113146,
curStatus: {
status: RECORD_STATUS.NEW,
date: 1711025113146,
},
difTime: 30518,
data: {
data458: '15000000000',
data515: '115019',
data450: '450111000000000000',
data405: '浙江省杭州市西湖区xxx',
data770: '123456@qq.com',
},
optionTextAndId: {
data515: [
{
hash: '115019',
text: '<p>男</p>',
},
{
hash: '115020',
text: '<p>女</p>',
},
],
},
pageId: '65f29f3192862d6a9067ad1c',
statusList: [
{
status: RECORD_STATUS.NEW,
date: 1711025113146,
},
],
surveyPath: 'EBzdmnSp',
updateDate: 1711025113146,
secretKeys: [],
};
// Mock response schema data
const responseSchema = {
_id: new ObjectId('65f29f8892862d6a9067ad25'),
pageId: '65f29f3192862d6a9067ad1c',
surveyPath: 'EBzdmnSp',
code: {
dataConf: {
dataList: [
{
field: 'data458',
title: '<p>您的手机号</p>',
},
{
field: 'data515',
title: '<p>您的性别</p>',
options: [
{
text: '<p>男</p>',
others: false,
mustOthers: false,
othersKey: '',
placeholderDesc: '',
hash: '115019',
},
{
text: '<p>女</p>',
others: false,
mustOthers: false,
othersKey: '',
placeholderDesc: '',
hash: '115020',
},
],
},
{
field: 'data450',
title: '<p>身份证</p>',
},
{
field: 'data405',
title: '<p>地址</p>',
},
{
field: 'data770',
title: '<p>邮箱</p>',
},
],
},
},
};
const result = getPushingData({
surveyResponse,
questionList: responseSchema?.code?.dataConf?.dataList || [],
surveyId: responseSchema.pageId,
surveyPath: responseSchema.surveyPath,
});
// Assertions
expect(result).toEqual({
surveyId: responseSchema.pageId,
surveyPath: responseSchema.surveyPath,
surveyResponseId: surveyResponse._id.toString(),
data: [
{
questionId: 'data458',
title: '<p>您的手机号</p>',
valueType: 'text',
alias: '',
value: ['15000000000'],
},
{
questionId: 'data515',
title: '<p>您的性别</p>',
valueType: 'option',
alias: '',
value: [
{
alias: '',
id: '115019',
text: '<p>男</p>',
},
],
},
{
questionId: 'data450',
title: '<p>身份证</p>',
valueType: 'text',
alias: '',
value: ['450111000000000000'],
},
{
questionId: 'data405',
title: '<p>地址</p>',
valueType: 'text',
alias: '',
value: ['浙江省杭州市西湖区xxx'],
},
{
questionId: 'data770',
title: '<p>邮箱</p>',
valueType: 'text',
alias: '',
value: ['123456@qq.com'],
},
],
});
});
});

View File

@ -0,0 +1,87 @@
export enum VALUE_TYPE {
TEXT = 'text',
OPTION = 'option',
}
/**
*
* @param param0.surveyResponse
* @param param0.responseSchema
* @returns
*/
export const getPushingData = ({
surveyResponse,
questionList,
surveyId,
surveyPath,
}) => {
const surveyResponseId = surveyResponse._id.toString();
const data = questionList
.filter((question) => {
const value = surveyResponse.data[question.field];
return value !== undefined;
})
.map((question) => {
// 遍历题目列表
let value = surveyResponse.data[question.field];
// 统一数组格式,不区分题型还有单选多选
value = Array.isArray(value) ? value : [value];
let valueType = VALUE_TYPE.TEXT;
const optionTextAndId = surveyResponse?.optionTextAndId?.[question.field];
if (Array.isArray(optionTextAndId) && optionTextAndId.length > 0) {
// 选项类的
value = value.map((val) => {
const index = optionTextAndId.findIndex((item) => item.hash === val);
if (index > -1) {
valueType = VALUE_TYPE.OPTION;
// 拿到选项id、选项文本和别名
const ret: Record<string, any> = {
alias: '',
id: optionTextAndId[index].hash,
text: optionTextAndId[index].text,
};
const extraKey = `${question.field}_${ret.id}`;
if (surveyResponse.data[extraKey]) {
// 更多输入框
ret.extraText = surveyResponse.data[extraKey];
}
return ret;
}
return val;
});
}
if (typeof value[0] === 'number') {
// 评分、nps类的
value = value.map((val) => {
valueType = VALUE_TYPE.OPTION;
const extraKey = `${question.field}_${val}`;
// 组装成选项类的格式
const ret: Record<string, any> = {
alias: '',
id: val,
text: val.toString(),
};
if (surveyResponse.data[extraKey]) {
// 更多输入框
ret.extraText = surveyResponse.data[extraKey];
}
return ret;
});
}
// 返回题目id、题目标题、数据类型、别名目前未开放、还有用户的答案
return {
questionId: question.field,
title: question.title,
valueType,
alias: '',
value,
};
});
// 返回问卷id、问卷path、回收id和组装好的问卷和答案数据
return {
surveyId: surveyId,
surveyPath: surveyPath,
surveyResponseId,
data,
};
};