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 { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
|
||||
describe('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 surveyMeta = { owner: 'testUser', workspaceId: null };
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
|
||||
@ -108,7 +121,35 @@ describe('SurveyGuard', () => {
|
||||
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 surveyMeta = { owner: 'anotherUser', workspaceId: null };
|
||||
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 {
|
||||
return {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
|
@ -6,6 +6,7 @@ import { User } from 'src/models/user.entity';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { hash256 } from 'src/utils/hash256';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
@ -21,6 +22,7 @@ describe('UserService', () => {
|
||||
create: jest.fn(),
|
||||
save: 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 () => {
|
||||
const userInfo = {
|
||||
username: 'testUser',
|
||||
@ -102,7 +108,7 @@ describe('UserService', () => {
|
||||
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 = {
|
||||
username: 'nonExistingUser',
|
||||
password: 'nonExistingPassword',
|
||||
@ -129,7 +135,8 @@ describe('UserService', () => {
|
||||
const userInfo = {
|
||||
username: username,
|
||||
password: 'existingPassword',
|
||||
} as User;
|
||||
curStatus: { status: 'ACTIVE' },
|
||||
} as unknown as User;
|
||||
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(userInfo);
|
||||
|
||||
@ -137,10 +144,129 @@ describe('UserService', () => {
|
||||
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
username: username,
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
});
|
||||
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 { SurveyMetaService } from '../services/surveyMeta.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/survey.guard');
|
||||
@ -21,6 +27,8 @@ describe('CollaboratorController', () => {
|
||||
let collaboratorService: CollaboratorService;
|
||||
let logger: Logger;
|
||||
let userService: UserService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let workspaceMemberServie: WorkspaceMemberService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -34,12 +42,18 @@ describe('CollaboratorController', () => {
|
||||
changeUserPermission: jest.fn(),
|
||||
deleteCollaborator: jest.fn(),
|
||||
getCollaborator: jest.fn(),
|
||||
batchDeleteBySurveyId: jest.fn(),
|
||||
batchCreate: jest.fn(),
|
||||
batchDelete: jest.fn(),
|
||||
updateById: jest.fn(),
|
||||
batchSaveCollaborator: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -72,6 +86,10 @@ describe('CollaboratorController', () => {
|
||||
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
||||
logger = module.get<Logger>(Logger);
|
||||
userService = module.get<UserService>(UserService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
workspaceMemberServie = module.get<WorkspaceMemberService>(
|
||||
WorkspaceMemberService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@ -115,6 +133,59 @@ describe('CollaboratorController', () => {
|
||||
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', () => {
|
||||
@ -217,4 +288,229 @@ describe('CollaboratorController', () => {
|
||||
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 { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
|
||||
jest.mock('../services/dataStatistic.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/survey.guard');
|
||||
jest.mock('src/guards/workspace.guard');
|
||||
|
||||
describe('DataStatisticController', () => {
|
||||
let controller: DataStatisticController;
|
||||
let dataStatisticService: DataStatisticService;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let pluginManager: XiaojuSurveyPluginManager;
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -64,9 +67,14 @@ describe('DataStatisticController', () => {
|
||||
controller = module.get<DataStatisticController>(DataStatisticController);
|
||||
dataStatisticService =
|
||||
module.get<DataStatisticService>(DataStatisticService);
|
||||
const pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
logger = module.get<Logger>(Logger);
|
||||
|
||||
pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
@ -82,6 +90,9 @@ describe('DataStatisticController', () => {
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId,
|
||||
isDesensitive: false,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
user: {
|
||||
username: 'testUser',
|
||||
@ -105,13 +116,13 @@ describe('DataStatisticController', () => {
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||
.mockResolvedValueOnce({} as any);
|
||||
jest
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, {});
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
@ -125,6 +136,8 @@ describe('DataStatisticController', () => {
|
||||
query: {
|
||||
surveyId,
|
||||
isDesensitive: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
user: {
|
||||
username: 'testUser',
|
||||
@ -146,19 +159,499 @@ describe('DataStatisticController', () => {
|
||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
||||
],
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||
.mockResolvedValueOnce({} as any);
|
||||
jest
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, {});
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
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: 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,
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 });
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
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';
|
||||
|
||||
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<number>;
|
||||
permissions: Array<string>;
|
||||
}
|
||||
|
||||
export class BatchSaveCollaboratorDto {
|
||||
|
@ -7,7 +7,7 @@ import moment from 'moment';
|
||||
import { keyBy } from 'lodash';
|
||||
import { DataItem } from 'src/interfaces/survey';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { getListHeadByDataList } from '../utils';
|
||||
import { getListHeadByDataList, transformAndMergeArrayFields } from '../utils';
|
||||
@Injectable()
|
||||
export class DataStatisticService {
|
||||
private radioType = ['radio-star', 'radio-nps'];
|
||||
@ -101,4 +101,62 @@ export class DataStatisticService {
|
||||
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;
|
||||
}
|
||||
|
||||
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