Compare commits

..

94 Commits

Author SHA1 Message Date
luch1994
ac0216cb64 fix: 解决lint问题 2024-09-27 19:47:55 +08:00
luch1994
bcb41c68fb feat: merge develop 2024-09-27 19:27:15 +08:00
luch1994
ef2126f1ca fix: 修改刷数据逻辑错误和环境变量问题 2024-09-27 19:10:04 +08:00
luch
1125e2ae39
feat: 修改状态相关功能 (#433)
Co-authored-by: luchunhui <luchunhui@didiglobal.com>
2024-09-27 18:26:36 +08:00
sudoooooo
054095e499 feat: 优化内容 2024-09-27 18:24:54 +08:00
Jiangchunfu
81df0eae05
feat: 结果页新增跳转设置 (#432)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-09-27 18:14:32 +08:00
sudoooooo
0a27554bb6 feat: 升级README 2024-09-27 17:21:50 +08:00
sudoooooo
5dfd3d941f feat: Create CODE_OF_CONDUCT.md 2024-09-27 15:16:55 +08:00
sudoooooo
60f96386a9 fix: 修复文档链接 2024-09-24 20:55:41 +08:00
sudoooooo
7f832ce885 fix: 优化配置内容 2024-09-24 15:48:39 +08:00
Jiangchunfu
47ea148866
feat: 皮肤设置2.0 (#426)
* fix: 修复windows上传文件路径反斜杠问题

* feat: 补全服务端skinConf定义

* feat: web端皮肤背景设置2.0

* feat: 删除console.log

* feat: 皮肤设置内容结果增加背景设置以及应用皮肤设置方法抽离

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
2024-09-24 14:09:37 +08:00
sudoooooo
129a803e9a fix: 优化类型校验 2024-09-24 12:13:44 +08:00
sudoooooo
bc0597efe0 fix: 修改api 2024-09-23 15:20:13 +08:00
sudoooooo
365bea25e1 fix: 修复投票题问题 2024-09-23 14:25:09 +08:00
dayou
3d5f04b4a8
fix: 修复最少最多选择 (#412)
* fix: 修复最少最多选择

* fix: 优化获取协作下拉框在协作弹窗出现时调用

* fix: 回退最多最少选择

* perl: 代码优化以及fix lint

---------

Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
2024-09-23 12:33:30 +08:00
Jiangchunfu
f3ebc11b3f
fix: 修复做题页面提交reload页面 (#419)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
2024-09-23 12:29:50 +08:00
sudoooooo
d5669352ed fix: 优化schema初始化管理 2024-09-23 11:47:11 +08:00
sudoooooo
0b686ab4ac fix: 优化代码内容 2024-09-22 12:38:42 +08:00
luch
54a86e1501
feat: 修改导出和登录状态检测代码CR问题 (#431) 2024-09-22 01:42:27 +08:00
sudoooooo
cdb8b6532a fix: 修复写法问题、C端组件问题 2024-09-22 01:41:32 +08:00
dayou
56c37fce3c
fix: 断点续答and编辑检测代码cr优化 (#428)
* fix: 优化代码以及去掉无用字段

* fix: 断点续答验收问题优化

* fix: 优化字段

* fix: lint

* fix: 编辑检测相关问题优化

* fix: lint

* fix: 第二批cr优化

* fix: 去掉无用代码

* fix: 调整session守卫位置

* fix: 文件大小写
2024-09-20 13:26:59 +08:00
sudoooooo
43b20b1be6 fix: 移除有问题的功能 2024-09-14 13:50:51 +08:00
dayou
b749cfa6f6
【Feature】:北大实践课作业 (#424)
* 【北大开源实践】增加数据导出功能 (#294)

* feat:添加了一个文件数据导出的功能和相应前端页面

* fix lint

* fix conflict

---------

Co-authored-by: dayou <853094838@qq.com>

* fix: components.d.ts文件ignore

* feat: Update README_EN.md

* feat: Update README.md

* feat:新增预览功能 (#257)

* feat:问卷预览功能
* feat:修复样式问题

* fix: 优化预览展示

* refactor: 重构vue3组合式API写法 (#265)

* feat: 抽离题型枚举 (#272)

* feat: 抽离题型枚举

* fix: 投放的链接加时间戳去掉ifream缓存

* feat: serve端的node engines

* feat: 权限接口请求优化以及修复其他问题 (#290)

* feat: c端路由改造 (#296)

* 【北大开源实践】增加数据导出功能 (#294)

* feat:添加了一个文件数据导出的功能和相应前端页面

* fix lint

* fix conflict

---------

Co-authored-by: dayou <853094838@qq.com>

* fix: 删除components.d.ts文件

* 【北大开源实践】- 问卷断点续答 - 前端 (#282)

* feat:增加断点续答功能

* feat:增加断点续答功能

* fix: 同步代码并且解决冲突

---------

Co-authored-by: dayou <853094838@qq.com>

* fix: 删除components.d.ts文件最终

* 【北大开源实践】-选项限制 (#284)

* format: 代码格式化 (#160)

* feat: 选项限制

* fix: 同步代码并解决冲突

* fix conflict

* fix conflict

* fix lint

* fix server lint

---------

Co-authored-by: dayou <853094838@qq.com>
Co-authored-by: XiaoYuan <2521510174@qq.com>

* feat: 登录失效检测 & 协作冲突检测 (#287)

Co-authored-by: Liuxinyi <liuxy0406@163.com>
Co-authored-by: dayou <853094838@qq.com>

* fix: peking分支同步develop并解决冲突

* fix: 修正颜色不统一 (#338)

* fix: 修正颜色不统一

* fix: 删除server下的lock文件

* 编辑冲突检测 (#351)

* perl: 选项配额优化

* fix: pinia改写

* feat: 完善北大课程相关的内容

* fix: 修复断点续答以及样式问题 (#420)

* feat: 修改readme

* [Feature]: 密码复杂度检测 (#407)

* feat: 密码复杂度检测

* chore: 改为服务端校验

* feat: 优化展示

* fix:修复编辑页在不同element版本下表现不一致问题 (#406)

* fix: 通过声明element最低版本来确定tab样式表现

* fix lint

* feat(选项设置扩展):选择类题型增加选项排列配置 (#403)

* build: add optimizeDeps packages

* feat(选项设置扩展):选择类题型增加选项排列配置

* feat(选项设置扩展): 验收问题修复

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>

* fix: 删除多余内容

* feat: 优化登录窗口

* fix: 修复断点续答以及样式问题

fix: 修复选项引用验收bug

fix: 修复断点续答问题

fix: 修复断点续答

fix: ignore

fix: 修复投票题默认值

fix: 优化断点续答逻辑

fix: 选中图标适应高度

fix: 回退最大最小选择

fix: 修复断点续答

fix: 修复elswitch不更新问题

fix: 修复访问密码更新不生效问题

fix: 修复样式

fix: 修复多选题最大最小限制

fix: 优化断点续答问题

修复多选题命中最多选择后无法取消问题

fix: 修复服务端的富文本解析

fix:  lint

fix: min error

fix: 修复最少最多选择

fix: 修复投票问卷的最少最多选择

fix: 兼容断点续答情况下选项配额为0的情况

fix: 兼容断点续答情况下选项配额为0的情况

fix: 兼容单选题的断点续答下的选项配额

fix: 修复添加选项问题

fix: 前端提示服务的配额已满

fix: 更新填写的过程中配额减少情况

---------

Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
Co-authored-by: Stahsf <30379566+50431040@users.noreply.github.com>
Co-authored-by: Jiangchunfu <mrj_kevin@163.com>
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>

* feat: 修改验收问题 (#421)

* fix lint

---------

Co-authored-by: Oseast <162945153+Oseast@users.noreply.github.com>
Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
Co-authored-by: chaorenluo <1243357953@qq.com>
Co-authored-by: Realabiha <48506355+Realabiha@users.noreply.github.com>
Co-authored-by: shiyiting763 <70299297+shiyiting763@users.noreply.github.com>
Co-authored-by: yiyeah <68832436+yiyeah@users.noreply.github.com>
Co-authored-by: XiaoYuan <2521510174@qq.com>
Co-authored-by: Xinyi Liu <74805961+colmon46@users.noreply.github.com>
Co-authored-by: Liuxinyi <liuxy0406@163.com>
Co-authored-by: nil <wangweiguo2013@icloud.com>
Co-authored-by: 王晓聪 <wang86976110@126.com>
Co-authored-by: taoshuang <taoshuang@didiglobal.com>
Co-authored-by: luch1994 <1097650398@qq.com>
Co-authored-by: Stahsf <30379566+50431040@users.noreply.github.com>
Co-authored-by: Jiangchunfu <mrj_kevin@163.com>
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
Co-authored-by: luch <32321690+luch1994@users.noreply.github.com>
2024-09-12 22:10:18 +08:00
chaorenluo
0b4e1fa13b
feat:新增暂停功能 (#416)
* feat:新增暂停功能
2024-09-11 16:19:55 +08:00
Jiangchunfu
43001a12c7
fix: 修复富文本编辑器上传图片 (#410)
* fix: 修复题目标题插入图片异常问题

* fix: 修改事件监听顺序,避免编辑图片百分比时重新渲染工具条而找不到点击的dom

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-09-06 15:51:12 +08:00
Stahsf
d3c2180ac8
test: 密码检测用例 (#411)
* feat: 密码复杂度检测

* chore: 改为服务端校验

* test: 修改用例

* test: 添加getPasswordStrength测试用例
2024-09-06 15:37:03 +08:00
sudoooooo
63e16e1694 feat: 优化登录窗口 2024-09-03 17:55:59 +08:00
sudoooooo
c6cc0d22e5 fix: 删除多余内容 2024-09-03 16:47:33 +08:00
Jiangchunfu
949a989dcf
feat(选项设置扩展):选择类题型增加选项排列配置 (#403)
* build: add optimizeDeps packages

* feat(选项设置扩展):选择类题型增加选项排列配置

* feat(选项设置扩展): 验收问题修复

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-09-03 15:54:42 +08:00
dayou
9427e0efe5
fix:修复编辑页在不同element版本下表现不一致问题 (#406)
* fix: 通过声明element最低版本来确定tab样式表现

* fix lint
2024-09-02 17:39:47 +08:00
sudoooooo
70c236c879 feat: 优化展示 2024-09-02 17:36:18 +08:00
Stahsf
3d31245ae5
[Feature]: 密码复杂度检测 (#407)
* feat: 密码复杂度检测

* chore: 改为服务端校验
2024-09-02 16:58:53 +08:00
sudoooooo
98fc21995a feat: 修改readme 2024-08-30 12:01:39 +08:00
sudoooooo
f6e3778a2d feat: 优化换行 2024-08-14 21:32:06 +08:00
dayou
6775a9df5e
fix: 删除分页判断是否存在题目的逻辑关联 (#402) 2024-08-14 21:30:16 +08:00
Liang-Yaxin
3cb843e493
fix: 问卷列表更多按钮图标优化 (#401) 2024-08-14 21:12:43 +08:00
dayou
bc3ce31c9e
feat: 跳转逻辑稳定版 (#399)
* feat: 跳转逻辑 (#388)

* fix: 跳转逻辑优化 (#397)

* fix: 跳转逻辑优化

* fix: processJumpSkip逻辑放在题目组件中
2024-08-14 17:59:51 +08:00
sudoooooo
3e7f0cac90 fix: 修复高级设置迁移后无法交互问题 2024-08-14 17:28:59 +08:00
dayou
f3b8ab278a
fix: 更新iconfont链接 (#398) 2024-08-14 10:10:29 +08:00
sudoooooo
013f9ac811 feat: 高级设置组件迁移 2024-08-13 23:44:19 +08:00
sudoooooo
9e07e8330a feat: 升级iconfont 2024-08-13 11:46:59 +08:00
sudoooooo
8950073141 fix: 新建问卷重置计数 2024-08-12 23:10:21 +08:00
Jiangchunfu
4d580bb789
feat: 整卷增加基础配置:必填、显示类型、显示序号、显示分割线 (#391)
* feat: 增加整卷配置功能

* fix: 限制单题修改配置时只对基础配置进行更新全局基础配置操作

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-12 21:39:19 +08:00
Jiangchunfu
f73bfb0ab3
feat: C端增加重新填写入口, #182 (#392)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-12 19:43:55 +08:00
sudoooooo
8109d350e4 feat: 优化readme 2024-08-12 19:42:01 +08:00
sudoooooo
b233023bb3 fix: 连续添加题目频繁触发事件导致页面卡顿 2024-08-12 16:08:57 +08:00
sudoooooo
fd7cc2ea96 feat: action不处理format 2024-08-12 14:09:29 +08:00
Jiangchunfu
724535a735
refactor: 题目删除和逻辑关联优化,#182 (#393)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-12 14:06:38 +08:00
sudoooooo
b5c7ec3008 fix: 皮肤设置tab问题 2024-08-07 22:09:55 +08:00
sudoooooo
c5698ad631 fix: 修复白名单切换和空间成员名字问题 2024-08-07 22:07:00 +08:00
chaorenluo
6fb337633c
fix:修复C端白名单验证弹框不出现的问题 (#389) 2024-08-07 18:33:23 +08:00
Jiangchunfu
42b8d74ead
refactor: 设置器加载统一,代码优化 #269 (#383)
* feat: 小功能建设(4)

* refactor: 设置器加载统一,代码优化 #269

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-07 14:53:14 +08:00
sudoooooo
d8e76dc2e6 feat: 修改Readme 2024-08-06 23:46:21 +08:00
sudoooooo
4f2cd4ca47 feat: 修改字段 2024-08-06 19:59:35 +08:00
sudoooooo
82c98ec1f5 feat: 优化分页器结构 2024-08-06 19:33:11 +08:00
chaorenluo
fbc654f21b
feat:新增分页功能 (#382)
* feat:新增分页功能

* 修复type-check检查

* fix: server  type-check

* fix:修改服务端测试用例

* fix:修复分页bug
2024-08-06 17:30:12 +08:00
Jiangchunfu
9d89a1ceca fix: 修复题目未聚焦时拖拽按钮失效问题 (#375)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-08-04 12:41:42 +08:00
Jiangchunfu
517906f77f
feat: 小功能建设(4) (#379) 2024-08-04 12:38:28 +08:00
sudoooooo
681e8fa3ae fix: 修复字段difTime->diffTime 2024-08-04 12:21:08 +08:00
Jiangchunfu
4d5c3eb15d
小功能优化8 (#371)
* refactor: 去除重复元素

* feat: edit 布局优化

* style: 题目大纲滚动时固定tab-header

---------

Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-07-31 17:21:54 +08:00
离谱
9a35de7e36
fix(Navbar):修复了短标题hover空白处会触发指示框的bug (#365)
fix(Navbar):修复了短标题hover空白处会触发指示框的bug
2024-07-30 15:27:49 +08:00
dayou
c4b730c8af
fix: 修复提交设置 (#370) 2024-07-30 14:51:33 +08:00
Jiangchunfu
8ea8869ca7 fix: 修复新增同类型题目设置值不变问题 (#359)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-07-23 15:43:10 +08:00
dayou
7e5a8ae5c1
fix: 修复设置器不更新问题 (#361) 2024-07-23 15:39:43 +08:00
sudoooooo
cf495b60d1 Merge branch 'feature/pinia' into develop
# Conflicts:
#	web/src/management/components/LeftMenu.vue
#	web/src/management/pages/edit/components/ModuleNavbar.vue
#	web/src/management/pages/edit/modules/contentModule/PublishPanel.vue
#	web/src/management/pages/edit/modules/contentModule/SavePanel.vue
#	web/src/management/pages/edit/modules/resultModule/CatalogPanel.vue
#	web/src/management/pages/edit/modules/settingModule/SettingPanel.vue
#	web/src/management/pages/list/components/BaseList.vue
#	web/src/management/pages/list/components/SpaceList.vue
#	web/src/management/pages/list/components/SpaceModify.vue
#	web/src/management/pages/list/index.vue
#	web/src/management/router/index.ts
#	web/src/management/store/edit/getters.js
#	web/src/management/store/list/index.js
#	web/src/management/utils/index.js
#	web/src/materials/questions/widgets/BinaryChoiceModule/meta.js
#	web/src/materials/questions/widgets/CheckboxModule/meta.js
#	web/src/materials/questions/widgets/RadioModule/meta.js
#	web/src/materials/questions/widgets/VoteModule/meta.js
#	web/src/render/pages/RenderPage.vue
#	web/src/render/store/actions.js
#	web/src/render/store/mutations.js
#	web/src/render/store/state.js
2024-07-23 14:14:52 +08:00
sudoooooo
fb57eaaba7 feat: 优化结构 2024-07-22 20:37:11 +08:00
sudoooooo
b494bd6174 feat: 优化编辑页结构 2024-07-22 20:27:12 +08:00
sudoooooo
310fe0d325 feat: 优化编辑页模块和配置结构 2024-07-22 17:33:59 +08:00
Stahsf
ba418c5cd7
feat: 白名单功能-服务端 (#357) 2024-07-20 14:11:19 +08:00
chaorenluo
9596cd07a1
前端新增白名单功能 (#356) 2024-07-19 22:45:40 +08:00
sudoooooo
2ed5b64b18 feat: 优化图片尺寸用于移动端 2024-07-18 21:07:25 +08:00
nil
492e0055f0
feat: 题目与选项支持图片 (#291)
* feat: 题目与选项支持图片

* fix: 修复使用本地存储时文件访问路径不正确的问题

* fix: 图片编辑表单无法输入

* chore: 添加上传文件夹到gitignore

* fix: 两个#app的问题
2024-07-18 10:21:09 +08:00
sudoooooo
93938702fe feat: 更新readme和docker tag 2024-07-17 23:12:17 +08:00
sudoooooo
e8907ca4fb fix: format报错 2024-07-16 11:33:16 +08:00
Jiangchunfu
2b32850046
feat: 题型硬编码优化 (#343)
Co-authored-by: jiangchunfu <jiangchunfu@kaike.la>
2024-07-16 11:25:08 +08:00
sudoooooo
da1749fb53 feat: 代码format、升级ts和lint 2024-07-15 22:38:14 +08:00
Ken
5bc5eb8719
feat: 空间列表分页和搜索及数据列表优化 (#344)
1、fix: 修复因滚动条宽度影响皮肤标签的问题
2、feat: 空间列表分页和搜索及数据列表样式优化
3、feat: 对部分代码进行了优化
2024-07-15 18:30:50 +08:00
sudoooooo
3227a799f9 fix: 移除选项默认选中 2024-07-15 18:00:07 +08:00
Jiangchunfu
8740685a4d
feat: 功能18优化 (#342)
题目标题编辑自动focus
2024-07-15 17:50:00 +08:00
sudoooooo
5c3915a74d fix: 空间列表高度 2024-07-15 12:11:57 +08:00
ysansan
b1958ec8ff
问卷编辑页面题型选择tab、团队空间列表按钮优化 (#337)
* feat:问卷编辑页面题型选择tab优化

* feat: 团队空间列表按钮优化
2024-07-15 11:24:51 +08:00
sudoooooo
bc39e9933d fix: 避免element-plus提示sass语法 2024-07-15 11:21:35 +08:00
sudoooooo
36dd5a4f2d feat: 增加csdn和x 2024-07-12 16:49:40 +08:00
sudoooooo
a101878313 feat: 优化协作管理路径 2024-07-10 18:36:54 +08:00
Ken
32e43b8260
feat: 协作者管理优化和皮肤标签样式调整 (#333)
* feat: 皮肤设置内主题分类标签样式调整

* feat: 协作者管理优化
2024-07-10 18:06:39 +08:00
Jiangchunfu
9afb23c08e
feat(小功能建设): 15团队空间问卷列表页优化 (#334) 2024-07-10 16:20:31 +08:00
sudoooooo
eaa1abda82 fix: sass1.77.7规则升级warning 2024-07-10 16:07:29 +08:00
hiStephen
6431cc3210
feat: echarts按需引入 (#332) 2024-07-10 15:38:08 +08:00
sudoooooo
122f584cad fix: 修复预览页logo展示问题 2024-07-09 15:23:02 +08:00
Jiangchunfu
f45cf7982f
小功能优化 (#329)
* feat(换肤设置优化): 边距的颜色优化成背景色一致

* feat: 问卷设置优化
2024-07-09 14:17:44 +08:00
Ken
2f0736fd95
feat: 移动端预览优化 (#326)
* feat: 移动端预览优化

* feat: C端底部logo优化
2024-07-09 12:12:13 +08:00
sudoooooo
6c72344204 fix: 修复nginx启动render页空白问题 2024-07-08 21:06:30 +08:00
sudoooooo
61fd6e09af fix: 优化依赖项 2024-07-01 14:14:07 +08:00
若川
349b4dad8c
fix: 🐛 修复引入 import lodash cloneDeep debounce 错误 改为 lodash-es 了 (#322) 2024-07-01 14:08:57 +08:00
60 changed files with 927 additions and 1497 deletions

View File

@ -1,5 +1,5 @@
# 镜像集成 # 镜像集成
FROM node:18-slim FROM node:18
# 设置工作区间 # 设置工作区间
WORKDIR /xiaoju-survey WORKDIR /xiaoju-survey

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright (C) 2023 Beijing Didi Infinity Technology and Development Co.,Ltd. All rights reserved. Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -35,27 +35,23 @@
# 功能特性 # 功能特性
**🌈 用** **🌈 用**
- 多类型数据采集,轻松创建调研表单:文本输入、数据选择、评分、投票、文件上传等 - 多类型数据采集,轻松创建调研表单。
- 智能逻辑编排,设计多规则动态表单:显示逻辑、跳转逻辑、选项引用、题目引用等 - 智能逻辑编排,设计多规则动态表单。
- 精细权限管理,支持高效团队协同:空间管理、多角色权限管理等。 - 数据在线分析和导出,洞察调研结果。
- 数据在线分析和导出,洞察调研结果:数据导出、回收数据管理、分题统计、交叉分析等。
**🎨 好看** **🎨 好看**
- 主题自由定制,适配您的品牌自定义颜色、背景、图片、Logo、结果页规则等 - 主题自由定制,适配您的品牌。
- 无缝嵌入各终端,满足不同场景需求:多端嵌入式小问卷 SDK - 无缝嵌入各终端,满足不同场景需求。
**🚀 安全、可扩展** **🚀 扩展**
- 安全能力可扩展,提供安全相关建设的经验指导:传输加密、敏感词库、发布审查等。 - 自定义 Hook 配置,轻松集成多方系统与各类工具。
- 自定义 Hook 配置,轻松集成多方系统与各类工具:数据推送集成、消息推送集成等。
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" /> <img src="https://github.com/didi/xiaoju-survey/assets/16012672/dd427471-368d-49d9-bc44-13c34d84e3be" width="700" />
@ -144,7 +140,7 @@ _在线平台建设中_
_手册编写中_ _手册编写中_
<br /> <br /><br />
## Star ## Star
@ -157,7 +153,6 @@ _手册编写中_
官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入。 官方群会发布项目最新消息、建设计划和社区活动,欢迎你的加入。
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" /> <img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
_任何问题和合作可以联系小助手。_ _任何问题和合作可以联系小助手。_
## 案例 ## 案例
@ -174,4 +169,4 @@ _任何问题和合作可以联系小助手。_
## CHANGELOG ## CHANGELOG
关注项目重大变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)。 关注重大项目变更:[MAJOR CHANGELOG](https://github.com/didi/xiaoju-survey/issues/48)。

View File

@ -33,43 +33,27 @@
&ensp;&ensp;The internal system has accumulated over 40 question types and more than 100 selected templates, suitable for market research, customer satisfaction surveys, online exams, voting, reporting, evaluations, and many other scenarios. In terms of data capabilities, it has been honed through hundreds of millions of iterations, resulting in the ability to provide online reports with per-question statistics, cross-analysis, and multi-channel analysis, quickly meeting professional analysis needs. &ensp;&ensp;The internal system has accumulated over 40 question types and more than 100 selected templates, suitable for market research, customer satisfaction surveys, online exams, voting, reporting, evaluations, and many other scenarios. In terms of data capabilities, it has been honed through hundreds of millions of iterations, resulting in the ability to provide online reports with per-question statistics, cross-analysis, and multi-channel analysis, quickly meeting professional analysis needs.
# Features # Function Overview
**🌈 Easy to use** - Questionnaire Management: Create, edit, distribute, collect, data analysis.
- Multi-type data collection, easy to create forms: text input, data selection, scoring, voting, file upload, etc. - Diverse Question Types: Single-line input, multi-line input, single choice, multiple choice, true/false, rating, voting, etc.
- Smart logic arrangement, design multi-rule dynamic forms: display logic, jump logic, option reference, title reference, etc. - User Management: Login, registration, permissions management.
- Multiple permission management, support efficient team collaboration: space management, multi-role permission management, etc. - Data Security: Encrypted transmission, data masking, etc.
- Online data analysis and export, insight into survey results: data export, recycled data management, sub-topic statistics, cross-analysis, etc. > For more comprehensive features, please refer to the [documentation](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D/%E5%9F%BA%E7%A1%80%E6%B5%81%E7%A8%8B).
**🎨 Good-looking**
- Free customization of themes to adapt to your brand: custom colors, backgrounds, pictures, logos, result page rules, etc.
- Seamlessly embedded in various terminals to meet the needs of different scenarios: multi-terminal embedded small questionnaire SDK.
**🚀 Secure and scalable**
- Scalable security capabilities, providing experience guidance for security-related construction: encrypted transmission, data masking, etc.
- Customized Hook configuration, easy integration of multiple systems and various tools: data push, message push, etc.
<img src="https://github.com/didi/xiaoju-survey/assets/16012672/508ce30f-0ae8-4f5f-84a7-e96de8238a7f" width="700" /> <img src="https://github.com/didi/xiaoju-survey/assets/16012672/508ce30f-0ae8-4f5f-84a7-e96de8238a7f" width="700" />
1. For more comprehensive features, please refer to the [documentation](https://xiaojusurvey.didi.cn/docs/next/document/%E4%BA%A7%E5%93%81%E6%89%8B%E5%86%8C/%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D/%E5%9F%BA%E7%A1%80%E6%B5%81%E7%A8%8B). _**(Both individual and enterprise users can quickly build survey solutions specific to their fields.)**_
2. Both individual and enterprise users can quickly build survey solutions specific to their fields.
# Technology # Technology
Web: Vue3 + ElementPlus; Multi-end rendering for C-end (planning). Web: Vue3 + ElementPlus; Multi-end rendering for C-end (planning).
Server: NestJS + MongoDB; Java ([under construction](https://github.com/didi/xiaoju-survey/issues/306)). Server: Nestjs + MongoDB; Java ([under construction](https://github.com/didi/xiaoju-survey/issues/306)).
Online Platform: (under construction).
Intelligent Foundation: (planning). Intelligent Foundation: (planning).
@ -161,7 +145,7 @@ npm run local
#### 1.Configure Database #### 1.Configure Database
> The project uses MongoDB: [MongoDB Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93#%E5%AE%89%E8%A3%85) > The project uses MongoDB: [MongoDB Guide](https://xiaojusurvey.didi.cn/docs/next/document/%E6%A6%82%E8%BF%B0/%E6%95%B0%E6%8D%AE%E5%BA%93)
Configure the database, check MongoDB configuration. Configure the database, check MongoDB configuration.
@ -207,18 +191,22 @@ Create and publish a questionnaire.
<br /><br /> <br /><br />
## Star
Open source is not easy. If this project helps you, please star it ❤️❤️❤️. Your support is our greatest motivation.
[![Star History Chart](https://api.star-history.com/svg?repos=didi/xiaoju-survey&type=Date)](https://star-history.com/#didi/xiaoju-survey&Date)
## WeChat Group ## WeChat Group
The official group will release the latest project news, construction plans, and community activities. Any questions and cooperation can contact the assistant: The official group will release the latest project news, construction plans, and community activities. Any questions and cooperation can contact the assistant:
<img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" /> <img src="https://img-hxy021.didistatic.com/static/starimg/img/KXKvc7sjHz1700061188156.png" width="200" />
## QQ Group
The official group will release the latest project news, construction plans, and community activities. Welcome to join:
[<img src="https://img-hxy021.didistatic.com/static/starimg/img/iJUmLIHKV21700192846057.png" width="210" />](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=P61UJI_q8AzizyBLGOm-bUvzNrUnSQq-&authKey=yZFtL9biGB5yiIME3%2Bi%2Bf6XMOdTNiuf0pCIaviEEAIryySNzVy6LJ4xl7uHdEcrM&noverify=0&group_code=920623419)
## Star
Open source is not easy. If this project helps you, please star it ❤️❤️❤️. Your support is our greatest motivation.
## Feedback ## Feedback
If you use this project, please leave feedback:[I'm using](https://github.com/didi/xiaoju-survey/issues/64), Your support is our greatest. If you use this project, please leave feedback:[I'm using](https://github.com/didi/xiaoju-survey/issues/64), Your support is our greatest.

View File

@ -15,7 +15,7 @@ services:
- xiaoju-survey - xiaoju-survey
xiaoju-survey: xiaoju-survey:
image: "xiaojusurvey/xiaoju-survey:1.3.0-slim" # 最新版本https://hub.docker.com/r/xiaojusurvey/xiaoju-survey/tags image: "xiaojusurvey/xiaoju-survey:1.2.0-slim" # 最新版本https://hub.docker.com/r/xiaojusurvey/xiaoju-survey/tags
container_name: xiaoju-survey container_name: xiaoju-survey
restart: always restart: always
ports: ports:

View File

@ -1,17 +0,0 @@
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
XIAOJU_SURVEY_MONGO_URL=
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=admin
XIAOJU_SURVEY_REDIS_HOST=
XIAOJU_SURVEY_REDIS_PORT=
XIAOJU_SURVEY_REDIS_USERNAME=
XIAOJU_SURVEY_REDIS_PASSWORD=
XIAOJU_SURVEY_REDIS_DB=
XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
XIAOJU_SURVEY_HTTP_DATA_ENCRYPT_TYPE=rsa
XIAOJU_SURVEY_JWT_SECRET=xiaojuSurveyJwtSecret
XIAOJU_SURVEY_JWT_EXPIRES_IN=8h
XIAOJU_SURVEY_LOGGER_FILENAME=./logs/app.log

View File

@ -95,9 +95,7 @@
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
}, },
"collectCoverageFrom": [ "collectCoverageFrom": [
"**/*.(t|j)s", "**/*.(t|j)s"
"!**/*.module.ts",
"!**/upgrade.*.ts"
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node", "testEnvironment": "node",

View File

@ -88,10 +88,17 @@ export interface MsgContent {
msg_9004: string; msg_9004: string;
} }
export interface JumpConfig {
type: string;
link: string;
buttonText?: string;
}
export interface SubmitConf { export interface SubmitConf {
submitTitle: string; submitTitle: string;
confirmAgain: ConfirmAgain; confirmAgain: ConfirmAgain;
msgContent: MsgContent; msgContent: MsgContent;
jumpConfig?: JumpConfig;
} }
// 白名单类型 // 白名单类型

View File

@ -1,7 +1,7 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import './report'; import 'scripts/run-report';
async function bootstrap() { async function bootstrap() {
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;

View File

@ -0,0 +1,30 @@
import { BaseEntity } from '../base.entity';
import { RECORD_STATUS } from 'src/enums';
describe('BaseEntity', () => {
let baseEntity: BaseEntity;
beforeEach(() => {
baseEntity = new BaseEntity();
});
it('should initialize default info before insert', () => {
const now = Date.now();
baseEntity.initDefaultInfo();
expect(baseEntity.curStatus.status).toBe(RECORD_STATUS.NEW);
expect(baseEntity.curStatus.date).toBeCloseTo(now, -3);
expect(baseEntity.statusList).toHaveLength(1);
expect(baseEntity.statusList[0].status).toBe(RECORD_STATUS.NEW);
expect(baseEntity.statusList[0].date).toBeCloseTo(now, -3);
expect(baseEntity.createDate).toBeCloseTo(now, -3);
expect(baseEntity.updateDate).toBeCloseTo(now, -3);
});
it('should update updateDate before update', () => {
const now = Date.now();
baseEntity.onUpdate();
expect(baseEntity.updateDate).toBeCloseTo(now, -3);
});
});

View File

@ -1,57 +0,0 @@
import { SurveyMeta } from '../surveyMeta.entity';
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
// 模拟日期
const mockDateNow = Date.now();
describe('SurveyMeta Entity', () => {
let surveyMeta: SurveyMeta;
// 在每个测试之前,初始化 SurveyMeta 实例
beforeEach(() => {
surveyMeta = new SurveyMeta();
// 模拟 Date.now() 返回固定的时间
jest.spyOn(Date, 'now').mockReturnValue(mockDateNow);
});
afterEach(() => {
jest.restoreAllMocks(); // 每次测试后还原所有 mock
});
it('should set default curStatus and subStatus on insert when they are not provided', () => {
surveyMeta.initDefaultInfo();
// 验证 curStatus 是否被初始化为默认值
expect(surveyMeta.curStatus).toEqual({
status: RECORD_STATUS.NEW,
date: mockDateNow,
});
// 验证 statusList 是否包含 curStatus
expect(surveyMeta.statusList).toEqual([
{
status: RECORD_STATUS.NEW,
date: mockDateNow,
},
]);
// 验证 subStatus 是否被初始化为默认值
expect(surveyMeta.subStatus).toEqual({
status: RECORD_SUB_STATUS.DEFAULT,
date: mockDateNow,
});
});
it('should initialize statusList if curStatus is provided but statusList is empty', () => {
surveyMeta.curStatus = null;
surveyMeta.initDefaultInfo();
expect(surveyMeta.statusList).toEqual([
{
status: RECORD_STATUS.NEW,
date: expect.any(Number),
},
]);
});
});

View File

@ -206,8 +206,8 @@ describe('UserService', () => {
it('should return a list of users by username', async () => { it('should return a list of users by username', async () => {
const username = 'test'; const username = 'test';
const userList = [ const userList = [
{ _id: new ObjectId(), username: 'testUser1', createdAt: new Date() }, { _id: new ObjectId(), username: 'testUser1', createDate: new Date() },
{ _id: new ObjectId(), username: 'testUser2', createdAt: new Date() }, { _id: new ObjectId(), username: 'testUser2', createDate: new Date() },
]; ];
jest jest
@ -226,7 +226,7 @@ describe('UserService', () => {
}, },
skip: 0, skip: 0,
take: 10, take: 10,
select: ['_id', 'username', 'createdAt'], select: ['_id', 'username', 'createDate'],
}); });
expect(result).toEqual(userList); expect(result).toEqual(userList);
}); });
@ -237,12 +237,12 @@ describe('UserService', () => {
{ {
_id: new ObjectId(idList[0]), _id: new ObjectId(idList[0]),
username: 'testUser1', username: 'testUser1',
createdAt: new Date(), createDate: new Date(),
}, },
{ {
_id: new ObjectId(idList[1]), _id: new ObjectId(idList[1]),
username: 'testUser2', username: 'testUser2',
createdAt: new Date(), createDate: new Date(),
}, },
]; ];
@ -258,7 +258,7 @@ describe('UserService', () => {
$in: idList.map((id) => new ObjectId(id)), $in: idList.map((id) => new ObjectId(id)),
}, },
}, },
select: ['_id', 'username', 'createdAt'], select: ['_id', 'username', 'createDate'],
}); });
expect(result).toEqual(userList); expect(result).toEqual(userList);
}); });

View File

@ -8,6 +8,7 @@ import {
MESSAGE_PUSHING_TYPE, MESSAGE_PUSHING_TYPE,
MESSAGE_PUSHING_HOOK, MESSAGE_PUSHING_HOOK,
} from 'src/enums/messagePushing'; } from 'src/enums/messagePushing';
import { RECORD_STATUS } from 'src/enums';
describe('MessagePushingTaskDto', () => { describe('MessagePushingTaskDto', () => {
let dto: MessagePushingTaskDto; let dto: MessagePushingTaskDto;
@ -33,9 +34,9 @@ describe('MessagePushingTaskDto', () => {
}); });
it('should have a type', () => { it('should have a type', () => {
dto.type = MESSAGE_PUSHING_TYPE.HTTP; dto.type = MESSAGE_PUSHING_TYPE.HTTP; // Set your desired type here
expect(dto.type).toBeDefined(); expect(dto.type).toBeDefined();
expect(dto.type).toEqual(MESSAGE_PUSHING_TYPE.HTTP); expect(dto.type).toEqual(MESSAGE_PUSHING_TYPE.HTTP); // Adjust based on your enum
}); });
it('should have a push address', () => { it('should have a push address', () => {
@ -45,13 +46,13 @@ describe('MessagePushingTaskDto', () => {
}); });
it('should have a trigger hook', () => { it('should have a trigger hook', () => {
dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED; dto.triggerHook = MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED; // Set your desired hook here
expect(dto.triggerHook).toBeDefined(); expect(dto.triggerHook).toBeDefined();
expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED); expect(dto.triggerHook).toEqual(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED); // Adjust based on your enum
}); });
it('should have an array of surveys', () => { it('should have an array of surveys', () => {
dto.surveys = ['survey1', 'survey2']; dto.surveys = ['survey1', 'survey2']; // Set your desired surveys here
expect(dto.surveys).toBeDefined(); expect(dto.surveys).toBeDefined();
expect(dto.surveys).toEqual(['survey1', 'survey2']); expect(dto.surveys).toEqual(['survey1', 'survey2']);
}); });
@ -61,6 +62,13 @@ describe('MessagePushingTaskDto', () => {
expect(dto.owner).toBeDefined(); expect(dto.owner).toBeDefined();
expect(dto.owner).toBe('test_owner'); expect(dto.owner).toBe('test_owner');
}); });
it('should have current status', () => {
dto.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
expect(dto.curStatus).toBeDefined();
expect(dto.curStatus.status).toEqual(RECORD_STATUS.NEW);
expect(dto.curStatus.date).toBeDefined();
});
}); });
describe('CodeDto', () => { describe('CodeDto', () => {

View File

@ -117,9 +117,6 @@ describe('MessagePushingTaskService', () => {
expect(result).toEqual(tasks); expect(result).toEqual(tasks);
expect(repository.find).toHaveBeenCalledWith({ expect(repository.find).toHaveBeenCalledWith({
where: { where: {
isDeleted: {
$ne: true,
},
ownerId: mockOwnerId, ownerId: mockOwnerId,
surveys: { $all: [surveyId] }, surveys: { $all: [surveyId] },
triggerHook: hook, triggerHook: hook,
@ -147,19 +144,9 @@ describe('MessagePushingTaskService', () => {
where: { where: {
ownerId: mockOwnerId, ownerId: mockOwnerId,
_id: new ObjectId(taskId), _id: new ObjectId(taskId),
isDeleted: {
$ne: true,
},
}, },
}); });
}); });
it('should throw an error when message pushing task is not found', async () => {
const taskId = '65afc62904d5db18534c0f78';
jest.spyOn(repository, 'findOne').mockResolvedValue(null); // 模拟未找到任务
const mockOwnerId = '66028642292c50f8b71a9eee';
await expect(service.findOne({ id: taskId, ownerId: mockOwnerId }));
});
}); });
describe('update', () => { describe('update', () => {
@ -196,20 +183,6 @@ describe('MessagePushingTaskService', () => {
}); });
expect(repository.save).toHaveBeenCalledWith(updatedTask); expect(repository.save).toHaveBeenCalledWith(updatedTask);
}); });
it('should throw an error if the task to be updated is not found', async () => {
const taskId = '65afc62904d5db18534c0f78';
const updateDto: UpdateMessagePushingTaskDto = { name: 'Updated Task' };
jest.spyOn(repository, 'findOne').mockResolvedValue(null); // 模拟任务未找到
const mockOwnerId = '66028642292c50f8b71a9eee';
await expect(
service.update({
ownerId: mockOwnerId,
id: taskId,
updateData: updateDto,
}),
).rejects.toThrow(`Message pushing task with id ${taskId} not found`);
});
}); });
describe('remove', () => { describe('remove', () => {
@ -231,40 +204,16 @@ describe('MessagePushingTaskService', () => {
expect(result).toEqual(updateResult); expect(result).toEqual(updateResult);
expect(repository.updateOne).toHaveBeenCalledWith( expect(repository.updateOne).toHaveBeenCalledWith(
{ {
ownerId: mockOperatorId,
_id: new ObjectId(taskId), _id: new ObjectId(taskId),
}, },
{ {
$set: { $set: {
isDeleted: true, isDeleted: true,
operatorId: mockOperatorId,
operator: mockOperator,
deletedAt: expect.any(Date),
}, },
}, },
); );
}); });
it('should throw an error if the task to be removed is not found', async () => {
const taskId = '65afc62904d5db18534c0f78';
jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({ modifiedCount: 0 }); // 模拟删除失败
const mockOperatorId = '66028642292c50f8b71a9eee';
const mockOperator = 'mockOperator';
const result = await service.remove({
id: taskId,
operatorId: mockOperatorId,
operator: mockOperator,
});
expect(result.modifiedCount).toBe(0);
expect(repository.updateOne).toHaveBeenCalledWith(
{
_id: new ObjectId(taskId),
},
expect.any(Object),
);
});
}); });
describe('surveyAuthorizeTask', () => { describe('surveyAuthorizeTask', () => {
@ -294,35 +243,8 @@ describe('MessagePushingTaskService', () => {
$push: { $push: {
surveys: surveyId, surveys: surveyId,
}, },
$set: {
updatedAt: expect.any(Date),
},
}, },
); );
}); });
it('should not add the surveyId if it already exists in the task', async () => {
const taskId = '65afc62904d5db18534c0f78';
const surveyId = '65af380475b64545e5277dd9';
const mockOwnerId = '66028642292c50f8b71a9eee';
jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({ modifiedCount: 0 }); // 模拟重复添加
const result = await service.surveyAuthorizeTask({
taskId,
surveyId,
ownerId: mockOwnerId,
});
expect(result.modifiedCount).toBe(0);
expect(repository.updateOne).toHaveBeenCalledWith(
{
_id: new ObjectId(taskId),
surveys: { $nin: [surveyId] }, // 确保只有不包含时才插入
ownerId: mockOwnerId,
},
expect.any(Object),
);
});
}); });
}); });

View File

@ -37,4 +37,9 @@ describe('UpdateMessagePushingTaskDto', () => {
dto.surveys = null; dto.surveys = null;
expect(dto.surveys).toBeNull(); expect(dto.surveys).toBeNull();
}); });
it('should have a nullable curStatus', () => {
dto.curStatus = null;
expect(dto.curStatus).toBeNull();
});
}); });

View File

@ -63,14 +63,14 @@ export class MessagePushingTaskService {
}); });
} }
findOne({ async findOne({
id, id,
ownerId, ownerId,
}: { }: {
id: string; id: string;
ownerId: string; ownerId: string;
}): Promise<MessagePushingTask> { }): Promise<MessagePushingTask> {
return this.messagePushingTaskRepository.findOne({ return await this.messagePushingTaskRepository.findOne({
where: { where: {
ownerId, ownerId,
_id: new ObjectId(id), _id: new ObjectId(id),

View File

@ -75,25 +75,6 @@ describe('CollaboratorService', () => {
}); });
}); });
describe('deleteCollaborator', () => {
it('should delete a collaborator by userId and surveyId', async () => {
const deleteOneSpy = jest
.spyOn(repository, 'deleteOne')
.mockResolvedValue({ acknowledged: true, deletedCount: 1 });
const result = await service.deleteCollaborator({
userId: '1',
surveyId: '1',
});
expect(deleteOneSpy).toHaveBeenCalledWith({
userId: '1',
surveyId: '1',
});
expect(result).toEqual({ acknowledged: true, deletedCount: 1 });
});
});
describe('batchCreate', () => { describe('batchCreate', () => {
it('should batch create collaborators', async () => { it('should batch create collaborators', async () => {
const insertManySpy = jest const insertManySpy = jest
@ -105,193 +86,15 @@ describe('CollaboratorService', () => {
const result = await service.batchCreate({ const result = await service.batchCreate({
surveyId: '1', surveyId: '1',
collaboratorList: [{ userId: '1', permissions: [] }], collaboratorList: [{ userId: '1', permissions: [] }],
creator: 'testCreator',
creatorId: 'testCreatorId',
}); });
expect(insertManySpy).toHaveBeenCalledWith([ expect(insertManySpy).toHaveBeenCalledWith([
{ { surveyId: '1', userId: '1', permissions: [] },
userId: '1',
permissions: [],
surveyId: '1',
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
creator: 'testCreator',
creatorId: 'testCreatorId',
},
]); ]);
expect(result).toEqual({ insertedCount: 1 }); expect(result).toEqual({ insertedCount: 1 });
}); });
}); });
describe('changeUserPermission', () => {
it("should update a user's permissions", async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const result = await service.changeUserPermission({
userId: '1',
surveyId: '1',
permission: 'read',
operator: 'testOperator',
operatorId: 'testOperatorId',
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
surveyId: '1',
userId: '1',
},
{
$set: {
permission: 'read',
operator: 'testOperator',
operatorId: 'testOperatorId',
updatedAt: expect.any(Date),
},
},
);
expect(result).toEqual({});
});
});
describe('batchDelete', () => {
it('should batch delete collaborators', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const collaboratorId = new ObjectId().toString();
const result = await service.batchDelete({
surveyId: '1',
idList: [collaboratorId],
});
const expectedQuery = {
surveyId: '1',
$or: [
{
_id: {
$in: [new ObjectId(collaboratorId)],
},
},
],
};
expect(logger.info).toHaveBeenCalledWith(JSON.stringify(expectedQuery));
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual(mockResult);
});
it('should batch delete collaborators by idList and userIdList', async () => {
const collaboratorId = new ObjectId().toString();
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue({ acknowledged: true, deletedCount: 2 });
const result = await service.batchDelete({
idList: [collaboratorId],
userIdList: ['user1', 'user2'],
surveyId: '1',
});
const expectedQuery = {
surveyId: '1',
$or: [
{
userId: {
$in: ['user1', 'user2'],
},
},
{
_id: {
$in: [new ObjectId(collaboratorId)],
},
},
],
};
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual({ acknowledged: true, deletedCount: 2 });
});
it('should handle batch delete with neIdList only', async () => {
const neCollaboratorId = new ObjectId().toString();
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue({ acknowledged: true, deletedCount: 1 });
const result = await service.batchDelete({
neIdList: [neCollaboratorId],
surveyId: '1',
});
const expectedQuery = {
surveyId: '1',
$or: [
{
_id: {
$nin: [new ObjectId(neCollaboratorId)],
},
},
],
};
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual({ acknowledged: true, deletedCount: 1 });
});
});
describe('batchDeleteBySurveyId', () => {
it('should batch delete collaborators by survey id', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const surveyId = new ObjectId().toString();
const result = await service.batchDeleteBySurveyId(surveyId);
expect(deleteManySpy).toHaveBeenCalledWith({
surveyId,
});
expect(result).toEqual(mockResult);
});
});
describe('updateById', () => {
it('should update collaborator by id', async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const collaboratorId = new ObjectId().toString();
const result = await service.updateById({
collaboratorId,
permissions: [],
operator: 'testOperator',
operatorId: 'testOperatorId',
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
_id: new ObjectId(collaboratorId),
},
{
$set: {
permissions: [],
operator: 'testOperator',
operatorId: 'testOperatorId',
updatedAt: expect.any(Date),
},
},
);
expect(result).toEqual({});
});
});
describe('getSurveyCollaboratorList', () => { describe('getSurveyCollaboratorList', () => {
it('should return a list of collaborators for a survey', async () => { it('should return a list of collaborators for a survey', async () => {
const collaboratorId = new ObjectId().toString(); const collaboratorId = new ObjectId().toString();
@ -318,6 +121,38 @@ describe('CollaboratorService', () => {
}); });
}); });
describe('getCollaboratorListByIds', () => {
it('should return a list of collaborators by ids', async () => {
const collaboratorId = new ObjectId().toString();
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
{
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
},
] as Collaborator[]);
const result = await service.getCollaboratorListByIds({
idList: [collaboratorId],
});
expect(findSpy).toHaveBeenCalledWith({
_id: {
$in: [new ObjectId(collaboratorId)],
},
});
expect(result).toEqual([
{
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
},
]);
});
});
describe('getCollaborator', () => { describe('getCollaborator', () => {
it('should return a collaborator', async () => { it('should return a collaborator', async () => {
const collaboratorId = new ObjectId().toString(); const collaboratorId = new ObjectId().toString();
@ -348,6 +183,127 @@ describe('CollaboratorService', () => {
}); });
}); });
describe('changeUserPermission', () => {
it("should update a user's permissions", async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const result = await service.changeUserPermission({
userId: '1',
surveyId: '1',
permission: 'read',
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
surveyId: '1',
userId: '1',
},
{
$set: {
permission: 'read',
},
},
);
expect(result).toEqual({});
});
});
describe('deleteCollaborator', () => {
it('should delete a collaborator', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteOneSpy = jest
.spyOn(repository, 'deleteOne')
.mockResolvedValue(mockResult);
const result = await service.deleteCollaborator({
userId: '1',
surveyId: '1',
});
expect(deleteOneSpy).toHaveBeenCalledWith({
userId: '1',
surveyId: '1',
});
expect(result).toEqual(mockResult);
});
});
describe('batchDelete', () => {
it('should batch delete collaborators', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const collaboratorId = new ObjectId().toString();
const result = await service.batchDelete({
surveyId: '1',
idList: [collaboratorId],
});
const expectedQuery = {
surveyId: '1',
$or: [
{
_id: {
$in: [new ObjectId(collaboratorId)],
},
},
],
};
expect(logger.info).toHaveBeenCalledWith(JSON.stringify(expectedQuery));
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual(mockResult);
});
});
describe('batchDeleteBySurveyId', () => {
it('should batch delete collaborators by survey id', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const surveyId = new ObjectId().toString();
const result = await service.batchDeleteBySurveyId(surveyId);
expect(deleteManySpy).toHaveBeenCalledWith({
surveyId,
});
expect(result).toEqual(mockResult);
});
});
describe('updateById', () => {
it('should update collaborator by id', async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const collaboratorId = new ObjectId().toString();
const result = await service.updateById({
collaboratorId,
permissions: [],
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
_id: new ObjectId(collaboratorId),
},
{
$set: {
permissions: [],
},
},
);
expect(result).toEqual({});
});
});
describe('getCollaboratorListByUserId', () => { describe('getCollaboratorListByUserId', () => {
it('should return a list of collaborators by user id', async () => { it('should return a list of collaborators by user id', async () => {
const userId = new ObjectId().toString(); const userId = new ObjectId().toString();
@ -371,48 +327,5 @@ describe('CollaboratorService', () => {
{ _id: '1', surveyId: '1', userId, permissions: [] }, { _id: '1', surveyId: '1', userId, permissions: [] },
]); ]);
}); });
it('should return a list of collaborators by their IDs', async () => {
const collaboratorId1 = new ObjectId().toString();
const collaboratorId2 = new ObjectId().toString();
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
{
_id: new ObjectId(collaboratorId1),
surveyId: '1',
userId: 'user1',
permissions: [],
},
{
_id: new ObjectId(collaboratorId2),
surveyId: '2',
userId: 'user2',
permissions: [],
},
] as Collaborator[]);
const result = await service.getCollaboratorListByIds({
idList: [collaboratorId1, collaboratorId2],
});
expect(findSpy).toHaveBeenCalledWith({
_id: {
$in: [new ObjectId(collaboratorId1), new ObjectId(collaboratorId2)],
},
});
expect(result).toEqual([
{
_id: new ObjectId(collaboratorId1),
surveyId: '1',
userId: 'user1',
permissions: [],
},
{
_id: new ObjectId(collaboratorId2),
surveyId: '2',
userId: 'user2',
permissions: [],
},
]);
});
}); });
}); });

View File

@ -109,8 +109,8 @@ describe('DataStatisticController', () => {
}, },
], ],
listBody: [ listBody: [
{ diffTime: '0.5', createdAt: '2024-02-11' }, { diffTime: '0.5', createDate: '2024-02-11' },
{ diffTime: '0.5', createdAt: '2024-02-11' }, { diffTime: '0.5', createDate: '2024-02-11' },
], ],
}; };
@ -155,8 +155,8 @@ describe('DataStatisticController', () => {
}, },
], ],
listBody: [ listBody: [
{ diffTime: '0.5', createdAt: '2024-02-11', data123: '15200000000' }, { diffTime: '0.5', createDate: '2024-02-11', data123: '15200000000' },
{ diffTime: '0.5', createdAt: '2024-02-11', data123: '13800000000' }, { diffTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
], ],
}; };
@ -212,8 +212,8 @@ describe('DataStatisticController', () => {
date: 1717158851823, date: 1717158851823,
}, },
], ],
createdAt: 1717158851823, createDate: 1717158851823,
updatedAt: 1717159136025, updateDate: 1717159136025,
title: '问卷调研', title: '问卷调研',
surveyPath: 'ZdGNzTTR', surveyPath: 'ZdGNzTTR',
code: { code: {
@ -275,6 +275,11 @@ describe('DataStatisticController', () => {
again_text: '确认要提交吗?', again_text: '确认要提交吗?',
}, },
link: '', link: '',
jumpConfig: {
type: 'link',
link: '',
buttonText: '',
},
}, },
logicConf: { logicConf: {
showLogicConf: [], showLogicConf: [],

View File

@ -151,8 +151,8 @@ describe('DataStatisticService', () => {
date: 1710340863123.0, date: 1710340863123.0,
}, },
], ],
createdAt: 1710340863123.0, createDate: 1710340863123.0,
updatedAt: 1710340863123.0, updateDate: 1710340863123.0,
}, },
] as unknown as Array<SurveyResponse>; ] as unknown as Array<SurveyResponse>;
@ -196,7 +196,7 @@ describe('DataStatisticService', () => {
data413: expect.any(Number), data413: expect.any(Number),
data863: expect.any(String), data863: expect.any(String),
diffTime: expect.any(String), diffTime: expect.any(String),
createdAt: expect.any(String), createDate: expect.any(String),
}), }),
]), ]),
}); });
@ -273,8 +273,8 @@ describe('DataStatisticService', () => {
date: 1710400232161.0, date: 1710400232161.0,
}, },
], ],
createdAt: 1710400232161.0, createDate: 1710400232161.0,
updatedAt: 1710400232161.0, updateDate: 1710400232161.0,
}, },
] as unknown as Array<SurveyResponse>; ] as unknown as Array<SurveyResponse>;
@ -295,7 +295,7 @@ describe('DataStatisticService', () => {
expect(result.listBody).toEqual( expect(result.listBody).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
createdAt: expect.any(String), createDate: expect.any(String),
data405: expect.any(String), data405: expect.any(String),
data450: expect.any(String), data450: expect.any(String),
data458: expect.any(String), data458: expect.any(String),

View File

@ -145,7 +145,7 @@ describe('DownloadTaskController', () => {
filename: 'mockFile.csv', filename: 'mockFile.csv',
url: 'http://mock-url.com', url: 'http://mock-url.com',
fileSize: 1024, fileSize: 1024,
createdAt: Date.now(), createDate: Date.now(),
}, },
], ],
}; };
@ -219,13 +219,10 @@ describe('DownloadTaskController', () => {
describe('deleteFileByName', () => { describe('deleteFileByName', () => {
it('should delete a download task successfully', async () => { it('should delete a download task successfully', async () => {
const mockBody = { taskId: 'mockTaskId' }; const mockBody = { taskId: 'mockTaskId' };
const mockUserId = new ObjectId(); const mockReq = { user: { _id: 'mockUserId' } };
const mockReq = {
user: { _id: mockUserId, username: 'mockUsername' },
};
const mockTaskInfo: any = { const mockTaskInfo: any = {
_id: new ObjectId(), _id: new ObjectId(),
creatorId: mockUserId.toString(), creatorId: 'mockUserId',
}; };
const mockDelRes = { modifiedCount: 1 }; const mockDelRes = { modifiedCount: 1 };
@ -240,8 +237,6 @@ describe('DownloadTaskController', () => {
expect(downloadTaskService.deleteDownloadTask).toHaveBeenCalledWith({ expect(downloadTaskService.deleteDownloadTask).toHaveBeenCalledWith({
taskId: mockBody.taskId, taskId: mockBody.taskId,
operator: mockReq.user.username,
operatorId: mockReq.user._id.toString(),
}); });
expect(result).toEqual({ code: 200, data: true }); expect(result).toEqual({ code: 200, data: true });
}); });

View File

@ -9,7 +9,7 @@ import { DataStatisticService } from '../services/dataStatistic.service';
import { FileService } from 'src/modules/file/services/file.service'; import { FileService } from 'src/modules/file/services/file.service';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus'; import { RECORD_STATUS } from 'src/enums';
describe('DownloadTaskService', () => { describe('DownloadTaskService', () => {
let service: DownloadTaskService; let service: DownloadTaskService;
@ -93,7 +93,6 @@ describe('DownloadTaskService', () => {
title: mockParams.responseSchema.title, title: mockParams.responseSchema.title,
}, },
filename: expect.any(String), filename: expect.any(String),
status: DOWNLOAD_TASK_STATUS.WAITING,
}); });
expect(downloadTaskRepository.save).toHaveBeenCalled(); expect(downloadTaskRepository.save).toHaveBeenCalled();
expect(result).toEqual(mockTaskId); expect(result).toEqual(mockTaskId);
@ -119,11 +118,10 @@ describe('DownloadTaskService', () => {
expect(downloadTaskRepository.findAndCount).toHaveBeenCalledWith({ expect(downloadTaskRepository.findAndCount).toHaveBeenCalledWith({
where: { where: {
creatorId: mockCreatorId, creatorId: mockCreatorId,
isDeleted: { $ne: true },
}, },
take: 10, take: 10,
skip: 0, skip: 0,
order: { createdAt: -1 }, order: { createDate: -1 },
}); });
expect(result).toEqual({ expect(result).toEqual({
@ -162,84 +160,32 @@ describe('DownloadTaskService', () => {
}); });
describe('deleteDownloadTask', () => { describe('deleteDownloadTask', () => {
it('should mark task as deleted and set deletedAt', async () => { it('should update task status to REMOVED', async () => {
const mockTaskId = new ObjectId().toString(); const mockTaskId = new ObjectId().toString();
const mockOperator = 'operatorName';
const mockOperatorId = 'operatorId1';
const mockUpdateResult = { matchedCount: 1 }; const mockUpdateResult = { matchedCount: 1 };
jest jest
.spyOn(downloadTaskRepository, 'updateOne') .spyOn(downloadTaskRepository, 'updateOne')
.mockResolvedValue(mockUpdateResult as any); .mockResolvedValue(mockUpdateResult as any);
const result = await service.deleteDownloadTask({ const result = await service.deleteDownloadTask({ taskId: mockTaskId });
taskId: mockTaskId,
operator: mockOperator,
operatorId: mockOperatorId,
});
expect(downloadTaskRepository.updateOne).toHaveBeenCalledWith( expect(downloadTaskRepository.updateOne).toHaveBeenCalledWith(
{ _id: new ObjectId(mockTaskId) }, {
_id: new ObjectId(mockTaskId),
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
},
{ {
$set: { $set: {
isDeleted: true, curStatus: {
operator: mockOperator, status: RECORD_STATUS.REMOVED,
operatorId: mockOperatorId, date: expect.any(Number),
deletedAt: expect.any(Date), },
}, },
$push: { statusList: expect.any(Object) },
}, },
); );
expect(result).toEqual(mockUpdateResult); expect(result).toEqual(mockUpdateResult);
}); });
}); });
describe('processDownloadTask', () => {
it('should push task to queue and execute if not executing', async () => {
const mockTaskId = new ObjectId().toString();
jest.spyOn(service, 'executeTask').mockImplementation(jest.fn());
service.processDownloadTask({ taskId: mockTaskId });
expect(DownloadTaskService.taskList).toContain(mockTaskId);
expect(service.executeTask).toHaveBeenCalled();
});
it('should handle already executing case', async () => {
const mockTaskId = new ObjectId().toString();
DownloadTaskService.isExecuting = true;
jest.spyOn(service, 'executeTask').mockImplementation(jest.fn());
service.processDownloadTask({ taskId: mockTaskId });
expect(DownloadTaskService.taskList).toContain(mockTaskId);
expect(service.executeTask).not.toHaveBeenCalled();
});
});
describe('executeTask', () => {
it('should process and execute tasks in queue', async () => {
const mockTaskId = new ObjectId().toString();
DownloadTaskService.taskList.push(mockTaskId);
jest.spyOn(service, 'getDownloadTaskById').mockResolvedValue({
_id: new ObjectId(mockTaskId),
isDeleted: false,
} as any);
jest.spyOn(service, 'handleDownloadTask').mockResolvedValue(undefined);
await service.executeTask();
expect(service.getDownloadTaskById).toHaveBeenCalledWith({
taskId: mockTaskId,
});
expect(service.handleDownloadTask).toHaveBeenCalled();
});
it('should stop executing when queue is empty', async () => {
DownloadTaskService.taskList = [];
await service.executeTask();
expect(DownloadTaskService.isExecuting).toBe(false);
});
});
}); });

View File

@ -14,8 +14,8 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
date: 1710399368439, date: 1710399368439,
}, },
], ],
createdAt: 1710399368440, createDate: 1710399368440,
updatedAt: 1710399368440, updateDate: 1710399368440,
title: '加密全流程', title: '加密全流程',
surveyPath: 'EBzdmnSp', surveyPath: 'EBzdmnSp',
code: { code: {
@ -71,6 +71,11 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
is_again: true, is_again: true,
again_text: '确认要提交吗?', again_text: '确认要提交吗?',
}, },
jumpConfig: {
type: 'link',
link: '',
buttonText: '',
},
}, },
dataConf: { dataConf: {
dataList: [ dataList: [
@ -365,6 +370,11 @@ export const mockResponseSchema: ResponseSchema = {
is_again: true, is_again: true,
again_text: '确认要提交吗?', again_text: '确认要提交吗?',
}, },
jumpConfig: {
type: 'link',
link: '',
buttonText: '',
},
}, },
dataConf: { dataConf: {
dataList: [ dataList: [
@ -654,6 +664,6 @@ export const mockResponseSchema: ResponseSchema = {
}, },
}, },
pageId: '65afc62904d5db18534c0f78', pageId: '65afc62904d5db18534c0f78',
createdAt: 1710340841289, createDate: 1710340841289,
updatedAt: 1710340841289.0, updateDate: 1710340841289.0,
} as unknown as ResponseSchema; } as unknown as ResponseSchema;

View File

@ -1,144 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MongoRepository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { SessionService } from '../services/session.service';
import { Session } from 'src/models/session.entity';
import { ObjectId } from 'mongodb';
import { SESSION_STATUS } from 'src/enums/surveySessionStatus';
describe('SessionService', () => {
let service: SessionService;
let repository: MongoRepository<Session>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SessionService,
{
provide: getRepositoryToken(Session),
useClass: MongoRepository,
},
],
}).compile();
service = module.get<SessionService>(SessionService);
repository = module.get<MongoRepository<Session>>(
getRepositoryToken(Session),
);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should create and save a new session', async () => {
const mockSession = {
surveyId: 'survey123',
userId: 'user123',
status: SESSION_STATUS.DEACTIVATED,
};
const createdSession: any = { ...mockSession, _id: new ObjectId() };
jest.spyOn(repository, 'create').mockReturnValue(createdSession);
jest.spyOn(repository, 'save').mockResolvedValue(createdSession);
const result = await service.create({
surveyId: mockSession.surveyId,
userId: mockSession.userId,
});
expect(result).toEqual(createdSession);
expect(repository.create).toHaveBeenCalledWith(mockSession);
expect(repository.save).toHaveBeenCalledWith(createdSession);
});
});
describe('findOne', () => {
it('should find a session by id', async () => {
const sessionId = '65afc62904d5db18534c0f78';
const foundSession = {
_id: new ObjectId(sessionId),
surveyId: 'survey123',
userId: 'user123',
status: SESSION_STATUS.ACTIVATED,
};
jest
.spyOn(repository, 'findOne')
.mockResolvedValue(foundSession as Session);
const result = await service.findOne(sessionId);
expect(result).toEqual(foundSession);
expect(repository.findOne).toHaveBeenCalledWith({
where: { _id: new ObjectId(sessionId) },
});
});
});
describe('findLatestEditingOne', () => {
it('should find the latest editing session for a survey', async () => {
const surveyId = 'survey123';
const latestSession = {
_id: new ObjectId(),
surveyId: surveyId,
userId: 'user123',
status: SESSION_STATUS.ACTIVATED,
};
jest
.spyOn(repository, 'findOne')
.mockResolvedValue(latestSession as Session);
const result = await service.findLatestEditingOne({ surveyId });
expect(result).toEqual(latestSession);
expect(repository.findOne).toHaveBeenCalledWith({
where: {
surveyId,
status: SESSION_STATUS.ACTIVATED,
},
});
});
});
describe('updateSessionToEditing', () => {
it('should update a session to editing and deactivate other sessions', async () => {
const sessionId = '65afc62904d5db18534c0f78';
const surveyId = 'survey123';
const updateResult: any = { affected: 1 };
const updateManyResult = { modifiedCount: 1 };
jest.spyOn(repository, 'update').mockResolvedValue(updateResult);
jest.spyOn(repository, 'updateMany').mockResolvedValue(updateManyResult);
const result = await service.updateSessionToEditing({
sessionId,
surveyId,
});
expect(result).toEqual([updateResult, updateManyResult]);
expect(repository.update).toHaveBeenCalledWith(
{ _id: new ObjectId(sessionId) },
{
status: SESSION_STATUS.ACTIVATED,
updatedAt: expect.any(Date),
},
);
expect(repository.updateMany).toHaveBeenCalledWith(
{
surveyId,
_id: { $ne: new ObjectId(sessionId) },
},
{
$set: {
status: SESSION_STATUS.DEACTIVATED,
updatedAt: expect.any(Date),
},
},
);
});
});
});

