完善保险端前后端

This commit is contained in:
shenquanyi
2025-09-24 18:12:37 +08:00
parent 111ebaec84
commit b17bdcc24c
56 changed files with 9862 additions and 1111 deletions

View File

@@ -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. 后续计划
- 引入微服务架构
- 优化性能监控
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不支持&&操作符,请使用;符号

Binary file not shown.

View File

@@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置认证Token</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.step {
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-left: 4px solid #007bff;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #0056b3;
}
.success {
color: #28a745;
font-weight: bold;
}
.error {
color: #dc3545;
font-weight: bold;
}
.code {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
font-family: monospace;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 保险管理系统 - 认证Token设置</h1>
<div class="step">
<h3>步骤1: 检查当前状态</h3>
<button onclick="checkCurrentStatus()">检查当前Token状态</button>
<div id="currentStatus"></div>
</div>
<div class="step">
<h3>步骤2: 设置新Token</h3>
<button onclick="setNewToken()">设置最新Token</button>
<div id="tokenStatus"></div>
</div>
<div class="step">
<h3>步骤3: 测试API连接</h3>
<button onclick="testAPI()">测试数据仓库API</button>
<div id="apiStatus"></div>
</div>
<div class="step">
<h3>步骤4: 跳转到数据仓库</h3>
<button onclick="goToDataWarehouse()">前往数据仓库页面</button>
</div>
</div>
<script>
// 最新的有效token
const VALID_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGVfaWQiOjEsInBlcm1pc3Npb25zIjpbInVzZXI6cmVhZCIsInVzZXI6Y3JlYXRlIiwidXNlcjp1cGRhdGUiLCJ1c2VyOmRlbGV0ZSIsImluc3VyYW5jZTpyZWFkIiwiaW5zdXJhbmNlOmNyZWF0ZSIsImluc3VyYW5jZTp1cGRhdGUiLCJpbnN1cmFuY2U6ZGVsZXRlIiwiaW5zdXJhbmNlOnJldmlldyIsInBvbGljeTpyZWFkIiwicG9saWN5OmNyZWF0ZSIsInBvbGljeTp1cGRhdGUiLCJwb2xpY3k6ZGVsZXRlIiwibGl2ZXN0b2NrX3BvbGljeTpyZWFkIiwibGl2ZXN0b2NrX3BvbGljeTpjcmVhdGUiLCJsaXZlc3RvY2tfcG9saWN5OnVwZGF0ZSIsImxpdmVzdG9ja19wb2xpY3k6ZGVsZXRlIiwiY2xhaW06cmVhZCIsImNsYWltOmNyZWF0ZSIsImNsYWltOnVwZGF0ZSIsImNsYWltOnJldmlldyIsInN5c3RlbTpyZWFkIiwic3lzdGVtOnVwZGF0ZSIsInN5c3RlbTphZG1pbiIsImRhdGE6cmVhZCIsImRhdGE6Y3JlYXRlIiwiZGF0YTp1cGRhdGUiLCJkYXRhOmRlbGV0ZSJdLCJpYXQiOjE3NTg2OTQ3NjMsImV4cCI6MTc1OTI5OTU2M30.O2yZYBQSnagg7gC_yjLNnXD2C-Yk8W8IJuescTu1K_I';
const USER_INFO = {
"id": 1,
"username": "admin",
"role_id": 1,
"permissions": ["user:read","user:create","user:update","user:delete","insurance:read","insurance:create","insurance:update","insurance:delete","insurance:review","policy:read","policy:create","policy:update","policy:delete","livestock_policy:read","livestock_policy:create","livestock_policy:update","livestock_policy:delete","claim:read","claim:create","claim:update","claim:review","system:read","system:update","system:admin","data:read","data:create","data:update","data:delete"],
"iat": 1758694763,
"exp": 1759299563
};
function checkCurrentStatus() {
const currentToken = localStorage.getItem('token');
const currentUser = localStorage.getItem('userInfo');
const statusDiv = document.getElementById('currentStatus');
let html = '<div class="code">';
if (currentToken) {
html += `当前Token: ${currentToken.substring(0, 50)}...<br>`;
// 检查token是否过期
try {
const payload = JSON.parse(atob(currentToken.split('.')[1]));
const now = Math.floor(Date.now() / 1000);
const isExpired = payload.exp < now;
html += `Token过期时间: ${new Date(payload.exp * 1000).toLocaleString()}<br>`;
html += `当前时间: ${new Date().toLocaleString()}<br>`;
html += `<span class="${isExpired ? 'error' : 'success'}">Token状态: ${isExpired ? '已过期' : '有效'}</span><br>`;
if (payload.permissions && payload.permissions.includes('data:read')) {
html += '<span class="success">✅ 包含data:read权限</span><br>';
} else {
html += '<span class="error">❌ 缺少data:read权限</span><br>';
}
} catch (e) {
html += '<span class="error">❌ Token格式错误</span><br>';
}
} else {
html += '<span class="error">❌ 未找到Token</span><br>';
}
if (currentUser) {
html += `用户信息: ${currentUser.substring(0, 100)}...<br>`;
} else {
html += '<span class="error">❌ 未找到用户信息</span><br>';
}
html += '</div>';
statusDiv.innerHTML = html;
}
function setNewToken() {
try {
localStorage.setItem('token', VALID_TOKEN);
localStorage.setItem('userInfo', JSON.stringify(USER_INFO));
document.getElementById('tokenStatus').innerHTML =
'<div class="success">✅ Token设置成功</div>';
// 自动检查状态
setTimeout(checkCurrentStatus, 500);
} catch (error) {
document.getElementById('tokenStatus').innerHTML =
`<div class="error">❌ Token设置失败: ${error.message}</div>`;
}
}
async function testAPI() {
const statusDiv = document.getElementById('apiStatus');
statusDiv.innerHTML = '<div>🔄 测试中...</div>';
try {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('未找到Token请先设置Token');
}
const response = await fetch('/api/data-warehouse/overview', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
statusDiv.innerHTML = `
<div class="success">✅ API测试成功</div>
<div class="code">响应数据: ${JSON.stringify(data, null, 2)}</div>
`;
} else {
throw new Error(`API调用失败: ${response.status} ${response.statusText}`);
}
} catch (error) {
statusDiv.innerHTML = `<div class="error">❌ API测试失败: ${error.message}</div>`;
}
}
function goToDataWarehouse() {
window.location.href = '/#/data-warehouse';
}
// 页面加载时自动检查状态
window.onload = function() {
checkCurrentStatus();
};
</script>
</body>
</html>

