Compare commits
102 Commits
main
...
feature/pe
Author | SHA1 | Date | |
---|---|---|---|
|
cdb724d705 | ||
|
944d23b4d6 | ||
|
238f2a8613 | ||
|
b484b786ea | ||
|
6cbfe20be1 | ||
|
43001a12c7 | ||
|
d3c2180ac8 | ||
|
63e16e1694 | ||
|
c6cc0d22e5 | ||
|
949a989dcf | ||
|
9427e0efe5 | ||
|
70c236c879 | ||
|
3d31245ae5 | ||
|
98fc21995a | ||
|
d08f1c71e5 | ||
|
2162f3cffd | ||
|
20ef020a19 | ||
|
f6e3778a2d | ||
|
6775a9df5e | ||
|
3cb843e493 | ||
|
bc3ce31c9e | ||
|
3e7f0cac90 | ||
|
f3b8ab278a | ||
|
013f9ac811 | ||
|
9e07e8330a | ||
|
8950073141 | ||
|
4d580bb789 | ||
|
f73bfb0ab3 | ||
|
8109d350e4 | ||
|
b233023bb3 | ||
|
fd7cc2ea96 | ||
|
724535a735 | ||
|
b5c7ec3008 | ||
|
c5698ad631 | ||
|
6fb337633c | ||
|
42b8d74ead | ||
|
275940ec88 | ||
|
a0237c8c5f | ||
|
d8e76dc2e6 | ||
|
4f2cd4ca47 | ||
|
82c98ec1f5 | ||
|
fbc654f21b | ||
|
9d89a1ceca | ||
|
517906f77f | ||
|
681e8fa3ae | ||
|
4d5c3eb15d | ||
|
9a35de7e36 | ||
|
c4b730c8af | ||
|
8ea8869ca7 | ||
|
7e5a8ae5c1 | ||
|
cf495b60d1 | ||
|
fb57eaaba7 | ||
|
b494bd6174 | ||
|
310fe0d325 | ||
|
ba418c5cd7 | ||
|
9596cd07a1 | ||
|
2ed5b64b18 | ||
|
492e0055f0 | ||
|
93938702fe | ||
|
0db3b22ecf | ||
|
05414b1386 | ||
|
bb331af9eb | ||
|
e8907ca4fb | ||
|
2b32850046 | ||
|
da1749fb53 | ||
|
5bc5eb8719 | ||
|
3227a799f9 | ||
|
8740685a4d | ||
|
5c3915a74d | ||
|
b1958ec8ff | ||
|
bc39e9933d | ||
|
36dd5a4f2d | ||
|
a101878313 | ||
|
32e43b8260 | ||
|
9afb23c08e | ||
|
eaa1abda82 | ||
|
6431cc3210 | ||
|
122f584cad | ||
|
f45cf7982f | ||
|
2f0736fd95 | ||
|
212a3329ad | ||
|
6c72344204 | ||
|
3003c2cbfa | ||
|
b5bcb7ff7e | ||
|
400aee9fda | ||
|
9047e6a344 | ||
|
ef5d775276 | ||
|
4912cfd4b7 | ||
|
1849c73328 | ||
|
6d0a772686 | ||
|
c93b29f397 | ||
|
dc7275aeee | ||
|
4eda73e669 | ||
|
6ae9886cac | ||
|
304c813b17 | ||
|
f49a5931dd | ||
|
d98f605f01 | ||
|
aaedaef3bc | ||
|
3fd51010e4 | ||
|
fd2ad752a0 | ||
|
61fd6e09af | ||
|
349b4dad8c |
3
.github/workflows/server-lint.yml
vendored
3
.github/workflows/server-lint.yml
vendored
@ -37,6 +37,3 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: cd server && npm run lint
|
||||
|
||||
- name: Format
|
||||
run: cd server && npm run format
|
||||
|
3
.github/workflows/web-lint.yml
vendored
3
.github/workflows/web-lint.yml
vendored
@ -40,6 +40,3 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: cd web && npm run lint
|
||||
|
||||
- name: Format
|
||||
run: cd web && npm run format
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -25,3 +25,10 @@ pnpm-debug.log*
|
||||
*.sw?
|
||||
|
||||
.history
|
||||
|
||||
components.d.ts
|
||||
|
||||
# 默认的上传文件夹
|
||||
userUpload
|
||||
exportfile
|
||||
yarn.lock
|
24
README.md
24
README.md
@ -29,12 +29,10 @@
|
||||
|
||||
<br />
|
||||
|
||||
  **XIAOJUSURVEY**是一套轻量、安全的**问卷系统基座**,提供面向个人和企业的一站式产品级解决方案,快速满足各类线上调研场景。
|
||||
  **XIAOJUSURVEY**是一套轻量、安全的调研系统,提供面向个人和企业的一站式产品级解决方案,用于构建各类问卷、考试、测评和复杂表单,快速满足各类线上调研场景。
|
||||
|
||||
  内部系统已沉淀 40+种题型,累积精选模板 100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。