View File

@ -5,12 +5,13 @@ import { SurveyConfService } from '../services/surveyConf.service';
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service'; import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
import { ContentSecurityService } from '../services/contentSecurity.service'; import { ContentSecurityService } from '../services/contentSecurity.service';
import { SurveyHistoryService } from '../services/surveyHistory.service'; import { SurveyHistoryService } from '../services/surveyHistory.service';
import { CounterService } from '../../surveyResponse/services/counter.service';
import { SessionService } from '../services/session.service'; import { SessionService } from '../services/session.service';
import { UserService } from '../../auth/services/user.service'; import { UserService } from '../../auth/services/user.service';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { SurveyConf } from 'src/models/surveyConf.entity';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { HttpException } from 'src/exceptions/httpException';
import { Authentication } from 'src/guards/authentication.guard';
jest.mock('../services/surveyMeta.service'); jest.mock('../services/surveyMeta.service');
jest.mock('../services/surveyConf.service'); jest.mock('../services/surveyConf.service');
@ -18,7 +19,9 @@ jest.mock('../../surveyResponse/services/responseScheme.service');
jest.mock('../services/contentSecurity.service'); jest.mock('../services/contentSecurity.service');
jest.mock('../services/surveyHistory.service'); jest.mock('../services/surveyHistory.service');
jest.mock('../services/session.service'); jest.mock('../services/session.service');
jest.mock('../../surveyResponse/services/counter.service');
jest.mock('../../auth/services/user.service'); jest.mock('../../auth/services/user.service');
jest.mock('src/guards/authentication.guard'); jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard'); jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard'); jest.mock('src/guards/workspace.guard');
@ -28,17 +31,28 @@ describe('SurveyController', () => {
let surveyMetaService: SurveyMetaService; let surveyMetaService: SurveyMetaService;
let surveyConfService: SurveyConfService; let surveyConfService: SurveyConfService;
let responseSchemaService: ResponseSchemaService; let responseSchemaService: ResponseSchemaService;
let surveyHistoryService: SurveyHistoryService;
let sessionService: SessionService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [SurveyController], controllers: [SurveyController],
providers: [ providers: [
SurveyMetaService, SurveyMetaService,
SurveyConfService, {
provide: SurveyConfService,
useValue: {
getSurveyConfBySurveyId: jest.fn(),
getSurveyContentByCode: jest.fn(),
createSurveyConf: jest.fn(),
saveSurveyConf: jest.fn(),
},
},
ResponseSchemaService, ResponseSchemaService,
ContentSecurityService, ContentSecurityService,
SurveyHistoryService, SurveyHistoryService,
SessionService, SessionService,
CounterService,
UserService, UserService,
{ {
provide: Logger, provide: Logger,
@ -47,12 +61,6 @@ describe('SurveyController', () => {
info: jest.fn(), info: jest.fn(),
}, },
}, },
{
provide: Authentication,
useClass: jest.fn().mockImplementation(() => ({
canActivate: () => true,
})),
},
], ],
}).compile(); }).compile();
@ -62,6 +70,9 @@ describe('SurveyController', () => {
responseSchemaService = module.get<ResponseSchemaService>( responseSchemaService = module.get<ResponseSchemaService>(
ResponseSchemaService, ResponseSchemaService,
); );
surveyHistoryService =
module.get<SurveyHistoryService>(SurveyHistoryService);
sessionService = module.get<SessionService>(SessionService);
}); });
describe('getBannerData', () => { describe('getBannerData', () => {
@ -83,11 +94,12 @@ describe('SurveyController', () => {
const newId = new ObjectId(); const newId = new ObjectId();
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({ jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
_id: newId, _id: newId,
} as any); } as SurveyMeta);
jest.spyOn(surveyConfService, 'createSurveyConf').mockResolvedValue({ jest.spyOn(surveyConfService, 'createSurveyConf').mockResolvedValue({
_id: new ObjectId(), _id: new ObjectId(),
} as any); pageId: newId.toString(),
} as SurveyConf);
const result = await controller.createSurvey(surveyInfo, { const result = await controller.createSurvey(surveyInfo, {
user: { username: 'testUser', _id: new ObjectId() }, user: { username: 'testUser', _id: new ObjectId() },
@ -101,15 +113,13 @@ describe('SurveyController', () => {
}); });
}); });
it('should throw an error if validation fails', async () => {
const surveyInfo = {}; // Invalid data
await expect(
controller.createSurvey(surveyInfo as any, { user: {} }),
).rejects.toThrow(HttpException);
});
it('should create a new survey by copy', async () => { it('should create a new survey by copy', async () => {
const existsSurveyId = new ObjectId(); const existsSurveyId = new ObjectId();
const existsSurveyMeta = {
_id: existsSurveyId,
surveyType: 'exam',
owner: 'testUser',
} as SurveyMeta;
const params = { const params = {
surveyType: 'normal', surveyType: 'normal',
remark: '问卷调研', remark: '问卷调研',
@ -118,14 +128,14 @@ describe('SurveyController', () => {
createFrom: existsSurveyId.toString(), createFrom: existsSurveyId.toString(),
}; };
const request = {
user: { username: 'testUser', _id: new ObjectId() },
surveyMeta: { _id: existsSurveyId, surveyType: 'exam' },
};
jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({ jest.spyOn(surveyMetaService, 'createSurveyMeta').mockResolvedValue({
_id: new ObjectId(), _id: new ObjectId(),
} as any); } as SurveyMeta);
const request = {
user: { username: 'testUser', _id: new ObjectId() },
surveyMeta: existsSurveyMeta,
};
const result = await controller.createSurvey(params, request); const result = await controller.createSurvey(params, request);
expect(result?.data?.id).toBeDefined(); expect(result?.data?.id).toBeDefined();
@ -135,30 +145,69 @@ describe('SurveyController', () => {
describe('updateConf', () => { describe('updateConf', () => {
it('should update survey configuration', async () => { it('should update survey configuration', async () => {
const surveyId = new ObjectId(); const surveyId = new ObjectId();
const surveyMeta = {
_id: surveyId,
surveyType: 'exam',
owner: 'testUser',
} as SurveyMeta;
jest
.spyOn(surveyConfService, 'saveSurveyConf')
.mockResolvedValue(undefined);
jest
.spyOn(surveyHistoryService, 'addHistory')
.mockResolvedValue(undefined);
jest
.spyOn(sessionService, 'findLatestEditingOne')
.mockResolvedValue(null);
jest
.spyOn(sessionService, 'updateSessionToEditing')
.mockResolvedValue(undefined);
const reqBody = { const reqBody = {
surveyId: surveyId.toString(), surveyId: surveyId.toString(),
configData: { configData: {
/* ... your config data here ... */ bannerConf: {
titleConfig: {},
bannerConfig: {},
},
baseConf: {
beginTime: '2024-01-23 21:59:05',
endTime: '2034-01-23 21:59:05',
},
bottomConf: { logoImage: '/imgs/Logo.webp', logoImageWidth: '60%' },
skinConf: {
skinColor: '#4a4c5b',
inputBgColor: '#ffffff',
backgroundConf: {
color: '#fff',
type: 'color',
image: '',
},
themeConf: {
color: '#ffa600',
},
contentConf: {
opacity: 100,
},
},
submitConf: {},
dataConf: {
dataList: [],
},
}, },
sessionId: 'mock-session-id', sessionId: 'mock-session-id',
}; };
const result = await controller.updateConf(reqBody, { const result = await controller.updateConf(reqBody, {
user: { username: 'testUser', _id: 'testUserId' }, user: { username: 'testUser', _id: 'testUserId' },
surveyMeta: { _id: surveyId }, surveyMeta,
}); });
expect(result).toEqual({ expect(result).toEqual({
code: 200, code: 200,
}); });
}); });
it('should throw an error if validation fails', async () => {
const reqBody = {}; // Invalid data
await expect(
controller.updateConf(reqBody, { user: {} }),
).rejects.toThrow(HttpException);
});
}); });
describe('deleteSurvey', () => { describe('deleteSurvey', () => {
@ -168,7 +217,7 @@ describe('SurveyController', () => {
_id: surveyId, _id: surveyId,
surveyType: 'exam', surveyType: 'exam',
owner: 'testUser', owner: 'testUser',
}; } as SurveyMeta;
jest jest
.spyOn(surveyMetaService, 'deleteSurveyMeta') .spyOn(surveyMetaService, 'deleteSurveyMeta')
@ -178,10 +227,13 @@ describe('SurveyController', () => {
.mockResolvedValue(undefined); .mockResolvedValue(undefined);
const result = await controller.deleteSurvey({ const result = await controller.deleteSurvey({
user: { username: 'testUser' },
surveyMeta, surveyMeta,
user: { username: 'testUser', _id: new ObjectId() },
}); });
expect(result).toEqual({ code: 200 });
expect(result).toEqual({
code: 200,
});
}); });
}); });
@ -192,19 +244,23 @@ describe('SurveyController', () => {
_id: surveyId, _id: surveyId,
surveyType: 'exam', surveyType: 'exam',
owner: 'testUser', owner: 'testUser',
}; } as SurveyMeta;
jest jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId') .spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue({} as any); .mockResolvedValue({
_id: new ObjectId(),
pageId: surveyId.toString(),
} as SurveyConf);
const request = {
user: { username: 'testUser', _id: new ObjectId() },
surveyMeta,
};
const result = await controller.getSurvey( const result = await controller.getSurvey(
{ surveyId: surveyId.toString() }, { surveyId: surveyId.toString() },
{ request,
surveyMeta,
user: { username: 'testUser', _id: new ObjectId() },
},
); );
expect(result?.data?.surveyMetaRes).toBeDefined(); expect(result?.data?.surveyMetaRes).toBeDefined();
expect(result?.data?.surveyConfRes).toBeDefined(); expect(result?.data?.surveyConfRes).toBeDefined();
}); });
@ -217,77 +273,28 @@ describe('SurveyController', () => {
_id: surveyId, _id: surveyId,
surveyType: 'exam', surveyType: 'exam',
owner: 'testUser', owner: 'testUser',
isDeleted: false, } as SurveyMeta;
};
jest jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId') .spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue({ .mockResolvedValue({
_id: new ObjectId(),
pageId: surveyId.toString(),
code: {}, code: {},
} as any); } as SurveyConf);
jest jest
.spyOn(surveyConfService, 'getSurveyContentByCode') .spyOn(surveyConfService, 'getSurveyContentByCode')
.mockResolvedValue({ text: '' }); .mockResolvedValue({ text: '' });
jest
.spyOn(surveyMetaService, 'publishSurveyMeta')
.mockResolvedValue(undefined);
const result = await controller.publishSurvey( const result = await controller.publishSurvey(
{ surveyId: surveyId.toString() }, { surveyId: surveyId.toString() },
{ surveyMeta, user: { username: 'testUser', _id: new ObjectId() } }, {
user: { username: 'testUser', _id: new ObjectId() },
surveyMeta,
},
); );
expect(result.code).toBe(200); expect(result.code).toBe(200);
}); });
it('should throw an error if the survey is deleted', async () => {
const surveyId = new ObjectId();
const surveyMeta = { _id: surveyId, isDeleted: true };
await expect(
controller.publishSurvey(
{ surveyId: surveyId.toString() },
{ surveyMeta, user: { username: 'testUser' } },
),
).rejects.toThrow(HttpException);
});
});
// New tests for additional methods
describe('pausingSurvey', () => {
it('should pause the survey successfully', async () => {
const surveyMeta = { surveyPath: 'some/path' };
jest
.spyOn(surveyMetaService, 'pausingSurveyMeta')
.mockResolvedValue(undefined);
jest
.spyOn(responseSchemaService, 'pausingResponseSchema')
.mockResolvedValue(undefined);
const result = await controller.pausingSurvey({
surveyMeta,
user: { username: 'testUser' },
});
expect(result.code).toBe(200);
});
});
describe('getPreviewSchema', () => {
it('should get the preview schema successfully', async () => {
const surveyId = new ObjectId();
jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue({} as any);
jest.spyOn(surveyMetaService, 'getSurveyById').mockResolvedValue({
title: 'Test Survey',
surveyPath: 'some/path',
} as any);
const result = await controller.getPreviewSchema({
surveyPath: surveyId.toString(),
});
expect(result.code).toBe(200);
});
}); });
}); });

