Compare commits
94 Commits
main
...
feature/st
Author | SHA1 | Date | |
---|---|---|---|
|
ac0216cb64 | ||
|
bcb41c68fb | ||
|
ef2126f1ca | ||
|
1125e2ae39 | ||
|
054095e499 | ||
|
81df0eae05 | ||
|
0a27554bb6 | ||
|
5dfd3d941f | ||
|
60f96386a9 | ||
|
7f832ce885 | ||
|
47ea148866 | ||
|
129a803e9a | ||
|
bc0597efe0 | ||
|
365bea25e1 | ||
|
3d5f04b4a8 | ||
|
f3ebc11b3f | ||
|
d5669352ed | ||
|
0b686ab4ac | ||
|
54a86e1501 | ||
|
cdb8b6532a | ||
|
56c37fce3c | ||
|
43b20b1be6 | ||
|
b749cfa6f6 | ||
|
0b4e1fa13b | ||
|
43001a12c7 | ||
|
d3c2180ac8 | ||
|
63e16e1694 | ||
|
c6cc0d22e5 | ||
|
949a989dcf | ||
|
9427e0efe5 | ||
|
70c236c879 | ||
|
3d31245ae5 | ||
|
98fc21995a | ||
|
f6e3778a2d | ||
|
6775a9df5e | ||
|
3cb843e493 | ||
|
bc3ce31c9e | ||
|
3e7f0cac90 | ||
|
f3b8ab278a | ||
|
013f9ac811 | ||
|
9e07e8330a | ||
|
8950073141 | ||
|
4d580bb789 | ||
|
f73bfb0ab3 | ||
|
8109d350e4 | ||
|
b233023bb3 | ||
|
fd7cc2ea96 | ||
|
724535a735 | ||
|
b5c7ec3008 | ||
|
c5698ad631 | ||
|
6fb337633c | ||
|
42b8d74ead | ||
|
d8e76dc2e6 | ||
|
4f2cd4ca47 | ||
|
82c98ec1f5 | ||
|
fbc654f21b | ||
|
9d89a1ceca | ||
|
517906f77f | ||
|
681e8fa3ae | ||
|
4d5c3eb15d | ||
|
9a35de7e36 | ||
|
c4b730c8af | ||
|
8ea8869ca7 | ||
|
7e5a8ae5c1 | ||
|
cf495b60d1 | ||
|
fb57eaaba7 | ||
|
b494bd6174 | ||
|
310fe0d325 | ||
|
ba418c5cd7 | ||
|
9596cd07a1 | ||
|
2ed5b64b18 | ||
|
492e0055f0 | ||
|
93938702fe | ||
|
e8907ca4fb | ||
|
2b32850046 | ||
|
da1749fb53 | ||
|
5bc5eb8719 | ||
|
3227a799f9 | ||
|
8740685a4d | ||
|
5c3915a74d | ||
|
b1958ec8ff | ||
|
bc39e9933d | ||
|
36dd5a4f2d | ||
|
a101878313 | ||
|
32e43b8260 | ||
|
9afb23c08e | ||
|
eaa1abda82 | ||
|
6431cc3210 | ||
|
122f584cad | ||
|
f45cf7982f | ||
|
2f0736fd95 | ||
|
6c72344204 | ||
|
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
|
133
CODE_OF_CONDUCT.md
Normal file
133
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[xiaojusurvey@gmail.com](mailto:xiaojusurvey@gmail.com).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
22
DATA_COLLECTION.md
Normal file
22
DATA_COLLECTION.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Important Disclosure re:XIAOJUSURVEY Data Collection
|
||||
|
||||
XIAOJUSURVEY is open-source software developed and maintained by XIAOJUSURVEY Team and available at https://github.com/didi/xiaoju-survey.
|
||||
We hereby state the purpose and reason for collecting data.
|
||||
|
||||
## Purpose of data collection
|
||||
|
||||
Data collected is used to help improve XIAOJUSURVEY for all users. It is important that our team understands the usage patterns as soon as possible, so we can best decide how to design future features and prioritize current work.
|
||||
|
||||
## Types of data collected
|
||||
|
||||
XIAOJUSURVEY just collects data about version's information. The data collected is subsequently reported to the XIAOJUSURVEY's backend services.
|
||||
|
||||
All data collected will be used exclusively by the XIAOJUSURVEY team for analytical purposes only. The data will be neither accessible nor sold to any third party.
|
||||
|
||||
## Sensitive data
|
||||
|
||||
XIAOJUSURVEY will never collect and/or report sensitive information, such as private keys, API keys, or passwords.
|
||||
|
||||
## How do I opt-in to or opt-out of data sharing?
|
||||
|
||||
See [docs](https://xiaojusurvey.didi.cn/docs/next/community/%E6%95%B0%E6%8D%AE%E4%B8%8A%E6%8A%A5%E5%A3%B0%E6%98%8E) for information on configuring this functionality.
|
159
README.md
159
README.md
@ -29,35 +29,49 @@
|
||||
|
||||
<br />
|
||||
|
||||
  **XIAOJUSURVEY**是一套轻量、安全的**问卷系统基座**,提供面向个人和企业的一站式产品级解决方案,快速满足各类线上调研场景。
|
||||
  **XIAOJUSURVEY**是一套轻量、安全的调研系统,提供面向个人和企业的一站式产品级解决方案,用于构建各类问卷、考试、测评和复杂表单,快速满足各类线上调研场景。
|
||||
|
||||
  内部系统已沉淀 40+种题型,累积精选模板 100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。
|
||||
|
||||
  开源项目以打造**调研基座**为核心,围绕**平台能力**、**工程架构**、**研发体系**进行建设,大家可以「快速」打造「专属」问卷系统:[快速了解生态发展理念](https://xiaojusurvey.didi.cn/docs/next/community/%E7%94%9F%E6%80%81%E5%BB%BA%E8%AE%BE)
|
||||
# 功能特性
|
||||
|
||||
# 功能简介
|
||||
**🌈 好用**
|
||||
|
||||
- 问卷管理:创、编、投、收、数据分析
|
||||
- 多类型数据采集,轻松创建调研表单。
|
||||
|
||||
- 多样化题型:单行输入框、多行输入框、单项选择、多项选择、判断题、评分、投票、...
|
||||
- 智能逻辑编排,设计多规则动态表单。
|
||||
|
||||
- 用户管理:登录、注册、权限管理
|
||||
- 数据在线分析和导出,洞察调研结果。
|
||||
|
||||
- 数据安全:传输加密、脱敏等
|
||||
**🎨 好看**
|
||||
|
||||
> 更全的建设请查阅 [官方 Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
- 主题自由定制,适配您的品牌。
|
||||
|
||||
- 无缝嵌入各终端,满足不同场景需求。
|
||||
|
||||
**🚀 好扩展**
|
||||
|
||||
- 自定义 Hook 配置,轻松集成多方系统与各类工具。
|
||||
|
||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" />
|
||||
|
||||
_**(个人和企业用户均可快速构建特定领域的调研类解决方案。)**_
|
||||
1、 **全部功能**请查看 [功能介绍](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)。
|
||||
|
||||
2、**企业**和**个人**均可快速构建特定领域的调研类解决方案。
|
||||
|
||||
# 技术
|
||||
|
||||
Web 端:Vue3 + ElementPlus;C 端多端渲染(规划中)
|
||||
**1、Web 端:Vue3 + ElementPlus**
|
||||
|
||||
Server 端:Nestjs + MongoDB;Java(在建,[欢迎加入共建](https://github.com/didi/xiaoju-survey/issues/306))
|
||||
  C 端多端渲染:ReactNative SDK 建设中
|
||||
|
||||
智能化基座:(规划中)
|
||||
**2、Server 端:NestJS + MongoDB**
|
||||
|
||||
  Java 版:建设中,[欢迎加入共建](https://github.com/didi/xiaoju-survey/issues/306)
|
||||
|
||||
**3、能力增强**
|
||||
|
||||
  在线平台:建设中、智能化问卷:规划中
|
||||
|
||||
# 项目优势
|
||||
|
||||
@ -108,128 +122,51 @@ Server 端:Nestjs + MongoDB;Java(在建,[欢迎加入共建](https://git
|
||||
|
||||
前后端分离,提供 Docker 化方案,提供了完善的部署指导手册。
|
||||
|
||||
# 快速启动
|
||||
# 快速使用
|
||||
|
||||
Node 版本 >= 18.x,
|
||||
[查看环境准备指导](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
|
||||
_(在线平台建设中)_
|
||||
|
||||
复制工程
|
||||
# 本地开发
|
||||
|
||||
```shell
|
||||
git clone git@github.com:didi/xiaoju-survey.git
|
||||
```
|
||||
请查看 [本地安装手册](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) 来启动项目。
|
||||
|
||||
## 服务端启动
|
||||
# 快速部署
|
||||
|
||||
### 方案一、快速启动,无需安装数据库
|
||||
### 服务部署
|
||||
|
||||
> _便于快速预览工程,对于正式项目需要使用方案二。_
|
||||
请查看 [快速部署指导](https://xiaojusurvey.didi.cn/docs/next/document/%E5%B7%A5%E7%A8%8B%E9%83%A8%E7%BD%B2/Docker%E9%83%A8%E7%BD%B2) 。
|
||||
|
||||
#### 1、安装依赖
|
||||
### 一键部署
|
||||
|
||||
```shell
|
||||
cd server
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 2、启动
|
||||
|
||||
```shell
|
||||
npm run local
|
||||
```
|
||||
|
||||
> 服务运行依赖 [mongodb-memory-server](https://github.com/nodkz/mongodb-memory-server):
|
||||
>
|
||||
> 1、数据保存在内存中,重启服务会更新数据。<br />2、启动内存服务器新实例时,如果找不到 MongoDB 二进制文件会自动下载,因此首次可能需要一些时间。
|
||||
|
||||
### 方案二、(生产推荐)
|
||||
|
||||
#### 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 配置](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)
|
||||
|
||||
#### 2、安装依赖
|
||||
|
||||
```shell
|
||||
cd server
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 3、启动
|
||||
|
||||
```shell
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 前端启动
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```shell
|
||||
cd web
|
||||
npm install
|
||||
```
|
||||
|
||||
### 启动
|
||||
|
||||
```shell
|
||||
npm run serve
|
||||
```
|
||||
|
||||
## 访问
|
||||
|
||||
### 问卷管理端
|
||||
|
||||
[http://localhost:8080/management](http://localhost:8080)
|
||||
|
||||
### 问卷投放端
|
||||
|
||||
创建并发布问卷。
|
||||
|
||||
[http://localhost:8080/render/:surveyPath](http://localhost:8080/render/:surveyPath)
|
||||
_(手册编写中)_
|
||||
|
||||
<br /><br />
|
||||
|
||||
## 微信交流群(推荐)
|
||||
|
||||
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入。任何问题和合作可以联系小助手:
|
||||
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
||||
|
||||
## QQ 交流群
|
||||
|
||||
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入:
|
||||
|
||||
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="210" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
||||
|
||||
## Star
|
||||
|
||||
开源不易,如果该项目对你有帮助,请 star 一下 ❤️❤️❤️,你的支持是我们最大的动力。
|
||||
|
||||
[![Star History Chart](https://api.star-history.com/svg?repos=didi/xiaoju-survey&type=Date)](https://star-history.com/#didi/xiaoju-survey&Date)
|
||||
|
||||
## 记录
|
||||
## 交流群
|
||||
|
||||
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入。
|
||||
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
|
||||
_任何问题和合作可以联系小助手。_
|
||||
|
||||
## 案例
|
||||
|
||||
如果你使用了该项目,请记录反馈:[我在使用](https://github.com/didi/xiaoju-survey/issues/64),你的支持是我们最大的动力。
|
||||
|
||||
## Future Tasks
|
||||
|
||||
[欢迎了解项目发展和共建](https://github.com/didi/xiaoju-survey/issues/85),你的支持是我们最大的动力。
|
||||
|
||||
## 贡献
|
||||
|
||||
如果你想成为贡献者或者扩展技术栈,请查看:[贡献者指南](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
|
||||
|
||||
关注每周推出的建设:[官方 Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
|
||||
## 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)
|
||||
关注重大项目变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)。
|
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)
|
||||
|
||||
- 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.2.0-slim" # 最新版本:https://hub.docker.com/r/xiaojusurvey/xiaoju-survey/tags
|
||||
container_name: xiaoju-survey
|
||||
restart: always
|
||||
ports:
|
||||
|
@ -52,6 +52,15 @@ http {
|
||||
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;
|
||||
}
|
||||
|
@ -1,7 +1,13 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||
XIAOJU_SURVEY_MONGO_URL=
|
||||
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_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
|
@ -0,0 +1,20 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
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_HTTP_DATA_ENCRYPT_TYPE=rsa
|
||||
|
||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||
XIAOJU_SURVEY_JWT_EXPIRES_IN=8h
|
||||
|
||||
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log
|
||||
|
||||
XIAOJU_SURVEY_REPORT=true
|
3
server/.gitignore
vendored
3
server/.gitignore
vendored
@ -13,6 +13,7 @@ pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
yarn.lock
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@ -38,3 +39,5 @@ lerna-debug.log*
|
||||
!.vscode/extensions.json
|
||||
|
||||
tmp
|
||||
exportfile
|
||||
userUpload
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"name": "xiaoju-survey-server",
|
||||
"version": "1.3.0",
|
||||
"description": "XIAOJUSURVEY的server端",
|
||||
"author": "",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
@ -22,15 +22,17 @@
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/microservices": "^10.4.4",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@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,7 +43,9 @@
|
||||
"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",
|
||||
@ -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",
|
||||
"supertest": "^7.0.0",
|
||||
"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();
|
||||
});
|
||||
}
|
||||
|
||||
|
62
server/scripts/run-report.ts
Normal file
62
server/scripts/run-report.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import fs, { promises as fsa } from 'fs-extra';
|
||||
import path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
interface PackageJson {
|
||||
type?: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
id?: string;
|
||||
msg?: string;
|
||||
}
|
||||
|
||||
const getId = () => {
|
||||
const id = new Date().getTime().toString();
|
||||
process.env.XIAOJU_SURVEY_REPORT_ID = id;
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
const readData = async (directory: string): Promise<PackageJson | null> => {
|
||||
const packageJsonPath = path.join(directory, 'package.json');
|
||||
const id = process.env.XIAOJU_SURVEY_REPORT_ID || getId();
|
||||
try {
|
||||
if (!fs.existsSync(directory)) {
|
||||
return {
|
||||
type: 'server',
|
||||
name: '',
|
||||
version: '',
|
||||
description: '',
|
||||
id,
|
||||
msg: '文件不存在',
|
||||
};
|
||||
}
|
||||
const data = await fsa.readFile(packageJsonPath, 'utf8').catch((e) => e);
|
||||
const { name, version, description } = JSON.parse(data) as PackageJson;
|
||||
return { type: 'server', name, version, description, id };
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
(async (): Promise<void> => {
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
!process.env.XIAOJU_SURVEY_REPORT
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await readData(path.join(process.cwd()));
|
||||
|
||||
// 上报
|
||||
fetch('https://xiaojusurveysrc.didi.cn/reportSourceData', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json, */*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(res),
|
||||
}).catch(() => {});
|
||||
})();
|
@ -14,6 +14,7 @@ import { AuthModule } from './modules/auth/auth.module';
|
||||
import { MessageModule } from './modules/message/message.module';
|
||||
import { FileModule } from './modules/file/file.module';
|
||||
import { WorkspaceModule } from './modules/workspace/workspace.module';
|
||||
import { UpgradeModule } from './modules/upgrade/upgrade.module';
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
@ -35,16 +36,21 @@ import { MessagePushingLog } from './models/messagePushingLog.entity';
|
||||
import { WorkspaceMember } from './models/workspaceMember.entity';
|
||||
import { Workspace } from './models/workspace.entity';
|
||||
import { Collaborator } from './models/collaborator.entity';
|
||||
import { DownloadTask } from './models/downloadTask.entity';
|
||||
import { Session } from './models/session.entity';
|
||||
|
||||
import { LoggerProvider } from './logger/logger.provider';
|
||||
import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
|
||||
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
||||
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
|
||||
import { PluginManager } from './securityPlugin/pluginManager';
|
||||
import { Logger } from './logger';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({}),
|
||||
ConfigModule.forRoot({
|
||||
envFilePath: `.env.${process.env.NODE_ENV}`, // 根据 NODE_ENV 动态加载对应的 .env 文件
|
||||
isGlobal: true, // 使配置模块在应用的任何地方可用
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
@ -81,6 +87,8 @@ import { Logger } from './logger';
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
Collaborator,
|
||||
DownloadTask,
|
||||
Session,
|
||||
],
|
||||
};
|
||||
},
|
||||
@ -100,6 +108,7 @@ import { Logger } from './logger';
|
||||
MessageModule,
|
||||
FileModule,
|
||||
WorkspaceModule,
|
||||
UpgradeModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
@ -114,7 +123,7 @@ import { Logger } from './logger';
|
||||
export class AppModule {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly pluginManager: PluginManager,
|
||||
) {}
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(LogRequestMiddleware).forRoutes('*');
|
||||
|
6
server/src/enums/downloadTaskStatus.ts
Normal file
6
server/src/enums/downloadTaskStatus.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum DOWNLOAD_TASK_STATUS {
|
||||
WAITING = 'waiting', // 排队中
|
||||
COMPUTING = 'computing', // 计算中
|
||||
SUCCEED = 'succeed', // 导出成功
|
||||
FAILED = 'failed', // 导出失败
|
||||
}
|
@ -6,18 +6,22 @@ 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, // 当前时间不允许提交
|
||||
RESPONSE_OVER_LIMIT = 9003, // 超出限制
|
||||
RESPONSE_SCHEMA_REMOVED = 9004, // 问卷已删除
|
||||
RESPONSE_DATA_DECRYPT_ERROR = 9005, // 问卷已删除
|
||||
RESPONSE_PAUSING = 9006, // 问卷已暂停
|
||||
|
||||
UPLOAD_FILE_ERROR = 5001, // 上传文件错误
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
// 状态类型
|
||||
export enum RECORD_STATUS {
|
||||
NEW = 'new', // 新建
|
||||
EDITING = 'editing', // 编辑
|
||||
PAUSING = 'pausing', // 暂停
|
||||
NEW = 'new', // 新建 | 未发布
|
||||
PUBLISHED = 'published', // 发布
|
||||
REMOVED = 'removed', // 删除
|
||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
||||
EDITING = 'editing', // 编辑
|
||||
FINISHED = 'finished', // 已结束
|
||||
REMOVED = 'removed',
|
||||
}
|
||||
|
||||
export const enum RECORD_SUB_STATUS {
|
||||
DEFAULT = '', // 默认
|
||||
PAUSING = 'pausing', // 暂停
|
||||
}
|
||||
|
||||
// 历史类型
|
||||
|
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',
|
||||
}
|
4
server/src/enums/surveySessionStatus.ts
Normal file
4
server/src/enums/surveySessionStatus.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum SESSION_STATUS {
|
||||
ACTIVATED = 'activated',
|
||||
DEACTIVATED = 'deactivated',
|
||||
}
|
68
server/src/guards/__test/session.guard.spec.ts
Normal file
68
server/src/guards/__test/session.guard.spec.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { SessionService } from 'src/modules/survey/services/session.service';
|
||||
import { SessionGuard } from '../session.guard';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
|
||||
describe('SessionGuard', () => {
|
||||
let sessionGuard: SessionGuard;
|
||||
let reflector: Reflector;
|
||||
let sessionService: SessionService;
|
||||
|
||||
beforeEach(() => {
|
||||
reflector = new Reflector();
|
||||
sessionService = {
|
||||
findOne: jest.fn(),
|
||||
} as unknown as SessionService;
|
||||
sessionGuard = new SessionGuard(reflector, sessionService);
|
||||
});
|
||||
|
||||
it('should return true when sessionId exists and sessionService returns sessionInfo', async () => {
|
||||
const mockSessionId = '12345';
|
||||
const mockSessionInfo = { id: mockSessionId, name: 'test session' };
|
||||
|
||||
const context = {
|
||||
switchToHttp: jest.fn().mockReturnThis(),
|
||||
getRequest: jest.fn().mockReturnValue({
|
||||
sessionId: mockSessionId,
|
||||
}),
|
||||
getHandler: jest.fn(),
|
||||
} as unknown as ExecutionContext;
|
||||
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('sessionId');
|
||||
|
||||
jest
|
||||
.spyOn(sessionService, 'findOne')
|
||||
.mockResolvedValue(mockSessionInfo as any);
|
||||
|
||||
const result = await sessionGuard.canActivate(context);
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(reflector.get).toHaveBeenCalledWith(
|
||||
'sessionId',
|
||||
context.getHandler(),
|
||||
);
|
||||
expect(sessionService.findOne).toHaveBeenCalledWith(mockSessionId);
|
||||
expect(request.sessionInfo).toEqual(mockSessionInfo);
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException when sessionId is missing', async () => {
|
||||
const context = {
|
||||
switchToHttp: jest.fn().mockReturnThis(),
|
||||
getRequest: jest.fn().mockReturnValue({}),
|
||||
getHandler: jest.fn(),
|
||||
} as unknown as ExecutionContext;
|
||||
|
||||
jest.spyOn(reflector, 'get').mockReturnValue('sessionId');
|
||||
|
||||
await expect(sessionGuard.canActivate(context)).rejects.toThrow(
|
||||
NoPermissionException,
|
||||
);
|
||||
expect(reflector.get).toHaveBeenCalledWith(
|
||||
'sessionId',
|
||||
context.getHandler(),
|
||||
);
|
||||
});
|
||||
});
|
30
server/src/guards/session.guard.ts
Normal file
30
server/src/guards/session.guard.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { get } from 'lodash';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
import { SessionService } from 'src/modules/survey/services/session.service';
|
||||
@Injectable()
|
||||
export class SessionGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private readonly sessionService: SessionService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const sessionIdKey = this.reflector.get<string>(
|
||||
'sessionId',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
const sessionId = get(request, sessionIdKey);
|
||||
|
||||
if (!sessionId) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
const sessionInfo = await this.sessionService.findOne(sessionId);
|
||||
request.sessionInfo = sessionInfo;
|
||||
request.surveyId = sessionInfo.surveyId;
|
||||
return true;
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -88,24 +88,71 @@ export interface MsgContent {
|
||||
msg_9004: string;
|
||||
}
|
||||
|
||||
export interface JumpConfig {
|
||||
type: string;
|
||||
link: string;
|
||||
buttonText?: string;
|
||||
}
|
||||
|
||||
export interface SubmitConf {
|
||||
submitTitle: string;
|
||||
confirmAgain: ConfirmAgain;
|
||||
msgContent: MsgContent;
|
||||
jumpConfig?: JumpConfig;
|
||||
}
|
||||
|
||||
// 白名单类型
|
||||
export enum WhitelistType {
|
||||
ALL = 'ALL',
|
||||
// 空间成员
|
||||
MEMBER = 'MEMBER',
|
||||
// 自定义
|
||||
CUSTOM = 'CUSTOM',
|
||||
}
|
||||
|
||||
// 白名单用户类型
|
||||
export enum MemberType {
|
||||
// 手机号
|
||||
MOBILE = 'MOBILE',
|
||||
// 邮箱
|
||||
EMAIL = 'EMAIL',
|
||||
}
|
||||
|
||||
export interface BaseConf {
|
||||
begTime: string;
|
||||
beginTime: string;
|
||||
endTime: string;
|
||||
answerBegTime: string;
|
||||
answerEndTime: string;
|
||||
tLimit: number;
|
||||
language: string;
|
||||
// 访问密码开关
|
||||
passwordSwitch?: boolean;
|
||||
// 密码
|
||||
password?: string | null;
|
||||
// 白名单类型
|
||||
whitelistType?: WhitelistType;
|
||||
// 白名单用户类型
|
||||
memberType?: MemberType;
|
||||
// 白名单列表
|
||||
whitelist?: string[];
|
||||
// 提示语
|
||||
whitelistTip?: string;
|
||||
}
|
||||
|
||||
export interface SkinConf {
|
||||
skinColor: string;
|
||||
inputBgColor: string;
|
||||
backgroundConf: {
|
||||
color: string;
|
||||
type: string;
|
||||
image: string;
|
||||
};
|
||||
contentConf: {
|
||||
opacity: number;
|
||||
};
|
||||
themeConf: {
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BottomConf {
|
||||
|
@ -1,15 +1,18 @@
|
||||
import * as log4js from 'log4js';
|
||||
import moment from 'moment';
|
||||
import { Request } from 'express';
|
||||
import { Injectable, Scope, Inject } from '@nestjs/common';
|
||||
import { CONTEXT, RequestContext } from '@nestjs/microservices';
|
||||
|
||||
const log4jsLogger = log4js.getLogger();
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class Logger {
|
||||
private static inited = false;
|
||||
|
||||
constructor() {}
|
||||
constructor(@Inject(CONTEXT) private readonly ctx: RequestContext) {}
|
||||
|
||||
static init(config: { filename: string }) {
|
||||
if (this.inited) {
|
||||
if (Logger.inited) {
|
||||
return;
|
||||
}
|
||||
log4js.configure({
|
||||
@ -30,25 +33,26 @@ export class Logger {
|
||||
default: { appenders: ['app'], level: 'trace' },
|
||||
},
|
||||
});
|
||||
Logger.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.ctx?.['traceId']
|
||||
? `traceid=${this.ctx?.['traceId']}||`
|
||||
: '';
|
||||
return log4jsLogger[level](
|
||||
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
info(message, options?: { dltag?: string; req?: Request }) {
|
||||
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' });
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
@ -20,7 +20,9 @@ export const genTraceId = ({ ip }) => {
|
||||
} else {
|
||||
ipArr = ip
|
||||
.split('.')
|
||||
.map((item) => parseInt(item).toString(16).padStart(2, '0'));
|
||||
.map((item) =>
|
||||
item ? parseInt(item).toString(16).padStart(2, '0') : '',
|
||||
);
|
||||
}
|
||||
|
||||
return `${ipArr.join('')}${Date.now().toString()}${getCountStr()}${process.pid.toString().slice(-5)}`;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import 'scripts/run-report';
|
||||
|
||||
async function bootstrap() {
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
@ -1,4 +1,3 @@
|
||||
// logger.middleware.ts
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Logger } from '../logger/index'; // 替换为你实际的logger路径
|
||||
@ -20,7 +19,6 @@ export class LogRequestMiddleware implements NestMiddleware {
|
||||
`method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`,
|
||||
{
|
||||
dltag: 'request_in',
|
||||
req,
|
||||
},
|
||||
);
|
||||
|
||||
@ -30,7 +28,6 @@ export class LogRequestMiddleware implements NestMiddleware {
|
||||
`status=${res.statusCode.toString()}||duration=${duration}ms`,
|
||||
{
|
||||
dltag: 'request_out',
|
||||
req,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -1,43 +1,13 @@
|
||||
import { Column, ObjectIdColumn, BeforeInsert, BeforeUpdate } from 'typeorm';
|
||||
import { ObjectIdColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
export class BaseEntity {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
@CreateDateColumn({ type: 'timestamp', precision: 3 })
|
||||
createdAt: Date;
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
this.createDate = now;
|
||||
this.updateDate = now;
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
@UpdateDateColumn({ type: 'timestamp', precision: 3 })
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -11,4 +11,16 @@ export class Collaborator extends BaseEntity {
|
||||
|
||||
@Column('jsonb')
|
||||
permissions: Array<string>;
|
||||
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
}
|
||||
|
48
server/src/models/downloadTask.entity.ts
Normal file
48
server/src/models/downloadTask.entity.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus';
|
||||
|
||||
@Entity({ name: 'downloadTask' })
|
||||
export class DownloadTask extends BaseEntity {
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
// 文件路径
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
// 文件key
|
||||
@Column()
|
||||
fileKey: string;
|
||||
|
||||
// 任务创建人
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
// 任务创建人
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
// 文件名
|
||||
@Column()
|
||||
filename: string;
|
||||
|
||||
// 文件大小
|
||||
@Column()
|
||||
fileSize: string;
|
||||
|
||||
@Column()
|
||||
params: string;
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column()
|
||||
deletedAt: Date;
|
||||
|
||||
@Column()
|
||||
status: DOWNLOAD_TASK_STATUS;
|
||||
}
|
@ -27,4 +27,16 @@ export class MessagePushingTask extends BaseEntity {
|
||||
|
||||
@Column()
|
||||
ownerId: string;
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column()
|
||||
deletedAt: Date;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { SurveySchemaInterface } from '../interfaces/survey';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'surveyPublish' })
|
||||
export class ResponseSchema extends BaseEntity {
|
||||
@ -15,4 +16,19 @@ export class ResponseSchema extends BaseEntity {
|
||||
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
subStatus: {
|
||||
status: RECORD_SUB_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
22
server/src/models/session.entity.ts
Normal file
22
server/src/models/session.entity.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Entity, Column, Index, ObjectIdColumn } from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { SESSION_STATUS } from 'src/enums/surveySessionStatus';
|
||||
|
||||
@Entity({ name: 'session' })
|
||||
export class Session extends BaseEntity {
|
||||
@Index({
|
||||
expireAfterSeconds: 3600,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
surveyId: string;
|
||||
|
||||
@Column()
|
||||
userId: string;
|
||||
|
||||
@Column()
|
||||
status: SESSION_STATUS;
|
||||
}
|
@ -19,4 +19,7 @@ export class SurveyHistory extends BaseEntity {
|
||||
username: string;
|
||||
_id: string;
|
||||
};
|
||||
|
||||
@Column('string')
|
||||
sessionId: string;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { Entity, Column, BeforeInsert } from 'typeorm';
|
||||
import { BaseEntity } from './base.entity';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'surveyMeta' })
|
||||
export class SurveyMeta extends BaseEntity {
|
||||
@ -18,6 +19,9 @@ export class SurveyMeta extends BaseEntity {
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
@Column()
|
||||
owner: string;
|
||||
|
||||
@ -32,4 +36,48 @@ export class SurveyMeta extends BaseEntity {
|
||||
|
||||
@Column()
|
||||
workspaceId: string;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
subStatus: {
|
||||
status: RECORD_SUB_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS | RECORD_SUB_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column()
|
||||
deletedAt: Date;
|
||||
|
||||
@BeforeInsert()
|
||||
initDefaultInfo() {
|
||||
const now = Date.now();
|
||||
if (!this.curStatus) {
|
||||
const curStatus = { status: RECORD_STATUS.NEW, date: now };
|
||||
this.curStatus = curStatus;
|
||||
this.statusList = [curStatus];
|
||||
}
|
||||
if (!this.subStatus) {
|
||||
const subStatus = { status: RECORD_SUB_STATUS.DEFAULT, date: now };
|
||||
this.subStatus = subStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export class SurveyResponse extends BaseEntity {
|
||||
data: Record<string, any>;
|
||||
|
||||
@Column()
|
||||
difTime: number;
|
||||
diffTime: number;
|
||||
|
||||
@Column()
|
||||
clientTime: number;
|
||||
@ -27,11 +27,11 @@ export class SurveyResponse extends BaseEntity {
|
||||
|
||||
@BeforeInsert()
|
||||
async onDataInsert() {
|
||||
return await pluginManager.triggerHook('beforeResponseDataCreate', this);
|
||||
return await pluginManager.triggerHook('encryptResponseData', this);
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
async onDataLoaded() {
|
||||
return await pluginManager.triggerHook('afterResponseDataReaded', this);
|
||||
return await pluginManager.triggerHook('decryptResponseData', this);
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,33 @@ import { BaseEntity } from './base.entity';
|
||||
|
||||
@Entity({ name: 'workspace' })
|
||||
export class Workspace extends BaseEntity {
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
ownerId: string;
|
||||
|
||||
@Column()
|
||||
owner: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
description: string;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
|
||||
@Column()
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column()
|
||||
deletedAt: Date;
|
||||
}
|
||||
|
@ -11,4 +11,16 @@ export class WorkspaceMember extends BaseEntity {
|
||||
|
||||
@Column()
|
||||
role: string;
|
||||
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
creatorId: string;
|
||||
|
||||
@Column()
|
||||
operator: string;
|
||||
|
||||
@Column()
|
||||
operatorId: string;
|
||||
}
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,6 @@ import { UserService } from '../services/user.service';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { hash256 } from 'src/utils/hash256';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
describe('UserService', () => {
|
||||
@ -145,7 +144,6 @@ describe('UserService', () => {
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
username: username,
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
});
|
||||
expect(user).toEqual(userInfo);
|
||||
@ -163,7 +161,6 @@ describe('UserService', () => {
|
||||
expect(findOneSpy).toHaveBeenCalledWith({
|
||||
where: {
|
||||
username: username,
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
});
|
||||
expect(user).toBe(null);
|
||||
@ -184,7 +181,6 @@ describe('UserService', () => {
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
});
|
||||
expect(user).toEqual(userInfo);
|
||||
@ -202,7 +198,6 @@ describe('UserService', () => {
|
||||
expect(findOneSpy).toHaveBeenCalledWith({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
});
|
||||
expect(user).toBe(null);
|
||||
@ -228,7 +223,6 @@ describe('UserService', () => {
|
||||
expect(userRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
username: new RegExp(username),
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
skip: 0,
|
||||
take: 10,
|
||||
@ -263,7 +257,6 @@ describe('UserService', () => {
|
||||
_id: {
|
||||
$in: idList.map((id) => new ObjectId(id)),
|
||||
},
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
select: ['_id', 'username', 'createDate'],
|
||||
});
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
HttpCode,
|
||||
Get,
|
||||
Query,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { CaptchaService } from '../services/captcha.service';
|
||||
@ -7,6 +15,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 +39,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 +191,59 @@ export class AuthController {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码强度
|
||||
*/
|
||||
@Get('/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',
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/verifyToken')
|
||||
@HttpCode(200)
|
||||
async verifyToken(@Request() req) {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) {
|
||||
return {
|
||||
code: 200,
|
||||
data: false,
|
||||
};
|
||||
}
|
||||
try {
|
||||
await this.authService.verifyToken(token);
|
||||
return {
|
||||
code: 200,
|
||||
data: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 200,
|
||||
data: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import { User } from 'src/models/user.entity';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { hash256 } from 'src/utils/hash256';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
@Injectable()
|
||||
@ -53,9 +52,6 @@ export class UserService {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
username: username,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -66,9 +62,6 @@ export class UserService {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -79,13 +72,10 @@ export class UserService {
|
||||
const list = await this.userRepository.find({
|
||||
where: {
|
||||
username: new RegExp(username),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
select: ['_id', 'username', 'createDate'],
|
||||
select: ['_id', 'username', 'createdAt'],
|
||||
});
|
||||
return list;
|
||||
}
|
||||
@ -96,11 +86,8 @@ export class UserService {
|
||||
_id: {
|
||||
$in: idList.map((item) => new ObjectId(item)),
|
||||
},
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
select: ['_id', 'username', 'createDate'],
|
||||
select: ['_id', 'username', 'createdAt'],
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
@ -14,13 +14,18 @@ export class FileService {
|
||||
configKey,
|
||||
file,
|
||||
pathPrefix,
|
||||
filename,
|
||||
}: {
|
||||
configKey: string;
|
||||
file: Express.Multer.File;
|
||||
pathPrefix: string;
|
||||
filename?: string;
|
||||
}) {
|
||||
const handler = this.getHandler(configKey);
|
||||
const { key } = await handler.upload(file, { pathPrefix });
|
||||
const { key } = await handler.upload(file, {
|
||||
pathPrefix,
|
||||
filename,
|
||||
});
|
||||
const url = await handler.getUrl(key);
|
||||
return {
|
||||
key,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { join, dirname } from 'path';
|
||||
import { join, dirname, sep } from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { FileUploadHandler } from './uploadHandler.interface';
|
||||
@ -12,13 +12,20 @@ export class LocalHandler implements FileUploadHandler {
|
||||
|
||||
async upload(
|
||||
file: Express.Multer.File,
|
||||
options?: { pathPrefix?: string },
|
||||
options?: { pathPrefix?: string; filename?: string },
|
||||
): Promise<{ key: string }> {
|
||||
const filename = await generateUniqueFilename(file.originalname);
|
||||
let filename;
|
||||
if (options?.filename) {
|
||||
filename = file.filename;
|
||||
} else {
|
||||
filename = await generateUniqueFilename(file.originalname);
|
||||
}
|
||||
const filePath = join(
|
||||
options?.pathPrefix ? options?.pathPrefix : '',
|
||||
filename,
|
||||
);
|
||||
)
|
||||
.split(sep)
|
||||
.join('/');
|
||||
const physicalPath = join(this.physicalRootPath, filePath);
|
||||
await fse.mkdir(dirname(physicalPath), { recursive: true });
|
||||
const writeStream = createWriteStream(physicalPath);
|
||||
@ -35,6 +42,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}`;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import { MessagePushingLogService } from '../services/messagePushingLog.service'
|
||||
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
|
||||
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
|
||||
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing';
|
||||
import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
|
||||
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
|
||||
@ -121,7 +120,6 @@ describe('MessagePushingTaskService', () => {
|
||||
ownerId: mockOwnerId,
|
||||
surveys: { $all: [surveyId] },
|
||||
triggerHook: hook,
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -146,7 +144,6 @@ describe('MessagePushingTaskService', () => {
|
||||
where: {
|
||||
ownerId: mockOwnerId,
|
||||
_id: new ObjectId(taskId),
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -161,10 +158,6 @@ describe('MessagePushingTaskService', () => {
|
||||
pushAddress: 'http://update.example.com',
|
||||
triggerHook: MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED,
|
||||
surveys: ['new survey id'],
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.EDITING,
|
||||
date: Date.now(),
|
||||
},
|
||||
};
|
||||
const existingTask = new MessagePushingTask();
|
||||
existingTask._id = new ObjectId(taskId);
|
||||
@ -197,34 +190,26 @@ describe('MessagePushingTaskService', () => {
|
||||
const taskId = '65afc62904d5db18534c0f78';
|
||||
|
||||
const updateResult = { modifiedCount: 1 };
|
||||
const mockOwnerId = '66028642292c50f8b71a9eee';
|
||||
const mockOperatorId = '66028642292c50f8b71a9eee';
|
||||
const mockOperator = 'mockOperator';
|
||||
|
||||
jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult);
|
||||
|
||||
const result = await service.remove({
|
||||
id: taskId,
|
||||
ownerId: mockOwnerId,
|
||||
operatorId: mockOperatorId,
|
||||
operator: mockOperator,
|
||||
});
|
||||
|
||||
expect(result).toEqual(updateResult);
|
||||
expect(repository.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: mockOwnerId,
|
||||
ownerId: mockOperatorId,
|
||||
_id: new ObjectId(taskId),
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
},
|
||||
$push: {
|
||||
statusList: {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
isDeleted: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -150,8 +150,9 @@ export class MessagePushingTaskController {
|
||||
async remove(@Request() req, @Param('id') id: string) {
|
||||
const userId = req.user._id;
|
||||
const res = await this.messagePushingTaskService.remove({
|
||||
ownerId: userId,
|
||||
id,
|
||||
operator: req.user.username,
|
||||
operatorId: userId,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
MESSAGE_PUSHING_TYPE,
|
||||
MESSAGE_PUSHING_HOOK,
|
||||
} from 'src/enums/messagePushing';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
export class MessagePushingTaskDto {
|
||||
@ApiProperty({ description: '任务id' })
|
||||
@ -27,12 +26,6 @@ export class MessagePushingTaskDto {
|
||||
|
||||
@ApiProperty({ description: '所有者' })
|
||||
owner: string;
|
||||
|
||||
@ApiProperty({ description: '任务状态', required: false })
|
||||
curStatus?: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class CodeDto {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import {
|
||||
MESSAGE_PUSHING_TYPE,
|
||||
MESSAGE_PUSHING_HOOK,
|
||||
@ -20,10 +19,4 @@ export class UpdateMessagePushingTaskDto {
|
||||
|
||||
@ApiProperty({ description: '绑定的问卷id', required: false })
|
||||
surveys?: string[];
|
||||
|
||||
@ApiProperty({ description: '任务状态', required: false })
|
||||
curStatus?: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { MESSAGE_PUSHING_HOOK } from 'src/enums/messagePushing';
|
||||
import { CreateMessagePushingTaskDto } from '../dto/createMessagePushingTask.dto';
|
||||
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { MESSAGE_PUSHING_TYPE } from 'src/enums/messagePushing';
|
||||
import { MessagePushingLogService } from './messagePushingLog.service';
|
||||
import { httpPost } from 'src/utils/request';
|
||||
@ -44,8 +43,8 @@ export class MessagePushingTaskService {
|
||||
ownerId?: string;
|
||||
}): Promise<MessagePushingTask[]> {
|
||||
const where: Record<string, any> = {
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
};
|
||||
if (surveyId) {
|
||||
@ -75,8 +74,8 @@ export class MessagePushingTaskService {
|
||||
where: {
|
||||
ownerId,
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -104,25 +103,25 @@ export class MessagePushingTaskService {
|
||||
return await this.messagePushingTaskRepository.save(updatedTask);
|
||||
}
|
||||
|
||||
async remove({ id, ownerId }: { id: string; ownerId: string }) {
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: Date.now(),
|
||||
};
|
||||
async remove({
|
||||
id,
|
||||
operator,
|
||||
operatorId,
|
||||
}: {
|
||||
id: string;
|
||||
operator: string;
|
||||
operatorId: string;
|
||||
}) {
|
||||
return this.messagePushingTaskRepository.updateOne(
|
||||
{
|
||||
ownerId,
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus,
|
||||
},
|
||||
$push: {
|
||||
statusList: curStatus as never,
|
||||
isDeleted: true,
|
||||
operator,
|
||||
operatorId,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -147,6 +146,9 @@ export class MessagePushingTaskService {
|
||||
$push: {
|
||||
surveys: surveyId as never,
|
||||
},
|
||||
$set: {
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -8,7 +8,7 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { Logger } from 'src/logger';
|
||||
|
||||
import { UserService } from 'src/modules/auth/services/user.service';
|
||||
@ -27,7 +27,7 @@ describe('DataStatisticController', () => {
|
||||
let controller: DataStatisticController;
|
||||
let dataStatisticService: DataStatisticService;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let pluginManager: XiaojuSurveyPluginManager;
|
||||
let pluginManager: PluginManager;
|
||||
let logger: Logger;
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -70,9 +70,7 @@ describe('DataStatisticController', () => {
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
pluginManager = module.get<PluginManager>(PluginManager);
|
||||
logger = module.get<Logger>(Logger);
|
||||
|
||||
pluginManager.registerPlugin(
|
||||
@ -90,7 +88,7 @@ describe('DataStatisticController', () => {
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId,
|
||||
isDesensitive: false,
|
||||
isMasked: false,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
@ -106,12 +104,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 +121,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,
|
||||
@ -130,12 +129,12 @@ describe('DataStatisticController', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return data table with isDesensitive', async () => {
|
||||
it('should return data table with isMasked', async () => {
|
||||
const surveyId = new ObjectId().toString();
|
||||
const mockRequest = {
|
||||
query: {
|
||||
surveyId,
|
||||
isDesensitive: true,
|
||||
isMasked: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
@ -151,12 +150,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 +167,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 +185,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);
|
||||
});
|
||||
});
|
||||
@ -233,7 +233,7 @@ describe('DataStatisticController', () => {
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-05-31 20:31:36',
|
||||
beginTime: '2024-05-31 20:31:36',
|
||||
endTime: '2034-05-31 20:31:36',
|
||||
language: 'chinese',
|
||||
showVoteProcess: 'allow',
|
||||
@ -249,6 +249,8 @@ describe('DataStatisticController', () => {
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
@ -273,6 +275,11 @@ describe('DataStatisticController', () => {
|
||||
again_text: '确认要提交吗?',
|
||||
},
|
||||
link: '',
|
||||
jumpConfig: {
|
||||
type: 'link',
|
||||
link: '',
|
||||
buttonText: '',
|
||||
},
|
||||
},
|
||||
logicConf: {
|
||||
showLogicConf: [],
|
||||
|
@ -11,7 +11,7 @@ import { cloneDeep } from 'lodash';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
|
||||
describe('DataStatisticService', () => {
|
||||
@ -34,9 +34,7 @@ describe('DataStatisticService', () => {
|
||||
surveyResponseRepository = module.get<MongoRepository<SurveyResponse>>(
|
||||
getRepositoryToken(SurveyResponse),
|
||||
);
|
||||
const manager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
const manager = module.get<PluginManager>(PluginManager);
|
||||
manager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
@ -70,7 +68,7 @@ describe('DataStatisticService', () => {
|
||||
data413: 3,
|
||||
data863: '109239',
|
||||
},
|
||||
difTime: 21278,
|
||||
diffTime: 21278,
|
||||
clientTime: 1710340862733.0,
|
||||
secretKeys: [],
|
||||
optionTextAndId: {
|
||||
@ -197,14 +195,14 @@ 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),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return desensitive table data', async () => {
|
||||
it('should return desensitized table data', async () => {
|
||||
const mockSchema = cloneDeep(mockSensitiveResponseSchema);
|
||||
const surveyResponseList: Array<SurveyResponse> = [
|
||||
{
|
||||
@ -220,7 +218,7 @@ describe('DataStatisticService', () => {
|
||||
'U2FsdGVkX19bRmf3uEmXAJ/6zXd1Znr3cZsD5v4Nocr2v5XG1taXluz8cohFkDyH',
|
||||
data770: 'U2FsdGVkX18ldQMhJjFXO8aerjftZLpFnRQ4/FVcCLI=',
|
||||
},
|
||||
difTime: 806707,
|
||||
diffTime: 806707,
|
||||
clientTime: 1710400229573.0,
|
||||
secretKeys: ['data458', 'data450', 'data405', 'data770'],
|
||||
optionTextAndId: {
|
||||
@ -303,7 +301,7 @@ describe('DataStatisticService', () => {
|
||||
data458: expect.any(String),
|
||||
data515: expect.any(String),
|
||||
data770: expect.any(String),
|
||||
difTime: expect.any(String),
|
||||
diffTime: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
259
server/src/modules/survey/__test/downloadTask.controller.spec.ts
Normal file
259
server/src/modules/survey/__test/downloadTask.controller.spec.ts
Normal file
@ -0,0 +1,259 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { DownloadTaskController } from '../controllers/downloadTask.controller';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
import { AuthService } from 'src/modules/auth/services/auth.service';
|
||||
import { DownloadTaskService } from '../services/downloadTask.service';
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
|
||||
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { NoPermissionException } from 'src/exceptions/noPermissionException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
|
||||
describe('DownloadTaskController', () => {
|
||||
let controller: DownloadTaskController;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let downloadTaskService: DownloadTaskService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [DownloadTaskController],
|
||||
providers: [
|
||||
{
|
||||
provide: ResponseSchemaService,
|
||||
useValue: {
|
||||
getResponseSchemaByPageId: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DownloadTaskService,
|
||||
useValue: {
|
||||
createDownloadTask: jest.fn(),
|
||||
processDownloadTask: jest.fn(),
|
||||
getDownloadTaskList: jest.fn(),
|
||||
getDownloadTaskById: jest.fn(),
|
||||
deleteDownloadTask: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
varifytoken() {
|
||||
return {};
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: CollaboratorService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: SurveyMetaService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: Authentication,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
{
|
||||
provide: SurveyGuard,
|
||||
useClass: jest.fn().mockImplementation(() => ({
|
||||
canActivate: () => true,
|
||||
})),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<DownloadTaskController>(DownloadTaskController);
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
downloadTaskService = module.get<DownloadTaskService>(DownloadTaskService);
|
||||
});
|
||||
|
||||
describe('createTask', () => {
|
||||
it('should create a download task successfully', async () => {
|
||||
const mockReqBody = {
|
||||
surveyId: new ObjectId().toString(),
|
||||
isMasked: false,
|
||||
};
|
||||
const mockReq = { user: { _id: 'mockUserId', username: 'mockUsername' } };
|
||||
const mockTaskId = 'mockTaskId';
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPageId')
|
||||
.mockResolvedValue({} as any);
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'createDownloadTask')
|
||||
.mockResolvedValue(mockTaskId);
|
||||
|
||||
const result = await controller.createTask(mockReqBody, mockReq);
|
||||
|
||||
expect(
|
||||
responseSchemaService.getResponseSchemaByPageId,
|
||||
).toHaveBeenCalledWith(mockReqBody.surveyId);
|
||||
expect(downloadTaskService.createDownloadTask).toHaveBeenCalledWith({
|
||||
surveyId: mockReqBody.surveyId,
|
||||
responseSchema: {},
|
||||
creatorId: mockReq.user._id.toString(),
|
||||
creator: mockReq.user.username,
|
||||
params: { isMasked: mockReqBody.isMasked },
|
||||
});
|
||||
expect(downloadTaskService.processDownloadTask).toHaveBeenCalledWith({
|
||||
taskId: mockTaskId,
|
||||
});
|
||||
expect(result).toEqual({ code: 200, data: { taskId: mockTaskId } });
|
||||
});
|
||||
|
||||
it('should throw HttpException if validation fails', async () => {
|
||||
const mockReqBody: any = { isMasked: false };
|
||||
const mockReq = { user: { _id: 'mockUserId', username: 'mockUsername' } };
|
||||
|
||||
await expect(controller.createTask(mockReqBody, mockReq)).rejects.toThrow(
|
||||
HttpException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadList', () => {
|
||||
it('should return the download task list', async () => {
|
||||
const mockQueryInfo = { pageIndex: 1, pageSize: 10 };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
const mockTaskList: any = {
|
||||
total: 1,
|
||||
list: [
|
||||
{
|
||||
_id: 'mockTaskId',
|
||||
curStatus: 'completed',
|
||||
filename: 'mockFile.csv',
|
||||
url: 'http://mock-url.com',
|
||||
fileSize: 1024,
|
||||
createDate: Date.now(),
|
||||
},
|
||||
],
|
||||
};
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskList')
|
||||
.mockResolvedValue(mockTaskList);
|
||||
|
||||
const result = await controller.downloadList(mockQueryInfo, mockReq);
|
||||
|
||||
expect(downloadTaskService.getDownloadTaskList).toHaveBeenCalledWith({
|
||||
creatorId: mockReq.user._id.toString(),
|
||||
pageIndex: mockQueryInfo.pageIndex,
|
||||
pageSize: mockQueryInfo.pageSize,
|
||||
});
|
||||
expect(result.data.total).toEqual(mockTaskList.total);
|
||||
expect(result.data.list[0].taskId).toEqual(
|
||||
mockTaskList.list[0]._id.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException if validation fails', async () => {
|
||||
const mockQueryInfo: any = { pageIndex: 'invalid', pageSize: 10 };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
|
||||
await expect(
|
||||
controller.downloadList(mockQueryInfo, mockReq),
|
||||
).rejects.toThrow(HttpException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadTask', () => {
|
||||
it('should return a download task', async () => {
|
||||
const mockQuery = { taskId: 'mockTaskId' };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
const mockTaskInfo: any = {
|
||||
_id: 'mockTaskId',
|
||||
creatorId: 'mockUserId',
|
||||
curStatus: 'completed',
|
||||
};
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskById')
|
||||
.mockResolvedValue(mockTaskInfo);
|
||||
|
||||
const result = await controller.getDownloadTask(mockQuery, mockReq);
|
||||
|
||||
expect(downloadTaskService.getDownloadTaskById).toHaveBeenCalledWith({
|
||||
taskId: mockQuery.taskId,
|
||||
});
|
||||
expect(result.data.taskId).toEqual(mockTaskInfo._id.toString());
|
||||
});
|
||||
|
||||
it('should throw NoPermissionException if user has no permission', async () => {
|
||||
const mockQuery = { taskId: 'mockTaskId' };
|
||||
const mockReq = { user: { _id: new ObjectId() } };
|
||||
const mockTaskInfo: any = {
|
||||
_id: 'mockTaskId',
|
||||
creatorId: 'mockUserId',
|
||||
curStatus: 'completed',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskById')
|
||||
.mockResolvedValue(mockTaskInfo);
|
||||
|
||||
await expect(
|
||||
controller.getDownloadTask(mockQuery, mockReq),
|
||||
).rejects.toThrow(new NoPermissionException('没有权限'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFileByName', () => {
|
||||
it('should delete a download task successfully', async () => {
|
||||
const mockBody = { taskId: 'mockTaskId' };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
const mockTaskInfo: any = {
|
||||
_id: new ObjectId(),
|
||||
creatorId: 'mockUserId',
|
||||
};
|
||||
const mockDelRes = { modifiedCount: 1 };
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskById')
|
||||
.mockResolvedValue(mockTaskInfo);
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'deleteDownloadTask')
|
||||
.mockResolvedValue(mockDelRes);
|
||||
|
||||
const result = await controller.deleteFileByName(mockBody, mockReq);
|
||||
|
||||
expect(downloadTaskService.deleteDownloadTask).toHaveBeenCalledWith({
|
||||
taskId: mockBody.taskId,
|
||||
});
|
||||
expect(result).toEqual({ code: 200, data: true });
|
||||
});
|
||||
|
||||
it('should throw HttpException if task does not exist', async () => {
|
||||
const mockBody = { taskId: 'mockTaskId' };
|
||||
const mockReq = { user: { _id: 'mockUserId' } };
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskService, 'getDownloadTaskById')
|
||||
.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
controller.deleteFileByName(mockBody, mockReq),
|
||||
).rejects.toThrow(
|
||||
new HttpException('任务不存在', EXCEPTION_CODE.PARAMETER_ERROR),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
191
server/src/modules/survey/__test/downloadTask.service.spec.ts
Normal file
191
server/src/modules/survey/__test/downloadTask.service.spec.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DownloadTaskService } from '../services/downloadTask.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { DownloadTask } from 'src/models/downloadTask.entity';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { ResponseSchemaService } from 'src/modules/surveyResponse/services/responseScheme.service';
|
||||
import { DataStatisticService } from '../services/dataStatistic.service';
|
||||
import { FileService } from 'src/modules/file/services/file.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
describe('DownloadTaskService', () => {
|
||||
let service: DownloadTaskService;
|
||||
let downloadTaskRepository: MongoRepository<DownloadTask>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DownloadTaskService,
|
||||
{
|
||||
provide: getRepositoryToken(DownloadTask),
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(SurveyResponse),
|
||||
useClass: MongoRepository,
|
||||
},
|
||||
{
|
||||
provide: ResponseSchemaService,
|
||||
useValue: {
|
||||
getResponseSchemaByPageId: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DataStatisticService,
|
||||
useValue: {
|
||||
getDataTable: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: FileService,
|
||||
useValue: {
|
||||
upload: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DownloadTaskService>(DownloadTaskService);
|
||||
downloadTaskRepository = module.get<MongoRepository<DownloadTask>>(
|
||||
getRepositoryToken(DownloadTask),
|
||||
);
|
||||
});
|
||||
|
||||
describe('createDownloadTask', () => {
|
||||
it('should create and save a download task', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
const mockDownloadTask = { _id: new ObjectId(mockTaskId) };
|
||||
const mockParams: any = {
|
||||
surveyId: 'survey1',
|
||||
responseSchema: { title: 'test-title', surveyPath: '/path' },
|
||||
creatorId: 'creator1',
|
||||
creator: 'creatorName',
|
||||
params: { isMasked: true },
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'create')
|
||||
.mockReturnValue(mockDownloadTask as any);
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'save')
|
||||
.mockResolvedValue(mockDownloadTask as any);
|
||||
|
||||
const result = await service.createDownloadTask(mockParams);
|
||||
|
||||
expect(downloadTaskRepository.create).toHaveBeenCalledWith({
|
||||
surveyId: mockParams.surveyId,
|
||||
surveyPath: mockParams.responseSchema.surveyPath,
|
||||
fileSize: '计算中',
|
||||
creatorId: mockParams.creatorId,
|
||||
creator: mockParams.creator,
|
||||
params: {
|
||||
...mockParams.params,
|
||||
title: mockParams.responseSchema.title,
|
||||
},
|
||||
filename: expect.any(String),
|
||||
});
|
||||
expect(downloadTaskRepository.save).toHaveBeenCalled();
|
||||
expect(result).toEqual(mockTaskId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadTaskList', () => {
|
||||
it('should return task list and total count', async () => {
|
||||
const mockCreatorId = 'creator1';
|
||||
const mockTasks = [{ _id: '1' }, { _id: '2' }];
|
||||
const mockTotal = 2;
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'findAndCount')
|
||||
.mockResolvedValue([mockTasks as any, mockTotal]);
|
||||
|
||||
const result = await service.getDownloadTaskList({
|
||||
creatorId: mockCreatorId,
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
expect(downloadTaskRepository.findAndCount).toHaveBeenCalledWith({
|
||||
where: {
|
||||
creatorId: mockCreatorId,
|
||||
},
|
||||
take: 10,
|
||||
skip: 0,
|
||||
order: { createDate: -1 },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
total: mockTotal,
|
||||
list: mockTasks,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDownloadTaskById', () => {
|
||||
it('should return task by id', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
const mockTask = { _id: new ObjectId(mockTaskId) };
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'find')
|
||||
.mockResolvedValue([mockTask as any]);
|
||||
|
||||
const result = await service.getDownloadTaskById({ taskId: mockTaskId });
|
||||
|
||||
expect(downloadTaskRepository.find).toHaveBeenCalledWith({
|
||||
where: { _id: new ObjectId(mockTaskId) },
|
||||
});
|
||||
expect(result).toEqual(mockTask);
|
||||
});
|
||||
|
||||
it('should return null if task is not found', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
|
||||
jest.spyOn(downloadTaskRepository, 'find').mockResolvedValue([]);
|
||||
|
||||
const result = await service.getDownloadTaskById({ taskId: mockTaskId });
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteDownloadTask', () => {
|
||||
it('should update task status to REMOVED', async () => {
|
||||
const mockTaskId = new ObjectId().toString();
|
||||
const mockUpdateResult = { matchedCount: 1 };
|
||||
|
||||
jest
|
||||
.spyOn(downloadTaskRepository, 'updateOne')
|
||||
.mockResolvedValue(mockUpdateResult as any);
|
||||
|
||||
const result = await service.deleteDownloadTask({ taskId: mockTaskId });
|
||||
|
||||
expect(downloadTaskRepository.updateOne).toHaveBeenCalledWith(
|
||||
{
|
||||
_id: new ObjectId(mockTaskId),
|
||||
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
},
|
||||
$push: { statusList: expect.any(Object) },
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(mockUpdateResult);
|
||||
});
|
||||
});
|
||||
});
|
@ -32,7 +32,7 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-03-14 14:54:41',
|
||||
beginTime: '2024-03-14 14:54:41',
|
||||
endTime: '2034-03-14 14:54:41',
|
||||
language: 'chinese',
|
||||
tLimit: 0,
|
||||
@ -44,6 +44,17 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
@ -60,6 +71,11 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
is_again: true,
|
||||
again_text: '确认要提交吗?',
|
||||
},
|
||||
jumpConfig: {
|
||||
type: 'link',
|
||||
link: '',
|
||||
buttonText: '',
|
||||
},
|
||||
},
|
||||
dataConf: {
|
||||
dataList: [
|
||||
@ -284,7 +300,7 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
pageId: '65f29f3192862d6a9067ad1c',
|
||||
} as ResponseSchema;
|
||||
} as unknown as ResponseSchema;
|
||||
|
||||
export const mockResponseSchema: ResponseSchema = {
|
||||
_id: new ObjectId('65b0d46e04d5db18534c0f7c'),
|
||||
@ -315,7 +331,7 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-01-23 21:59:05',
|
||||
beginTime: '2024-01-23 21:59:05',
|
||||
endTime: '2034-01-23 21:59:05',
|
||||
language: 'chinese',
|
||||
tLimit: 0,
|
||||
@ -327,6 +343,17 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
@ -343,6 +370,11 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
is_again: true,
|
||||
again_text: '确认要提交吗?',
|
||||
},
|
||||
jumpConfig: {
|
||||
type: 'link',
|
||||
link: '',
|
||||
buttonText: '',
|
||||
},
|
||||
},
|
||||
dataConf: {
|
||||
dataList: [
|
||||
@ -634,4 +666,4 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
pageId: '65afc62904d5db18534c0f78',
|
||||
createDate: 1710340841289,
|
||||
updateDate: 1710340841289.0,
|
||||
} as ResponseSchema;
|
||||
} as unknown as ResponseSchema;
|
||||
|
87
server/src/modules/survey/__test/session.controller.spec.ts
Normal file
87
server/src/modules/survey/__test/session.controller.spec.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SessionController } from '../controllers/session.controller';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SessionGuard } from 'src/guards/session.guard';
|
||||
|
||||
describe('SessionController', () => {
|
||||
let controller: SessionController;
|
||||
let sessionService: jest.Mocked<SessionService>;
|
||||
let logger: jest.Mocked<Logger>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SessionController],
|
||||
providers: [
|
||||
{
|
||||
provide: SessionService,
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
updateSessionToEditing: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(Authentication)
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(SurveyGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.overrideGuard(SessionGuard)
|
||||
.useValue({ canActivate: () => true })
|
||||
.compile();
|
||||
|
||||
controller = module.get<SessionController>(SessionController);
|
||||
sessionService = module.get<jest.Mocked<SessionService>>(SessionService);
|
||||
logger = module.get<jest.Mocked<Logger>>(Logger);
|
||||
});
|
||||
|
||||
it('should create a session', async () => {
|
||||
const reqBody = { surveyId: '123' };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
const session: any = { _id: 'sessionId' };
|
||||
|
||||
sessionService.create.mockResolvedValue(session);
|
||||
|
||||
const result = await controller.create(reqBody, req);
|
||||
|
||||
expect(sessionService.create).toHaveBeenCalledWith({
|
||||
surveyId: '123',
|
||||
userId: 'userId',
|
||||
});
|
||||
expect(result).toEqual({ code: 200, data: { sessionId: 'sessionId' } });
|
||||
});
|
||||
|
||||
it('should throw an exception if validation fails', async () => {
|
||||
const reqBody = { surveyId: null };
|
||||
const req = { user: { _id: 'userId' } };
|
||||
|
||||
try {
|
||||
await controller.create(reqBody, req);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('should seize a session', async () => {
|
||||
const req = {
|
||||
sessionInfo: { _id: 'sessionId', surveyId: 'surveyId' },
|
||||
};
|
||||
|
||||
await controller.seize(req);
|
||||
|
||||
expect(sessionService.updateSessionToEditing).toHaveBeenCalledWith({
|
||||
sessionId: 'sessionId',
|
||||
surveyId: 'surveyId',
|
||||
});
|
||||
});
|
||||
});
|
@ -5,18 +5,22 @@ 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 '../../surveyResponse/services/counter.service';
|
||||
import { SessionService } from '../services/session.service';
|
||||
import { UserService } from '../../auth/services/user.service';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
import { Logger } from 'src/logger';
|
||||
|
||||
jest.mock('../services/surveyMeta.service');
|
||||
jest.mock('../services/surveyConf.service');
|
||||
jest.mock('../../surveyResponse/services/responseScheme.service');
|
||||
jest.mock('../services/contentSecurity.service');
|
||||
jest.mock('../services/surveyHistory.service');
|
||||
jest.mock('../services/session.service');
|
||||
jest.mock('../../surveyResponse/services/counter.service');
|
||||
jest.mock('../../auth/services/user.service');
|
||||
|
||||
jest.mock('src/guards/authentication.guard');
|
||||
jest.mock('src/guards/survey.guard');
|
||||
@ -27,19 +31,36 @@ describe('SurveyController', () => {
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let surveyConfService: SurveyConfService;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let contentSecurityService: ContentSecurityService;
|
||||
let surveyHistoryService: SurveyHistoryService;
|
||||
let sessionService: SessionService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SurveyController],
|
||||
providers: [
|
||||
SurveyMetaService,
|
||||
SurveyConfService,
|
||||
{
|
||||
provide: SurveyConfService,
|
||||
useValue: {
|
||||
getSurveyConfBySurveyId: jest.fn(),
|
||||
getSurveyContentByCode: jest.fn(),
|
||||
createSurveyConf: jest.fn(),
|
||||
saveSurveyConf: jest.fn(),
|
||||
},
|
||||
},
|
||||
ResponseSchemaService,
|
||||
ContentSecurityService,
|
||||
SurveyHistoryService,
|
||||
LoggerProvider,
|
||||
SessionService,
|
||||
CounterService,
|
||||
UserService,
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -49,17 +70,14 @@ describe('SurveyController', () => {
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
contentSecurityService = module.get<ContentSecurityService>(
|
||||
ContentSecurityService,
|
||||
);
|
||||
surveyHistoryService =
|
||||
module.get<SurveyHistoryService>(SurveyHistoryService);
|
||||
sessionService = module.get<SessionService>(SessionService);
|
||||
});
|
||||
|
||||
describe('getBannerData', () => {
|
||||
it('should return banner data', async () => {
|
||||
const result = await controller.getBannerData();
|
||||
|
||||
expect(result.code).toBe(200);
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
@ -71,33 +89,17 @@ describe('SurveyController', () => {
|
||||
surveyType: 'normal',
|
||||
remark: '问卷调研',
|
||||
title: '问卷调研',
|
||||
} as SurveyMeta;
|
||||
};
|
||||
|
||||
const newId = new ObjectId();
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'createSurveyMeta')
|
||||
.mockImplementation(() => {
|
||||
const result = {
|
||||
_id: newId,
|
||||
} as SurveyMeta;
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
jest
|
||||
.spyOn(surveyConfService, 'createSurveyConf')
|
||||
.mockImplementation(
|
||||
(params: {
|
||||
surveyId: string;
|
||||
surveyType: string;
|
||||
createMethod: string;
|
||||
createFrom: string;
|
||||
}) => {
|
||||
const result = {
|
||||
_id: new ObjectId(),
|
||||
pageId: params.surveyId,
|
||||
code: {},
|
||||
} as SurveyConf;
|
||||
return Promise.resolve(result);
|
||||
},
|
||||
);
|
||||
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
|
||||
_id: newId,
|
||||
} as SurveyMeta);
|
||||
|
||||
jest.spyOn(surveyConfService, 'createSurveyConf').mockResolvedValue({
|
||||
_id: new ObjectId(),
|
||||
pageId: newId.toString(),
|
||||
} as SurveyConf);
|
||||
|
||||
const result = await controller.createSurvey(surveyInfo, {
|
||||
user: { username: 'testUser', _id: new ObjectId() },
|
||||
@ -126,19 +128,15 @@ describe('SurveyController', () => {
|
||||
createFrom: existsSurveyId.toString(),
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'createSurveyMeta')
|
||||
.mockImplementation(() => {
|
||||
const result = {
|
||||
_id: new ObjectId(),
|
||||
} as SurveyMeta;
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
|
||||
_id: new ObjectId(),
|
||||
} as SurveyMeta);
|
||||
|
||||
const request = {
|
||||
user: { username: 'testUser', _id: new ObjectId() },
|
||||
surveyMeta: existsSurveyMeta,
|
||||
}; // 模拟请求对象,根据实际情况进行调整
|
||||
};
|
||||
|
||||
const result = await controller.createSurvey(params, request);
|
||||
expect(result?.data?.id).toBeDefined();
|
||||
});
|
||||
@ -159,6 +157,12 @@ describe('SurveyController', () => {
|
||||
jest
|
||||
.spyOn(surveyHistoryService, 'addHistory')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(sessionService, 'findLatestEditingOne')
|
||||
.mockResolvedValue(null);
|
||||
jest
|
||||
.spyOn(sessionService, 'updateSessionToEditing')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const reqBody = {
|
||||
surveyId: surveyId.toString(),
|
||||
@ -168,16 +172,31 @@ describe('SurveyController', () => {
|
||||
bannerConfig: {},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-01-23 21:59:05',
|
||||
beginTime: '2024-01-23 21:59:05',
|
||||
endTime: '2034-01-23 21:59:05',
|
||||
},
|
||||
bottomConf: { logoImage: '/imgs/Logo.webp', logoImageWidth: '60%' },
|
||||
skinConf: { skinColor: '#4a4c5b', inputBgColor: '#ffffff' },
|
||||
skinConf: {
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
},
|
||||
submitConf: {},
|
||||
dataConf: {
|
||||
dataList: [],
|
||||
},
|
||||
},
|
||||
sessionId: 'mock-session-id',
|
||||
};
|
||||
|
||||
const result = await controller.updateConf(reqBody, {
|
||||
@ -229,12 +248,10 @@ describe('SurveyController', () => {
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf),
|
||||
);
|
||||
.mockResolvedValue({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf);
|
||||
|
||||
const request = {
|
||||
user: { username: 'testUser', _id: new ObjectId() },
|
||||
@ -250,7 +267,7 @@ describe('SurveyController', () => {
|
||||
});
|
||||
|
||||
describe('publishSurvey', () => {
|
||||
it('should publish a survey success', async () => {
|
||||
it('should publish a survey successfully', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
@ -260,80 +277,24 @@ describe('SurveyController', () => {
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf),
|
||||
);
|
||||
.mockResolvedValue({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
code: {},
|
||||
} as SurveyConf);
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyContentByCode')
|
||||
.mockResolvedValue({
|
||||
text: '题目1',
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(contentSecurityService, 'isForbiddenContent')
|
||||
.mockResolvedValue(false);
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'publishSurveyMeta')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'publishResponseSchema')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(surveyHistoryService, 'addHistory')
|
||||
.mockResolvedValue(undefined);
|
||||
.mockResolvedValue({ text: '' });
|
||||
|
||||
const result = await controller.publishSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
{ user: { username: 'testUser', _id: 'testUserId' }, surveyMeta },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not publish a survey with forbidden content', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'normal',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf),
|
||||
);
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyContentByCode')
|
||||
.mockResolvedValue({
|
||||
text: '违禁词',
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(contentSecurityService, 'isForbiddenContent')
|
||||
.mockResolvedValue(true);
|
||||
|
||||
await expect(
|
||||
controller.publishSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
{ user: { username: 'testUser', _id: 'testUserId' }, surveyMeta },
|
||||
),
|
||||
).rejects.toThrow(
|
||||
new HttpException(
|
||||
'问卷存在非法关键字,不允许发布',
|
||||
EXCEPTION_CODE.SURVEY_CONTENT_NOT_ALLOW,
|
||||
),
|
||||
{
|
||||
user: { username: 'testUser', _id: new ObjectId() },
|
||||
surveyMeta,
|
||||
},
|
||||
);
|
||||
expect(result.code).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -42,7 +42,7 @@ describe('SurveyHistoryService', () => {
|
||||
msgContent: undefined,
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '',
|
||||
beginTime: '',
|
||||
endTime: '',
|
||||
answerBegTime: '',
|
||||
answerEndTime: '',
|
||||
@ -78,7 +78,12 @@ describe('SurveyHistoryService', () => {
|
||||
.spyOn(repository, 'save')
|
||||
.mockResolvedValueOnce({} as SurveyHistory);
|
||||
|
||||
await service.addHistory({ surveyId, schema, type, user });
|
||||
await service.addHistory({
|
||||
surveyId,
|
||||
schema,
|
||||
type,
|
||||
user,
|
||||
});
|
||||
|
||||
expect(spyCreate).toHaveBeenCalledWith({
|
||||
pageId: surveyId,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyMetaController } from '../controllers/surveyMeta.controller';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { LoggerProvider } from 'src/logger/logger.provider';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { CollaboratorService } from '../services/collaborator.service';
|
||||
@ -28,7 +28,12 @@ describe('SurveyMetaController', () => {
|
||||
.mockResolvedValue({ count: 0, data: [] }),
|
||||
},
|
||||
},
|
||||
LoggerProvider,
|
||||
{
|
||||
provide: Logger,
|
||||
useValue: {
|
||||
error() {},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CollaboratorService,
|
||||
useValue: {
|
||||
@ -116,6 +121,10 @@ describe('SurveyMetaController', () => {
|
||||
curStatus: {
|
||||
date: date,
|
||||
},
|
||||
subStatus: {
|
||||
date: date,
|
||||
},
|
||||
surveyType: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -132,18 +141,22 @@ describe('SurveyMetaController', () => {
|
||||
createDate: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
updateDate: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
curStatus: expect.objectContaining({
|
||||
date: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
}),
|
||||
subStatus: expect.objectContaining({
|
||||
date: expect.stringMatching(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
),
|
||||
}),
|
||||
surveyType: 'normal',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(surveyMetaService.getSurveyMetaList).toHaveBeenCalledWith({
|
||||
pageNum: queryInfo.curPage,
|
||||
pageSize: queryInfo.pageSize,
|
||||
@ -194,4 +207,24 @@ describe('SurveyMetaController', () => {
|
||||
workspaceId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle Joi validation in getList', async () => {
|
||||
const invalidQueryInfo: any = {
|
||||
curPage: 'invalid',
|
||||
pageSize: 10,
|
||||
};
|
||||
const req = {
|
||||
user: {
|
||||
username: 'test-user',
|
||||
_id: new ObjectId(),
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await controller.getList(invalidQueryInfo, req);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect(error.code).toBe(EXCEPTION_CODE.PARAMETER_ERROR);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -3,8 +3,8 @@ import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { SurveyUtilPlugin } from 'src/securityPlugin/surveyUtilPlugin';
|
||||
@ -13,7 +13,7 @@ import { ObjectId } from 'mongodb';
|
||||
describe('SurveyMetaService', () => {
|
||||
let service: SurveyMetaService;
|
||||
let surveyRepository: MongoRepository<SurveyMeta>;
|
||||
let pluginManager: XiaojuSurveyPluginManager;
|
||||
let pluginManager: PluginManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@ -37,9 +37,7 @@ describe('SurveyMetaService', () => {
|
||||
surveyRepository = module.get<MongoRepository<SurveyMeta>>(
|
||||
getRepositoryToken(SurveyMeta),
|
||||
);
|
||||
pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
pluginManager = module.get<PluginManager>(PluginManager);
|
||||
pluginManager.registerPlugin(new SurveyUtilPlugin());
|
||||
});
|
||||
|
||||
@ -100,6 +98,10 @@ describe('SurveyMetaService', () => {
|
||||
it('should edit a survey meta and return it if in NEW or EDITING status', async () => {
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() };
|
||||
survey.subStatus = {
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
date: Date.now(),
|
||||
};
|
||||
survey.statusList = [];
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
|
||||
|
||||
@ -118,6 +120,10 @@ describe('SurveyMetaService', () => {
|
||||
// 准备假的SurveyMeta对象
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
|
||||
survey.subStatus = {
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
date: Date.now(),
|
||||
};
|
||||
survey.statusList = [];
|
||||
|
||||
// 模拟save方法
|
||||
@ -128,7 +134,7 @@ describe('SurveyMetaService', () => {
|
||||
|
||||
// 验证结果
|
||||
expect(result).toBe(survey);
|
||||
expect(survey.curStatus.status).toBe(RECORD_STATUS.REMOVED);
|
||||
expect(survey.subStatus.status).toBe(RECORD_STATUS.REMOVED);
|
||||
expect(survey.statusList.length).toBe(1);
|
||||
expect(survey.statusList[0].status).toBe(RECORD_STATUS.REMOVED);
|
||||
expect(surveyRepository.save).toHaveBeenCalledTimes(1);
|
||||
@ -138,7 +144,10 @@ describe('SurveyMetaService', () => {
|
||||
it('should throw exception when survey is already removed', async () => {
|
||||
// 准备假的SurveyMeta对象,其状态已设置为REMOVED
|
||||
const survey = new SurveyMeta();
|
||||
survey.curStatus = { status: RECORD_STATUS.REMOVED, date: Date.now() };
|
||||
survey.curStatus = {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: Date.now(),
|
||||
};
|
||||
|
||||
// 调用要测试的方法并期待异常
|
||||
await expect(service.deleteSurveyMeta(survey)).rejects.toThrow(
|
||||
@ -195,6 +204,10 @@ describe('SurveyMetaService', () => {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
subStatus: {
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
date: expect.any(Number),
|
||||
},
|
||||
} as unknown as SurveyMeta;
|
||||
|
||||
jest.spyOn(surveyRepository, 'save').mockResolvedValue(savedSurveyMeta);
|
||||
|
@ -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,11 +184,15 @@ export class CollaboratorController {
|
||||
neIdList: collaboratorIdList,
|
||||
userIdList: newCollaboratorUserIdList,
|
||||
});
|
||||
this.logger.info('batchDelete:' + JSON.stringify(delRes), { req });
|
||||
this.logger.info('batchDelete:' + JSON.stringify(delRes));
|
||||
const username = req.user.username;
|
||||
const userId = req.user._id.toString();
|
||||
if (Array.isArray(newCollaborator) && newCollaborator.length > 0) {
|
||||
const insertRes = await this.collaboratorService.batchCreate({
|
||||
surveyId: value.surveyId,
|
||||
collaboratorList: newCollaborator,
|
||||
creator: username,
|
||||
creatorId: userId,
|
||||
});
|
||||
this.logger.info(`${JSON.stringify(insertRes)}`);
|
||||
}
|
||||
@ -198,6 +202,8 @@ export class CollaboratorController {
|
||||
this.collaboratorService.updateById({
|
||||
collaboratorId: item._id,
|
||||
permissions: item.permissions,
|
||||
operator: username,
|
||||
operatorId: userId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
@ -208,7 +214,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 +231,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 +268,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 +294,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 +321,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';
|
||||
@ -14,7 +13,7 @@ import { DataStatisticService } from '../services/dataStatistic.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
|
||||
import { Authentication } from 'src/guards/authentication.guard';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { SurveyGuard } from 'src/guards/survey.guard';
|
||||
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
@ -22,6 +21,7 @@ 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()
|
||||
@ -30,7 +30,7 @@ export class DataStatisticController {
|
||||
constructor(
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly dataStatisticService: DataStatisticService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly pluginManager: PluginManager,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@ -43,19 +43,18 @@ export class DataStatisticController {
|
||||
async data(
|
||||
@Query()
|
||||
queryInfo,
|
||||
@Request() req,
|
||||
) {
|
||||
const { value, error } = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
isDesensitive: Joi.boolean().default(true), // 默认true就是需要脱敏
|
||||
isMasked: Joi.boolean().default(true), // 默认true就是需要脱敏
|
||||
page: Joi.number().default(1),
|
||||
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;
|
||||
const { surveyId, isMasked, page, pageSize } = value;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const { total, listHead, listBody } =
|
||||
@ -66,10 +65,10 @@ export class DataStatisticController {
|
||||
pageSize,
|
||||
});
|
||||
|
||||
if (isDesensitive) {
|
||||
if (isMasked) {
|
||||
// 脱敏
|
||||
listBody.forEach((item) => {
|
||||
this.pluginManager.triggerHook('desensitiveData', item);
|
||||
this.pluginManager.triggerHook('maskData', item);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
189
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
189
server/src/modules/survey/controllers/downloadTask.controller.ts
Normal file
@ -0,0 +1,189 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
Post,
|
||||
Body,
|
||||
} 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 { Logger } 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: Logger,
|
||||
) {}
|
||||
|
||||
@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, isMasked } = value;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const id = await this.downloadTaskService.createDownloadTask({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
creatorId: req.user._id.toString(),
|
||||
creator: req.user.username,
|
||||
params: { isMasked },
|
||||
});
|
||||
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({
|
||||
creatorId: 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.status = data.status;
|
||||
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.createdAt = moment(data.createdAt).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.creatorId !== 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.creatorId !== req.user._id.toString()) {
|
||||
throw new NoPermissionException('没有权限');
|
||||
}
|
||||
|
||||
const delRes = await this.downloadTaskService.deleteDownloadTask({
|
||||
taskId,
|
||||
operator: req.user.username,
|
||||
operatorId: req.user._id.toString(),
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: delRes.modifiedCount === 1,
|
||||
};
|
||||
}
|
||||
}
|
90
server/src/modules/survey/controllers/session.controller.ts
Normal file
90
server/src/modules/survey/controllers/session.controller.ts
Normal file
@ -0,0 +1,90 @@
|
||||
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 { Logger } 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: Logger,
|
||||
) {}
|
||||
|
||||
@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, SurveyGuard)
|
||||
@SetMetadata('sessionId', 'body.sessionId')
|
||||
@SetMetadata('surveyId', 'surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async seize(
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const sessionInfo = req.sessionInfo;
|
||||
|
||||
await this.sessionService.updateSessionToEditing({
|
||||
sessionId: sessionInfo._id.toString(),
|
||||
surveyId: sessionInfo.surveyId,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
}
|
@ -31,6 +31,8 @@ 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 { UserService } from 'src/modules/auth/services/user.service';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/survey')
|
||||
@ -42,6 +44,8 @@ export class SurveyController {
|
||||
private readonly contentSecurityService: ContentSecurityService,
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
private readonly logger: Logger,
|
||||
private readonly sessionService: SessionService,
|
||||
private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
@Get('/getBannerData')
|
||||
@ -70,9 +74,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 +130,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.createdAt <= latestEditingOne.updatedAt) {
|
||||
// 在当前用户打开之后,被其他页面保存过了
|
||||
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({
|
||||
@ -164,8 +194,35 @@ export class SurveyController {
|
||||
async deleteSurvey(@Request() req) {
|
||||
const surveyMeta = req.surveyMeta;
|
||||
|
||||
await this.surveyMetaService.deleteSurveyMeta(surveyMeta);
|
||||
await this.responseSchemaService.deleteResponseSchema({
|
||||
const delMetaRes = await this.surveyMetaService.deleteSurveyMeta({
|
||||
surveyId: surveyMeta._id.toString(),
|
||||
operator: req.user.username,
|
||||
operatorId: req.user._id.toString(),
|
||||
});
|
||||
const delResponseRes =
|
||||
await this.responseSchemaService.deleteResponseSchema({
|
||||
surveyPath: surveyMeta.surveyPath,
|
||||
});
|
||||
|
||||
this.logger.info(JSON.stringify(delMetaRes));
|
||||
this.logger.info(JSON.stringify(delResponseRes));
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Post('/pausingSurvey')
|
||||
@UseGuards(SurveyGuard)
|
||||
@SetMetadata('surveyId', 'body.surveyId')
|
||||
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
|
||||
@UseGuards(Authentication)
|
||||
async pausingSurvey(@Request() req) {
|
||||
const surveyMeta = req.surveyMeta;
|
||||
|
||||
await this.surveyMetaService.pausingSurveyMeta(surveyMeta);
|
||||
await this.responseSchemaService.pausingResponseSchema({
|
||||
surveyPath: surveyMeta.surveyPath,
|
||||
});
|
||||
|
||||
@ -197,7 +254,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);
|
||||
}
|
||||
|
||||
@ -230,15 +287,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,12 +326,18 @@ 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;
|
||||
const surveyId = value.surveyId;
|
||||
const surveyMeta = req.surveyMeta;
|
||||
if (surveyMeta.isDeleted) {
|
||||
throw new HttpException(
|
||||
'问卷已删除,无法发布',
|
||||
EXCEPTION_CODE.SURVEY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
const surveyConf =
|
||||
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
|
||||
|
||||
@ -302,7 +363,8 @@ export class SurveyController {
|
||||
pageId: surveyId,
|
||||
});
|
||||
|
||||
await this.surveyHistoryService.addHistory({
|
||||
// 添加发布历史可以异步添加
|
||||
this.surveyHistoryService.addHistory({
|
||||
surveyId,
|
||||
schema: surveyConf.code,
|
||||
type: HISTORY_TYPE.PUBLISH_HIS,
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
SetMetadata,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import * as Joi from 'joi';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
@ -18,9 +17,8 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
|
||||
import { Logger } from 'src/logger';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
@ApiTags('survey')
|
||||
@Controller('/api/surveyHisotry')
|
||||
@Controller('/api/surveyHistory')
|
||||
export class SurveyHistoryController {
|
||||
constructor(
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
@ -51,16 +51,18 @@ 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;
|
||||
survey.title = value.title;
|
||||
survey.remark = value.remark;
|
||||
|
||||
await this.surveyMetaService.editSurveyMeta(survey);
|
||||
await this.surveyMetaService.editSurveyMeta({
|
||||
survey,
|
||||
operator: req.user.username,
|
||||
operatorId: req.user._id.toString(),
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
@ -81,7 +83,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 +93,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();
|
||||
@ -129,9 +131,10 @@ export class SurveyMetaController {
|
||||
if (!item.surveyType) {
|
||||
item.surveyType = item.questionType || 'normal';
|
||||
}
|
||||
item.createDate = moment(item.createDate).format(fmt);
|
||||
item.updateDate = moment(item.updateDate).format(fmt);
|
||||
item.createdAt = moment(item.createdAt).format(fmt);
|
||||
item.curStatus.date = moment(item.curStatus.date).format(fmt);
|
||||
item.subStatus.date = moment(item.subStatus.date).format(fmt);
|
||||
item.updatedAt = moment(item.updatedAt).format(fmt);
|
||||
const surveyId = item._id.toString();
|
||||
if (cooperSurveyIdMap[surveyId]) {
|
||||
item.isCollaborated = true;
|
||||
|
@ -12,10 +12,10 @@ export class CreateSurveyDto {
|
||||
surveyType: string;
|
||||
|
||||
@ApiProperty({ description: '创建方法', required: false })
|
||||
createMethod: string;
|
||||
createMethod?: string;
|
||||
|
||||
@ApiProperty({ description: '创建来源', required: false })
|
||||
createFrom: string;
|
||||
createFrom?: string;
|
||||
|
||||
@ApiProperty({ description: '问卷创建在哪个空间下', required: false })
|
||||
workspaceId?: string;
|
||||
|
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 })
|
||||
isMasked: boolean;
|
||||
|
||||
static validate(data) {
|
||||
return Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
isMasked: 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);
|
||||
}
|
||||
}
|
@ -22,12 +22,17 @@ export class CollaboratorService {
|
||||
return this.collaboratorRepository.save(collaborator);
|
||||
}
|
||||
|
||||
async batchCreate({ surveyId, collaboratorList }) {
|
||||
async batchCreate({ surveyId, collaboratorList, creator, creatorId }) {
|
||||
const now = new Date();
|
||||
const res = await this.collaboratorRepository.insertMany(
|
||||
collaboratorList.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
surveyId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
creator,
|
||||
creatorId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@ -60,7 +65,13 @@ export class CollaboratorService {
|
||||
return info;
|
||||
}
|
||||
|
||||
async changeUserPermission({ userId, surveyId, permission }) {
|
||||
async changeUserPermission({
|
||||
userId,
|
||||
surveyId,
|
||||
permission,
|
||||
operator,
|
||||
operatorId,
|
||||
}) {
|
||||
const updateRes = await this.collaboratorRepository.updateOne(
|
||||
{
|
||||
surveyId,
|
||||
@ -69,6 +80,9 @@ export class CollaboratorService {
|
||||
{
|
||||
$set: {
|
||||
permission,
|
||||
operator,
|
||||
operatorId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -134,7 +148,7 @@ export class CollaboratorService {
|
||||
return delRes;
|
||||
}
|
||||
|
||||
updateById({ collaboratorId, permissions }) {
|
||||
updateById({ collaboratorId, permissions, operator, operatorId }) {
|
||||
return this.collaboratorRepository.updateOne(
|
||||
{
|
||||
_id: new ObjectId(collaboratorId),
|
||||
@ -142,6 +156,9 @@ export class CollaboratorService {
|
||||
{
|
||||
$set: {
|
||||
permissions,
|
||||
operator,
|
||||
operatorId,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -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)
|
||||
@ -33,8 +34,8 @@ export class DataStatisticService {
|
||||
const dataListMap = keyBy(dataList, 'field');
|
||||
const where = {
|
||||
pageId: surveyId,
|
||||
'curStatus.status': {
|
||||
$ne: 'removed',
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
};
|
||||
const [surveyResponseList, total] =
|
||||
@ -43,7 +44,7 @@ export class DataStatisticService {
|
||||
take: pageSize,
|
||||
skip: (pageNum - 1) * pageSize,
|
||||
order: {
|
||||
createDate: -1,
|
||||
createdAt: -1,
|
||||
},
|
||||
});
|
||||
|
||||
@ -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,10 +90,10 @@ export class DataStatisticService {
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
difTime: (submitedData.difTime / 1000).toFixed(2),
|
||||
createDate: moment(submitedData.createDate).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
),
|
||||
diffTime: submitedData.diffTime
|
||||
? (submitedData.diffTime / 1000).toFixed(2)
|
||||
: '0',
|
||||
createdAt: moment(submitedData.createdAt).format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
});
|
||||
return {
|
||||
@ -123,8 +124,8 @@ export class DataStatisticService {
|
||||
{
|
||||
$match: {
|
||||
pageId: surveyId,
|
||||
'curStatus.status': {
|
||||
$ne: 'removed',
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
273
server/src/modules/survey/services/downloadTask.service.ts
Normal file
273
server/src/modules/survey/services/downloadTask.service.ts
Normal file
@ -0,0 +1,273 @@
|
||||
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 { 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 { Logger } from 'src/logger';
|
||||
import moment from 'moment';
|
||||
import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus';
|
||||
|
||||
@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: Logger,
|
||||
) {}
|
||||
|
||||
async createDownloadTask({
|
||||
surveyId,
|
||||
responseSchema,
|
||||
creatorId,
|
||||
creator,
|
||||
params,
|
||||
}: {
|
||||
surveyId: string;
|
||||
responseSchema: ResponseSchema;
|
||||
creatorId: string;
|
||||
creator: string;
|
||||
params: any;
|
||||
}) {
|
||||
const filename = `${responseSchema.title}-${params.isMasked ? '脱敏' : '原'}回收数据-${moment().format('YYYYMMDDHHmmss')}.xlsx`;
|
||||
const downloadTask = this.downloadTaskRepository.create({
|
||||
surveyId,
|
||||
surveyPath: responseSchema.surveyPath,
|
||||
fileSize: '计算中',
|
||||
creatorId,
|
||||
creator,
|
||||
params: {
|
||||
...params,
|
||||
title: responseSchema.title,
|
||||
},
|
||||
filename,
|
||||
status: DOWNLOAD_TASK_STATUS.WAITING,
|
||||
});
|
||||
await this.downloadTaskRepository.save(downloadTask);
|
||||
return downloadTask._id.toString();
|
||||
}
|
||||
|
||||
async getDownloadTaskList({
|
||||
creatorId,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}: {
|
||||
creatorId: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
}) {
|
||||
const where = {
|
||||
creatorId,
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
};
|
||||
const [surveyDownloadList, total] =
|
||||
await this.downloadTaskRepository.findAndCount({
|
||||
where,
|
||||
take: pageSize,
|
||||
skip: (pageIndex - 1) * pageSize,
|
||||
order: {
|
||||
createdAt: -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,
|
||||
operator,
|
||||
operatorId,
|
||||
}: {
|
||||
taskId: string;
|
||||
operator: string;
|
||||
operatorId: string;
|
||||
}) {
|
||||
return this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: new ObjectId(taskId),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
isDeleted: true,
|
||||
operator,
|
||||
operatorId,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
processDownloadTask({ taskId }) {
|
||||
DownloadTaskService.taskList.push(taskId);
|
||||
if (!DownloadTaskService.isExecuting) {
|
||||
this.executeTask();
|
||||
DownloadTaskService.isExecuting = true;
|
||||
}
|
||||
}
|
||||
|
||||
async executeTask() {
|
||||
try {
|
||||
while (DownloadTaskService.taskList.length > 0) {
|
||||
const taskId = DownloadTaskService.taskList.shift();
|
||||
this.logger.info(`handle taskId: ${taskId}`);
|
||||
const taskInfo = await this.getDownloadTaskById({ taskId });
|
||||
if (!taskInfo || taskInfo.isDeleted) {
|
||||
// 不存在或者已删除的,不处理
|
||||
continue;
|
||||
}
|
||||
await this.handleDownloadTask({ taskInfo });
|
||||
}
|
||||
} finally {
|
||||
DownloadTaskService.isExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDownloadTask({ taskInfo }) {
|
||||
try {
|
||||
// 更新任务状态为计算中
|
||||
const updateRes = await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status: DOWNLOAD_TASK_STATUS.COMPUTING,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.info(JSON.stringify(updateRes));
|
||||
|
||||
// 开始计算任务
|
||||
const surveyId = taskInfo.surveyId;
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const where = {
|
||||
pageId: surveyId,
|
||||
};
|
||||
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, '');
|
||||
if (typeof val === 'string') {
|
||||
const $ = load(val);
|
||||
const text = $.text();
|
||||
bodyData.push(text);
|
||||
} else {
|
||||
bodyData.push(val);
|
||||
}
|
||||
}
|
||||
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',
|
||||
filename: taskInfo.filename,
|
||||
});
|
||||
|
||||
// 更新计算结果
|
||||
const updateFinishRes = await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status: DOWNLOAD_TASK_STATUS.SUCCEED,
|
||||
url,
|
||||
fileKey: key,
|
||||
fileSize: buffer.length,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
this.logger.info(JSON.stringify(updateFinishRes));
|
||||
} catch (error) {
|
||||
await this.downloadTaskRepository.updateOne(
|
||||
{
|
||||
_id: taskInfo._id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status: DOWNLOAD_TASK_STATUS.FAILED,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
this.logger.error(
|
||||
`导出文件失败 taskId: ${taskInfo._id.toString()}, surveyId: ${taskInfo.surveyId}, message: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
68
server/src/modules/survey/services/session.service.ts
Normal file
68
server/src/modules/survey/services/session.service.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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 { SESSION_STATUS } from 'src/enums/surveySessionStatus';
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
constructor(
|
||||
@InjectRepository(Session)
|
||||
private readonly sessionRepository: MongoRepository<Session>,
|
||||
) {}
|
||||
|
||||
create({ surveyId, userId }) {
|
||||
const session = this.sessionRepository.create({
|
||||
surveyId,
|
||||
userId,
|
||||
status: SESSION_STATUS.DEACTIVATED,
|
||||
});
|
||||
return this.sessionRepository.save(session);
|
||||
}
|
||||
|
||||
findOne(sessionId) {
|
||||
return this.sessionRepository.findOne({
|
||||
where: {
|
||||
_id: new ObjectId(sessionId),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findLatestEditingOne({ surveyId }) {
|
||||
return this.sessionRepository.findOne({
|
||||
where: {
|
||||
surveyId,
|
||||
status: SESSION_STATUS.ACTIVATED,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateSessionToEditing({ sessionId, surveyId }) {
|
||||
return Promise.all([
|
||||
this.sessionRepository.update(
|
||||
{
|
||||
_id: new ObjectId(sessionId),
|
||||
},
|
||||
{
|
||||
status: SESSION_STATUS.ACTIVATED,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
),
|
||||
this.sessionRepository.updateMany(
|
||||
{
|
||||
surveyId,
|
||||
_id: {
|
||||
$ne: new ObjectId(sessionId),
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
status: SESSION_STATUS.DEACTIVATED,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
@ -45,9 +45,9 @@ export class SurveyHistoryService {
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
createDate: -1,
|
||||
createdAt: -1,
|
||||
},
|
||||
select: ['createDate', 'operator', 'type', '_id'],
|
||||
select: ['createdAt', 'operator', 'type', '_id'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,18 +2,18 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository, FindOptionsOrder } from 'typeorm';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
|
||||
@Injectable()
|
||||
export class SurveyMetaService {
|
||||
constructor(
|
||||
@InjectRepository(SurveyMeta)
|
||||
private readonly surveyRepository: MongoRepository<SurveyMeta>,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly pluginManager: PluginManager,
|
||||
) {}
|
||||
|
||||
async getNewSurveyPath(): Promise<string> {
|
||||
@ -65,6 +65,7 @@ export class SurveyMetaService {
|
||||
surveyType: surveyType,
|
||||
surveyPath,
|
||||
creator: username,
|
||||
creatorId: userId,
|
||||
owner: username,
|
||||
ownerId: userId,
|
||||
createMethod,
|
||||
@ -75,11 +76,36 @@ export class SurveyMetaService {
|
||||
return await this.surveyRepository.save(newSurvey);
|
||||
}
|
||||
|
||||
async editSurveyMeta(survey: SurveyMeta) {
|
||||
if (
|
||||
survey.curStatus.status !== RECORD_STATUS.NEW &&
|
||||
survey.curStatus.status !== RECORD_STATUS.EDITING
|
||||
) {
|
||||
async pausingSurveyMeta(survey: SurveyMeta) {
|
||||
if (survey?.curStatus?.status === RECORD_STATUS.NEW) {
|
||||
throw new HttpException(
|
||||
'问卷不能暂停',
|
||||
EXCEPTION_CODE.SURVEY_STATUS_TRANSFORM_ERROR,
|
||||
);
|
||||
}
|
||||
const subCurStatus = {
|
||||
status: RECORD_SUB_STATUS.PAUSING,
|
||||
date: Date.now(),
|
||||
};
|
||||
survey.subStatus = subCurStatus;
|
||||
if (Array.isArray(survey.statusList)) {
|
||||
survey.statusList.push(subCurStatus);
|
||||
} else {
|
||||
survey.statusList = [subCurStatus];
|
||||
}
|
||||
return this.surveyRepository.save(survey);
|
||||
}
|
||||
|
||||
async editSurveyMeta({
|
||||
survey,
|
||||
operator,
|
||||
operatorId,
|
||||
}: {
|
||||
survey: SurveyMeta;
|
||||
operator: string;
|
||||
operatorId: string;
|
||||
}) {
|
||||
if (survey?.curStatus?.status !== RECORD_STATUS.EDITING) {
|
||||
const newStatus = {
|
||||
status: RECORD_STATUS.EDITING,
|
||||
date: Date.now(),
|
||||
@ -87,27 +113,26 @@ export class SurveyMetaService {
|
||||
survey.curStatus = newStatus;
|
||||
survey.statusList.push(newStatus);
|
||||
}
|
||||
survey.updatedAt = new Date();
|
||||
survey.operator = operator;
|
||||
survey.operatorId = operatorId;
|
||||
return this.surveyRepository.save(survey);
|
||||
}
|
||||
|
||||
async deleteSurveyMeta(survey: SurveyMeta) {
|
||||
if (survey.curStatus.status === RECORD_STATUS.REMOVED) {
|
||||
throw new HttpException(
|
||||
'问卷已删除,不能重复删除',
|
||||
EXCEPTION_CODE.SURVEY_STATUS_TRANSFORM_ERROR,
|
||||
);
|
||||
}
|
||||
const newStatusInfo = {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: Date.now(),
|
||||
};
|
||||
survey.curStatus = newStatusInfo;
|
||||
if (Array.isArray(survey.statusList)) {
|
||||
survey.statusList.push(newStatusInfo);
|
||||
} else {
|
||||
survey.statusList = [newStatusInfo];
|
||||
}
|
||||
return this.surveyRepository.save(survey);
|
||||
async deleteSurveyMeta({ surveyId, operator, operatorId }) {
|
||||
return this.surveyRepository.updateOne(
|
||||
{
|
||||
_id: new ObjectId(surveyId),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
isDeleted: true,
|
||||
operator,
|
||||
operatorId,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getSurveyMetaList(condition: {
|
||||
@ -125,14 +150,16 @@ export class SurveyMetaService {
|
||||
const skip = (pageNum - 1) * pageSize;
|
||||
try {
|
||||
const query: Record<string, any> = Object.assign(
|
||||
{},
|
||||
{
|
||||
'curStatus.status': {
|
||||
$ne: 'removed',
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
},
|
||||
condition.filter,
|
||||
);
|
||||
if (condition.filter['curStatus.status']) {
|
||||
query['subStatus.status'] = RECORD_SUB_STATUS.DEFAULT;
|
||||
}
|
||||
if (workspaceId) {
|
||||
query.workspaceId = workspaceId;
|
||||
} else {
|
||||
@ -160,9 +187,8 @@ export class SurveyMetaService {
|
||||
condition.order && Object.keys(condition.order).length > 0
|
||||
? (condition.order as FindOptionsOrder<SurveyMeta>)
|
||||
: ({
|
||||
createDate: -1,
|
||||
createdAt: -1,
|
||||
} as FindOptionsOrder<SurveyMeta>);
|
||||
|
||||
const [data, count] = await this.surveyRepository.findAndCount({
|
||||
where: query,
|
||||
skip,
|
||||
@ -181,6 +207,10 @@ export class SurveyMetaService {
|
||||
date: Date.now(),
|
||||
};
|
||||
surveyMeta.curStatus = curStatus;
|
||||
surveyMeta.subStatus = {
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
date: Date.now(),
|
||||
};
|
||||
if (Array.isArray(surveyMeta.statusList)) {
|
||||
surveyMeta.statusList.push(curStatus);
|
||||
} else {
|
||||
@ -192,8 +222,8 @@ export class SurveyMetaService {
|
||||
async countSurveyMetaByWorkspaceId({ workspaceId }) {
|
||||
const total = await this.surveyRepository.count({
|
||||
workspaceId,
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
isDeleted: {
|
||||
$ne: true,
|
||||
},
|
||||
});
|
||||
return total;
|
||||
|
@ -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 {}
|
||||
|
@ -43,7 +43,6 @@
|
||||
"options": [
|
||||
{
|
||||
"text": "选项1",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
@ -52,7 +51,6 @@
|
||||
},
|
||||
{
|
||||
"text": "选项2",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
|
@ -47,7 +47,6 @@
|
||||
{
|
||||
"text": "课程1",
|
||||
"hash": "115019",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
@ -56,7 +55,6 @@
|
||||
{
|
||||
"text": "课程2",
|
||||
"hash": "115020",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
@ -65,7 +63,6 @@
|
||||
{
|
||||
"text": "课程3",
|
||||
"hash": "115021",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
@ -74,7 +71,6 @@
|
||||
{
|
||||
"text": "课程4",
|
||||
"hash": "115022",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
|
@ -41,12 +41,11 @@
|
||||
"innerType": "radio",
|
||||
"field": "data606",
|
||||
"title": "标题2",
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"minNum": 0,
|
||||
"maxNum": 0,
|
||||
"options": [
|
||||
{
|
||||
"text": "选项1",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
@ -55,7 +54,6 @@
|
||||
},
|
||||
{
|
||||
"text": "选项2",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
|
@ -1,55 +1,69 @@
|
||||
{
|
||||
"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": "<p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style=\"color: rgb(204, 0, 0)\">期待您的参与!</span></p>"
|
||||
},
|
||||
"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",
|
||||
"bgImageAllowJump": false,
|
||||
"bgImageJumpLink": "",
|
||||
"videoLink": "",
|
||||
"postImg": ""
|
||||
}
|
||||
},
|
||||
"submitConf": {
|
||||
"submitTitle": "提交",
|
||||
"confirmAgain": {
|
||||
"is_again": true,
|
||||
"again_text": "确认要提交吗?"
|
||||
},
|
||||
"msgContent": {
|
||||
"msg_200": "提交成功",
|
||||
"msg_9001": "您来晚了,感谢支持问卷~",
|
||||
"msg_9002": "请勿多次提交!",
|
||||
"msg_9003": "您来晚了,已经满额!",
|
||||
"msg_9004": "提交失败!"
|
||||
},
|
||||
"link": ""
|
||||
},
|
||||
"bottomConf": {
|
||||
"logoImage": "/imgs/Logo.webp",
|
||||
"logoImageWidth": "60%"
|
||||
},
|
||||
"baseConf": {
|
||||
"beginTime": "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",
|
||||
"passwordSwitch": false,
|
||||
"password": "",
|
||||
"whitelistType": "ALL",
|
||||
"whitelist": [],
|
||||
"memberType": "MOBILE",
|
||||
"fillAnswer": false,
|
||||
"fillSubmitAnswer": false
|
||||
},
|
||||
"skinConf": {
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff",
|
||||
"backgroundConf": {
|
||||
"color": "#b8dbff",
|
||||
"type": "color",
|
||||
"image": ""
|
||||
},
|
||||
"themeConf": {
|
||||
"color": "#ffa600"
|
||||
},
|
||||
"contentConf": {
|
||||
"opacity": 100
|
||||
}
|
||||
},
|
||||
"pageConf": [],
|
||||
"logicConf": {
|
||||
"showLogicConf": [],
|
||||
"jumpLogicConf": []
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
@ -21,7 +22,7 @@ export async function getSchemaBySurveyType(surveyType: string) {
|
||||
}
|
||||
const code = Object.assign({}, templateBase, codeData);
|
||||
const nowMoment = moment();
|
||||
code.baseConf.begTime = nowMoment.format('YYYY-MM-DD HH:mm:ss');
|
||||
code.baseConf.beginTime = nowMoment.format('YYYY-MM-DD HH:mm:ss');
|
||||
code.baseConf.endTime = nowMoment
|
||||
.add(10, 'years')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
@ -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',
|
||||
field: 'createdAt',
|
||||
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);
|
||||
|
@ -3,7 +3,6 @@ import { MongoRepository } from 'typeorm';
|
||||
import { ClientEncryptService } from '../services/clientEncrypt.service';
|
||||
import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
|
||||
import { ENCRYPT_TYPE } from 'src/enums/encrypt';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
@ -88,9 +87,6 @@ describe('ClientEncryptService', () => {
|
||||
expect(repository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(encryptInfo);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
export const mockResponseSchema: ResponseSchema = {
|
||||
@ -8,6 +8,10 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: 1710399368439,
|
||||
},
|
||||
subStatus: {
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
date: 1710399368439,
|
||||
},
|
||||
statusList: [
|
||||
{
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
@ -32,7 +36,7 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '2024-03-14 14:54:41',
|
||||
beginTime: '2024-03-14 14:54:41',
|
||||
endTime: '2034-03-14 14:54:41',
|
||||
language: 'chinese',
|
||||
tLimit: 10,
|
||||
@ -44,6 +48,17 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
@ -60,6 +75,11 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
is_again: true,
|
||||
again_text: '确认要提交吗?',
|
||||
},
|
||||
jumpConfig: {
|
||||
type: 'link',
|
||||
link: '',
|
||||
buttonText: '',
|
||||
},
|
||||
},
|
||||
dataConf: {
|
||||
dataList: [
|
||||
@ -236,4 +256,4 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
},
|
||||
},
|
||||
pageId: '65f29f3192862d6a9067ad1c',
|
||||
} as ResponseSchema;
|
||||
} as unknown as ResponseSchema;
|
||||
|
@ -3,9 +3,14 @@ import { ResponseSchemaController } from '../controllers/responseSchema.controll
|
||||
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 { RECORD_STATUS, RECORD_SUB_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);
|
||||
@ -31,6 +69,7 @@ describe('ResponseSchemaController', () => {
|
||||
const mockResponseSchema = {
|
||||
surveyPath: 'testSurveyPath',
|
||||
curStatus: { status: RECORD_STATUS.PUBLISHED, date: Date.now() },
|
||||
subStatus: { status: RECORD_SUB_STATUS.DEFAULT, date: Date.now() },
|
||||
} as ResponseSchema;
|
||||
|
||||
jest
|
||||
@ -59,12 +98,168 @@ describe('ResponseSchemaController', () => {
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
|
||||
.mockResolvedValue({
|
||||
curStatus: { status: RECORD_STATUS.REMOVED },
|
||||
subStatus: { status: RECORD_SUB_STATUS.REMOVED },
|
||||
} as ResponseSchema);
|
||||
|
||||
await expect(controller.getSchema(mockQueryInfo)).rejects.toThrow(
|
||||
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',
|
||||
},
|
||||
subStatus: {
|
||||
status: '',
|
||||
},
|
||||
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',
|
||||
},
|
||||
subStatus: {
|
||||
status: '',
|
||||
},
|
||||
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',
|
||||
},
|
||||
subStatus: {
|
||||
status: '',
|
||||
},
|
||||
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',
|
||||
},
|
||||
subStatus: {
|
||||
status: '',
|
||||
},
|
||||
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',
|
||||
},
|
||||
subStatus: {
|
||||
status: '',
|
||||
},
|
||||
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 });
|
||||
});
|
||||
});
|
||||
|
@ -13,14 +13,18 @@ import { ClientEncryptService } from '../services/clientEncrypt.service';
|
||||
import { MessagePushingTaskService } from 'src/modules/message/services/messagePushingTask.service';
|
||||
|
||||
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { PluginManager } from 'src/securityPlugin/pluginManager';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
|
||||
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
import { Logger } 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 = {
|
||||
@ -124,6 +128,18 @@ describe('SurveyResponseController', () => {
|
||||
info: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
getUserByUsername: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: WorkspaceMemberService,
|
||||
useValue: {
|
||||
findAllByUserId: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@ -137,9 +153,7 @@ describe('SurveyResponseController', () => {
|
||||
clientEncryptService =
|
||||
module.get<ClientEncryptService>(ClientEncryptService);
|
||||
|
||||
const pluginManager = module.get<XiaojuSurveyPluginManager>(
|
||||
XiaojuSurveyPluginManager,
|
||||
);
|
||||
const pluginManager = module.get<PluginManager>(PluginManager);
|
||||
pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin('dataAesEncryptSecretKey'),
|
||||
);
|
||||
@ -169,7 +183,7 @@ describe('SurveyResponseController', () => {
|
||||
status: RECORD_STATUS.NEW,
|
||||
date: 1711025113146,
|
||||
},
|
||||
difTime: 30518,
|
||||
diffTime: 30518,
|
||||
data: {
|
||||
data458: '15000000000',
|
||||
data515: '115019',
|
||||
@ -205,7 +219,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 +238,7 @@ describe('SurveyResponseController', () => {
|
||||
data770: '123456@qq.com',
|
||||
},
|
||||
clientTime: reqBody.clientTime,
|
||||
difTime: reqBody.difTime,
|
||||
diffTime: reqBody.diffTime,
|
||||
surveyId: mockResponseSchema.pageId,
|
||||
optionTextAndId: {
|
||||
data515: [
|
||||
@ -252,7 +266,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 +275,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 +288,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 +304,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 +316,38 @@ 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,
|
||||
},
|
||||
subStatus: {
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
},
|
||||
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,
|
||||
@ -77,7 +77,7 @@ describe('SurveyResponseService', () => {
|
||||
expect(surveyResponseRepository.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
surveyPath,
|
||||
'curStatus.status': { $ne: 'removed' },
|
||||
'subStatus.status': { $ne: 'removed' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -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 { RECORD_SUB_STATUS } from 'src/enums';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import Joi from 'joi';
|
||||
import { Logger } 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: Logger,
|
||||
private readonly userService: UserService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
@Get('/getSchema')
|
||||
@HttpCode(200)
|
||||
@ -25,18 +44,95 @@ export class ResponseSchemaController {
|
||||
await this.responseSchemaService.getResponseSchemaByPath(
|
||||
queryInfo.surveyPath,
|
||||
);
|
||||
if (
|
||||
!responseSchema ||
|
||||
responseSchema.curStatus.status === RECORD_STATUS.REMOVED
|
||||
) {
|
||||
if (!responseSchema || responseSchema.isDeleted) {
|
||||
throw new HttpException(
|
||||
'问卷已删除',
|
||||
'问卷不存在或已删除',
|
||||
EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED,
|
||||
);
|
||||
}
|
||||
|
||||
if (responseSchema.subStatus.status === RECORD_SUB_STATUS.PAUSING) {
|
||||
throw new HttpException(
|
||||
'该问卷已暂停回收',
|
||||
EXCEPTION_CODE.RESPONSE_PAUSING,
|
||||
);
|
||||
}
|
||||
|
||||
// 去掉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.isDeleted) {
|
||||
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,38 +1,55 @@
|
||||
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';
|
||||
import { ENCRYPT_TYPE } from 'src/enums/encrypt';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { getPushingData } from 'src/utils/messagePushing';
|
||||
import { RECORD_SUB_STATUS } from 'src/enums';
|
||||
|
||||
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 { CounterService } from '../services/counter.service';
|
||||
import { Logger } 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 counterService: CounterService,
|
||||
private readonly logger: Logger,
|
||||
// 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,34 +59,92 @@ 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 =
|
||||
await this.responseSchemaService.getResponseSchemaByPath(surveyPath);
|
||||
if (!responseSchema || responseSchema.curStatus.status === 'removed') {
|
||||
if (!responseSchema || responseSchema.isDeleted) {
|
||||
throw new SurveyNotFoundException('该问卷不存在,无法提交');
|
||||
}
|
||||
if (responseSchema?.subStatus?.status === RECORD_SUB_STATUS.PAUSING) {
|
||||
throw new HttpException(
|
||||
'该问卷已暂停,无法提交',
|
||||
EXCEPTION_CODE.RESPONSE_PAUSING,
|
||||
);
|
||||
}
|
||||
|
||||
// 白名单的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;
|
||||
const beginTime = responseSchema.code?.baseConf?.beginTime || 0;
|
||||
const endTime = responseSchema?.code?.baseConf?.endTime || 0;
|
||||
if (begTime && endTime) {
|
||||
const begTimeStamp = new Date(begTime).getTime();
|
||||
if (beginTime && endTime) {
|
||||
const beginTimeStamp = new Date(beginTime).getTime();
|
||||
const endTimeStamp = new Date(endTime).getTime();
|
||||
if (now < begTimeStamp || now > endTimeStamp) {
|
||||
if (now < beginTimeStamp || now > endTimeStamp) {
|
||||
throw new HttpException(
|
||||
'不在答题有效期内',
|
||||
EXCEPTION_CODE.RESPONSE_CURRENT_TIME_NOT_ALLOW,
|
||||
@ -146,6 +221,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]
|
||||
@ -160,34 +236,55 @@ export class SurveyResponseController {
|
||||
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 {
|
||||
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) {
|
||||
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}`);
|
||||
// }
|
||||
|
||||
// 入库
|
||||
const surveyResponse =
|
||||
@ -195,12 +292,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 || [],
|
||||
|
@ -4,7 +4,6 @@ import { MongoRepository } from 'typeorm';
|
||||
import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
|
||||
import { ENCRYPT_TYPE } from 'src/enums/encrypt';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
|
||||
@Injectable()
|
||||
export class ClientEncryptService {
|
||||
@ -38,26 +37,13 @@ export class ClientEncryptService {
|
||||
return this.clientEncryptRepository.findOne({
|
||||
where: {
|
||||
_id: new ObjectId(id),
|
||||
'curStatus.status': {
|
||||
$ne: RECORD_STATUS.REMOVED,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteEncryptInfo(id: string) {
|
||||
return this.clientEncryptRepository.updateOne(
|
||||
{
|
||||
_id: new ObjectId(id),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
curStatus: {
|
||||
status: RECORD_STATUS.REMOVED,
|
||||
date: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
return this.clientEncryptRepository.deleteOne({
|
||||
_id: new ObjectId(id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ export class CounterService {
|
||||
surveyPath,
|
||||
type,
|
||||
data,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||
import { RECORD_STATUS } from 'src/enums';
|
||||
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseSchemaService {
|
||||
@ -23,19 +23,27 @@ export class ResponseSchemaService {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: Date.now(),
|
||||
};
|
||||
clientSurvey.subStatus = {
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
date: Date.now(),
|
||||
};
|
||||
return this.responseSchemaRepository.save(clientSurvey);
|
||||
} else {
|
||||
const curStatus = {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
date: Date.now(),
|
||||
};
|
||||
const subStatus = {
|
||||
status: RECORD_SUB_STATUS.DEFAULT,
|
||||
date: Date.now(),
|
||||
};
|
||||
const newClientSurvey = this.responseSchemaRepository.create({
|
||||
title,
|
||||
surveyPath,
|
||||
code,
|
||||
pageId,
|
||||
curStatus,
|
||||
statusList: [curStatus],
|
||||
subStatus,
|
||||
});
|
||||
return this.responseSchemaRepository.save(newClientSurvey);
|
||||
}
|
||||
@ -53,22 +61,32 @@ export class ResponseSchemaService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteResponseSchema({ surveyPath }) {
|
||||
async pausingResponseSchema({ surveyPath }) {
|
||||
const responseSchema = await this.responseSchemaRepository.findOne({
|
||||
where: { surveyPath },
|
||||
});
|
||||
if (responseSchema) {
|
||||
const newStatus = {
|
||||
status: RECORD_STATUS.PUBLISHED,
|
||||
const subStatus = {
|
||||
status: RECORD_SUB_STATUS.PAUSING,
|
||||
date: Date.now(),
|
||||
};
|
||||
responseSchema.curStatus = newStatus;
|
||||
if (Array.isArray(responseSchema.statusList)) {
|
||||
responseSchema.statusList.push(newStatus);
|
||||
} else {
|
||||
responseSchema.statusList = [newStatus];
|
||||
}
|
||||
responseSchema.subStatus = subStatus;
|
||||
responseSchema.curStatus.status = RECORD_STATUS.PUBLISHED;
|
||||
return this.responseSchemaRepository.save(responseSchema);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteResponseSchema({ surveyPath }) {
|
||||
return this.responseSchemaRepository.updateOne(
|
||||
{
|
||||
surveyPath,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
isDeleted: true,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||
|
||||
@Injectable()
|
||||
export class SurveyResponseService {
|
||||
constructor(
|
||||
@ -12,7 +13,7 @@ export class SurveyResponseService {
|
||||
async createSurveyResponse({
|
||||
data,
|
||||
clientTime,
|
||||
difTime,
|
||||
diffTime,
|
||||
surveyId,
|
||||
surveyPath,
|
||||
optionTextAndId,
|
||||
@ -22,7 +23,7 @@ export class SurveyResponseService {
|
||||
data,
|
||||
secretKeys: [],
|
||||
clientTime,
|
||||
difTime,
|
||||
diffTime,
|
||||
pageId: surveyId,
|
||||
optionTextAndId,
|
||||
});
|
||||
@ -35,14 +36,11 @@ export class SurveyResponseService {
|
||||
}
|
||||
|
||||
async getSurveyResponseTotalByPath(surveyPath: string) {
|
||||
const count = await this.surveyResponseRepository.count({
|
||||
const data = await this.surveyResponseRepository.find({
|
||||
where: {
|
||||
surveyPath,
|
||||
'curStatus.status': {
|
||||
$ne: 'removed',
|
||||
},
|
||||
},
|
||||
});
|
||||
return count;
|
||||
return (data || []).length;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user