From b17bdcc24c4c879d2c0a16c8ec875aa1e95e9ac5 Mon Sep 17 00:00:00 2001 From: shenquanyi Date: Wed, 24 Sep 2025 18:12:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BF=9D=E9=99=A9=E7=AB=AF?= =?UTF-8?q?=E5=89=8D=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trae/rules/project_rules.md | 104 +- insurance_admin-system/debug_menu.js | Bin 0 -> 378 bytes insurance_admin-system/public/set-token.html | 199 ++ .../src/components/Layout.vue | 5 +- insurance_admin-system/src/main.js | 6 + insurance_admin-system/src/router/index.js | 53 +- .../src/services/authService.js | 232 +++ insurance_admin-system/src/stores/user.js | 201 +- insurance_admin-system/src/utils/api.js | 83 +- insurance_admin-system/src/utils/request.js | 199 ++ insurance_admin-system/src/views/Login.vue | 62 +- .../src/views/MessageNotification.vue | 802 ++++++++ .../src/views/SystemSettings.vue | 987 ++++------ .../src/views/UserProfile.vue | 345 ++++ insurance_admin-system/vite.config.js | 3 +- insurance_backend/check_database.js | 55 + insurance_backend/check_frontend_storage.js | 110 ++ insurance_backend/check_menus.js | 29 + insurance_backend/check_table_structure.js | 37 + insurance_backend/check_users.js | 61 + .../controllers/authController.js | 104 +- .../controllers/dataWarehouseController.js | 415 +++-- .../controllers/deviceAlertController.js | 421 +++++ .../controllers/deviceController.js | 435 +++++ .../controllers/menuController.js | 21 +- .../controllers/operationLogController.js | 344 ++++ .../controllers/userController.js | 163 +- insurance_backend/create_missing_tables.js | 112 ++ insurance_backend/debug-api.js | 54 + insurance_backend/debug_frontend_token.js | 77 + .../docs/data-warehouse-api.yaml | 376 ++++ insurance_backend/generate_test_data.js | 160 ++ insurance_backend/middleware/auth.js | 14 +- .../middleware/operationLogger.js | 302 +++ .../20241225000001-create-operation-logs.js | 146 ++ ...0250122000003-create-devices-and-alerts.js | 190 ++ insurance_backend/models/Device.js | 86 + insurance_backend/models/DeviceAlert.js | 114 ++ insurance_backend/models/OperationLog.js | 270 +++ insurance_backend/models/index.js | 48 +- insurance_backend/package-lock.json | 1626 +++++++++++++++-- insurance_backend/package.json | 1 + insurance_backend/routes/auth.js | 5 +- insurance_backend/routes/deviceAlerts.js | 485 +++++ insurance_backend/routes/devices.js | 467 +++++ insurance_backend/routes/operationLogs.js | 319 ++++ insurance_backend/routes/users.js | 13 + .../scripts/check-table-structure.js | 43 + .../scripts/check_user_permissions.js | 96 + .../scripts/create-device-tables.js | 89 + insurance_backend/scripts/test-device-data.js | 161 ++ .../scripts/test_frontend_auth.js | 76 + insurance_backend/src/app.js | 7 +- insurance_backend/test-routes.js | 29 + insurance_backend/test_browser_behavior.js | 99 + insurance_backend/test_menu_api.js | 32 + 56 files changed, 9862 insertions(+), 1111 deletions(-) create mode 100644 insurance_admin-system/debug_menu.js create mode 100644 insurance_admin-system/public/set-token.html create mode 100644 insurance_admin-system/src/services/authService.js create mode 100644 insurance_admin-system/src/utils/request.js create mode 100644 insurance_admin-system/src/views/MessageNotification.vue create mode 100644 insurance_admin-system/src/views/UserProfile.vue create mode 100644 insurance_backend/check_database.js create mode 100644 insurance_backend/check_frontend_storage.js create mode 100644 insurance_backend/check_menus.js create mode 100644 insurance_backend/check_table_structure.js create mode 100644 insurance_backend/check_users.js create mode 100644 insurance_backend/controllers/deviceAlertController.js create mode 100644 insurance_backend/controllers/deviceController.js create mode 100644 insurance_backend/controllers/operationLogController.js create mode 100644 insurance_backend/create_missing_tables.js create mode 100644 insurance_backend/debug-api.js create mode 100644 insurance_backend/debug_frontend_token.js create mode 100644 insurance_backend/docs/data-warehouse-api.yaml create mode 100644 insurance_backend/generate_test_data.js create mode 100644 insurance_backend/middleware/operationLogger.js create mode 100644 insurance_backend/migrations/20241225000001-create-operation-logs.js create mode 100644 insurance_backend/migrations/20250122000003-create-devices-and-alerts.js create mode 100644 insurance_backend/models/Device.js create mode 100644 insurance_backend/models/DeviceAlert.js create mode 100644 insurance_backend/models/OperationLog.js create mode 100644 insurance_backend/routes/deviceAlerts.js create mode 100644 insurance_backend/routes/devices.js create mode 100644 insurance_backend/routes/operationLogs.js create mode 100644 insurance_backend/scripts/check-table-structure.js create mode 100644 insurance_backend/scripts/check_user_permissions.js create mode 100644 insurance_backend/scripts/create-device-tables.js create mode 100644 insurance_backend/scripts/test-device-data.js create mode 100644 insurance_backend/scripts/test_frontend_auth.js create mode 100644 insurance_backend/test-routes.js create mode 100644 insurance_backend/test_browser_behavior.js create mode 100644 insurance_backend/test_menu_api.js diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md index 8546c41..27d0471 100644 --- a/.trae/rules/project_rules.md +++ b/.trae/rules/project_rules.md @@ -1,55 +1,49 @@ -# 项目架构文档 - -## 1. 概述 - -本文档描述了项目的整体架构设计,包括技术栈、模块划分、数据流和关键组件。 - -## 2. 技术栈 - -- **前端**: Vue.js 3.x -- **后端**: Node.js (Express/NestJS) -- **数据库**: MySQL -- **构建工具**: Vite - -## 3. 模块划分 - -### 3.1 前端模块 - -- **用户界面**: 基于 Vue 3 的组件化开发 -- **UI组件库**: Ant Design Vue -- **地图服务**: 百度地图API -- **图表库**: ECharts -- **状态管理**: Pinia -- **路由管理**: Vue Router - -### 3.2 后端模块 - -- **API 服务**: RESTful API -- **认证与授权**: JWT -- **数据库访问**: ORM (TypeORM/Sequelize) - -## 4. 数据流 - -- 前端通过 HTTP 请求与后端交互 -- 后端处理业务逻辑并返回数据 -- 数据库持久化存储 - -## 5. 关键组件 - -- **前端**: `App.vue` 为入口组件 -- **后端**: `server.js` 为入口文件 - -## 6. 部署架构 - -- **开发环境**: 本地运行 -- **生产环境**: Docker 容器化部署 - -## 7. 扩展性 - -- 支持模块化扩展 -- 易于集成第三方服务 - -## 8. 后续计划 - -- 引入微服务架构 -- 优化性能监控 \ No newline at end of file +1. 请保持对话语言为中文 +2. 我的系统为 Windows +3. 远程服务器为centos10 64位 +4. 项目文件夹结构为: + - docs 文档目录 + - admin-system 养殖PC端管理后台目录 + - mini-program 养殖端小程序app目录 + - backend 养殖端后端服务目录 + - website 官网目录 + - insurance_backend 保险管理后台目录 + - insurance_admin-system 保险管理后台web目录 + - insurance_mini_program 保险小程序app目录 + - scripts 脚本目录 放置一些脚本,如: + - 数据库脚本 + - 部署脚本 + - 测试脚本 + - 运维脚本 +5. 整个项目入口文档为根目录下的readme.md,其他文档请放在docs目录下 +6. 请使用markdown格式编写文档,整个项目文档包括: + - 需求文档:整个项目需求文档.md 官网需求文档.md 后端管理需求文档.md 管理后台需求文档.md 小程序app需求文档.md + - 架构文档:整个项目的架构文档.md 后端架构文档.md 小程序架构文档.md 管理后台架构文档.md + - 详细设计文档: + - 数据库设计文档.md + - 管理后台接口设计文档.md + - 小程序app接口设计文档.md + - 开发文档: + - 后端开发文档.md 包含:细分到每个子任务的开发计划 + - 小程序app开发文档.md 包含:细分到每个子任务的开发计划 + - 管理后台开发文档.md 包含:细分到每个子任务的开发计划 + - 后端管理开发文档.md 包含:细分到每个子任务的开发计划 + - 测试文档.md + - 部署文档.md + - 运维文档.md + - 安全文档.md + - 用户手册文档.md +7. DB_DIALECT || 'mysql', +DB_HOST = '129.211.213.226', +DB_PORT = 9527, +DB_DATABASE = 'insurance_data', +DB_USER = 'root', +DB_PASSWORD = 'aiotAiot123!', +8. 创建的测试文件全部都自动删除,不用我来点击删除。 +9. 遇到大模型请求次数上限时自动继续。 +10. 测试的账户为:admin 密码为:123456 +11. 项目中所有的接口都需要做好接口文档,全部都写在接口文档中,并在文档中说明请求方式、请求参数、请求示例、返回参数、返回示例等信息。 +12. 不要修改前后端端口号。发现端口占用先杀死端口,再打开,不要修改端口号。规定死养殖端的后端端口为5350,前端端口为5300. +13. 不要修改前后端端口号。发现端口占用先杀死端口,再打开,不要修改端口号。规定死保险端的后端端口为3000,前端端口为3001. +14. 每次运行命令都要先看项目规则。 +15. PowerShell不支持&&操作符,请使用;符号 \ No newline at end of file diff --git a/insurance_admin-system/debug_menu.js b/insurance_admin-system/debug_menu.js new file mode 100644 index 0000000000000000000000000000000000000000..581ced0545852e91a0782563bc26de4d4c9c7a55 GIT binary patch literal 378 zcmZ{gF$%&!5Jg`tcmO-GQjm>c7g4aZ6U4$wG)ALn5==aSrL|}A5cXEy!5gT56Sfg# z*_qw>fBxUu&pRL?CR5xKViXfLG-<&6KD(VCljnIlNy9iWLXGMzdem8qof8lh+(_q~ z6dzB^Y6L8lBb_oKK3zJ+@>EaRigl1e%t^SX%88};TlHB7qn@b1n;exhIrbSSmN&z$ zcylcG)(?Lrhg{or{GZTG=F77!zUwR%?F4SZP`<~%iY1zeVgJpl4jD6|c70u(cFD2I IJ*rgr0$2V>CjbBd literal 0 HcmV?d00001 diff --git a/insurance_admin-system/public/set-token.html b/insurance_admin-system/public/set-token.html new file mode 100644 index 0000000..3dba8c6 --- /dev/null +++ b/insurance_admin-system/public/set-token.html @@ -0,0 +1,199 @@ + + + + + + 设置认证Token + + + +
+

🔐 保险管理系统 - 认证Token设置

+ +
+

步骤1: 检查当前状态

+ +
+
+ +
+

步骤2: 设置新Token

+ +
+
+ +
+

步骤3: 测试API连接

+ +
+
+ +
+

步骤4: 跳转到数据仓库