|
||||
|
||||
  开源项目以打造**调研基座**为核心,围绕**平台能力**、**工程架构**、**研发体系**进行建设,大家可以「快速」打造「专属」问卷系统:[快速了解生态发展理念](https://xiaojusurvey.didi.cn/docs/next/community/%E7%94%9F%E6%80%81%E5%BB%BA%E8%AE%BE)
|
||||
|
||||
# 功能简介
|
||||
|
||||
- 问卷管理:创、编、投、收、数据分析
|
||||
@ -45,7 +43,7 @@
|
||||
|
||||
- 数据安全:传输加密、脱敏等
|
||||
|
||||
> 更全的建设请查阅 [官方 Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
> 更全的建设请查阅 [功能介绍](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D/%E5%9F%BA%E7%A1%80%E6%B5%81%E7%A8%8B)
|
||||
|
||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" />
|
||||
|
||||
@ -144,13 +142,11 @@ npm run local
|
||||
|
||||
### 方案二、(生产推荐)
|
||||
|
||||
#### 1、启动数据库
|
||||
#### 1、配置数据库
|
||||
|
||||
> 项目使用 MongoDB:[MongoDB 安装指导](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85)
|
||||
> 项目使用 MongoDB,需要提前准备,请查看[如何拥有 MongoDB 指南](./数据库#安装)
|
||||
|
||||
- 配置数据库,查看[MongoDB 配置](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93)
|
||||
|
||||
- 启动本地数据库,查看[MongoDB 启动](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E4%BA%94%E5%90%AF%E5%8A%A8)
|
||||
配置数据库信息,查看[MongoDB 配置](./数据库)。
|
||||
|
||||
#### 2、安装依赖
|
||||
|
||||
@ -220,16 +216,10 @@ npm run serve
|
||||
|
||||
如果你想成为贡献者或者扩展技术栈,请查看:[贡献者指南](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE),你的加入使我们最大的荣幸。
|
||||
|
||||
## Feature
|
||||
## Future Tasks
|
||||
|
||||
关注每周推出的建设:[官方 Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
[欢迎共建](https://github.com/didi/xiaoju-survey/issues/85)
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
关注重大变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
||||
|
||||
## 文章分享
|
||||
|
||||
1、[掘金](https://juejin.cn/user/3705833332160473/posts)、2、[InfoQ](https://www.infoq.cn/profile/7E08AC616A07B2/publish)
|
||||
|
||||
[欢迎投稿](https://xiaojusurvey.didi.cn/docs/next/article/%E7%AE%80%E4%BB%8B)
|
||||
|
24
README_EN.md
24
README_EN.md
@ -29,12 +29,10 @@
|
||||
|
||||
<br />
|
||||
|
||||
  XIAOJUSURVEY is a lightweight, secure questionnaire system foundation that provides one-stop product-level solutions for individuals and enterprises, quickly meeting various online survey scenarios.
|
||||
  XIAOJUSURVEY is an enterprises form builder and analytics platform to create questionnaires, exams, polls, quizzes, and analyze data online.
|
||||
|
||||
  The internal system has accumulated over 40 question types and more than 100 selected templates, suitable for market research, customer satisfaction surveys, online exams, voting, reporting, evaluations, and many other scenarios. In terms of data capabilities, it has been honed through hundreds of millions of iterations, resulting in the ability to provide online reports with per-question statistics, cross-analysis, and multi-channel analysis, quickly meeting professional analysis needs.
|
||||
|
||||
  The open-source project focuses on building a survey foundation, constructing around platform capabilities, engineering structure, and development systems, allowing everyone to 「quickly」 create their own 「exclusive」 questionnaire system: [quickly understanding the ecological development philosophy](https://xiaojusurvey.didi.cn/docs/next/community/%E7%94%9F%E6%80%81%E5%BB%BA%E8%AE%BE).
|
||||
|
||||
# Function Overview
|
||||
|
||||
- Questionnaire Management: Create, edit, distribute, collect, data analysis.
|
||||
@ -45,7 +43,7 @@
|
||||
|
||||
- Data Security: Encrypted transmission, data masking, etc.
|
||||
|
||||
> For more comprehensive features, please refer to the official Feature documentation.
|
||||
> For more comprehensive features, please refer to the [documentation](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D/%E5%9F%BA%E7%A1%80%E6%B5%81%E7%A8%8B).
|
||||
|
||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/508ce30f-0ae8-4f5f-84a7-e96de8238a7f" width="700" />
|
||||
|
||||
@ -145,12 +143,11 @@ npm run local
|
||||
|
||||
### Option 2: (Recommended for Production)
|
||||
|
||||
#### 1.Start Database
|
||||
#### 1.Configure Database
|
||||
|
||||
> The project uses MongoDB: [MongoDB Installation Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85)
|
||||
> The project uses MongoDB: [MongoDB Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85)
|
||||
|
||||
- Configure the database, check MongoDB configuration.
|
||||
- Start local database, check MongoDB startup.
|
||||
Configure the database, check MongoDB configuration.
|
||||
|
||||
#### 2.Install Dependencies
|
||||
|
||||
@ -218,16 +215,11 @@ If you use this project, please leave feedback:[I'm using](https://github.com/di
|
||||
|
||||
If you want to become a contributor or expand your technical stack, please check: [Contributor Guide](https://xiaojusurvey.didi.cn/docs/next/share/%E5%A6%82%E4%BD%95%E5%8F%82%E4%B8%8E%E8%B4%A1%E7%8C%AE). Your participation is our greatest honor.
|
||||
|
||||
## Feature
|
||||
## Future Tasks
|
||||
|
||||
Pay attention to weekly construction updates: [Official Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
1. [Official Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
2. [WIP](https://github.com/didi/xiaoju-survey/labels/WIP)
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
Follow major changes: [MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)
|
||||
|
||||
## Article Sharing
|
||||
|
||||
1、[JueJin](https://juejin.cn/user/3705833332160473/posts)、2、[InfoQ](https://www.infoq.cn/profile/7E08AC616A07B2/publish)
|
||||
|
||||
[Welcome to contribute.](https://xiaojusurvey.didi.cn/docs/next/article/%E7%AE%80%E4%BB%8B)
|
||||
|
@ -15,7 +15,7 @@ services:
|
||||
- xiaoju-survey
|
||||
|
||||
xiaoju-survey:
|
||||
image: "xiaojusurvey/xiaoju-survey:1.1.2-slim"
|
||||
image: "xiaojusurvey/xiaoju-survey:1.1.6-slim" # 最新版本:https://hub.docker.com/r/xiaojusurvey/xiaoju-survey/tags
|
||||
container_name: xiaoju-survey
|
||||
restart: always
|
||||
ports:
|
||||
|
@ -51,6 +51,15 @@ http {
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
location /exportfile {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
# 静态文件的默认存储文件夹
|
||||
# 文件夹的配置在 server/src/modules/file/config/index.ts SERVER_LOCAL_CONFIG.FILE_KEY_PREFIX
|
||||
location /userUpload {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /500.html;
|
||||
client_max_body_size 20M;
|
||||
|
12
server/.env
12
server/.env
@ -1,9 +1,15 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
|
||||
XIAOJU_SURVEY_MONGO_URL= # mongodb://127.0.0.1:27017 # 建议设置强密码
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE= # admin
|
||||
|
||||
XIAOJU_SURVEY_REDIS_HOST=
|
||||
XIAOJU_SURVEY_REDIS_PORT=
|
||||
XIAOJU_SURVEY_REDIS_USERNAME=
|
||||
XIAOJU_SURVEY_REDIS_PASSWORD=
|
||||
XIAOJU_SURVEY_REDIS_DB=
|
||||
|
||||
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY= # dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
|
||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||
|
5
server/.gitignore
vendored
5
server/.gitignore
vendored
@ -13,6 +13,7 @@ pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
yarn.lock
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@ -37,4 +38,6 @@ lerna-debug.log*
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
tmp
|
||||
tmp
|
||||
exportfile
|
||||
userUpload
|
@ -27,10 +27,11 @@
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/typeorm": "^10.0.1",
|
||||
"ali-oss": "^6.20.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dotenv": "^16.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
@ -41,11 +42,14 @@
|
||||
"nanoid": "^3.3.7",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-xlsx": "^0.24.0",
|
||||
"qiniu": "^7.11.1",
|
||||
"redlock": "^5.0.0-beta.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"typeorm": "^0.3.19"
|
||||
"typeorm": "^0.3.19",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
@ -61,8 +65,8 @@
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
||||
"@typescript-eslint/parser": "^7.16.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
@ -70,13 +74,14 @@
|
||||
"jest": "^29.5.0",
|
||||
"mongodb-memory-server": "^9.1.4",
|
||||
"prettier": "^3.0.0",
|
||||
"redis-memory-server": "^0.11.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { MongoMemoryServer } from 'mongodb-memory-server';
|
||||
import { spawn } from 'child_process';
|
||||
import { RedisMemoryServer } from 'redis-memory-server';
|
||||
|
||||
async function startServerAndRunScript() {
|
||||
// 启动 MongoDB 内存服务器
|
||||
@ -8,12 +9,19 @@ async function startServerAndRunScript() {
|
||||
|
||||
console.log('MongoDB Memory Server started:', mongoUri);
|
||||
|
||||
const redisServer = new RedisMemoryServer();
|
||||
const redisHost = await redisServer.getHost();
|
||||
const redisPort = await redisServer.getPort();
|
||||
|
||||
// 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量
|
||||
const tsnode = spawn(
|
||||
'cross-env',
|
||||
[
|
||||
`XIAOJU_SURVEY_MONGO_URL=${mongoUri}`,
|
||||
`XIAOJU_SURVEY_REDIS_HOST=${redisHost}`,
|
||||
`XIAOJU_SURVEY_REDIS_PORT=${redisPort}`,
|
||||
'NODE_ENV=development',
|
||||
'SERVER_ENV=local',
|
||||
'npm',
|
||||
'run',
|
||||
'start:dev',
|
||||
@ -31,9 +39,10 @@ async function startServerAndRunScript() {
|
||||
console.error(data);
|
||||
});
|
||||
|
||||
tsnode.on('close', (code) => {
|
||||
tsnode.on('close', async (code) => {
|
||||
console.log(`Nodemon process exited with code ${code}`);
|
||||
mongod.stop(); // 停止 MongoDB 内存服务器
|
||||
await mongod.stop(); // 停止 MongoDB 内存服务器
|
||||
await redisServer.stop();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,9 @@ import { LoggerProvider } from './logger/logger.provider';
|
||||
import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
|
||||
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
||||
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
||||
import { Logger } from './logger';
|
||||
import { XiaojuSurveyLogger } from './logger';
|
||||
import { DownloadTask } from './models/downloadTask.entity';
|
||||
import { Session } from './models/session.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -81,6 +83,8 @@ import { Logger } from './logger';
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
Collaborator,
|
||||
DownloadTask,
|
||||
Session,
|
||||
],
|
||||
};
|
||||
},
|
||||
@ -128,7 +132,7 @@ export class AppModule {
|
||||
),
|
||||
new SurveyUtilPlugin(),
|
||||
);
|
||||
Logger.init({
|
||||
XiaojuSurveyLogger.init({
|
||||
filename: this.configService.get<string>('XIAOJU_SURVEY_LOGGER_FILENAME'),
|
||||
});
|
||||
}
|
||||
|
21
server/src/config/index.ts
Normal file
21
server/src/config/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
const mongo = {
|
||||
url: process.env.XIAOJU_SURVEY_MONGO_URL || 'mongodb://localhost:27017',
|
||||
dbName: process.env.XIAOJU_SURVER_MONGO_DBNAME || 'xiaojuSurvey',
|
||||
};
|
||||
|
||||
const session = {
|
||||
expireTime:
|
||||
parseInt(process.env.XIAOJU_SURVEY_JWT_EXPIRES_IN) || 8 * 3600 * 1000,
|
||||
};
|
||||
|
||||
const encrypt = {
|
||||
type: process.env.XIAOJU_SURVEY_ENCRYPT_TYPE || 'aes',
|
||||
aesCodelength: parseInt(process.env.XIAOJU_SURVEY_ENCRYPT_TYPE_LEN) || 10, //aes密钥长度
|
||||
};
|
||||
|
||||
const jwt = {
|
||||
secret: process.env.XIAOJU_SURVEY_JWT_SECRET || 'xiaojuSurveyJwtSecret',
|
||||
expiresIn: process.env.XIAOJU_SURVEY_JWT_EXPIRES_IN || '8h',
|
||||
};
|
||||
|
||||
export { mongo, session, encrypt, jwt };
|
@ -6,12 +6,15 @@ export enum EXCEPTION_CODE {
|
||||
USER_EXISTS = 2001, // 用户已存在
|
||||
USER_NOT_EXISTS = 2002, // 用户不存在
|
||||
USER_PASSWORD_WRONG = 2003, // 用户名或密码错误
|
||||
PASSWORD_INVALID = 2004, // 密码无效
|
||||
NO_SURVEY_PERMISSION = 3001, // 没有问卷权限
|
||||
SURVEY_STATUS_TRANSFORM_ERROR = 3002, // 问卷状态转换报错
|
||||
SURVEY_TYPE_ERROR = 3003, // 问卷类型错误
|
||||
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
||||
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
||||
SURVEY_SAVE_CONFLICT = 3006, // 问卷冲突
|
||||
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
||||
WHITELIST_ERROR = 4002, // 白名单校验错误
|
||||
|
||||
RESPONSE_SIGN_ERROR = 9001, // 签名不正确
|
||||
RESPONSE_CURRENT_TIME_NOT_ALLOW = 9002, // 当前时间不允许提交
|
||||
|
@ -6,6 +6,9 @@ export enum RECORD_STATUS {
|
||||
PUBLISHED = 'published', // 发布
|
||||
REMOVED = 'removed', // 删除
|
||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
||||
COMOPUTETING = 'computing', // 计算中
|
||||
FINISHED = 'finished', // 已完成
|
||||
ERROR = 'error', // 错误
|
||||
}
|
||||
|
||||
// 历史类型
|
||||
|
37
server/src/enums/question.ts
Normal file
37
server/src/enums/question.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @description 问卷题目类型
|
||||
*/
|
||||
export enum QUESTION_TYPE {
|
||||
/**
|
||||
* 单行输入框
|
||||
*/
|
||||
TEXT = 'text',
|
||||
/**
|
||||
* 多行输入框
|
||||
*/
|
||||
TEXTAREA = 'textarea',
|
||||
/**
|
||||
* 单项选择
|
||||
*/
|
||||
RADIO = 'radio',
|
||||
/**
|
||||
* 多项选择
|
||||
*/
|
||||
CHECKBOX = 'checkbox',
|
||||
/**
|
||||
* 判断题
|
||||
*/
|
||||
BINARY_CHOICE = 'binary-choice',
|
||||
/**
|
||||
* 评分
|
||||
*/
|
||||
RADIO_STAR = 'radio-star',
|
||||
/**
|
||||
* nps评分
|
||||
*/
|
||||
RADIO_NPS = 'radio-nps',
|
||||
/**
|
||||
* 投票
|
||||
*/
|
||||
VOTE = 'vote',
|
||||
}
|
94
server/src/guards/session.guard.ts
Normal file
94
server/src/guards/session.guard.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { get } from 'lodash';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { SessionService } from 'src/modules/survey/services/session.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
|
||||
|
||||
@Injectable()
|
||||
export class SessionGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
private readonly collaboratorService: CollaboratorService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
const sessionIdKey = this.reflector.get<string>(
|
||||
'sessionId',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
const sessionId = get(request, sessionIdKey);
|
||||
|
||||
if (!sessionId) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const saveSession = await this.sessionService.findOne(sessionId);
|
||||
|
||||
request.saveSession = saveSession;
|
||||
|
||||
const surveyId = saveSession.surveyId;
|
||||
|
||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||
|
||||
if (!surveyMeta) {
|
||||
throw new SurveyNotFoundException('问卷不存在');
|
||||
}
|
||||
|
||||
request.surveyMeta = surveyMeta;
|
||||
|
||||
// 兼容老的问卷没有ownerId
|
||||
if (
|
||||
surveyMeta.ownerId === user._id.toString() ||
|
||||
surveyMeta.owner === user.username
|
||||
) {
|
||||
// 问卷的owner,可以访问和操作问卷
|
||||
return true;
|
||||
}
|
||||
|
||||
if (surveyMeta.workspaceId) {
|
||||
const memberInfo = await this.workspaceMemberService.findOne({
|
||||
workspaceId: surveyMeta.workspaceId,
|
||||
userId: user._id.toString(),
|
||||
});
|
||||
if (!memberInfo) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const permissions = this.reflector.get<string[]>(
|
||||
'surveyPermission',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
if (!Array.isArray(permissions) || permissions.length === 0) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const info = await this.collaboratorService.getCollaborator({
|
||||
surveyId,
|
||||
userId: user._id.toString(),
|
||||
});
|
||||
|
||||
if (!info) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
request.collaborator = info;
|
||||
if (
|
||||
permissions.some((permission) => info.permissions.includes(permission))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import { Reflector } from '@nestjs/core';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
|
@ -60,6 +60,7 @@ export interface DataItem {
|
||||
rangeConfig?: any;
|
||||
starStyle?: string;
|
||||
innerType?: string;
|
||||
quotaNoDisplay?: boolean;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@ -69,6 +70,7 @@ export interface Option {
|
||||
othersKey?: string;
|
||||
placeholderDesc: string;
|
||||
hash: string;
|
||||
quota?: number;
|
||||
}
|
||||
|
||||
export interface DataConf {
|
||||
@ -94,6 +96,23 @@ export interface SubmitConf {
|
||||
msgContent: MsgContent;
|
||||
}
|
||||
|
||||
// 白名单类型
|
||||
export enum WhitelistType {
|
||||
ALL = 'ALL',
|
||||
// 空间成员
|
||||
MEMBER = 'MEMBER',
|
||||
// 自定义
|
||||
CUSTOM = 'CUSTOM',
|
||||
}
|
||||
|
||||
// 白名单用户类型
|
||||
export enum MemberType {
|
||||
// 手机号
|
||||
MOBILE = 'MOBILE',
|
||||
// 邮箱
|
||||
EMAIL = 'EMAIL',
|
||||
}
|
||||
|
||||
export interface BaseConf {
|
||||
begTime: string;
|
||||
endTime: string;
|
||||
@ -101,6 +120,18 @@ export interface BaseConf {
|
||||
answerEndTime: string;
|
||||
tLimit: number;
|
||||
language: string;
|
||||
// 访问密码开关
|
||||
passwordSwitch?: boolean;
|
||||
// 密码
|
||||
password?: string | null;
|
||||
// 白名单类型
|
||||
whitelistType?: WhitelistType;
|
||||
// 白名单用户类型
|
||||
memberType?: MemberType;
|
||||
// 白名单列表
|
||||
whitelist?: string[];
|
||||
// 提示语
|
||||
whitelistTip?: string;
|
||||
}
|
||||
|
||||
export interface SkinConf {
|
||||
|
@ -1,15 +1,15 @@
|
||||
import * as log4js from 'log4js';
|
||||
import moment from 'moment';
|
||||
import { Request } from 'express';
|
||||
import { Injectable, Scope } from '@nestjs/common';
|
||||
const log4jsLogger = log4js.getLogger();
|
||||
|
||||
export class Logger {
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class XiaojuSurveyLogger {
|
||||
private static inited = false;
|
||||
|
||||
constructor() {}
|
||||
private traceId: string;
|
||||
|
||||
static init(config: { filename: string }) {
|
||||
if (this.inited) {
|
||||
if (XiaojuSurveyLogger.inited) {
|
||||
return;
|
||||
}
|
||||
log4js.configure({
|
||||
@ -30,25 +30,28 @@ export class Logger {
|
||||
default: { appenders: ['app'], level: 'trace' },
|
||||
},
|
||||
});
|
||||
XiaojuSurveyLogger.inited = true;
|
||||
}
|
||||
|
||||
_log(message, options: { dltag?: string; level: string; req?: Request }) {
|
||||
_log(message, options: { dltag?: string; level: string }) {
|
||||
const datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
const level = options?.level;
|
||||
const dltag = options?.dltag ? `${options.dltag}||` : '';
|
||||
const traceIdStr = options?.req?.['traceId']
|
||||
? `traceid=${options?.req?.['traceId']}||`
|
||||
: '';
|
||||
const traceIdStr = this.traceId ? `traceid=${this.traceId}||` : '';
|
||||
return log4jsLogger[level](
|
||||
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
info(message, options?: { dltag?: string; req?: Request }) {
|
||||
setTraceId(traceId: string) {
|
||||
this.traceId = traceId;
|
||||
}
|
||||
|
||||
info(message, options?: { dltag?: string }) {
|
||||
return this._log(message, { ...options, level: 'info' });
|
||||
}
|
||||
|
||||
error(message, options: { dltag?: string; req?: Request }) {
|
||||
error(message, options?: { dltag?: string }) {
|
||||
return this._log(message, { ...options, level: 'error' });
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
import { Logger } from './index';
|
||||
import { XiaojuSurveyLogger } from './index';
|
||||
|
||||
export const LoggerProvider: Provider = {
|
||||
provide: Logger,
|
||||
useClass: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useClass: XiaojuSurveyLogger,
|
||||
};
|
||||
|
@ -10,9 +10,9 @@ const getCountStr = () => {
|
||||
|
||||
export const genTraceId = ({ ip }) => {
|
||||
// ip转16位 + 当前时间戳(毫秒级)+自增序列(1000开始自增到9000)+ 当前进程id的后5位
|
||||
ip = ip.replace('::ffff:', '');
|
||||
ip = ip.replace('::ffff:', '').replace('::1', '');
|
||||
let ipArr;
|
||||
if (ip.indexOf(':') > 0) {
|
||||
if (ip.indexOf(':') >= 0) {
|
||||
ipArr = ip.split(':').map((segment) => {
|
||||
// 将IPv6每个段转为16位,并补0到长度为4
|
||||
return parseInt(segment, 16).toString(16).padStart(4, '0');
|
||||
|
@ -1,26 +1,25 @@
|
||||
// logger.middleware.ts
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Logger } from '../logger/index'; // 替换为你实际的logger路径
|
||||
import { XiaojuSurveyLogger } from '../logger/index'; // 替换为你实际的logger路径
|
||||
import { genTraceId } from '../logger/util';
|
||||
|
||||
@Injectable()
|
||||
export class LogRequestMiddleware implements NestMiddleware {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
constructor(private readonly logger: XiaojuSurveyLogger) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const { method, originalUrl, ip } = req;
|
||||
const userAgent = req.get('user-agent') || '';
|
||||
const startTime = Date.now();
|
||||
const traceId = genTraceId({ ip });
|
||||
req['traceId'] = traceId;
|
||||
this.logger.setTraceId(traceId);
|
||||
const query = JSON.stringify(req.query);
|
||||
const body = JSON.stringify(req.body);
|
||||
this.logger.info(
|
||||
`method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`,
|
||||
{
|
||||
dltag: 'request_in',
|
||||
req,
|
||||
},
|
||||
);
|
||||
|
||||
@ -30,7 +29,6 @@ export class LogRequestMiddleware implements NestMiddleware {
|
||||
`status=${res.statusCode.toString()}||duration=${duration}ms`,
|
||||
{
|
||||
dltag: 'request_out',
|
||||
req,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -5,8 +5,7 @@ import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'captcha' })
|
||||
export class Captcha extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
@ -6,8 +6,7 @@ import { BaseEntity } from './base.entity';
|
||||
@Entity({ name: 'clientEncrypt' })
|
||||
export class ClientEncrypt extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
34
server/src/models/downloadTask.entity.ts
Normal file
34
server/src/models/downloadTask.entity.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'downloadTask' })
|
||||
export class DownloadTask extends BaseEntity {
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
// 文件路径
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
// 文件key
|
||||
@Column()
|
||||
fileKey: string;
|
||||
|
||||
// 任务创建人
|
||||
@Column()
|
||||
ownerId: string;
|
||||
|
||||
// 文件名
|
||||
@Column()
|
||||
filename: string;
|
||||
|
||||
// 文件大小
|
||||
@Column()
|
||||
fileSize: string;
|
||||
|
||||
@Column()
|
||||
params: string;
|
||||
}
|
18
server/src/models/session.entity.ts
Normal file
18
server/src/models/session.entity.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Entity, Column, Index, ObjectIdColumn } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'session' })
|
||||
export class Session extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
userId: string;
|
||||
}
|
@ -19,4 +19,7 @@ export class SurveyHistory extends BaseEntity {
|
||||
username: string;
|
||||
_id: string;
|
||||
};
|
||||
|
||||
@Column('string')
|
||||
sessionId: string;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export class SurveyResponse extends BaseEntity {
|
||||
data: Record<string, any>;
|
||||
|
||||
@Column()
|
||||
difTime: number;
|
||||
diffTime: number;
|
||||
|
||||
@Column()
|
||||
clientTime: number;
|
||||
|
@ -82,6 +82,22 @@ describe('AuthController', () => {
|
||||
new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException with PASSWORD_INVALID code when password is invalid', async () => {
|
||||
const mockUserInfo = {
|
||||
username: 'testUser',
|
||||
password: '无效的密码abc123',
|
||||
captchaId: 'testCaptchaId',
|
||||
captcha: 'testCaptcha',
|
||||
};
|
||||
|
||||
await expect(controller.register(mockUserInfo)).rejects.toThrow(
|
||||
new HttpException(
|
||||
'密码只能输入数字、字母、特殊字符',
|
||||
EXCEPTION_CODE.PASSWORD_INVALID,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
@ -204,4 +220,29 @@ describe('AuthController', () => {
|
||||
expect(typeof result.data.img).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('password strength', () => {
|
||||
it('it should return strong', async () => {
|
||||
await expect(
|
||||
controller.getPasswordStrength('abcd&1234'),
|
||||
).resolves.toEqual({
|
||||
code: 200,
|
||||
data: 'Strong',
|
||||
});
|
||||
});
|
||||
|
||||
it('it should return medium', async () => {
|
||||
await expect(controller.getPasswordStrength('abc123')).resolves.toEqual({
|
||||
code: 200,
|
||||
data: 'Medium',
|
||||
});
|
||||
});
|
||||
|
||||
it('it should return weak', async () => {
|
||||
await expect(controller.getPasswordStrength('123456')).resolves.toEqual({
|
||||
code: 200,
|
||||
data: 'Weak',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import { Controller, Post, Body, HttpCode, Get, Query } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { CaptchaService } from '../services/captcha.service';
|
||||
@ -7,6 +7,9 @@ import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { create } from 'svg-captcha';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
const passwordReg = /^[a-zA-Z0-9!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]+$/;
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('/api/auth')
|
||||
export class AuthController {
|
||||
@ -28,6 +31,24 @@ export class AuthController {
|
||||
captcha: string;
|
||||
},
|
||||
) {
|
||||
if (!userInfo.password) {
|
||||
throw new HttpException('密码无效', EXCEPTION_CODE.PASSWORD_INVALID);
|
||||
}
|
||||
|
||||
if (userInfo.password.length < 6 || userInfo.password.length > 16) {
|
||||
throw new HttpException(
|
||||
'密码长度在 6 到 16 个字符',
|
||||
EXCEPTION_CODE.PASSWORD_INVALID,
|
||||
);
|
||||
}
|
||||
|
||||
if (!passwordReg.test(userInfo.password)) {
|
||||
throw new HttpException(
|
||||
'密码只能输入数字、字母、特殊字符',
|
||||
EXCEPTION_CODE.PASSWORD_INVALID,
|
||||
);
|
||||
}
|
||||
|
||||
const isCorrect = await this.captchaService.checkCaptchaIsCorrect({
|
||||
captcha: userInfo.captcha,
|
||||
id: userInfo.captchaId,
|
||||
@ -162,4 +183,35 @@ export class AuthController {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码强度
|
||||
*/
|
||||
@Get('register/password/strength')
|
||||
@HttpCode(200)
|
||||
async getPasswordStrength(@Query('password') password: string) {
|
||||
const numberReg = /[0-9]/.test(password);
|
||||
const letterReg = /[a-zA-Z]/.test(password);
|
||||
const symbolReg = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
|
||||
// 包含三种、且长度大于8
|
||||
if (numberReg && letterReg && symbolReg && password.length >= 8) {
|
||||
return {
|
||||
code: 200,
|
||||
data: 'Strong',
|
||||
};
|
||||
}
|
||||
|
||||
// 满足任意两种
|
||||
if ([numberReg, letterReg, symbolReg].filter(Boolean).length >= 2) {
|
||||
return {
|
||||
code: 200,
|
||||
data: 'Medium',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: 'Weak',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { Controller, Get, Query, HttpCode, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
@ -43,4 +50,16 @@ export class UserController {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authentication)
|
||||
@Get('/getUserInfo')
|
||||
async getUserInfo(@Request() req) {
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
userId: req.user._id.toString(),
|
||||
username: req.user.username,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,13 @@ export class AuthService {
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async expiredCheck(token: string) {
|
||||
try {
|
||||
verify(token, this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'));
|
||||
} catch (err) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -14,13 +14,18 @@ export class FileService {
|
||||
configKey,
|
||||
file,
|
||||
pathPrefix,
|
||||
keepOriginFilename,
|
||||
}: {
|
||||
configKey: string;
|
||||
file: Express.Multer.File;
|
||||
pathPrefix: string;
|
||||
keepOriginFilename?: boolean;
|
||||
}) {
|
||||
const handler = this.getHandler(configKey);
|
||||
const { key } = await handler.upload(file, { pathPrefix });
|
||||
const { key } = await handler.upload(file, {
|
||||
pathPrefix,
|
||||
keepOriginFilename,
|
||||
});
|
||||
const url = await handler.getUrl(key);
|
||||
return {
|
||||
key,
|
||||
|
@ -12,9 +12,14 @@ export class LocalHandler implements FileUploadHandler {
|
||||
|
||||
async upload(
|
||||
file: Express.Multer.File,
|
||||
options?: { pathPrefix?: string },
|
||||
options?: { pathPrefix?: string; keepOriginFilename?: boolean },
|
||||
): Promise<{ key: string }> {
|
||||
const filename = await generateUniqueFilename(file.originalname);
|
||||
let filename;
|
||||
if (options?.keepOriginFilename) {
|
||||
filename = file.originalname;
|
||||
} else {
|
||||
filename = await generateUniqueFilename(file.originalname);
|
||||
}
|
||||
const filePath = join(
|
||||
options?.pathPrefix ? options?.pathPrefix : '',
|
||||
filename,
|
||||
@ -35,6 +40,10 @@ export class LocalHandler implements FileUploadHandler {
|
||||
}
|
||||
|
||||
getUrl(key: string): string {
|
||||
if (process.env.SERVER_ENV === 'local') {
|
||||
const port = process.env.PORT || 3000;
|
||||
return `http://localhost:${port}/${key}`;
|
||||
}
|
||||
return `/${key}`;
|
||||
}
|
||||
}
|
||||
|
9
server/src/modules/redis/redis.module.ts
Normal file
9
server/src/modules/redis/redis.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// src/redis/redis.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
@Module({
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
32
server/src/modules/redis/redis.service.ts
Normal file
32
server/src/modules/redis/redis.service.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Redis } from 'ioredis';
|
||||
import Redlock, { Lock } from 'redlock';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService {
|
||||
private readonly redisClient: Redis;
|
||||
private readonly redlock: Redlock;
|
||||
|
||||
constructor() {
|
||||
this.redisClient = new Redis({
|
||||
host: process.env.XIAOJU_SURVEY_REDIS_HOST,
|
||||
port: parseInt(process.env.XIAOJU_SURVEY_REDIS_PORT),
|
||||
password: process.env.XIAOJU_SURVEY_REDIS_PASSWORD || undefined,
|
||||
username: process.env.XIAOJU_SURVEY_REDIS_USERNAME || undefined,
|
||||
db: parseInt(process.env.XIAOJU_SURVEY_REDIS_DB) || 0,
|
||||
});
|
||||
this.redlock = new Redlock([this.redisClient], {
|
||||
retryCount: 10,
|
||||
retryDelay: 200, // ms
|
||||
retryJitter: 200, // ms
|
||||
});
|
||||
}
|
||||
|
||||
async lockResource(resource: string, ttl: number): Promise<Lock> {
|
||||
return this.redlock.acquire([resource], ttl);
|
||||
}
|
||||
|
||||
async unlockResource(lock: Lock): Promise<void> {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CollaboratorController } from '../controllers/collaborator.controller';
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { CreateCollaboratorDto } from '../dto/createCollaborator.dto';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
@ -25,7 +25,7 @@ jest.mock('src/guards/workspace.guard');
|
||||
describe('CollaboratorController', () => {
|
||||
let controller: CollaboratorController;
|
||||
let collaboratorService: CollaboratorService;
|
||||
let logger: Logger;
|
||||
let logger: XiaojuSurveyLogger;
|
||||
let userService: UserService;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let workspaceMemberServie: WorkspaceMemberService;
|
||||
@ -50,7 +50,7 @@ describe('CollaboratorController', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
@ -84,7 +84,7 @@ describe('CollaboratorController', () => {
|
||||
|
||||
controller = module.get<CollaboratorController>(CollaboratorController);
|
||||
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
|
||||
logger = module.get<Logger>(Logger);
|
||||
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||
userService = module.get<UserService>(UserService);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
workspaceMemberServie = module.get<WorkspaceMemberService>(
|
||||
@ -191,7 +191,6 @@ describe('CollaboratorController', () => {
|
||||
describe('getSurveyCollaboratorList', () => {
|
||||
it('should return collaborator list', async () => {
|
||||
const query = { surveyId: 'surveyId' };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const result = [
|
||||
{ _id: 'collaboratorId', userId: 'userId', username: '' },
|
||||
];
|
||||
@ -202,7 +201,7 @@ describe('CollaboratorController', () => {
|
||||
|
||||
jest.spyOn(userService, 'getUserListByIds').mockResolvedValueOnce([]);
|
||||
|
||||
const response = await controller.getSurveyCollaboratorList(query, req);
|
||||
const response = await controller.getSurveyCollaboratorList(query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
@ -214,11 +213,10 @@ describe('CollaboratorController', () => {
|
||||
const query: GetSurveyCollaboratorListDto = {
|
||||
surveyId: '',
|
||||
};
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
await expect(
|
||||
controller.getSurveyCollaboratorList(query, req),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(controller.getSurveyCollaboratorList(query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -230,14 +228,13 @@ describe('CollaboratorController', () => {
|
||||
userId: 'userId',
|
||||
permissions: ['read'],
|
||||
};
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const result = { _id: 'userId', permissions: ['read'] };
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'changeUserPermission')
|
||||
.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.changeUserPermission(reqBody, req);
|
||||
const response = await controller.changeUserPermission(reqBody);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
@ -251,11 +248,10 @@ describe('CollaboratorController', () => {
|
||||
userId: '',
|
||||
permissions: ['surveyManage'],
|
||||
};
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
await expect(
|
||||
controller.changeUserPermission(reqBody, req),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(controller.changeUserPermission(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -263,14 +259,13 @@ describe('CollaboratorController', () => {
|
||||
describe('deleteCollaborator', () => {
|
||||
it('should delete collaborator successfully', async () => {
|
||||
const query = { surveyId: 'surveyId', userId: 'userId' };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const result = { acknowledged: true, deletedCount: 1 };
|
||||
|
||||
jest
|
||||
.spyOn(collaboratorService, 'deleteCollaborator')
|
||||
.mockResolvedValue(result);
|
||||
|
||||
const response = await controller.deleteCollaborator(query, req);
|
||||
const response = await controller.deleteCollaborator(query);
|
||||
|
||||
expect(response).toEqual({
|
||||
code: 200,
|
||||
@ -280,9 +275,8 @@ describe('CollaboratorController', () => {
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const query = { surveyId: '', userId: '' };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
await expect(controller.deleteCollaborator(query, req)).rejects.toThrow(
|
||||
await expect(controller.deleteCollaborator(query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
|
@ -3,13 +3,13 @@ import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { InsertManyResult, ObjectId } from 'mongodb';
|
||||
|
||||
describe('CollaboratorService', () => {
|
||||
let service: CollaboratorService;
|
||||
let repository: MongoRepository<Collaborator>;
|
||||
let logger: Logger;
|
||||
let logger: XiaojuSurveyLogger;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -20,7 +20,7 @@ describe('CollaboratorService', () => {
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
},
|
||||
@ -32,7 +32,7 @@ describe('CollaboratorService', () => {
|
||||
repository = module.get<MongoRepository<Collaborator>>(
|
||||
getRepositoryToken(Collaborator),
|
||||
);
|
||||
logger = module.get<Logger>(Logger);
|
||||
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
|
@ -9,7 +9,7 @@ import { ResponseSchemaService } from '../../surveyResponse/services/responseSch
|
||||
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
@ -28,7 +28,7 @@ describe('DataStatisticController', () => {
|
||||
let dataStatisticService: DataStatisticService;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let pluginManager: XiaojuSurveyPluginManager;
|
||||
let logger: Logger;
|
||||
let logger: XiaojuSurveyLogger;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -56,7 +56,7 @@ describe('DataStatisticController', () => {
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
@ -73,7 +73,7 @@ describe('DataStatisticController', () => {
|
||||
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
logger = module.get<Logger>(Logger);
|
||||
logger = module.get<XiaojuSurveyLogger>(XiaojuSurveyLogger);
|
||||
|
||||
pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
@ -106,12 +106,13 @@ describe('DataStatisticController', () => {
|
||||
field: 'xxx',
|
||||
title: 'xxx',
|
||||
type: 'xxx',
|
||||
diffTime: 'xxx',
|
||||
othersCode: 'xxx',
|
||||
},
|
||||
],
|
||||
listBody: [
|
||||
{ difTime: '0.5', createDate: '2024-02-11' },
|
||||
{ difTime: '0.5', createDate: '2024-02-11' },
|
||||
{ diffTime: '0.5', createDate: '2024-02-11' },
|
||||
{ diffTime: '0.5', createDate: '2024-02-11' },
|
||||
],
|
||||
};
|
||||
|
||||
@ -122,7 +123,7 @@ describe('DataStatisticController', () => {
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
const result = await controller.data(mockRequest.query);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
@ -151,12 +152,13 @@ describe('DataStatisticController', () => {
|
||||
field: 'xxx',
|
||||
title: 'xxx',
|
||||
type: 'xxx',
|
||||
diffTime: 'xxx',
|
||||
othersCode: 'xxx',
|
||||
},
|
||||
],
|
||||
listBody: [
|
||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '15200000000' },
|
||||
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
||||
{ diffTime: '0.5', createDate: '2024-02-11', data123: '15200000000' },
|
||||
{ diffTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
|
||||
],
|
||||
};
|
||||
|
||||
@ -167,7 +169,7 @@ describe('DataStatisticController', () => {
|
||||
.spyOn(dataStatisticService, 'getDataTable')
|
||||
.mockResolvedValueOnce(mockDataTable);
|
||||
|
||||
const result = await controller.data(mockRequest.query, mockRequest);
|
||||
const result = await controller.data(mockRequest.query);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
@ -185,9 +187,9 @@ describe('DataStatisticController', () => {
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
controller.data(mockRequest.query, mockRequest),
|
||||
).rejects.toThrow(HttpException);
|
||||
await expect(controller.data(mockRequest.query)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@ -70,7 +70,7 @@ describe('DataStatisticService', () => {
|
||||
data413: 3,
|
||||
data863: '109239',
|
||||
},
|
||||
difTime: 21278,
|
||||
diffTime: 21278,
|
||||
clientTime: 1710340862733.0,
|
||||
secretKeys: [],
|
||||
optionTextAndId: {
|
||||
@ -197,7 +197,7 @@ describe('DataStatisticService', () => {
|
||||
data413_3: expect.any(String),
|
||||
data413: expect.any(Number),
|
||||
data863: expect.any(String),
|
||||
difTime: expect.any(String),
|
||||
diffTime: expect.any(String),
|
||||
createDate: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
@ -220,7 +220,7 @@ describe('DataStatisticService', () => {
|
||||
'U2FsdGVkX19bRmf3uEmXAJ/6zXd1Znr3cZsD5v4Nocr2v5XG1taXluz8cohFkDyH',
|
||||
data770: 'U2FsdGVkX18ldQMhJjFXO8aerjftZLpFnRQ4/FVcCLI=',
|
||||
},
|
||||
difTime: 806707,
|
||||
diffTime: 806707,
|
||||
clientTime: 1710400229573.0,
|
||||
secretKeys: ['data458', 'data450', 'data405', 'data770'],
|
||||
optionTextAndId: {
|
||||
@ -303,7 +303,7 @@ describe('DataStatisticService', () => {
|
||||
data458: expect.any(String),
|
||||
data515: expect.any(String),
|
||||
data770: expect.any(String),
|
||||
difTime: expect.any(String),
|
||||
diffTime: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
@ -7,7 +7,7 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
jest.mock('src/guards/survey.guard');
|
||||
@ -49,7 +49,7 @@ describe('SurveyHistoryController', () => {
|
||||
useClass: jest.fn().mockImplementation(() => ({})),
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
@ -66,7 +66,7 @@ describe('SurveyHistoryController', () => {
|
||||
it('should return history list when query is valid', async () => {
|
||||
const queryInfo = { surveyId: 'survey123', historyType: 'published' };
|
||||
|
||||
await controller.getList(queryInfo, {});
|
||||
await controller.getList(queryInfo);
|
||||
|
||||
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledWith({
|
||||
surveyId: queryInfo.surveyId,
|
||||
|
@ -78,7 +78,13 @@ describe('SurveyHistoryService', () => {
|
||||
.spyOn(repository, 'save')
|
||||
.mockResolvedValueOnce({} as SurveyHistory);
|
||||
|
||||
await service.addHistory({ surveyId, schema, type, user });
|
||||
await service.addHistory({
|
||||
surveyId,
|
||||
schema,
|
||||
type,
|
||||
user,
|
||||
sessionId: '',
|
||||
});
|
||||
|
||||
expect(spyCreate).toHaveBeenCalledWith({
|
||||
pageId: surveyId,
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
SURVEY_PERMISSION,
|
||||
SURVEY_PERMISSION_DESCRIPTION,
|
||||
} from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
@ -40,7 +40,7 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
export class CollaboratorController {
|
||||
constructor(
|
||||
private readonly collaboratorService: CollaboratorService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly userService: UserService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly workspaceMemberServie: WorkspaceMemberService,
|
||||
@ -69,7 +69,7 @@ export class CollaboratorController {
|
||||
) {
|
||||
const { error, value } = CreateCollaboratorDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException(
|
||||
'系统错误,请联系管理员',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
@ -124,7 +124,7 @@ export class CollaboratorController {
|
||||
) {
|
||||
const { error, value } = BatchSaveCollaboratorDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException(
|
||||
'系统错误,请联系管理员',
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
@ -184,7 +184,7 @@ export class CollaboratorController {
|
||||
neIdList: collaboratorIdList,
|
||||
userIdList: newCollaboratorUserIdList,
|
||||
});
|
||||
this.logger.info('batchDelete:' + JSON.stringify(delRes), { req });
|
||||
this.logger.info('batchDelete:' + JSON.stringify(delRes));
|
||||
if (Array.isArray(newCollaborator) && newCollaborator.length > 0) {
|
||||
const insertRes = await this.collaboratorService.batchCreate({
|
||||
surveyId: value.surveyId,
|
||||
@ -208,7 +208,7 @@ export class CollaboratorController {
|
||||
const delRes = await this.collaboratorService.batchDeleteBySurveyId(
|
||||
value.surveyId,
|
||||
);
|
||||
this.logger.info(JSON.stringify(delRes), { req });
|
||||
this.logger.info(JSON.stringify(delRes));
|
||||
}
|
||||
|
||||
return {
|
||||
@ -225,11 +225,10 @@ export class CollaboratorController {
|
||||
])
|
||||
async getSurveyCollaboratorList(
|
||||
@Query() query: GetSurveyCollaboratorListDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { error, value } = GetSurveyCollaboratorListDto.validate(query);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -263,17 +262,14 @@ export class CollaboratorController {
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async changeUserPermission(
|
||||
@Body() reqBody: ChangeUserPermissionDto,
|
||||
@Request() req,
|
||||
) {
|
||||
async changeUserPermission(@Body() reqBody: ChangeUserPermissionDto) {
|
||||
const { error, value } = Joi.object({
|
||||
surveyId: Joi.string(),
|
||||
userId: Joi.string(),
|
||||
permissions: Joi.array().items(Joi.string().required()),
|
||||
}).validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -292,13 +288,13 @@ export class CollaboratorController {
|
||||
@SetMetadata('surveyPermission', [
|
||||
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
|
||||
])
|
||||
async deleteCollaborator(@Query() query, @Request() req) {
|
||||
async deleteCollaborator(@Query() query) {
|
||||
const { error, value } = Joi.object({
|
||||
surveyId: Joi.string(),
|
||||
userId: Joi.string(),
|
||||
}).validate(query);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -319,7 +315,7 @@ export class CollaboratorController {
|
||||
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
|
||||
|
||||
if (!surveyMeta) {
|
||||
this.logger.error(`问卷不存在: ${surveyId}`, { req });
|
||||
this.logger.error(`问卷不存在: ${surveyId}`);
|
||||
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
@ -17,11 +16,12 @@ import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } 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';
|
||||
import { QUESTION_TYPE } from 'src/enums/question';
|
||||
|
||||
@ApiTags('survey')
|
||||
@ApiBearerAuth()
|
||||
@ -31,7 +31,7 @@ export class DataStatisticController {
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly dataStatisticService: DataStatisticService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Get('/dataTable')
|
||||
@ -43,7 +43,6 @@ export class DataStatisticController {
|
||||
async data(
|
||||
@Query()
|
||||
queryInfo,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
@ -52,7 +51,7 @@ export class DataStatisticController {
|
||||
pageSize: Joi.number().default(10),
|
||||
}).validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { surveyId, isDesensitive, page, pageSize } = value;
|
||||
@ -103,15 +102,15 @@ export class DataStatisticController {
|
||||
};
|
||||
}
|
||||
const allowQuestionType = [
|
||||
'radio',
|
||||
'checkbox',
|
||||
'binary-choice',
|
||||
'radio-star',
|
||||
'radio-nps',
|
||||
'vote',
|
||||
QUESTION_TYPE.RADIO,
|
||||
QUESTION_TYPE.CHECKBOX,
|
||||
QUESTION_TYPE.BINARY_CHOICE,
|
||||
QUESTION_TYPE.RADIO_STAR,
|
||||
QUESTION_TYPE.RADIO_NPS,
|
||||
QUESTION_TYPE.VOTE,
|
||||
];
|
||||
const fieldList = responseSchema.code.dataConf.dataList
|
||||
.filter((item) => allowQuestionType.includes(item.type))
|
||||
.filter((item) => allowQuestionType.includes(item.type as QUESTION_TYPE))
|
||||
.map((item) => item.field);
|
||||
const dataMap = responseSchema.code.dataConf.dataList.reduce((pre, cur) => {
|
||||
pre[cur.field] = cur;
|
||||
|
188
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
188
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
Post,
|
||||
Body,
|
||||
// Response,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
//后添加
|
||||
import { DownloadTaskService } from '../services/downloadTask.service';
|
||||
import {
|
||||
GetDownloadTaskDto,
|
||||
CreateDownloadDto,
|
||||
GetDownloadTaskListDto,
|
||||
DeleteDownloadTaskDto,
|
||||
} from '../dto/downloadTask.dto';
|
||||
import moment from 'moment';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
|
||||
@ApiTags('downloadTask')
|
||||
@ApiBearerAuth()
|
||||
@Controller('/api/downloadTask')
|
||||
export class DownloadTaskController {
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly downloadTaskService: DownloadTaskService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Post('/createTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async createTask(
|
||||
@Body()
|
||||
reqBody: CreateDownloadDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = CreateDownloadDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { surveyId, isDesensitive } = value;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const id = await this.downloadTaskService.createDownloadTask({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
operatorId: req.user._id.toString(),
|
||||
params: { isDesensitive },
|
||||
});
|
||||
this.downloadTaskService.processDownloadTask({ taskId: id });
|
||||
return {
|
||||
code: 200,
|
||||
data: { taskId: id },
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/getDownloadTaskList')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async downloadList(
|
||||
@Query()
|
||||
queryInfo: GetDownloadTaskListDto,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = GetDownloadTaskListDto.validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { pageIndex, pageSize } = value;
|
||||
const { total, list } = await this.downloadTaskService.getDownloadTaskList({
|
||||
ownerId: req.user._id.toString(),
|
||||
pageIndex,
|
||||
pageSize,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
total: total,
|
||||
list: list.map((data) => {
|
||||
const item: Record<string, any> = {};
|
||||
item.taskId = data._id.toString();
|
||||
item.curStatus = data.curStatus;
|
||||
item.filename = data.filename;
|
||||
item.url = data.url;
|
||||
const fmt = 'YYYY-MM-DD HH:mm:ss';
|
||||
const units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
let unitIndex = 0;
|
||||
let size = Number(data.fileSize);
|
||||
if (isNaN(size)) {
|
||||
item.fileSize = data.fileSize;
|
||||
} else {
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
item.fileSize = `${size.toFixed()} ${units[unitIndex]}`;
|
||||
}
|
||||
item.createDate = moment(Number(data.createDate)).format(fmt);
|
||||
return item;
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/getDownloadTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async getDownloadTask(@Query() query: GetDownloadTaskDto, @Request() req) {
|
||||
const { value, error } = GetDownloadTaskDto.validate(query);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const taskInfo = await this.downloadTaskService.getDownloadTaskById({
|
||||
taskId: value.taskId,
|
||||
});
|
||||
|
||||
if (!taskInfo) {
|
||||
throw new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
if (taskInfo.ownerId !== req.user._id.toString()) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
const res: Record<string, any> = {
|
||||
...taskInfo,
|
||||
};
|
||||
res.taskId = taskInfo._id.toString();
|
||||
delete res._id;
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: res,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/deleteDownloadTask')
|
||||
@HttpCode(200)
|
||||
@UseGuards(Authentication)
|
||||
async deleteFileByName(@Body() body: DeleteDownloadTaskDto, @Request() req) {
|
||||
const { value, error } = DeleteDownloadTaskDto.validate(body);
|
||||
if (error) {
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { taskId } = value;
|
||||
|
||||
const taskInfo = await this.downloadTaskService.getDownloadTaskById({
|
||||
taskId,
|
||||
});
|
||||
|
||||
if (!taskInfo) {
|
||||
throw new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
if (taskInfo.ownerId !== req.user._id.toString()) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const delRes = await this.downloadTaskService.deleteDownloadTask({
|
||||
taskId,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: delRes.modifiedCount === 1,
|
||||
};
|
||||
}
|
||||
}
|
89
server/src/modules/survey/controllers/session.controller.ts
Normal file
89
server/src/modules/survey/controllers/session.controller.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { SessionService } from '../services/session.service';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { SessionGuard } from 'src/guards/session.guard';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/session')
|
||||
export class SessionController {
|
||||
constructor(
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Post('/create')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async create(
|
||||
@Body()
|
||||
reqBody: {
|
||||
surveyId: string;
|
||||
},
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate(reqBody);
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const surveyId = value.surveyId;
|
||||
const session = await this.sessionService.create({
|
||||
surveyId,
|
||||
userId: req.user._id.toString(),
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
sessionId: session._id.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/seize')
|
||||
@HttpCode(200)
|
||||
@UseGuards(SessionGuard)
|
||||
@SetMetadata('sessionId', 'body.sessionId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async seize(
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const saveSession = req.saveSession;
|
||||
|
||||
await this.sessionService.updateSessionToEditing({
|
||||
sessionId: saveSession._id.toString(),
|
||||
surveyId: saveSession.surveyId,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import { SurveyConfService } from '../services/surveyConf.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
import { ContentSecurityService } from '../services/contentSecurity.service';
|
||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { CounterService } from 'src/modules/surveyResponse/services/counter.service';
|
||||
|
||||
import BannerData from '../template/banner/index.json';
|
||||
import { CreateSurveyDto } from '../dto/createSurvey.dto';
|
||||
@ -25,12 +26,15 @@ import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { HISTORY_TYPE } from 'src/enums';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
|
||||
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
||||
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { MemberType, WhitelistType } from 'src/interfaces/survey';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/survey')
|
||||
@ -41,7 +45,10 @@ export class SurveyController {
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly contentSecurityService: ContentSecurityService,
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly counterService: CounterService,
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@Get('/getBannerData')
|
||||
@ -70,9 +77,7 @@ export class SurveyController {
|
||||
) {
|
||||
const { error, value } = CreateSurveyDto.validate(reqBody);
|
||||
if (error) {
|
||||
this.logger.error(`createSurvey_parameter error: ${error.message}`, {
|
||||
req,
|
||||
});
|
||||
this.logger.error(`createSurvey_parameter error: ${error.message}`);
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -128,13 +133,41 @@ export class SurveyController {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
configData: Joi.any().required(),
|
||||
sessionId: Joi.string().required(),
|
||||
}).validate(surveyInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const username = req.user.username;
|
||||
const sessionId = value.sessionId;
|
||||
const surveyId = value.surveyId;
|
||||
const latestEditingOne = await this.sessionService.findLatestEditingOne({
|
||||
surveyId,
|
||||
});
|
||||
|
||||
if (latestEditingOne && latestEditingOne._id.toString() !== sessionId) {
|
||||
const curSession = await this.sessionService.findOne(sessionId);
|
||||
if (curSession.createDate <= latestEditingOne.updateDate) {
|
||||
// 在当前用户打开之后,被其他页面保存过了
|
||||
const isSameOperator =
|
||||
latestEditingOne.userId === req.user._id.toString();
|
||||
let preOperator;
|
||||
if (!isSameOperator) {
|
||||
preOperator = await this.userService.getUserById(
|
||||
latestEditingOne.userId,
|
||||
);
|
||||
}
|
||||
return {
|
||||
code: EXCEPTION_CODE.SURVEY_SAVE_CONFLICT,
|
||||
errmsg: isSameOperator
|
||||
? '当前问卷已在其它页面开启编辑,刷新以获取最新内容'
|
||||
: `当前问卷已由 ${preOperator.username} 编辑,刷新以获取最新内容`,
|
||||
};
|
||||
}
|
||||
}
|
||||
await this.sessionService.updateSessionToEditing({ sessionId, surveyId });
|
||||
|
||||
const username = req.user.username;
|
||||
|
||||
const configData = value.configData;
|
||||
await this.surveyConfService.saveSurveyConf({
|
||||
@ -197,7 +230,7 @@ export class SurveyController {
|
||||
}).validate(queryInfo);
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
@ -214,6 +247,16 @@ export class SurveyController {
|
||||
surveyMeta.isCollaborated = false;
|
||||
}
|
||||
|
||||
// 白名单相关字段的默认值
|
||||
const baseConf = surveyConf.code?.baseConf;
|
||||
if (baseConf) {
|
||||
baseConf.passwordSwitch = baseConf.passwordSwitch ?? false;
|
||||
baseConf.password = baseConf.password ?? '';
|
||||
baseConf.whitelistType = baseConf.whitelistType ?? WhitelistType.ALL;
|
||||
baseConf.whitelist = baseConf.whitelist ?? [];
|
||||
baseConf.memberType = baseConf.memberType ?? MemberType.MOBILE;
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
@ -230,15 +273,13 @@ export class SurveyController {
|
||||
queryInfo: {
|
||||
surveyPath: string;
|
||||
},
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate({ surveyId: queryInfo.surveyPath });
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const surveyId = value.surveyId;
|
||||
@ -271,7 +312,7 @@ export class SurveyController {
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate(surveyInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const username = req.user.username;
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
@ -15,16 +14,15 @@ import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/surveyHisotry')
|
||||
export class SurveyHistoryController {
|
||||
constructor(
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Get('/getList')
|
||||
@ -43,7 +41,6 @@ export class SurveyHistoryController {
|
||||
surveyId: string;
|
||||
historyType: string;
|
||||
},
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
@ -51,7 +48,7 @@ export class SurveyHistoryController {
|
||||
}).validate(queryInfo);
|
||||
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ import { getFilter, getOrder } from 'src/utils/surveyUtil';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { WorkspaceGuard } from 'src/guards/workspace.guard';
|
||||
@ -33,7 +33,7 @@ import { CollaboratorService } from '../services/collaborator.service';
|
||||
export class SurveyMetaController {
|
||||
constructor(
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly collaboratorService: CollaboratorService,
|
||||
) {}
|
||||
|
||||
@ -51,9 +51,7 @@ export class SurveyMetaController {
|
||||
}).validate(reqBody, { allowUnknown: true });
|
||||
|
||||
if (error) {
|
||||
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
|
||||
req,
|
||||
});
|
||||
this.logger.error(`updateMeta_parameter error: ${error.message}`);
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const survey = req.surveyMeta;
|
||||
@ -81,7 +79,7 @@ export class SurveyMetaController {
|
||||
) {
|
||||
const { value, error } = GetSurveyListDto.validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
const { curPage, pageSize, workspaceId } = value;
|
||||
@ -91,14 +89,14 @@ export class SurveyMetaController {
|
||||
try {
|
||||
filter = getFilter(JSON.parse(decodeURIComponent(value.filter)));
|
||||
} catch (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
}
|
||||
if (value.order) {
|
||||
try {
|
||||
order = order = getOrder(JSON.parse(decodeURIComponent(value.order)));
|
||||
} catch (error) {
|
||||
this.logger.error(error.message, { req });
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
}
|
||||
const userId = req.user._id.toString();
|
||||
|
51
server/src/modules/survey/dto/downloadTask.dto.ts
Normal file
51
server/src/modules/survey/dto/downloadTask.dto.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
|
||||
export class CreateDownloadDto {
|
||||
@ApiProperty({ description: '问卷id', required: true })
|
||||
surveyId: string;
|
||||
@ApiProperty({ description: '是否脱敏', required: false })
|
||||
isDesensitive: boolean;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
isDesensitive: Joi.boolean().allow(null).default(false),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
export class GetDownloadTaskListDto {
|
||||
@ApiProperty({ description: '当前页', required: false })
|
||||
pageIndex: number;
|
||||
@ApiProperty({ description: '一页大小', required: false })
|
||||
pageSize: number;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
pageIndex: Joi.number().default(1),
|
||||
pageSize: Joi.number().default(20),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class GetDownloadTaskDto {
|
||||
@ApiProperty({ description: '任务id', required: true })
|
||||
taskId: string;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
taskId: Joi.string().required(),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteDownloadTaskDto {
|
||||
@ApiProperty({ description: '任务id', required: true })
|
||||
taskId: string;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
taskId: Joi.string().required(),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
@ -3,14 +3,14 @@ import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
|
||||
@Injectable()
|
||||
export class CollaboratorService {
|
||||
constructor(
|
||||
@InjectRepository(Collaborator)
|
||||
private readonly collaboratorRepository: MongoRepository<Collaborator>,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
async create({ surveyId, userId, permissions }) {
|
||||
|
@ -8,9 +8,10 @@ import { keyBy } from 'lodash';
|
||||
import { DataItem } from 'src/interfaces/survey';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { getListHeadByDataList, transformAndMergeArrayFields } from '../utils';
|
||||
import { QUESTION_TYPE } from 'src/enums/question';
|
||||
@Injectable()
|
||||
export class DataStatisticService {
|
||||
private radioType = ['radio-star', 'radio-nps'];
|
||||
private radioType = [QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS];
|
||||
|
||||
constructor(
|
||||
@InjectRepository(SurveyResponse)
|
||||
@ -68,7 +69,7 @@ export class DataStatisticService {
|
||||
}
|
||||
// 处理选项的更多输入框
|
||||
if (
|
||||
this.radioType.includes(itemConfig.type) &&
|
||||
this.radioType.includes(itemConfig.type as QUESTION_TYPE) &&
|
||||
!data[`${itemConfigKey}_custom`]
|
||||
) {
|
||||
data[`${itemConfigKey}_custom`] =
|
||||
@ -89,7 +90,7 @@ export class DataStatisticService {
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
difTime: (submitedData.difTime / 1000).toFixed(2),
|
||||
diffTime: (submitedData.diffTime / 1000).toFixed(2),
|
||||
createDate: moment(submitedData.createDate).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
),
|
||||
|
280
server/src/modules/survey/services/downloadTask.service.ts
Normal file
280
server/src/modules/survey/services/downloadTask.service.ts
Normal file
@ -0,0 +1,280 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { DownloadTask } from 'src/models/downloadTask.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { ResponseSchemaService } from 'src/modules/surveyResponse/services/responseScheme.service';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { DataStatisticService } from './dataStatistic.service';
|
||||
import xlsx from 'node-xlsx';
|
||||
import { load } from 'cheerio';
|
||||
import { get } from 'lodash';
|
||||
import { FileService } from 'src/modules/file/services/file.service';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import moment from 'moment';
|
||||
|
||||
@Injectable()
|
||||
export class DownloadTaskService {
|
||||
private static taskList: Array<any> = [];
|
||||
private static isExecuting: boolean = false;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(DownloadTask)
|
||||
private readonly downloadTaskRepository: MongoRepository<DownloadTask>,
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
@InjectRepository(SurveyResponse)
|
||||
private readonly surveyResponseRepository: MongoRepository<SurveyResponse>,
|
||||
private readonly dataStatisticService: DataStatisticService,
|
||||
private readonly fileService: FileService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
async createDownloadTask({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
operatorId,
|
||||
params,
|
||||
}: {
|
||||
surveyId: string;
|
||||
responseSchema: ResponseSchema;
|
||||
operatorId: string;
|
||||
params: any;
|
||||
}) {
|
||||
const filename = `${responseSchema.title}-${params.isDesensitive ? '脱敏' : '原'}回收数据-${moment().format('YYYYMMDDHHmmss')}.xlsx`;
|
||||
const downloadTask = this.downloadTaskRepository.create({
|
||||
surveyId,
|
||||
surveyPath: responseSchema.surveyPath,
|
||||
fileSize: '计算中',
|
||||
ownerId: operatorId,
|
||||
params: {
|
||||
...params,
|
||||
title: responseSchema.title,
|
||||
},
|
||||
filename,
|
||||
});
|
||||
await this.downloadTaskRepository.save(downloadTask);
|
||||
return downloadTask._id.toString();
|
||||
}
|
||||
|
||||
async getDownloadTaskList({
|
||||
ownerId,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}: {
|
||||
ownerId: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
}) {
|
||||
const where = {
|
||||
ownerId,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
};
|
||||
const [surveyDownloadList, total] =
|
||||
await this.downloadTaskRepository.findAndCount({
|
||||
where,
|
||||
take: pageSize,
|
||||
skip: (pageIndex - 1) * pageSize,
|
||||
order: {
|
||||
createDate: -1,
|
||||
},
|
||||
});
|
||||
return {
|
||||
total,
|
||||
list: surveyDownloadList,
|
||||
};
|
||||
}
|
||||
|
||||
async getDownloadTaskById({ taskId }) {
|
||||
const res = await this.downloadTaskRepository.find({
|
||||
where: {
|
||||
_id: new ObjectId(taskId),
|
||||
},
|
||||
});
|
||||
if (Array.isArray(res) && res.length > 0) {
|
||||
return res[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteDownloadTask({ taskId }: { taskId: string }) {
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: Date.now(),
|
||||
};
|
||||
return this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: new ObjectId(taskId),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus,
|
||||
},
|
||||
$push: {
|
||||
statusList: curStatus as never,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
processDownloadTask({ taskId }) {
|
||||
DownloadTaskService.taskList.push(taskId);
|
||||
if (!DownloadTaskService.isExecuting) {
|
||||
this.executeTask();
|
||||
DownloadTaskService.isExecuting = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeTask() {
|
||||
try {
|
||||
for (const taskId of DownloadTaskService.taskList) {
|
||||
const taskInfo = await this.getDownloadTaskById({ taskId });
|
||||
if (!taskInfo || taskInfo.curStatus.status === RECORD_STATUS.REMOVED) {
|
||||
// 不存在或者已删除的,不处理
|
||||
continue;
|
||||
}
|
||||
await this.handleDownloadTask({ taskInfo });
|
||||
}
|
||||
} finally {
|
||||
DownloadTaskService.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownloadTask({ taskInfo }) {
|
||||
try {
|
||||
// 更新任务状态为计算中
|
||||
const updateRes = await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.COMOPUTETING,
|
||||
date: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.info(JSON.stringify(updateRes));
|
||||
|
||||
// 开始计算任务
|
||||
const surveyId = taskInfo.surveyId;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const where = {
|
||||
pageId: surveyId,
|
||||
'curStatus.status': {
|
||||
$ne: 'removed',
|
||||
},
|
||||
};
|
||||
const total = await this.surveyResponseRepository.count(where);
|
||||
const pageSize = 200;
|
||||
const pageTotal = Math.ceil(total / pageSize);
|
||||
const xlsxHead = [];
|
||||
const xlsxBody = [];
|
||||
for (let pageIndex = 1; pageIndex <= pageTotal; pageIndex++) {
|
||||
const { listHead, listBody } =
|
||||
await this.dataStatisticService.getDataTable({
|
||||
surveyId,
|
||||
pageNum: pageIndex,
|
||||
pageSize,
|
||||
responseSchema,
|
||||
});
|
||||
if (xlsxHead.length === 0) {
|
||||
for (const item of listHead) {
|
||||
const $ = load(item.title);
|
||||
const text = $.text();
|
||||
xlsxHead.push(text);
|
||||
}
|
||||
}
|
||||
for (const bodyItem of listBody) {
|
||||
const bodyData = [];
|
||||
for (const headItem of listHead) {
|
||||
const field = headItem.field;
|
||||
const val = get(bodyItem, field, '');
|
||||
const $ = load(val);
|
||||
const text = $.text();
|
||||
bodyData.push(text);
|
||||
}
|
||||
xlsxBody.push(bodyData);
|
||||
}
|
||||
}
|
||||
const xlsxData = [xlsxHead, ...xlsxBody];
|
||||
const buffer = await xlsx.build([
|
||||
{ name: 'sheet1', data: xlsxData, options: {} },
|
||||
]);
|
||||
|
||||
const file: Express.Multer.File = {
|
||||
fieldname: 'file',
|
||||
originalname: taskInfo.filename,
|
||||
encoding: '7bit',
|
||||
mimetype: 'application/octet-stream',
|
||||
filename: taskInfo.filename,
|
||||
size: buffer.length,
|
||||
buffer: buffer,
|
||||
stream: null,
|
||||
destination: null,
|
||||
path: '',
|
||||
};
|
||||
const { url, key } = await this.fileService.upload({
|
||||
configKey: 'SERVER_LOCAL_CONFIG',
|
||||
file,
|
||||
pathPrefix: 'exportfile',
|
||||
keepOriginFilename: true,
|
||||
});
|
||||
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.FINISHED,
|
||||
date: Date.now(),
|
||||
};
|
||||
|
||||
// 更新计算结果
|
||||
const updateFinishRes = await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus,
|
||||
url,
|
||||
fileKey: key,
|
||||
fileSize: buffer.length,
|
||||
},
|
||||
$push: {
|
||||
statusList: curStatus as never,
|
||||
},
|
||||
},
|
||||
);
|
||||
this.logger.info(JSON.stringify(updateFinishRes));
|
||||
} catch (error) {
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.ERROR,
|
||||
date: Date.now(),
|
||||
};
|
||||
await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus,
|
||||
},
|
||||
$push: {
|
||||
statusList: curStatus as never,
|
||||
},
|
||||
},
|
||||
);
|
||||
this.logger.error(
|
||||
`导出文件失败 taskId: ${taskInfo._id.toString()}, surveyId: ${taskInfo.surveyId}, message: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
80
server/src/modules/survey/services/session.service.ts
Normal file
80
server/src/modules/survey/services/session.service.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Session } from 'src/models/session.entity';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
constructor(
|
||||
@InjectRepository(Session)
|
||||
private readonly sessionRepository: MongoRepository<Session>,
|
||||
) {}
|
||||
|
||||
create({ surveyId, userId }) {
|
||||
const session = this.sessionRepository.create({
|
||||
surveyId,
|
||||
userId,
|
||||
});
|
||||
return this.sessionRepository.save(session);
|
||||
}
|
||||
|
||||
findOne(sessionId) {
|
||||
return this.sessionRepository.findOne({
|
||||
where: {
|
||||
_id: new ObjectId(sessionId),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findLatestEditingOne({ surveyId }) {
|
||||
return this.sessionRepository.findOne({
|
||||
where: {
|
||||
surveyId,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.NEW,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateSessionToEditing({ sessionId, surveyId }) {
|
||||
const now = Date.now();
|
||||
const editingStatus = {
|
||||
status: RECORD_STATUS.EDITING,
|
||||
date: now,
|
||||
};
|
||||
const newStatus = {
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: now,
|
||||
};
|
||||
return Promise.all([
|
||||
this.sessionRepository.updateOne(
|
||||
{
|
||||
_id: new ObjectId(sessionId),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: editingStatus,
|
||||
updateDate: now,
|
||||
},
|
||||
},
|
||||
),
|
||||
this.sessionRepository.updateMany(
|
||||
{
|
||||
surveyId,
|
||||
_id: {
|
||||
$ne: new ObjectId(sessionId),
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: newStatus,
|
||||
updateDate: now,
|
||||
},
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
import { SurveyResponseModule } from '../surveyResponse/surveyResponse.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
import { FileModule } from '../file/file.module';
|
||||
|
||||
import { DataStatisticController } from './controllers/dataStatistic.controller';
|
||||
import { SurveyController } from './controllers/survey.controller';
|
||||
@ -14,6 +15,8 @@ import { SurveyHistoryController } from './controllers/surveyHistory.controller'
|
||||
import { SurveyMetaController } from './controllers/surveyMeta.controller';
|
||||
import { SurveyUIController } from './controllers/surveyUI.controller';
|
||||
import { CollaboratorController } from './controllers/collaborator.controller';
|
||||
import { DownloadTaskController } from './controllers/downloadTask.controller';
|
||||
import { SessionController } from './controllers/session.controller';
|
||||
|
||||
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||
import { SurveyHistory } from 'src/models/surveyHistory.entity';
|
||||
@ -21,14 +24,21 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { Word } from 'src/models/word.entity';
|
||||
import { Collaborator } from 'src/models/collaborator.entity';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { DownloadTask } from 'src/models/downloadTask.entity';
|
||||
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { DataStatisticService } from './services/dataStatistic.service';
|
||||
import { SurveyConfService } from './services/surveyConf.service';
|
||||
import { SurveyHistoryService } from './services/surveyHistory.service';
|
||||
import { SurveyMetaService } from './services/surveyMeta.service';
|
||||
import { ContentSecurityService } from './services/contentSecurity.service';
|
||||
import { CollaboratorService } from './services/collaborator.service';
|
||||
import { Counter } from 'src/models/counter.entity';
|
||||
import { CounterService } from '../surveyResponse/services/counter.service';
|
||||
import { FileService } from '../file/services/file.service';
|
||||
import { DownloadTaskService } from './services/downloadTask.service';
|
||||
import { SessionService } from './services/session.service';
|
||||
import { Session } from 'src/models/session.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -39,11 +49,15 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
SurveyResponse,
|
||||
Word,
|
||||
Collaborator,
|
||||
Counter,
|
||||
DownloadTask,
|
||||
Session,
|
||||
]),
|
||||
ConfigModule,
|
||||
SurveyResponseModule,
|
||||
AuthModule,
|
||||
WorkspaceModule,
|
||||
FileModule,
|
||||
],
|
||||
controllers: [
|
||||
DataStatisticController,
|
||||
@ -52,6 +66,8 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
SurveyMetaController,
|
||||
SurveyUIController,
|
||||
CollaboratorController,
|
||||
DownloadTaskController,
|
||||
SessionController,
|
||||
],
|
||||
providers: [
|
||||
DataStatisticService,
|
||||
@ -62,6 +78,10 @@ import { CollaboratorService } from './services/collaborator.service';
|
||||
ContentSecurityService,
|
||||
CollaboratorService,
|
||||
LoggerProvider,
|
||||
CounterService,
|
||||
DownloadTaskService,
|
||||
FileService,
|
||||
SessionService,
|
||||
],
|
||||
})
|
||||
export class SurveyModule {}
|
||||
|
@ -48,7 +48,8 @@
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115019"
|
||||
"hash": "115019",
|
||||
"quota": "0"
|
||||
},
|
||||
{
|
||||
"text": "选项2",
|
||||
@ -57,9 +58,11 @@
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115020"
|
||||
"hash": "115020",
|
||||
"quota": "0"
|
||||
}
|
||||
]
|
||||
],
|
||||
"quotaNoDisplay": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -41,8 +41,8 @@
|
||||
"innerType": "radio",
|
||||
"field": "data606",
|
||||
"title": "标题2",
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"minNum": 0,
|
||||
"maxNum": 0,
|
||||
"options": [
|
||||
{
|
||||
"text": "选项1",
|
||||
|
@ -1,55 +1,56 @@
|
||||
{
|
||||
"bannerConf": {
|
||||
"titleConfig": {
|
||||
"mainTitle": "<h3 style=\"text-align: center\">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style=\"color: rgb(204, 0, 0)\">期待您的参与!</span></p>",
|
||||
"subTitle": ""
|
||||
},
|
||||
"bannerConfig": {
|
||||
"bgImage": "/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.webp",
|
||||
"videoLink": "",
|
||||
"postImg": ""
|
||||
}
|
||||
"bannerConf": {
|
||||
"titleConfig": {
|
||||
"mainTitle": "<h3 style=\"text-align: center\">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style=\"color: rgb(204, 0, 0)\">期待您的参与!</span></p>",
|
||||
"subTitle": ""
|
||||
},
|
||||
"submitConf": {
|
||||
"submitTitle": "提交",
|
||||
"confirmAgain": {
|
||||
"is_again": true,
|
||||
"again_text": "确认要提交吗?"
|
||||
},
|
||||
"msgContent": {
|
||||
"msg_200": "提交成功",
|
||||
"msg_9001": "您来晚了,感谢支持问卷~",
|
||||
"msg_9002": "请勿多次提交!",
|
||||
"msg_9003": "您来晚了,已经满额!",
|
||||
"msg_9004": "提交失败!"
|
||||
}
|
||||
},
|
||||
"bottomConf": {
|
||||
"logoImage": "/imgs/Logo.webp",
|
||||
"logoImageWidth": "60%"
|
||||
},
|
||||
"baseConf": {
|
||||
"begTime": "2024-01-01 00:00:00",
|
||||
"endTime": "2034-01-01 00:00:00",
|
||||
"tLimit": 0,
|
||||
"language": "chinese",
|
||||
"answerBegTime": "00:00:00",
|
||||
"answerEndTime": "23:59:59"
|
||||
},
|
||||
"skinConf": {
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff",
|
||||
"backgroundConf": {
|
||||
"color": "#fff"
|
||||
},
|
||||
"themeConf": {
|
||||
"color": "#ffa600"
|
||||
},
|
||||
"contentConf": {
|
||||
"opacity": 100
|
||||
}
|
||||
},
|
||||
"logicConf": {
|
||||
"showLogicConf": []
|
||||
"bannerConfig": {
|
||||
"bgImage": "/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.webp",
|
||||
"videoLink": "",
|
||||
"postImg": ""
|
||||
}
|
||||
},
|
||||
"submitConf": {
|
||||
"submitTitle": "提交",
|
||||
"confirmAgain": {
|
||||
"is_again": true,
|
||||
"again_text": "确认要提交吗?"
|
||||
},
|
||||
"msgContent": {
|
||||
"msg_200": "提交成功",
|
||||
"msg_9001": "您来晚了,感谢支持问卷~",
|
||||
"msg_9002": "请勿多次提交!",
|
||||
"msg_9003": "您来晚了,已经满额!",
|
||||
"msg_9004": "提交失败!"
|
||||
}
|
||||
},
|
||||
"bottomConf": {
|
||||
"logoImage": "/imgs/Logo.webp",
|
||||
"logoImageWidth": "60%"
|
||||
},
|
||||
"baseConf": {
|
||||
"begTime": "2024-01-01 00:00:00",
|
||||
"endTime": "2034-01-01 00:00:00",
|
||||
"tLimit": 0,
|
||||
"language": "chinese",
|
||||
"answerBegTime": "00:00:00",
|
||||
"answerEndTime": "23:59:59"
|
||||
},
|
||||
"skinConf": {
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff",
|
||||
"backgroundConf": {
|
||||
"color": "#ffffff"
|
||||
},
|
||||
"themeConf": {
|
||||
"color": "#ffa600"
|
||||
},
|
||||
"contentConf": {
|
||||
"opacity": 100
|
||||
}
|
||||
},
|
||||
"pageConf": [],
|
||||
"logicConf": {
|
||||
"showLogicConf": []
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import normalCode from '../template/surveyTemplate/survey/normal.json';
|
||||
import npsCode from '../template/surveyTemplate/survey/nps.json';
|
||||
import registerCode from '../template/surveyTemplate/survey/register.json';
|
||||
import voteCode from '../template/surveyTemplate/survey/vote.json';
|
||||
import { QUESTION_TYPE } from 'src/enums/question';
|
||||
|
||||
const schemaDataMap = {
|
||||
normal: normalCode,
|
||||
@ -31,9 +32,11 @@ export async function getSchemaBySurveyType(surveyType: string) {
|
||||
export function getListHeadByDataList(dataList) {
|
||||
const listHead = dataList.map((question) => {
|
||||
let othersCode;
|
||||
const radioType = ['radio-star', 'radio-nps'];
|
||||
const radioType = [QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS];
|
||||
if (radioType.includes(question.type)) {
|
||||
const rangeConfigKeys = question.rangeConfig ? Object.keys(question.rangeConfig) : [];
|
||||
const rangeConfigKeys = question.rangeConfig
|
||||
? Object.keys(question.rangeConfig)
|
||||
: [];
|
||||
if (rangeConfigKeys.length > 0) {
|
||||
othersCode = [{ code: `${question.field}_custom`, option: '填写理由' }];
|
||||
}
|
||||
@ -55,14 +58,14 @@ export function getListHeadByDataList(dataList) {
|
||||
};
|
||||
});
|
||||
listHead.push({
|
||||
field: 'difTime',
|
||||
field: 'diffTime',
|
||||
title: '答题耗时(秒)',
|
||||
type: 'text',
|
||||
type: QUESTION_TYPE.TEXT,
|
||||
});
|
||||
listHead.push({
|
||||
field: 'createDate',
|
||||
title: '提交时间',
|
||||
type: 'text',
|
||||
type: QUESTION_TYPE.TEXT,
|
||||
});
|
||||
return listHead;
|
||||
}
|
||||
@ -109,7 +112,14 @@ export function handleAggretionData({ dataMap, item }) {
|
||||
pre[cur.id] = cur;
|
||||
return pre;
|
||||
}, {});
|
||||
if (['radio', 'checkbox', 'vote', 'binary-choice'].includes(type)) {
|
||||
if (
|
||||
[
|
||||
QUESTION_TYPE.RADIO,
|
||||
QUESTION_TYPE.CHECKBOX,
|
||||
QUESTION_TYPE.VOTE,
|
||||
QUESTION_TYPE.BINARY_CHOICE,
|
||||
].includes(type)
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
title: dataMap[item.field].title,
|
||||
@ -125,7 +135,9 @@ export function handleAggretionData({ dataMap, item }) {
|
||||
}),
|
||||
},
|
||||
};
|
||||
} else if (['radio-star', 'radio-nps'].includes(type)) {
|
||||
} else if (
|
||||
[QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS].includes(type)
|
||||
) {
|
||||
const summary: Record<string, any> = {};
|
||||
const average = getAverage({ aggregation: item.data.aggregation });
|
||||
const median = getMedian({ aggregation: item.data.aggregation });
|
||||
@ -136,10 +148,10 @@ export function handleAggretionData({ dataMap, item }) {
|
||||
summary['average'] = average;
|
||||
summary['median'] = median;
|
||||
summary['variance'] = variance;
|
||||
if (type === 'radio-nps') {
|
||||
if (type === QUESTION_TYPE.RADIO_NPS) {
|
||||
summary['nps'] = getNps({ aggregation: item.data.aggregation });
|
||||
}
|
||||
const range = type === 'radio-nps' ? [0, 10] : [1, 5];
|
||||
const range = type === QUESTION_TYPE.RADIO_NPS ? [0, 10] : [1, 5];
|
||||
const arr = [];
|
||||
for (let i = range[0]; i <= range[1]; i++) {
|
||||
arr.push(i);
|
||||
|
@ -6,6 +6,11 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { Logger } from 'src/logger';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
|
||||
jest.mock('../services/responseScheme.service');
|
||||
|
||||
@ -16,7 +21,40 @@ describe('ResponseSchemaController', () => {
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [ResponseSchemaController],
|
||||
providers: [ResponseSchemaService],
|
||||
providers: [
|
||||
ResponseSchemaService,
|
||||
AuthService,
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
getUserByUsername: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {
|
||||
findAllByUserId: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<ResponseSchemaController>(ResponseSchemaController);
|
||||
@ -66,5 +104,146 @@ describe('ResponseSchemaController', () => {
|
||||
new HttpException('问卷已删除', EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED),
|
||||
);
|
||||
});
|
||||
|
||||
it('whitelistValidate should throw SurveyNotFoundException when survey is removed', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue(null);
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
}),
|
||||
).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交'));
|
||||
});
|
||||
|
||||
it('whitelistValidate should throw WHITELIST_ERROR code when password is incorrect', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123457',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
|
||||
it('whitelistValidate should be successfully', async () => {
|
||||
const surveyPath = 'test';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
}),
|
||||
).resolves.toEqual({ code: 200, data: null });
|
||||
});
|
||||
|
||||
it('whitelistValidate should throw WHITELIST_ERROR code when mobile or email is incorrect', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
whitelistType: 'CUSTOM',
|
||||
memberType: 'MOBILE',
|
||||
whitelist: ['13500000000'],
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
whitelist: '13500000001',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
|
||||
it('whitelistValidate should throw WHITELIST_ERROR code when member is incorrect', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
whitelistType: 'MEMBER',
|
||||
whitelist: ['Jack'],
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
whitelist: 'James',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('whitelistValidate should return verifyId successfully', async () => {
|
||||
const surveyPath = '';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: {
|
||||
status: 'published',
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
whitelistType: 'CUSTOM',
|
||||
memberType: 'MOBILE',
|
||||
whitelist: ['13500000000'],
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
|
||||
await expect(
|
||||
controller.whitelistValidate(surveyPath, {
|
||||
password: '123456',
|
||||
whitelist: '13500000000',
|
||||
}),
|
||||
).resolves.toEqual({ code: 200, data: null });
|
||||
});
|
||||
});
|
||||
|
@ -20,7 +20,11 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi
|
||||
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
const mockDecryptErrorBody = {
|
||||
surveyPath: 'EBzdmnSp',
|
||||
@ -28,11 +32,11 @@ const mockDecryptErrorBody = {
|
||||
'SkyfsbS6MDvFrrxFJQDMxsvm53G3PTURktfZikJP2fKilC8wPW5ZdfX29Fixor5ldHBBNyILsDtxhbNahEbNCDw8n1wS8IIckFuQcaJtn6MLOD+h+Iuywka3ig4ecTN87RpdcfEQe7f38rSSx0zoFU8j37eojjSF7eETBSrz5m9WaNesQo4hhC6p7wmAo1jggkdbG8PVrFqrZPbkHN5jOBrzQEqdqYu9A5wHMM7nUteqlPpkiogEDYmBIccqmPdtO54y7LoPslXgXj6jNja8oVNaYlnp7UsisT+i5cuQ7lbDukEhrfpAIFRsT2IUwVlLjWHtFQm4/4I5HyvVBirTng==',
|
||||
'IMn0E7R6cYCQPI497mz3x99CPA4cImAFEfIv8Q98Gm5bFcgKJX6KFYS7PF/VtIuI1leKwwNYexQy7+2HnF40by/huVugoPYnPd4pTpUdG6f1kh8EpzIir2+8P98Dcz2+NZ/khP2RIAM8nOr+KSC99TNGhuKaKQCItyLFDkr80s3zv+INieGc8wULIrGoWDJGN2KdU/jSq+hkV0QXypd81N5IyAoNhZLkZeM/FU6grGFPnGRtcDDc5W8YWVHO87VymlxPCTRawXRTDcvGIUqb3GuZfxvA7AULqbspmN9kzt3rktuZLNb2TFQDsJfqUuCmi+b28qP/G4OrT9/VAHhYKw==',
|
||||
],
|
||||
difTime: 806707,
|
||||
diffTime: 806707,
|
||||
clientTime: 1710400229573,
|
||||
encryptType: 'rsa',
|
||||
sessionId: '65f2664c92862d6a9067ad18',
|
||||
sign: '8c9ca8804c9d94de6055d68a1f3c423fe50c95b4bd69f809ee2da8fcd82fd960.1710400229589',
|
||||
sign: '95d6ff5dd3d9ddc205cbab88defe40ebe889952961f1d60e760fa411e2cb39fe.1710400229589',
|
||||
};
|
||||
|
||||
const mockSubmitData = {
|
||||
@ -41,11 +45,11 @@ const mockSubmitData = {
|
||||
'SkyfsbS6MDvFrrxFJQDMxsvm53G3PTURktfZikJP2fKilC8wPW5ZdfX29Fixor5ldHBBNyILsDtxhbNahEbNCDw8n1wS8IIckFuQcaJtn6MLOD+h+Iuywka3ig4ecTN87RpdcfEQe7f38rSSx0zoFU8j37eojjSF7eETBSrz5m9WaNesQo4hhC6p7wmAo1jggkdbG8PVrFqrZPbkHN5jOBrzQEqdqYu9A5wHMM7nUteqlPpkiogEDYmBIccqmPdtO54y7LoPslXgXj6jNja8oVNaYlnp7UsisT+i5cuQ7lbDukEhrfpAIFRsT2IUwVlLjWHtFQm4/4I5HyvVBirTng==',
|
||||
'IMn0E7R6cYCQPI497mz3x99CPA4cImAFEfIv8Q98Gm5bFcgKJX6KFYS7PF/VtIuI1leKwwNYexQy7+2HnF40by/huVugoPYnPd4pTpUdG6f1kh8EpzIir2+8P98Dcz2+NZ/khP2RIAM8nOr+KSC99TNGhuKaKQCItyLFDkr80s3zv+INieGc8wULIrGoWDJGN2KdU/jSq+hkV0QXypd81N5IyAoNhZLkZeM/FU6grGFPnGRtcDDc5W8YWVHO87VymlxPCTRawXRTDcvGIUqb3GuZfxvA7AULqbspmN9kzt3rktuZLNb2TFQDsJfqUuCmi+b28qP/G4OrT9/VAHhYKw==',
|
||||
],
|
||||
difTime: 806707,
|
||||
diffTime: 806707,
|
||||
clientTime: 1710400229573,
|
||||
encryptType: 'rsa',
|
||||
sessionId: '65f29fc192862d6a9067ad28',
|
||||
sign: '8c9ca8804c9d94de6055d68a1f3c423fe50c95b4bd69f809ee2da8fcd82fd960.1710400229589',
|
||||
sign: '95d6ff5dd3d9ddc205cbab88defe40ebe889952961f1d60e760fa411e2cb39fe.1710400229589',
|
||||
};
|
||||
|
||||
const mockClientEncryptInfo = {
|
||||
@ -118,12 +122,24 @@ describe('SurveyResponseController', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
getUserByUsername: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {
|
||||
findAllByUserId: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -169,7 +185,7 @@ describe('SurveyResponseController', () => {
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: 1711025113146,
|
||||
},
|
||||
difTime: 30518,
|
||||
diffTime: 30518,
|
||||
data: {
|
||||
data458: '15000000000',
|
||||
data515: '115019',
|
||||
@ -205,7 +221,7 @@ describe('SurveyResponseController', () => {
|
||||
.spyOn(clientEncryptService, 'deleteEncryptInfo')
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
const result = await controller.createResponse(reqBody, {});
|
||||
const result = await controller.createResponse(reqBody);
|
||||
|
||||
expect(result).toEqual({ code: 200, msg: '提交成功' });
|
||||
expect(
|
||||
@ -224,7 +240,7 @@ describe('SurveyResponseController', () => {
|
||||
data770: '123456@qq.com',
|
||||
},
|
||||
clientTime: reqBody.clientTime,
|
||||
difTime: reqBody.difTime,
|
||||
diffTime: reqBody.diffTime,
|
||||
surveyId: mockResponseSchema.pageId,
|
||||
optionTextAndId: {
|
||||
data515: [
|
||||
@ -252,7 +268,7 @@ describe('SurveyResponseController', () => {
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
SurveyNotFoundException,
|
||||
);
|
||||
});
|
||||
@ -261,7 +277,7 @@ describe('SurveyResponseController', () => {
|
||||
const reqBody = cloneDeep(mockSubmitData);
|
||||
delete reqBody.sign;
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
|
||||
@ -274,7 +290,7 @@ describe('SurveyResponseController', () => {
|
||||
const reqBody = cloneDeep(mockDecryptErrorBody);
|
||||
reqBody.sign = 'mock sign';
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
|
||||
@ -290,7 +306,7 @@ describe('SurveyResponseController', () => {
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
@ -302,9 +318,35 @@ describe('SurveyResponseController', () => {
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce(mockResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException if password does not match', async () => {
|
||||
const reqBody = {
|
||||
...mockSubmitData,
|
||||
password: '123457',
|
||||
sign: '145595d85079af3b1fb30784177c348555f442837c051d90f57a01ce1ff53c32.1710400229589',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValueOnce({
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
},
|
||||
code: {
|
||||
baseConf: {
|
||||
passwordSwitch: true,
|
||||
password: '123456',
|
||||
},
|
||||
},
|
||||
} as ResponseSchema);
|
||||
|
||||
await expect(controller.createResponse(reqBody)).rejects.toThrow(
|
||||
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -33,7 +33,7 @@ describe('SurveyResponseService', () => {
|
||||
const surveyData = {
|
||||
data: {},
|
||||
clientTime: new Date(),
|
||||
difTime: 0,
|
||||
diffTime: 0,
|
||||
surveyId: 'testId',
|
||||
surveyPath: 'testPath',
|
||||
optionTextAndId: {},
|
||||
@ -59,7 +59,7 @@ describe('SurveyResponseService', () => {
|
||||
surveyPath: surveyData.surveyPath,
|
||||
data: surveyData.data,
|
||||
clientTime: surveyData.clientTime,
|
||||
difTime: surveyData.difTime,
|
||||
diffTime: surveyData.diffTime,
|
||||
pageId: surveyData.surveyId,
|
||||
secretKeys: [],
|
||||
optionTextAndId: surveyData.optionTextAndId,
|
||||
|
@ -1,14 +1,33 @@
|
||||
import { Controller, Get, HttpCode, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ResponseSchemaService } from '../services/responseScheme.service';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { WhitelistType } from 'src/interfaces/survey';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
@ApiTags('surveyResponse')
|
||||
@Controller('/api/responseSchema')
|
||||
export class ResponseSchemaController {
|
||||
constructor(private readonly responseSchemaService: ResponseSchemaService) {}
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly userService: UserService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
@Get('/getSchema')
|
||||
@HttpCode(200)
|
||||
@ -34,9 +53,79 @@ export class ResponseSchemaController {
|
||||
EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED,
|
||||
);
|
||||
}
|
||||
|
||||
// 去掉C端的敏感字段
|
||||
if (responseSchema.code?.baseConf) {
|
||||
responseSchema.code.baseConf.password = null;
|
||||
responseSchema.code.baseConf.whitelist = [];
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
data: responseSchema,
|
||||
};
|
||||
}
|
||||
|
||||
// 白名单验证
|
||||
@Post('/:surveyPath/validate')
|
||||
@HttpCode(200)
|
||||
async whitelistValidate(@Param('surveyPath') surveyPath, @Body() body) {
|
||||
const { value, error } = Joi.object({
|
||||
password: Joi.string().allow(null, ''),
|
||||
whitelist: Joi.string().allow(null, ''),
|
||||
}).validate(body, { allowUnknown: true });
|
||||
|
||||
if (error) {
|
||||
this.logger.error(`whitelistValidate error: ${error.message}`, {});
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
// 问卷信息
|
||||
const schema =
|
||||
await this.responseSchemaService.getResponseSchemaByPath(surveyPath);
|
||||
if (!schema || schema.curStatus.status === 'removed') {
|
||||
throw new SurveyNotFoundException('该问卷不存在,无法提交');
|
||||
}
|
||||
|
||||
const { password, whitelist: whitelistValue } = value;
|
||||
const {
|
||||
passwordSwitch,
|
||||
password: settingPassword,
|
||||
whitelistType,
|
||||
whitelist,
|
||||
} = schema.code.baseConf;
|
||||
|
||||
// 密码校验
|
||||
if (passwordSwitch) {
|
||||
if (settingPassword !== password) {
|
||||
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// 名单校验(手机号/邮箱)
|
||||
if (whitelistType === WhitelistType.CUSTOM) {
|
||||
if (!whitelist.includes(whitelistValue)) {
|
||||
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// 团队成员昵称校验
|
||||
if (whitelistType === WhitelistType.MEMBER) {
|
||||
const user = await this.userService.getUserByUsername(whitelistValue);
|
||||
if (!user) {
|
||||
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||
}
|
||||
|
||||
const workspaceMember = await this.workspaceMemberService.findAllByUserId(
|
||||
{ userId: user._id.toString() },
|
||||
);
|
||||
if (!workspaceMember.length) {
|
||||
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Controller, Post, Body, HttpCode, Request } from '@nestjs/common';
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { checkSign } from 'src/utils/checkSign';
|
||||
@ -7,32 +7,48 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { getPushingData } from 'src/utils/messagePushing';
|
||||
|
||||
import { ResponseSchemaService } from '../services/responseScheme.service';
|
||||
import { CounterService } from '../services/counter.service';
|
||||
import { SurveyResponseService } from '../services/surveyResponse.service';
|
||||
import { ClientEncryptService } from '../services/clientEncrypt.service';
|
||||
import { MessagePushingTaskService } from '../../message/services/messagePushingTask.service';
|
||||
import { RedisService } from 'src/modules/redis/redis.service';
|
||||
|
||||
import moment from 'moment';
|
||||
import * as Joi from 'joi';
|
||||
import * as forge from 'node-forge';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Logger } from 'src/logger';
|
||||
|
||||
import { CounterService } from '../services/counter.service';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { WhitelistType } from 'src/interfaces/survey';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
import { QUESTION_TYPE } from 'src/enums/question';
|
||||
|
||||
const optionQuestionType: Array<string> = [
|
||||
QUESTION_TYPE.RADIO,
|
||||
QUESTION_TYPE.CHECKBOX,
|
||||
QUESTION_TYPE.BINARY_CHOICE,
|
||||
QUESTION_TYPE.VOTE,
|
||||
];
|
||||
|
||||
@ApiTags('surveyResponse')
|
||||
@Controller('/api/surveyResponse')
|
||||
export class SurveyResponseController {
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly counterService: CounterService,
|
||||
private readonly surveyResponseService: SurveyResponseService,
|
||||
private readonly clientEncryptService: ClientEncryptService,
|
||||
private readonly messagePushingTaskService: MessagePushingTaskService,
|
||||
private readonly logger: Logger,
|
||||
private readonly counterService: CounterService,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
private readonly redisService: RedisService,
|
||||
private readonly userService: UserService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
@Post('/createResponse')
|
||||
@HttpCode(200)
|
||||
async createResponse(@Body() reqBody, @Request() req) {
|
||||
async createResponse(@Body() reqBody) {
|
||||
// 检查签名
|
||||
checkSign(reqBody);
|
||||
// 校验参数
|
||||
@ -42,18 +58,26 @@ export class SurveyResponseController {
|
||||
encryptType: Joi.string(),
|
||||
sessionId: Joi.string(),
|
||||
clientTime: Joi.number().required(),
|
||||
difTime: Joi.number(),
|
||||
diffTime: Joi.number(),
|
||||
password: Joi.string().allow(null, ''),
|
||||
whitelist: Joi.string().allow(null, ''),
|
||||
}).validate(reqBody, { allowUnknown: true });
|
||||
|
||||
if (error) {
|
||||
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
|
||||
req,
|
||||
});
|
||||
this.logger.error(`updateMeta_parameter error: ${error.message}`);
|
||||
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
|
||||
const { surveyPath, encryptType, data, sessionId, clientTime, difTime } =
|
||||
value;
|
||||
const {
|
||||
surveyPath,
|
||||
encryptType,
|
||||
data,
|
||||
sessionId,
|
||||
clientTime,
|
||||
diffTime,
|
||||
password,
|
||||
whitelist: whitelistValue,
|
||||
} = value;
|
||||
|
||||
// 查询schema
|
||||
const responseSchema =
|
||||
@ -62,6 +86,50 @@ export class SurveyResponseController {
|
||||
throw new SurveyNotFoundException('该问卷不存在,无法提交');
|
||||
}
|
||||
|
||||
// 白名单的verifyId校验
|
||||
const baseConf = responseSchema.code.baseConf;
|
||||
|
||||
// 密码校验
|
||||
if (baseConf?.passwordSwitch && baseConf.password) {
|
||||
if (baseConf.password !== password) {
|
||||
throw new HttpException(
|
||||
'白名单验证失败',
|
||||
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 名单校验(手机号/邮箱)
|
||||
if (baseConf?.whitelistType === WhitelistType.CUSTOM) {
|
||||
if (!baseConf.whitelist.includes(whitelistValue)) {
|
||||
throw new HttpException(
|
||||
'白名单验证失败',
|
||||
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 团队成员昵称校验
|
||||
if (baseConf?.whitelistType === WhitelistType.MEMBER) {
|
||||
const user = await this.userService.getUserByUsername(whitelistValue);
|
||||
if (!user) {
|
||||
throw new HttpException(
|
||||
'白名单验证失败',
|
||||
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceMember = await this.workspaceMemberService.findAllByUserId(
|
||||
{ userId: user._id.toString() },
|
||||
);
|
||||
if (!workspaceMember.length) {
|
||||
throw new HttpException(
|
||||
'白名单验证失败',
|
||||
EXCEPTION_CODE.WHITELIST_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
// 提交时间限制
|
||||
const begTime = responseSchema.code?.baseConf?.begTime || 0;
|
||||
@ -146,6 +214,7 @@ export class SurveyResponseController {
|
||||
const optionTextAndId = dataList
|
||||
.filter((questionItem) => {
|
||||
return (
|
||||
optionQuestionType.includes(questionItem.type) &&
|
||||
Array.isArray(questionItem.options) &&
|
||||
questionItem.options.length > 0 &&
|
||||
decryptedData[questionItem.field]
|
||||
@ -155,38 +224,77 @@ export class SurveyResponseController {
|
||||
const arr = cur.options.map((optionItem) => ({
|
||||
hash: optionItem.hash,
|
||||
text: optionItem.text,
|
||||
quota: optionItem.quota,
|
||||
}));
|
||||
pre[cur.field] = arr;
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
// 对用户提交的数据进行遍历处理
|
||||
for (const field in decryptedData) {
|
||||
const val = decryptedData[field];
|
||||
const vals = Array.isArray(val) ? val : [val];
|
||||
if (field in optionTextAndId) {
|
||||
// 记录选项的提交数量,用于投票题回显、或者拓展上限限制功能
|
||||
const optionCountData: Record<string, any> =
|
||||
(await this.counterService.get({
|
||||
surveyPath,
|
||||
key: field,
|
||||
type: 'option',
|
||||
})) || { total: 0 };
|
||||
optionCountData.total++;
|
||||
for (const val of vals) {
|
||||
if (!optionCountData[val]) {
|
||||
optionCountData[val] = 1;
|
||||
} else {
|
||||
// 使用redis作为锁,校验选项配额
|
||||
const surveyId = responseSchema.pageId;
|
||||
const lockKey = `locks:optionSelectedCount:${surveyId}`;
|
||||
const lock = await this.redisService.lockResource(lockKey, 1000);
|
||||
this.logger.info(`lockKey: ${lockKey}`);
|
||||
try {
|
||||
const successParams = [];
|
||||
for (const field in decryptedData) {
|
||||
const value = decryptedData[field];
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
if (field in optionTextAndId) {
|
||||
const optionCountData =
|
||||
(await this.counterService.get({
|
||||
key: field,
|
||||
surveyPath,
|
||||
type: 'option',
|
||||
})) || {};
|
||||
|
||||
//遍历选项hash值
|
||||
for (const val of values) {
|
||||
const option = optionTextAndId[field].find(
|
||||
(opt) => opt['hash'] === val,
|
||||
);
|
||||
const quota = parseInt(option['quota']);
|
||||
if (
|
||||
quota &&
|
||||
optionCountData?.[val] &&
|
||||
quota <= optionCountData[val]
|
||||
) {
|
||||
return {
|
||||
code: EXCEPTION_CODE.RESPONSE_OVER_LIMIT,
|
||||
data: {
|
||||
field,
|
||||
optionHash: option.hash,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (!optionCountData[val]) {
|
||||
optionCountData[val] = 0;
|
||||
}
|
||||
optionCountData[val]++;
|
||||
}
|
||||
if (!optionCountData['total']) {
|
||||
optionCountData['total'] = 1;
|
||||
} else {
|
||||
optionCountData['total']++;
|
||||
}
|
||||
successParams.push({
|
||||
key: field,
|
||||
surveyPath,
|
||||
type: 'option',
|
||||
data: optionCountData,
|
||||
});
|
||||
}
|
||||
this.counterService.set({
|
||||
surveyPath,
|
||||
key: field,
|
||||
data: optionCountData,
|
||||
type: 'option',
|
||||
});
|
||||
}
|
||||
// 校验通过后统一更新
|
||||
await Promise.all(
|
||||
successParams.map((item) => this.counterService.set(item)),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
await this.redisService.unlockResource(lock);
|
||||
this.logger.info(`unlockResource: ${lockKey}`);
|
||||
}
|
||||
|
||||
// 入库
|
||||
@ -195,12 +303,11 @@ export class SurveyResponseController {
|
||||
surveyPath: value.surveyPath,
|
||||
data: decryptedData,
|
||||
clientTime,
|
||||
difTime,
|
||||
diffTime,
|
||||
surveyId: responseSchema.pageId,
|
||||
optionTextAndId,
|
||||
});
|
||||
|
||||
const surveyId = responseSchema.pageId;
|
||||
const sendData = getPushingData({
|
||||
surveyResponse,
|
||||
questionList: responseSchema?.code?.dataConf?.dataList || [],
|
||||
|
@ -12,7 +12,7 @@ export class SurveyResponseService {
|
||||
async createSurveyResponse({
|
||||
data,
|
||||
clientTime,
|
||||
difTime,
|
||||
diffTime,
|
||||
surveyId,
|
||||
surveyPath,
|
||||
optionTextAndId,
|
||||
@ -22,7 +22,7 @@ export class SurveyResponseService {
|
||||
data,
|
||||
secretKeys: [],
|
||||
clientTime,
|
||||
difTime,
|
||||
diffTime,
|
||||
pageId: surveyId,
|
||||
optionTextAndId,
|
||||
});
|
||||
|
@ -1,25 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { MessageModule } from '../message/message.module';
|
||||
import { RedisModule } from '../redis/redis.module';
|
||||
|
||||
import { ResponseSchemaService } from './services/responseScheme.service';
|
||||
import { SurveyResponseService } from './services/surveyResponse.service';
|
||||
import { CounterService } from './services/counter.service';
|
||||
import { ClientEncryptService } from './services/clientEncrypt.service';
|
||||
import { RedisService } from '../redis/redis.service';
|
||||
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { Counter } from 'src/models/counter.entity';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
|
||||
import { Logger } from 'src/logger';
|
||||
import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
|
||||
import { ClientEncryptController } from './controllers/clientEncrpt.controller';
|
||||
import { CounterController } from './controllers/counter.controller';
|
||||
import { ResponseSchemaController } from './controllers/responseSchema.controller';
|
||||
import { SurveyResponseController } from './controllers/surveyResponse.controller';
|
||||
import { SurveyResponseUIController } from './controllers/surveyResponseUI.controller';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -31,6 +35,9 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr
|
||||
]),
|
||||
ConfigModule,
|
||||
MessageModule,
|
||||
RedisModule,
|
||||
AuthModule,
|
||||
WorkspaceModule,
|
||||
],
|
||||
controllers: [
|
||||
ClientEncryptController,
|
||||
@ -44,7 +51,8 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr
|
||||
SurveyResponseService,
|
||||
CounterService,
|
||||
ClientEncryptService,
|
||||
Logger,
|
||||
LoggerProvider,
|
||||
RedisService,
|
||||
],
|
||||
exports: [
|
||||
ResponseSchemaService,
|
||||
|
@ -10,7 +10,7 @@ import { Workspace } from 'src/models/workspace.entity';
|
||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { User } from 'src/models/user.entity';
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
@ -32,8 +32,10 @@ describe('WorkspaceController', () => {
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
findAllById: jest.fn(),
|
||||
findAllByIdWithPagination: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
findAllByUserId: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -45,6 +47,7 @@ describe('WorkspaceController', () => {
|
||||
batchUpdate: jest.fn(),
|
||||
batchDelete: jest.fn(),
|
||||
countByWorkspaceId: jest.fn(),
|
||||
batchSearchByWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -62,7 +65,7 @@ describe('WorkspaceController', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
provide: XiaojuSurveyLogger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
@ -145,20 +148,30 @@ describe('WorkspaceController', () => {
|
||||
jest
|
||||
.spyOn(workspaceMemberService, 'findAllByUserId')
|
||||
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
|
||||
|
||||
jest
|
||||
.spyOn(workspaceService, 'findAllById')
|
||||
.mockResolvedValue(workspaces as Array<Workspace>);
|
||||
.spyOn(workspaceService, 'findAllByIdWithPagination')
|
||||
.mockResolvedValue({
|
||||
list: workspaces as Array<Workspace>,
|
||||
count: workspaces.length,
|
||||
});
|
||||
|
||||
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([]);
|
||||
|
||||
const result = await controller.findAll(req);
|
||||
const result = await controller.findAll(req, {
|
||||
curPage: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
expect(result.code).toEqual(200);
|
||||
expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({
|
||||
userId: req.user._id.toString(),
|
||||
});
|
||||
expect(workspaceService.findAllById).toHaveBeenCalledWith({
|
||||
expect(workspaceService.findAllByIdWithPagination).toHaveBeenCalledWith({
|
||||
workspaceIdList: memberList.map((item) => item.workspaceId),
|
||||
page: 1,
|
||||
limit: 10,
|
||||
name: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -226,4 +239,36 @@ describe('WorkspaceController', () => {
|
||||
expect(workspaceService.delete).toHaveBeenCalledWith(id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkspaceAndMember', () => {
|
||||
it('should return a list of workspaces and members for the user', async () => {
|
||||
const req = { user: { _id: new ObjectId() } };
|
||||
|
||||
const workspaceId = new ObjectId();
|
||||
const userId = new ObjectId();
|
||||
const memberList = [{ workspaceId, userId: userId }];
|
||||
const workspaces = [{ _id: workspaceId, name: 'Test Workspace' }];
|
||||
const userList = [{ _id: userId, username: 'Test User' }];
|
||||
|
||||
jest
|
||||
.spyOn(workspaceService, 'findAllByUserId')
|
||||
.mockResolvedValue(workspaces as Array<Workspace>);
|
||||
jest
|
||||
.spyOn(workspaceMemberService, 'batchSearchByWorkspace')
|
||||
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
|
||||
jest
|
||||
.spyOn(userService, 'getUserListByIds')
|
||||
.mockResolvedValue(userList as User[]);
|
||||
|
||||
const result = await controller.getWorkspaceAndMember(req);
|
||||
|
||||
expect(result.code).toEqual(200);
|
||||
expect(workspaceService.findAllByUserId).toHaveBeenCalledWith(
|
||||
req.user._id.toString(),
|
||||
);
|
||||
expect(
|
||||
workspaceMemberService.batchSearchByWorkspace,
|
||||
).toHaveBeenCalledWith(workspaces.map((item) => item._id.toString()));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -123,4 +123,25 @@ describe('WorkspaceService', () => {
|
||||
expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAllByUserId', () => {
|
||||
it('should return all workspaces under a user', async () => {
|
||||
const workspaceIdList = [
|
||||
new ObjectId().toString(),
|
||||
new ObjectId().toString(),
|
||||
];
|
||||
const workspaces = [
|
||||
{ _id: workspaceIdList[0], name: 'Workspace 1' },
|
||||
{ _id: workspaceIdList[1], name: 'Workspace 2' },
|
||||
];
|
||||
|
||||
jest
|
||||
.spyOn(workspaceRepository, 'find')
|
||||
.mockResolvedValue(workspaces as any);
|
||||
|
||||
const result = await service.findAllByUserId('');
|
||||
expect(result).toEqual(workspaces);
|
||||
expect(workspaceRepository.find).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -193,4 +193,21 @@ describe('WorkspaceMemberService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchSearchByWorkspace', () => {
|
||||
it('should return all workspace members by workspace id list', async () => {
|
||||
const workspaceList = ['workspaceId1', 'workspaceId2'];
|
||||
const members = [
|
||||
{ userId: 'userId1', workspaceId: workspaceList[0] },
|
||||
{ userId: 'userId2', workspaceId: workspaceList[1] },
|
||||
];
|
||||
|
||||
jest.spyOn(repository, 'find').mockResolvedValue(members as any);
|
||||
|
||||
const result = await service.batchSearchByWorkspace(workspaceList);
|
||||
|
||||
expect(result).toEqual(members);
|
||||
expect(repository.find).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
Request,
|
||||
SetMetadata,
|
||||
HttpCode,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import moment from 'moment';
|
||||
@ -30,7 +31,10 @@ import {
|
||||
import { splitMembers } from '../utils/splitMember';
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { XiaojuSurveyLogger } from 'src/logger';
|
||||
import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto';
|
||||
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
|
||||
import { Workspace } from 'src/models/workspace.entity';
|
||||
|
||||
@ApiTags('workspace')
|
||||
@ApiBearerAuth()
|
||||
@ -42,7 +46,7 @@ export class WorkspaceController {
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
private readonly userService: UserService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly logger: Logger,
|
||||
private readonly logger: XiaojuSurveyLogger,
|
||||
) {}
|
||||
|
||||
@Get('getRoleList')
|
||||
@ -60,10 +64,7 @@ export class WorkspaceController {
|
||||
async create(@Body() workspace: CreateWorkspaceDto, @Request() req) {
|
||||
const { value, error } = CreateWorkspaceDto.validate(workspace);
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`CreateWorkspaceDto validate failed: ${error.message}`,
|
||||
{ req },
|
||||
);
|
||||
this.logger.error(`CreateWorkspaceDto validate failed: ${error.message}`);
|
||||
throw new HttpException(
|
||||
`参数错误: 请联系管理员`,
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
@ -128,8 +129,20 @@ export class WorkspaceController {
|
||||
|
||||
@Get()
|
||||
@HttpCode(200)
|
||||
async findAll(@Request() req) {
|
||||
async findAll(@Request() req, @Query() queryInfo: GetWorkspaceListDto) {
|
||||
const { value, error } = GetWorkspaceListDto.validate(queryInfo);
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`GetWorkspaceListDto validate failed: ${error.message}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
`参数错误: 请联系管理员`,
|
||||
EXCEPTION_CODE.PARAMETER_ERROR,
|
||||
);
|
||||
}
|
||||
const userId = req.user._id.toString();
|
||||
const curPage = Number(value.curPage);
|
||||
const pageSize = Number(value.pageSize);
|
||||
// 查询当前用户参与的空间
|
||||
const workspaceInfoList = await this.workspaceMemberService.findAllByUserId(
|
||||
{ userId },
|
||||
@ -139,9 +152,16 @@ export class WorkspaceController {
|
||||
pre[cur.workspaceId] = cur;
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
// 查询当前用户的空间列表
|
||||
const list = await this.workspaceService.findAllById({ workspaceIdList });
|
||||
const ownerIdList = list.map((item) => item.ownerId);
|
||||
const { list, count } =
|
||||
await this.workspaceService.findAllByIdWithPagination({
|
||||
workspaceIdList,
|
||||
page: curPage,
|
||||
limit: pageSize,
|
||||
name: queryInfo.name,
|
||||
});
|
||||
const ownerIdList = list.map((item: { ownerId: any }) => item.ownerId);
|
||||
const userList = await this.userService.getUserListByIds({
|
||||
idList: ownerIdList,
|
||||
});
|
||||
@ -150,6 +170,7 @@ export class WorkspaceController {
|
||||
pre[id] = cur;
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
const surveyTotalList = await Promise.all(
|
||||
workspaceIdList.map((item) => {
|
||||
return this.surveyMetaService.countSurveyMetaByWorkspaceId({
|
||||
@ -193,6 +214,7 @@ export class WorkspaceController {
|
||||
memberTotal: memberTotalMap[workspaceId] || 0,
|
||||
};
|
||||
}),
|
||||
count,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -326,4 +348,53 @@ export class WorkspaceController {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/member/list')
|
||||
@HttpCode(200)
|
||||
async getWorkspaceAndMember(@Request() req) {
|
||||
const userId = req.user._id.toString();
|
||||
|
||||
// 所在所有空间
|
||||
const workspaceList = await this.workspaceService.findAllByUserId(userId);
|
||||
if (!workspaceList.length) {
|
||||
return {
|
||||
code: 200,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 所有空间下的所有成员
|
||||
const workspaceMemberList =
|
||||
await this.workspaceMemberService.batchSearchByWorkspace(
|
||||
workspaceList.map((item) => item._id.toString()),
|
||||
);
|
||||
|
||||
// 查询成员姓名
|
||||
const userList = await this.userService.getUserListByIds({
|
||||
idList: workspaceMemberList.map((member) => member.userId),
|
||||
});
|
||||
const userInfoMap = userList.reduce((pre, cur) => {
|
||||
const id = cur._id.toString();
|
||||
pre[id] = cur;
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
const temp: Record<string, WorkspaceMember[]> = {};
|
||||
const list = workspaceList.map(
|
||||
(item: Workspace & { members: WorkspaceMember[] }) => {
|
||||
temp[item._id.toString()] = item.members = [];
|
||||
return item;
|
||||
},
|
||||
);
|
||||
|
||||
workspaceMemberList.forEach((member: WorkspaceMember) => {
|
||||
(member as any).username = userInfoMap[member.userId.toString()].username;
|
||||
temp[member.workspaceId.toString()].push(member);
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: list,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
21
server/src/modules/workspace/dto/getWorkspaceList.dto.ts
Normal file
21
server/src/modules/workspace/dto/getWorkspaceList.dto.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
|
||||
export class GetWorkspaceListDto {
|
||||
@ApiProperty({ description: '当前页码', required: true })
|
||||
curPage: number;
|
||||
|
||||
@ApiProperty({ description: '分页', required: false })
|
||||
pageSize: number;
|
||||
|
||||
@ApiProperty({ description: '空间名称', required: false })
|
||||
name?: string;
|
||||
|
||||
static validate(data: Partial<GetWorkspaceListDto>): Joi.ValidationResult {
|
||||
return Joi.object({
|
||||
curPage: Joi.number().required(),
|
||||
pageSize: Joi.number().allow(null).default(10),
|
||||
name: Joi.string().allow(null, '').optional(),
|
||||
}).validate(data);
|
||||
}
|
||||
}
|
@ -8,6 +8,17 @@ import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
interface FindAllByIdWithPaginationParams {
|
||||
workspaceIdList: string[];
|
||||
page: number;
|
||||
limit: number;
|
||||
name?: string;
|
||||
}
|
||||
interface FindAllByIdWithPaginationResult {
|
||||
list: Workspace[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceService {
|
||||
constructor(
|
||||
@ -41,15 +52,17 @@ export class WorkspaceService {
|
||||
}: {
|
||||
workspaceIdList: string[];
|
||||
}): Promise<Workspace[]> {
|
||||
return this.workspaceRepository.find({
|
||||
where: {
|
||||
_id: {
|
||||
$in: workspaceIdList.map((item) => new ObjectId(item)),
|
||||
},
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
const query = {
|
||||
_id: {
|
||||
$in: workspaceIdList.map((item) => new ObjectId(item)),
|
||||
},
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
};
|
||||
|
||||
return this.workspaceRepository.find({
|
||||
where: query,
|
||||
order: {
|
||||
_id: -1,
|
||||
},
|
||||
@ -64,6 +77,38 @@ export class WorkspaceService {
|
||||
});
|
||||
}
|
||||
|
||||
async findAllByIdWithPagination({
|
||||
workspaceIdList,
|
||||
page,
|
||||
limit,
|
||||
name,
|
||||
}: FindAllByIdWithPaginationParams): Promise<FindAllByIdWithPaginationResult> {
|
||||
const skip = (page - 1) * limit;
|
||||
if (!Array.isArray(workspaceIdList) || workspaceIdList.length === 0) {
|
||||
return { list: [], count: 0 };
|
||||
}
|
||||
const query = {
|
||||
_id: {
|
||||
$in: workspaceIdList.map((m) => new ObjectId(m)),
|
||||
},
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
};
|
||||
if (name) {
|
||||
query['name'] = { $regex: name, $options: 'i' };
|
||||
}
|
||||
const [data, count] = await this.workspaceRepository.findAndCount({
|
||||
where: query,
|
||||
skip,
|
||||
take: limit,
|
||||
order: {
|
||||
createDate: -1,
|
||||
},
|
||||
});
|
||||
return { list: data, count };
|
||||
}
|
||||
|
||||
update(id: string, workspace: Partial<Workspace>) {
|
||||
return this.workspaceRepository.update(id, workspace);
|
||||
}
|
||||
@ -104,4 +149,27 @@ export class WorkspaceService {
|
||||
surveyRes,
|
||||
};
|
||||
}
|
||||
|
||||
// 用户下的所有空间
|
||||
async findAllByUserId(userId: string) {
|
||||
return await this.workspaceRepository.find({
|
||||
where: {
|
||||
ownerId: userId,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
_id: -1,
|
||||
},
|
||||
select: [
|
||||
'_id',
|
||||
'curStatus',
|
||||
'name',
|
||||
'description',
|
||||
'ownerId',
|
||||
'createDate',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -140,4 +140,19 @@ export class WorkspaceMemberService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 根据空间id批量查询成员
|
||||
async batchSearchByWorkspace(workspaceList: string[]) {
|
||||
return await this.workspaceMemberRepository.find({
|
||||
where: {
|
||||
workspaceId: {
|
||||
$in: workspaceList,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
_id: -1,
|
||||
},
|
||||
select: ['_id', 'userId', 'role', 'workspaceId'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ describe('getPushingData', () => {
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: 1711025113146,
|
||||
},
|
||||
difTime: 30518,
|
||||
diffTime: 30518,
|
||||
data: {
|
||||
data458: '15000000000',
|
||||
data515: '115019',
|
||||
|
53
server/src/utils/xss.ts
Normal file
53
server/src/utils/xss.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import xss from 'xss';
|
||||
|
||||
const myxss = new (xss as any).FilterXSS({
|
||||
onIgnoreTagAttr(tag, name, value) {
|
||||
if (name === 'style' || name === 'class') {
|
||||
return `${name}="${value}"`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
onIgnoreTag(tag, html) {
|
||||
// <xxx>过滤为空,否则不过滤为空
|
||||
const re1 = new RegExp('<.+?>', 'g');
|
||||
if (re1.test(html)) {
|
||||
return '';
|
||||
} else {
|
||||
return html;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const cleanRichTextWithMediaTag = (text) => {
|
||||
if (!text) {
|
||||
return text === 0 ? 0 : '';
|
||||
}
|
||||
const html = transformHtmlTag(text)
|
||||
.replace(/<img([\w\W]+?)\/>/g, '[图片]')
|
||||
.replace(/<video.*\/video>/g, '[视频]');
|
||||
const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, '');
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export function escapeHtml(html) {
|
||||
return html.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
export const transformHtmlTag = (html) => {
|
||||
if (!html) return '';
|
||||
if (typeof html !== 'string') return html + '';
|
||||
return html
|
||||
.replace(html ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\\\n/g, '\\n');
|
||||
//.replace(/ /g, "")
|
||||
};
|
||||
|
||||
const filterXSSClone = myxss.process.bind(myxss);
|
||||
|
||||
export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html));
|
||||
|
||||
export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html));
|
2
web/.gitignore
vendored
2
web/.gitignore
vendored
@ -8,6 +8,8 @@ node_modules
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
components.d.ts
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
73
web/components.d.ts
vendored
73
web/components.d.ts
vendored
@ -1,73 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
IEpBottom: typeof import('~icons/ep/bottom')['default']
|
||||
IEpCheck: typeof import('~icons/ep/check')['default']
|
||||
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
|
||||
IEpClose: typeof import('~icons/ep/close')['default']
|
||||
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
|
||||
IEpDelete: typeof import('~icons/ep/delete')['default']
|
||||
IEpIphone: typeof import('~icons/ep/iphone')['default']
|
||||
IEpLoading: typeof import('~icons/ep/loading')['default']
|
||||
IEpMinus: typeof import('~icons/ep/minus')['default']
|
||||
IEpMonitor: typeof import('~icons/ep/monitor')['default']
|
||||
IEpMore: typeof import('~icons/ep/more')['default']
|
||||
IEpPlus: typeof import('~icons/ep/plus')['default']
|
||||
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
|
||||
IEpRank: typeof import('~icons/ep/rank')['default']
|
||||
IEpRemove: typeof import('~icons/ep/remove')['default']
|
||||
IEpSearch: typeof import('~icons/ep/search')['default']
|
||||
IEpSort: typeof import('~icons/ep/sort')['default']
|
||||
IEpSortDown: typeof import('~icons/ep/sort-down')['default']
|
||||
IEpSortUp: typeof import('~icons/ep/sort-up')['default']
|
||||
IEpTop: typeof import('~icons/ep/top')['default']
|
||||
IEpView: typeof import('~icons/ep/view')['default']
|
||||
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
7
web/env.d.ts
vendored
7
web/env.d.ts
vendored
@ -1,8 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module "vuex" {
|
||||
export * from "vuex/types/index.d.ts";
|
||||
export * from "vuex/types/helpers.d.ts";
|
||||
export * from "vuex/types/logger.d.ts";
|
||||
export * from "vuex/types/vue.d.ts";
|
||||
}
|
||||
|
@ -11,10 +11,11 @@
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/materials/setters/widgets/QuotaConfig.vue"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@logicflow/core": "2.0.0",
|
||||
"@logicflow/extension": "2.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"async-validator": "^4.2.5",
|
||||
@ -22,17 +23,17 @@
|
||||
"clipboard": "^2.0.11",
|
||||
"crypto-js": "^4.2.0",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.7.0",
|
||||
"element-plus": "^2.8.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-forge": "^1.3.1",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode": "^1.5.3",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuex": "^4.0.2",
|
||||
"xss": "^1.0.14",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
@ -40,8 +41,10 @@
|
||||
"@iconify-json/ep": "^1.1.15",
|
||||
"@rushstack/eslint-patch": "^1.10.2",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
@ -52,7 +55,7 @@
|
||||
"husky": "^9.0.11",
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.72.0",
|
||||
"sass": "1.77.6",
|
||||
"typescript": "~5.3.0",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
|
@ -23,31 +23,69 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, shallowRef, onBeforeMount, watch, computed } from 'vue'
|
||||
import { get as _get } from 'lodash-es'
|
||||
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import './styles/reset-wangeditor.scss'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { ref, shallowRef, onBeforeMount, watch } from 'vue'
|
||||
|
||||
const emit = defineEmits(['input', 'onFocus', 'change', 'blur'])
|
||||
import { useUserStore } from '@/management/stores/user'
|
||||
import { replacePxWithRem } from './utils'
|
||||
|
||||
const emit = defineEmits(['input', 'onFocus', 'change', 'blur', 'created'])
|
||||
const model = defineModel()
|
||||
const props = defineProps(['staticToolBar'])
|
||||
const props = defineProps({
|
||||
staticToolBar: { default: false, required: false },
|
||||
needUploadImage: { default: false, required: false }
|
||||
})
|
||||
|
||||
const curValue = ref('')
|
||||
const editorRef = shallowRef()
|
||||
const showToolbar = ref(props.staticToolBar || false)
|
||||
const showToolbar = ref(props.staticToolBar)
|
||||
|
||||
const mode = 'simple'
|
||||
|
||||
const toolbarConfig = {
|
||||
toolbarKeys: [
|
||||
'color', // 字体色
|
||||
'bgColor', // 背景色
|
||||
'bold',
|
||||
'insertLink' // 链接
|
||||
]
|
||||
const toolbarConfig = computed(() => {
|
||||
const config = {
|
||||
toolbarKeys: [
|
||||
'color', // 字体色
|
||||
'bgColor', // 背景色
|
||||
'bold',
|
||||
'insertLink' // 链接
|
||||
]
|
||||
}
|
||||
if (props.needUploadImage) {
|
||||
config.toolbarKeys.push('uploadImage')
|
||||
}
|
||||
|
||||
return config
|
||||
})
|
||||
|
||||
const editorConfig = {
|
||||
MENU_CONF: {}
|
||||
}
|
||||
|
||||
const editorConfig = {}
|
||||
const userStore = useUserStore()
|
||||
const token = _get(userStore, 'userInfo.token')
|
||||
|
||||
// 图片
|
||||
editorConfig.MENU_CONF['uploadImage'] = {
|
||||
allowedFileTypes: ['image/jpeg', 'image/png'],
|
||||
server: '/api/file/upload',
|
||||
fieldName: 'file',
|
||||
meta: {
|
||||
//! 此处的channel需要跟上传接口内配置的channel一致
|
||||
channel: 'upload'
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
customInsert(res, insertFn) {
|
||||
const url = res.data.url
|
||||
insertFn(url, '', '')
|
||||
}
|
||||
}
|
||||
|
||||
const setHtml = (newHtml) => {
|
||||
const editor = editorRef.value
|
||||
@ -60,9 +98,11 @@ const onCreated = (editor) => {
|
||||
if (model.value) {
|
||||
setHtml(model.value)
|
||||
}
|
||||
emit('created', editor)
|
||||
}
|
||||
const onChange = (editor) => {
|
||||
const editorHtml = editor.getHtml()
|
||||
const editorHtml = replacePxWithRem(editor.getHtml())
|
||||
|
||||
curValue.value = editorHtml // 记录当前 html 内容
|
||||
emit('input', editorHtml) // 用于自定义 v-model
|
||||
}
|
||||
@ -113,6 +153,7 @@ onBeforeMount(() => {
|
||||
.static-toolbar {
|
||||
border-bottom: 1px solid #dedede;
|
||||
}
|
||||
|
||||
.dynamic-toolbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
28
web/src/common/Editor/utils.js
Normal file
28
web/src/common/Editor/utils.js
Normal file
@ -0,0 +1,28 @@
|
||||
// px 转换为 rem
|
||||
const pxToRem = (px) => {
|
||||
return `${(parseFloat(px) / 50).toFixed(2)}rem`
|
||||
}
|
||||
|
||||
// 图片style的宽高改成rem
|
||||
export const replacePxWithRem = (html) => {
|
||||
const imgRegex = /<img[^>]*style=["'][^"']*\b(?:width|height):\s*\d+(\.\d+)?px[^"']*["'][^>]*>/gi
|
||||
const styleRegex = /style="([^"]*)"/g
|
||||
if (!imgRegex.test(html)) {
|
||||
return html
|
||||
}
|
||||
|
||||
const res = html.replaceAll(imgRegex, (imgHtml) => {
|
||||
return imgHtml.replace(styleRegex, (match, content) => {
|
||||
let styleContent = content
|
||||
const pxRegex = /(width|height):\s*(\d+(\.\d+)?)px/g
|
||||
|
||||
styleContent = styleContent.replace(pxRegex, (pxMatch, prop, value) => {
|
||||
return `${prop}: ${pxToRem(value)}`
|
||||
})
|
||||
|
||||
return `style="${styleContent}"`
|
||||
})
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
@ -33,8 +33,8 @@ export class RuleNode {
|
||||
conditions: ConditionNode[] = []
|
||||
scope: string = Scope.Question
|
||||
target: string = ''
|
||||
constructor(scope: string = Scope.Question, target: string = '') {
|
||||
this.id = generateID(PrefixID.Rule)
|
||||
constructor(target: string = '', scope: string = Scope.Question, id?: string) {
|
||||
this.id = id || generateID(PrefixID.Rule)
|
||||
this.scope = scope
|
||||
this.target = target
|
||||
}
|
||||
@ -54,14 +54,8 @@ export class RuleNode {
|
||||
|
||||
export class RuleBuild {
|
||||
rules: RuleNode[] = []
|
||||
static instance: RuleBuild
|
||||
constructor() {
|
||||
this.rules = []
|
||||
if (!RuleBuild.instance) {
|
||||
RuleBuild.instance = this
|
||||
}
|
||||
|
||||
return RuleBuild.instance
|
||||
}
|
||||
|
||||
// 添加条件规则到规则引擎中
|
||||
@ -71,6 +65,9 @@ export class RuleBuild {
|
||||
removeRule(ruleId: string) {
|
||||
this.rules = this.rules.filter((rule) => rule.id !== ruleId)
|
||||
}
|
||||
clear() {
|
||||
this.rules = []
|
||||
}
|
||||
findRule(ruleId: string) {
|
||||
return this.rules.find((rule) => rule.id === ruleId)
|
||||
}
|
||||
@ -94,7 +91,7 @@ export class RuleBuild {
|
||||
if (ruleConf instanceof Array) {
|
||||
ruleConf.forEach((rule: any) => {
|
||||
const { scope, target } = rule
|
||||
const ruleNode = new RuleNode(scope, target)
|
||||
const ruleNode = new RuleNode(target, scope)
|
||||
rule.conditions.forEach((condition: any) => {
|
||||
const { field, operator, value } = condition
|
||||
const conditionNode = new ConditionNode(field, operator, value)
|
||||
@ -112,19 +109,19 @@ export class RuleBuild {
|
||||
findTargetsByScope(scope: string) {
|
||||
return this.rules.filter((rule) => rule.scope === scope).map((rule) => rule.target)
|
||||
}
|
||||
// 实现前置题删除校验
|
||||
findTargetsByFields(field: string) {
|
||||
const nodes = this.rules.filter((rule: RuleNode) => {
|
||||
const conditions = rule.conditions.filter((item: any) => {
|
||||
return item.field === field
|
||||
})
|
||||
return conditions.length > 0
|
||||
findRulesByField(field: string) {
|
||||
return this.rules.filter((rule) => {
|
||||
return rule.conditions.filter((condition) => condition.field === field).length
|
||||
})
|
||||
}
|
||||
// 实现前置题删除校验
|
||||
findTargetsByField(field: string) {
|
||||
const nodes = this.findRulesByField(field)
|
||||
return nodes.map((item: any) => {
|
||||
return item.target
|
||||
})
|
||||
}
|
||||
// 根据目标题获取显示逻辑
|
||||
// 根据目标题获取关联的逻辑条件
|
||||
findConditionByTarget(target: string) {
|
||||
return this.rules.filter((rule) => rule.target === target).map((item) => item.conditions)
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Operator, type FieldTypes, type Fact } from './BasicType'
|
||||
// 定义条件规则类
|
||||
export class ConditionNode<F extends string, O extends Operator> {
|
||||
// 默认显示
|
||||
public result: boolean = false
|
||||
public result: boolean | undefined = undefined
|
||||
constructor(
|
||||
public field: F,
|
||||
public operator: O,
|
||||
@ -16,7 +16,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
||||
return this.field + this.operator + this.value
|
||||
}
|
||||
|
||||
match(facts: Fact): boolean {
|
||||
match(facts: Fact): boolean | undefined {
|
||||
// console.log(this.calculateHash())
|
||||
// 如果该特征在事实对象中不存在,则直接返回false
|
||||
if (!facts[this.field]) {
|
||||
@ -45,7 +45,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
||||
this.result = this.value.some((v) => !facts[this.field].includes(v))
|
||||
return this.result
|
||||
} else {
|
||||
this.result = facts[this.field].includes(this.value)
|
||||
this.result = !facts[this.field].includes(this.value)
|
||||
return this.result
|
||||
}
|
||||
case Operator.NotEqual:
|
||||
@ -53,7 +53,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
||||
this.result = this.value.every((v) => !facts[this.field].includes(v))
|
||||
return this.result
|
||||
} else {
|
||||
this.result = facts[this.field].includes(this.value)
|
||||
this.result = facts[this.field].toString() !== this.value
|
||||
return this.result
|
||||
}
|
||||
// 其他比较操作符的判断逻辑
|
||||
@ -69,7 +69,7 @@ export class ConditionNode<F extends string, O extends Operator> {
|
||||
|
||||
export class RuleNode {
|
||||
conditions: Map<string, ConditionNode<string, Operator>> // 使用哈希表存储条件规则对象
|
||||
public result: boolean = false
|
||||
public result: boolean | undefined
|
||||
constructor(
|
||||
public target: string,
|
||||
public scope: string
|
||||
@ -83,15 +83,28 @@ export class RuleNode {
|
||||
}
|
||||
|
||||
// 匹配条件规则
|
||||
match(fact: Fact) {
|
||||
const res = Array.from(this.conditions.entries()).every(([, value]) => {
|
||||
const res = value.match(fact)
|
||||
if (res) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
match(fact: Fact, comparor?: any) {
|
||||
let res: boolean | undefined = undefined
|
||||
if (comparor === 'or') {
|
||||
res = Array.from(this.conditions.entries()).some(([, value]) => {
|
||||
const res = value.match(fact)
|
||||
if (res) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
} else {
|
||||
res = Array.from(this.conditions.entries()).every(([, value]) => {
|
||||
const res = value.match(fact)
|
||||
if (res) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.result = res
|
||||
return res
|
||||
}
|
||||
@ -121,14 +134,14 @@ export class RuleNode {
|
||||
|
||||
export class RuleMatch {
|
||||
rules: Map<string, RuleNode>
|
||||
static instance: any
|
||||
// static instance: any
|
||||
constructor() {
|
||||
this.rules = new Map()
|
||||
if (!RuleMatch.instance) {
|
||||
RuleMatch.instance = this
|
||||
}
|
||||
// if (!RuleMatch.instance) {
|
||||
// RuleMatch.instance = this
|
||||
// }
|
||||
|
||||
return RuleMatch.instance
|
||||
// return RuleMatch.instance
|
||||
}
|
||||
fromJson(ruleConf: any) {
|
||||
if (ruleConf instanceof Array) {
|
||||
@ -145,6 +158,7 @@ export class RuleMatch {
|
||||
this.addRule(ruleNode)
|
||||
})
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
// 添加条件规则到规则引擎中
|
||||
@ -160,22 +174,31 @@ export class RuleMatch {
|
||||
this.rules.set(hash, rule)
|
||||
}
|
||||
|
||||
// 匹配条件规则
|
||||
match(target: string, scope: string, fact: Fact) {
|
||||
// 特定目标题规则匹配
|
||||
match(target: string, scope: string, fact: Fact, comparor?: any) {
|
||||
const hash = this.calculateHash(target, scope)
|
||||
|
||||
const rule = this.rules.get(hash)
|
||||
if (rule) {
|
||||
const result = rule.match(fact)
|
||||
// this.matchCache.set(hash, result);
|
||||
const result = rule.match(fact, comparor)
|
||||
return result
|
||||
} else {
|
||||
// 默认显示
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
getResult(target: string, scope: string) {
|
||||
/* 获取条件题关联的多个目标题匹配情况 */
|
||||
getResultsByField(field: string, fact: Fact) {
|
||||
const rules = this.findRulesByField(field)
|
||||
return rules.map(([, rule]) => {
|
||||
return {
|
||||
target: rule.target,
|
||||
result: this.match(rule.target, 'question', fact, 'or')
|
||||
}
|
||||
})
|
||||
}
|
||||
/* 获取目标题的规则是否匹配 */
|
||||
getResultByTarget(target: string, scope: string) {
|
||||
const hash = this.calculateHash(target, scope)
|
||||
const rule = this.rules.get(hash)
|
||||
if (rule) {
|
||||
@ -191,15 +214,18 @@ export class RuleMatch {
|
||||
// 假设哈希值计算方法为简单的字符串拼接或其他哈希算法
|
||||
return target + scope
|
||||
}
|
||||
findTargetsByField(field: string) {
|
||||
const rules = new Map(
|
||||
[...this.rules.entries()].filter(([, value]) => {
|
||||
return [...value.conditions.entries()].filter(([, value]) => {
|
||||
return value.field === field
|
||||
})
|
||||
// 查找条件题的规则
|
||||
findRulesByField(field: string) {
|
||||
const list = [...this.rules.entries()]
|
||||
const match = list.filter(([, ruleValue]) => {
|
||||
const list = [...ruleValue.conditions.entries()]
|
||||
const res = list.filter(([, conditionValue]) => {
|
||||
const hit = conditionValue.field === field
|
||||
return hit
|
||||
})
|
||||
)
|
||||
return [...rules.values()].map((obj) => obj.target)
|
||||
return res.length
|
||||
})
|
||||
return match
|
||||
}
|
||||
toJson() {
|
||||
return Array.from(this.rules.entries()).map(([, value]) => {
|
||||
|
11
web/src/common/regexpMap.ts
Normal file
11
web/src/common/regexpMap.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export const regexpMap = {
|
||||
nd: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
|
||||
m: /^[1]([3-9])[0-9]{9}$/,
|
||||
idcard: /^(\d{15}$|^\d{18}$|^\d{17}(\d|X|x))$/,
|
||||
strictIdcard:
|
||||
/(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/,
|
||||
n: /^[0-9]+([.]{1}[0-9]+){0,1}$/,
|
||||
e: /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/,
|
||||
licensePlate:
|
||||
/^([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[a-zA-Z](([DFAG]((?![IO])[a-zA-Z0-9](?![IO]))[0-9]{4})|([0-9]{5}[DF]))|[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4,5}[A-Z0-9挂学警港澳]{1})$/
|
||||
}
|
@ -27,6 +27,18 @@ const isVideo = (html) => {
|
||||
return html.indexOf('<video') > -1
|
||||
}
|
||||
|
||||
export const cleanRichTextWithMediaTag = (text) => {
|
||||
if (!text) {
|
||||
return text === 0 ? 0 : ''
|
||||
}
|
||||
const html = transformHtmlTag(text)
|
||||
.replace(/<img([\w\W]+?)\/>/g, '[图片]')
|
||||
.replace(/<video.*\/video>/g, '[视频]')
|
||||
const content = html.replace(/<[^<>]+>/g, '').replace(/ /g, '')
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
export const cleanRichText = (text) => {
|
||||
if (!text) {
|
||||
return text === 0 ? 0 : ''
|
||||
|
@ -1,19 +1,79 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue'
|
||||
import { get as _get } from 'lodash-es'
|
||||
import { useUserStore } from '@/management/stores/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage, type Action } from 'element-plus'
|
||||
|
||||
// 这里不需要自动跳转登录页面,所以单独引入axios
|
||||
import axios from 'axios'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
let timer: any
|
||||
|
||||
const showConfirmBox = () => {
|
||||
ElMessageBox.alert('登录状态已失效,请重新登陆。', '提示', {
|
||||
confirmButtonText: '确认',
|
||||
showClose: false,
|
||||
callback: (action: Action) => {
|
||||
if (action === 'confirm') {
|
||||
userStore.logout();
|
||||
router.replace({ name: 'login' });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const token = _get(userStore, 'userInfo.token')
|
||||
|
||||
const res = await axios({
|
||||
url: '/api/user/getUserInfo',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (res.data.code !== 200) {
|
||||
showConfirmBox();
|
||||
} else {
|
||||
timer = setTimeout(() => {
|
||||
checkAuth()
|
||||
}, 30 * 60 * 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
const e = error as any
|
||||
ElMessage.error(e.message)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
watch(() => userStore.hasLogined, (hasLogined) => {
|
||||
if (hasLogined) {
|
||||
timer = setTimeout(() => {
|
||||
checkAuth()
|
||||
}, 30 * 60 * 1000);
|
||||
} else {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import url('./styles/icon.scss');
|
||||
@import url('../materials/questions/common/css/icon.scss');
|
||||
@import url('./styles/reset.scss');
|
||||
@import url('./styles/common.scss');
|
||||
|
||||
html {
|
||||
font-size: 50px;
|
||||
|
@ -16,3 +16,4 @@ export const getStatisticList = (data) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -7,3 +7,15 @@ export const register = (data) => {
|
||||
export const login = (data) => {
|
||||
return axios.post('/auth/login', data)
|
||||
}
|
||||
|
||||
export const getUserInfo = () => {
|
||||
return axios.get('/user/getUserInfo')
|
||||
}
|
||||
/** 获取密码强度 */
|
||||
export const getPasswordStrength = (password) => {
|
||||
return axios.get('/auth/register/password/strength', {
|
||||
params: {
|
||||
password
|
||||
}
|
||||
})
|
||||
}
|
||||
|
29
web/src/management/api/downloadTask.js
Normal file
29
web/src/management/api/downloadTask.js
Normal file
@ -0,0 +1,29 @@
|
||||
import axios from './base'
|
||||
|
||||
export const createDownloadSurveyResponseTask = ({ surveyId, isDesensitive }) => {
|
||||
return axios.post('/downloadTask/createTask', {
|
||||
surveyId,
|
||||
isDesensitive
|
||||
})
|
||||
}
|
||||
|
||||
export const getDownloadTask = taskId => {
|
||||
return axios.get('/downloadTask/getDownloadTask', { params: { taskId } })
|
||||
}
|
||||
|
||||
|
||||
export const getDownloadTaskList = ({ pageIndex, pageSize }) => {
|
||||
return axios.get('/downloadTask/getDownloadTaskList', {
|
||||
params: {
|
||||
pageIndex,
|
||||
pageSize
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//问卷删除
|
||||
export const deleteDownloadTask = (taskId) => {
|
||||
return axios.post('/downloadTask/deleteDownloadTask', {
|
||||
taskId,
|
||||
})
|
||||
}
|
@ -8,14 +8,20 @@ export const updateSpace = ({ workspaceId, name, description, members }: any) =>
|
||||
return axios.post(`/workspace/${workspaceId}`, { name, description, members })
|
||||
}
|
||||
|
||||
export const getSpaceList = () => {
|
||||
return axios.get('/workspace')
|
||||
export const getSpaceList = (params: any) => {
|
||||
return axios.get('/workspace', {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getSpaceDetail = (workspaceId: string) => {
|
||||
return axios.get(`/workspace/${workspaceId}`)
|
||||
}
|
||||
|
||||
export const getMemberList = () => {
|
||||
return axios.get('/workspace/member/list')
|
||||
}
|
||||
|
||||
export const deleteSpace = (workspaceId: string) => {
|
||||
return axios.delete(`/workspace/${workspaceId}`)
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ export const getSurveyById = (id) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const saveSurvey = ({ surveyId, configData }) => {
|
||||
return axios.post('/survey/updateConf', { surveyId, configData })
|
||||
export const saveSurvey = ({ surveyId, configData, sessionId }) => {
|
||||
return axios.post('/survey/updateConf', { surveyId, configData, sessionId })
|
||||
}
|
||||
|
||||
export const publishSurvey = ({ surveyId }) => {
|
||||
@ -52,3 +52,11 @@ export const deleteSurvey = (surveyId) => {
|
||||
export const updateSurvey = (data) => {
|
||||
return axios.post('/survey/updateMeta', data)
|
||||
}
|
||||
|
||||
export const getSessionId = ({ surveyId }) => {
|
||||
return axios.post('/session/create', { surveyId })
|
||||
}
|
||||
|
||||
export const seizeSession = ({ sessionId }) => {
|
||||
return axios.post('/session/seize', { sessionId })
|
||||
}
|
@ -43,10 +43,13 @@
|
||||
import { computed, ref, shallowRef, onMounted, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import MemberSelect from './MemberSelect.vue'
|
||||
|
||||
import { getPermissionList, getCollaborator, saveCollaborator } from '@/management/api/space'
|
||||
import { type IMember, SurveyPermissions } from '@/management/utils/types/workSpace'
|
||||
import { CODE_MAP } from '@/management/api/base'
|
||||
|
||||
import MemberSelect from './MemberSelect.vue'
|
||||
|
||||
const emit = defineEmits(['on-close-codify', 'onFocus', 'change', 'blur'])
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -110,9 +113,6 @@ const rules = {
|
||||
{
|
||||
trigger: 'change',
|
||||
validator: (rule: any, value: IMember[], callback: Function) => {
|
||||
if (value.length === 0) {
|
||||
callback('请至少添加一名协作者')
|
||||
}
|
||||
if (value.filter((item: IMember) => !item.role.length).length) {
|
||||
callback('请设置协作者对应权限')
|
||||
}
|
||||
@ -132,7 +132,6 @@ const rules = {
|
||||
const formDisabled = computed(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
const onClose = () => {
|
||||
emit('on-close-codify')
|
||||
}
|
||||
@ -140,7 +139,7 @@ const onConfirm = async () => {
|
||||
ruleForm.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const collaborators = formModel.value.members.map((i: any) => {
|
||||
const collaborators = formModel.value.members.map((i) => {
|
||||
const collaborator = {
|
||||
userId: i.userId,
|
||||
permissions: i.role
|
@ -6,9 +6,12 @@
|
||||
<div
|
||||
:class="[
|
||||
'tab-btn',
|
||||
(['QuestionEditIndex', 'QuestionEditSetting', 'QuestionSkinSetting'].includes(
|
||||
route.name
|
||||
) &&
|
||||
([
|
||||
'QuestionEditIndex',
|
||||
'QuestionEditSetting',
|
||||
'QuestionSkinSetting',
|
||||
'QuestionEditResultConfig'
|
||||
].includes(route.name) &&
|
||||
tab.to.name === 'QuestionEditIndex') ||
|
||||
isActive
|
||||
? 'router-link-active'
|
||||
|
@ -1 +1,2 @@
|
||||
export const DND_GROUP = 'question'
|
||||
export const QUESTION_CATALOG = 'questionCatalog'
|
||||
|
@ -9,7 +9,7 @@ export const spaceListConfig = {
|
||||
name: {
|
||||
title: '空间名称',
|
||||
key: 'name',
|
||||
width: 300
|
||||
width: 200
|
||||
},
|
||||
surveyTotal: {
|
||||
title: '问卷数',
|
||||
@ -82,6 +82,16 @@ export const noListDataConfig = {
|
||||
img: '/imgs/icons/list-empty.webp'
|
||||
}
|
||||
|
||||
export const noSpaceDataConfig = {
|
||||
title: '您还没有创建团队空间',
|
||||
desc: '赶快点击右上角立即创建团队空间吧!',
|
||||
img: '/imgs/icons/list-empty.webp'
|
||||
}
|
||||
export const noSpaceSearchDataConfig = {
|
||||
title: '没有满足该查询条件的团队空间哦',
|
||||
desc: '可以更换条件查询试试',
|
||||
img: '/imgs/icons/list-empty.webp'
|
||||
}
|
||||
export const noSearchDataConfig = {
|
||||
title: '没有满足该查询条件的问卷哦',
|
||||
desc: '可以更换条件查询试试',
|
||||
@ -92,7 +102,7 @@ export const statusMaps = {
|
||||
new: '未发布',
|
||||
editing: '修改中',
|
||||
published: '已发布',
|
||||
removed: '',
|
||||
removed: '已删除',
|
||||
pausing: ''
|
||||
}
|
||||
|
||||
|
@ -41,18 +41,21 @@ export const defaultQuestionConfig = {
|
||||
text: '选项1',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: ''
|
||||
placeholderDesc: '',
|
||||
quota: '0'
|
||||
},
|
||||
{
|
||||
text: '选项2',
|
||||
others: false,
|
||||
othersKey: '',
|
||||
placeholderDesc: ''
|
||||
placeholderDesc: '',
|
||||
quota: '0'
|
||||
}
|
||||
],
|
||||
star: 5,
|
||||
optionOrigin: '',
|
||||
originType: 'selected',
|
||||
innerType: '',
|
||||
matrixOptionsRely: '',
|
||||
numberRange: {
|
||||
min: {
|
||||
@ -73,5 +76,6 @@ export const defaultQuestionConfig = {
|
||||
placeholder: '500',
|
||||
value: 500
|
||||
}
|
||||
}
|
||||
},
|
||||
quotaNoDisplay: false
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user