diff --git a/server/package.json b/server/package.json index d0a7fa02..4123bdd2 100644 --- a/server/package.json +++ b/server/package.json @@ -24,6 +24,7 @@ "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", "@nestjs/serve-static": "^4.0.0", + "@nestjs/swagger": "^7.3.0", "@nestjs/typeorm": "^10.0.1", "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", @@ -35,6 +36,7 @@ "moment": "^2.30.1", "mongodb": "^5.9.2", "nanoid": "^3.3.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", @@ -87,4 +89,4 @@ "^src/(.*)$": "/$1" } } -} \ No newline at end of file +} diff --git a/server/src/app.module.ts b/server/src/app.module.ts index eabda8b9..5137aa10 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -27,6 +27,8 @@ import { Counter } from './models/counter.entity'; import { SurveyResponse } from './models/surveyResponse.entity'; import { ClientEncrypt } from './models/clientEncrypt.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 { PluginManagerProvider } from './securityPlugin/pluginManager.provider'; @@ -69,6 +71,8 @@ import { Logger } from './logger'; ResponseSchema, ClientEncrypt, Word, + MessagePushingTask, + MessagePushingLog, ], }; }, diff --git a/server/src/enums/messagePushing.ts b/server/src/enums/messagePushing.ts new file mode 100644 index 00000000..4e9410ea --- /dev/null +++ b/server/src/enums/messagePushing.ts @@ -0,0 +1,7 @@ +export enum MESSAGE_PUSHING_TYPE { + HTTP = 'http', +} + +export enum MESSAGE_PUSHING_HOOK { + RESPONSE_INSERTED = 'response_inserted', +} diff --git a/server/src/logger/index.ts b/server/src/logger/index.ts index d9eda72b..f9e789d0 100644 --- a/server/src/logger/index.ts +++ b/server/src/logger/index.ts @@ -1,13 +1,12 @@ import * as log4js from 'log4js'; import moment from 'moment'; -import { REQUEST } from '@nestjs/core'; -import { Inject, Request } from '@nestjs/common'; +import { Request } from 'express'; const log4jsLogger = log4js.getLogger(); export class Logger { private static inited = false; - constructor(@Inject(REQUEST) private req: Request) {} + constructor() {} static init(config: { filename: string }) { 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 level = options.level; const dltag = options.dltag ? `${options.dltag}||` : ''; - const traceIdStr = this.req['traceId'] - ? `traceid=${this.req['traceId']}||` + const traceIdStr = options?.req['traceId'] + ? `traceid=${options?.req['traceId']}||` : ''; return log4jsLogger[level]( `[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`, ); } - info(message, options = { dltag: '' }) { + info(message, options?: { dltag?: string; req?: Request }) { 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' }); } } diff --git a/server/src/main.ts b/server/src/main.ts index df3eff45..d764600e 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,9 +1,26 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; async function bootstrap() { const PORT = process.env.PORT || 3000; 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); console.log(`server is running at: http://127.0.0.1:${PORT}`); } diff --git a/server/src/middlewares/logRequest.middleware.ts b/server/src/middlewares/logRequest.middleware.ts index dac00d01..ade62496 100644 --- a/server/src/middlewares/logRequest.middleware.ts +++ b/server/src/middlewares/logRequest.middleware.ts @@ -20,6 +20,7 @@ export class LogRequestMiddleware implements NestMiddleware { `method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`, { dltag: 'request_in', + req, }, ); @@ -29,6 +30,7 @@ export class LogRequestMiddleware implements NestMiddleware { `status=${res.statusCode.toString()}||duration=${duration}ms`, { dltag: 'request_out', + req, }, ); }); diff --git a/server/src/models/__test/base.entity.spec.ts b/server/src/models/__test/base.entity.spec.ts index cd7a78f9..eb4c90c9 100644 --- a/server/src/models/__test/base.entity.spec.ts +++ b/server/src/models/__test/base.entity.spec.ts @@ -25,6 +25,6 @@ describe('BaseEntity', () => { const now = Date.now(); baseEntity.onUpdate(); - expect(baseEntity.updateDate).toBeCloseTo(now, -3); // Check if date is close to current time + expect(baseEntity.updateDate).toBeCloseTo(now, -3); }); }); diff --git a/server/src/models/messagePushingLog.entity.ts b/server/src/models/messagePushingLog.entity.ts new file mode 100644 index 00000000..fef2dccf --- /dev/null +++ b/server/src/models/messagePushingLog.entity.ts @@ -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; + + @Column('jsonb') + response: Record; + + @Column() + status: number; // http状态码 +} diff --git a/server/src/models/messagePushingTask.entity.ts b/server/src/models/messagePushingTask.entity.ts new file mode 100644 index 00000000..90aad278 --- /dev/null +++ b/server/src/models/messagePushingTask.entity.ts @@ -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; + + @Column() + creatorId: string; + + @Column() + ownerId: string; +} diff --git a/server/src/modules/auth/__test/captcha.service.spec.ts b/server/src/modules/auth/__test/captcha.service.spec.ts index 62be802f..4c45a243 100644 --- a/server/src/modules/auth/__test/captcha.service.spec.ts +++ b/server/src/modules/auth/__test/captcha.service.spec.ts @@ -82,8 +82,6 @@ describe('CaptchaService', () => { expect(captchaRepository.delete).toHaveBeenCalledWith(mockCaptchaId); }); - - // Add more test cases for different scenarios }); describe('checkCaptchaIsCorrect', () => { diff --git a/server/src/modules/auth/controllers/auth.controller.ts b/server/src/modules/auth/controllers/auth.controller.ts index 45db77e6..8211517e 100644 --- a/server/src/modules/auth/controllers/auth.controller.ts +++ b/server/src/modules/auth/controllers/auth.controller.ts @@ -1,14 +1,18 @@ 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 { 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 { HttpException } from 'src/exceptions/httpException'; - -import { create } from 'svg-captcha'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; - +import { create } from 'svg-captcha'; +import { ApiTags } from '@nestjs/swagger'; +@ApiTags('auth') @Controller('/api/auth') export class AuthController { constructor( diff --git a/server/src/modules/survey/__test/createMessagePushingTask.dto.spec.ts b/server/src/modules/survey/__test/createMessagePushingTask.dto.spec.ts new file mode 100644 index 00000000..f82a74c0 --- /dev/null +++ b/server/src/modules/survey/__test/createMessagePushingTask.dto.spec.ts @@ -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']); + }); +}); diff --git a/server/src/modules/survey/__test/dataStatistic.controller.spec.ts b/server/src/modules/survey/__test/dataStatistic.controller.spec.ts index e7685f50..428ccadc 100644 --- a/server/src/modules/survey/__test/dataStatistic.controller.spec.ts +++ b/server/src/modules/survey/__test/dataStatistic.controller.spec.ts @@ -1,17 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { ObjectId } from 'mongodb'; + import { DataStatisticController } from '../controllers/dataStatistic.controller'; import { DataStatisticService } from '../services/dataStatistic.service'; import { SurveyMetaService } from '../services/surveyMeta.service'; import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; + import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider'; import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; import { Authtication } from 'src/guards/authtication'; import { UserService } from 'src/modules/auth/services/user.service'; -import { ConfigService } from '@nestjs/config'; import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin'; -import { ObjectId } from 'mongodb'; - jest.mock('../services/dataStatistic.service'); jest.mock('../services/surveyMeta.service'); jest.mock('../../surveyResponse/services/responseScheme.service'); diff --git a/server/src/modules/survey/__test/messagePushingTask.controller.spec.ts b/server/src/modules/survey/__test/messagePushingTask.controller.spec.ts new file mode 100644 index 00000000..bf0bc0e8 --- /dev/null +++ b/server/src/modules/survey/__test/messagePushingTask.controller.spec.ts @@ -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, + ); + service = module.get(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 }); + }); + }); +}); diff --git a/server/src/modules/survey/__test/messagePushingTask.dto.spec.ts b/server/src/modules/survey/__test/messagePushingTask.dto.spec.ts new file mode 100644 index 00000000..5a8ba713 --- /dev/null +++ b/server/src/modules/survey/__test/messagePushingTask.dto.spec.ts @@ -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); + }); +}); diff --git a/server/src/modules/survey/__test/messagePushingTask.service.spec.ts b/server/src/modules/survey/__test/messagePushingTask.service.spec.ts new file mode 100644 index 00000000..5e171849 --- /dev/null +++ b/server/src/modules/survey/__test/messagePushingTask.service.spec.ts @@ -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; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MessagePushingTaskService, + { + provide: getRepositoryToken(MessagePushingTask), + useClass: MongoRepository, + }, + ], + }).compile(); + + service = module.get(MessagePushingTaskService); + repository = module.get>( + 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, + }, + }, + ); + }); + }); +}); diff --git a/server/src/modules/survey/__test/queryMessagePushingTaskList.dto.spec.ts b/server/src/modules/survey/__test/queryMessagePushingTaskList.dto.spec.ts new file mode 100644 index 00000000..d4bef618 --- /dev/null +++ b/server/src/modules/survey/__test/queryMessagePushingTaskList.dto.spec.ts @@ -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 + }); +}); diff --git a/server/src/modules/survey/__test/survey.controller.spec.ts b/server/src/modules/survey/__test/survey.controller.spec.ts index b1202c2e..80366955 100644 --- a/server/src/modules/survey/__test/survey.controller.spec.ts +++ b/server/src/modules/survey/__test/survey.controller.spec.ts @@ -10,8 +10,8 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyConf } from 'src/models/surveyConf.entity'; import { HttpException } from 'src/exceptions/httpException'; 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/surveyConf.service'); jest.mock('../../surveyResponse/services/responseScheme.service'); @@ -37,6 +37,7 @@ describe('SurveyController', () => { ResponseSchemaService, ContentSecurityService, SurveyHistoryService, + LoggerProvider, ], }).compile(); diff --git a/server/src/modules/survey/__test/surveyConf.service.spec.ts b/server/src/modules/survey/__test/surveyConf.service.spec.ts index 824af2ae..010de720 100644 --- a/server/src/modules/survey/__test/surveyConf.service.spec.ts +++ b/server/src/modules/survey/__test/surveyConf.service.spec.ts @@ -87,7 +87,6 @@ describe('SurveyConfService', () => { code: schema, } as unknown as SurveyConf); - // 调用待测试的方法 await service.saveSurveyConf({ surveyId, schema }); // 验证save方法被调用了一次,并且传入了正确的参数 @@ -120,7 +119,6 @@ describe('SurveyConfService', () => { expect(surveyConfRepository.save).not.toHaveBeenCalled(); }); - // getSurveyContentByCode方法的单元测试 it('should get survey content by code', async () => { // 准备参数和模拟数据 const schema = { diff --git a/server/src/modules/survey/__test/surveyHistory.service.spec.ts b/server/src/modules/survey/__test/surveyHistory.service.spec.ts index 5c856a1b..f226cfc0 100644 --- a/server/src/modules/survey/__test/surveyHistory.service.spec.ts +++ b/server/src/modules/survey/__test/surveyHistory.service.spec.ts @@ -115,6 +115,10 @@ describe('SurveyHistoryService', () => { type: historyType, }, take: 100, + order: { + createDate: -1, + }, + select: ['createDate', 'operator', 'type', '_id'], }); }); }); diff --git a/server/src/modules/survey/__test/surveyMeta.controller.spec.ts b/server/src/modules/survey/__test/surveyMeta.controller.spec.ts index 1835d88a..3773eaa4 100644 --- a/server/src/modules/survey/__test/surveyMeta.controller.spec.ts +++ b/server/src/modules/survey/__test/surveyMeta.controller.spec.ts @@ -2,8 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SurveyMetaController } from '../controllers/surveyMeta.controller'; import { SurveyMetaService } from '../services/surveyMeta.service'; import { Authtication } from 'src/guards/authtication'; -import * as Joi from 'joi'; 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', () => { let controller: SurveyMetaController; @@ -23,6 +25,7 @@ describe('SurveyMetaController', () => { .mockResolvedValue({ count: 0, data: [] }), }, }, + LoggerProvider, ], }) .overrideGuard(Authtication) @@ -74,9 +77,7 @@ describe('SurveyMetaController', () => { }); it('should validate request body with Joi', async () => { - const reqBody = { - // Missing title and surveyId - }; + const reqBody = {}; const req = { user: { username: 'test-user', @@ -86,8 +87,8 @@ describe('SurveyMetaController', () => { try { await controller.updateMeta(reqBody, req); } catch (error) { - expect(error).toBeInstanceOf(Joi.ValidationError); - expect(error.details[0].message).toMatch('"title" is required'); + expect(error).toBeInstanceOf(HttpException); + expect(error.code).toBe(EXCEPTION_CODE.PARAMETER_ERROR); } expect(surveyMetaService.checkSurveyAccess).not.toHaveBeenCalled(); diff --git a/server/src/modules/survey/__test/updateMessagePushingTask.dto.spec.ts b/server/src/modules/survey/__test/updateMessagePushingTask.dto.spec.ts new file mode 100644 index 00000000..ac2bae62 --- /dev/null +++ b/server/src/modules/survey/__test/updateMessagePushingTask.dto.spec.ts @@ -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(); + }); +}); diff --git a/server/src/modules/survey/controllers/dataStatistic.controller.ts b/server/src/modules/survey/controllers/dataStatistic.controller.ts index 258b6c6c..0dde2d4e 100644 --- a/server/src/modules/survey/controllers/dataStatistic.controller.ts +++ b/server/src/modules/survey/controllers/dataStatistic.controller.ts @@ -12,9 +12,11 @@ import { SurveyMetaService } from '../services/surveyMeta.service'; import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; import * as Joi from 'joi'; +import { ApiTags } from '@nestjs/swagger'; import { Authtication } from 'src/guards/authtication'; import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; +@ApiTags('survey') @Controller('/api/survey/dataStatistic') export class DataStatisticController { constructor( diff --git a/server/src/modules/survey/controllers/messagePushingTask.controller.ts b/server/src/modules/survey/controllers/messagePushingTask.controller.ts new file mode 100644 index 00000000..c3a03559 --- /dev/null +++ b/server/src/modules/survey/controllers/messagePushingTask.controller.ts @@ -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, + }; + } +} diff --git a/server/src/modules/survey/controllers/survey.controller.ts b/server/src/modules/survey/controllers/survey.controller.ts index a577c9b2..7516cf5e 100644 --- a/server/src/modules/survey/controllers/survey.controller.ts +++ b/server/src/modules/survey/controllers/survey.controller.ts @@ -16,12 +16,16 @@ import { ContentSecurityService } from '../services/contentSecurity.service'; import { SurveyHistoryService } from '../services/surveyHistory.service'; import BannerData from '../template/banner/index.json'; + import * as Joi from 'joi'; +import { ApiTags } from '@nestjs/swagger'; import { Authtication } from 'src/guards/authtication'; import { HISTORY_TYPE } from 'src/enums'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; +import { Logger } from 'src/logger'; +@ApiTags('survey') @Controller('/api/survey') export class SurveyController { constructor( @@ -30,6 +34,7 @@ export class SurveyController { private readonly responseSchemaService: ResponseSchemaService, private readonly contentSecurityService: ContentSecurityService, private readonly surveyHistoryService: SurveyHistoryService, + private readonly logger: Logger, ) {} @Get('/getBannerData') @@ -68,6 +73,9 @@ export class SurveyController { }), }).validateAsync(reqBody); } catch (error) { + this.logger.error(`createSurvey_parameter error: ${error.message}`, { + req, + }); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); } diff --git a/server/src/modules/survey/controllers/surveyHistory.controller.ts b/server/src/modules/survey/controllers/surveyHistory.controller.ts index d977e69b..6a48d4ac 100644 --- a/server/src/modules/survey/controllers/surveyHistory.controller.ts +++ b/server/src/modules/survey/controllers/surveyHistory.controller.ts @@ -11,8 +11,10 @@ import { SurveyHistoryService } from '../services/surveyHistory.service'; import { SurveyMetaService } from '../services/surveyMeta.service'; import * as Joi from 'joi'; +import { ApiTags } from '@nestjs/swagger'; import { Authtication } from 'src/guards/authtication'; +@ApiTags('survey') @Controller('/api/surveyHisotry') export class SurveyHistoryController { constructor( diff --git a/server/src/modules/survey/controllers/surveyMeta.controller.ts b/server/src/modules/survey/controllers/surveyMeta.controller.ts index f3e9d822..f9101fd3 100644 --- a/server/src/modules/survey/controllers/surveyMeta.controller.ts +++ b/server/src/modules/survey/controllers/surveyMeta.controller.ts @@ -10,17 +10,23 @@ import { } from '@nestjs/common'; import * as Joi from 'joi'; import moment from 'moment'; +import { ApiTags } from '@nestjs/swagger'; import { getFilter, getOrder } from 'src/utils/surveyUtil'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { Authtication } from 'src/guards/authtication'; +import { Logger } from 'src/logger'; import { SurveyMetaService } from '../services/surveyMeta.service'; +@ApiTags('survey') @Controller('/api/survey') export class SurveyMetaController { - constructor(private readonly surveyMetaService: SurveyMetaService) {} + constructor( + private readonly surveyMetaService: SurveyMetaService, + private readonly logger: Logger, + ) {} @UseGuards(Authtication) @Post('/updateMeta') @@ -34,6 +40,9 @@ export class SurveyMetaController { surveyId: Joi.string().required(), }).validateAsync(reqBody, { allowUnknown: true }); } catch (error) { + this.logger.error(`updateMeta_parameter error: ${error.message}`, { + req, + }); throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); } diff --git a/server/src/modules/survey/controllers/surveyUI.controller.ts b/server/src/modules/survey/controllers/surveyUI.controller.ts index b1ec37ac..c010f780 100644 --- a/server/src/modules/survey/controllers/surveyUI.controller.ts +++ b/server/src/modules/survey/controllers/surveyUI.controller.ts @@ -1,7 +1,8 @@ import { Controller, Get, Res } from '@nestjs/common'; import { Response } from 'express'; import { join } from 'path'; - +import { ApiTags } from '@nestjs/swagger'; +@ApiTags('ui') @Controller() export class SurveyUIController { constructor() {} diff --git a/server/src/modules/survey/dto/createMessagePushingTask.dto.ts b/server/src/modules/survey/dto/createMessagePushingTask.dto.ts new file mode 100644 index 00000000..ab53c869 --- /dev/null +++ b/server/src/modules/survey/dto/createMessagePushingTask.dto.ts @@ -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[]; +} diff --git a/server/src/modules/survey/dto/messagePushingTask.dto.ts b/server/src/modules/survey/dto/messagePushingTask.dto.ts new file mode 100644 index 00000000..5144b178 --- /dev/null +++ b/server/src/modules/survey/dto/messagePushingTask.dto.ts @@ -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]; +} diff --git a/server/src/modules/survey/dto/queryMessagePushingTaskList.dto.ts b/server/src/modules/survey/dto/queryMessagePushingTaskList.dto.ts new file mode 100644 index 00000000..5f8196ca --- /dev/null +++ b/server/src/modules/survey/dto/queryMessagePushingTaskList.dto.ts @@ -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; +} diff --git a/server/src/modules/survey/dto/updateMessagePushingTask.dto.ts b/server/src/modules/survey/dto/updateMessagePushingTask.dto.ts new file mode 100644 index 00000000..bf4eb43b --- /dev/null +++ b/server/src/modules/survey/dto/updateMessagePushingTask.dto.ts @@ -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; + }; +} diff --git a/server/src/modules/survey/services/messagePushingTask.service.ts b/server/src/modules/survey/services/messagePushingTask.service.ts new file mode 100644 index 00000000..b5332857 --- /dev/null +++ b/server/src/modules/survey/services/messagePushingTask.service.ts @@ -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, + ) {} + + async create( + createMessagePushingTaskDto: CreateMessagePushingTaskDto & { + ownerId: string; + }, + ): Promise { + 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 { + const where: Record = { + '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 { + 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 { + 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, + }, + }, + ); + } +} diff --git a/server/src/modules/survey/services/surveyHistory.service.ts b/server/src/modules/survey/services/surveyHistory.service.ts index 264ee763..0b512faf 100644 --- a/server/src/modules/survey/services/surveyHistory.service.ts +++ b/server/src/modules/survey/services/surveyHistory.service.ts @@ -47,6 +47,7 @@ export class SurveyHistoryService { order: { createDate: -1, }, + select: ['createDate', 'operator', 'type', '_id'], }); } } diff --git a/server/src/modules/survey/survey.module.ts b/server/src/modules/survey/survey.module.ts index e651da0a..8b1b5860 100644 --- a/server/src/modules/survey/survey.module.ts +++ b/server/src/modules/survey/survey.module.ts @@ -2,6 +2,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { LoggerProvider } from 'src/logger/logger.provider'; + import { SurveyResponseModule } from '../surveyResponse/surveyResponse.module'; import { AuthModule } from '../auth/auth.module'; @@ -10,18 +12,21 @@ import { SurveyController } from './controllers/survey.controller'; import { SurveyHistoryController } from './controllers/surveyHistory.controller'; import { SurveyMetaController } from './controllers/surveyMeta.controller'; import { SurveyUIController } from './controllers/surveyUI.controller'; +import { MessagePushingTaskController } from './controllers/messagePushingTask.controller'; 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 { Word } from 'src/models/word.entity'; +import { MessagePushingTask } from 'src/models/messagePushingTask.entity'; import { DataStatisticService } from './services/dataStatistic.service'; import { SurveyConfService } from './services/surveyConf.service'; import { SurveyHistoryService } from './services/surveyHistory.service'; import { SurveyMetaService } from './services/surveyMeta.service'; import { ContentSecurityService } from './services/contentSecurity.service'; +import { MessagePushingTaskService } from './services/messagePushingTask.service'; import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider'; @@ -33,6 +38,7 @@ import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider SurveyHistory, SurveyResponse, Word, + MessagePushingTask, ]), ConfigModule, SurveyResponseModule, @@ -44,6 +50,7 @@ import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider SurveyHistoryController, SurveyMetaController, SurveyUIController, + MessagePushingTaskController, ], providers: [ DataStatisticService, @@ -52,6 +59,8 @@ import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider SurveyMetaService, PluginManagerProvider, ContentSecurityService, + MessagePushingTaskService, + LoggerProvider, ], }) export class SurveyModule {} diff --git a/server/src/modules/survey/template/surveyTemplate/survey/nps.json b/server/src/modules/survey/template/surveyTemplate/survey/nps.json index 2618856d..40d8127b 100644 --- a/server/src/modules/survey/template/surveyTemplate/survey/nps.json +++ b/server/src/modules/survey/template/surveyTemplate/survey/nps.json @@ -113,7 +113,6 @@ "type": "radio-star", "title": "标题2", "answer": "", - "options": [], "textRange": { "min": { "placeholder": "0", diff --git a/server/src/modules/survey/template/surveyTemplate/survey/vote.json b/server/src/modules/survey/template/surveyTemplate/survey/vote.json index 9c25ad76..26a9fa9e 100644 --- a/server/src/modules/survey/template/surveyTemplate/survey/vote.json +++ b/server/src/modules/survey/template/surveyTemplate/survey/vote.json @@ -27,24 +27,6 @@ "checked": false, "minNum": "", "maxNum": "", - "options": [ - { - "text": "选项1", - "imageUrl": "", - "others": false, - "mustOthers": false, - "othersKey": "", - "placeholderDesc": "" - }, - { - "text": "选项2", - "imageUrl": "", - "others": false, - "mustOthers": false, - "othersKey": "", - "placeholderDesc": "" - } - ], "star": 5, "nps": { "leftText": "极不满意", diff --git a/server/src/modules/surveyResponse/__test/messagePushingLog.service.spec.ts b/server/src/modules/surveyResponse/__test/messagePushingLog.service.spec.ts new file mode 100644 index 00000000..556320b7 --- /dev/null +++ b/server/src/modules/surveyResponse/__test/messagePushingLog.service.spec.ts @@ -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; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MessagePushingLogService, + { + provide: getRepositoryToken(MessagePushingLog), + useClass: MongoRepository, + }, + ], + }).compile(); + + service = module.get(MessagePushingLogService); + repository = module.get>( + 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) }, + }); + }); + }); +}); diff --git a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts index 91d76315..910c80c2 100644 --- a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts +++ b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts @@ -1,17 +1,27 @@ 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 { ResponseSchemaService } from '../services/responseScheme.service'; import { CounterService } from '../services/counter.service'; import { SurveyResponseService } from '../services/surveyResponse.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 { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager'; -import { ObjectId } from 'mongodb'; import { HttpException } from 'src/exceptions/httpException'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; -import { cloneDeep } from 'lodash'; 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 = { surveyPath: 'EBzdmnSp', @@ -65,9 +75,10 @@ const mockClientEncryptInfo = { describe('SurveyResponseController', () => { let controller: SurveyResponseController; let responseSchemaService: ResponseSchemaService; - // let counterService: CounterService; let surveyResponseService: SurveyResponseService; let clientEncryptService: ClientEncryptService; + let messagePushingTaskService: MessagePushingTaskService; + let messagePushingLogService: MessagePushingLogService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -102,6 +113,18 @@ describe('SurveyResponseController', () => { .mockResolvedValue(mockClientEncryptInfo), }, }, + { + provide: MessagePushingTaskService, + useValue: { + findAll: jest.fn(), + }, + }, + { + provide: MessagePushingLogService, + useValue: { + createPushingLog: jest.fn(), + }, + }, PluginManagerProvider, ], }).compile(); @@ -110,13 +133,20 @@ describe('SurveyResponseController', () => { responseSchemaService = module.get( ResponseSchemaService, ); - // counterService = module.get(CounterService); surveyResponseService = module.get( SurveyResponseService, ); clientEncryptService = module.get(ClientEncryptService); + messagePushingTaskService = module.get( + MessagePushingTaskService, + ); + + messagePushingLogService = module.get( + MessagePushingLogService, + ); + const pluginManager = module.get( XiaojuSurveyPluginManager, ); @@ -141,10 +171,52 @@ describe('SurveyResponseController', () => { .mockResolvedValueOnce(0); jest .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: '

', + }, + { + hash: '115020', + text: '

', + }, + ], + }, + pageId: '65f29f3192862d6a9067ad1c', + statusList: [ + { + status: RECORD_STATUS.NEW, + date: 1711025113146, + }, + ], + + surveyPath: 'EBzdmnSp', + updateDate: 1711025113146, + secretKeys: [], + } as unknown as SurveyResponse); jest .spyOn(clientEncryptService, 'deleteEncryptInfo') .mockResolvedValueOnce(undefined); + jest + .spyOn(controller, 'sendSurveyResponseMessage') + .mockReturnValueOnce(undefined); const result = await controller.createResponse(reqBody); @@ -166,7 +238,7 @@ describe('SurveyResponseController', () => { }, clientTime: reqBody.clientTime, difTime: reqBody.difTime, - surveyId: mockResponseSchema.pageId, // mock response schema 的 pageId + surveyId: mockResponseSchema.pageId, optionTextAndId: { data515: [ { @@ -180,6 +252,7 @@ describe('SurveyResponseController', () => { ], }, }); + expect(clientEncryptService.deleteEncryptInfo).toHaveBeenCalledWith( 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: '

您的手机号

', + valueType: 'text', + alias: '', + value: ['15000000000'], + }, + { + questionId: 'data515', + title: '

您的性别

', + valueType: 'option', + alias: '', + value: [ + { + alias: '', + id: '115019', + text: '

', + }, + ], + }, + { + questionId: 'data450', + title: '

身份证

', + valueType: 'text', + alias: '', + value: ['450111000000000000'], + }, + { + questionId: 'data405', + title: '

地址

', + valueType: 'text', + alias: '', + value: ['浙江省杭州市西湖区xxx'], + }, + { + questionId: 'data770', + title: '

邮箱

', + 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(); + }); + }); }); diff --git a/server/src/modules/surveyResponse/__test/surveyResponseUI.controller.spec.ts b/server/src/modules/surveyResponse/__test/surveyResponseUI.controller.spec.ts index ae0a6e71..5e9fa1b4 100644 --- a/server/src/modules/surveyResponse/__test/surveyResponseUI.controller.spec.ts +++ b/server/src/modules/surveyResponse/__test/surveyResponseUI.controller.spec.ts @@ -21,10 +21,9 @@ describe('SurveyResponseUIController', () => { }); it('should render the survey response with the correct path', () => { - const surveyPath = 'some-survey-path'; const expectedFilePath = join(process.cwd(), 'public', 'render.html'); - controller.render(surveyPath, res); + controller.render(res); expect(res.sendFile).toHaveBeenCalledWith(expectedFilePath); }); diff --git a/server/src/modules/surveyResponse/controllers/clientEncrpt.controller.ts b/server/src/modules/surveyResponse/controllers/clientEncrpt.controller.ts index 1af1c7c4..71bc9e72 100644 --- a/server/src/modules/surveyResponse/controllers/clientEncrpt.controller.ts +++ b/server/src/modules/surveyResponse/controllers/clientEncrpt.controller.ts @@ -3,7 +3,9 @@ import { ConfigService } from '@nestjs/config'; import { ClientEncryptService } from '../services/clientEncrypt.service'; import * as forge from 'node-forge'; import { ENCRYPT_TYPE } from 'src/enums/encrypt'; +import { ApiTags } from '@nestjs/swagger'; +@ApiTags('surveyResponse') @Controller('/api/clientEncrypt') export class ClientEncryptController { constructor( diff --git a/server/src/modules/surveyResponse/controllers/counter.controller.ts b/server/src/modules/surveyResponse/controllers/counter.controller.ts index 29f4444b..a795e62a 100644 --- a/server/src/modules/surveyResponse/controllers/counter.controller.ts +++ b/server/src/modules/surveyResponse/controllers/counter.controller.ts @@ -2,7 +2,9 @@ import { Controller, Get, HttpCode, Query } from '@nestjs/common'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { CounterService } from '../services/counter.service'; +import { ApiTags } from '@nestjs/swagger'; +@ApiTags('surveyResponse') @Controller('/api/counter') export class CounterController { constructor(private readonly counterService: CounterService) {} diff --git a/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts b/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts index bd567b9e..30fd2055 100644 --- a/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts +++ b/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts @@ -3,7 +3,9 @@ import { ResponseSchemaService } from '../services/responseScheme.service'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { RECORD_STATUS } from 'src/enums'; +import { ApiTags } from '@nestjs/swagger'; +@ApiTags('surveyResponse') @Controller('/api/responseSchema') export class ResponseSchemaController { constructor(private readonly responseSchemaService: ResponseSchemaService) {} diff --git a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts index 5e5b68ad..06aaee6e 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts @@ -2,16 +2,28 @@ import { Controller, Post, Body, HttpCode } from '@nestjs/common'; import { HttpException } from 'src/exceptions/httpException'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; import { checkSign } from 'src/utils/checkSign'; -import * as Joi from 'joi'; +import { ENCRYPT_TYPE } from 'src/enums/encrypt'; 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 { CounterService } from '../services/counter.service'; -import moment from 'moment'; import { SurveyResponseService } from '../services/surveyResponse.service'; import { ClientEncryptService } from '../services/clientEncrypt.service'; -import { ENCRYPT_TYPE } from 'src/enums/encrypt'; -import * as forge from 'node-forge'; +import { MessagePushingTaskService } from '../../survey/services/messagePushingTask.service'; +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') export class SurveyResponseController { constructor( @@ -19,6 +31,8 @@ export class SurveyResponseController { private readonly counterService: CounterService, private readonly surveyResponseService: SurveyResponseService, private readonly clientEncryptService: ClientEncryptService, + private readonly messagePushingTaskService: MessagePushingTaskService, + private readonly messagePushingLogService: MessagePushingLogService, ) {} @Post('/createResponse') @@ -174,13 +188,27 @@ export class SurveyResponseController { } // 入库 - await this.surveyResponseService.createSurveyResponse({ - surveyPath: validationResult.surveyPath, - data: decryptedData, - clientTime, - difTime, + const surveyResponse = + await this.surveyResponseService.createSurveyResponse({ + surveyPath: validationResult.surveyPath, + data: decryptedData, + 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, - optionTextAndId, }); // 入库成功后,要把密钥删掉,防止被重复使用 @@ -191,4 +219,53 @@ export class SurveyResponseController { 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) {} + } } diff --git a/server/src/modules/surveyResponse/controllers/surveyResponseUI.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponseUI.controller.ts index e0c16784..6bbe3999 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponseUI.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponseUI.controller.ts @@ -1,13 +1,14 @@ -import { Controller, Get, Param, Res } from '@nestjs/common'; +import { Controller, Get, Res } from '@nestjs/common'; import { Response } from 'express'; import { join } from 'path'; - +import { ApiTags } from '@nestjs/swagger'; +@ApiTags('ui') @Controller() export class SurveyResponseUIController { constructor() {} - @Get('/render/:surveyPath') - render(@Param('surveyPath') surveyPath: string, @Res() res: Response) { + @Get('/render/:path*') + render(@Res() res: Response) { res.sendFile(join(process.cwd(), 'public', 'render.html')); } } diff --git a/server/src/modules/surveyResponse/services/messagePushingLog.service.ts b/server/src/modules/surveyResponse/services/messagePushingLog.service.ts new file mode 100644 index 00000000..ccae8cf4 --- /dev/null +++ b/server/src/modules/surveyResponse/services/messagePushingLog.service.ts @@ -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, + ) {} + + async createPushingLog({ + taskId, + request, + response, + status, + }): Promise { + const createdLog = this.messagePushingLogRepository.create({ + taskId, + request, + response, + status, + }); + return await this.messagePushingLogRepository.save(createdLog); + } + + async findAllByTaskId(taskId: string): Promise { + return await this.messagePushingLogRepository.find({ + where: { + taskId, + }, + }); + } + + async findOne(id: string): Promise { + return await this.messagePushingLogRepository.findOne({ + where: { + _id: new ObjectId(id), + }, + }); + } +} diff --git a/server/src/modules/surveyResponse/services/surveyResponse.service.ts b/server/src/modules/surveyResponse/services/surveyResponse.service.ts index 341bb44d..b9438a81 100644 --- a/server/src/modules/surveyResponse/services/surveyResponse.service.ts +++ b/server/src/modules/surveyResponse/services/surveyResponse.service.ts @@ -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) { diff --git a/server/src/modules/surveyResponse/surveyResponse.module.ts b/server/src/modules/surveyResponse/surveyResponse.module.ts index e6f7edc7..50fc790b 100644 --- a/server/src/modules/surveyResponse/surveyResponse.module.ts +++ b/server/src/modules/surveyResponse/surveyResponse.module.ts @@ -4,11 +4,15 @@ import { ResponseSchemaService } from './services/responseScheme.service'; import { SurveyResponseService } from './services/surveyResponse.service'; import { CounterService } from './services/counter.service'; import { ClientEncryptService } from './services/clientEncrypt.service'; +import { MessagePushingTaskService } from '../survey/services/messagePushingTask.service'; +import { MessagePushingLogService } from './services/messagePushingLog.service'; import { ResponseSchema } from 'src/models/responseSchema.entity'; import { Counter } from 'src/models/counter.entity'; import { SurveyResponse } from 'src/models/surveyResponse.entity'; import { ClientEncrypt } from 'src/models/clientEncrypt.entity'; +import { MessagePushingTask } from 'src/models/messagePushingTask.entity'; +import { MessagePushingLog } from 'src/models/messagePushingLog.entity'; import { ClientEncryptController } from './controllers/clientEncrpt.controller'; import { CounterController } from './controllers/counter.controller'; @@ -26,6 +30,8 @@ import { ConfigModule } from '@nestjs/config'; Counter, SurveyResponse, ClientEncrypt, + MessagePushingTask, + MessagePushingLog, ]), ConfigModule, ], @@ -41,6 +47,8 @@ import { ConfigModule } from '@nestjs/config'; SurveyResponseService, CounterService, ClientEncryptService, + MessagePushingTaskService, + MessagePushingLogService, ], exports: [ ResponseSchemaService, diff --git a/server/src/utils/messagePushing.spec.ts b/server/src/utils/messagePushing.spec.ts new file mode 100644 index 00000000..010706ad --- /dev/null +++ b/server/src/utils/messagePushing.spec.ts @@ -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: '

', + }, + { + hash: '115020', + text: '

', + }, + ], + }, + 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: '

您的手机号

', + }, + { + field: 'data515', + title: '

您的性别

', + options: [ + { + text: '

', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115019', + }, + { + text: '

', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115020', + }, + ], + }, + { + field: 'data450', + title: '

身份证

', + }, + { + field: 'data405', + title: '

地址

', + }, + { + field: 'data770', + title: '

邮箱

', + }, + ], + }, + }, + }; + + 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: '

您的手机号

', + valueType: 'text', + alias: '', + value: ['15000000000'], + }, + { + questionId: 'data515', + title: '

您的性别

', + valueType: 'option', + alias: '', + value: [ + { + alias: '', + id: '115019', + text: '

', + }, + ], + }, + { + questionId: 'data450', + title: '

身份证

', + valueType: 'text', + alias: '', + value: ['450111000000000000'], + }, + { + questionId: 'data405', + title: '

地址

', + valueType: 'text', + alias: '', + value: ['浙江省杭州市西湖区xxx'], + }, + { + questionId: 'data770', + title: '

邮箱

', + valueType: 'text', + alias: '', + value: ['123456@qq.com'], + }, + ], + }); + }); +}); diff --git a/server/src/utils/messagePushing.ts b/server/src/utils/messagePushing.ts new file mode 100644 index 00000000..a5908fa8 --- /dev/null +++ b/server/src/utils/messagePushing.ts @@ -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 = { + 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 = { + 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, + }; +};