Compare commits
7 Commits
main
...
feature/se
Author | SHA1 | Date | |
---|---|---|---|
|
ab17f0ef97 | ||
|
5e8de461bb | ||
|
d8b80bca1b | ||
|
c942854477 | ||
|
3992c23f03 | ||
|
6ce03e6ab9 | ||
|
393528fbe7 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
dist
|
||||
|
||||
package-lock.json
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@ -23,4 +24,4 @@ pnpm-debug.log*
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.history
|
||||
.history
|
||||
|
19
Dockerfile
19
Dockerfile
@ -1,16 +1,5 @@
|
||||
# 镜像集成
|
||||
FROM ubuntu:latest
|
||||
|
||||
# 安装依赖
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y install wget gcc
|
||||
|
||||
# 安装node环境
|
||||
ENV NODE_VERSION v18.17.1
|
||||
RUN mkdir -p /node/$NODE_VERSION
|
||||
RUN wget https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.gz
|
||||
RUN tar xzf node-$NODE_VERSION-linux-x64.tar.gz -C /node/
|
||||
ENV PATH /node/node-$NODE_VERSION-linux-x64/bin:$PATH
|
||||
FROM node:16
|
||||
|
||||
# 设置工作区间
|
||||
WORKDIR /xiaoju-survey
|
||||
@ -22,10 +11,10 @@ RUN npm config set registry https://registry.npmjs.org/
|
||||
|
||||
# 安装项目依赖
|
||||
RUN cd /xiaoju-survey/web && npm install && npm run build
|
||||
# 用了后端服务代理启动,建议使用nginx启动
|
||||
RUN cd /xiaoju-survey && cp -af ./web/dist/* ./server/public/
|
||||
|
||||
RUN cd /xiaoju-survey && cp -af ./web/dist/* ./server/src/apps/ui/public/
|
||||
|
||||
RUN cd /xiaoju-survey/server && npm install && npm run copy && npm run build
|
||||
RUN cd /xiaoju-survey/server && npm install && npm run build
|
||||
|
||||
# 暴露端口 需要跟server的port一致
|
||||
EXPOSE 3000
|
||||
|
47
README.md
47
README.md
@ -23,7 +23,7 @@
|
||||
|
||||
<br />
|
||||
|
||||
  **XiaoJuSurvey**是一套轻量、安全的问卷系统,提供面向个人和企业的一站式产品级解决方案,快速满足各类线上调研场景。
|
||||
  **XIAOJUSURVEY**是一套轻量、安全的**问卷系统**,提供面向个人和企业的一站式产品级解决方案,快速满足各类线上调研场景。
|
||||
|
||||
  系统已沉淀40+种题型,累积精选模板100+,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,经过上亿量级打磨,沉淀了分题统计、交叉分析、多渠道分析等在线报表能力,快速满足专业化分析。
|
||||
|
||||
@ -34,16 +34,26 @@
|
||||
|
||||
- 多样化题型:单行输入框、多行输入框、单项选择、多项选择、判断题、评分、投票
|
||||
|
||||
_(更多题型将陆续开放,也欢迎您参与共建提交自定义题型)_
|
||||
_(更多题型将陆续开放。快速[自定义题型](https://xiaojusurvey.didi.cn/docs/document/%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/%E9%A2%98%E5%9E%8B%E6%89%A9%E5%B1%95))_
|
||||
|
||||
- 用户管理:登录、注册、权限管理
|
||||
|
||||
- 数据安全:传输加密、脱敏等
|
||||
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/nJ5fyGhocH1698903177499.png" width="900" />
|
||||
> 查阅[官方Feature](https://github.com/didi/xiaoju-survey/issues/45)
|
||||
|
||||
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" />
|
||||
|
||||
_**(个人和企业用户均可快速构建特定领域的调研类解决方案。)**_
|
||||
|
||||
# 技术
|
||||
Web端:Vue2(Vue3版本24年上半年推出)+ ElementUI
|
||||
|
||||
Server端:Nestjs + MongoDB
|
||||
|
||||
架构:[架构解读](https://xiaojusurvey.didi.cn/docs/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E6%9E%B6%E6%9E%84)
|
||||
|
||||
|
||||
# 项目优势
|
||||
**一、具备全面的综合性和专业性**
|
||||
|
||||
@ -91,7 +101,7 @@ _**(个人和企业用户均可快速构建特定领域的调研类解决方案
|
||||
|
||||
# 快速启动
|
||||
|
||||
Node版本>=14.21.0,
|
||||
Node版本 >= 16.x,
|
||||
[查看环境准备指导](https://xiaojusurvey.didi.cn/docs/document/%E6%A6%82%E8%BF%B0/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
|
||||
|
||||
复制工程
|
||||
@ -102,7 +112,7 @@ git clone git@github.com:didi/xiaoju-survey.git
|
||||
## 服务端启动
|
||||
|
||||
### 方案一、快速启动,无需安装数据库
|
||||
_便于快速预览工程,对于正式项目需要使用方案二。_
|
||||
> _便于快速预览工程,对于正式项目需要使用方案二。_
|
||||
|
||||
#### 1、安装依赖
|
||||
```shell
|
||||
@ -115,33 +125,17 @@ npm install
|
||||
npm run local
|
||||
```
|
||||
|
||||
:cyclone:NOTE:
|
||||
|
||||
服务运行依赖 [mongodb-memory-server](https://github.com/nodkz/mongodb-memory-server):
|
||||
> 服务运行依赖 [mongodb-memory-server](https://github.com/nodkz/mongodb-memory-server):
|
||||
>
|
||||
> 1、数据保存在内存中,重启服务会更新数据。<br />2、启动内存服务器新实例时,如果找不到MongoDB二进制文件会自动下载,因此首次可能需要一些时间。
|
||||
|
||||
### 方案二、(推荐)
|
||||
### 方案二、(生产推荐)
|
||||
|
||||
#### 1、启动数据库
|
||||
|
||||
项目使用MongoDB:
|
||||
> 项目使用MongoDB:[MongoDB安装指导](https://xiaojusurvey.didi.cn/docs/document/%E6%A6%82%E8%BF%B0/%E5%AE%89%E8%A3%85%E7%8E%AF%E5%A2%83)
|
||||
|
||||
> 没有安装可以查看 [MongoDB安装指导](https://xiaojusurvey.didi.cn/docs/document/%E6%A6%82%E8%BF%B0/%E5%AE%89%E8%A3%85%E7%8E%AF%E5%A2%83)
|
||||
|
||||
```
|
||||
mongod --dbpath ~/data/db --logpath ~/data/log/mongodb/mongo.log --fork
|
||||
```
|
||||
验证启动
|
||||
```
|
||||
ps aux | grep -v grep | grep mongod
|
||||
```
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/q2kOGoR8MV1700072541114.jpg" width="800" />
|
||||
|
||||
> 工程默认的mongo链接如下,需要修改可查看[配置修改指导](https://xiaojusurvey.didi.cn/docs/document/%E6%A6%82%E8%BF%B0/%E5%AE%89%E8%A3%85%E7%8E%AF%E5%A2%83#%E9%85%8D%E7%BD%AE):
|
||||
|
||||
```
|
||||
mongodb://localhost:27017
|
||||
```
|
||||
启动和配置数据库,查看[MongoDB启动](http://localhost:5000/docs/next/document/%E6%A6%82%E8%BF%B0/%E5%AE%89%E8%A3%85%E7%8E%AF%E5%A2%83#%E4%BA%94%E5%90%AF%E5%8A%A8)
|
||||
|
||||
#### 2、安装依赖
|
||||
```shell
|
||||
@ -176,7 +170,6 @@ npm run serve
|
||||
[http://localhost:8080/render/:surveyPath](http://localhost:8080/render/:surveyPath)
|
||||
|
||||
|
||||
|
||||
# 交流群
|
||||
## 微信
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="300" />
|
||||
|
@ -1,6 +1,5 @@
|
||||
version: '3.6'
|
||||
version: "3.6"
|
||||
services:
|
||||
|
||||
mongo:
|
||||
image: mongo:4
|
||||
container_name: xiaoju-survey-mongo
|
||||
@ -16,11 +15,13 @@ services:
|
||||
- xiaoju-survey
|
||||
|
||||
xiaoju-survey:
|
||||
image: "xiaojusurvey/xiaoju-survey:1.0.0"
|
||||
image: "xiaojusurvey/xiaoju-survey:1.0.3"
|
||||
container_name: xiaoju-survey
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:3000" # API端口
|
||||
environment:
|
||||
XIAOJU_SURVEY_MONGO_URL: mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@xiaoju-survey-mongo:27017 # docker-compose 会根据容器名称自动处理
|
||||
links:
|
||||
- mongo:mongo
|
||||
depends_on:
|
||||
@ -33,5 +34,5 @@ volumes:
|
||||
|
||||
networks:
|
||||
xiaoju-survey:
|
||||
name: xiaoju-survey
|
||||
driver: bridge
|
||||
name: xiaoju-survey
|
||||
driver: bridge
|
||||
|
@ -1,3 +1,3 @@
|
||||
#! /bin/bash
|
||||
cd /xiaoju-survey/server
|
||||
npm run start
|
||||
npm run start:prod
|
12
server/.env
Normal file
12
server/.env
Normal file
@ -0,0 +1,12 @@
|
||||
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=
|
||||
|
||||
|
||||
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
|
0
server/.env.development
Normal file
0
server/.env.development
Normal file
@ -1,16 +0,0 @@
|
||||
# mongo
|
||||
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||
XIAOJU_SURVER_MONGO_DBNAME=xiaojuSurvey
|
||||
|
||||
# session
|
||||
# 8 * 3600 * 1000
|
||||
XIAOJU_SURVEY_SESSION_EXPIRE_TIME=28800000
|
||||
|
||||
# encrypt
|
||||
XIAOJU_SURVEY_ENCRYPT_TYPE=aes
|
||||
XIAOJU_SURVEY_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||
XIAOJU_SURVEY_ENCRYPT_TYPE_LEN=10
|
||||
|
||||
# jwt
|
||||
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
|
||||
XIAOJU_SURVEY_JWT_EXPIRES_IN=8h
|
0
server/.env.production
Normal file
0
server/.env.production
Normal file
25
server/.eslintrc.js
Normal file
25
server/.eslintrc.js
Normal file
@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"key-spacing": ["error", { "mode": "strict" }],
|
||||
"comma-spacing": ["error", { "before": false, "after": true }],
|
||||
"arrow-spacing": ["error", { "before": true, "after": true }],
|
||||
"space-before-blocks": 2,
|
||||
"object-curly-spacing": ["error", "always"]
|
||||
}
|
||||
}
|
122
server/.gitignore
vendored
122
server/.gitignore
vendored
@ -1,106 +1,38 @@
|
||||
|
||||
package-lock.json
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
package-lock.json
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# project
|
||||
build/
|
||||
src/apps/question/config/env/local.ts
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
4
server/.prettierrc
Normal file
4
server/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
8
server/nest-cli.json
Normal file
8
server/nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
@ -1,52 +1,90 @@
|
||||
{
|
||||
"name": "survey-template",
|
||||
"version": "1.0.0",
|
||||
"description": "survey server template",
|
||||
"main": "index.js",
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"scripts": {
|
||||
"copy": "mkdir -p ./build/ && cp -rf ./src/* ./build/",
|
||||
"build": "tsc",
|
||||
"start:stable": "SERVER_ENV=stable node ./build/index.js",
|
||||
"start:preonline": "SERVER_ENV=preonline node ./build/index.js",
|
||||
"start:online": "SERVER_ENV=online node ./build/index.js",
|
||||
"start": "npm run start:online",
|
||||
"local": "npx ts-node scripts/run-local.ts",
|
||||
"dev": "npx ts-node-dev ./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/koa": "^2.13.8",
|
||||
"@types/koa-bodyparser": "^4.3.10",
|
||||
"@types/koa-router": "^7.4.4",
|
||||
"@types/koa-static": "^4.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||
"@typescript-eslint/parser": "^6.15.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"mongodb-memory-server": "^9.0.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^4.8.4"
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"local": "ts-node ./scripts/run-local.ts",
|
||||
"start": "nest start",
|
||||
"dev": "npm run start:dev",
|
||||
"start:dev": "cross-env NODE_ENV=development nest start --watch",
|
||||
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
|
||||
"start:prod": "NODE_ENV=production node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@nestjs/typeorm": "^10.0.1",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"crypto-js": "^4.2.0",
|
||||
"glob": "^10.3.10",
|
||||
"joi": "^17.9.2",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"koa": "^2.14.2",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"koa-pino-logger": "^4.0.0",
|
||||
"koa-router": "^12.0.0",
|
||||
"koa-static": "^4.0.3",
|
||||
"dotenv": "^16.3.2",
|
||||
"joi": "^17.11.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"mongodb": "^5.7.0",
|
||||
"svg-captcha": "^1.4.0"
|
||||
"log4js": "^6.9.1",
|
||||
"moment": "^2.30.1",
|
||||
"mongodb": "^5.9.2",
|
||||
"nanoid": "^3.3.7",
|
||||
"node-forge": "^1.3.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"typeorm": "^0.3.19"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.21.0",
|
||||
"npm": ">=6.14.17"
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@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",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"mongodb-memory-server": "^9.1.4",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node",
|
||||
"moduleNameMapper": {
|
||||
"^src/(.*)$": "<rootDir>/$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
#main{
|
||||
#main {
|
||||
width: 100wh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@ -6,7 +6,8 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.title{
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
color: #4A4C5B;
|
||||
}
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
16
server/public/management.html
Normal file
16
server/public/management.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>问卷管理端</title>
|
||||
<link rel="stylesheet" href="./commom.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main">
|
||||
<img src="./nodata.png" alt="">
|
||||
<p class="title">暂无数据</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@ -9,10 +9,20 @@ async function startServerAndRunScript() {
|
||||
console.log('MongoDB Memory Server started:', mongoUri);
|
||||
|
||||
// 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量
|
||||
const tsnode = spawn('cross-env', [`XIAOJU_SURVEY_MONGO_URL="${mongoUri}"`, 'npx', 'ts-node-dev', './src/index.ts'], {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
const tsnode = spawn(
|
||||
'cross-env',
|
||||
[
|
||||
`XIAOJU_SURVEY_MONGO_URL=${mongoUri}`,
|
||||
'NODE_ENV=development',
|
||||
'npm',
|
||||
'run',
|
||||
'start:dev',
|
||||
],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
},
|
||||
);
|
||||
tsnode.stdout?.on('data', (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
@ -29,4 +39,4 @@ async function startServerAndRunScript() {
|
||||
|
||||
startServerAndRunScript().catch((err) => {
|
||||
console.error('Error starting server and script:', err);
|
||||
});
|
||||
});
|
||||
|
4
server/src/app.controller.ts
Normal file
4
server/src/app.controller.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Controller } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class AppController {}
|
115
server/src/app.module.ts
Normal file
115
server/src/app.module.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
import { ResponseSecurityPlugin } from './securityPlugin/responseSecurityPlugin';
|
||||
import { SurveyUtilPlugin } from './securityPlugin/surveyUtilPlugin';
|
||||
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { SurveyModule } from './modules/survey/survey.module';
|
||||
import { SurveyResponseModule } from './modules/surveyResponse/surveyResponse.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
import { APP_FILTER } from '@nestjs/core';
|
||||
import { HttpExceptionsFilter } from './exceptions/httpExceptions.filter';
|
||||
|
||||
import { Captcha } from './models/captcha.entity';
|
||||
import { User } from './models/user.entity';
|
||||
import { SurveyMeta } from './models/surveyMeta.entity';
|
||||
import { SurveyConf } from './models/surveyConf.entity';
|
||||
import { SurveyHistory } from './models/surveyHistory.entity';
|
||||
import { ResponseSchema } from './models/responseSchema.entity';
|
||||
import { Counter } from './models/counter.entity';
|
||||
import { SurveyResponse } from './models/surveyResponse.entity';
|
||||
import { ClientEncrypt } from './models/clientEncrypt.entity';
|
||||
import { Word } from './models/word.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 { Logger } from './logger';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({}),
|
||||
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const url = await configService.get<string>('XIAOJU_SURVEY_MONGO_URL');
|
||||
const authSource =
|
||||
(await configService.get<string>(
|
||||
'XIAOJU_SURVEY_MONGO_AUTH_SOURCE',
|
||||
)) || '';
|
||||
const database = await configService.get<string>(
|
||||
'XIAOJU_SURVEY_MONGO_DB_NAME',
|
||||
);
|
||||
return {
|
||||
type: 'mongodb',
|
||||
connectTimeoutMS: 10000,
|
||||
socketTimeoutMS: 10000,
|
||||
url,
|
||||
authSource,
|
||||
useNewUrlParser: true,
|
||||
database,
|
||||
entities: [
|
||||
Captcha,
|
||||
User,
|
||||
SurveyMeta,
|
||||
SurveyConf,
|
||||
SurveyHistory,
|
||||
SurveyResponse,
|
||||
Counter,
|
||||
ResponseSchema,
|
||||
ClientEncrypt,
|
||||
Word,
|
||||
],
|
||||
};
|
||||
},
|
||||
}),
|
||||
AuthModule,
|
||||
SurveyModule,
|
||||
SurveyResponseModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '..', 'public'),
|
||||
}),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: HttpExceptionsFilter,
|
||||
},
|
||||
LoggerProvider,
|
||||
PluginManagerProvider,
|
||||
],
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(LogRequestMiddleware).forRoutes('*');
|
||||
}
|
||||
onModuleInit() {
|
||||
this.pluginManager.registerPlugin(
|
||||
new ResponseSecurityPlugin(
|
||||
this.configService.get<string>(
|
||||
'XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY',
|
||||
),
|
||||
),
|
||||
new SurveyUtilPlugin(),
|
||||
);
|
||||
this.logger.init({
|
||||
filename: this.configService.get<string>('XIAOJU_SURVEY_LOGGER_FILENAME'),
|
||||
});
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { mongo } from '../../../config';
|
||||
|
||||
const aesEncrypt = {
|
||||
key: process.env.XIAOJU_SURVEY_ENCRYPT_SECRET_KEY || 'dataAesEncryptSecretKey',
|
||||
};
|
||||
|
||||
export function getConfig() {
|
||||
return {
|
||||
mongo,
|
||||
aesEncrypt,
|
||||
};
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { DataSecurityPlugin } from '../interface';
|
||||
import * as CryptoJS from 'crypto-js';
|
||||
import { isAddress, isEmail, isGender, isIdCard, isPhone } from './util';
|
||||
|
||||
|
||||
export default class AesDataSecurityPlugin implements DataSecurityPlugin {
|
||||
secretKey: string;
|
||||
constructor({ secretKey }) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
isDataSensitive(data: string): boolean {
|
||||
const testArr = [isPhone, isIdCard, isAddress, isEmail, isGender];
|
||||
for (const test of testArr) {
|
||||
if (test(data)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
encryptData(data: string): string {
|
||||
return CryptoJS.AES.encrypt(data, this.secretKey).toString();
|
||||
}
|
||||
decryptData(data: string): string {
|
||||
return CryptoJS.AES.decrypt(data, this.secretKey).toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
desensitiveData(data: string): string {
|
||||
if (data.length === 1) {
|
||||
return '*';
|
||||
}
|
||||
if (data.length === 2) {
|
||||
return data[0] + '*';
|
||||
}
|
||||
return data[0] + '***' + data[data.length - 1];
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
const phoneRegex = /^1[3456789]\d{9}$/; // 手机号码正则表达式
|
||||
const idCardRegex = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/; // 身份证号码正则表达式
|
||||
const addressRegex = /.*省|.*自治区|.*市|.*区|.*镇|.*县/; // 地址正则表达式
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 邮箱正则
|
||||
const genderArr = ['男', '女']; // 性别
|
||||
const nameRegex = /^([\u4e00-\u9fa5]{1,6}|[a-zA-Z.\s]{1,20})$/; // 只能识别是否包含中文,无法识别是否是姓名,暂时不启用
|
||||
|
||||
|
||||
export const isPhone = data => phoneRegex.test(data);
|
||||
|
||||
export const isIdCard = data => idCardRegex.test(data);
|
||||
|
||||
export const isAddress = data => addressRegex.test(data);
|
||||
|
||||
export const isEmail = data => emailRegex.test(data);
|
||||
|
||||
export const isGender = data => genderArr.includes(data);
|
||||
|
||||
export const isName = data => nameRegex.test(data);
|
@ -1,6 +0,0 @@
|
||||
export interface DataSecurityPlugin {
|
||||
isDataSensitive(data: string): boolean;
|
||||
encryptData(data: string): string;
|
||||
decryptData(data: string): string;
|
||||
desensitiveData(data: string): string;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { getConfig } from '../config/index';
|
||||
|
||||
import MongoService from '../../../utils/mongoService';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
export const mongo = new MongoService({ url: config.mongo.url, dbName: config.mongo.dbName });
|
@ -1,61 +0,0 @@
|
||||
import { SurveyApp, SurveyServer } from '../../decorator';
|
||||
import { securityService } from './service/securityService';
|
||||
import { isString } from './utils';
|
||||
import AesDataSecurityPlugin from './dataSecurityPlugins/aesPlugin/index';
|
||||
import { load } from 'cheerio';
|
||||
import { getConfig } from './config/index';
|
||||
|
||||
const config = getConfig();
|
||||
const pluginInstance = new AesDataSecurityPlugin({ secretKey: config.aesEncrypt.key });
|
||||
|
||||
@SurveyApp('/api/security')
|
||||
export default class Security {
|
||||
@SurveyServer({ type: 'rpc' })
|
||||
async isHitKeys({ params, context }) {
|
||||
const data = securityService.isHitKeys({
|
||||
content: params.content,
|
||||
dictType: params.dictType,
|
||||
});
|
||||
return {
|
||||
result: data,
|
||||
context, // 上下文主要是传递调用方信息使用,比如traceid
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'rpc' })
|
||||
isDataSensitive(data) {
|
||||
if (!isString(data)) {
|
||||
return false;
|
||||
}
|
||||
const $ = load(data);
|
||||
const text = $.text();
|
||||
return pluginInstance.isDataSensitive(text);
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'rpc' })
|
||||
encryptData(data) {
|
||||
if (!isString(data)) {
|
||||
return data;
|
||||
}
|
||||
return pluginInstance.encryptData(data);
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'rpc' })
|
||||
decryptData(data) {
|
||||
if (!isString(data)) {
|
||||
return data;
|
||||
}
|
||||
return pluginInstance.decryptData(data);
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'rpc' })
|
||||
desensitiveData(data) {
|
||||
// 数据脱敏
|
||||
if (!isString(data)) {
|
||||
return '*';
|
||||
}
|
||||
const $ = load(data);
|
||||
const text = $.text();
|
||||
return pluginInstance.desensitiveData(text);
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { mongo } from '../db/mongo';
|
||||
import { DICT_TYPE } from '../../../types';
|
||||
import { participle } from '../utils/index';
|
||||
|
||||
class SecurityService {
|
||||
async isHitKeys({ content, dictType }: { content: string, dictType: DICT_TYPE }) {
|
||||
const securityDictModel = await mongo.getCollection({ collectionName: 'securityDict' });
|
||||
const keywordList = participle({ content });
|
||||
const hitCount = await securityDictModel.countDocuments({ keyword: { $in: keywordList }, type: dictType });
|
||||
return hitCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const securityService = new SecurityService();
|
@ -1,18 +0,0 @@
|
||||
|
||||
export function participle({ content, minLen, maxLen }: { content: string, minLen?: number, maxLen?: number }) {
|
||||
const keys: Array<string> = [];
|
||||
minLen = minLen || 2;
|
||||
maxLen = maxLen || 13;
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let tempStr = content[i];
|
||||
for (let j = 1; j < maxLen && i + j < content.length; j++) {
|
||||
tempStr += content[i + j];
|
||||
if (j >= minLen - 1) {
|
||||
keys.push(tempStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
export const isString = data => typeof data === 'string';
|
@ -1,7 +0,0 @@
|
||||
import { mongo } from '../../../config';
|
||||
|
||||
export function getConfig() {
|
||||
return {
|
||||
mongo,
|
||||
};
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { getConfig } from '../config/index';
|
||||
import MongoService from '../../../utils/mongoService';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
export const mongo = new MongoService({ url: config.mongo.url, dbName: config.mongo.dbName });
|
@ -1,293 +0,0 @@
|
||||
import { SurveyApp, SurveyServer } from '../../decorator';
|
||||
import { surveyService } from './service/surveyService';
|
||||
import { userService } from './service/userService';
|
||||
import { surveyHistoryService } from './service/surveyHistoryService';
|
||||
import { HISTORY_TYPE } from '../../types/index';
|
||||
import { getValidateValue } from './utils/index';
|
||||
import * as Joi from 'joi';
|
||||
import { CommonError } from '../../types/index';
|
||||
|
||||
type FilterItem = {
|
||||
comparator?: string;
|
||||
condition: Array<FilterCondition>;
|
||||
}
|
||||
|
||||
type FilterCondition = {
|
||||
field: string;
|
||||
comparator?: string;
|
||||
value: string & Array<FilterItem>;
|
||||
}
|
||||
|
||||
@SurveyApp('/api/surveyManage')
|
||||
export default class SurveyManage {
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/getBannerData' })
|
||||
async getBannerData() {
|
||||
const data = await surveyService.getBannerData();
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/add' })
|
||||
async add({ req }) {
|
||||
const params = getValidateValue(Joi.object({
|
||||
remark: Joi.string().required(),
|
||||
questionType: Joi.string().required(),
|
||||
title: Joi.string().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
params.userData = await userService.checkLogin({ req });
|
||||
const addRes = await surveyService.add(params);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
id: addRes.pageId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/create' })
|
||||
async create({ req }) {
|
||||
const params = getValidateValue(Joi.object({
|
||||
remark: Joi.string().required(),
|
||||
title: Joi.string().required(),
|
||||
questionType: Joi.string().when('createMethod', {
|
||||
is: 'copy',
|
||||
then: Joi.allow(null),
|
||||
otherwise: Joi.required(),
|
||||
}),
|
||||
createMethod: Joi.string().allow(null).default('basic'),
|
||||
createFrom: Joi.string().when('createMethod', {
|
||||
is: 'copy',
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.allow(null),
|
||||
}),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
params.userData = await userService.checkLogin({ req });
|
||||
const addRes = await surveyService.create(params);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
id: addRes.pageId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/update' })
|
||||
async update({ req }) {
|
||||
const surveyParams = getValidateValue(Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
remark: Joi.string().required(),
|
||||
title: Joi.string().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
const userData = await userService.checkLogin({ req });
|
||||
surveyParams.userData = userData;
|
||||
const data = await surveyService.update(surveyParams);
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/delete' })
|
||||
async delete({ req }) {
|
||||
const surveyParams = getValidateValue(Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
const userData = await userService.checkLogin({ req });
|
||||
surveyParams.userData = userData;
|
||||
const data = await surveyService.delete(surveyParams);
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
};
|
||||
}
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/list' })
|
||||
async list({ req }) {
|
||||
const condition = getValidateValue(Joi.object({
|
||||
curPage: Joi.number().default(1),
|
||||
pageSize: Joi.number().default(10),
|
||||
filter: Joi.string().allow(null),
|
||||
order: Joi.string().allow(null),
|
||||
}).validate(req.query, { allowUnknown: true }));
|
||||
let filter = {}, order = {};
|
||||
if (condition.filter) {
|
||||
try {
|
||||
filter = this.getFilter(JSON.parse(decodeURIComponent(condition.filter)));
|
||||
} catch (error) {
|
||||
throw new CommonError('filter参数格式不正确');
|
||||
}
|
||||
}
|
||||
if (condition.order) {
|
||||
try {
|
||||
order = this.getOrder(JSON.parse(decodeURIComponent(condition.order)));
|
||||
} catch (error) {
|
||||
throw new CommonError('order参数格式不正确');
|
||||
}
|
||||
}
|
||||
const userData = await userService.checkLogin({ req });
|
||||
const listRes = await surveyService.list({
|
||||
pageNum: condition.curPage,
|
||||
pageSize: condition.pageSize,
|
||||
filter,
|
||||
order,
|
||||
userData
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: listRes,
|
||||
};
|
||||
}
|
||||
|
||||
private getFilter(filterList: Array<FilterItem>) {
|
||||
const allowFilterField = ['title', 'remark', 'questionType', 'curStatus.status'];
|
||||
return filterList.reduce((preItem, curItem) => {
|
||||
const condition = curItem.condition.filter(item => allowFilterField.includes(item.field)).reduce((pre, cur) => {
|
||||
switch(cur.comparator) {
|
||||
case '$ne':
|
||||
pre[cur.field] = {
|
||||
$ne: cur.value,
|
||||
};
|
||||
break;
|
||||
case '$regex':
|
||||
pre[cur.field] = {
|
||||
$regex: cur.value,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
pre[cur.field] = cur.value;
|
||||
break;
|
||||
}
|
||||
return pre;
|
||||
}, {});
|
||||
switch(curItem.comparator) {
|
||||
case '$or':
|
||||
if (!Array.isArray(preItem.$or)) {
|
||||
preItem.$or = [];
|
||||
}
|
||||
preItem.$or.push(condition);
|
||||
break;
|
||||
default:
|
||||
Object.assign(preItem, condition);
|
||||
break;
|
||||
}
|
||||
return preItem;
|
||||
}, { } as { $or?: Array<Record<string, string>>; } & Record<string, string>);
|
||||
}
|
||||
|
||||
private getOrder(order) {
|
||||
|
||||
const allowOrderFields = ['createDate', 'updateDate', 'curStatus.date'];
|
||||
|
||||
const orderList = order.filter((orderItem) =>
|
||||
allowOrderFields.includes(orderItem.field),
|
||||
);
|
||||
return orderList.reduce((pre, cur) => {
|
||||
pre[cur.field] = cur.value === 1 ? 1 : -1;
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/saveConf' })
|
||||
async saveConf({ req }) {
|
||||
const surveyData = getValidateValue(Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
configData: Joi.object().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
const userData = await userService.checkLogin({ req });
|
||||
// 保存数据
|
||||
const saveRes = await surveyService.saveConf(surveyData);
|
||||
// 保存历史
|
||||
const historyRes = await surveyHistoryService.addHistory({
|
||||
surveyId: surveyData.surveyId,
|
||||
configData: surveyData.configData,
|
||||
type: HISTORY_TYPE.dailyHis,
|
||||
userData
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
saveRes,
|
||||
historyRes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/get' })
|
||||
async get({ req }) {
|
||||
const params = getValidateValue(Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate(req.query, { allowUnknown: true }));
|
||||
const userData = await userService.checkLogin({ req });
|
||||
const data = await surveyService.get({
|
||||
surveyId: params.surveyId,
|
||||
userData
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/getHistoryList' })
|
||||
async getHistoryList({ req }) {
|
||||
const historyParams = getValidateValue(Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
historyType: Joi.string().required(),
|
||||
}).validate(req.query, { allowUnknown: true }));
|
||||
const data = await surveyHistoryService.getHistoryList(historyParams);
|
||||
return {
|
||||
code: 200,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/publish' })
|
||||
async publish({ req }) {
|
||||
const surveyParams = getValidateValue(Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
// 鉴权
|
||||
const userData = await userService.checkLogin({ req });
|
||||
// 发布
|
||||
surveyParams.userData = userData;
|
||||
const surveyData = await surveyService.publish(surveyParams);
|
||||
// 保存历史
|
||||
const historyRes = await surveyHistoryService.addHistory({
|
||||
surveyId: surveyData.surveyConfRes.pageId,
|
||||
configData: surveyData.surveyConfRes.code,
|
||||
type: HISTORY_TYPE.publishHis,
|
||||
userData
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
...surveyData,
|
||||
historyRes
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/data' })
|
||||
async data({ req }) {
|
||||
const surveyParams = getValidateValue(Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
isShowSecret: Joi.boolean().default(true), // 默认true就是需要脱敏
|
||||
page: Joi.number().default(1),
|
||||
pageSize: Joi.number().default(10),
|
||||
}).validate(req.query, { allowUnknown: true }));
|
||||
const userData = await userService.checkLogin({ req });
|
||||
const data = await surveyService.data({
|
||||
userData,
|
||||
surveyId: surveyParams.surveyId,
|
||||
isShowSecret: surveyParams.isShowSecret,
|
||||
pageNum: surveyParams.page,
|
||||
pageSize: surveyParams.pageSize,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data
|
||||
};
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import { mongo } from '../db/mongo';
|
||||
import { getStatusObject } from '../utils/index';
|
||||
import { SURVEY_STATUS, HISTORY_TYPE, UserType } from '../../../types/index';
|
||||
|
||||
class SurveyHistoryService {
|
||||
async addHistory(surveyData: { surveyId: string, configData: unknown, type: HISTORY_TYPE, userData: UserType }) {
|
||||
const surveyHistory = await mongo.getCollection({ collectionName: 'surveyHistory' });
|
||||
const surveyHistoryRes = await surveyHistory.insertOne({
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.new }),
|
||||
pageId: surveyData.surveyId,
|
||||
type: surveyData.type,
|
||||
code: {
|
||||
data: surveyData.configData
|
||||
},
|
||||
createDate: Date.now(),
|
||||
operator: {
|
||||
_id: surveyData.userData._id,
|
||||
username: surveyData.userData.username,
|
||||
}
|
||||
});
|
||||
return surveyHistoryRes;
|
||||
}
|
||||
|
||||
async getHistoryList(historyParams: { surveyId: string, historyType: HISTORY_TYPE }) {
|
||||
const surveyHistory = await mongo.getCollection({ collectionName: 'surveyHistory' });
|
||||
const surveyHistoryListRes = await surveyHistory.find({
|
||||
pageId: historyParams.surveyId,
|
||||
type: historyParams.historyType,
|
||||
})
|
||||
.sort({ createDate: -1 })
|
||||
.limit(100)
|
||||
.toArray();
|
||||
return mongo.convertId2StringByList(surveyHistoryListRes);
|
||||
}
|
||||
}
|
||||
|
||||
export const surveyHistoryService = new SurveyHistoryService();
|
@ -1,408 +0,0 @@
|
||||
import { mongo } from '../db/mongo';
|
||||
import { rpcInvote } from '../../../rpc';
|
||||
import { SURVEY_STATUS, QUESTION_TYPE, CommonError, UserType, DICT_TYPE } from '../../../types/index';
|
||||
import { getStatusObject, genSurveyPath } from '../utils/index';
|
||||
import * as path from 'path';
|
||||
import { keyBy, merge, cloneDeep } from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import { DataItem } from '../../../types/survey';
|
||||
import { Sort } from 'mongodb';
|
||||
|
||||
class SurveyService {
|
||||
async checkSecurity({ content, dictType }: { content: string, dictType: DICT_TYPE }) {
|
||||
const rpcResult = await rpcInvote<unknown, { result: boolean }>('security.isHitKeys', {
|
||||
params: { content, dictType },
|
||||
context: {}
|
||||
});
|
||||
return rpcResult.result;
|
||||
}
|
||||
|
||||
async getBannerData() {
|
||||
const bannerConfPath = path.resolve(__dirname, '../template/banner/index.json');
|
||||
return await import(bannerConfPath);
|
||||
}
|
||||
|
||||
async getCodeData({
|
||||
questionType,
|
||||
}: { questionType: QUESTION_TYPE }): Promise<unknown> {
|
||||
const baseConfPath = path.resolve(__dirname, '../template/surveyTemplate/templateBase.json');
|
||||
const templateConfPath = path.resolve(
|
||||
__dirname,
|
||||
`../template/surveyTemplate/survey/${questionType}.json`,
|
||||
);
|
||||
|
||||
const baseConf = cloneDeep(await import(baseConfPath));
|
||||
const templateConf = cloneDeep(await import(templateConfPath));
|
||||
const codeData = merge(baseConf, templateConf);
|
||||
const nowMoment = moment();
|
||||
codeData.baseConf.begTime = nowMoment.format('YYYY-MM-DD HH:mm:ss');
|
||||
codeData.baseConf.endTime = nowMoment.add(10, 'years').format('YYYY-MM-DD HH:mm:ss');
|
||||
return codeData;
|
||||
}
|
||||
async getNewSurveyPath() {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const surveyPath = genSurveyPath();
|
||||
const surveyPathCount = await surveyMeta.countDocuments({ surveyPath });
|
||||
if (surveyPathCount > 0) { return await this.getNewSurveyPath(); }
|
||||
return surveyPath;
|
||||
}
|
||||
|
||||
async add(surveyMetaInfo: { remark: string, questionType: QUESTION_TYPE, title: string, userData: UserType }) {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const now = Date.now();
|
||||
const surveyPath = await this.getNewSurveyPath();
|
||||
const surveyMetaRes = await surveyMeta.insertOne({
|
||||
surveyPath,
|
||||
remark: surveyMetaInfo.remark,
|
||||
questionType: surveyMetaInfo.questionType,
|
||||
title: surveyMetaInfo.title,
|
||||
creator: surveyMetaInfo.userData.username,
|
||||
owner: surveyMetaInfo.userData.username,
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.new }),
|
||||
createDate: now,
|
||||
updateDate: now,
|
||||
});
|
||||
const pageId = surveyMetaRes.insertedId.toString();
|
||||
const surveyConf = await mongo.getCollection({ collectionName: 'surveyConf' });
|
||||
const surveyConfRes = await surveyConf.insertOne({
|
||||
pageId,
|
||||
pageType: surveyMetaInfo.questionType,
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.new }),
|
||||
code: await this.getCodeData({
|
||||
questionType: surveyMetaInfo.questionType,
|
||||
})
|
||||
});
|
||||
return {
|
||||
pageId,
|
||||
surveyMetaRes,
|
||||
surveyConfRes
|
||||
};
|
||||
}
|
||||
|
||||
async create(surveyMetaInfo: { remark: string, questionType: QUESTION_TYPE, title: string, userData: UserType, createMethod: string; createFrom: string; }) {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const now = Date.now();
|
||||
const surveyPath = await this.getNewSurveyPath();
|
||||
|
||||
let originSurvey;
|
||||
if (surveyMetaInfo.createMethod === 'copy') {
|
||||
originSurvey = await this.get({ surveyId: surveyMetaInfo.createFrom, userData: surveyMetaInfo.userData });
|
||||
surveyMetaInfo.questionType = originSurvey.surveyMetaRes.questionType;
|
||||
}
|
||||
|
||||
const surveyMetaRes = await surveyMeta.insertOne({
|
||||
surveyPath,
|
||||
remark: surveyMetaInfo.remark,
|
||||
questionType: surveyMetaInfo.questionType,
|
||||
createMethod: surveyMetaInfo.createMethod || 'basic',
|
||||
createFrom: surveyMetaInfo.createFrom || '',
|
||||
title: surveyMetaInfo.title,
|
||||
creator: surveyMetaInfo.userData.username,
|
||||
owner: surveyMetaInfo.userData.username,
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.new }),
|
||||
createDate: now,
|
||||
updateDate: now,
|
||||
});
|
||||
const pageId = surveyMetaRes.insertedId.toString();
|
||||
const surveyConf = await mongo.getCollection({ collectionName: 'surveyConf' });
|
||||
const code = originSurvey ? originSurvey.surveyConfRes.code : await this.getCodeData({
|
||||
questionType: surveyMetaInfo.questionType,
|
||||
});
|
||||
const surveyConfRes = await surveyConf.insertOne({
|
||||
pageId,
|
||||
pageType: surveyMetaInfo.questionType,
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.new }),
|
||||
code,
|
||||
});
|
||||
return {
|
||||
pageId,
|
||||
surveyMetaRes,
|
||||
surveyConfRes
|
||||
};
|
||||
}
|
||||
|
||||
async update(surveyParams: { surveyId: string, remark: string, title: string, userData: UserType }) {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const _id = mongo.getObjectIdByStr(surveyParams.surveyId);
|
||||
const surveyMetaUpdateRes = await surveyMeta.updateOne({
|
||||
_id,
|
||||
owner: surveyParams.userData.username,
|
||||
}, [{
|
||||
$set: {
|
||||
remark: surveyParams.remark,
|
||||
title: surveyParams.title,
|
||||
updateDate: Date.now(),
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
'curStatus': {
|
||||
$cond: {
|
||||
if: {
|
||||
$eq: ['$curStatus.status', 'new']
|
||||
},
|
||||
then: '$curStatus',
|
||||
else: getStatusObject({ status: SURVEY_STATUS.editing })
|
||||
}
|
||||
}
|
||||
}
|
||||
}]);
|
||||
if (surveyMetaUpdateRes.matchedCount < 1) {
|
||||
throw new CommonError('更新问卷信息失败,问卷不存在或您不是该问卷所有者');
|
||||
}
|
||||
return {
|
||||
surveyMetaUpdateRes
|
||||
};
|
||||
}
|
||||
|
||||
async delete(surveyParams: { surveyId: string, userData: UserType }) {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const _id = mongo.getObjectIdByStr(surveyParams.surveyId);
|
||||
const surveyMetaDeleteRes = await surveyMeta.deleteOne({
|
||||
_id,
|
||||
owner: surveyParams.userData.username,
|
||||
});
|
||||
if (surveyMetaDeleteRes.deletedCount < 1) {
|
||||
throw new CommonError('删除问卷失败,问卷已被删除或您不是该问卷所有者');
|
||||
}
|
||||
return {
|
||||
surveyMetaDeleteRes
|
||||
};
|
||||
}
|
||||
|
||||
async list(condition: { pageNum: number, pageSize: number, userData: UserType, filter: object, order: object }) {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
|
||||
const query = Object.assign(
|
||||
{},
|
||||
{
|
||||
owner: condition.userData.username,
|
||||
'curStatus.status': {
|
||||
$ne: 'removed',
|
||||
},
|
||||
},
|
||||
condition.filter,
|
||||
);
|
||||
const order = condition.order && Object.keys(condition.order).length > 0 ? condition.order as Sort : {
|
||||
createDate: -1,
|
||||
} as Sort;
|
||||
const data = await surveyMeta.find(query)
|
||||
.sort(order)
|
||||
.limit(condition.pageSize)
|
||||
.skip((condition.pageNum - 1) * condition.pageSize)
|
||||
.toArray();
|
||||
const count = await surveyMeta.countDocuments(query);
|
||||
return { data: mongo.convertId2StringByList(data), count };
|
||||
}
|
||||
|
||||
getListHeadByDataList(dataList) {
|
||||
const listHead = dataList.map(surveyItem => {
|
||||
let othersCode;
|
||||
if (surveyItem.type === 'radio-star') {
|
||||
const rangeConfigKeys = Object.keys(surveyItem.rangeConfig);
|
||||
if (rangeConfigKeys.length > 0) {
|
||||
othersCode = [{ code: `${surveyItem.field}_custom`, option: '填写理由' }];
|
||||
}
|
||||
} else {
|
||||
othersCode = (surveyItem.options || [])
|
||||
.filter(optionItem => optionItem.othersKey)
|
||||
.map((optionItem) => {
|
||||
return {
|
||||
code: optionItem.othersKey,
|
||||
option: optionItem.text
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
field: surveyItem.field,
|
||||
title: surveyItem.title,
|
||||
type: surveyItem.type,
|
||||
othersCode
|
||||
};
|
||||
});
|
||||
listHead.push({
|
||||
field: 'difTime',
|
||||
title: '答题耗时(秒)',
|
||||
type: 'text',
|
||||
});
|
||||
listHead.push({
|
||||
field: 'createDate',
|
||||
title: '提交时间',
|
||||
type: 'text',
|
||||
});
|
||||
return listHead;
|
||||
}
|
||||
|
||||
async data(condition: { userData: UserType, surveyId: string, pageNum: number, pageSize: number, isShowSecret: boolean }) {
|
||||
const surveyObjectId = mongo.getObjectIdByStr(condition.surveyId);
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const surveyMetaData = await surveyMeta.findOne({ _id: surveyObjectId });
|
||||
if (surveyMetaData.owner !== condition.userData.username) {
|
||||
throw new CommonError('问卷回收数据列表仅所有人才能打开');
|
||||
}
|
||||
const surveyPublish = await mongo.getCollection({ collectionName: 'surveyPublish' });
|
||||
const publishConf = await surveyPublish.findOne({ pageId: condition.surveyId });
|
||||
const dataList = publishConf?.code?.dataConf?.dataList || [];
|
||||
const listHead = this.getListHeadByDataList(dataList);
|
||||
const dataListMap = keyBy(dataList, 'field');
|
||||
const surveySubmit = await mongo.getCollection({ collectionName: 'surveySubmit' });
|
||||
const surveySubmitDataList = await surveySubmit.find({ pageId: condition.surveyId })
|
||||
.sort({ createDate: -1 })
|
||||
.limit(condition.pageSize)
|
||||
.skip((condition.pageNum - 1) * condition.pageSize)
|
||||
.toArray();
|
||||
|
||||
const listBody = surveySubmitDataList.map(submitedData => {
|
||||
const data = submitedData.data;
|
||||
const secretKeys = submitedData.secretKeys || [];
|
||||
const dataKeys = Object.keys(data);
|
||||
|
||||
for (const itemKey of dataKeys) {
|
||||
if (typeof itemKey !== 'string') { continue; }
|
||||
if (itemKey.indexOf('data') !== 0) { continue; }
|
||||
// 获取题目id
|
||||
const itemConfigKey = itemKey.split('_')[0];
|
||||
// 获取题目
|
||||
const itemConfig: DataItem = dataListMap[itemConfigKey];
|
||||
// 题目删除会出现,数据列表报错
|
||||
if (!itemConfig) { continue; }
|
||||
// 处理选项的更多输入框
|
||||
if (itemConfig.type === 'radio-star' && !data[`${itemConfigKey}_custom`]) {
|
||||
data[`${itemConfigKey}_custom`] = data[`${itemConfigKey}_${data[itemConfigKey]}`];
|
||||
}
|
||||
// 解密数据
|
||||
if (secretKeys.includes(itemKey)) {
|
||||
data[itemKey] = Array.isArray(data[itemKey]) ? data[itemKey].map(item => rpcInvote('security.decryptData', item)) : rpcInvote('security.decryptData', data[itemKey]);
|
||||
}
|
||||
// 将选项id还原成选项文案
|
||||
if (Array.isArray(itemConfig.options) && itemConfig.options.length > 0) {
|
||||
const optionTextMap = keyBy(itemConfig.options, 'hash');
|
||||
data[itemKey] = Array.isArray(data[itemKey]) ? data[itemKey].map(item => optionTextMap[item]?.text || item).join(',') : optionTextMap[data[itemKey]]?.text || data[itemKey];
|
||||
}
|
||||
// 数据脱敏
|
||||
if (condition.isShowSecret && rpcInvote('security.isDataSensitive', data[itemKey])) {
|
||||
data[itemKey] = rpcInvote('security.desensitiveData', data[itemKey]);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
difTime: (submitedData.difTime / 1000).toFixed(2),
|
||||
createDate: moment(submitedData.createDate).format('YYYY-MM-DD HH:mm:ss')
|
||||
};
|
||||
});
|
||||
const total = await surveySubmit.countDocuments({ pageId: condition.surveyId });
|
||||
return {
|
||||
total,
|
||||
listHead,
|
||||
listBody
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async get({ surveyId, userData }: { surveyId: string, userData: UserType }) {
|
||||
const surveyObjectId = mongo.getObjectIdByStr(surveyId);
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const surveyMetaData = await surveyMeta.findOne({ _id: surveyObjectId });
|
||||
if (!surveyMetaData) {
|
||||
throw new CommonError('问卷不存在或已被删除');
|
||||
}
|
||||
if (surveyMetaData.owner !== userData.username) {
|
||||
throw new CommonError('问卷仅所有人才能打开');
|
||||
}
|
||||
const surveyMetaRes = mongo.convertId2StringByDoc(surveyMetaData);
|
||||
const surveyConf = await mongo.getCollection({ collectionName: 'surveyConf' });
|
||||
const surveyConfData = await surveyConf.findOne({ pageId: surveyId });
|
||||
if (!surveyConfData) {
|
||||
throw new CommonError('问卷配置不存在或已被删除');
|
||||
}
|
||||
const surveyConfRes = mongo.convertId2StringByDoc(surveyConfData);
|
||||
return {
|
||||
surveyMetaRes,
|
||||
surveyConfRes
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async saveConf(surveyData: { surveyId: string, configData: unknown }) {
|
||||
const surveyConf = await mongo.getCollection({ collectionName: 'surveyConf' });
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const saveRes = await surveyConf.updateOne({
|
||||
pageId: surveyData.surveyId
|
||||
}, {
|
||||
$set: {
|
||||
code: surveyData.configData,
|
||||
}
|
||||
});
|
||||
const _id = mongo.getObjectIdByStr(surveyData.surveyId);
|
||||
surveyMeta.updateOne({
|
||||
_id,
|
||||
}, [{
|
||||
$set: {
|
||||
'curStatus': {
|
||||
$cond: {
|
||||
if: {
|
||||
$eq: ['$curStatus.status', 'new']
|
||||
},
|
||||
then: '$curStatus',
|
||||
else: getStatusObject({ status: SURVEY_STATUS.editing })
|
||||
}
|
||||
}
|
||||
}
|
||||
}]);
|
||||
return saveRes;
|
||||
}
|
||||
|
||||
async publish({ surveyId, userData }: { surveyId: string, userData: UserType }) {
|
||||
const surveyObjectId = mongo.getObjectIdByStr(surveyId);
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const surveyConf = await mongo.getCollection({ collectionName: 'surveyConf' });
|
||||
const surveyMetaRes = await surveyMeta.findOne({ _id: surveyObjectId });
|
||||
if (!surveyMetaRes) {
|
||||
throw new CommonError('问卷不存在或已被删除,无法发布');
|
||||
}
|
||||
if (surveyMetaRes.owner !== userData.username) {
|
||||
throw new CommonError('只有问卷的所有者才能发布该问卷');
|
||||
}
|
||||
const surveyConfRes = await surveyConf.findOne({ pageId: surveyId });
|
||||
if (!surveyConfRes) {
|
||||
throw new CommonError('问卷配置不存在或已被删除,无法发布');
|
||||
}
|
||||
const surveyPublish = await mongo.getCollection({ collectionName: 'surveyPublish' });
|
||||
// 清除id存储发布
|
||||
delete surveyConfRes._id;
|
||||
surveyConfRes.title = surveyMetaRes.title;
|
||||
surveyConfRes.curStatus = surveyMetaRes.curStatus;
|
||||
surveyConfRes.surveyPath = surveyMetaRes.surveyPath;
|
||||
const dataList = surveyConfRes?.code?.dataConf?.dataList || [];
|
||||
for (const data of dataList) {
|
||||
const isDangerKey = await this.checkSecurity({ content: data.title, dictType: DICT_TYPE.danger });
|
||||
if (isDangerKey) {
|
||||
throw new CommonError('问卷存在非法关键字,不允许发布');
|
||||
}
|
||||
const isSecretKey = await this.checkSecurity({ content: data.title, dictType: DICT_TYPE.secret });
|
||||
if (isSecretKey) {
|
||||
data.isSecret = true;
|
||||
}
|
||||
}
|
||||
const publishRes = await surveyPublish.updateOne({
|
||||
pageId: surveyId
|
||||
}, {
|
||||
$set: surveyConfRes
|
||||
}, {
|
||||
upsert: true //如果不存在则插入
|
||||
});
|
||||
const updateMetaRes = await surveyMeta.updateOne({
|
||||
_id: surveyObjectId
|
||||
}, {
|
||||
$set: {
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.published }),
|
||||
}
|
||||
});
|
||||
return {
|
||||
updateMetaRes,
|
||||
surveyConfRes,
|
||||
publishRes
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const surveyService = new SurveyService();
|
@ -1,19 +0,0 @@
|
||||
import { rpcInvote } from '../../../rpc';
|
||||
import { Request } from 'koa';
|
||||
import { UserType, CommonError } from '../../../types/index';
|
||||
|
||||
class UserService {
|
||||
async checkLogin({ req }: { req: Request }) {
|
||||
if (!req.headers['authorization']) {
|
||||
throw new CommonError('请先登录', 403);
|
||||
}
|
||||
const token = (String(req.headers['authorization']) || '').replace('Bearer ', '');
|
||||
const rpcResult = await rpcInvote<unknown, { result: UserType }>('user.getUserByToken', {
|
||||
params: { token },
|
||||
context: req
|
||||
});
|
||||
return rpcResult.result;
|
||||
}
|
||||
}
|
||||
|
||||
export const userService = new UserService();
|
@ -1,174 +0,0 @@
|
||||
{
|
||||
"temp": {
|
||||
"key": "default",
|
||||
"name": "默认分类",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.jpg",
|
||||
"title": "1"
|
||||
}]
|
||||
},
|
||||
"activity": {
|
||||
"key": "activity",
|
||||
"name": "节日",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/Ixx8hqiwwk1660979120801.jpg",
|
||||
"title": "1"
|
||||
}]
|
||||
},
|
||||
"creative": {
|
||||
"key": "creative",
|
||||
"name": "创意",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/PLwNH1rAie1558430219772.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/NnXsAOhBNm1558430219312.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/ujpUoWqhw31558430220124.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/5OCvbjqJQm1558430220362.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/0k7Jg7In8I1558430221154.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/UH0A8DbTai1558430221033.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/FRIzPC6ZtN1558430221344.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"scenery": {
|
||||
"key": "scenery",
|
||||
"name": "风景",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/SyiLRcukyE1558430525760.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/sqYig4AcWr1558430525663.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/ElNeqJT2I21558430526165.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/CxQkSU6AY21558430526163.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/VTUwbp6vY61558430527320.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/SHs0K703Yn1558430527218.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/oVTedX9V4s1558430527671.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"transportation": {
|
||||
"key": "transportation",
|
||||
"name": "交通",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/XYKqJZuMig1558430904735.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/GnPatsr48Z1558430904680.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/UqIvVvEXAK1558430905204.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/PUssufh5uI1558430905104.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/O409pRTDlW1558430905738.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/A9FzlbYXqI1558430905739.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/HN9YGctDeF1558430906686.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"delicacy": {
|
||||
"key": "delicacy",
|
||||
"name": "美食",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/lE6PSclCcU1558434536703.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/OnSdbm7u6n1558434536641.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/N9Z2ZuyO731558434537314.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/YP9PoW8pX51558434537301.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/zUtDv378bg1558434538351.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/gY1JljCow21558434538303.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/oOjHPbABdd1558434538864.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"business": {
|
||||
"key": "business",
|
||||
"name": "商务",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/3ABKqvDaVn1558514860472.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/OewuaQmWoq1558514860285.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/HuVqqtbFjs1558514860570.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/icSlqsr0uZ1558514860875.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/Qu9rg33wmq1558514861015.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/145gBCRtNP1558514861211.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/ykWLFV0QWj1558514861444.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"campus": {
|
||||
"key": "campus",
|
||||
"name": "校园",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/4aWi5JxG471558514268698.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/j8C2OBP7WK1558514268563.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/q3uJoQhYsR1558514268877.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/W5PPlNsmsr1558514269088.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/6xQk1IAmKt1558514269874.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/XQE2iyF0rj1558514269935.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/POHlQiSwPR1558514270379.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
const base58KeysObject = {
|
||||
'1': 0, '2': 1, '3': 2, '4': 3, '5': 4, '6': 5, '7': 6, '8': 7, '9': 8,
|
||||
'A': 9, 'B': 10, 'C': 11, 'D': 12, 'E': 13, 'F': 14, 'G': 15, 'H': 16, 'J': 17,
|
||||
'K': 18, 'L': 19, 'M': 20, 'N': 21, 'P': 22, 'Q': 23, 'R': 24, 'S': 25, 'T': 26,
|
||||
'U': 27, 'V': 28, 'W': 29, 'X': 30, 'Y': 31, 'Z': 32,
|
||||
'a': 33, 'b': 34, 'c': 35, 'd': 36, 'e': 37, 'f': 38, 'g': 39, 'h': 40, 'i': 41, 'j': 42,
|
||||
'k': 43, 'm': 44, 'n': 45, 'o': 46, 'p': 47, 'q': 48, 'r': 49, 's': 50, 't': 51,
|
||||
'u': 52, 'v': 53, 'w': 54, 'x': 55, 'y': 56, 'z': 57
|
||||
};
|
||||
const base58Keys = Object.keys(base58KeysObject);
|
||||
const base58Len = 58n;
|
||||
|
||||
export function hex2Base58(hexNum:string):string
|
||||
{
|
||||
const base58NumArray =[];
|
||||
let bigHexNumber = BigInt(`0x${hexNum}`);
|
||||
while (bigHexNumber>=58n)
|
||||
{
|
||||
base58NumArray.unshift(base58Keys[(bigHexNumber % base58Len).toString()]);
|
||||
bigHexNumber = bigHexNumber / base58Len;
|
||||
}
|
||||
base58NumArray.unshift(base58Keys[bigHexNumber.toString()]);
|
||||
return base58NumArray.join('');
|
||||
}
|
||||
|
||||
export function base582Hex(base58Num:string):string
|
||||
{
|
||||
const base58NumArray =base58Num.split('');
|
||||
let big58Number = 0n;
|
||||
const len = base58NumArray.length;
|
||||
for(let i = 1;i<=len;i++)
|
||||
{
|
||||
const big58NumberTemp = BigInt(base58KeysObject[base58NumArray[len-i]])*(base58Len** BigInt(i-1));
|
||||
big58Number += big58NumberTemp;
|
||||
}
|
||||
return big58Number.toString(16);
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import { SURVEY_STATUS, CommonError } from '../../../types/index';
|
||||
import { hex2Base58 } from './base58';
|
||||
import * as Joi from 'joi';
|
||||
import * as fs from 'fs';
|
||||
|
||||
export function getStatusObject({ status }: { status: SURVEY_STATUS }) {
|
||||
return {
|
||||
status,
|
||||
id: status,
|
||||
date: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getValidateValue<T = unknown>(validationResult: Joi.ValidationResult<T>): T {
|
||||
if (validationResult.error) {
|
||||
throw new CommonError(validationResult.error.details.map(e => e.message).join());
|
||||
}
|
||||
return validationResult.value;
|
||||
}
|
||||
|
||||
export function genSurveyPath() {
|
||||
return hex2Base58(process.hrtime.bigint().toString(16));
|
||||
}
|
||||
|
||||
export const getFile = function(path, { encoding }: { encoding } = { encoding: 'utf-8' }): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
fs.stat(path, err => {
|
||||
if (!err) {
|
||||
fs.readFile(path, { encoding }, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data.toString());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import { mongo, session, encrypt } from '../../../config';
|
||||
|
||||
export function getConfig() {
|
||||
return {
|
||||
mongo,
|
||||
session,
|
||||
encrypt,
|
||||
};
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { getConfig } from '../config/index';
|
||||
import MongoService from '../../../utils/mongoService';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
export const mongo = new MongoService({ url: config.mongo.url, dbName: config.mongo.dbName });
|
@ -1,63 +0,0 @@
|
||||
import { SurveyApp, SurveyServer } from '../../decorator';
|
||||
import { surveySubmitService } from './service/surveySubmitService';
|
||||
import { surveyPublishService } from './service/surveyPublishService';
|
||||
import { getValidateValue } from './utils/index';
|
||||
import { checkSign } from './utils/checkSign';
|
||||
import * as Joi from 'joi';
|
||||
|
||||
@SurveyApp('/api/surveyPublish')
|
||||
export default class SurveyPublish {
|
||||
// 获取发布配置
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/getSurveyPublish' })
|
||||
async getSurveyPublish({ req }) {
|
||||
const surveySubmitData = getValidateValue(Joi.object({
|
||||
surveyPath: Joi.string().required(),
|
||||
}).validate(req.query, { allowUnknown: true }));
|
||||
const data = await surveyPublishService.get(surveySubmitData);
|
||||
return {
|
||||
code: 200,
|
||||
data: data.surveyPublishRes,
|
||||
};
|
||||
}
|
||||
// 获取投票
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/queryVote' })
|
||||
async queryVote({ req }) {
|
||||
const params = getValidateValue(Joi.object({
|
||||
surveyPath: Joi.string().required(),
|
||||
voteKeyList: Joi.string().required(),
|
||||
}).validate(req.query, { allowUnknown: true }));
|
||||
params.voteKeyList = params.voteKeyList.split(',');
|
||||
const data = await surveyPublishService.queryVote(params);
|
||||
return {
|
||||
code: 200,
|
||||
data: data,
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/getEncryptInfo' })
|
||||
async getEncryptInfo() {
|
||||
const data = await surveySubmitService.getEncryptInfo();
|
||||
return {
|
||||
code: 200,
|
||||
data: data,
|
||||
};
|
||||
}
|
||||
// 提交问卷
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/submit' })
|
||||
async submit({ req }) {
|
||||
// 检查签名
|
||||
checkSign(req.body);
|
||||
// 校验参数
|
||||
const surveySubmitData = getValidateValue(Joi.object({
|
||||
surveyPath: Joi.string().required(),
|
||||
data: Joi.string().required(),
|
||||
encryptType: Joi.string(),
|
||||
sessionId: Joi.string(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
await surveySubmitService.submit({ surveySubmitData });
|
||||
return {
|
||||
code: 200,
|
||||
msg: '提交成功',
|
||||
};
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import { mongo } from '../db/mongo';
|
||||
import { KeyStore } from '../../../types/keyStore';
|
||||
// 该服务用于模拟redis
|
||||
class SurveyKeyStoreService {
|
||||
getKeyStoreResult(surveyKeyStoreData: Array<KeyStore>) {
|
||||
const surveyKeyStoreReult = {};
|
||||
for (const surveyKeyStoreItem of surveyKeyStoreData) {
|
||||
surveyKeyStoreReult[surveyKeyStoreItem.key] = surveyKeyStoreItem.data;
|
||||
}
|
||||
return surveyKeyStoreReult;
|
||||
}
|
||||
|
||||
async set({ surveyPath, key, data, type }) {
|
||||
const surveyKeyStore = await mongo.getCollection({ collectionName: 'surveyKeyStore' });
|
||||
const setResult = await surveyKeyStore.updateOne({
|
||||
key,
|
||||
surveyPath,
|
||||
type
|
||||
}, {
|
||||
$set: {
|
||||
key,
|
||||
surveyPath,
|
||||
type,
|
||||
data,
|
||||
createDate: Date.now(),
|
||||
updateDate: Date.now(),
|
||||
}
|
||||
}, {
|
||||
upsert: true //如果不存在则插入
|
||||
});
|
||||
return setResult;
|
||||
}
|
||||
|
||||
async get({ surveyPath, key, type }) {
|
||||
const surveyKeyStore = await mongo.getCollection({ collectionName: 'surveyKeyStore' });
|
||||
const surveyKeyStoreData = await surveyKeyStore.findOne({
|
||||
key,
|
||||
surveyPath,
|
||||
type
|
||||
});
|
||||
return surveyKeyStoreData?.data;
|
||||
}
|
||||
|
||||
async getAll({ surveyPath, keyList, type }) {
|
||||
const surveyKeyStore = await mongo.getCollection({ collectionName: 'surveyKeyStore' });
|
||||
const res = await surveyKeyStore.find({
|
||||
key: { $in: keyList },
|
||||
surveyPath,
|
||||
type
|
||||
}).toArray();
|
||||
const surveyKeyStoreData : Array<KeyStore> = res.map(doc => {
|
||||
return {
|
||||
key: doc.key,
|
||||
surveyPath: doc.surveyPath,
|
||||
type: doc.type,
|
||||
data: doc.data,
|
||||
createDate: doc.createDate,
|
||||
updateDate: doc.updateDate,
|
||||
};
|
||||
});
|
||||
return this.getKeyStoreResult(surveyKeyStoreData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const surveyKeyStoreService = new SurveyKeyStoreService();
|
@ -1,31 +0,0 @@
|
||||
import { mongo } from '../db/mongo';
|
||||
import { surveyKeyStoreService } from './surveyKeyStoreService';
|
||||
import { CommonError } from '../../../types/index';
|
||||
|
||||
class SurveyPublishService {
|
||||
async get({ surveyPath }: { surveyPath: string }) {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const surveyMetaData = await surveyMeta.findOne({ surveyPath });
|
||||
if (!surveyMetaData) {
|
||||
throw new CommonError('该问卷已不存在');
|
||||
}
|
||||
const surveyMetaRes = mongo.convertId2StringByDoc(surveyMetaData);
|
||||
const surveyPublish = await mongo.getCollection({ collectionName: 'surveyPublish' });
|
||||
const surveyPublishData = await surveyPublish.findOne({ pageId: surveyMetaRes._id.toString() }, { sort: { createDate: -1 } });
|
||||
if (!surveyPublishData) {
|
||||
throw new CommonError('该问卷未发布');
|
||||
}
|
||||
const surveyPublishRes = mongo.convertId2StringByDoc(surveyPublishData);
|
||||
return {
|
||||
surveyMetaRes,
|
||||
surveyPublishRes
|
||||
};
|
||||
}
|
||||
|
||||
async queryVote({ surveyPath, voteKeyList }: { surveyPath: string, voteKeyList: Array<string> }) {
|
||||
return await surveyKeyStoreService.getAll({ surveyPath, keyList: voteKeyList, type: 'vote' });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const surveyPublishService = new SurveyPublishService();
|
@ -1,155 +0,0 @@
|
||||
import { mongo } from '../db/mongo';
|
||||
import { getStatusObject, randomCode } from '../utils/index';
|
||||
import { SURVEY_STATUS, CommonError } from '../../../types/index';
|
||||
import { surveyKeyStoreService } from './surveyKeyStoreService';
|
||||
import { getConfig } from '../config/index';
|
||||
import * as CryptoJS from 'crypto-js';
|
||||
import * as aes from 'crypto-js/aes';
|
||||
import * as moment from 'moment';
|
||||
import { keyBy } from 'lodash';
|
||||
import { rpcInvote } from '../../../rpc';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
class SurveySubmitService {
|
||||
|
||||
async addSessionData(data) {
|
||||
const surveySession = await mongo.getCollection({ collectionName: 'surveySession' });
|
||||
const surveySessionRes = await surveySession.insertOne({
|
||||
data,
|
||||
expireDate: Date.now() + config.session.expireTime
|
||||
});
|
||||
return {
|
||||
sessionId: surveySessionRes.insertedId.toString(),
|
||||
...data
|
||||
};
|
||||
}
|
||||
|
||||
async getSessionData(sessionId) {
|
||||
const surveySession = await mongo.getCollection({ collectionName: 'surveySession' });
|
||||
const sessionObjectId = mongo.getObjectIdByStr(sessionId);
|
||||
const surveySessionRes = await surveySession.findOne({ _id: sessionObjectId });
|
||||
await surveySession.deleteMany({ expireDate: { $lt: Date.now() } });
|
||||
return { sessionId, data: surveySessionRes.data };
|
||||
}
|
||||
|
||||
async getEncryptInfo() {
|
||||
const encryptType = config.encrypt.type;
|
||||
let data = {};
|
||||
if (encryptType === 'aes') {
|
||||
data = await this.addSessionData({
|
||||
code: randomCode(config.encrypt.aesCodelength)
|
||||
});
|
||||
}
|
||||
return {
|
||||
encryptType,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
async submit({ surveySubmitData }) {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const surveyMetaRes = mongo.convertId2StringByDoc(
|
||||
await surveyMeta.findOne({ surveyPath: surveySubmitData.surveyPath })
|
||||
);
|
||||
if (!surveyMetaRes) {
|
||||
throw new CommonError('该问卷已不存在,无法提交');
|
||||
}
|
||||
const pageId = surveyMetaRes._id.toString();
|
||||
const surveyPublish = await mongo.getCollection({ collectionName: 'surveyPublish' });
|
||||
const publishConf = await surveyPublish.findOne({ pageId });
|
||||
const surveySubmit = await mongo.getCollection({ collectionName: 'surveySubmit' });
|
||||
if (surveySubmitData.encryptType === 'base64') {
|
||||
surveySubmitData.data = JSON.parse(decodeURIComponent(Buffer.from(surveySubmitData.data, 'base64').toString()));
|
||||
} else if (surveySubmitData.encryptType === 'aes') {
|
||||
const sessionData = await this.getSessionData(surveySubmitData.sessionId);
|
||||
surveySubmitData.data = JSON.parse(decodeURIComponent(aes.decrypt(surveySubmitData.data, sessionData.data.code).toString(CryptoJS.enc.Utf8)));
|
||||
} else {
|
||||
surveySubmitData.data = JSON.parse(surveySubmitData.data);
|
||||
}
|
||||
// 提交时间限制
|
||||
const begTime = publishConf?.code?.baseConf?.begTime || 0;
|
||||
const endTime = publishConf?.code?.baseConf?.endTime || 0;
|
||||
if (begTime && endTime) {
|
||||
const nowStamp = Date.now();
|
||||
const begTimeStamp = new Date(begTime).getTime();
|
||||
const endTimeStamp = new Date(endTime).getTime();
|
||||
if (nowStamp < begTimeStamp || nowStamp > endTimeStamp) {
|
||||
throw new CommonError('不在答题有效期内');
|
||||
}
|
||||
}
|
||||
// 提交时间段限制
|
||||
const answerBegTime = publishConf?.code?.baseConf?.answerBegTime || '00:00:00';
|
||||
const answerEndTime = publishConf?.code?.baseConf?.answerEndTime || '23:59:59';
|
||||
if (answerBegTime && answerEndTime) {
|
||||
const nowStamp = Date.now();
|
||||
const ymdString = moment().format('YYYY-MM-DD');
|
||||
const answerBegTimeStamp = new Date(`${ymdString} ${answerBegTime}`).getTime();
|
||||
const answerEndTimeStamp = new Date(`${ymdString} ${answerEndTime}`).getTime();
|
||||
if (nowStamp < answerBegTimeStamp || nowStamp > answerEndTimeStamp) {
|
||||
throw new CommonError('不在答题时段内');
|
||||
}
|
||||
}
|
||||
// 提交总数限制
|
||||
const tLimit = publishConf?.code?.baseConf?.tLimit || 0;
|
||||
if (tLimit > 0) {
|
||||
// 提升性能可以使用redis
|
||||
const nowSubmitCount = await surveySubmit.countDocuments({ surveyPath: surveySubmitData.surveyPath }) || 0;
|
||||
if (nowSubmitCount >= tLimit) {
|
||||
throw new CommonError('超出提交总数限制');
|
||||
}
|
||||
}
|
||||
const dataList = publishConf?.code?.dataConf?.dataList || [];
|
||||
const dataListMap = keyBy(dataList, 'field');
|
||||
|
||||
const surveySubmitDataKeys = Object.keys(surveySubmitData.data);
|
||||
|
||||
const secretKeys = [];
|
||||
|
||||
for (const field of surveySubmitDataKeys) {
|
||||
const configData = dataListMap[field];
|
||||
const value = surveySubmitData.data[field];
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
|
||||
if (configData && /vote/.exec(configData.type)) {
|
||||
// 投票信息保存
|
||||
const voteData = (await surveyKeyStoreService.get({ surveyPath: surveySubmitData.surveyPath, key: field, type: 'vote' })) || { total: 0 };
|
||||
voteData.total++;
|
||||
for (const val of values) {
|
||||
if (!voteData[val]) {
|
||||
voteData[val] = 1;
|
||||
} else {
|
||||
voteData[val]++;
|
||||
}
|
||||
}
|
||||
await surveyKeyStoreService.set({ surveyPath: surveySubmitData.surveyPath, key: field, data: voteData, type: 'vote' });
|
||||
}
|
||||
// 检查敏感数据,对敏感数据进行加密存储
|
||||
let isSecret = false;
|
||||
for (const val of values) {
|
||||
if (rpcInvote('security.isDataSensitive', val)) {
|
||||
isSecret = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isSecret) {
|
||||
secretKeys.push(field);
|
||||
surveySubmitData.data[field] = Array.isArray(value) ? value.map(item => rpcInvote('security.encryptData', item)) : rpcInvote('security.encryptData', value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
surveySubmitData.secretKeys = secretKeys;
|
||||
|
||||
// 提交问卷
|
||||
const surveySubmitRes = await surveySubmit.insertOne({
|
||||
...surveySubmitData,
|
||||
pageId,
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.new }),
|
||||
createDate: Date.now()
|
||||
});
|
||||
return surveySubmitRes;
|
||||
}
|
||||
}
|
||||
|
||||
export const surveySubmitService = new SurveySubmitService();
|
@ -1,24 +0,0 @@
|
||||
import { SURVEY_STATUS, CommonError } from '../../../types/index';
|
||||
import * as Joi from 'joi';
|
||||
|
||||
export function getStatusObject({ status }: { status: SURVEY_STATUS }) {
|
||||
return {
|
||||
status,
|
||||
id: status,
|
||||
date: Date.now(),
|
||||
};
|
||||
}
|
||||
export function getValidateValue<T = unknown>(validationResult: Joi.ValidationResult<T>): T {
|
||||
if (validationResult.error) {
|
||||
throw new CommonError(validationResult.error.details.map(e => e.message).join());
|
||||
}
|
||||
return validationResult.value;
|
||||
}
|
||||
|
||||
export function randomCode(length) {
|
||||
const charList: Array<string> = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
charList.push(Math.floor(Math.random() * 16).toString(16));
|
||||
}
|
||||
return charList.join('');
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { SurveyApp, SurveyServer } from '../../decorator';
|
||||
import { createReadStream } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@SurveyApp('')
|
||||
export default class UI {
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/render/(.*)' })
|
||||
async render({ res }) {
|
||||
const filePath = path.join(__dirname, 'public', 'render.html');
|
||||
res.type = path.extname(filePath);
|
||||
return createReadStream(filePath);
|
||||
}
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/management/(.*)' })
|
||||
async management({ res }) {
|
||||
const filePath = path.join(__dirname, 'public', 'management.html');
|
||||
res.type = path.extname(filePath);
|
||||
return createReadStream(filePath);
|
||||
}
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/' })
|
||||
async index({ res }) {
|
||||
const filePath = path.join(__dirname, 'public', 'management.html');
|
||||
res.type = path.extname(filePath);
|
||||
return createReadStream(filePath);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { mongo, jwt } from '../../../config';
|
||||
|
||||
export function getConfig() {
|
||||
return {
|
||||
mongo,
|
||||
jwt,
|
||||
};
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { getConfig } from '../config/index';
|
||||
import MongoService from '../../../utils/mongoService';
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
export const mongo = new MongoService({ url: config.mongo.url, dbName: config.mongo.dbName });
|
@ -1,85 +0,0 @@
|
||||
import { SurveyApp, SurveyServer } from '../../decorator';
|
||||
import { Request, Response } from 'koa';
|
||||
import * as Joi from 'joi';
|
||||
import { userService } from './service/userService';
|
||||
import { captchaService } from './service/captchaService';
|
||||
import { getValidateValue } from './utils/index';
|
||||
|
||||
import { CommonError } from '../../types/index';
|
||||
|
||||
@SurveyApp('/api/user')
|
||||
export default class User {
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/register' })
|
||||
async register({ req }: { req: Request, res: Response }) {
|
||||
const userInfo = getValidateValue(Joi.object({
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
captchaId: Joi.string().required(),
|
||||
captcha: Joi.string().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
const isCorrect = await captchaService.checkCaptchaIsCorrect({ captcha: userInfo.captcha, id: userInfo.captchaId });
|
||||
if (!isCorrect) {
|
||||
throw new CommonError('验证码不正确');
|
||||
}
|
||||
const userRegisterRes = await userService.register({
|
||||
username: userInfo.username,
|
||||
password: userInfo.password,
|
||||
});
|
||||
// 删除验证码
|
||||
captchaService.deleteCaptcha({ id: userInfo.captchaId });
|
||||
return {
|
||||
code: 200,
|
||||
data: userRegisterRes,
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/login' })
|
||||
async login({ req }: { req: Request, res: Response }) {
|
||||
const userInfo = getValidateValue(Joi.object({
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
captchaId: Joi.string().required(),
|
||||
captcha: Joi.string().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
const isCorrect = await captchaService.checkCaptchaIsCorrect({ captcha: userInfo.captcha, id: userInfo.captchaId });
|
||||
if (!isCorrect) {
|
||||
throw new CommonError('验证码不正确');
|
||||
}
|
||||
const data = await userService.login({
|
||||
username: userInfo.username,
|
||||
password: userInfo.password,
|
||||
});
|
||||
// 删除验证码
|
||||
captchaService.deleteCaptcha({ id: userInfo.captchaId });
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'rpc' })
|
||||
async getUserByToken({ params, context }) {
|
||||
const data = await userService.getUserByToken({ token: params.token });
|
||||
return {
|
||||
result: data,
|
||||
context, // 上下文主要是传递调用方信息使用,比如traceid
|
||||
};
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/captcha' })
|
||||
async refreshCaptcha({ req }) {
|
||||
const captchaData = captchaService.createCaptcha();
|
||||
const res = await captchaService.addCaptchaData({ text: captchaData.text });
|
||||
if (req.body && req.body.captchaId) {
|
||||
// 删除验证码
|
||||
captchaService.deleteCaptcha({ id: req.body.captchaId });
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
id: res.insertedId,
|
||||
img: captchaData.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { mongo } from '../db/mongo';
|
||||
import { create } from 'svg-captcha';
|
||||
class CaptchaService {
|
||||
|
||||
createCaptcha() {
|
||||
return create({
|
||||
size: 4, // 验证码长度
|
||||
ignoreChars: '0o1i', // 忽略字符
|
||||
noise: 3, // 干扰线数量
|
||||
color: true, // 启用彩色
|
||||
background: '#f0f0f0', // 背景色
|
||||
});
|
||||
}
|
||||
|
||||
async addCaptchaData({ text }) {
|
||||
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
|
||||
const addRes = await captchaDb.insertOne({
|
||||
text,
|
||||
});
|
||||
return addRes;
|
||||
}
|
||||
|
||||
async checkCaptchaIsCorrect({ captcha, id }) {
|
||||
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
|
||||
const captchaData = await captchaDb.findOne({
|
||||
_id: mongo.getObjectIdByStr(id),
|
||||
});
|
||||
return captcha.toLowerCase() === captchaData?.text?.toLowerCase();
|
||||
}
|
||||
|
||||
async deleteCaptcha({ id }) {
|
||||
const captchaDb = await mongo.getCollection({ collectionName: 'captcha' });
|
||||
const _id = mongo.getObjectIdByStr(id);
|
||||
await captchaDb.deleteOne({
|
||||
_id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const captchaService = new CaptchaService();
|
@ -1,81 +0,0 @@
|
||||
import {
|
||||
verify as jwtVerify,
|
||||
sign as jwtSign
|
||||
} from 'jsonwebtoken';
|
||||
import {
|
||||
createHash
|
||||
} from 'crypto';
|
||||
import { mongo } from '../db/mongo';
|
||||
import { getStatusObject } from '../utils/index';
|
||||
import { SURVEY_STATUS, CommonError } from '../../../types/index';
|
||||
import { getConfig } from '../config/index';
|
||||
const config = getConfig();
|
||||
|
||||
|
||||
class UserService {
|
||||
|
||||
hash256(text) {
|
||||
return createHash('sha256').update(text).digest('hex');
|
||||
}
|
||||
|
||||
|
||||
getToken(userInfo) {
|
||||
return jwtSign(userInfo, config.jwt.secret, { expiresIn: config.jwt.expiresIn });
|
||||
}
|
||||
|
||||
async register(userInfo: { username: string, password: string }) {
|
||||
const user = await mongo.getCollection({ collectionName: 'user' });
|
||||
const userRes = await user.findOne({
|
||||
username: userInfo.username,
|
||||
});
|
||||
if (userRes) {
|
||||
throw new CommonError('该用户已存在');
|
||||
}
|
||||
const userInsertRes = await user.insertOne({
|
||||
username: userInfo.username,
|
||||
password: this.hash256(userInfo.password),
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.new }),
|
||||
createDate: Date.now()
|
||||
});
|
||||
const token = this.getToken({
|
||||
_id: userInsertRes.insertedId.toString(),
|
||||
username: userInfo.username
|
||||
});
|
||||
return { userInsertRes, token, username: userInfo.username };
|
||||
}
|
||||
|
||||
async login(userInfo: { username: string, password: string }) {
|
||||
const user = await mongo.getCollection({ collectionName: 'user' });
|
||||
const userRes = await user.findOne({
|
||||
username: userInfo.username,
|
||||
password: this.hash256(userInfo.password),
|
||||
});
|
||||
if (!userRes) {
|
||||
throw new CommonError('用户名或密码错误');
|
||||
}
|
||||
const token = this.getToken({
|
||||
_id: userRes._id.toString(),
|
||||
username: userInfo.username
|
||||
});
|
||||
return { token, username: userInfo.username };
|
||||
}
|
||||
|
||||
async getUserByToken(tokenInfo: { token: string }) {
|
||||
let userInfo;
|
||||
try {
|
||||
userInfo = jwtVerify(tokenInfo.token, config.jwt.secret);
|
||||
} catch (err) {
|
||||
throw new CommonError('用户凭证无效或已过期', 403);
|
||||
}
|
||||
const user = await mongo.getCollection({ collectionName: 'user' });
|
||||
const userRes = await user.findOne({
|
||||
_id: mongo.getObjectIdByStr(userInfo._id),
|
||||
});
|
||||
if (!userRes) {
|
||||
throw new CommonError('用户已不存在');
|
||||
}
|
||||
return mongo.convertId2StringByDoc(userRes);
|
||||
}
|
||||
}
|
||||
|
||||
export const userService = new UserService();
|
@ -1,15 +0,0 @@
|
||||
import { SURVEY_STATUS, CommonError } from '../../../types/index';
|
||||
import * as Joi from 'joi';
|
||||
export function getStatusObject({ status }: { status: SURVEY_STATUS }) {
|
||||
return {
|
||||
status,
|
||||
id: status,
|
||||
date: Date.now(),
|
||||
};
|
||||
}
|
||||
export function getValidateValue<T = unknown>(validationResult: Joi.ValidationResult<T>): T {
|
||||
if (validationResult.error) {
|
||||
throw new CommonError(validationResult.error.details.map(e => e.message).join());
|
||||
}
|
||||
return validationResult.value;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
const mongo = {
|
||||
url: process.env.XIAOJU_SURVEY_MONGO_URL || 'mongodb://localhost:27017',
|
||||
dbName: process.env.XIAOJU_SURVER_MONGO_DBNAME || 'xiaojuSurvey',
|
||||
}
|
||||
|
||||
const session = {
|
||||
expireTime: parseInt(process.env.XIAOJU_SURVEY_JWT_EXPIRES_IN) || 8 * 3600 * 1000
|
||||
}
|
||||
|
||||
const encrypt = {
|
||||
type: process.env.XIAOJU_SURVEY_ENCRYPT_TYPE || 'aes',
|
||||
aesCodelength: parseInt(process.env.XIAOJU_SURVEY_ENCRYPT_TYPE_LEN) || 10 //aes密钥长度
|
||||
}
|
||||
|
||||
const jwt = {
|
||||
secret: process.env.XIAOJU_SURVEY_JWT_SECRET || 'xiaojuSurveyJwtSecret',
|
||||
expiresIn: process.env.XIAOJU_SURVEY_JWT_EXPIRES_IN || '8h'
|
||||
}
|
||||
|
||||
|
||||
export{
|
||||
mongo,
|
||||
session,
|
||||
encrypt,
|
||||
jwt,
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
type ServerType = 'http' | 'websocket' | 'rpc'
|
||||
export interface RouterOptions {
|
||||
type: ServerType,
|
||||
method?: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options';
|
||||
routerName?: string;
|
||||
}
|
||||
|
||||
export const surveyServerKey = Symbol('surveyServer'); // vm环境和worker环境上下文不一致导致不能使用Symbol
|
||||
export const surveyAppKey = Symbol('surveyApp');
|
||||
|
||||
export function SurveyApp(routerName) {
|
||||
return (target: unknown) => {
|
||||
if (!target[surveyAppKey]) {
|
||||
target[surveyAppKey] = routerName;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function SurveyServer(options: RouterOptions) {
|
||||
return function(target: unknown, propertyKey: string) {
|
||||
if (!target[surveyServerKey]) {
|
||||
target[surveyServerKey] = new Map<string, RouterOptions>();
|
||||
}
|
||||
target[surveyServerKey].set(
|
||||
propertyKey,
|
||||
options
|
||||
);
|
||||
};
|
||||
}
|
4
server/src/enums/encrypt.ts
Normal file
4
server/src/enums/encrypt.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum ENCRYPT_TYPE {
|
||||
AES = 'aes',
|
||||
RSA = 'rsa',
|
||||
}
|
18
server/src/enums/exceptionCode.ts
Normal file
18
server/src/enums/exceptionCode.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export enum EXCEPTION_CODE {
|
||||
AUTHTIFICATION_FAILED = 1001, // 没有权限
|
||||
PARAMETER_ERROR = 1002, // 参数有误
|
||||
USER_EXISTS = 2001, // 用户已存在
|
||||
USER_NOT_EXISTS = 2002, // 用户不存在
|
||||
NO_SURVEY_PERMISSION = 3001, // 没有问卷权限
|
||||
SURVEY_STATUS_TRANSFORM_ERROR = 3002, // 问卷状态转换报错
|
||||
SURVEY_TYPE_ERROR = 3003, // 问卷类型错误
|
||||
SURVEY_NOT_FOUND = 3004, // 问卷不存在
|
||||
SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容
|
||||
CAPTCHA_INCORRECT = 4001, // 验证码不正确
|
||||
|
||||
RESPONSE_SIGN_ERROR = 9001, // 签名不正确
|
||||
RESPONSE_CURRENT_TIME_NOT_ALLOW = 9002, // 当前时间不允许提交
|
||||
RESPONSE_OVER_LIMIT = 9003, // 超出限制
|
||||
RESPONSE_SCHEMA_REMOVED = 9004, // 问卷已删除
|
||||
RESPONSE_DATA_DECRYPT_ERROR = 9005, // 问卷已删除
|
||||
}
|
15
server/src/enums/index.ts
Normal file
15
server/src/enums/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// 状态类型
|
||||
export enum RECORD_STATUS {
|
||||
NEW = 'new', // 新建
|
||||
EDITING = 'editing', // 编辑
|
||||
PAUSING = 'pausing', // 暂停
|
||||
PUBLISHED = 'published', // 发布
|
||||
REMOVED = 'removed', // 删除
|
||||
FORCE_REMOVED = 'forceRemoved', // 从回收站删除
|
||||
}
|
||||
|
||||
// 历史类型
|
||||
export enum HISTORY_TYPE {
|
||||
DAILY_HIS = 'dailyHis', //保存历史
|
||||
PUBLISH_HIS = 'publishHis', //发布历史
|
||||
}
|
7
server/src/exceptions/authException.ts
Normal file
7
server/src/exceptions/authException.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { HttpException } from './httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
export class AuthtificationException extends HttpException {
|
||||
constructor(public readonly message: string) {
|
||||
super(message, EXCEPTION_CODE.AUTHTIFICATION_FAILED);
|
||||
}
|
||||
}
|
10
server/src/exceptions/httpException.ts
Normal file
10
server/src/exceptions/httpException.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
export class HttpException extends Error {
|
||||
constructor(
|
||||
public readonly message: string,
|
||||
public readonly code: EXCEPTION_CODE,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
33
server/src/exceptions/httpExceptions.filter.ts
Normal file
33
server/src/exceptions/httpExceptions.filter.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// all-exceptions.filter.ts
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { HttpException } from './httpException';
|
||||
|
||||
@Catch(Error)
|
||||
export class HttpExceptionsFilter implements ExceptionFilter {
|
||||
catch(exception: Error, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let message = 'Internal Server Error';
|
||||
let code = 500;
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = HttpStatus.OK; // 非系统报错状态码为200
|
||||
message = exception.message;
|
||||
code = exception.code;
|
||||
}
|
||||
|
||||
response.status(status).json({
|
||||
message,
|
||||
code,
|
||||
errmsg: exception.message,
|
||||
});
|
||||
}
|
||||
}
|
8
server/src/exceptions/noSurveyPermissionException.ts
Normal file
8
server/src/exceptions/noSurveyPermissionException.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { HttpException } from './httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
export class NoSurveyPermissionException extends HttpException {
|
||||
constructor(public readonly message: string) {
|
||||
super(message, EXCEPTION_CODE.NO_SURVEY_PERMISSION);
|
||||
}
|
||||
}
|
8
server/src/exceptions/surveyNotFoundException.ts
Normal file
8
server/src/exceptions/surveyNotFoundException.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { HttpException } from './httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
export class SurveyNotFoundException extends HttpException {
|
||||
constructor(public readonly message: string) {
|
||||
super(message, EXCEPTION_CODE.SURVEY_NOT_FOUND);
|
||||
}
|
||||
}
|
40
server/src/guards/authtication.ts
Normal file
40
server/src/guards/authtication.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { UserService } from '../modules/auth/services/user.service';
|
||||
import { verify } from 'jsonwebtoken';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthtificationException } from '../exceptions/authException';
|
||||
|
||||
@Injectable()
|
||||
export class Authtication implements CanActivate {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = request.headers.authorization?.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
throw new AuthtificationException('请登录');
|
||||
}
|
||||
|
||||
let decoded;
|
||||
try {
|
||||
decoded = verify(
|
||||
token,
|
||||
this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),
|
||||
);
|
||||
} catch (err) {
|
||||
throw new AuthtificationException('用户凭证错误');
|
||||
}
|
||||
const user = await this.userService.getUserByUsername(decoded.username); // 从数据库中查找用户
|
||||
|
||||
if (!user) {
|
||||
throw new AuthtificationException('用户不存在');
|
||||
}
|
||||
|
||||
request.user = user; // 将用户信息存储在请求中
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import * as os from 'os';
|
||||
import * as Koa from 'koa';
|
||||
import * as KoaBodyparser from 'koa-bodyparser';
|
||||
import * as KoaStatic from 'koa-static';
|
||||
import * as logger from 'koa-pino-logger';
|
||||
import { initRouter } from './router';
|
||||
import { outputCatch } from './middleware/outputCatch';
|
||||
import * as path from 'path';
|
||||
|
||||
async function main() {
|
||||
const app = new Koa();
|
||||
app.use(outputCatch({ showErrorStack: true }));
|
||||
app.use(logger());
|
||||
app.use(KoaBodyparser({
|
||||
formLimit: '30mb',
|
||||
jsonLimit: '30mb',
|
||||
textLimit: '30mb',
|
||||
xmlLimit: '30mb',
|
||||
}));
|
||||
|
||||
await initRouter(app);
|
||||
|
||||
app.use(KoaStatic(path.join(__dirname, './apps/ui/public')));
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
app.listen(port);
|
||||
process.stdout.write(`${os.EOL}server run: http://127.0.0.1:${port} ${os.EOL}`);
|
||||
}
|
||||
|
||||
main();
|
@ -1,3 +1,5 @@
|
||||
// 问卷配置内容定义
|
||||
|
||||
export interface TitleConfig {
|
||||
mainTitle: string;
|
||||
subTitle: string;
|
||||
@ -9,16 +11,12 @@ export interface BannerConfig {
|
||||
postImg: string;
|
||||
}
|
||||
|
||||
// 问卷头部内容:标题和头图
|
||||
export interface BannerConf {
|
||||
titleConfig: TitleConfig;
|
||||
bannerConfig: BannerConfig;
|
||||
}
|
||||
|
||||
export interface TimeStep {
|
||||
hour: number;
|
||||
min: number;
|
||||
}
|
||||
|
||||
export interface NPS {
|
||||
leftText: string;
|
||||
rightText: string;
|
||||
@ -49,9 +47,7 @@ export interface DataItem {
|
||||
checked: boolean;
|
||||
minNum: string;
|
||||
maxNum: string;
|
||||
maxPhotos: number;
|
||||
star: number;
|
||||
timeStep: TimeStep;
|
||||
nps: NPS;
|
||||
placeholderDesc: string;
|
||||
addressType: number;
|
||||
@ -102,7 +98,9 @@ export interface SubmitConf {
|
||||
export interface BaseConf {
|
||||
begTime: string;
|
||||
endTime: string;
|
||||
tLimit: string;
|
||||
answerBegTime: string;
|
||||
answerEndTime: string;
|
||||
tLimit: number;
|
||||
language: string;
|
||||
}
|
||||
|
||||
@ -111,7 +109,7 @@ export interface SkinConf {
|
||||
inputBgColor: string;
|
||||
}
|
||||
|
||||
export interface ParsedData {
|
||||
export interface SurveySchemaInterface {
|
||||
bannerConf: BannerConf;
|
||||
dataConf: DataConf;
|
||||
submitConf: SubmitConf;
|
57
server/src/logger/index.ts
Normal file
57
server/src/logger/index.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as log4js from 'log4js';
|
||||
import moment from 'moment';
|
||||
|
||||
const log4jsLogger = log4js.getLogger();
|
||||
|
||||
export class Logger {
|
||||
private traceId: string = '';
|
||||
private inited = false;
|
||||
|
||||
init(config: { filename: string }) {
|
||||
if (this.inited) {
|
||||
return;
|
||||
}
|
||||
log4js.configure({
|
||||
appenders: {
|
||||
app: {
|
||||
type: 'dateFile',
|
||||
filename: config.filename || './logs/app.log',
|
||||
pattern: 'yyyy-MM-dd',
|
||||
alwaysIncludePattern: true,
|
||||
numBackups: 7,
|
||||
layout: {
|
||||
type: 'pattern',
|
||||
pattern: '%m',
|
||||
},
|
||||
},
|
||||
},
|
||||
categories: {
|
||||
default: { appenders: ['app'], level: 'trace' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setTraceId(traceId: string) {
|
||||
this.traceId = traceId;
|
||||
}
|
||||
|
||||
_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 traceId = this.traceId ? `traceid=${this.traceId}||` : '';
|
||||
return log4jsLogger[level](
|
||||
`[${datetime}][${level.toUpperCase()}]${dltag}${traceId}${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
info(message, options = { dltag: '' }) {
|
||||
return this._log(message, { ...options, level: 'info' });
|
||||
}
|
||||
|
||||
error(message, options = { dltag: '' }) {
|
||||
return this._log(message, { ...options, level: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
export default new Logger();
|
8
server/src/logger/logger.provider.ts
Normal file
8
server/src/logger/logger.provider.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
|
||||
import logger, { Logger } from './index';
|
||||
|
||||
export const LoggerProvider: Provider = {
|
||||
provide: Logger,
|
||||
useValue: logger,
|
||||
};
|
27
server/src/logger/util.ts
Normal file
27
server/src/logger/util.ts
Normal file
@ -0,0 +1,27 @@
|
||||
let count = 999;
|
||||
|
||||
const getCountStr = () => {
|
||||
count++;
|
||||
if (count > 9000) {
|
||||
count = 1000;
|
||||
}
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
export const genTraceId = ({ ip }) => {
|
||||
// ip转16位 + 当前时间戳(毫秒级)+自增序列(1000开始自增到9000)+ 当前进程id的后5位
|
||||
ip = ip.replace('::ffff:', '');
|
||||
let ipArr;
|
||||
if (ip.indexOf(':') > 0) {
|
||||
ipArr = ip.split(':').map((segment) => {
|
||||
// 将IPv6每个段转为16位,并补0到长度为4
|
||||
return parseInt(segment, 16).toString(16).padStart(4, '0');
|
||||
});
|
||||
} else {
|
||||
ipArr = ip
|
||||
.split('.')
|
||||
.map((item) => parseInt(item).toString(16).padStart(2, '0'));
|
||||
}
|
||||
|
||||
return `${ipArr.join('')}${Date.now().toString()}${getCountStr()}${process.pid.toString().slice(-5)}`;
|
||||
};
|
11
server/src/main.ts
Normal file
11
server/src/main.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(PORT);
|
||||
console.log(`server is running at: http://127.0.0.1:${PORT}`);
|
||||
}
|
||||
|
||||
bootstrap();
|
@ -1,13 +0,0 @@
|
||||
export function outputCatch({ showErrorStack }: { showErrorStack: boolean }) {
|
||||
return async function(ctx, next) {
|
||||
try {
|
||||
await next();
|
||||
} catch (err) {
|
||||
const outputData = { ...err };
|
||||
if (showErrorStack) {
|
||||
outputData.stack = err.stack;
|
||||
}
|
||||
return ctx.body = outputData;
|
||||
}
|
||||
};
|
||||
}
|
38
server/src/middlewares/logRequest.middleware.ts
Normal file
38
server/src/middlewares/logRequest.middleware.ts
Normal file
@ -0,0 +1,38 @@
|
||||
// logger.middleware.ts
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Logger } from '../logger/index'; // 替换为你实际的logger路径
|
||||
import { genTraceId } from '../logger/util';
|
||||
|
||||
@Injectable()
|
||||
export class LogRequestMiddleware implements NestMiddleware {
|
||||
constructor(private readonly logger: Logger) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const { method, originalUrl, ip } = req;
|
||||
const userAgent = req.get('user-agent') || '';
|
||||
const startTime = Date.now();
|
||||
const traceId = genTraceId({ ip });
|
||||
this.logger.setTraceId(traceId);
|
||||
const query = JSON.stringify(req.query);
|
||||
const body = JSON.stringify(req.body);
|
||||
this.logger.info(
|
||||
`method=${method}||uri=${originalUrl}||ip=${ip}||ua=${userAgent}||query=${query}||body=${body}`,
|
||||
{
|
||||
dltag: 'request_in',
|
||||
},
|
||||
);
|
||||
|
||||
res.once('finish', () => {
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.info(
|
||||
`status=${res.statusCode.toString()}||duration=${duration}ms`,
|
||||
{
|
||||
dltag: 'request_out',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
58
server/src/models/captcha.entity.ts
Normal file
58
server/src/models/captcha.entity.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
Index,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'captcha' })
|
||||
export class Captcha {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@Column()
|
||||
text: string;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
66
server/src/models/clientEncrypt.entity.ts
Normal file
66
server/src/models/clientEncrypt.entity.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
Index,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { ENCRYPT_TYPE } from '../enums/encrypt';
|
||||
|
||||
@Entity({ name: 'clientEncrypt' })
|
||||
export class ClientEncrypt {
|
||||
@Index({
|
||||
expireAfterSeconds:
|
||||
new Date(Date.now() + 2 * 60 * 60 * 1000).getTime() / 1000,
|
||||
})
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column('jsonb')
|
||||
data: {
|
||||
secretKey?: string; // aes加密的密钥
|
||||
publicKey?: string; // rsa加密的公钥
|
||||
privateKey?: string; // rsa加密的私钥
|
||||
};
|
||||
|
||||
@Column()
|
||||
type: ENCRYPT_TYPE;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
62
server/src/models/counter.entity.ts
Normal file
62
server/src/models/counter.entity.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'counter' })
|
||||
export class Counter {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@Column()
|
||||
key: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column('jsonb')
|
||||
data: Record<string, any>;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
63
server/src/models/responseSchema.entity.ts
Normal file
63
server/src/models/responseSchema.entity.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { SurveySchemaInterface } from '../interfaces/survey';
|
||||
|
||||
@Entity({ name: 'surveyPublish' })
|
||||
export class ResponseSchema {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
@Column('jsonb')
|
||||
code: SurveySchemaInterface;
|
||||
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
57
server/src/models/surveyConf.entity.ts
Normal file
57
server/src/models/surveyConf.entity.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import { SurveySchemaInterface } from '../interfaces/survey';
|
||||
|
||||
@Entity({ name: 'surveyConf' })
|
||||
export class SurveyConf {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
createDate: number;
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
updateDate: number;
|
||||
|
||||
@Column('jsonb')
|
||||
code: SurveySchemaInterface;
|
||||
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
66
server/src/models/surveyHistory.entity.ts
Normal file
66
server/src/models/surveyHistory.entity.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { HISTORY_TYPE, RECORD_STATUS } from '../enums';
|
||||
import { SurveySchemaInterface } from '../interfaces/survey';
|
||||
|
||||
@Entity({ name: 'surveyHistory' })
|
||||
export class SurveyHistory {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@Column()
|
||||
type: HISTORY_TYPE;
|
||||
|
||||
@Column('jsonb')
|
||||
schema: SurveySchemaInterface;
|
||||
|
||||
@Column('jsonb')
|
||||
operator: {
|
||||
username: string;
|
||||
_id: string;
|
||||
};
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
74
server/src/models/surveyMeta.entity.ts
Normal file
74
server/src/models/surveyMeta.entity.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'surveyMeta' })
|
||||
export class SurveyMeta {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
remark: string;
|
||||
|
||||
@Column()
|
||||
surveyType: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
@Column()
|
||||
creator: string;
|
||||
|
||||
@Column()
|
||||
owner: string;
|
||||
|
||||
@Column()
|
||||
createMethod: string;
|
||||
|
||||
@Column()
|
||||
createFrom: string;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
79
server/src/models/surveyResponse.entity.ts
Normal file
79
server/src/models/surveyResponse.entity.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
AfterLoad,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
import pluginManager from '../securityPlugin/pluginManager';
|
||||
|
||||
@Entity({ name: 'surveySubmit' })
|
||||
export class SurveyResponse {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
pageId: string;
|
||||
|
||||
@Column()
|
||||
surveyPath: string;
|
||||
|
||||
@Column('jsonb')
|
||||
data: Record<string, any>;
|
||||
|
||||
@Column()
|
||||
difTime: number;
|
||||
|
||||
@Column()
|
||||
clientTime: number;
|
||||
|
||||
@Column('jsonb')
|
||||
secretKeys: Array<string>;
|
||||
|
||||
@Column('jsonb')
|
||||
optionTextAndId: Record<string, any>;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@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;
|
||||
pluginManager.triggerHook('beforeResponseDataCreate', this);
|
||||
}
|
||||
|
||||
@BeforeUpdate()
|
||||
onUpdate() {
|
||||
this.updateDate = Date.now();
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
onDataLoaded() {
|
||||
pluginManager.triggerHook('afterResponseDataReaded', this);
|
||||
}
|
||||
}
|
56
server/src/models/user.entity.ts
Normal file
56
server/src/models/user.entity.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'user' })
|
||||
export class User {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@Column()
|
||||
statusList: Array<{
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
}>;
|
||||
|
||||
@Column()
|
||||
createDate: number;
|
||||
|
||||
@Column()
|
||||
updateDate: number;
|
||||
|
||||
@Column()
|
||||
username: string;
|
||||
|
||||
@Column()
|
||||
password: string;
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
56
server/src/models/word.entity.ts
Normal file
56
server/src/models/word.entity.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ObjectIdColumn,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
} from 'typeorm';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { RECORD_STATUS } from '../enums';
|
||||
|
||||
@Entity({ name: 'word' })
|
||||
export class Word {
|
||||
@ObjectIdColumn()
|
||||
_id: ObjectId;
|
||||
|
||||
@Column()
|
||||
text: string;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
curStatus: {
|
||||
status: RECORD_STATUS;
|
||||
date: number;
|
||||
};
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
20
server/src/modules/auth/auth.module.ts
Normal file
20
server/src/modules/auth/auth.module.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserService } from './services/user.service';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { CaptchaService } from './services/captcha.service';
|
||||
|
||||
import { AuthController } from './controllers/auth.controller';
|
||||
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { Captcha } from 'src/models/captcha.entity';
|
||||
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User, Captcha]), ConfigModule],
|
||||
controllers: [AuthController],
|
||||
providers: [UserService, AuthService, CaptchaService],
|
||||
exports: [UserService],
|
||||
})
|
||||
export class AuthModule {}
|
152
server/src/modules/auth/controllers/auth.controller.spec.ts
Normal file
152
server/src/modules/auth/controllers/auth.controller.spec.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
import { AuthController } from './auth.controller';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { CaptchaService } from '../services/captcha.service';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { User } from 'src/models/user.entity';
|
||||
|
||||
jest.mock('../services/captcha.service');
|
||||
jest.mock('../services/auth.service');
|
||||
jest.mock('../services/user.service');
|
||||
|
||||
describe('AuthController', () => {
|
||||
let controller: AuthController;
|
||||
let userService: UserService;
|
||||
let captchaService: CaptchaService;
|
||||
let authService: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [ConfigModule.forRoot()],
|
||||
controllers: [AuthController],
|
||||
providers: [UserService, CaptchaService, ConfigService, AuthService],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AuthController>(AuthController);
|
||||
userService = module.get<UserService>(UserService);
|
||||
captchaService = module.get<CaptchaService>(CaptchaService);
|
||||
authService = module.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a user and return a token when captcha is correct', async () => {
|
||||
const mockUserInfo = {
|
||||
username: 'testUser',
|
||||
password: 'testPassword',
|
||||
captchaId: 'testCaptchaId',
|
||||
captcha: 'testCaptcha',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(captchaService, 'checkCaptchaIsCorrect')
|
||||
.mockResolvedValue(true);
|
||||
jest.spyOn(userService, 'createUser').mockResolvedValue(
|
||||
Promise.resolve({
|
||||
username: 'testUser',
|
||||
_id: new ObjectId(),
|
||||
} as User),
|
||||
);
|
||||
jest.spyOn(authService, 'generateToken').mockResolvedValue('testToken');
|
||||
|
||||
const result = await controller.register(mockUserInfo);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
token: 'testToken',
|
||||
username: 'testUser',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw HttpException with CAPTCHA_INCORRECT code when captcha is incorrect', async () => {
|
||||
const mockUserInfo = {
|
||||
username: 'testUser',
|
||||
password: 'testPassword',
|
||||
captchaId: 'testCaptchaId',
|
||||
captcha: 'testCaptcha',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(captchaService, 'checkCaptchaIsCorrect')
|
||||
.mockResolvedValue(false);
|
||||
|
||||
await expect(controller.register(mockUserInfo)).rejects.toThrow(
|
||||
new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should login a user and return a token when captcha is correct', async () => {
|
||||
const mockUserInfo = {
|
||||
username: 'testUser',
|
||||
password: 'testPassword',
|
||||
captchaId: 'testCaptchaId',
|
||||
captcha: 'testCaptcha',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(captchaService, 'checkCaptchaIsCorrect')
|
||||
.mockResolvedValue(true);
|
||||
jest.spyOn(userService, 'getUser').mockResolvedValue(
|
||||
Promise.resolve({
|
||||
username: 'testUser',
|
||||
_id: new ObjectId(),
|
||||
} as User),
|
||||
);
|
||||
jest.spyOn(authService, 'generateToken').mockResolvedValue('testToken');
|
||||
|
||||
const result = await controller.login(mockUserInfo);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
token: 'testToken',
|
||||
username: 'testUser',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw HttpException with CAPTCHA_INCORRECT code when captcha is incorrect', async () => {
|
||||
const mockUserInfo = {
|
||||
username: 'testUser',
|
||||
password: 'testPassword',
|
||||
captchaId: 'testCaptchaId',
|
||||
captcha: 'testCaptcha',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(captchaService, 'checkCaptchaIsCorrect')
|
||||
.mockResolvedValue(false);
|
||||
|
||||
await expect(controller.login(mockUserInfo)).rejects.toThrow(
|
||||
new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw HttpException with USER_NOT_EXISTS code when user is not found', async () => {
|
||||
const mockUserInfo = {
|
||||
username: 'testUser',
|
||||
password: 'testPassword',
|
||||
captchaId: 'testCaptchaId',
|
||||
captcha: 'testCaptcha',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(captchaService, 'checkCaptchaIsCorrect')
|
||||
.mockResolvedValue(true);
|
||||
jest.spyOn(userService, 'getUser').mockResolvedValue(null);
|
||||
|
||||
await expect(controller.login(mockUserInfo)).rejects.toThrow(
|
||||
new HttpException('用户名或密码错误', EXCEPTION_CODE.USER_NOT_EXISTS),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
156
server/src/modules/auth/controllers/auth.controller.ts
Normal file
156
server/src/modules/auth/controllers/auth.controller.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { CaptchaService } from '../services/captcha.service'; // 假设你的验证码服务在这里
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
|
||||
import { create } from 'svg-captcha';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
@Controller('/api/auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
readonly captchaService: CaptchaService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly authService: AuthService,
|
||||
) {}
|
||||
|
||||
@Post('/register')
|
||||
@HttpCode(200)
|
||||
async register(
|
||||
@Body()
|
||||
userInfo: {
|
||||
username: string;
|
||||
password: string;
|
||||
captchaId: string;
|
||||
captcha: string;
|
||||
},
|
||||
) {
|
||||
const isCorrect = await this.captchaService.checkCaptchaIsCorrect({
|
||||
captcha: userInfo.captcha,
|
||||
id: userInfo.captchaId,
|
||||
});
|
||||
|
||||
if (!isCorrect) {
|
||||
throw new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT);
|
||||
}
|
||||
|
||||
const user = await this.userService.createUser({
|
||||
username: userInfo.username,
|
||||
password: userInfo.password,
|
||||
});
|
||||
|
||||
const token = await this.authService.generateToken(
|
||||
{
|
||||
username: user.username,
|
||||
_id: user._id.toString(),
|
||||
},
|
||||
{
|
||||
secret: this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),
|
||||
expiresIn: this.configService.get<string>(
|
||||
'XIAOJU_SURVEY_JWT_EXPIRES_IN',
|
||||
),
|
||||
},
|
||||
);
|
||||
// 验证过的验证码要删掉,防止被别人保存重复调用
|
||||
this.captchaService.deleteCaptcha(userInfo.captchaId);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
token,
|
||||
username: user.username,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/login')
|
||||
@HttpCode(200)
|
||||
async login(
|
||||
@Body()
|
||||
userInfo: {
|
||||
username: string;
|
||||
password: string;
|
||||
captchaId: string;
|
||||
captcha: string;
|
||||
},
|
||||
) {
|
||||
const isCorrect = await this.captchaService.checkCaptchaIsCorrect({
|
||||
captcha: userInfo.captcha,
|
||||
id: userInfo.captchaId,
|
||||
});
|
||||
|
||||
if (!isCorrect) {
|
||||
throw new HttpException('验证码不正确', EXCEPTION_CODE.CAPTCHA_INCORRECT);
|
||||
}
|
||||
|
||||
const user = await this.userService.getUser({
|
||||
username: userInfo.username,
|
||||
password: userInfo.password,
|
||||
});
|
||||
if (user === null) {
|
||||
throw new HttpException(
|
||||
'用户名或密码错误',
|
||||
EXCEPTION_CODE.USER_NOT_EXISTS,
|
||||
);
|
||||
}
|
||||
let token;
|
||||
try {
|
||||
token = await this.authService.generateToken(
|
||||
{
|
||||
username: user.username,
|
||||
_id: user._id.toString(),
|
||||
},
|
||||
{
|
||||
secret: this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET'),
|
||||
expiresIn: this.configService.get<string>(
|
||||
'XIAOJU_SURVEY_JWT_EXPIRES_IN',
|
||||
),
|
||||
},
|
||||
);
|
||||
// 验证过的验证码要删掉,防止被别人保存重复调用
|
||||
this.captchaService.deleteCaptcha(userInfo.captchaId);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'generateToken erro:' +
|
||||
error.message +
|
||||
this.configService.get<string>('XIAOJU_SURVEY_JWT_SECRET') +
|
||||
this.configService.get<string>('XIAOJU_SURVEY_JWT_EXPIRES_IN'),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
token,
|
||||
username: user.username,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post('/captcha')
|
||||
@HttpCode(200)
|
||||
async getCaptcha(): Promise<{
|
||||
code: number;
|
||||
data: { id: string; img: string };
|
||||
}> {
|
||||
const captchaData = create({
|
||||
size: 4, // 验证码长度
|
||||
ignoreChars: '0o1i', // 忽略字符
|
||||
noise: 3, // 干扰线数量
|
||||
color: true, // 启用彩色
|
||||
background: '#f0f0f0', // 背景色
|
||||
});
|
||||
const res = await this.captchaService.createCaptcha(captchaData.text);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
id: res._id.toString(),
|
||||
img: captchaData.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
33
server/src/modules/auth/services/auth.service.spec.ts
Normal file
33
server/src/modules/auth/services/auth.service.spec.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
import { sign } from 'jsonwebtoken';
|
||||
|
||||
jest.mock('jsonwebtoken');
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
describe('generateToken', () => {
|
||||
it('should generate token successfully', async () => {
|
||||
const userData = { _id: 'mockUserId', username: 'mockUsername' };
|
||||
const tokenConfig = {
|
||||
secret: 'mockSecretKey',
|
||||
expiresIn: '8h',
|
||||
};
|
||||
|
||||
await service.generateToken(userData, tokenConfig);
|
||||
|
||||
expect(sign).toHaveBeenCalledWith(userData, tokenConfig.secret, {
|
||||
expiresIn: tokenConfig.expiresIn,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
15
server/src/modules/auth/services/auth.service.ts
Normal file
15
server/src/modules/auth/services/auth.service.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { sign } from 'jsonwebtoken';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
async generateToken(
|
||||
{ _id, username }: { _id: string; username: string },
|
||||
{ secret, expiresIn }: { secret: string; expiresIn: string },
|
||||
) {
|
||||
return sign({ _id, username }, secret, {
|
||||
expiresIn,
|
||||
});
|
||||
}
|
||||
}
|
111
server/src/modules/auth/services/captcha.service.spec.ts
Normal file
111
server/src/modules/auth/services/captcha.service.spec.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CaptchaService } from './captcha.service';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Captcha } from 'src/models/captcha.entity';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
describe('CaptchaService', () => {
|
||||
let service: CaptchaService;
|
||||
let captchaRepository: MongoRepository<Captcha>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
CaptchaService,
|
||||
{
|
||||
provide: getRepositoryToken(Captcha),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<CaptchaService>(CaptchaService);
|
||||
captchaRepository = module.get<MongoRepository<Captcha>>(
|
||||
getRepositoryToken(Captcha),
|
||||
);
|
||||
});
|
||||
|
||||
describe('createCaptcha', () => {
|
||||
it('should create a captcha successfully', async () => {
|
||||
const mockCaptchaText = 'xsfd';
|
||||
jest.spyOn(captchaRepository, 'create').mockImplementation((data) => {
|
||||
return {
|
||||
...data,
|
||||
} as Captcha;
|
||||
});
|
||||
jest.spyOn(captchaRepository, 'save').mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
...data,
|
||||
} as Captcha);
|
||||
});
|
||||
|
||||
const result = await service.createCaptcha(mockCaptchaText);
|
||||
|
||||
expect(result.text).toBe(mockCaptchaText);
|
||||
expect(captchaRepository.create).toHaveBeenCalledWith({
|
||||
text: mockCaptchaText,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCaptcha', () => {
|
||||
it('should get a captcha by ID successfully', async () => {
|
||||
const mockCaptchaId = new ObjectId();
|
||||
const mockCaptcha = new Captcha();
|
||||
mockCaptcha._id = mockCaptchaId;
|
||||
|
||||
jest
|
||||
.spyOn(captchaRepository, 'findOne')
|
||||
.mockImplementation(() => Promise.resolve(mockCaptcha));
|
||||
|
||||
const result = await service.getCaptcha(mockCaptchaId.toString());
|
||||
|
||||
expect(result).toBe(mockCaptcha);
|
||||
expect(captchaRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: mockCaptchaId },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCaptcha', () => {
|
||||
it('should delete a captcha by ID successfully', async () => {
|
||||
const mockCaptchaId = new ObjectId();
|
||||
|
||||
await service.deleteCaptcha(mockCaptchaId.toString());
|
||||
|
||||
expect(captchaRepository.delete).toHaveBeenCalledWith(mockCaptchaId);
|
||||
});
|
||||
|
||||
// Add more test cases for different scenarios
|
||||
});
|
||||
|
||||
describe('checkCaptchaIsCorrect', () => {
|
||||
it('should check if captcha is correct successfully', async () => {
|
||||
const mockCaptchaId = new ObjectId();
|
||||
const mockCaptcha = new Captcha();
|
||||
mockCaptcha._id = mockCaptchaId;
|
||||
mockCaptcha.text = 'asfq';
|
||||
jest
|
||||
.spyOn(captchaRepository, 'findOne')
|
||||
.mockImplementation(() => Promise.resolve(mockCaptcha));
|
||||
|
||||
const mockCaptchaData = {
|
||||
captcha: 'asfq',
|
||||
id: mockCaptchaId.toString(),
|
||||
};
|
||||
const result = await service.checkCaptchaIsCorrect(mockCaptchaData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(captchaRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { _id: mockCaptchaId },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
36
server/src/modules/auth/services/captcha.service.ts
Normal file
36
server/src/modules/auth/services/captcha.service.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { Captcha } from 'src/models/captcha.entity';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
@Injectable()
|
||||
export class CaptchaService {
|
||||
constructor(
|
||||
@InjectRepository(Captcha)
|
||||
private readonly captchaRepository: MongoRepository<Captcha>,
|
||||
) {}
|
||||
|
||||
async createCaptcha(captchaText: string): Promise<Captcha> {
|
||||
const captcha = this.captchaRepository.create({
|
||||
text: captchaText,
|
||||
});
|
||||
|
||||
return this.captchaRepository.save(captcha);
|
||||
}
|
||||
|
||||
async getCaptcha(id: string): Promise<Captcha | undefined> {
|
||||
return this.captchaRepository.findOne({ where: { _id: new ObjectId(id) } });
|
||||
}
|
||||
|
||||
async deleteCaptcha(id: string): Promise<void> {
|
||||
await this.captchaRepository.delete(new ObjectId(id));
|
||||
}
|
||||
|
||||
async checkCaptchaIsCorrect({ captcha, id }) {
|
||||
const captchaData = await this.captchaRepository.findOne({
|
||||
where: { _id: new ObjectId(id) },
|
||||
});
|
||||
return captcha.toLowerCase() === captchaData?.text?.toLowerCase();
|
||||
}
|
||||
}
|
63
server/src/modules/auth/services/user.service.ts
Normal file
63
server/src/modules/auth/services/user.service.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
import { User } from 'src/models/user.entity';
|
||||
import { createHash } from 'crypto';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: MongoRepository<User>,
|
||||
) {}
|
||||
|
||||
private hash256(text) {
|
||||
return createHash('sha256').update(text).digest('hex');
|
||||
}
|
||||
|
||||
async createUser(userInfo: {
|
||||
username: string;
|
||||
password: string;
|
||||
}): Promise<User> {
|
||||
const existingUser = await this.userRepository.findOne({
|
||||
where: { username: userInfo.username },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
throw new HttpException('该用户已存在', EXCEPTION_CODE.USER_EXISTS);
|
||||
}
|
||||
|
||||
const newUser = this.userRepository.create({
|
||||
username: userInfo.username,
|
||||
password: this.hash256(userInfo.password),
|
||||
});
|
||||
|
||||
return this.userRepository.save(newUser);
|
||||
}
|
||||
|
||||
async getUser(userInfo: {
|
||||
username: string;
|
||||
password: string;
|
||||
}): Promise<User | undefined> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
username: userInfo.username,
|
||||
password: this.hash256(userInfo.password), // Please handle password hashing here
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async getUserByUsername(username) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
username: username,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ContentSecurityService } from '../services/contentSecurity.service';
|
||||
import { Word } from 'src/models/word.entity';
|
||||
|
||||
describe('ContentSecurityService', () => {
|
||||
let service: ContentSecurityService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ContentSecurityService,
|
||||
{
|
||||
provide: getRepositoryToken(Word),
|
||||
useValue: {
|
||||
find: jest.fn().mockResolvedValue([
|
||||
{
|
||||
text: '违禁词1',
|
||||
},
|
||||
{
|
||||
text: '违禁词2',
|
||||
},
|
||||
]),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ContentSecurityService>(ContentSecurityService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('isForbiddenContent', () => {
|
||||
it('should return true if text contains forbidden word', async () => {
|
||||
const result = await service.isForbiddenContent({
|
||||
text: '这是违禁词1',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if text does not contain forbidden word', async () => {
|
||||
const result = await service.isForbiddenContent({
|
||||
text: '这句话不包含违禁词',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
308
server/src/modules/survey/__test/survey.controller.spec.ts
Normal file
308
server/src/modules/survey/__test/survey.controller.spec.ts
Normal file
@ -0,0 +1,308 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SurveyController } from '../controllers/survey.controller';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
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 { ObjectId } from 'mongodb';
|
||||
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||
|
||||
// Mock the services
|
||||
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('src/guards/authtication');
|
||||
|
||||
describe('SurveyController', () => {
|
||||
let controller: SurveyController;
|
||||
let surveyMetaService: SurveyMetaService;
|
||||
let surveyConfService: SurveyConfService;
|
||||
let responseSchemaService: ResponseSchemaService;
|
||||
let contentSecurityService: ContentSecurityService;
|
||||
let surveyHistoryService: SurveyHistoryService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [SurveyController],
|
||||
providers: [
|
||||
SurveyMetaService,
|
||||
SurveyConfService,
|
||||
ResponseSchemaService,
|
||||
ContentSecurityService,
|
||||
SurveyHistoryService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<SurveyController>(SurveyController);
|
||||
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
|
||||
surveyConfService = module.get<SurveyConfService>(SurveyConfService);
|
||||
responseSchemaService = module.get<ResponseSchemaService>(
|
||||
ResponseSchemaService,
|
||||
);
|
||||
contentSecurityService = module.get<ContentSecurityService>(
|
||||
ContentSecurityService,
|
||||
);
|
||||
surveyHistoryService =
|
||||
module.get<SurveyHistoryService>(SurveyHistoryService);
|
||||
});
|
||||
|
||||
describe('getBannerData', () => {
|
||||
it('should return banner data', async () => {
|
||||
const result = await controller.getBannerData();
|
||||
|
||||
expect(result.code).toBe(200);
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSurvey', () => {
|
||||
it('should create a survey and return the survey id', async () => {
|
||||
const surveyInfo = {
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
||||
const result = await controller.createSurvey(surveyInfo, {
|
||||
user: { username: 'testUser' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
data: {
|
||||
id: newId.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new survey by copy', async () => {
|
||||
const existsSurveyId = new ObjectId();
|
||||
const existsSurveyMeta = {
|
||||
_id: existsSurveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
const params = {
|
||||
surveyType: 'normal',
|
||||
remark: '问卷调研',
|
||||
title: '问卷调研',
|
||||
createMethod: 'copy',
|
||||
createFrom: existsSurveyId.toString(),
|
||||
};
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(existsSurveyMeta));
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'createSurveyMeta')
|
||||
.mockImplementation(() => {
|
||||
const result = {
|
||||
_id: new ObjectId(),
|
||||
} as SurveyMeta;
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
|
||||
const request = { user: { username: 'testUser' } }; // 模拟请求对象,根据实际情况进行调整
|
||||
const result = await controller.createSurvey(params, request);
|
||||
expect(result?.data?.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateConf', () => {
|
||||
it('should update survey configuration', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
jest
|
||||
.spyOn(surveyConfService, 'saveSurveyConf')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(surveyHistoryService, 'addHistory')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const reqBody = {
|
||||
surveyId: surveyId.toString(),
|
||||
configData: {
|
||||
bannerConf: {
|
||||
titleConfig: {},
|
||||
bannerConfig: {},
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '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' },
|
||||
submitConf: {},
|
||||
dataConf: {
|
||||
dataList: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await controller.updateConf(reqBody, {
|
||||
user: { username: 'testUser', _id: 'testUserId' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
// Add more test cases for different scenarios
|
||||
});
|
||||
|
||||
describe('deleteSurvey', () => {
|
||||
it('should delete a survey and its related data', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'deleteSurveyMeta')
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(responseSchemaService, 'deleteResponseSchema')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const result = await controller.deleteSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
{ user: { username: 'testUser' } },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
// Add more test cases for different scenarios
|
||||
});
|
||||
|
||||
describe('getSurvey', () => {
|
||||
it('should return survey metadata and configuration', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} as SurveyConf),
|
||||
);
|
||||
|
||||
const request = { user: { username: 'testUser' } };
|
||||
const result = await controller.getSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
request,
|
||||
);
|
||||
expect(result?.data?.surveyMetaRes).toBeDefined();
|
||||
expect(result?.data?.surveyConfRes).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('publishSurvey', () => {
|
||||
it('should publish a survey and its response schema', async () => {
|
||||
const surveyId = new ObjectId();
|
||||
const surveyMeta = {
|
||||
_id: surveyId,
|
||||
surveyType: 'exam',
|
||||
owner: 'testUser',
|
||||
} as SurveyMeta;
|
||||
|
||||
jest
|
||||
.spyOn(surveyMetaService, 'checkSurveyAccess')
|
||||
.mockResolvedValue(Promise.resolve(surveyMeta));
|
||||
|
||||
jest
|
||||
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
|
||||
.mockResolvedValue(
|
||||
Promise.resolve({
|
||||
_id: new ObjectId(),
|
||||
pageId: surveyId.toString(),
|
||||
} 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);
|
||||
|
||||
const result = await controller.publishSurvey(
|
||||
{ surveyId: surveyId.toString() },
|
||||
{ user: { username: 'testUser', _id: 'testUserId' } },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,74 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { DataStatisticService } from '../services/dataStatistic.service';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
|
||||
|
||||
import * as Joi from 'joi';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
|
||||
|
||||
@Controller('/api/survey/dataStatistic')
|
||||
export class DataStatisticController {
|
||||
constructor(
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly dataStatisticService: DataStatisticService,
|
||||
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||
) {}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Get('/dataTable')
|
||||
@HttpCode(200)
|
||||
async data(
|
||||
@Query()
|
||||
queryInfo,
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
isDesensitive: Joi.boolean().default(true), // 默认true就是需要脱敏
|
||||
page: Joi.number().default(1),
|
||||
pageSize: Joi.number().default(10),
|
||||
}).validateAsync(queryInfo);
|
||||
const { surveyId, isDesensitive, page, pageSize } = validationResult;
|
||||
const username = req.user.username;
|
||||
await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
const responseSchema =
|
||||
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
|
||||
const { total, listHead, listBody } =
|
||||
await this.dataStatisticService.getDataTable({
|
||||
responseSchema,
|
||||
surveyId,
|
||||
pageNum: page,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
if (isDesensitive) {
|
||||
// 脱敏
|
||||
listBody.forEach((item) => {
|
||||
this.pluginManager.triggerHook('desensitiveData', item);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
total,
|
||||
listHead,
|
||||
listBody,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
256
server/src/modules/survey/controllers/survey.controller.ts
Normal file
256
server/src/modules/survey/controllers/survey.controller.ts
Normal file
@ -0,0 +1,256 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
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 BannerData from '../template/banner/index.json';
|
||||
import * as Joi from 'joi';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import { HISTORY_TYPE } from 'src/enums';
|
||||
import { HttpException } from 'src/exceptions/httpException';
|
||||
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||
|
||||
@Controller('/api/survey')
|
||||
export class SurveyController {
|
||||
constructor(
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
private readonly surveyConfService: SurveyConfService,
|
||||
private readonly responseSchemaService: ResponseSchemaService,
|
||||
private readonly contentSecurityService: ContentSecurityService,
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
) {}
|
||||
|
||||
@Get('/getBannerData')
|
||||
@HttpCode(200)
|
||||
async getBannerData() {
|
||||
return {
|
||||
code: 200,
|
||||
data: BannerData,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Post('/createSurvey')
|
||||
@HttpCode(200)
|
||||
async createSurvey(
|
||||
@Body()
|
||||
reqBody,
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
remark: Joi.string().required(),
|
||||
title: Joi.string().required(),
|
||||
surveyType: Joi.string().when('createMethod', {
|
||||
is: 'copy',
|
||||
then: Joi.allow(null),
|
||||
otherwise: Joi.required(),
|
||||
}),
|
||||
createMethod: Joi.string().allow(null).default('basic'),
|
||||
createFrom: Joi.string().when('createMethod', {
|
||||
is: 'copy',
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.allow(null),
|
||||
}),
|
||||
}).validateAsync(reqBody);
|
||||
|
||||
const { title, remark, createMethod, createFrom } = validationResult;
|
||||
|
||||
const username = req.user.username;
|
||||
let surveyType = '';
|
||||
if (createMethod === 'copy') {
|
||||
const survey = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId: createFrom,
|
||||
username,
|
||||
});
|
||||
surveyType = survey.surveyType;
|
||||
} else {
|
||||
surveyType = validationResult.surveyType;
|
||||
}
|
||||
|
||||
const surveyMeta = await this.surveyMetaService.createSurveyMeta({
|
||||
title,
|
||||
remark,
|
||||
surveyType,
|
||||
username,
|
||||
createMethod,
|
||||
createFrom,
|
||||
});
|
||||
await this.surveyConfService.createSurveyConf({
|
||||
surveyId: surveyMeta._id.toString(),
|
||||
surveyType: surveyType,
|
||||
createMethod: validationResult.createMethod,
|
||||
createFrom: validationResult.createFrom,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
id: surveyMeta._id.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Post('/updateConf')
|
||||
@HttpCode(200)
|
||||
async updateConf(
|
||||
@Body()
|
||||
surveyInfo,
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
configData: Joi.any().required(),
|
||||
}).validateAsync(surveyInfo);
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
const configData = validationResult.configData;
|
||||
await this.surveyConfService.saveSurveyConf({
|
||||
surveyId,
|
||||
schema: configData,
|
||||
});
|
||||
await this.surveyHistoryService.addHistory({
|
||||
surveyId,
|
||||
schema: configData,
|
||||
type: HISTORY_TYPE.DAILY_HIS,
|
||||
user: {
|
||||
_id: req.user._id.toString(),
|
||||
username,
|
||||
},
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@HttpCode(200)
|
||||
@Post('/deleteSurvey')
|
||||
async deleteSurvey(@Body() reqBody, @Request() req) {
|
||||
const validationResult = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validateAsync(reqBody, { allowUnknown: true });
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
const survey = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
|
||||
await this.surveyMetaService.deleteSurveyMeta(survey);
|
||||
await this.responseSchemaService.deleteResponseSchema({
|
||||
surveyPath: survey.surveyPath,
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Get('/getSurvey')
|
||||
@HttpCode(200)
|
||||
async getSurvey(
|
||||
@Query()
|
||||
queryInfo: {
|
||||
surveyId: string;
|
||||
},
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validateAsync(queryInfo);
|
||||
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
const surveyMeta = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
const surveyConf =
|
||||
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
surveyMetaRes: surveyMeta,
|
||||
surveyConfRes: surveyConf,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Post('/publishSurvey')
|
||||
@HttpCode(200)
|
||||
async publishSurvey(
|
||||
@Body()
|
||||
surveyInfo,
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
}).validateAsync(surveyInfo);
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
const surveyMeta = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
const surveyConf =
|
||||
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
|
||||
|
||||
const { text } = await this.surveyConfService.getSurveyContentByCode(
|
||||
surveyConf.code,
|
||||
);
|
||||
|
||||
if (await this.contentSecurityService.isForbiddenContent({ text })) {
|
||||
throw new HttpException(
|
||||
'问卷存在非法关键字,不允许发布',
|
||||
EXCEPTION_CODE.SURVEY_CONTENT_NOT_ALLOW,
|
||||
);
|
||||
}
|
||||
|
||||
await this.surveyMetaService.publishSurveyMeta({
|
||||
surveyMeta,
|
||||
});
|
||||
|
||||
await this.responseSchemaService.publishResponseSchema({
|
||||
title: surveyMeta.title,
|
||||
surveyPath: surveyMeta.surveyPath,
|
||||
code: surveyConf.code,
|
||||
pageId: surveyId,
|
||||
});
|
||||
|
||||
await this.surveyHistoryService.addHistory({
|
||||
surveyId,
|
||||
schema: surveyConf.code,
|
||||
type: HISTORY_TYPE.PUBLISH_HIS,
|
||||
user: {
|
||||
_id: req.user._id.toString(),
|
||||
username,
|
||||
},
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { SurveyHistoryService } from '../services/surveyHistory.service';
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
|
||||
import * as Joi from 'joi';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
|
||||
@Controller('/api/surveyHisotry')
|
||||
export class SurveyHistoryController {
|
||||
constructor(
|
||||
private readonly surveyHistoryService: SurveyHistoryService,
|
||||
private readonly surveyMetaService: SurveyMetaService,
|
||||
) {}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Get('/getList')
|
||||
@HttpCode(200)
|
||||
async getList(
|
||||
@Query()
|
||||
queryInfo: {
|
||||
surveyId: string;
|
||||
historyType: string;
|
||||
},
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
surveyId: Joi.string().required(),
|
||||
historyType: Joi.string().required(),
|
||||
}).validateAsync(queryInfo);
|
||||
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
const historyType = validationResult.historyType;
|
||||
await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
const data = await this.surveyHistoryService.getHistoryList({
|
||||
surveyId,
|
||||
historyType,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
};
|
||||
}
|
||||
}
|
185
server/src/modules/survey/controllers/surveyMeta.controller.ts
Normal file
185
server/src/modules/survey/controllers/surveyMeta.controller.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
Query,
|
||||
HttpCode,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { SurveyMetaService } from '../services/surveyMeta.service';
|
||||
|
||||
import * as Joi from 'joi';
|
||||
import { Authtication } from 'src/guards/authtication';
|
||||
import moment from 'moment';
|
||||
|
||||
type FilterItem = {
|
||||
comparator?: string;
|
||||
condition: Array<FilterCondition>;
|
||||
};
|
||||
|
||||
type FilterCondition = {
|
||||
field: string;
|
||||
comparator?: string;
|
||||
value: string & Array<FilterItem>;
|
||||
};
|
||||
|
||||
type OrderItem = {
|
||||
field: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
@Controller('/api/survey')
|
||||
export class SurveyMetaController {
|
||||
constructor(private readonly surveyMetaService: SurveyMetaService) {}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Post('/updateMeta')
|
||||
@HttpCode(200)
|
||||
async updateMeta(@Body() reqBody, @Request() req) {
|
||||
const validationResult = await Joi.object({
|
||||
remark: Joi.string().allow(null).default(''),
|
||||
title: Joi.string().required(),
|
||||
surveyId: Joi.string().required(),
|
||||
}).validateAsync(reqBody, { allowUnknown: true });
|
||||
const username = req.user.username;
|
||||
const surveyId = validationResult.surveyId;
|
||||
const survey = await this.surveyMetaService.checkSurveyAccess({
|
||||
surveyId,
|
||||
username,
|
||||
});
|
||||
survey.title = validationResult.title;
|
||||
survey.remark = validationResult.remark;
|
||||
|
||||
await this.surveyMetaService.editSurveyMeta(survey);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(Authtication)
|
||||
@Get('/getList')
|
||||
@HttpCode(200)
|
||||
async getList(
|
||||
@Query()
|
||||
queryInfo: {
|
||||
curPage: number;
|
||||
pageSize: number;
|
||||
},
|
||||
@Request()
|
||||
req,
|
||||
) {
|
||||
const validationResult = await Joi.object({
|
||||
curPage: Joi.number().required(),
|
||||
pageSize: Joi.number().allow(null).default(10),
|
||||
filter: Joi.string().allow(null),
|
||||
order: Joi.string().allow(null),
|
||||
}).validateAsync(queryInfo);
|
||||
const { curPage, pageSize } = validationResult;
|
||||
let filter = {},
|
||||
order = {};
|
||||
if (validationResult.filter) {
|
||||
try {
|
||||
filter = this.getFilter(
|
||||
JSON.parse(decodeURIComponent(validationResult.filter)),
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
if (validationResult.order) {
|
||||
try {
|
||||
order = order = this.getOrder(
|
||||
JSON.parse(decodeURIComponent(validationResult.order)),
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
const username = req.user.username;
|
||||
const data = await this.surveyMetaService.getSurveyMetaList({
|
||||
pageNum: curPage,
|
||||
pageSize: pageSize,
|
||||
username,
|
||||
filter,
|
||||
order,
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
count: data.count,
|
||||
data: data.data.map((item) => {
|
||||
const fmt = 'YYYY-MM-DD HH:mm:ss';
|
||||
if (!item.surveyType) {
|
||||
item.surveyType = item.questionType || 'normal';
|
||||
}
|
||||
item.createDate = moment(item.createDate).format(fmt);
|
||||
item.updateDate = moment(item.updateDate).format(fmt);
|
||||
item.curStatus.date = moment(item.curStatus.date).format(fmt);
|
||||
return item;
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getFilter(filterList: Array<FilterItem>) {
|
||||
const allowFilterField = [
|
||||
'title',
|
||||
'remark',
|
||||
'surveyType',
|
||||
'curStatus.status',
|
||||
];
|
||||
return filterList.reduce(
|
||||
(preItem, curItem) => {
|
||||
const condition = curItem.condition
|
||||
.filter((item) => allowFilterField.includes(item.field))
|
||||
.reduce((pre, cur) => {
|
||||
switch (cur.comparator) {
|
||||
case '$ne':
|
||||
pre[cur.field] = {
|
||||
$ne: cur.value,
|
||||
};
|
||||
break;
|
||||
case '$regex':
|
||||
pre[cur.field] = {
|
||||
$regex: cur.value,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
pre[cur.field] = cur.value;
|
||||
break;
|
||||
}
|
||||
return pre;
|
||||
}, {});
|
||||
switch (curItem.comparator) {
|
||||
case '$or':
|
||||
if (!Array.isArray(preItem.$or)) {
|
||||
preItem.$or = [];
|
||||
}
|
||||
preItem.$or.push(condition);
|
||||
break;
|
||||
default:
|
||||
Object.assign(preItem, condition);
|
||||
break;
|
||||
}
|
||||
return preItem;
|
||||
},
|
||||
{} as { $or?: Array<Record<string, string>> } & Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
private getOrder(order: Array<OrderItem>) {
|
||||
const allowOrderFields = ['createDate', 'updateDate', 'curStatus.date'];
|
||||
|
||||
const orderList = order.filter((orderItem) =>
|
||||
allowOrderFields.includes(orderItem.field),
|
||||
);
|
||||
return orderList.reduce((pre, cur) => {
|
||||
pre[cur.field] = cur.value === 1 ? 1 : -1;
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
}
|
18
server/src/modules/survey/controllers/surveyUI.controller.ts
Normal file
18
server/src/modules/survey/controllers/surveyUI.controller.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Controller, Get, Param, Res } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { join } from 'path';
|
||||
|
||||
@Controller()
|
||||
export class SurveyUIController {
|
||||
constructor() {}
|
||||
|
||||
@Get('/')
|
||||
home(@Res() res: Response) {
|
||||
res.sendFile(join(process.cwd(), 'public', 'management.html'));
|
||||
}
|
||||
|
||||
@Get('/management/:surveyId')
|
||||
management(@Param('surveyId') surveyId: string, @Res() res: Response) {
|
||||
res.sendFile(join(process.cwd(), 'public', 'management.html'));
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Word } from 'src/models/word.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { MongoRepository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class ContentSecurityService {
|
||||
private forbiddenWords = null;
|
||||
constructor(
|
||||
@InjectRepository(Word)
|
||||
private readonly wordRepository: MongoRepository<Word>,
|
||||
) {}
|
||||
|
||||
private async initForbiddenWords() {
|
||||
const words = await this.wordRepository.find({
|
||||
where: {
|
||||
type: 'forbidden',
|
||||
},
|
||||
});
|
||||
this.forbiddenWords = words.map((item) => item.text);
|
||||
}
|
||||
|
||||
async isForbiddenContent({ text }: { text: string }) {
|
||||
if (!this.forbiddenWords) {
|
||||
await this.initForbiddenWords();
|
||||
}
|
||||
for (const word of this.forbiddenWords) {
|
||||
if (text.includes(word)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user