feat: 新增分题统计相关功能 (#275)
This commit is contained in:
parent
053d9751c3
commit
0b42899347
@ -11,6 +11,7 @@ import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
|||||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||||
import { Collaborator } from 'src/models/collaborator.entity';
|
import { Collaborator } from 'src/models/collaborator.entity';
|
||||||
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
|
|
||||||
describe('SurveyGuard', () => {
|
describe('SurveyGuard', () => {
|
||||||
let guard: SurveyGuard;
|
let guard: SurveyGuard;
|
||||||
@ -81,7 +82,19 @@ describe('SurveyGuard', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow access if user is the owner of the survey', async () => {
|
it('should allow access if user is the owner of the survey by ownerId', async () => {
|
||||||
|
const context = createMockExecutionContext();
|
||||||
|
const surveyMeta = { ownerId: 'testUserId', workspaceId: null };
|
||||||
|
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||||
|
|
||||||
|
const result = await guard.canActivate(context);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow access if user is the owner of the survey by username', async () => {
|
||||||
const context = createMockExecutionContext();
|
const context = createMockExecutionContext();
|
||||||
const surveyMeta = { owner: 'testUser', workspaceId: null };
|
const surveyMeta = { owner: 'testUser', workspaceId: null };
|
||||||
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||||
@ -108,7 +121,35 @@ describe('SurveyGuard', () => {
|
|||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw NoPermissionException if user has no permissions', async () => {
|
it('should throw NoPermissionException if user is not a workspace member', async () => {
|
||||||
|
const context = createMockExecutionContext();
|
||||||
|
const surveyMeta = { owner: 'anotherUser', workspaceId: 'workspaceId' };
|
||||||
|
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||||
|
jest.spyOn(workspaceMemberService, 'findOne').mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||||
|
NoPermissionException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw NoPermissionException if no permissions are provided', async () => {
|
||||||
|
const context = createMockExecutionContext();
|
||||||
|
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
||||||
|
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
|
||||||
|
jest.spyOn(reflector, 'get').mockReturnValueOnce(null);
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||||
|
|
||||||
|
await expect(guard.canActivate(context)).rejects.toThrow(
|
||||||
|
NoPermissionException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw NoPermissionException if user has no matching permissions', async () => {
|
||||||
const context = createMockExecutionContext();
|
const context = createMockExecutionContext();
|
||||||
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
||||||
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
|
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
|
||||||
@ -125,6 +166,24 @@ describe('SurveyGuard', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow access if user has the required permissions', async () => {
|
||||||
|
const context = createMockExecutionContext();
|
||||||
|
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
||||||
|
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
|
||||||
|
jest
|
||||||
|
.spyOn(reflector, 'get')
|
||||||
|
.mockReturnValueOnce([SURVEY_PERMISSION.SURVEY_CONF_MANAGE]);
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||||
|
jest.spyOn(collaboratorService, 'getCollaborator').mockResolvedValue({
|
||||||
|
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||||
|
} as Collaborator);
|
||||||
|
|
||||||
|
const result = await guard.canActivate(context);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
function createMockExecutionContext(): ExecutionContext {
|
function createMockExecutionContext(): ExecutionContext {
|
||||||
return {
|
return {
|
||||||
switchToHttp: jest.fn().mockReturnValue({
|
switchToHttp: jest.fn().mockReturnValue({
|
||||||
|
@ -6,6 +6,7 @@ import { User } from 'src/models/user.entity';
|
|||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { hash256 } from 'src/utils/hash256';
|
import { hash256 } from 'src/utils/hash256';
|
||||||
import { RECORD_STATUS } from 'src/enums';
|
import { RECORD_STATUS } from 'src/enums';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
|
||||||
describe('UserService', () => {
|
describe('UserService', () => {
|
||||||
let service: UserService;
|
let service: UserService;
|
||||||
@ -21,6 +22,7 @@ describe('UserService', () => {
|
|||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
save: jest.fn(),
|
save: jest.fn(),
|
||||||
findOne: jest.fn(),
|
findOne: jest.fn(),
|
||||||
|
find: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -32,6 +34,10 @@ describe('UserService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('should create a user', async () => {
|
it('should create a user', async () => {
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
username: 'testUser',
|
username: 'testUser',
|
||||||
@ -102,7 +108,7 @@ describe('UserService', () => {
|
|||||||
expect(user).toEqual({ ...userInfo, password: hashedPassword });
|
expect(user).toEqual({ ...userInfo, password: hashedPassword });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined when user is not found by credentials', async () => {
|
it('should return null when user is not found by credentials', async () => {
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
username: 'nonExistingUser',
|
username: 'nonExistingUser',
|
||||||
password: 'nonExistingPassword',
|
password: 'nonExistingPassword',
|
||||||
@ -129,7 +135,8 @@ describe('UserService', () => {
|
|||||||
const userInfo = {
|
const userInfo = {
|
||||||
username: username,
|
username: username,
|
||||||
password: 'existingPassword',
|
password: 'existingPassword',
|
||||||
} as User;
|
curStatus: { status: 'ACTIVE' },
|
||||||
|
} as unknown as User;
|
||||||
|
|
||||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
||||||
|
|
||||||
@ -137,10 +144,129 @@ describe('UserService', () => {
|
|||||||
|
|
||||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
|
||||||
username: username,
|
username: username,
|
||||||
|
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(user).toEqual(userInfo);
|
expect(user).toEqual(userInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return null when user is not found by username', async () => {
|
||||||
|
const username = 'nonExistingUser';
|
||||||
|
|
||||||
|
const findOneSpy = jest
|
||||||
|
.spyOn(userRepository, 'findOne')
|
||||||
|
.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const user = await service.getUserByUsername(username);
|
||||||
|
|
||||||
|
expect(findOneSpy).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
username: username,
|
||||||
|
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a user by id', async () => {
|
||||||
|
const id = '60c72b2f9b1e8a5f4b123456';
|
||||||
|
const userInfo = {
|
||||||
|
_id: new ObjectId(id),
|
||||||
|
username: 'testUser',
|
||||||
|
curStatus: { status: 'ACTIVE' },
|
||||||
|
} as unknown as User;
|
||||||
|
|
||||||
|
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
||||||
|
|
||||||
|
const user = await service.getUserById(id);
|
||||||
|
|
||||||
|
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
_id: new ObjectId(id),
|
||||||
|
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user).toEqual(userInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when user is not found by id', async () => {
|
||||||
|
const id = '60c72b2f9b1e8a5f4b123456';
|
||||||
|
|
||||||
|
const findOneSpy = jest
|
||||||
|
.spyOn(userRepository, 'findOne')
|
||||||
|
.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const user = await service.getUserById(id);
|
||||||
|
|
||||||
|
expect(findOneSpy).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
_id: new ObjectId(id),
|
||||||
|
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of users by username', async () => {
|
||||||
|
const username = 'test';
|
||||||
|
const userList = [
|
||||||
|
{ _id: new ObjectId(), username: 'testUser1', createDate: new Date() },
|
||||||
|
{ _id: new ObjectId(), username: 'testUser2', createDate: new Date() },
|
||||||
|
];
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(userRepository, 'find')
|
||||||
|
.mockResolvedValue(userList as unknown as User[]);
|
||||||
|
|
||||||
|
const result = await service.getUserListByUsername({
|
||||||
|
username,
|
||||||
|
skip: 0,
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userRepository.find).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
username: new RegExp(username),
|
||||||
|
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||||
|
},
|
||||||
|
skip: 0,
|
||||||
|
take: 10,
|
||||||
|
select: ['_id', 'username', 'createDate'],
|
||||||
|
});
|
||||||
|
expect(result).toEqual(userList);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of users by ids', async () => {
|
||||||
|
const idList = ['60c72b2f9b1e8a5f4b123456', '60c72b2f9b1e8a5f4b123457'];
|
||||||
|
const userList = [
|
||||||
|
{
|
||||||
|
_id: new ObjectId(idList[0]),
|
||||||
|
username: 'testUser1',
|
||||||
|
createDate: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: new ObjectId(idList[1]),
|
||||||
|
username: 'testUser2',
|
||||||
|
createDate: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(userRepository, 'find')
|
||||||
|
.mockResolvedValue(userList as unknown as User[]);
|
||||||
|
|
||||||
|
const result = await service.getUserListByIds({ idList });
|
||||||
|
|
||||||
|
expect(userRepository.find).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
_id: {
|
||||||
|
$in: idList.map((id) => new ObjectId(id)),
|
||||||
|
},
|
||||||
|
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||||
|
},
|
||||||
|
select: ['_id', 'username', 'createDate'],
|
||||||
|
});
|
||||||
|
expect(result).toEqual(userList);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,13 @@ import { UserService } from 'src/modules/auth/services/user.service';
|
|||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
import {
|
||||||
|
SURVEY_PERMISSION,
|
||||||
|
SURVEY_PERMISSION_DESCRIPTION,
|
||||||
|
} from 'src/enums/surveyPermission';
|
||||||
|
import { BatchSaveCollaboratorDto } from '../dto/batchSaveCollaborator.dto';
|
||||||
|
import { User } from 'src/models/user.entity';
|
||||||
|
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||||
|
|
||||||
jest.mock('src/guards/authentication.guard');
|
jest.mock('src/guards/authentication.guard');
|
||||||
jest.mock('src/guards/survey.guard');
|
jest.mock('src/guards/survey.guard');
|
||||||
@ -21,6 +27,8 @@ describe('CollaboratorController', () => {
|
|||||||
let collaboratorService: CollaboratorService;
|
let collaboratorService: CollaboratorService;
|
||||||
let logger: Logger;
|
let logger: Logger;
|
||||||
let userService: UserService;
|
let userService: UserService;
|
||||||
|
let surveyMetaService: SurveyMetaService;
|
||||||
|
let workspaceMemberServie: WorkspaceMemberService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -34,12 +42,18 @@ describe('CollaboratorController', () => {
|
|||||||
changeUserPermission: jest.fn(),
|
changeUserPermission: jest.fn(),
|
||||||
deleteCollaborator: jest.fn(),
|
deleteCollaborator: jest.fn(),
|
||||||
getCollaborator: jest.fn(),
|
getCollaborator: jest.fn(),
|
||||||
|
batchDeleteBySurveyId: jest.fn(),
|
||||||
|
batchCreate: jest.fn(),
|
||||||
|
batchDelete: jest.fn(),
|
||||||
|
updateById: jest.fn(),
|
||||||
|
batchSaveCollaborator: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: Logger,
|
provide: Logger,
|
||||||
useValue: {
|
useValue: {
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -72,6 +86,10 @@ describe('CollaboratorController', () => {
|
|||||||
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
||||||
logger = module.get<Logger>(Logger);
|
logger = module.get<Logger>(Logger);
|
||||||
userService = module.get<UserService>(UserService);
|
userService = module.get<UserService>(UserService);
|
||||||
|
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||||
|
workspaceMemberServie = module.get<WorkspaceMemberService>(
|
||||||
|
WorkspaceMemberService,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@ -115,6 +133,59 @@ describe('CollaboratorController', () => {
|
|||||||
HttpException,
|
HttpException,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if user does not exist', async () => {
|
||||||
|
const reqBody: CreateCollaboratorDto = {
|
||||||
|
surveyId: 'surveyId',
|
||||||
|
userId: new ObjectId().toString(),
|
||||||
|
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||||
|
};
|
||||||
|
const req = {
|
||||||
|
user: { _id: 'userId' },
|
||||||
|
surveyMeta: { ownerId: new ObjectId().toString() },
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(userService, 'getUserById').mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow(
|
||||||
|
HttpException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if user is the survey owner', async () => {
|
||||||
|
const userId = new ObjectId().toString();
|
||||||
|
const reqBody: CreateCollaboratorDto = {
|
||||||
|
surveyId: 'surveyId',
|
||||||
|
userId: userId,
|
||||||
|
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||||
|
};
|
||||||
|
const req = { user: { _id: 'userId' }, surveyMeta: { ownerId: userId } };
|
||||||
|
|
||||||
|
await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow(
|
||||||
|
HttpException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if user is already a collaborator', async () => {
|
||||||
|
const userId = new ObjectId().toString();
|
||||||
|
const reqBody: CreateCollaboratorDto = {
|
||||||
|
surveyId: 'surveyId',
|
||||||
|
userId: userId,
|
||||||
|
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||||
|
};
|
||||||
|
const req = {
|
||||||
|
user: { _id: 'userId' },
|
||||||
|
surveyMeta: { ownerId: new ObjectId().toString() },
|
||||||
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(collaboratorService, 'getCollaborator')
|
||||||
|
.mockResolvedValue({} as unknown as Collaborator);
|
||||||
|
|
||||||
|
await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow(
|
||||||
|
HttpException,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getSurveyCollaboratorList', () => {
|
describe('getSurveyCollaboratorList', () => {
|
||||||
@ -217,4 +288,229 @@ describe('CollaboratorController', () => {
|
|||||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 新增的测试方法
|
||||||
|
describe('getPermissionList', () => {
|
||||||
|
it('should return the permission list', async () => {
|
||||||
|
const result = Object.values(SURVEY_PERMISSION_DESCRIPTION);
|
||||||
|
|
||||||
|
const response = await controller.getPermissionList();
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
code: 200,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('batchSaveCollaborator', () => {
|
||||||
|
it('should batch save collaborators successfully', async () => {
|
||||||
|
const userId0 = new ObjectId().toString();
|
||||||
|
const userId1 = new ObjectId().toString();
|
||||||
|
const existsCollaboratorId = new ObjectId().toString();
|
||||||
|
const surveyId = new ObjectId().toString();
|
||||||
|
const reqBody: BatchSaveCollaboratorDto = {
|
||||||
|
surveyId: surveyId,
|
||||||
|
collaborators: [
|
||||||
|
{
|
||||||
|
userId: userId0,
|
||||||
|
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: existsCollaboratorId,
|
||||||
|
userId: userId1,
|
||||||
|
permissions: [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const req = {
|
||||||
|
user: { _id: 'requestUserId' },
|
||||||
|
surveyMeta: { ownerId: 'ownerId' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const userList = [
|
||||||
|
{ _id: new ObjectId(userId0) },
|
||||||
|
{ _id: new ObjectId(userId1) },
|
||||||
|
];
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(userService, 'getUserListByIds')
|
||||||
|
.mockResolvedValue(userList as unknown as User[]);
|
||||||
|
jest
|
||||||
|
.spyOn(collaboratorService, 'batchDelete')
|
||||||
|
.mockResolvedValue({ deletedCount: 1, acknowledged: true });
|
||||||
|
jest
|
||||||
|
.spyOn(collaboratorService, 'batchCreate')
|
||||||
|
.mockResolvedValue([{}] as any);
|
||||||
|
jest.spyOn(collaboratorService, 'updateById').mockResolvedValue({});
|
||||||
|
|
||||||
|
const response = await controller.batchSaveCollaborator(reqBody, req);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
code: 200,
|
||||||
|
});
|
||||||
|
expect(userService.getUserListByIds).toHaveBeenCalled();
|
||||||
|
expect(collaboratorService.batchDelete).toHaveBeenCalled();
|
||||||
|
expect(collaboratorService.batchCreate).toHaveBeenCalled();
|
||||||
|
expect(collaboratorService.updateById).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if validation fails', async () => {
|
||||||
|
const reqBody: BatchSaveCollaboratorDto = {
|
||||||
|
surveyId: '',
|
||||||
|
collaborators: [
|
||||||
|
{
|
||||||
|
userId: '',
|
||||||
|
permissions: [SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const req = { user: { _id: 'userId' } };
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.batchSaveCollaborator(reqBody, req),
|
||||||
|
).rejects.toThrow(HttpException);
|
||||||
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUserSurveyPermissions', () => {
|
||||||
|
it('should return owner permissions if user is the owner', async () => {
|
||||||
|
const req = {
|
||||||
|
user: { _id: new ObjectId(), username: 'owner' },
|
||||||
|
};
|
||||||
|
const query = { surveyId: 'surveyId' };
|
||||||
|
const surveyMeta = {
|
||||||
|
ownerId: req.user._id.toString(),
|
||||||
|
owner: req.user.username,
|
||||||
|
workspaceId: 'workspaceId',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||||
|
|
||||||
|
const response = await controller.getUserSurveyPermissions(req, query);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
isOwner: true,
|
||||||
|
permissions: [
|
||||||
|
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||||
|
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||||
|
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default permissions if user is a workspace member', async () => {
|
||||||
|
const req = {
|
||||||
|
user: { _id: new ObjectId(), username: 'user' },
|
||||||
|
};
|
||||||
|
const query = { surveyId: 'surveyId' };
|
||||||
|
const surveyMeta = {
|
||||||
|
ownerId: 'ownerId',
|
||||||
|
owner: 'owner',
|
||||||
|
workspaceId: 'workspaceId',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||||
|
jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue({} as any);
|
||||||
|
|
||||||
|
const response = await controller.getUserSurveyPermissions(req, query);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
isOwner: false,
|
||||||
|
permissions: [
|
||||||
|
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||||
|
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||||
|
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return collaborator permissions if user is a collaborator', async () => {
|
||||||
|
const req = {
|
||||||
|
user: { _id: new ObjectId(), username: 'user' },
|
||||||
|
};
|
||||||
|
const query = { surveyId: 'surveyId' };
|
||||||
|
const surveyMeta = {
|
||||||
|
ownerId: 'ownerId',
|
||||||
|
owner: 'owner',
|
||||||
|
workspaceId: 'workspaceId',
|
||||||
|
};
|
||||||
|
const collaborator = {
|
||||||
|
permissions: ['read', 'write'],
|
||||||
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||||
|
jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue(null);
|
||||||
|
jest
|
||||||
|
.spyOn(collaboratorService, 'getCollaborator')
|
||||||
|
.mockResolvedValue(collaborator as Collaborator);
|
||||||
|
|
||||||
|
const response = await controller.getUserSurveyPermissions(req, query);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
isOwner: false,
|
||||||
|
permissions: collaborator.permissions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty permissions if user has no permissions', async () => {
|
||||||
|
const req = {
|
||||||
|
user: { _id: new ObjectId(), username: 'user' },
|
||||||
|
};
|
||||||
|
const query = { surveyId: 'surveyId' };
|
||||||
|
const surveyMeta = {
|
||||||
|
ownerId: 'ownerId',
|
||||||
|
owner: 'owner',
|
||||||
|
workspaceId: 'workspaceId',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValue(surveyMeta as SurveyMeta);
|
||||||
|
jest.spyOn(workspaceMemberServie, 'findOne').mockResolvedValue(null);
|
||||||
|
jest
|
||||||
|
.spyOn(collaboratorService, 'getCollaborator')
|
||||||
|
.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const response = await controller.getUserSurveyPermissions(req, query);
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
isOwner: false,
|
||||||
|
permissions: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if survey does not exist', async () => {
|
||||||
|
const req = { user: { _id: 'userId' } };
|
||||||
|
const query = { surveyId: 'nonexistentSurveyId' };
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(surveyMetaService, 'getSurveyById')
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.getUserSurveyPermissions(req, query),
|
||||||
|
).rejects.toThrow(HttpException);
|
||||||
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,7 @@ import { Logger } from 'src/logger';
|
|||||||
import { UserService } from 'src/modules/auth/services/user.service';
|
import { UserService } from 'src/modules/auth/services/user.service';
|
||||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||||
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
|
|
||||||
jest.mock('../services/dataStatistic.service');
|
jest.mock('../services/dataStatistic.service');
|
||||||
jest.mock('../services/surveyMeta.service');
|
jest.mock('../services/surveyMeta.service');
|
||||||
@ -21,11 +22,13 @@ jest.mock('../../surveyResponse/services/responseScheme.service');
|
|||||||
|
|
||||||
jest.mock('src/guards/authentication.guard');
|
jest.mock('src/guards/authentication.guard');
|
||||||
jest.mock('src/guards/survey.guard');
|
jest.mock('src/guards/survey.guard');
|
||||||
jest.mock('src/guards/workspace.guard');
|
|
||||||
|
|
||||||
describe('DataStatisticController', () => {
|
describe('DataStatisticController', () => {
|
||||||
let controller: DataStatisticController;
|
let controller: DataStatisticController;
|
||||||
let dataStatisticService: DataStatisticService;
|
let dataStatisticService: DataStatisticService;
|
||||||
|
let responseSchemaService: ResponseSchemaService;
|
||||||
|
let pluginManager: XiaojuSurveyPluginManager;
|
||||||
|
let logger: Logger;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
@ -64,9 +67,14 @@ describe('DataStatisticController', () => {
|
|||||||
controller = module.get<DataStatisticController>(DataStatisticController);
|
controller = module.get<DataStatisticController>(DataStatisticController);
|
||||||
dataStatisticService =
|
dataStatisticService =
|
||||||
module.get<DataStatisticService>(DataStatisticService);
|
module.get<DataStatisticService>(DataStatisticService);
|
||||||
const pluginManager = module.get<XiaojuSurveyPluginManager>(
|
responseSchemaService = module.get<ResponseSchemaService>(
|
||||||
|
ResponseSchemaService,
|
||||||
|
);
|
||||||
|
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||||
XiaojuSurveyPluginManager,
|
XiaojuSurveyPluginManager,
|
||||||
);
|
);
|
||||||
|
logger = module.get<Logger>(Logger);
|
||||||
|
|
||||||
pluginManager.registerPlugin(
|
pluginManager.registerPlugin(
|
||||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||||
);
|
);
|
||||||
@ -82,6 +90,9 @@ describe('DataStatisticController', () => {
|
|||||||
const mockRequest = {
|
const mockRequest = {
|
||||||
query: {
|
query: {
|
||||||
surveyId,
|
surveyId,
|
||||||
|
isDesensitive: false,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
username: 'testUser',
|
username: 'testUser',
|
||||||
@ -105,13 +116,13 @@ describe('DataStatisticController', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||||
.mockResolvedValueOnce({} as any);
|
.mockResolvedValueOnce({} as any);
|
||||||
jest
|
jest
|
||||||
.spyOn(dataStatisticService, 'getDataTable')
|
.spyOn(dataStatisticService, 'getDataTable')
|
||||||
.mockResolvedValueOnce(mockDataTable);
|
.mockResolvedValueOnce(mockDataTable);
|
||||||
|
|
||||||
const result = await controller.data(mockRequest.query, {});
|
const result = await controller.data(mockRequest.query, mockRequest);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
@ -125,6 +136,8 @@ describe('DataStatisticController', () => {
|
|||||||
query: {
|
query: {
|
||||||
surveyId,
|
surveyId,
|
||||||
isDesensitive: true,
|
isDesensitive: true,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
username: 'testUser',
|
username: 'testUser',
|
||||||
@ -146,19 +159,499 @@ describe('DataStatisticController', () => {
|
|||||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||||
.mockResolvedValueOnce({} as any);
|
.mockResolvedValueOnce({} as any);
|
||||||
jest
|
jest
|
||||||
.spyOn(dataStatisticService, 'getDataTable')
|
.spyOn(dataStatisticService, 'getDataTable')
|
||||||
.mockResolvedValueOnce(mockDataTable);
|
.mockResolvedValueOnce(mockDataTable);
|
||||||
|
|
||||||
const result = await controller.data(mockRequest.query, {});
|
const result = await controller.data(mockRequest.query, mockRequest);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
code: 200,
|
code: 200,
|
||||||
data: mockDataTable,
|
data: mockDataTable,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an exception if validation fails', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
query: {
|
||||||
|
surveyId: '',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
username: 'testUser',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.data(mockRequest.query, mockRequest),
|
||||||
|
).rejects.toThrow(HttpException);
|
||||||
|
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('aggregationStatis', () => {
|
||||||
|
it('should return aggregation statistics', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
query: {
|
||||||
|
surveyId: new ObjectId().toString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockResponseSchema = {
|
||||||
|
_id: new ObjectId('6659c3283b1cb279bc2e2b0c'),
|
||||||
|
curStatus: {
|
||||||
|
status: 'published',
|
||||||
|
date: 1717159136024,
|
||||||
|
},
|
||||||
|
statusList: [
|
||||||
|
{
|
||||||
|
status: 'published',
|
||||||
|
date: 1717158851823,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createDate: 1717158851823,
|
||||||
|
updateDate: 1717159136025,
|
||||||
|
title: '问卷调研',
|
||||||
|
surveyPath: 'ZdGNzTTR',
|
||||||
|
code: {
|
||||||
|
bannerConf: {
|
||||||
|
titleConfig: {
|
||||||
|
mainTitle:
|
||||||
|
'<h3 style="text-align: center">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style="color: rgb(204, 0, 0)">期待您的参与!</span></p>',
|
||||||
|
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: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -197,7 +197,6 @@ describe('DataStatisticService', () => {
|
|||||||
data413_3: expect.any(String),
|
data413_3: expect.any(String),
|
||||||
data413: expect.any(Number),
|
data413: expect.any(Number),
|
||||||
data863: expect.any(String),
|
data863: expect.any(String),
|
||||||
data413_custom: expect.any(String),
|
|
||||||
difTime: expect.any(String),
|
difTime: expect.any(String),
|
||||||
createDate: 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -319,6 +319,7 @@ export class CollaboratorController {
|
|||||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||||
|
|
||||||
if (!surveyMeta) {
|
if (!surveyMeta) {
|
||||||
|
this.logger.error(`问卷不存在: ${surveyId}`, { req });
|
||||||
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
|||||||
import { Logger } from 'src/logger';
|
import { Logger } from 'src/logger';
|
||||||
import { HttpException } from 'src/exceptions/httpException';
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
|
import { AggregationStatisDto } from '../dto/aggregationStatis.dto';
|
||||||
|
import { handleAggretionData } from '../utils';
|
||||||
|
|
||||||
@ApiTags('survey')
|
@ApiTags('survey')
|
||||||
@ApiBearerAuth()
|
@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 });
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
server/src/modules/survey/dto/aggregationStatis.dto.ts
Normal file
13
server/src/modules/survey/dto/aggregationStatis.dto.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,10 @@ import Joi from 'joi';
|
|||||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||||
|
|
||||||
export class CollaboratorDto {
|
export class CollaboratorDto {
|
||||||
@ApiProperty({ description: '用户id', required: false })
|
@ApiProperty({ description: '协作id', required: false })
|
||||||
|
_id?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户id', required: true })
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
@ -16,7 +19,7 @@ export class CollaboratorDto {
|
|||||||
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
permissions: Array<number>;
|
permissions: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BatchSaveCollaboratorDto {
|
export class BatchSaveCollaboratorDto {
|
||||||
|
@ -7,7 +7,7 @@ import moment from 'moment';
|
|||||||
import { keyBy } from 'lodash';
|
import { keyBy } from 'lodash';
|
||||||
import { DataItem } from 'src/interfaces/survey';
|
import { DataItem } from 'src/interfaces/survey';
|
||||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
import { getListHeadByDataList } from '../utils';
|
import { getListHeadByDataList, transformAndMergeArrayFields } from '../utils';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataStatisticService {
|
export class DataStatisticService {
|
||||||
private radioType = ['radio-star', 'radio-nps'];
|
private radioType = ['radio-star', 'radio-nps'];
|
||||||
@ -101,4 +101,62 @@ export class DataStatisticService {
|
|||||||
listBody,
|
listBody,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async aggregationStatis({ surveyId, fieldList }) {
|
||||||
|
const $facet = fieldList.reduce((pre, cur) => {
|
||||||
|
const $match = { $match: { [`data.${cur}`]: { $nin: [[], '', null] } } };
|
||||||
|
const $group = { $group: { _id: `$data.${cur}`, count: { $sum: 1 } } };
|
||||||
|
const $project = {
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
count: 1,
|
||||||
|
secretKeys: 1,
|
||||||
|
sensitiveKeys: 1,
|
||||||
|
[`data.${cur}`]: '$_id',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
pre[cur] = [$match, $group, $project];
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
const aggregation = this.surveyResponseRepository.aggregate(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
pageId: surveyId,
|
||||||
|
'curStatus.status': {
|
||||||
|
$ne: 'removed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $facet },
|
||||||
|
],
|
||||||
|
{ maxTimeMS: 30000, allowDiskUse: true },
|
||||||
|
);
|
||||||
|
const res = await aggregation.next();
|
||||||
|
const submitionCountMap: Record<string, number> = {};
|
||||||
|
for (const field in res) {
|
||||||
|
let count = 0;
|
||||||
|
if (Array.isArray(res[field])) {
|
||||||
|
for (const optionItem of res[field]) {
|
||||||
|
count += optionItem.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
submitionCountMap[field] = count;
|
||||||
|
}
|
||||||
|
const transformedData = transformAndMergeArrayFields(res);
|
||||||
|
return fieldList.map((field) => {
|
||||||
|
return {
|
||||||
|
field,
|
||||||
|
data: {
|
||||||
|
aggregation: (transformedData?.[field] || []).map((optionItem) => {
|
||||||
|
return {
|
||||||
|
id: optionItem.data[field],
|
||||||
|
count: optionItem.count,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
submitionCount: submitionCountMap?.[field] || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,3 +66,177 @@ export function getListHeadByDataList(dataList) {
|
|||||||
});
|
});
|
||||||
return listHead;
|
return listHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function transformAndMergeArrayFields(data) {
|
||||||
|
const transformedData = {};
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
const valueMap: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const entry of data[key]) {
|
||||||
|
const nestedDataKey = Object.keys(entry.data)[0];
|
||||||
|
const nestedDataValue = entry.data[nestedDataKey];
|
||||||
|
|
||||||
|
if (Array.isArray(nestedDataValue)) {
|
||||||
|
for (const value of nestedDataValue) {
|
||||||
|
if (!valueMap[value]) {
|
||||||
|
valueMap[value] = 0;
|
||||||
|
}
|
||||||
|
valueMap[value] += entry.count;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!valueMap[nestedDataValue]) {
|
||||||
|
valueMap[nestedDataValue] = 0;
|
||||||
|
}
|
||||||
|
valueMap[nestedDataValue] += entry.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transformedData[key] = Object.keys(valueMap).map((value) => ({
|
||||||
|
count: valueMap[value],
|
||||||
|
data: {
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleAggretionData({ dataMap, item }) {
|
||||||
|
const type = dataMap[item.field].type;
|
||||||
|
const aggregationMap = item.data.aggregation.reduce((pre, cur) => {
|
||||||
|
pre[cur.id] = cur;
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
if (['radio', 'checkbox', 'vote', 'binary-choice'].includes(type)) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
title: dataMap[item.field].title,
|
||||||
|
type: dataMap[item.field].type,
|
||||||
|
data: {
|
||||||
|
...item.data,
|
||||||
|
aggregation: dataMap[item.field].options.map((optionItem) => {
|
||||||
|
return {
|
||||||
|
id: optionItem.hash,
|
||||||
|
text: optionItem.text,
|
||||||
|
count: aggregationMap[optionItem.hash]?.count || 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (['radio-star', 'radio-nps'].includes(type)) {
|
||||||
|
const summary: Record<string, any> = {};
|
||||||
|
const average = getAverage({ aggregation: item.data.aggregation });
|
||||||
|
const median = getMedian({ aggregation: item.data.aggregation });
|
||||||
|
const variance = getVariance({
|
||||||
|
aggregation: item.data.aggregation,
|
||||||
|
average,
|
||||||
|
});
|
||||||
|
summary['average'] = average;
|
||||||
|
summary['median'] = median;
|
||||||
|
summary['variance'] = variance;
|
||||||
|
if (type === 'radio-nps') {
|
||||||
|
summary['nps'] = getNps({ aggregation: item.data.aggregation });
|
||||||
|
}
|
||||||
|
const range = type === 'radio-nps' ? [0, 10] : [1, 5];
|
||||||
|
const arr = [];
|
||||||
|
for (let i = range[0]; i <= range[1]; i++) {
|
||||||
|
arr.push(i);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
title: dataMap[item.field].title,
|
||||||
|
type: dataMap[item.field].type,
|
||||||
|
data: {
|
||||||
|
aggregation: arr.map((item) => {
|
||||||
|
const num = item.toString();
|
||||||
|
return {
|
||||||
|
text: num,
|
||||||
|
id: num,
|
||||||
|
count: aggregationMap?.[num]?.count || 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
submitionCount: item.data.submitionCount,
|
||||||
|
summary,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
title: dataMap[item.field].title,
|
||||||
|
type: dataMap[item.field].type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAverage({ aggregation }) {
|
||||||
|
const { sum, count } = aggregation.reduce(
|
||||||
|
(pre, cur) => {
|
||||||
|
const num = parseInt(cur.id);
|
||||||
|
pre.sum += num * cur.count;
|
||||||
|
pre.count += cur.count;
|
||||||
|
return pre;
|
||||||
|
},
|
||||||
|
{ sum: 0, count: 0 },
|
||||||
|
);
|
||||||
|
if (count === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (sum / count).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMedian({ aggregation }) {
|
||||||
|
const sortedArr = aggregation.sort((a, b) => {
|
||||||
|
return parseInt(a.id) - parseInt(b.id);
|
||||||
|
});
|
||||||
|
const resArr = [];
|
||||||
|
for (const item of sortedArr) {
|
||||||
|
const tmp = new Array(item.count).fill(parseInt(item.id));
|
||||||
|
resArr.push(...tmp);
|
||||||
|
}
|
||||||
|
if (resArr.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (resArr.length % 2 === 1) {
|
||||||
|
const midIndex = Math.floor(resArr.length / 2);
|
||||||
|
return resArr[midIndex].toFixed(2);
|
||||||
|
}
|
||||||
|
const rightIndex = resArr.length / 2;
|
||||||
|
const leftIndex = rightIndex - 1;
|
||||||
|
return ((resArr[leftIndex] + resArr[rightIndex]) / 2).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariance({ aggregation, average }) {
|
||||||
|
const { sum, count } = aggregation.reduce(
|
||||||
|
(pre, cur) => {
|
||||||
|
const sub = Number(cur.id) - average;
|
||||||
|
pre.sum += sub * sub;
|
||||||
|
pre.count += cur.count;
|
||||||
|
return pre;
|
||||||
|
},
|
||||||
|
{ sum: 0, count: 0 },
|
||||||
|
);
|
||||||
|
if (count === 0 || count === 1) {
|
||||||
|
return '0.00';
|
||||||
|
}
|
||||||
|
return (sum / (count - 1)).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNps({ aggregation }) {
|
||||||
|
// 净推荐值(NPS)=(推荐者数/总样本数)×100%-(贬损者数/总样本数)×100%
|
||||||
|
// 0~10分举例子:推荐者(9-10分);被动者(7-8分);贬损者(0-6分)
|
||||||
|
let recommand = 0,
|
||||||
|
derogatory = 0,
|
||||||
|
total = 0;
|
||||||
|
for (const item of aggregation) {
|
||||||
|
const num = parseInt(item.id);
|
||||||
|
if (num >= 9) {
|
||||||
|
recommand += item.count;
|
||||||
|
} else if (num <= 6) {
|
||||||
|
derogatory += item.count;
|
||||||
|
}
|
||||||
|
total += item.count;
|
||||||
|
}
|
||||||
|
return ((recommand / total - derogatory / total) * 100).toFixed(2) + '%';
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user