feat: Initial project
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.history
|
0
CONTRIBUTING.md
Normal file
45
Dockerfile
Normal file
@ -0,0 +1,45 @@
|
||||
# 镜像集成
|
||||
FROM ubuntu:latest
|
||||
|
||||
# 安装依赖
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y install wget gcc
|
||||
|
||||
# 安装node环境
|
||||
ENV NODE_VERSION v18.17.1
|
||||
RUN mkdir -p /node/$NODE_VERSION
|
||||
RUN wget https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-x64.tar.gz
|
||||
RUN tar xzf node-$NODE_VERSION-linux-x64.tar.gz -C /node/
|
||||
ENV PATH /node/node-$NODE_VERSION-linux-x64/bin:$PATH
|
||||
|
||||
# 设置工作区间
|
||||
WORKDIR /xiaoju-survey
|
||||
|
||||
# 复制文件到工作区间
|
||||
COPY . /xiaoju-survey
|
||||
|
||||
RUN npm config set registry https://registry.npmjs.org/
|
||||
|
||||
# 安装项目依赖
|
||||
RUN cd /xiaoju-survey/web && npm install
|
||||
RUN cd /xiaoju-survey/server && sh init.sh
|
||||
|
||||
# 构建项目,并把产物推送到服务公共目录
|
||||
RUN cd /xiaoju-survey/web && npm run build
|
||||
RUN cd /xiaoju-survey && cp -af ./web/dist/ ./server/src/apps/ui/public/
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# docker入口文件,运行pm2启动,并保证监听不断
|
||||
CMD ["sh","docker-run.sh"]
|
||||
# 构建镜像
|
||||
# docker build -t xiaoju-survey-app .
|
||||
# 运行容器
|
||||
# docker run --rm --name running-xiaoju-survey-app -p 8080:8080 xiaoju-survey-app
|
||||
# 进入容器
|
||||
# docker exec -it running-xiaoju-survey-app bash
|
||||
# 停止容器
|
||||
# docker stop running-xiaoju-survey-app
|
||||
# 查看日志
|
||||
# docker logs running-xiaoju-survey-app
|
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
115
README.md
Normal file
@ -0,0 +1,115 @@
|
||||
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/j8lBA6yy201698840712358.jpg" width="300" />](https://xiaojusurvey.didi.cn)
|
||||
|
||||
**XiaoJuSurvey**是一套轻量、安全的问卷系统,提供面向个人和企业的一站式产品级解决方案,快速满足各类线上调研场景。
|
||||
|
||||
## 简介
|
||||
平台在集团内部已沉淀了**40+**种题型,累积精选模板**100+**,适用于市场调研、客户满意度调研、在线考试、投票、报道、测评等众多场景。数据能力上,沉淀了数据表格、分题统计、交叉分析、多渠道分析等在线报表能力,快速满足回收数据专业化线上分析,协助业务挖掘数据价值。
|
||||
|
||||
本次开源主要围绕问卷生命周期提供了完整的产品化能力:
|
||||
|
||||
- 问卷管理:创、编、投、收、数据分析
|
||||
|
||||
- 多样化题型:单行输入框、多行输入框、单项选择、多项选择、判断题、评分、投票
|
||||
_(更多题型将陆续开放,也欢迎您参与共建提交自定义题型)_
|
||||
|
||||
- 用户管理:登录、注册、权限管理
|
||||
|
||||
- 数据安全:传输加密、脱敏等
|
||||
|
||||
<img src="https://img-hxy021.didistatic.com/static/starimg/img/nJ5fyGhocH1698903177499.png" width="900" />
|
||||
|
||||
_**(个人和企业用户均可快速构建特定领域的调研类解决方案。)**_
|
||||
|
||||
## 项目优势
|
||||
**一、具备全面的综合性和专业性**
|
||||
|
||||
- [制定了问卷标准化协议规范](https://xiaojusurvey.didi.cn/docs/agreement/%E3%80%8A%E9%97%AE%E5%8D%B7Meta%E5%8D%8F%E8%AE%AE%E3%80%8B)
|
||||
|
||||
领域标准保障概念互通,是全系统的基础和核心。基于实际业务经验,沉淀了两大类:
|
||||
- 业务描述:问卷协议、题型协议
|
||||
- 物料描述:题型物料协议,包含题型和设置器
|
||||
|
||||
- [制定了问卷UI/UX规范](https://xiaojusurvey.didi.cn/docs/design/%E3%80%8A%E8%AE%BE%E8%AE%A1%E8%A7%84%E8%8C%83%E3%80%8B)
|
||||
|
||||
设计语言是系统灵活性、一致性的基石,保障系统支撑的实际业务运转拥有极高的用户体验。包含两部分:
|
||||
- 设计规范:灵活、降噪、统一
|
||||
- 交互规范:遵循用户行为特征,遵循产品定位,遵循成熟的用户习惯
|
||||
|
||||
- [所见即所得,搭建渲染一致性高](https://xiaojusurvey.didi.cn/docs/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E9%A2%98%E5%9E%8B%E5%9C%BA%E6%99%AF%E5%8C%96%E8%AE%BE%E8%AE%A1)
|
||||
|
||||
实际业务使用上包含问卷生成和投放使用,即对于系统的搭建端和渲染端。我们将题型场景化设计,以满足一份问卷从加工生产到投放应用的高度一致。
|
||||
|
||||
- [题型物料化设计,自由定制扩展](https://xiaojusurvey.didi.cn/docs/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E9%A2%98%E5%9E%8B%E7%89%A9%E6%96%99%E5%8C%96%E8%AE%BE%E8%AE%A1/%E5%9F%BA%E7%A1%80%E8%AE%BE%E8%AE%A1)
|
||||
|
||||
题型是问卷最核心的组成部分,而题型可配置化能力决定了上层业务可扩展的场景以及系统自身可复用的场景。
|
||||
题型架构设计上,主打每一类题型拥有通用基础能力,每一种题型拥有原子化特性能力,并保障高度定制化。
|
||||
|
||||
- [合规建设沉淀积累,安全能力拓展性高](https://xiaojusurvey.didi.cn/docs/document/%E6%95%B0%E6%8D%AE%E5%AE%89%E5%85%A8)
|
||||
|
||||
数据加密传输、敏感信息精细化检测、投票防刷等能力,保障问卷发布、数据回收链路安全性。
|
||||
|
||||
**二、轻量化设计,快速接入、灵活扩展**
|
||||
- [产品级开源方案,快速产出一套调研流程](https://xiaojusurvey.didi.cn/docs/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E6%A6%82%E8%BF%B0)
|
||||
|
||||
围绕问卷生命周期提供了完整的产品化能力,包含用户管理: 登录、注册、问卷权限,问卷管理: 创、编、投、收、数据分析,可快速构建特定领域的调研类解决方案。
|
||||
|
||||
- [问卷设计开箱即用,降低领域复杂度](https://xiaojusurvey.didi.cn/docs/document/%E8%AE%BE%E8%AE%A1%E5%8E%9F%E7%90%86/%E9%97%AE%E5%8D%B7%E6%90%AD%E5%BB%BA%E9%A2%86%E5%9F%9F%E5%8C%96%E8%AE%BE%E8%AE%A1)
|
||||
|
||||
问卷组成具有高灵活性,此业务特征带来问卷编辑能力的高复杂性设计。我们将问卷编辑划分为五大子领域,进行产品能力聚类,同时指导系统模块化设计和开发。基于模块编排和管理,能够开箱即用。
|
||||
|
||||
- [二次开发成本低,轻松定制专属调研系统](https://xiaojusurvey.didi.cn/docs/document/%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/%E5%B7%A5%E7%A8%8B%E9%85%8D%E7%BD%AE%E5%8C%96)
|
||||
|
||||
全系统设计原则基于协议标准化、功能模块化、管理配置化,并提供了一些列完整的文档和开发及扩展手册。
|
||||
|
||||
- [部署成本低,快速上线](https://xiaojusurvey.didi.cn/docs/document/%E5%B7%A5%E7%A8%8B%E9%83%A8%E7%BD%B2)
|
||||
|
||||
前后端分离,提供Docker化方案,提供了完善的部署指导手册。
|
||||
|
||||
## 快速启动
|
||||
|
||||
### 复制工程
|
||||
```shell
|
||||
git clone http://github.com/didi/xiaoju-survey
|
||||
```
|
||||
|
||||
## 启动
|
||||
|
||||
### 后端启动
|
||||
|
||||
#### 安装数据库
|
||||
详情查看 [环境准备](https://xiaojusurvey.didi.cn/docs/document/%E6%A6%82%E8%BF%B0/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B#%E5%AE%89%E8%A3%85%E6%95%B0%E6%8D%AE%E5%BA%93)
|
||||
|
||||
#### 安装依赖
|
||||
```shell
|
||||
cd server
|
||||
sh init.sh
|
||||
```
|
||||
|
||||
#### 启动
|
||||
```shell
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 前端启动
|
||||
#### 安装依赖
|
||||
```shell
|
||||
cd web
|
||||
npm install
|
||||
```
|
||||
#### 启动
|
||||
```shell
|
||||
npm run serve
|
||||
```
|
||||
|
||||
## 访问
|
||||
### 问卷管理端
|
||||
|
||||
[http://localhost:8080/management](http://localhost:8080)
|
||||
|
||||
### 问卷投放端
|
||||
创建并发布问卷:
|
||||
|
||||
[http://localhost:8080/render/:surveyPath](http://localhost:8080/render/:surveyPath)
|
||||
|
||||
## 交流群
|
||||
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/NSVCeskQL81698905740736.png" width="200" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
|
3
docker-run.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#! /bin/bash
|
||||
cd /xiaoju-survey/server
|
||||
npm run start
|
106
server/.gitignore
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
package-lock.json
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# project
|
||||
build/
|
||||
src/apps/question/config/env/local.ts
|
18
server/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# 问卷引擎服务端
|
||||
问卷引擎服务端主要用于提供问卷的的管理端和发布端接口,其中管理端包括但不限于创建,编辑,修改,发布问卷接口。发布端主要为问卷提交接口和问卷信息获取接口,再通过问卷的渲染服务来渲染前端内容。
|
||||
|
||||
# 项目依赖安装
|
||||
进入服务端目录,运行init.sh来进行依赖安装
|
||||
```sh
|
||||
sh init.sh
|
||||
```
|
||||
# 项目的启动
|
||||
开发模式的启动
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
正式环境的启动
|
||||
```
|
||||
npm run start
|
||||
```
|
||||
服务默认启动端口为8080
|
10
server/init.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
npm install
|
||||
for appDir in `ls src/apps`
|
||||
do
|
||||
cd src/apps/$appDir
|
||||
# 进行安装依赖
|
||||
npm install
|
||||
cd ../../../
|
||||
done
|
||||
cd ..
|
34
server/package.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "survey-template",
|
||||
"version": "1.0.0",
|
||||
"description": "survey server template",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"copy": "cp -rf ./src/ ./build/",
|
||||
"build": "tsc",
|
||||
"launch": "npm run build && SERVER_ENV=local node ./build/index.js",
|
||||
"start:stable": "SERVER_ENV=stable node ./build/index.js",
|
||||
"start:preonline": "SERVER_ENV=preonline node ./build/index.js",
|
||||
"start:online": "SERVER_ENV=online node ./build/index.js",
|
||||
"start": "npm run copy && npm run launch",
|
||||
"dev": "npm run copy && nodemon -e js,mjs,json,ts --exec 'npm run launch' --watch ./src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.20",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/koa": "^2.13.8",
|
||||
"@types/koa-bodyparser": "^4.3.10",
|
||||
"@types/koa-router": "^7.4.4",
|
||||
"koa": "^2.14.2",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"koa-pino-logger": "^4.0.0",
|
||||
"koa-router": "^12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.15.4",
|
||||
"npm": ">=6.14.10"
|
||||
}
|
||||
}
|
10
server/src/apps/security/config/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
const config = {
|
||||
mongo: {
|
||||
url: process.env.xiaojuSurveyMongoUrl || 'mongodb://localhost:27017',
|
||||
dbName: 'xiaojuSurvey'
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return config
|
||||
}
|
33
server/src/apps/security/db/mongo.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Collection, MongoClient, ObjectId } from 'mongodb'
|
||||
import { getConfig } from '../config/index'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
class mongoService {
|
||||
isInit: boolean
|
||||
client: MongoClient
|
||||
constructor() {
|
||||
this.client = new MongoClient(config.mongo.url);
|
||||
}
|
||||
async getCollection({ collectionName }): Promise<Collection> {
|
||||
await this.client.connect()
|
||||
return this.client.db(config.mongo.dbName).collection(collectionName)
|
||||
}
|
||||
|
||||
convertId2StringByDoc(doc: any): any {
|
||||
doc._id = doc._id.toString()
|
||||
return doc;
|
||||
}
|
||||
|
||||
convertId2StringByList(list: Array<any>): Array<any> {
|
||||
return list.map(e => {
|
||||
return this.convertId2StringByDoc(e);
|
||||
})
|
||||
}
|
||||
|
||||
getObjectIdByStr(str: string): ObjectId {
|
||||
return new ObjectId(str)
|
||||
}
|
||||
}
|
||||
|
||||
export const mongo = new mongoService()
|
16
server/src/apps/security/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { SurveyServer } from '../../decorator'
|
||||
import { securityService } from './service/securityService'
|
||||
|
||||
export default class Security {
|
||||
@SurveyServer({ type: 'rpc' })
|
||||
async isHitKeys({ params, context }: { params: any, context: any }) {
|
||||
const data = securityService.isHitKeys({
|
||||
content: params.content,
|
||||
dictType: params.dictType,
|
||||
})
|
||||
return {
|
||||
result: data,
|
||||
context, // 上下文主要是传递调用方信息使用,比如traceid
|
||||
}
|
||||
}
|
||||
}
|
10
server/src/apps/security/package.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "sceurity",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"mongodb": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
14
server/src/apps/security/service/securityService.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { mongo } from '../db/mongo'
|
||||
import { DICT_TYPE } from '../types/index'
|
||||
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()
|
14
server/src/apps/security/types/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export enum DICT_TYPE {
|
||||
danger = "danger",
|
||||
secret = "secret",
|
||||
}
|
||||
|
||||
export class CommonError extends Error {
|
||||
code: number
|
||||
errmsg: number
|
||||
constructor(msg, code = 500) {
|
||||
super(msg)
|
||||
this.errmsg = msg;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
17
server/src/apps/security/utils/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { CommonError } from '../types/index'
|
||||
|
||||
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
|
||||
}
|
10
server/src/apps/surveyManage/config/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
const config = {
|
||||
mongo: {
|
||||
url: process.env.xiaojuSurveyMongoUrl || 'mongodb://localhost:27017',
|
||||
dbName: 'surveyEengine'
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return config
|
||||
}
|
33
server/src/apps/surveyManage/db/mongo.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Collection, MongoClient, ObjectId } from 'mongodb'
|
||||
import { getConfig } from '../config/index'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
class mongoService {
|
||||
isInit: boolean
|
||||
client: MongoClient
|
||||
constructor() {
|
||||
this.client = new MongoClient(config.mongo.url);
|
||||
}
|
||||
async getCollection({ collectionName }): Promise<Collection> {
|
||||
await this.client.connect()
|
||||
return this.client.db(config.mongo.dbName).collection(collectionName)
|
||||
}
|
||||
|
||||
convertId2StringByDoc(doc: any): any {
|
||||
doc._id = doc._id.toString()
|
||||
return doc;
|
||||
}
|
||||
|
||||
convertId2StringByList(list: Array<any>): Array<any> {
|
||||
return list.map(e => {
|
||||
return this.convertId2StringByDoc(e);
|
||||
})
|
||||
}
|
||||
|
||||
getObjectIdByStr(str: string): ObjectId {
|
||||
return new ObjectId(str)
|
||||
}
|
||||
}
|
||||
|
||||
export const mongo = new mongoService()
|
184
server/src/apps/surveyManage/index.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { SurveyServer } from '../../decorator'
|
||||
import { Request, Response } from 'koa'
|
||||
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'
|
||||
|
||||
export default class SurveyManage {
|
||||
@SurveyServer({type:'http',method:'get',routerName:'/getBannerData'})
|
||||
async getBannerData({req,res}:{req:Request, res:Response}) {
|
||||
const data = await surveyService.getBannerData()
|
||||
return {
|
||||
code:200,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
@SurveyServer({type:'http',method:'post',routerName:'/add'})
|
||||
async add({req,res}:{req:Request, res:Response}) {
|
||||
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:'/update'})
|
||||
async update({req,res}:{req:Request, res:Response}) {
|
||||
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,res}:{req:Request, res:Response}) {
|
||||
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,res}:{req:Request, res:Response}) {
|
||||
const condition = getValidateValue(Joi.object({
|
||||
curPage: Joi.number().default(1),
|
||||
pageSize: Joi.number().default(10),
|
||||
}).validate(req.query,{allowUnknown: true}));
|
||||
const userData = await userService.checkLogin({req})
|
||||
const listRes = await surveyService.list({
|
||||
pageNum:condition.curPage,
|
||||
pageSize:condition.pageSize,
|
||||
userData
|
||||
})
|
||||
return {
|
||||
code:200,
|
||||
data: listRes,
|
||||
}
|
||||
}
|
||||
@SurveyServer({type:'http',method:'post',routerName:'/saveConf'})
|
||||
async saveConf({req,res}:{req:Request, res:Response}) {
|
||||
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,res}:{req:Request, res:Response}) {
|
||||
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,res}:{req:Request, res:Response}) {
|
||||
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,res}:{req:Request, res:Response}) {
|
||||
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,res}:{req:Request, res:Response}) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
13
server/src/apps/surveyManage/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "survey_manage",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"joi": "^17.9.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"mongodb": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
37
server/src/apps/surveyManage/service/surveyHistoryService.ts
Normal file
@ -0,0 +1,37 @@
|
||||
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: any, 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()
|
307
server/src/apps/surveyManage/service/surveyService.ts
Normal file
@ -0,0 +1,307 @@
|
||||
import { mongo } from '../db/mongo'
|
||||
import { rpcInvote } from '../../../rpc'
|
||||
import { SURVEY_STATUS, QUESTION_TYPE, CommonError, UserType, DICT_TYPE } from '../types/index'
|
||||
import { getStatusObject, genSurveyPath, getMapByKey, hanleSensitiveDate } from '../utils/index'
|
||||
import * as path from "path";
|
||||
import * as _ from "lodash";
|
||||
import * as moment from "moment";
|
||||
|
||||
class SurveyService {
|
||||
async checkSecurity({ content, dictType }: { content: string, dictType: DICT_TYPE }) {
|
||||
const rpcResult = await rpcInvote<any, { result: boolean }>('security.isHitKeys', {
|
||||
params: { content, dictType },
|
||||
context: {}
|
||||
})
|
||||
return rpcResult.result
|
||||
}
|
||||
|
||||
async getBannerData() {
|
||||
const bannerConfPath = path.resolve(__dirname, "../template/banner/index.json");
|
||||
return require(bannerConfPath)
|
||||
}
|
||||
|
||||
async getCodeData({
|
||||
questionType,
|
||||
}: { questionType: QUESTION_TYPE }): Promise<any> {
|
||||
const baseConfPath = path.resolve(__dirname, "../template/surveyTemplate/templateBase.json");
|
||||
const templateConfPath = path.resolve(
|
||||
__dirname,
|
||||
`../template/surveyTemplate/survey/${questionType}.json`,
|
||||
);
|
||||
const baseConf = _.cloneDeep(require(baseConfPath))
|
||||
const templateConf = _.cloneDeep(require(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 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(),
|
||||
}
|
||||
})
|
||||
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 }) {
|
||||
const surveyMeta = await mongo.getCollection({ collectionName: 'surveyMeta' });
|
||||
const cond = {
|
||||
owner: condition.userData.username
|
||||
}
|
||||
const data = await surveyMeta.find(cond)
|
||||
.sort({ updateDate: -1 })
|
||||
.limit(condition.pageSize)
|
||||
.skip((condition.pageNum - 1) * condition.pageSize)
|
||||
.toArray()
|
||||
const count = await surveyMeta.countDocuments(cond);
|
||||
return { data: mongo.convertId2StringByList(data), count }
|
||||
}
|
||||
|
||||
getListHeadByDataList(dataList) {
|
||||
return 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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 = getMapByKey({ data: dataList, key: 'field' })
|
||||
const surveySubmit = await mongo.getCollection({ collectionName: 'surveySubmit' });
|
||||
const surveySubmitData = await surveySubmit.find({ pageId: condition.surveyId })
|
||||
.sort({ createDate: -1 })
|
||||
.limit(condition.pageSize)
|
||||
.skip((condition.pageNum - 1) * condition.pageSize)
|
||||
.toArray()
|
||||
const listBody = surveySubmitData.map((surveySubmitResList) => {
|
||||
const data = surveySubmitResList.data
|
||||
const dataKeys = Object.keys(data)
|
||||
for(const itemKey of dataKeys) {
|
||||
if(typeof itemKey !== 'string') {continue}
|
||||
if(itemKey.indexOf("data")!==0) {continue}
|
||||
const itemConfigKey = itemKey.split("_")[0];
|
||||
const itemConfig = dataListMap[itemConfigKey];
|
||||
// 题目删除会出现,数据列表报错
|
||||
if(!itemConfig) {continue}
|
||||
const doSecretData = (data)=>{
|
||||
if(itemConfig.isSecret && condition.isShowSecret) {
|
||||
return hanleSensitiveDate(data)
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
data[itemKey] = doSecretData(data[itemKey])
|
||||
// 处理选项
|
||||
if(itemConfig.type === 'radio-star' && !data[`${itemConfigKey}_custom`]) {
|
||||
data[`${itemConfigKey}_custom`] = data[`${itemConfigKey}_${data[itemConfigKey]}`]
|
||||
}
|
||||
if (!itemConfig?.options?.length) { continue }
|
||||
const options = itemConfig.options
|
||||
const optionsMap = getMapByKey({ data: options, key: 'hash' })
|
||||
const getText = e => doSecretData(optionsMap?.[e]?.text || e);
|
||||
if (Array.isArray(data[itemKey])) {
|
||||
data[itemKey] = data[itemKey].map(getText)
|
||||
} else {
|
||||
data[itemKey] = getText(data[itemKey])
|
||||
}
|
||||
}
|
||||
return data
|
||||
})
|
||||
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: any }) {
|
||||
const surveyConf = await mongo.getCollection({ collectionName: 'surveyConf' });
|
||||
const saveRes = await surveyConf.updateOne({
|
||||
pageId: surveyData.surveyId
|
||||
}, {
|
||||
$set: {
|
||||
code: surveyData.configData
|
||||
}
|
||||
})
|
||||
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()
|
19
server/src/apps/surveyManage/service/userService.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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<any, { result: UserType }>('user.getUserByToken', {
|
||||
params: { token },
|
||||
context: req
|
||||
})
|
||||
return rpcResult.result
|
||||
}
|
||||
}
|
||||
|
||||
export const userService = new UserService()
|
174
server/src/apps/surveyManage/template/banner/index.json
Normal file
@ -0,0 +1,174 @@
|
||||
{
|
||||
"temp": {
|
||||
"key": "default",
|
||||
"name": "默认分类",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.jpg",
|
||||
"title": "1"
|
||||
}]
|
||||
},
|
||||
"activity": {
|
||||
"key": "activity",
|
||||
"name": "节日",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/Ixx8hqiwwk1660979120801.jpg",
|
||||
"title": "1"
|
||||
}]
|
||||
},
|
||||
"creative": {
|
||||
"key": "creative",
|
||||
"name": "创意",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/PLwNH1rAie1558430219772.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/NnXsAOhBNm1558430219312.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/ujpUoWqhw31558430220124.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/5OCvbjqJQm1558430220362.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/0k7Jg7In8I1558430221154.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/UH0A8DbTai1558430221033.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/FRIzPC6ZtN1558430221344.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"scenery": {
|
||||
"key": "scenery",
|
||||
"name": "风景",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/SyiLRcukyE1558430525760.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/sqYig4AcWr1558430525663.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/ElNeqJT2I21558430526165.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/CxQkSU6AY21558430526163.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/VTUwbp6vY61558430527320.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/SHs0K703Yn1558430527218.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/oVTedX9V4s1558430527671.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"transportation": {
|
||||
"key": "transportation",
|
||||
"name": "交通",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/XYKqJZuMig1558430904735.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/GnPatsr48Z1558430904680.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/UqIvVvEXAK1558430905204.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/PUssufh5uI1558430905104.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/O409pRTDlW1558430905738.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/A9FzlbYXqI1558430905739.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/HN9YGctDeF1558430906686.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"delicacy": {
|
||||
"key": "delicacy",
|
||||
"name": "美食",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/lE6PSclCcU1558434536703.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/OnSdbm7u6n1558434536641.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/N9Z2ZuyO731558434537314.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/YP9PoW8pX51558434537301.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/zUtDv378bg1558434538351.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/gY1JljCow21558434538303.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/oOjHPbABdd1558434538864.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"business": {
|
||||
"key": "business",
|
||||
"name": "商务",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/3ABKqvDaVn1558514860472.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/OewuaQmWoq1558514860285.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/HuVqqtbFjs1558514860570.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/icSlqsr0uZ1558514860875.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/Qu9rg33wmq1558514861015.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/145gBCRtNP1558514861211.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/ykWLFV0QWj1558514861444.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
},
|
||||
"campus": {
|
||||
"key": "campus",
|
||||
"name": "校园",
|
||||
"list": [{
|
||||
"src": "/imgs/skin/4aWi5JxG471558514268698.jpg",
|
||||
"title": "1"
|
||||
}, {
|
||||
"src": "/imgs/skin/j8C2OBP7WK1558514268563.jpg",
|
||||
"title": "2"
|
||||
}, {
|
||||
"src": "/imgs/skin/q3uJoQhYsR1558514268877.jpg",
|
||||
"title": "3"
|
||||
}, {
|
||||
"src": "/imgs/skin/W5PPlNsmsr1558514269088.jpg",
|
||||
"title": "4"
|
||||
}, {
|
||||
"src": "/imgs/skin/6xQk1IAmKt1558514269874.jpg",
|
||||
"title": "5"
|
||||
}, {
|
||||
"src": "/imgs/skin/XQE2iyF0rj1558514269935.jpg",
|
||||
"title": "6"
|
||||
}, {
|
||||
"src": "/imgs/skin/POHlQiSwPR1558514270379.jpg",
|
||||
"title": "7"
|
||||
}]
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
{
|
||||
"bannerConf": {
|
||||
"titleConfig": {
|
||||
"mainTitle": "<h3 style=\"text-align: center\">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style=\"color: rgb(204, 0, 0)\">期待您的参与!</span></p>",
|
||||
"subTitle": ""
|
||||
},
|
||||
"bannerConfig": {
|
||||
"bgImage": "//img-hxy021.didistatic.com/static/starimg/img/BP0HXOXDv01698311607854.jpg",
|
||||
"videoLink": "",
|
||||
"postImg": ""
|
||||
}
|
||||
},
|
||||
"dataConf": {
|
||||
"dataList": [
|
||||
{
|
||||
"isRequired": true,
|
||||
"showIndex": true,
|
||||
"showType": true,
|
||||
"showSpliter": true,
|
||||
"type": "text",
|
||||
"valid": "",
|
||||
"field": "data458",
|
||||
"title": "标题1",
|
||||
"placeholder": "",
|
||||
"randomSort": false,
|
||||
"checked": false,
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"maxPhotos": 5,
|
||||
"star": 5,
|
||||
"timeStep": {
|
||||
"hour": 1,
|
||||
"min": 10
|
||||
},
|
||||
"nps": {
|
||||
"leftText": "极不满意",
|
||||
"rightText": "极满意"
|
||||
},
|
||||
"placeholderDesc": "",
|
||||
"addressType": 3,
|
||||
"isAuto": false,
|
||||
"urlKey": "",
|
||||
"othersKeyMap": {},
|
||||
"textRange": {
|
||||
"min": {
|
||||
"placeholder": "0",
|
||||
"value": 0
|
||||
},
|
||||
"max": {
|
||||
"placeholder": "500",
|
||||
"value": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"isRequired": true,
|
||||
"showIndex": true,
|
||||
"showType": true,
|
||||
"showSpliter": true,
|
||||
"type": "radio",
|
||||
"placeholderDesc": "",
|
||||
"field": "data515",
|
||||
"title": "标题2",
|
||||
"placeholder": "",
|
||||
"randomSort": false,
|
||||
"checked": false,
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"options": [
|
||||
{
|
||||
"text": "选项1",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115019"
|
||||
},
|
||||
{
|
||||
"text": "选项2",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115020"
|
||||
}
|
||||
],
|
||||
"importKey": "single",
|
||||
"importData": "",
|
||||
"cOption": "",
|
||||
"cOptions": [],
|
||||
"timeStep": {
|
||||
"hour": 1,
|
||||
"min": 10
|
||||
},
|
||||
"maxPhotos": 5,
|
||||
"nps": {
|
||||
"leftText": "极不满意",
|
||||
"rightText": "极满意"
|
||||
},
|
||||
"star": 5,
|
||||
"exclude": false,
|
||||
"addressType": 3,
|
||||
"isAuto": false,
|
||||
"othersKeyMap": {},
|
||||
"textRange": {
|
||||
"min": {
|
||||
"placeholder": "0",
|
||||
"value": 0
|
||||
},
|
||||
"max": {
|
||||
"placeholder": "500",
|
||||
"value": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"submitConf": {
|
||||
"submitTitle": "提交",
|
||||
"confirmAgain": {
|
||||
"is_again": true,
|
||||
"again_text": "确认要提交吗?"
|
||||
},
|
||||
"msgContent": {
|
||||
"msg_200": "提交成功",
|
||||
"msg_9001": "您来晚了,感谢支持问卷~",
|
||||
"msg_9002": "请勿多次提交!",
|
||||
"msg_9003": "您来晚了,已经满额!",
|
||||
"msg_9004": "提交失败!"
|
||||
}
|
||||
},
|
||||
"bottomConf": {
|
||||
"logoImage": "",
|
||||
"logoImageWidth": "33%"
|
||||
},
|
||||
"baseConf": {
|
||||
"begTime": "2018-05-22 17:17:48",
|
||||
"endTime": "2028-05-22 17:17:48",
|
||||
"tLimit": "0",
|
||||
"language": "chinese"
|
||||
},
|
||||
"skinConf": {
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff"
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
{
|
||||
"submitConf": {
|
||||
"submitTitle": "提交",
|
||||
"confirmAgain": {
|
||||
"is_again": true,
|
||||
"again_text": "确认要提交吗?"
|
||||
},
|
||||
"msgContent": {
|
||||
"msg_200": "提交成功",
|
||||
"msg_9001": "您来晚了,感谢支持问卷~",
|
||||
"msg_9002": "请勿多次提交!",
|
||||
"msg_9003": "您来晚了,已经满额!",
|
||||
"msg_9004": "提交失败!"
|
||||
}
|
||||
},
|
||||
"skinConf": {
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff"
|
||||
},
|
||||
"bottomConf": {
|
||||
"logoImage": "",
|
||||
"logoImageWidth": "40%"
|
||||
},
|
||||
"bannerConf": {
|
||||
"titleConfig": {
|
||||
"mainTitle": "<h3 style=\"text-align: center\">满意度调研</h3> <p> </p> <p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style=\"color: rgb(204, 0, 0)\">期待您的参与</span></p>",
|
||||
"subTitle": ""
|
||||
},
|
||||
"bannerConfig": {
|
||||
"bgImage": "//img-hxy021.didistatic.com/static/starimg/img/BP0HXOXDv01698311607854.jpg",
|
||||
"videoLink": "",
|
||||
"postImg": ""
|
||||
}
|
||||
},
|
||||
"baseConf": {
|
||||
"showVoteProcess": "allow",
|
||||
"begTime": "2018-05-22 17:17:48",
|
||||
"endTime": "2028-05-22 17:17:48",
|
||||
"tLimit": "0",
|
||||
"language": "chinese"
|
||||
},
|
||||
"dataConf": {
|
||||
"dataList": [
|
||||
{
|
||||
"field": "data8",
|
||||
"showIndex": true,
|
||||
"showType": true,
|
||||
"showSpliter": true,
|
||||
"placeholderDesc": "",
|
||||
"placeholder": "",
|
||||
"isRequired": true,
|
||||
"randomSort": false,
|
||||
"innerRandom": false,
|
||||
"hideSubTitleIndex": false,
|
||||
"checked": false,
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"relyType": "and",
|
||||
"extraOptions": [],
|
||||
"importKey": "single",
|
||||
"importData": "",
|
||||
"npsMin": 0,
|
||||
"addressType": 3,
|
||||
"isAuto": false,
|
||||
"urlKey": "",
|
||||
"hasRely": true,
|
||||
"relyList": [],
|
||||
"jumpTo": "",
|
||||
"optionOrigin": "",
|
||||
"answerTip": "",
|
||||
"type": "text",
|
||||
"valid": "",
|
||||
"title": "标题1",
|
||||
"answer": "",
|
||||
"othersKeyMap": {},
|
||||
"textRange": {
|
||||
"min": {
|
||||
"placeholder": "0",
|
||||
"value": 0
|
||||
},
|
||||
"max": {
|
||||
"placeholder": "500",
|
||||
"value": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"field": "data4",
|
||||
"showIndex": true,
|
||||
"showType": true,
|
||||
"showSpliter": true,
|
||||
"placeholderDesc": "",
|
||||
"placeholder": "",
|
||||
"isRequired": true,
|
||||
"randomSort": false,
|
||||
"innerRandom": false,
|
||||
"hideSubTitleIndex": false,
|
||||
"checked": false,
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"relyType": "and",
|
||||
"extraOptions": [],
|
||||
"importKey": "single",
|
||||
"importData": "",
|
||||
"cOption": "",
|
||||
"cOptions": [],
|
||||
"npsMin": 0,
|
||||
"star": 5,
|
||||
"addressType": 3,
|
||||
"isAuto": false,
|
||||
"urlKey": "",
|
||||
"defaultProps": {
|
||||
"children": "children",
|
||||
"label": "name",
|
||||
"id": "id"
|
||||
},
|
||||
"hasRely": true,
|
||||
"relyList": [],
|
||||
"jumpTo": "",
|
||||
"optionOrigin": "",
|
||||
"answerTip": "",
|
||||
"type": "radio-star",
|
||||
"title": "标题3",
|
||||
"answer": "",
|
||||
"othersKeyMap": {},
|
||||
"options": [],
|
||||
"textRange": {
|
||||
"min": {
|
||||
"placeholder": "0",
|
||||
"value": 0
|
||||
},
|
||||
"max": {
|
||||
"placeholder": "500",
|
||||
"value": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
{
|
||||
"bannerConf": {
|
||||
"titleConfig": {
|
||||
"mainTitle": "<h3 style=\"text-align: center\">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style=\"color: rgb(204, 0, 0)\">期待您的参与!</span></p>",
|
||||
"subTitle": ""
|
||||
},
|
||||
"bannerConfig": {
|
||||
"bgImage": "//img-hxy021.didistatic.com/static/starimg/img/BP0HXOXDv01698311607854.jpg",
|
||||
"videoLink": "",
|
||||
"postImg": ""
|
||||
}
|
||||
},
|
||||
"dataConf": {
|
||||
"dataList": [
|
||||
{
|
||||
"isRequired": true,
|
||||
"showIndex": true,
|
||||
"showType": true,
|
||||
"showSpliter": true,
|
||||
"type": "text",
|
||||
"valid": "",
|
||||
"field": "data458",
|
||||
"title": "姓名",
|
||||
"placeholder": "",
|
||||
"randomSort": false,
|
||||
"checked": false,
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"maxPhotos": 5,
|
||||
"star": 5,
|
||||
"timeStep": {
|
||||
"hour": 1,
|
||||
"min": 10
|
||||
},
|
||||
"nps": {
|
||||
"leftText": "极不满意",
|
||||
"rightText": "极满意"
|
||||
},
|
||||
"exclude": false,
|
||||
"relTypes": {
|
||||
"textarea": "多行文本框",
|
||||
"text": "单行输入框"
|
||||
},
|
||||
"placeholderDesc": "",
|
||||
"mhLimit": 0,
|
||||
"addressType": 3,
|
||||
"isAuto": false,
|
||||
"jumpTo": "",
|
||||
"startDate": "",
|
||||
"endDate": "",
|
||||
"othersKeyMap": {},
|
||||
"textRange": {
|
||||
"min": {
|
||||
"placeholder": "0",
|
||||
"value": 0
|
||||
},
|
||||
"max": {
|
||||
"placeholder": "500",
|
||||
"value": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"isRequired": true,
|
||||
"showIndex": true,
|
||||
"showType": true,
|
||||
"showSpliter": true,
|
||||
"type": "radio",
|
||||
"placeholderDesc": "",
|
||||
"field": "data515",
|
||||
"title": "选择您感兴趣的课程进行报名",
|
||||
"placeholder": "",
|
||||
"valid": "",
|
||||
"randomSort": false,
|
||||
"checked": false,
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"options": [
|
||||
{
|
||||
"text": "课程1",
|
||||
"hash": "115019",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": ""
|
||||
},
|
||||
{
|
||||
"text": "课程2",
|
||||
"hash": "115020",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": ""
|
||||
},
|
||||
{
|
||||
"text": "课程3",
|
||||
"hash": "115021",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": ""
|
||||
},
|
||||
{
|
||||
"text": "课程4",
|
||||
"hash": "115022",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": ""
|
||||
}
|
||||
],
|
||||
"timeStep": {
|
||||
"hour": 1,
|
||||
"min": 10
|
||||
},
|
||||
"maxPhotos": 5,
|
||||
"nps": {
|
||||
"leftText": "极不满意",
|
||||
"rightText": "极满意"
|
||||
},
|
||||
"star": 5,
|
||||
"exclude": false,
|
||||
"addressType": 3,
|
||||
"isAuto": false,
|
||||
"urlKey": "",
|
||||
"defaultProps": {
|
||||
"children": "children",
|
||||
"label": "name",
|
||||
"id": "id"
|
||||
},
|
||||
"startDate": "",
|
||||
"endDate": "",
|
||||
"othersKeyMap": {},
|
||||
"textRange": {
|
||||
"min": {
|
||||
"placeholder": "0",
|
||||
"value": 0
|
||||
},
|
||||
"max": {
|
||||
"placeholder": "500",
|
||||
"value": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"submitConf": {
|
||||
"submitTitle": "提交",
|
||||
"confirmAgain": {
|
||||
"is_again": true,
|
||||
"again_text": "确认要提交吗?"
|
||||
},
|
||||
"msgContent": {
|
||||
"msg_200": "提交成功",
|
||||
"msg_9001": "您来晚了,感谢支持问卷~",
|
||||
"msg_9002": "请勿多次提交!",
|
||||
"msg_9003": "您来晚了,已经满额!",
|
||||
"msg_9004": "提交失败!"
|
||||
},
|
||||
"link": ""
|
||||
},
|
||||
"bottomConf": {
|
||||
"logoImage": "",
|
||||
"logoImageWidth": "33%"
|
||||
},
|
||||
"baseConf": {
|
||||
"begTime": "2018-05-22 17:17:48",
|
||||
"endTime": "2028-05-22 17:17:48",
|
||||
"tLimit": "0",
|
||||
"language": "chinese"
|
||||
},
|
||||
"skinConf": {
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff"
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
{
|
||||
"bannerConf": {
|
||||
"titleConfig": {
|
||||
"mainTitle": "<h3 style=\"text-align: center\">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style=\"color: rgb(204, 0, 0)\">期待您的参与!</span></p>",
|
||||
"subTitle": ""
|
||||
},
|
||||
"bannerConfig": {
|
||||
"bgImage": "//img-hxy021.didistatic.com/static/starimg/img/BP0HXOXDv01698311607854.jpg",
|
||||
"videoLink": "",
|
||||
"postImg": ""
|
||||
}
|
||||
},
|
||||
"dataConf": {
|
||||
"dataList": [
|
||||
{
|
||||
"isRequired": true,
|
||||
"showIndex": true,
|
||||
"showType": true,
|
||||
"showSpliter": true,
|
||||
"type": "text",
|
||||
"valid": "",
|
||||
"field": "data631",
|
||||
"title": "标题3",
|
||||
"placeholder": "",
|
||||
"sLimit": 1,
|
||||
"randomSort": false,
|
||||
"checked": false,
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"options": [
|
||||
{
|
||||
"text": "选项1",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": ""
|
||||
},
|
||||
{
|
||||
"text": "选项2",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": ""
|
||||
}
|
||||
],
|
||||
"maxPhotos": 5,
|
||||
"star": 5,
|
||||
"timeStep": {
|
||||
"hour": 1,
|
||||
"min": 10
|
||||
},
|
||||
"nps": {
|
||||
"leftText": "极不满意",
|
||||
"rightText": "极满意"
|
||||
},
|
||||
"exclude": false,
|
||||
"relTypes": {
|
||||
"textarea": "多行文本框",
|
||||
"text": "单行输入框"
|
||||
},
|
||||
"placeholderDesc": "",
|
||||
"addressType": 3,
|
||||
"isAuto": false,
|
||||
"urlKey": "",
|
||||
"othersKeyMap": {},
|
||||
"textRange": {
|
||||
"min": {
|
||||
"placeholder": "0",
|
||||
"value": 0
|
||||
},
|
||||
"max": {
|
||||
"placeholder": "500",
|
||||
"value": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"isRequired": true,
|
||||
"showIndex": true,
|
||||
"showType": true,
|
||||
"showSpliter": true,
|
||||
"type": "vote",
|
||||
"innerType": "radio",
|
||||
"placeholderDesc": "",
|
||||
"field": "data606",
|
||||
"title": "标题1",
|
||||
"placeholder": "",
|
||||
"randomSort": false,
|
||||
"checked": false,
|
||||
"minNum": "",
|
||||
"maxNum": "",
|
||||
"options": [
|
||||
{
|
||||
"text": "甜的",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115019"
|
||||
},
|
||||
{
|
||||
"text": "咸的",
|
||||
"imageUrl": "",
|
||||
"others": false,
|
||||
"mustOthers": false,
|
||||
"othersKey": "",
|
||||
"placeholderDesc": "",
|
||||
"hash": "115020"
|
||||
}
|
||||
],
|
||||
"timeStep": {
|
||||
"hour": 1,
|
||||
"min": 10
|
||||
},
|
||||
"maxPhotos": 5,
|
||||
"nps": {
|
||||
"leftText": "极不满意",
|
||||
"rightText": "极满意"
|
||||
},
|
||||
"star": 5,
|
||||
"addressType": 3,
|
||||
"othersKeyMap": {},
|
||||
"textRange": {
|
||||
"min": {
|
||||
"placeholder": "0",
|
||||
"value": 0
|
||||
},
|
||||
"max": {
|
||||
"placeholder": "500",
|
||||
"value": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"submitConf": {
|
||||
"submitTitle": "提交",
|
||||
"confirmAgain": {
|
||||
"is_again": true,
|
||||
"again_text": "确认要提交吗?"
|
||||
},
|
||||
"msgContent": {
|
||||
"msg_200": "提交成功",
|
||||
"msg_9001": "您来晚了,感谢支持问卷~",
|
||||
"msg_9002": "请勿多次提交!",
|
||||
"msg_9003": "您来晚了,已经满额!",
|
||||
"msg_9004": "提交失败!"
|
||||
}
|
||||
},
|
||||
"bottomConf": {
|
||||
"logoImage": "",
|
||||
"logoImageWidth": "33%"
|
||||
},
|
||||
"baseConf": {
|
||||
"begTime": "2018-05-25 10:22:23",
|
||||
"endTime": "2028-05-25 10:22:23",
|
||||
"tLimit": "0",
|
||||
"language": "chinese"
|
||||
},
|
||||
"skinConf": {
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff"
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
{
|
||||
"bannerConf": {
|
||||
"titleConfig": {
|
||||
"mainTitle": "<h3 style=\"text-align: center\">欢迎填写问卷</h3><p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style=\"color: rgb(204, 0, 0)\">期待您的参与!</span></p>",
|
||||
"subTitle": ""
|
||||
},
|
||||
"bannerConfig": {
|
||||
"bgImage": "//img-hxy021.didistatic.com/static/starimg/img/BP0HXOXDv01698311607854.jpg",
|
||||
"videoLink": "",
|
||||
"postImg": ""
|
||||
}
|
||||
},
|
||||
"submitConf": {
|
||||
"submitTitle": "提交",
|
||||
"confirmAgain": {
|
||||
"is_again": true,
|
||||
"again_text": "确认要提交吗?"
|
||||
},
|
||||
"msgContent": {
|
||||
"msg_200": "提交成功",
|
||||
"msg_9001": "您来晚了,感谢支持问卷~",
|
||||
"msg_9002": "请勿多次提交!",
|
||||
"msg_9003": "您来晚了,已经满额!",
|
||||
"msg_9004": "提交失败!"
|
||||
}
|
||||
},
|
||||
"bottomConf": {
|
||||
"logoImage": "",
|
||||
"logoImageWidth": "28%"
|
||||
},
|
||||
"baseConf": {
|
||||
"begTime": "2018-05-30 10:38:31",
|
||||
"endTime": "2028-05-30 10:38:31",
|
||||
"tLimit": "0",
|
||||
"language": "chinese",
|
||||
"showVoteProcess": "allow"
|
||||
},
|
||||
"skinConf": {
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff"
|
||||
}
|
||||
}
|
43
server/src/apps/surveyManage/types/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
export enum DICT_TYPE {
|
||||
danger = "danger",
|
||||
secret = "secret",
|
||||
}
|
||||
|
||||
export enum SURVEY_STATUS {
|
||||
new = "new",
|
||||
editing = "editing",
|
||||
pausing = "pausing",
|
||||
published = "published",
|
||||
removed = "removed",
|
||||
}
|
||||
|
||||
export enum QUESTION_TYPE {
|
||||
enps = "enps",
|
||||
nps = "nps",
|
||||
question = "question", //通用问卷
|
||||
register = "register", //报名
|
||||
vote = "vote", //投票
|
||||
}
|
||||
|
||||
export enum HISTORY_TYPE {
|
||||
dailyHis = "dailyHis", //保存历史
|
||||
publishHis = "publishHis", //发布历史
|
||||
}
|
||||
|
||||
export interface UserType {
|
||||
_id:string,
|
||||
username:string,
|
||||
password:string,
|
||||
curStatus:any,
|
||||
createDate:number
|
||||
}
|
||||
|
||||
export class CommonError extends Error {
|
||||
code:number
|
||||
errmsg:number
|
||||
constructor(msg,code=500) {
|
||||
super(msg)
|
||||
this.errmsg = msg;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
37
server/src/apps/surveyManage/utils/base58.ts
Normal file
@ -0,0 +1,37 @@
|
||||
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
|
||||
{
|
||||
let 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
|
||||
{
|
||||
let base58NumArray =base58Num.split('');
|
||||
let big58Number = 0n;
|
||||
let len = base58NumArray.length;
|
||||
for(let i = 1;i<=len;i++)
|
||||
{
|
||||
let big58NumberTemp = BigInt(base58KeysObject[base58NumArray[len-i]])*(base58Len** BigInt(i-1));
|
||||
big58Number += big58NumberTemp;
|
||||
}
|
||||
return big58Number.toString(16);
|
||||
}
|
49
server/src/apps/surveyManage/utils/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { SURVEY_STATUS } from '../types/index'
|
||||
import { CommonError } from '../types/index'
|
||||
import { hex2Base58 } from './base58'
|
||||
import * as Joi from 'joi'
|
||||
|
||||
export function getStatusObject({status}:{status:SURVEY_STATUS}) {
|
||||
return {
|
||||
status,
|
||||
id: status,
|
||||
date: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getValidateValue<T=any>(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 function getMapByKey({data,key}:{data:Array<any>,key:string}) {
|
||||
const datamap = {}
|
||||
for(const item of data) {
|
||||
datamap[item[key]] = item
|
||||
}
|
||||
return datamap
|
||||
}
|
||||
|
||||
export function hanleSensitiveDate(value:string = ''): string {
|
||||
if (!value) {
|
||||
return '*'
|
||||
}
|
||||
|
||||
let str = '' + value
|
||||
if (str.length === 1) {
|
||||
str = '*'
|
||||
}
|
||||
if (str.length === 2) {
|
||||
str = str[0] + '*'
|
||||
}
|
||||
if (str.length >= 3) {
|
||||
str = str[0] + '***' + str.slice(str.length - 1)
|
||||
}
|
||||
return str
|
||||
}
|
10
server/src/apps/surveyPublish/config/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
const config = {
|
||||
mongo:{
|
||||
url: process.env.xiaojuSurveyMongoUrl ||'mongodb://localhost:27017',
|
||||
dbName:'surveyEengine'
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return config
|
||||
}
|
33
server/src/apps/surveyPublish/db/mongo.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Collection, MongoClient, ObjectId } from 'mongodb'
|
||||
import { getConfig } from '../config/index'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
class mongoService {
|
||||
isInit:boolean
|
||||
client:MongoClient
|
||||
constructor() {
|
||||
this.client = new MongoClient(config.mongo.url);
|
||||
}
|
||||
async getCollection({collectionName}):Promise<Collection> {
|
||||
await this.client.connect()
|
||||
return this.client.db(config.mongo.dbName).collection(collectionName)
|
||||
}
|
||||
|
||||
convertId2StringByDoc(doc:any):any {
|
||||
doc._id = doc._id.toString()
|
||||
return doc;
|
||||
}
|
||||
|
||||
convertId2StringByList(list:Array<any>):Array<any> {
|
||||
return list.map(e=>{
|
||||
return this.convertId2StringByDoc(e);
|
||||
})
|
||||
}
|
||||
|
||||
getObjectIdByStr(str:string) {
|
||||
return new ObjectId(str)
|
||||
}
|
||||
}
|
||||
|
||||
export const mongo = new mongoService()
|
53
server/src/apps/surveyPublish/index.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { SurveyServer } from '../../decorator'
|
||||
import { Request, Response } from 'koa'
|
||||
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'
|
||||
|
||||
export default class SurveyPublish {
|
||||
// 获取发布配置
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/getSurveyPublish' })
|
||||
async getSurveyPublish({ req, res }: { req: Request, res: Response }) {
|
||||
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, res }: { req: Request, res: Response }) {
|
||||
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: 'post', routerName: '/submit' })
|
||||
async submit({ req, res }: { req: Request, res: Response }) {
|
||||
// 检查签名
|
||||
checkSign(req.body)
|
||||
// 校验参数
|
||||
const surveySubmitData = getValidateValue(Joi.object({
|
||||
surveyPath: Joi.string().required(),
|
||||
data: Joi.string().required(),
|
||||
encryptType: Joi.string(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
await surveySubmitService.submit({ surveySubmitData })
|
||||
return {
|
||||
code: 200,
|
||||
msg: "报名成功",
|
||||
}
|
||||
}
|
||||
}
|
13
server/src/apps/surveyPublish/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "survey_publish",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"joi": "^17.9.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"mongodb": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import { mongo } from '../db/mongo'
|
||||
|
||||
// 该服务用于模拟redis
|
||||
class SurveyKeyStoreService {
|
||||
getKeyStoreResult(surveyKeyStoreData:Array<any>) {
|
||||
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 surveyKeyStoreData = await surveyKeyStore.find({
|
||||
key:{$in:keyList},
|
||||
surveyPath,
|
||||
type
|
||||
}).toArray()
|
||||
return this.getKeyStoreResult(surveyKeyStoreData)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const surveyKeyStoreService = new SurveyKeyStoreService()
|
@ -0,0 +1,31 @@
|
||||
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()
|
85
server/src/apps/surveyPublish/service/surveySubmitService.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { mongo } from '../db/mongo'
|
||||
import { getStatusObject, getMapByKey } from '../utils/index'
|
||||
import { SURVEY_STATUS, CommonError } from '../types/index'
|
||||
import { surveyKeyStoreService } from './surveyKeyStoreService'
|
||||
import * as moment from 'moment'
|
||||
|
||||
class SurveySubmitService {
|
||||
async submit({ surveySubmitData }: { surveySubmitData: any }) {
|
||||
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 {
|
||||
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
|
||||
let nowSubmitCount = await surveySubmit.countDocuments({ surveyPath: surveySubmitData.surveyPath }) || 0
|
||||
if (nowSubmitCount >= tLimit) {
|
||||
throw new CommonError('超出提交总数限制')
|
||||
}
|
||||
}
|
||||
// 投票信息保存
|
||||
const dataList = publishConf?.code?.dataConf?.dataList || []
|
||||
const dataListMap = getMapByKey({ data: dataList, key: 'field' })
|
||||
const surveySubmitDataKeys = Object.keys(surveySubmitData.data)
|
||||
for (const field of surveySubmitDataKeys) {
|
||||
const configData = dataListMap[field]
|
||||
if (configData && /vote/.exec(configData.type)) {
|
||||
const voteData = (await surveyKeyStoreService.get({ surveyPath: surveySubmitData.surveyPath, key: field, type: 'vote' })) || { total: 0 }
|
||||
voteData.total++;
|
||||
if (!voteData[surveySubmitData.data[field]]) {
|
||||
voteData[surveySubmitData.data[field]] = 1
|
||||
} else {
|
||||
voteData[surveySubmitData.data[field]]++;
|
||||
}
|
||||
await surveyKeyStoreService.set({ surveyPath: surveySubmitData.surveyPath, key: field, data: voteData, type: 'vote' })
|
||||
}
|
||||
}
|
||||
// 提交问卷
|
||||
const surveySubmitRes = await surveySubmit.insertOne({
|
||||
...surveySubmitData,
|
||||
pageId,
|
||||
curStatus: getStatusObject({ status: SURVEY_STATUS.new }),
|
||||
createDate: Date.now()
|
||||
})
|
||||
return surveySubmitRes
|
||||
}
|
||||
}
|
||||
|
||||
export const surveySubmitService = new SurveySubmitService()
|
17
server/src/apps/surveyPublish/types/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export enum SURVEY_STATUS {
|
||||
new = "new",
|
||||
editing = "editing",
|
||||
pausing = "pausing",
|
||||
published = "published",
|
||||
removed = "removed",
|
||||
}
|
||||
|
||||
export class CommonError extends Error {
|
||||
code:number
|
||||
errmsg:string
|
||||
constructor(msg,code=500) {
|
||||
super(msg)
|
||||
this.code = code;
|
||||
this.errmsg = msg;
|
||||
}
|
||||
}
|
48
server/src/apps/surveyPublish/utils/checkSign.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {
|
||||
createHash
|
||||
} from 'crypto';
|
||||
import { CommonError } from '../types/index'
|
||||
|
||||
const hash256 = (text) => {
|
||||
return createHash('sha256').update(text).digest('hex')
|
||||
}
|
||||
const undefinedToString = (data) => {
|
||||
const res = {}
|
||||
for (const key in data) {
|
||||
if (data[key] === undefined) {
|
||||
res[key] = ''
|
||||
} else {
|
||||
res[key] = data[key]
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const getSignByData = (sourceData, ts) => {
|
||||
const data = undefinedToString(sourceData)
|
||||
const keysArr = Object.keys(data)
|
||||
keysArr.sort()
|
||||
let signArr = keysArr.map(key => {
|
||||
if (typeof data[key] === 'string') {
|
||||
return `${key}=${encodeURIComponent(data[key])}`
|
||||
}
|
||||
return `${key}=${JSON.stringify(data[key])}`
|
||||
})
|
||||
const sign = hash256(signArr.join('') + ts)
|
||||
return `${sign}`
|
||||
}
|
||||
|
||||
export const checkSign = (sourceData) =>{
|
||||
const sign = sourceData.sign
|
||||
if(!sign) {
|
||||
throw new CommonError('请求签名不存在')
|
||||
}
|
||||
delete sourceData.sign
|
||||
const [inSign, ts] = sign.split('.');
|
||||
const realSign = getSignByData(sourceData, ts)
|
||||
if(inSign!==realSign) {
|
||||
throw new CommonError('请求签名异常')
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
25
server/src/apps/surveyPublish/utils/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { SURVEY_STATUS } from '../types/index'
|
||||
import { CommonError } from '../types/index'
|
||||
import * as Joi from 'joi'
|
||||
|
||||
export function getMapByKey({data,key}:{data:Array<any>,key:string}) {
|
||||
const datamap = {}
|
||||
for(const item of data) {
|
||||
datamap[item[key]] = item
|
||||
}
|
||||
return datamap
|
||||
}
|
||||
|
||||
export function getStatusObject({status}:{status:SURVEY_STATUS}) {
|
||||
return {
|
||||
status,
|
||||
id: status,
|
||||
date: Date.now(),
|
||||
};
|
||||
}
|
||||
export function getValidateValue<T=any>(validationResult:Joi.ValidationResult<T>):T {
|
||||
if(validationResult.error) {
|
||||
throw new CommonError(validationResult.error.details.map(e=>e.message).join())
|
||||
}
|
||||
return validationResult.value;
|
||||
}
|
29
server/src/apps/ui/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { SurveyServer } from '../../decorator'
|
||||
import { Request, Response } from 'koa'
|
||||
import { createReadStream } from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
export default class UI {
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/render/(.*)' })
|
||||
async render({ req, res }: {req:Request, res:Response}) {
|
||||
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({ req, res }: {req:Request, res:Response}) {
|
||||
const filePath = path.join(__dirname, 'public', 'management.html');
|
||||
res.type = path.extname(filePath)
|
||||
return createReadStream(filePath)
|
||||
}
|
||||
@SurveyServer({ type: 'http', method: 'get', routerName: '/(.*)' })
|
||||
async index({ req, res }: {req:Request, res:Response}) {
|
||||
let reqPath = req.path;
|
||||
if (req.path === '/') {
|
||||
reqPath = '/management.html'
|
||||
}
|
||||
const filePath = path.join(__dirname, 'public', reqPath);
|
||||
res.type = path.extname(filePath)
|
||||
return createReadStream(filePath)
|
||||
}
|
||||
}
|
7
server/src/apps/ui/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {}
|
||||
}
|
BIN
server/src/apps/ui/public/favicon.ico
Normal file
After Width: | Height: | Size: 66 KiB |
12
server/src/apps/ui/public/management.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!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>
|
||||
</head>
|
||||
<body>
|
||||
<h2 id="hello">问卷管理端</h2>
|
||||
</body>
|
||||
</html>
|
12
server/src/apps/ui/public/render.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!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>
|
||||
</head>
|
||||
<body>
|
||||
<h2 id="hello">问卷投放端</h2>
|
||||
</body>
|
||||
</html>
|
14
server/src/apps/user/config/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
const config = {
|
||||
mongo:{
|
||||
url: process.env.xiaojuSurveyMongoUrl ||'mongodb://localhost:27017',
|
||||
dbName:'surveyEengine'
|
||||
},
|
||||
jwt:{
|
||||
secret: process.env.xiaojuSurveyJwtSecret || 'xiaojuSurveyJwtSecret',
|
||||
expiresIn: process.env.xiaojuSurveyJwtExpiresIn || '8h',
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
return config
|
||||
}
|
33
server/src/apps/user/db/mongo.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Collection, MongoClient, ObjectId } from 'mongodb'
|
||||
import { getConfig } from '../config/index'
|
||||
|
||||
const config = getConfig()
|
||||
|
||||
class mongoService {
|
||||
isInit:boolean
|
||||
client:MongoClient
|
||||
constructor() {
|
||||
this.client = new MongoClient(config.mongo.url);
|
||||
}
|
||||
async getCollection({collectionName}):Promise<Collection> {
|
||||
await this.client.connect()
|
||||
return this.client.db(config.mongo.dbName).collection(collectionName)
|
||||
}
|
||||
|
||||
convertId2StringByDoc(doc:any):any {
|
||||
doc._id = doc._id.toString()
|
||||
return doc;
|
||||
}
|
||||
|
||||
convertId2StringByList(list:Array<any>):Array<any> {
|
||||
return list.map(e=>{
|
||||
return this.convertId2StringByDoc(e);
|
||||
})
|
||||
}
|
||||
|
||||
getObjectIdByStr(str:string) {
|
||||
return new ObjectId(str)
|
||||
}
|
||||
}
|
||||
|
||||
export const mongo = new mongoService()
|
43
server/src/apps/user/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { SurveyServer } from '../../decorator'
|
||||
import { Request, Response } from 'koa'
|
||||
import * as Joi from 'joi'
|
||||
import { userService } from './service/userService'
|
||||
import { getValidateValue } from './utils/index'
|
||||
|
||||
|
||||
export default class User {
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/register' })
|
||||
async register({ req, res }: { req: Request, res: Response }) {
|
||||
const userInfo = getValidateValue(Joi.object({
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
const userRegisterRes = await userService.register(userInfo)
|
||||
return {
|
||||
code: 200,
|
||||
data: userRegisterRes,
|
||||
}
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'http', method: 'post', routerName: '/login' })
|
||||
async login({ req, res }: { req: Request, res: Response }) {
|
||||
const userInfo = getValidateValue(Joi.object({
|
||||
username: Joi.string().required(),
|
||||
password: Joi.string().required(),
|
||||
}).validate(req.body, { allowUnknown: true }));
|
||||
const data = await userService.login(userInfo)
|
||||
return {
|
||||
code: 200,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
@SurveyServer({ type: 'rpc' })
|
||||
async getUserByToken({ params, context }: { params: any, context: any }) {
|
||||
const data = await userService.getUserByToken({ token: params.token })
|
||||
return {
|
||||
result: data,
|
||||
context, // 上下文主要是传递调用方信息使用,比如traceid
|
||||
}
|
||||
}
|
||||
}
|
13
server/src/apps/user/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "user",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"joi": "^17.9.2",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mongodb": "^5.7.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
81
server/src/apps/user/service/userService.ts
Normal file
@ -0,0 +1,81 @@
|
||||
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()
|
30
server/src/apps/user/types/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
export enum SURVEY_STATUS {
|
||||
new = "new",
|
||||
editing = "editing",
|
||||
pausing = "pausing",
|
||||
published = "published",
|
||||
removed = "removed",
|
||||
}
|
||||
|
||||
export enum QUESTION_TYPE {
|
||||
enps = "enps",
|
||||
nps = "nps",
|
||||
question = "question", //通用问卷
|
||||
register = "register", //报名
|
||||
vote = "vote", //投票
|
||||
}
|
||||
|
||||
export enum HISTORY_TYPE {
|
||||
dailyHis = "dailyHis", //保存历史
|
||||
publishHis = "publishHis", //发布历史
|
||||
}
|
||||
|
||||
export class CommonError extends Error {
|
||||
code:number
|
||||
errmsg:string
|
||||
constructor(msg,code=500) {
|
||||
super(msg)
|
||||
this.code = code;
|
||||
this.errmsg = msg;
|
||||
}
|
||||
}
|
16
server/src/apps/user/utils/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { SURVEY_STATUS } from '../types/index'
|
||||
import { 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=any>(validationResult:Joi.ValidationResult<T>):T {
|
||||
if(validationResult.error) {
|
||||
throw new CommonError(validationResult.error.details.map(e=>e.message).join())
|
||||
}
|
||||
return validationResult.value;
|
||||
}
|
19
server/src/decorator.ts
Normal file
@ -0,0 +1,19 @@
|
||||
type ServerType = 'http' | 'websocket' | 'rpc'
|
||||
export interface ServerValue {
|
||||
type: ServerType,
|
||||
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options';
|
||||
routerName: string;
|
||||
}
|
||||
|
||||
export const SurveyServerConfigKey = Symbol('appConfig') // vm环境和worker环境上下文不一致导致不能使用Symbol
|
||||
export function SurveyServer(surveyServer) {
|
||||
return function (target: any, propertyKey: string, _descriptor: PropertyDescriptor) {
|
||||
if (!target[SurveyServerConfigKey]) {
|
||||
target[SurveyServerConfigKey] = new Map<string, ServerValue>()
|
||||
}
|
||||
target[SurveyServerConfigKey].set(
|
||||
propertyKey,
|
||||
surveyServer
|
||||
)
|
||||
}
|
||||
}
|
21
server/src/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as os from 'os'
|
||||
import * as Koa from 'koa';
|
||||
import * as KoaBodyparser from 'koa-bodyparser'
|
||||
import * as logger from 'koa-pino-logger'
|
||||
import { getRouter } from './router'
|
||||
import { outputCatch } from './middleware/outputCatch'
|
||||
|
||||
const app = new Koa();
|
||||
app.use(outputCatch({ showErrorStack: true }))
|
||||
app.use(logger())
|
||||
app.use(KoaBodyparser({
|
||||
formLimit: '30mb',
|
||||
jsonLimit: '30mb',
|
||||
textLimit: '30mb',
|
||||
xmlLimit: '30mb',
|
||||
}))
|
||||
|
||||
app.use(getRouter().routes())
|
||||
const port = 8080
|
||||
app.listen(port)
|
||||
process.stdout.write(`${os.EOL}server run: http://127.0.0.1:${port} ${os.EOL}`)
|
13
server/src/middleware/outputCatch.ts
Normal file
@ -0,0 +1,13 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
39
server/src/router.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import * as Router from 'koa-router';
|
||||
import { Context } from 'koa'
|
||||
import { SurveyServerConfigKey } from './decorator';
|
||||
|
||||
import Ui from './apps/ui/index'
|
||||
import SurveyManage from './apps/surveyManage/index'
|
||||
import SurveyPublish from './apps/surveyPublish/index'
|
||||
import User from './apps/user/index'
|
||||
|
||||
|
||||
function loadAppRouter(app, appRouter) {
|
||||
const appServerConfigMap = app[SurveyServerConfigKey]
|
||||
for (const [serveName, serveValue] of appServerConfigMap) {
|
||||
const middleware = async (ctx: Context) => {
|
||||
const data = await app[serveName]({ req: ctx.request, res: ctx.response })
|
||||
return ctx.body = data
|
||||
}
|
||||
const method = serveValue.method || 'all'
|
||||
const routerName = serveValue.routerName || `/${serveName}`
|
||||
appRouter[method](routerName, middleware)
|
||||
}
|
||||
return appRouter
|
||||
}
|
||||
|
||||
export function getRouter() {
|
||||
const rootRouter = new Router()
|
||||
const apiAppMap = {
|
||||
surveyManage: new SurveyManage(),
|
||||
surveyPublish: new SurveyPublish(),
|
||||
user: new User(),
|
||||
}
|
||||
for (const [apiAppName, apiApp] of Object.entries(apiAppMap)) {
|
||||
const appRouter = new Router()
|
||||
loadAppRouter(apiApp, appRouter)
|
||||
rootRouter.use(`/api/${apiAppName}`, appRouter.routes(), rootRouter.allowedMethods())
|
||||
}
|
||||
loadAppRouter(new Ui(), rootRouter)
|
||||
return rootRouter
|
||||
}
|
11
server/src/rpc.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export async function rpcInvote<P, R>(appServerName: string, params: P): Promise<R> {
|
||||
const appServerNameData = /^(\w+)\.(\w+)$/.exec(appServerName);
|
||||
if (!appServerNameData) {
|
||||
throw new Error('rpc调用必须按照app.function名方式填写,app和function名称只支持数字字母下划线')
|
||||
}
|
||||
const appName = appServerNameData[1];
|
||||
const serverName = appServerNameData[2];
|
||||
const App = require(`./apps/${appName}/index`).default
|
||||
const instance = new App()
|
||||
return await instance[serverName](params)
|
||||
}
|
16
server/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target":"ES2020",
|
||||
"rootDir": "./src/",
|
||||
"outDir": "./build/",
|
||||
"declaration": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"incremental":true,
|
||||
"tsBuildInfoFile": "./build/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
3
web/.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
20
web/.eslintrc.js
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
root: false,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
'eslint:recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
semi: ['error', 'always'],
|
||||
},
|
||||
};
|
27
web/.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
/output
|
||||
|
||||
# local env files
|
||||
# .env.*.local 提交 .env.development.local,使得用户能够在本地修改
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
package-lock.json
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.history
|
5
web/.prettierrc.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
singleQuote: true, // 使用单引号
|
||||
semi: true, // 不使用分号
|
||||
// trailingComma: 'all', // 在对象和数组末尾加上逗号
|
||||
};
|
44
web/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
## 环境
|
||||
node版本推荐>=v16.15.0
|
||||
|
||||
## 项目启动
|
||||
1、安装依赖
|
||||
```
|
||||
npm install
|
||||
```
|
||||
2、启动
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
3、访问问卷管理端
|
||||
```bash
|
||||
http://localhost:8080/management
|
||||
```
|
||||
会自动重定向到问卷列表页,也就是问卷管理端系统的首页
|
||||
```bash
|
||||
http://localhost:8080/management/survey
|
||||
```
|
||||
问卷管理端所有的页面都需要登陆,需要自行注册账号
|
||||
|
||||
4、访问问卷投放端
|
||||
创建一份问卷并且编辑好之后,需要点击发布,发布后会跳转到问卷投放页面
|
||||
投放页面能看到问卷的问卷投放端的链接,点击打开即可访问
|
||||
或者如果你知道问卷配置的surveyPath字段,也可以通过下面的路径直接访问某张问卷
|
||||
```bash
|
||||
http://localhost:8080/render/:surveyPath
|
||||
```
|
||||
|
||||
5、编译
|
||||
执行下面的命令即可编译项目
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
编译结果会产出两个html:management.html和render.html
|
||||
|
||||
6、部署和访问
|
||||
前端的部署和访问依赖服务端
|
||||
需要先将整个dist文件夹里面的内容移动到后端的静态文件夹下面
|
||||
需要后端做一层代理:
|
||||
当访问路径由/management开头的时候,读取management.html的内容返回
|
||||
当访问路径由/render开头的时候,读取render.html的内容返回
|
||||
此功能已经实现,做一个简单了解即可
|
11
web/babel.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
[
|
||||
'@vue/babel-preset-jsx',
|
||||
{
|
||||
injectH: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
19
web/jsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"baseUrl": "./",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
56
web/package.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"lintfix": "eslint --fix ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0",
|
||||
"@vue/babel-preset-jsx": "^1.4.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "1.0.2",
|
||||
"axios": "^1.4.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"core-js": "^3.8.3",
|
||||
"crypto-js": "^4.1.1",
|
||||
"element-ui": "^2.15.13",
|
||||
"moment": "^2.29.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"vue": "^2.7.14",
|
||||
"vue-demi": "^0.14.5",
|
||||
"vue-router": "^3.5.1",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2",
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.16",
|
||||
"@babel/eslint-parser": "^7.12.16",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^8.7.1",
|
||||
"less-loader": "^11.1.3",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^2.4.1",
|
||||
"sass": "^1.32.7",
|
||||
"sass-loader": "^12.0.0",
|
||||
"style-resources-loader": "^1.5.0",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.1",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
}
|
BIN
web/public/imgs/Logo.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
web/public/imgs/avatar.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
web/public/imgs/create/background.jpg
Normal file
After Width: | Height: | Size: 227 KiB |
BIN
web/public/imgs/create/normal-icon.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
web/public/imgs/create/nps-icon.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
web/public/imgs/create/register-icon.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
web/public/imgs/create/vote-icon.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
web/public/imgs/favicon.ico
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
web/public/imgs/icons/analysis-empty.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
web/public/imgs/icons/error.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
web/public/imgs/icons/list-empty.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
web/public/imgs/icons/overtime.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
web/public/imgs/icons/success.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
web/public/imgs/icons/unpublished.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
web/public/imgs/icons/unselected.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
web/public/imgs/phone-bg.png
Normal file
After Width: | Height: | Size: 188 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 100 KiB |
After Width: | Height: | Size: 66 KiB |
BIN
web/public/imgs/s-logo.jpg
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
web/public/imgs/skin/0k7Jg7In8I1558430221154.jpg
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
web/public/imgs/skin/145gBCRtNP1558514861211.jpg
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
web/public/imgs/skin/17e06b7604a007e1d3e1453b9ddadc3c.jpg
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
web/public/imgs/skin/3ABKqvDaVn1558514860472.jpg
Normal file
After Width: | Height: | Size: 96 KiB |