View File

@ -121,9 +121,9 @@ describe('SurveyHistoryService', () => {
}, },
take: 100, take: 100,
order: { order: {
createdAt: -1, createDate: -1,
}, },
select: ['createdAt', 'operator', 'type', '_id'], select: ['createDate', 'operator', 'type', '_id'],
}); });
}); });
}); });

View File

@ -59,25 +59,18 @@ describe('SurveyMetaController', () => {
remark: '', remark: '',
}; };
const mockUser = {
username: 'test-user',
_id: new ObjectId(),
};
const req = { const req = {
user: mockUser, user: {
username: 'test-user',
},
surveyMeta: survey, surveyMeta: survey,
}; };
const result = await controller.updateMeta(reqBody, req); const result = await controller.updateMeta(reqBody, req);
expect(surveyMetaService.editSurveyMeta).toHaveBeenCalledWith({ expect(surveyMetaService.editSurveyMeta).toHaveBeenCalledWith({
operator: mockUser.username, title: reqBody.title,
operatorId: mockUser._id.toString(), remark: reqBody.remark,
survey: {
title: reqBody.title,
remark: reqBody.remark,
},
}); });
expect(result).toEqual({ code: 200 }); expect(result).toEqual({ code: 200 });
@ -123,8 +116,8 @@ describe('SurveyMetaController', () => {
data: [ data: [
{ {
_id: new ObjectId(), _id: new ObjectId(),
createdAt: date, createDate: date,
updatedAt: date, updateDate: date,
curStatus: { curStatus: {
date: date, date: date,
}, },
@ -145,7 +138,7 @@ describe('SurveyMetaController', () => {
count: 10, count: 10,
data: expect.arrayContaining([ data: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
createdAt: expect.stringMatching( createDate: expect.stringMatching(
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
), ),
curStatus: expect.objectContaining({ curStatus: expect.objectContaining({
@ -190,7 +183,7 @@ describe('SurveyMetaController', () => {
condition: [{ field: 'surveyType', value: 'normal' }], condition: [{ field: 'surveyType', value: 'normal' }],
}, },
]), ]),
order: JSON.stringify([{ field: 'createdAt', value: -1 }]), order: JSON.stringify([{ field: 'createDate', value: -1 }]),
}; };
const userId = new ObjectId().toString(); const userId = new ObjectId().toString();
const req = { const req = {
@ -210,7 +203,7 @@ describe('SurveyMetaController', () => {
surveyIdList: [], surveyIdList: [],
userId, userId,
filter: { surveyType: 'normal', title: { $regex: 'hahah' } }, filter: { surveyType: 'normal', title: { $regex: 'hahah' } },
order: { createdAt: -1 }, order: { createDate: -1 },
workspaceId: undefined, workspaceId: undefined,
}); });
}); });

View File

@ -2,10 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
import { SurveyMetaService } from '../services/surveyMeta.service'; import { SurveyMetaService } from '../services/surveyMeta.service';
import { MongoRepository } from 'typeorm'; import { MongoRepository } from 'typeorm';
import { SurveyMeta } from 'src/models/surveyMeta.entity'; import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
import { PluginManager } from 'src/securityPlugin/pluginManager'; import { PluginManager } from 'src/securityPlugin/pluginManager';
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
import { getRepositoryToken } from '@nestjs/typeorm'; import { getRepositoryToken } from '@nestjs/typeorm';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums'; import { SurveyUtilPlugin } from 'src/securityPlugin/surveyUtilPlugin';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
describe('SurveyMetaService', () => { describe('SurveyMetaService', () => {
@ -24,11 +26,10 @@ describe('SurveyMetaService', () => {
count: jest.fn(), count: jest.fn(),
create: jest.fn(), create: jest.fn(),
save: jest.fn(), save: jest.fn(),
updateOne: jest.fn(),
findAndCount: jest.fn(), findAndCount: jest.fn(),
}, },
}, },
PluginManager, PluginManagerProvider,
], ],
}).compile(); }).compile();
@ -37,18 +38,18 @@ describe('SurveyMetaService', () => {
getRepositoryToken(SurveyMeta), getRepositoryToken(SurveyMeta),
); );
pluginManager = module.get<PluginManager>(PluginManager); pluginManager = module.get<PluginManager>(PluginManager);
pluginManager.registerPlugin(new SurveyUtilPlugin());
}); });
describe('getNewSurveyPath', () => { describe('getNewSurveyPath', () => {
it('should generate a new survey path', async () => { it('should generate a new survey path', async () => {
jest.spyOn(pluginManager, 'triggerHook').mockResolvedValueOnce('path1'); jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(1);
jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(0); jest.spyOn(surveyRepository, 'count').mockResolvedValueOnce(0);
const surveyPath = await service.getNewSurveyPath(); const surveyPath = await service.getNewSurveyPath();
expect(surveyPath).toBe('path1'); expect(typeof surveyPath).toBe('string');
expect(pluginManager.triggerHook).toHaveBeenCalledTimes(1); expect(surveyRepository.count).toHaveBeenCalledTimes(2);
expect(surveyRepository.count).toHaveBeenCalledTimes(1);
}); });
}); });
@ -62,11 +63,14 @@ describe('SurveyMetaService', () => {
userId: new ObjectId().toString(), userId: new ObjectId().toString(),
createMethod: '', createMethod: '',
createFrom: '', createFrom: '',
workspaceId: 'workspace1',
}; };
const newSurvey = new SurveyMeta(); const newSurvey = new SurveyMeta();
jest.spyOn(service, 'getNewSurveyPath').mockResolvedValue('path1'); const mockedSurveyPath = 'mockedSurveyPath';
jest
.spyOn(service, 'getNewSurveyPath')
.mockResolvedValue(mockedSurveyPath);
jest jest
.spyOn(surveyRepository, 'create') .spyOn(surveyRepository, 'create')
.mockImplementation(() => newSurvey); .mockImplementation(() => newSurvey);
@ -78,118 +82,97 @@ describe('SurveyMetaService', () => {
title: params.title, title: params.title,
remark: params.remark, remark: params.remark,
surveyType: params.surveyType, surveyType: params.surveyType,
surveyPath: 'path1', surveyPath: mockedSurveyPath,
creator: params.username, creator: params.username,
creatorId: params.userId,
owner: params.username,
ownerId: params.userId, ownerId: params.userId,
owner: params.username,
createMethod: params.createMethod, createMethod: params.createMethod,
createFrom: params.createFrom, createFrom: params.createFrom,
workspaceId: params.workspaceId,
}); });
expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey); expect(surveyRepository.save).toHaveBeenCalledWith(newSurvey);
expect(result).toEqual(newSurvey); expect(result).toEqual(newSurvey);
}); });
}); });
describe('pausingSurveyMeta', () => {
it('should throw an exception if survey is in NEW status', async () => {
const survey = new SurveyMeta();
survey.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
await expect(service.pausingSurveyMeta(survey)).rejects.toThrow(
HttpException,
);
});
it('should pause a survey and update subStatus', async () => {
const survey = new SurveyMeta();
survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() };
survey.statusList = [];
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
const result = await service.pausingSurveyMeta(survey);
expect(survey.subStatus.status).toBe(RECORD_SUB_STATUS.PAUSING);
expect(survey.statusList.length).toBe(1);
expect(survey.statusList[0].status).toBe(RECORD_SUB_STATUS.PAUSING);
expect(surveyRepository.save).toHaveBeenCalledWith(survey);
expect(result).toEqual(survey);
});
});
describe('editSurveyMeta', () => { describe('editSurveyMeta', () => {
it('should edit a survey meta and return it', async () => { it('should edit a survey meta and return it if in NEW or EDITING status', async () => {
const survey = new SurveyMeta(); const survey = new SurveyMeta();
survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() }; survey.curStatus = { status: RECORD_STATUS.PUBLISHED, date: Date.now() };
survey.subStatus = {
status: RECORD_SUB_STATUS.DEFAULT,
date: Date.now(),
};
survey.statusList = []; survey.statusList = [];
const operator = 'editor';
const operatorId = 'editorId';
jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey); jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
const result = await service.editSurveyMeta({ const result = await service.editSurveyMeta(survey);
survey,
operator,
operatorId,
});
expect(survey.curStatus.status).toBe(RECORD_STATUS.EDITING); expect(survey.curStatus.status).toEqual(RECORD_STATUS.EDITING);
expect(survey.statusList.length).toBe(1); expect(survey.statusList.length).toBe(1);
expect(survey.statusList[0].status).toBe(RECORD_STATUS.EDITING); expect(survey.statusList[0].status).toEqual(RECORD_STATUS.EDITING);
expect(survey.operator).toBe(operator);
expect(survey.operatorId).toBe(operatorId);
expect(surveyRepository.save).toHaveBeenCalledWith(survey); expect(surveyRepository.save).toHaveBeenCalledWith(survey);
expect(result).toEqual(survey); expect(result).toEqual(survey);
}); });
}); });
describe('deleteSurveyMeta', () => { describe('deleteSurveyMeta', () => {
it('should mark a survey as deleted', async () => { it('should delete survey meta and update status', async () => {
const surveyId = new ObjectId().toString(); // 准备假的SurveyMeta对象
const operator = 'deleter'; const survey = new SurveyMeta();
const operatorId = 'deleterId'; survey.curStatus = { status: RECORD_STATUS.NEW, date: Date.now() };
survey.subStatus = {
status: RECORD_SUB_STATUS.DEFAULT,
date: Date.now(),
};
survey.statusList = [];
jest.spyOn(surveyRepository, 'updateOne').mockResolvedValue({ // 模拟save方法
matchedCount: 1, jest.spyOn(surveyRepository, 'save').mockResolvedValue(survey);
modifiedCount: 1,
acknowledged: true,
});
const result = await service.deleteSurveyMeta({ // 调用要测试的方法
surveyId, const result = await service.deleteSurveyMeta(survey);
operator,
operatorId,
});
expect(surveyRepository.updateOne).toHaveBeenCalledWith( // 验证结果
{ _id: new ObjectId(surveyId) }, expect(result).toBe(survey);
{ expect(survey.subStatus.status).toBe(RECORD_STATUS.REMOVED);
$set: { expect(survey.statusList.length).toBe(1);
isDeleted: true, expect(survey.statusList[0].status).toBe(RECORD_STATUS.REMOVED);
operator, expect(surveyRepository.save).toHaveBeenCalledTimes(1);
operatorId, expect(surveyRepository.save).toHaveBeenCalledWith(survey);
deletedAt: expect.any(Date), });
},
}, it('should throw exception when survey is already removed', async () => {
// 准备假的SurveyMeta对象其状态已设置为REMOVED
const survey = new SurveyMeta();
survey.curStatus = {
status: RECORD_STATUS.REMOVED,
date: Date.now(),
};
// 调用要测试的方法并期待异常
await expect(service.deleteSurveyMeta(survey)).rejects.toThrow(
HttpException,
); );
expect(result.matchedCount).toBe(1);
// 验证save方法没有被调用
expect(surveyRepository.save).not.toHaveBeenCalled();
}); });
}); });
describe('getSurveyMetaList', () => { describe('getSurveyMetaList', () => {
it('should return a list of survey metadata', async () => { it('should return a list of survey metadata', async () => {
// 准备模拟数据
const mockData = [ const mockData = [
{ _id: 1, title: 'Survey 1' }, { _id: 1, title: 'Survey 1' },
{ _id: 2, title: 'Survey 2' },
] as unknown as Array<SurveyMeta>; ] as unknown as Array<SurveyMeta>;
const mockCount = 1; const mockCount = 2;
jest jest
.spyOn(surveyRepository, 'findAndCount') .spyOn(surveyRepository, 'findAndCount')
.mockResolvedValue([mockData, mockCount]); .mockResolvedValue([mockData, mockCount]);
// 调用方法并检查返回值
const condition = { const condition = {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
@ -198,47 +181,44 @@ describe('SurveyMetaService', () => {
filter: {}, filter: {},
order: {}, order: {},
}; };
const result = await service.getSurveyMetaList(condition); const result = await service.getSurveyMetaList(condition);
// 验证返回值
expect(result).toEqual({ data: mockData, count: mockCount }); expect(result).toEqual({ data: mockData, count: mockCount });
// 验证repository方法被正确调用
expect(surveyRepository.findAndCount).toHaveBeenCalledTimes(1); expect(surveyRepository.findAndCount).toHaveBeenCalledTimes(1);
}); });
}); });
describe('publishSurveyMeta', () => { describe('publishSurveyMeta', () => {
it('should publish a survey and update curStatus', async () => { it('should publish a survey meta and add status to statusList', async () => {
const surveyMeta = new SurveyMeta(); // 准备模拟数据
surveyMeta.statusList = []; const surveyMeta = {
id: 1,
title: 'Test Survey',
statusList: [],
} as unknown as SurveyMeta;
const savedSurveyMeta = {
...surveyMeta,
curStatus: {
status: RECORD_STATUS.PUBLISHED,
date: expect.any(Number),
},
subStatus: {
status: RECORD_SUB_STATUS.DEFAULT,
date: expect.any(Number),
},
} as unknown as SurveyMeta;
jest.spyOn(surveyRepository, 'save').mockResolvedValue(surveyMeta); jest.spyOn(surveyRepository, 'save').mockResolvedValue(savedSurveyMeta);
// 调用方法并检查返回值
const result = await service.publishSurveyMeta({ surveyMeta }); const result = await service.publishSurveyMeta({ surveyMeta });
expect(surveyMeta.curStatus.status).toBe(RECORD_STATUS.PUBLISHED); // 验证返回值
expect(surveyMeta.statusList.length).toBe(1); expect(result).toEqual(savedSurveyMeta);
expect(surveyMeta.statusList[0].status).toBe(RECORD_STATUS.PUBLISHED); // 验证repository方法被正确调用
expect(surveyRepository.save).toHaveBeenCalledWith(surveyMeta); expect(surveyRepository.save).toHaveBeenCalledWith(savedSurveyMeta);
expect(result).toEqual(surveyMeta);
});
});
describe('countSurveyMetaByWorkspaceId', () => {
it('should return the count of surveys in a workspace', async () => {
const workspaceId = 'workspace1';
const mockCount = 5;
jest.spyOn(surveyRepository, 'count').mockResolvedValue(mockCount);
const result = await service.countSurveyMetaByWorkspaceId({
workspaceId,
});
expect(result).toBe(mockCount);
expect(surveyRepository.count).toHaveBeenCalledWith({
workspaceId,
isDeleted: { $ne: true },
});
}); });
}); });
}); });