View File

@@ -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',

View File

@@ -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')

View File

@@ -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
}

View File

@@ -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<boolean>} 是否成功
*/
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -43,6 +43,12 @@
</a-input-password>
</a-form-item>
<a-form-item name="remember">
<a-checkbox v-model:checked="formState.remember">
记住登录
</a-checkbox>
</a-form-item>
<a-form-item>
<a-button
type="primary"
@@ -64,12 +70,13 @@
</template>
<script setup>
import { reactive, ref } from 'vue'
import { reactive, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { authAPI } from '@/utils/api'
import { useUserStore } from '@/stores/user'
import authService from '@/services/authService'
const router = useRouter()
const userStore = useUserStore()
@@ -77,28 +84,69 @@ const loading = ref(false)
const formState = reactive({
username: '',
password: ''
password: '',
remember: false
})
const onFinish = async (values) => {
loading.value = true
try {
console.log('登录请求参数:', values)
const response = await authAPI.login(values)
if (response.status === 'success') {
userStore.setToken(response.data.token)
userStore.setUserInfo(response.data.user)
console.log('登录响应:', response)
// 修复响应结构解析:后端返回的是 response.data.status 而不是 response.status
if (response.data && (response.data.status === 'success' || response.data.code === 200)) {
const data = response.data.data
// 检查是否为新的双Token格式
if (data.accessToken && data.refreshToken) {
// 新的双Token格式
console.log('使用新的双Token格式存储认证信息')
console.log('用户数据:', data.user)
userStore.setAuthData({
accessToken: data.accessToken,
refreshToken: data.refreshToken,
accessTokenExpiresAt: data.accessTokenExpiresAt,
refreshTokenExpiresAt: data.refreshTokenExpiresAt,
user: data.user || data.userInfo // 修复用户信息在data.user中
})
} else if (data.token) {
// 兼容旧的单Token格式
console.log('使用旧的单Token格式存储认证信息')
userStore.setToken(data.token)
userStore.setUserInfo(data.user || data.userInfo) // 修复用户信息在data.user中
} else {
throw new Error('响应数据格式不正确缺少token信息')
}
// 保存登录凭据(如果用户选择记住登录)
authService.saveLoginCredentials(values.username, values.remember)
message.success('登录成功')
router.push('/dashboard')
} else {
message.error(response.message || '登录失败')
message.error(response.data?.message || response.message || '登录失败')
}
} catch (error) {
message.error(error.response?.data?.message || '登录失败')
console.error('登录失败:', error)
message.error(error.response?.data?.message || error.message || '登录失败')
} finally {
loading.value = false
}
}
// 页面加载时检查是否有记住的用户名
onMounted(() => {
const rememberedUsername = localStorage.getItem('rememberedUsername')
const rememberLogin = localStorage.getItem('rememberLogin') === 'true'
if (rememberedUsername && rememberLogin) {
formState.username = rememberedUsername
formState.remember = true
}
})
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo)
}

