为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,期待您的参与!
', + subTitle: '', + applyTitle: '', + }, + bannerConfig: { + bgImage: '/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.webp', + bgImageAllowJump: false, + bgImageJumpLink: '', + videoLink: '', + postImg: '', + }, + }, + baseConf: { + begTime: '2024-05-31 20:31:36', + endTime: '2034-05-31 20:31:36', + language: 'chinese', + showVoteProcess: 'allow', + tLimit: 0, + answerBegTime: '00:00:00', + answerEndTime: '23:59:59', + answerLimitTime: 0, + }, + bottomConf: { + logoImage: '/imgs/Logo.webp', + logoImageWidth: '60%', + }, + skinConf: { + backgroundConf: { + color: '#fff', + }, + themeConf: { + color: '#ffa600', + }, + contentConf: { + opacity: 100, + }, + skinColor: '#4a4c5b', + inputBgColor: '#ffffff', + }, + submitConf: { + submitTitle: '提交', + msgContent: { + msg_200: '提交成功', + msg_9001: '您来晚了,感谢支持问卷~', + msg_9002: '请勿多次提交!', + msg_9003: '您来晚了,已经满额!', + msg_9004: '提交失败!', + }, + confirmAgain: { + is_again: true, + again_text: '确认要提交吗?', + }, + link: '', + }, + logicConf: { + showLogicConf: [], + }, + dataConf: { + dataList: [ + { + isRequired: true, + showIndex: true, + showType: true, + showSpliter: true, + type: 'radio', + placeholderDesc: '', + field: 'data515', + title: '标题2', + placeholder: '', + randomSort: false, + checked: false, + minNum: '', + maxNum: '', + options: [ + { + text: '选项1', + imageUrl: '', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115019', + }, + { + text: '选项2', + imageUrl: '', + others: false, + mustOthers: false, + othersKey: '', + placeholderDesc: '', + hash: '115020', + }, + ], + importKey: 'single', + importData: '', + cOption: '', + cOptions: [], + star: 5, + exclude: false, + textRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '500', + value: 500, + }, + }, + }, + { + field: 'data893', + showIndex: true, + showType: true, + showSpliter: true, + type: 'checkbox', + placeholderDesc: '', + sLimit: 0, + mhLimit: 0, + title: '标题2', + placeholder: '', + valid: '', + isRequired: true, + randomSort: false, + showLeftNum: true, + innerRandom: false, + checked: false, + selectType: 'radio', + sortWay: 'v', + noNps: '', + minNum: '', + maxNum: '', + starStyle: 'star', + starMin: 1, + starMax: 5, + min: 0, + max: 10, + minMsg: '极不满意', + maxMsg: '十分满意', + rangeConfig: {}, + options: [ + { + text: '选项1', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '466671', + }, + { + text: '选项2', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '095415', + }, + ], + star: 5, + optionOrigin: '', + originType: 'selected', + matrixOptionsRely: '', + numberRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '1000', + value: 1000, + }, + }, + textRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '500', + value: 500, + }, + }, + }, + { + field: 'data820', + showIndex: true, + showType: true, + showSpliter: true, + type: 'radio-nps', + placeholderDesc: '', + sLimit: 0, + mhLimit: 0, + title: '标题3', + placeholder: '', + valid: '', + isRequired: true, + randomSort: false, + showLeftNum: true, + innerRandom: false, + checked: false, + selectType: 'radio', + sortWay: 'v', + noNps: '', + minNum: '', + maxNum: '', + starStyle: 'star', + starMin: 1, + starMax: 5, + min: 0, + max: 10, + minMsg: '极不满意', + maxMsg: '十分满意', + rangeConfig: {}, + options: [ + { + text: '选项1', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '268884', + }, + { + text: '选项2', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '371166', + }, + ], + star: 5, + optionOrigin: '', + originType: 'selected', + matrixOptionsRely: '', + numberRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '1000', + value: 1000, + }, + }, + textRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '500', + value: 500, + }, + }, + }, + { + field: 'data549', + showIndex: true, + showType: true, + showSpliter: true, + type: 'radio-star', + placeholderDesc: '', + sLimit: 0, + mhLimit: 0, + title: '标题4', + placeholder: '', + valid: '', + isRequired: true, + randomSort: false, + showLeftNum: true, + innerRandom: false, + checked: false, + selectType: 'radio', + sortWay: 'v', + noNps: '', + minNum: '', + maxNum: '', + starStyle: 'star', + starMin: 1, + starMax: 5, + min: 0, + max: 10, + minMsg: '极不满意', + maxMsg: '十分满意', + rangeConfig: {}, + options: [ + { + text: '选项1', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '274183', + }, + { + text: '选项2', + others: false, + othersKey: '', + placeholderDesc: '', + hash: '842967', + }, + ], + star: 5, + optionOrigin: '', + originType: 'selected', + matrixOptionsRely: '', + numberRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '1000', + value: 1000, + }, + }, + textRange: { + min: { + placeholder: '0', + value: 0, + }, + max: { + placeholder: '500', + value: 500, + }, + }, + }, + ], + }, + }, + pageId: '6659c3283b1cb279bc2e2b0c', + }; + + const mockAggregationResult = [ + { + field: 'data515', + data: { + aggregation: [ + { + id: '115019', + count: 1, + }, + { + id: '115020', + count: 1, + }, + ], + submitionCount: 2, + }, + }, + { + field: 'data893', + data: { + aggregation: [ + { + id: '466671', + count: 2, + }, + { + id: '095415', + count: 1, + }, + ], + submitionCount: 2, + }, + }, + { + field: 'data820', + data: { + aggregation: [ + { + id: '8', + count: 1, + }, + ], + submitionCount: 1, + }, + }, + { + field: 'data549', + data: { + aggregation: [ + { + id: '5', + count: 1, + }, + ], + submitionCount: 1, + }, + }, + ]; + + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPageId') + .mockResolvedValueOnce(mockResponseSchema as any); + jest + .spyOn(dataStatisticService, 'aggregationStatis') + .mockResolvedValueOnce(mockAggregationResult); + + const result = await controller.aggregationStatis(mockRequest.query); + + expect(result).toEqual({ + code: 200, + data: expect.any(Array), + }); + }); + + it('should throw an exception if validation fails', async () => { + const mockRequest = { + query: { + surveyId: '', + }, + }; + + await expect( + controller.aggregationStatis(mockRequest.query), + ).rejects.toThrow(HttpException); + }); + + it('should return empty data if response schema does not exist', async () => { + const mockRequest = { + query: { + surveyId: new ObjectId().toString(), + }, + }; + + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPageId') + .mockResolvedValueOnce(null); + + const result = await controller.aggregationStatis(mockRequest.query); + + expect(result).toEqual({ + code: 200, + data: [], + }); + }); }); }); diff --git a/server/src/modules/survey/__test/dataStatistic.service.spec.ts b/server/src/modules/survey/__test/dataStatistic.service.spec.ts index 830c7b59..1130364f 100644 --- a/server/src/modules/survey/__test/dataStatistic.service.spec.ts +++ b/server/src/modules/survey/__test/dataStatistic.service.spec.ts @@ -197,7 +197,6 @@ describe('DataStatisticService', () => { data413_3: expect.any(String), data413: expect.any(Number), data863: expect.any(String), - data413_custom: expect.any(String), difTime: expect.any(String), createDate: expect.any(String), }), @@ -310,4 +309,161 @@ describe('DataStatisticService', () => { ); }); }); + + describe('aggregationStatis', () => { + it('should return correct aggregation data', async () => { + const surveyId = '65afc62904d5db18534c0f78'; + const mockAggregationResult = { + data515: [ + { + count: 1, + data: { + data515: '115019', + }, + }, + { + count: 1, + data: { + data515: '115020', + }, + }, + ], + data893: [ + { + count: 1, + data: { + data893: ['466671'], + }, + }, + { + count: 1, + data: { + data893: ['466671', '095415'], + }, + }, + ], + data820: [ + { + count: 1, + data: { + data820: 8, + }, + }, + ], + data549: [ + { + count: 1, + data: { + data549: 5, + }, + }, + ], + }; + + const fieldList = Object.keys(mockAggregationResult); + + jest.spyOn(surveyResponseRepository, 'aggregate').mockReturnValue({ + next: jest.fn().mockResolvedValue(mockAggregationResult), + } as any); + + const result = await service.aggregationStatis({ + surveyId, + fieldList, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { + field: 'data515', + data: { + aggregation: [ + { + id: '115019', + count: 1, + }, + { + id: '115020', + count: 1, + }, + ], + submitionCount: 2, + }, + }, + { + field: 'data893', + data: { + aggregation: [ + { + id: '466671', + count: 2, + }, + { + id: '095415', + count: 1, + }, + ], + submitionCount: 2, + }, + }, + { + field: 'data820', + data: { + aggregation: [ + { + id: '8', + count: 1, + }, + ], + submitionCount: 1, + }, + }, + { + field: 'data549', + data: { + aggregation: [ + { + id: '5', + count: 1, + }, + ], + submitionCount: 1, + }, + }, + ]), + ); + }); + + it('should return empty aggregation data when no responses', async () => { + const surveyId = '65afc62904d5db18534c0f78'; + const fieldList = ['data458', 'data515']; + + jest.spyOn(surveyResponseRepository, 'aggregate').mockReturnValue({ + next: jest.fn().mockResolvedValue({}), + } as any); + + const result = await service.aggregationStatis({ + surveyId, + fieldList, + }); + + expect(result).toEqual( + expect.arrayContaining([ + { + field: 'data458', + data: { + aggregation: [], + submitionCount: 0, + }, + }, + { + field: 'data515', + data: { + aggregation: [], + submitionCount: 0, + }, + }, + ]), + ); + }); + }); }); diff --git a/server/src/modules/survey/controllers/collaborator.controller.ts b/server/src/modules/survey/controllers/collaborator.controller.ts index 35fc1d11..08c97068 100644 --- a/server/src/modules/survey/controllers/collaborator.controller.ts +++ b/server/src/modules/survey/controllers/collaborator.controller.ts @@ -319,6 +319,7 @@ export class CollaboratorController { const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId }); if (!surveyMeta) { + this.logger.error(`问卷不存在: ${surveyId}`, { req }); throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND); } diff --git a/server/src/modules/survey/controllers/dataStatistic.controller.ts b/server/src/modules/survey/controllers/dataStatistic.controller.ts index a0b1754c..452b4fc6 100644 --- a/server/src/modules/survey/controllers/dataStatistic.controller.ts +++ b/server/src/modules/survey/controllers/dataStatistic.controller.ts @@ -20,6 +20,8 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; import { Logger } from 'src/logger'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; +import { AggregationStatisDto } from '../dto/aggregationStatis.dto'; +import { handleAggretionData } from '../utils'; @ApiTags('survey') @ApiBearerAuth() @@ -80,4 +82,50 @@ export class DataStatisticController { }, }; } + + @Get('/aggregationStatis') + @HttpCode(200) + @UseGuards(Authentication) + async aggregationStatis(@Query() queryInfo: AggregationStatisDto) { + // 聚合统计 + const { value, error } = AggregationStatisDto.validate(queryInfo); + if (error) { + throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); + } + const responseSchema = + await this.responseSchemaService.getResponseSchemaByPageId( + value.surveyId, + ); + if (!responseSchema) { + return { + code: 200, + data: [], + }; + } + const allowQuestionType = [ + 'radio', + 'checkbox', + 'binary-choice', + 'radio-star', + 'radio-nps', + 'vote', + ]; + const fieldList = responseSchema.code.dataConf.dataList + .filter((item) => allowQuestionType.includes(item.type)) + .map((item) => item.field); + const dataMap = responseSchema.code.dataConf.dataList.reduce((pre, cur) => { + pre[cur.field] = cur; + return pre; + }, {}); + const res = await this.dataStatisticService.aggregationStatis({ + surveyId: value.surveyId, + fieldList, + }); + return { + code: 200, + data: res.map((item) => { + return handleAggretionData({ item, dataMap }); + }), + }; + } } diff --git a/server/src/modules/survey/dto/aggregationStatis.dto.ts b/server/src/modules/survey/dto/aggregationStatis.dto.ts new file mode 100644 index 00000000..a8f747a7 --- /dev/null +++ b/server/src/modules/survey/dto/aggregationStatis.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import Joi from 'joi'; + +export class AggregationStatisDto { + @ApiProperty({ description: '问卷id', required: true }) + surveyId: string; + + static validate(data) { + return Joi.object({ + surveyId: Joi.string().required(), + }).validate(data); + } +} diff --git a/server/src/modules/survey/dto/batchSaveCollaborator.dto.ts b/server/src/modules/survey/dto/batchSaveCollaborator.dto.ts index 1ae04787..119fe584 100644 --- a/server/src/modules/survey/dto/batchSaveCollaborator.dto.ts +++ b/server/src/modules/survey/dto/batchSaveCollaborator.dto.ts @@ -3,7 +3,10 @@ import Joi from 'joi'; import { SURVEY_PERMISSION } from 'src/enums/surveyPermission'; export class CollaboratorDto { - @ApiProperty({ description: '用户id', required: false }) + @ApiProperty({ description: '协作id', required: false }) + _id?: string; + + @ApiProperty({ description: '用户id', required: true }) userId: string; @ApiProperty({ @@ -16,7 +19,7 @@ export class CollaboratorDto { SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE, ], }) - permissions: Array{{ questionTypeDesc }}
+