View File

@ -17,8 +17,8 @@ import { DOWNLOAD_TASK_STATUS } from 'src/enums/downloadTaskStatus';
@Injectable() @Injectable()
export class DownloadTaskService { export class DownloadTaskService {
static taskList: Array<any> = []; private static taskList: Array<any> = [];
static isExecuting: boolean = false; private static isExecuting: boolean = false;
constructor( constructor(
@InjectRepository(DownloadTask) @InjectRepository(DownloadTask)
@ -153,7 +153,7 @@ export class DownloadTaskService {
} }
} }
async handleDownloadTask({ taskInfo }) { private async handleDownloadTask({ taskInfo }) {
try { try {
// 更新任务状态为计算中 // 更新任务状态为计算中
const updateRes = await this.downloadTaskRepository.updateOne( const updateRes = await this.downloadTaskRepository.updateOne(

View File

@ -39,8 +39,8 @@
"showSpliter": true, "showSpliter": true,
"placeholder": "", "placeholder": "",
"isRequired": true, "isRequired": true,
"starMin": "", "min": "",
"starMax": "", "max": "",
"type": "radio-star", "type": "radio-star",
"title": "标题2" "title": "标题2"
} }

View File

@ -21,7 +21,6 @@ describe('ClientEncryptService', () => {
save: jest.fn(), save: jest.fn(),
findOne: jest.fn(), findOne: jest.fn(),
updateOne: jest.fn(), updateOne: jest.fn(),
deleteOne: jest.fn(),
}, },
}, },
], ],
@ -106,13 +105,11 @@ describe('ClientEncryptService', () => {
describe('deleteEncryptInfo', () => { describe('deleteEncryptInfo', () => {
it('should delete encrypt info by id', async () => { it('should delete encrypt info by id', async () => {
const id = new ObjectId().toHexString(); const id = new ObjectId().toHexString();
const deleteResult = { matchedCount: 1, modifiedCount: 1 }; const updateResult = { matchedCount: 1, modifiedCount: 1 };
jest jest.spyOn(repository, 'updateOne').mockResolvedValue(updateResult);
.spyOn(repository, 'deleteOne')
.mockResolvedValue(deleteResult as any);
const result = await service.deleteEncryptInfo(id); const result = await service.deleteEncryptInfo(id);
expect(result).toEqual(deleteResult); expect(result).toEqual(updateResult);
}); });
}); });
}); });