+ +
+
+ + + + \ No newline at end of file diff --git a/insurance_admin-system/src/components/Layout.vue b/insurance_admin-system/src/components/Layout.vue index aeac40b..297421c 100644 --- a/insurance_admin-system/src/components/Layout.vue +++ b/insurance_admin-system/src/components/Layout.vue @@ -185,7 +185,6 @@ const fetchMenus = async () => { menus.value = formatMenuItems(response.data); } } catch (error) { - console.error('获取菜单失败:', error); // 提供默认菜单作为备用 menus.value = [ { @@ -275,7 +274,7 @@ const fetchMenus = async () => { key: 'Notifications', icon: () => h(BellOutlined), label: '消息通知', - path: '/dashboard' // 重定向到仪表板 + path: '/notifications' }, { key: 'UserManagement', @@ -287,7 +286,7 @@ const fetchMenus = async () => { key: 'SystemSettings', icon: () => h(SettingOutlined), label: '系统设置', - path: '/dashboard' // 重定向到仪表板 + path: '/system-settings' }, { key: 'UserProfile', diff --git a/insurance_admin-system/src/main.js b/insurance_admin-system/src/main.js index 8ba3df1..44a59c0 100644 --- a/insurance_admin-system/src/main.js +++ b/insurance_admin-system/src/main.js @@ -6,6 +6,8 @@ import Antd from 'ant-design-vue' import 'ant-design-vue/dist/reset.css' // Ant Design Vue的中文语言包 import antdZhCN from 'ant-design-vue/es/locale/zh_CN' +// 导入API拦截器和Token自动刷新机制 +import './utils/request' // 抑制ResizeObserver警告 const resizeObserverErrorHandler = (e) => { @@ -43,4 +45,8 @@ app.use(router) app.use(store) app.use(Antd, antdConfig) +// 启动Token过期提醒功能 +import { setupTokenExpirationWarning } from './utils/request' +setupTokenExpirationWarning() + app.mount('#app') \ No newline at end of file diff --git a/insurance_admin-system/src/router/index.js b/insurance_admin-system/src/router/index.js index d1a04dc..ac83408 100644 --- a/insurance_admin-system/src/router/index.js +++ b/insurance_admin-system/src/router/index.js @@ -19,6 +19,7 @@ import SimpleDayjsTest from '@/views/SimpleDayjsTest.vue' import RangePickerTest from '@/views/RangePickerTest.vue' import LoginTest from '@/views/LoginTest.vue' import LivestockPolicyManagement from '@/views/LivestockPolicyManagement.vue' +import SystemSettings from '@/views/SystemSettings.vue' const routes = [ { @@ -43,6 +44,12 @@ const routes = [ component: UserManagement, meta: { title: '用户管理' } }, + { + path: 'notifications', + name: 'MessageNotification', + component: () => import('@/views/MessageNotification.vue'), + meta: { title: '消息通知', requiresAuth: true } + }, { path: 'insurance-types', name: 'InsuranceTypeManagement', @@ -99,6 +106,21 @@ const routes = [ component: LivestockPolicyManagement, meta: { title: '生资保单管理' } }, + { + path: 'system-settings', + name: 'SystemSettings', + component: SystemSettings, + meta: { title: '系统设置' } + }, + { + path: 'profile', + name: 'UserProfile', + component: () => import('@/views/UserProfile.vue'), + meta: { + title: '个人中心', + requiresAuth: true + } + }, { path: 'date-picker-test', name: 'DatePickerTest', @@ -151,18 +173,39 @@ const router = createRouter({ }) // 路由守卫 -router.beforeEach((to, from, next) => { +router.beforeEach(async (to, from, next) => { const userStore = useUserStore() // 如果访问登录页面且已登录,重定向到仪表板 - if (to.path === '/login' && userStore.token) { + if (to.path === '/login' && (userStore.token || userStore.accessToken)) { next('/dashboard') return } - // 如果访问受保护的路由但未登录,重定向到登录页 - if (to.path !== '/login' && !userStore.token) { - next('/login') + // 如果访问受保护的路由 + if (to.path !== '/login') { + try { + // 确保Token有效(自动刷新或重新登录) + await userStore.ensureValidToken() + + // 检查是否有有效的Token + if (!userStore.accessToken && !userStore.token) { + // 尝试自动重新登录 + const autoLoginSuccess = await userStore.autoRelogin() + + if (!autoLoginSuccess) { + // 自动重新登录失败,跳转到登录页 + next('/login') + return + } + } + + next() + } catch (error) { + console.error('路由守卫认证检查失败:', error) + // 认证失败,跳转到登录页 + next('/login') + } return } diff --git a/insurance_admin-system/src/services/authService.js b/insurance_admin-system/src/services/authService.js new file mode 100644 index 0000000..0651bc4 --- /dev/null +++ b/insurance_admin-system/src/services/authService.js @@ -0,0 +1,232 @@ +/** + * 认证服务 + * 处理用户认证、Token管理、自动重新登录等功能 + */ + +import { useUserStore } from '@/stores/user' +import { authAPI } from '@/utils/api' + +class AuthService { + constructor() { + this.isAutoReloginInProgress = false + this.autoReloginPromise = null + } + + /** + * 自动重新登录 + * @returns {Promise} 是否成功 + */ + async autoRelogin() { + // 如果正在进行自动重新登录,返回现有的Promise + if (this.isAutoReloginInProgress) { + return this.autoReloginPromise + } + + this.isAutoReloginInProgress = true + this.autoReloginPromise = this._performAutoRelogin() + + try { + const result = await this.autoReloginPromise + return result + } finally { + this.isAutoReloginInProgress = false + this.autoReloginPromise = null + } + } + + /** + * 执行自动重新登录 + * @private + */ + async _performAutoRelogin() { + const userStore = useUserStore() + + try { + console.log('开始自动重新登录流程...') + + // 1. 检查是否有有效的refresh token + if (userStore.refreshToken) { + try { + console.log('尝试使用refresh token刷新访问令牌...') + await userStore.refreshAccessToken() + console.log('使用refresh token自动重新登录成功') + return true + } catch (error) { + console.error('refresh token刷新失败:', error) + // refresh token可能已过期,继续尝试其他方式 + } + } + + // 2. 检查是否有记住的登录信息 + const rememberedCredentials = this.getRememberedCredentials() + if (rememberedCredentials) { + try { + console.log('尝试使用记住的登录信息自动登录...') + const response = await authAPI.login(rememberedCredentials) + + if (response.status === 'success') { + // 更新用户store中的认证信息 + const authData = response.data + if (authData.accessToken && authData.refreshToken) { + userStore.setAuthData({ + accessToken: authData.accessToken, + refreshToken: authData.refreshToken, + accessTokenExpiresAt: authData.accessTokenExpiresAt, + refreshTokenExpiresAt: authData.refreshTokenExpiresAt, + userInfo: authData.userInfo + }) + } + + console.log('使用记住的登录信息自动重新登录成功') + return true + } + } catch (error) { + console.error('使用记住的登录信息自动登录失败:', error) + } + } + + // 3. 尝试静默登录(如果支持) + if (this.supportsSilentLogin()) { + try { + console.log('尝试静默登录...') + const success = await this.performSilentLogin() + if (success) { + console.log('静默登录成功') + return true + } + } catch (error) { + console.error('静默登录失败:', error) + } + } + + console.log('所有自动重新登录方式都失败了') + return false + + } catch (error) { + console.error('自动重新登录过程中发生未预期的错误:', error) + return false + } + } + + /** + * 获取记住的登录凭据 + * 注意:出于安全考虑,这里只是示例,实际项目中不应该保存密码 + */ + getRememberedCredentials() { + try { + // 检查localStorage中是否有记住的用户名 + const rememberedUsername = localStorage.getItem('rememberedUsername') + const rememberLogin = localStorage.getItem('rememberLogin') === 'true' + + if (rememberedUsername && rememberLogin) { + // 注意:这里不应该保存密码,这只是一个示例 + // 实际项目中可以考虑使用设备指纹、生物识别等更安全的方式 + console.log('找到记住的用户名:', rememberedUsername) + + // 可以返回用户名,让用户重新输入密码 + // 或者使用其他安全的认证方式 + return null // 暂时返回null,因为不保存密码 + } + + return null + } catch (error) { + console.error('获取记住的登录凭据失败:', error) + return null + } + } + + /** + * 检查是否支持静默登录 + */ + supportsSilentLogin() { + // 可以根据环境、设备能力等判断是否支持静默登录 + // 例如:生物识别、设备证书等 + return false + } + + /** + * 执行静默登录 + */ + async performSilentLogin() { + // 这里可以实现各种静默登录方式 + // 例如: + // - 生物识别登录 + // - 设备证书登录 + // - SSO单点登录 + // - 第三方OAuth登录 + + return false + } + + /** + * 保存登录凭据(记住登录) + * @param {string} username 用户名 + * @param {boolean} remember 是否记住 + */ + saveLoginCredentials(username, remember = false) { + try { + if (remember) { + localStorage.setItem('rememberedUsername', username) + localStorage.setItem('rememberLogin', 'true') + console.log('已保存记住的登录信息') + } else { + localStorage.removeItem('rememberedUsername') + localStorage.removeItem('rememberLogin') + console.log('已清除记住的登录信息') + } + } catch (error) { + console.error('保存登录凭据失败:', error) + } + } + + /** + * 清除所有保存的登录凭据 + */ + clearSavedCredentials() { + try { + localStorage.removeItem('rememberedUsername') + localStorage.removeItem('rememberLogin') + console.log('已清除所有保存的登录凭据') + } catch (error) { + console.error('清除保存的登录凭据失败:', error) + } + } + + /** + * 检查认证状态 + */ + async checkAuthStatus() { + const userStore = useUserStore() + + try { + // 确保Token有效 + const isValid = await userStore.ensureValidToken() + + if (!isValid) { + // 尝试自动重新登录 + const autoLoginSuccess = await this.autoRelogin() + return autoLoginSuccess + } + + return true + } catch (error) { + console.error('检查认证状态失败:', error) + return false + } + } + + /** + * 登出并清除所有认证信息 + */ + logout() { + const userStore = useUserStore() + userStore.logout() + this.clearSavedCredentials() + console.log('用户已登出,所有认证信息已清除') + } +} + +// 创建单例实例 +const authService = new AuthService() + +export default authService \ No newline at end of file diff --git a/insurance_admin-system/src/stores/user.js b/insurance_admin-system/src/stores/user.js index 5231082..393b1c1 100644 --- a/insurance_admin-system/src/stores/user.js +++ b/insurance_admin-system/src/stores/user.js @@ -1,30 +1,211 @@ import { defineStore } from 'pinia' -import { ref } from 'vue' +import { ref, computed } from 'vue' +import axios from 'axios' export const useUserStore = defineStore('user', () => { - const token = ref(localStorage.getItem('token')) + // 兼容旧版本的token存储 + const accessToken = ref(localStorage.getItem('accessToken') || localStorage.getItem('token')) + const refreshToken = ref(localStorage.getItem('refreshToken')) const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}')) + const tokenExpiresAt = ref(localStorage.getItem('tokenExpiresAt')) + + // 计算属性:检查token是否即将过期(提前5分钟刷新) + const isTokenExpiringSoon = computed(() => { + if (!tokenExpiresAt.value) return false + const expiresAt = new Date(tokenExpiresAt.value) + const now = new Date() + const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000) + return expiresAt <= fiveMinutesFromNow + }) + + // 计算属性:检查token是否已过期 + const isTokenExpired = computed(() => { + if (!tokenExpiresAt.value) return false + const expiresAt = new Date(tokenExpiresAt.value) + const now = new Date() + return expiresAt <= now + }) - const setToken = (newToken) => { - token.value = newToken + // 设置访问令牌 + const setAccessToken = (newToken) => { + accessToken.value = newToken + localStorage.setItem('accessToken', newToken) + // 兼容旧版本 localStorage.setItem('token', newToken) } + // 设置刷新令牌 + const setRefreshToken = (newRefreshToken) => { + refreshToken.value = newRefreshToken + localStorage.setItem('refreshToken', newRefreshToken) + } + + // 设置令牌过期时间 + const setTokenExpiresAt = (expiresIn) => { + const expiresAt = new Date(Date.now() + expiresIn * 1000) + tokenExpiresAt.value = expiresAt.toISOString() + localStorage.setItem('tokenExpiresAt', expiresAt.toISOString()) + } + + // 设置完整的认证信息 + const setAuthData = (authData) => { + if (authData.accessToken) { + setAccessToken(authData.accessToken) + } + if (authData.refreshToken) { + setRefreshToken(authData.refreshToken) + } + if (authData.accessTokenExpiresIn) { + setTokenExpiresAt(authData.accessTokenExpiresIn) + } + if (authData.user) { + setUserInfo(authData.user) + } + } + const setUserInfo = (info) => { userInfo.value = info localStorage.setItem('userInfo', JSON.stringify(info)) } - const logout = () => { - token.value = null - userInfo.value = {} - localStorage.removeItem('token') - localStorage.removeItem('userInfo') + // Token刷新方法 + const refreshAccessToken = async () => { + try { + if (!refreshToken.value) { + throw new Error('没有刷新令牌') + } + + const response = await axios.post('/api/auth/refresh', { + refreshToken: refreshToken.value + }) + + if (response.data.success) { + const authData = response.data.data + setAuthData(authData) + return authData.accessToken + } else { + throw new Error(response.data.message || '刷新令牌失败') + } + } catch (error) { + console.error('刷新令牌失败:', error) + // 刷新失败,清除所有认证信息 + logout() + throw error + } } + // 自动刷新令牌(如果需要的话) + const ensureValidToken = async () => { + if (!accessToken.value) { + throw new Error('没有访问令牌') + } + + if (isTokenExpired.value) { + // Token已过期,尝试刷新 + return await refreshAccessToken() + } else if (isTokenExpiringSoon.value) { + // Token即将过期,主动刷新 + try { + return await refreshAccessToken() + } catch (error) { + // 刷新失败但当前token还未过期,继续使用当前token + console.warn('主动刷新失败,继续使用当前token:', error) + return accessToken.value + } + } + + return accessToken.value + } + + // 自动重新登录(委托给认证服务) + const autoRelogin = async () => { + // 导入认证服务(避免循环依赖) + const { default: authService } = await import('@/services/authService') + return authService.autoRelogin() + } + + // 获取保存的登录凭据 + const getSavedCredentials = () => { + try { + // 检查是否有有效的refresh token + if (refreshToken.value) { + return { + refreshToken: refreshToken.value + } + } + + // 可以在这里添加其他类型的保存凭据检查 + // 例如:记住的用户名、设备指纹等 + + return null + } catch (error) { + console.error('获取保存的登录凭据失败:', error) + return null + } + } + + // 获取用户信息 + const fetchUserInfo = async () => { + try { + // 动态导入API以避免循环依赖 + const { authAPI } = await import('@/utils/api') + const response = await authAPI.getProfile() + + if (response.data && response.data.status === 'success') { + const userData = response.data.data + setUserInfo(userData) + return userData + } else { + throw new Error(response.data?.message || '获取用户信息失败') + } + } catch (error) { + console.error('获取用户信息失败:', error) + throw error + } + } + + const logout = () => { + accessToken.value = null + refreshToken.value = null + userInfo.value = {} + tokenExpiresAt.value = null + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + localStorage.removeItem('userInfo') + localStorage.removeItem('tokenExpiresAt') + // 兼容旧版本 + localStorage.removeItem('token') + } + + // 兼容旧版本的方法 + const setToken = (newToken) => { + setAccessToken(newToken) + } + + const token = computed(() => accessToken.value) + return { - token, + // 新的双Token属性 + accessToken, + refreshToken, userInfo, + tokenExpiresAt, + isTokenExpiringSoon, + isTokenExpired, + + // 新的方法 + setAccessToken, + setRefreshToken, + setTokenExpiresAt, + setAuthData, + refreshAccessToken, + ensureValidToken, + autoRelogin, + getSavedCredentials, + fetchUserInfo, + + // 兼容旧版本的属性和方法 + token, setToken, setUserInfo, logout diff --git a/insurance_admin-system/src/utils/api.js b/insurance_admin-system/src/utils/api.js index 130f333..648dbdf 100644 --- a/insurance_admin-system/src/utils/api.js +++ b/insurance_admin-system/src/utils/api.js @@ -1,58 +1,37 @@ -import axios from 'axios' -import { useUserStore } from '@/stores/user' +// 使用新的请求拦截器,支持Token自动刷新 +import { apiClient } from './request' -// 创建axios实例 -const api = axios.create({ - baseURL: '/api', - timeout: 10000 -}) - -// 请求拦截器 -api.interceptors.request.use( - (config) => { - const userStore = useUserStore() - if (userStore.token) { - config.headers.Authorization = `Bearer ${userStore.token}` - } - return config - }, - (error) => { - return Promise.reject(error) - } -) - -// 响应拦截器 -api.interceptors.response.use( - (response) => { - return response.data - }, - (error) => { - if (error.response?.status === 401) { - const userStore = useUserStore() - userStore.logout() - window.location.href = '/login' - } - return Promise.reject(error) - } -) +// 使用配置了自动刷新功能的axios实例 +const api = apiClient // API接口 export const authAPI = { login: (data) => api.post('/auth/login', data), logout: () => api.post('/auth/logout'), - getProfile: () => api.get('/auth/profile') + getProfile: () => api.get('/users/profile') } export const userAPI = { getList: (params) => api.get('/users', { params }), create: (data) => api.post('/users', data), update: (id, data) => api.put(`/users/${id}`, data), - delete: (id) => api.delete(`/users/${id}`) + delete: (id) => api.delete(`/users/${id}`), + updateProfile: (data) => api.put('/users/profile', data), + changePassword: (data) => api.put('/users/change-password', data), + uploadAvatar: (formData) => api.post('/users/avatar', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) }; export const menuAPI = { - getMenus: () => api.get('/menus/public'), - getAllMenus: () => api.get('/menus/all') + getMenus: async () => { + const response = await api.get('/menus/public'); + return response.data; // 返回响应的data部分 + }, + getAllMenus: async () => { + const response = await api.get('/menus/all'); + return response.data; // 返回响应的data部分 + } } export const insuranceTypeAPI = { @@ -95,12 +74,22 @@ export const dashboardAPI = { getRecentActivities: () => api.get('/system/logs?limit=10') } +// 设备预警API +export const deviceAlertAPI = { + getStats: () => api.get('/device-alerts/stats'), + getList: (params) => api.get('/device-alerts', { params }), + getDetail: (id) => api.get(`/device-alerts/${id}`), + markAsRead: (id) => api.patch(`/device-alerts/${id}/read`), + markAllAsRead: () => api.patch('/device-alerts/read-all'), + handle: (id, data) => api.patch(`/device-alerts/${id}/handle`, data) +} + // 数据览仓API export const dataWarehouseAPI = { getOverview: () => api.get('/data-warehouse/overview'), - getInsuranceTypeDistribution: () => api.get('/data-warehouse/insurance-types'), - getApplicationStatusDistribution: () => api.get('/data-warehouse/application-status'), - getTrendData: () => api.get('/data-warehouse/trend'), + getInsuranceTypeDistribution: () => api.get('/data-warehouse/insurance-type-distribution'), + getApplicationStatusDistribution: () => api.get('/data-warehouse/application-status-distribution'), + getTrendData: () => api.get('/data-warehouse/trend-data'), getClaimStats: () => api.get('/data-warehouse/claim-stats') } @@ -163,4 +152,12 @@ export const livestockClaimApi = { getStats: () => api.get('/livestock-claims/stats') } +// 操作日志API +export const operationLogAPI = { + getList: (params) => api.get('/operation-logs', { params }), + getStats: () => api.get('/operation-logs/stats'), + getById: (id) => api.get(`/operation-logs/${id}`), + export: (params) => api.get('/operation-logs/export', { params }) +} + export default api \ No newline at end of file diff --git a/insurance_admin-system/src/utils/request.js b/insurance_admin-system/src/utils/request.js new file mode 100644 index 0000000..7c03cff --- /dev/null +++ b/insurance_admin-system/src/utils/request.js @@ -0,0 +1,199 @@ +import axios from 'axios' +import { useUserStore } from '@/stores/user' +import { message, Modal } from 'ant-design-vue' +import router from '@/router' + +// 创建axios实例 +const request = axios.create({ + baseURL: 'http://localhost:3000/api', + timeout: 10000 +}) + +// 是否正在刷新token的标志 +let isRefreshing = false +// 存储待重试的请求 +let failedQueue = [] + +// 处理队列中的请求 +const processQueue = (error, token = null) => { + failedQueue.forEach(({ resolve, reject }) => { + if (error) { + reject(error) + } else { + resolve(token) + } + }) + + failedQueue = [] +} + +// 请求拦截器 +request.interceptors.request.use( + async (config) => { + const userStore = useUserStore() + + // 对于登录、刷新token和公开接口,跳过token检查 + const skipTokenCheck = config.url?.includes('/auth/login') || + config.url?.includes('/auth/refresh') || + config.url?.includes('/auth/register') || + config.url?.includes('/menus/public') + + if (!skipTokenCheck) { + try { + // 确保token有效(自动刷新如果需要) + const validToken = await userStore.ensureValidToken() + + if (validToken) { + config.headers.Authorization = `Bearer ${validToken}` + } + } catch (error) { + console.error('获取有效token失败:', error) + // 如果无法获取有效token,继续发送请求,让响应拦截器处理 + } + } + + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +request.interceptors.response.use( + (response) => { + return response + }, + async (error) => { + const userStore = useUserStore() + const originalRequest = error.config + + // 如果是401错误且不是刷新token的请求 + if (error.response?.status === 401 && !originalRequest._retry) { + const errorCode = error.response?.data?.code + + // 如果是token过期错误 + if (errorCode === 'TOKEN_EXPIRED') { + // 如果已经在刷新token,将请求加入队列 + if (isRefreshing) { + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }) + }).then(token => { + originalRequest.headers.Authorization = `Bearer ${token}` + return request(originalRequest) + }).catch(err => { + return Promise.reject(err) + }) + } + + originalRequest._retry = true + isRefreshing = true + + try { + // 尝试刷新token + const newToken = await userStore.refreshAccessToken() + + // 处理队列中的请求 + processQueue(null, newToken) + + // 重试原始请求 + originalRequest.headers.Authorization = `Bearer ${newToken}` + return request(originalRequest) + } catch (refreshError) { + // 刷新失败,处理队列并跳转到登录页 + processQueue(refreshError, null) + + message.error('登录已过期,请重新登录') + + // 清除用户信息 + userStore.logout() + + // 跳转到登录页 + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } + + return Promise.reject(refreshError) + } finally { + isRefreshing = false + } + } else { + // 其他401错误(如token无效),直接跳转登录页 + message.error('认证失败,请重新登录') + userStore.logout() + + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } + } + } + + // 处理其他错误 + if (error.response?.data?.message) { + message.error(error.response.data.message) + } else if (error.message) { + message.error(error.message) + } + + return Promise.reject(error) + } +) + +// 自动重新登录功能 +export const autoRelogin = async (username, password) => { + try { + const response = await axios.post('http://localhost:3000/api/auth/login', { + username, + password + }) + + if (response.data.success) { + const userStore = useUserStore() + userStore.setAuthData(response.data.data) + + message.success('自动重新登录成功') + return true + } + } catch (error) { + console.error('自动重新登录失败:', error) + message.error('自动重新登录失败,请手动登录') + } + + return false +} + +// Token过期提醒 +export const setupTokenExpirationWarning = () => { + const userStore = useUserStore() + + // 每分钟检查一次token状态 + setInterval(() => { + if (userStore.isTokenExpiringSoon && !userStore.isTokenExpired) { + // Token即将过期,显示提醒 + Modal.confirm({ + title: '登录提醒', + content: '您的登录即将过期,是否继续保持登录状态?', + okText: '继续登录', + cancelText: '退出登录', + onOk: async () => { + try { + await userStore.refreshAccessToken() + message.success('登录状态已延长') + } catch (error) { + message.error('刷新登录状态失败') + userStore.logout() + router.push('/login') + } + }, + onCancel: () => { + userStore.logout() + router.push('/login') + } + }) + } + }, 60000) // 每分钟检查一次 +} + +// 导出默认的axios实例和别名 +export default request +export const apiClient = request \ No newline at end of file diff --git a/insurance_admin-system/src/views/Login.vue b/insurance_admin-system/src/views/Login.vue index e39ccf6..d4e4e32 100644 --- a/insurance_admin-system/src/views/Login.vue +++ b/insurance_admin-system/src/views/Login.vue @@ -43,6 +43,12 @@ + + + 记住登录 + + + + + \ No newline at end of file diff --git a/insurance_admin-system/src/views/SystemSettings.vue b/insurance_admin-system/src/views/SystemSettings.vue index b4f3d11..c675869 100644 --- a/insurance_admin-system/src/views/SystemSettings.vue +++ b/insurance_admin-system/src/views/SystemSettings.vue @@ -2,706 +2,407 @@
- - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 保存设置 - 重置 - - - - - - - {{ systemStatus.uptime }} - {{ systemStatus.memory_usage }} - - - {{ systemStatus.database_status }} - - - {{ systemStatus.last_backup }} - {{ systemStatus.user_count }} - {{ systemStatus.policy_count }} - - - - - - - - 创建 + 更新 + 删除 + 登录 + 登出 + + + + - - - - - - - - - - - - - - - - SSL - TLS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 保存设置 - 测试连接 - - - - - - - - - - - - - - - - 保存模板 - - - - - - - - - - - - - - 保存模板 - - - - - - - - - - - - - - 保存模板 - - - - - - - - - - - - - - - - - - - - - - - - - -

通知类型

- - - - 邮件 - 短信 - 站内 - - - - - - 邮件 - 短信 - 站内 - - - - - - 邮件 - 短信 - 站内 - - - - - - 邮件 - 短信 - 站内 - - - - - 保存设置 - -
-
- - - - - - 阿里云 - 腾讯云 - 其他 - - - - - - - - - - - - - - - - - - - - - - - - - - 保存设置 - 测试发送 - - - -
- - - - - - - - - - - - 每天 - 每周 - 每月 - - - - - - - - - - - - - - - 选择路径 - - - - - 保存设置 - - 立即备份 - - - - - - - 用户管理 + 保险管理 + 申请管理 + 保单管理 + 理赔管理 + 系统管理 + + + + - - - - -
+ 成功 + 失败 + + + + + + + + 搜索 + + + 重置 + + + + + + + + + + + + + + {{ selectedLog.id }} + {{ selectedLog.username }} + + + {{ getOperationTypeText(selectedLog.operation_type) }} + + + {{ selectedLog.operation_module }} + {{ selectedLog.operation_description }} + {{ selectedLog.ip_address }} + {{ selectedLog.user_agent }} + {{ formatDateTime(selectedLog.operation_time) }} + + + {{ selectedLog.status === 'success' ? '成功' : '失败' }} + + + + {{ selectedLog.error_message }} + + +
{{ formatJSON(selectedLog.request_data) }}
+
+ +
{{ formatJSON(selectedLog.response_data) }}
+
+
+
\ No newline at end of file diff --git a/insurance_admin-system/src/views/UserProfile.vue b/insurance_admin-system/src/views/UserProfile.vue new file mode 100644 index 0000000..f763a2e --- /dev/null +++ b/insurance_admin-system/src/views/UserProfile.vue @@ -0,0 +1,345 @@ + + + + + \ No newline at end of file diff --git a/insurance_admin-system/vite.config.js b/insurance_admin-system/vite.config.js index aab57c7..129120b 100644 --- a/insurance_admin-system/vite.config.js +++ b/insurance_admin-system/vite.config.js @@ -14,7 +14,8 @@ export default defineConfig(({ mode }) => { } }, server: { - port: parseInt(env.VITE_PORT) || 3004, + port: parseInt(env.VITE_PORT) || 3001, + historyApiFallback: true, proxy: { '/api': { target: env.VITE_API_BASE_URL || 'http://localhost:3000', diff --git a/insurance_backend/check_database.js b/insurance_backend/check_database.js new file mode 100644 index 0000000..e199c4b --- /dev/null +++ b/insurance_backend/check_database.js @@ -0,0 +1,55 @@ +const mysql = require('mysql2/promise'); + +async function checkDatabase() { + const connection = await mysql.createConnection({ + host: '129.211.213.226', + port: 9527, + user: 'root', + password: 'aiotAiot123!', + database: 'insurance_data' + }); + + try { + console.log('=== 检查数据库表结构 ==='); + + // 查看所有表 + const [tables] = await connection.execute('SHOW TABLES'); + console.log('\n当前数据库中的表:'); + tables.forEach(table => { + console.log(`- ${Object.values(table)[0]}`); + }); + + // 检查每个表的结构和数据量 + for (const table of tables) { + const tableName = Object.values(table)[0]; + console.log(`\n=== 表: ${tableName} ===`); + + // 查看表结构 + const [structure] = await connection.execute(`DESCRIBE ${tableName}`); + console.log('表结构:'); + structure.forEach(col => { + console.log(` ${col.Field}: ${col.Type} ${col.Null === 'NO' ? 'NOT NULL' : 'NULL'} ${col.Key ? `(${col.Key})` : ''}`); + }); + + // 查看数据量 + const [count] = await connection.execute(`SELECT COUNT(*) as count FROM ${tableName}`); + console.log(`数据量: ${count[0].count} 条记录`); + + // 如果有数据,显示前几条 + if (count[0].count > 0) { + const [sample] = await connection.execute(`SELECT * FROM ${tableName} LIMIT 3`); + console.log('示例数据:'); + sample.forEach((row, index) => { + console.log(` 记录${index + 1}:`, JSON.stringify(row, null, 2)); + }); + } + } + + } catch (error) { + console.error('检查数据库错误:', error); + } finally { + await connection.end(); + } +} + +checkDatabase(); \ No newline at end of file diff --git a/insurance_backend/check_frontend_storage.js b/insurance_backend/check_frontend_storage.js new file mode 100644 index 0000000..392e8a8 --- /dev/null +++ b/insurance_backend/check_frontend_storage.js @@ -0,0 +1,110 @@ +const axios = require('axios'); + +// 模拟前端可能遇到的各种token问题 +async function checkFrontendIssues() { + console.log('=== 前端Token问题排查 ===\n'); + + const browserAPI = axios.create({ + baseURL: 'http://localhost:3001', + timeout: 10000 + }); + + try { + // 1. 测试无token访问 + console.log('1. 测试无token访问数据仓库接口...'); + try { + await browserAPI.get('/api/data-warehouse/overview'); + console.log('✅ 无token访问成功 (这不应该发生)'); + } catch (error) { + console.log('❌ 无token访问失败:', error.response?.status, error.response?.data?.message); + } + + // 2. 测试错误token + console.log('\n2. 测试错误token...'); + browserAPI.defaults.headers.common['Authorization'] = 'Bearer invalid_token'; + try { + await browserAPI.get('/api/data-warehouse/overview'); + console.log('✅ 错误token访问成功 (这不应该发生)'); + } catch (error) { + console.log('❌ 错误token访问失败:', error.response?.status, error.response?.data?.message); + } + + // 3. 测试过期token + console.log('\n3. 测试过期token...'); + const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGVfaWQiOjEsInBlcm1pc3Npb25zIjpbXSwiaWF0IjoxNjAwMDAwMDAwLCJleHAiOjE2MDAwMDAwMDB9.invalid'; + browserAPI.defaults.headers.common['Authorization'] = `Bearer ${expiredToken}`; + try { + await browserAPI.get('/api/data-warehouse/overview'); + console.log('✅ 过期token访问成功 (这不应该发生)'); + } catch (error) { + console.log('❌ 过期token访问失败:', error.response?.status, error.response?.data?.message); + } + + // 4. 测试正确登录流程 + console.log('\n4. 测试正确登录流程...'); + const loginResponse = await browserAPI.post('/api/auth/login', { + username: 'admin', + password: '123456' + }); + + if (loginResponse.data?.code === 200) { + const token = loginResponse.data.data.token; + console.log('✅ 登录成功,获取token'); + + // 5. 测试正确token + console.log('\n5. 测试正确token...'); + browserAPI.defaults.headers.common['Authorization'] = `Bearer ${token}`; + + try { + const response = await browserAPI.get('/api/data-warehouse/overview'); + console.log('✅ 正确token访问成功:', response.status); + } catch (error) { + console.log('❌ 正确token访问失败:', error.response?.status, error.response?.data?.message); + } + + // 6. 测试token格式问题 + console.log('\n6. 测试各种token格式问题...'); + + // 测试没有Bearer前缀 + browserAPI.defaults.headers.common['Authorization'] = token; + try { + await browserAPI.get('/api/data-warehouse/overview'); + console.log('✅ 无Bearer前缀访问成功 (这不应该发生)'); + } catch (error) { + console.log('❌ 无Bearer前缀访问失败:', error.response?.status); + } + + // 测试错误的Bearer格式 + browserAPI.defaults.headers.common['Authorization'] = `bearer ${token}`; + try { + await browserAPI.get('/api/data-warehouse/overview'); + console.log('✅ 小写bearer访问成功'); + } catch (error) { + console.log('❌ 小写bearer访问失败:', error.response?.status); + } + + // 测试多余空格 + browserAPI.defaults.headers.common['Authorization'] = `Bearer ${token}`; + try { + await browserAPI.get('/api/data-warehouse/overview'); + console.log('✅ 多余空格访问成功'); + } catch (error) { + console.log('❌ 多余空格访问失败:', error.response?.status); + } + } + + // 7. 检查中间件处理 + console.log('\n7. 检查认证中间件...'); + console.log('建议检查以下几点:'); + console.log('- 浏览器开发者工具中的Network标签页'); + console.log('- 请求头中是否包含正确的Authorization'); + console.log('- 响应头中是否有CORS相关错误'); + console.log('- localStorage中是否正确存储了token'); + console.log('- 前端代码中token获取逻辑是否正确'); + + } catch (error) { + console.log('❌ 测试过程中出错:', error.message); + } +} + +checkFrontendIssues(); \ No newline at end of file diff --git a/insurance_backend/check_menus.js b/insurance_backend/check_menus.js new file mode 100644 index 0000000..eb77b72 --- /dev/null +++ b/insurance_backend/check_menus.js @@ -0,0 +1,29 @@ +const { Menu } = require('./models'); +const { sequelize } = require('./models'); + +async function checkMenus() { + try { + await sequelize.authenticate(); + console.log('数据库连接成功'); + + const menus = await Menu.findAll({ + order: [['order', 'ASC']] + }); + + console.log('菜单数据总数:', menus.length); + if (menus.length > 0) { + console.log('前5条菜单数据:'); + menus.slice(0, 5).forEach(menu => { + console.log(`- ID: ${menu.id}, Name: ${menu.name}, Key: ${menu.key}, Path: ${menu.path}`); + }); + } else { + console.log('数据库中没有菜单数据'); + } + } catch (error) { + console.error('检查菜单数据失败:', error.message); + } finally { + await sequelize.close(); + } +} + +checkMenus(); \ No newline at end of file diff --git a/insurance_backend/check_table_structure.js b/insurance_backend/check_table_structure.js new file mode 100644 index 0000000..f1bc8cb --- /dev/null +++ b/insurance_backend/check_table_structure.js @@ -0,0 +1,37 @@ +const mysql = require('mysql2/promise'); + +async function checkTableStructure() { + const connection = await mysql.createConnection({ + host: '129.211.213.226', + port: 9527, + user: 'root', + password: 'aiotAiot123!', + database: 'insurance_data' + }); + + try { + console.log('=== 检查表结构 ==='); + + const tables = ['insurance_types', 'insurance_applications', 'policies', 'claims']; + + for (const table of tables) { + try { + console.log(`\n=== 表: ${table} ===`); + const [columns] = await connection.execute(`DESCRIBE ${table}`); + console.log('字段结构:'); + columns.forEach(col => { + console.log(` ${col.Field}: ${col.Type} ${col.Null === 'NO' ? 'NOT NULL' : 'NULL'} ${col.Key ? `(${col.Key})` : ''}`); + }); + } catch (error) { + console.log(`表 ${table} 不存在或有错误:`, error.message); + } + } + + } catch (error) { + console.error('检查表结构错误:', error); + } finally { + await connection.end(); + } +} + +checkTableStructure(); \ No newline at end of file diff --git a/insurance_backend/check_users.js b/insurance_backend/check_users.js new file mode 100644 index 0000000..c9ed2f3 --- /dev/null +++ b/insurance_backend/check_users.js @@ -0,0 +1,61 @@ +const { User, Role } = require('./models'); + +async function checkUsers() { + try { + console.log('=== 检查数据库中的用户数据 ===\n'); + + // 1. 查询所有用户 + const users = await User.findAll({ + include: [{ + model: Role, + as: 'role' + }] + }); + + console.log(`数据库中共有 ${users.length} 个用户:`); + + users.forEach(user => { + console.log(`- ID: ${user.id}, 用户名: ${user.username}, 姓名: ${user.real_name}, 状态: ${user.status}, 角色: ${user.role?.name || '无角色'}`); + }); + + // 2. 特别检查ID为1的用户 + console.log('\n=== 检查ID为1的用户 ==='); + const user1 = await User.findByPk(1, { + include: [{ + model: Role, + as: 'role' + }] + }); + + if (user1) { + console.log('✅ 找到ID为1的用户:'); + console.log(JSON.stringify(user1.toJSON(), null, 2)); + } else { + console.log('❌ 没有找到ID为1的用户'); + } + + // 3. 检查admin用户 + console.log('\n=== 检查admin用户 ==='); + const adminUser = await User.findOne({ + where: { username: 'admin' }, + include: [{ + model: Role, + as: 'role' + }] + }); + + if (adminUser) { + console.log('✅ 找到admin用户:'); + console.log(`ID: ${adminUser.id}, 用户名: ${adminUser.username}, 状态: ${adminUser.status}`); + } else { + console.log('❌ 没有找到admin用户'); + } + + } catch (error) { + console.error('检查用户数据时出错:', error); + } finally { + process.exit(0); + } +} + +checkUsers(); \ No newline at end of file diff --git a/insurance_backend/controllers/authController.js b/insurance_backend/controllers/authController.js index 7dc50b4..a307b5e 100644 --- a/insurance_backend/controllers/authController.js +++ b/insurance_backend/controllers/authController.js @@ -91,23 +91,64 @@ const login = async (req, res) => { // 更新最后登录时间 await user.update({ last_login: new Date() }); - // 生成JWT令牌 - const token = jwt.sign( + // 生成访问令牌(短期有效) + const accessToken = jwt.sign( { id: user.id, username: user.username, role_id: user.role_id, - permissions: user.role?.permissions || [] + permissions: user.role?.permissions || [], + type: 'access' }, process.env.JWT_SECRET, - { expiresIn: process.env.JWT_EXPIRE || '7d' } + { expiresIn: '15m' } // 15分钟 ); - res.json(responseFormat.success({ - user: user.toJSON(), - token, - expires_in: 7 * 24 * 60 * 60 // 7天 - }, '登录成功')); + // 生成刷新令牌(长期有效) + const refreshToken = jwt.sign( + { + id: user.id, + username: user.username, + type: 'refresh' + }, + process.env.JWT_SECRET, + { expiresIn: '7d' } // 7天 + ); + + const userJson = user.toJSON(); + console.log('用户JSON数据:', userJson); + + // 确保用户数据是纯JSON对象,包含角色信息 + const userData = { + id: user.id, + username: user.username, + real_name: user.real_name, + email: user.email, + phone: user.phone, + role_id: user.role_id, + status: user.status, + last_login: user.last_login, + avatar: user.avatar, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + role: user.role ? { + id: user.role.id, + name: user.role.name, + permissions: user.role.permissions + } : null + }; + + const responseData = { + user: userData, + accessToken, + refreshToken, + accessTokenExpiresIn: 15 * 60, // 15分钟(秒) + refreshTokenExpiresIn: 7 * 24 * 60 * 60 // 7天(秒) + }; + + console.log('响应数据:', responseData); + + res.json(responseFormat.success(responseData, '登录成功')); } catch (error) { console.error('登录错误详情:', { message: error.message, @@ -174,20 +215,30 @@ const changePassword = async (req, res) => { // 刷新令牌 const refreshToken = async (req, res) => { try { - const { refresh_token } = req.body; + const { refreshToken: refresh_token } = req.body; if (!refresh_token) { return res.status(400).json(responseFormat.error('刷新令牌不能为空')); } - // 验证刷新令牌(这里简化处理,实际应该使用专门的刷新令牌机制) - const decoded = jwt.verify(refresh_token, process.env.JWT_SECRET); + // 验证刷新令牌 + let decoded; + try { + decoded = jwt.verify(refresh_token, process.env.JWT_SECRET); + } catch (error) { + return res.status(401).json(responseFormat.error('刷新令牌无效或已过期')); + } + + // 检查令牌类型 + if (decoded.type !== 'refresh') { + return res.status(401).json(responseFormat.error('无效的令牌类型')); + } const user = await User.findByPk(decoded.id, { include: [{ model: Role, as: 'role', - attributes: ['permissions'] + attributes: ['id', 'name', 'permissions'] }] }); @@ -196,24 +247,39 @@ const refreshToken = async (req, res) => { } // 生成新的访问令牌 - const newToken = jwt.sign( + const newAccessToken = jwt.sign( { id: user.id, username: user.username, role_id: user.role_id, - permissions: user.role?.permissions || [] + permissions: user.role?.permissions || [], + type: 'access' }, process.env.JWT_SECRET, - { expiresIn: process.env.JWT_EXPIRE || '7d' } + { expiresIn: '15m' } // 15分钟 + ); + + // 可选:生成新的刷新令牌(滚动刷新) + const newRefreshToken = jwt.sign( + { + id: user.id, + username: user.username, + type: 'refresh' + }, + process.env.JWT_SECRET, + { expiresIn: '7d' } // 7天 ); res.json(responseFormat.success({ - token: newToken, - expires_in: 7 * 24 * 60 * 60 + accessToken: newAccessToken, + refreshToken: newRefreshToken, + accessTokenExpiresIn: 15 * 60, // 15分钟(秒) + refreshTokenExpiresIn: 7 * 24 * 60 * 60, // 7天(秒) + user: user.toJSON() }, '令牌刷新成功')); } catch (error) { console.error('刷新令牌错误:', error); - res.status(401).json(responseFormat.error('刷新令牌无效')); + res.status(401).json(responseFormat.error('刷新令牌失败')); } }; diff --git a/insurance_backend/controllers/dataWarehouseController.js b/insurance_backend/controllers/dataWarehouseController.js index 6b58dac..d96ce0c 100644 --- a/insurance_backend/controllers/dataWarehouseController.js +++ b/insurance_backend/controllers/dataWarehouseController.js @@ -1,206 +1,307 @@ -const { User, Role, InsuranceApplication, Policy, Claim, InsuranceType } = require('../models'); -const responseFormat = require('../utils/response'); -const { Op } = require('sequelize'); +const { InsuranceApplication, Policy, Claim, InsuranceType, User } = require('../models'); +const { Op, Sequelize } = require('sequelize'); -// 获取数据览仓概览数据 +// 获取数据仓库概览 const getOverview = async (req, res) => { try { - const [ - totalUsers, - totalApplications, - totalPolicies, - totalClaims, - activePolicies, - approvedClaims, - pendingClaims - ] = await Promise.all([ - User.count(), - InsuranceApplication.count(), - Policy.count(), - Claim.count(), - Policy.count({ where: { policy_status: 'active' } }), - Claim.count({ where: { claim_status: 'approved' } }), - Claim.count({ where: { claim_status: 'pending' } }) - ]); + // 获取总申请数 + const totalApplications = await InsuranceApplication.count(); - res.json(responseFormat.success({ - totalUsers, - totalApplications, - totalPolicies, - totalClaims, - activePolicies, - approvedClaims, - pendingClaims - }, '获取数据览仓概览成功')); + // 获取总保单数 + const totalPolicies = await Policy.count(); + + // 获取总理赔数 + const totalClaims = await Claim.count(); + + // 获取总保费收入 + const totalPremium = await Policy.sum('premium_amount') || 0; + + // 获取总理赔支出 + const totalClaimAmount = await Claim.sum('claim_amount') || 0; + + // 获取活跃保单数 + const activePolicies = await Policy.count({ + where: { + policy_status: 'active' + } + }); + + // 获取待处理申请数 + const pendingApplications = await InsuranceApplication.count({ + where: { + status: 'pending' + } + }); + + // 获取待处理理赔数 + const pendingClaims = await Claim.count({ + where: { + claim_status: 'pending' + } + }); + + res.json({ + success: true, + data: { + totalApplications, + totalPolicies, + totalClaims, + totalPremium: parseFloat(totalPremium), + totalClaimAmount: parseFloat(totalClaimAmount), + activePolicies, + pendingApplications, + pendingClaims, + profitLoss: parseFloat(totalPremium) - parseFloat(totalClaimAmount) + } + }); } catch (error) { - console.error('获取数据览仓概览错误:', error); - res.status(500).json(responseFormat.error('获取数据览仓概览失败')); + console.error('获取数据仓库概览失败:', error); + res.status(500).json({ + success: false, + message: '获取数据仓库概览失败', + error: error.message + }); } }; // 获取保险类型分布数据 const getInsuranceTypeDistribution = async (req, res) => { try { - const types = await InsuranceType.findAll({ - attributes: ['id', 'name', 'description'], - where: { status: 'active' } + const distribution = await InsuranceApplication.findAll({ + attributes: [ + 'insurance_type_id', + [Sequelize.fn('COUNT', Sequelize.col('InsuranceApplication.id')), 'count'] + ], + include: [{ + model: InsuranceType, + as: 'insurance_type', + attributes: ['name'] + }], + group: ['insurance_type_id', 'insurance_type.id'], + order: [[Sequelize.fn('COUNT', Sequelize.col('InsuranceApplication.id')), 'DESC']] }); - - const distribution = await Promise.all( - types.map(async type => { - const count = await InsuranceApplication.count({ - where: { insurance_type_id: type.id } - }); + + // 计算总数用于百分比计算 + const totalCount = distribution.reduce((sum, item) => sum + parseInt(item.dataValues.count), 0); + + res.json({ + success: true, + data: distribution.map(item => { + const count = parseInt(item.dataValues.count); return { - id: type.id, - name: type.name, - description: type.description, - count + type: item.insurance_type.name, + count: count, + percentage: totalCount > 0 ? ((count / totalCount) * 100).toFixed(2) : 0 }; }) - ); - - res.json(responseFormat.success(distribution, '获取保险类型分布成功')); + }); } catch (error) { - console.error('获取保险类型分布错误:', error); - res.status(500).json(responseFormat.error('获取保险类型分布失败')); + console.error('获取保险类型分布失败:', error); + res.status(500).json({ + success: false, + message: '获取保险类型分布失败', + error: error.message + }); } }; // 获取申请状态分布数据 const getApplicationStatusDistribution = async (req, res) => { try { - const [ - pendingCount, - approvedCount, - rejectedCount, - underReviewCount - ] = await Promise.all([ - InsuranceApplication.count({ where: { status: 'pending' } }), - InsuranceApplication.count({ where: { status: 'approved' } }), - InsuranceApplication.count({ where: { status: 'rejected' } }), - InsuranceApplication.count({ where: { status: 'under_review' } }) - ]); - - res.json(responseFormat.success([ - { status: 'pending', name: '待处理', count: pendingCount }, - { status: 'under_review', name: '审核中', count: underReviewCount }, - { status: 'approved', name: '已批准', count: approvedCount }, - { status: 'rejected', name: '已拒绝', count: rejectedCount } - ], '获取申请状态分布成功')); + const distribution = await InsuranceApplication.findAll({ + attributes: [ + 'status', + [Sequelize.fn('COUNT', Sequelize.col('id')), 'count'] + ], + group: ['status'], + order: [[Sequelize.fn('COUNT', Sequelize.col('id')), 'DESC']] + }); + + // 计算总数用于百分比计算 + const totalCount = distribution.reduce((sum, item) => sum + parseInt(item.dataValues.count), 0); + + res.json({ + success: true, + data: distribution.map(item => { + const count = parseInt(item.dataValues.count); + return { + status: item.status, + label: getStatusLabel(item.status), + count: count, + percentage: totalCount > 0 ? ((count / totalCount) * 100).toFixed(2) : 0 + }; + }) + }); } catch (error) { - console.error('获取申请状态分布错误:', error); - res.status(500).json(responseFormat.error('获取申请状态分布失败')); + console.error('获取申请状态分布失败:', error); + res.status(500).json({ + success: false, + message: '获取申请状态分布失败', + error: error.message + }); } }; +// 状态标签映射 +function getStatusLabel(status) { + const statusMap = { + 'pending': '待初审', + 'initial_approved': '初审通过待复核', + 'under_review': '已支付待复核', + 'approved': '已批准', + 'rejected': '已拒绝', + 'paid': '已支付' + }; + return statusMap[status] || status; +} + // 获取近7天趋势数据 const getTrendData = async (req, res) => { try { - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + const { days = 7 } = req.query; + const endDate = new Date(); + const startDate = new Date(); + startDate.setDate(endDate.getDate() - parseInt(days)); - // 生成近7天的日期数组 - const dateArray = []; - for (let i = 6; i >= 0; i--) { - const date = new Date(sevenDaysAgo); - date.setDate(date.getDate() + i); - dateArray.push(date.toISOString().split('T')[0]); // 格式化为YYYY-MM-DD - } + // 获取每日申请数据 + const applicationTrend = await InsuranceApplication.findAll({ + attributes: [ + [Sequelize.fn('DATE', Sequelize.col('application_date')), 'date'], + [Sequelize.fn('COUNT', Sequelize.col('id')), 'count'] + ], + where: { + application_date: { + [Op.between]: [startDate, endDate] + } + }, + group: [Sequelize.fn('DATE', Sequelize.col('application_date'))], + order: [[Sequelize.fn('DATE', Sequelize.col('application_date')), 'ASC']] + }); - // 获取每天的新增数据 - const dailyData = await Promise.all( - dateArray.map(async date => { - const startDate = new Date(`${date} 00:00:00`); - const endDate = new Date(`${date} 23:59:59`); - - const [newApplications, newPolicies, newClaims] = await Promise.all([ - InsuranceApplication.count({ - where: { - created_at: { - [Op.between]: [startDate, endDate] - } - } - }), - Policy.count({ - where: { - created_at: { - [Op.between]: [startDate, endDate] - } - } - }), - Claim.count({ - where: { - created_at: { - [Op.between]: [startDate, endDate] - } - } - }) - ]); - - return { - date, - newApplications, - newPolicies, - newClaims - }; - }) - ); + // 获取每日保单数据 + const policyTrend = await Policy.findAll({ + attributes: [ + [Sequelize.fn('DATE', Sequelize.col('created_at')), 'date'], + [Sequelize.fn('COUNT', Sequelize.col('id')), 'count'] + ], + where: { + created_at: { + [Op.between]: [startDate, endDate] + } + }, + group: [Sequelize.fn('DATE', Sequelize.col('created_at'))], + order: [[Sequelize.fn('DATE', Sequelize.col('created_at')), 'ASC']] + }); - res.json(responseFormat.success(dailyData, '获取趋势数据成功')); + // 获取每日理赔数据 + const claimTrend = await Claim.findAll({ + attributes: [ + [Sequelize.fn('DATE', Sequelize.col('claim_date')), 'date'], + [Sequelize.fn('COUNT', Sequelize.col('id')), 'count'] + ], + where: { + claim_date: { + [Op.between]: [startDate, endDate] + } + }, + group: [Sequelize.fn('DATE', Sequelize.col('claim_date'))], + order: [[Sequelize.fn('DATE', Sequelize.col('claim_date')), 'ASC']] + }); + + res.json({ + success: true, + data: { + applications: applicationTrend.map(item => ({ + date: item.dataValues.date, + count: parseInt(item.dataValues.count) + })), + policies: policyTrend.map(item => ({ + date: item.dataValues.date, + count: parseInt(item.dataValues.count) + })), + claims: claimTrend.map(item => ({ + date: item.dataValues.date, + count: parseInt(item.dataValues.count) + })) + } + }); } catch (error) { - console.error('获取趋势数据错误:', error); - res.status(500).json(responseFormat.error('获取趋势数据失败')); + console.error('获取趋势数据失败:', error); + res.status(500).json({ + success: false, + message: '获取趋势数据失败', + error: error.message + }); } }; -// 获取赔付统计数据 +// 获取理赔统计 const getClaimStats = async (req, res) => { try { - const claims = await Claim.findAll({ - attributes: ['id', 'claim_amount', 'policy_id'], - include: [{ - model: Policy, - as: 'policy', - attributes: ['policy_no', 'insurance_type_id'] - }] + // 获取理赔状态分布 + const claimStatusDistribution = await Claim.findAll({ + attributes: [ + 'claim_status', + [Sequelize.fn('COUNT', Sequelize.col('id')), 'count'], + [Sequelize.fn('SUM', Sequelize.col('claim_amount')), 'total_amount'] + ], + group: ['claim_status'] }); - - // 按保险类型分组统计赔付金额 - const typeStats = {}; - claims.forEach(claim => { - const typeId = claim.policy?.insurance_type_id; - if (typeId) { - if (!typeStats[typeId]) { - typeStats[typeId] = { id: typeId, totalAmount: 0, count: 0 }; + + // 获取月度理赔趋势 + const monthlyClaimTrend = await Claim.findAll({ + attributes: [ + [Sequelize.fn('DATE_FORMAT', Sequelize.col('claim_date'), '%Y-%m'), 'month'], + [Sequelize.fn('COUNT', Sequelize.col('id')), 'count'], + [Sequelize.fn('SUM', Sequelize.col('claim_amount')), 'total_amount'] + ], + where: { + claim_date: { + [Op.gte]: Sequelize.literal('DATE_SUB(NOW(), INTERVAL 12 MONTH)') } - typeStats[typeId].totalAmount += parseFloat(claim.claim_amount || 0); - typeStats[typeId].count += 1; + }, + group: [Sequelize.fn('DATE_FORMAT', Sequelize.col('claim_date'), '%Y-%m')], + order: [[Sequelize.fn('DATE_FORMAT', Sequelize.col('claim_date'), '%Y-%m'), 'ASC']] + }); + + res.json({ + success: true, + data: { + statusDistribution: claimStatusDistribution.map(item => ({ + status: item.claim_status, + label: getClaimStatusLabel(item.claim_status), + count: parseInt(item.dataValues.count), + totalAmount: parseFloat(item.dataValues.total_amount || 0) + })), + monthlyTrend: monthlyClaimTrend.map(item => ({ + month: item.dataValues.month, + count: parseInt(item.dataValues.count), + totalAmount: parseFloat(item.dataValues.total_amount || 0) + })) } }); - - // 获取保险类型名称 - const typeIds = Object.keys(typeStats).map(id => parseInt(id)); - const types = await InsuranceType.findAll({ - attributes: ['id', 'name'], - where: { id: typeIds } - }); - - types.forEach(type => { - if (typeStats[type.id]) { - typeStats[type.id].name = type.name; - } - }); - - const result = Object.values(typeStats); - - res.json(responseFormat.success(result, '获取赔付统计成功')); } catch (error) { - console.error('获取赔付统计错误:', error); - res.status(500).json(responseFormat.error('获取赔付统计失败')); + console.error('获取理赔统计失败:', error); + res.status(500).json({ + success: false, + message: '获取理赔统计失败', + error: error.message + }); } }; +// 理赔状态标签映射 +function getClaimStatusLabel(status) { + const statusMap = { + 'pending': '待审核', + 'approved': '已批准', + 'rejected': '已拒绝', + 'processing': '处理中', + 'paid': '已支付' + }; + return statusMap[status] || status; +} + module.exports = { getOverview, getInsuranceTypeDistribution, diff --git a/insurance_backend/controllers/deviceAlertController.js b/insurance_backend/controllers/deviceAlertController.js new file mode 100644 index 0000000..33011ff --- /dev/null +++ b/insurance_backend/controllers/deviceAlertController.js @@ -0,0 +1,421 @@ +const { DeviceAlert, Device, User } = require('../models'); +const { Op } = require('sequelize'); +const logger = require('../utils/logger'); + +/** + * 设备预警控制器 + */ +class DeviceAlertController { + + /** + * 获取预警统计信息 + */ + static async getAlertStats(req, res) { + try { + const { farm_id, start_date, end_date } = req.query; + + // 构建查询条件 + const whereCondition = {}; + if (farm_id) { + whereCondition.farm_id = farm_id; + } + if (start_date && end_date) { + whereCondition.alert_time = { + [Op.between]: [new Date(start_date), new Date(end_date)] + }; + } + + // 获取总预警数 + const totalAlerts = await DeviceAlert.count({ + where: whereCondition + }); + + // 按级别统计 + const alertsByLevel = await DeviceAlert.findAll({ + attributes: [ + 'alert_level', + [DeviceAlert.sequelize.fn('COUNT', DeviceAlert.sequelize.col('id')), 'count'] + ], + where: whereCondition, + group: ['alert_level'], + raw: true + }); + + // 按状态统计 + const alertsByStatus = await DeviceAlert.findAll({ + attributes: [ + 'status', + [DeviceAlert.sequelize.fn('COUNT', DeviceAlert.sequelize.col('id')), 'count'] + ], + where: whereCondition, + group: ['status'], + raw: true + }); + + // 按类型统计 + const alertsByType = await DeviceAlert.findAll({ + attributes: [ + 'alert_type', + [DeviceAlert.sequelize.fn('COUNT', DeviceAlert.sequelize.col('id')), 'count'] + ], + where: whereCondition, + group: ['alert_type'], + raw: true + }); + + // 未读预警数 + const unreadAlerts = await DeviceAlert.count({ + where: { + ...whereCondition, + is_read: false + } + }); + + // 今日新增预警 + const today = new Date(); + today.setHours(0, 0, 0, 0); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + + const todayAlerts = await DeviceAlert.count({ + where: { + ...whereCondition, + alert_time: { + [Op.gte]: today, + [Op.lt]: tomorrow + } + } + }); + + res.json({ + success: true, + data: { + total_alerts: totalAlerts, + unread_alerts: unreadAlerts, + today_alerts: todayAlerts, + alerts_by_level: alertsByLevel, + alerts_by_status: alertsByStatus, + alerts_by_type: alertsByType + } + }); + + } catch (error) { + logger.error('获取预警统计失败:', error); + res.status(500).json({ + success: false, + message: '获取预警统计失败', + error: error.message + }); + } + } + + /** + * 获取预警列表 + */ + static async getAlertList(req, res) { + try { + const { + page = 1, + limit = 20, + alert_level, + status, + alert_type, + farm_id, + start_date, + end_date, + is_read, + device_code, + keyword + } = req.query; + + // 构建查询条件 + const whereCondition = {}; + if (alert_level) { + whereCondition.alert_level = alert_level; + } + if (status) { + whereCondition.status = status; + } + if (alert_type) { + whereCondition.alert_type = alert_type; + } + if (farm_id) { + whereCondition.farm_id = farm_id; + } + if (is_read !== undefined) { + whereCondition.is_read = is_read === 'true'; + } + if (start_date && end_date) { + whereCondition.alert_time = { + [Op.between]: [new Date(start_date), new Date(end_date)] + }; + } + if (keyword) { + whereCondition[Op.or] = [ + { alert_title: { [Op.like]: `%${keyword}%` } }, + { alert_content: { [Op.like]: `%${keyword}%` } } + ]; + } + + // 设备查询条件 + const deviceWhere = {}; + if (device_code) { + deviceWhere.device_number = { [Op.like]: `%${device_code}%` }; + } + + const offset = (page - 1) * limit; + + const { count, rows } = await DeviceAlert.findAndCountAll({ + where: whereCondition, + include: [ + { + model: Device, + as: 'device', + where: Object.keys(deviceWhere).length > 0 ? deviceWhere : undefined, + attributes: ['id', 'device_number', 'device_name', 'device_type', 'installation_location'] + }, + { + model: User, + as: 'handler', + attributes: ['id', 'username', 'real_name'], + required: false + } + ], + order: [['alert_time', 'DESC']], + limit: parseInt(limit), + offset: offset + }); + + res.json({ + success: true, + data: { + alerts: rows, + pagination: { + current_page: parseInt(page), + per_page: parseInt(limit), + total: count, + total_pages: Math.ceil(count / limit) + } + } + }); + + } catch (error) { + logger.error('获取预警列表失败:', error); + res.status(500).json({ + success: false, + message: '获取预警列表失败', + error: error.message + }); + } + } + + /** + * 获取预警详情 + */ + static async getAlertDetail(req, res) { + try { + const { id } = req.params; + + const alert = await DeviceAlert.findByPk(id, { + include: [ + { + model: Device, + as: 'device', + attributes: ['id', 'device_number', 'device_name', 'device_type', 'device_model', 'manufacturer', 'installation_location'] + }, + { + model: User, + as: 'handler', + attributes: ['id', 'username', 'real_name'], + required: false + } + ] + }); + + if (!alert) { + return res.status(404).json({ + success: false, + message: '预警信息不存在' + }); + } + + res.json({ + success: true, + data: alert + }); + + } catch (error) { + logger.error('获取预警详情失败:', error); + res.status(500).json({ + success: false, + message: '获取预警详情失败', + error: error.message + }); + } + } + + /** + * 标记预警为已读 + */ + static async markAsRead(req, res) { + try { + const { id } = req.params; + const userId = req.user.id; + + const alert = await DeviceAlert.findByPk(id); + if (!alert) { + return res.status(404).json({ + success: false, + message: '预警信息不存在' + }); + } + + await alert.update({ + is_read: true, + read_time: new Date() + }); + + res.json({ + success: true, + message: '标记已读成功' + }); + + } catch (error) { + logger.error('标记预警已读失败:', error); + res.status(500).json({ + success: false, + message: '标记预警已读失败', + error: error.message + }); + } + } + + /** + * 批量标记预警为已读 + */ + static async batchMarkAsRead(req, res) { + try { + const { alert_ids } = req.body; + const userId = req.user.id; + + if (!alert_ids || !Array.isArray(alert_ids) || alert_ids.length === 0) { + return res.status(400).json({ + success: false, + message: '请提供有效的预警ID列表' + }); + } + + await DeviceAlert.update( + { + is_read: true, + read_time: new Date() + }, + { + where: { + id: { + [Op.in]: alert_ids + } + } + } + ); + + res.json({ + success: true, + message: '批量标记已读成功' + }); + + } catch (error) { + logger.error('批量标记预警已读失败:', error); + res.status(500).json({ + success: false, + message: '批量标记预警已读失败', + error: error.message + }); + } + } + + /** + * 处理预警 + */ + static async handleAlert(req, res) { + try { + const { id } = req.params; + const { status, handle_note } = req.body; + const userId = req.user.id; + + const alert = await DeviceAlert.findByPk(id); + if (!alert) { + return res.status(404).json({ + success: false, + message: '预警信息不存在' + }); + } + + await alert.update({ + status, + handler_id: userId, + handle_time: new Date(), + handle_note, + is_read: true, + read_time: alert.read_time || new Date() + }); + + res.json({ + success: true, + message: '预警处理成功' + }); + + } catch (error) { + logger.error('处理预警失败:', error); + res.status(500).json({ + success: false, + message: '处理预警失败', + error: error.message + }); + } + } + + /** + * 创建预警(系统内部使用) + */ + static async createAlert(req, res) { + try { + const { + device_id, + alert_type, + alert_level, + alert_title, + alert_content, + farm_id, + barn_id + } = req.body; + + const alert = await DeviceAlert.create({ + device_id, + alert_type, + alert_level, + alert_title, + alert_content, + farm_id, + barn_id, + alert_time: new Date() + }); + + res.json({ + success: true, + message: '预警创建成功', + data: alert + }); + + } catch (error) { + logger.error('创建预警失败:', error); + res.status(500).json({ + success: false, + message: '创建预警失败', + error: error.message + }); + } + } +} + +module.exports = DeviceAlertController; \ No newline at end of file diff --git a/insurance_backend/controllers/deviceController.js b/insurance_backend/controllers/deviceController.js new file mode 100644 index 0000000..78bd0b4 --- /dev/null +++ b/insurance_backend/controllers/deviceController.js @@ -0,0 +1,435 @@ +const { Device, DeviceAlert, User } = require('../models'); +const { Op } = require('sequelize'); +const logger = require('../utils/logger'); + +/** + * 设备控制器 + */ +class DeviceController { + + /** + * 获取设备列表 + */ + static async getDeviceList(req, res) { + try { + const { + page = 1, + limit = 20, + device_type, + status, + farm_id, + pen_id, + keyword + } = req.query; + + // 构建查询条件 + const whereCondition = {}; + if (device_type) { + whereCondition.device_type = device_type; + } + if (status) { + whereCondition.status = status; + } + if (farm_id) { + whereCondition.farm_id = farm_id; + } + if (pen_id) { + whereCondition.pen_id = pen_id; + } + if (keyword) { + whereCondition[Op.or] = [ + { device_code: { [Op.like]: `%${keyword}%` } }, + { device_name: { [Op.like]: `%${keyword}%` } }, + { device_model: { [Op.like]: `%${keyword}%` } }, + { manufacturer: { [Op.like]: `%${keyword}%` } } + ]; + } + + const offset = (page - 1) * limit; + + const { count, rows } = await Device.findAndCountAll({ + where: whereCondition, + include: [ + { + model: User, + as: 'creator', + attributes: ['id', 'username', 'real_name'], + required: false + } + ], + order: [['created_at', 'DESC']], + limit: parseInt(limit), + offset: offset + }); + + res.json({ + success: true, + data: { + devices: rows, + pagination: { + current_page: parseInt(page), + per_page: parseInt(limit), + total: count, + total_pages: Math.ceil(count / limit) + } + } + }); + + } catch (error) { + logger.error('获取设备列表失败:', error); + res.status(500).json({ + success: false, + message: '获取设备列表失败', + error: error.message + }); + } + } + + /** + * 获取设备详情 + */ + static async getDeviceDetail(req, res) { + try { + const { id } = req.params; + + const device = await Device.findByPk(id, { + include: [ + { + model: User, + as: 'creator', + attributes: ['id', 'username', 'real_name'], + required: false + }, + { + model: User, + as: 'updater', + attributes: ['id', 'username', 'real_name'], + required: false + } + ] + }); + + if (!device) { + return res.status(404).json({ + success: false, + message: '设备不存在' + }); + } + + // 获取设备相关的预警统计 + const alertStats = await DeviceAlert.findAll({ + attributes: [ + 'alert_level', + [DeviceAlert.sequelize.fn('COUNT', DeviceAlert.sequelize.col('id')), 'count'] + ], + where: { + device_id: id + }, + group: ['alert_level'], + raw: true + }); + + // 获取最近的预警记录 + const recentAlerts = await DeviceAlert.findAll({ + where: { + device_id: id + }, + order: [['alert_time', 'DESC']], + limit: 5, + attributes: ['id', 'alert_type', 'alert_level', 'alert_title', 'alert_time', 'status'] + }); + + res.json({ + success: true, + data: { + device, + alert_stats: alertStats, + recent_alerts: recentAlerts + } + }); + + } catch (error) { + logger.error('获取设备详情失败:', error); + res.status(500).json({ + success: false, + message: '获取设备详情失败', + error: error.message + }); + } + } + + /** + * 创建设备 + */ + static async createDevice(req, res) { + try { + const { + device_code, + device_name, + device_type, + device_model, + manufacturer, + installation_location, + installation_date, + farm_id, + pen_id, + status = 'normal' + } = req.body; + + const userId = req.user.id; + + // 检查设备编号是否已存在 + const existingDevice = await Device.findOne({ + where: { device_code } + }); + + if (existingDevice) { + return res.status(400).json({ + success: false, + message: '设备编号已存在' + }); + } + + const device = await Device.create({ + device_code, + device_name, + device_type, + device_model, + manufacturer, + installation_location, + installation_date, + farm_id, + pen_id, + status, + created_by: userId, + updated_by: userId + }); + + res.json({ + success: true, + message: '设备创建成功', + data: device + }); + + } catch (error) { + logger.error('创建设备失败:', error); + res.status(500).json({ + success: false, + message: '创建设备失败', + error: error.message + }); + } + } + + /** + * 更新设备 + */ + static async updateDevice(req, res) { + try { + const { id } = req.params; + const { + device_code, + device_name, + device_type, + device_model, + manufacturer, + installation_location, + installation_date, + farm_id, + pen_id, + status + } = req.body; + + const userId = req.user.id; + + const device = await Device.findByPk(id); + if (!device) { + return res.status(404).json({ + success: false, + message: '设备不存在' + }); + } + + // 如果修改了设备编号,检查是否与其他设备重复 + if (device_code && device_code !== device.device_code) { + const existingDevice = await Device.findOne({ + where: { + device_code, + id: { [Op.ne]: id } + } + }); + + if (existingDevice) { + return res.status(400).json({ + success: false, + message: '设备编号已存在' + }); + } + } + + await device.update({ + device_code, + device_name, + device_type, + device_model, + manufacturer, + installation_location, + installation_date, + farm_id, + pen_id, + status, + updated_by: userId + }); + + res.json({ + success: true, + message: '设备更新成功', + data: device + }); + + } catch (error) { + logger.error('更新设备失败:', error); + res.status(500).json({ + success: false, + message: '更新设备失败', + error: error.message + }); + } + } + + /** + * 删除设备 + */ + static async deleteDevice(req, res) { + try { + const { id } = req.params; + + const device = await Device.findByPk(id); + if (!device) { + return res.status(404).json({ + success: false, + message: '设备不存在' + }); + } + + // 检查是否有关联的预警记录 + const alertCount = await DeviceAlert.count({ + where: { device_id: id } + }); + + if (alertCount > 0) { + return res.status(400).json({ + success: false, + message: '该设备存在预警记录,无法删除' + }); + } + + await device.destroy(); + + res.json({ + success: true, + message: '设备删除成功' + }); + + } catch (error) { + logger.error('删除设备失败:', error); + res.status(500).json({ + success: false, + message: '删除设备失败', + error: error.message + }); + } + } + + /** + * 获取设备类型列表 + */ + static async getDeviceTypes(req, res) { + try { + const deviceTypes = await Device.findAll({ + attributes: [ + [Device.sequelize.fn('DISTINCT', Device.sequelize.col('device_type')), 'device_type'] + ], + where: { + device_type: { + [Op.ne]: null + } + }, + raw: true + }); + + res.json({ + success: true, + data: deviceTypes.map(item => item.device_type) + }); + + } catch (error) { + logger.error('获取设备类型失败:', error); + res.status(500).json({ + success: false, + message: '获取设备类型失败', + error: error.message + }); + } + } + + /** + * 获取设备状态统计 + */ + static async getDeviceStats(req, res) { + try { + const { farm_id } = req.query; + + const whereCondition = {}; + if (farm_id) { + whereCondition.farm_id = farm_id; + } + + // 按状态统计 + const devicesByStatus = await Device.findAll({ + attributes: [ + 'status', + [Device.sequelize.fn('COUNT', Device.sequelize.col('id')), 'count'] + ], + where: whereCondition, + group: ['status'], + raw: true + }); + + // 按类型统计 + const devicesByType = await Device.findAll({ + attributes: [ + 'device_type', + [Device.sequelize.fn('COUNT', Device.sequelize.col('id')), 'count'] + ], + where: whereCondition, + group: ['device_type'], + raw: true + }); + + // 总设备数 + const totalDevices = await Device.count({ + where: whereCondition + }); + + res.json({ + success: true, + data: { + total_devices: totalDevices, + devices_by_status: devicesByStatus, + devices_by_type: devicesByType + } + }); + + } catch (error) { + logger.error('获取设备统计失败:', error); + res.status(500).json({ + success: false, + message: '获取设备统计失败', + error: error.message + }); + } + } +} + +module.exports = DeviceController; \ No newline at end of file diff --git a/insurance_backend/controllers/menuController.js b/insurance_backend/controllers/menuController.js index 11f4ef3..5a88bc3 100644 --- a/insurance_backend/controllers/menuController.js +++ b/insurance_backend/controllers/menuController.js @@ -7,11 +7,16 @@ const { User, Role, Menu } = require('../models'); */ exports.getMenus = async (req, res) => { try { + console.log('开始获取菜单,用户信息:', req.user); + // 获取用户ID(从JWT中解析或通过其他方式获取) const userId = req.user?.id; // 假设通过认证中间件解析后存在 + console.log('用户ID:', userId); + // 如果没有用户ID,返回基础菜单 if (!userId) { + console.log('没有用户ID,返回基础菜单'); const menus = await Menu.findAll({ where: { parent_id: null, @@ -33,6 +38,8 @@ exports.getMenus = async (req, res) => { order: [['order', 'ASC']] }); + console.log('基础菜单查询结果:', menus.length); + return res.json({ code: 200, status: 'success', @@ -41,16 +48,20 @@ exports.getMenus = async (req, res) => { }); } + console.log('查询用户信息...'); // 获取用户信息和角色 const user = await User.findByPk(userId, { include: [ { model: Role, + as: 'role', attributes: ['id', 'name', 'permissions'] } ] }); + console.log('用户查询结果:', user ? '找到用户' : '用户不存在'); + if (!user) { return res.status(404).json({ code: 404, @@ -60,8 +71,10 @@ exports.getMenus = async (req, res) => { } // 获取角色的权限列表 - const userPermissions = user.Role?.permissions || []; + const userPermissions = user.role?.permissions || []; + console.log('用户权限:', userPermissions); + console.log('查询菜单数据...'); // 查询菜单,这里简化处理,实际应用中可能需要根据权限过滤 const menus = await Menu.findAll({ where: { @@ -84,6 +97,8 @@ exports.getMenus = async (req, res) => { order: [['order', 'ASC']] }); + console.log('菜单查询结果:', menus.length); + // 这里可以添加根据权限过滤菜单的逻辑 // 简化示例,假设所有用户都能看到所有激活的菜单 @@ -95,10 +110,12 @@ exports.getMenus = async (req, res) => { }); } catch (error) { console.error('获取菜单失败:', error); + console.error('错误堆栈:', error.stack); return res.status(500).json({ code: 500, status: 'error', - message: '服务器内部错误' + message: '服务器内部错误', + error: error.message }); } }; diff --git a/insurance_backend/controllers/operationLogController.js b/insurance_backend/controllers/operationLogController.js new file mode 100644 index 0000000..e266be8 --- /dev/null +++ b/insurance_backend/controllers/operationLogController.js @@ -0,0 +1,344 @@ +const { OperationLog, User } = require('../models'); +const { Op } = require('sequelize'); +const ExcelJS = require('exceljs'); + +/** + * 操作日志控制器 + */ +class OperationLogController { + /** + * 获取操作日志列表 + */ + async getOperationLogs(req, res) { + try { + const { + page = 1, + limit = 20, + user_id, + operation_type, + operation_module, + status, + start_date, + end_date, + keyword + } = req.query; + + // 构建查询条件 + const whereConditions = {}; + + if (user_id) { + whereConditions.user_id = user_id; + } + + if (operation_type) { + whereConditions.operation_type = operation_type; + } + + if (operation_module) { + whereConditions.operation_module = operation_module; + } + + if (status) { + whereConditions.status = status; + } + + // 时间范围查询 + if (start_date || end_date) { + whereConditions.created_at = {}; + if (start_date) { + whereConditions.created_at[Op.gte] = new Date(start_date); + } + if (end_date) { + const endDateTime = new Date(end_date); + endDateTime.setHours(23, 59, 59, 999); + whereConditions.created_at[Op.lte] = endDateTime; + } + } + + // 关键词搜索 + if (keyword) { + whereConditions[Op.or] = [ + { operation_content: { [Op.like]: `%${keyword}%` } }, + { operation_target: { [Op.like]: `%${keyword}%` } }, + { request_url: { [Op.like]: `%${keyword}%` } }, + { ip_address: { [Op.like]: `%${keyword}%` } } + ]; + } + + // 分页参数 + const offset = (parseInt(page) - 1) * parseInt(limit); + + // 查询操作日志 + const { count, rows } = await OperationLog.findAndCountAll({ + where: whereConditions, + include: [ + { + model: User, + as: 'user', + attributes: ['id', 'username', 'real_name', 'email'] + } + ], + order: [['created_at', 'DESC']], + limit: parseInt(limit), + offset: offset + }); + + const totalPages = Math.ceil(count / parseInt(limit)); + + res.json({ + status: 'success', + message: '获取操作日志列表成功', + data: { + logs: rows, + total: count, + page: parseInt(page), + limit: parseInt(limit), + totalPages: totalPages + } + }); + } catch (error) { + console.error('获取操作日志列表失败:', error); + res.status(500).json({ + status: 'error', + message: '获取操作日志列表失败', + error: error.message + }); + } + } + + /** + * 获取操作日志统计信息 + */ + async getOperationStats(req, res) { + try { + const { + user_id, + start_date, + end_date + } = req.query; + + // 构建查询条件 + const whereConditions = {}; + + if (user_id) { + whereConditions.user_id = user_id; + } + + // 时间范围查询 + if (start_date || end_date) { + whereConditions.created_at = {}; + if (start_date) { + whereConditions.created_at[Op.gte] = new Date(start_date); + } + if (end_date) { + const endDateTime = new Date(end_date); + endDateTime.setHours(23, 59, 59, 999); + whereConditions.created_at[Op.lte] = endDateTime; + } + } + + // 获取统计数据 + const stats = await OperationLog.getOperationStats(whereConditions); + + res.json({ + status: 'success', + message: '获取操作日志统计成功', + data: stats + }); + } catch (error) { + console.error('获取操作日志统计失败:', error); + res.status(500).json({ + status: 'error', + message: '获取操作日志统计失败', + error: error.message + }); + } + } + + /** + * 获取操作日志详情 + */ + async getOperationLogById(req, res) { + try { + const { id } = req.params; + + const log = await OperationLog.findByPk(id, { + include: [ + { + model: User, + as: 'user', + attributes: ['id', 'username', 'real_name', 'email'] + } + ] + }); + + if (!log) { + return res.status(404).json({ + status: 'error', + message: '操作日志不存在' + }); + } + + res.json({ + status: 'success', + message: '获取操作日志详情成功', + data: log + }); + } catch (error) { + console.error('获取操作日志详情失败:', error); + res.status(500).json({ + status: 'error', + message: '获取操作日志详情失败', + error: error.message + }); + } + } + + /** + * 导出操作日志 + */ + async exportOperationLogs(req, res) { + try { + const { + user_id, + operation_type, + operation_module, + status, + start_date, + end_date, + keyword + } = req.body; + + // 构建查询条件 + const whereConditions = {}; + + if (user_id) { + whereConditions.user_id = user_id; + } + + if (operation_type) { + whereConditions.operation_type = operation_type; + } + + if (operation_module) { + whereConditions.operation_module = operation_module; + } + + if (status) { + whereConditions.status = status; + } + + // 时间范围查询 + if (start_date || end_date) { + whereConditions.created_at = {}; + if (start_date) { + whereConditions.created_at[Op.gte] = new Date(start_date); + } + if (end_date) { + const endDateTime = new Date(end_date); + endDateTime.setHours(23, 59, 59, 999); + whereConditions.created_at[Op.lte] = endDateTime; + } + } + + // 关键词搜索 + if (keyword) { + whereConditions[Op.or] = [ + { operation_content: { [Op.like]: `%${keyword}%` } }, + { operation_target: { [Op.like]: `%${keyword}%` } }, + { request_url: { [Op.like]: `%${keyword}%` } }, + { ip_address: { [Op.like]: `%${keyword}%` } } + ]; + } + + // 查询所有符合条件的日志 + const logs = await OperationLog.findAll({ + where: whereConditions, + include: [ + { + model: User, + as: 'user', + attributes: ['id', 'username', 'real_name', 'email'] + } + ], + order: [['created_at', 'DESC']] + }); + + // 创建Excel工作簿 + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('操作日志'); + + // 设置表头 + worksheet.columns = [ + { header: 'ID', key: 'id', width: 10 }, + { header: '操作用户', key: 'username', width: 15 }, + { header: '真实姓名', key: 'real_name', width: 15 }, + { header: '操作类型', key: 'operation_type', width: 15 }, + { header: '操作模块', key: 'operation_module', width: 15 }, + { header: '操作内容', key: 'operation_content', width: 30 }, + { header: '操作目标', key: 'operation_target', width: 20 }, + { header: '请求方法', key: 'request_method', width: 10 }, + { header: '请求URL', key: 'request_url', width: 30 }, + { header: 'IP地址', key: 'ip_address', width: 15 }, + { header: '执行时间(ms)', key: 'execution_time', width: 12 }, + { header: '状态', key: 'status', width: 10 }, + { header: '错误信息', key: 'error_message', width: 30 }, + { header: '创建时间', key: 'created_at', width: 20 } + ]; + + // 添加数据 + logs.forEach(log => { + worksheet.addRow({ + id: log.id, + username: log.user ? log.user.username : '', + real_name: log.user ? log.user.real_name : '', + operation_type: log.operation_type, + operation_module: log.operation_module, + operation_content: log.operation_content, + operation_target: log.operation_target, + request_method: log.request_method, + request_url: log.request_url, + ip_address: log.ip_address, + execution_time: log.execution_time, + status: log.status, + error_message: log.error_message, + created_at: log.created_at + }); + }); + + // 设置响应头 + const filename = `操作日志_${new Date().toISOString().slice(0, 10)}.xlsx`; + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`); + + // 写入响应 + await workbook.xlsx.write(res); + res.end(); + + // 记录导出操作日志 + await OperationLog.logOperation({ + user_id: req.user.id, + operation_type: 'export', + operation_module: 'operation_logs', + operation_content: '导出操作日志', + operation_target: `导出${logs.length}条记录`, + request_method: 'POST', + request_url: '/api/operation-logs/export', + request_params: req.body, + ip_address: req.ip, + user_agent: req.get('User-Agent'), + status: 'success' + }); + + } catch (error) { + console.error('导出操作日志失败:', error); + res.status(500).json({ + status: 'error', + message: '导出操作日志失败', + error: error.message + }); + } + } +} + +module.exports = new OperationLogController(); \ No newline at end of file diff --git a/insurance_backend/controllers/userController.js b/insurance_backend/controllers/userController.js index c32eb10..c2a8963 100644 --- a/insurance_backend/controllers/userController.js +++ b/insurance_backend/controllers/userController.js @@ -1,4 +1,5 @@ const { User, Role } = require('../models'); +const { Op } = require('sequelize'); const responseFormat = require('../utils/response'); // 获取用户列表 @@ -222,11 +223,171 @@ const updateUserStatus = async (req, res) => { } }; +// 获取个人资料 +const getProfile = async (req, res) => { + try { + const userId = req.user.id; + + const user = await User.findByPk(userId, { + include: [{ + model: Role, + as: 'role' + }] + }); + + if (!user) { + return res.status(404).json(responseFormat.error('用户不存在')); + } + + // 手动排除密码字段 + const userProfile = { + id: user.id, + username: user.username, + real_name: user.real_name, + email: user.email, + phone: user.phone, + role_id: user.role_id, + status: user.status, + last_login: user.last_login, + avatar: user.avatar, + created_at: user.created_at, + updated_at: user.updated_at, + role: user.role + }; + + res.json(responseFormat.success(userProfile, '获取个人资料成功')); + } catch (error) { + console.error('获取个人资料错误:', error); + res.status(500).json(responseFormat.error('获取个人资料失败')); + } +}; + +// 更新个人资料 +const updateProfile = async (req, res) => { + try { + const userId = req.user.id; + const { real_name, email, phone } = req.body; + + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json(responseFormat.error('用户不存在')); + } + + // 检查邮箱是否已被其他用户使用 + if (email && email !== user.email) { + const existingEmail = await User.findOne({ + where: { email, id: { [Op.ne]: userId } } + }); + if (existingEmail) { + return res.status(400).json(responseFormat.error('邮箱已被其他用户使用')); + } + } + + // 检查手机号是否已被其他用户使用 + if (phone && phone !== user.phone) { + const existingPhone = await User.findOne({ + where: { phone, id: { [Op.ne]: userId } } + }); + if (existingPhone) { + return res.status(400).json(responseFormat.error('手机号已被其他用户使用')); + } + } + + // 更新个人资料 + await user.update({ + real_name: real_name || user.real_name, + email: email || user.email, + phone: phone || user.phone + }); + + // 返回更新后的用户信息(不包含密码) + const updatedUser = await User.findByPk(userId, { + include: [{ + model: Role, + as: 'role', + attributes: ['id', 'name'] + }], + attributes: { exclude: ['password'] } + }); + + res.json(responseFormat.success(updatedUser, '个人资料更新成功')); + } catch (error) { + console.error('更新个人资料错误:', error); + res.status(500).json(responseFormat.error('更新个人资料失败')); + } +}; + +// 修改密码 +const changePassword = async (req, res) => { + try { + const userId = req.user.id; + const { currentPassword, newPassword } = req.body; + + if (!currentPassword || !newPassword) { + return res.status(400).json(responseFormat.error('当前密码和新密码不能为空')); + } + + if (newPassword.length < 6) { + return res.status(400).json(responseFormat.error('新密码长度至少6位')); + } + + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json(responseFormat.error('用户不存在')); + } + + // 验证当前密码 + const isValidPassword = await user.validatePassword(currentPassword); + if (!isValidPassword) { + return res.status(400).json(responseFormat.error('当前密码错误')); + } + + // 更新密码 + await user.update({ password: newPassword }); + + res.json(responseFormat.success(null, '密码修改成功')); + } catch (error) { + console.error('修改密码错误:', error); + res.status(500).json(responseFormat.error('修改密码失败')); + } +}; + +// 上传头像 +const uploadAvatar = async (req, res) => { + try { + const userId = req.user.id; + + if (!req.file) { + return res.status(400).json(responseFormat.error('请选择头像文件')); + } + + const user = await User.findByPk(userId); + if (!user) { + return res.status(404).json(responseFormat.error('用户不存在')); + } + + // 构建头像URL(这里假设文件已经通过multer中间件处理) + const avatarUrl = `/uploads/avatars/${req.file.filename}`; + + // 更新用户头像 + await user.update({ avatar: avatarUrl }); + + res.json(responseFormat.success({ avatar: avatarUrl }, '头像上传成功')); + } catch (error) { + console.error('上传头像错误:', error); + res.status(500).json(responseFormat.error('头像上传失败')); + } +}; + module.exports = { getUsers, getUser, createUser, updateUser, deleteUser, - updateUserStatus + updateUserStatus, + getProfile, + updateProfile, + changePassword, + uploadAvatar }; \ No newline at end of file diff --git a/insurance_backend/create_missing_tables.js b/insurance_backend/create_missing_tables.js new file mode 100644 index 0000000..796d13b --- /dev/null +++ b/insurance_backend/create_missing_tables.js @@ -0,0 +1,112 @@ +const mysql = require('mysql2/promise'); + +async function createMissingTables() { + const connection = await mysql.createConnection({ + host: '129.211.213.226', + port: 9527, + user: 'root', + password: 'aiotAiot123!', + database: 'insurance_data' + }); + + try { + console.log('=== 创建缺失的数据库表 ==='); + + // 1. 创建保险类型表 + await connection.execute(` + CREATE TABLE IF NOT EXISTS insurance_types ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '保险类型ID', + name VARCHAR(100) NOT NULL UNIQUE COMMENT '保险类型名称', + description TEXT NULL COMMENT '保险类型描述', + coverage_amount_min DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '最小保额', + coverage_amount_max DECIMAL(15,2) NOT NULL DEFAULT 1000000.00 COMMENT '最大保额', + premium_rate DECIMAL(5,4) NOT NULL DEFAULT 0.001 COMMENT '保费费率', + status ENUM('active', 'inactive') NOT NULL DEFAULT 'active' COMMENT '状态', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保险类型表' + `); + console.log('✓ 保险类型表创建成功'); + + // 2. 创建保险申请表 + await connection.execute(` + CREATE TABLE IF NOT EXISTS insurance_applications ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '申请ID', + application_no VARCHAR(50) NOT NULL UNIQUE COMMENT '申请编号', + customer_name VARCHAR(100) NOT NULL COMMENT '客户姓名', + customer_id_card VARCHAR(18) NOT NULL COMMENT '客户身份证号', + customer_phone VARCHAR(20) NOT NULL COMMENT '客户手机号', + customer_address VARCHAR(255) NOT NULL COMMENT '客户地址', + insurance_type_id INT NOT NULL COMMENT '保险类型ID', + insurance_category VARCHAR(50) NULL COMMENT '保险类别', + livestock_type VARCHAR(50) NULL COMMENT '牲畜类型', + livestock_count INT NULL DEFAULT 0 COMMENT '牲畜数量', + application_amount DECIMAL(15,2) NOT NULL COMMENT '申请金额', + status ENUM('pending', 'approved', 'rejected', 'under_review') NOT NULL DEFAULT 'pending' COMMENT '状态', + application_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请日期', + review_notes TEXT NULL COMMENT '审核备注', + reviewer_id INT NULL COMMENT '审核人ID', + review_date TIMESTAMP NULL COMMENT '审核日期', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保险申请表' + `); + console.log('✓ 保险申请表创建成功'); + + // 3. 创建保单表 + await connection.execute(` + CREATE TABLE IF NOT EXISTS policies ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '保单ID', + policy_no VARCHAR(50) NOT NULL UNIQUE COMMENT '保单编号', + application_id INT NOT NULL COMMENT '关联的保险申请ID', + insurance_type_id INT NOT NULL COMMENT '保险类型ID', + customer_id INT NOT NULL COMMENT '客户ID', + customer_name VARCHAR(100) NOT NULL COMMENT '客户姓名', + customer_id_card VARCHAR(18) NOT NULL COMMENT '客户身份证号', + customer_phone VARCHAR(20) NOT NULL COMMENT '客户手机号', + coverage_amount DECIMAL(15,2) NOT NULL COMMENT '保额', + premium_amount DECIMAL(15,2) NOT NULL COMMENT '保费金额', + start_date DATE NOT NULL COMMENT '保险开始日期', + end_date DATE NOT NULL COMMENT '保险结束日期', + policy_status ENUM('active', 'expired', 'cancelled', 'suspended') NOT NULL DEFAULT 'active' COMMENT '保单状态', + payment_status ENUM('paid', 'unpaid', 'partial') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态', + payment_date DATE NULL COMMENT '支付日期', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保单表' + `); + console.log('✓ 保单表创建成功'); + + // 4. 创建理赔表 + await connection.execute(` + CREATE TABLE IF NOT EXISTS claims ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '理赔ID', + claim_no VARCHAR(50) NOT NULL UNIQUE COMMENT '理赔编号', + policy_id INT NOT NULL COMMENT '关联的保单ID', + customer_id INT NOT NULL COMMENT '客户ID', + claim_amount DECIMAL(15,2) NOT NULL COMMENT '理赔金额', + claim_date DATE NOT NULL COMMENT '理赔发生日期', + incident_description TEXT NOT NULL COMMENT '事故描述', + claim_status ENUM('pending', 'approved', 'rejected', 'processing', 'paid') NOT NULL DEFAULT 'pending' COMMENT '理赔状态', + review_notes TEXT NULL COMMENT '审核备注', + reviewer_id INT NULL COMMENT '审核人ID', + review_date DATE NULL COMMENT '审核日期', + payment_date DATE NULL COMMENT '支付日期', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='理赔表' + `); + console.log('✓ 理赔表创建成功'); + + console.log('\n=== 检查创建的表 ==='); + const [tables] = await connection.execute('SHOW TABLES'); + console.log('当前数据库表:', tables.map(t => Object.values(t)[0])); + + } catch (error) { + console.error('创建表错误:', error); + } finally { + await connection.end(); + } +} + +createMissingTables(); \ No newline at end of file diff --git a/insurance_backend/debug-api.js b/insurance_backend/debug-api.js new file mode 100644 index 0000000..ddf1346 --- /dev/null +++ b/insurance_backend/debug-api.js @@ -0,0 +1,54 @@ +const express = require('express'); +const cors = require('cors'); + +// 创建简单的测试应用 +const app = express(); +const PORT = 3002; + +app.use(cors()); +app.use(express.json()); + +// 请求日志 +app.use((req, res, next) => { + console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); + next(); +}); + +// 测试路由 +app.get('/health', (req, res) => { + res.json({ status: 'ok', message: '测试服务器运行正常' }); +}); + +// 导入认证路由 +try { + const authRoutes = require('./routes/auth'); + app.use('/api/auth', authRoutes); + console.log('✅ 认证路由加载成功'); +} catch (error) { + console.error('❌ 认证路由加载失败:', error.message); +} + +// 导入设备路由 +try { + const deviceRoutes = require('./routes/devices'); + app.use('/api/devices', deviceRoutes); + console.log('✅ 设备路由加载成功'); +} catch (error) { + console.error('❌ 设备路由加载失败:', error.message); +} + +// 404处理 +app.use('*', (req, res) => { + console.log(`404 - 未找到路由: ${req.method} ${req.originalUrl}`); + res.status(404).json({ + code: 404, + status: 'error', + message: '接口不存在', + path: req.originalUrl + }); +}); + +app.listen(PORT, () => { + console.log(`🚀 调试服务器启动在端口 ${PORT}`); + console.log(`📍 测试地址: http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/insurance_backend/debug_frontend_token.js b/insurance_backend/debug_frontend_token.js new file mode 100644 index 0000000..5a40c46 --- /dev/null +++ b/insurance_backend/debug_frontend_token.js @@ -0,0 +1,77 @@ +const axios = require('axios'); + +// 模拟前端登录和API调用流程 +async function debugFrontendToken() { + console.log('=== 调试前端Token问题 ===\n'); + + try { + // 1. 模拟前端登录 + console.log('1. 模拟前端登录...'); + const loginResponse = await axios.post('http://localhost:3001/api/auth/login', { + username: 'admin', + password: '123456' + }); + + console.log('登录响应状态:', loginResponse.status); + console.log('登录响应数据:', JSON.stringify(loginResponse.data, null, 2)); + + if (!loginResponse.data || loginResponse.data.code !== 200) { + console.log('❌ 登录失败'); + return; + } + + const token = loginResponse.data.data.token; + console.log('✅ 获取到Token:', token.substring(0, 50) + '...'); + + // 2. 模拟前端API调用 - 使用前端的baseURL + console.log('\n2. 模拟前端API调用...'); + + // 前端使用的是 /api 作为baseURL,实际请求会被代理到 localhost:3000 + // 但我们直接测试 localhost:3001 的代理 + try { + const apiResponse = await axios.get('http://localhost:3001/api/data-warehouse/overview', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + console.log('✅ API调用成功!'); + console.log('状态码:', apiResponse.status); + console.log('响应数据:', JSON.stringify(apiResponse.data, null, 2)); + + } catch (apiError) { + console.log('❌ API调用失败:', apiError.response?.status, apiError.response?.statusText); + console.log('错误详情:', apiError.response?.data); + + // 3. 尝试直接调用后端API + console.log('\n3. 尝试直接调用后端API...'); + try { + const directResponse = await axios.get('http://localhost:3000/api/data-warehouse/overview', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + console.log('✅ 直接调用后端成功!'); + console.log('状态码:', directResponse.status); + console.log('响应数据:', JSON.stringify(directResponse.data, null, 2)); + + } catch (directError) { + console.log('❌ 直接调用后端也失败:', directError.response?.status, directError.response?.statusText); + console.log('错误详情:', directError.response?.data); + } + } + + // 4. 检查前端代理配置 + console.log('\n4. 检查前端代理配置...'); + console.log('前端应该配置代理将 /api/* 请求转发到 http://localhost:3000'); + console.log('请检查 vite.config.js 或类似的代理配置文件'); + + } catch (error) { + console.log('❌ 登录失败:', error.response?.data || error.message); + } +} + +debugFrontendToken(); \ No newline at end of file diff --git a/insurance_backend/docs/data-warehouse-api.yaml b/insurance_backend/docs/data-warehouse-api.yaml new file mode 100644 index 0000000..a2febda --- /dev/null +++ b/insurance_backend/docs/data-warehouse-api.yaml @@ -0,0 +1,376 @@ +openapi: 3.0.0 +info: + title: 保险管理系统 - 数据仓库API + description: 保险管理系统数据仓库模块的API接口文档 + version: 1.0.0 + contact: + name: 开发团队 + email: dev@example.com + +servers: + - url: http://localhost:3000/api + description: 本地开发环境 + +paths: + /data-warehouse/overview: + get: + tags: + - 数据仓库 + summary: 获取数据仓库概览 + description: 获取保险业务的总体概览数据,包括申请数、保单数、理赔数、保费收入等关键指标 + security: + - bearerAuth: [] + responses: + '200': + description: 成功获取概览数据 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + totalApplications: + type: integer + description: 总申请数 + example: 137 + totalPolicies: + type: integer + description: 总保单数 + example: 26 + totalClaims: + type: integer + description: 总理赔数 + example: 7 + totalPremium: + type: number + format: float + description: 总保费收入 + example: 125000.50 + totalClaimAmount: + type: number + format: float + description: 总理赔支出 + example: 35000.00 + activePolicies: + type: integer + description: 活跃保单数 + example: 20 + pendingApplications: + type: integer + description: 待处理申请数 + example: 15 + pendingClaims: + type: integer + description: 待处理理赔数 + example: 3 + profitLoss: + type: number + format: float + description: 盈亏情况(保费收入-理赔支出) + example: 90000.50 + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /data-warehouse/insurance-type-distribution: + get: + tags: + - 数据仓库 + summary: 获取保险类型分布 + description: 获取各保险类型的申请数量分布统计 + security: + - bearerAuth: [] + responses: + '200': + description: 成功获取保险类型分布数据 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: array + items: + type: object + properties: + type: + type: string + description: 保险类型名称 + example: "牛保险" + count: + type: integer + description: 申请数量 + example: 45 + percentage: + type: string + description: 占比百分比 + example: "32.85" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /data-warehouse/application-status-distribution: + get: + tags: + - 数据仓库 + summary: 获取申请状态分布 + description: 获取保险申请各状态的数量分布统计 + security: + - bearerAuth: [] + responses: + '200': + description: 成功获取申请状态分布数据 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: array + items: + type: object + properties: + status: + type: string + description: 申请状态 + enum: [pending, initial_approved, under_review, approved, rejected, paid] + example: "pending" + label: + type: string + description: 状态标签 + example: "待初审" + count: + type: integer + description: 数量 + example: 25 + percentage: + type: string + description: 占比百分比 + example: "18.25" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /data-warehouse/trend-data: + get: + tags: + - 数据仓库 + summary: 获取趋势数据 + description: 获取指定天数内的申请、保单、理赔趋势数据 + security: + - bearerAuth: [] + parameters: + - name: days + in: query + description: 查询天数,默认7天 + required: false + schema: + type: integer + default: 7 + minimum: 1 + maximum: 365 + example: 7 + responses: + '200': + description: 成功获取趋势数据 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + applications: + type: array + description: 申请趋势数据 + items: + type: object + properties: + date: + type: string + format: date + description: 日期 + example: "2024-01-15" + count: + type: integer + description: 当日数量 + example: 5 + policies: + type: array + description: 保单趋势数据 + items: + type: object + properties: + date: + type: string + format: date + description: 日期 + example: "2024-01-15" + count: + type: integer + description: 当日数量 + example: 3 + claims: + type: array + description: 理赔趋势数据 + items: + type: object + properties: + date: + type: string + format: date + description: 日期 + example: "2024-01-15" + count: + type: integer + description: 当日数量 + example: 1 + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /data-warehouse/claim-stats: + get: + tags: + - 数据仓库 + summary: 获取理赔统计 + description: 获取理赔状态分布和月度理赔趋势统计数据 + security: + - bearerAuth: [] + responses: + '200': + description: 成功获取理赔统计数据 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + statusDistribution: + type: array + description: 理赔状态分布 + items: + type: object + properties: + status: + type: string + description: 理赔状态 + enum: [pending, approved, rejected, processing, paid] + example: "pending" + label: + type: string + description: 状态标签 + example: "待审核" + count: + type: integer + description: 数量 + example: 5 + totalAmount: + type: number + format: float + description: 总金额 + example: 15000.00 + monthlyTrend: + type: array + description: 月度理赔趋势 + items: + type: object + properties: + month: + type: string + description: 月份(YYYY-MM格式) + example: "2024-01" + count: + type: integer + description: 理赔数量 + example: 8 + totalAmount: + type: number + format: float + description: 理赔总金额 + example: 25000.00 + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + responses: + Unauthorized: + description: 未授权访问 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: "未授权访问" + + InternalServerError: + description: 服务器内部错误 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: "服务器内部错误" + error: + type: string + example: "具体错误信息" + + schemas: + ErrorResponse: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + description: 错误消息 + error: + type: string + description: 详细错误信息 + +tags: + - name: 数据仓库 + description: 数据仓库相关接口,提供业务数据的统计分析功能 \ No newline at end of file diff --git a/insurance_backend/generate_test_data.js b/insurance_backend/generate_test_data.js new file mode 100644 index 0000000..7ce8a23 --- /dev/null +++ b/insurance_backend/generate_test_data.js @@ -0,0 +1,160 @@ +const mysql = require('mysql2/promise'); + +async function generateTestData() { + const connection = await mysql.createConnection({ + host: '129.211.213.226', + port: 9527, + user: 'root', + password: 'aiotAiot123!', + database: 'insurance_data' + }); + + try { + console.log('=== 生成测试数据 ==='); + + // 1. 插入保险类型数据 + console.log('插入保险类型数据...'); + await connection.execute(` + INSERT IGNORE INTO insurance_types (name, description, coverage_amount_min, coverage_amount_max, premium_rate, status) VALUES + ('生猪养殖保险', '针对生猪养殖的综合保险,覆盖疾病、意外死亡等风险', 1000.00, 500000.00, 0.0350, 'active'), + ('肉牛养殖保险', '针对肉牛养殖的保险产品,保障牲畜健康和意外损失', 2000.00, 800000.00, 0.0280, 'active'), + ('奶牛养殖保险', '专为奶牛养殖设计的保险,包含产奶量保障', 3000.00, 1000000.00, 0.0320, 'active'), + ('羊群养殖保险', '山羊、绵羊等小型反刍动物的养殖保险', 500.00, 200000.00, 0.0400, 'active'), + ('家禽养殖保险', '鸡、鸭、鹅等家禽的养殖风险保险', 300.00, 100000.00, 0.0450, 'active'), + ('水产养殖保险', '鱼类、虾类等水产品的养殖保险', 1000.00, 300000.00, 0.0380, 'active'), + ('综合农业保险', '涵盖多种农业生产活动的综合性保险产品', 5000.00, 2000000.00, 0.0250, 'active') + `); + + // 2. 插入保险申请数据 + console.log('插入保险申请数据...'); + const applications = []; + const statuses = ['pending', 'approved', 'rejected', 'under_review']; + const insuranceTypes = [1, 2, 3, 4, 5, 6, 7]; + const livestockTypes = ['生猪', '肉牛', '奶牛', '山羊', '绵羊', '鸡', '鸭', '鹅', '鱼', '虾']; + + for (let i = 1; i <= 150; i++) { + const applicationNo = `APP${new Date().getFullYear()}${String(i).padStart(6, '0')}`; + const customerName = `客户${i}`; + const customerIdCard = `${Math.floor(Math.random() * 900000) + 100000}${Math.floor(Math.random() * 90000000) + 10000000}`; + const customerPhone = `1${Math.floor(Math.random() * 9) + 3}${Math.floor(Math.random() * 900000000) + 100000000}`; + const customerAddress = `某省某市某区某街道${i}号`; + const insuranceTypeId = insuranceTypes[Math.floor(Math.random() * insuranceTypes.length)]; + const livestockType = livestockTypes[Math.floor(Math.random() * livestockTypes.length)]; + const livestockCount = Math.floor(Math.random() * 500) + 10; + const applicationAmount = (Math.random() * 400000 + 10000).toFixed(2); + const status = statuses[Math.floor(Math.random() * statuses.length)]; + const applicationDate = new Date(Date.now() - Math.random() * 90 * 24 * 60 * 60 * 1000); + const reviewerId = Math.random() > 0.5 ? Math.floor(Math.random() * 13) + 1 : null; + + applications.push([ + applicationNo, customerName, customerIdCard, customerPhone, customerAddress, + insuranceTypeId, '畜牧养殖', livestockType, livestockCount, applicationAmount, + status, applicationDate, '系统生成测试数据', reviewerId, + status !== 'pending' ? new Date(applicationDate.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000) : null + ]); + } + + for (const app of applications) { + await connection.execute(` + INSERT IGNORE INTO insurance_applications + (application_no, customer_name, customer_id_card, customer_phone, customer_address, + insurance_type_id, insurance_category, application_quantity, application_amount, + status, application_date, review_notes, reviewer_id, review_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + app[0], app[1], app[2], app[3], app[4], app[5], app[6], app[8], app[9], + app[10], app[11], app[12], app[13], app[14] + ]); + } + + // 3. 插入保单数据(基于已批准的申请) + console.log('插入保单数据...'); + const [approvedApps] = await connection.execute(` + SELECT id, application_no, customer_name, customer_id_card, customer_phone, + insurance_type_id, application_amount, application_date + FROM insurance_applications + WHERE status = 'approved' + `); + + for (const app of approvedApps) { + const policyNo = `POL${new Date().getFullYear()}${String(app.id).padStart(6, '0')}`; + const coverageAmount = app.application_amount; + const premiumAmount = (app.application_amount * 0.035).toFixed(2); + const startDate = new Date(app.application_date); + startDate.setDate(startDate.getDate() + Math.floor(Math.random() * 30) + 1); + const endDate = new Date(startDate); + endDate.setFullYear(endDate.getFullYear() + 1); + const policyStatus = Math.random() > 0.1 ? 'active' : 'expired'; + const paymentStatus = Math.random() > 0.2 ? 'paid' : 'unpaid'; + const paymentDate = paymentStatus === 'paid' ? startDate : null; + + await connection.execute(` + INSERT IGNORE INTO policies + (policy_no, application_id, insurance_type_id, customer_id, + coverage_amount, premium_amount, start_date, end_date, + policy_status, payment_status, payment_date, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + policyNo, app.id, app.insurance_type_id, 1, + coverageAmount, premiumAmount, startDate, endDate, + policyStatus, paymentStatus, paymentDate, 1 + ]); + } + + // 4. 插入理赔数据(基于有效保单) + console.log('插入理赔数据...'); + const [activePolicies] = await connection.execute(` + SELECT id, policy_no, customer_id, coverage_amount, start_date, end_date + FROM policies + WHERE policy_status = 'active' AND payment_status = 'paid' + `); + + const claimStatuses = ['pending', 'approved', 'rejected', 'processing', 'paid']; + const incidents = [ + '牲畜疾病导致死亡', '自然灾害造成损失', '意外事故导致伤亡', + '饲料中毒事件', '设备故障造成损失', '盗窃事件', '火灾事故' + ]; + + for (let i = 0; i < Math.min(activePolicies.length * 0.3, 80); i++) { + const policy = activePolicies[Math.floor(Math.random() * activePolicies.length)]; + const claimNo = `CLM${new Date().getFullYear()}${String(i + 1).padStart(6, '0')}`; + const claimAmount = (Math.random() * policy.coverage_amount * 0.8).toFixed(2); + const claimDate = new Date(policy.start_date.getTime() + Math.random() * (policy.end_date.getTime() - policy.start_date.getTime())); + const incidentDescription = incidents[Math.floor(Math.random() * incidents.length)]; + const claimStatus = claimStatuses[Math.floor(Math.random() * claimStatuses.length)]; + const reviewerId = Math.random() > 0.3 ? Math.floor(Math.random() * 13) + 1 : null; + const reviewDate = claimStatus !== 'pending' ? new Date(claimDate.getTime() + Math.random() * 14 * 24 * 60 * 60 * 1000) : null; + const paymentDate = claimStatus === 'paid' ? new Date(reviewDate.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000) : null; + + await connection.execute(` + INSERT IGNORE INTO claims + (claim_no, policy_id, customer_id, claim_amount, claim_date, + claim_status, review_notes, reviewer_id, review_date, payment_date, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + claimNo, policy.id, policy.customer_id, claimAmount, claimDate, + claimStatus, '系统生成测试数据', reviewerId, reviewDate, paymentDate, 1 + ]); + } + + console.log('\n=== 测试数据生成完成 ==='); + + // 显示统计信息 + const [typeCount] = await connection.execute('SELECT COUNT(*) as count FROM insurance_types'); + const [appCount] = await connection.execute('SELECT COUNT(*) as count FROM insurance_applications'); + const [policyCount] = await connection.execute('SELECT COUNT(*) as count FROM policies'); + const [claimCount] = await connection.execute('SELECT COUNT(*) as count FROM claims'); + + console.log(`保险类型: ${typeCount[0].count} 条`); + console.log(`保险申请: ${appCount[0].count} 条`); + console.log(`保单: ${policyCount[0].count} 条`); + console.log(`理赔: ${claimCount[0].count} 条`); + + } catch (error) { + console.error('生成测试数据错误:', error); + } finally { + await connection.end(); + } +} + +generateTestData(); \ No newline at end of file diff --git a/insurance_backend/middleware/auth.js b/insurance_backend/middleware/auth.js index 77fa410..5188f52 100644 --- a/insurance_backend/middleware/auth.js +++ b/insurance_backend/middleware/auth.js @@ -11,10 +11,22 @@ const jwtAuth = (req, res, next) => { try { const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // 检查Token类型,只接受访问令牌 + if (decoded.type && decoded.type !== 'access') { + return res.status(401).json(responseFormat.error('无效的令牌类型')); + } + req.user = decoded; next(); } catch (error) { - return res.status(401).json(responseFormat.error('认证令牌无效或已过期')); + if (error.name === 'TokenExpiredError') { + return res.status(401).json(responseFormat.error('认证令牌已过期', 'TOKEN_EXPIRED')); + } else if (error.name === 'JsonWebTokenError') { + return res.status(401).json(responseFormat.error('认证令牌无效', 'TOKEN_INVALID')); + } else { + return res.status(401).json(responseFormat.error('认证失败', 'AUTH_FAILED')); + } } }; diff --git a/insurance_backend/middleware/operationLogger.js b/insurance_backend/middleware/operationLogger.js new file mode 100644 index 0000000..c441974 --- /dev/null +++ b/insurance_backend/middleware/operationLogger.js @@ -0,0 +1,302 @@ +const { OperationLog } = require('../models'); + +/** + * 操作日志记录中间件 + * 自动记录用户的操作行为 + */ +class OperationLogger { + /** + * 记录操作日志的中间件 + */ + static logOperation(options = {}) { + return async (req, res, next) => { + const startTime = Date.now(); + + // 保存原始的res.json方法 + const originalJson = res.json; + + // 重写res.json方法以捕获响应数据 + res.json = function(data) { + const endTime = Date.now(); + const executionTime = endTime - startTime; + + // 异步记录操作日志,不阻塞响应 + setImmediate(async () => { + try { + await OperationLogger.recordLog(req, res, data, executionTime, options); + } catch (error) { + console.error('记录操作日志失败:', error); + } + }); + + // 调用原始的json方法 + return originalJson.call(this, data); + }; + + next(); + }; + } + + /** + * 记录操作日志 + */ + static async recordLog(req, res, responseData, executionTime, options) { + try { + // 如果用户未登录,不记录日志 + if (!req.user || !req.user.id) { + return; + } + + // 获取操作类型 + const operationType = OperationLogger.getOperationType(req.method, req.url, options); + + // 获取操作模块 + const operationModule = OperationLogger.getOperationModule(req.url, options); + + // 获取操作内容 + const operationContent = OperationLogger.getOperationContent(req, operationType, operationModule, options); + + // 获取操作目标 + const operationTarget = OperationLogger.getOperationTarget(req, responseData, options); + + // 获取操作状态 + const status = OperationLogger.getOperationStatus(res.statusCode, responseData); + + // 获取错误信息 + const errorMessage = OperationLogger.getErrorMessage(responseData, status); + + // 记录操作日志 + await OperationLog.logOperation({ + user_id: req.user.id, + operation_type: operationType, + operation_module: operationModule, + operation_content: operationContent, + operation_target: operationTarget, + request_method: req.method, + request_url: req.originalUrl || req.url, + request_params: { + query: req.query, + body: OperationLogger.sanitizeRequestBody(req.body), + params: req.params + }, + response_status: res.statusCode, + response_data: OperationLogger.sanitizeResponseData(responseData), + ip_address: OperationLogger.getClientIP(req), + user_agent: req.get('User-Agent') || '', + execution_time: executionTime, + status: status, + error_message: errorMessage + }); + } catch (error) { + console.error('记录操作日志时发生错误:', error); + } + } + + /** + * 获取操作类型 + */ + static getOperationType(method, url, options) { + if (options.operation_type) { + return options.operation_type; + } + + // 根据URL和HTTP方法推断操作类型 + if (url.includes('/login')) return 'login'; + if (url.includes('/logout')) return 'logout'; + if (url.includes('/export')) return 'export'; + if (url.includes('/import')) return 'import'; + if (url.includes('/approve')) return 'approve'; + if (url.includes('/reject')) return 'reject'; + + switch (method.toUpperCase()) { + case 'GET': + return 'view'; + case 'POST': + return 'create'; + case 'PUT': + case 'PATCH': + return 'update'; + case 'DELETE': + return 'delete'; + default: + return 'other'; + } + } + + /** + * 获取操作模块 + */ + static getOperationModule(url, options) { + if (options.operation_module) { + return options.operation_module; + } + + // 从URL中提取模块名 + const pathSegments = url.split('/').filter(segment => segment && segment !== 'api'); + if (pathSegments.length > 0) { + return pathSegments[0].replace(/-/g, '_'); + } + + return 'unknown'; + } + + /** + * 获取操作内容 + */ + static getOperationContent(req, operationType, operationModule, options) { + if (options.operation_content) { + return options.operation_content; + } + + const actionMap = { + 'login': '用户登录', + 'logout': '用户退出', + 'view': '查看', + 'create': '创建', + 'update': '更新', + 'delete': '删除', + 'export': '导出', + 'import': '导入', + 'approve': '审批通过', + 'reject': '审批拒绝' + }; + + const moduleMap = { + 'users': '用户', + 'roles': '角色', + 'insurance': '保险', + 'policies': '保单', + 'claims': '理赔', + 'system': '系统', + 'operation_logs': '操作日志', + 'devices': '设备', + 'device_alerts': '设备告警' + }; + + const action = actionMap[operationType] || operationType; + const module = moduleMap[operationModule] || operationModule; + + return `${action}${module}`; + } + + /** + * 获取操作目标 + */ + static getOperationTarget(req, responseData, options) { + if (options.operation_target) { + return options.operation_target; + } + + // 尝试从请求参数中获取ID + if (req.params.id) { + return `ID: ${req.params.id}`; + } + + // 尝试从响应数据中获取信息 + if (responseData && responseData.data) { + if (responseData.data.id) { + return `ID: ${responseData.data.id}`; + } + if (responseData.data.name) { + return `名称: ${responseData.data.name}`; + } + if (responseData.data.username) { + return `用户: ${responseData.data.username}`; + } + } + + return ''; + } + + /** + * 获取操作状态 + */ + static getOperationStatus(statusCode, responseData) { + if (statusCode >= 200 && statusCode < 300) { + return 'success'; + } else if (statusCode >= 400 && statusCode < 500) { + return 'failed'; + } else { + return 'error'; + } + } + + /** + * 获取错误信息 + */ + static getErrorMessage(responseData, status) { + if (status === 'success') { + return null; + } + + if (responseData && responseData.message) { + return responseData.message; + } + + if (responseData && responseData.error) { + return responseData.error; + } + + return null; + } + + /** + * 清理请求体数据(移除敏感信息) + */ + static sanitizeRequestBody(body) { + if (!body || typeof body !== 'object') { + return body; + } + + const sanitized = { ...body }; + const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth']; + + sensitiveFields.forEach(field => { + if (sanitized[field]) { + sanitized[field] = '***'; + } + }); + + return sanitized; + } + + /** + * 清理响应数据(移除敏感信息) + */ + static sanitizeResponseData(data) { + if (!data || typeof data !== 'object') { + return data; + } + + // 只保留基本的响应信息,不保存完整的响应数据 + return { + status: data.status, + message: data.message, + code: data.code + }; + } + + /** + * 获取客户端IP地址 + */ + static getClientIP(req) { + return req.ip || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.connection.socket ? req.connection.socket.remoteAddress : null) || + '127.0.0.1'; + } + + /** + * 创建特定操作的日志记录器 + */ + static createLogger(operationType, operationModule, operationContent) { + return OperationLogger.logOperation({ + operation_type: operationType, + operation_module: operationModule, + operation_content: operationContent + }); + } +} + +module.exports = OperationLogger; \ No newline at end of file diff --git a/insurance_backend/migrations/20241225000001-create-operation-logs.js b/insurance_backend/migrations/20241225000001-create-operation-logs.js new file mode 100644 index 0000000..ad8a148 --- /dev/null +++ b/insurance_backend/migrations/20241225000001-create-operation-logs.js @@ -0,0 +1,146 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('operation_logs', { + id: { + type: Sequelize.INTEGER, + autoIncrement: true, + primaryKey: true + }, + user_id: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + operation_type: { + type: Sequelize.ENUM( + 'login', // 登录 + 'logout', // 登出 + 'create', // 创建 + 'update', // 更新 + 'delete', // 删除 + 'view', // 查看 + 'export', // 导出 + 'import', // 导入 + 'approve', // 审批 + 'reject', // 拒绝 + 'system_config', // 系统配置 + 'user_manage', // 用户管理 + 'role_manage', // 角色管理 + 'other' // 其他 + ), + allowNull: false, + comment: '操作类型' + }, + operation_module: { + type: Sequelize.STRING(50), + allowNull: false, + comment: '操作模块(如:用户管理、设备管理、预警管理等)' + }, + operation_content: { + type: Sequelize.TEXT, + allowNull: false, + comment: '操作内容描述' + }, + operation_target: { + type: Sequelize.STRING(100), + allowNull: true, + comment: '操作目标(如:用户ID、设备ID等)' + }, + request_method: { + type: Sequelize.ENUM('GET', 'POST', 'PUT', 'DELETE', 'PATCH'), + allowNull: true, + comment: 'HTTP请求方法' + }, + request_url: { + type: Sequelize.STRING(500), + allowNull: true, + comment: '请求URL' + }, + request_params: { + type: Sequelize.TEXT, + allowNull: true, + comment: '请求参数(JSON格式)' + }, + response_status: { + type: Sequelize.INTEGER, + allowNull: true, + comment: '响应状态码' + }, + ip_address: { + type: Sequelize.STRING(45), + allowNull: true, + comment: 'IP地址(支持IPv6)' + }, + user_agent: { + type: Sequelize.TEXT, + allowNull: true, + comment: '用户代理信息' + }, + execution_time: { + type: Sequelize.INTEGER, + allowNull: true, + comment: '执行时间(毫秒)' + }, + status: { + type: Sequelize.ENUM('success', 'failed', 'error'), + defaultValue: 'success', + comment: '操作状态' + }, + error_message: { + type: Sequelize.TEXT, + allowNull: true, + comment: '错误信息' + }, + created_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') + }, + updated_at: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP') + } + }, { + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + comment: '系统操作日志表' + }); + + // 创建索引 + await queryInterface.addIndex('operation_logs', ['user_id'], { + name: 'idx_operation_logs_user_id' + }); + + await queryInterface.addIndex('operation_logs', ['operation_type'], { + name: 'idx_operation_logs_operation_type' + }); + + await queryInterface.addIndex('operation_logs', ['operation_module'], { + name: 'idx_operation_logs_operation_module' + }); + + await queryInterface.addIndex('operation_logs', ['created_at'], { + name: 'idx_operation_logs_created_at' + }); + + await queryInterface.addIndex('operation_logs', ['status'], { + name: 'idx_operation_logs_status' + }); + + await queryInterface.addIndex('operation_logs', ['ip_address'], { + name: 'idx_operation_logs_ip_address' + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('operation_logs'); + } +}; \ No newline at end of file diff --git a/insurance_backend/migrations/20250122000003-create-devices-and-alerts.js b/insurance_backend/migrations/20250122000003-create-devices-and-alerts.js new file mode 100644 index 0000000..47007e3 --- /dev/null +++ b/insurance_backend/migrations/20250122000003-create-devices-and-alerts.js @@ -0,0 +1,190 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + // 创建设备表 + await queryInterface.createTable('devices', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '设备ID' + }, + device_code: { + type: Sequelize.STRING(50), + allowNull: false, + unique: true, + comment: '设备编号' + }, + device_name: { + type: Sequelize.STRING(100), + allowNull: false, + comment: '设备名称' + }, + device_type: { + type: Sequelize.STRING(50), + allowNull: false, + comment: '设备类型' + }, + device_model: { + type: Sequelize.STRING(100), + comment: '设备型号' + }, + manufacturer: { + type: Sequelize.STRING(100), + comment: '制造商' + }, + installation_location: { + type: Sequelize.STRING(200), + comment: '安装位置' + }, + installation_date: { + type: Sequelize.DATE, + comment: '安装日期' + }, + status: { + type: Sequelize.ENUM('normal', 'warning', 'error', 'offline'), + defaultValue: 'normal', + comment: '设备状态' + }, + farm_id: { + type: Sequelize.INTEGER, + comment: '所属养殖场ID' + }, + pen_id: { + type: Sequelize.INTEGER, + comment: '所属栏舍ID' + }, + created_by: { + type: Sequelize.INTEGER, + comment: '创建人ID' + }, + updated_by: { + type: Sequelize.INTEGER, + comment: '更新人ID' + }, + created_at: { + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + comment: '创建时间' + }, + updated_at: { + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + comment: '更新时间' + } + }, { + comment: '设备信息表' + }); + + // 创建设备预警表 + await queryInterface.createTable('device_alerts', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '预警ID' + }, + device_id: { + type: Sequelize.INTEGER, + allowNull: false, + comment: '设备ID', + references: { + model: 'devices', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE' + }, + alert_type: { + type: Sequelize.STRING(50), + allowNull: false, + comment: '预警类型' + }, + alert_level: { + type: Sequelize.ENUM('low', 'medium', 'high', 'critical'), + allowNull: false, + comment: '预警级别' + }, + alert_title: { + type: Sequelize.STRING(200), + allowNull: false, + comment: '预警标题' + }, + alert_content: { + type: Sequelize.TEXT, + comment: '预警内容' + }, + alert_time: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW, + comment: '预警时间' + }, + status: { + type: Sequelize.ENUM('pending', 'processing', 'resolved', 'ignored'), + defaultValue: 'pending', + comment: '处理状态' + }, + handler_id: { + type: Sequelize.INTEGER, + comment: '处理人ID', + references: { + model: 'users', + key: 'id' + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL' + }, + handle_time: { + type: Sequelize.DATE, + comment: '处理时间' + }, + handle_remark: { + type: Sequelize.TEXT, + comment: '处理备注' + }, + farm_id: { + type: Sequelize.INTEGER, + comment: '所属养殖场ID' + }, + pen_id: { + type: Sequelize.INTEGER, + comment: '所属栏舍ID' + }, + is_read: { + type: Sequelize.BOOLEAN, + defaultValue: false, + comment: '是否已读' + }, + read_time: { + type: Sequelize.DATE, + comment: '阅读时间' + }, + created_at: { + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + comment: '创建时间' + }, + updated_at: { + type: Sequelize.DATE, + defaultValue: Sequelize.NOW, + comment: '更新时间' + } + }, { + comment: '设备预警表' + }); + + // 添加索引 + await queryInterface.addIndex('device_alerts', ['device_id']); + await queryInterface.addIndex('device_alerts', ['alert_level']); + await queryInterface.addIndex('device_alerts', ['status']); + await queryInterface.addIndex('device_alerts', ['alert_time']); + await queryInterface.addIndex('device_alerts', ['farm_id']); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('device_alerts'); + await queryInterface.dropTable('devices'); + } +}; \ No newline at end of file diff --git a/insurance_backend/models/Device.js b/insurance_backend/models/Device.js new file mode 100644 index 0000000..1a09239 --- /dev/null +++ b/insurance_backend/models/Device.js @@ -0,0 +1,86 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +/** + * 设备模型 + * 用于管理保险相关的设备信息 + */ +const Device = sequelize.define('Device', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '设备ID' + }, + device_number: { + type: DataTypes.STRING(50), + allowNull: false, + unique: true, + comment: '设备编号' + }, + device_name: { + type: DataTypes.STRING(100), + allowNull: false, + comment: '设备名称' + }, + device_type: { + type: DataTypes.STRING(50), + allowNull: false, + comment: '设备类型' + }, + device_model: { + type: DataTypes.STRING(100), + comment: '设备型号' + }, + manufacturer: { + type: DataTypes.STRING(100), + comment: '制造商' + }, + installation_location: { + type: DataTypes.STRING(200), + comment: '安装位置' + }, + installation_date: { + type: DataTypes.DATE, + comment: '安装日期' + }, + status: { + type: DataTypes.ENUM('normal', 'warning', 'error', 'offline'), + defaultValue: 'normal', + comment: '设备状态:normal-正常,warning-警告,error-故障,offline-离线' + }, + farm_id: { + type: DataTypes.INTEGER, + comment: '所属养殖场ID' + }, + barn_id: { + type: DataTypes.INTEGER, + comment: '所属栏舍ID' + }, + created_by: { + type: DataTypes.INTEGER, + comment: '创建人ID' + }, + updated_by: { + type: DataTypes.INTEGER, + comment: '更新人ID' + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + comment: '创建时间' + }, + updated_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + comment: '更新时间' + } +}, { + tableName: 'devices', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + comment: '设备信息表' +}); + +module.exports = Device; \ No newline at end of file diff --git a/insurance_backend/models/DeviceAlert.js b/insurance_backend/models/DeviceAlert.js new file mode 100644 index 0000000..24389c5 --- /dev/null +++ b/insurance_backend/models/DeviceAlert.js @@ -0,0 +1,114 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +/** + * 设备预警模型 + * 用于管理设备预警信息 + */ +const DeviceAlert = sequelize.define('DeviceAlert', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + comment: '预警ID' + }, + device_id: { + type: DataTypes.INTEGER, + allowNull: false, + comment: '设备ID' + }, + alert_type: { + type: DataTypes.STRING(50), + allowNull: false, + comment: '预警类型' + }, + alert_level: { + type: DataTypes.ENUM('info', 'warning', 'critical'), + allowNull: false, + comment: '预警级别:info-信息,warning-警告,critical-严重' + }, + alert_title: { + type: DataTypes.STRING(200), + allowNull: false, + comment: '预警标题' + }, + alert_content: { + type: DataTypes.TEXT, + comment: '预警内容' + }, + alert_time: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + comment: '预警时间' + }, + status: { + type: DataTypes.ENUM('pending', 'processing', 'resolved', 'ignored'), + defaultValue: 'pending', + comment: '处理状态:pending-待处理,processing-处理中,resolved-已解决,ignored-已忽略' + }, + handler_id: { + type: DataTypes.INTEGER, + comment: '处理人ID' + }, + handle_time: { + type: DataTypes.DATE, + comment: '处理时间' + }, + handle_note: { + type: DataTypes.TEXT, + comment: '处理备注' + }, + farm_id: { + type: DataTypes.INTEGER, + comment: '所属养殖场ID' + }, + barn_id: { + type: DataTypes.INTEGER, + comment: '所属栏舍ID' + }, + is_read: { + type: DataTypes.BOOLEAN, + defaultValue: false, + comment: '是否已读' + }, + read_time: { + type: DataTypes.DATE, + comment: '阅读时间' + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + comment: '创建时间' + }, + updated_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + comment: '更新时间' + } +}, { + tableName: 'device_alerts', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + comment: '设备预警表', + indexes: [ + { + fields: ['device_id'] + }, + { + fields: ['alert_level'] + }, + { + fields: ['status'] + }, + { + fields: ['alert_time'] + }, + { + fields: ['farm_id'] + } + ] +}); + +module.exports = DeviceAlert; \ No newline at end of file diff --git a/insurance_backend/models/OperationLog.js b/insurance_backend/models/OperationLog.js new file mode 100644 index 0000000..1ea5b63 --- /dev/null +++ b/insurance_backend/models/OperationLog.js @@ -0,0 +1,270 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +const OperationLog = sequelize.define('OperationLog', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'users', + key: 'id' + }, + comment: '操作用户ID' + }, + operation_type: { + type: DataTypes.ENUM( + 'login', // 登录 + 'logout', // 登出 + 'create', // 创建 + 'update', // 更新 + 'delete', // 删除 + 'view', // 查看 + 'export', // 导出 + 'import', // 导入 + 'approve', // 审批 + 'reject', // 拒绝 + 'system_config', // 系统配置 + 'user_manage', // 用户管理 + 'role_manage', // 角色管理 + 'other' // 其他 + ), + allowNull: false, + comment: '操作类型' + }, + operation_module: { + type: DataTypes.STRING(50), + allowNull: false, + comment: '操作模块(如:用户管理、设备管理、预警管理等)' + }, + operation_content: { + type: DataTypes.TEXT, + allowNull: false, + comment: '操作内容描述' + }, + operation_target: { + type: DataTypes.STRING(100), + allowNull: true, + comment: '操作目标(如:用户ID、设备ID等)' + }, + request_method: { + type: DataTypes.ENUM('GET', 'POST', 'PUT', 'DELETE', 'PATCH'), + allowNull: true, + comment: 'HTTP请求方法' + }, + request_url: { + type: DataTypes.STRING(500), + allowNull: true, + comment: '请求URL' + }, + request_params: { + type: DataTypes.TEXT, + allowNull: true, + comment: '请求参数(JSON格式)', + get() { + const value = this.getDataValue('request_params'); + return value ? JSON.parse(value) : null; + }, + set(value) { + this.setDataValue('request_params', value ? JSON.stringify(value) : null); + } + }, + response_status: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '响应状态码' + }, + ip_address: { + type: DataTypes.STRING(45), + allowNull: true, + comment: 'IP地址(支持IPv6)' + }, + user_agent: { + type: DataTypes.TEXT, + allowNull: true, + comment: '用户代理信息' + }, + execution_time: { + type: DataTypes.INTEGER, + allowNull: true, + comment: '执行时间(毫秒)' + }, + status: { + type: DataTypes.ENUM('success', 'failed', 'error'), + defaultValue: 'success', + comment: '操作状态' + }, + error_message: { + type: DataTypes.TEXT, + allowNull: true, + comment: '错误信息' + } +}, { + tableName: 'operation_logs', + timestamps: true, + underscored: true, + indexes: [ + { fields: ['user_id'] }, + { fields: ['operation_type'] }, + { fields: ['operation_module'] }, + { fields: ['created_at'] }, + { fields: ['status'] }, + { fields: ['ip_address'] } + ] +}); + +// 定义关联关系 +OperationLog.associate = function(models) { + // 操作日志属于用户 + OperationLog.belongsTo(models.User, { + foreignKey: 'user_id', + as: 'user', + onDelete: 'CASCADE', + onUpdate: 'CASCADE' + }); +}; + +// 静态方法:记录操作日志 +OperationLog.logOperation = async function(logData) { + try { + const log = await this.create({ + user_id: logData.userId, + operation_type: logData.operationType, + operation_module: logData.operationModule, + operation_content: logData.operationContent, + operation_target: logData.operationTarget, + request_method: logData.requestMethod, + request_url: logData.requestUrl, + request_params: logData.requestParams, + response_status: logData.responseStatus, + ip_address: logData.ipAddress, + user_agent: logData.userAgent, + execution_time: logData.executionTime, + status: logData.status || 'success', + error_message: logData.errorMessage + }); + return log; + } catch (error) { + console.error('记录操作日志失败:', error); + throw error; + } +}; + +// 静态方法:获取操作日志列表 +OperationLog.getLogsList = async function(options = {}) { + const { + page = 1, + limit = 20, + userId, + operationType, + operationModule, + status, + startDate, + endDate, + keyword + } = options; + + const where = {}; + + // 构建查询条件 + if (userId) where.user_id = userId; + if (operationType) where.operation_type = operationType; + if (operationModule) where.operation_module = operationModule; + if (status) where.status = status; + + // 时间范围查询 + if (startDate || endDate) { + where.created_at = {}; + if (startDate) where.created_at[sequelize.Op.gte] = new Date(startDate); + if (endDate) where.created_at[sequelize.Op.lte] = new Date(endDate); + } + + // 关键词搜索 + if (keyword) { + where[sequelize.Op.or] = [ + { operation_content: { [sequelize.Op.like]: `%${keyword}%` } }, + { operation_target: { [sequelize.Op.like]: `%${keyword}%` } } + ]; + } + + const offset = (page - 1) * limit; + + const result = await this.findAndCountAll({ + where, + include: [{ + model: sequelize.models.User, + as: 'user', + attributes: ['id', 'username', 'real_name'] + }], + order: [['created_at', 'DESC']], + limit: parseInt(limit), + offset: parseInt(offset) + }); + + return { + logs: result.rows, + total: result.count, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(result.count / limit) + }; +}; + +// 静态方法:获取操作统计 +OperationLog.getOperationStats = async function(options = {}) { + const { startDate, endDate, userId } = options; + + const where = {}; + if (userId) where.user_id = userId; + + if (startDate || endDate) { + where.created_at = {}; + if (startDate) where.created_at[sequelize.Op.gte] = new Date(startDate); + if (endDate) where.created_at[sequelize.Op.lte] = new Date(endDate); + } + + // 按操作类型统计 + const typeStats = await this.findAll({ + where, + attributes: [ + 'operation_type', + [sequelize.fn('COUNT', sequelize.col('id')), 'count'] + ], + group: ['operation_type'], + raw: true + }); + + // 按操作模块统计 + const moduleStats = await this.findAll({ + where, + attributes: [ + 'operation_module', + [sequelize.fn('COUNT', sequelize.col('id')), 'count'] + ], + group: ['operation_module'], + raw: true + }); + + // 按状态统计 + const statusStats = await this.findAll({ + where, + attributes: [ + 'status', + [sequelize.fn('COUNT', sequelize.col('id')), 'count'] + ], + group: ['status'], + raw: true + }); + + return { + typeStats, + moduleStats, + statusStats + }; +}; + +module.exports = OperationLog; \ No newline at end of file diff --git a/insurance_backend/models/index.js b/insurance_backend/models/index.js index 741d61b..da2e43b 100644 --- a/insurance_backend/models/index.js +++ b/insurance_backend/models/index.js @@ -12,6 +12,9 @@ const InstallationTask = require('./InstallationTask'); const LivestockType = require('./LivestockType'); const LivestockPolicy = require('./LivestockPolicy'); const LivestockClaim = require('./LivestockClaim'); +const Device = require('./Device'); +const DeviceAlert = require('./DeviceAlert'); +const OperationLog = require('./OperationLog'); // 定义模型关联关系 @@ -150,6 +153,46 @@ LivestockClaim.belongsTo(User, { as: 'reviewer' }); +// 设备和用户关联 +Device.belongsTo(User, { + foreignKey: 'created_by', + as: 'creator' +}); +Device.belongsTo(User, { + foreignKey: 'updated_by', + as: 'updater' +}); + +// 设备预警和设备关联 +DeviceAlert.belongsTo(Device, { + foreignKey: 'device_id', + as: 'device' +}); +Device.hasMany(DeviceAlert, { + foreignKey: 'device_id', + as: 'alerts' +}); + +// 设备预警和用户关联 +DeviceAlert.belongsTo(User, { + foreignKey: 'handler_id', + as: 'handler' +}); +User.hasMany(DeviceAlert, { + foreignKey: 'handler_id', + as: 'handled_alerts' +}); + +// 操作日志和用户关联 +OperationLog.belongsTo(User, { + foreignKey: 'user_id', + as: 'user' +}); +User.hasMany(OperationLog, { + foreignKey: 'user_id', + as: 'operation_logs' +}); + // 导出所有模型 module.exports = { sequelize, @@ -164,5 +207,8 @@ module.exports = { InstallationTask, LivestockType, LivestockPolicy, - LivestockClaim + LivestockClaim, + Device, + DeviceAlert, + OperationLog }; \ No newline at end of file diff --git a/insurance_backend/package-lock.json b/insurance_backend/package-lock.json index 6e90469..cd1783b 100644 --- a/insurance_backend/package-lock.json +++ b/insurance_backend/package-lock.json @@ -13,6 +13,7 @@ "bcrypt": "^5.1.0", "cors": "^2.8.5", "dotenv": "^16.0.3", + "exceljs": "^4.4.0", "express": "^4.18.2", "express-rate-limit": "^8.1.0", "helmet": "^8.1.0", @@ -162,6 +163,47 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmmirror.com/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2956,6 +2998,81 @@ "resolved": "https://registry.npmmirror.com/aproba/-/aproba-2.1.0.tgz", "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==" }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", @@ -8004,6 +8121,32 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/bcrypt/-/bcrypt-5.1.1.tgz", @@ -8017,6 +8160,28 @@ "node": ">= 10.0.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -8029,6 +8194,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", @@ -8085,11 +8267,6 @@ "concat-map": "0.0.1" } }, - "node_modules/brace-expansion/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, "node_modules/brace-expansion/node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", @@ -8172,6 +8349,39 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -8182,6 +8392,23 @@ "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -8267,6 +8494,18 @@ "node": ">=6" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -8485,6 +8724,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", @@ -8499,11 +8753,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/concat-stream/node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, "node_modules/concat-stream/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", @@ -8606,6 +8855,12 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz", @@ -8618,6 +8873,31 @@ "node": ">= 0.10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8653,6 +8933,12 @@ "node": ">= 8" } }, + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", @@ -8761,6 +9047,51 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8803,6 +9134,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", @@ -9332,6 +9672,26 @@ "node": ">= 0.6" } }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", @@ -9407,6 +9767,19 @@ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -9594,6 +9967,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", @@ -9628,6 +10007,35 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", @@ -9825,8 +10233,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -9907,6 +10314,26 @@ "node": ">= 0.8" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", @@ -9922,6 +10349,12 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", @@ -12165,12 +12598,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/js-beautify/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, "node_modules/js-beautify/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -12491,6 +12918,54 @@ "semver": "bin/semver" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.2.tgz", @@ -12532,6 +13007,54 @@ "resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", @@ -12575,6 +13098,21 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -12595,12 +13133,42 @@ "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead." }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -12617,11 +13185,23 @@ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmmirror.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", @@ -12637,6 +13217,12 @@ "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -12653,6 +13239,18 @@ "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmmirror.com/logform/-/logform-2.7.0.tgz", @@ -13010,7 +13608,6 @@ "version": "3.0.0", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -13057,27 +13654,6 @@ "resolved": "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, - "node_modules/npmlog/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/npmlog/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", @@ -15819,6 +16395,12 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", @@ -16139,6 +16721,50 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", @@ -16482,6 +17108,18 @@ "node": ">=0.10.0" } }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", @@ -16719,6 +17357,12 @@ "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -16823,6 +17467,15 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/string-length/-/string-length-4.0.2.tgz", @@ -17221,6 +17874,22 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-6.0.0.tgz", @@ -17246,6 +17915,15 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -17280,6 +17958,15 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz", @@ -17388,6 +18075,60 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmmirror.com/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -17506,48 +18247,6 @@ "node": ">= 12.0.0" } }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston-transport/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/winston/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/wkx": { "version": "0.5.0", "resolved": "https://registry.npmmirror.com/wkx/-/wkx-0.5.0.tgz", @@ -17605,6 +18304,12 @@ "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", @@ -17665,6 +18370,41 @@ "engines": { "node": "^12.20.0 || >=14" } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } } }, "dependencies": { @@ -17755,6 +18495,47 @@ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, + "@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmmirror.com/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "requires": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + }, + "dependencies": { + "@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + } + } + }, + "@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "requires": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + }, + "dependencies": { + "@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + } + } + }, "@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -19932,6 +20713,71 @@ "resolved": "https://registry.npmmirror.com/aproba/-/aproba-2.1.0.tgz", "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==" }, + "archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", @@ -23355,6 +24201,16 @@ "proxy-from-env": "^1.1.0" } }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/bcrypt/-/bcrypt-5.1.1.tgz", @@ -23364,12 +24220,41 @@ "node-addon-api": "^5.0.0" } }, + "big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==" + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, "binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, "body-parser": { "version": "1.20.3", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz", @@ -23421,11 +24306,6 @@ "concat-map": "0.0.1" }, "dependencies": { - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", @@ -23475,6 +24355,20 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -23485,6 +24379,16 @@ "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==" + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==" + }, "builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -23548,6 +24452,14 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -23718,6 +24630,17 @@ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", "dev": true }, + "compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, "concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", @@ -23729,11 +24652,6 @@ "typedarray": "^0.0.6" }, "dependencies": { - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", @@ -23831,6 +24749,11 @@ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "cors": { "version": "2.8.5", "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz", @@ -23840,6 +24763,20 @@ "vary": "^1" } }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, + "crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + } + }, "cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -23868,6 +24805,11 @@ } } }, + "dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==" + }, "debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", @@ -23940,6 +24882,48 @@ "gopd": "^1.2.0" } }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "requires": { + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -23973,6 +24957,14 @@ "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "requires": { + "once": "^1.4.0" + } + }, "error-ex": { "version": "1.3.4", "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", @@ -24345,6 +25337,22 @@ "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "requires": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + } + }, "express": { "version": "4.21.2", "resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz", @@ -24406,6 +25414,15 @@ "ip-address": "10.0.1" } }, + "fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "requires": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -24556,6 +25573,11 @@ "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", @@ -24580,6 +25602,27 @@ "dev": true, "optional": true }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", @@ -24719,8 +25762,7 @@ "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "graphemer": { "version": "1.4.0", @@ -24777,6 +25819,11 @@ "toidentifier": "1.0.1" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "ignore": { "version": "5.3.2", "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", @@ -24789,6 +25836,11 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", @@ -26452,12 +27504,6 @@ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, "brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -26694,6 +27740,51 @@ } } }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "jwa": { "version": "1.4.2", "resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.2.tgz", @@ -26734,6 +27825,48 @@ "resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", @@ -26767,6 +27900,19 @@ } } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -26781,11 +27927,36 @@ "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, + "lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmmirror.com/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, + "lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -26801,11 +27972,21 @@ "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" }, + "lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmmirror.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" }, + "lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" + }, "lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", @@ -26821,6 +28002,11 @@ "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, + "lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -26837,6 +28023,16 @@ "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, "logform": { "version": "2.7.0", "resolved": "https://registry.npmmirror.com/logform/-/logform-2.7.0.tgz", @@ -27112,8 +28308,7 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "npm-run-path": { "version": "4.0.1", @@ -27148,24 +28343,6 @@ "version": "1.0.0", "resolved": "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } } } }, @@ -29036,6 +30213,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", @@ -29263,6 +30445,42 @@ } } }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", @@ -29466,6 +30684,14 @@ } } }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "requires": { + "xmlchars": "^2.2.0" + } + }, "semver": { "version": "7.7.2", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", @@ -29628,6 +30854,11 @@ "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -29709,6 +30940,14 @@ "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/string-length/-/string-length-4.0.2.tgz", @@ -30001,6 +31240,18 @@ "swagger-ui-dist": ">=5.0.0" } }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-6.0.0.tgz", @@ -30023,6 +31274,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -30048,6 +31304,11 @@ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "dev": true }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" + }, "triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz", @@ -30136,6 +31397,57 @@ "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmmirror.com/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -30203,26 +31515,6 @@ "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - } } }, "winston-transport": { @@ -30233,26 +31525,6 @@ "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - } } }, "wkx": { @@ -30296,6 +31568,11 @@ "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", @@ -30335,6 +31612,35 @@ "optional": true } } + }, + "zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "requires": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "requires": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + } + } } } } diff --git a/insurance_backend/package.json b/insurance_backend/package.json index 46c5583..f5e020a 100644 --- a/insurance_backend/package.json +++ b/insurance_backend/package.json @@ -25,6 +25,7 @@ "bcrypt": "^5.1.0", "cors": "^2.8.5", "dotenv": "^16.0.3", + "exceljs": "^4.4.0", "express": "^4.18.2", "express-rate-limit": "^8.1.0", "helmet": "^8.1.0", diff --git a/insurance_backend/routes/auth.js b/insurance_backend/routes/auth.js index 53c2e18..8484530 100644 --- a/insurance_backend/routes/auth.js +++ b/insurance_backend/routes/auth.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const authController = require('../controllers/authController'); const { jwtAuth } = require('../middleware/auth'); +const OperationLogger = require('../middleware/operationLogger'); /** * @swagger @@ -92,7 +93,7 @@ router.post('/register', authController.register); * 401: * $ref: '#/components/responses/UnauthorizedError' */ -router.post('/login', authController.login); +router.post('/login', OperationLogger.createLogger('login', 'auth', '用户登录'), authController.login); /** * @swagger @@ -168,7 +169,7 @@ router.post('/refresh', authController.refreshToken); * 401: * $ref: '#/components/responses/UnauthorizedError' */ -router.post('/logout', jwtAuth, authController.logout); +router.post('/logout', jwtAuth, OperationLogger.createLogger('logout', 'auth', '用户退出'), authController.logout); /** * @swagger diff --git a/insurance_backend/routes/deviceAlerts.js b/insurance_backend/routes/deviceAlerts.js new file mode 100644 index 0000000..0f346bf --- /dev/null +++ b/insurance_backend/routes/deviceAlerts.js @@ -0,0 +1,485 @@ +const express = require('express'); +const router = express.Router(); +const deviceAlertController = require('../controllers/deviceAlertController'); +const { jwtAuth } = require('../middleware/auth'); + +/** + * @swagger + * /api/device-alerts/stats: + * get: + * tags: + * - 设备预警 + * summary: 获取预警统计信息 + * description: 获取设备预警的统计数据,包括总数、按级别分类、按状态分类等 + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: farm_id + * schema: + * type: integer + * description: 养殖场ID + * - in: query + * name: start_date + * schema: + * type: string + * format: date + * description: 开始日期 + * - in: query + * name: end_date + * schema: + * type: string + * format: date + * description: 结束日期 + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * total_alerts: + * type: integer + * description: 总预警数 + * unread_alerts: + * type: integer + * description: 未读预警数 + * today_alerts: + * type: integer + * description: 今日新增预警数 + * alerts_by_level: + * type: array + * items: + * type: object + * properties: + * alert_level: + * type: string + * count: + * type: integer + * alerts_by_status: + * type: array + * items: + * type: object + * properties: + * status: + * type: string + * count: + * type: integer + * alerts_by_type: + * type: array + * items: + * type: object + * properties: + * alert_type: + * type: string + * count: + * type: integer + */ +router.get('/stats', jwtAuth, deviceAlertController.getAlertStats); + +/** + * @swagger + * /api/device-alerts: + * get: + * tags: + * - 设备预警 + * summary: 获取预警列表 + * description: 分页获取设备预警列表,支持多种筛选条件 + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 20 + * description: 每页数量 + * - in: query + * name: alert_level + * schema: + * type: string + * enum: [low, medium, high, critical] + * description: 预警级别 + * - in: query + * name: status + * schema: + * type: string + * enum: [pending, processing, resolved, ignored] + * description: 处理状态 + * - in: query + * name: alert_type + * schema: + * type: string + * description: 预警类型 + * - in: query + * name: farm_id + * schema: + * type: integer + * description: 养殖场ID + * - in: query + * name: start_date + * schema: + * type: string + * format: date + * description: 开始日期 + * - in: query + * name: end_date + * schema: + * type: string + * format: date + * description: 结束日期 + * - in: query + * name: is_read + * schema: + * type: boolean + * description: 是否已读 + * - in: query + * name: device_code + * schema: + * type: string + * description: 设备编号 + * - in: query + * name: keyword + * schema: + * type: string + * description: 关键词搜索(标题或内容) + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * alerts: + * type: array + * items: + * $ref: '#/components/schemas/DeviceAlert' + * pagination: + * $ref: '#/components/schemas/Pagination' + */ +router.get('/', jwtAuth, deviceAlertController.getAlertList); + +/** + * @swagger + * /api/device-alerts/{id}: + * get: + * tags: + * - 设备预警 + * summary: 获取预警详情 + * description: 根据ID获取设备预警的详细信息 + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 预警ID + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * $ref: '#/components/schemas/DeviceAlert' + * 404: + * description: 预警信息不存在 + */ +router.get('/:id', jwtAuth, deviceAlertController.getAlertDetail); + +/** + * @swagger + * /api/device-alerts/{id}/read: + * put: + * tags: + * - 设备预警 + * summary: 标记预警为已读 + * description: 将指定的预警标记为已读状态 + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 预警ID + * responses: + * 200: + * description: 标记成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 404: + * description: 预警信息不存在 + */ +router.put('/:id/read', jwtAuth, deviceAlertController.markAsRead); + +/** + * @swagger + * /api/device-alerts/batch/read: + * put: + * tags: + * - 设备预警 + * summary: 批量标记预警为已读 + * description: 批量将多个预警标记为已读状态 + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - alert_ids + * properties: + * alert_ids: + * type: array + * items: + * type: integer + * description: 预警ID列表 + * responses: + * 200: + * description: 批量标记成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + */ +router.put('/batch/read', jwtAuth, deviceAlertController.batchMarkAsRead); + +/** + * @swagger + * /api/device-alerts/{id}/handle: + * put: + * tags: + * - 设备预警 + * summary: 处理预警 + * description: 处理指定的设备预警,更新处理状态和备注 + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 预警ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - status + * properties: + * status: + * type: string + * enum: [processing, resolved, ignored] + * description: 处理状态 + * handle_remark: + * type: string + * description: 处理备注 + * responses: + * 200: + * description: 处理成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 404: + * description: 预警信息不存在 + */ +router.put('/:id/handle', jwtAuth, deviceAlertController.handleAlert); + +/** + * @swagger + * /api/device-alerts: + * post: + * tags: + * - 设备预警 + * summary: 创建预警 + * description: 创建新的设备预警(系统内部使用) + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - device_id + * - alert_type + * - alert_level + * - alert_title + * - alert_content + * properties: + * device_id: + * type: integer + * description: 设备ID + * alert_type: + * type: string + * description: 预警类型 + * alert_level: + * type: string + * enum: [low, medium, high, critical] + * description: 预警级别 + * alert_title: + * type: string + * description: 预警标题 + * alert_content: + * type: string + * description: 预警内容 + * farm_id: + * type: integer + * description: 养殖场ID + * pen_id: + * type: integer + * description: 栏舍ID + * responses: + * 200: + * description: 创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * $ref: '#/components/schemas/DeviceAlert' + */ +router.post('/', jwtAuth, deviceAlertController.createAlert); + +/** + * @swagger + * components: + * schemas: + * DeviceAlert: + * type: object + * properties: + * id: + * type: integer + * description: 预警ID + * device_id: + * type: integer + * description: 设备ID + * alert_type: + * type: string + * description: 预警类型 + * alert_level: + * type: string + * enum: [low, medium, high, critical] + * description: 预警级别 + * alert_title: + * type: string + * description: 预警标题 + * alert_content: + * type: string + * description: 预警内容 + * alert_time: + * type: string + * format: date-time + * description: 预警时间 + * status: + * type: string + * enum: [pending, processing, resolved, ignored] + * description: 处理状态 + * handler_id: + * type: integer + * description: 处理人ID + * handle_time: + * type: string + * format: date-time + * description: 处理时间 + * handle_remark: + * type: string + * description: 处理备注 + * farm_id: + * type: integer + * description: 养殖场ID + * pen_id: + * type: integer + * description: 栏舍ID + * is_read: + * type: boolean + * description: 是否已读 + * read_time: + * type: string + * format: date-time + * description: 阅读时间 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * device: + * $ref: '#/components/schemas/Device' + * handler: + * $ref: '#/components/schemas/User' + * Pagination: + * type: object + * properties: + * current_page: + * type: integer + * description: 当前页码 + * per_page: + * type: integer + * description: 每页数量 + * total: + * type: integer + * description: 总记录数 + * total_pages: + * type: integer + * description: 总页数 + */ + +module.exports = router; \ No newline at end of file diff --git a/insurance_backend/routes/devices.js b/insurance_backend/routes/devices.js new file mode 100644 index 0000000..6cbb1b6 --- /dev/null +++ b/insurance_backend/routes/devices.js @@ -0,0 +1,467 @@ +const express = require('express'); +const router = express.Router(); +const deviceController = require('../controllers/deviceController'); +const { jwtAuth } = require('../middleware/auth'); + +/** + * @swagger + * /api/devices: + * get: + * tags: + * - 设备管理 + * summary: 获取设备列表 + * description: 分页获取设备列表,支持多种筛选条件 + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 20 + * description: 每页数量 + * - in: query + * name: device_type + * schema: + * type: string + * description: 设备类型 + * - in: query + * name: status + * schema: + * type: string + * enum: [normal, maintenance, fault, offline] + * description: 设备状态 + * - in: query + * name: farm_id + * schema: + * type: integer + * description: 养殖场ID + * - in: query + * name: pen_id + * schema: + * type: integer + * description: 栏舍ID + * - in: query + * name: keyword + * schema: + * type: string + * description: 关键词搜索(设备编号、名称、型号、制造商) + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * devices: + * type: array + * items: + * $ref: '#/components/schemas/Device' + * pagination: + * $ref: '#/components/schemas/Pagination' + */ +router.get('/', jwtAuth, deviceController.getDeviceList); + +/** + * @swagger + * /api/devices/stats: + * get: + * tags: + * - 设备管理 + * summary: 获取设备统计信息 + * description: 获取设备的统计数据,包括总数、按状态分类、按类型分类等 + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: farm_id + * schema: + * type: integer + * description: 养殖场ID + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * total_devices: + * type: integer + * description: 总设备数 + * devices_by_status: + * type: array + * items: + * type: object + * properties: + * status: + * type: string + * count: + * type: integer + * devices_by_type: + * type: array + * items: + * type: object + * properties: + * device_type: + * type: string + * count: + * type: integer + */ +router.get('/stats', jwtAuth, deviceController.getDeviceStats); + +/** + * @swagger + * /api/devices/types: + * get: + * tags: + * - 设备管理 + * summary: 获取设备类型列表 + * description: 获取系统中所有的设备类型 + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * type: string + */ +router.get('/types', jwtAuth, deviceController.getDeviceTypes); + +/** + * @swagger + * /api/devices/{id}: + * get: + * tags: + * - 设备管理 + * summary: 获取设备详情 + * description: 根据ID获取设备的详细信息,包括预警统计和最近预警记录 + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 设备ID + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * device: + * $ref: '#/components/schemas/Device' + * alert_stats: + * type: array + * items: + * type: object + * properties: + * alert_level: + * type: string + * count: + * type: integer + * recent_alerts: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * alert_type: + * type: string + * alert_level: + * type: string + * alert_title: + * type: string + * alert_time: + * type: string + * format: date-time + * status: + * type: string + * 404: + * description: 设备不存在 + */ +router.get('/:id', jwtAuth, deviceController.getDeviceDetail); + +/** + * @swagger + * /api/devices: + * post: + * tags: + * - 设备管理 + * summary: 创建设备 + * description: 创建新的设备记录 + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - device_code + * - device_name + * - device_type + * properties: + * device_code: + * type: string + * description: 设备编号 + * device_name: + * type: string + * description: 设备名称 + * device_type: + * type: string + * description: 设备类型 + * device_model: + * type: string + * description: 设备型号 + * manufacturer: + * type: string + * description: 制造商 + * installation_location: + * type: string + * description: 安装位置 + * installation_date: + * type: string + * format: date + * description: 安装日期 + * farm_id: + * type: integer + * description: 养殖场ID + * pen_id: + * type: integer + * description: 栏舍ID + * status: + * type: string + * enum: [normal, maintenance, fault, offline] + * default: normal + * description: 设备状态 + * responses: + * 200: + * description: 创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * $ref: '#/components/schemas/Device' + * 400: + * description: 设备编号已存在 + */ +router.post('/', jwtAuth, deviceController.createDevice); + +/** + * @swagger + * /api/devices/{id}: + * put: + * tags: + * - 设备管理 + * summary: 更新设备 + * description: 更新指定设备的信息 + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 设备ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * device_code: + * type: string + * description: 设备编号 + * device_name: + * type: string + * description: 设备名称 + * device_type: + * type: string + * description: 设备类型 + * device_model: + * type: string + * description: 设备型号 + * manufacturer: + * type: string + * description: 制造商 + * installation_location: + * type: string + * description: 安装位置 + * installation_date: + * type: string + * format: date + * description: 安装日期 + * farm_id: + * type: integer + * description: 养殖场ID + * pen_id: + * type: integer + * description: 栏舍ID + * status: + * type: string + * enum: [normal, maintenance, fault, offline] + * description: 设备状态 + * responses: + * 200: + * description: 更新成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * $ref: '#/components/schemas/Device' + * 404: + * description: 设备不存在 + * 400: + * description: 设备编号已存在 + */ +router.put('/:id', jwtAuth, deviceController.updateDevice); + +/** + * @swagger + * /api/devices/{id}: + * delete: + * tags: + * - 设备管理 + * summary: 删除设备 + * description: 删除指定的设备记录 + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 设备ID + * responses: + * 200: + * description: 删除成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 404: + * description: 设备不存在 + * 400: + * description: 该设备存在预警记录,无法删除 + */ +router.delete('/:id', jwtAuth, deviceController.deleteDevice); + +/** + * @swagger + * components: + * schemas: + * Device: + * type: object + * properties: + * id: + * type: integer + * description: 设备ID + * device_code: + * type: string + * description: 设备编号 + * device_name: + * type: string + * description: 设备名称 + * device_type: + * type: string + * description: 设备类型 + * device_model: + * type: string + * description: 设备型号 + * manufacturer: + * type: string + * description: 制造商 + * installation_location: + * type: string + * description: 安装位置 + * installation_date: + * type: string + * format: date + * description: 安装日期 + * status: + * type: string + * enum: [normal, maintenance, fault, offline] + * description: 设备状态 + * farm_id: + * type: integer + * description: 养殖场ID + * pen_id: + * type: integer + * description: 栏舍ID + * created_by: + * type: integer + * description: 创建人ID + * updated_by: + * type: integer + * description: 更新人ID + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * creator: + * $ref: '#/components/schemas/User' + */ + +module.exports = router; \ No newline at end of file diff --git a/insurance_backend/routes/operationLogs.js b/insurance_backend/routes/operationLogs.js new file mode 100644 index 0000000..1235314 --- /dev/null +++ b/insurance_backend/routes/operationLogs.js @@ -0,0 +1,319 @@ +const express = require('express'); +const router = express.Router(); +const operationLogController = require('../controllers/operationLogController'); +const { jwtAuth, checkPermission } = require('../middleware/auth'); + +/** + * @swagger + * tags: + * name: OperationLogs + * description: 系统操作日志管理 + */ + +/** + * @swagger + * /api/operation-logs: + * get: + * summary: 获取操作日志列表 + * tags: [OperationLogs] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 20 + * description: 每页数量 + * - in: query + * name: user_id + * schema: + * type: integer + * description: 用户ID + * - in: query + * name: operation_type + * schema: + * type: string + * enum: [login, logout, create, update, delete, view, export, import, approve, reject, system_config, user_manage, role_manage, other] + * description: 操作类型 + * - in: query + * name: operation_module + * schema: + * type: string + * description: 操作模块 + * - in: query + * name: status + * schema: + * type: string + * enum: [success, failed, error] + * description: 操作状态 + * - in: query + * name: start_date + * schema: + * type: string + * format: date + * description: 开始日期 + * - in: query + * name: end_date + * schema: + * type: string + * format: date + * description: 结束日期 + * - in: query + * name: keyword + * schema: + * type: string + * description: 关键词搜索 + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * type: object + * properties: + * logs: + * type: array + * items: + * $ref: '#/components/schemas/OperationLog' + * total: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * totalPages: + * type: integer + */ +router.get('/', jwtAuth, checkPermission('system', 'read'), operationLogController.getOperationLogs); + +/** + * @swagger + * /api/operation-logs/stats: + * get: + * summary: 获取操作日志统计信息 + * tags: [OperationLogs] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: user_id + * schema: + * type: integer + * description: 用户ID + * - in: query + * name: start_date + * schema: + * type: string + * format: date + * description: 开始日期 + * - in: query + * name: end_date + * schema: + * type: string + * format: date + * description: 结束日期 + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * type: object + * properties: + * typeStats: + * type: array + * items: + * type: object + * properties: + * operation_type: + * type: string + * count: + * type: integer + * moduleStats: + * type: array + * items: + * type: object + * properties: + * operation_module: + * type: string + * count: + * type: integer + * statusStats: + * type: array + * items: + * type: object + * properties: + * status: + * type: string + * count: + * type: integer + */ +router.get('/stats', jwtAuth, checkPermission('system', 'read'), operationLogController.getOperationStats); + +/** + * @swagger + * /api/operation-logs/{id}: + * get: + * summary: 获取操作日志详情 + * tags: [OperationLogs] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 操作日志ID + * responses: + * 200: + * description: 获取成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * data: + * $ref: '#/components/schemas/OperationLog' + */ +router.get('/:id', jwtAuth, checkPermission('system', 'read'), operationLogController.getOperationLogById); + +/** + * @swagger + * /api/operation-logs/export: + * post: + * summary: 导出操作日志 + * tags: [OperationLogs] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * user_id: + * type: integer + * operation_type: + * type: string + * operation_module: + * type: string + * status: + * type: string + * start_date: + * type: string + * format: date + * end_date: + * type: string + * format: date + * keyword: + * type: string + * responses: + * 200: + * description: 导出成功 + * content: + * application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + * schema: + * type: string + * format: binary + */ +router.post('/export', jwtAuth, checkPermission('system', 'export'), operationLogController.exportOperationLogs); + +/** + * @swagger + * components: + * schemas: + * OperationLog: + * type: object + * properties: + * id: + * type: integer + * description: 日志ID + * user_id: + * type: integer + * description: 操作用户ID + * operation_type: + * type: string + * enum: [login, logout, create, update, delete, view, export, import, approve, reject, system_config, user_manage, role_manage, other] + * description: 操作类型 + * operation_module: + * type: string + * description: 操作模块 + * operation_content: + * type: string + * description: 操作内容描述 + * operation_target: + * type: string + * description: 操作目标 + * request_method: + * type: string + * enum: [GET, POST, PUT, DELETE, PATCH] + * description: HTTP请求方法 + * request_url: + * type: string + * description: 请求URL + * request_params: + * type: object + * description: 请求参数 + * response_status: + * type: integer + * description: 响应状态码 + * ip_address: + * type: string + * description: IP地址 + * user_agent: + * type: string + * description: 用户代理信息 + * execution_time: + * type: integer + * description: 执行时间(毫秒) + * status: + * type: string + * enum: [success, failed, error] + * description: 操作状态 + * error_message: + * type: string + * description: 错误信息 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * user: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * real_name: + * type: string + */ + +module.exports = router; \ No newline at end of file diff --git a/insurance_backend/routes/users.js b/insurance_backend/routes/users.js index 6d6b744..2bc8731 100644 --- a/insurance_backend/routes/users.js +++ b/insurance_backend/routes/users.js @@ -6,6 +6,13 @@ const { jwtAuth, checkPermission } = require('../middleware/auth'); // 获取用户列表(需要管理员权限) router.get('/', jwtAuth, checkPermission('user', 'read'), userController.getUsers); +// 个人中心相关路由(必须放在 /:id 路由之前) +// 获取个人资料(不需要特殊权限,用户可以查看自己的资料) +router.get('/profile', jwtAuth, userController.getProfile); + +// 更新个人资料(不需要特殊权限,用户可以更新自己的资料) +router.put('/profile', jwtAuth, userController.updateProfile); + // 获取单个用户信息 router.get('/:id', jwtAuth, checkPermission('user', 'read'), userController.getUser); @@ -21,4 +28,10 @@ router.delete('/:id', jwtAuth, checkPermission('user', 'delete'), userController // 更新用户状态 router.patch('/:id/status', jwtAuth, checkPermission('user', 'update'), userController.updateUserStatus); +// 修改密码(不需要特殊权限,用户可以修改自己的密码) +router.put('/change-password', jwtAuth, userController.changePassword); + +// 上传头像(不需要特殊权限,用户可以上传自己的头像) +router.post('/avatar', jwtAuth, userController.uploadAvatar); + module.exports = router; \ No newline at end of file diff --git a/insurance_backend/scripts/check-table-structure.js b/insurance_backend/scripts/check-table-structure.js new file mode 100644 index 0000000..58d4d75 --- /dev/null +++ b/insurance_backend/scripts/check-table-structure.js @@ -0,0 +1,43 @@ +const mysql = require('mysql2/promise'); + +async function checkTableStructure() { + try { + // 创建数据库连接 + const connection = await mysql.createConnection({ + host: '129.211.213.226', + port: 9527, + user: 'root', + password: 'aiotAiot123!', + database: 'insurance_data' + }); + + console.log('✅ 数据库连接成功'); + + // 查看devices表结构 + console.log('\n📋 devices表结构:'); + const [devicesColumns] = await connection.execute('DESCRIBE devices'); + console.table(devicesColumns); + + // 查看device_alerts表结构 + console.log('\n📋 device_alerts表结构:'); + const [alertsColumns] = await connection.execute('DESCRIBE device_alerts'); + console.table(alertsColumns); + + await connection.end(); + console.log('\n✅ 检查完成'); + + } catch (error) { + console.error('❌ 检查表结构失败:', error); + } +} + +if (require.main === module) { + checkTableStructure().then(() => { + process.exit(0); + }).catch(error => { + console.error('❌ 脚本执行失败:', error); + process.exit(1); + }); +} + +module.exports = checkTableStructure; \ No newline at end of file diff --git a/insurance_backend/scripts/check_user_permissions.js b/insurance_backend/scripts/check_user_permissions.js new file mode 100644 index 0000000..5b1ef65 --- /dev/null +++ b/insurance_backend/scripts/check_user_permissions.js @@ -0,0 +1,96 @@ +const mysql = require('mysql2/promise'); +const jwt = require('jsonwebtoken'); + +async function checkUserPermissions() { + const connection = await mysql.createConnection({ + host: '129.211.213.226', + port: 9527, + user: 'root', + password: 'aiotAiot123!', + database: 'insurance_data' + }); + + try { + console.log('=== 检查用户权限和JWT Token ==='); + + // 1. 查询admin用户信息 + const [adminUsers] = await connection.execute( + 'SELECT * FROM users WHERE username = ?', + ['admin'] + ); + + if (adminUsers.length === 0) { + console.log('❌ Admin用户不存在'); + return; + } + + const adminUser = adminUsers[0]; + console.log('\n1. Admin用户信息:'); + console.log(`- ID: ${adminUser.id}`); + console.log(`- 用户名: ${adminUser.username}`); + console.log(`- 角色ID: ${adminUser.role_id}`); + console.log(`- 状态: ${adminUser.status}`); + + // 2. 查询admin角色权限 + const [roles] = await connection.execute( + 'SELECT * FROM roles WHERE id = ?', + [adminUser.role_id] + ); + + if (roles.length === 0) { + console.log('❌ Admin角色不存在'); + return; + } + + const adminRole = roles[0]; + console.log('\n2. Admin角色信息:'); + console.log(`- 角色名: ${adminRole.name}`); + console.log(`- 权限类型: ${typeof adminRole.permissions}`); + console.log(`- 权限内容: ${JSON.stringify(adminRole.permissions, null, 2)}`); + + // 3. 模拟JWT token生成 + console.log('\n3. 模拟JWT Token生成:'); + const tokenPayload = { + id: adminUser.id, + username: adminUser.username, + role_id: adminUser.role_id, + permissions: adminRole.permissions || [] + }; + + console.log('Token Payload:', JSON.stringify(tokenPayload, null, 2)); + + // 使用默认密钥生成token(实际应用中应该从环境变量获取) + const jwtSecret = process.env.JWT_SECRET || 'your_jwt_secret_key'; + const token = jwt.sign(tokenPayload, jwtSecret, { expiresIn: '7d' }); + + console.log('\n4. 生成的JWT Token:'); + console.log(token); + + // 5. 验证token + console.log('\n5. 验证JWT Token:'); + try { + const decoded = jwt.verify(token, jwtSecret); + console.log('解码后的Token:', JSON.stringify(decoded, null, 2)); + + // 检查权限 + const hasDataRead = decoded.permissions && + (Array.isArray(decoded.permissions) ? + decoded.permissions.includes('data:read') : + decoded.permissions.includes && decoded.permissions.includes('data:read')); + + console.log(`\n6. 权限检查结果:`); + console.log(`- 是否有data:read权限: ${hasDataRead}`); + console.log(`- 权限数组长度: ${Array.isArray(decoded.permissions) ? decoded.permissions.length : 'N/A'}`); + + } catch (error) { + console.error('Token验证失败:', error.message); + } + + } catch (error) { + console.error('检查时出错:', error); + } finally { + await connection.end(); + } +} + +checkUserPermissions(); \ No newline at end of file diff --git a/insurance_backend/scripts/create-device-tables.js b/insurance_backend/scripts/create-device-tables.js new file mode 100644 index 0000000..eaa1af3 --- /dev/null +++ b/insurance_backend/scripts/create-device-tables.js @@ -0,0 +1,89 @@ +const { sequelize } = require('../models'); + +async function createTables() { + try { + console.log('开始创建设备相关表...'); + + // 创建设备表 + await sequelize.query(` + CREATE TABLE IF NOT EXISTS devices ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '设备ID', + device_number VARCHAR(50) NOT NULL UNIQUE COMMENT '设备编号', + device_name VARCHAR(100) NOT NULL COMMENT '设备名称', + device_type VARCHAR(50) NOT NULL COMMENT '设备类型', + device_model VARCHAR(100) COMMENT '设备型号', + manufacturer VARCHAR(100) COMMENT '制造商', + installation_location VARCHAR(200) COMMENT '安装位置', + installation_date DATE COMMENT '安装日期', + status ENUM('normal', 'warning', 'error', 'offline') DEFAULT 'normal' COMMENT '设备状态', + farm_id INT COMMENT '养殖场ID', + barn_id INT COMMENT '栏舍ID', + created_by INT COMMENT '创建人ID', + updated_by INT COMMENT '更新人ID', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_device_number (device_number), + INDEX idx_device_type (device_type), + INDEX idx_status (status), + INDEX idx_farm_barn (farm_id, barn_id), + INDEX idx_created_by (created_by), + INDEX idx_updated_by (updated_by) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备表'; + `); + + console.log('✅ 设备表创建成功'); + + // 创建设备预警表 + await sequelize.query(` + CREATE TABLE IF NOT EXISTS device_alerts ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '预警ID', + device_id INT NOT NULL COMMENT '设备ID', + alert_type VARCHAR(50) NOT NULL COMMENT '预警类型', + alert_level ENUM('info', 'warning', 'critical') NOT NULL COMMENT '预警级别', + alert_title VARCHAR(200) NOT NULL COMMENT '预警标题', + alert_content TEXT COMMENT '预警内容', + alert_time TIMESTAMP NOT NULL COMMENT '预警时间', + status ENUM('pending', 'processing', 'resolved', 'ignored') DEFAULT 'pending' COMMENT '处理状态', + handler_id INT COMMENT '处理人ID', + handle_time TIMESTAMP NULL COMMENT '处理时间', + handle_note TEXT COMMENT '处理备注', + farm_id INT COMMENT '养殖场ID', + barn_id INT COMMENT '栏舍ID', + is_read BOOLEAN DEFAULT FALSE COMMENT '是否已读', + read_time TIMESTAMP NULL COMMENT '阅读时间', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX idx_device_id (device_id), + INDEX idx_alert_type (alert_type), + INDEX idx_alert_level (alert_level), + INDEX idx_alert_time (alert_time), + INDEX idx_status (status), + INDEX idx_farm_barn (farm_id, barn_id), + INDEX idx_handler_id (handler_id), + INDEX idx_is_read (is_read), + FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE, + FOREIGN KEY (handler_id) REFERENCES users(id) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='设备预警表'; + `); + + console.log('✅ 设备预警表创建成功'); + console.log('🎉 所有表创建完成!'); + + } catch (error) { + console.error('❌ 创建表失败:', error); + throw error; + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + createTables().then(() => { + console.log('数据库表创建完成'); + process.exit(0); + }).catch(error => { + console.error('创建失败:', error); + process.exit(1); + }); +} + +module.exports = createTables; \ No newline at end of file diff --git a/insurance_backend/scripts/test-device-data.js b/insurance_backend/scripts/test-device-data.js new file mode 100644 index 0000000..b065470 --- /dev/null +++ b/insurance_backend/scripts/test-device-data.js @@ -0,0 +1,161 @@ +const { Device, DeviceAlert, User } = require('../models'); + +async function createTestData() { + try { + console.log('开始创建测试数据...'); + + // 获取第一个用户作为创建者 + const user = await User.findOne(); + if (!user) { + console.log('❌ 没有找到用户,请先创建用户'); + return; + } + + // 检查是否已有测试设备 + const existingDevice = await Device.findOne({ where: { device_number: 'DEV001' } }); + let devices; + + if (existingDevice) { + console.log('📋 测试设备已存在,使用现有设备'); + devices = await Device.findAll({ + where: { + device_number: ['DEV001', 'DEV002', 'DEV003'] + } + }); + } else { + // 创建测试设备 + devices = await Device.bulkCreate([ + { + device_number: 'DEV001', + device_name: '温度传感器A', + device_type: '温度传感器', + device_model: 'TMP-100', + manufacturer: '智能科技', + installation_location: '1号栏舍', + installation_date: new Date('2024-01-15'), + status: 'normal', + farm_id: 1, + barn_id: 1, + created_by: user.id, + updated_by: user.id + }, + { + device_number: 'DEV002', + device_name: '湿度传感器B', + device_type: '湿度传感器', + device_model: 'HUM-200', + manufacturer: '智能科技', + installation_location: '2号栏舍', + installation_date: new Date('2024-01-20'), + status: 'warning', + farm_id: 1, + barn_id: 2, + created_by: user.id, + updated_by: user.id + }, + { + device_number: 'DEV003', + device_name: '监控摄像头C', + device_type: '监控设备', + device_model: 'CAM-300', + manufacturer: '安防科技', + installation_location: '3号栏舍', + installation_date: new Date('2024-02-01'), + status: 'error', + farm_id: 1, + barn_id: 3, + created_by: user.id, + updated_by: user.id + } + ]); + + console.log(`✅ 创建了 ${devices.length} 个测试设备`); + } + + console.log(`📊 当前有 ${devices.length} 个测试设备`); + + // 检查是否已有测试预警 + const existingAlert = await DeviceAlert.findOne({ where: { device_id: devices[0].id } }); + let alerts; + + if (existingAlert) { + console.log('📋 测试预警已存在,清除旧数据并创建新数据'); + await DeviceAlert.destroy({ where: { device_id: devices.map(d => d.id) } }); + } + + // 创建测试预警 + alerts = await DeviceAlert.bulkCreate([ + { + device_id: devices[0].id, + alert_type: 'temperature', + alert_level: 'warning', + alert_title: '温度异常', + alert_content: '1号栏舍温度传感器检测到温度过高,当前温度35°C', + alert_time: new Date(), + status: 'pending', + farm_id: 1, + barn_id: 1, + is_read: false + }, + { + device_id: devices[1].id, + alert_type: 'humidity', + alert_level: 'critical', + alert_title: '湿度严重异常', + alert_content: '2号栏舍湿度传感器检测到湿度过低,当前湿度30%', + alert_time: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2小时前 + status: 'pending', + farm_id: 1, + barn_id: 2, + is_read: false + }, + { + device_id: devices[2].id, + alert_type: 'offline', + alert_level: 'critical', + alert_title: '设备离线', + alert_content: '3号栏舍监控摄像头已离线超过30分钟', + alert_time: new Date(Date.now() - 4 * 60 * 60 * 1000), // 4小时前 + status: 'pending', + farm_id: 1, + barn_id: 3, + is_read: true, + read_time: new Date(Date.now() - 3 * 60 * 60 * 1000) + }, + { + device_id: devices[0].id, + alert_type: 'maintenance', + alert_level: 'info', + alert_title: '设备维护提醒', + alert_content: '温度传感器A需要进行定期维护检查', + alert_time: new Date(Date.now() - 24 * 60 * 60 * 1000), // 1天前 + status: 'resolved', + handler_id: user.id, + handle_time: new Date(Date.now() - 20 * 60 * 60 * 1000), + handle_note: '已完成维护检查,设备运行正常', + farm_id: 1, + barn_id: 1, + is_read: true, + read_time: new Date(Date.now() - 23 * 60 * 60 * 1000) + } + ]); + + console.log(`✅ 创建了 ${alerts.length} 个测试预警`); + console.log('🎉 测试数据创建完成!'); + + } catch (error) { + console.error('❌ 创建测试数据失败:', error); + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + createTestData().then(() => { + process.exit(0); + }).catch(error => { + console.error(error); + process.exit(1); + }); +} + +module.exports = createTestData; \ No newline at end of file diff --git a/insurance_backend/scripts/test_frontend_auth.js b/insurance_backend/scripts/test_frontend_auth.js new file mode 100644 index 0000000..f4e10ab --- /dev/null +++ b/insurance_backend/scripts/test_frontend_auth.js @@ -0,0 +1,76 @@ +const axios = require('axios'); +const jwt = require('jsonwebtoken'); + +async function testFrontendAuth() { + const baseURL = 'http://localhost:3000'; + + try { + console.log('=== 测试前端认证流程 ==='); + + // 1. 模拟前端登录 + console.log('\n1. 模拟前端登录...'); + const loginResponse = await axios.post(`${baseURL}/api/auth/login`, { + username: 'admin', + password: '123456' + }); + + if (loginResponse.data.status === 'success' || loginResponse.data.success) { + console.log('✅ 登录成功'); + const token = loginResponse.data.data.token; + console.log(`Token: ${token.substring(0, 50)}...`); + + // 2. 解码token查看内容 + console.log('\n2. 解码JWT Token:'); + try { + const jwtSecret = 'insurance_super_secret_jwt_key_2024'; + const decoded = jwt.verify(token, jwtSecret); + console.log('Token内容:', JSON.stringify(decoded, null, 2)); + + // 检查权限 + const hasDataRead = decoded.permissions && + (Array.isArray(decoded.permissions) ? + decoded.permissions.includes('data:read') : + decoded.permissions.includes && decoded.permissions.includes('data:read')); + + console.log(`\n权限检查:`); + console.log(`- 是否有data:read权限: ${hasDataRead}`); + console.log(`- 权限数组长度: ${Array.isArray(decoded.permissions) ? decoded.permissions.length : 'N/A'}`); + + } catch (error) { + console.error('❌ Token解码失败:', error.message); + } + + // 3. 测试数据仓库接口 + console.log('\n3. 测试数据仓库接口访问:'); + try { + const overviewResponse = await axios.get(`${baseURL}/api/data-warehouse/overview`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + console.log('✅ 数据仓库接口访问成功'); + console.log('响应状态:', overviewResponse.status); + console.log('响应数据:', JSON.stringify(overviewResponse.data, null, 2)); + + } catch (error) { + console.error('❌ 数据仓库接口访问失败:'); + console.error('状态码:', error.response?.status); + console.error('错误信息:', error.response?.data); + console.error('完整错误:', error.message); + } + + } else { + console.error('❌ 登录失败:', loginResponse.data); + } + + } catch (error) { + console.error('❌ 测试过程中出错:', error.message); + if (error.response) { + console.error('响应状态:', error.response.status); + console.error('响应数据:', error.response.data); + } + } +} + +testFrontendAuth(); \ No newline at end of file diff --git a/insurance_backend/src/app.js b/insurance_backend/src/app.js index 51828d1..aefd73e 100644 --- a/insurance_backend/src/app.js +++ b/insurance_backend/src/app.js @@ -13,7 +13,7 @@ const PORT = process.env.PORT || 3000; // 安全中间件 app.use(helmet()); app.use(cors({ - origin: process.env.FRONTEND_URL || 'http://localhost:5173', + origin: process.env.FRONTEND_URL || 'http://localhost:3001', credentials: true })); @@ -56,6 +56,7 @@ app.use('/api/insurance-types', require('../routes/insuranceTypes')); app.use('/api/policies', require('../routes/policies')); app.use('/api/claims', require('../routes/claims')); app.use('/api/system', require('../routes/system')); +app.use('/api/operation-logs', require('../routes/operationLogs')); app.use('/api/menus', require('../routes/menus')); app.use('/api/data-warehouse', require('../routes/dataWarehouse')); app.use('/api/supervisory-tasks', require('../routes/supervisoryTasks')); @@ -68,6 +69,10 @@ app.use('/api/livestock-types', require('../routes/livestockTypes')); app.use('/api/livestock-policies', require('../routes/livestockPolicies')); app.use('/api/livestock-claims', require('../routes/livestockClaims')); +// 设备管理相关路由 +app.use('/api/devices', require('../routes/devices')); +app.use('/api/device-alerts', require('../routes/deviceAlerts')); + // API文档路由 app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, { explorer: true, diff --git a/insurance_backend/test-routes.js b/insurance_backend/test-routes.js new file mode 100644 index 0000000..529f4b4 --- /dev/null +++ b/insurance_backend/test-routes.js @@ -0,0 +1,29 @@ +const express = require('express'); + +// 测试路由加载 +console.log('开始测试路由加载...'); + +try { + // 测试设备路由 + const deviceRoutes = require('./routes/devices'); + console.log('✅ 设备路由加载成功'); + + // 测试设备控制器 + const deviceController = require('./controllers/deviceController'); + console.log('✅ 设备控制器加载成功'); + + // 测试模型 + const { Device, DeviceAlert } = require('./models'); + console.log('✅ 设备模型加载成功'); + + // 创建简单的Express应用测试 + const app = express(); + app.use('/api/devices', deviceRoutes); + + console.log('✅ 路由注册成功'); + console.log('所有组件加载正常!'); + +} catch (error) { + console.error('❌ 路由加载失败:', error.message); + console.error('错误详情:', error); +} \ No newline at end of file diff --git a/insurance_backend/test_browser_behavior.js b/insurance_backend/test_browser_behavior.js new file mode 100644 index 0000000..c46dd5c --- /dev/null +++ b/insurance_backend/test_browser_behavior.js @@ -0,0 +1,99 @@ +const axios = require('axios'); + +// 创建一个模拟浏览器的axios实例 +const browserAPI = axios.create({ + baseURL: 'http://localhost:3001', + timeout: 10000, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + } +}); + +async function testBrowserBehavior() { + console.log('=== 模拟浏览器行为测试 ===\n'); + + try { + // 1. 模拟前端登录 + console.log('1. 模拟浏览器登录...'); + const loginResponse = await browserAPI.post('/api/auth/login', { + username: 'admin', + password: '123456' + }); + + console.log('登录响应状态:', loginResponse.status); + console.log('登录响应数据:', JSON.stringify(loginResponse.data, null, 2)); + + if (!loginResponse.data || loginResponse.data.code !== 200) { + console.log('❌ 登录失败'); + return; + } + + const token = loginResponse.data.data.token; + console.log('✅ 获取到Token:', token.substring(0, 50) + '...'); + + // 2. 设置Authorization header + browserAPI.defaults.headers.common['Authorization'] = `Bearer ${token}`; + + // 3. 模拟前端API调用 + console.log('\n2. 模拟浏览器API调用...'); + + try { + const apiResponse = await browserAPI.get('/api/data-warehouse/overview'); + + console.log('✅ API调用成功!'); + console.log('状态码:', apiResponse.status); + console.log('响应数据:', JSON.stringify(apiResponse.data, null, 2)); + + } catch (apiError) { + console.log('❌ API调用失败:', apiError.response?.status, apiError.response?.statusText); + console.log('错误详情:', apiError.response?.data); + console.log('请求头:', apiError.config?.headers); + + // 检查是否是权限问题 + if (apiError.response?.status === 403) { + console.log('\n🔍 403错误分析:'); + console.log('- Token是否正确传递:', !!apiError.config?.headers?.Authorization); + console.log('- Authorization头:', apiError.config?.headers?.Authorization?.substring(0, 50) + '...'); + + // 尝试验证token + console.log('\n3. 验证Token有效性...'); + try { + const profileResponse = await browserAPI.get('/api/auth/profile'); + console.log('✅ Token验证成功,用户信息:', profileResponse.data); + } catch (profileError) { + console.log('❌ Token验证失败:', profileError.response?.status, profileError.response?.data); + } + } + } + + // 4. 测试其他需要权限的接口 + console.log('\n4. 测试其他权限接口...'); + const testAPIs = [ + '/api/insurance/applications', + '/api/device-alerts/stats', + '/api/system/stats' + ]; + + for (const apiPath of testAPIs) { + try { + const response = await browserAPI.get(apiPath); + console.log(`✅ ${apiPath}: 成功 (${response.status})`); + } catch (error) { + console.log(`❌ ${apiPath}: 失败 (${error.response?.status}) - ${error.response?.data?.message || error.message}`); + } + } + + } catch (error) { + console.log('❌ 测试失败:', error.response?.data || error.message); + if (error.response) { + console.log('错误状态:', error.response.status); + console.log('错误数据:', error.response.data); + } + } +} + +testBrowserBehavior(); \ No newline at end of file diff --git a/insurance_backend/test_menu_api.js b/insurance_backend/test_menu_api.js new file mode 100644 index 0000000..36c53d8 --- /dev/null +++ b/insurance_backend/test_menu_api.js @@ -0,0 +1,32 @@ +const axios = require('axios'); + +async function testMenuAPI() { + try { + // 1. 先登录获取token + console.log('1. 登录获取token...'); + const loginResponse = await axios.post('http://localhost:3000/api/auth/login', { + username: 'admin', + password: '123456' + }); + + const token = loginResponse.data.data.accessToken; + console.log('登录成功,token:', token.substring(0, 50) + '...'); + + // 2. 测试菜单接口 + console.log('\n2. 测试菜单接口...'); + const menuResponse = await axios.get('http://localhost:3000/api/menus', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + console.log('菜单接口响应状态:', menuResponse.status); + console.log('菜单接口响应数据:', JSON.stringify(menuResponse.data, null, 2)); + + } catch (error) { + console.error('测试失败:', error.response?.data || error.message); + } +} + +testMenuAPI(); \ No newline at end of file