View File

@@ -0,0 +1,802 @@
<template>
<div class="message-notification">
<!-- 页面标题 -->
<div class="page-header">
<h2>消息通知</h2>
<p>设备预警和系统通知管理</p>
</div>
<!-- 统计卡片 -->
<div class="stats-cards">
<a-row :gutter="16">
<a-col :span="6">
<a-card class="stat-card">
<div class="stat-content">
<div class="stat-icon total">
<bell-outlined />
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.total_alerts || 0 }}</div>
<div class="stat-label">总预警数</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<div class="stat-content">
<div class="stat-icon unread">
<exclamation-circle-outlined />
</div>
<div class="stat-info">
<div class="stat-number">{{ stats.unread_alerts || 0 }}</div>
<div class="stat-label">未读预警</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<div class="stat-content">
<div class="stat-icon critical">
<warning-outlined />
</div>
<div class="stat-info">
<div class="stat-number">{{ getCriticalCount() }}</div>
<div class="stat-label">严重预警</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card class="stat-card">
<div class="stat-content">
<div class="stat-icon pending">
<clock-circle-outlined />
</div>
<div class="stat-info">
<div class="stat-number">{{ getPendingCount() }}</div>
<div class="stat-label">待处理</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 筛选和操作栏 -->
<div class="filter-bar">
<a-row :gutter="16" align="middle">
<a-col :span="4">
<a-select
v-model:value="filters.alert_level"
placeholder="预警级别"
allowClear
@change="handleFilterChange"
>
<a-select-option value="info">信息</a-select-option>
<a-select-option value="warning">警告</a-select-option>
<a-select-option value="critical">严重</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="filters.alert_type"
placeholder="预警类型"
allowClear
@change="handleFilterChange"
>
<a-select-option value="temperature">温度异常</a-select-option>
<a-select-option value="humidity">湿度异常</a-select-option>
<a-select-option value="offline">设备离线</a-select-option>
<a-select-option value="maintenance">设备维护</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="filters.status"
placeholder="处理状态"
allowClear
@change="handleFilterChange"
>
<a-select-option value="pending">待处理</a-select-option>
<a-select-option value="processing">处理中</a-select-option>
<a-select-option value="resolved">已解决</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="filters.is_read"
placeholder="阅读状态"
allowClear
@change="handleFilterChange"
>
<a-select-option :value="false">未读</a-select-option>
<a-select-option :value="true">已读</a-select-option>
</a-select>
</a-col>
<a-col :span="8">
<a-space>
<a-button type="primary" @click="markAllAsRead" :disabled="!hasUnreadAlerts">
<check-outlined />
全部标记已读
</a-button>
<a-button @click="refreshData">
<reload-outlined />
刷新
</a-button>
</a-space>
</a-col>
</a-row>
</div>
<!-- 预警列表 -->
<div class="alert-list">
<a-list
:data-source="alerts"
:loading="loading"
:pagination="pagination"
@change="handlePageChange"
>
<template #renderItem="{ item }">
<a-list-item
:class="['alert-item', `alert-${item.alert_level}`, { 'unread': !item.is_read }]"
@click="handleAlertClick(item)"
>
<a-list-item-meta>
<template #avatar>
<a-avatar :class="`avatar-${item.alert_level}`">
<template #icon>
<component :is="getAlertIcon(item.alert_type)" />
</template>
</a-avatar>
</template>
<template #title>
<div class="alert-title">
<span class="title-text">{{ item.alert_title }}</span>
<a-tag :color="getAlertLevelColor(item.alert_level)" class="level-tag">
{{ getAlertLevelText(item.alert_level) }}
</a-tag>
<a-tag :color="getStatusColor(item.status)" class="status-tag">
{{ getStatusText(item.status) }}
</a-tag>
<span v-if="!item.is_read" class="unread-dot"></span>
</div>
</template>
<template #description>
<div class="alert-description">
<p class="alert-content">{{ item.alert_content }}</p>
<div class="alert-meta">
<span class="device-info">
<desktop-outlined />
{{ item.Device?.device_name || '未知设备' }} ({{ item.Device?.device_number || 'N/A' }})
</span>
<span class="location-info">
<environment-outlined />
{{ item.Device?.installation_location || '未知位置' }}
</span>
<span class="time-info">
<clock-circle-outlined />
{{ formatTime(item.alert_time) }}
</span>
</div>
</div>
</template>
</a-list-item-meta>
<template #actions>
<a-space>
<a-button
v-if="!item.is_read"
type="link"
size="small"
@click.stop="markAsRead(item)"
>
标记已读
</a-button>
<a-button
v-if="item.status === 'pending'"
type="link"
size="small"
@click.stop="handleAlert(item)"
>
处理
</a-button>
<a-button
type="link"
size="small"
@click.stop="viewDetail(item)"
>
详情
</a-button>
</a-space>
</template>
</a-list-item>
</template>
</a-list>
</div>
<!-- 预警详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="预警详情"
width="800px"
:footer="null"
>
<div v-if="selectedAlert" class="alert-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="预警标题" :span="2">
{{ selectedAlert.alert_title }}
</a-descriptions-item>
<a-descriptions-item label="预警级别">
<a-tag :color="getAlertLevelColor(selectedAlert.alert_level)">
{{ getAlertLevelText(selectedAlert.alert_level) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="预警类型">
{{ getAlertTypeText(selectedAlert.alert_type) }}
</a-descriptions-item>
<a-descriptions-item label="处理状态">
<a-tag :color="getStatusColor(selectedAlert.status)">
{{ getStatusText(selectedAlert.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="阅读状态">
<a-tag :color="selectedAlert.is_read ? 'green' : 'red'">
{{ selectedAlert.is_read ? '已读' : '未读' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="预警内容" :span="2">
{{ selectedAlert.alert_content }}
</a-descriptions-item>
<a-descriptions-item label="设备名称">
{{ selectedAlert.Device?.device_name || '未知设备' }}
</a-descriptions-item>
<a-descriptions-item label="设备编号">
{{ selectedAlert.Device?.device_number || 'N/A' }}
</a-descriptions-item>
<a-descriptions-item label="安装位置">
{{ selectedAlert.Device?.installation_location || '未知位置' }}
</a-descriptions-item>
<a-descriptions-item label="设备状态">
<a-tag :color="getDeviceStatusColor(selectedAlert.Device?.status)">
{{ getDeviceStatusText(selectedAlert.Device?.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="预警时间">
{{ formatTime(selectedAlert.alert_time) }}
</a-descriptions-item>
<a-descriptions-item label="阅读时间">
{{ selectedAlert.read_time ? formatTime(selectedAlert.read_time) : '未读' }}
</a-descriptions-item>
<a-descriptions-item v-if="selectedAlert.handler_id" label="处理人员">
{{ selectedAlert.handler_id }}
</a-descriptions-item>
<a-descriptions-item v-if="selectedAlert.handle_time" label="处理时间">
{{ formatTime(selectedAlert.handle_time) }}
</a-descriptions-item>
<a-descriptions-item v-if="selectedAlert.handle_note" label="处理备注" :span="2">
{{ selectedAlert.handle_note }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
<!-- 处理预警模态框 -->
<a-modal
v-model:open="handleModalVisible"
title="处理预警"
@ok="submitHandle"
@cancel="handleModalVisible = false"
>
<a-form :model="handleForm" layout="vertical">
<a-form-item label="处理备注" required>
<a-textarea
v-model:value="handleForm.handle_note"
placeholder="请输入处理备注"
:rows="4"
/>
</a-form-item>
<a-form-item label="处理状态">
<a-select v-model:value="handleForm.status">
<a-select-option value="processing">处理中</a-select-option>
<a-select-option value="resolved">已解决</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
BellOutlined,
ExclamationCircleOutlined,
WarningOutlined,
ClockCircleOutlined,
CheckOutlined,
ReloadOutlined,
DesktopOutlined,
EnvironmentOutlined,
FireOutlined,
CloudOutlined,
WifiOutlined,
ToolOutlined
} from '@ant-design/icons-vue'
import { deviceAlertAPI } from '@/utils/api'
import dayjs from 'dayjs'
// 响应式数据
const loading = ref(false)
const alerts = ref([])
const stats = ref({})
const selectedAlert = ref(null)
const detailModalVisible = ref(false)
const handleModalVisible = ref(false)
// 筛选条件
const filters = reactive({
alert_level: undefined,
alert_type: undefined,
status: undefined,
is_read: undefined
})
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 处理表单
const handleForm = reactive({
handle_note: '',
status: 'processing'
})
// 计算属性
const hasUnreadAlerts = computed(() => {
return alerts.value.some(alert => !alert.is_read)
})
// 获取严重预警数量
const getCriticalCount = () => {
if (!stats.value.alerts_by_level) return 0
const critical = stats.value.alerts_by_level.find(item => item.alert_level === 'critical')
return critical ? critical.count : 0
}
// 获取待处理数量
const getPendingCount = () => {
if (!stats.value.alerts_by_status) return 0
const pending = stats.value.alerts_by_status.find(item => item.status === 'pending')
return pending ? pending.count : 0
}
// 获取预警图标
const getAlertIcon = (type) => {
const iconMap = {
temperature: FireOutlined,
humidity: CloudOutlined,
offline: WifiOutlined,
maintenance: ToolOutlined
}
return iconMap[type] || BellOutlined
}
// 获取预警级别颜色
const getAlertLevelColor = (level) => {
const colorMap = {
info: 'blue',
warning: 'orange',
critical: 'red'
}
return colorMap[level] || 'default'
}
// 获取预警级别文本
const getAlertLevelText = (level) => {
const textMap = {
info: '信息',
warning: '警告',
critical: '严重'
}
return textMap[level] || level
}
// 获取状态颜色
const getStatusColor = (status) => {
const colorMap = {
pending: 'red',
processing: 'orange',
resolved: 'green'
}
return colorMap[status] || 'default'
}
// 获取状态文本
const getStatusText = (status) => {
const textMap = {
pending: '待处理',
processing: '处理中',
resolved: '已解决'
}
return textMap[status] || status
}
// 获取预警类型文本
const getAlertTypeText = (type) => {
const textMap = {
temperature: '温度异常',
humidity: '湿度异常',
offline: '设备离线',
maintenance: '设备维护'
}
return textMap[type] || type
}
// 获取设备状态颜色
const getDeviceStatusColor = (status) => {
const colorMap = {
normal: 'green',
warning: 'orange',
error: 'red',
offline: 'gray'
}
return colorMap[status] || 'default'
}
// 获取设备状态文本
const getDeviceStatusText = (status) => {
const textMap = {
normal: '正常',
warning: '警告',
error: '错误',
offline: '离线'
}
return textMap[status] || status
}
// 格式化时间
const formatTime = (time) => {
if (!time) return ''
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
}
// 获取统计数据
const fetchStats = async () => {
try {
const response = await deviceAlertAPI.getStats()
if (response.success) {
stats.value = response.data
}
} catch (error) {
console.error('获取统计数据失败:', error)
}
}
// 获取预警列表
const fetchAlerts = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
limit: pagination.pageSize,
...filters
}
// 移除undefined值
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
delete params[key]
}
})
const response = await deviceAlertAPI.getList(params)
if (response.success) {
alerts.value = response.data.alerts
pagination.total = response.data.total
}
} catch (error) {
console.error('获取预警列表失败:', error)
message.error('获取预警列表失败')
} finally {
loading.value = false
}
}
// 筛选变化处理
const handleFilterChange = () => {
pagination.current = 1
fetchAlerts()
}
// 分页变化处理
const handlePageChange = (page, pageSize) => {
pagination.current = page
pagination.pageSize = pageSize
fetchAlerts()
}
// 预警点击处理
const handleAlertClick = async (alert) => {
if (!alert.is_read) {
await markAsRead(alert)
}
}
// 标记为已读
const markAsRead = async (alert) => {
try {
const response = await deviceAlertAPI.markAsRead(alert.id)
if (response.success) {
alert.is_read = true
alert.read_time = new Date().toISOString()
message.success('已标记为已读')
await fetchStats() // 刷新统计数据
}
} catch (error) {
console.error('标记已读失败:', error)
message.error('标记已读失败')
}
}
// 全部标记已读
const markAllAsRead = async () => {
try {
const response = await deviceAlertAPI.markAllAsRead()
if (response.success) {
alerts.value.forEach(alert => {
alert.is_read = true
alert.read_time = new Date().toISOString()
})
message.success('已全部标记为已读')
await fetchStats() // 刷新统计数据
}
} catch (error) {
console.error('全部标记已读失败:', error)
message.error('全部标记已读失败')
}
}
// 查看详情
const viewDetail = (alert) => {
selectedAlert.value = alert
detailModalVisible.value = true
}
// 处理预警
const handleAlert = (alert) => {
selectedAlert.value = alert
handleForm.handle_note = ''
handleForm.status = 'processing'
handleModalVisible.value = true
}
// 提交处理
const submitHandle = async () => {
if (!handleForm.handle_note.trim()) {
message.error('请输入处理备注')
return
}
try {
const response = await deviceAlertAPI.handle(selectedAlert.value.id, handleForm)
if (response.success) {
message.success('处理成功')
handleModalVisible.value = false
await fetchAlerts()
await fetchStats()
}
} catch (error) {
console.error('处理失败:', error)
message.error('处理失败')
}
}
// 刷新数据
const refreshData = async () => {
await Promise.all([fetchStats(), fetchAlerts()])
message.success('数据已刷新')
}
// 组件挂载时获取数据
onMounted(() => {
fetchStats()
fetchAlerts()
})
</script>
<style scoped>
.message-notification {
padding: 0;
}
.page-header {
margin-bottom: 24px;
}
.page-header h2 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: #262626;
}
.page-header p {
margin: 0;
color: #8c8c8c;
font-size: 14px;
}
.stats-cards {
margin-bottom: 24px;
}
.stat-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-content {
display: flex;
align-items: center;
padding: 8px 0;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-right: 16px;
}
.stat-icon.total {
background: #e6f7ff;
color: #1890ff;
}
.stat-icon.unread {
background: #fff2e8;
color: #fa8c16;
}
.stat-icon.critical {
background: #fff1f0;
color: #f5222d;
}
.stat-icon.pending {
background: #f6ffed;
color: #52c41a;
}
.stat-info {
flex: 1;
}
.stat-number {
font-size: 24px;
font-weight: 600;
color: #262626;
line-height: 1;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #8c8c8c;
}
.filter-bar {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
.alert-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.alert-item {
cursor: pointer;
transition: all 0.3s;
border-left: 4px solid transparent;
}
.alert-item:hover {
background: #fafafa;
}
.alert-item.unread {
background: #f6ffed;
border-left-color: #52c41a;
}
.alert-item.alert-critical {
border-left-color: #f5222d;
}
.alert-item.alert-warning {
border-left-color: #fa8c16;
}
.alert-item.alert-info {
border-left-color: #1890ff;
}
.alert-title {
display: flex;
align-items: center;
gap: 8px;
}
.title-text {
font-weight: 500;
color: #262626;
}
.level-tag,
.status-tag {
font-size: 12px;
}
.unread-dot {
width: 8px;
height: 8px;
background: #f5222d;
border-radius: 50%;
display: inline-block;
}
.alert-description {
margin-top: 8px;
}
.alert-content {
margin: 0 0 8px 0;
color: #595959;
line-height: 1.5;
}
.alert-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #8c8c8c;
}
.alert-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.avatar-critical {
background: #f5222d;
}
.avatar-warning {
background: #fa8c16;
}
.avatar-info {
background: #1890ff;
}
.alert-detail {
margin-top: 16px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,345 @@
<template>
<div class="user-profile">
<a-card title="个人中心" :bordered="false">
<a-row :gutter="24">
<!-- 左侧个人信息 -->
<a-col :span="8">
<a-card title="个人信息" size="small">
<div class="profile-info">
<div class="avatar-section">
<a-avatar :size="80" :src="userInfo.avatar">
<template #icon><UserOutlined /></template>
</a-avatar>
<a-button type="link" @click="showAvatarModal = true">
更换头像
</a-button>
</div>
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label="用户名">
{{ userInfo.username }}
</a-descriptions-item>
<a-descriptions-item label="真实姓名">
{{ userInfo.real_name }}
</a-descriptions-item>
<a-descriptions-item label="邮箱">
{{ userInfo.email }}
</a-descriptions-item>
<a-descriptions-item label="手机号">
{{ userInfo.phone }}
</a-descriptions-item>
<a-descriptions-item label="角色">
<a-tag color="blue">{{ getRoleName(userInfo.role_id) }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="userInfo.status === 'active' ? 'green' : 'red'">
{{ userInfo.status === 'active' ? '正常' : '禁用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="最后登录">
{{ formatDate(userInfo.last_login) }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-card>
</a-col>
<!-- 右侧操作区域 -->
<a-col :span="16">
<a-tabs v-model:activeKey="activeTab">
<a-tab-pane key="edit" tab="编辑资料">
<a-form
:model="editForm"
:rules="rules"
layout="vertical"
@finish="handleUpdateProfile"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="真实姓名" name="real_name">
<a-input v-model:value="editForm.real_name" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="邮箱" name="email">
<a-input v-model:value="editForm.email" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="手机号" name="phone">
<a-input v-model:value="editForm.phone" />
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="updating">
更新资料
</a-button>
<a-button style="margin-left: 8px" @click="resetEditForm">
重置
</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="password" tab="修改密码">
<a-form
:model="passwordForm"
:rules="passwordRules"
layout="vertical"
@finish="handleChangePassword"
>
<a-form-item label="当前密码" name="currentPassword">
<a-input-password v-model:value="passwordForm.currentPassword" />
</a-form-item>
<a-form-item label="新密码" name="newPassword">
<a-input-password v-model:value="passwordForm.newPassword" />
</a-form-item>
<a-form-item label="确认新密码" name="confirmPassword">
<a-input-password v-model:value="passwordForm.confirmPassword" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="changingPassword">
修改密码
</a-button>
<a-button style="margin-left: 8px" @click="resetPasswordForm">
重置
</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-col>
</a-row>
</a-card>
<!-- 头像上传模态框 -->
<a-modal
v-model:open="showAvatarModal"
title="更换头像"
@ok="handleAvatarUpload"
@cancel="showAvatarModal = false"
>
<a-upload
v-model:file-list="avatarFileList"
:before-upload="beforeAvatarUpload"
list-type="picture-card"
:max-count="1"
>
<div v-if="avatarFileList.length < 1">
<PlusOutlined />
<div style="margin-top: 8px">上传头像</div>
</div>
</a-upload>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { UserOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/user'
import { userAPI } from '@/utils/api'
import dayjs from 'dayjs'
const userStore = useUserStore()
// 响应式数据
const activeTab = ref('edit')
const updating = ref(false)
const changingPassword = ref(false)
const showAvatarModal = ref(false)
const avatarFileList = ref([])
// 用户信息
const userInfo = computed(() => userStore.userInfo || {})
// 编辑表单
const editForm = reactive({
real_name: '',
email: '',
phone: ''
})
// 密码表单
const passwordForm = reactive({
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
// 表单验证规则
const rules = {
real_name: [
{ required: true, message: '请输入真实姓名' },
{ min: 2, max: 50, message: '姓名长度在2-50个字符' }
],
email: [
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效的邮箱地址' }
],
phone: [
{ required: true, message: '请输入手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' }
]
}
const passwordRules = {
currentPassword: [
{ required: true, message: '请输入当前密码' }
],
newPassword: [
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码长度至少6位' }
],
confirmPassword: [
{ required: true, message: '请确认新密码' },
{
validator: (rule, value) => {
if (value !== passwordForm.newPassword) {
return Promise.reject('两次输入的密码不一致')
}
return Promise.resolve()
}
}
]
}
// 方法
const getRoleName = (roleId) => {
const roleMap = {
1: '管理员',
2: '代理人',
3: '客户',
4: '审核员'
}
return roleMap[roleId] || '未知'
}
const formatDate = (date) => {
if (!date) return '从未登录'
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
const resetEditForm = () => {
editForm.real_name = userInfo.value.real_name || ''
editForm.email = userInfo.value.email || ''
editForm.phone = userInfo.value.phone || ''
}
const resetPasswordForm = () => {
passwordForm.currentPassword = ''
passwordForm.newPassword = ''
passwordForm.confirmPassword = ''
}
const handleUpdateProfile = async () => {
try {
updating.value = true
await userAPI.updateProfile(editForm)
// 更新本地用户信息
await userStore.fetchUserInfo()
message.success('个人资料更新成功')
} catch (error) {
console.error('更新个人资料失败:', error)
message.error(error.response?.data?.message || '更新失败')
} finally {
updating.value = false
}
}
const handleChangePassword = async () => {
try {
changingPassword.value = true
await userAPI.changePassword(passwordForm)
message.success('密码修改成功')
resetPasswordForm()
} catch (error) {
console.error('修改密码失败:', error)
message.error(error.response?.data?.message || '修改密码失败')
} finally {
changingPassword.value = false
}
}
const beforeAvatarUpload = (file) => {
const isImage = file.type.startsWith('image/')
if (!isImage) {
message.error('只能上传图片文件')
return false
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('图片大小不能超过2MB')
return false
}
return false // 阻止自动上传
}
const handleAvatarUpload = async () => {
if (avatarFileList.value.length === 0) {
message.error('请选择头像文件')
return
}
try {
const formData = new FormData()
formData.append('avatar', avatarFileList.value[0].originFileObj)
await userAPI.uploadAvatar(formData)
await userStore.fetchUserInfo()
message.success('头像更新成功')
showAvatarModal.value = false
avatarFileList.value = []
} catch (error) {
console.error('头像上传失败:', error)
message.error(error.response?.data?.message || '头像上传失败')
}
}
// 初始化
onMounted(async () => {
try {
// 获取最新的用户信息
await userStore.fetchUserInfo()
resetEditForm()
} catch (error) {
console.error('获取用户信息失败:', error)
// 如果获取失败,仍然使用本地存储的用户信息
resetEditForm()
}
})
</script>
<style scoped>
.user-profile {
padding: 24px;
}
.profile-info {
text-align: center;
}
.avatar-section {
margin-bottom: 24px;
}
.avatar-section .ant-btn {
display: block;
margin: 8px auto 0;
}
</style>

View File

@@ -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',

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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('刷新令牌失败'));
}
};

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
});
}
};

View File

@@ -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();

View File

@@ -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
};

View File

@@ -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();

View File

@@ -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}`);
});

View File

@@ -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();

View File

@@ -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: 数据仓库相关接口,提供业务数据的统计分析功能

View File

@@ -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();

View File

@@ -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'));
}
}
};

View File

@@ -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;

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
};

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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();