View File

@ -50,7 +50,6 @@ describe('CounterService', () => {
surveyPath: 'testPath', surveyPath: 'testPath',
type: 'testType', type: 'testType',
data, data,
updatedAt: expect.any(Date),
}, },
}, },
{ upsert: true }, { upsert: true },

View File

@ -18,8 +18,8 @@ export const mockResponseSchema: ResponseSchema = {
date: 1710399368439, date: 1710399368439,
}, },
], ],
createdAt: 1710399368440, createDate: 1710399368440,
updatedAt: 1710399368440, updateDate: 1710399368440,
title: '加密全流程', title: '加密全流程',
surveyPath: 'EBzdmnSp', surveyPath: 'EBzdmnSp',
code: { code: {
@ -75,6 +75,11 @@ export const mockResponseSchema: ResponseSchema = {
is_again: true, is_again: true,
again_text: '确认要提交吗?', again_text: '确认要提交吗?',
}, },
jumpConfig: {
type: 'link',
link: '',
buttonText: '',
},
}, },
dataConf: { dataConf: {
dataList: [ dataList: [

View File

@ -3,28 +3,27 @@ import { ResponseSchemaController } from '../controllers/responseSchema.controll
import { ResponseSchemaService } from '../services/responseScheme.service'; import { ResponseSchemaService } from '../services/responseScheme.service';
import { HttpException } from 'src/exceptions/httpException'; import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { RECORD_SUB_STATUS } from 'src/enums'; import { RECORD_STATUS, RECORD_SUB_STATUS } from 'src/enums';
import { ResponseSchema } from 'src/models/responseSchema.entity'; import { ResponseSchema } from 'src/models/responseSchema.entity';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { UserService } from 'src/modules/auth/services/user.service'; import { UserService } from 'src/modules/auth/services/user.service';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { AuthService } from 'src/modules/auth/services/auth.service';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
jest.mock('../services/responseScheme.service'); jest.mock('../services/responseScheme.service');
jest.mock('src/modules/auth/services/user.service');
jest.mock('src/modules/workspace/services/workspaceMember.service');
describe('ResponseSchemaController', () => { describe('ResponseSchemaController', () => {
let controller: ResponseSchemaController; let controller: ResponseSchemaController;
let responseSchemaService: ResponseSchemaService; let responseSchemaService: ResponseSchemaService;
let userService: UserService;
let workspaceMemberService: WorkspaceMemberService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [ResponseSchemaController], controllers: [ResponseSchemaController],
providers: [ providers: [
ResponseSchemaService, ResponseSchemaService,
AuthService,
{ {
provide: Logger, provide: Logger,
useValue: { useValue: {
@ -43,6 +42,12 @@ describe('ResponseSchemaController', () => {
findAllByUserId: jest.fn(), findAllByUserId: jest.fn(),
}, },
}, },
{
provide: AuthService,
useValue: {
create: jest.fn(),
},
},
{ {
provide: Logger, provide: Logger,
useValue: { useValue: {
@ -56,10 +61,6 @@ describe('ResponseSchemaController', () => {
responseSchemaService = module.get<ResponseSchemaService>( responseSchemaService = module.get<ResponseSchemaService>(
ResponseSchemaService, ResponseSchemaService,
); );
userService = module.get<UserService>(UserService);
workspaceMemberService = module.get<WorkspaceMemberService>(
WorkspaceMemberService,
);
}); });
describe('getSchema', () => { describe('getSchema', () => {
@ -67,20 +68,13 @@ describe('ResponseSchemaController', () => {
const mockQueryInfo = { surveyPath: 'validSurveyPath' }; const mockQueryInfo = { surveyPath: 'validSurveyPath' };
const mockResponseSchema = { const mockResponseSchema = {
surveyPath: 'testSurveyPath', surveyPath: 'testSurveyPath',
curStatus: { status: 'published', date: Date.now() }, curStatus: { status: RECORD_STATUS.PUBLISHED, date: Date.now() },
subStatus: { status: RECORD_SUB_STATUS.DEFAULT, date: Date.now() }, subStatus: { status: RECORD_SUB_STATUS.DEFAULT, date: Date.now() },
code: {
baseConf: {
passwordSwitch: false,
password: null,
whitelist: [],
},
},
} as ResponseSchema; } as ResponseSchema;
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockResponseSchema); .mockResolvedValue(Promise.resolve(mockResponseSchema));
const result = await controller.getSchema(mockQueryInfo); const result = await controller.getSchema(mockQueryInfo);
@ -104,180 +98,168 @@ describe('ResponseSchemaController', () => {
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({ .mockResolvedValue({
isDeleted: true, subStatus: { status: RECORD_SUB_STATUS.REMOVED },
} as ResponseSchema); } as ResponseSchema);
await expect(controller.getSchema(mockQueryInfo)).rejects.toThrow( await expect(controller.getSchema(mockQueryInfo)).rejects.toThrow(
new HttpException( new HttpException('问卷已删除', EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED),
'问卷不存在或已删除',
EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED,
),
); );
}); });
it('should throw HttpException with RESPONSE_PAUSING code when survey is paused', async () => { it('whitelistValidate should throw SurveyNotFoundException when survey is removed', async () => {
const mockQueryInfo = { surveyPath: 'pausedSurveyPath' }; const surveyPath = '';
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: { status: 'published' },
subStatus: { status: RECORD_SUB_STATUS.PAUSING },
} as ResponseSchema);
await expect(controller.getSchema(mockQueryInfo)).rejects.toThrow(
new HttpException('该问卷已暂停回收', EXCEPTION_CODE.RESPONSE_PAUSING),
);
});
});
describe('whitelistValidate', () => {
it('should throw HttpException when parameters are invalid', async () => {
const surveyPath = 'testSurveyPath';
const body = { password: 1 };
await expect(
controller.whitelistValidate(surveyPath, body),
).rejects.toThrow(HttpException);
});
it('should throw SurveyNotFoundException when survey is removed', async () => {
const surveyPath = 'removedSurveyPath';
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(null); .mockResolvedValue(null);
await expect( await expect(
controller.whitelistValidate(surveyPath, { password: '123456' }), controller.whitelistValidate(surveyPath, {
password: '123456',
}),
).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交')); ).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交'));
}); });
it('should throw HttpException when password is incorrect', async () => { it('whitelistValidate should throw WHITELIST_ERROR code when password is incorrect', async () => {
const surveyPath = 'testSurveyPath'; const surveyPath = '';
const mockSchema = {
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
},
},
};
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any); .mockResolvedValue({
curStatus: {
await expect( status: 'published',
controller.whitelistValidate(surveyPath, { password: 'wrongPassword' }),
).rejects.toThrow(
new HttpException('密码验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
);
});
it('should validate successfully when password is correct', async () => {
const surveyPath = 'testSurveyPath';
const mockSchema = {
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
whitelistType: 'CUSTOM',
whitelist: ['allowed@example.com'],
}, },
}, subStatus: {
}; status: '',
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any);
const result = await controller.whitelistValidate(surveyPath, {
password: '123456',
whitelist: 'allowed@example.com',
});
expect(result).toEqual({ code: 200, data: null });
});
it('should throw HttpException when whitelist value is not in CUSTOM whitelist', async () => {
const surveyPath = 'testSurveyPath';
const mockSchema = {
code: {
baseConf: {
whitelistType: 'CUSTOM',
whitelist: ['allowed@example.com'],
}, },
}, code: {
}; baseConf: {
passwordSwitch: true,
jest password: '123456',
.spyOn(responseSchemaService, 'getResponseSchemaByPath') },
.mockResolvedValue(mockSchema as any); },
} as ResponseSchema);
await expect( await expect(
controller.whitelistValidate(surveyPath, { controller.whitelistValidate(surveyPath, {
password: '123456', password: '123457',
whitelist: 'notAllowed@example.com',
}), }),
).rejects.toThrow( ).rejects.toThrow(
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR), new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
); );
}); });
it('should throw HttpException when user is not found in MEMBER whitelist', async () => { it('whitelistValidate should be successfully', async () => {
const surveyPath = 'testSurveyPath'; const surveyPath = 'test';
const mockSchema = {
code: {
baseConf: {
whitelistType: 'MEMBER',
whitelist: [],
},
},
};
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any); .mockResolvedValue({
jest.spyOn(userService, 'getUserByUsername').mockResolvedValue(null); curStatus: {
status: 'published',
},
subStatus: {
status: '',
},
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
},
},
} as ResponseSchema);
await expect( await expect(
controller.whitelistValidate(surveyPath, { controller.whitelistValidate(surveyPath, {
password: '123456', password: '123456',
whitelist: 'nonExistentUser', }),
).resolves.toEqual({ code: 200, data: null });
});
it('whitelistValidate should throw WHITELIST_ERROR code when mobile or email is incorrect', async () => {
const surveyPath = '';
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: {
status: 'published',
},
subStatus: {
status: '',
},
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
whitelistType: 'CUSTOM',
memberType: 'MOBILE',
whitelist: ['13500000000'],
},
},
} as ResponseSchema);
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
whitelist: '13500000001',
}), }),
).rejects.toThrow( ).rejects.toThrow(
new HttpException('名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR), new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
); );
}); });
it('should throw HttpException when user is not a workspace member', async () => { it('whitelistValidate should throw WHITELIST_ERROR code when member is incorrect', async () => {
const surveyPath = 'testSurveyPath'; const surveyPath = '';
const mockSchema = {
code: {
baseConf: {
whitelistType: 'MEMBER',
whitelist: [],
},
},
};
jest jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath') .spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue(mockSchema as any); .mockResolvedValue({
jest curStatus: {
.spyOn(userService, 'getUserByUsername') status: 'published',
.mockResolvedValue({ _id: new Object(), username: '' } as any); },
jest subStatus: {
.spyOn(workspaceMemberService, 'findAllByUserId') status: '',
.mockResolvedValue([]); },
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
whitelistType: 'MEMBER',
whitelist: ['Jack'],
},
},
} as ResponseSchema);
await expect( await expect(
controller.whitelistValidate(surveyPath, { controller.whitelistValidate(surveyPath, {
password: '123456', password: '123456',
whitelist: 'testUser', whitelist: 'James',
}), }),
).rejects.toThrow( ).rejects.toThrow(
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
); );
}); });
}); });
it('whitelistValidate should return verifyId successfully', async () => {
const surveyPath = '';
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValue({
curStatus: {
status: 'published',
},
subStatus: {
status: '',
},
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
whitelistType: 'CUSTOM',
memberType: 'MOBILE',
whitelist: ['13500000000'],
},
},
} as ResponseSchema);
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
whitelist: '13500000000',
}),
).resolves.toEqual({ code: 200, data: null });
});
}); });

