diff --git a/database/README.md b/database/README.md deleted file mode 100644 index 4236037..0000000 --- a/database/README.md +++ /dev/null @@ -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 -**维护者**: 开发团队 diff --git a/database/db_init.sql b/database/db_init.sql new file mode 100644 index 0000000..6a558fd --- /dev/null +++ b/database/db_init.sql @@ -0,0 +1,1387 @@ +-- ============================================================ +-- 鑫潼智电系统 PostgreSQL 数据库构建脚本 V1.0 +-- 基于《鑫潼智电系统需求分析报告 V1.0》+ 数据库设计文档 +-- 数据库: PostgreSQL 15+ +-- 字符集: UTF8 +-- 表数量: 37 张(含 1 张按月分区表) +-- +-- 用法: +-- 1. 先安装 pg_cron 扩展并配置 postgresql.conf(见文件末尾说明) +-- 2. psql -U postgres -d xintong -f db_init.sql +-- +-- 特性: +-- - 幂等执行(DROP IF EXISTS + CREATE) +-- - updated_at 自动更新触发器 +-- - 全部列 COMMENT 注释 +-- - 逻辑隔离索引(customer_id) +-- - 用电采集按月 RANGE 分区 +-- - 自动分区管理(纯 PG 函数 + pg_cron 定时调度) +-- - 月度汇总物化视图(历史数据分析) +-- - 初始角色/菜单/字典数据 +-- ============================================================ + +BEGIN; + +-- 扩展 +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ============================================================ +-- updated_at 自动更新触发器函数 +-- ============================================================ +CREATE OR REPLACE FUNCTION trigger_set_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- ============================================================ +-- 一、系统管理域(7 张表) +-- ============================================================ + +-- 1. 系统用户表 +DROP TABLE IF EXISTS sys_user CASCADE; +CREATE TABLE sys_user ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR(64) NOT NULL UNIQUE, + password_hash VARCHAR(256) NOT NULL, + real_name VARCHAR(64) NOT NULL, + phone VARCHAR(20), + email VARCHAR(128), + status SMALLINT NOT NULL DEFAULT 1, + last_login_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE sys_user IS '系统用户表(鑫潼内部人员:业务员、运营管理员等)'; +COMMENT ON COLUMN sys_user.id IS '自增主键'; +COMMENT ON COLUMN sys_user.username IS '登录用户名'; +COMMENT ON COLUMN sys_user.password_hash IS '密码哈希值'; +COMMENT ON COLUMN sys_user.real_name IS '真实姓名'; +COMMENT ON COLUMN sys_user.phone IS '手机号'; +COMMENT ON COLUMN sys_user.email IS '邮箱'; +COMMENT ON COLUMN sys_user.status IS '状态:1启用 0禁用'; +COMMENT ON COLUMN sys_user.last_login_at IS '最后登录时间'; +COMMENT ON COLUMN sys_user.created_at IS '创建时间'; +COMMENT ON COLUMN sys_user.updated_at IS '更新时间'; +CREATE TRIGGER trg_sys_user_updated_at BEFORE UPDATE ON sys_user + FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); + +-- 2. 角色表 +DROP TABLE IF EXISTS sys_role CASCADE; +CREATE TABLE sys_role ( + id BIGSERIAL PRIMARY KEY, + role_code VARCHAR(32) NOT NULL UNIQUE, + role_name VARCHAR(64) NOT NULL, + description TEXT, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE sys_role IS '角色表'; +COMMENT ON COLUMN sys_role.id IS '自增主键'; +COMMENT ON COLUMN sys_role.role_code IS '角色编码'; +COMMENT ON COLUMN sys_role.role_name IS '角色名称'; +COMMENT ON COLUMN sys_role.description IS '描述'; +COMMENT ON COLUMN sys_role.sort_order IS '排序号'; +COMMENT ON COLUMN sys_role.created_at IS '创建时间'; + +-- 3. 用户角色关联表 +DROP TABLE IF EXISTS sys_user_role CASCADE; +CREATE TABLE sys_user_role ( + user_id BIGINT NOT NULL REFERENCES sys_user(id) ON DELETE CASCADE, + role_id BIGINT NOT NULL REFERENCES sys_role(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, role_id) +); +COMMENT ON TABLE sys_user_role IS '用户角色关联表'; +COMMENT ON COLUMN sys_user_role.user_id IS '用户 ID(→ sys_user)'; +COMMENT ON COLUMN sys_user_role.role_id IS '角色 ID(→ sys_role)'; + +-- 4. 菜单表 +DROP TABLE IF EXISTS sys_menu CASCADE; +CREATE TABLE sys_menu ( + id BIGSERIAL PRIMARY KEY, + parent_id BIGINT REFERENCES sys_menu(id) ON DELETE SET NULL, + menu_name VARCHAR(64) NOT NULL, + menu_path VARCHAR(256), + component VARCHAR(256), + icon VARCHAR(64), + sort_order INT NOT NULL DEFAULT 0, + menu_type SMALLINT NOT NULL DEFAULT 1, + permission VARCHAR(128), + visible SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE sys_menu IS '菜单表(RBAC 菜单级权限)'; +COMMENT ON COLUMN sys_menu.id IS '自增主键'; +COMMENT ON COLUMN sys_menu.parent_id IS '父菜单 ID(自关联)'; +COMMENT ON COLUMN sys_menu.menu_name IS '菜单名称'; +COMMENT ON COLUMN sys_menu.menu_path IS '路由路径'; +COMMENT ON COLUMN sys_menu.component IS '前端组件路径'; +COMMENT ON COLUMN sys_menu.icon IS '图标'; +COMMENT ON COLUMN sys_menu.sort_order IS '排序号'; +COMMENT ON COLUMN sys_menu.menu_type IS '类型:1目录 2菜单 3按钮'; +COMMENT ON COLUMN sys_menu.permission IS '权限标识,如 customer:list'; +COMMENT ON COLUMN sys_menu.visible IS '是否可见:1是 0否'; +COMMENT ON COLUMN sys_menu.created_at IS '创建时间'; + +-- 5. 角色菜单关联表 +DROP TABLE IF EXISTS sys_role_menu CASCADE; +CREATE TABLE sys_role_menu ( + role_id BIGINT NOT NULL REFERENCES sys_role(id) ON DELETE CASCADE, + menu_id BIGINT NOT NULL REFERENCES sys_menu(id) ON DELETE CASCADE, + PRIMARY KEY (role_id, menu_id) +); +COMMENT ON TABLE sys_role_menu IS '角色菜单关联表'; +COMMENT ON COLUMN sys_role_menu.role_id IS '角色 ID(→ sys_role)'; +COMMENT ON COLUMN sys_role_menu.menu_id IS '菜单 ID(→ sys_menu)'; + +-- 6. 数据字典表 +DROP TABLE IF EXISTS sys_dict CASCADE; +CREATE TABLE sys_dict ( + id BIGSERIAL PRIMARY KEY, + dict_type VARCHAR(64) NOT NULL, + dict_label VARCHAR(128) NOT NULL, + dict_value VARCHAR(128) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + status SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE sys_dict IS '数据字典表'; +COMMENT ON COLUMN sys_dict.id IS '自增主键'; +COMMENT ON COLUMN sys_dict.dict_type IS '字典类型'; +COMMENT ON COLUMN sys_dict.dict_label IS '字典标签'; +COMMENT ON COLUMN sys_dict.dict_value IS '字典值'; +COMMENT ON COLUMN sys_dict.sort_order IS '排序号'; +COMMENT ON COLUMN sys_dict.status IS '状态:1启用 0禁用'; +COMMENT ON COLUMN sys_dict.created_at IS '创建时间'; +CREATE INDEX idx_sys_dict_type ON sys_dict (dict_type); + +-- 7. 操作日志表 +DROP TABLE IF EXISTS sys_operation_log CASCADE; +CREATE TABLE sys_operation_log ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT, + module VARCHAR(64), + action VARCHAR(64), + method VARCHAR(256), + params TEXT, + ip VARCHAR(64), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE sys_operation_log IS '操作日志表'; +COMMENT ON COLUMN sys_operation_log.id IS '自增主键'; +COMMENT ON COLUMN sys_operation_log.user_id IS '操作用户 ID'; +COMMENT ON COLUMN sys_operation_log.module IS '操作模块'; +COMMENT ON COLUMN sys_operation_log.action IS '操作动作'; +COMMENT ON COLUMN sys_operation_log.method IS '请求方法'; +COMMENT ON COLUMN sys_operation_log.params IS '请求参数'; +COMMENT ON COLUMN sys_operation_log.ip IS '操作 IP'; +COMMENT ON COLUMN sys_operation_log.created_at IS '操作时间'; +CREATE INDEX idx_oplog_user ON sys_operation_log (user_id); +CREATE INDEX idx_oplog_time ON sys_operation_log (created_at); + +-- ============================================================ +-- 二、客户域(7 张表) +-- ============================================================ + +-- 8. 客户信息表 +DROP TABLE IF EXISTS customer CASCADE; +CREATE TABLE customer ( + id BIGSERIAL PRIMARY KEY, + customer_name VARCHAR(128) NOT NULL, + province VARCHAR(64), + city VARCHAR(64), + district VARCHAR(64), + address VARCHAR(256), + contact_person VARCHAR(64), + contact_phone VARCHAR(20), + postal_code VARCHAR(10), + fax VARCHAR(20), + customer_level VARCHAR(32), + status SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE customer IS '客户信息表'; +COMMENT ON COLUMN customer.id IS '自增主键'; +COMMENT ON COLUMN customer.customer_name IS '客户名称'; +COMMENT ON COLUMN customer.province IS '省'; +COMMENT ON COLUMN customer.city IS '市'; +COMMENT ON COLUMN customer.district IS '区/县'; +COMMENT ON COLUMN customer.address IS '地址'; +COMMENT ON COLUMN customer.contact_person IS '联系人'; +COMMENT ON COLUMN customer.contact_phone IS '电话'; +COMMENT ON COLUMN customer.postal_code IS '邮编'; +COMMENT ON COLUMN customer.fax IS '传真'; +COMMENT ON COLUMN customer.customer_level IS '客户级别(学校/医院等)'; +COMMENT ON COLUMN customer.status IS '状态:1意向 2签约 3运维中 4终止'; +COMMENT ON COLUMN customer.created_at IS '创建时间'; +COMMENT ON COLUMN customer.updated_at IS '更新时间'; +CREATE INDEX idx_customer_name ON customer (customer_name); +CREATE INDEX idx_customer_region ON customer (province, city, district); +CREATE TRIGGER trg_customer_updated_at BEFORE UPDATE ON customer + FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); + +-- 9. 客户部门表 +DROP TABLE IF EXISTS customer_department CASCADE; +CREATE TABLE customer_department ( + id BIGSERIAL PRIMARY KEY, + customer_id BIGINT NOT NULL REFERENCES customer(id), + dept_name VARCHAR(128) NOT NULL, + contact_person VARCHAR(64), + contact_phone VARCHAR(20), + function_desc TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE customer_department IS '客户部门信息表'; +COMMENT ON COLUMN customer_department.id IS '自增主键'; +COMMENT ON COLUMN customer_department.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN customer_department.dept_name IS '部门名称'; +COMMENT ON COLUMN customer_department.contact_person IS '联系人'; +COMMENT ON COLUMN customer_department.contact_phone IS '电话'; +COMMENT ON COLUMN customer_department.function_desc IS '职能'; +COMMENT ON COLUMN customer_department.created_at IS '创建时间'; +CREATE INDEX idx_cust_dept_customer ON customer_department (customer_id); + +-- 10. 客户拜访信息表 +DROP TABLE IF EXISTS customer_visit CASCADE; +CREATE TABLE customer_visit ( + id BIGSERIAL PRIMARY KEY, + visit_no VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + visit_person VARCHAR(64) NOT NULL, + visit_phone VARCHAR(20), + visit_content TEXT, + visit_time DATE, + salesman_id BIGINT, + will_plan SMALLINT DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE customer_visit IS '客户拜访信息表'; +COMMENT ON COLUMN customer_visit.id IS '自增主键'; +COMMENT ON COLUMN customer_visit.visit_no IS '拜访单号'; +COMMENT ON COLUMN customer_visit.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN customer_visit.visit_person IS '拜访人'; +COMMENT ON COLUMN customer_visit.visit_phone IS '拜访人电话'; +COMMENT ON COLUMN customer_visit.visit_content IS '拜访内容'; +COMMENT ON COLUMN customer_visit.visit_time IS '拜访时间'; +COMMENT ON COLUMN customer_visit.salesman_id IS '业务员'; +COMMENT ON COLUMN customer_visit.will_plan IS '是否做方案:0不同意 1同意'; +COMMENT ON COLUMN customer_visit.created_at IS '创建时间'; +CREATE INDEX idx_visit_customer ON customer_visit (customer_id); +CREATE INDEX idx_visit_salesman ON customer_visit (salesman_id); + +-- 11. 客户智电改造建筑信息表 +DROP TABLE IF EXISTS customer_building CASCADE; +CREATE TABLE customer_building ( + id BIGSERIAL PRIMARY KEY, + customer_id BIGINT NOT NULL REFERENCES customer(id), + building_name VARCHAR(128) NOT NULL, + basic_info TEXT, + purpose VARCHAR(128), + floor_count INT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE customer_building IS '客户智电改造建筑信息表'; +COMMENT ON COLUMN customer_building.id IS '自增主键'; +COMMENT ON COLUMN customer_building.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN customer_building.building_name IS '建筑名称'; +COMMENT ON COLUMN customer_building.basic_info IS '基本情况'; +COMMENT ON COLUMN customer_building.purpose IS '用途'; +COMMENT ON COLUMN customer_building.floor_count IS '层数'; +COMMENT ON COLUMN customer_building.created_at IS '创建时间'; +CREATE INDEX idx_building_customer ON customer_building (customer_id); + +-- 12. 客户建筑房间信息表 +DROP TABLE IF EXISTS customer_room CASCADE; +CREATE TABLE customer_room ( + id BIGSERIAL PRIMARY KEY, + room_code VARCHAR(64) NOT NULL, + customer_id BIGINT NOT NULL REFERENCES customer(id), + room_name VARCHAR(128) NOT NULL, + building_id BIGINT NOT NULL REFERENCES customer_building(id), + floor_no VARCHAR(16), + room_purpose VARCHAR(128), + dept_id BIGINT REFERENCES customer_department(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE customer_room IS '客户建筑房间信息表'; +COMMENT ON COLUMN customer_room.id IS '自增主键'; +COMMENT ON COLUMN customer_room.room_code IS '房间编码'; +COMMENT ON COLUMN customer_room.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN customer_room.room_name IS '房间名称'; +COMMENT ON COLUMN customer_room.building_id IS '所属建筑 ID(→ customer_building)'; +COMMENT ON COLUMN customer_room.floor_no IS '所在楼层'; +COMMENT ON COLUMN customer_room.room_purpose IS '房间用途'; +COMMENT ON COLUMN customer_room.dept_id IS '房间所属客户部门(→ customer_department)'; +COMMENT ON COLUMN customer_room.created_at IS '创建时间'; +CREATE INDEX idx_room_customer ON customer_room (customer_id); +CREATE INDEX idx_room_building ON customer_room (building_id); + +-- 13. 客户设备信息表(配电间、配电井、配电箱) +DROP TABLE IF EXISTS customer_equipment CASCADE; +CREATE TABLE customer_equipment ( + id BIGSERIAL PRIMARY KEY, + equip_code VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + equip_name VARCHAR(128) NOT NULL, + equip_type VARCHAR(32) NOT NULL, + building_id BIGINT REFERENCES customer_building(id), + floor_no VARCHAR(16), + location VARCHAR(256), + description TEXT, + remark TEXT, + photo_url VARCHAR(512), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE customer_equipment IS '客户智电改造设备信息表(配电间/井/箱)'; +COMMENT ON COLUMN customer_equipment.id IS '自增主键'; +COMMENT ON COLUMN customer_equipment.equip_code IS '客户设备编码(唯一)'; +COMMENT ON COLUMN customer_equipment.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN customer_equipment.equip_name IS '客户设备名称'; +COMMENT ON COLUMN customer_equipment.equip_type IS '设备类型:配电间/配电井/配电箱'; +COMMENT ON COLUMN customer_equipment.building_id IS '所属建筑 ID(→ customer_building)'; +COMMENT ON COLUMN customer_equipment.floor_no IS '所在楼层'; +COMMENT ON COLUMN customer_equipment.location IS '所在位置'; +COMMENT ON COLUMN customer_equipment.description IS '设备描述'; +COMMENT ON COLUMN customer_equipment.remark IS '备注'; +COMMENT ON COLUMN customer_equipment.photo_url IS '未改造前设备照片'; +COMMENT ON COLUMN customer_equipment.created_at IS '创建时间'; +CREATE INDEX idx_equip_customer ON customer_equipment (customer_id); +CREATE INDEX idx_equip_building ON customer_equipment (building_id); + +-- 14. 客户访问账号表 +DROP TABLE IF EXISTS customer_account CASCADE; +CREATE TABLE customer_account ( + id BIGSERIAL PRIMARY KEY, + customer_id BIGINT NOT NULL REFERENCES customer(id), + phone VARCHAR(20) NOT NULL, + password_hash VARCHAR(256) NOT NULL, + wx_openid VARCHAR(128), + status SMALLINT NOT NULL DEFAULT 1, + last_login_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE customer_account IS '客户访问账号表(小程序登录)'; +COMMENT ON COLUMN customer_account.id IS '自增主键'; +COMMENT ON COLUMN customer_account.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN customer_account.phone IS '登录电话(唯一)'; +COMMENT ON COLUMN customer_account.password_hash IS '密码哈希'; +COMMENT ON COLUMN customer_account.wx_openid IS '微信 openid'; +COMMENT ON COLUMN customer_account.status IS '状态:1启用 0禁用'; +COMMENT ON COLUMN customer_account.last_login_at IS '最后登录时间'; +COMMENT ON COLUMN customer_account.created_at IS '创建时间'; +CREATE UNIQUE INDEX idx_cust_account_phone ON customer_account (phone); + +-- ============================================================ +-- 三、智电设备域(1 张表) +-- ============================================================ + +-- 15. 智电设备型号表(产品目录) +DROP TABLE IF EXISTS smart_device CASCADE; +CREATE TABLE smart_device ( + id BIGSERIAL PRIMARY KEY, + device_model VARCHAR(64) NOT NULL, + device_name VARCHAR(128) NOT NULL, + device_spec VARCHAR(256), + device_type VARCHAR(32) NOT NULL, + device_function TEXT, + supplier VARCHAR(128), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE smart_device IS '鑫潼智电设备信息表(产品目录)'; +COMMENT ON COLUMN smart_device.id IS '自增主键'; +COMMENT ON COLUMN smart_device.device_model IS '智电设备型号'; +COMMENT ON COLUMN smart_device.device_name IS '智电设备名称'; +COMMENT ON COLUMN smart_device.device_spec IS '智电设备规格'; +COMMENT ON COLUMN smart_device.device_type IS '设备类型:网关/采集器/探测器等'; +COMMENT ON COLUMN smart_device.device_function IS '智电设备功能'; +COMMENT ON COLUMN smart_device.supplier IS '供应商'; +COMMENT ON COLUMN smart_device.created_at IS '创建时间'; + +-- ============================================================ +-- 四、项目方案与合同域(3 张表) +-- ============================================================ + +-- 16. 智电改造方案表 +DROP TABLE IF EXISTS project_plan CASCADE; +CREATE TABLE project_plan ( + id BIGSERIAL PRIMARY KEY, + plan_no VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + registrar VARCHAR(64), + register_time DATE, + plan_desc TEXT, + plan_file_url VARCHAR(512), + status SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE project_plan IS '智电改造方案信息表'; +COMMENT ON COLUMN project_plan.id IS '自增主键'; +COMMENT ON COLUMN project_plan.plan_no IS '方案单号(唯一)'; +COMMENT ON COLUMN project_plan.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN project_plan.registrar IS '方案登记人(业务员)'; +COMMENT ON COLUMN project_plan.register_time IS '方案登记时间'; +COMMENT ON COLUMN project_plan.plan_desc IS '方案说明'; +COMMENT ON COLUMN project_plan.plan_file_url IS '方案最终文件'; +COMMENT ON COLUMN project_plan.status IS '状态:0草稿 1已确认'; +COMMENT ON COLUMN project_plan.created_at IS '创建时间'; +CREATE INDEX idx_plan_customer ON project_plan (customer_id); + +-- 17. 项目方案智电设备明细表 +DROP TABLE IF EXISTS project_plan_detail CASCADE; +CREATE TABLE project_plan_detail ( + id BIGSERIAL PRIMARY KEY, + plan_id BIGINT NOT NULL REFERENCES project_plan(id) ON DELETE CASCADE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + equip_id BIGINT REFERENCES customer_equipment(id), + smart_device_id BIGINT REFERENCES smart_device(id), + quantity NUMERIC(10,2) NOT NULL DEFAULT 1, + remark TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE project_plan_detail IS '项目方案智电设备明细信息表'; +COMMENT ON COLUMN project_plan_detail.id IS '自增主键'; +COMMENT ON COLUMN project_plan_detail.plan_id IS '方案 ID(→ project_plan)'; +COMMENT ON COLUMN project_plan_detail.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN project_plan_detail.equip_id IS '客户设备 ID(→ customer_equipment)'; +COMMENT ON COLUMN project_plan_detail.smart_device_id IS '智电设备型号 ID(→ smart_device)'; +COMMENT ON COLUMN project_plan_detail.quantity IS '数量'; +COMMENT ON COLUMN project_plan_detail.remark IS '备注'; +COMMENT ON COLUMN project_plan_detail.created_at IS '创建时间'; +CREATE INDEX idx_pd_plan ON project_plan_detail (plan_id); +CREATE INDEX idx_pd_customer ON project_plan_detail (customer_id); + +-- 18. 智电项目合同表 +DROP TABLE IF EXISTS contract CASCADE; +CREATE TABLE contract ( + id BIGSERIAL PRIMARY KEY, + contract_no VARCHAR(64) NOT NULL UNIQUE, + xt_contract_no VARCHAR(64), + customer_id BIGINT NOT NULL REFERENCES customer(id), + contract_amount NUMERIC(14,2), + pay_cycle VARCHAR(32), + salesman_id BIGINT, + sign_date DATE, + contract_desc TEXT, + start_date DATE, + end_date DATE, + plan_id BIGINT REFERENCES project_plan(id), + contract_file_url VARCHAR(512), + status SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE contract IS '智电项目合同信息表'; +COMMENT ON COLUMN contract.id IS '自增主键'; +COMMENT ON COLUMN contract.contract_no IS '合同单号(唯一)'; +COMMENT ON COLUMN contract.xt_contract_no IS '鑫潼合同号(XT+年份+序号)'; +COMMENT ON COLUMN contract.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN contract.contract_amount IS '合同金额'; +COMMENT ON COLUMN contract.pay_cycle IS '支付周期:按月/按年'; +COMMENT ON COLUMN contract.salesman_id IS '业务员'; +COMMENT ON COLUMN contract.sign_date IS '签订日期'; +COMMENT ON COLUMN contract.contract_desc IS '合同说明'; +COMMENT ON COLUMN contract.start_date IS '开始日期'; +COMMENT ON COLUMN contract.end_date IS '结束日期'; +COMMENT ON COLUMN contract.plan_id IS '方案 ID(→ project_plan)'; +COMMENT ON COLUMN contract.contract_file_url IS '签字盖章电子合同'; +COMMENT ON COLUMN contract.status IS '状态:1有效 0终止'; +COMMENT ON COLUMN contract.created_at IS '创建时间'; +CREATE INDEX idx_contract_customer ON contract (customer_id); +CREATE INDEX idx_contract_xt ON contract (xt_contract_no); + +-- ============================================================ +-- 五、线路与设备关联域(2 张表,ICC 同步) +-- ============================================================ + +-- 19. 智电改造线路表(ICC 同步 + 业务分配) +DROP TABLE IF EXISTS electric_line CASCADE; +CREATE TABLE electric_line ( + id BIGSERIAL PRIMARY KEY, + line_code VARCHAR(64) NOT NULL UNIQUE, + equip_id BIGINT NOT NULL REFERENCES customer_equipment(id), + icc_device_code VARCHAR(128), + line_name VARCHAR(128), + customer_id BIGINT NOT NULL REFERENCES customer(id), + building_id BIGINT REFERENCES customer_building(id), + floor_no VARCHAR(16), + location VARCHAR(256), + status SMALLINT NOT NULL DEFAULT 1, + synced_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE electric_line IS '智电改造线路信息表(ICC 同步)'; +COMMENT ON COLUMN electric_line.id IS '自增主键'; +COMMENT ON COLUMN electric_line.line_code IS '线路编码(唯一)'; +COMMENT ON COLUMN electric_line.equip_id IS '客户设备 ID(→ customer_equipment)'; +COMMENT ON COLUMN electric_line.icc_device_code IS 'ICC 智电设备编号'; +COMMENT ON COLUMN electric_line.line_name IS '线路名称(ICC 同步)'; +COMMENT ON COLUMN electric_line.customer_id IS '客户 ID(逻辑隔离键)'; +COMMENT ON COLUMN electric_line.building_id IS '所属建筑 ID(→ customer_building)'; +COMMENT ON COLUMN electric_line.floor_no IS '所在楼层'; +COMMENT ON COLUMN electric_line.location IS '所在位置'; +COMMENT ON COLUMN electric_line.status IS '状态:1在线 0离线'; +COMMENT ON COLUMN electric_line.synced_at IS 'ICC 同步时间'; +COMMENT ON COLUMN electric_line.created_at IS '创建时间'; +CREATE INDEX idx_line_customer ON electric_line (customer_id); +CREATE INDEX idx_line_equip ON electric_line (equip_id); +CREATE INDEX idx_line_icc ON electric_line (icc_device_code); + +-- 20. 线路所属房间关联表 +DROP TABLE IF EXISTS line_room CASCADE; +CREATE TABLE line_room ( + id BIGSERIAL PRIMARY KEY, + line_id BIGINT NOT NULL REFERENCES electric_line(id) ON DELETE CASCADE, + room_id BIGINT NOT NULL REFERENCES customer_room(id), + customer_id BIGINT NOT NULL REFERENCES customer(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE line_room IS '线路所属房间关联表'; +COMMENT ON COLUMN line_room.id IS '自增主键'; +COMMENT ON COLUMN line_room.line_id IS '线路 ID(→ electric_line)'; +COMMENT ON COLUMN line_room.room_id IS '房间 ID(→ customer_room)'; +COMMENT ON COLUMN line_room.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN line_room.created_at IS '创建时间'; +CREATE INDEX idx_lr_line ON line_room (line_id); +CREATE INDEX idx_lr_room ON line_room (room_id); +CREATE INDEX idx_lr_customer ON line_room (customer_id); + +-- ============================================================ +-- 六、派单域(4 张表) +-- ============================================================ + +-- 21. 勘察派单表 +DROP TABLE IF EXISTS survey_order CASCADE; +CREATE TABLE survey_order ( + id BIGSERIAL PRIMARY KEY, + survey_no VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + surveyor VARCHAR(64), + worker_id BIGINT, + survey_time TIMESTAMPTZ, + remark TEXT, + status SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE survey_order IS '客户勘察派单信息表'; +COMMENT ON COLUMN survey_order.id IS '自增主键'; +COMMENT ON COLUMN survey_order.survey_no IS '勘察单号(唯一)'; +COMMENT ON COLUMN survey_order.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN survey_order.surveyor IS '勘察人(共享电工)'; +COMMENT ON COLUMN survey_order.worker_id IS '共享电工 ID'; +COMMENT ON COLUMN survey_order.survey_time IS '勘察时间'; +COMMENT ON COLUMN survey_order.remark IS '备注'; +COMMENT ON COLUMN survey_order.status IS '状态:0已派发 1已接单 2已完成'; +COMMENT ON COLUMN survey_order.created_at IS '创建时间'; +CREATE INDEX idx_survey_customer ON survey_order (customer_id); +CREATE INDEX idx_survey_worker ON survey_order (worker_id); + +-- 22. 安装派单表 +DROP TABLE IF EXISTS install_order CASCADE; +CREATE TABLE install_order ( + id BIGSERIAL PRIMARY KEY, + install_no VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + installer VARCHAR(64), + worker_id BIGINT, + dispatch_date DATE, + start_date DATE, + end_date DATE, + plan_id BIGINT REFERENCES project_plan(id), + remark TEXT, + status SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE install_order IS '项目安装任务派单信息表'; +COMMENT ON COLUMN install_order.id IS '自增主键'; +COMMENT ON COLUMN install_order.install_no IS '安装单号(唯一)'; +COMMENT ON COLUMN install_order.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN install_order.installer IS '安装人(共享电工)'; +COMMENT ON COLUMN install_order.worker_id IS '共享电工 ID'; +COMMENT ON COLUMN install_order.dispatch_date IS '派单日期'; +COMMENT ON COLUMN install_order.start_date IS '开始日期'; +COMMENT ON COLUMN install_order.end_date IS '结束日期'; +COMMENT ON COLUMN install_order.plan_id IS '方案 ID(→ project_plan)'; +COMMENT ON COLUMN install_order.remark IS '备注'; +COMMENT ON COLUMN install_order.status IS '状态:0已派发 1已完成'; +COMMENT ON COLUMN install_order.created_at IS '创建时间'; +CREATE INDEX idx_install_customer ON install_order (customer_id); +CREATE INDEX idx_install_worker ON install_order (worker_id); + +-- 23. 项目验收表 +DROP TABLE IF EXISTS acceptance CASCADE; +CREATE TABLE acceptance ( + id BIGSERIAL PRIMARY KEY, + accept_no VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + acceptor VARCHAR(64), + accept_opinion TEXT, + accept_date DATE, + installer VARCHAR(64), + accept_file_url VARCHAR(512), + contract_id BIGINT REFERENCES contract(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE acceptance IS '项目验收信息表'; +COMMENT ON COLUMN acceptance.id IS '自增主键'; +COMMENT ON COLUMN acceptance.accept_no IS '验收单号(唯一)'; +COMMENT ON COLUMN acceptance.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN acceptance.acceptor IS '验收人'; +COMMENT ON COLUMN acceptance.accept_opinion IS '验收意见'; +COMMENT ON COLUMN acceptance.accept_date IS '验收日期'; +COMMENT ON COLUMN acceptance.installer IS '安装人'; +COMMENT ON COLUMN acceptance.accept_file_url IS '验收文件电子版'; +COMMENT ON COLUMN acceptance.contract_id IS '合同 ID(→ contract)'; +COMMENT ON COLUMN acceptance.created_at IS '创建时间'; +CREATE INDEX idx_accept_customer ON acceptance (customer_id); + +-- 24. 客户回访信息表 +DROP TABLE IF EXISTS visit_record CASCADE; +CREATE TABLE visit_record ( + id BIGSERIAL PRIMARY KEY, + visit_no VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + contact_person VARCHAR(64), + contact_phone VARCHAR(20), + visitor VARCHAR(64), + visit_time DATE, + customer_opinion TEXT, + photos JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE visit_record IS '客户回访信息表'; +COMMENT ON COLUMN visit_record.id IS '自增主键'; +COMMENT ON COLUMN visit_record.visit_no IS '回访单号(唯一)'; +COMMENT ON COLUMN visit_record.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN visit_record.contact_person IS '联系人'; +COMMENT ON COLUMN visit_record.contact_phone IS '电话'; +COMMENT ON COLUMN visit_record.visitor IS '回访人'; +COMMENT ON COLUMN visit_record.visit_time IS '回访时间'; +COMMENT ON COLUMN visit_record.customer_opinion IS '客户意见'; +COMMENT ON COLUMN visit_record.photos IS '回访照片(JSONB)'; +COMMENT ON COLUMN visit_record.created_at IS '创建时间'; +CREATE INDEX idx_visit_rec_customer ON visit_record (customer_id); + +-- ============================================================ +-- 七、报警域(4 张表) +-- ============================================================ + +-- 25. 报警故障类型表 +DROP TABLE IF EXISTS fault_type CASCADE; +CREATE TABLE fault_type ( + id BIGSERIAL PRIMARY KEY, + fault_type_code VARCHAR(64) NOT NULL UNIQUE, + fault_type_name VARCHAR(128) NOT NULL, + description TEXT, + remark TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE fault_type IS '报警故障类型信息表'; +COMMENT ON COLUMN fault_type.id IS '自增主键'; +COMMENT ON COLUMN fault_type.fault_type_code IS '故障类型编码(唯一)'; +COMMENT ON COLUMN fault_type.fault_type_name IS '故障类型名称'; +COMMENT ON COLUMN fault_type.description IS '故障类型说明'; +COMMENT ON COLUMN fault_type.remark IS '备注'; +COMMENT ON COLUMN fault_type.created_at IS '创建时间'; + +-- 26. 报警故障类型明细表 +DROP TABLE IF EXISTS fault_type_detail CASCADE; +CREATE TABLE fault_type_detail ( + id BIGSERIAL PRIMARY KEY, + fault_type_id BIGINT NOT NULL REFERENCES fault_type(id) ON DELETE CASCADE, + detail_category VARCHAR(32) NOT NULL, + detail_desc TEXT, + remark TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE fault_type_detail IS '报警故障类型明细信息表'; +COMMENT ON COLUMN fault_type_detail.id IS '自增主键'; +COMMENT ON COLUMN fault_type_detail.fault_type_id IS '故障类型 ID(→ fault_type)'; +COMMENT ON COLUMN fault_type_detail.detail_category IS '明细分类:故障原因/处理流程'; +COMMENT ON COLUMN fault_type_detail.detail_desc IS '明细说明'; +COMMENT ON COLUMN fault_type_detail.remark IS '备注'; +COMMENT ON COLUMN fault_type_detail.created_at IS '创建时间'; +CREATE INDEX idx_ftd_type ON fault_type_detail (fault_type_id); + +-- 27. 设备报警信息表(ICC 拉取) +DROP TABLE IF EXISTS device_alarm CASCADE; +CREATE TABLE device_alarm ( + id BIGSERIAL PRIMARY KEY, + alarm_code VARCHAR(128) NOT NULL UNIQUE, + line_id BIGINT, + equip_id BIGINT, + icc_device_code VARCHAR(128), + line_name VARCHAR(128), + customer_id BIGINT, + building_id BIGINT, + floor_no VARCHAR(16), + location VARCHAR(256), + alarm_date DATE, + fault_type VARCHAR(128), + alarm_value NUMERIC(14,2), + history_count INT DEFAULT 0, + synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE device_alarm IS '设备报警信息表(ICC 拉取)'; +COMMENT ON COLUMN device_alarm.id IS '自增主键'; +COMMENT ON COLUMN device_alarm.alarm_code IS '报警编码(唯一)'; +COMMENT ON COLUMN device_alarm.line_id IS '线路 ID'; +COMMENT ON COLUMN device_alarm.equip_id IS '客户设备 ID'; +COMMENT ON COLUMN device_alarm.icc_device_code IS 'ICC 设备编号'; +COMMENT ON COLUMN device_alarm.line_name IS '线路名称'; +COMMENT ON COLUMN device_alarm.customer_id IS '客户 ID'; +COMMENT ON COLUMN device_alarm.building_id IS '所属建筑 ID'; +COMMENT ON COLUMN device_alarm.floor_no IS '所在楼层'; +COMMENT ON COLUMN device_alarm.location IS '所在位置'; +COMMENT ON COLUMN device_alarm.alarm_date IS '报警日期'; +COMMENT ON COLUMN device_alarm.fault_type IS 'ICC 预警类型'; +COMMENT ON COLUMN device_alarm.alarm_value IS '报警数值'; +COMMENT ON COLUMN device_alarm.history_count IS '历史出现次数'; +COMMENT ON COLUMN device_alarm.synced_at IS 'ICC 同步时间'; +CREATE INDEX idx_alarm_customer ON device_alarm (customer_id); +CREATE INDEX idx_alarm_date ON device_alarm (alarm_date); +CREATE INDEX idx_alarm_line ON device_alarm (line_id); + +-- 28. 报警派单信息表 +DROP TABLE IF EXISTS alarm_dispatch CASCADE; +CREATE TABLE alarm_dispatch ( + id BIGSERIAL PRIMARY KEY, + dispatch_no VARCHAR(64) NOT NULL UNIQUE, + alarm_id BIGINT, + line_id BIGINT, + equip_id BIGINT, + icc_device_code VARCHAR(128), + line_name VARCHAR(128), + customer_id BIGINT NOT NULL REFERENCES customer(id), + contact_person VARCHAR(64), + contact_phone VARCHAR(20), + building_id BIGINT, + floor_no VARCHAR(16), + location VARCHAR(256), + fault_type_id BIGINT REFERENCES fault_type(id), + fault_desc TEXT, + dispatcher VARCHAR(64), + fault_reason TEXT, + process_flow TEXT, + worker_id BIGINT, + status SMALLINT NOT NULL DEFAULT 0, + process_photos JSONB, + rating SMALLINT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + completed_at TIMESTAMPTZ +); +COMMENT ON TABLE alarm_dispatch IS '报警派单信息表'; +COMMENT ON COLUMN alarm_dispatch.id IS '自增主键'; +COMMENT ON COLUMN alarm_dispatch.dispatch_no IS '报警派单号(唯一)'; +COMMENT ON COLUMN alarm_dispatch.alarm_id IS '报警 ID(→ device_alarm)'; +COMMENT ON COLUMN alarm_dispatch.line_id IS '线路 ID'; +COMMENT ON COLUMN alarm_dispatch.equip_id IS '客户设备 ID'; +COMMENT ON COLUMN alarm_dispatch.icc_device_code IS 'ICC 设备编号'; +COMMENT ON COLUMN alarm_dispatch.line_name IS '线路名称'; +COMMENT ON COLUMN alarm_dispatch.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN alarm_dispatch.contact_person IS '联系人'; +COMMENT ON COLUMN alarm_dispatch.contact_phone IS '电话'; +COMMENT ON COLUMN alarm_dispatch.building_id IS '建筑 ID'; +COMMENT ON COLUMN alarm_dispatch.floor_no IS '所在楼层'; +COMMENT ON COLUMN alarm_dispatch.location IS '所在位置'; +COMMENT ON COLUMN alarm_dispatch.fault_type_id IS '故障类型 ID(→ fault_type)'; +COMMENT ON COLUMN alarm_dispatch.fault_desc IS '故障事项(类型+数值)'; +COMMENT ON COLUMN alarm_dispatch.dispatcher IS '派发人'; +COMMENT ON COLUMN alarm_dispatch.fault_reason IS '故障原因(来自明细)'; +COMMENT ON COLUMN alarm_dispatch.process_flow IS '处理流程(来自明细)'; +COMMENT ON COLUMN alarm_dispatch.worker_id IS '共享电工 ID'; +COMMENT ON COLUMN alarm_dispatch.status IS '状态:0已派发 1已处理'; +COMMENT ON COLUMN alarm_dispatch.process_photos IS '报警处理照片(JSONB 多张)'; +COMMENT ON COLUMN alarm_dispatch.rating IS '派单处理评分'; +COMMENT ON COLUMN alarm_dispatch.created_at IS '创建时间'; +COMMENT ON COLUMN alarm_dispatch.completed_at IS '完成时间'; +CREATE INDEX idx_alarm_disp_customer ON alarm_dispatch (customer_id); +CREATE INDEX idx_alarm_disp_worker ON alarm_dispatch (worker_id); +CREATE INDEX idx_alarm_disp_status ON alarm_dispatch (status); + +-- ============================================================ +-- 八、报修域(2 张表) +-- ============================================================ + +-- 29. 报修单信息表 +DROP TABLE IF EXISTS repair_request CASCADE; +CREATE TABLE repair_request ( + id BIGSERIAL PRIMARY KEY, + repair_no VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + requester VARCHAR(64), + requester_phone VARCHAR(20), + request_time TIMESTAMPTZ, + building_id BIGINT, + floor_no VARCHAR(16), + location VARCHAR(256), + repair_content TEXT, + photos JSONB, + status SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE repair_request IS '报修单信息表'; +COMMENT ON COLUMN repair_request.id IS '自增主键'; +COMMENT ON COLUMN repair_request.repair_no IS '报修单号(唯一)'; +COMMENT ON COLUMN repair_request.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN repair_request.requester IS '报修人'; +COMMENT ON COLUMN repair_request.requester_phone IS '报修电话'; +COMMENT ON COLUMN repair_request.request_time IS '报修时间'; +COMMENT ON COLUMN repair_request.building_id IS '所属建筑 ID'; +COMMENT ON COLUMN repair_request.floor_no IS '所在楼层'; +COMMENT ON COLUMN repair_request.location IS '所在位置'; +COMMENT ON COLUMN repair_request.repair_content IS '报修内容'; +COMMENT ON COLUMN repair_request.photos IS '报修照片(JSONB)'; +COMMENT ON COLUMN repair_request.status IS '状态:0待处理 1已派单 2已完成'; +COMMENT ON COLUMN repair_request.created_at IS '创建时间'; +CREATE INDEX idx_repair_customer ON repair_request (customer_id); +CREATE INDEX idx_repair_status ON repair_request (status); + +-- 30. 报修派单信息表 +DROP TABLE IF EXISTS repair_dispatch CASCADE; +CREATE TABLE repair_dispatch ( + id BIGSERIAL PRIMARY KEY, + dispatch_no VARCHAR(64) NOT NULL UNIQUE, + repair_id BIGINT NOT NULL REFERENCES repair_request(id), + customer_id BIGINT NOT NULL REFERENCES customer(id), + requester VARCHAR(64), + requester_phone VARCHAR(20), + request_time TIMESTAMPTZ, + building_id BIGINT, + floor_no VARCHAR(16), + location VARCHAR(256), + repair_content TEXT, + dispatcher VARCHAR(64), + worker_id BIGINT, + status SMALLINT NOT NULL DEFAULT 0, + process_photos JSONB, + rating SMALLINT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + completed_at TIMESTAMPTZ +); +COMMENT ON TABLE repair_dispatch IS '报修派单信息表'; +COMMENT ON COLUMN repair_dispatch.id IS '自增主键'; +COMMENT ON COLUMN repair_dispatch.dispatch_no IS '报修派单号(唯一)'; +COMMENT ON COLUMN repair_dispatch.repair_id IS '报修单 ID(→ repair_request)'; +COMMENT ON COLUMN repair_dispatch.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN repair_dispatch.requester IS '报修人'; +COMMENT ON COLUMN repair_dispatch.requester_phone IS '报修电话'; +COMMENT ON COLUMN repair_dispatch.request_time IS '报修时间'; +COMMENT ON COLUMN repair_dispatch.building_id IS '建筑 ID'; +COMMENT ON COLUMN repair_dispatch.floor_no IS '所在楼层'; +COMMENT ON COLUMN repair_dispatch.location IS '所在位置'; +COMMENT ON COLUMN repair_dispatch.repair_content IS '报修内容'; +COMMENT ON COLUMN repair_dispatch.dispatcher IS '派发人'; +COMMENT ON COLUMN repair_dispatch.worker_id IS '共享电工 ID'; +COMMENT ON COLUMN repair_dispatch.status IS '状态:0已派发 1已处理'; +COMMENT ON COLUMN repair_dispatch.process_photos IS '处理完成照片(JSONB)'; +COMMENT ON COLUMN repair_dispatch.rating IS '报修派单处理评分(客户评)'; +COMMENT ON COLUMN repair_dispatch.created_at IS '创建时间'; +COMMENT ON COLUMN repair_dispatch.completed_at IS '完成时间'; +CREATE INDEX idx_repair_disp_customer ON repair_dispatch (customer_id); +CREATE INDEX idx_repair_disp_worker ON repair_dispatch (worker_id); + +-- ============================================================ +-- 九、维保域(2 张表) +-- ============================================================ + +-- 31. 维保单表 +DROP TABLE IF EXISTS maintenance_order CASCADE; +CREATE TABLE maintenance_order ( + id BIGSERIAL PRIMARY KEY, + maintenance_no VARCHAR(64) NOT NULL UNIQUE, + customer_id BIGINT NOT NULL REFERENCES customer(id), + maintainer VARCHAR(64), + worker_id BIGINT, + maintenance_date DATE, + remark TEXT, + status SMALLINT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE maintenance_order IS '维保单信息表'; +COMMENT ON COLUMN maintenance_order.id IS '自增主键'; +COMMENT ON COLUMN maintenance_order.maintenance_no IS '维保单号(录入,不自动生成)'; +COMMENT ON COLUMN maintenance_order.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN maintenance_order.maintainer IS '维保人(共享电工)'; +COMMENT ON COLUMN maintenance_order.worker_id IS '共享电工 ID'; +COMMENT ON COLUMN maintenance_order.maintenance_date IS '维保日期'; +COMMENT ON COLUMN maintenance_order.remark IS '备注'; +COMMENT ON COLUMN maintenance_order.status IS '状态:0已派发 1已完成'; +COMMENT ON COLUMN maintenance_order.created_at IS '创建时间'; +CREATE INDEX idx_maint_customer ON maintenance_order (customer_id); +CREATE INDEX idx_maint_worker ON maintenance_order (worker_id); + +-- 32. 维保单明细表 +DROP TABLE IF EXISTS maintenance_detail CASCADE; +CREATE TABLE maintenance_detail ( + id BIGSERIAL PRIMARY KEY, + maintenance_id BIGINT NOT NULL REFERENCES maintenance_order(id) ON DELETE CASCADE, + equip_id BIGINT, + customer_id BIGINT NOT NULL REFERENCES customer(id), + equip_name VARCHAR(128), + equip_type VARCHAR(32), + building_id BIGINT, + floor_no VARCHAR(16), + location VARCHAR(256), + panel_condition TEXT, + circuit_condition TEXT, + terminal_condition TEXT, + maintenance_desc TEXT, + photos JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE maintenance_detail IS '维保单明细表'; +COMMENT ON COLUMN maintenance_detail.id IS '自增主键'; +COMMENT ON COLUMN maintenance_detail.maintenance_id IS '维保单 ID(→ maintenance_order)'; +COMMENT ON COLUMN maintenance_detail.equip_id IS '客户设备 ID'; +COMMENT ON COLUMN maintenance_detail.customer_id IS '客户 ID(→ customer)'; +COMMENT ON COLUMN maintenance_detail.equip_name IS '客户设备名称'; +COMMENT ON COLUMN maintenance_detail.equip_type IS '设备类型:配电间/井/箱'; +COMMENT ON COLUMN maintenance_detail.building_id IS '建筑 ID'; +COMMENT ON COLUMN maintenance_detail.floor_no IS '所在楼层'; +COMMENT ON COLUMN maintenance_detail.location IS '所在位置'; +COMMENT ON COLUMN maintenance_detail.panel_condition IS '配电间或电箱情况'; +COMMENT ON COLUMN maintenance_detail.circuit_condition IS '回路情况'; +COMMENT ON COLUMN maintenance_detail.terminal_condition IS '终端情况'; +COMMENT ON COLUMN maintenance_detail.maintenance_desc IS '维保情况说明'; +COMMENT ON COLUMN maintenance_detail.photos IS '维保现场照片(JSONB 水印)'; +COMMENT ON COLUMN maintenance_detail.created_at IS '创建时间'; +CREATE INDEX idx_md_maint ON maintenance_detail (maintenance_id); +CREATE INDEX idx_md_customer ON maintenance_detail (customer_id); + +-- ============================================================ +-- 十、共享电工域(4 张表) +-- ============================================================ + +-- 33. 共享电工信息表 +DROP TABLE IF EXISTS worker CASCADE; +CREATE TABLE worker ( + id BIGSERIAL PRIMARY KEY, + worker_name VARCHAR(64) NOT NULL, + phone VARCHAR(20) NOT NULL, + id_card VARCHAR(18), + status SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE worker IS '共享电工信息表'; +COMMENT ON COLUMN worker.id IS '自增主键'; +COMMENT ON COLUMN worker.worker_name IS '电工姓名'; +COMMENT ON COLUMN worker.phone IS '手机号'; +COMMENT ON COLUMN worker.id_card IS '身份证号'; +COMMENT ON COLUMN worker.status IS '状态:1在岗 0离职'; +COMMENT ON COLUMN worker.created_at IS '创建时间'; +COMMENT ON COLUMN worker.updated_at IS '更新时间'; +CREATE INDEX idx_worker_phone ON worker (phone); +CREATE TRIGGER trg_worker_updated_at BEFORE UPDATE ON worker + FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); + +-- 34. 派单价格表 +DROP TABLE IF EXISTS dispatch_price CASCADE; +CREATE TABLE dispatch_price ( + id BIGSERIAL PRIMARY KEY, + price_code VARCHAR(64) NOT NULL UNIQUE, + dispatch_type VARCHAR(32) NOT NULL, + price_type VARCHAR(32), + unit_price NUMERIC(10,2), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE dispatch_price IS '共享电工派单价格信息表'; +COMMENT ON COLUMN dispatch_price.id IS '自增主键'; +COMMENT ON COLUMN dispatch_price.price_code IS '派单价格编码(唯一)'; +COMMENT ON COLUMN dispatch_price.dispatch_type IS '派单类型:勘察/安装/预警/维修/维保'; +COMMENT ON COLUMN dispatch_price.price_type IS '价格类型:单价/总价提成'; +COMMENT ON COLUMN dispatch_price.unit_price IS '服务单价'; +COMMENT ON COLUMN dispatch_price.created_at IS '创建时间'; + +-- 35. 共享电工派单收入表(月度汇总) +DROP TABLE IF EXISTS worker_income CASCADE; +CREATE TABLE worker_income ( + id BIGSERIAL PRIMARY KEY, + income_code VARCHAR(64) NOT NULL UNIQUE, + worker_id BIGINT NOT NULL REFERENCES worker(id), + income_month VARCHAR(7) NOT NULL, + monthly_total NUMERIC(14,2), + rating_summary NUMERIC(5,2), + final_total NUMERIC(14,2), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE worker_income IS '共享电工派单收入信息表(月度汇总)'; +COMMENT ON COLUMN worker_income.id IS '自增主键'; +COMMENT ON COLUMN worker_income.income_code IS '派单收入编码(唯一)'; +COMMENT ON COLUMN worker_income.worker_id IS '共享电工 ID(→ worker)'; +COMMENT ON COLUMN worker_income.income_month IS '收入月份(YYYY-MM)'; +COMMENT ON COLUMN worker_income.monthly_total IS '月度收入合计'; +COMMENT ON COLUMN worker_income.rating_summary IS '评分综合统计'; +COMMENT ON COLUMN worker_income.final_total IS '月度最终收入合计'; +COMMENT ON COLUMN worker_income.created_at IS '创建时间'; +CREATE INDEX idx_wi_worker ON worker_income (worker_id); +CREATE INDEX idx_wi_month ON worker_income (income_month); + +-- 36. 共享电工派单收入明细表 +DROP TABLE IF EXISTS worker_income_detail CASCADE; +CREATE TABLE worker_income_detail ( + id BIGSERIAL PRIMARY KEY, + income_id BIGINT NOT NULL REFERENCES worker_income(id) ON DELETE CASCADE, + worker_id BIGINT NOT NULL REFERENCES worker(id), + income_month VARCHAR(7) NOT NULL, + dispatch_type VARCHAR(32) NOT NULL, + dispatch_no VARCHAR(64), + income_amount NUMERIC(10,2), + rating_level VARCHAR(16), + final_income NUMERIC(10,2), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE worker_income_detail IS '共享电工派单收入明细信息表'; +COMMENT ON COLUMN worker_income_detail.id IS '自增主键'; +COMMENT ON COLUMN worker_income_detail.income_id IS '收入 ID(→ worker_income)'; +COMMENT ON COLUMN worker_income_detail.worker_id IS '共享电工 ID(→ worker)'; +COMMENT ON COLUMN worker_income_detail.income_month IS '收入月份'; +COMMENT ON COLUMN worker_income_detail.dispatch_type IS '派单类型'; +COMMENT ON COLUMN worker_income_detail.dispatch_no IS '各类型派单单号'; +COMMENT ON COLUMN worker_income_detail.income_amount IS '本单收入'; +COMMENT ON COLUMN worker_income_detail.rating_level IS '评价等级'; +COMMENT ON COLUMN worker_income_detail.final_income IS '本单最终收入合计'; +COMMENT ON COLUMN worker_income_detail.created_at IS '创建时间'; +CREATE INDEX idx_wid_income ON worker_income_detail (income_id); +CREATE INDEX idx_wid_worker ON worker_income_detail (worker_id); + +-- ============================================================ +-- 十一、用电采集数据(1 张表,按月 RANGE 分区) +-- ============================================================ + +-- 37. 线路用电信息表(按月分区,时序数据) +DROP TABLE IF EXISTS electricity_data CASCADE; +CREATE TABLE electricity_data ( + id BIGSERIAL, + line_id BIGINT NOT NULL, + line_code VARCHAR(64) NOT NULL, + equip_id BIGINT, + icc_device_code VARCHAR(128), + line_name VARCHAR(128), + customer_id BIGINT NOT NULL, + electricity NUMERIC(14,4), + collect_time TIMESTAMPTZ NOT NULL, + PRIMARY KEY (id, collect_time) +) PARTITION BY RANGE (collect_time); + +COMMENT ON TABLE electricity_data IS '线路用电信息表(按月分区,时序数据)'; +COMMENT ON COLUMN electricity_data.id IS '自增(复合主键含 collect_time)'; +COMMENT ON COLUMN electricity_data.line_id IS '线路 ID(→ electric_line)'; +COMMENT ON COLUMN electricity_data.line_code IS '线路编码(冗余,加速查询)'; +COMMENT ON COLUMN electricity_data.equip_id IS '客户设备 ID'; +COMMENT ON COLUMN electricity_data.icc_device_code IS 'ICC 设备编号'; +COMMENT ON COLUMN electricity_data.line_name IS '线路名称'; +COMMENT ON COLUMN electricity_data.customer_id IS '客户 ID(逻辑隔离键)'; +COMMENT ON COLUMN electricity_data.electricity IS '用电量'; +COMMENT ON COLUMN electricity_data.collect_time IS '采集时间(分区键)'; + +-- 初始分区(2026 年 8~12 月) +CREATE TABLE electricity_data_2026_08 PARTITION OF electricity_data + FOR VALUES FROM ('2026-08-01') TO ('2026-09-01'); +CREATE TABLE electricity_data_2026_09 PARTITION OF electricity_data + FOR VALUES FROM ('2026-09-01') TO ('2026-10-01'); +CREATE TABLE electricity_data_2026_10 PARTITION OF electricity_data + FOR VALUES FROM ('2026-10-01') TO ('2026-11-01'); +CREATE TABLE electricity_data_2026_11 PARTITION OF electricity_data + FOR VALUES FROM ('2026-11-01') TO ('2026-12-01'); +CREATE TABLE electricity_data_2026_12 PARTITION OF electricity_data + FOR VALUES FROM ('2026-12-01') TO ('2027-01-01'); + +-- 分区索引(在父表创建,自动继承到各分区) +CREATE INDEX idx_ed_customer ON electricity_data (customer_id); +CREATE INDEX idx_ed_line ON electricity_data (line_id); +CREATE INDEX idx_ed_time ON electricity_data (collect_time); +CREATE INDEX idx_ed_customer_time ON electricity_data (customer_id, collect_time); + +-- ============================================================ +-- 自动分区管理(纯 PostgreSQL,无需 pg_partman 扩展) +-- ============================================================ + +-- 1) 确保指定月份的分区存在(不存在则创建) +CREATE OR REPLACE FUNCTION ensure_electricity_partition(p_date DATE DEFAULT CURRENT_DATE) +RETURNS VOID AS $$ +DECLARE + v_month_start DATE; + v_month_end DATE; + v_part_name TEXT; + v_exists BOOLEAN; +BEGIN + v_month_start := DATE_TRUNC('month', p_date)::DATE; + v_month_end := (v_month_start + INTERVAL '1 month')::DATE; + v_part_name := 'electricity_data_' || TO_CHAR(v_month_start, 'YYYY_MM'); + + SELECT EXISTS( + SELECT 1 FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = v_part_name AND n.nspname = 'public' + ) INTO v_exists; + + IF NOT v_exists THEN + EXECUTE format( + 'CREATE TABLE %I PARTITION OF electricity_data FOR VALUES FROM (%L) TO (%L)', + v_part_name, v_month_start, v_month_end + ); + END IF; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION ensure_electricity_partition IS '确保指定月份的用电数据分区存在(不存在则自动创建)'; + +-- 2) 批量预创建未来分区(p_premake = 预创建未来几个月) +CREATE OR REPLACE FUNCTION run_electricity_partition_maintenance(p_premake INT DEFAULT 3) +RETURNS TABLE(created_month TEXT, action TEXT) AS $$ +DECLARE + v_date DATE := CURRENT_DATE; + v_i INT; + v_month_start DATE; + v_part_name TEXT; + v_exists BOOLEAN; +BEGIN + FOR v_i IN 0..p_premake LOOP + v_month_start := DATE_TRUNC('month', v_date + (v_i || ' month')::INTERVAL)::DATE; + v_part_name := 'electricity_data_' || TO_CHAR(v_month_start, 'YYYY_MM'); + + SELECT EXISTS( + SELECT 1 FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = v_part_name AND n.nspname = 'public' + ) INTO v_exists; + + IF NOT v_exists THEN + EXECUTE format( + 'CREATE TABLE %I PARTITION OF electricity_data FOR VALUES FROM (%L) TO (%L)', + v_part_name, + v_month_start, + (v_month_start + INTERVAL '1 month')::DATE + ); + created_month := v_part_name; + action := 'CREATED'; + ELSE + created_month := v_part_name; + action := 'EXISTS'; + END IF; + RETURN NEXT; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION run_electricity_partition_maintenance IS '批量预创建用电数据分区(默认当前月+未来3个月),返回创建结果'; + +-- 3) pg_cron 定时调度(数据库全自动管理,无需后端 cron) +-- pg_cron 扩展在数据库内部定时执行,后端服务状态不影响分区管理 +-- 注意: PostgreSQL 分区表的 BEFORE INSERT 触发器在分区路由之后才执行, +-- 无法在写入时动态创建分区。因此通过 pg_cron 定时预创建 + 应用层 ensure 兜底。 + +-- 删除已存在的同名定时任务(幂等) +DO $$ +BEGIN + PERFORM cron.unschedule(jobid) FROM cron.job WHERE jobname IN ('electricity_partition_maintenance', 'electricity_monthly_refresh'); +EXCEPTION WHEN OTHERS THEN NULL; +END $$; + +-- 每月25号凌晨3点:预创建当前月 + 未来3个月分区 +SELECT cron.schedule( + 'electricity_partition_maintenance', + '0 3 25 * *', + 'SELECT run_electricity_partition_maintenance(3)' +); + +-- 每月1号凌晨2点:刷新月度汇总物化视图 +SELECT cron.schedule( + 'electricity_monthly_refresh', + '0 2 1 * *', + 'SELECT refresh_electricity_monthly()' +); + +-- 4) 立即执行一次维护:确保当前月+未来3个月分区就绪 +SELECT * FROM run_electricity_partition_maintenance(3); + +-- ============================================================ +-- 用电数据月度汇总物化视图(历史数据分析) +-- ============================================================ + +CREATE MATERIALIZED VIEW IF NOT EXISTS mv_electricity_monthly AS +SELECT + customer_id, + line_id, + TO_CHAR(collect_time, 'YYYY-MM') AS month, + SUM(electricity) AS total_usage, + AVG(electricity) AS avg_usage, + MAX(electricity) AS peak_usage, + MIN(electricity) AS min_usage, + COUNT(*) AS record_count, + MIN(collect_time) AS first_collect, + MAX(collect_time) AS last_collect +FROM electricity_data +GROUP BY customer_id, line_id, TO_CHAR(collect_time, 'YYYY-MM') +WITH NO DATA; + +COMMENT ON MATERIALIZED VIEW mv_electricity_monthly IS '用电数据月度汇总(历史分析用,每月刷新)'; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_mv_em_key + ON mv_electricity_monthly (customer_id, line_id, month); +CREATE INDEX IF NOT EXISTS idx_mv_em_customer + ON mv_electricity_monthly (customer_id); +CREATE INDEX IF NOT EXISTS idx_mv_em_month + ON mv_electricity_monthly (month); + +-- 刷新函数(应用层每月初调用,或用 pg_cron 定时) +-- 首次刷新用非并发模式初始化,后续用 CONCURRENTLY 不阻塞查询 +CREATE OR REPLACE FUNCTION refresh_electricity_monthly() +RETURNS VOID AS $$ +BEGIN + IF NOT pg_is_in_recovery() AND EXISTS ( + SELECT 1 FROM pg_matviews WHERE matviewname = 'mv_electricity_monthly' + ) THEN + BEGIN + REFRESH MATERIALIZED VIEW CONCURRENTLY mv_electricity_monthly; + EXCEPTION WHEN feature_not_supported THEN + REFRESH MATERIALIZED VIEW mv_electricity_monthly; + END; + ELSE + REFRESH MATERIALIZED VIEW mv_electricity_monthly; + END IF; +END; +$$ LANGUAGE plpgsql; + +COMMENT ON FUNCTION refresh_electricity_monthly IS '刷新用电月度汇总物化视图(自动处理首次初始化,后续用 CONCURRENTLY)'; + +-- 首次初始化物化视图(WITH NO DATA 创建后必须先刷新一次) +REFRESH MATERIALIZED VIEW mv_electricity_monthly; + +-- ============================================================ +-- 十二、辅助域(2 张表) +-- ============================================================ + +-- ICC 同步日志表 +DROP TABLE IF EXISTS icc_sync_log CASCADE; +CREATE TABLE icc_sync_log ( + id BIGSERIAL PRIMARY KEY, + sync_type VARCHAR(32) NOT NULL, + start_time TIMESTAMPTZ, + end_time TIMESTAMPTZ, + record_count INT, + status SMALLINT NOT NULL DEFAULT 1, + error_message TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE icc_sync_log IS 'ICC 数据同步日志表'; +COMMENT ON COLUMN icc_sync_log.id IS '自增主键'; +COMMENT ON COLUMN icc_sync_log.sync_type IS '同步类型:客户/设备/用电/报警'; +COMMENT ON COLUMN icc_sync_log.start_time IS '开始时间'; +COMMENT ON COLUMN icc_sync_log.end_time IS '结束时间'; +COMMENT ON COLUMN icc_sync_log.record_count IS '同步记录数'; +COMMENT ON COLUMN icc_sync_log.status IS '状态:1成功 0失败'; +COMMENT ON COLUMN icc_sync_log.error_message IS '错误信息'; +COMMENT ON COLUMN icc_sync_log.created_at IS '创建时间'; +CREATE INDEX idx_icc_sync_type ON icc_sync_log (sync_type); +CREATE INDEX idx_icc_sync_time ON icc_sync_log (created_at); + +-- 文件存储记录表 +DROP TABLE IF EXISTS file_record CASCADE; +CREATE TABLE file_record ( + id BIGSERIAL PRIMARY KEY, + file_name VARCHAR(256) NOT NULL, + file_url VARCHAR(512) NOT NULL, + file_type VARCHAR(32), + file_size BIGINT, + business_type VARCHAR(64), + business_id BIGINT, + uploaded_by BIGINT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +COMMENT ON TABLE file_record IS '文件存储记录表'; +COMMENT ON COLUMN file_record.id IS '自增主键'; +COMMENT ON COLUMN file_record.file_name IS '文件名'; +COMMENT ON COLUMN file_record.file_url IS '文件 URL'; +COMMENT ON COLUMN file_record.file_type IS '文件类型:取证/合同/验收/方案/月报'; +COMMENT ON COLUMN file_record.file_size IS '文件大小(字节)'; +COMMENT ON COLUMN file_record.business_type IS '关联业务类型'; +COMMENT ON COLUMN file_record.business_id IS '关联业务 ID'; +COMMENT ON COLUMN file_record.uploaded_by IS '上传人 ID'; +COMMENT ON COLUMN file_record.created_at IS '创建时间'; +CREATE INDEX idx_file_business ON file_record (business_type, business_id); + +-- ============================================================ +-- 十三、初始数据 +-- ============================================================ + +-- 初始角色 +INSERT INTO sys_role (role_code, role_name, description, sort_order) VALUES +('ADMIN', '运营管理员', '全部菜单权限,管理所有客户', 1), +('SALESMAN', '业务员', '客户/项目/方案/合同/回访', 2), +('WORKER', '共享电工', 'APP 接单处理,无后台菜单', 3), +('CUSTOMER', '客户', '小程序查看用电/报警/报修', 4); + +-- 初始菜单 +INSERT INTO sys_menu (menu_name, menu_path, menu_type, sort_order, permission) VALUES +('客户管理', '/customer', 2, 1, 'customer:list'), +('项目管理', '/project', 2, 2, 'project:list'), +('运营派单', '/dispatch', 2, 3, 'dispatch:list'), +('设备监控', '/device', 2, 4, 'device:list'), +('统计报表', '/report', 2, 5, 'report:list'), +('系统管理', '/system', 2, 6, 'system:list'); + +-- 初始字典 +INSERT INTO sys_dict (dict_type, dict_label, dict_value, sort_order) VALUES +('customer_status', '意向', '1', 1), +('customer_status', '签约', '2', 2), +('customer_status', '运维中', '3', 3), +('customer_status', '终止', '4', 4), +('equip_type', '配电间', '配电间', 1), +('equip_type', '配电井', '配电井', 2), +('equip_type', '配电箱', '配电箱', 3), +('device_type', '网关', '网关', 1), +('device_type', '探测器', '探测器', 2), +('device_type', '采集器', '采集器', 3), +('dispatch_status', '已派发', '0', 1), +('dispatch_status', '已接单', '1', 2), +('dispatch_status', '已完成', '2', 3), +('pay_cycle', '按月支付', '按月', 1), +('pay_cycle', '按年支付', '按年', 2), +('dispatch_type', '勘察派单', 'survey', 1), +('dispatch_type', '安装派单', 'install', 2), +('dispatch_type', '报警派单', 'alarm', 3), +('dispatch_type', '维修派单', 'repair', 4), +('dispatch_type', '维保派单', 'maintenance', 5); + +COMMIT; + +-- ============================================================ +-- 完 · 37 张表 · 50+ 索引 · 3 个触发器 · 3 个分区管理函数 · 1 个物化视图 · 2 个 pg_cron 定时任务 · 初始数据 +-- ============================================================ + +-- ============================================================ +-- 附录: pg_cron 安装说明(首次部署时执行一次) +-- ============================================================ +-- +-- 1. 下载 Windows 预编译包: +-- https://github.com/hakanrw/pg_cron_windows/releases/download/windows/pg_cron_windows_msvc_x64.zip +-- +-- 2. 复制文件到 PostgreSQL 目录: +-- pg_cron.dll → C:\Program Files\PostgreSQL\17\lib\ +-- pg_cron.control → C:\Program Files\PostgreSQL\17\share\extension\ +-- pg_cron--*.sql → C:\Program Files\PostgreSQL\17\share\extension\ +-- +-- 3. 编辑 postgresql.conf 添加以下内容: +-- shared_preload_libraries = 'pg_cron' +-- cron.database_name = 'xintong' +-- cron.host = '127.0.0.1' +-- cron.use_background_workers = on +-- cron.timezone = 'Asia/Shanghai' +-- +-- 4. 编辑 pg_hba.conf,在 IPv4 local connections 之前添加: +-- host all postgres 127.0.0.1/32 trust +-- host all postgres ::1/128 trust +-- +-- 5. 重启 PostgreSQL 服务 +-- +-- 6. 执行本脚本: psql -U postgres -d xintong -f db_init.sql +-- 脚本会自动创建 pg_cron 扩展并注册定时任务 +-- +-- 注意: cron.use_background_workers = on 是 Windows 上的关键配置, +-- 使用后台工作进程代替 TCP 连接,避免连接认证超时问题。 +-- ============================================================ diff --git a/database/fix-admin-permissions.sql b/database/fix-admin-permissions.sql deleted file mode 100644 index 42a58aa..0000000 --- a/database/fix-admin-permissions.sql +++ /dev/null @@ -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; diff --git a/database/init.sql b/database/init.sql deleted file mode 100644 index ba62b17..0000000 --- a/database/init.sql +++ /dev/null @@ -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'; diff --git a/系统设计/db_design.html b/系统设计/db_design.html new file mode 100644 index 0000000..d8f4561 --- /dev/null +++ b/系统设计/db_design.html @@ -0,0 +1,1116 @@ + + + + + + +鑫潼智电系统 · PostgreSQL 数据库设计文档 + + + +
+ +
+ DATABASE DESIGN · V1.0 +

鑫潼智电系统 · PostgreSQL 数据库设计文档

+

基于《鑫潼智电系统需求分析报告 V1.0》数据说明,设计 37 张数据表 + 3 个分区管理函数 + 1 个物化视图,覆盖客户、项目、设备、派单、报警、报修、维保、报酬、用电采集等全部业务域。采用逻辑隔离 + 时序分区方案。

+
+ 数据库:PostgreSQL 15+ + 表数量:37 张 + 隔离方案:customer_id 逻辑隔离 + 时序分区:electricity_data 按月分区 + 自动分区:3 个管理函数 + 历史分析:1 个物化视图 + 字符集:UTF8 +
+
+ + +
+
01

设计概述

设计原则与关键决策
+ +
+

设计原则

+

① 逻辑隔离:所有业务表携带 customer_id 字段实现行级数据隔离,不做物理分库。② 时序分区:用电采集数据(electricity_data)按月 RANGE 分区,解决数据量暴涨问题。③ 外键约束:核心关联表使用外键保证数据完整性。④ JSONB 灵活存储:照片路径列表、ICC 灵活字段使用 JSONB 类型。⑤ 全部表加 COMMENT:便于团队协作和 ORM 映射。

+
+ +
+

命名规范

+

表名:snake_case 小写下划线,业务表无前缀,系统表 sys_ 前缀。主键:统一 BIGSERIAL 自增。时间字段:统一 TIMESTAMPTZ 带时区。布尔状态:用 SMALLINT(0/1)。编码字段:如 customer_no、plan_no 等业务编号用 VARCHAR(64)。

+
+
+ + +
+
02

业务域划分

14 个业务域 · 37 张表
+ +
+
+
D1

系统管理域

+
+
sys_user系统用户(业务员/管理员)
+
sys_role角色
+
sys_user_role用户角色关联
+
sys_menu菜单(RBAC 权限)
+
sys_role_menu角色菜单关联
+
sys_dict数据字典
+
sys_operation_log操作日志
+
+
+ +
+
D2

客户域

+
+
customer客户信息
+
customer_department客户部门
+
customer_visit客户拜访
+
customer_building客户建筑
+
customer_room客户房间
+
customer_equipment客户设备(配电间/井/箱)
+
customer_account客户访问账号
+
+
+ +
+
D3

智电设备域

+
+
smart_device智电设备型号(产品目录)
+
+
+ +
+
D4

项目方案与合同域

+
+
project_plan智电改造方案
+
project_plan_detail方案设备明细
+
contract项目合同
+
+
+ +
+
D5

线路与设备关联域

+
+
electric_line智电改造线路(ICC 同步)
+
line_room线路房间关联
+
+
+ +
+
D6

派单域

+
+
survey_order勘察派单
+
install_order安装派单
+
acceptance项目验收
+
visit_record客户回访
+
+
+ +
+
D7

报警域

+
+
fault_type报警故障类型
+
fault_type_detail故障类型明细
+
device_alarm设备报警(ICC 拉取)
+
alarm_dispatch报警派单
+
+
+ +
+
D8

报修域

+
+
repair_request报修单
+
repair_dispatch报修派单
+
+
+ +
+
D9

维保域

+
+
maintenance_order维保单
+
maintenance_detail维保明细
+
+
+ +
+
D10

共享电工域

+
+
worker共享电工信息
+
dispatch_price派单价格
+
worker_income派单收入(月度)
+
worker_income_detail派单收入明细
+
+
+ +
+
D11

用电采集域(分区)

+
+
electricity_data线路用电信息(按月分区)
+
+
+ +
+
D12

辅助域

+
+
icc_sync_logICC 同步日志
+
file_record文件存储记录
+
+
+
+
+ + +
+
03

核心 ER 关系图

关键实体关联
+ +

以下展示客户域→项目域→线路域→报警/报修域的核心关联路径,省略系统管理域和辅助域。

+ +
+erDiagram + customer ||--o{ customer_department : "1对多" + customer ||--o{ customer_building : "1对多" + customer ||--o{ customer_equipment : "1对多" + customer ||--o{ customer_room : "1对多" + customer ||--o{ customer_visit : "1对多" + customer ||--o{ customer_account : "1对多" + customer ||--o{ project_plan : "1对多" + customer ||--o{ contract : "1对多" + customer ||--o{ survey_order : "1对多" + customer ||--o{ install_order : "1对多" + customer ||--o{ acceptance : "1对多" + customer ||--o{ visit_record : "1对多" + customer ||--o{ repair_request : "1对多" + customer ||--o{ maintenance_order : "1对多" + customer ||--o{ electricity_data : "1对多" + + customer_building ||--o{ customer_equipment : "建筑含设备" + customer_building ||--o{ customer_room : "建筑含房间" + customer_department ||--o{ customer_room : "部门含房间" + + project_plan ||--o{ project_plan_detail : "方案含明细" + project_plan ||--o{ contract : "方案签合同" + smart_device ||--o{ project_plan_detail : "设备型号" + + customer_equipment ||--o{ electric_line : "设备含线路" + electric_line ||--o{ line_room : "线路关联房间" + customer_room ||--o{ line_room : "房间关联线路" + + fault_type ||--o{ fault_type_detail : "类型含明细" + fault_type ||--o{ alarm_dispatch : "故障派单" + electric_line ||--o{ device_alarm : "线路报警" + device_alarm ||--o{ alarm_dispatch : "报警派单" + + repair_request ||--|{ repair_dispatch : "报修派单" + maintenance_order ||--|{ maintenance_detail : "维保明细" + customer_equipment ||--o{ maintenance_detail : "维保设备" + + worker ||--o{ worker_income : "电工收入" + worker_income ||--o{ worker_income_detail : "收入明细" + dispatch_price ||--o{ worker_income_detail : "价格参考" +
+
+ + +
+
04

用电数据时序分区

electricity_data 按月 RANGE 分区
+ +
+

分区策略

+

electricity_data 表是数据量增量大头(每条线路按 ICC 采集频次持续写入)。采用 PostgreSQL 声明式 RANGE 分区,按 collect_time 字段按月切分。查询只扫描目标月份分区,性能稳定。历史分区可归档,不影响热数据查询。

+
+ +
+

2026_05

已归档

+

2026_06

已归档

+

2026_07

已归档

+

2026_08

当前活跃

+

2026_09

预创建

+

2026_10

预创建

+

2026_11

预创建

+

2026_12

预创建

+

2027_01

待创建

+

2027_02

待创建

+

2027_03

待创建

+

2027_04

待创建

+
+ +
+

分区自动管理

+

建议使用 pg_partman 扩展或编写定时任务(Midway.js cron)自动创建未来月份分区。每月初自动 CREATE 新分区,超过 N 个月的分区可 DETACH 后归档至冷存储。构建脚本已预创建 2026 年 8~12 月分区。

+
+
+ + +
+
05

全部数据表字段说明

37 张表 · 按业务域分组
+ + +
D1

系统管理域

7 张表 · 用户/角色/菜单/字典/日志
+ +
sys_user— 系统用户表(鑫潼内部人员)
+
+ + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
usernameVARCHAR(64)UNIQUE登录用户名
password_hashVARCHAR(256)密码哈希值
real_nameVARCHAR(64)真实姓名
phoneVARCHAR(20)手机号
emailVARCHAR(128)邮箱
statusSMALLINT状态:1启用 0禁用
last_login_atTIMESTAMPTZ最后登录时间
created_atTIMESTAMPTZ创建时间
updated_atTIMESTAMPTZ更新时间
+ +
sys_role— 角色表
+
+ + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
role_codeVARCHAR(32)UNIQUE角色编码
role_nameVARCHAR(64)角色名称
descriptionTEXT描述
sort_orderINT排序号
created_atTIMESTAMPTZ创建时间
+ +
sys_user_role— 用户角色关联表
+
+ + +
字段类型键说明
user_idBIGINTPKFK用户 ID(→ sys_user)
role_idBIGINTPKFK角色 ID(→ sys_role)
+ +
sys_menu— 菜单表(RBAC 权限)
+
+ + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
parent_idBIGINTFK父菜单 ID(自关联)
menu_nameVARCHAR(64)菜单名称
menu_pathVARCHAR(256)路由路径
componentVARCHAR(256)前端组件路径
iconVARCHAR(64)图标
sort_orderINT排序号
menu_typeSMALLINT类型:1目录 2菜单 3按钮
permissionVARCHAR(128)权限标识,如 customer:list
visibleSMALLINT是否可见:1是 0否
created_atTIMESTAMPTZ创建时间
+ +
sys_role_menu— 角色菜单关联表
+
+ + +
字段类型键说明
role_idBIGINTPKFK角色 ID(→ sys_role)
menu_idBIGINTPKFK菜单 ID(→ sys_menu)
+ +
sys_dict— 数据字典表
+
+ + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
dict_typeVARCHAR(64)IDX字典类型
dict_labelVARCHAR(128)字典标签
dict_valueVARCHAR(128)字典值
sort_orderINT排序号
statusSMALLINT状态:1启用 0禁用
created_atTIMESTAMPTZ创建时间
+ +
sys_operation_log— 操作日志表
+
+ + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
user_idBIGINTIDX操作用户 ID
moduleVARCHAR(64)操作模块
actionVARCHAR(64)操作动作
methodVARCHAR(256)请求方法
paramsTEXT请求参数
ipVARCHAR(64)操作 IP
created_atTIMESTAMPTZIDX操作时间
+ + +
D2

客户域

7 张表 · 客户/部门/建筑/房间/设备/账号
+ +
customer— 客户信息表
+
+ + + + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
customer_nameVARCHAR(128)IDX客户名称
provinceVARCHAR(64)省
cityVARCHAR(64)市
districtVARCHAR(64)区/县
addressVARCHAR(256)地址
contact_personVARCHAR(64)联系人
contact_phoneVARCHAR(20)电话
postal_codeVARCHAR(10)邮编
faxVARCHAR(20)传真
customer_levelVARCHAR(32)客户级别(学校/医院等)
statusSMALLINT状态:1意向 2签约 3运维中 4终止
created_atTIMESTAMPTZ创建时间
updated_atTIMESTAMPTZ更新时间
+ +
customer_department— 客户部门信息表
+
+ + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
customer_idBIGINTFK客户 ID(→ customer)
dept_nameVARCHAR(128)部门名称
contact_personVARCHAR(64)联系人
contact_phoneVARCHAR(20)电话
function_descTEXT职能
created_atTIMESTAMPTZ创建时间
+ +
customer_visit— 客户拜访信息表
+
+ + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
visit_noVARCHAR(64)UNIQUE拜访单号
customer_idBIGINTFK客户 ID
visit_personVARCHAR(64)拜访人
visit_phoneVARCHAR(20)拜访人电话
visit_contentTEXT拜访内容
visit_timeDATE拜访时间
salesman_idBIGINTIDX业务员
will_planSMALLINT是否做方案:0不同意 1同意
created_atTIMESTAMPTZ创建时间
+ +
customer_building— 客户智电改造建筑信息表
+
+ + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
customer_idBIGINTFK客户 ID
building_nameVARCHAR(128)建筑名称
basic_infoTEXT基本情况
purposeVARCHAR(128)用途
floor_countINT层数
created_atTIMESTAMPTZ创建时间
+ +
customer_room— 客户建筑房间信息表
+
+ + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
room_codeVARCHAR(64)房间编码
customer_idBIGINTFK客户 ID
room_nameVARCHAR(128)房间名称
building_idBIGINTFK所属建筑 ID(→ customer_building)
floor_noVARCHAR(16)所在楼层
room_purposeVARCHAR(128)房间用途
dept_idBIGINTFK房间所属客户部门(→ customer_department)
created_atTIMESTAMPTZ创建时间
+ +
customer_equipment— 客户智电改造设备信息表(配电间/井/箱)
+
+ + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
equip_codeVARCHAR(64)UNIQUE客户设备编码
customer_idBIGINTFK客户 ID
equip_nameVARCHAR(128)客户设备名称
equip_typeVARCHAR(32)设备类型:配电间/配电井/配电箱
building_idBIGINTFK所属建筑 ID
floor_noVARCHAR(16)所在楼层
locationVARCHAR(256)所在位置
descriptionTEXT设备描述
remarkTEXT备注
photo_urlVARCHAR(512)未改造前设备照片
created_atTIMESTAMPTZ创建时间
+ +
customer_account— 客户访问账号表(小程序登录)
+
+ + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
customer_idBIGINTFK客户 ID
phoneVARCHAR(20)UNIQUE登录电话
password_hashVARCHAR(256)密码哈希
wx_openidVARCHAR(128)微信 openid
statusSMALLINT状态:1启用 0禁用
last_login_atTIMESTAMPTZ最后登录时间
created_atTIMESTAMPTZ创建时间
+ + +
D3

智电设备域

1 张表 · 产品目录
+ +
smart_device— 鑫潼智电设备信息表(产品目录)
+
+ + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
device_modelVARCHAR(64)智电设备型号
device_nameVARCHAR(128)智电设备名称
device_specVARCHAR(256)智电设备规格
device_typeVARCHAR(32)设备类型:网关/采集器/探测器等
device_functionTEXT智电设备功能
supplierVARCHAR(128)供应商
created_atTIMESTAMPTZ创建时间
+ + +
D4

项目方案与合同域

3 张表 · 方案/明细/合同
+ +
project_plan— 智电改造方案信息表
+
+ + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
plan_noVARCHAR(64)UNIQUE方案单号
customer_idBIGINTFK客户 ID
registrarVARCHAR(64)方案登记人(业务员)
register_timeDATE方案登记时间
plan_descTEXT方案说明
plan_file_urlVARCHAR(512)方案最终文件
statusSMALLINT状态:0草稿 1已确认
created_atTIMESTAMPTZ创建时间
+ +
project_plan_detail— 项目方案智电设备明细表
+
+ + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
plan_idBIGINTFK方案 ID(→ project_plan)
customer_idBIGINTFK客户 ID
equip_idBIGINTFK客户设备 ID(→ customer_equipment)
smart_device_idBIGINTFK智电设备型号 ID(→ smart_device)
quantityNUMERIC(10,2)数量
remarkTEXT备注
created_atTIMESTAMPTZ创建时间
+ +
contract— 智电项目合同信息表
+
+ + + + + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
contract_noVARCHAR(64)UNIQUE合同单号
xt_contract_noVARCHAR(64)IDX鑫潼合同号(XT+年份+序号)
customer_idBIGINTFK客户 ID
contract_amountNUMERIC(14,2)合同金额
pay_cycleVARCHAR(32)支付周期:按月/按年
salesman_idBIGINT业务员
sign_dateDATE签订日期
contract_descTEXT合同说明
start_dateDATE开始日期
end_dateDATE结束日期
plan_idBIGINTFK方案 ID(→ project_plan)
contract_file_urlVARCHAR(512)签字盖章电子合同
statusSMALLINT状态:1有效 0终止
created_atTIMESTAMPTZ创建时间
+ + +
D5

线路与设备关联域

2 张表 · ICC 同步 + 房间关联
+ +
electric_line— 智电改造线路表(ICC 同步)
+
+ + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
line_codeVARCHAR(64)UNIQUE线路编码(唯一)
equip_idBIGINTFK客户设备 ID(→ customer_equipment)
icc_device_codeVARCHAR(128)IDXICC 智电设备编号
line_nameVARCHAR(128)线路名称(ICC 同步)
customer_idBIGINTFK客户 ID(逻辑隔离键)
building_idBIGINTFK所属建筑 ID
floor_noVARCHAR(16)所在楼层
locationVARCHAR(256)所在位置
statusSMALLINT状态:1在线 0离线
synced_atTIMESTAMPTZICC 同步时间
created_atTIMESTAMPTZ创建时间
+ +
line_room— 线路所属房间关联表
+
+ + + + + +
字段类型键说明
idBIGSERIALPK自增主键
line_idBIGINTFK线路 ID(→ electric_line)
room_idBIGINTFK房间 ID(→ customer_room)
customer_idBIGINTFK客户 ID
created_atTIMESTAMPTZ创建时间
+ + +
D6

派单域

4 张表 · 勘察/安装/验收/回访
+ +
survey_order— 客户勘察派单信息表
+
+ + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
survey_noVARCHAR(64)UNIQUE勘察单号
customer_idBIGINTFK客户 ID
surveyorVARCHAR(64)勘察人(共享电工)
worker_idBIGINT共享电工 ID
survey_timeTIMESTAMPTZ勘察时间
remarkTEXT备注
statusSMALLINT状态:0已派发 1已接单 2已完成
created_atTIMESTAMPTZ创建时间
+ +
install_order— 项目安装任务派单信息表
+
+ + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
install_noVARCHAR(64)UNIQUE安装单号
customer_idBIGINTFK客户 ID
installerVARCHAR(64)安装人(共享电工)
worker_idBIGINT共享电工 ID
dispatch_dateDATE派单日期
start_dateDATE开始日期
end_dateDATE结束日期
plan_idBIGINTFK方案 ID(→ project_plan)
remarkTEXT备注
statusSMALLINT状态:0已派发 1已完成
created_atTIMESTAMPTZ创建时间
+ +
acceptance— 项目验收信息表
+
+ + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
accept_noVARCHAR(64)UNIQUE验收单号
customer_idBIGINTFK客户 ID
acceptorVARCHAR(64)验收人
accept_opinionTEXT验收意见
accept_dateDATE验收日期
installerVARCHAR(64)安装人
accept_file_urlVARCHAR(512)验收文件电子版
contract_idBIGINTFK合同 ID(→ contract)
created_atTIMESTAMPTZ创建时间
+ +
visit_record— 客户回访信息表
+
+ + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
visit_noVARCHAR(64)UNIQUE回访单号
customer_idBIGINTFK客户 ID
contact_personVARCHAR(64)联系人
contact_phoneVARCHAR(20)电话
visitorVARCHAR(64)回访人
visit_timeDATE回访时间
customer_opinionTEXT客户意见
photosJSONB回访照片
created_atTIMESTAMPTZ创建时间
+ + +
D7

报警域

4 张表 · 故障类型/设备报警/报警派单
+ +
fault_type— 报警故障类型信息表
+
+ + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
fault_type_codeVARCHAR(64)UNIQUE故障类型编码
fault_type_nameVARCHAR(128)故障类型名称
descriptionTEXT故障类型说明
remarkTEXT备注
created_atTIMESTAMPTZ创建时间
+ +
fault_type_detail— 报警故障类型明细表
+
+ + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
fault_type_idBIGINTFK故障类型 ID(→ fault_type)
detail_categoryVARCHAR(32)明细分类:故障原因/处理流程
detail_descTEXT明细说明
remarkTEXT备注
created_atTIMESTAMPTZ创建时间
+ +
device_alarm— 设备报警信息表(ICC 拉取)
+
+ + + + + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
alarm_codeVARCHAR(128)UNIQUE报警编码
line_idBIGINTIDX线路 ID
equip_idBIGINT客户设备 ID
icc_device_codeVARCHAR(128)ICC 设备编号
line_nameVARCHAR(128)线路名称
customer_idBIGINTIDX客户 ID
building_idBIGINT所属建筑 ID
floor_noVARCHAR(16)所在楼层
locationVARCHAR(256)所在位置
alarm_dateDATEIDX报警日期
fault_typeVARCHAR(128)ICC 预警类型
alarm_valueNUMERIC(14,2)报警数值
history_countINT历史出现次数
synced_atTIMESTAMPTZICC 同步时间
+ +
alarm_dispatch— 报警派单信息表
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
dispatch_noVARCHAR(64)UNIQUE报警派单号
alarm_idBIGINTFK报警 ID(→ device_alarm)
line_idBIGINT线路 ID
equip_idBIGINT客户设备 ID
icc_device_codeVARCHAR(128)ICC 设备编号
line_nameVARCHAR(128)线路名称
customer_idBIGINTFK客户 ID
contact_personVARCHAR(64)联系人
contact_phoneVARCHAR(20)电话
building_idBIGINT建筑 ID
floor_noVARCHAR(16)所在楼层
locationVARCHAR(256)所在位置
fault_type_idBIGINTFK故障类型 ID(→ fault_type)
fault_descTEXT故障事项(类型+数值)
dispatcherVARCHAR(64)派发人
fault_reasonTEXT故障原因(来自明细)
process_flowTEXT处理流程(来自明细)
worker_idBIGINTIDX共享电工 ID
statusSMALLINTIDX状态:0已派发 1已处理
process_photosJSONB报警处理照片(多张)
ratingSMALLINT派单处理评分
created_atTIMESTAMPTZ创建时间
completed_atTIMESTAMPTZ完成时间
+ + +
D8

报修域

2 张表 · 报修单/报修派单
+ +
repair_request— 报修单信息表
+
+ + + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
repair_noVARCHAR(64)UNIQUE报修单号
customer_idBIGINTFK客户 ID
requesterVARCHAR(64)报修人
requester_phoneVARCHAR(20)报修电话
request_timeTIMESTAMPTZ报修时间
building_idBIGINT所属建筑 ID
floor_noVARCHAR(16)所在楼层
locationVARCHAR(256)所在位置
repair_contentTEXT报修内容
photosJSONB报修照片
statusSMALLINTIDX状态:0待处理 1已派单 2已完成
created_atTIMESTAMPTZ创建时间
+ +
repair_dispatch— 报修派单信息表
+
+ + + + + + + + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
dispatch_noVARCHAR(64)UNIQUE报修派单号
repair_idBIGINTFK报修单 ID(→ repair_request)
customer_idBIGINTFK客户 ID
requesterVARCHAR(64)报修人
requester_phoneVARCHAR(20)报修电话
request_timeTIMESTAMPTZ报修时间
building_idBIGINT建筑 ID
floor_noVARCHAR(16)所在楼层
locationVARCHAR(256)所在位置
repair_contentTEXT报修内容
dispatcherVARCHAR(64)派发人
worker_idBIGINTIDX共享电工 ID
statusSMALLINT状态:0已派发 1已处理
process_photosJSONB处理完成照片
ratingSMALLINT报修派单处理评分(客户评)
created_atTIMESTAMPTZ创建时间
completed_atTIMESTAMPTZ完成时间
+ + +
D9

维保域

2 张表 · 维保单/维保明细
+ +
maintenance_order— 维保单信息表
+
+ + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
maintenance_noVARCHAR(64)UNIQUE维保单号(录入,不自动生成)
customer_idBIGINTFK客户 ID
maintainerVARCHAR(64)维保人(共享电工)
worker_idBIGINT共享电工 ID
maintenance_dateDATE维保日期
remarkTEXT备注
statusSMALLINT状态:0已派发 1已完成
created_atTIMESTAMPTZ创建时间
+ +
maintenance_detail— 维保单明细表
+
+ + + + + + + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
maintenance_idBIGINTFK维保单 ID(→ maintenance_order)
equip_idBIGINT客户设备 ID
customer_idBIGINTFK客户 ID
equip_nameVARCHAR(128)客户设备名称
equip_typeVARCHAR(32)设备类型:配电间/井/箱
building_idBIGINT建筑 ID
floor_noVARCHAR(16)所在楼层
locationVARCHAR(256)所在位置
panel_conditionTEXT配电间或电箱情况
circuit_conditionTEXT回路情况
terminal_conditionTEXT终端情况
maintenance_descTEXT维保情况说明
photosJSONB维保现场照片(水印)
created_atTIMESTAMPTZ创建时间
+ + +
D10

共享电工域

4 张表 · 电工/价格/收入/明细
+ +
worker— 共享电工信息表
+
+ + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
worker_nameVARCHAR(64)电工姓名
phoneVARCHAR(20)IDX手机号
id_cardVARCHAR(18)身份证号
statusSMALLINT状态:1在岗 0离职
created_atTIMESTAMPTZ创建时间
updated_atTIMESTAMPTZ更新时间
+ +
dispatch_price— 共享电工派单价格信息表
+
+ + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
price_codeVARCHAR(64)UNIQUE派单价格编码
dispatch_typeVARCHAR(32)派单类型:勘察/安装/预警/维修/维保
price_typeVARCHAR(32)价格类型:单价/总价提成
unit_priceNUMERIC(10,2)服务单价
created_atTIMESTAMPTZ创建时间
+ +
worker_income— 共享电工派单收入信息表(月度汇总)
+
+ + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
income_codeVARCHAR(64)UNIQUE派单收入编码
worker_idBIGINTFK共享电工 ID(→ worker)
income_monthVARCHAR(7)IDX收入月份(YYYY-MM)
monthly_totalNUMERIC(14,2)月度收入合计
rating_summaryNUMERIC(5,2)评分综合统计
final_totalNUMERIC(14,2)月度最终收入合计
created_atTIMESTAMPTZ创建时间
+ +
worker_income_detail— 共享电工派单收入明细信息表
+
+ + + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
income_idBIGINTFK收入 ID(→ worker_income)
worker_idBIGINTFK共享电工 ID
income_monthVARCHAR(7)收入月份
dispatch_typeVARCHAR(32)派单类型
dispatch_noVARCHAR(64)各类型派单单号
income_amountNUMERIC(10,2)本单收入
rating_levelVARCHAR(16)评价等级
final_incomeNUMERIC(10,2)本单最终收入合计
created_atTIMESTAMPTZ创建时间
+ + +
D11

用电采集域(分区表)

1 张表 · 按月分区
+ +
electricity_data— 线路用电信息表(按月 RANGE 分区)
+
+ + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增(复合主键含 collect_time)
line_idBIGINTFKIDX线路 ID(→ electric_line)
line_codeVARCHAR(64)线路编码(冗余,加速查询)
equip_idBIGINT客户设备 ID
icc_device_codeVARCHAR(128)ICC 设备编号
line_nameVARCHAR(128)线路名称
customer_idBIGINTIDX客户 ID(逻辑隔离键)
electricityNUMERIC(14,4)用电量
collect_timeTIMESTAMPTZPKIDX采集时间(分区键)
+

分区键:collect_time。主键为复合主键 (id, collect_time)。已预建分区:electricity_data_2026_08 ~ electricity_data_2026_12。组合索引:(customer_id, collect_time) 用于月报统计。

+ + +
D12

辅助域

2 张表 · ICC 日志/文件记录
+ +
icc_sync_log— ICC 数据同步日志表
+
+ + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
sync_typeVARCHAR(32)IDX同步类型:客户/设备/用电/报警
start_timeTIMESTAMPTZ开始时间
end_timeTIMESTAMPTZ结束时间
record_countINT同步记录数
statusSMALLINT状态:1成功 0失败
error_messageTEXT错误信息
created_atTIMESTAMPTZIDX创建时间
+ +
file_record— 文件存储记录表
+
+ + + + + + + + + +
字段类型键说明
idBIGSERIALPK自增主键
file_nameVARCHAR(256)文件名
file_urlVARCHAR(512)文件 URL
file_typeVARCHAR(32)文件类型:取证/合同/验收/方案/月报
file_sizeBIGINT文件大小(字节)
business_typeVARCHAR(64)IDX关联业务类型
business_idBIGINT关联业务 ID
uploaded_byBIGINT上传人 ID
created_atTIMESTAMPTZ创建时间
+
+ + +
+
06

索引策略

关键索引一览
+ +
+ + + + + + + + + + + + + + +
表索引类型用途
customeridx_customer_nameB-Tree按客户名称搜索
customeridx_customer_regionB-Tree 组合按省市县区域查询
electric_lineidx_line_customerB-Tree按客户查线路(逻辑隔离)
electric_lineidx_line_iccB-TreeICC 设备码匹配同步
electricity_dataidx_ed_customer_timeB-Tree 组合按客户+时间查用电(月报核心查询)
device_alarmidx_alarm_dateB-Tree按报警日期查询
device_alarmidx_alarm_customerB-Tree按客户查报警
alarm_dispatchidx_alarm_disp_workerB-Tree电工查自己的派单(接口数据权限)
repair_dispatchidx_repair_disp_workerB-Tree电工查报修派单
sys_dictidx_sys_dict_typeB-Tree按字典类型查询
icc_sync_logidx_icc_sync_timeB-Tree按时间查同步日志
+ +
+

索引设计原则

+

① 所有 customer_id 外键字段建索引(逻辑隔离查询基础)。② 用电采集表建 (customer_id, collect_time) 组合索引(月报统计核心路径)。③ ICC 同步相关字段建索引(设备码匹配)。④ 共享电工查询字段建索引(接口数据权限过滤)。

+
+
+ + +
+
07

初始数据

角色/菜单/字典
+ +
+ + + + + + +
表初始数据说明
sys_role4 条运营管理员/业务员/共享电工/客户
sys_menu6 条客户管理/项目管理/运营派单/设备监控/统计报表/系统管理
sys_dict19 条客户状态/设备类型/派单状态/支付周期/派单类型等
+
+ + +
+
08

自动分区管理

pg_cron 定时调度 · 数据库自治
+ +
+

设计背景

+

PostgreSQL 声明式分区不会自动创建子分区。若数据写入时对应月份的分区不存在,INSERT 会直接报错:no partition of relation found for row。因此需要一套自动化的分区管理机制,确保未来月份的分区始终提前就绪。

+
+ +
+

为什么不用触发器兜底?

+

PostgreSQL 分区表的 BEFORE INSERT 触发器在分区路由之后才执行——即数据库先判断数据该落入哪个子分区,如果子分区不存在直接报错,触发器根本没有机会执行。因此触发器无法在写入时动态创建分区,必须通过定时预创建 + 应用层调用来保证分区就绪。

+
+ +

管理函数一览

+
+ + + + + + + + + + + + + + + + + + + + + +
函数名参数返回用途
ensure_electricity_partitionp_date DATEVOID确保指定日期所在月的分区存在(不存在则创建)。应用层在写入数据前调用此函数兜底。
run_electricity_partition_maintenancep_premake INT(默认 3)TABLE(created_month, action)批量预创建当前月 + 未来 N 个月的分区。Midway.js cron 每月调用一次,返回各分区状态(CREATED / EXISTS)。
refresh_electricity_monthly无VOID刷新月度汇总物化视图。自动处理首次初始化(非并发),后续刷新用 CONCURRENTLY 不阻塞查询。
+ +

分区生命周期

+
+flowchart LR + subgraph P1 ["阶段一:预创建(pg_cron 自动执行)"] + A1["pg_cron 定时调度
每月25号 03:00"] --> A2["run_electricity_partition_maintenance(3)"] + A2 --> A3{"分区是否
已存在?"} + A3 --"不存在"--> A4["CREATE TABLE ... PARTITION OF"] + A3 --"已存在"--> A5["跳过"] + end + subgraph P2 ["阶段二:写入数据"] + B1["ICC 数据拉取
服务写入数据"] --> B2["ensure_electricity_partition
(collect_time)"] + B2 --> B3{"分区是否
已存在?"} + B3 --"不存在"--> B4["自动创建分区"] + B3 --"已存在"--> B5["直接写入"] + B4 --> B5 + end + subgraph P3 ["阶段三:归档"] + C1["超过 24 个月的
分区"] --> C2["DETACH PARTITION
从父表分离"] + C2 --> C3["冷数据存档
或直接查询子表"] + C3 --> C4["如需分析
ATTACH 临时挂回"] + end + P1 --> P2 + P2 --> P3 + + style A4 fill:#e8f1ff,stroke:#1d6fff + style A5 fill:#f0fdf4,stroke:#0d9488 + style B4 fill:#e8f1ff,stroke:#1d6fff + style B5 fill:#f0fdf4,stroke:#0d9488 + style C2 fill:#fef3e8,stroke:#e0852b + style C4 fill:#fef3e8,stroke:#e0852b +
+ +

双重保障机制

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
保障层触发源调用函数频率角色
第一层:定时预创建pg_cron(数据库自治)run_electricity_partition_maintenance(3)每月 25 号 03:00主动保障 — 提前创建未来 3 个月分区
第一层:物化视图刷新pg_cron(数据库自治)refresh_electricity_monthly()每月 1 号 02:00月度汇总数据刷新
第二层:写入兜底Midway.js(应用层)ensure_electricity_partition(collect_time)每次 ICC 拉取批次被动兜底 — 万一分区不存在时写入前自动创建
+ +
+

pg_cron 定时任务配置

+

两个定时任务已在 db_init.sql 中通过 cron.schedule() 自动注册,数据库重启后任务仍然有效:

+

-- 每月25号凌晨3点:预创建未来3个月分区
+ SELECT cron.schedule('electricity_partition_maintenance', '0 3 25 * *', 'SELECT run_electricity_partition_maintenance(3)');

+ -- 每月1号凌晨2点:刷新物化视图
+ SELECT cron.schedule('electricity_monthly_refresh', '0 2 1 * *', 'SELECT refresh_electricity_monthly()');

+
+ +

Midway.js 应用层兜底示例

+

pg_cron 负责定时预创建,后端无需配置 @Cron。但 ICC 数据拉取服务在写入前仍建议调用 ensure_electricity_partition() 做兜底:

+
// ICC 数据写入前兜底(在 ICC 同步服务中)
+async saveElectricityData(items: ElectricityData[]) {
+  for (const item of items) {
+    // 写入前确保分区存在(99.9%的情况下什么都不做直接返回)
+    await this.entityManager.query(
+      'SELECT ensure_electricity_partition($1)',
+      [item.collectTime]
+    );
+  }
+  // 批量写入
+  await this.electricityRepo.save(items);
+}
+ +

月度汇总物化视图

+

物化视图 mv_electricity_monthly 按客户 + 线路 + 月份预聚合用电数据,用于历史数据分析和月报生成。每月初刷新一次,查询走预聚合数据,极快。

+
+ + + + + + + + + + + + + +
字段类型说明
customer_idBIGINT客户 ID(逻辑隔离键)
line_idBIGINT线路 ID
monthTEXT月份(YYYY-MM 格式)
total_usageNUMERIC月度总用电量
avg_usageNUMERIC平均用电量
peak_usageNUMERIC峰值用电量
min_usageNUMERIC最低用电量
record_countBIGINT采集记录数
first_collectTIMESTAMPTZ最早采集时间
last_collectTIMESTAMPTZ最晚采集时间
+ +
+

刷新策略

+

首次初始化:脚本执行时自动 REFRESH MATERIALIZED VIEW(非并发模式)。后续刷新:通过 refresh_electricity_monthly() 函数,使用 CONCURRENTLY 不阻塞查询,函数内部自动处理异常回退。

+
+ +

历史分区归档策略

+
+ + + + + + + + +
分区年龄操作原因
0 ~ 12 个月保持在父表上热数据,查询频繁
12 ~ 24 个月保持在父表上温数据,偶尔查询
超过 24 个月DETACH PARTITION 分离冷数据,减少父表子分区数量,加速分区裁剪
需要分析冷数据直接查子表 或 ATTACH 临时挂回按需分析,分析完再 DETACH
确定不再需要DROP TABLE释放磁盘空间(需确认)
+
+ + + +
+ + + + diff --git a/系统设计/xintong-architecture-full.html b/系统设计/xintong-architecture-full.html new file mode 100644 index 0000000..2455399 --- /dev/null +++ b/系统设计/xintong-architecture-full.html @@ -0,0 +1,864 @@ + + + + + + +鑫潼智电系统 · 完整架构设计 + + + +
+ + +
+ SYSTEM ARCHITECTURE · V1.0 +

鑫潼智电系统 · 实现架构设计

+

基于《鑫潼智电系统需求分析报告 V1.0》,按功能模块拆解为后端、前端、小程序、APP 的完整系统实现架构。覆盖 ICC 平台 API 对接、数据隔离、权限控制等关键设计决策。

+
+ 运营主体:鑫潼科技(单一运营方) + 后端:Midway.js + TypeORM + 前端:Vue3 + Ant Design Vue + APP/小程序:uni-app + 数据库:PostgreSQL +
+
+ + +
+
+ 01 +

架构总览框图

+ 五层架构 · 一图全局 +
+ +
+
运营管理平台 Web
+
共享电工 APP
+
客户微信小程序
+
大华 ICC 外部
+
后端服务 Midway
+
权限控制
+
+ +
+ + +
+
+ L1 + 客户端层 — 四端入口 + 不同角色访问不同端 +
+
+
+
PC
运营管理平台
Vue3 + Ant Design Vue
+
  • 用户:业务员 / 运营管理员
  • 客户/项目/派单/设备管理
  • 统计报表 / 月度报告
  • 系统管理(RBAC 菜单权限)
+
+
+
APP
共享电工 APP
uni-app → iOS / Android
+
  • 用户:共享电工
  • 五类派单接单处理
  • 现场水印拍照取证上传
  • 领料管理 / 报酬查询
+
+
+
WX
客户微信小程序
uni-app → 微信小程序
+
  • 用户:客户(学校/医院)
  • 设备状态 / 用电统计查看
  • 报警信息 / 月度报告下载
  • 水电报修 / 服务评分
+
+
+
ICC
大华 ICC 平台
外部系统 · API 对接
+
  • 设备数据采集平台(厂家)
  • 网关/探测器设备管理
  • 设备运行状态 / 报警
  • 用电采集数据
+
+
+
+ +
▼
+ + +
+
+ L2 + 接入网关层 — 统一入口 + 认证 · 路由 · 限流 · 数据权限拦截 +
+
+

统一鉴权

JWT 双 Token
多端统一登录态

+

API 路由分发

/admin · /worker
/customer 分组路由

+

接口数据权限

Interceptor 注入过滤
给什么数据看什么

+

限流与日志

rate-limiter
操作审计 winston

+
+
+ +
▼
+ + +
+
+ L3 + 业务服务层 — Midway.js(Node.js + TypeScript + TypeORM) + 10 个核心服务模块 +
+
+
👥

客户管理

客户/部门
建筑/房间
账号分配

+
📋

项目管理

勘察/方案
合同/安装
验收/回访

+
📡

设备管理

ICC 设备同步
线路分配
部门/房间映射

+
📮

派单管理

勘察/安装
报警/报修/维保
全生命周期

+
🔄

ICC 数据采集

定时拉取
队列缓冲
清洗映射

+
📊

统计报表

多维用电统计
月度报告生成
报酬计算

+
📸

取证文件

水印拍照取证
合同/验收存档
月报文件

+
🔔

消息通知

APP 推送
微信订阅消息
短信(按需)

+
⚙️

系统管理

用户/角色/菜单
数据字典
日志审计

+
🔐

认证授权

JWT 签发校验
微信 openid 登录
RBAC 权限

+
+
+ +
▼
+ + +
+
+ L4 + 数据层 — 存储与缓存 + 逻辑隔离 + 时序分区 · 不做物理分库 +
+
+
🐘

PostgreSQL 主库

业务数据(customer_id 逻辑隔离)
用电采集数据(按月分区)
TypeORM + 声明式分区

+
⚡

Redis 缓存

JWT 会话 Token
数据字典/热点配置
BullMQ 队列底层

+
📁

对象存储

取证照片(水印)
合同/验收/方案文件
月度报告文档

+
📬

BullMQ 消息队列

ICC 拉取数据缓冲
报警事件异步处理
基于 Redis 轻量级

+
+
+ +
▼
+ + +
+
+ L5 + 外部系统对接层 + 仅 API 对接 · 不直接操作硬件 +
+
+
+
ICC
大华 ICC OpenAPI
HTTP REST · 主动拉取
+
  • 客户信息接口 → 每小时/事件触发
  • 设备信息接口(网关/探测器)→ 每小时
  • 用电采集数据接口 → 5~15 分钟
  • 设备报警数据接口 → 1 分钟
+
+
+
WX
微信开放平台
小程序登录 + 订阅消息
+
  • openid 登录授权
  • 订阅消息推送
  • 客户账号绑定
+
+
+
SMS
短信服务
阿里云短信(可选)
+
  • 客户初始密码通知
  • 报警短信提醒
+
+
+
+ +
+ + +
+

🔒 权限控制方案(贯穿 L1 → L3)

+
+
运营后台(Web)— RBAC 菜单级
角色绑定菜单/按钮:运营管理员(全菜单)、业务员(客户/项目/方案/合同/回访)
+
共享电工 APP — 接口级数据权限
接口按 worker_id 过滤:仅自己接的派单、个人报酬明细,给什么看什么
+
客户小程序 — 接口级数据权限
接口按 customer_id 过滤:仅本客户设备/用电/报警/月报/报修记录
+
+
+
+ + +
+
+ 02 +

总体分层架构(详细)

+ 五层架构 · 自上而下数据流转 +
+ +
+ +
+
CLIENT客户端层 · 四端入口不同角色访问不同端,统一鉴权
+
+

运营管理平台(Web)

鑫潼内部人员使用:业务员、运营管理员

Vue3 + Ant Design Vue
+

共享电工 APP

共享电工接单、现场拍照取证、报酬查询

uni-app (iOS / Android)
+

客户微信小程序

客户查看用电/报警/月报、报修申请、评分

uni-app → 微信小程序
+

大华 ICC 管理平台

设备数据采集平台(厂家后台,非自研)

外部系统 · API 对接
+
+
+
▼
+ +
+
GATEWAY接入网关层 · 统一入口认证、路由、限流、日志
+
+

统一鉴权服务

JWT Token 签发与校验,多端统一登录态

Midway Guard + JWT
+

API 路由分发

按端区分路由前缀:/admin · /worker · /customer

Midway Controller 分组
+

接口级数据权限

接口决定可见数据范围,APP/小程序给什么看什么

Guard + Interceptor
+

限流与日志

常规 ERP 并发量,请求日志与操作审计

rate-limiter + winston
+
+
+
▼
+ +
+
SERVICE业务服务层 · Midway.js 核心模块按业务域划分,模块化服务
+
+

客户与项目模块

客户/部门/建筑/房间、勘察/方案/合同/验收/回访

Customer · Project Service
+

运营派单模块

报警派单、报修派单、维保派单、安装派单

Dispatch Service
+

设备与线路模块

设备信息同步、线路分配、设备状态管理

Device · Line Service
+

统计与报表模块

多维度用电统计、月度报告、共享电工报酬

Report Service
+
+
+

ICC 数据采集模块

定时拉取设备/报警/用电数据,缓冲写入

ICC Sync Service + 定时任务
+

取证与文件模块

现场拍照取证、合同/验收/方案文件存档

File Service + 对象存储
+

系统管理模块

用户、角色、菜单权限(RBAC)、字典、日志

System Service
+

消息通知模块

报警推送到 APP、派单提醒、微信消息

Notify Service
+
+
+
▼
+ +
+
DATA数据层 · 存储与缓存逻辑隔离 + 时序分区,不做物理分库
+
+

PostgreSQL 主库

业务数据(逻辑隔离)+ 用电采集数据(按月分区)

TypeORM + 声明式分区
+

Redis 缓存

会话 Token、数据字典、热点配置、限流计数

ioredis
+

对象存储

取证照片、合同/验收/方案电子文件、月度报告

MinIO / 阿里云 OSS
+

消息队列

ICC 拉取数据缓冲、报警事件异步处理

BullMQ (Redis 队列)
+
+
+
▼
+ +
+
EXTERNAL外部系统对接层仅 API 对接,不直接操作硬件
+
+

大华 ICC OpenAPI

设备信息、设备状态、报警数据、用电采集数据

HTTP REST · 主动拉取
+

微信开放平台

小程序登录授权(openid)、消息订阅推送

微信登录 + 订阅消息
+

短信服务(可选)

客户账号初始密码、报警短信通知(按需)

阿里云短信
+

智电终端设备

网关/探测器 → ICC 平台采集 → 鑫潼系统拉取

不直接对接
+
+
+
+ +
+

架构设计要点

+

① 不直接对接硬件:所有设备数据通过大华 ICC 平台 API 拉取,鑫潼系统只做数据消费方。② 四端共享一套后端:Midway.js 统一提供 /admin、/worker、/customer 三组路由,避免重复开发。③ ICC 内部虽实时推送,但鑫潼侧采用主动拉取,通过定时任务按采集频次拉取,进入 BullMQ 队列缓冲后写入分区表。

+
+
+ + +
+
+ 03 +

技术栈选型

+ 含数据库选型建议 +
+ +
+ + + + + + + + + + + + + + +
层级技术选型版本/说明选型理由
后端框架Midway.jsNode.js · IoC + TypeORM阿里出品,TypeScript 原生支持,装饰器风格,模块化清晰
前端框架Vue 3 + Ant Design VueComposition APIAntDV 组件丰富,适合中后台 ERP 型管理系统
APP 框架uni-app跨 iOS / Android一套代码多端发布,共享电工端仅需移动 App
小程序uni-app → 微信小程序客户端使用复用 uni-app 技术栈,降低学习成本
数据库PostgreSQL15+时序数据分区成熟、多维聚合分析强、JSONB 灵活(详见下方对比)
ORMTypeORMMidway 内置集成与 Midway 深度集成,支持 PostgreSQL 分区表
缓存Redis7.x会话、字典缓存、限流、BullMQ 队列底层
消息队列BullMQ基于 Redis轻量级,无需额外引入 Kafka,满足常规 ERP 并发
对象存储MinIO / 阿里云 OSS—取证照片、合同/验收文件、月度报告存档
鉴权JWT双 Token 刷新多端统一,无状态易扩展
+
+ +

数据库选型对比:PostgreSQL vs MySQL

+

基于本系统的数据特征(时序用电数据 + 多维统计报表 + ICC 灵活设备数据)进行对比。

+ +
+
+ 推荐 +

PostgreSQL

+
v15+ · 开源 · 对象关系型
+
    +
  • ✓声明式分区成熟,用电采集数据按月分区查询性能优
  • +
  • ✓窗口函数/CTE/物化视图,多维统计(部门/楼栋/楼层/房间)能力强
  • +
  • ✓JSONB 类型存储 ICC 灵活设备数据,支持索引查询
  • +
  • ✓Midway + TypeORM 完整支持,迁移成本低
  • +
  • ✓行级安全(RLS)可选,未来可做更细数据隔离
  • +
+
+
+

MySQL

+
v8.x · 开源 · 关系型
+
    +
  • △分区功能有但不如 PG 灵活,大表管理偏弱
  • +
  • △聚合分析能力可用,但复杂多维统计不如 PG
  • +
  • △JSON 类型有但查询和索引能力弱于 JSONB
  • +
  • ✓国内团队更熟悉,运维资源好找
  • +
  • △月度报告按部门/楼栋统计时性能不如 PG
  • +
+
+
+ +
+

结论:推荐 PostgreSQL

+

本系统最核心的数据特征是用电采集数据持续增长(时序)和月度报告多维统计。PostgreSQL 的声明式分区能直接解决您担心的"数据量暴涨"问题——按月分区后,单表数据被切分为多个物理子表,查询只扫描相关分区,性能稳定。配合物化视图预聚合月度统计,即使客户和设备数量增长,报表查询依然高效。

+
+
+ + +
+
+ 04 +

四端功能模块拆解

+ 后端 · 前端 · APP · 小程序 +
+ +
+
+
BE

后端服务(Midway.js)

四端共享 · 统一 API
+
+

客户管理服务

客户信息(省市县分类)、客户部门、客户访问账号分配

+

项目管理服务

建筑/房间、勘察派单、智电方案、合同、安装、验收、回访

+

设备管理服务

ICC 设备信息同步、智电设备/网关/探测器、线路分配(部门/房间)

+

派单管理服务

勘察/安装/报警/报修/维保五类派单全生命周期管理

+

ICC 数据采集服务

定时拉取设备/报警/用电数据,队列缓冲,写入分区表

+

统计报表服务

按部门/楼栋/楼层/房间维度用电统计,月度报告生成,报酬计算

+

取证文件服务

现场拍照取证(水印)、合同/验收/方案/月报文件存档

+

消息通知服务

APP 推送(报警/派单)、微信订阅消息、短信(按需)

+

系统管理服务

用户/角色/菜单(RBAC)、数据字典、操作日志、定时任务

+

认证授权服务

JWT 签发校验、微信 openid 登录、接口级数据权限拦截

+
+
+
+ +
+
+
PC

运营管理平台(Web 前端)

业务员 / 运营管理员
+
+

客户管理

客户录入(省市县)、部门、建筑/房间配置、访问账号分配

+

项目管理

勘察派单、方案制定上传、合同登记、安装派单、验收管理、回访

+

运营派单

报警派单(从 ICC 拉取报警)、报修派单、维保派单、评分管理

+

设备监控

设备列表、线路分配(部门/房间)、设备状态、报警查询

+

统计报表

多维度用电统计、月度报告生成下载、共享电工报酬统计

+

系统管理

用户/角色/菜单权限配置、数据字典、日志审计

+
+
+ +
+
APP

共享电工 APP(uni-app)

共享电工
+
+

派单接收

勘察/安装/报警/报修/维保五类派单推送与接单确认

+

领料管理

安装派单自动生成设备/耗材领用单,领料确认

+

现场处理

配电井/箱位置记录、按标准流程处置、水印拍照取证上传

+

任务提交

处理完成后照片上传、提交派单、查看历史任务

+

报酬查询

月度派单完成统计、单笔/月度收入明细查询

+

客户回访

业务员回访记录上传(拍照+意见)

+
+
+ +
+
WX

客户微信小程序(uni-app)

客户(学校/医院等)
+
+

设备状态查看

所属设备在线状态、线路用电实时数据

+

报警信息

设备报警记录查询、处理进度跟踪

+

用电统计

按部门/楼栋/楼层/房间多维度用电查询、节电情况

+

月度报告

当月用电统计查看、用电及改善报告文件下载

+

报修申请

水电问题报修提交(描述+拍照)、报修进度查询

+

服务评分

报修/维保派单完成后对共享电工评分

+
+
+ +
+
ICC

大华 ICC 平台(外部)

API 对接 · 非自研
+
+

客户信息接口

ICC 录入意向客户 → 推送到运营管理平台

+

设备信息接口

网关/探测器设备数据同步到运营管理平台

+

设备数据采集接口

用电实时数据按采集频次同步拉取

+

设备报警接口

设备运行状态、预警类型、报警数值拉取

+
+
+
+
+ + +
+
+ 05 +

ICC 平台对接架构

+ 主动拉取 · 队列缓冲 · 分区写入 +
+ +
+

对接方式说明

+

ICC 平台内部是实时推送机制,但鑫潼系统侧采用主动拉取(定时任务调 ICC OpenAPI)。ICC 接口文档待研究,当前按通用 REST API 模式设计,后续接入时按实际文档调整鉴权方式(AppKey/Token)和接口字段。

+
+ +
+flowchart LR + subgraph ICC ["大华 ICC 平台(外部)"] + A1["客户信息"] + A2["设备信息
网关/探测器"] + A3["用电采集数据
(按频次)"] + A4["设备报警数据"] + end + subgraph SYNC ["鑫潼 ICC 采集服务"] + T1["定时任务调度
不同频次拉取"] + Q1[("BullMQ 队列
数据缓冲")] + P1["数据清洗与映射
设备码 → 线路 → 客户"] + end + subgraph DB ["数据层"] + D1[("业务表
设备/线路/客户")] + D2[("用电采集分区表
按月分区")] + D3[("报警表")] + end + subgraph BIZ ["业务消费"] + B1["报警派单服务
实时推送 APP"] + B2["统计报表服务
多维聚合"] + B3["月度报告生成"] + end + A1 -->|"HTTP API 拉取"| T1 + A2 -->|"HTTP API 拉取"| T1 + A3 -->|"HTTP API 拉取
高频(如5分钟)"| T1 + A4 -->|"HTTP API 拉取
中频(如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 +
+ +
+ + + + + + + + +
拉取数据拉取频次(建议)写入目标消费方
客户信息事件触发 / 每小时业务表客户管理模块
设备信息(网关/探测器)每小时设备表 + 线路表设备管理模块
用电采集数据5~15 分钟(按 ICC 采集频次)用电采集分区表(按月)统计报表 / 月度报告
设备报警数据1 分钟报警表报警派单服务 → APP 推送
+
+
+ + +
+
+ 06 +

数据隔离方案

+ 逻辑隔离 + 时序分区 · 不做物理分库 +
+ +
+

核心决策:是否需要多租户 / 分库?

+

鑫潼是唯一运营主体,客户(学校/医院)是平台上的数据主体而非独立租户,因此不需要严格多租户架构。采用逻辑隔离(customer_id 行级隔离)+ 时序数据按月分区的混合方案,兼顾性能与运维成本。

+
+ +
+
+

业务数据 · 逻辑隔离 customer_id

+

客户、合同、派单、设备等业务表数据量有限,通过 customer_id 字段 + 索引隔离。

+
+
C001 客户A · 合同 · 派单 · 设备
+
C002 客户B · 合同 · 派单 · 设备
+
C003 客户C · 合同 · 派单 · 设备
+
... 共享同一张表,行级隔离
+
+
+
+

用电采集数据 · 按月分区 RANGE BY 采集时间

+

数据量增量大头。按采集时间分区,查询只扫描相关月份分区,性能稳定。

+
+
electricity_2026_05 · 分区子表
+
electricity_2026_06 · 分区子表
+
electricity_2026_07 · 分区子表
+
electricity_2026_08 · 分区子表(当前)
+
历史分区可归档,不影响热数据查询
+
+
+
+ +
+ + + + + + + + + +
方案是否采用适用数据理由
逻辑隔离(customer_id 行级)采用业务数据(客户/合同/派单/设备)数据量有限,索引隔离足够,支持跨客户运营统计
时序数据按月分区采用用电采集数据解决数据量暴涨问题,查询只扫相关分区
物理分库(每客户独立库)不采用—运维成本高,跨客户统计困难,无合规强制要求
独立 Schema 隔离不采用—增加复杂度但收益有限,逻辑隔离已满足
严格多租户(Tenant 模型)不采用—鑫潼是单一运营方,客户非独立租户
+
+ +
+

关于"数据量暴涨影响性能"的回应

+

数据增长的大头是用电采集数据(每条线路按采集频次持续写入),而非业务数据。业务数据(客户/合同/派单)即使客户增长到上千家,数据量仍在百万级以内,逻辑隔离 + 索引完全够用。用电采集数据通过按月分区 + 物化视图预聚合月度统计,即使积累数年数据,查询也只扫描目标月份分区,性能可控。不需要物理分库。

+
+
+ + +
+
+ 07 +

权限控制方案

+ 后台 RBAC 菜单级 · APP/小程序接口级 +
+ +
+

权限设计原则

+

现阶段权限粗放管理:运营后台采用 RBAC 菜单级权限(角色绑定菜单/按钮);APP 和小程序的数据权限由接口定义——接口返回什么数据,端上就只能看到什么数据,不在端上做复杂权限判断。

+
+ +

权限流转链路

+
+
STEP 1

用户登录

运营后台:账号密码
APP:账号密码
小程序:微信 openid

+
→
+
STEP 2

JWT 签发

Token 携带:userId、role、customerId
双 Token 刷新

+
→
+
STEP 3

菜单权限(后台)

RBAC:角色 → 菜单/按钮
后台根据角色渲染菜单

+
→
+
STEP 4

接口数据权限

接口根据 Token 身份过滤数据
APP/小程序给什么看什么

+
+ +

角色与权限矩阵

+
+ + + + + + + + +
角色使用端菜单权限(RBAC)接口数据权限
运营管理员运营管理平台全部菜单:客户/项目/派单/设备/报表/系统管理可查看所有客户数据,全量派单管理
业务员运营管理平台客户/项目/方案/合同/回访(按需配置)仅自己负责的客户(user_id 过滤)
共享电工共享电工 APP—(APP 无菜单权限,固定功能)仅分配给自己的派单、个人报酬明细
客户微信小程序—(小程序固定功能页)仅本客户设备/用电/报警/月报/报修记录
+
+ +
+

接口级数据权限实现要点

+

① 共享电工:查询派单接口自动按 worker_id = 当前用户 过滤;报酬接口只返回本人数据。② 客户:查询设备/用电/报警接口自动按 customer_id = 当前客户 过滤;报修只能提交本客户的。③ 业务员:查询客户/项目接口按 salesman_id = 当前用户 过滤(运营管理员不受限)。通过 Midway Interceptor 统一注入过滤条件,避免每个接口手动处理。

+
+
+ + +
+
+ 08 +

关键业务数据流

+ 端到端数据流转 +
+ +
+flowchart TB + subgraph PHASE1 ["项目阶段"] + P1["ICC 录入客户"] --> P2["运营平台配置建筑/房间"] + P2 --> P3["APP 勘察派单
现场拍照"] + P3 --> P4["运营平台制定方案"] + P4 --> P5["签订合同"] + P5 --> P6["APP 安装派单
设备配置指向 ICC"] + P6 --> P7["ICC 同步设备信息
到运营平台"] + 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 +
+
+ + +
+
+ 09 +

设计假设清单

+ 已采用的假设 · 如需调整请告知 +
+ +
+ + + + + + + + + + + + + + + + + + + + +
假设项假设内容来源
运营主体鑫潼为唯一运营方,不做白标输出用户确认
多租户不需要严格多租户,采用逻辑隔离用户确认 + 架构建议
分库不做物理分库,用电数据按月分区架构建议(解决数据量问题)
ICC 对接主动拉取(定时任务),非 ICC 推送用户确认
ICC 接口文档待研究,按通用 REST API 模式预留用户确认
后台权限RBAC 菜单级,现阶段粗放管理用户确认
APP/小程序数据权限接口定义数据范围,给什么看什么用户确认
APP 技术栈uni-app 跨 iOS/Android用户确认
客户小程序微信小程序,运营后台分配账号用户确认
子账号待定,当前按单账号预留扩展用户确认(待定)
电子签章不需要,但需取证拍照(水印)用户确认
并发量常规 ERP 级,无需特殊高并发架构用户确认
数据库PostgreSQL(架构建议,用户待定)架构建议
消息队列BullMQ(Redis 基础),不引入 Kafka架构建议(匹配并发量)
后端框架Midway.js + TypeORM用户确认
前端框架Vue3 + Ant Design Vue用户确认
+
+
+ + + +
+ + + + +