feat: 升级server到nestjs框架
This commit is contained in:
parent
dfdc8025e9
commit
9390295f5b
2
.gitignore
vendored
2
.gitignore
vendored
@ -23,4 +23,4 @@ pnpm-debug.log*
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.history
|
.history
|
||||||
|
@ -23,9 +23,9 @@ RUN npm config set registry https://registry.npmjs.org/
|
|||||||
# 安装项目依赖
|
# 安装项目依赖
|
||||||
RUN cd /xiaoju-survey/web && npm install && npm run build
|
RUN cd /xiaoju-survey/web && npm install && npm run build
|
||||||
|
|
||||||
RUN cd /xiaoju-survey && cp -af ./web/dist/* ./server/src/apps/ui/public/
|
RUN cd /xiaoju-survey && cp -af ./web/dist/* ./server/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一致
|
# 暴露端口 需要跟server的port一致
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
cd /xiaoju-survey/server
|
cd /xiaoju-survey/server
|
||||||
npm run start
|
npm run start:prod
|
11
server/.env
Normal file
11
server/.env
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
|
||||||
|
XIAOJU_SURVEY_MONGO_URL=mongodb://localhost:27017
|
||||||
|
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
|
||||||
|
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=
|
||||||
|
|
||||||
|
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
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
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# OS
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
.DS_Store
|
||||||
|
|
||||||
# Runtime data
|
# Tests
|
||||||
pids
|
/coverage
|
||||||
*.pid
|
/.nyc_output
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# IDEs and editors
|
||||||
lib-cov
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# IDE - VSCode
|
||||||
coverage
|
.vscode/*
|
||||||
*.lcov
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
# nyc test coverage
|
!.vscode/launch.json
|
||||||
.nyc_output
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# 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
|
|
4
server/.prettierrc
Normal file
4
server/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
73
server/README.md
Normal file
73
server/README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||||
|
</p>
|
||||||
|
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
|
||||||
|
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](LICENSE).
|
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,91 @@
|
|||||||
{
|
{
|
||||||
"name": "survey-template",
|
"name": "server-new",
|
||||||
"version": "1.0.0",
|
"version": "0.0.1",
|
||||||
"description": "survey server template",
|
"description": "",
|
||||||
"main": "index.js",
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"copy": "mkdir -p ./build/ && cp -rf ./src/* ./build/",
|
"build": "nest build",
|
||||||
"build": "tsc",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start:stable": "SERVER_ENV=stable node ./build/index.js",
|
"local": "ts-node ./scripts/run-local.ts",
|
||||||
"start:preonline": "SERVER_ENV=preonline node ./build/index.js",
|
"start": "nest start",
|
||||||
"start:online": "SERVER_ENV=online node ./build/index.js",
|
"start:dev": "NODE_ENV=development nest start --watch",
|
||||||
"start": "npm run start:online",
|
"start:debug": "NODE_ENV=development nest start --debug --watch",
|
||||||
"local": "npx ts-node scripts/run-local.ts",
|
"start:prod": "NODE_ENV=production node dist/main",
|
||||||
"dev": "npx ts-node-dev ./src/index.ts"
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
},
|
"test": "NODE_ENV=development jest",
|
||||||
"devDependencies": {
|
"test:watch": "NODE_ENV=development jest --watch",
|
||||||
"@types/crypto-js": "^4.2.1",
|
"test:cov": "NODE_ENV=development jest --coverage",
|
||||||
"@types/koa": "^2.13.8",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
|
||||||
"@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"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"cheerio": "^1.0.0-rc.12",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"glob": "^10.3.10",
|
"dotenv": "^16.3.2",
|
||||||
"joi": "^17.9.2",
|
"joi": "^17.11.0",
|
||||||
"jsonwebtoken": "^9.0.1",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"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",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"log4js": "^6.9.1",
|
||||||
"mongodb": "^5.7.0",
|
"moment": "^2.30.1",
|
||||||
"svg-captcha": "^1.4.0"
|
"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": {
|
"devDependencies": {
|
||||||
"node": ">=14.21.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"npm": ">=6.14.17"
|
"@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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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);
|
console.log('MongoDB Memory Server started:', mongoUri);
|
||||||
|
|
||||||
// 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量
|
// 通过 spawn 运行另一个脚本,并传递 MongoDB 连接 URL 作为环境变量
|
||||||
const tsnode = spawn('cross-env', [`XIAOJU_SURVEY_MONGO_URL="${mongoUri}"`, 'npx', 'ts-node-dev', './src/index.ts'], {
|
const tsnode = spawn(
|
||||||
stdio: 'inherit',
|
'cross-env',
|
||||||
shell: process.platform === 'win32'
|
[
|
||||||
});
|
`XIAOJU_SURVEY_MONGO_URL=${mongoUri}`,
|
||||||
|
'NODE_ENV=development',
|
||||||
|
'npm',
|
||||||
|
'run',
|
||||||
|
'start:dev',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
},
|
||||||
|
);
|
||||||
tsnode.stdout?.on('data', (data) => {
|
tsnode.stdout?.on('data', (data) => {
|
||||||
console.log(data.toString());
|
console.log(data.toString());
|
||||||
});
|
});
|
||||||
@ -29,4 +39,4 @@ async function startServerAndRunScript() {
|
|||||||
|
|
||||||
startServerAndRunScript().catch((err) => {
|
startServerAndRunScript().catch((err) => {
|
||||||
console.error('Error starting server and script:', 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 './plugins/responseSecurityPlugin';
|
||||||
|
import { SurveyUtilPlugin } from './plugins/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 './plugins/pluginManager.provider';
|
||||||
|
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
|
||||||
|
import { XiaojuSurveyPluginManager } from './plugins/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,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,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 {
|
export interface TitleConfig {
|
||||||
mainTitle: string;
|
mainTitle: string;
|
||||||
subTitle: string;
|
subTitle: string;
|
||||||
@ -9,16 +11,12 @@ export interface BannerConfig {
|
|||||||
postImg: string;
|
postImg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 问卷头部内容:标题和头图
|
||||||
export interface BannerConf {
|
export interface BannerConf {
|
||||||
titleConfig: TitleConfig;
|
titleConfig: TitleConfig;
|
||||||
bannerConfig: BannerConfig;
|
bannerConfig: BannerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeStep {
|
|
||||||
hour: number;
|
|
||||||
min: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NPS {
|
export interface NPS {
|
||||||
leftText: string;
|
leftText: string;
|
||||||
rightText: string;
|
rightText: string;
|
||||||
@ -49,9 +47,7 @@ export interface DataItem {
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
minNum: string;
|
minNum: string;
|
||||||
maxNum: string;
|
maxNum: string;
|
||||||
maxPhotos: number;
|
|
||||||
star: number;
|
star: number;
|
||||||
timeStep: TimeStep;
|
|
||||||
nps: NPS;
|
nps: NPS;
|
||||||
placeholderDesc: string;
|
placeholderDesc: string;
|
||||||
addressType: number;
|
addressType: number;
|
||||||
@ -102,7 +98,9 @@ export interface SubmitConf {
|
|||||||
export interface BaseConf {
|
export interface BaseConf {
|
||||||
begTime: string;
|
begTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
tLimit: string;
|
answerBegTime: string;
|
||||||
|
answerEndTime: string;
|
||||||
|
tLimit: number;
|
||||||
language: string;
|
language: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +109,7 @@ export interface SkinConf {
|
|||||||
inputBgColor: string;
|
inputBgColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParsedData {
|
export interface SurveySchemaInterface {
|
||||||
bannerConf: BannerConf;
|
bannerConf: BannerConf;
|
||||||
dataConf: DataConf;
|
dataConf: DataConf;
|
||||||
submitConf: SubmitConf;
|
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,
|
||||||
|
};
|
15
server/src/logger/util.ts
Normal file
15
server/src/logger/util.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
const traceIdAlphabet = 'abcdef0123456789';
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const getCountStr = () => {
|
||||||
|
count++;
|
||||||
|
return count.toString().padStart(8, '0');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRandom = customAlphabet(traceIdAlphabet, 10);
|
||||||
|
|
||||||
|
export const genTraceId = (): string => {
|
||||||
|
return getRandom() + Math.round(Date.now() / 1000).toString() + getCountStr();
|
||||||
|
};
|
8
server/src/main.ts
Normal file
8
server/src/main.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
await app.listen(3000);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
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 '../plugins/pluginManager';
|
||||||
|
|
||||||
|
@Entity({ name: 'surveySubmit' })
|
||||||
|
export class SurveyResponse {
|
||||||
|
@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()
|
||||||
|
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>;
|
||||||
|
|
||||||
|
@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.jpg', 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/plugins/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',
|
||||||
|
'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: 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(__dirname, 'src/../', 'public', 'management.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/management/:surveyId')
|
||||||
|
management(@Param('surveyId') surveyId: string, @Res() res: Response) {
|
||||||
|
res.sendFile(join(__dirname, 'src/../', '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;
|
||||||
|
}
|
||||||
|
}
|
142
server/src/modules/survey/services/dataStatistic.service.ts
Normal file
142
server/src/modules/survey/services/dataStatistic.service.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { MongoRepository } from 'typeorm';
|
||||||
|
import { SurveyResponse } from 'src/models/surveyResponse.entity';
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
import { keyBy } from 'lodash';
|
||||||
|
import { DataItem } from 'src/interfaces/survey';
|
||||||
|
import { ResponseSchema } from 'src/models/responseSchema.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DataStatisticService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SurveyResponse)
|
||||||
|
private readonly surveyResponseRepository: MongoRepository<SurveyResponse>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private getListHeadByDataList(dataList) {
|
||||||
|
const listHead = dataList.map((question) => {
|
||||||
|
let othersCode;
|
||||||
|
if (question.type === 'radio-star') {
|
||||||
|
const rangeConfigKeys = Object.keys(question.rangeConfig);
|
||||||
|
if (rangeConfigKeys.length > 0) {
|
||||||
|
othersCode = [
|
||||||
|
{ code: `${question.field}_custom`, option: '填写理由' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
othersCode = (question.options || [])
|
||||||
|
.filter((optionItem) => optionItem.othersKey)
|
||||||
|
.map((optionItem) => {
|
||||||
|
return {
|
||||||
|
code: optionItem.othersKey,
|
||||||
|
option: optionItem.text,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
field: question.field,
|
||||||
|
title: question.title,
|
||||||
|
type: question.type,
|
||||||
|
othersCode,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
listHead.push({
|
||||||
|
field: 'difTime',
|
||||||
|
title: '答题耗时(秒)',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
listHead.push({
|
||||||
|
field: 'createDate',
|
||||||
|
title: '提交时间',
|
||||||
|
type: 'text',
|
||||||
|
});
|
||||||
|
return listHead;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDataTable({
|
||||||
|
surveyId,
|
||||||
|
pageNum,
|
||||||
|
pageSize,
|
||||||
|
responseSchema,
|
||||||
|
}: {
|
||||||
|
surveyId: string;
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
responseSchema: ResponseSchema;
|
||||||
|
}) {
|
||||||
|
const dataList = responseSchema?.code?.dataConf?.dataList || [];
|
||||||
|
const listHead = this.getListHeadByDataList(dataList);
|
||||||
|
const dataListMap = keyBy(dataList, 'field');
|
||||||
|
const where = {
|
||||||
|
pageId: surveyId,
|
||||||
|
'curStatus.status': {
|
||||||
|
$ne: 'removed',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const [surveyResponseList, total] =
|
||||||
|
await this.surveyResponseRepository.findAndCount({
|
||||||
|
where,
|
||||||
|
take: pageSize,
|
||||||
|
skip: (pageNum - 1) * pageSize,
|
||||||
|
order: {
|
||||||
|
createDate: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const listBody = surveyResponseList.map((submitedData) => {
|
||||||
|
const data = submitedData.data;
|
||||||
|
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]}`];
|
||||||
|
}
|
||||||
|
// 将选项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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
difTime: (submitedData.difTime / 1000).toFixed(2),
|
||||||
|
createDate: moment(submitedData.createDate).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
listHead,
|
||||||
|
listBody,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
110
server/src/modules/survey/services/surveyConf.service.ts
Normal file
110
server/src/modules/survey/services/surveyConf.service.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { MongoRepository } from 'typeorm';
|
||||||
|
import { SurveyConf } from 'src/models/surveyConf.entity';
|
||||||
|
import templateBase from '../template/surveyTemplate/templateBase.json';
|
||||||
|
import normalCode from '../template/surveyTemplate/survey/normal.json';
|
||||||
|
import npsCode from '../template/surveyTemplate/survey/nps.json';
|
||||||
|
import registerCode from '../template/surveyTemplate/survey/register.json';
|
||||||
|
import voteCode from '../template/surveyTemplate/survey/vote.json';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
|
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||||
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
|
import { SurveySchemaInterface } from 'src/interfaces/survey';
|
||||||
|
|
||||||
|
const schemaDataMap = {
|
||||||
|
normal: normalCode,
|
||||||
|
nps: npsCode,
|
||||||
|
register: registerCode,
|
||||||
|
vote: voteCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SurveyConfService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SurveyConf)
|
||||||
|
private readonly surveyConfRepository: MongoRepository<SurveyConf>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private async getSchemaBySurveyType(surveyType: string) {
|
||||||
|
// Implement your logic here
|
||||||
|
const codeData = get(schemaDataMap, surveyType);
|
||||||
|
if (!codeData) {
|
||||||
|
throw new HttpException(
|
||||||
|
'问卷类型不存在',
|
||||||
|
EXCEPTION_CODE.SURVEY_TYPE_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const code = Object.assign({}, templateBase, codeData);
|
||||||
|
const nowMoment = moment();
|
||||||
|
code.baseConf.begTime = nowMoment.format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
code.baseConf.endTime = nowMoment
|
||||||
|
.add(10, 'years')
|
||||||
|
.format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSurveyConf(params: {
|
||||||
|
surveyId: string;
|
||||||
|
surveyType: string;
|
||||||
|
createMethod: string;
|
||||||
|
createFrom: string;
|
||||||
|
}) {
|
||||||
|
const { surveyId, surveyType, createMethod, createFrom } = params;
|
||||||
|
let schemaData = null;
|
||||||
|
if (createMethod === 'copy') {
|
||||||
|
const codeInfo = await this.getSurveyConfBySurveyId(createFrom);
|
||||||
|
schemaData = codeInfo.code;
|
||||||
|
} else {
|
||||||
|
schemaData = await this.getSchemaBySurveyType(surveyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCode = this.surveyConfRepository.create({
|
||||||
|
pageId: surveyId,
|
||||||
|
code: schemaData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.surveyConfRepository.save(newCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSurveyConfBySurveyId(surveyId: string) {
|
||||||
|
const code = await this.surveyConfRepository.findOne({
|
||||||
|
where: { pageId: surveyId },
|
||||||
|
});
|
||||||
|
if (!code) {
|
||||||
|
throw new SurveyNotFoundException('问卷配置不存在');
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSurveyConf(params: {
|
||||||
|
surveyId: string;
|
||||||
|
schema: SurveySchemaInterface;
|
||||||
|
}) {
|
||||||
|
const codeInfo = await this.getSurveyConfBySurveyId(params.surveyId);
|
||||||
|
if (!codeInfo) {
|
||||||
|
throw new SurveyNotFoundException('问卷配置不存在');
|
||||||
|
}
|
||||||
|
codeInfo.code = params.schema;
|
||||||
|
await this.surveyConfRepository.save(codeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSurveyContentByCode(codeInfo: SurveySchemaInterface) {
|
||||||
|
const dataList = codeInfo.dataConf.dataList;
|
||||||
|
const arr: Array<string> = [];
|
||||||
|
for (const item of dataList) {
|
||||||
|
arr.push(item.title);
|
||||||
|
if (Array.isArray(item.options)) {
|
||||||
|
for (const option of item.options) {
|
||||||
|
arr.push(option.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: arr.join('\n'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
49
server/src/modules/survey/services/surveyHistory.service.ts
Normal file
49
server/src/modules/survey/services/surveyHistory.service.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { MongoRepository } from 'typeorm';
|
||||||
|
import { SurveyHistory } from 'src/models/surveyHistory.entity';
|
||||||
|
import { HISTORY_TYPE } from 'src/enums';
|
||||||
|
import { SurveySchemaInterface } from 'src/interfaces/survey';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SurveyHistoryService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SurveyHistory)
|
||||||
|
private readonly surveyHistory: MongoRepository<SurveyHistory>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async addHistory(params: {
|
||||||
|
surveyId: string;
|
||||||
|
schema: SurveySchemaInterface;
|
||||||
|
type: HISTORY_TYPE;
|
||||||
|
user: any;
|
||||||
|
}) {
|
||||||
|
const { surveyId, schema, type, user } = params;
|
||||||
|
const newHistory = this.surveyHistory.create({
|
||||||
|
pageId: surveyId,
|
||||||
|
type,
|
||||||
|
schema,
|
||||||
|
operator: {
|
||||||
|
_id: user._id.toString(),
|
||||||
|
username: user.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return this.surveyHistory.save(newHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHistoryList({
|
||||||
|
surveyId,
|
||||||
|
historyType,
|
||||||
|
}: {
|
||||||
|
surveyId: string;
|
||||||
|
historyType: HISTORY_TYPE;
|
||||||
|
}) {
|
||||||
|
return this.surveyHistory.find({
|
||||||
|
where: {
|
||||||
|
pageId: surveyId,
|
||||||
|
type: historyType,
|
||||||
|
},
|
||||||
|
take: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
163
server/src/modules/survey/services/surveyMeta.service.ts
Normal file
163
server/src/modules/survey/services/surveyMeta.service.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { MongoRepository, FindOptionsOrder } from 'typeorm';
|
||||||
|
import { SurveyMeta } from 'src/models/surveyMeta.entity';
|
||||||
|
import { RECORD_STATUS } from 'src/enums';
|
||||||
|
import { ObjectId } from 'mongodb';
|
||||||
|
import { NoSurveyPermissionException } from 'src/exceptions/noSurveyPermissionException';
|
||||||
|
import { HttpException } from 'src/exceptions/httpException';
|
||||||
|
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
|
||||||
|
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
|
||||||
|
import { XiaojuSurveyPluginManager } from 'src/plugins/pluginManager';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SurveyMetaService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SurveyMeta)
|
||||||
|
private readonly surveyRepository: MongoRepository<SurveyMeta>,
|
||||||
|
private readonly pluginManager: XiaojuSurveyPluginManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private async getNewSurveyPath(): Promise<string> {
|
||||||
|
let surveyPath = this.pluginManager.triggerHook('genSurveyPath');
|
||||||
|
while (true) {
|
||||||
|
const count = await this.surveyRepository.count({
|
||||||
|
where: {
|
||||||
|
surveyPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (count === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
surveyPath = this.pluginManager.triggerHook('genSurveyPath');
|
||||||
|
}
|
||||||
|
return surveyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkSurveyAccess({ surveyId, username }) {
|
||||||
|
const survey = await this.surveyRepository.findOne({
|
||||||
|
where: { _id: new ObjectId(surveyId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!survey) {
|
||||||
|
throw new SurveyNotFoundException('问卷不存在');
|
||||||
|
}
|
||||||
|
if (survey.owner !== username) {
|
||||||
|
throw new NoSurveyPermissionException('没有权限');
|
||||||
|
}
|
||||||
|
return survey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSurveyMeta(params: {
|
||||||
|
title: string;
|
||||||
|
remark: string;
|
||||||
|
surveyType: string;
|
||||||
|
username: string;
|
||||||
|
createMethod: string;
|
||||||
|
createFrom: string;
|
||||||
|
}) {
|
||||||
|
const { title, remark, surveyType, username, createMethod, createFrom } =
|
||||||
|
params;
|
||||||
|
const surveyPath = await this.getNewSurveyPath();
|
||||||
|
const newSurvey = this.surveyRepository.create({
|
||||||
|
title,
|
||||||
|
remark: remark || '',
|
||||||
|
surveyType: surveyType,
|
||||||
|
surveyPath,
|
||||||
|
creator: username,
|
||||||
|
owner: username,
|
||||||
|
createMethod,
|
||||||
|
createFrom,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.surveyRepository.save(newSurvey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async editSurveyMeta(survey: SurveyMeta) {
|
||||||
|
if (
|
||||||
|
survey.curStatus.status !== RECORD_STATUS.NEW &&
|
||||||
|
survey.curStatus.status !== RECORD_STATUS.EDITING
|
||||||
|
) {
|
||||||
|
const newStatus = {
|
||||||
|
status: RECORD_STATUS.EDITING,
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
survey.curStatus = newStatus;
|
||||||
|
survey.statusList.push(newStatus);
|
||||||
|
}
|
||||||
|
return this.surveyRepository.save(survey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSurveyMeta(survey: SurveyMeta) {
|
||||||
|
if (survey.curStatus.status === RECORD_STATUS.REMOVED) {
|
||||||
|
throw new HttpException(
|
||||||
|
'问卷已删除,不能重复删除',
|
||||||
|
EXCEPTION_CODE.SURVEY_STATUS_TRANSFORM_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const newStatusInfo = {
|
||||||
|
status: RECORD_STATUS.REMOVED,
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
survey.curStatus = newStatusInfo;
|
||||||
|
if (Array.isArray(survey.statusList)) {
|
||||||
|
survey.statusList.push(newStatusInfo);
|
||||||
|
} else {
|
||||||
|
survey.statusList = [newStatusInfo];
|
||||||
|
}
|
||||||
|
return this.surveyRepository.save(survey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSurveyMetaList(condition: {
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
username: string;
|
||||||
|
filter: Record<string, any>;
|
||||||
|
order: Record<string, any>;
|
||||||
|
}): Promise<{ data: any[]; count: number }> {
|
||||||
|
const { pageNum, pageSize, username } = condition;
|
||||||
|
const skip = (pageNum - 1) * pageSize;
|
||||||
|
try {
|
||||||
|
const query = Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
owner: username,
|
||||||
|
'curStatus.status': {
|
||||||
|
$ne: 'removed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
condition.filter,
|
||||||
|
);
|
||||||
|
const order =
|
||||||
|
condition.order && Object.keys(condition.order).length > 0
|
||||||
|
? (condition.order as FindOptionsOrder<SurveyMeta>)
|
||||||
|
: ({
|
||||||
|
createDate: -1,
|
||||||
|
} as FindOptionsOrder<SurveyMeta>);
|
||||||
|
|
||||||
|
const [data, count] = await this.surveyRepository.findAndCount({
|
||||||
|
where: query,
|
||||||
|
skip,
|
||||||
|
take: pageSize,
|
||||||
|
order,
|
||||||
|
});
|
||||||
|
return { data, count };
|
||||||
|
} catch (error) {
|
||||||
|
return { data: [], count: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async publishSurveyMeta({ surveyMeta }: { surveyMeta: SurveyMeta }) {
|
||||||
|
const curStatus = {
|
||||||
|
status: RECORD_STATUS.PUBLISHED,
|
||||||
|
date: Date.now(),
|
||||||
|
};
|
||||||
|
surveyMeta.curStatus = curStatus;
|
||||||
|
if (Array.isArray(surveyMeta.statusList)) {
|
||||||
|
surveyMeta.statusList.push(curStatus);
|
||||||
|
} else {
|
||||||
|
surveyMeta.statusList = [curStatus];
|
||||||
|
}
|
||||||
|
return this.surveyRepository.save(surveyMeta);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user