View File

@ -20,7 +20,6 @@ describe('ResponseSchemaService', () => {
findOne: jest.fn().mockResolvedValue(mockResponseSchema), findOne: jest.fn().mockResolvedValue(mockResponseSchema),
create: jest.fn(), create: jest.fn(),
save: jest.fn(), save: jest.fn(),
updateOne: jest.fn(),
}, },
}, },
], ],
@ -121,24 +120,22 @@ describe('ResponseSchemaService', () => {
describe('deleteResponseSchema', () => { describe('deleteResponseSchema', () => {
it('should delete response schema by survey path', async () => { it('should delete response schema by survey path', async () => {
jest jest
.spyOn(responseSchemaRepository, 'updateOne') .spyOn(responseSchemaRepository, 'findOne')
.mockResolvedValueOnce(cloneDeep(mockResponseSchema)); .mockResolvedValueOnce(cloneDeep(mockResponseSchema));
jest
.spyOn(responseSchemaRepository, 'save')
.mockResolvedValueOnce(undefined);
await service.deleteResponseSchema({ await service.deleteResponseSchema({
surveyPath: mockResponseSchema.surveyPath, surveyPath: mockResponseSchema.surveyPath,
}); });
expect(responseSchemaRepository.updateOne).toHaveBeenCalledWith( expect(responseSchemaRepository.findOne).toHaveBeenCalledWith({
{ where: {
surveyPath: mockResponseSchema.surveyPath, surveyPath: mockResponseSchema.surveyPath,
}, },
{ });
$set: { expect(responseSchemaRepository.save).toHaveBeenCalledTimes(1);
isDeleted: true,
updatedAt: expect.any(Date),
},
},
);
}); });
}); });
}); });

View File

