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 @@ + + + +
+ + +基于《鑫潼智电系统需求分析报告 V1.0》数据说明,设计 37 张数据表 + 3 个分区管理函数 + 1 个物化视图,覆盖客户、项目、设备、派单、报警、报修、维保、报酬、用电采集等全部业务域。采用逻辑隔离 + 时序分区方案。
+ +① 逻辑隔离:所有业务表携带 customer_id 字段实现行级数据隔离,不做物理分库。② 时序分区:用电采集数据(electricity_data)按月 RANGE 分区,解决数据量暴涨问题。③ 外键约束:核心关联表使用外键保证数据完整性。④ JSONB 灵活存储:照片路径列表、ICC 灵活字段使用 JSONB 类型。⑤ 全部表加 COMMENT:便于团队协作和 ORM 映射。
表名:snake_case 小写下划线,业务表无前缀,系统表 sys_ 前缀。主键:统一 BIGSERIAL 自增。时间字段:统一 TIMESTAMPTZ 带时区。布尔状态:用 SMALLINT(0/1)。编码字段:如 customer_no、plan_no 等业务编号用 VARCHAR(64)。
以下展示客户域→项目域→线路域→报警/报修域的核心关联路径,省略系统管理域和辅助域。
electricity_data 表是数据量增量大头(每条线路按 ICC 采集频次持续写入)。采用 PostgreSQL 声明式 RANGE 分区,按 collect_time 字段按月切分。查询只扫描目标月份分区,性能稳定。历史分区可归档,不影响热数据查询。
已归档
已归档
已归档
当前活跃
预创建
预创建
预创建
预创建
待创建
待创建
待创建
待创建
建议使用 pg_partman 扩展或编写定时任务(Midway.js cron)自动创建未来月份分区。每月初自动 CREATE 新分区,超过 N 个月的分区可 DETACH 后归档至冷存储。构建脚本已预创建 2026 年 8~12 月分区。
sys_user— 系统用户表(鑫潼内部人员)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
username | VARCHAR(64) | UNIQUE | 登录用户名 |
password_hash | VARCHAR(256) | 密码哈希值 | |
real_name | VARCHAR(64) | 真实姓名 | |
phone | VARCHAR(20) | 手机号 | |
email | VARCHAR(128) | 邮箱 | |
status | SMALLINT | 状态:1启用 0禁用 | |
last_login_at | TIMESTAMPTZ | 最后登录时间 | |
created_at | TIMESTAMPTZ | 创建时间 | |
updated_at | TIMESTAMPTZ | 更新时间 |
sys_role— 角色表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
role_code | VARCHAR(32) | UNIQUE | 角色编码 |
role_name | VARCHAR(64) | 角色名称 | |
description | TEXT | 描述 | |
sort_order | INT | 排序号 | |
created_at | TIMESTAMPTZ | 创建时间 |
sys_user_role— 用户角色关联表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
user_id | BIGINT | PKFK | 用户 ID(→ sys_user) |
role_id | BIGINT | PKFK | 角色 ID(→ sys_role) |
sys_menu— 菜单表(RBAC 权限)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
parent_id | BIGINT | FK | 父菜单 ID(自关联) |
menu_name | VARCHAR(64) | 菜单名称 | |
menu_path | VARCHAR(256) | 路由路径 | |
component | VARCHAR(256) | 前端组件路径 | |
icon | VARCHAR(64) | 图标 | |
sort_order | INT | 排序号 | |
menu_type | SMALLINT | 类型:1目录 2菜单 3按钮 | |
permission | VARCHAR(128) | 权限标识,如 customer:list | |
visible | SMALLINT | 是否可见:1是 0否 | |
created_at | TIMESTAMPTZ | 创建时间 |
sys_role_menu— 角色菜单关联表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
role_id | BIGINT | PKFK | 角色 ID(→ sys_role) |
menu_id | BIGINT | PKFK | 菜单 ID(→ sys_menu) |
sys_dict— 数据字典表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
dict_type | VARCHAR(64) | IDX | 字典类型 |
dict_label | VARCHAR(128) | 字典标签 | |
dict_value | VARCHAR(128) | 字典值 | |
sort_order | INT | 排序号 | |
status | SMALLINT | 状态:1启用 0禁用 | |
created_at | TIMESTAMPTZ | 创建时间 |
sys_operation_log— 操作日志表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
user_id | BIGINT | IDX | 操作用户 ID |
module | VARCHAR(64) | 操作模块 | |
action | VARCHAR(64) | 操作动作 | |
method | VARCHAR(256) | 请求方法 | |
params | TEXT | 请求参数 | |
ip | VARCHAR(64) | 操作 IP | |
created_at | TIMESTAMPTZ | IDX | 操作时间 |
customer— 客户信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
customer_name | VARCHAR(128) | IDX | 客户名称 |
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 | 状态:1意向 2签约 3运维中 4终止 | |
created_at | TIMESTAMPTZ | 创建时间 | |
updated_at | TIMESTAMPTZ | 更新时间 |
customer_department— 客户部门信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
customer_id | BIGINT | FK | 客户 ID(→ customer) |
dept_name | VARCHAR(128) | 部门名称 | |
contact_person | VARCHAR(64) | 联系人 | |
contact_phone | VARCHAR(20) | 电话 | |
function_desc | TEXT | 职能 | |
created_at | TIMESTAMPTZ | 创建时间 |
customer_visit— 客户拜访信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
visit_no | VARCHAR(64) | UNIQUE | 拜访单号 |
customer_id | BIGINT | FK | 客户 ID |
visit_person | VARCHAR(64) | 拜访人 | |
visit_phone | VARCHAR(20) | 拜访人电话 | |
visit_content | TEXT | 拜访内容 | |
visit_time | DATE | 拜访时间 | |
salesman_id | BIGINT | IDX | 业务员 |
will_plan | SMALLINT | 是否做方案:0不同意 1同意 | |
created_at | TIMESTAMPTZ | 创建时间 |
customer_building— 客户智电改造建筑信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
customer_id | BIGINT | FK | 客户 ID |
building_name | VARCHAR(128) | 建筑名称 | |
basic_info | TEXT | 基本情况 | |
purpose | VARCHAR(128) | 用途 | |
floor_count | INT | 层数 | |
created_at | TIMESTAMPTZ | 创建时间 |
customer_room— 客户建筑房间信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
room_code | VARCHAR(64) | 房间编码 | |
customer_id | BIGINT | FK | 客户 ID |
room_name | VARCHAR(128) | 房间名称 | |
building_id | BIGINT | FK | 所属建筑 ID(→ customer_building) |
floor_no | VARCHAR(16) | 所在楼层 | |
room_purpose | VARCHAR(128) | 房间用途 | |
dept_id | BIGINT | FK | 房间所属客户部门(→ customer_department) |
created_at | TIMESTAMPTZ | 创建时间 |
customer_equipment— 客户智电改造设备信息表(配电间/井/箱)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
equip_code | VARCHAR(64) | UNIQUE | 客户设备编码 |
customer_id | BIGINT | FK | 客户 ID |
equip_name | VARCHAR(128) | 客户设备名称 | |
equip_type | VARCHAR(32) | 设备类型:配电间/配电井/配电箱 | |
building_id | BIGINT | FK | 所属建筑 ID |
floor_no | VARCHAR(16) | 所在楼层 | |
location | VARCHAR(256) | 所在位置 | |
description | TEXT | 设备描述 | |
remark | TEXT | 备注 | |
photo_url | VARCHAR(512) | 未改造前设备照片 | |
created_at | TIMESTAMPTZ | 创建时间 |
customer_account— 客户访问账号表(小程序登录)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
customer_id | BIGINT | FK | 客户 ID |
phone | VARCHAR(20) | UNIQUE | 登录电话 |
password_hash | VARCHAR(256) | 密码哈希 | |
wx_openid | VARCHAR(128) | 微信 openid | |
status | SMALLINT | 状态:1启用 0禁用 | |
last_login_at | TIMESTAMPTZ | 最后登录时间 | |
created_at | TIMESTAMPTZ | 创建时间 |
smart_device— 鑫潼智电设备信息表(产品目录)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
device_model | VARCHAR(64) | 智电设备型号 | |
device_name | VARCHAR(128) | 智电设备名称 | |
device_spec | VARCHAR(256) | 智电设备规格 | |
device_type | VARCHAR(32) | 设备类型:网关/采集器/探测器等 | |
device_function | TEXT | 智电设备功能 | |
supplier | VARCHAR(128) | 供应商 | |
created_at | TIMESTAMPTZ | 创建时间 |
project_plan— 智电改造方案信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
plan_no | VARCHAR(64) | UNIQUE | 方案单号 |
customer_id | BIGINT | FK | 客户 ID |
registrar | VARCHAR(64) | 方案登记人(业务员) | |
register_time | DATE | 方案登记时间 | |
plan_desc | TEXT | 方案说明 | |
plan_file_url | VARCHAR(512) | 方案最终文件 | |
status | SMALLINT | 状态:0草稿 1已确认 | |
created_at | TIMESTAMPTZ | 创建时间 |
project_plan_detail— 项目方案智电设备明细表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
plan_id | BIGINT | FK | 方案 ID(→ project_plan) |
customer_id | BIGINT | FK | 客户 ID |
equip_id | BIGINT | FK | 客户设备 ID(→ customer_equipment) |
smart_device_id | BIGINT | FK | 智电设备型号 ID(→ smart_device) |
quantity | NUMERIC(10,2) | 数量 | |
remark | TEXT | 备注 | |
created_at | TIMESTAMPTZ | 创建时间 |
contract— 智电项目合同信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
contract_no | VARCHAR(64) | UNIQUE | 合同单号 |
xt_contract_no | VARCHAR(64) | IDX | 鑫潼合同号(XT+年份+序号) |
customer_id | BIGINT | FK | 客户 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 | FK | 方案 ID(→ project_plan) |
contract_file_url | VARCHAR(512) | 签字盖章电子合同 | |
status | SMALLINT | 状态:1有效 0终止 | |
created_at | TIMESTAMPTZ | 创建时间 |
electric_line— 智电改造线路表(ICC 同步)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
line_code | VARCHAR(64) | UNIQUE | 线路编码(唯一) |
equip_id | BIGINT | FK | 客户设备 ID(→ customer_equipment) |
icc_device_code | VARCHAR(128) | IDX | ICC 智电设备编号 |
line_name | VARCHAR(128) | 线路名称(ICC 同步) | |
customer_id | BIGINT | FK | 客户 ID(逻辑隔离键) |
building_id | BIGINT | FK | 所属建筑 ID |
floor_no | VARCHAR(16) | 所在楼层 | |
location | VARCHAR(256) | 所在位置 | |
status | SMALLINT | 状态:1在线 0离线 | |
synced_at | TIMESTAMPTZ | ICC 同步时间 | |
created_at | TIMESTAMPTZ | 创建时间 |
line_room— 线路所属房间关联表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
line_id | BIGINT | FK | 线路 ID(→ electric_line) |
room_id | BIGINT | FK | 房间 ID(→ customer_room) |
customer_id | BIGINT | FK | 客户 ID |
created_at | TIMESTAMPTZ | 创建时间 |
survey_order— 客户勘察派单信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
survey_no | VARCHAR(64) | UNIQUE | 勘察单号 |
customer_id | BIGINT | FK | 客户 ID |
surveyor | VARCHAR(64) | 勘察人(共享电工) | |
worker_id | BIGINT | 共享电工 ID | |
survey_time | TIMESTAMPTZ | 勘察时间 | |
remark | TEXT | 备注 | |
status | SMALLINT | 状态:0已派发 1已接单 2已完成 | |
created_at | TIMESTAMPTZ | 创建时间 |
install_order— 项目安装任务派单信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
install_no | VARCHAR(64) | UNIQUE | 安装单号 |
customer_id | BIGINT | FK | 客户 ID |
installer | VARCHAR(64) | 安装人(共享电工) | |
worker_id | BIGINT | 共享电工 ID | |
dispatch_date | DATE | 派单日期 | |
start_date | DATE | 开始日期 | |
end_date | DATE | 结束日期 | |
plan_id | BIGINT | FK | 方案 ID(→ project_plan) |
remark | TEXT | 备注 | |
status | SMALLINT | 状态:0已派发 1已完成 | |
created_at | TIMESTAMPTZ | 创建时间 |
acceptance— 项目验收信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
accept_no | VARCHAR(64) | UNIQUE | 验收单号 |
customer_id | BIGINT | FK | 客户 ID |
acceptor | VARCHAR(64) | 验收人 | |
accept_opinion | TEXT | 验收意见 | |
accept_date | DATE | 验收日期 | |
installer | VARCHAR(64) | 安装人 | |
accept_file_url | VARCHAR(512) | 验收文件电子版 | |
contract_id | BIGINT | FK | 合同 ID(→ contract) |
created_at | TIMESTAMPTZ | 创建时间 |
visit_record— 客户回访信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
visit_no | VARCHAR(64) | UNIQUE | 回访单号 |
customer_id | BIGINT | FK | 客户 ID |
contact_person | VARCHAR(64) | 联系人 | |
contact_phone | VARCHAR(20) | 电话 | |
visitor | VARCHAR(64) | 回访人 | |
visit_time | DATE | 回访时间 | |
customer_opinion | TEXT | 客户意见 | |
photos | JSONB | 回访照片 | |
created_at | TIMESTAMPTZ | 创建时间 |
fault_type— 报警故障类型信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
fault_type_code | VARCHAR(64) | UNIQUE | 故障类型编码 |
fault_type_name | VARCHAR(128) | 故障类型名称 | |
description | TEXT | 故障类型说明 | |
remark | TEXT | 备注 | |
created_at | TIMESTAMPTZ | 创建时间 |
fault_type_detail— 报警故障类型明细表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
fault_type_id | BIGINT | FK | 故障类型 ID(→ fault_type) |
detail_category | VARCHAR(32) | 明细分类:故障原因/处理流程 | |
detail_desc | TEXT | 明细说明 | |
remark | TEXT | 备注 | |
created_at | TIMESTAMPTZ | 创建时间 |
device_alarm— 设备报警信息表(ICC 拉取)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
alarm_code | VARCHAR(128) | UNIQUE | 报警编码 |
line_id | BIGINT | IDX | 线路 ID |
equip_id | BIGINT | 客户设备 ID | |
icc_device_code | VARCHAR(128) | ICC 设备编号 | |
line_name | VARCHAR(128) | 线路名称 | |
customer_id | BIGINT | IDX | 客户 ID |
building_id | BIGINT | 所属建筑 ID | |
floor_no | VARCHAR(16) | 所在楼层 | |
location | VARCHAR(256) | 所在位置 | |
alarm_date | DATE | IDX | 报警日期 |
fault_type | VARCHAR(128) | ICC 预警类型 | |
alarm_value | NUMERIC(14,2) | 报警数值 | |
history_count | INT | 历史出现次数 | |
synced_at | TIMESTAMPTZ | ICC 同步时间 |
alarm_dispatch— 报警派单信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
dispatch_no | VARCHAR(64) | UNIQUE | 报警派单号 |
alarm_id | BIGINT | FK | 报警 ID(→ device_alarm) |
line_id | BIGINT | 线路 ID | |
equip_id | BIGINT | 客户设备 ID | |
icc_device_code | VARCHAR(128) | ICC 设备编号 | |
line_name | VARCHAR(128) | 线路名称 | |
customer_id | BIGINT | FK | 客户 ID |
contact_person | VARCHAR(64) | 联系人 | |
contact_phone | VARCHAR(20) | 电话 | |
building_id | BIGINT | 建筑 ID | |
floor_no | VARCHAR(16) | 所在楼层 | |
location | VARCHAR(256) | 所在位置 | |
fault_type_id | BIGINT | FK | 故障类型 ID(→ fault_type) |
fault_desc | TEXT | 故障事项(类型+数值) | |
dispatcher | VARCHAR(64) | 派发人 | |
fault_reason | TEXT | 故障原因(来自明细) | |
process_flow | TEXT | 处理流程(来自明细) | |
worker_id | BIGINT | IDX | 共享电工 ID |
status | SMALLINT | IDX | 状态:0已派发 1已处理 |
process_photos | JSONB | 报警处理照片(多张) | |
rating | SMALLINT | 派单处理评分 | |
created_at | TIMESTAMPTZ | 创建时间 | |
completed_at | TIMESTAMPTZ | 完成时间 |
repair_request— 报修单信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
repair_no | VARCHAR(64) | UNIQUE | 报修单号 |
customer_id | BIGINT | FK | 客户 ID |
requester | VARCHAR(64) | 报修人 | |
requester_phone | VARCHAR(20) | 报修电话 | |
request_time | TIMESTAMPTZ | 报修时间 | |
building_id | BIGINT | 所属建筑 ID | |
floor_no | VARCHAR(16) | 所在楼层 | |
location | VARCHAR(256) | 所在位置 | |
repair_content | TEXT | 报修内容 | |
photos | JSONB | 报修照片 | |
status | SMALLINT | IDX | 状态:0待处理 1已派单 2已完成 |
created_at | TIMESTAMPTZ | 创建时间 |
repair_dispatch— 报修派单信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
dispatch_no | VARCHAR(64) | UNIQUE | 报修派单号 |
repair_id | BIGINT | FK | 报修单 ID(→ repair_request) |
customer_id | BIGINT | FK | 客户 ID |
requester | VARCHAR(64) | 报修人 | |
requester_phone | VARCHAR(20) | 报修电话 | |
request_time | TIMESTAMPTZ | 报修时间 | |
building_id | BIGINT | 建筑 ID | |
floor_no | VARCHAR(16) | 所在楼层 | |
location | VARCHAR(256) | 所在位置 | |
repair_content | TEXT | 报修内容 | |
dispatcher | VARCHAR(64) | 派发人 | |
worker_id | BIGINT | IDX | 共享电工 ID |
status | SMALLINT | 状态:0已派发 1已处理 | |
process_photos | JSONB | 处理完成照片 | |
rating | SMALLINT | 报修派单处理评分(客户评) | |
created_at | TIMESTAMPTZ | 创建时间 | |
completed_at | TIMESTAMPTZ | 完成时间 |
maintenance_order— 维保单信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
maintenance_no | VARCHAR(64) | UNIQUE | 维保单号(录入,不自动生成) |
customer_id | BIGINT | FK | 客户 ID |
maintainer | VARCHAR(64) | 维保人(共享电工) | |
worker_id | BIGINT | 共享电工 ID | |
maintenance_date | DATE | 维保日期 | |
remark | TEXT | 备注 | |
status | SMALLINT | 状态:0已派发 1已完成 | |
created_at | TIMESTAMPTZ | 创建时间 |
maintenance_detail— 维保单明细表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
maintenance_id | BIGINT | FK | 维保单 ID(→ maintenance_order) |
equip_id | BIGINT | 客户设备 ID | |
customer_id | BIGINT | FK | 客户 ID |
equip_name | VARCHAR(128) | 客户设备名称 | |
equip_type | VARCHAR(32) | 设备类型:配电间/井/箱 | |
building_id | BIGINT | 建筑 ID | |
floor_no | VARCHAR(16) | 所在楼层 | |
location | VARCHAR(256) | 所在位置 | |
panel_condition | TEXT | 配电间或电箱情况 | |
circuit_condition | TEXT | 回路情况 | |
terminal_condition | TEXT | 终端情况 | |
maintenance_desc | TEXT | 维保情况说明 | |
photos | JSONB | 维保现场照片(水印) | |
created_at | TIMESTAMPTZ | 创建时间 |
worker— 共享电工信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
worker_name | VARCHAR(64) | 电工姓名 | |
phone | VARCHAR(20) | IDX | 手机号 |
id_card | VARCHAR(18) | 身份证号 | |
status | SMALLINT | 状态:1在岗 0离职 | |
created_at | TIMESTAMPTZ | 创建时间 | |
updated_at | TIMESTAMPTZ | 更新时间 |
dispatch_price— 共享电工派单价格信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
price_code | VARCHAR(64) | UNIQUE | 派单价格编码 |
dispatch_type | VARCHAR(32) | 派单类型:勘察/安装/预警/维修/维保 | |
price_type | VARCHAR(32) | 价格类型:单价/总价提成 | |
unit_price | NUMERIC(10,2) | 服务单价 | |
created_at | TIMESTAMPTZ | 创建时间 |
worker_income— 共享电工派单收入信息表(月度汇总)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
income_code | VARCHAR(64) | UNIQUE | 派单收入编码 |
worker_id | BIGINT | FK | 共享电工 ID(→ worker) |
income_month | VARCHAR(7) | IDX | 收入月份(YYYY-MM) |
monthly_total | NUMERIC(14,2) | 月度收入合计 | |
rating_summary | NUMERIC(5,2) | 评分综合统计 | |
final_total | NUMERIC(14,2) | 月度最终收入合计 | |
created_at | TIMESTAMPTZ | 创建时间 |
worker_income_detail— 共享电工派单收入明细信息表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
income_id | BIGINT | FK | 收入 ID(→ worker_income) |
worker_id | BIGINT | FK | 共享电工 ID |
income_month | VARCHAR(7) | 收入月份 | |
dispatch_type | VARCHAR(32) | 派单类型 | |
dispatch_no | VARCHAR(64) | 各类型派单单号 | |
income_amount | NUMERIC(10,2) | 本单收入 | |
rating_level | VARCHAR(16) | 评价等级 | |
final_income | NUMERIC(10,2) | 本单最终收入合计 | |
created_at | TIMESTAMPTZ | 创建时间 |
electricity_data— 线路用电信息表(按月 RANGE 分区)| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增(复合主键含 collect_time) |
line_id | BIGINT | FKIDX | 线路 ID(→ electric_line) |
line_code | VARCHAR(64) | 线路编码(冗余,加速查询) | |
equip_id | BIGINT | 客户设备 ID | |
icc_device_code | VARCHAR(128) | ICC 设备编号 | |
line_name | VARCHAR(128) | 线路名称 | |
customer_id | BIGINT | IDX | 客户 ID(逻辑隔离键) |
electricity | NUMERIC(14,4) | 用电量 | |
collect_time | TIMESTAMPTZ | PKIDX | 采集时间(分区键) |
分区键:collect_time。主键为复合主键 (id, collect_time)。已预建分区:electricity_data_2026_08 ~ electricity_data_2026_12。组合索引:(customer_id, collect_time) 用于月报统计。
icc_sync_log— ICC 数据同步日志表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
sync_type | VARCHAR(32) | IDX | 同步类型:客户/设备/用电/报警 |
start_time | TIMESTAMPTZ | 开始时间 | |
end_time | TIMESTAMPTZ | 结束时间 | |
record_count | INT | 同步记录数 | |
status | SMALLINT | 状态:1成功 0失败 | |
error_message | TEXT | 错误信息 | |
created_at | TIMESTAMPTZ | IDX | 创建时间 |
file_record— 文件存储记录表| 字段 | 类型 | 键 | 说明 |
|---|---|---|---|
id | BIGSERIAL | PK | 自增主键 |
file_name | VARCHAR(256) | 文件名 | |
file_url | VARCHAR(512) | 文件 URL | |
file_type | VARCHAR(32) | 文件类型:取证/合同/验收/方案/月报 | |
file_size | BIGINT | 文件大小(字节) | |
business_type | VARCHAR(64) | IDX | 关联业务类型 |
business_id | BIGINT | 关联业务 ID | |
uploaded_by | BIGINT | 上传人 ID | |
created_at | TIMESTAMPTZ | 创建时间 |
| 表 | 索引 | 类型 | 用途 |
|---|---|---|---|
| customer | idx_customer_name | B-Tree | 按客户名称搜索 |
| customer | idx_customer_region | B-Tree 组合 | 按省市县区域查询 |
| electric_line | idx_line_customer | B-Tree | 按客户查线路(逻辑隔离) |
| electric_line | idx_line_icc | B-Tree | ICC 设备码匹配同步 |
| electricity_data | idx_ed_customer_time | B-Tree 组合 | 按客户+时间查用电(月报核心查询) |
| device_alarm | idx_alarm_date | B-Tree | 按报警日期查询 |
| device_alarm | idx_alarm_customer | B-Tree | 按客户查报警 |
| alarm_dispatch | idx_alarm_disp_worker | B-Tree | 电工查自己的派单(接口数据权限) |
| repair_dispatch | idx_repair_disp_worker | B-Tree | 电工查报修派单 |
| sys_dict | idx_sys_dict_type | B-Tree | 按字典类型查询 |
| icc_sync_log | idx_icc_sync_time | B-Tree | 按时间查同步日志 |
① 所有 customer_id 外键字段建索引(逻辑隔离查询基础)。② 用电采集表建 (customer_id, collect_time) 组合索引(月报统计核心路径)。③ ICC 同步相关字段建索引(设备码匹配)。④ 共享电工查询字段建索引(接口数据权限过滤)。
| 表 | 初始数据 | 说明 |
|---|---|---|
sys_role | 4 条 | 运营管理员/业务员/共享电工/客户 |
sys_menu | 6 条 | 客户管理/项目管理/运营派单/设备监控/统计报表/系统管理 |
sys_dict | 19 条 | 客户状态/设备类型/派单状态/支付周期/派单类型等 |
PostgreSQL 声明式分区不会自动创建子分区。若数据写入时对应月份的分区不存在,INSERT 会直接报错:no partition of relation found for row。因此需要一套自动化的分区管理机制,确保未来月份的分区始终提前就绪。
PostgreSQL 分区表的 BEFORE INSERT 触发器在分区路由之后才执行——即数据库先判断数据该落入哪个子分区,如果子分区不存在直接报错,触发器根本没有机会执行。因此触发器无法在写入时动态创建分区,必须通过定时预创建 + 应用层调用来保证分区就绪。
| 函数名 | 参数 | 返回 | 用途 |
|---|---|---|---|
ensure_electricity_partition |
+ p_date DATE |
+ VOID |
+ 确保指定日期所在月的分区存在(不存在则创建)。应用层在写入数据前调用此函数兜底。 | +
run_electricity_partition_maintenance |
+ p_premake INT(默认 3) |
+ TABLE(created_month, action) |
+ 批量预创建当前月 + 未来 N 个月的分区。Midway.js cron 每月调用一次,返回各分区状态(CREATED / EXISTS)。 | +
refresh_electricity_monthly |
+ 无 | +VOID |
+ 刷新月度汇总物化视图。自动处理首次初始化(非并发),后续刷新用 CONCURRENTLY 不阻塞查询。 |
+
| 保障层 | 触发源 | 调用函数 | 频率 | 角色 |
|---|---|---|---|---|
| 第一层:定时预创建 | +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 拉取批次 | +被动兜底 — 万一分区不存在时写入前自动创建 | +
两个定时任务已在 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()');
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_id | BIGINT | 客户 ID(逻辑隔离键) |
line_id | BIGINT | 线路 ID |
month | TEXT | 月份(YYYY-MM 格式) |
total_usage | NUMERIC | 月度总用电量 |
avg_usage | NUMERIC | 平均用电量 |
peak_usage | NUMERIC | 峰值用电量 |
min_usage | NUMERIC | 最低用电量 |
record_count | BIGINT | 采集记录数 |
first_collect | TIMESTAMPTZ | 最早采集时间 |
last_collect | TIMESTAMPTZ | 最晚采集时间 |
首次初始化:脚本执行时自动 REFRESH MATERIALIZED VIEW(非并发模式)。后续刷新:通过 refresh_electricity_monthly() 函数,使用 CONCURRENTLY 不阻塞查询,函数内部自动处理异常回退。
| 分区年龄 | 操作 | 原因 |
|---|---|---|
| 0 ~ 12 个月 | 保持在父表上 | 热数据,查询频繁 |
| 12 ~ 24 个月 | 保持在父表上 | 温数据,偶尔查询 |
| 超过 24 个月 | DETACH PARTITION 分离 | 冷数据,减少父表子分区数量,加速分区裁剪 |
| 需要分析冷数据 | 直接查子表 或 ATTACH 临时挂回 | 按需分析,分析完再 DETACH |
| 确定不再需要 | DROP TABLE | 释放磁盘空间(需确认) |
基于《鑫潼智电系统需求分析报告 V1.0》,按功能模块拆解为后端、前端、小程序、APP 的完整系统实现架构。覆盖 ICC 平台 API 对接、数据隔离、权限控制等关键设计决策。
+ +JWT 双 Token
多端统一登录态
/admin · /worker
/customer 分组路由
Interceptor 注入过滤
给什么数据看什么
rate-limiter
操作审计 winston
客户/部门
建筑/房间
账号分配
勘察/方案
合同/安装
验收/回访
ICC 设备同步
线路分配
部门/房间映射
勘察/安装
报警/报修/维保
全生命周期
定时拉取
队列缓冲
清洗映射
多维用电统计
月度报告生成
报酬计算
水印拍照取证
合同/验收存档
月报文件
APP 推送
微信订阅消息
短信(按需)
用户/角色/菜单
数据字典
日志审计
JWT 签发校验
微信 openid 登录
RBAC 权限
业务数据(customer_id 逻辑隔离)
用电采集数据(按月分区)
TypeORM + 声明式分区
JWT 会话 Token
数据字典/热点配置
BullMQ 队列底层
取证照片(水印)
合同/验收/方案文件
月度报告文档
ICC 拉取数据缓冲
报警事件异步处理
基于 Redis 轻量级
鑫潼内部人员使用:业务员、运营管理员
Vue3 + Ant Design Vue共享电工接单、现场拍照取证、报酬查询
uni-app (iOS / Android)客户查看用电/报警/月报、报修申请、评分
uni-app → 微信小程序设备数据采集平台(厂家后台,非自研)
外部系统 · API 对接JWT Token 签发与校验,多端统一登录态
Midway Guard + JWT按端区分路由前缀:/admin · /worker · /customer
Midway Controller 分组接口决定可见数据范围,APP/小程序给什么看什么
Guard + Interceptor常规 ERP 并发量,请求日志与操作审计
rate-limiter + winston客户/部门/建筑/房间、勘察/方案/合同/验收/回访
Customer · Project Service报警派单、报修派单、维保派单、安装派单
Dispatch Service设备信息同步、线路分配、设备状态管理
Device · Line Service多维度用电统计、月度报告、共享电工报酬
Report Service定时拉取设备/报警/用电数据,缓冲写入
ICC Sync Service + 定时任务现场拍照取证、合同/验收/方案文件存档
File Service + 对象存储用户、角色、菜单权限(RBAC)、字典、日志
System Service报警推送到 APP、派单提醒、微信消息
Notify Service业务数据(逻辑隔离)+ 用电采集数据(按月分区)
TypeORM + 声明式分区会话 Token、数据字典、热点配置、限流计数
ioredis取证照片、合同/验收/方案电子文件、月度报告
MinIO / 阿里云 OSSICC 拉取数据缓冲、报警事件异步处理
BullMQ (Redis 队列)设备信息、设备状态、报警数据、用电采集数据
HTTP REST · 主动拉取小程序登录授权(openid)、消息订阅推送
微信登录 + 订阅消息客户账号初始密码、报警短信通知(按需)
阿里云短信网关/探测器 → ICC 平台采集 → 鑫潼系统拉取
不直接对接① 不直接对接硬件:所有设备数据通过大华 ICC 平台 API 拉取,鑫潼系统只做数据消费方。② 四端共享一套后端:Midway.js 统一提供 /admin、/worker、/customer 三组路由,避免重复开发。③ ICC 内部虽实时推送,但鑫潼侧采用主动拉取,通过定时任务按采集频次拉取,进入 BullMQ 队列缓冲后写入分区表。
+| 层级 | 技术选型 | 版本/说明 | 选型理由 |
|---|---|---|---|
| 后端框架 | Midway.js | Node.js · IoC + TypeORM | 阿里出品,TypeScript 原生支持,装饰器风格,模块化清晰 |
| 前端框架 | Vue 3 + Ant Design Vue | Composition API | AntDV 组件丰富,适合中后台 ERP 型管理系统 |
| APP 框架 | uni-app | 跨 iOS / Android | 一套代码多端发布,共享电工端仅需移动 App |
| 小程序 | uni-app → 微信小程序 | 客户端使用 | 复用 uni-app 技术栈,降低学习成本 |
| 数据库 | PostgreSQL | 15+ | 时序数据分区成熟、多维聚合分析强、JSONB 灵活(详见下方对比) |
| ORM | TypeORM | Midway 内置集成 | 与 Midway 深度集成,支持 PostgreSQL 分区表 |
| 缓存 | Redis | 7.x | 会话、字典缓存、限流、BullMQ 队列底层 |
| 消息队列 | BullMQ | 基于 Redis | 轻量级,无需额外引入 Kafka,满足常规 ERP 并发 |
| 对象存储 | MinIO / 阿里云 OSS | — | 取证照片、合同/验收文件、月度报告存档 |
| 鉴权 | JWT | 双 Token 刷新 | 多端统一,无状态易扩展 |
基于本系统的数据特征(时序用电数据 + 多维统计报表 + ICC 灵活设备数据)进行对比。
+ +本系统最核心的数据特征是用电采集数据持续增长(时序)和月度报告多维统计。PostgreSQL 的声明式分区能直接解决您担心的"数据量暴涨"问题——按月分区后,单表数据被切分为多个物理子表,查询只扫描相关分区,性能稳定。配合物化视图预聚合月度统计,即使客户和设备数量增长,报表查询依然高效。
+客户信息(省市县分类)、客户部门、客户访问账号分配
建筑/房间、勘察派单、智电方案、合同、安装、验收、回访
ICC 设备信息同步、智电设备/网关/探测器、线路分配(部门/房间)
勘察/安装/报警/报修/维保五类派单全生命周期管理
定时拉取设备/报警/用电数据,队列缓冲,写入分区表
按部门/楼栋/楼层/房间维度用电统计,月度报告生成,报酬计算
现场拍照取证(水印)、合同/验收/方案/月报文件存档
APP 推送(报警/派单)、微信订阅消息、短信(按需)
用户/角色/菜单(RBAC)、数据字典、操作日志、定时任务
JWT 签发校验、微信 openid 登录、接口级数据权限拦截
客户录入(省市县)、部门、建筑/房间配置、访问账号分配
勘察派单、方案制定上传、合同登记、安装派单、验收管理、回访
报警派单(从 ICC 拉取报警)、报修派单、维保派单、评分管理
设备列表、线路分配(部门/房间)、设备状态、报警查询
多维度用电统计、月度报告生成下载、共享电工报酬统计
用户/角色/菜单权限配置、数据字典、日志审计
勘察/安装/报警/报修/维保五类派单推送与接单确认
安装派单自动生成设备/耗材领用单,领料确认
配电井/箱位置记录、按标准流程处置、水印拍照取证上传
处理完成后照片上传、提交派单、查看历史任务
月度派单完成统计、单笔/月度收入明细查询
业务员回访记录上传(拍照+意见)
所属设备在线状态、线路用电实时数据
设备报警记录查询、处理进度跟踪
按部门/楼栋/楼层/房间多维度用电查询、节电情况
当月用电统计查看、用电及改善报告文件下载
水电问题报修提交(描述+拍照)、报修进度查询
报修/维保派单完成后对共享电工评分
ICC 录入意向客户 → 推送到运营管理平台
网关/探测器设备数据同步到运营管理平台
用电实时数据按采集频次同步拉取
设备运行状态、预警类型、报警数值拉取
ICC 平台内部是实时推送机制,但鑫潼系统侧采用主动拉取(定时任务调 ICC OpenAPI)。ICC 接口文档待研究,当前按通用 REST API 模式设计,后续接入时按实际文档调整鉴权方式(AppKey/Token)和接口字段。
+| 拉取数据 | 拉取频次(建议) | 写入目标 | 消费方 |
|---|---|---|---|
| 客户信息 | 事件触发 / 每小时 | 业务表 | 客户管理模块 |
| 设备信息(网关/探测器) | 每小时 | 设备表 + 线路表 | 设备管理模块 |
| 用电采集数据 | 5~15 分钟(按 ICC 采集频次) | 用电采集分区表(按月) | 统计报表 / 月度报告 |
| 设备报警数据 | 1 分钟 | 报警表 | 报警派单服务 → APP 推送 |
鑫潼是唯一运营主体,客户(学校/医院)是平台上的数据主体而非独立租户,因此不需要严格多租户架构。采用逻辑隔离(customer_id 行级隔离)+ 时序数据按月分区的混合方案,兼顾性能与运维成本。
+客户、合同、派单、设备等业务表数据量有限,通过 customer_id 字段 + 索引隔离。
+数据量增量大头。按采集时间分区,查询只扫描相关月份分区,性能稳定。
+| 方案 | 是否采用 | 适用数据 | 理由 |
|---|---|---|---|
| 逻辑隔离(customer_id 行级) | 采用 | 业务数据(客户/合同/派单/设备) | 数据量有限,索引隔离足够,支持跨客户运营统计 |
| 时序数据按月分区 | 采用 | 用电采集数据 | 解决数据量暴涨问题,查询只扫相关分区 |
| 物理分库(每客户独立库) | 不采用 | — | 运维成本高,跨客户统计困难,无合规强制要求 |
| 独立 Schema 隔离 | 不采用 | — | 增加复杂度但收益有限,逻辑隔离已满足 |
| 严格多租户(Tenant 模型) | 不采用 | — | 鑫潼是单一运营方,客户非独立租户 |
数据增长的大头是用电采集数据(每条线路按采集频次持续写入),而非业务数据。业务数据(客户/合同/派单)即使客户增长到上千家,数据量仍在百万级以内,逻辑隔离 + 索引完全够用。用电采集数据通过按月分区 + 物化视图预聚合月度统计,即使积累数年数据,查询也只扫描目标月份分区,性能可控。不需要物理分库。
+现阶段权限粗放管理:运营后台采用 RBAC 菜单级权限(角色绑定菜单/按钮);APP 和小程序的数据权限由接口定义——接口返回什么数据,端上就只能看到什么数据,不在端上做复杂权限判断。
+运营后台:账号密码
APP:账号密码
小程序:微信 openid
Token 携带:userId、role、customerId
双 Token 刷新
RBAC:角色 → 菜单/按钮
后台根据角色渲染菜单
接口根据 Token 身份过滤数据
APP/小程序给什么看什么
| 角色 | 使用端 | 菜单权限(RBAC) | 接口数据权限 |
|---|---|---|---|
| 运营管理员 | 运营管理平台 | 全部菜单:客户/项目/派单/设备/报表/系统管理 | 可查看所有客户数据,全量派单管理 |
| 业务员 | 运营管理平台 | 客户/项目/方案/合同/回访(按需配置) | 仅自己负责的客户(user_id 过滤) |
| 共享电工 | 共享电工 APP | —(APP 无菜单权限,固定功能) | 仅分配给自己的派单、个人报酬明细 |
| 客户 | 微信小程序 | —(小程序固定功能页) | 仅本客户设备/用电/报警/月报/报修记录 |
① 共享电工:查询派单接口自动按 worker_id = 当前用户 过滤;报酬接口只返回本人数据。② 客户:查询设备/用电/报警接口自动按 customer_id = 当前客户 过滤;报修只能提交本客户的。③ 业务员:查询客户/项目接口按 salesman_id = 当前用户 过滤(运营管理员不受限)。通过 Midway Interceptor 统一注入过滤条件,避免每个接口手动处理。
| 假设项 | 假设内容 | 来源 |
|---|---|---|
| 运营主体 | 鑫潼为唯一运营方,不做白标输出 | 用户确认 |
| 多租户 | 不需要严格多租户,采用逻辑隔离 | 用户确认 + 架构建议 |
| 分库 | 不做物理分库,用电数据按月分区 | 架构建议(解决数据量问题) |
| ICC 对接 | 主动拉取(定时任务),非 ICC 推送 | 用户确认 |
| ICC 接口文档 | 待研究,按通用 REST API 模式预留 | 用户确认 |
| 后台权限 | RBAC 菜单级,现阶段粗放管理 | 用户确认 |
| APP/小程序数据权限 | 接口定义数据范围,给什么看什么 | 用户确认 |
| APP 技术栈 | uni-app 跨 iOS/Android | 用户确认 |
| 客户小程序 | 微信小程序,运营后台分配账号 | 用户确认 |
| 子账号 | 待定,当前按单账号预留扩展 | 用户确认(待定) |
| 电子签章 | 不需要,但需取证拍照(水印) | 用户确认 |
| 并发量 | 常规 ERP 级,无需特殊高并发架构 | 用户确认 |
| 数据库 | PostgreSQL(架构建议,用户待定) | 架构建议 |
| 消息队列 | BullMQ(Redis 基础),不引入 Kafka | 架构建议(匹配并发量) |
| 后端框架 | Midway.js + TypeORM | 用户确认 |
| 前端框架 | Vue3 + Ant Design Vue | 用户确认 |