完善保险端前后端
This commit is contained in:
@@ -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不支持&&操作符,请使用;符号
|
||||
BIN
insurance_admin-system/debug_menu.js
Normal file
BIN
insurance_admin-system/debug_menu.js
Normal file
Binary file not shown.
199
insurance_admin-system/public/set-token.html
Normal file
199
insurance_admin-system/public/set-token.html
Normal 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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
232
insurance_admin-system/src/services/authService.js
Normal file
232
insurance_admin-system/src/services/authService.js
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
199
insurance_admin-system/src/utils/request.js
Normal file
199
insurance_admin-system/src/utils/request.js
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
802
insurance_admin-system/src/views/MessageNotification.vue
Normal file
802
insurance_admin-system/src/views/MessageNotification.vue
Normal 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
345
insurance_admin-system/src/views/UserProfile.vue
Normal file
345
insurance_admin-system/src/views/UserProfile.vue
Normal 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>
|
||||
@@ -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',
|
||||
|
||||
55
insurance_backend/check_database.js
Normal file
55
insurance_backend/check_database.js
Normal 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();
|
||||
110
insurance_backend/check_frontend_storage.js
Normal file
110
insurance_backend/check_frontend_storage.js
Normal 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();
|
||||
29
insurance_backend/check_menus.js
Normal file
29
insurance_backend/check_menus.js
Normal 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();
|
||||
37
insurance_backend/check_table_structure.js
Normal file
37
insurance_backend/check_table_structure.js
Normal 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();
|
||||
61
insurance_backend/check_users.js
Normal file
61
insurance_backend/check_users.js
Normal 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();
|
||||
@@ -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('刷新令牌失败'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
421
insurance_backend/controllers/deviceAlertController.js
Normal file
421
insurance_backend/controllers/deviceAlertController.js
Normal 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;
|
||||
435
insurance_backend/controllers/deviceController.js
Normal file
435
insurance_backend/controllers/deviceController.js
Normal 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;
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
344
insurance_backend/controllers/operationLogController.js
Normal file
344
insurance_backend/controllers/operationLogController.js
Normal 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();
|
||||
@@ -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
|
||||
};
|
||||
112
insurance_backend/create_missing_tables.js
Normal file
112
insurance_backend/create_missing_tables.js
Normal 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();
|
||||
54
insurance_backend/debug-api.js
Normal file
54
insurance_backend/debug-api.js
Normal 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}`);
|
||||
});
|
||||
77
insurance_backend/debug_frontend_token.js
Normal file
77
insurance_backend/debug_frontend_token.js
Normal 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();
|
||||
376
insurance_backend/docs/data-warehouse-api.yaml
Normal file
376
insurance_backend/docs/data-warehouse-api.yaml
Normal 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: 数据仓库相关接口,提供业务数据的统计分析功能
|
||||
160
insurance_backend/generate_test_data.js
Normal file
160
insurance_backend/generate_test_data.js
Normal 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();
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
302
insurance_backend/middleware/operationLogger.js
Normal file
302
insurance_backend/middleware/operationLogger.js
Normal 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;
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
86
insurance_backend/models/Device.js
Normal file
86
insurance_backend/models/Device.js
Normal 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;
|
||||
114
insurance_backend/models/DeviceAlert.js
Normal file
114
insurance_backend/models/DeviceAlert.js
Normal 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;
|
||||
270
insurance_backend/models/OperationLog.js
Normal file
270
insurance_backend/models/OperationLog.js
Normal 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;
|
||||
@@ -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
|
||||
};
|
||||
1626
insurance_backend/package-lock.json
generated
1626
insurance_backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
485
insurance_backend/routes/deviceAlerts.js
Normal file
485
insurance_backend/routes/deviceAlerts.js
Normal 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;
|
||||
467
insurance_backend/routes/devices.js
Normal file
467
insurance_backend/routes/devices.js
Normal 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;
|
||||
319
insurance_backend/routes/operationLogs.js
Normal file
319
insurance_backend/routes/operationLogs.js
Normal 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;
|
||||
@@ -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;
|
||||
43
insurance_backend/scripts/check-table-structure.js
Normal file
43
insurance_backend/scripts/check-table-structure.js
Normal 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;
|
||||
96
insurance_backend/scripts/check_user_permissions.js
Normal file
96
insurance_backend/scripts/check_user_permissions.js
Normal 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();
|
||||
89
insurance_backend/scripts/create-device-tables.js
Normal file
89
insurance_backend/scripts/create-device-tables.js
Normal 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;
|
||||
161
insurance_backend/scripts/test-device-data.js
Normal file
161
insurance_backend/scripts/test-device-data.js
Normal 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;
|
||||
76
insurance_backend/scripts/test_frontend_auth.js
Normal file
76
insurance_backend/scripts/test_frontend_auth.js
Normal 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();
|
||||
@@ -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,
|
||||
|
||||
29
insurance_backend/test-routes.js
Normal file
29
insurance_backend/test-routes.js
Normal 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);
|
||||
}
|
||||
99
insurance_backend/test_browser_behavior.js
Normal file
99
insurance_backend/test_browser_behavior.js
Normal 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();
|
||||
32
insurance_backend/test_menu_api.js
Normal file
32
insurance_backend/test_menu_api.js
Normal 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();
|
||||
Reference in New Issue
Block a user