@ -71,8 +71,8 @@ const mockClientEncryptInfo = {
date: 1710399425273.0, date: 1710399425273.0,
}, },
], ],
createdAt: 1710399425273.0, createDate: 1710399425273.0,
updatedAt: 1710399425273.0, updateDate: 1710399425273.0,
}; };
describe('SurveyResponseController', () => { describe('SurveyResponseController', () => {
@ -178,7 +178,7 @@ describe('SurveyResponseController', () => {
.mockResolvedValueOnce({ .mockResolvedValueOnce({
_id: new ObjectId('65fc2dd77f4520858046e129'), _id: new ObjectId('65fc2dd77f4520858046e129'),
clientTime: 1711025112552, clientTime: 1711025112552,
createdAt: 1711025113146, createDate: 1711025113146,
curStatus: { curStatus: {
status: RECORD_STATUS.NEW, status: RECORD_STATUS.NEW,
date: 1711025113146, date: 1711025113146,
@ -212,7 +212,7 @@ describe('SurveyResponseController', () => {
], ],
surveyPath: 'EBzdmnSp', surveyPath: 'EBzdmnSp',
updatedAt: 1711025113146, updateDate: 1711025113146,
secretKeys: [], secretKeys: [],
} as unknown as SurveyResponse); } as unknown as SurveyResponse);
jest jest

View File

@ -18,7 +18,6 @@ describe('SurveyResponseService', () => {
create: jest.fn(), create: jest.fn(),
save: jest.fn(), save: jest.fn(),
count: jest.fn(), count: jest.fn(),
find: jest.fn(),
}, },
}, },
], ],
@ -70,12 +69,16 @@ describe('SurveyResponseService', () => {
it('should get the total survey response count by path', async () => { it('should get the total survey response count by path', async () => {
const surveyPath = 'testPath'; const surveyPath = 'testPath';
const count = 10; const count = 10;
jest jest.spyOn(surveyResponseRepository, 'count').mockResolvedValue(count);
.spyOn(surveyResponseRepository, 'find')
.mockResolvedValue(new Array(10));
const result = await service.getSurveyResponseTotalByPath(surveyPath); const result = await service.getSurveyResponseTotalByPath(surveyPath);
expect(result).toEqual(count); expect(result).toEqual(count);
expect(surveyResponseRepository.count).toHaveBeenCalledWith({
where: {
surveyPath,
'subStatus.status': { $ne: 'removed' },
},
});
}); });
}); });

View File

@ -12,7 +12,6 @@ import { UserService } from 'src/modules/auth/services/user.service';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { Logger } from 'src/logger'; import { Logger } from 'src/logger';
import { User } from 'src/models/user.entity'; import { User } from 'src/models/user.entity';
import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto';
jest.mock('src/guards/authentication.guard'); jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard'); jest.mock('src/guards/survey.guard');
@ -23,7 +22,6 @@ describe('WorkspaceController', () => {
let workspaceService: WorkspaceService; let workspaceService: WorkspaceService;
let workspaceMemberService: WorkspaceMemberService; let workspaceMemberService: WorkspaceMemberService;
let userService: UserService; let userService: UserService;
let logger: Logger;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@ -33,6 +31,7 @@ describe('WorkspaceController', () => {
provide: WorkspaceService, provide: WorkspaceService,
useValue: { useValue: {
create: jest.fn(), create: jest.fn(),
findAllById: jest.fn(),
findAllByIdWithPagination: jest.fn(), findAllByIdWithPagination: jest.fn(),
update: jest.fn(), update: jest.fn(),
delete: jest.fn(), delete: jest.fn(),
@ -48,6 +47,7 @@ describe('WorkspaceController', () => {
batchUpdate: jest.fn(), batchUpdate: jest.fn(),
batchDelete: jest.fn(), batchDelete: jest.fn(),
countByWorkspaceId: jest.fn(), countByWorkspaceId: jest.fn(),
batchSearchByWorkspace: jest.fn(),
}, },
}, },
{ {
@ -80,26 +80,24 @@ describe('WorkspaceController', () => {
WorkspaceMemberService, WorkspaceMemberService,
); );
userService = module.get<UserService>(UserService); userService = module.get<UserService>(UserService);
logger = module.get<Logger>(Logger);
}); });
describe('create', () => { describe('create', () => {
it('should create a workspace and return workspaceId', async () => { it('should create a workspace and return workspaceId', async () => {
const mockUserId = new ObjectId(),
mockUsername = 'username';
const createWorkspaceDto: CreateWorkspaceDto = { const createWorkspaceDto: CreateWorkspaceDto = {
name: 'Test Workspace', name: 'Test Workspace',
description: 'Test Description', description: 'Test Description',
members: [{ userId: mockUserId.toString(), role: WORKSPACE_ROLE.USER }], members: [{ userId: 'userId1', role: WORKSPACE_ROLE.USER }],
}; };
const req = { user: { _id: new ObjectId(), username: 'testuser' } }; const req = { user: { _id: new ObjectId() } };
const createdWorkspace = { _id: new ObjectId() }; const createdWorkspace = { _id: new ObjectId() };
jest jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([
.spyOn(userService, 'getUserListByIds') {
.mockResolvedValue([ _id: 'userId1',
{ _id: mockUserId, username: mockUsername }, },
] as unknown as Array<User>); ] as unknown as Array<User>);
jest jest
.spyOn(workspaceService, 'create') .spyOn(workspaceService, 'create')
.mockResolvedValue(createdWorkspace as Workspace); .mockResolvedValue(createdWorkspace as Workspace);
@ -115,7 +113,6 @@ describe('WorkspaceController', () => {
expect(workspaceService.create).toHaveBeenCalledWith({ expect(workspaceService.create).toHaveBeenCalledWith({
name: createWorkspaceDto.name, name: createWorkspaceDto.name,
description: createWorkspaceDto.description, description: createWorkspaceDto.description,
owner: req.user.username,
ownerId: req.user._id.toString(), ownerId: req.user._id.toString(),
}); });
expect(workspaceMemberService.create).toHaveBeenCalledWith({ expect(workspaceMemberService.create).toHaveBeenCalledWith({
@ -126,31 +123,31 @@ describe('WorkspaceController', () => {
expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({ expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({
workspaceId: createdWorkspace._id.toString(), workspaceId: createdWorkspace._id.toString(),
members: createWorkspaceDto.members, members: createWorkspaceDto.members,
creator: req.user.username,
creatorId: req.user._id.toString(),
}); });
}); });
it('should throw an exception if validation fails', async () => { it('should throw an exception if validation fails', async () => {
const createWorkspaceDto = { name: '', members: [] }; const createWorkspaceDto: CreateWorkspaceDto = {
name: '',
members: [],
};
const req = { user: { _id: new ObjectId() } }; const req = { user: { _id: new ObjectId() } };
await expect(controller.create(createWorkspaceDto, req)).rejects.toThrow( await expect(controller.create(createWorkspaceDto, req)).rejects.toThrow(
HttpException, HttpException,
); );
expect(logger.error).toHaveBeenCalledTimes(1);
}); });
}); });
describe('findAll', () => { describe('findAll', () => {
it('should return a list of workspaces for the user', async () => { it('should return a list of workspaces for the user', async () => {
const req = { user: { _id: new ObjectId() } }; const req = { user: { _id: new ObjectId() } };
const queryInfo: GetWorkspaceListDto = { curPage: 1, pageSize: 10 };
const memberList = [{ workspaceId: new ObjectId().toString() }]; const memberList = [{ workspaceId: new ObjectId().toString() }];
const workspaces = [{ _id: new ObjectId(), name: 'Test Workspace' }]; const workspaces = [{ _id: new ObjectId(), name: 'Test Workspace' }];
jest jest
.spyOn(workspaceMemberService, 'findAllByUserId') .spyOn(workspaceMemberService, 'findAllByUserId')
.mockResolvedValue(memberList as Array<WorkspaceMember>); .mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
jest jest
.spyOn(workspaceService, 'findAllByIdWithPagination') .spyOn(workspaceService, 'findAllByIdWithPagination')
@ -159,11 +156,12 @@ describe('WorkspaceController', () => {
count: workspaces.length, count: workspaces.length,
}); });
jest jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([]);
.spyOn(userService, 'getUserListByIds')
.mockResolvedValue([{ _id: new ObjectId() }] as unknown as Array<User>);
const result = await controller.findAll(req, queryInfo); const result = await controller.findAll(req, {
curPage: 1,
pageSize: 10,
});
expect(result.code).toEqual(200); expect(result.code).toEqual(200);
expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({ expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({
@ -176,18 +174,6 @@ describe('WorkspaceController', () => {
name: undefined, name: undefined,
}); });
}); });
it('should throw an exception if validation fails', async () => {
const req = { user: { _id: new ObjectId() } };
const queryInfo: GetWorkspaceListDto = {
curPage: 'not_a_number',
pageSize: 10,
} as any;
await expect(controller.findAll(req, queryInfo)).rejects.toThrow(
HttpException,
);
});
}); });
describe('update', () => { describe('update', () => {
@ -199,9 +185,11 @@ describe('WorkspaceController', () => {
adminMembers: [], adminMembers: [],
userMembers: [], userMembers: [],
}; };
jest jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([
.spyOn(userService, 'getUserListByIds') {
.mockResolvedValue([{ _id: userId }] as Array<User>); _id: userId,
},
] as Array<User>);
const updateDto = { const updateDto = {
name: 'Updated Workspace', name: 'Updated Workspace',
members: [ members: [
@ -215,41 +203,76 @@ describe('WorkspaceController', () => {
jest.spyOn(workspaceService, 'update').mockResolvedValue(updateResult); jest.spyOn(workspaceService, 'update').mockResolvedValue(updateResult);
jest.spyOn(workspaceMemberService, 'batchCreate').mockResolvedValue(null); jest.spyOn(workspaceMemberService, 'batchCreate').mockResolvedValue(null);
jest.spyOn(workspaceMemberService, 'batchUpdate').mockResolvedValue(null); jest.spyOn(workspaceMemberService, 'batchUpdate').mockResolvedValue(null);
jest.spyOn(workspaceMemberService, 'batchDelete').mockResolvedValue(null);
const result = await controller.update(id, updateDto, { const result = await controller.update(id, updateDto);
user: { username: 'testuser', _id: new ObjectId() },
expect(result).toEqual({
code: 200,
}); });
expect(workspaceService.update).toHaveBeenCalledWith(id, {
expect(result).toEqual({ code: 200 }); name: updateDto.name,
expect(workspaceService.update).toHaveBeenCalledWith({
id,
workspace: { name: updateDto.name },
operator: 'testuser',
operatorId: expect.any(String),
}); });
expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({ expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({
workspaceId: id, workspaceId: id,
members: members.newMembers, members: members.newMembers,
creator: 'testuser',
creatorId: expect.any(String),
}); });
expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({ expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({
idList: members.adminMembers, idList: members.adminMembers,
role: WORKSPACE_ROLE.ADMIN, role: WORKSPACE_ROLE.ADMIN,
operator: 'testuser',
operatorId: expect.any(String),
}); });
expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({ expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({
idList: members.userMembers, idList: members.userMembers,
role: WORKSPACE_ROLE.USER, role: WORKSPACE_ROLE.USER,
operator: 'testuser',
operatorId: expect.any(String),
});
expect(workspaceMemberService.batchDelete).toHaveBeenCalledWith({
idList: [],
neIdList: [],
}); });
}); });
}); });
describe('delete', () => {
it('should delete a workspace', async () => {
const id = 'workspaceId';
jest.spyOn(workspaceService, 'delete').mockResolvedValue(null);
const result = await controller.delete(id);
expect(result).toEqual({ code: 200 });
expect(workspaceService.delete).toHaveBeenCalledWith(id);
});
});
describe('getWorkspaceAndMember', () => {
it('should return a list of workspaces and members for the user', async () => {
const userId = new ObjectId();
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([
{
_id: userId,
},
] as Array<User>);
const req = { user: { _id: userId } };
const workspaceId = new ObjectId();
const memberList = [{ workspaceId, userId }];
const workspaces = [{ _id: workspaceId, name: 'Test Workspace' }];
const userList = [{ _id: userId, username: 'Test User' }];
jest
.spyOn(workspaceService, 'findAllByUserId')
.mockResolvedValue(workspaces as Array<Workspace>);
jest
.spyOn(workspaceMemberService, 'batchSearchByWorkspace')
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
jest
.spyOn(userService, 'getUserListByIds')
.mockResolvedValue(userList as User[]);
const result = await controller.getWorkspaceAndMember(req);
expect(result.code).toEqual(200);
expect(workspaceService.findAllByUserId).toHaveBeenCalledWith(
req.user._id.toString(),
);
expect(
workspaceMemberService.batchSearchByWorkspace,
).toHaveBeenCalledWith(workspaces.map((item) => item._id.toString()));
});
});
}); });

View File

@ -45,7 +45,6 @@ describe('WorkspaceService', () => {
const workspace = { const workspace = {
name: 'Test Workspace', name: 'Test Workspace',
description: 'Description', description: 'Description',
owner: 'Test Owner', // 添加 owner 属性
ownerId: 'ownerId', ownerId: 'ownerId',
}; };
const createdWorkspace = { ...workspace, _id: new ObjectId() }; const createdWorkspace = { ...workspace, _id: new ObjectId() };
@ -60,11 +59,7 @@ describe('WorkspaceService', () => {
const result = await service.create(workspace); const result = await service.create(workspace);
expect(result).toEqual(createdWorkspace); expect(result).toEqual(createdWorkspace);
expect(workspaceRepository.create).toHaveBeenCalledWith({ expect(workspaceRepository.create).toHaveBeenCalledWith(workspace);
...workspace,
creator: workspace.owner,
creatorId: workspace.ownerId,
});
expect(workspaceRepository.save).toHaveBeenCalledWith(createdWorkspace); expect(workspaceRepository.save).toHaveBeenCalledWith(createdWorkspace);
}); });
}); });
@ -95,35 +90,24 @@ describe('WorkspaceService', () => {
it('should update a workspace', async () => { it('should update a workspace', async () => {
const workspaceId = 'workspaceId'; const workspaceId = 'workspaceId';
const updateData = { name: 'Updated Workspace' }; const updateData = { name: 'Updated Workspace' };
const operator = 'Test Operator';
const operatorId = 'operatorId';
jest jest
.spyOn(workspaceRepository, 'update') .spyOn(workspaceRepository, 'update')
.mockResolvedValue({ affected: 1 } as any); .mockResolvedValue({ affected: 1 } as any);
const result = await service.update({ const result = await service.update(workspaceId, updateData);
id: workspaceId,
workspace: updateData,
operator,
operatorId,
});
expect(result).toEqual({ affected: 1 }); expect(result).toEqual({ affected: 1 });
expect(workspaceRepository.update).toHaveBeenCalledWith(workspaceId, { expect(workspaceRepository.update).toHaveBeenCalledWith(
...updateData, workspaceId,
updatedAt: expect.any(Date), updateData,
operator, );
operatorId,
});
}); });
}); });
describe('delete', () => { describe('delete', () => {
it('should delete a workspace and update related surveyMeta', async () => { it('should delete a workspace and update related surveyMeta', async () => {
const workspaceId = new ObjectId().toString(); const workspaceId = new ObjectId().toString();
const operator = 'Test Operator';
const operatorId = 'operatorId';
jest jest
.spyOn(workspaceRepository, 'updateOne') .spyOn(workspaceRepository, 'updateOne')
@ -132,17 +116,11 @@ describe('WorkspaceService', () => {
.spyOn(surveyMetaRepository, 'updateMany') .spyOn(surveyMetaRepository, 'updateMany')
.mockResolvedValue({ modifiedCount: 1 } as any); .mockResolvedValue({ modifiedCount: 1 } as any);
const result = await service.delete(workspaceId, { await service.delete(workspaceId);
operator,
operatorId,
});
expect(workspaceRepository.updateOne).toHaveBeenCalledTimes(1); expect(workspaceRepository.updateOne).toHaveBeenCalledTimes(1);
expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1); expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1);
expect(result).toEqual({
workspaceRes: { modifiedCount: 1 },
surveyRes: { modifiedCount: 1 },
});
}); });
}); });
@ -161,43 +139,9 @@ describe('WorkspaceService', () => {
.spyOn(workspaceRepository, 'find') .spyOn(workspaceRepository, 'find')
.mockResolvedValue(workspaces as any); .mockResolvedValue(workspaces as any);
const result = await service.findAllByUserId('userId'); const result = await service.findAllByUserId('');
expect(result).toEqual(workspaces); expect(result).toEqual(workspaces);
expect(workspaceRepository.find).toHaveBeenCalledTimes(1); expect(workspaceRepository.find).toHaveBeenCalledTimes(1);
}); });
}); });
describe('findAllByIdWithPagination', () => {
it('should return paginated workspaces', async () => {
const workspaceIdList = [
new ObjectId().toString(),
new ObjectId().toString(),
];
const page = 1;
const limit = 10;
const workspaces = [
{ _id: workspaceIdList[0], name: 'Workspace 1' },
{ _id: workspaceIdList[1], name: 'Workspace 2' },
];
jest
.spyOn(workspaceRepository, 'findAndCount')
.mockResolvedValue([workspaces, workspaces.length] as any);
const result = await service.findAllByIdWithPagination({
workspaceIdList,
page,
limit,
});
expect(result).toEqual({ list: workspaces, count: workspaces.length });
expect(workspaceRepository.findAndCount).toHaveBeenCalledWith({
where: expect.any(Object),
skip: 0,
take: limit,
order: { createdAt: -1 },
});
});
});
}); });

View File

@ -115,16 +115,11 @@ describe('WorkspaceMemberController', () => {
}; };
const updateResult = { modifiedCount: 1 }; const updateResult = { modifiedCount: 1 };
// Mock request object
const req = {
user: { username: 'admin', _id: 'operatorId' },
};
jest jest
.spyOn(workspaceMemberService, 'updateRole') .spyOn(workspaceMemberService, 'updateRole')
.mockResolvedValue(updateResult); .mockResolvedValue(updateResult);
const result = await controller.updateRole(updateDto, req); const result = await controller.updateRole(updateDto);
expect(result).toEqual({ expect(result).toEqual({
code: 200, code: 200,
@ -133,11 +128,9 @@ describe('WorkspaceMemberController', () => {
}, },
}); });
expect(workspaceMemberService.updateRole).toHaveBeenCalledWith({ expect(workspaceMemberService.updateRole).toHaveBeenCalledWith({
role: updateDto.role,
workspaceId: updateDto.workspaceId, workspaceId: updateDto.workspaceId,
userId: updateDto.userId, userId: updateDto.userId,
operator: req.user.username, role: updateDto.role,
operatorId: req.user._id.toString(),
}); });
}); });
@ -147,11 +140,8 @@ describe('WorkspaceMemberController', () => {
userId: '', userId: '',
role: '', role: '',
}; };
const req = {
user: { username: 'admin', _id: 'operatorId' },
};
await expect(controller.updateRole(updateDto, req)).rejects.toThrow( await expect(controller.updateRole(updateDto)).rejects.toThrow(
HttpException, HttpException,
); );
}); });

View File

@ -58,53 +58,23 @@ describe('WorkspaceMemberService', () => {
{ userId: 'userId1', role: 'admin' }, { userId: 'userId1', role: 'admin' },
{ userId: 'userId2', role: 'user' }, { userId: 'userId2', role: 'user' },
]; ];
const creator = 'creatorName'; const dataToInsert = members.map((item) => ({ ...item, workspaceId }));
const creatorId = 'creatorId';
const now = new Date();
const dataToInsert = members.map((item) => ({
...item,
workspaceId,
createdAt: now,
updatedAt: now,
creator,
creatorId,
}));
jest.spyOn(repository, 'insertMany').mockResolvedValueOnce({ jest
insertedCount: members.length, .spyOn(repository, 'insertMany')
} as any); .mockResolvedValueOnce({ insertedCount: members.length } as any);
const result = await service.batchCreate({ const result = await service.batchCreate({ workspaceId, members });
workspaceId,
members,
creator,
creatorId,
});
expect(result).toEqual({ insertedCount: members.length }); expect(result).toEqual({ insertedCount: members.length });
expect(repository.insertMany).toHaveBeenCalledWith( expect(repository.insertMany).toHaveBeenCalledWith(dataToInsert);
dataToInsert.map((item) => {
return {
...item,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
};
}),
);
}); });
it('should return insertedCount 0 if no members to insert', async () => { it('should return insertedCount 0 if no members to insert', async () => {
const workspaceId = new ObjectId().toString(); const workspaceId = new ObjectId().toString();
const members = []; const members = [];
const creator = 'creatorName';
const creatorId = 'creatorId';
const result = await service.batchCreate({ const result = await service.batchCreate({ workspaceId, members });
workspaceId,
members,
creator,
creatorId,
});
expect(result).toEqual({ insertedCount: 0 }); expect(result).toEqual({ insertedCount: 0 });
}); });
@ -114,19 +84,12 @@ describe('WorkspaceMemberService', () => {
it('should batch update workspace members roles', async () => { it('should batch update workspace members roles', async () => {
const idList = [new ObjectId().toString(), new ObjectId().toString()]; const idList = [new ObjectId().toString(), new ObjectId().toString()];
const role = 'user'; const role = 'user';
const operator = 'operatorName';
const operatorId = 'operatorId';
jest jest
.spyOn(repository, 'updateMany') .spyOn(repository, 'updateMany')
.mockResolvedValue({ modifiedCount: idList.length } as any); .mockResolvedValue({ modifiedCount: idList.length } as any);
const result = await service.batchUpdate({ const result = await service.batchUpdate({ idList, role });
idList,
role,
operator,
operatorId,
});
expect(result).toEqual({ modifiedCount: idList.length }); expect(result).toEqual({ modifiedCount: idList.length });
}); });
@ -134,15 +97,8 @@ describe('WorkspaceMemberService', () => {
it('should return modifiedCount 0 if no ids to update', async () => { it('should return modifiedCount 0 if no ids to update', async () => {
const idList = []; const idList = [];
const role = 'user'; const role = 'user';
const operator = 'operatorName';
const operatorId = 'operatorId';
const result = await service.batchUpdate({ const result = await service.batchUpdate({ idList, role });
idList,
role,
operator,
operatorId,
});
expect(result).toEqual({ modifiedCount: 0 }); expect(result).toEqual({ modifiedCount: 0 });
}); });
@ -204,25 +160,17 @@ describe('WorkspaceMemberService', () => {
const workspaceId = 'workspaceId'; const workspaceId = 'workspaceId';
const userId = 'userId'; const userId = 'userId';
const role = 'admin'; const role = 'admin';
const operator = 'operatorName';
const operatorId = 'operatorId';
jest jest
.spyOn(repository, 'updateOne') .spyOn(repository, 'updateOne')
.mockResolvedValue({ modifiedCount: 1 } as any); .mockResolvedValue({ modifiedCount: 1 } as any);
const result = await service.updateRole({ const result = await service.updateRole({ workspaceId, userId, role });
workspaceId,
userId,
role,
operator,
operatorId,
});
expect(result).toEqual({ modifiedCount: 1 }); expect(result).toEqual({ modifiedCount: 1 });
expect(repository.updateOne).toHaveBeenCalledWith( expect(repository.updateOne).toHaveBeenCalledWith(
{ workspaceId, userId }, { workspaceId, userId },
{ $set: { role, operator, operatorId, updatedAt: expect.any(Date) } }, { $set: { role } },
); );
}); });
}); });

