feat(database): 新增数据库设计文档和PostgreSQL建表脚本
- 创建数据库结构文档 backend/design/db_schema.md - 创建PostgreSQL建表脚本 backend/design/db_schema_pgsql.sql - 定义租户与组织相关表结构 (sys_tenant, sys_org) - 定义用户与角色相关表结构 (sys_user, sys_role, sys_user_role, sys_tenant_user) - 定义权限字典参数表结构 (sys_permission, sys_dict_type, sys_dict_item, sys_param) - 定义日志与平台配置表结构 (sys_log, sys_platform_config) - 初始化基础字典数据和平台配置默认值 feat(frontend): 添加个人资料页面和密码重置功能 - 创建个人资料页面 frontend/src/pages/Profile.tsx - 添加密码重置页面 frontend/src/pages/ResetPassword.tsx - 实现用户信息编辑和密码修改功能 - 添加页面头部组件 frontend/src/components/shared/PageHeader - 实现分页工具函数 frontend/src/utils/pagination.ts feat(frontend): 添加字典数据缓存Hook - 创建useDict Hook用于字典数据获取缓存 frontend/src/hooks/useDict.ts - 实现字典数据请求去重和内存缓存机制master
parent
3f31ec0eb1
commit
f93d797382
|
|
@ -0,0 +1,223 @@
|
|||
# 数据库结构文档(PostgreSQL)
|
||||
|
||||
本文档根据 `backend/design/db_schema_pgsql.sql` 生成,描述当前核心表结构、字段、约束与索引。
|
||||
|
||||
## 0. 租户与组织
|
||||
|
||||
### 0.1 `sys_tenant`(租户表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 租户ID |
|
||||
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 租户编码 |
|
||||
| tenant_name | VARCHAR(128) | NOT NULL | 租户名称 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
||||
| expire_time | TIMESTAMP(6) | | 过期时间 |
|
||||
| contact_name | VARCHAR(64) | | 联系人 |
|
||||
| contact_phone | VARCHAR(32) | | 联系电话 |
|
||||
| remark | VARCHAR(255) | | 备注 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
|
||||
索引:
|
||||
- `uk_tenant_code`:`UNIQUE (tenant_code) WHERE is_deleted = FALSE`
|
||||
|
||||
### 0.2 `sys_org`(组织架构表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 组织ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||
| parent_id | BIGINT | | 父级组织ID |
|
||||
| org_name | VARCHAR(128) | NOT NULL | 组织名称 |
|
||||
| org_code | VARCHAR(64) | | 组织编码 |
|
||||
| org_path | VARCHAR(512) | | 组织路径 |
|
||||
| sort_order | INTEGER | DEFAULT 0 | 排序 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
|
||||
外键:
|
||||
- `fk_org_parent`:`parent_id -> sys_org(id)`
|
||||
- `fk_org_tenant`:`tenant_id -> sys_tenant(id)`
|
||||
|
||||
索引:
|
||||
- `idx_org_tenant`:`(tenant_id)`
|
||||
|
||||
## 1. 用户与角色
|
||||
|
||||
### 1.1 `sys_user`(用户表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| user_id | BIGSERIAL | PK | 用户ID |
|
||||
| username | VARCHAR(50) | NOT NULL, UNIQUE | 登录名 |
|
||||
| display_name | VARCHAR(50) | NOT NULL | 显示名 |
|
||||
| email | VARCHAR(100) | | 邮箱 |
|
||||
| phone | VARCHAR(30) | UNIQUE | 手机号 |
|
||||
| password_hash | VARCHAR(255) | NOT NULL | 密码哈希 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
||||
| pwd_reset_required | SMALLINT | DEFAULT 1 | 首次登录是否需改密 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| is_platform_admin | BOOLEAN | DEFAULT false | 是否平台管理员 |
|
||||
|
||||
索引:
|
||||
- `uk_user_username`:`UNIQUE (username) WHERE is_deleted = FALSE`
|
||||
|
||||
### 1.2 `sys_role`(角色表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| role_id | BIGSERIAL | PK | 角色ID |
|
||||
| tenant_id | BIGINT | | 租户ID |
|
||||
| role_code | VARCHAR(50) | NOT NULL, UNIQUE | 角色编码 |
|
||||
| role_name | VARCHAR(50) | NOT NULL | 角色名称 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
||||
| remark | TEXT | | 备注 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
索引:
|
||||
- `idx_sys_role_tenant`:`(tenant_id)`
|
||||
- `uk_role_code`:`UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE`
|
||||
|
||||
### 1.3 `sys_user_role`(用户-角色关联表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 关联ID |
|
||||
| tenant_id | BIGINT | | 租户ID |
|
||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
||||
| role_id | BIGINT | NOT NULL | 角色ID |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
唯一约束:
|
||||
- `UNIQUE (user_id, role_id)`
|
||||
|
||||
### 1.4 `sys_tenant_user`(租户成员关联表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 关联ID |
|
||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||
| org_id | BIGINT | | 组织ID |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
索引:
|
||||
- `uk_tenant_user`:`UNIQUE (user_id, tenant_id) WHERE is_deleted = 0`
|
||||
|
||||
## 2. 权限/字典/参数(全局共享)
|
||||
|
||||
### 2.1 `sys_permission`(权限表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| perm_id | BIGSERIAL | PK | 权限ID |
|
||||
| parent_id | BIGINT | | 父级权限ID |
|
||||
| name | VARCHAR(100) | NOT NULL | 权限名称 |
|
||||
| code | VARCHAR(100) | NOT NULL, UNIQUE | 权限编码 |
|
||||
| perm_type | VARCHAR(20) | NOT NULL | 权限类型 |
|
||||
| level | INTEGER | NOT NULL | 层级 |
|
||||
| path | VARCHAR(255) | | 路径 |
|
||||
| component | VARCHAR(255) | | 组件 |
|
||||
| icon | VARCHAR(100) | | 图标 |
|
||||
| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 排序 |
|
||||
| is_visible | SMALLINT | NOT NULL, DEFAULT 1 | 是否可见 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
||||
| description | TEXT | | 描述 |
|
||||
| meta | JSONB | | 扩展信息 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
|
||||
### 2.2 `sys_dict_type`(字典类型表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| dict_type_id | BIGSERIAL | PK | 类型ID |
|
||||
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 类型编码 |
|
||||
| type_name | VARCHAR(50) | NOT NULL | 类型名称 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| remark | TEXT | | 备注 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
|
||||
**初始化数据:**
|
||||
- `sys_common_status`: 通用状态 (启用/禁用)
|
||||
- `sys_permission_type`: 权限类型 (目录/菜单/按钮)
|
||||
- `sys_common_visibility`: 可见性 (显示/隐藏)
|
||||
- `sys_permission_level`: 权限层级 (1, 2, 3)
|
||||
- `sys_log_type`: 日志类型 (LOGIN/OPERATION)
|
||||
- `sys_param_type`: 参数类型 (String/Number/Boolean/JSON)
|
||||
- `sys_log_status`: 操作状态 (成功/失败)
|
||||
|
||||
### 2.3 `sys_dict_item`(字典项表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| dict_item_id | BIGSERIAL | PK | 字典项ID |
|
||||
| type_code | VARCHAR(50) | NOT NULL | 字典类型编码 |
|
||||
| item_label | VARCHAR(100) | NOT NULL | 展示文本 |
|
||||
| item_value | VARCHAR(100) | NOT NULL | 存储值 |
|
||||
| sort_order | INT | DEFAULT 0 | 排序 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
|
||||
索引:
|
||||
- `idx_dict_item_type`:`(type_code)`
|
||||
- `uk_dict_item_value`:`UNIQUE (type_code, item_value)`
|
||||
|
||||
### 2.4 `sys_param`(系统参数表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 参数ID |
|
||||
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 参数键 |
|
||||
| param_value | TEXT | NOT NULL | 参数值 |
|
||||
| param_type | VARCHAR(20) | NOT NULL | 参数类型 |
|
||||
| is_system | SMALLINT | DEFAULT 0 | 是否系统内置 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| description | TEXT | | 描述 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
|
||||
## 3. 日志(租户隔离)
|
||||
|
||||
### 3.1 `sys_log`(系统日志表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 日志ID |
|
||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 租户ID |
|
||||
| user_id | BIGINT | | 用户ID |
|
||||
| username | VARCHAR(50) | | 用户名 |
|
||||
| log_type | VARCHAR(20) | | 日志类型(如 LOGIN、OPERATION) |
|
||||
| operation | VARCHAR(100) | NOT NULL | 操作描述 |
|
||||
| method | VARCHAR(200) | | 方法 |
|
||||
| params | TEXT | | 请求参数 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| ip | VARCHAR(50) | | IP |
|
||||
| duration | BIGINT | | 耗时(ms) |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
|
||||
索引:
|
||||
- `idx_log_tenant_type`:`(tenant_id, log_type, created_at)`
|
||||
|
||||
## 4. 平台配置
|
||||
|
||||
### 4.1 `sys_platform_config`(平台管理表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGINT | PK | 固定为 1 |
|
||||
| project_name | VARCHAR(128) | NOT NULL | 项目名称 |
|
||||
| logo_url | VARCHAR(512) | | Logo URL |
|
||||
| icon_url | VARCHAR(512) | | Icon URL |
|
||||
| login_bg_url | VARCHAR(512) | | 登录页背景 |
|
||||
| icp_info | VARCHAR(128) | | 备案信息 |
|
||||
| copyright_info | VARCHAR(255) | | 版权信息 |
|
||||
| system_description | TEXT | | 系统描述 |
|
||||
| created_at | TIMESTAMP | NOT NULL | 创建时间 |
|
||||
| updated_at | TIMESTAMP | NOT NULL | 更新时间 |
|
||||
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
-- PostgreSQL Database Schema for iMeeting (Multi-tenant)
|
||||
-- 0 为系统预留租户 ID
|
||||
|
||||
-- ----------------------------
|
||||
-- 0. 租户与组织
|
||||
-- ----------------------------
|
||||
|
||||
-- 租户表
|
||||
CREATE TABLE sys_tenant (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_code VARCHAR(64) NOT NULL UNIQUE,
|
||||
tenant_name VARCHAR(128) NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
expire_time TIMESTAMP(6),
|
||||
contact_name VARCHAR(64),
|
||||
contact_phone VARCHAR(32),
|
||||
remark VARCHAR(255),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
is_deleted SMALLINT DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_tenant_code ON sys_tenant (tenant_code) WHERE is_deleted = FALSE;
|
||||
|
||||
-- 组织架构表
|
||||
DROP TABLE IF EXISTS sys_org CASCADE;
|
||||
|
||||
CREATE TABLE sys_org (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
parent_id BIGINT,
|
||||
org_name VARCHAR(128) NOT NULL,
|
||||
org_code VARCHAR(64),
|
||||
org_path VARCHAR(512),
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
status SMALLINT DEFAULT 1,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
is_deleted SMALLINT DEFAULT 0,
|
||||
CONSTRAINT fk_org_parent FOREIGN KEY (parent_id)
|
||||
REFERENCES sys_org(id),
|
||||
CONSTRAINT fk_org_tenant FOREIGN KEY (tenant_id)
|
||||
REFERENCES sys_tenant(id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_org_tenant ON sys_org (tenant_id);
|
||||
|
||||
-- ----------------------------
|
||||
-- 1. 用户与角色
|
||||
-- ----------------------------
|
||||
|
||||
-- 用户表
|
||||
DROP TABLE IF EXISTS sys_user CASCADE;
|
||||
|
||||
CREATE TABLE sys_user (
|
||||
user_id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
display_name VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(100),
|
||||
phone VARCHAR(30) UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
pwd_reset_required SMALLINT DEFAULT 1,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
is_platform_admin BOOLEAN DEFAULT false
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_user_username ON sys_user (username) WHERE is_deleted = FALSE;
|
||||
|
||||
-- 角色表
|
||||
DROP TABLE IF EXISTS sys_role CASCADE;
|
||||
|
||||
CREATE TABLE sys_role (
|
||||
role_id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT,
|
||||
role_code VARCHAR(50) NOT NULL UNIQUE,
|
||||
role_name VARCHAR(50) NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
remark TEXT,
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sys_role_tenant ON sys_role (tenant_id);
|
||||
CREATE UNIQUE INDEX uk_role_code ON sys_role (tenant_id, role_code) WHERE is_deleted = FALSE;
|
||||
|
||||
-- 用户-角色关联表 (无 tenant_id, 随 User/Role 隔离)
|
||||
DROP TABLE IF EXISTS sys_user_role CASCADE;
|
||||
|
||||
CREATE TABLE sys_user_role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
UNIQUE (user_id, role_id)
|
||||
);
|
||||
|
||||
-- ----------------------------
|
||||
-- 2. 权限/字典/参数 (全局共享, 无 tenant_id)
|
||||
-- ----------------------------
|
||||
|
||||
DROP TABLE IF EXISTS sys_permission CASCADE;
|
||||
|
||||
CREATE TABLE sys_permission (
|
||||
perm_id BIGSERIAL PRIMARY KEY,
|
||||
parent_id BIGINT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(100) NOT NULL UNIQUE,
|
||||
perm_type VARCHAR(20) NOT NULL,
|
||||
level INTEGER NOT NULL,
|
||||
path VARCHAR(255),
|
||||
component VARCHAR(255),
|
||||
icon VARCHAR(100),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_visible SMALLINT NOT NULL DEFAULT 1,
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
description TEXT,
|
||||
meta JSONB,
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS sys_tenant_user CASCADE;
|
||||
|
||||
CREATE TABLE sys_tenant_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
org_id BIGINT,
|
||||
status SMALLINT DEFAULT 1,
|
||||
is_deleted SMALLINT DEFAULT 0,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_tenant_user
|
||||
ON sys_tenant_user (user_id, tenant_id)
|
||||
WHERE is_deleted = 0;
|
||||
CREATE TABLE sys_dict_type (
|
||||
dict_type_id BIGSERIAL PRIMARY KEY,
|
||||
type_code VARCHAR(50) UNIQUE NOT NULL,
|
||||
type_name VARCHAR(50) NOT NULL,
|
||||
status SMALLINT DEFAULT 1,
|
||||
remark TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
is_deleted SMALLINT DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE sys_dict_item (
|
||||
dict_item_id BIGSERIAL PRIMARY KEY,
|
||||
type_code VARCHAR(50) NOT NULL,
|
||||
item_label VARCHAR(100) NOT NULL,
|
||||
item_value VARCHAR(100) NOT NULL,
|
||||
sort_order INT DEFAULT 0,
|
||||
status SMALLINT DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
is_deleted SMALLINT DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_dict_item_type ON sys_dict_item (type_code);
|
||||
CREATE UNIQUE INDEX uk_dict_item_value ON sys_dict_item (type_code, item_value);
|
||||
|
||||
CREATE TABLE sys_param (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
param_key VARCHAR(100) UNIQUE NOT NULL,
|
||||
param_value TEXT NOT NULL,
|
||||
param_type VARCHAR(20) NOT NULL,
|
||||
is_system SMALLINT DEFAULT 0,
|
||||
status SMALLINT DEFAULT 1,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
is_deleted SMALLINT DEFAULT 0
|
||||
);
|
||||
|
||||
-- ----------------------------
|
||||
-- 3. 日志 (租户隔离)
|
||||
-- ----------------------------
|
||||
|
||||
CREATE TABLE sys_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT,
|
||||
username VARCHAR(50),
|
||||
log_type VARCHAR(20), -- LOGIN, OPERATION
|
||||
operation VARCHAR(100) NOT NULL,
|
||||
method VARCHAR(200),
|
||||
params TEXT,
|
||||
status SMALLINT DEFAULT 1,
|
||||
ip VARCHAR(50),
|
||||
duration BIGINT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_log_tenant_type ON sys_log (tenant_id, log_type, created_at);
|
||||
|
||||
-- ----------------------------
|
||||
-- 4. 平台配置 (系统品牌化)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS sys_platform_config CASCADE;
|
||||
CREATE TABLE sys_platform_config (
|
||||
id BIGINT PRIMARY KEY, -- 固定为 1
|
||||
project_name VARCHAR(128) NOT NULL,
|
||||
logo_url VARCHAR(512),
|
||||
icon_url VARCHAR(512),
|
||||
login_bg_url VARCHAR(512),
|
||||
icp_info VARCHAR(128),
|
||||
copyright_info VARCHAR(255),
|
||||
system_description TEXT,
|
||||
created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
is_deleted SMALLINT DEFAULT 0
|
||||
);
|
||||
|
||||
INSERT INTO sys_platform_config (id, project_name, copyright_info)
|
||||
VALUES (1, 'iMeeting 智能会议系统', '© 2026 iMeeting Team. All rights reserved.');
|
||||
|
||||
-- ----------------------------
|
||||
-- 5. 基础初始化数据
|
||||
-- ----------------------------
|
||||
|
||||
-- 字典初始化数据
|
||||
-- sys_common_status
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_common_status', '通用状态', '0=禁用, 1=启用');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_status', '启用', '1', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_status', '禁用', '0', 2);
|
||||
|
||||
-- sys_permission_type
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_permission_type', '权限类型', 'directory=目录, menu=菜单, button=按钮');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '目录', 'directory', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '菜单', 'menu', 2);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '按钮', 'button', 3);
|
||||
|
||||
-- sys_common_visibility
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_common_visibility', '可见性', '0=隐藏, 1=显示');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_visibility', '显示', '1', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_visibility', '隐藏', '0', 2);
|
||||
|
||||
-- sys_permission_level
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_permission_level', '权限层级', '1=一级入口, 2=二级子项, 3=三级按钮');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '一级入口', '1', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '二级子项', '2', 2);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '三级按钮', '3', 3);
|
||||
|
||||
-- sys_log_type
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_type', '日志类型', 'LOGIN=登录, OPERATION=操作');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_type', '登录', 'LOGIN', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_type', '操作', 'OPERATION', 2);
|
||||
|
||||
-- sys_param_type
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_param_type', '参数类型', 'String, Number, Boolean, JSON');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'String', 'String', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'Number', 'Number', 2);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'Boolean', 'Boolean', 3);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'JSON', 'JSON', 4);
|
||||
|
||||
-- sys_log_status
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_status', '操作状态', '1=成功, 0=失败');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '成功', '1', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2);
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Typography, Space } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PageHeader: React.FC<PageHeaderProps> = ({ title, subtitle, extra, className = '' }) => {
|
||||
return (
|
||||
<div className={`page-header flex justify-between items-end mb-6 ${className}`}>
|
||||
<div>
|
||||
<Title level={4} className="mb-1" style={{ margin: 0 }}>
|
||||
{title}
|
||||
</Title>
|
||||
{subtitle && (
|
||||
<Text type="secondary" style={{ display: 'block' }}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{extra && <div className="page-header-extra">{extra}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageHeader;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { fetchDictItemsByTypeCode } from '../api/dict';
|
||||
import { SysDictItem } from '../types';
|
||||
|
||||
const dictCache: Record<string, SysDictItem[]> = {};
|
||||
const pendingRequests: Record<string, Promise<SysDictItem[]>[]> = {};
|
||||
|
||||
export function useDict(typeCode: string) {
|
||||
const [items, setItems] = useState<SysDictItem[]>(dictCache[typeCode] || []);
|
||||
const [loading, setLoading] = useState(!dictCache[typeCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dictCache[typeCode]) {
|
||||
setItems(dictCache[typeCode]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let isMounted = true;
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const data = await fetchDictItemsByTypeCode(typeCode);
|
||||
if (isMounted) {
|
||||
dictCache[typeCode] = data;
|
||||
setItems(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to fetch dictionary ${typeCode}:`, e);
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [typeCode]);
|
||||
|
||||
return { items, loading };
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Tabs,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Space,
|
||||
Avatar
|
||||
} from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getCurrentUser, updateMyProfile, updateMyPassword } from "../api";
|
||||
import { UserOutlined, LockOutlined, SaveOutlined, SolutionOutlined } from "@ant-design/icons";
|
||||
import type { UserProfile } from "../types";
|
||||
import PageHeader from "../components/shared/PageHeader";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Profile() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [user, setUser] = useState<UserProfile | null>(null);
|
||||
const [profileForm] = Form.useForm();
|
||||
const [pwdForm] = Form.useForm();
|
||||
|
||||
const loadUser = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getCurrentUser();
|
||||
setUser(data);
|
||||
profileForm.setFieldsValue(data);
|
||||
} catch (e) {
|
||||
// Interceptor handles error
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
const handleUpdateProfile = async () => {
|
||||
try {
|
||||
const values = await profileForm.validateFields();
|
||||
setSaving(true);
|
||||
await updateMyProfile(values);
|
||||
message.success(t('common.success'));
|
||||
loadUser();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePassword = async () => {
|
||||
try {
|
||||
const values = await pwdForm.validateFields();
|
||||
setSaving(true);
|
||||
await updateMyPassword(values);
|
||||
message.success(t('common.success'));
|
||||
pwdForm.resetFields();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl mx-auto">
|
||||
<PageHeader
|
||||
title="个人中心"
|
||||
subtitle="管理您的个人基础信息及账号安全设置"
|
||||
/>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Card className="text-center shadow-sm">
|
||||
<Avatar size={80} icon={<UserOutlined />} style={{ backgroundColor: '#1677ff', marginBottom: 16 }} />
|
||||
<Title level={5} style={{ margin: 0 }}>{user?.displayName}</Title>
|
||||
<Text type="secondary">@{user?.username}</Text>
|
||||
<div className="mt-4">
|
||||
{user?.isPlatformAdmin ? <Tag color="gold">平台管理员</Tag> : <Tag color="blue">普通用户</Tag>}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={16}>
|
||||
<Card className="shadow-sm">
|
||||
<Tabs defaultActiveKey="basic">
|
||||
<Tabs.TabPane
|
||||
tab={<span><SolutionOutlined />基本信息</span>}
|
||||
key="basic"
|
||||
>
|
||||
<Form form={profileForm} layout="vertical" onFinish={handleUpdateProfile} style={{ marginTop: 16 }}>
|
||||
<Form.Item label="显示姓名" name="displayName" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="邮箱" name="email">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="手机号" name="phone">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => profileForm.submit()}>
|
||||
保存修改
|
||||
</Button>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane
|
||||
tab={<span><LockOutlined />安全设置</span>}
|
||||
key="password"
|
||||
>
|
||||
<Form form={pwdForm} layout="vertical" onFinish={handleUpdatePassword} style={{ marginTop: 16 }}>
|
||||
<Form.Item label="旧密码" name="oldPassword" rules={[{ required: true }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item label="新密码" name="newPassword" rules={[{ required: true, min: 6 }]}>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="确认新密码"
|
||||
name="confirmPassword"
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
<Button type="primary" danger loading={saving} onClick={() => pwdForm.submit()}>
|
||||
修改密码
|
||||
</Button>
|
||||
</Form>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
import { Tag } from "antd";
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { Button, Card, Form, Input, message, Typography, Layout } from "antd";
|
||||
import { useState } from "react";
|
||||
import { updateMyPassword } from "../api";
|
||||
import { LockOutlined, LogoutOutlined } from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function ResetPassword() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateMyPassword({
|
||||
oldPassword: values.oldPassword,
|
||||
newPassword: values.newPassword
|
||||
});
|
||||
message.success("密码修改成功,请重新登录");
|
||||
// 清理并重新登录
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
navigate("/login");
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh', background: '#f0f2f5', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Card style={{ width: 400, borderRadius: 8, boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}>
|
||||
<div className="text-center mb-6">
|
||||
<LockOutlined style={{ fontSize: 40, color: '#1890ff' }} />
|
||||
<Title level={3} style={{ marginTop: 16 }}>强制修改密码</Title>
|
||||
<Text type="secondary">为了您的账户安全,首次登录或密码被重置后需要修改密码方可继续使用系统。</Text>
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item label="当前初始密码" name="oldPassword" rules={[{ required: true }]}>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="设置新密码" name="newPassword" rules={[{ required: true, min: 6 }]}>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="确认新密码"
|
||||
name="confirmPassword"
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} />
|
||||
</Form.Item>
|
||||
|
||||
<Button type="primary" htmlType="submit" block size="large" loading={loading} style={{ marginTop: 8 }}>
|
||||
立即修改并重新登录
|
||||
</Button>
|
||||
|
||||
<Button type="link" block icon={<LogoutOutlined />} onClick={handleLogout} style={{ marginTop: 8 }}>
|
||||
退出登录
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ import {
|
|||
MinusCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import PageHeader from "../components/shared/PageHeader";
|
||||
import {SysOrg, SysRole, SysTenant, SysUser} from "../types";
|
||||
import { getStandardPagination } from "../utils/pagination";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
|
@ -313,7 +314,7 @@ export default function Users() {
|
|||
<div>
|
||||
<Space size={4}>
|
||||
<div className="user-display-name">{record.displayName}</div>
|
||||
{record.isPlatformAdmin && <Tag color="gold" size="small" style={{ fontSize: 10 }}>{t('users.platformAdmin')}</Tag>}
|
||||
{record.isPlatformAdmin && <Tag color="gold" style={{ fontSize: 10 }}>{t('users.platformAdmin')}</Tag>}
|
||||
</Space>
|
||||
<div className="user-username tabular-nums">@{record.username}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { TablePaginationConfig } from 'antd';
|
||||
import i18n from '../i18n';
|
||||
|
||||
/**
|
||||
* Returns a standardized Ant Design pagination configuration.
|
||||
*/
|
||||
export const getStandardPagination = (
|
||||
total: number,
|
||||
current: number,
|
||||
pageSize: number,
|
||||
onChange?: (page: number, size: number) => void
|
||||
): TablePaginationConfig => {
|
||||
return {
|
||||
total,
|
||||
current,
|
||||
pageSize,
|
||||
onChange,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (totalCount) => i18n.t('common.total', { total: totalCount }),
|
||||
pageSizeOptions: ['10', '20', '50', '100'],
|
||||
size: 'middle',
|
||||
position: ['bottomRight']
|
||||
};
|
||||
};
|
||||
Loading…
Reference in New Issue