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 0000000..581ced0
Binary files /dev/null and b/insurance_admin-system/debug_menu.js differ
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: 检查当前状态
+
检查当前Token状态
+
+
+
+
+
步骤2: 设置新Token
+
设置最新Token
+
+
+
+
+
步骤3: 测试API连接
+
测试数据仓库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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 保存设置
- 测试连接
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 保存模板
-
-
-
-
-
-
-
-
-
-
-
-
-
- 保存模板
-
-
-
-
-
-
-
-
-
-
-
-
-
- 保存模板
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 通知类型
-
-
-
- 邮件
- 短信
- 站内
-
-
-
-
-
- 邮件
- 短信
- 站内
-
-
-
-
-
- 邮件
- 短信
- 站内
-
-
-
-
-
- 邮件
- 短信
- 站内
-
-
-
-
- 保存设置
-
-
-
-
-
-
-
-
- 阿里云
- 腾讯云
- 其他
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 保存设置
- 测试发送
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 每天
- 每周
- 每月
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 选择路径
-
-
-
-
- 保存设置
-
- 立即备份
-
-
-
-
-
-
- 用户管理
+ 保险管理
+ 申请管理
+ 保单管理
+ 理赔管理
+ 系统管理
+
+
+
+
-
-
- {{ formatFileSize(record.size) }}
-
-
-
- 下载
- 恢复
-
- 删除
-
-
-
-
-
-
-
-
+
成功
+
失败
+
+
+
+
+
+
+
+ 搜索
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+ {{ getOperationTypeText(record.operation_type) }}
+
+
+
+
+ {{ record.status === 'success' ? '成功' : '失败' }}
+
+
+
+ {{ formatDateTime(record.operation_time) }}
+
+
+
+ 查看详情
+
+
+
+
+
+
+
+
+
+ {{ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ userInfo.username }}
+
+
+ {{ userInfo.real_name }}
+
+
+ {{ userInfo.email }}
+
+
+ {{ userInfo.phone }}
+
+
+ {{ getRoleName(userInfo.role_id) }}
+
+
+
+ {{ userInfo.status === 'active' ? '正常' : '禁用' }}
+
+
+
+ {{ formatDate(userInfo.last_login) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 更新资料
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 修改密码
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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