View File

@ -1,5 +1,5 @@
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { getPushingData } from '../messagePushing'; import { getPushingData } from './messagePushing';
import { RECORD_STATUS } from 'src/enums'; import { RECORD_STATUS } from 'src/enums';
describe('getPushingData', () => { describe('getPushingData', () => {

View File

@ -23,12 +23,12 @@
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"element-plus": "^2.8.5", "element-plus": "^2.8.3",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"pinia": "2.2.7", "pinia": "^2.1.7",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"vue": "^3.4.15", "vue": "^3.4.15",
@ -57,7 +57,7 @@
"husky": "^9.0.11", "husky": "^9.0.11",
"npm-run-all2": "^6.1.1", "npm-run-all2": "^6.1.1",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"sass": "1.79.6", "sass": "1.77.6",
"typescript": "~5.3.0", "typescript": "~5.3.0",
"unplugin-auto-import": "^0.17.5", "unplugin-auto-import": "^0.17.5",
"unplugin-icons": "^0.18.5", "unplugin-icons": "^0.18.5",

View File

@ -13,7 +13,7 @@ export const getUserInfo = () => {
} }
/** 获取密码强度 */ /** 获取密码强度 */
export const getPasswordStrength = (password) => { export const getPasswordStrength = (password) => {
return axios.get('/auth/password/strength', { return axios.get('/auth/register/password/strength', {
params: { params: {
password password
} }

View File

@ -44,7 +44,7 @@
background background
layout="prev, pager, next" layout="prev, pager, next"
:total="total" :total="total"
size="small" small
:page-size="pageSize" :page-size="pageSize"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
> >

View File

@ -14,6 +14,7 @@
v-for="(content, contentIndex) in item.content" v-for="(content, contentIndex) in item.content"
:key="`${item.key}${contentIndex}`" :key="`${item.key}${contentIndex}`"
:form-config="content" :form-config="content"
v-show="isShowFormItem(content)"
> >
<Component <Component
:is="components[content.type]" :is="components[content.type]"
@ -82,6 +83,14 @@ const formFieldData = ref<Array<any>>([])
const init = ref<boolean>(true) const init = ref<boolean>(true)
const components = shallowRef<any>(props.customComponents || {}) const components = shallowRef<any>(props.customComponents || {})
const isShowFormItem = (content: any) => {
if (_isFunction(content.toggleShowFn)) {
return content.toggleShowFn(props.moduleConfig)
} else {
return true
}
}
const handleFormChange = (data: any, formConfig: any) => { const handleFormChange = (data: any, formConfig: any) => {
// //
if (_isFunction(formConfig?.valueSetter)) { if (_isFunction(formConfig?.valueSetter)) {

View File

@ -8,7 +8,7 @@
{{ saveText }} {{ saveText }}
</span> </span>
<i-ep-loading class="icon" v-if="autoSaveStatus === 'saving'" /> <i-ep-loading class="icon" v-if="autoSaveStatus === 'saving'" />
<i-ep-check class="icon succeed" v-if="autoSaveStatus === 'succeed'" /> <i-ep-check class="icon succeed" v-else-if="autoSaveStatus === 'succeed'" />
</div> </div>
</transition> </transition>
</div> </div>
@ -99,23 +99,20 @@ const triggerAutoSave = () => {
isShowAutoSave.value = true isShowAutoSave.value = true
nextTick(async () => { nextTick(async () => {
try { try {
const res: any = await doSave() const res: any = await handleSave()
if (res !== undefined) { if (res.code === 200) {
if (res.code === 200) { autoSaveStatus.value = 'succeed'
autoSaveStatus.value = 'succeed' } else {
} else { autoSaveStatus.value = 'failed'
autoSaveStatus.value = 'failed'
}
isShowAutoSave.value = true
} }
} catch (err) {
autoSaveStatus.value = 'failed'
isShowAutoSave.value = true
} finally {
setTimeout(() => { setTimeout(() => {
isShowAutoSave.value = false isShowAutoSave.value = false
timerHandle.value = null timerHandle.value = null
}, 300) }, 300)
} catch (err) {
autoSaveStatus.value = 'failed'
isShowAutoSave.value = true
} }
}) })
}, 2000) }, 2000)
@ -123,17 +120,6 @@ const triggerAutoSave = () => {
} }
const handleSave = async () => { const handleSave = async () => {
const res: any = await doSave()
if (res !== undefined && res.code === 200) {
ElMessage.success('保存成功')
}
}
/**
* 保存问卷
* @return 无返回时说明保存失败并由函数内部完成统一提示有返回时code为200为保存成功不为200时使用errmsg由外部实现错误信息展示
*/
const doSave = async () => {
if (isSaving.value) { if (isSaving.value) {
return return
} }
@ -155,6 +141,7 @@ const doSave = async () => {
return return
} }
if (res.code === 200) { if (res.code === 200) {
ElMessage.success('保存成功')
return res return res
} else if (res.code === 3006) { } else if (res.code === 3006) {
ElMessageBox.alert(res.errmsg, '提示', { ElMessageBox.alert(res.errmsg, '提示', {

View File

@ -5,7 +5,7 @@
</div> </div>
<SetterField <SetterField
class="question-config-form" class="question-config-form"
label-position="top" label-position="left"
:form-config-list="formFields" :form-config-list="formFields"
:module-config="moduleConfig" :module-config="moduleConfig"
@form-change="handleFormChange" @form-change="handleFormChange"
@ -71,4 +71,25 @@ const handleFormChange = ({ key, value }: any) => {
.question-config-form { .question-config-form {
padding: 30px 20px 50px 20px; padding: 30px 20px 50px 20px;
} }
:deep(.group-wrap) {
margin-bottom: 0;
&:not(:last-child) {
margin-bottom: 32px;
}
.group-title {
margin-bottom: 12px;
}
.el-form-item {
margin-bottom: 16px;
.el-radio {
height: initial;
line-height: initial;
margin-bottom: initial;
}
}
}
</style> </style>

View File

@ -5,7 +5,9 @@
<img src="/imgs/icons/success.webp" class="success-img" /> <img src="/imgs/icons/success.webp" class="success-img" />
<div class="title-msg" v-safe-html="successText"></div> <div class="title-msg" v-safe-html="successText"></div>
</div> </div>
<div class="bottom-btn"></div> <div v-if="jumpConfig.buttonText && jumpConfig.type === 'button'" class="jump-btn">
{{ jumpConfig.buttonText }}
</div>
</div> </div>
</div> </div>
</template> </template>
@ -17,6 +19,7 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const successText = computed(() => props.moduleConfig?.msgContent?.msg_200 || '') const successText = computed(() => props.moduleConfig?.msgContent?.msg_200 || '')
const jumpConfig = computed(() => props.moduleConfig?.jumpConfig || {})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/*成功页面跳转全屏展示浮层*/ /*成功页面跳转全屏展示浮层*/
@ -50,7 +53,18 @@ const successText = computed(() => props.moduleConfig?.msgContent?.msg_200 || ''
font-size: 0.36rem; font-size: 0.36rem;
} }
} }
.bottom-btn { .jump-btn {
height: 300px; background: var(--primary-color);
width: 90%;
border-radius: 0.08rem;
padding: 0.25rem 0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.25rem;
font-weight: 500;
margin: 0 auto;
border: none;
} }
</style> </style>

View File

@ -73,7 +73,6 @@ const handleChange = (name: any) => {
:deep(.el-tabs__content) { :deep(.el-tabs__content) {
height: calc(100% - 10px); height: calc(100% - 10px);
padding-bottom: 10px; padding-bottom: 10px;
overflow-y: auto;
:deep(el-tab-pane) { :deep(el-tab-pane) {
height: 100%; height: 100%;
overflow: auto; overflow: auto;

View File

@ -103,7 +103,7 @@ export default {
type: 'WhiteList', type: 'WhiteList',
custom: true, // 自定义导入高级组件 custom: true, // 自定义导入高级组件
relyFunc: (data) => { relyFunc: (data) => {
return data.whitelistType === 'CUSTOM' return data.whitelistType == 'CUSTOM'
} }
}, },
team_list: { team_list: {
@ -112,7 +112,7 @@ export default {
type: 'TeamMemberList', type: 'TeamMemberList',
custom: true, // 自定义导入高级组件 custom: true, // 自定义导入高级组件
relyFunc: (data) => { relyFunc: (data) => {
return data.whitelistType === 'MEMBER' return data.whitelistType == 'MEMBER'
} }
}, },
} }

View File

@ -1,7 +1,7 @@
export default { export default {
Success: [ Success: [
{ {
label: '提示文案', title: '提示文案',
type: 'RichText', type: 'RichText',
key: 'msgContent.msg_200', key: 'msgContent.msg_200',
placeholder: '提交成功', placeholder: '提交成功',
@ -9,6 +9,46 @@ export default {
labelStyle: { labelStyle: {
'font-weight': 'bold' 'font-weight': 'bold'
} }
},
{
title: '交卷跳转',
type: 'Customed',
key: 'jumpConfig',
content: [
{
key: 'jumpConfig.type',
type: 'RadioGroup',
value: 'link',
options: [
{
label: '跳转网页',
value: 'link'
},
{
label: '跳转按钮',
value: 'button'
},
],
},
{
key: 'jumpConfig.buttonText',
label: '按钮文案',
type: 'InputSetter',
placeholder: '请输入按钮文案',
value: '',
toggleShowFn: (data) => {
return data?.jumpConfig?.type === 'button'
},
},
{
key: 'jumpConfig.link',
label: '跳转链接',
type: 'InputSetter',
placeholder: '请输入网址',
value: '',
},
]
} }
], ],
OverTime: [ OverTime: [

View File

@ -17,9 +17,7 @@ export default function usePageEdit(
updateTime: () => void updateTime: () => void
) { ) {
const pageConf = computed(() => schema.pageConf) const pageConf = computed(() => schema.pageConf)
const pageEditOne = computed(() => { const pageEditOne = computed(() => schema.pageEditOne)
return schema.pageEditOne
})
const isFinallyPage = computed(() => { const isFinallyPage = computed(() => {
return pageEditOne.value === pageConf.value.length return pageEditOne.value === pageConf.value.length
}) })

View File

@ -64,6 +64,7 @@ export default defineComponent({
const onRadioClick = (item, $event) => { const onRadioClick = (item, $event) => {
$event && $event.stopPropagation() $event && $event.stopPropagation()
$event && $event.preventDefault() $event && $event.preventDefault()
if (!isChecked(item)) { if (!isChecked(item)) {
emit('change', item.hash) emit('change', item.hash)
} }

View File

@ -91,8 +91,10 @@ const questionConfig = computed(() => {
const updateFormData = (value) => { const updateFormData = (value) => {
const key = props.moduleConfig.field const key = props.moduleConfig.field
const formData = cloneDeep(formValues.value) const formData = cloneDeep(formValues.value)
formData[key] = value if (key in formData) {
console.log(formData) formData[key] = value
}
return formData return formData
} }

View File

@ -1,147 +0,0 @@
<template>
<el-dialog
v-model="whiteVisible"
title="验证"
:show-close="false"
class="verify-white-wrap"
width="315"
:close-on-press-escape="false"
:close-on-click-modal="false"
align-center
>
<template #header>
<div class="verify-white-head">
<div class="verify-white-title">验证</div>
<div v-if="whitelistTip" class="verify-white-tips">{{ whitelistTip }}</div>
</div>
</template>
<div class="verify-white-body">
<el-input
v-if="isPwd"
v-model="state.password"
class="wd255 mb16"
placeholder="请输入6位字符串类型访问密码"
/>
<el-input
v-if="isValue"
v-model="state.value"
class="wd255 mb16"
:placeholder="placeholder"
/>
<div class="submit-btn" @click="handleSubmit">验证并开始答题</div>
</div>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { validate } from '../api/survey'
import { useSurveyStore } from '../stores/survey'
const whiteVisible = ref(false)
const surveyStore = useSurveyStore()
const state = reactive({
password: '',
value: '',
is_submit: false
})
const baseConf = computed(() => store.state.baseConf || {})
const isPwd = computed(() => baseConf.value.passwordSwitch)
const whitelistType = computed(() => baseConf.value.whitelistType)
const memberType = computed(() => baseConf.value.memberType)
const whitelistTip = computed(() => baseConf.value.whitelistTip)
const surveyPath = computed(() => store.state?.surveyPath || '')
const isValue = computed(() => {
if (!whitelistType.value) return false
return whitelistType.value != 'ALL'
})
const placeholder = computed(() => {
if (whitelistType.value == 'MEMBER') {
return '请输入用户名'
}
if (memberType.value == 'MOBILE') {
return '请输入手机号'
}
if (memberType.value == 'EMAIL') {
return '请输入邮箱'
}
return ''
})
const handleSubmit = async () => {
if (state.is_submit) return
const params = {
surveyPath: surveyPath.value
}
if (isValue.value) {
params.whitelist = state.value
}
if (isPwd.value) {
params.password = state.password
}
const res = await validate(params)
if (res.code != 200) {
ElMessage.error(res.errmsg || '验证失败')
return
}
whiteVisible.value = false
surveyStore.setWhiteData(params)
}
watch(
() => baseConf.value,
() => {
if (whiteVisible.value) return
if (isValue.value || isPwd.value) {
whiteVisible.value = true
}
}
)
</script>
<style lang="scss" scoped>
.verify-white-wrap {
.verify-white-body {
padding: 0 14px;
}
.verify-white-head {
padding: 0 14px;
margin-bottom: 8px;
margin-top: 2px;
}
.mb16 {
margin-bottom: 16px;
}
.verify-white-tips {
text-align: center;
margin-top: 8px;
font-size: 14px;
color: #92949d;
}
.verify-white-title {
font-size: 16px;
color: #292a36;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.submit-btn {
background: #faa600;
border-radius: 2px;
width: 255px;
height: 32px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
margin-bottom: 14px;
}
}
</style>

View File

@ -16,13 +16,17 @@
> >
重新填写 重新填写
</router-link> </router-link>
<a v-if="showJumpButton" :href="jumpConfig.link" class="jump-btn">
{{ jumpConfig.buttonText }}
</a>
</div> </div>
<LogoIcon :logo-conf="logoConf" :readonly="true" /> <LogoIcon :logo-conf="logoConf" :readonly="true" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, ref, watchEffect } from 'vue'
import { useSurveyStore } from '../stores/survey' import { useSurveyStore } from '../stores/survey'
// @ts-ignore // @ts-ignore
import communalLoader from '@materials/communals/communalLoader.js' import communalLoader from '@materials/communals/communalLoader.js'
@ -37,6 +41,20 @@ const successMsg = computed(() => {
const msgContent = (surveyStore?.submitConf as any)?.msgContent || {} const msgContent = (surveyStore?.submitConf as any)?.msgContent || {}
return msgContent?.msg_200 || '提交成功' return msgContent?.msg_200 || '提交成功'
}) })
const jumpConfig = computed(() => {
return (surveyStore?.submitConf as any)?.jumpConfig || {}
})
const showJumpButton = ref(false)
watchEffect(() => {
const { jumpConfig } = (surveyStore?.submitConf || {}) as any
if (jumpConfig?.type === 'link' && jumpConfig?.link) {
window.location.href = jumpConfig.link
}
showJumpButton.value = jumpConfig?.type === 'button' && jumpConfig?.buttonText
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '@/render/styles/variable.scss'; @import '@/render/styles/variable.scss';
@ -87,5 +105,20 @@ const successMsg = computed(() => {
text-decoration: underline; text-decoration: underline;
display: block; display: block;
} }
.jump-btn {
background: var(--primary-color);
width: 90%;
border-radius: 0.08rem;
padding: 0.2rem 0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.3rem;
font-weight: 500;
margin: 0.5rem auto 0;
border: none;
}
} }
</style> </style>

View File

@ -103,7 +103,7 @@ export const useQuestionStore = defineStore('question', () => {
const pageIndex = ref(1) // 当前分页的索引 const pageIndex = ref(1) // 当前分页的索引
const changeField = ref(null) const changeField = ref(null)
const changeIndex = computed(() => { const changeIndex = computed(() => {
return questionData.value[changeField.value]?.index return questionData.value[changeField.value].index
}) })
const needHideFields = ref([]) const needHideFields = ref([])

View File

@ -161,7 +161,9 @@ export const useSurveyStore = defineStore('survey', () => {
// 用户输入或者选择后,更新表单数据 // 用户输入或者选择后,更新表单数据
const changeData = (data) => { const changeData = (data) => {
let { key, value } = data let { key, value } = data
formValues.value[key] = value if (key in formValues.value) {
formValues.value[key] = value
}
questionStore.setChangeField(key) questionStore.setChangeField(key)
} }

View File

@ -114,7 +114,6 @@ export default defineConfig({
css: { css: {
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
api: 'modern-compiler',
additionalData: `@use "@/management/styles/element-variables.scss" as *;` additionalData: `@use "@/management/styles/element-variables.scss" as *;`
} }
} }