chore(database): remove outdated database files and docs

删除了不再使用的数据库初始化脚本、权限补丁SQL以及完整的数据库文档文件,清理冗余的旧数据库相关文件。
This commit is contained in:
andyesfly committed 2026-08-19 10:47:52 +08:00
1 parent 2534723842
commit 98c22f6afb
6 files changed
+3367 -522

No files matched your search

-277
View File
@@ -1,277 +0,0 @@
# 智能电网物联网设备后台管理系统 - 数据库文档
## 数据库基本信息
- **数据库名称**: `smart_grid_iot`
- **字符集**: `utf8mb4` (支持完整Unicode,包括emoji表情)
- **排序规则**: `utf8mb4_unicode_ci`
- **存储引擎**: `InnoDB` (支持事务、外键约束、行级锁)
- **MySQL版本要求**: 5.7+ / 8.0+ (推荐8.0+)
## 表清单
### 核心业务表(6张)
| 序号 | 表名 | 中文名称 | 说明 |
|------|------|----------|------|
| 1 | `users` | 用户表 | 存储系统用户信息,包括管理员和普通用户 |
| 2 | `roles` | 角色表 | 存储系统角色信息,用于RBAC权限控制 |
| 3 | `permissions` | 权限表 | 存储权限信息,支持菜单、按钮、API三种类型,支持树形结构 |
| 4 | `user_roles` | 用户角色关联表 | 用户与角色的多对多关联关系 |
| 5 | `role_permissions` | 角色权限关联表 | 角色与权限的多对多关联关系 |
| 6 | `operation_logs` | 操作日志表 | 记录用户操作行为,用于审计和安全追踪 |
## 表结构详细说明
### 1. users (用户表)
**用途**: 存储系统所有用户的基本信息和认证信息
**主要字段**:
- `id`: 主键,自增bigint
- `username`: 用户名,唯一索引
- `password`: bcrypt加密密码
- `nickname`: 昵称/显示名称
- `email`: 邮箱地址
- `phone`: 手机号码
- `avatar`: 头像URL
- `status`: 账户状态(0-禁用, 1-启用)
- `last_login_at`: 最后登录时间
- `last_login_ip`: 最后登录IP(支持IPv6)
**索引**:
- PRIMARY KEY (`id`)
- UNIQUE KEY `uk_username` (`username`)
- KEY `idx_status` (`status`) - 按状态筛选用户
- KEY `idx_created_at` (`created_at`) - 按时间排序
### 2. roles (角色表)
**用途**: 定义系统中的角色,实现基于角色的访问控制(RBAC)
**主要字段**:
- `id`: 主键
- `name`: 角色显示名称
- `code`: 角色编码(唯一标识符,如 super_admin)
- `description`: 角色描述
- `status`: 启用状态
- `sort_order`: 排序权重
**初始角色**:
1. **超级管理员** (super_admin) - 拥有系统所有权限
2. **普通管理员** (admin) - 拥有大部分管理权限
3. **运维人员** (operator) - 拥有设备管理和查看权限
### 3. permissions (权限表)
**用途**: 定义系统中的细粒度权限,支持树形菜单结构
**权限类型**:
- `menu`: 菜单权限(对应前端路由页面)
- `button`: 按钮权限(控制界面按钮显隐)
- `api`: API接口权限(控制后端接口访问)
**主要字段**:
- `parent_id`: 父权限ID(NULL表示一级菜单)
- `route`: 前端路由路径
- `component`: Vue组件路径
- `icon`: 菜单图标
**初始权限结构**:
```
├── 仪表盘 (dashboard)
├── 系统管理 (system)
│ ├── 用户管理 (user:list)
│ │ ├── 新增用户 (user:create)
│ │ ├── 编辑用户 (user:update)
│ │ ├── 删除用户 (user:delete)
│ │ └── 重置密码 (user:resetPwd)
│ └── 角色管理 (role:list)
│ ├── 新增角色 (role:create)
│ ├── 编辑角色 (role:update)
│ ├── 删除角色 (role:delete)
│ └── 分配权限 (role:assignPermission)
├── 系统设置 (settings)
│ ├── 基本设置 (system:basicSetting)
│ └── 安全设置 (system:securitySetting)
└── 操作日志 (log:operation)
```
### 4. user_roles (用户角色关联表)
**用途**: 实现用户与角色的多对多关系
**特点**:
- 联合唯一索引 `(user_id, role_id)` 防止重复分配
- 支持一个用户拥有多个角色
- 支持一个角色分配给多个用户
### 5. role_permissions (角色权限关联表)
**用途**: 实现角色与权限的多对多关系
**特点**:
- 联合唯一索引 `(role_id, permission_id)` 防止重复分配
- 通过批量授权提高效率
- 支持灵活的权限组合
### 6. operation_logs (操作日志表)
**用途**: 记录系统的操作日志,用于安全审计和问题追踪
**记录内容**:
- 操作人信息(user_id, username)
- 操作详情(action, module, method, url)
- 网络信息(ip_address, user_agent)
- 请求数据(request_params - JSON)
- 响应数据(response_data - JSON)
- 性能指标(duration - 毫秒)
- 错误信息(error_message - 仅失败时记录)
**索引优化**:
- `idx_user_id`: 按用户查询日志
- `idx_username`: 按用户名查询
- `idx_module`: 按模块筛选
- `idx_created_at`: 按时间范围查询
## 初始账户信息
### 管理员账户
- **用户名**: `admin`
- **密码**: `admin123456`
- **角色**: 超级管理员
- **权限**: 拥有系统所有权限
⚠️ **重要提示**: 首次登录后请立即修改默认密码!
## 如何执行初始化脚本
### 方式一: MySQL命令行客户端
```bash
# 登录MySQL
mysql -u root -p
# 执行初始化脚本
source /path/to/database/init.sql;
# 或者一行命令
mysql -u root -p < /path/to/database/init.sql
```
### 方式二: Navicat/DBeaver等GUI工具
1. 打开数据库连接工具
2. 新建查询窗口
3. 复制 init.sql 的全部内容
4. 执行SQL脚本(F5或点击执行按钮)
### 方式三: Node.js程序化执行
```javascript
const mysql = require('mysql2/promise');
const fs = require('fs');
const path = require('path');
async function initDatabase() {
const connection = await mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'your_password'
});
const sql = fs.readFileSync(
path.join(__dirname, 'database/init.sql'),
'utf-8'
);
await connection.query(sql);
console.log('数据库初始化完成!');
await connection.end();
}
initDatabase().catch(console.error);
```
## 注意事项
### 🔐 安全相关
1. **修改默认密码**: 首次登录后立即修改admin账户的默认密码
2. **密码加密**: 所有密码使用bcrypt加密存储(saltRounds=10)
3. **敏感数据**: operation_logs表可能包含敏感信息,注意访问控制
4. **备份策略**: 定期备份数据库,建议每日自动备份
### 📊 性能优化建议
1. **索引使用**: 已为常用查询字段建立索引,避免全表扫描
2. **日志清理**: operation_logs表会持续增长,建议定期归档或清理历史数据(如保留90天)
3. **连接池**: 应用层使用连接池管理数据库连接
4. **查询优化**: 避免SELECT *,只查询需要的字段
### 🔧 维护操作
```sql
-- 查看表结构和索引
SHOW CREATE TABLE users;
SHOW INDEX FROM users;
-- 查看数据量
SELECT
table_name,
table_rows,
data_length/1024/1024 AS size_mb
FROM information_schema.tables
WHERE table_schema = 'smart_grid_iot';
-- 清理90天前的操作日志(谨慎执行!)
DELETE FROM operation_logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY);
```
### 📝 字符集说明
- 使用 **utf8mb4** 而非 utf8,因为:
- utf8mb4支持完整的Unicode字符集(包括emoji)
- utf8在MySQL中实际是utf8mb3,最多3字节
- 某些特殊字符(如💡🔧)需要4字节编码
### 🚀 版本兼容性
- **MySQL 5.7+**: 完全兼容
- **MySQL 8.0+**: 推荐,支持更多特性(如CTE、窗口函数)
- **MariaDB 10.3+**: 基本兼容,但部分语法可能有差异
## ER关系图
```
users (1) ─────< (N) user_roles (N) >───── (1) roles (1) ─────< (N) role_permissions (N) >───── (1) permissions
│
├─ type: menu (菜单)
├─ type: button (按钮)
└─ type: api (接口)
users (1) ─────────────────────────────────────────────────────────────────────────────────────> (N) operation_logs
```
## 技术栈对接
### 后端 (Node.js + Sequelize)
实体文件位置: `server/src/entity/`
- [user.entity.ts](../server/src/entity/user.entity.ts) - User模型
- [role.entity.ts](../server/src/entity/role.entity.ts) - Role模型
- [permission.entity.ts](../server/src/entity/permission.entity.ts) - Permission模型
- [userRole.entity.ts](../server/src/entity/userRole.entity.ts) - UserRole关联模型
- [rolePermission.entity.ts](../server/src/entity/rolePermission.entity.ts) - RolePermission关联模型
- [operationLog.entity.ts](../server/src/entity/operationLog.entity.ts) - OperationLog模型
### 配置文件
数据库连接配置: `server/src/config/config.default.ts` 或 `config.local.ts`
---
**最后更新**: 2026-05-27
**维护者**: 开发团队
+1387
View File
File diff suppressed because it is too large. Load diff
-28
View File
@@ -1,28 +0,0 @@
-- ============================================================
-- 普通管理员权限补丁 (用于已有数据库)
-- 执行方式: 在MySQL中执行此SQL即可
-- ============================================================
USE smart_grid_iot;
-- 给普通管理员角色(role_id=2, code='admin')分配基础管理权限
-- 先删除已存在的(避免重复), 再插入
DELETE FROM `role_permissions` WHERE `role_id` = 2;
INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES
(2, (SELECT id FROM permissions WHERE code = 'dashboard')),
(2, (SELECT id FROM permissions WHERE code = 'system')),
(2, (SELECT id FROM permissions WHERE code = 'settings')),
(2, (SELECT id FROM permissions WHERE code = 'user:list')),
(2, (SELECT id FROM permissions WHERE code = 'user:create')),
(2, (SELECT id FROM permissions WHERE code = 'user:update')),
(2, (SELECT id FROM permissions WHERE code = 'role:list')),
(2, (SELECT id FROM permissions WHERE code = 'system:basicSetting')),
(2, (SELECT id FROM permissions WHERE code = 'system:securitySetting')),
(2, (SELECT id FROM permissions WHERE code = 'log:operation'));
-- 验证结果
SELECT r.name AS 角色名称, r.code AS 角色编码, COUNT(rp.permission_id) AS 权限数量
FROM roles r
LEFT JOIN role_permissions rp ON r.id = rp.role_id
WHERE r.id = 2
GROUP BY r.id;
-217
View File
@@ -1,217 +0,0 @@
-- ============================================================
-- 智能电网物联网设备后台管理系统 - 数据库初始化脚本
-- 数据库名称: smart_grid_iot
-- 字符集: utf8mb4 (支持完整Unicode,包括emoji)
-- 排序规则: utf8mb4_unicode_ci
-- 引擎: InnoDB (支持事务、外键约束)
-- MySQL版本要求: 5.7+ / 8.0+
-- 创建时间: 2026-05-27
-- ============================================================
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS smart_grid_iot
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE smart_grid_iot;
-- ============================================================
-- 表1: users (用户表)
-- 存储系统用户信息,包括管理员和普通用户
-- ============================================================
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '加密密码(bcrypt)',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态: 0-禁用 1-启用',
`last_login_at` datetime DEFAULT NULL COMMENT '最后登录时间',
`last_login_ip` varchar(45) DEFAULT NULL COMMENT '最后登录IP(支持IPv6)',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_status` (`status`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
-- ============================================================
-- 表2: roles (角色表)
-- 存储系统角色信息,用于RBAC权限控制
-- ============================================================
CREATE TABLE `roles` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) NOT NULL COMMENT '角色名称',
`code` varchar(50) NOT NULL COMMENT '角色编码(唯一标识)',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态: 0-禁用 1-启用',
`sort_order` int NOT NULL DEFAULT '0' COMMENT '排序(数值越小越靠前)',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
-- ============================================================
-- 表3: permissions (权限表)
-- 存储系统权限信息,包括菜单、按钮、API三种类型
-- 支持树形结构(通过parent_id实现父子关系)
-- ============================================================
CREATE TABLE `permissions` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) NOT NULL COMMENT '权限名称',
`code` varchar(100) NOT NULL COMMENT '权限编码(唯一标识)',
`type` enum('menu','button','api') NOT NULL DEFAULT 'menu' COMMENT '权限类型: menu-菜单 button-按钮 api-接口',
`parent_id` bigint DEFAULT NULL COMMENT '父权限ID(NULL表示一级菜单)',
`sort_order` int NOT NULL DEFAULT '0' COMMENT '排序(数值越小越靠前)',
`icon` varchar(50) DEFAULT NULL COMMENT '图标(菜单类型使用)',
`route` varchar(255) DEFAULT NULL COMMENT '路由路径(菜单类型使用)',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径(菜单类型使用)',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态: 0-禁用 1-启用',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表';
-- ============================================================
-- 表4: user_roles (用户角色关联表)
-- 用户和角色的多对多关联关系
-- 一个用户可以拥有多个角色,一个角色可以分配给多个用户
-- ============================================================
CREATE TABLE `user_roles` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`),
KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';
-- ============================================================
-- 表5: role_permissions (角色权限关联表)
-- 角色和权限的多对多关联关系
-- 一个角色可以拥有多个权限,一个权限可以分配给多个角色
-- ============================================================
CREATE TABLE `role_permissions` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` bigint NOT NULL COMMENT '角色ID',
`permission_id` bigint NOT NULL COMMENT '权限ID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`),
KEY `idx_permission_id` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色权限关联表';
-- ============================================================
-- 表6: operation_logs (操作日志表)
-- 记录用户的操作行为,用于审计和安全追踪
-- 包含请求详情、响应信息和性能数据
-- ============================================================
CREATE TABLE `operation_logs` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint DEFAULT NULL COMMENT '操作用户ID',
`username` varchar(50) DEFAULT NULL COMMENT '操作人用户名',
`action` varchar(255) NOT NULL COMMENT '操作描述',
`module` varchar(50) DEFAULT NULL COMMENT '操作模块',
`method` varchar(10) DEFAULT NULL COMMENT '请求方法(GET/POST/PUT/DELETE等)',
`url` varchar(500) DEFAULT NULL COMMENT '请求URL',
`ip_address` varchar(45) DEFAULT NULL COMMENT 'IP地址(支持IPv6)',
`user_agent` varchar(500) DEFAULT NULL COMMENT '浏览器UA',
`request_params` text COMMENT '请求参数(JSON格式)',
`response_data` text COMMENT '响应数据(JSON格式)',
`status_code` int DEFAULT NULL COMMENT 'HTTP响应状态码',
`duration` int DEFAULT NULL COMMENT '请求耗时(毫秒)',
`error_message` text COMMENT '错误信息(仅失败时记录)',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_username` (`username`),
KEY `idx_module` (`module`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
-- ============================================================
-- 初始数据插入
-- ============================================================
-- 插入管理员账户
-- 用户名: admin
-- 密码: admin123456 (bcrypt加密, saltRounds=10)
INSERT INTO `users` (`username`, `password`, `nickname`, `avatar`, `status`) VALUES
('admin', '$2a$10$EBHdTgzyUj0ofbwLyEHZ7OifI/aC7j9ZWOnpg9Ha1b/GXSUEAVZVe', '超级管理员', NULL, 1);
-- 插入初始角色
INSERT INTO `roles` (`name`, `code`, `description`, `sort_order`) VALUES
('超级管理员', 'super_admin', '拥有系统所有权限', 1),
('普通管理员', 'admin', '拥有大部分管理权限', 2),
('运维人员', 'operator', '拥有设备管理和查看权限', 3);
-- 插入一级菜单权限
INSERT INTO `permissions` (`name`, `code`, `type`, `parent_id`, `sort_order`, `icon`, `route`, `component`) VALUES
('仪表盘', 'dashboard', 'menu', NULL, 1, 'DashboardOutlined', '/dashboard', 'dashboard/index'),
('系统管理', 'system', 'menu', NULL, 2, 'SettingOutlined', '/system', NULL),
('系统设置', 'settings', 'menu', NULL, 3, 'ToolOutlined', '/settings', NULL);
-- 插入二级菜单权限(系统管理下的子菜单)
INSERT INTO `permissions` (`name`, `code`, `type`, `parent_id`, `sort_order`, `icon`, `route`, `component`) VALUES
('用户管理', 'user:list', 'menu', 2, 1, 'UserOutlined', '/system/user', 'system/user/index'),
('角色管理', 'role:list', 'menu', 2, 2, 'SafetyCertificateOutlined', '/system/role', 'system/role/index');
-- 插入按钮权限(用户管理的操作按钮)
INSERT INTO `permissions` (`name`, `code`, `type`, `parent_id`, `sort_order`) VALUES
('新增用户', 'user:create', 'button', 4, 1),
('编辑用户', 'user:update', 'button', 4, 2),
('删除用户', 'user:delete', 'button', 4, 3),
('重置密码', 'user:resetPwd', 'button', 4, 4);
-- 插入按钮权限(角色管理的操作按钮)
INSERT INTO `permissions` (`name`, `code`, `type`, `parent_id`, `sort_order`) VALUES
('新增角色', 'role:create', 'button', 5, 1),
('编辑角色', 'role:update', 'button', 5, 2),
('删除角色', 'role:delete', 'button', 5, 3),
('分配权限', 'role:assignPermission', 'button', 5, 4);
-- 插入按钮权限(系统设置的操作按钮)
INSERT INTO `permissions` (`name`, `code`, `type`, `parent_id`, `sort_order`) VALUES
('基本设置', 'system:basicSetting', 'button', 3, 1),
('安全设置', 'system:securitySetting', 'button', 3, 2);
-- 插入一级菜单权限(操作日志)
INSERT INTO `permissions` (`name`, `code`, `type`, `parent_id`, `sort_order`, `icon`, `route`, `component`) VALUES
('操作日志', 'log:operation', 'menu', NULL, 4, 'FileTextOutlined', '/log/operation', 'log/operation');
-- 给admin用户分配超级管理员角色(user_id=1, role_id=1)
INSERT INTO `user_roles` (`user_id`, `role_id`) VALUES (1, 1);
-- 给超级管理员角色(role_id=1)分配所有权限
INSERT INTO `role_permissions` (`role_id`, `permission_id`)
SELECT 1, id FROM permissions;
-- 给普通管理员角色(role_id=2)分配基础管理权限(不含删除/重置密码/角色管理等高危操作)
INSERT INTO `role_permissions` (`role_id`, `permission_id`) VALUES
(2, 1), -- dashboard 仪表盘
(2, 2), -- system 系统管理(父菜单)
(2, 3), -- settings 系统设置(父菜单)
(2, 4), -- user:list 用户管理
(2, 6), -- user:create 新增用户
(2, 7), -- user:update 编辑用户
(2, 5), -- role:list 角色管理(仅查看)
(2, 14), -- system:basicSetting 基本设置
(2, 15), -- system:securitySetting 安全设置
(2, 16); -- log:operation 操作日志
-- ============================================================
-- 初始化完成提示
-- ============================================================
SELECT '数据库初始化完成!' AS message;
SELECT CONCAT('管理员账户: admin / admin123456') AS account_info;
SELECT COUNT(*) AS total_tables FROM information_schema.tables WHERE table_schema = 'smart_grid_iot';
File diff suppressed because it is too large. Load diff
+864
View File
@@ -0,0 +1,864 @@
<!-- Generated by Trae Work -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>鑫潼智电系统 · 完整架构设计</title>
<style>
@font-face { font-family:'InstrumentSans'; src:url('./_shared/fonts/InstrumentSans-Regular.ttf') format('truetype'); font-weight:400; font-display:swap; }
@font-face { font-family:'InstrumentSans'; src:url('./_shared/fonts/InstrumentSans-Bold.ttf') format('truetype'); font-weight:700; font-display:swap; }
@font-face { font-family:'JetBrainsMono'; src:url('./_shared/fonts/JetBrainsMono-Regular.ttf') format('truetype'); font-weight:400; font-display:swap; }
@font-face { font-family:'JetBrainsMono'; src:url('./_shared/fonts/JetBrainsMono-Bold.ttf') format('truetype'); font-weight:700; font-display:swap; }
:root{
--bg:#eef3f8; --bg2:#fff; --ink:#0f1e2e; --muted:#5b6b7e; --rule:#dbe4ee;
--accent:#1d6fff; --accent2:#0d9488; --accent3:#7c3aed; --warn:#e0852b; --red:#dc2626;
--shadow:0 4px 14px rgba(0,0,0,.07); --shadow-lg:0 8px 28px rgba(0,0,0,.1); --radius:12px;
/* block diagram palette */
--c-blue:#1d6fff; --c-blue-bg:#e8f1ff; --c-blue-d:#0c4dd1;
--c-green:#0d9488; --c-green-bg:#e6f7f5; --c-green-d:#0a7068;
--c-purple:#7c3aed; --c-purple-bg:#f0eaff; --c-purple-d:#5b21b6;
--c-amber:#e0852b; --c-amber-bg:#fef3e2; --c-amber-d:#b5681a;
--c-slate:#475569; --c-slate-bg:#f1f5f9; --c-slate-d:#334155;
--c-red:#dc2626; --c-red-bg:#fee2e2;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
font-family:'InstrumentSans','PingFang SC','Microsoft YaHei','Hiragino Sans GB',sans-serif;
background:var(--bg);color:var(--ink);line-height:1.7;font-size:15px;-webkit-font-smoothing:antialiased;
}
body::before{
content:"";position:fixed;inset:0;z-index:-1;
background:radial-gradient(900px 600px at 8% 0%,rgba(29,111,255,.07),transparent 60%),
radial-gradient(800px 600px at 92% 6%,rgba(13,148,136,.07),transparent 60%),
radial-gradient(700px 500px at 50% 100%,rgba(124,58,237,.05),transparent 60%);
}
.wrap{max-width:1200px;margin:0 auto;padding:0 24px}
/* ===== HERO ===== */
.hero{padding:56px 0 32px}
.hero .tag{display:inline-flex;align-items:center;gap:8px;font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--accent);background:rgba(29,111,255,.08);padding:6px 14px;border-radius:999px;margin-bottom:16px}
.hero h1{font-size:36px;font-weight:700;letter-spacing:-.01em;line-height:1.2;margin-bottom:12px;background:linear-gradient(135deg,var(--ink),var(--accent));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.hero .sub{font-size:16px;color:var(--muted);max-width:740px}
.hero .meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:22px}
.hero .meta span{font-size:12.5px;color:var(--muted);background:var(--bg2);border:1px solid var(--rule);padding:5px 11px;border-radius:8px}
.hero .meta b{color:var(--ink);font-weight:600}
/* ===== SECTION ===== */
section{padding:40px 0}
.section-head{display:flex;align-items:baseline;gap:14px;margin-bottom:24px}
.section-num{font-family:'JetBrainsMono',monospace;font-size:13px;font-weight:700;color:var(--accent);background:rgba(29,111,255,.08);padding:4px 10px;border-radius:6px;flex-shrink:0}
.section-head h2{font-size:23px;font-weight:700;letter-spacing:-.01em}
.section-head .desc{font-size:13px;color:var(--muted);margin-left:auto}
/* ===== DG: DIAGRAM BLOCK OVERVIEW ===== */
.dg-legend{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-bottom:20px}
.dg-legend .li{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);background:var(--bg2);border:1px solid var(--rule);padding:4px 11px;border-radius:8px}
.dg-legend .lc{width:11px;height:11px;border-radius:3px}
.dg-diagram{background:var(--bg2);border:1px solid var(--rule);border-radius:var(--radius);padding:24px;box-shadow:var(--shadow-lg);overflow-x:auto}
.dg-layer{margin-bottom:10px}
.dg-layer-label{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.dg-ll-tag{font-size:11px;font-weight:700;color:#fff;padding:2px 9px;border-radius:5px;letter-spacing:.05em;flex-shrink:0}
.dg-ll-name{font-size:14px;font-weight:700}
.dg-ll-desc{font-size:12px;color:var(--muted);margin-left:auto}
.dg-conn{text-align:center;color:var(--rule);height:24px;display:flex;justify-content:center;align-items:center}
.dg-blocks{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
.dg-block{flex:1;min-width:145px;max-width:225px;border-radius:10px;padding:11px 13px;border:1.5px solid;transition:transform .12s}
.dg-block:hover{transform:translateY(-2px);box-shadow:var(--shadow-lg)}
.dg-block .dh{display:flex;align-items:center;gap:7px;margin-bottom:5px}
.dg-block .di{width:24px;height:24px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:#fff;flex-shrink:0}
.dg-block .dt{font-size:13.5px;font-weight:700;line-height:1.2}
.dg-block .dx{font-size:11px;font-weight:600;margin-bottom:3px;opacity:.85}
.dg-block ul{list-style:none;font-size:11.5px;color:var(--muted);line-height:1.55}
.dg-block ul li::before{content:"·";font-weight:700;margin-right:4px}
.dg-blue{border-color:var(--c-blue);background:var(--c-blue-bg)} .dg-blue .di{background:var(--c-blue)} .dg-blue .dx{color:var(--c-blue-d)} .dg-blue .dt{color:var(--c-blue-d)}
.dg-green{border-color:var(--c-green);background:var(--c-green-bg)} .dg-green .di{background:var(--c-green)} .dg-green .dx{color:var(--c-green-d)} .dg-green .dt{color:var(--c-green-d)}
.dg-purple{border-color:var(--c-purple);background:var(--c-purple-bg)} .dg-purple .di{background:var(--c-purple)} .dg-purple .dx{color:var(--c-purple-d)} .dg-purple .dt{color:var(--c-purple-d)}
.dg-amber{border-color:var(--c-amber);background:var(--c-amber-bg)} .dg-amber .di{background:var(--c-amber)} .dg-amber .dx{color:var(--c-amber-d)} .dg-amber .dt{color:var(--c-amber-d)}
.dg-slate{border-color:var(--c-slate);background:var(--c-slate-bg)} .dg-slate .di{background:var(--c-slate)} .dg-slate .dx{color:var(--c-slate-d)} .dg-slate .dt{color:var(--c-slate-d)}
.dg-ext{border-style:dashed;border-width:2px}
.dg-gateway{display:flex;align-items:stretch;border-radius:10px;overflow:hidden;border:1.5px solid var(--c-purple)}
.dg-gateway .gi{flex:1;padding:9px 10px;text-align:center;background:var(--c-purple-bg);border-right:1px solid rgba(124,58,237,.15)}
.dg-gateway .gi:last-child{border:none}
.dg-gateway .gi h4{font-size:12.5px;font-weight:700;color:var(--c-purple-d);margin-bottom:1px}
.dg-gateway .gi p{font-size:10.5px;color:var(--muted);line-height:1.4}
.dg-backend{display:grid;grid-template-columns:repeat(5,1fr);gap:7px}
.dg-be{border-radius:8px;padding:9px 7px;text-align:center;border:1.5px solid var(--c-green);background:var(--c-green-bg);transition:transform .12s}
.dg-be:hover{transform:translateY(-2px)}
.dg-be .bi{font-size:17px;margin-bottom:3px}
.dg-be h4{font-size:11.5px;font-weight:700;line-height:1.2;margin-bottom:2px;color:var(--c-green-d)}
.dg-be p{font-size:10px;color:var(--muted);line-height:1.3}
.dg-data{display:grid;grid-template-columns:repeat(4,1fr);gap:9px}
.dg-db{border-radius:10px;padding:11px;border:1.5px solid;text-align:center}
.dg-db .dmi{font-size:20px;margin-bottom:3px}
.dg-db h4{font-size:12.5px;font-weight:700;margin-bottom:2px}
.dg-db p{font-size:10.5px;color:var(--muted);line-height:1.4}
.dg-db.d-amber{border-color:var(--c-amber);background:var(--c-amber-bg)} .dg-db.d-amber h4{color:var(--c-amber-d)}
.dg-db.d-red{border-color:var(--c-red);background:var(--c-red-bg)} .dg-db.d-red h4{color:var(--c-red)}
.dg-db.d-blue{border-color:var(--c-blue);background:var(--c-blue-bg)} .dg-db.d-blue h4{color:var(--c-blue-d)}
.dg-db.d-green{border-color:var(--c-green);background:var(--c-green-bg)} .dg-db.d-green h4{color:var(--c-green-d)}
.dg-perm{margin-top:18px;background:var(--c-red-bg);border:1.5px solid var(--c-red);border-radius:10px;padding:13px 16px}
.dg-perm h4{font-size:14px;font-weight:700;color:var(--c-red);margin-bottom:8px}
.dg-perm .pr{display:flex;gap:12px;flex-wrap:wrap}
.dg-perm .pi{flex:1;min-width:180px;background:var(--bg2);border-radius:8px;padding:9px 11px;border:1px solid rgba(220,38,38,.15)}
.dg-perm .pi .pt{font-size:12px;font-weight:700;color:var(--ink);margin-bottom:2px}
.dg-perm .pi .pd{font-size:11px;color:var(--muted)}
/* ===== DETAILED: LAYERED ARCH ===== */
.arch-stack{display:flex;flex-direction:column;gap:12px}
.alayer{position:relative;background:var(--bg2);border:1px solid var(--rule);border-radius:var(--radius);padding:16px 18px;box-shadow:var(--shadow)}
.alayer-head{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.alayer-tag{font-family:'JetBrainsMono',monospace;font-size:11px;font-weight:700;color:#fff;padding:3px 9px;border-radius:5px;letter-spacing:.05em}
.alayer-name{font-size:15px;font-weight:700}
.alayer-note{font-size:12px;color:var(--muted);margin-left:auto}
.alayer-body{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.anode{border:1px solid var(--rule);border-radius:10px;padding:11px 13px;background:linear-gradient(180deg,#fff,rgba(255,255,255,.6));transition:transform .15s,box-shadow .15s}
.anode:hover{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.06)}
.anode h4{font-size:13.5px;font-weight:700;margin-bottom:3px;display:flex;align-items:center;gap:6px}
.anode h4 .dot{width:7px;height:7px;border-radius:50%;display:inline-block}
.anode p{font-size:12px;color:var(--muted);line-height:1.5}
.anode .tech{font-family:'JetBrainsMono',monospace;font-size:11px;color:var(--accent);margin-top:5px;display:block}
.alayer.client .alayer-tag{background:linear-gradient(135deg,#1d6fff,#3b82f6)} .alayer.client .anode h4 .dot{background:#1d6fff}
.alayer.gw .alayer-tag{background:linear-gradient(135deg,#7c3aed,#8b5cf6)} .alayer.gw .anode h4 .dot{background:#7c3aed}
.alayer.svc .alayer-tag{background:linear-gradient(135deg,#0d9488,#14b8a6)} .alayer.svc .anode h4 .dot{background:#0d9488}
.alayer.data .alayer-tag{background:linear-gradient(135deg,#e0852b,#f59e0b)} .alayer.data .anode h4 .dot{background:#e0852b}
.alayer.ext .alayer-tag{background:linear-gradient(135deg,#475569,#64748b)} .alayer.ext .anode h4 .dot{background:#64748b}
.flow-arrow{text-align:center;color:var(--muted);font-size:16px;line-height:1;margin:-3px 0}
/* ===== MODULE CARDS ===== */
.client-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.client-card{background:var(--bg2);border:1px solid var(--rule);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow)}
.client-card.full{grid-column:1/-1}
.cc-head{padding:14px 18px;color:#fff;display:flex;align-items:center;gap:12px}
.cc-head .icon{width:32px;height:32px;border-radius:8px;background:rgba(255,255,255,.18);display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700}
.cc-head h3{font-size:16px;font-weight:700}
.cc-head .role{font-size:11.5px;opacity:.85;margin-left:auto;background:rgba(255,255,255,.15);padding:3px 10px;border-radius:999px}
.cc-blue .cc-head{background:linear-gradient(135deg,#1d6fff,#2563eb)}
.cc-green .cc-head{background:linear-gradient(135deg,#0d9488,#14b8a6)}
.cc-purple .cc-head{background:linear-gradient(135deg,#7c3aed,#8b5cf6)}
.cc-amber .cc-head{background:linear-gradient(135deg,#e0852b,#f59e0b)}
.cc-slate .cc-head{background:linear-gradient(135deg,#475569,#64748b)}
.cc-body{padding:7px 7px 11px}
.mod-list{display:grid;grid-template-columns:1fr 1fr;gap:7px;padding:5px}
.mod{background:rgba(0,0,0,.02);border:1px solid var(--rule);border-radius:8px;padding:9px 11px}
.mod h4{font-size:12.5px;font-weight:700;margin-bottom:2px}
.mod p{font-size:11px;color:var(--muted);line-height:1.45}
/* ===== TABLE ===== */
.table-wrap{overflow-x:auto;max-height:560px;overflow-y:auto;border:1px solid var(--rule);border-radius:var(--radius);background:var(--bg2)}
table{width:100%;border-collapse:collapse;font-size:13px}
thead th{position:sticky;top:0;background:#f1f5fb;color:var(--ink);font-weight:700;text-align:left;padding:9px 13px;border-bottom:2px solid var(--rule);z-index:1}
tbody td{padding:8px 13px;border-bottom:1px solid var(--rule);vertical-align:top}
tbody tr:hover{background:rgba(29,111,255,.03)}
tbody td code{font-family:'JetBrainsMono',monospace;font-size:12px;color:var(--accent);background:rgba(29,111,255,.06);padding:1px 5px;border-radius:4px}
.pill{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px}
.pill-yes{background:rgba(13,148,136,.12);color:#0d9488}
.pill-no{background:rgba(239,68,68,.1);color:#dc2626}
/* ===== CALLOUT ===== */
.callout{background:var(--bg2);border:1px solid var(--rule);border-left:4px solid var(--accent);border-radius:0 var(--radius) var(--radius) 0;padding:15px 18px;margin:16px 0;box-shadow:var(--shadow)}
.callout.warn{border-left-color:var(--warn)}
.callout h4{font-size:14px;font-weight:700;margin-bottom:5px}
.callout p{font-size:13.5px;color:var(--muted)}
.callout p strong{color:var(--ink)}
/* ===== DB COMPARISON ===== */
.db-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:16px 0}
.db-card{background:var(--bg2);border:2px solid var(--rule);border-radius:var(--radius);padding:18px;box-shadow:var(--shadow);position:relative}
.db-card.rec{border-color:var(--accent2);background:linear-gradient(180deg,rgba(13,148,136,.04),#fff)}
.db-card .badge{position:absolute;top:-11px;right:14px;font-size:11px;font-weight:700;background:var(--accent2);color:#fff;padding:3px 10px;border-radius:999px}
.db-card h3{font-size:17px;font-weight:700;margin-bottom:3px}
.db-card .ver{font-family:'JetBrainsMono',monospace;font-size:12px;color:var(--muted);margin-bottom:10px}
.db-card ul{list-style:none;padding:0}
.db-card li{font-size:12.5px;padding:4px 0;border-bottom:1px dashed var(--rule);display:flex;gap:8px}
.db-card li:last-child{border:none}
.db-card li .ico{flex-shrink:0;font-weight:700}
.db-card.rec li .ico{color:var(--accent2)} .db-card.alt li .ico{color:var(--muted)}
/* ===== MERMAID ===== */
.mermaid{background:var(--bg2);border:1px solid var(--rule);border-radius:var(--radius);padding:22px;text-align:center;margin:12px 0;box-shadow:var(--shadow)}
/* ===== PERMISSION FLOW ===== */
.perm-flow{display:flex;align-items:stretch;gap:0;margin:16px 0;flex-wrap:wrap}
.perm-step{flex:1;min-width:170px;background:var(--bg2);border:1px solid var(--rule);border-radius:var(--radius);padding:13px;margin:0 5px;box-shadow:var(--shadow)}
.perm-step .num{font-family:'JetBrainsMono',monospace;font-size:11px;font-weight:700;color:var(--accent);background:rgba(29,111,255,.08);padding:2px 7px;border-radius:5px;display:inline-block;margin-bottom:7px}
.perm-step h4{font-size:13.5px;font-weight:700;margin-bottom:3px}
.perm-step p{font-size:12px;color:var(--muted)}
.perm-arrow{display:flex;align-items:center;color:var(--accent);font-size:18px;font-weight:700;padding:0 2px}
/* ===== ISOLATION ===== */
.iso-vis{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:16px 0}
.iso-box{background:var(--bg2);border:1px solid var(--rule);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow)}
.iso-box h4{font-size:14.5px;font-weight:700;margin-bottom:8px;display:flex;align-items:center;gap:8px}
.iso-box h4 .tg{font-size:11px;font-weight:700;padding:2px 7px;border-radius:5px}
.iso-box .rows{display:flex;flex-direction:column;gap:5px}
.iso-row{display:flex;align-items:center;gap:8px;font-family:'JetBrainsMono',monospace;font-size:11.5px;padding:6px 10px;border-radius:6px;background:rgba(0,0,0,.02);border:1px solid var(--rule)}
.iso-row .cid{font-weight:700;color:var(--accent)}
.iso-box.business .tg{background:rgba(29,111,255,.12);color:var(--accent)}
.iso-box.timeseries .tg{background:rgba(13,148,136,.12);color:var(--accent2)}
/* ===== FOOTER ===== */
footer{margin-top:36px;padding:24px 0 44px;border-top:1px solid var(--rule)}
footer .ft{font-size:13px;color:var(--muted);text-align:center}
/* ===== RESPONSIVE ===== */
@media(max-width:860px){
.alayer-body{grid-template-columns:1fr 1fr}
.client-grid,.db-grid,.iso-vis{grid-template-columns:1fr}
.perm-flow{flex-direction:column} .perm-arrow{transform:rotate(90deg);justify-content:center;padding:4px 0}
.dg-backend{grid-template-columns:repeat(3,1fr)} .dg-data{grid-template-columns:1fr 1fr}
.hero h1{font-size:27px}
}
@media(max-width:520px){
.alayer-body,.mod-list,.dg-backend,.dg-data{grid-template-columns:1fr}
.dg-block{max-width:none;flex:1 1 100%}
.dg-gateway{flex-direction:column}
.wrap{padding:0 14px}
}
</style>
</head>
<body>
<div class="wrap">
<!-- ===== HERO ===== -->
<header class="hero">
<span class="tag">SYSTEM ARCHITECTURE · V1.0</span>
<h1>鑫潼智电系统 · 实现架构设计</h1>
<p class="sub">基于《鑫潼智电系统需求分析报告 V1.0》,按功能模块拆解为后端、前端、小程序、APP 的完整系统实现架构。覆盖 ICC 平台 API 对接、数据隔离、权限控制等关键设计决策。</p>
<div class="meta">
<span>运营主体:<b>鑫潼科技(单一运营方)</b></span>
<span>后端:<b>Midway.js + TypeORM</b></span>
<span>前端:<b>Vue3 + Ant Design Vue</b></span>
<span>APP/小程序:<b>uni-app</b></span>
<span>数据库:<b>PostgreSQL</b></span>
</div>
</header>
<!-- ===== 01. 架构总览框图 ===== -->
<section id="overview-diagram">
<div class="section-head">
<span class="section-num">01</span>
<h2>架构总览框图</h2>
<span class="desc">五层架构 · 一图全局</span>
</div>
<div class="dg-legend">
<div class="li"><span class="lc" style="background:var(--c-blue)"></span>运营管理平台 Web</div>
<div class="li"><span class="lc" style="background:var(--c-purple)"></span>共享电工 APP</div>
<div class="li"><span class="lc" style="background:var(--c-amber)"></span>客户微信小程序</div>
<div class="li"><span class="lc" style="background:var(--c-slate)"></span>大华 ICC 外部</div>
<div class="li"><span class="lc" style="background:var(--c-green)"></span>后端服务 Midway</div>
<div class="li"><span class="lc" style="background:var(--c-red)"></span>权限控制</div>
</div>
<div class="dg-diagram">
<!-- L1 Client -->
<div class="dg-layer">
<div class="dg-layer-label">
<span class="dg-ll-tag" style="background:var(--c-blue)">L1</span>
<span class="dg-ll-name">客户端层 — 四端入口</span>
<span class="dg-ll-desc">不同角色访问不同端</span>
</div>
<div class="dg-blocks">
<div class="dg-block dg-blue">
<div class="dh"><div class="di">PC</div><div><div class="dt">运营管理平台</div><div class="dx">Vue3 + Ant Design Vue</div></div></div>
<ul><li>用户:业务员 / 运营管理员</li><li>客户/项目/派单/设备管理</li><li>统计报表 / 月度报告</li><li>系统管理(RBAC 菜单权限)</li></ul>
</div>
<div class="dg-block dg-purple">
<div class="dh"><div class="di">APP</div><div><div class="dt">共享电工 APP</div><div class="dx">uni-app → iOS / Android</div></div></div>
<ul><li>用户:共享电工</li><li>五类派单接单处理</li><li>现场水印拍照取证上传</li><li>领料管理 / 报酬查询</li></ul>
</div>
<div class="dg-block dg-amber">
<div class="dh"><div class="di">WX</div><div><div class="dt">客户微信小程序</div><div class="dx">uni-app → 微信小程序</div></div></div>
<ul><li>用户:客户(学校/医院)</li><li>设备状态 / 用电统计查看</li><li>报警信息 / 月度报告下载</li><li>水电报修 / 服务评分</li></ul>
</div>
<div class="dg-block dg-slate dg-ext">
<div class="dh"><div class="di">ICC</div><div><div class="dt">大华 ICC 平台</div><div class="dx">外部系统 · API 对接</div></div></div>
<ul><li>设备数据采集平台(厂家)</li><li>网关/探测器设备管理</li><li>设备运行状态 / 报警</li><li>用电采集数据</li></ul>
</div>
</div>
</div>
<div class="dg-conn">▼</div>
<!-- L2 Gateway -->
<div class="dg-layer">
<div class="dg-layer-label">
<span class="dg-ll-tag" style="background:var(--c-purple)">L2</span>
<span class="dg-ll-name">接入网关层 — 统一入口</span>
<span class="dg-ll-desc">认证 · 路由 · 限流 · 数据权限拦截</span>
</div>
<div class="dg-gateway">
<div class="gi"><h4>统一鉴权</h4><p>JWT 双 Token<br/>多端统一登录态</p></div>
<div class="gi"><h4>API 路由分发</h4><p>/admin · /worker<br/>/customer 分组路由</p></div>
<div class="gi"><h4>接口数据权限</h4><p>Interceptor 注入过滤<br/>给什么数据看什么</p></div>
<div class="gi"><h4>限流与日志</h4><p>rate-limiter<br/>操作审计 winston</p></div>
</div>
</div>
<div class="dg-conn">▼</div>
<!-- L3 Backend -->
<div class="dg-layer">
<div class="dg-layer-label">
<span class="dg-ll-tag" style="background:var(--c-green)">L3</span>
<span class="dg-ll-name">业务服务层 — Midway.js(Node.js + TypeScript + TypeORM)</span>
<span class="dg-ll-desc">10 个核心服务模块</span>
</div>
<div class="dg-backend">
<div class="dg-be"><div class="bi">👥</div><h4>客户管理</h4><p>客户/部门<br/>建筑/房间<br/>账号分配</p></div>
<div class="dg-be"><div class="bi">📋</div><h4>项目管理</h4><p>勘察/方案<br/>合同/安装<br/>验收/回访</p></div>
<div class="dg-be"><div class="bi">📡</div><h4>设备管理</h4><p>ICC 设备同步<br/>线路分配<br/>部门/房间映射</p></div>
<div class="dg-be"><div class="bi">📮</div><h4>派单管理</h4><p>勘察/安装<br/>报警/报修/维保<br/>全生命周期</p></div>
<div class="dg-be"><div class="bi">🔄</div><h4>ICC 数据采集</h4><p>定时拉取<br/>队列缓冲<br/>清洗映射</p></div>
<div class="dg-be"><div class="bi">📊</div><h4>统计报表</h4><p>多维用电统计<br/>月度报告生成<br/>报酬计算</p></div>
<div class="dg-be"><div class="bi">📸</div><h4>取证文件</h4><p>水印拍照取证<br/>合同/验收存档<br/>月报文件</p></div>
<div class="dg-be"><div class="bi">🔔</div><h4>消息通知</h4><p>APP 推送<br/>微信订阅消息<br/>短信(按需)</p></div>
<div class="dg-be"><div class="bi">⚙️</div><h4>系统管理</h4><p>用户/角色/菜单<br/>数据字典<br/>日志审计</p></div>
<div class="dg-be"><div class="bi">🔐</div><h4>认证授权</h4><p>JWT 签发校验<br/>微信 openid 登录<br/>RBAC 权限</p></div>
</div>
</div>
<div class="dg-conn">▼</div>
<!-- L4 Data -->
<div class="dg-layer">
<div class="dg-layer-label">
<span class="dg-ll-tag" style="background:var(--c-amber)">L4</span>
<span class="dg-ll-name">数据层 — 存储与缓存</span>
<span class="dg-ll-desc">逻辑隔离 + 时序分区 · 不做物理分库</span>
</div>
<div class="dg-data">
<div class="dg-db d-amber"><div class="dmi">🐘</div><h4>PostgreSQL 主库</h4><p>业务数据(customer_id 逻辑隔离)<br/>用电采集数据(按月分区)<br/>TypeORM + 声明式分区</p></div>
<div class="dg-db d-red"><div class="dmi">⚡</div><h4>Redis 缓存</h4><p>JWT 会话 Token<br/>数据字典/热点配置<br/>BullMQ 队列底层</p></div>
<div class="dg-db d-blue"><div class="dmi">📁</div><h4>对象存储</h4><p>取证照片(水印)<br/>合同/验收/方案文件<br/>月度报告文档</p></div>
<div class="dg-db d-green"><div class="dmi">📬</div><h4>BullMQ 消息队列</h4><p>ICC 拉取数据缓冲<br/>报警事件异步处理<br/>基于 Redis 轻量级</p></div>
</div>
</div>
<div class="dg-conn">▼</div>
<!-- L5 External -->
<div class="dg-layer">
<div class="dg-layer-label">
<span class="dg-ll-tag" style="background:var(--c-slate)">L5</span>
<span class="dg-ll-name">外部系统对接层</span>
<span class="dg-ll-desc">仅 API 对接 · 不直接操作硬件</span>
</div>
<div class="dg-blocks">
<div class="dg-block dg-slate dg-ext" style="max-width:380px;flex:2">
<div class="dh"><div class="di">ICC</div><div><div class="dt">大华 ICC OpenAPI</div><div class="dx">HTTP REST · 主动拉取</div></div></div>
<ul><li>客户信息接口 → 每小时/事件触发</li><li>设备信息接口(网关/探测器)→ 每小时</li><li>用电采集数据接口 → 5~15 分钟</li><li>设备报警数据接口 → 1 分钟</li></ul>
</div>
<div class="dg-block dg-green dg-ext">
<div class="dh"><div class="di">WX</div><div><div class="dt">微信开放平台</div><div class="dx">小程序登录 + 订阅消息</div></div></div>
<ul><li>openid 登录授权</li><li>订阅消息推送</li><li>客户账号绑定</li></ul>
</div>
<div class="dg-block dg-amber dg-ext">
<div class="dh"><div class="di">SMS</div><div><div class="dt">短信服务</div><div class="dx">阿里云短信(可选)</div></div></div>
<ul><li>客户初始密码通知</li><li>报警短信提醒</li></ul>
</div>
</div>
</div>
</div>
<!-- Permission overlay -->
<div class="dg-perm">
<h4>🔒 权限控制方案(贯穿 L1 → L3)</h4>
<div class="pr">
<div class="pi"><div class="pt">运营后台(Web)— RBAC 菜单级</div><div class="pd">角色绑定菜单/按钮:运营管理员(全菜单)、业务员(客户/项目/方案/合同/回访)</div></div>
<div class="pi"><div class="pt">共享电工 APP — 接口级数据权限</div><div class="pd">接口按 worker_id 过滤:仅自己接的派单、个人报酬明细,给什么看什么</div></div>
<div class="pi"><div class="pt">客户小程序 — 接口级数据权限</div><div class="pd">接口按 customer_id 过滤:仅本客户设备/用电/报警/月报/报修记录</div></div>
</div>
</div>
</section>
<!-- ===== 02. 总体分层架构(详细) ===== -->
<section id="overview">
<div class="section-head">
<span class="section-num">02</span>
<h2>总体分层架构(详细)</h2>
<span class="desc">五层架构 · 自上而下数据流转</span>
</div>
<div class="arch-stack">
<div class="alayer client">
<div class="alayer-head"><span class="alayer-tag">CLIENT</span><span class="alayer-name">客户端层 · 四端入口</span><span class="alayer-note">不同角色访问不同端,统一鉴权</span></div>
<div class="alayer-body">
<div class="anode"><h4><span class="dot"></span>运营管理平台(Web)</h4><p>鑫潼内部人员使用:业务员、运营管理员</p><span class="tech">Vue3 + Ant Design Vue</span></div>
<div class="anode"><h4><span class="dot"></span>共享电工 APP</h4><p>共享电工接单、现场拍照取证、报酬查询</p><span class="tech">uni-app (iOS / Android)</span></div>
<div class="anode"><h4><span class="dot"></span>客户微信小程序</h4><p>客户查看用电/报警/月报、报修申请、评分</p><span class="tech">uni-app → 微信小程序</span></div>
<div class="anode"><h4><span class="dot"></span>大华 ICC 管理平台</h4><p>设备数据采集平台(厂家后台,非自研)</p><span class="tech">外部系统 · API 对接</span></div>
</div>
</div>
<div class="flow-arrow">▼</div>
<div class="alayer gw">
<div class="alayer-head"><span class="alayer-tag">GATEWAY</span><span class="alayer-name">接入网关层 · 统一入口</span><span class="alayer-note">认证、路由、限流、日志</span></div>
<div class="alayer-body">
<div class="anode"><h4><span class="dot"></span>统一鉴权服务</h4><p>JWT Token 签发与校验,多端统一登录态</p><span class="tech">Midway Guard + JWT</span></div>
<div class="anode"><h4><span class="dot"></span>API 路由分发</h4><p>按端区分路由前缀:/admin · /worker · /customer</p><span class="tech">Midway Controller 分组</span></div>
<div class="anode"><h4><span class="dot"></span>接口级数据权限</h4><p>接口决定可见数据范围,APP/小程序给什么看什么</p><span class="tech">Guard + Interceptor</span></div>
<div class="anode"><h4><span class="dot"></span>限流与日志</h4><p>常规 ERP 并发量,请求日志与操作审计</p><span class="tech">rate-limiter + winston</span></div>
</div>
</div>
<div class="flow-arrow">▼</div>
<div class="alayer svc">
<div class="alayer-head"><span class="alayer-tag">SERVICE</span><span class="alayer-name">业务服务层 · Midway.js 核心模块</span><span class="alayer-note">按业务域划分,模块化服务</span></div>
<div class="alayer-body">
<div class="anode"><h4><span class="dot"></span>客户与项目模块</h4><p>客户/部门/建筑/房间、勘察/方案/合同/验收/回访</p><span class="tech">Customer · Project Service</span></div>
<div class="anode"><h4><span class="dot"></span>运营派单模块</h4><p>报警派单、报修派单、维保派单、安装派单</p><span class="tech">Dispatch Service</span></div>
<div class="anode"><h4><span class="dot"></span>设备与线路模块</h4><p>设备信息同步、线路分配、设备状态管理</p><span class="tech">Device · Line Service</span></div>
<div class="anode"><h4><span class="dot"></span>统计与报表模块</h4><p>多维度用电统计、月度报告、共享电工报酬</p><span class="tech">Report Service</span></div>
</div>
<div class="alayer-body" style="margin-top:10px">
<div class="anode"><h4><span class="dot"></span>ICC 数据采集模块</h4><p>定时拉取设备/报警/用电数据,缓冲写入</p><span class="tech">ICC Sync Service + 定时任务</span></div>
<div class="anode"><h4><span class="dot"></span>取证与文件模块</h4><p>现场拍照取证、合同/验收/方案文件存档</p><span class="tech">File Service + 对象存储</span></div>
<div class="anode"><h4><span class="dot"></span>系统管理模块</h4><p>用户、角色、菜单权限(RBAC)、字典、日志</p><span class="tech">System Service</span></div>
<div class="anode"><h4><span class="dot"></span>消息通知模块</h4><p>报警推送到 APP、派单提醒、微信消息</p><span class="tech">Notify Service</span></div>
</div>
</div>
<div class="flow-arrow">▼</div>
<div class="alayer data">
<div class="alayer-head"><span class="alayer-tag">DATA</span><span class="alayer-name">数据层 · 存储与缓存</span><span class="alayer-note">逻辑隔离 + 时序分区,不做物理分库</span></div>
<div class="alayer-body">
<div class="anode"><h4><span class="dot"></span>PostgreSQL 主库</h4><p>业务数据(逻辑隔离)+ 用电采集数据(按月分区)</p><span class="tech">TypeORM + 声明式分区</span></div>
<div class="anode"><h4><span class="dot"></span>Redis 缓存</h4><p>会话 Token、数据字典、热点配置、限流计数</p><span class="tech">ioredis</span></div>
<div class="anode"><h4><span class="dot"></span>对象存储</h4><p>取证照片、合同/验收/方案电子文件、月度报告</p><span class="tech">MinIO / 阿里云 OSS</span></div>
<div class="anode"><h4><span class="dot"></span>消息队列</h4><p>ICC 拉取数据缓冲、报警事件异步处理</p><span class="tech">BullMQ (Redis 队列)</span></div>
</div>
</div>
<div class="flow-arrow">▼</div>
<div class="alayer ext">
<div class="alayer-head"><span class="alayer-tag">EXTERNAL</span><span class="alayer-name">外部系统对接层</span><span class="alayer-note">仅 API 对接,不直接操作硬件</span></div>
<div class="alayer-body">
<div class="anode"><h4><span class="dot"></span>大华 ICC OpenAPI</h4><p>设备信息、设备状态、报警数据、用电采集数据</p><span class="tech">HTTP REST · 主动拉取</span></div>
<div class="anode"><h4><span class="dot"></span>微信开放平台</h4><p>小程序登录授权(openid)、消息订阅推送</p><span class="tech">微信登录 + 订阅消息</span></div>
<div class="anode"><h4><span class="dot"></span>短信服务(可选)</h4><p>客户账号初始密码、报警短信通知(按需)</p><span class="tech">阿里云短信</span></div>
<div class="anode"><h4><span class="dot"></span>智电终端设备</h4><p>网关/探测器 → ICC 平台采集 → 鑫潼系统拉取</p><span class="tech">不直接对接</span></div>
</div>
</div>
</div>
<div class="callout">
<h4>架构设计要点</h4>
<p>① <strong>不直接对接硬件</strong>:所有设备数据通过大华 ICC 平台 API 拉取,鑫潼系统只做数据消费方。② <strong>四端共享一套后端</strong>:Midway.js 统一提供 /admin、/worker、/customer 三组路由,避免重复开发。③ <strong>ICC 内部虽实时推送,但鑫潼侧采用主动拉取</strong>,通过定时任务按采集频次拉取,进入 BullMQ 队列缓冲后写入分区表。</p>
</div>
</section>
<!-- ===== 03. 技术栈选型 ===== -->
<section id="tech">
<div class="section-head">
<span class="section-num">03</span>
<h2>技术栈选型</h2>
<span class="desc">含数据库选型建议</span>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>层级</th><th>技术选型</th><th>版本/说明</th><th>选型理由</th></tr></thead>
<tbody>
<tr><td>后端框架</td><td><code>Midway.js</code></td><td>Node.js · IoC + TypeORM</td><td>阿里出品,TypeScript 原生支持,装饰器风格,模块化清晰</td></tr>
<tr><td>前端框架</td><td><code>Vue 3 + Ant Design Vue</code></td><td>Composition API</td><td>AntDV 组件丰富,适合中后台 ERP 型管理系统</td></tr>
<tr><td>APP 框架</td><td><code>uni-app</code></td><td>跨 iOS / Android</td><td>一套代码多端发布,共享电工端仅需移动 App</td></tr>
<tr><td>小程序</td><td><code>uni-app → 微信小程序</code></td><td>客户端使用</td><td>复用 uni-app 技术栈,降低学习成本</td></tr>
<tr><td>数据库</td><td><code>PostgreSQL</code></td><td>15+</td><td>时序数据分区成熟、多维聚合分析强、JSONB 灵活(详见下方对比)</td></tr>
<tr><td>ORM</td><td><code>TypeORM</code></td><td>Midway 内置集成</td><td>与 Midway 深度集成,支持 PostgreSQL 分区表</td></tr>
<tr><td>缓存</td><td><code>Redis</code></td><td>7.x</td><td>会话、字典缓存、限流、BullMQ 队列底层</td></tr>
<tr><td>消息队列</td><td><code>BullMQ</code></td><td>基于 Redis</td><td>轻量级,无需额外引入 Kafka,满足常规 ERP 并发</td></tr>
<tr><td>对象存储</td><td><code>MinIO / 阿里云 OSS</code></td><td>—</td><td>取证照片、合同/验收文件、月度报告存档</td></tr>
<tr><td>鉴权</td><td><code>JWT</code></td><td>双 Token 刷新</td><td>多端统一,无状态易扩展</td></tr>
</tbody>
</table>
</div>
<h3 style="margin:24px 0 6px;font-size:17px">数据库选型对比:PostgreSQL vs MySQL</h3>
<p style="color:var(--muted);font-size:13px;margin-bottom:12px">基于本系统的数据特征(时序用电数据 + 多维统计报表 + ICC 灵活设备数据)进行对比。</p>
<div class="db-grid">
<div class="db-card rec">
<span class="badge">推荐</span>
<h3>PostgreSQL</h3>
<div class="ver">v15+ · 开源 · 对象关系型</div>
<ul>
<li><span class="ico">✓</span>声明式分区成熟,用电采集数据按月分区查询性能优</li>
<li><span class="ico">✓</span>窗口函数/CTE/物化视图,多维统计(部门/楼栋/楼层/房间)能力强</li>
<li><span class="ico">✓</span>JSONB 类型存储 ICC 灵活设备数据,支持索引查询</li>
<li><span class="ico">✓</span>Midway + TypeORM 完整支持,迁移成本低</li>
<li><span class="ico">✓</span>行级安全(RLS)可选,未来可做更细数据隔离</li>
</ul>
</div>
<div class="db-card alt">
<h3>MySQL</h3>
<div class="ver">v8.x · 开源 · 关系型</div>
<ul>
<li><span class="ico">△</span>分区功能有但不如 PG 灵活,大表管理偏弱</li>
<li><span class="ico">△</span>聚合分析能力可用,但复杂多维统计不如 PG</li>
<li><span class="ico">△</span>JSON 类型有但查询和索引能力弱于 JSONB</li>
<li><span class="ico">✓</span>国内团队更熟悉,运维资源好找</li>
<li><span class="ico">△</span>月度报告按部门/楼栋统计时性能不如 PG</li>
</ul>
</div>
</div>
<div class="callout">
<h4>结论:推荐 PostgreSQL</h4>
<p>本系统最核心的数据特征是<strong>用电采集数据持续增长(时序)</strong>和<strong>月度报告多维统计</strong>。PostgreSQL 的声明式分区能直接解决您担心的"数据量暴涨"问题——按月分区后,单表数据被切分为多个物理子表,查询只扫描相关分区,性能稳定。配合物化视图预聚合月度统计,即使客户和设备数量增长,报表查询依然高效。</p>
</div>
</section>
<!-- ===== 04. 四端功能模块拆解 ===== -->
<section id="modules">
<div class="section-head">
<span class="section-num">04</span>
<h2>四端功能模块拆解</h2>
<span class="desc">后端 · 前端 · APP · 小程序</span>
</div>
<div class="client-grid" style="margin-bottom:16px">
<div class="client-card full cc-green">
<div class="cc-head"><div class="icon">BE</div><h3>后端服务(Midway.js)</h3><span class="role">四端共享 · 统一 API</span></div>
<div class="cc-body"><div class="mod-list">
<div class="mod"><h4>客户管理服务</h4><p>客户信息(省市县分类)、客户部门、客户访问账号分配</p></div>
<div class="mod"><h4>项目管理服务</h4><p>建筑/房间、勘察派单、智电方案、合同、安装、验收、回访</p></div>
<div class="mod"><h4>设备管理服务</h4><p>ICC 设备信息同步、智电设备/网关/探测器、线路分配(部门/房间)</p></div>
<div class="mod"><h4>派单管理服务</h4><p>勘察/安装/报警/报修/维保五类派单全生命周期管理</p></div>
<div class="mod"><h4>ICC 数据采集服务</h4><p>定时拉取设备/报警/用电数据,队列缓冲,写入分区表</p></div>
<div class="mod"><h4>统计报表服务</h4><p>按部门/楼栋/楼层/房间维度用电统计,月度报告生成,报酬计算</p></div>
<div class="mod"><h4>取证文件服务</h4><p>现场拍照取证(水印)、合同/验收/方案/月报文件存档</p></div>
<div class="mod"><h4>消息通知服务</h4><p>APP 推送(报警/派单)、微信订阅消息、短信(按需)</p></div>
<div class="mod"><h4>系统管理服务</h4><p>用户/角色/菜单(RBAC)、数据字典、操作日志、定时任务</p></div>
<div class="mod"><h4>认证授权服务</h4><p>JWT 签发校验、微信 openid 登录、接口级数据权限拦截</p></div>
</div></div>
</div>
</div>
<div class="client-grid">
<div class="client-card cc-blue">
<div class="cc-head"><div class="icon">PC</div><h3>运营管理平台(Web 前端)</h3><span class="role">业务员 / 运营管理员</span></div>
<div class="cc-body"><div class="mod-list">
<div class="mod"><h4>客户管理</h4><p>客户录入(省市县)、部门、建筑/房间配置、访问账号分配</p></div>
<div class="mod"><h4>项目管理</h4><p>勘察派单、方案制定上传、合同登记、安装派单、验收管理、回访</p></div>
<div class="mod"><h4>运营派单</h4><p>报警派单(从 ICC 拉取报警)、报修派单、维保派单、评分管理</p></div>
<div class="mod"><h4>设备监控</h4><p>设备列表、线路分配(部门/房间)、设备状态、报警查询</p></div>
<div class="mod"><h4>统计报表</h4><p>多维度用电统计、月度报告生成下载、共享电工报酬统计</p></div>
<div class="mod"><h4>系统管理</h4><p>用户/角色/菜单权限配置、数据字典、日志审计</p></div>
</div></div>
</div>
<div class="client-card cc-purple">
<div class="cc-head"><div class="icon">APP</div><h3>共享电工 APP(uni-app)</h3><span class="role">共享电工</span></div>
<div class="cc-body"><div class="mod-list">
<div class="mod"><h4>派单接收</h4><p>勘察/安装/报警/报修/维保五类派单推送与接单确认</p></div>
<div class="mod"><h4>领料管理</h4><p>安装派单自动生成设备/耗材领用单,领料确认</p></div>
<div class="mod"><h4>现场处理</h4><p>配电井/箱位置记录、按标准流程处置、水印拍照取证上传</p></div>
<div class="mod"><h4>任务提交</h4><p>处理完成后照片上传、提交派单、查看历史任务</p></div>
<div class="mod"><h4>报酬查询</h4><p>月度派单完成统计、单笔/月度收入明细查询</p></div>
<div class="mod"><h4>客户回访</h4><p>业务员回访记录上传(拍照+意见)</p></div>
</div></div>
</div>
<div class="client-card cc-amber">
<div class="cc-head"><div class="icon">WX</div><h3>客户微信小程序(uni-app)</h3><span class="role">客户(学校/医院等)</span></div>
<div class="cc-body"><div class="mod-list">
<div class="mod"><h4>设备状态查看</h4><p>所属设备在线状态、线路用电实时数据</p></div>
<div class="mod"><h4>报警信息</h4><p>设备报警记录查询、处理进度跟踪</p></div>
<div class="mod"><h4>用电统计</h4><p>按部门/楼栋/楼层/房间多维度用电查询、节电情况</p></div>
<div class="mod"><h4>月度报告</h4><p>当月用电统计查看、用电及改善报告文件下载</p></div>
<div class="mod"><h4>报修申请</h4><p>水电问题报修提交(描述+拍照)、报修进度查询</p></div>
<div class="mod"><h4>服务评分</h4><p>报修/维保派单完成后对共享电工评分</p></div>
</div></div>
</div>
<div class="client-card cc-slate" style="border-style:dashed">
<div class="cc-head"><div class="icon">ICC</div><h3>大华 ICC 平台(外部)</h3><span class="role">API 对接 · 非自研</span></div>
<div class="cc-body" style="padding:12px"><div class="mod-list">
<div class="mod"><h4>客户信息接口</h4><p>ICC 录入意向客户 → 推送到运营管理平台</p></div>
<div class="mod"><h4>设备信息接口</h4><p>网关/探测器设备数据同步到运营管理平台</p></div>
<div class="mod"><h4>设备数据采集接口</h4><p>用电实时数据按采集频次同步拉取</p></div>
<div class="mod"><h4>设备报警接口</h4><p>设备运行状态、预警类型、报警数值拉取</p></div>
</div></div>
</div>
</div>
</section>
<!-- ===== 05. ICC 对接架构 ===== -->
<section id="icc">
<div class="section-head">
<span class="section-num">05</span>
<h2>ICC 平台对接架构</h2>
<span class="desc">主动拉取 · 队列缓冲 · 分区写入</span>
</div>
<div class="callout warn">
<h4>对接方式说明</h4>
<p>ICC 平台内部是实时推送机制,但<strong>鑫潼系统侧采用主动拉取</strong>(定时任务调 ICC OpenAPI)。ICC 接口文档待研究,当前按通用 REST API 模式设计,后续接入时按实际文档调整鉴权方式(AppKey/Token)和接口字段。</p>
</div>
<figure class="mermaid">
flowchart LR
subgraph ICC ["大华 ICC 平台(外部)"]
A1["客户信息"]
A2["设备信息<br/>网关/探测器"]
A3["用电采集数据<br/>(按频次)"]
A4["设备报警数据"]
end
subgraph SYNC ["鑫潼 ICC 采集服务"]
T1["定时任务调度<br/>不同频次拉取"]
Q1[("BullMQ 队列<br/>数据缓冲")]
P1["数据清洗与映射<br/>设备码 → 线路 → 客户"]
end
subgraph DB ["数据层"]
D1[("业务表<br/>设备/线路/客户")]
D2[("用电采集分区表<br/>按月分区")]
D3[("报警表")]
end
subgraph BIZ ["业务消费"]
B1["报警派单服务<br/>实时推送 APP"]
B2["统计报表服务<br/>多维聚合"]
B3["月度报告生成"]
end
A1 -->|"HTTP API 拉取"| T1
A2 -->|"HTTP API 拉取"| T1
A3 -->|"HTTP API 拉取<br/>高频(如5分钟)"| T1
A4 -->|"HTTP API 拉取<br/>中频(如1分钟)"| T1
T1 --> Q1
Q1 --> P1
P1 --> D1
P1 --> D2
P1 --> D3
D3 --> B1
D2 --> B2
D2 --> B3
style ICC fill:#f1f5f9,stroke:#475569,stroke-width:2px
style SYNC fill:#ecfdf5,stroke:#0d9488,stroke-width:2px
style DB fill:#fffbeb,stroke:#e0852b,stroke-width:2px
style BIZ fill:#eff6ff,stroke:#1d6fff,stroke-width:2px
</figure>
<div class="table-wrap" style="margin-top:16px">
<table>
<thead><tr><th>拉取数据</th><th>拉取频次(建议)</th><th>写入目标</th><th>消费方</th></tr></thead>
<tbody>
<tr><td>客户信息</td><td>事件触发 / 每小时</td><td>业务表</td><td>客户管理模块</td></tr>
<tr><td>设备信息(网关/探测器)</td><td>每小时</td><td>设备表 + 线路表</td><td>设备管理模块</td></tr>
<tr><td>用电采集数据</td><td><code>5~15 分钟</code>(按 ICC 采集频次)</td><td>用电采集分区表(按月)</td><td>统计报表 / 月度报告</td></tr>
<tr><td>设备报警数据</td><td><code>1 分钟</code></td><td>报警表</td><td>报警派单服务 → APP 推送</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ===== 06. 数据隔离方案 ===== -->
<section id="isolation">
<div class="section-head">
<span class="section-num">06</span>
<h2>数据隔离方案</h2>
<span class="desc">逻辑隔离 + 时序分区 · 不做物理分库</span>
</div>
<div class="callout">
<h4>核心决策:是否需要多租户 / 分库?</h4>
<p>鑫潼是<strong>唯一运营主体</strong>,客户(学校/医院)是平台上的数据主体而非独立租户,因此<strong>不需要严格多租户架构</strong>。采用<strong>逻辑隔离(customer_id 行级隔离)+ 时序数据按月分区</strong>的混合方案,兼顾性能与运维成本。</p>
</div>
<div class="iso-vis">
<div class="iso-box business">
<h4>业务数据 · 逻辑隔离 <span class="tg">customer_id</span></h4>
<p style="font-size:12px;color:var(--muted);margin-bottom:8px">客户、合同、派单、设备等业务表数据量有限,通过 customer_id 字段 + 索引隔离。</p>
<div class="rows">
<div class="iso-row"><span class="cid">C001</span> 客户A · 合同 · 派单 · 设备</div>
<div class="iso-row"><span class="cid">C002</span> 客户B · 合同 · 派单 · 设备</div>
<div class="iso-row"><span class="cid">C003</span> 客户C · 合同 · 派单 · 设备</div>
<div class="iso-row" style="opacity:.6"><span class="cid">...</span> 共享同一张表,行级隔离</div>
</div>
</div>
<div class="iso-box timeseries">
<h4>用电采集数据 · 按月分区 <span class="tg">RANGE BY 采集时间</span></h4>
<p style="font-size:12px;color:var(--muted);margin-bottom:8px">数据量增量大头。按采集时间分区,查询只扫描相关月份分区,性能稳定。</p>
<div class="rows">
<div class="iso-row">electricity_2026_05 · 分区子表</div>
<div class="iso-row">electricity_2026_06 · 分区子表</div>
<div class="iso-row">electricity_2026_07 · 分区子表</div>
<div class="iso-row">electricity_2026_08 · 分区子表(当前)</div>
<div class="iso-row" style="opacity:.6">历史分区可归档,不影响热数据查询</div>
</div>
</div>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>方案</th><th>是否采用</th><th>适用数据</th><th>理由</th></tr></thead>
<tbody>
<tr><td>逻辑隔离(customer_id 行级)</td><td><span class="pill pill-yes">采用</span></td><td>业务数据(客户/合同/派单/设备)</td><td>数据量有限,索引隔离足够,支持跨客户运营统计</td></tr>
<tr><td>时序数据按月分区</td><td><span class="pill pill-yes">采用</span></td><td>用电采集数据</td><td>解决数据量暴涨问题,查询只扫相关分区</td></tr>
<tr><td>物理分库(每客户独立库)</td><td><span class="pill pill-no">不采用</span></td><td>—</td><td>运维成本高,跨客户统计困难,无合规强制要求</td></tr>
<tr><td>独立 Schema 隔离</td><td><span class="pill pill-no">不采用</span></td><td>—</td><td>增加复杂度但收益有限,逻辑隔离已满足</td></tr>
<tr><td>严格多租户(Tenant 模型)</td><td><span class="pill pill-no">不采用</span></td><td>—</td><td>鑫潼是单一运营方,客户非独立租户</td></tr>
</tbody>
</table>
</div>
<div class="callout warn">
<h4>关于"数据量暴涨影响性能"的回应</h4>
<p>数据增长的大头是<strong>用电采集数据</strong>(每条线路按采集频次持续写入),而非业务数据。业务数据(客户/合同/派单)即使客户增长到上千家,数据量仍在百万级以内,逻辑隔离 + 索引完全够用。用电采集数据通过<strong>按月分区 + 物化视图预聚合月度统计</strong>,即使积累数年数据,查询也只扫描目标月份分区,性能可控。<strong>不需要物理分库。</strong></p>
</div>
</section>
<!-- ===== 07. 权限控制方案 ===== -->
<section id="permission">
<div class="section-head">
<span class="section-num">07</span>
<h2>权限控制方案</h2>
<span class="desc">后台 RBAC 菜单级 · APP/小程序接口级</span>
</div>
<div class="callout">
<h4>权限设计原则</h4>
<p>现阶段权限粗放管理:<strong>运营后台采用 RBAC 菜单级权限</strong>(角色绑定菜单/按钮);<strong>APP 和小程序的数据权限由接口定义</strong>——接口返回什么数据,端上就只能看到什么数据,不在端上做复杂权限判断。</p>
</div>
<h3 style="margin:18px 0 10px;font-size:16px">权限流转链路</h3>
<div class="perm-flow">
<div class="perm-step"><span class="num">STEP 1</span><h4>用户登录</h4><p>运营后台:账号密码<br/>APP:账号密码<br/>小程序:微信 openid</p></div>
<div class="perm-arrow">→</div>
<div class="perm-step"><span class="num">STEP 2</span><h4>JWT 签发</h4><p>Token 携带:userId、role、customerId<br/>双 Token 刷新</p></div>
<div class="perm-arrow">→</div>
<div class="perm-step"><span class="num">STEP 3</span><h4>菜单权限(后台)</h4><p>RBAC:角色 → 菜单/按钮<br/>后台根据角色渲染菜单</p></div>
<div class="perm-arrow">→</div>
<div class="perm-step"><span class="num">STEP 4</span><h4>接口数据权限</h4><p>接口根据 Token 身份过滤数据<br/>APP/小程序给什么看什么</p></div>
</div>
<h3 style="margin:24px 0 10px;font-size:16px">角色与权限矩阵</h3>
<div class="table-wrap">
<table>
<thead><tr><th>角色</th><th>使用端</th><th>菜单权限(RBAC)</th><th>接口数据权限</th></tr></thead>
<tbody>
<tr><td>运营管理员</td><td>运营管理平台</td><td>全部菜单:客户/项目/派单/设备/报表/系统管理</td><td>可查看所有客户数据,全量派单管理</td></tr>
<tr><td>业务员</td><td>运营管理平台</td><td>客户/项目/方案/合同/回访(按需配置)</td><td>仅自己负责的客户(user_id 过滤)</td></tr>
<tr><td>共享电工</td><td>共享电工 APP</td><td>—(APP 无菜单权限,固定功能)</td><td>仅分配给自己的派单、个人报酬明细</td></tr>
<tr><td>客户</td><td>微信小程序</td><td>—(小程序固定功能页)</td><td>仅本客户设备/用电/报警/月报/报修记录</td></tr>
</tbody>
</table>
</div>
<div class="callout">
<h4>接口级数据权限实现要点</h4>
<p>① <strong>共享电工</strong>:查询派单接口自动按 <code>worker_id = 当前用户</code> 过滤;报酬接口只返回本人数据。② <strong>客户</strong>:查询设备/用电/报警接口自动按 <code>customer_id = 当前客户</code> 过滤;报修只能提交本客户的。③ <strong>业务员</strong>:查询客户/项目接口按 <code>salesman_id = 当前用户</code> 过滤(运营管理员不受限)。通过 Midway Interceptor 统一注入过滤条件,避免每个接口手动处理。</p>
</div>
</section>
<!-- ===== 08. 关键业务数据流 ===== -->
<section id="dataflow">
<div class="section-head">
<span class="section-num">08</span>
<h2>关键业务数据流</h2>
<span class="desc">端到端数据流转</span>
</div>
<figure class="mermaid">
flowchart TB
subgraph PHASE1 ["项目阶段"]
P1["ICC 录入客户"] --> P2["运营平台配置建筑/房间"]
P2 --> P3["APP 勘察派单<br/>现场拍照"]
P3 --> P4["运营平台制定方案"]
P4 --> P5["签订合同"]
P5 --> P6["APP 安装派单<br/>设备配置指向 ICC"]
P6 --> P7["ICC 同步设备信息<br/>到运营平台"]
P7 --> P8["线路分配部门/房间"]
P8 --> P9["分配客户访问账号"]
P9 --> P10["项目验收"]
end
subgraph PHASE2 ["运营阶段"]
O1["ICC 报警数据"] -->|"定时拉取"| O2["运营平台生成报警派单"]
O2 --> O3["APP 推送接单"]
O3 --> O4["现场处置拍照"]
O4 --> O5["运营管理员评分"]
M1["客户小程序报修"] --> M2["运营平台生成报修派单"]
M2 --> M3["APP 接单处理"]
M3 --> M4["客户小程序评分"]
R1["ICC 用电数据"] -->|"定时拉取"| R2["分区表存储"]
R2 --> R3["多维统计聚合"]
R3 --> R4["月度报告生成"]
R4 --> R5["小程序下载查看"]
end
P10 --> O1
P10 --> M1
P10 --> R1
style PHASE1 fill:#eff6ff,stroke:#1d6fff,stroke-width:2px
style PHASE2 fill:#ecfdf5,stroke:#0d9488,stroke-width:2px
</figure>
</section>
<!-- ===== 09. 设计假设清单 ===== -->
<section id="assumptions">
<div class="section-head">
<span class="section-num">09</span>
<h2>设计假设清单</h2>
<span class="desc">已采用的假设 · 如需调整请告知</span>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>假设项</th><th>假设内容</th><th>来源</th></tr></thead>
<tbody>
<tr><td>运营主体</td><td>鑫潼为唯一运营方,不做白标输出</td><td>用户确认</td></tr>
<tr><td>多租户</td><td>不需要严格多租户,采用逻辑隔离</td><td>用户确认 + 架构建议</td></tr>
<tr><td>分库</td><td>不做物理分库,用电数据按月分区</td><td>架构建议(解决数据量问题)</td></tr>
<tr><td>ICC 对接</td><td>主动拉取(定时任务),非 ICC 推送</td><td>用户确认</td></tr>
<tr><td>ICC 接口文档</td><td>待研究,按通用 REST API 模式预留</td><td>用户确认</td></tr>
<tr><td>后台权限</td><td>RBAC 菜单级,现阶段粗放管理</td><td>用户确认</td></tr>
<tr><td>APP/小程序数据权限</td><td>接口定义数据范围,给什么看什么</td><td>用户确认</td></tr>
<tr><td>APP 技术栈</td><td>uni-app 跨 iOS/Android</td><td>用户确认</td></tr>
<tr><td>客户小程序</td><td>微信小程序,运营后台分配账号</td><td>用户确认</td></tr>
<tr><td>子账号</td><td>待定,当前按单账号预留扩展</td><td>用户确认(待定)</td></tr>
<tr><td>电子签章</td><td>不需要,但需取证拍照(水印)</td><td>用户确认</td></tr>
<tr><td>并发量</td><td>常规 ERP 级,无需特殊高并发架构</td><td>用户确认</td></tr>
<tr><td>数据库</td><td>PostgreSQL(架构建议,用户待定)</td><td>架构建议</td></tr>
<tr><td>消息队列</td><td>BullMQ(Redis 基础),不引入 Kafka</td><td>架构建议(匹配并发量)</td></tr>
<tr><td>后端框架</td><td>Midway.js + TypeORM</td><td>用户确认</td></tr>
<tr><td>前端框架</td><td>Vue3 + Ant Design Vue</td><td>用户确认</td></tr>
</tbody>
</table>
</div>
</section>
<footer>
<p class="ft">鑫潼智电系统实现架构设计 · 基于《鑫潼智电系统需求分析报告 V1.0》 · 2026-08</p>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.1/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'default',
securityLevel: 'loose',
flowchart: { htmlLabels: true, curve: 'basis', useMaxWidth: true },
themeVariables: {
primaryColor: '#ffffff', primaryTextColor: '#0f1e2e', primaryBorderColor: '#1d6fff',
lineColor: '#94a3b8', fontSize: '14px',
fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
clusterBkg: '#f8fafc', clusterBorder: '#1d6fff'
}
});
</script>
</body>
</html>