Compare commits
24 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
2b1d7b8a2e | |
|
|
af5c658bd9 | |
|
|
d8bfdb21fa | |
|
|
eaed89c9ec | |
|
|
364e49b3df | |
|
|
86b3616786 | |
|
|
d554849e8e | |
|
|
c60db64578 | |
|
|
430459c331 | |
|
|
61da050438 | |
|
|
0ccf0aa87d | |
|
|
11ab76f2ed | |
|
|
423327c61d | |
|
|
5c47b07ec6 | |
|
|
afff8a8d07 | |
|
|
35396104a0 | |
|
|
37025d3f02 | |
|
|
80a4682757 | |
|
|
eaadc4ee51 | |
|
|
5e4a2aa2d1 | |
|
|
70e739818a | |
|
|
f6ba04ebd8 | |
|
|
21b3ab3afc | |
|
|
1a392d96b9 |
|
|
@ -1,223 +1,368 @@
|
||||||
# 数据库结构文档(PostgreSQL)
|
# 鏁版嵁搴撶粨鏋勬枃妗o紙PostgreSQL锛?
|
||||||
|
|
||||||
本文档根据 `backend/design/db_schema_pgsql.sql` 生成,描述当前核心表结构、字段、约束与索引。
|
鏈枃妗f牴鎹?`backend/design/db_schema_pgsql.sql` 鐢熸垚锛屾弿杩板綋鍓嶆牳蹇冭〃缁撴瀯銆佸瓧娈点€佺害鏉熶笌绱㈠紩銆?
|
||||||
|
|
||||||
## 0. 租户与组织
|
## 0. 绉熸埛涓庣粍缁?
|
||||||
|
|
||||||
### 0.1 `sys_tenant`(租户表)
|
### 0.1 `sys_tenant`锛堢鎴疯〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 租户ID |
|
| id | BIGSERIAL | PK | 绉熸埛ID |
|
||||||
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 租户编码 |
|
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 绉熸埛缂栫爜 |
|
||||||
| tenant_name | VARCHAR(128) | NOT NULL | 租户名称 |
|
| tenant_name | VARCHAR(128) | NOT NULL | 绉熸埛鍚嶇О |
|
||||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
|
||||||
| expire_time | TIMESTAMP(6) | | 过期时间 |
|
| expire_time | TIMESTAMP(6) | | 杩囨湡鏃堕棿 |
|
||||||
| contact_name | VARCHAR(64) | | 联系人 |
|
| contact_name | VARCHAR(64) | | 鑱旂郴浜?|
|
||||||
| contact_phone | VARCHAR(32) | | 联系电话 |
|
| contact_phone | VARCHAR(32) | | 鑱旂郴鐢佃瘽 |
|
||||||
| remark | VARCHAR(255) | | 备注 |
|
| remark | VARCHAR(255) | | 澶囨敞 |
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
|
|
||||||
索引:
|
绱㈠紩锛?
|
||||||
- `uk_tenant_code`:`UNIQUE (tenant_code) WHERE is_deleted = FALSE`
|
- `uk_tenant_code`锛歚UNIQUE (tenant_code) WHERE is_deleted = FALSE`
|
||||||
|
|
||||||
### 0.2 `sys_org`(组织架构表)
|
### 0.2 `sys_org`锛堢粍缁囨灦鏋勮〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 组织ID |
|
| id | BIGSERIAL | PK | 缁勭粐ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
| parent_id | BIGINT | | 父级组织ID |
|
| parent_id | BIGINT | | 鐖剁骇缁勭粐ID |
|
||||||
| org_name | VARCHAR(128) | NOT NULL | 组织名称 |
|
| org_name | VARCHAR(128) | NOT NULL | 缁勭粐鍚嶇О |
|
||||||
| org_code | VARCHAR(64) | | 组织编码 |
|
| org_code | VARCHAR(64) | | 缁勭粐缂栫爜 |
|
||||||
| org_path | VARCHAR(512) | | 组织路径 |
|
| org_path | VARCHAR(512) | | 缁勭粐璺緞 |
|
||||||
| sort_order | INTEGER | DEFAULT 0 | 排序 |
|
| sort_order | INTEGER | DEFAULT 0 | 鎺掑簭 |
|
||||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
|
|
||||||
外键:
|
澶栭敭锛?
|
||||||
- `fk_org_parent`:`parent_id -> sys_org(id)`
|
- `fk_org_parent`锛歚parent_id -> sys_org(id)`
|
||||||
- `fk_org_tenant`:`tenant_id -> sys_tenant(id)`
|
- `fk_org_tenant`锛歚tenant_id -> sys_tenant(id)`
|
||||||
|
|
||||||
索引:
|
绱㈠紩锛?
|
||||||
- `idx_org_tenant`:`(tenant_id)`
|
- `idx_org_tenant`锛歚(tenant_id)`
|
||||||
|
|
||||||
## 1. 用户与角色
|
## 1. 鐢ㄦ埛涓庤鑹?
|
||||||
|
|
||||||
### 1.1 `sys_user`(用户表)
|
### 1.1 `sys_user`锛堢敤鎴疯〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| user_id | BIGSERIAL | PK | 用户ID |
|
| user_id | BIGSERIAL | PK | 鐢ㄦ埛ID |
|
||||||
| username | VARCHAR(50) | NOT NULL, UNIQUE | 登录名 |
|
| username | VARCHAR(50) | NOT NULL, UNIQUE | 鐧诲綍鍚?|
|
||||||
| display_name | VARCHAR(50) | NOT NULL | 显示名 |
|
| display_name | VARCHAR(50) | NOT NULL | 鏄剧ず鍚?|
|
||||||
| email | VARCHAR(100) | | 邮箱 |
|
| email | VARCHAR(100) | | 閭 |
|
||||||
| phone | VARCHAR(30) | UNIQUE | 手机号 |
|
| phone | VARCHAR(30) | UNIQUE | 鎵嬫満鍙?|
|
||||||
| password_hash | VARCHAR(255) | NOT NULL | 密码哈希 |
|
| password_hash | VARCHAR(255) | NOT NULL | 瀵嗙爜鍝堝笇 |
|
||||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
|
||||||
| pwd_reset_required | SMALLINT | DEFAULT 1 | 首次登录是否需改密 |
|
| pwd_reset_required | SMALLINT | DEFAULT 1 | 棣栨鐧诲綍鏄惁闇€鏀瑰瘑 |
|
||||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
| is_platform_admin | BOOLEAN | DEFAULT false | 是否平台管理员 |
|
| is_platform_admin | BOOLEAN | DEFAULT false | 鏄惁骞冲彴绠$悊鍛?|
|
||||||
|
|
||||||
索引:
|
绱㈠紩锛?
|
||||||
- `uk_user_username`:`UNIQUE (username) WHERE is_deleted = FALSE`
|
- `uk_user_username`锛歚UNIQUE (username) WHERE is_deleted = FALSE`
|
||||||
|
|
||||||
### 1.2 `sys_role`(角色表)
|
### 1.2 `sys_role`锛堣鑹茶〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| role_id | BIGSERIAL | PK | 角色ID |
|
| role_id | BIGSERIAL | PK | 瑙掕壊ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
| role_code | VARCHAR(50) | NOT NULL | 角色编码(租户内唯一) |
|
| role_code | VARCHAR(50) | NOT NULL | 瑙掕壊缂栫爜锛堢鎴峰唴鍞竴锛?|
|
||||||
| role_name | VARCHAR(50) | NOT NULL | 角色名称 |
|
| role_name | VARCHAR(50) | NOT NULL | 瑙掕壊鍚嶇О |
|
||||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
|
||||||
| remark | TEXT | | 备注 |
|
| remark | TEXT | | 澶囨敞 |
|
||||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
|
||||||
索引:
|
绱㈠紩锛?
|
||||||
- `idx_sys_role_tenant`:`(tenant_id)`
|
- `idx_sys_role_tenant`锛歚(tenant_id)`
|
||||||
- `uk_role_code`:`UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE`
|
- `uk_role_code`锛歚UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE`
|
||||||
|
|
||||||
### 1.3 `sys_user_role`(用户-角色关联表,租户强约束)
|
### 1.3 `sys_user_role`锛堢敤鎴?瑙掕壊鍏宠仈琛紝绉熸埛寮虹害鏉燂級
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 关联ID |
|
| id | BIGSERIAL | PK | 鍏宠仈ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
| user_id | BIGINT | NOT NULL | 鐢ㄦ埛ID |
|
||||||
| role_id | BIGINT | NOT NULL | 角色ID |
|
| role_id | BIGINT | NOT NULL | 瑙掕壊ID |
|
||||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
|
||||||
唯一约束:
|
鍞竴绾︽潫锛?
|
||||||
- `UNIQUE (tenant_id, user_id, role_id) WHERE is_deleted = 0`
|
- `UNIQUE (tenant_id, user_id, role_id) WHERE is_deleted = 0`
|
||||||
|
|
||||||
### 1.4 `sys_tenant_user`(租户成员关联表)
|
### 1.4 `sys_tenant_user`锛堢鎴锋垚鍛樺叧鑱旇〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 关联ID |
|
| id | BIGSERIAL | PK | 鍏宠仈ID |
|
||||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
| user_id | BIGINT | NOT NULL | 鐢ㄦ埛ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
| org_id | BIGINT | | 组织ID |
|
| org_id | BIGINT | | 缁勭粐ID |
|
||||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_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`
|
- `uk_tenant_user`锛歚UNIQUE (user_id, tenant_id) WHERE is_deleted = 0`
|
||||||
|
|
||||||
## 2. 权限/字典/参数(全局共享)
|
## 2. 鏉冮檺/瀛楀吀/鍙傛暟锛堝叏灞€鍏变韩锛?
|
||||||
|
|
||||||
### 2.1 `sys_permission`(权限表)
|
### 2.1 `sys_permission`锛堟潈闄愯〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| perm_id | BIGSERIAL | PK | 权限ID |
|
| perm_id | BIGSERIAL | PK | 鏉冮檺ID |
|
||||||
| parent_id | BIGINT | | 父级权限ID |
|
| parent_id | BIGINT | | 鐖剁骇鏉冮檺ID |
|
||||||
| name | VARCHAR(100) | NOT NULL | 权限名称 |
|
| name | VARCHAR(100) | NOT NULL | 鏉冮檺鍚嶇О |
|
||||||
| code | VARCHAR(100) | NOT NULL, UNIQUE | 权限编码 |
|
| code | VARCHAR(100) | NOT NULL, UNIQUE | 鏉冮檺缂栫爜 |
|
||||||
| perm_type | VARCHAR(20) | NOT NULL | 权限类型 |
|
| perm_type | VARCHAR(20) | NOT NULL | 鏉冮檺绫诲瀷 |
|
||||||
| level | INTEGER | NOT NULL | 层级 |
|
| level | INTEGER | NOT NULL | 灞傜骇 |
|
||||||
| path | VARCHAR(255) | | 路径 |
|
| path | VARCHAR(255) | | 璺緞 |
|
||||||
| component | VARCHAR(255) | | 组件 |
|
| component | VARCHAR(255) | | 缁勪欢 |
|
||||||
| icon | VARCHAR(100) | | 图标 |
|
| icon | VARCHAR(100) | | 鍥炬爣 |
|
||||||
| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 排序 |
|
| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 鎺掑簭 |
|
||||||
| is_visible | SMALLINT | NOT NULL, DEFAULT 1 | 是否可见 |
|
| is_visible | SMALLINT | NOT NULL, DEFAULT 1 | 鏄惁鍙 |
|
||||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
|
||||||
| description | TEXT | | 描述 |
|
| description | TEXT | | 鎻忚堪 |
|
||||||
| meta | JSONB | | 扩展信息 |
|
| meta | JSONB | | 鎵╁睍淇℃伅 |
|
||||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
|
||||||
### 2.2 `sys_dict_type`(字典类型表)
|
### 2.2 `sys_dict_type`锛堝瓧鍏哥被鍨嬭〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| dict_type_id | BIGSERIAL | PK | 类型ID |
|
| dict_type_id | BIGSERIAL | PK | 绫诲瀷ID |
|
||||||
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 类型编码 |
|
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 绫诲瀷缂栫爜 |
|
||||||
| type_name | VARCHAR(50) | NOT NULL | 类型名称 |
|
| type_name | VARCHAR(50) | NOT NULL | 绫诲瀷鍚嶇О |
|
||||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||||
| remark | TEXT | | 备注 |
|
| remark | TEXT | | 澶囨敞 |
|
||||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
|
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
|
|
||||||
**初始化数据:**
|
**鍒濆鍖栨暟鎹細**
|
||||||
- `sys_common_status`: 通用状态 (启用/禁用)
|
- `sys_common_status`: 閫氱敤鐘舵€?(鍚敤/绂佺敤)
|
||||||
- `sys_permission_type`: 权限类型 (目录/菜单/按钮)
|
- `sys_permission_type`: 鏉冮檺绫诲瀷 (鐩綍/鑿滃崟/鎸夐挳)
|
||||||
- `sys_common_visibility`: 可见性 (显示/隐藏)
|
- `sys_common_visibility`: 鍙鎬?(鏄剧ず/闅愯棌)
|
||||||
- `sys_permission_level`: 权限层级 (1, 2, 3)
|
- `sys_permission_level`: 鏉冮檺灞傜骇 (1, 2, 3)
|
||||||
- `sys_log_type`: 日志类型 (LOGIN/OPERATION)
|
- `sys_log_type`: 鏃ュ織绫诲瀷 (LOGIN/OPERATION)
|
||||||
- `sys_param_type`: 参数类型 (String/Number/Boolean/JSON)
|
- `sys_param_type`: 鍙傛暟绫诲瀷 (String/Number/Boolean/JSON)
|
||||||
- `sys_log_status`: 操作状态 (成功/失败)
|
- `sys_log_status`: 鎿嶄綔鐘舵€?(鎴愬姛/澶辫触)
|
||||||
|
|
||||||
### 2.3 `sys_dict_item`(字典项表)
|
### 2.3 `sys_dict_item`锛堝瓧鍏搁」琛級
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| dict_item_id | BIGSERIAL | PK | 字典项ID |
|
| dict_item_id | BIGSERIAL | PK | 瀛楀吀椤笽D |
|
||||||
| type_code | VARCHAR(50) | NOT NULL | 字典类型编码 |
|
| type_code | VARCHAR(50) | NOT NULL | 瀛楀吀绫诲瀷缂栫爜 |
|
||||||
| item_label | VARCHAR(100) | NOT NULL | 展示文本 |
|
| item_label | VARCHAR(100) | NOT NULL | 灞曠ず鏂囨湰 |
|
||||||
| item_value | VARCHAR(100) | NOT NULL | 存储值 |
|
| item_value | VARCHAR(100) | NOT NULL | 瀛樺偍鍊?|
|
||||||
| sort_order | INT | DEFAULT 0 | 排序 |
|
| sort_order | INT | DEFAULT 0 | 鎺掑簭 |
|
||||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
|
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
|
|
||||||
索引:
|
绱㈠紩锛?
|
||||||
- `idx_dict_item_type`:`(type_code)`
|
- `idx_dict_item_type`锛歚(type_code)`
|
||||||
- `uk_dict_item_value`:`UNIQUE (type_code, item_value)`
|
- `uk_dict_item_value`锛歚UNIQUE (type_code, item_value)`
|
||||||
|
|
||||||
### 2.4 `sys_param`(系统参数表)
|
### 2.4 `sys_param`锛堢郴缁熷弬鏁拌〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 参数ID |
|
| id | BIGSERIAL | PK | 鍙傛暟ID |
|
||||||
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 参数键 |
|
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 鍙傛暟閿?|
|
||||||
| param_value | TEXT | NOT NULL | 参数值 |
|
| param_value | TEXT | NOT NULL | 鍙傛暟鍊?|
|
||||||
| param_type | VARCHAR(20) | NOT NULL | 参数类型 |
|
| param_type | VARCHAR(20) | NOT NULL | 鍙傛暟绫诲瀷 |
|
||||||
| is_system | SMALLINT | DEFAULT 0 | 是否系统内置 |
|
| is_system | SMALLINT | DEFAULT 0 | 鏄惁绯荤粺鍐呯疆 |
|
||||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||||
| description | TEXT | | 描述 |
|
| description | TEXT | | 鎻忚堪 |
|
||||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
||||||
|
|
||||||
## 3. 日志(租户隔离)
|
## 3. 鏃ュ織锛堢鎴烽殧绂伙級
|
||||||
|
|
||||||
### 3.1 `sys_log`(系统日志表)
|
### 3.1 `sys_log`锛堢郴缁熸棩蹇楄〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 日志ID |
|
| id | BIGSERIAL | PK | 鏃ュ織ID |
|
||||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 租户ID |
|
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID |
|
||||||
| user_id | BIGINT | | 用户ID |
|
| user_id | BIGINT | | 鐢ㄦ埛ID |
|
||||||
| username | VARCHAR(50) | | 用户名 |
|
| username | VARCHAR(50) | | 鐢ㄦ埛鍚?|
|
||||||
| log_type | VARCHAR(20) | | 日志类型(如 LOGIN、OPERATION) |
|
| log_type | VARCHAR(20) | | 鏃ュ織绫诲瀷锛堝 LOGIN銆丱PERATION锛?|
|
||||||
| operation | VARCHAR(100) | NOT NULL | 操作描述 |
|
| operation | VARCHAR(100) | NOT NULL | 鎿嶄綔鎻忚堪 |
|
||||||
| method | VARCHAR(200) | | 方法 |
|
| method | VARCHAR(200) | | 鏂规硶 |
|
||||||
| params | TEXT | | 请求参数 |
|
| params | TEXT | | 璇锋眰鍙傛暟 |
|
||||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||||
| ip | VARCHAR(50) | | IP |
|
| ip | VARCHAR(50) | | IP |
|
||||||
| duration | BIGINT | | 耗时(ms) |
|
| duration | BIGINT | | 鑰楁椂锛坢s锛?|
|
||||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
||||||
|
|
||||||
索引:
|
绱㈠紩锛?
|
||||||
- `idx_log_tenant_type`:`(tenant_id, log_type, created_at)`
|
- `idx_log_tenant_type`锛歚(tenant_id, log_type, created_at)`
|
||||||
|
|
||||||
## 4. 平台配置
|
## 4. 骞冲彴閰嶇疆
|
||||||
|
|
||||||
### 4.1 `sys_platform_config`(平台管理表)
|
### 4.1 `sys_platform_config`锛堝钩鍙扮鐞嗚〃锛?
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGINT | PK | 固定为 1 |
|
| id | BIGINT | PK | 鍥哄畾涓?1 |
|
||||||
| project_name | VARCHAR(128) | NOT NULL | 项目名称 |
|
| project_name | VARCHAR(128) | NOT NULL | 椤圭洰鍚嶇О |
|
||||||
| logo_url | VARCHAR(512) | | Logo URL |
|
| logo_url | VARCHAR(512) | | Logo URL |
|
||||||
| icon_url | VARCHAR(512) | | Icon URL |
|
| icon_url | VARCHAR(512) | | Icon URL |
|
||||||
| login_bg_url | VARCHAR(512) | | 登录页背景 |
|
| login_bg_url | VARCHAR(512) | | 鐧诲綍椤佃儗鏅?|
|
||||||
| icp_info | VARCHAR(128) | | 备案信息 |
|
| icp_info | VARCHAR(128) | | 澶囨淇℃伅 |
|
||||||
| copyright_info | VARCHAR(255) | | 版权信息 |
|
| copyright_info | VARCHAR(255) | | 鐗堟潈淇℃伅 |
|
||||||
| system_description | TEXT | | 系统描述 |
|
| system_description | TEXT | | 绯荤粺鎻忚堪 |
|
||||||
| created_at | TIMESTAMP | NOT NULL | 创建时间 |
|
| created_at | TIMESTAMP | NOT NULL | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP | NOT NULL | 更新时间 |
|
| updated_at | TIMESTAMP | NOT NULL | 鏇存柊鏃堕棿 |
|
||||||
|
|
||||||
|
## 5. 涓氬姟妯″潡
|
||||||
|
|
||||||
|
### 5.1 `biz_speakers`锛堝0绾瑰彂瑷€浜鸿〃锛?
|
||||||
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
|
| user_id | BIGINT | | 鍏宠仈绯荤粺鐢ㄦ埛ID |
|
||||||
|
| name | VARCHAR(100) | NOT NULL | 鍙戣█浜哄鍚?|
|
||||||
|
| voice_path | VARCHAR(512) | | 鍘熷鏂囦欢璺緞 |
|
||||||
|
| voice_ext | VARCHAR(10) | | 鏂囦欢鍚庣紑 |
|
||||||
|
| voice_size | BIGINT | | 鏂囦欢澶у皬 |
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:宸蹭繚瀛? 2:娉ㄥ唽涓? 3:宸叉敞鍐? |
|
||||||
|
| embedding | VECTOR | | 澹扮汗鐗瑰緛鍚戦噺 |
|
||||||
|
| remark | TEXT | | 澶囨敞 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
||||||
|
|
||||||
|
绱㈠紩锛?
|
||||||
|
- `idx_speaker_tenant`: `(tenant_id)`
|
||||||
|
- `idx_speaker_user`: `(user_id) WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
### 5.2 `biz_hot_words`锛堢儹璇嶇鐞嗚〃锛?
|
||||||
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
|
| word | VARCHAR(100) | NOT NULL | 鐑瘝鍘熸枃 |
|
||||||
|
| pinyin_list | JSONB | | 鎷奸煶鏁扮粍 |
|
||||||
|
| match_strategy | SMALLINT | DEFAULT 1 | 鍖归厤绛栫暐 (1:绮剧‘, 2:妯$硦) |
|
||||||
|
| category | VARCHAR(50) | | 绫诲埆 (浜哄悕銆佹湳璇瓑) |
|
||||||
|
| weight | INTEGER | DEFAULT 10 | 鏉冮噸 (1-100) |
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:鍚敤, 0:绂佺敤) |
|
||||||
|
| is_synced | SMALLINT | DEFAULT 0 | 宸插悓姝ョ涓夋柟鏍囪 |
|
||||||
|
| remark | TEXT | | 澶囨敞 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
||||||
|
|
||||||
|
绱㈠紩锛?
|
||||||
|
- `idx_hotword_tenant`: `(tenant_id)`
|
||||||
|
- `idx_hotword_word`: `(word) WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
### 5.3 `biz_prompt_templates`锛堟彁绀鸿瘝妯℃澘琛級
|
||||||
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
|
| template_name | VARCHAR(100) | NOT NULL | 妯℃澘鍚嶇О |
|
||||||
|
| category | VARCHAR(20) | | 鍒嗙被 (瀛楀吀: biz_prompt_category) |
|
||||||
|
| is_system | SMALLINT | DEFAULT 0 | 鏄惁棰勭疆 (1:鏄? 0:鍚? |
|
||||||
|
| creator_id | BIGINT | | 鍒涘缓浜篒D |
|
||||||
|
| tags | JSONB | | 鏍囩鏁扮粍 |
|
||||||
|
| usage_count | INTEGER | DEFAULT 0 | 浣跨敤娆℃暟 |
|
||||||
|
| prompt_content | TEXT | NOT NULL | 鎻愮ず璇嶅唴瀹?|
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:鍚敤, 0:绂佺敤) |
|
||||||
|
| remark | VARCHAR(255) | | 澶囨敞 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
||||||
|
|
||||||
|
绱㈠紩锛?
|
||||||
|
- `idx_prompt_tenant`: `(tenant_id)`
|
||||||
|
- `idx_prompt_system`: `(is_system) WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
### 5.4 `biz_asr_models`(ASR 模型管理表)
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 主键ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||||
|
| model_name | VARCHAR(100) | NOT NULL | 模型显示名称 |
|
||||||
|
| provider | VARCHAR(50) | | 提供商 |
|
||||||
|
| base_url | VARCHAR(255) | | 接口基础地址 |
|
||||||
|
| api_key | VARCHAR(255) | | API 密钥 |
|
||||||
|
| model_code | VARCHAR(100) | | 模型代码 |
|
||||||
|
| ws_url | VARCHAR(255) | | WebSocket 地址 |
|
||||||
|
| media_config | JSON/TEXT | | 媒体参数 |
|
||||||
|
| is_default | SMALLINT | DEFAULT 0 | 默认模型标记 |
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||||
|
| remark | VARCHAR(255) | | 备注 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
|
||||||
|
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
||||||
|
|
||||||
|
索引:
|
||||||
|
- `idx_asr_model_tenant`: `(tenant_id)`
|
||||||
|
- `idx_asr_model_default`: `(is_default) WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
### 5.5 `biz_llm_models`(LLM 模型管理表)
|
||||||
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 主键ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||||
|
| model_name | VARCHAR(100) | NOT NULL | 模型显示名称 |
|
||||||
|
| provider | VARCHAR(50) | | 提供商 |
|
||||||
|
| base_url | VARCHAR(255) | | 接口基础地址 |
|
||||||
|
| api_path | VARCHAR(100) | | API 路径 |
|
||||||
|
| api_key | VARCHAR(255) | | API 密钥 |
|
||||||
|
| model_code | VARCHAR(100) | | 模型代码 |
|
||||||
|
| temperature | DECIMAL | DEFAULT 0.7 | 随机性 |
|
||||||
|
| top_p | DECIMAL | DEFAULT 0.9 | 核采样 |
|
||||||
|
| is_default | SMALLINT | DEFAULT 0 | 默认模型标记 |
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||||
|
| remark | VARCHAR(255) | | 备注 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
|
||||||
|
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
|
||||||
|
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
||||||
|
|
||||||
|
索引:
|
||||||
|
- `idx_llm_model_tenant`: `(tenant_id)`
|
||||||
|
- `idx_llm_model_default`: `(is_default) WHERE is_deleted = 0`
|
||||||
|
|
||||||
|
### 5.6 `biz_meetings`锛堜細璁富琛級
|
||||||
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
|
| title | VARCHAR(200) | NOT NULL | 浼氳鏍囬 |
|
||||||
|
| audio_url | VARCHAR(500) | | 涓撳睘闊抽璺緞 |
|
||||||
|
| latest_summary_task_id | BIGINT | | 鏈€鏂版垚鍔熺殑鎬荤粨浠诲姟ID |
|
||||||
|
| status | SMALLINT | DEFAULT 0 | 0:寰呭鐞? 1:璇嗗埆涓? 2:鎬荤粨涓? 3:宸插畬鎴? 4:澶辫触 |
|
||||||
|
|
||||||
|
### 5.6 `biz_meeting_transcripts`锛堣浆褰曟槑缁嗚〃锛?
|
||||||
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
|
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
|
||||||
|
| speaker_label | VARCHAR(50) | | 鍙戣█浜烘爣绛?|
|
||||||
|
| content | TEXT | | 杞綍鏂囧瓧 |
|
||||||
|
| start_time | INTEGER | | 寮€濮嬫椂闂?(ms) |
|
||||||
|
|
||||||
|
### 5.7 `biz_ai_tasks`锛圓I 浠诲姟娴佹按琛級
|
||||||
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
|
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
|
||||||
|
| task_type | VARCHAR(20) | | ASR / SUMMARY |
|
||||||
|
| request_data | JSONB | | 璇锋眰鍘熷鏁版嵁 |
|
||||||
|
| response_data | JSONB | | 鍝嶅簲鍘熷鏁版嵁 |
|
||||||
|
| task_config | TEXT | | **[蹇収]** 浠诲姟閰嶇疆(妯″瀷ID銆佹彁绀鸿瘝妯℃澘绛? |
|
||||||
|
| result_file_path | VARCHAR(500) | | 缁撴灉鏂囦欢鐩稿璺緞 (濡侻D鎬荤粨鏂囦欢) |
|
||||||
|
| status | SMALLINT | | 0:鎺掗槦, 1:澶勭悊涓? 2:鎴愬姛, 3:澶辫触 |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
-- PostgreSQL Database Schema for iMeeting (Multi-tenant)
|
-- PostgreSQL Database Schema for iMeeting (Multi-tenant)
|
||||||
-- 0 为系统预留租户 ID
|
-- 0 为系统预留租户 ID
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- 0. 租户与组织
|
-- 0. 租户与组织
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector;
|
||||||
-- 租户表
|
-- 租户表
|
||||||
CREATE TABLE sys_tenant (
|
CREATE TABLE sys_tenant (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
@ -19,7 +19,7 @@ CREATE TABLE sys_tenant (
|
||||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
is_deleted SMALLINT DEFAULT 0
|
is_deleted SMALLINT DEFAULT 0
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX uk_tenant_code ON sys_tenant (tenant_code) WHERE is_deleted = 0;
|
CREATE INDEX uk_tenant_code ON sys_tenant (tenant_code) WHERE is_deleted = 0;
|
||||||
|
|
||||||
-- 组织架构表
|
-- 组织架构表
|
||||||
DROP TABLE IF EXISTS sys_org CASCADE;
|
DROP TABLE IF EXISTS sys_org CASCADE;
|
||||||
|
|
@ -83,7 +83,7 @@ CREATE TABLE sys_role (
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_sys_role_tenant ON sys_role (tenant_id);
|
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 = 0;
|
CREATE INDEX uk_role_code ON sys_role (tenant_id, role_code) WHERE is_deleted = 0;
|
||||||
|
|
||||||
-- 用户-角色关联表 (按 tenant_id 强约束,避免跨租户角色污染)
|
-- 用户-角色关联表 (按 tenant_id 强约束,避免跨租户角色污染)
|
||||||
DROP TABLE IF EXISTS sys_user_role CASCADE;
|
DROP TABLE IF EXISTS sys_user_role CASCADE;
|
||||||
|
|
@ -109,7 +109,7 @@ CREATE TABLE sys_permission (
|
||||||
perm_id BIGSERIAL PRIMARY KEY,
|
perm_id BIGSERIAL PRIMARY KEY,
|
||||||
parent_id BIGINT,
|
parent_id BIGINT,
|
||||||
name VARCHAR(100) NOT NULL,
|
name VARCHAR(100) NOT NULL,
|
||||||
code VARCHAR(100) NOT NULL UNIQUE,
|
code VARCHAR(100) NOT NULL ,
|
||||||
perm_type VARCHAR(20) NOT NULL,
|
perm_type VARCHAR(20) NOT NULL,
|
||||||
level INTEGER NOT NULL,
|
level INTEGER NOT NULL,
|
||||||
path VARCHAR(255),
|
path VARCHAR(255),
|
||||||
|
|
@ -138,7 +138,7 @@ CREATE TABLE sys_tenant_user (
|
||||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now()
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX uk_tenant_user
|
CREATE INDEX uk_tenant_user
|
||||||
ON sys_tenant_user (user_id, tenant_id)
|
ON sys_tenant_user (user_id, tenant_id)
|
||||||
WHERE is_deleted = 0;
|
WHERE is_deleted = 0;
|
||||||
CREATE TABLE sys_dict_type (
|
CREATE TABLE sys_dict_type (
|
||||||
|
|
@ -161,13 +161,14 @@ CREATE TABLE sys_dict_item (
|
||||||
status SMALLINT DEFAULT 1,
|
status SMALLINT DEFAULT 1,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
is_deleted SMALLINT DEFAULT 0
|
is_deleted SMALLINT DEFAULT 0,
|
||||||
|
remark varchar(255)
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_dict_item_type ON sys_dict_item (type_code);
|
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 INDEX uk_dict_item_value ON sys_dict_item (type_code, item_value);
|
||||||
|
|
||||||
CREATE TABLE sys_param (
|
CREATE TABLE sys_param (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
param_id BIGSERIAL PRIMARY KEY,
|
||||||
param_key VARCHAR(100) UNIQUE NOT NULL,
|
param_key VARCHAR(100) UNIQUE NOT NULL,
|
||||||
param_value TEXT NOT NULL,
|
param_value TEXT NOT NULL,
|
||||||
param_type VARCHAR(20) NOT NULL,
|
param_type VARCHAR(20) NOT NULL,
|
||||||
|
|
@ -179,6 +180,14 @@ CREATE TABLE sys_param (
|
||||||
is_deleted SMALLINT DEFAULT 0
|
is_deleted SMALLINT DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE sys_role_permission (
|
||||||
|
"id" BIGSERIAL PRIMARY KEY,
|
||||||
|
"role_id" int8 NOT NULL,
|
||||||
|
"perm_id" int8 NOT NULL,
|
||||||
|
"is_deleted" int2 NOT NULL DEFAULT 0,
|
||||||
|
"created_at" timestamp(6) NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamp(6) NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- 3. 日志 (租户隔离)
|
-- 3. 日志 (租户隔离)
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|
@ -220,6 +229,208 @@ CREATE TABLE sys_platform_config (
|
||||||
INSERT INTO sys_platform_config (id, project_name, copyright_info)
|
INSERT INTO sys_platform_config (id, project_name, copyright_info)
|
||||||
VALUES (1, 'iMeeting 智能会议系统', '© 2026 iMeeting Team. All rights reserved.');
|
VALUES (1, 'iMeeting 智能会议系统', '© 2026 iMeeting Team. All rights reserved.');
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 6. 业务模块 - 声纹管理
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_speakers CASCADE;
|
||||||
|
CREATE TABLE biz_speakers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL, -- 关联系统用户ID
|
||||||
|
name VARCHAR(100) NOT NULL, -- 发言人姓名
|
||||||
|
voice_path VARCHAR(512), -- 原始声纹文件存储路径
|
||||||
|
voice_ext VARCHAR(10), -- 文件后缀
|
||||||
|
voice_size BIGINT, -- 文件大小
|
||||||
|
status SMALLINT DEFAULT 1, -- 状态: 1=已保存, 2=注册中, 3=已注册, 4=失败
|
||||||
|
embedding VECTOR(512), -- 声纹特征向量 (预留 pgvector 字段)
|
||||||
|
remark TEXT, -- 备注
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_speaker_user ON biz_speakers (user_id) WHERE is_deleted = 0;
|
||||||
|
|
||||||
|
COMMENT ON TABLE biz_speakers IS '声纹发言人基础信息表 (用户全局资源)';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 7. 业务模块 - 热词管理
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_hot_words CASCADE;
|
||||||
|
CREATE TABLE biz_hot_words (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL, -- 租户ID (强制隔离)
|
||||||
|
word VARCHAR(100) NOT NULL, -- 热词原文
|
||||||
|
is_public SMALLINT DEFAULT 0, -- 1:租户公开, 0:个人私有
|
||||||
|
creator_id BIGINT, -- 创建者ID
|
||||||
|
pinyin_list text, -- 拼音数组(支持多音字, 如 ["i mi ting", "i mei ting"])
|
||||||
|
match_strategy SMALLINT DEFAULT 1, -- 匹配策略: 1:精确匹配, 2:拼音模糊匹配
|
||||||
|
category VARCHAR(50), -- 类别 (人名、术语、地名)
|
||||||
|
weight INTEGER DEFAULT 10, -- 权重 (1-100)
|
||||||
|
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
|
||||||
|
is_synced SMALLINT DEFAULT 0, -- 是否已同步至第三方引擎: 0:未同步, 1:已同步
|
||||||
|
remark TEXT, -- 备注
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_hotword_tenant ON biz_hot_words (tenant_id);
|
||||||
|
CREATE INDEX idx_hotword_word ON biz_hot_words (word) WHERE is_deleted = 0;
|
||||||
|
|
||||||
|
COMMENT ON TABLE biz_hot_words IS '语音识别热词表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 8. 业务模块 - 提示词模板
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_prompt_templates CASCADE;
|
||||||
|
CREATE TABLE biz_prompt_templates (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID (0为系统级)
|
||||||
|
template_name VARCHAR(100) NOT NULL, -- 模板名称
|
||||||
|
category VARCHAR(20), -- 分类 (字典: biz_prompt_category)
|
||||||
|
is_system SMALLINT DEFAULT 0, -- 是否系统预置 (1:是, 0:否)
|
||||||
|
creator_id BIGINT, -- 创建人ID
|
||||||
|
tags text, -- 标签数组 (JSONB)
|
||||||
|
usage_count INTEGER DEFAULT 0, -- 使用次数
|
||||||
|
prompt_content TEXT NOT NULL, -- 提示词内容
|
||||||
|
status SMALLINT DEFAULT 1, -- 状态: 1:启用, 0:禁用
|
||||||
|
remark VARCHAR(255), -- 备注
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_prompt_tenant ON biz_prompt_templates (tenant_id);
|
||||||
|
CREATE INDEX idx_prompt_system ON biz_prompt_templates (is_system) WHERE is_deleted = 0;
|
||||||
|
|
||||||
|
COMMENT ON TABLE biz_prompt_templates IS '会议总结提示词模板表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 9. 业务模块 - AI 模型管理
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_asr_models CASCADE;
|
||||||
|
CREATE TABLE biz_asr_models (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
model_name VARCHAR(100) NOT NULL,
|
||||||
|
provider VARCHAR(50),
|
||||||
|
base_url VARCHAR(255),
|
||||||
|
api_key VARCHAR(255),
|
||||||
|
model_code VARCHAR(100),
|
||||||
|
ws_url VARCHAR(255),
|
||||||
|
media_config text,
|
||||||
|
is_default SMALLINT DEFAULT 0,
|
||||||
|
status SMALLINT DEFAULT 1,
|
||||||
|
remark VARCHAR(255),
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS biz_llm_models CASCADE;
|
||||||
|
CREATE TABLE biz_llm_models (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
model_name VARCHAR(100) NOT NULL,
|
||||||
|
provider VARCHAR(50),
|
||||||
|
base_url VARCHAR(255),
|
||||||
|
api_path VARCHAR(100),
|
||||||
|
api_key VARCHAR(255),
|
||||||
|
model_code VARCHAR(100),
|
||||||
|
temperature DECIMAL(3,2) DEFAULT 0.7,
|
||||||
|
top_p DECIMAL(3,2) DEFAULT 0.9,
|
||||||
|
is_default SMALLINT DEFAULT 0,
|
||||||
|
status SMALLINT DEFAULT 1,
|
||||||
|
remark VARCHAR(255),
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_asr_model_tenant ON biz_asr_models (tenant_id);
|
||||||
|
CREATE INDEX idx_asr_model_default ON biz_asr_models (is_default) WHERE is_deleted = 0;
|
||||||
|
CREATE INDEX idx_llm_model_tenant ON biz_llm_models (tenant_id);
|
||||||
|
CREATE INDEX idx_llm_model_default ON biz_llm_models (is_default) WHERE is_deleted = 0;
|
||||||
|
|
||||||
|
COMMENT ON TABLE biz_asr_models IS 'ASR 模型配置表';
|
||||||
|
COMMENT ON TABLE biz_llm_models IS 'LLM 模型配置表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 10. 业务模块 - 会议主表
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_meetings CASCADE;
|
||||||
|
CREATE TABLE biz_meetings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||||
|
title VARCHAR(200) NOT NULL,
|
||||||
|
meeting_time TIMESTAMP(6),
|
||||||
|
participants TEXT,
|
||||||
|
tags VARCHAR(255),
|
||||||
|
audio_url VARCHAR(500),
|
||||||
|
creator_id BIGINT, -- 发起人ID
|
||||||
|
creator_name VARCHAR(100), -- 发起人姓名
|
||||||
|
latest_summary_task_id BIGINT, -- 最新成功总结任务ID
|
||||||
|
status SMALLINT DEFAULT 0, -- 0:待处理, 1:处理中, 2:成功, 3:失败
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 11. 业务模块 - 转录明细表
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_meeting_transcripts CASCADE;
|
||||||
|
CREATE TABLE biz_meeting_transcripts (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
meeting_id BIGINT NOT NULL,
|
||||||
|
speaker_id VARCHAR(50), -- ASR返回的发言人标识
|
||||||
|
speaker_name VARCHAR(100), -- 修改后的发言人姓名
|
||||||
|
speaker_label VARCHAR(50), -- 发言人标签
|
||||||
|
content TEXT, -- 转录内容
|
||||||
|
start_time INTEGER, -- 开始时间(ms)
|
||||||
|
end_time INTEGER, -- 结束时间(ms)
|
||||||
|
sort_order INTEGER,
|
||||||
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- 12. 业务模块 - AI 异步任务日志表
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS biz_ai_tasks CASCADE;
|
||||||
|
CREATE TABLE biz_ai_tasks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
meeting_id BIGINT NOT NULL,
|
||||||
|
task_type VARCHAR(20), -- ASR / SUMMARY
|
||||||
|
status SMALLINT DEFAULT 0, -- 0:排队, 1:执行中, 2:成功, 3:失败
|
||||||
|
request_data text, -- 请求三方原始JSON
|
||||||
|
response_data text, -- 三方返回原始JSON
|
||||||
|
task_config text, -- 任务配置参数快照
|
||||||
|
result_file_path VARCHAR(500), -- 结果文件路径
|
||||||
|
error_msg TEXT, -- 错误堆栈
|
||||||
|
started_at TIMESTAMP(6),
|
||||||
|
completed_at TIMESTAMP(6)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_meeting_tenant ON biz_meetings (tenant_id);
|
||||||
|
CREATE INDEX idx_transcript_meeting ON biz_meeting_transcripts (meeting_id);
|
||||||
|
CREATE INDEX idx_aitask_meeting ON biz_ai_tasks (meeting_id);
|
||||||
|
|
||||||
|
COMMENT ON TABLE biz_meetings IS '会议管理主表';
|
||||||
|
COMMENT ON TABLE biz_meeting_transcripts IS '会议转录明细表';
|
||||||
|
COMMENT ON TABLE biz_ai_tasks IS 'AI 任务流水日志表';
|
||||||
|
DROP TABLE IF EXISTS "biz_prompt_template_user_config";
|
||||||
|
CREATE TABLE "biz_prompt_template_user_config" (
|
||||||
|
"id" BIGSERIAL PRIMARY KEY,
|
||||||
|
"tenant_id" int8 NOT NULL DEFAULT 0,
|
||||||
|
"user_id" int8 NOT NULL,
|
||||||
|
"template_id" int8 NOT NULL,
|
||||||
|
"status" int2 DEFAULT 1,
|
||||||
|
"created_at" timestamp(6) NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamp(6) NOT NULL DEFAULT now(),
|
||||||
|
"is_deleted" int2 NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- 5. 基础初始化数据
|
-- 5. 基础初始化数据
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
|
|
@ -263,3 +474,84 @@ INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES
|
||||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_status', '操作状态', '1=成功, 0=失败');
|
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', '成功', '1', 1);
|
||||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2);
|
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2);
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (21, 18, '查询租户', 'sys_tenant:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (28, 19, '删除组织', 'sys:org:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (22, 18, '新增租户', 'sys_tenant:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (23, 18, '修改租户', 'sys_tenant:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (24, 18, '删除租户', 'sys_tenant:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.647439', '2026-02-25 10:09:32.647439');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (48, 3, '权限查询', 'sys:permission:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 16:07:04.002702', '2026-02-26 16:07:04.003701');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (13, 12, '角色权限', 'menu:role:permission', 'menu', 2, '/role-permissions', NULL, NULL, 0, 1, 1, NULL, NULL, 1, '2026-02-10 18:01:32.999774', '2026-02-11 09:41:31.952294');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (39, 12, '平台管理', 'platform', 'menu', 2, '/platform-settings', NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 14:54:49.406968', '2026-02-26 14:54:49.407968');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (25, 19, '查询组织', 'sys:org:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (26, 19, '新增组织', 'sys:org:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (27, 19, '修改组织', 'sys:org:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.65871', '2026-02-25 10:09:32.65871');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (3, 12, '角色管理', 'sys:role:list', 'menu', 2, '/roles', NULL, NULL, 3, 1, 1, '角色管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-02-10 17:24:07.484806');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (40, 3, '角色查询', 'sys:role:query', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:35:35.692367', '2026-02-26 15:35:35.693366');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (41, 3, '角色创建', 'sys:role:create', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:35:50.081581', '2026-02-26 15:35:50.081581');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (42, 3, '角色更新', 'sys:role:update', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:03.385343', '2026-02-26 15:36:03.385343');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (43, 3, '角色删除', 'sys:role:delete', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:20.451039', '2026-02-26 15:36:20.451039');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (44, 3, '角色权限列表', 'sys:role:permission:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:39.902216', '2026-02-26 15:36:39.902216');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (45, 3, '角色权限更新', 'sys:role:permission:save', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:36:53.595974', '2026-02-26 15:36:53.595974');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (2, 12, '用户管理', 'sys:user:list', 'menu', 2, '/users', NULL, NULL, 2, 1, 1, '用户管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-02-26 15:43:21.037142');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (29, 2, '查询用户', 'sys:user:query', 'button', 3, NULL, NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (30, 2, '新增用户', 'sys:user:create', 'button', 3, NULL, NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (31, 2, '修改用户', 'sys:user:update', 'button', 3, NULL, NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (32, 2, '删除用户', 'sys:user:delete', 'button', 3, NULL, NULL, NULL, 4, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (33, 2, '分配角色', 'sys:user:role:save', 'button', 3, NULL, NULL, NULL, 5, 1, 1, NULL, NULL, 0, '2026-02-25 10:09:32.669947', '2026-02-25 10:09:32.669947');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (46, 2, '用户角色查询', 'sys:user:role:list', 'button', 3, NULL, NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-02-26 15:45:14.890567', '2026-02-26 15:45:28.553231');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (54, NULL, '热词管理', 'menu:hotword', 'menu', 1, '/hotwords', NULL, 'hotword', 11, 1, 1, NULL, NULL, 0, '2026-02-28 16:51:49.158997', '2026-02-28 16:51:49.158997');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (55, NULL, '总结模板', 'menu:prompt', 'menu', 1, '/prompts', NULL, 'prompt', 12, 1, 1, NULL, NULL, 0, '2026-02-28 17:47:51.015282', '2026-02-28 17:47:51.015282');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (56, NULL, '模型配置', 'menu:aimodel', 'menu', 1, '/aimodels', NULL, 'aimodel', 13, 1, 1, NULL, NULL, 0, '2026-03-02 09:48:27.179055', '2026-03-02 09:48:27.179055');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (57, NULL, '会议中心', 'menu:meeting', 'menu', 1, '/meetings', NULL, 'meeting', 20, 1, 1, NULL, NULL, 0, '2026-03-02 11:02:58.089065', '2026-03-02 11:02:58.089065');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (52, NULL, '测试菜单', 'test', 'directory', 1, '/role-permissions', NULL, NULL, 0, 1, 1, NULL, NULL, 1, '2026-02-27 10:39:04.576329', '2026-03-03 10:03:45.999369');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (59, NULL, '声纹注册', 'speaker', 'menu', 1, '/speaker-reg', NULL, NULL, 0, 1, 1, NULL, NULL, 0, '2026-03-06 15:23:09.314321', '2026-03-06 15:23:51.715481');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (18, 12, '租户管理', 'menu:tenant', 'menu', 2, '/tenants', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-12 14:06:13.672548', '2026-03-06 16:31:45.006699');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (58, NULL, '发起会议', 'menu:meeting:create', 'menu', 1, '/meeting-create', NULL, 'audio', 19, 1, 1, NULL, NULL, 1, '2026-03-02 16:21:47.326202', '2026-03-05 09:05:49.301092');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (4, 12, '菜单管理', 'sys:permission:list', 'menu', 2, '/permissions', NULL, NULL, 4, 1, 1, '权限管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 17:10:30.891258');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (5, 12, '设备管理', 'menu:devices', 'menu', 2, '/devices', NULL, NULL, 5, 0, 1, '设备管理菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 17:11:48.867451');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (15, 12, '字典管理', 'menu:dict', 'menu', 2, '/dictionaries', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-11 13:54:56.100838', '2026-03-05 17:12:28.223844');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (17, 12, '日志管理', 'menu:log', 'menu', 2, '/logs', NULL, NULL, 1, 1, 1, NULL, NULL, 0, '2026-02-12 09:49:02.814427', '2026-03-05 17:12:57.94561');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (38, 12, '参数管理', 'params', 'menu', 2, '/params', NULL, NULL, 2, 1, 1, NULL, NULL, 0, '2026-02-26 14:34:16.903552', '2026-03-06 16:31:51.714937');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (1, NULL, '任务监控', 'menu:dashboard', 'menu', 1, '/', NULL, NULL, 1, 1, 1, 'Dashboard 菜单', NULL, 0, '2026-02-10 07:24:30.148186', '2026-03-05 18:01:31.515477');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (19, 12, '组织管理', 'sys:org:list', 'menu', 2, '/orgs', NULL, NULL, 3, 1, 1, NULL, NULL, 0, '2026-02-12 14:09:01.818807', '2026-03-06 16:32:00.114277');
|
||||||
|
INSERT INTO "sys_permission" ("perm_id", "parent_id", "name", "code", "perm_type", "level", "path", "component", "icon", "sort_order", "is_visible", "status", "description", "meta", "is_deleted", "created_at", "updated_at") VALUES (12, NULL, '系统管理', 'system', 'directory', 1, NULL, NULL, NULL, 110, 1, 1, NULL, NULL, 0, '2026-02-10 17:23:52.877017', '2026-03-06 14:00:20.182181');
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (2, 'security.token.refresh_ttl_days', '7', 'int', 1, 1, 'Refresh Token 有效期(天)', 0, '2026-02-09 09:54:21.893832', '2026-02-09 09:54:21.893832');
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (4, 'tenant.init.default.menu.codes', 'sys:user:list,sys:user:create,sys:user:query,sys:role:create,sys:user:role:save,sys:org:delete,sys:org:query,sys:role:permission:list,sys:org:update,sys:role:permission:save,sys:role:update,system,sys:user:delete,sys:user:role:list,sys:org:list,sys:role:delete,sys:role:list,sys:org:create,sys:user:update,sys:permission:list,sys:role:query', 'String', 1, 1, '新建租户时角色权限', 0, '2026-02-26 16:46:20.392789', '2026-02-26 16:46:38.137264');
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (5, 'tenant.init.default.password', '123456', 'String', 1, 1, NULL, 0, '2026-02-26 16:46:52.124755', '2026-02-26 16:46:52.124755');
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (3, 'security.captcha.enabled', 'false', 'boolean', 1, 1, '是否开启验证码', 0, '2026-02-11 02:45:31.097324', '2026-03-10 09:40:33.084368');
|
||||||
|
INSERT INTO sys_param ("param_id", "param_key", "param_value", "param_type", "status", "is_system", "description", "is_deleted", "created_at", "updated_at") VALUES (1, 'security.token.access_ttl_minutes', '120', 'int', 1, 1, 'Access Token 有效期(分钟)', 0, '2026-02-09 09:54:21.888052', '2026-03-10 10:15:39.55035');
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO sys_user ( "username", "display_name", "email", "phone", "password_hash", "status", "is_deleted", "created_at", "updated_at", "is_platform_admin", "pwd_reset_required")
|
||||||
|
VALUES ( 'admin', '管理员', 'admin', NULL, '$2a$10$BOm1iCFj3ObfBeyQxOvjVO659vXvIRGOd4YR62r0TUHqSusWW5bFS', 1, 0, '2026-02-09 09:54:21.880637', '2026-02-28 17:57:32.63338', 't', NULL);
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (9, 'biz_hotword_category', '热词类别', 1, '语音识别纠错分类', '2026-02-28 17:08:52.362532', '2026-02-28 17:08:52.362532');
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (10, 'biz_prompt_category', '提示词分类', 1, '会议总结模板分类', '2026-02-28 17:47:50.999655', '2026-02-28 17:47:50.999655');
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (11, 'biz_ai_provider', '模型提供商', 1, 'AI 模型服务商分类', '2026-03-02 10:10:16.653182', '2026-03-02 10:10:16.653182');
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (12, 'biz_speaker_label', '发言人角色', 1, '会议发言人的身份标签', '2026-03-02 16:15:58.193117', '2026-03-02 16:15:58.193117');
|
||||||
|
INSERT INTO "sys_dict_type" ("dict_type_id", "type_code", "type_name", "status", "remark", "created_at", "updated_at") VALUES (13, 'biz_prompt_level', '提示词模板属性', 1, '用于定义提示词模板的层级属性:1-预置模板(系统或租户级),0-个人模板', '2026-03-04 10:54:30.49116', '2026-03-04 10:54:30.49116');
|
||||||
|
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (22, 'biz_hotword_category', '人名', 'person', 1, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (23, 'biz_hotword_category', '术语', 'term', 2, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (24, 'biz_hotword_category', '地名', 'location', 3, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (25, 'biz_hotword_category', '通用', 'general', 4, 1, NULL, '2026-02-28 17:08:52.374667', '2026-02-28 17:08:52.374667');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (26, 'biz_prompt_category', '全文纪要', 'summary', 1, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (27, 'biz_prompt_category', '待办提取', 'todo', 2, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (28, 'biz_prompt_category', '访谈整理', 'interview', 3, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (29, 'biz_prompt_category', '创意构思', 'creative', 4, 1, NULL, '2026-02-28 17:47:51.013288', '2026-02-28 17:47:51.013288');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (30, 'biz_ai_provider', '阿里云', 'Aliyun', 1, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (31, 'biz_ai_provider', 'OpenAI', 'OpenAI', 2, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (32, 'biz_ai_provider', 'Gemini', 'Gemini', 3, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (33, 'biz_ai_provider', 'DeepSeek', 'DeepSeek', 4, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (34, 'biz_ai_provider', 'Kimi', 'Kimi', 5, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (35, 'biz_ai_provider', '自定义/本地', 'Custom', 6, 1, NULL, '2026-03-02 10:10:16.665646', '2026-03-02 10:10:16.665646');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (36, 'biz_speaker_label', '主持人', 'host', 1, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (37, 'biz_speaker_label', '汇报人', 'speaker', 2, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (38, 'biz_speaker_label', '技术专家', 'expert', 3, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (39, 'biz_speaker_label', '客户代表', 'customer', 4, 1, NULL, '2026-03-02 16:15:58.205277', '2026-03-02 16:15:58.205277');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (40, 'biz_prompt_level', '预置模板', '1', 1, 1, '平台系统预置或租户共享预置', '2026-03-04 10:55:42.163768', '2026-03-04 10:55:42.163768');
|
||||||
|
INSERT INTO "sys_dict_item" ("dict_item_id", "type_code", "item_label", "item_value", "sort_order", "status", "remark", "created_at", "updated_at") VALUES (41, 'biz_prompt_level', '个人模板', '0', 2, 1, '个人私有模板', '2026-03-04 10:55:42.175269', '2026-03-04 10:55:42.175269');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,11 @@
|
||||||
<artifactId>easy-captcha</artifactId>
|
<artifactId>easy-captcha</artifactId>
|
||||||
<version>${easycaptcha.version}</version>
|
<version>${easycaptcha.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.belerweb</groupId>
|
||||||
|
<artifactId>pinyin4j</artifactId>
|
||||||
|
<version>2.5.1</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
|
@ -89,6 +94,41 @@
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
|
<artifactId>pdfbox</artifactId>
|
||||||
|
<version>2.0.30</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.poi</groupId>
|
||||||
|
<artifactId>poi-ooxml</artifactId>
|
||||||
|
<version>5.2.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.commonmark</groupId>
|
||||||
|
<artifactId>commonmark</artifactId>
|
||||||
|
<version>0.21.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.openhtmltopdf</groupId>
|
||||||
|
<artifactId>openhtmltopdf-core</artifactId>
|
||||||
|
<version>1.0.10</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.openhtmltopdf</groupId>
|
||||||
|
<artifactId>openhtmltopdf-pdfbox</artifactId>
|
||||||
|
<version>1.0.10</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jsoup</groupId>
|
||||||
|
<artifactId>jsoup</artifactId>
|
||||||
|
<version>1.17.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.unisbase</groupId>
|
||||||
|
<artifactId>unisbase-spring-boot-starter</artifactId>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
package com.imeeting.auth;
|
|
||||||
|
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.entity.SysTenant;
|
|
||||||
import com.imeeting.entity.SysUser;
|
|
||||||
import com.imeeting.security.LoginUser;
|
|
||||||
import com.imeeting.service.AuthScopeService;
|
|
||||||
import com.imeeting.service.AuthVersionService;
|
|
||||||
import com.imeeting.service.SysParamService;
|
|
||||||
import com.imeeting.service.SysPermissionService;
|
|
||||||
import com.imeeting.mapper.SysTenantMapper;
|
|
||||||
import com.imeeting.mapper.SysUserMapper;
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
|
||||||
private final SysPermissionService sysPermissionService;
|
|
||||||
private final SysTenantMapper sysTenantMapper;
|
|
||||||
private final SysUserMapper sysUserMapper;
|
|
||||||
private final SysParamService sysParamService;
|
|
||||||
private final StringRedisTemplate redisTemplate;
|
|
||||||
private final AuthScopeService authScopeService;
|
|
||||||
private final AuthVersionService authVersionService;
|
|
||||||
|
|
||||||
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider,
|
|
||||||
@Lazy SysPermissionService sysPermissionService,
|
|
||||||
SysTenantMapper sysTenantMapper,
|
|
||||||
SysUserMapper sysUserMapper,
|
|
||||||
@Lazy SysParamService sysParamService,
|
|
||||||
StringRedisTemplate redisTemplate,
|
|
||||||
AuthScopeService authScopeService,
|
|
||||||
AuthVersionService authVersionService) {
|
|
||||||
this.jwtTokenProvider = jwtTokenProvider;
|
|
||||||
this.sysPermissionService = sysPermissionService;
|
|
||||||
this.sysTenantMapper = sysTenantMapper;
|
|
||||||
this.sysUserMapper = sysUserMapper;
|
|
||||||
this.sysParamService = sysParamService;
|
|
||||||
this.redisTemplate = redisTemplate;
|
|
||||||
this.authScopeService = authScopeService;
|
|
||||||
this.authVersionService = authVersionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
|
||||||
throws ServletException, IOException {
|
|
||||||
String uri = request.getRequestURI();
|
|
||||||
// Skip filter for public endpoints
|
|
||||||
if (uri.startsWith("/auth/") || uri.equals("/api/params/value")) {
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String authHeader = request.getHeader("Authorization");
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
||||||
String token = authHeader.substring(7);
|
|
||||||
try {
|
|
||||||
Claims claims = jwtTokenProvider.parseToken(token);
|
|
||||||
String username = claims.get("username", String.class);
|
|
||||||
Long userId = claims.get("userId", Long.class);
|
|
||||||
Long tenantId = claims.get("tenantId", Long.class);
|
|
||||||
Number tokenAuthVersionNum = claims.get("authVersion", Number.class);
|
|
||||||
|
|
||||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
|
||||||
// 1. Validate User Status (Ignore Tenant isolation here)
|
|
||||||
SysUser user = sysUserMapper.selectByIdIgnoreTenant(userId);
|
|
||||||
if (user == null || user.getStatus() != 1 || user.getIsDeleted() != 0) {
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
response.getWriter().write("{\"code\":\"401\",\"msg\":\"User account is disabled or deleted\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Validate Tenant Status & Grace Period
|
|
||||||
// Skip validation for system platform tenant (ID=0)
|
|
||||||
Long activeTenantId = tenantId;
|
|
||||||
if (activeTenantId != null && !Long.valueOf(0).equals(activeTenantId)) {
|
|
||||||
SysTenant tenant = sysTenantMapper.selectByIdIgnoreTenant(activeTenantId);
|
|
||||||
if (tenant == null || tenant.getStatus() != 1) {
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Tenant is disabled\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tenant.getExpireTime() != null) {
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
|
||||||
if (now.isAfter(tenant.getExpireTime())) {
|
|
||||||
String graceDaysStr = sysParamService.getParamValue("sys.tenant.grace_period_days", "0");
|
|
||||||
int graceDays = Integer.parseInt(graceDaysStr);
|
|
||||||
if (now.isAfter(tenant.getExpireTime().plusDays(graceDays))) {
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Tenant subscription expired\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
long currentAuthVersion = authVersionService.getVersion(userId, activeTenantId);
|
|
||||||
long requestAuthVersion = tokenAuthVersionNum == null ? 0L : tokenAuthVersionNum.longValue();
|
|
||||||
if (currentAuthVersion != requestAuthVersion) {
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Token revoked\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Get Permissions (With Redis Cache, Key must include tenantId)
|
|
||||||
String permKey = RedisKeys.authPermKey(userId, activeTenantId, currentAuthVersion);
|
|
||||||
Set<String> permissions;
|
|
||||||
String cachedPerms = redisTemplate.opsForValue().get(permKey);
|
|
||||||
if (cachedPerms != null && !cachedPerms.trim().isEmpty()) {
|
|
||||||
permissions = Set.of(cachedPerms.split(","));
|
|
||||||
} else {
|
|
||||||
permissions = sysPermissionService.listPermissionCodesByUserId(userId, activeTenantId);
|
|
||||||
if (permissions != null && !permissions.isEmpty()) {
|
|
||||||
redisTemplate.opsForValue().set(permKey, String.join(",", permissions), java.time.Duration.ofHours(2));
|
|
||||||
} else {
|
|
||||||
permissions = Collections.emptySet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isTenantAdmin = authScopeService.isTenantAdmin(userId, activeTenantId);
|
|
||||||
LoginUser loginUser = new LoginUser(userId, activeTenantId, username, user.getIsPlatformAdmin(), isTenantAdmin, permissions);
|
|
||||||
|
|
||||||
UsernamePasswordAuthenticationToken authentication =
|
|
||||||
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
|
|
||||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
|
||||||
}
|
|
||||||
} catch (io.jsonwebtoken.ExpiredJwtException e) {
|
|
||||||
SecurityContextHolder.clearContext();
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Token expired\"}");
|
|
||||||
return;
|
|
||||||
} catch (io.jsonwebtoken.JwtException e) {
|
|
||||||
SecurityContextHolder.clearContext();
|
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
|
||||||
response.getWriter().write("{\"code\":\"401\",\"msg\":\"Invalid token\"}");
|
|
||||||
return;
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
SecurityContextHolder.clearContext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
package com.imeeting.auth;
|
|
||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import io.jsonwebtoken.Jwts;
|
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
|
||||||
import io.jsonwebtoken.security.Keys;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.Key;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class JwtTokenProvider {
|
|
||||||
private final Key key;
|
|
||||||
|
|
||||||
public JwtTokenProvider(@Value("${security.jwt.secret}") String secret) {
|
|
||||||
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String createToken(Map<String, Object> claims, long ttlMillis) {
|
|
||||||
Date now = new Date();
|
|
||||||
Date exp = new Date(now.getTime() + ttlMillis);
|
|
||||||
return Jwts.builder()
|
|
||||||
.setClaims(claims)
|
|
||||||
.setIssuedAt(now)
|
|
||||||
.setExpiration(exp)
|
|
||||||
.signWith(key, SignatureAlgorithm.HS256)
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Claims parseToken(String token) {
|
|
||||||
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package com.imeeting.auth.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class CaptchaResponse {
|
|
||||||
private String captchaId;
|
|
||||||
private String imageBase64;
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package com.imeeting.auth.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class DeviceCodeRequest {
|
|
||||||
@NotBlank
|
|
||||||
private String username;
|
|
||||||
@NotBlank
|
|
||||||
private String password;
|
|
||||||
private String captchaId;
|
|
||||||
private String captchaCode;
|
|
||||||
private String deviceName;
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package com.imeeting.auth.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class LoginRequest {
|
|
||||||
private String tenantCode;
|
|
||||||
@NotBlank
|
|
||||||
private String username;
|
|
||||||
@NotBlank
|
|
||||||
private String password;
|
|
||||||
private String captchaId;
|
|
||||||
private String captchaCode;
|
|
||||||
private String deviceCode;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
package com.imeeting.auth.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class RefreshRequest {
|
|
||||||
@NotBlank
|
|
||||||
private String refreshToken;
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package com.imeeting.auth.dto;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
public class TokenResponse {
|
|
||||||
private String accessToken;
|
|
||||||
private String refreshToken;
|
|
||||||
private long accessExpiresInMinutes;
|
|
||||||
private long refreshExpiresInDays;
|
|
||||||
private List<TenantInfo> availableTenants;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
public static class TenantInfo {
|
|
||||||
private Long tenantId;
|
|
||||||
private String tenantCode;
|
|
||||||
private String tenantName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
package com.imeeting.common;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class ApiResponse<T> {
|
|
||||||
private String code;
|
|
||||||
private String msg;
|
|
||||||
private T data;
|
|
||||||
|
|
||||||
public static <T> ApiResponse<T> ok(T data) {
|
|
||||||
return new ApiResponse<>("0", "OK", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> ApiResponse<T> error(String msg) {
|
|
||||||
return new ApiResponse<>("-1", msg, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package com.imeeting.common;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
||||||
|
|
||||||
@RestControllerAdvice
|
|
||||||
public class GlobalExceptionHandler {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
|
||||||
|
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
|
||||||
public ApiResponse<Void> handleIllegalArgument(IllegalArgumentException ex) {
|
|
||||||
log.warn("Business error: {}", ex.getMessage());
|
|
||||||
return ApiResponse.error(ex.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
|
|
||||||
public ApiResponse<Void> handleAccessDenied(org.springframework.security.access.AccessDeniedException ex) {
|
|
||||||
log.warn("Access denied: {}", ex.getMessage());
|
|
||||||
return ApiResponse.error("无权限操作");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
|
||||||
public ApiResponse<Void> handleGeneric(Exception ex) {
|
|
||||||
log.error("Unhandled exception", ex);
|
|
||||||
return ApiResponse.error("系统异常");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.imeeting.common;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class PageResult<T> {
|
|
||||||
private long total;
|
|
||||||
private T records;
|
|
||||||
}
|
|
||||||
|
|
@ -35,6 +35,14 @@ public final class RedisKeys {
|
||||||
return "sys:platform:config";
|
return "sys:platform:config";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String meetingProgressKey(Long meetingId) {
|
||||||
|
return "biz:meeting:progress:" + meetingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String meetingPollingLockKey(Long meetingId) {
|
||||||
|
return "biz:meeting:polling:lock:" + meetingId;
|
||||||
|
}
|
||||||
|
|
||||||
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
|
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
|
||||||
public static final String SYS_PARAM_FIELD_VALUE = "value";
|
public static final String SYS_PARAM_FIELD_VALUE = "value";
|
||||||
public static final String SYS_PARAM_FIELD_TYPE = "type";
|
public static final String SYS_PARAM_FIELD_TYPE = "type";
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package com.imeeting.common.annotation;
|
|
||||||
|
|
||||||
import java.lang.annotation.*;
|
|
||||||
|
|
||||||
@Target(ElementType.METHOD)
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
|
||||||
@Documented
|
|
||||||
public @interface Log {
|
|
||||||
String value() default ""; // 操作描述
|
|
||||||
String type() default ""; // 资源类型/模块名
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
package com.imeeting.common.aspect;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.imeeting.common.annotation.Log;
|
|
||||||
import com.imeeting.entity.SysLog;
|
|
||||||
import com.imeeting.security.LoginUser;
|
|
||||||
import com.imeeting.service.SysLogService;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
|
||||||
import org.aspectj.lang.annotation.Around;
|
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Aspect
|
|
||||||
@Component
|
|
||||||
public class LogAspect {
|
|
||||||
|
|
||||||
private final SysLogService sysLogService;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
public LogAspect(SysLogService sysLogService, ObjectMapper objectMapper) {
|
|
||||||
this.sysLogService = sysLogService;
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Around("@annotation(com.imeeting.common.annotation.Log)")
|
|
||||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
Object result = null;
|
|
||||||
Exception exception = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = point.proceed();
|
|
||||||
return result;
|
|
||||||
} catch (Exception e) {
|
|
||||||
exception = e;
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
saveLog(point, result, exception, System.currentTimeMillis() - start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveLog(ProceedingJoinPoint joinPoint, Object result, Exception e, long duration) {
|
|
||||||
try {
|
|
||||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
||||||
if (attributes == null) return;
|
|
||||||
HttpServletRequest request = attributes.getRequest();
|
|
||||||
|
|
||||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
|
||||||
Method method = signature.getMethod();
|
|
||||||
Log logAnnotation = method.getAnnotation(Log.class);
|
|
||||||
|
|
||||||
SysLog sysLog = new SysLog();
|
|
||||||
sysLog.setLogType("OPERATION");
|
|
||||||
sysLog.setOperation(logAnnotation.value());
|
|
||||||
sysLog.setMethod(request.getMethod() + " " + request.getRequestURI());
|
|
||||||
sysLog.setDuration(duration);
|
|
||||||
sysLog.setIp(request.getRemoteAddr());
|
|
||||||
sysLog.setCreatedAt(LocalDateTime.now());
|
|
||||||
|
|
||||||
// 仅保留请求参数,移除响应结果
|
|
||||||
sysLog.setParams(getArgsJson(joinPoint));
|
|
||||||
|
|
||||||
// 获取当前租户和用户信息
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
|
|
||||||
LoginUser user = (LoginUser) auth.getPrincipal();
|
|
||||||
sysLog.setUserId(user.getUserId());
|
|
||||||
sysLog.setTenantId(user.getTenantId());
|
|
||||||
sysLog.setUsername(user.getUsername());
|
|
||||||
}
|
|
||||||
|
|
||||||
sysLog.setStatus(e != null ? 0 : 1);
|
|
||||||
sysLogService.recordLog(sysLog);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getArgsJson(ProceedingJoinPoint joinPoint) {
|
|
||||||
try {
|
|
||||||
Object[] args = joinPoint.getArgs();
|
|
||||||
if (args == null || args.length == 0) return null;
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (Object arg : args) {
|
|
||||||
if (arg instanceof jakarta.servlet.ServletRequest
|
|
||||||
|| arg instanceof jakarta.servlet.ServletResponse
|
|
||||||
|| arg instanceof org.springframework.web.multipart.MultipartFile) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
sb.append(objectMapper.writeValueAsString(arg)).append(" ");
|
|
||||||
} catch (Exception e) {
|
|
||||||
sb.append("[Unserializable Argument] ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString().trim();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "[Error capturing params]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package com.imeeting.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
|
||||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
|
||||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
|
||||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
|
||||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class CacheConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
|
|
||||||
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
|
|
||||||
.entryTtl(Duration.ofHours(1)) // Default TTL
|
|
||||||
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
|
|
||||||
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
|
|
||||||
.disableCachingNullValues();
|
|
||||||
|
|
||||||
return RedisCacheManager.builder(connectionFactory)
|
|
||||||
.cacheDefaults(config)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package com.imeeting.config;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
|
||||||
import com.imeeting.security.LoginUser;
|
|
||||||
import net.sf.jsqlparser.expression.Expression;
|
|
||||||
import net.sf.jsqlparser.expression.LongValue;
|
|
||||||
import org.apache.ibatis.reflection.MetaObject;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class MybatisPlusConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
|
||||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
|
||||||
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
|
|
||||||
@Override
|
|
||||||
public Expression getTenantId() {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
|
|
||||||
LoginUser user = (LoginUser) auth.getPrincipal();
|
|
||||||
if (user.getTenantId() != null) {
|
|
||||||
return new LongValue(user.getTenantId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If no tenant context (e.g. system task or error), return 0
|
|
||||||
return new LongValue(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getTenantIdColumn() {
|
|
||||||
return "tenant_id";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean ignoreTable(String tableName) {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
|
|
||||||
LoginUser user = (LoginUser) auth.getPrincipal();
|
|
||||||
// 只有当平台管理员处于系统租户(0)时,才忽略所有过滤。
|
|
||||||
// 如果他切换到了具体租户(>0),则必须接受过滤,确保只能看到当前租户数据。
|
|
||||||
if (Boolean.TRUE.equals(user.getIsPlatformAdmin()) && Long.valueOf(0).equals(user.getTenantId())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公共表始终忽略过滤
|
|
||||||
return List.of("sys_tenant","sys_platform_config", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param").contains(tableName.toLowerCase());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
|
||||||
return interceptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public MetaObjectHandler metaObjectHandler() {
|
|
||||||
return new MetaObjectHandler() {
|
|
||||||
@Override
|
|
||||||
public void insertFill(MetaObject metaObject) {
|
|
||||||
strictInsertFill(metaObject, "createdAt", LocalDateTime::now, LocalDateTime.class);
|
|
||||||
strictInsertFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
|
|
||||||
strictInsertFill(metaObject, "status", () -> 1, Integer.class);
|
|
||||||
strictInsertFill(metaObject, "isDeleted", () -> 0, Integer.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void updateFill(MetaObject metaObject) {
|
|
||||||
strictUpdateFill(metaObject, "updatedAt", LocalDateTime::now, LocalDateTime.class);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
package com.imeeting.config;
|
|
||||||
|
|
||||||
import com.imeeting.auth.JwtAuthenticationFilter;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
|
||||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@EnableMethodSecurity
|
|
||||||
public class SecurityConfig {
|
|
||||||
@Bean
|
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
|
|
||||||
http.csrf(csrf -> csrf.disable())
|
|
||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
||||||
.authorizeHttpRequests(auth -> auth
|
|
||||||
.requestMatchers("/auth/**").permitAll()
|
|
||||||
.requestMatchers("/api/open/**").permitAll()
|
|
||||||
.requestMatchers("/api/static/**").permitAll()
|
|
||||||
.requestMatchers("/api/params/value").permitAll()
|
|
||||||
.anyRequest().authenticated()
|
|
||||||
)
|
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
|
|
||||||
return configuration.getAuthenticationManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
|
||||||
CorsConfiguration config = new CorsConfiguration();
|
|
||||||
config.setAllowedOriginPatterns(List.of("*"));
|
|
||||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
|
||||||
config.setAllowedHeaders(List.of("*"));
|
|
||||||
config.setAllowCredentials(true);
|
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
||||||
source.registerCorsConfiguration("/**", config);
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package com.imeeting.config;
|
|
||||||
|
|
||||||
import com.imeeting.service.SysParamService;
|
|
||||||
import org.springframework.boot.ApplicationArguments;
|
|
||||||
import org.springframework.boot.ApplicationRunner;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class SysParamCacheInitializer implements ApplicationRunner {
|
|
||||||
private final SysParamService sysParamService;
|
|
||||||
|
|
||||||
public SysParamCacheInitializer(SysParamService sysParamService) {
|
|
||||||
this.sysParamService = sysParamService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(ApplicationArguments args) {
|
|
||||||
sysParamService.syncAllToCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,10 +10,10 @@ import java.io.File;
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Value("${app.upload-path}")
|
@Value("${unisbase.app.upload-path}")
|
||||||
private String uploadPath;
|
private String uploadPath;
|
||||||
|
|
||||||
@Value("${app.resource-prefix}")
|
@Value("${unisbase.app.resource-prefix}")
|
||||||
private String resourcePrefix;
|
private String resourcePrefix;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.imeeting.auth.JwtTokenProvider;
|
|
||||||
import com.imeeting.auth.dto.CaptchaResponse;
|
|
||||||
import com.imeeting.auth.dto.DeviceCodeRequest;
|
|
||||||
import com.imeeting.auth.dto.LoginRequest;
|
|
||||||
import com.imeeting.auth.dto.RefreshRequest;
|
|
||||||
import com.imeeting.auth.dto.TokenResponse;
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.common.RedisKeys;
|
|
||||||
import com.imeeting.common.SysParamKeys;
|
|
||||||
import com.imeeting.service.SysParamService;
|
|
||||||
import com.imeeting.service.AuthService;
|
|
||||||
import com.wf.captcha.SpecCaptcha;
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/auth")
|
|
||||||
public class AuthController {
|
|
||||||
private final AuthService authService;
|
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
|
||||||
private final SysParamService sysParamService;
|
|
||||||
|
|
||||||
@Value("${app.captcha.ttl-seconds:120}")
|
|
||||||
private long captchaTtlSeconds;
|
|
||||||
|
|
||||||
public AuthController(AuthService authService, StringRedisTemplate stringRedisTemplate,
|
|
||||||
JwtTokenProvider jwtTokenProvider, SysParamService sysParamService) {
|
|
||||||
this.authService = authService;
|
|
||||||
this.stringRedisTemplate = stringRedisTemplate;
|
|
||||||
this.jwtTokenProvider = jwtTokenProvider;
|
|
||||||
this.sysParamService = sysParamService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/captcha")
|
|
||||||
public ApiResponse<CaptchaResponse> captcha() {
|
|
||||||
if (!isCaptchaEnabled()) {
|
|
||||||
return ApiResponse.error("Captcha disabled");
|
|
||||||
}
|
|
||||||
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
|
|
||||||
String code = captcha.text();
|
|
||||||
String imageBase64 = captcha.toBase64();
|
|
||||||
String captchaId = UUID.randomUUID().toString().replace("-", "");
|
|
||||||
|
|
||||||
stringRedisTemplate.opsForValue().set(RedisKeys.captchaKey(captchaId), code, Duration.ofSeconds(captchaTtlSeconds));
|
|
||||||
return ApiResponse.ok(new CaptchaResponse(captchaId, imageBase64));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/device-code")
|
|
||||||
public ApiResponse<String> deviceCode(@Valid @RequestBody DeviceCodeRequest request) {
|
|
||||||
LoginRequest loginRequest = new LoginRequest();
|
|
||||||
loginRequest.setUsername(request.getUsername());
|
|
||||||
loginRequest.setPassword(request.getPassword());
|
|
||||||
loginRequest.setCaptchaId(request.getCaptchaId());
|
|
||||||
loginRequest.setCaptchaCode(request.getCaptchaCode());
|
|
||||||
String deviceCode = authService.createDeviceCode(loginRequest, request.getDeviceName());
|
|
||||||
return ApiResponse.ok(deviceCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/login")
|
|
||||||
public ApiResponse<TokenResponse> login(@Valid @RequestBody LoginRequest request) {
|
|
||||||
return ApiResponse.ok(authService.login(request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/refresh")
|
|
||||||
public ApiResponse<TokenResponse> refresh(@Valid @RequestBody RefreshRequest request) {
|
|
||||||
return ApiResponse.ok(authService.refresh(request.getRefreshToken()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/switch-tenant")
|
|
||||||
public ApiResponse<TokenResponse> switchTenant(@RequestParam Long tenantId, @RequestHeader("Authorization") String authorization) {
|
|
||||||
String token = authorization.replace("Bearer ", "");
|
|
||||||
var claims = jwtTokenProvider.parseToken(token);
|
|
||||||
Long userId = claims.get("userId", Long.class);
|
|
||||||
String deviceCode = claims.get("deviceCode", String.class);
|
|
||||||
return ApiResponse.ok(authService.switchTenant(userId, tenantId, deviceCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/logout")
|
|
||||||
public ApiResponse<Void> logout(@RequestHeader("Authorization") String authorization) {
|
|
||||||
String token = authorization.replace("Bearer ", "");
|
|
||||||
var claims = jwtTokenProvider.parseToken(token);
|
|
||||||
Long userId = claims.get("userId", Long.class);
|
|
||||||
String deviceCode = claims.get("deviceCode", String.class);
|
|
||||||
authService.logout(userId, deviceCode);
|
|
||||||
return ApiResponse.ok(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCaptchaEnabled() {
|
|
||||||
String value = sysParamService.getCachedParamValue(SysParamKeys.CAPTCHA_ENABLED, "true");
|
|
||||||
return Boolean.parseBoolean(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.entity.Device;
|
|
||||||
import com.imeeting.service.DeviceService;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/devices")
|
|
||||||
public class DeviceController {
|
|
||||||
private final DeviceService deviceService;
|
|
||||||
|
|
||||||
public DeviceController(DeviceService deviceService) {
|
|
||||||
this.deviceService = deviceService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public ApiResponse<List<Device>> list() {
|
|
||||||
return ApiResponse.ok(deviceService.list());
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public ApiResponse<Device> get(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(deviceService.getById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
public ApiResponse<Boolean> create(@RequestBody Device device) {
|
|
||||||
return ApiResponse.ok(deviceService.save(device));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody Device device) {
|
|
||||||
device.setDeviceId(id);
|
|
||||||
return ApiResponse.ok(deviceService.updateById(device));
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(deviceService.removeById(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.entity.SysDictItem;
|
|
||||||
import com.imeeting.service.SysDictItemService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/dict-items")
|
|
||||||
public class DictItemController {
|
|
||||||
private final SysDictItemService sysDictItemService;
|
|
||||||
|
|
||||||
public DictItemController(SysDictItemService sysDictItemService) {
|
|
||||||
this.sysDictItemService = sysDictItemService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:list')")
|
|
||||||
public ApiResponse<List<SysDictItem>> list(@RequestParam(required = false) String typeCode) {
|
|
||||||
LambdaQueryWrapper<SysDictItem> queryWrapper = new LambdaQueryWrapper<>();
|
|
||||||
if (typeCode != null && !typeCode.isEmpty()) {
|
|
||||||
queryWrapper.eq(SysDictItem::getTypeCode, typeCode);
|
|
||||||
}
|
|
||||||
queryWrapper.orderByAsc(SysDictItem::getSortOrder);
|
|
||||||
return ApiResponse.ok(sysDictItemService.list(queryWrapper));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:query')")
|
|
||||||
public ApiResponse<SysDictItem> get(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(sysDictItemService.getById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:create')")
|
|
||||||
public ApiResponse<Boolean> create(@RequestBody SysDictItem dictItem) {
|
|
||||||
return ApiResponse.ok(sysDictItemService.save(dictItem));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:update')")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysDictItem dictItem) {
|
|
||||||
dictItem.setDictItemId(id);
|
|
||||||
return ApiResponse.ok(sysDictItemService.updateById(dictItem));
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:delete')")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(sysDictItemService.removeById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/type/{typeCode}")
|
|
||||||
// @PreAuthorize("@ss.hasPermi('sys_dict:query')")
|
|
||||||
public ApiResponse<List<SysDictItem>> getByType(@PathVariable String typeCode) {
|
|
||||||
return ApiResponse.ok(sysDictItemService.getItemsByTypeCode(typeCode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.entity.SysDictType;
|
|
||||||
import com.imeeting.service.SysDictTypeService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/dict-types")
|
|
||||||
public class DictTypeController {
|
|
||||||
private final SysDictTypeService sysDictTypeService;
|
|
||||||
|
|
||||||
public DictTypeController(SysDictTypeService sysDictTypeService) {
|
|
||||||
this.sysDictTypeService = sysDictTypeService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:list')")
|
|
||||||
public ApiResponse<Page<SysDictType>> list(
|
|
||||||
@RequestParam(defaultValue = "1") Integer current,
|
|
||||||
@RequestParam(defaultValue = "10") Integer size,
|
|
||||||
@RequestParam(required = false) String typeCode,
|
|
||||||
@RequestParam(required = false) String typeName) {
|
|
||||||
Page<SysDictType> page = new Page<>(current, size);
|
|
||||||
LambdaQueryWrapper<SysDictType> queryWrapper = new LambdaQueryWrapper<>();
|
|
||||||
if (typeCode != null && !typeCode.isEmpty()) {
|
|
||||||
queryWrapper.like(SysDictType::getTypeCode, typeCode);
|
|
||||||
}
|
|
||||||
if (typeName != null && !typeName.isEmpty()) {
|
|
||||||
queryWrapper.like(SysDictType::getTypeName, typeName);
|
|
||||||
}
|
|
||||||
queryWrapper.orderByAsc(SysDictType::getTypeCode);
|
|
||||||
return ApiResponse.ok(sysDictTypeService.page(page, queryWrapper));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:query')")
|
|
||||||
public ApiResponse<SysDictType> get(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(sysDictTypeService.getById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:create')")
|
|
||||||
public ApiResponse<Boolean> create(@RequestBody SysDictType dictType) {
|
|
||||||
return ApiResponse.ok(sysDictTypeService.save(dictType));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:update')")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysDictType dictType) {
|
|
||||||
dictType.setDictTypeId(id);
|
|
||||||
return ApiResponse.ok(sysDictTypeService.updateById(dictType));
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:delete')")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(sysDictTypeService.removeById(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.dto.PermissionNode;
|
|
||||||
import com.imeeting.entity.SysPermission;
|
|
||||||
import com.imeeting.entity.SysRole;
|
|
||||||
import com.imeeting.mapper.SysRolePermissionMapper;
|
|
||||||
import com.imeeting.mapper.SysUserRoleMapper;
|
|
||||||
import com.imeeting.service.AuthVersionService;
|
|
||||||
import com.imeeting.service.SysPermissionService;
|
|
||||||
import com.imeeting.service.SysRoleService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/permissions")
|
|
||||||
public class PermissionController {
|
|
||||||
private final SysPermissionService sysPermissionService;
|
|
||||||
private final SysRolePermissionMapper sysRolePermissionMapper;
|
|
||||||
private final SysUserRoleMapper sysUserRoleMapper;
|
|
||||||
private final SysRoleService sysRoleService;
|
|
||||||
private final AuthVersionService authVersionService;
|
|
||||||
|
|
||||||
public PermissionController(SysPermissionService sysPermissionService,
|
|
||||||
SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper,
|
|
||||||
SysRoleService sysRoleService, AuthVersionService authVersionService) {
|
|
||||||
this.sysPermissionService = sysPermissionService;
|
|
||||||
this.sysRolePermissionMapper = sysRolePermissionMapper;
|
|
||||||
this.sysUserRoleMapper = sysUserRoleMapper;
|
|
||||||
this.sysRoleService = sysRoleService;
|
|
||||||
this.authVersionService = authVersionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:permission:list')")
|
|
||||||
public ApiResponse<List<SysPermission>> list() {
|
|
||||||
Long tenantId = getCurrentTenantId();
|
|
||||||
// 平台管理员查询所有
|
|
||||||
if (Long.valueOf(0).equals(tenantId)) {
|
|
||||||
return ApiResponse.ok(sysPermissionService.list());
|
|
||||||
}
|
|
||||||
// 非平台管理员只能查询自己拥有的权限
|
|
||||||
return ApiResponse.ok(sysPermissionService.listByUserId(getCurrentUserId(), tenantId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/me")
|
|
||||||
public ApiResponse<List<SysPermission>> myPermissions() {
|
|
||||||
return ApiResponse.ok(sysPermissionService.listByUserId(getCurrentUserId(), getCurrentTenantId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/tree")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:permission:list')")
|
|
||||||
public ApiResponse<List<PermissionNode>> tree() {
|
|
||||||
Long tenantId = getCurrentTenantId();
|
|
||||||
List<SysPermission> list;
|
|
||||||
if (Long.valueOf(0).equals(tenantId)) {
|
|
||||||
list = sysPermissionService.list();
|
|
||||||
} else {
|
|
||||||
list = sysPermissionService.listByUserId(getCurrentUserId(), tenantId);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(buildTree(list));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/tree/me")
|
|
||||||
public ApiResponse<List<PermissionNode>> myTree() {
|
|
||||||
return ApiResponse.ok(buildTree(sysPermissionService.listByUserId(getCurrentUserId(), getCurrentTenantId())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:permission:query')")
|
|
||||||
public ApiResponse<SysPermission> get(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(sysPermissionService.getById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:permission:create')")
|
|
||||||
public ApiResponse<Boolean> create(@RequestBody SysPermission perm) {
|
|
||||||
String error = validateParent(perm);
|
|
||||||
if (error != null) {
|
|
||||||
return ApiResponse.error(error);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(sysPermissionService.save(perm));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:permission:update')")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysPermission perm) {
|
|
||||||
List<Long> roleIds = sysRolePermissionMapper.selectRoleIdsByPermId(id);
|
|
||||||
perm.setPermId(id);
|
|
||||||
String error = validateParent(perm);
|
|
||||||
if (error != null) {
|
|
||||||
return ApiResponse.error(error);
|
|
||||||
}
|
|
||||||
boolean updated = sysPermissionService.updateById(perm);
|
|
||||||
if (perm.getLevel() != null && perm.getLevel() == 1) {
|
|
||||||
sysPermissionService.lambdaUpdate()
|
|
||||||
.set(SysPermission::getParentId, null)
|
|
||||||
.eq(SysPermission::getPermId, id)
|
|
||||||
.update();
|
|
||||||
}
|
|
||||||
if (updated) {
|
|
||||||
invalidateRoleUsers(roleIds);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:permission:delete')")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
List<Long> roleIds = sysRolePermissionMapper.selectRoleIdsByPermId(id);
|
|
||||||
boolean removed = sysPermissionService.removeById(id);
|
|
||||||
if (removed) {
|
|
||||||
invalidateRoleUsers(roleIds);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getCurrentUserId() {
|
|
||||||
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
|
|
||||||
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getUserId();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getCurrentTenantId() {
|
|
||||||
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
|
|
||||||
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getTenantId();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String validateParent(SysPermission perm) {
|
|
||||||
if (perm.getLevel() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (perm.getPermType() != null && "button".equalsIgnoreCase(perm.getPermType())) {
|
|
||||||
if (perm.getCode() == null || perm.getCode().trim().isEmpty()) {
|
|
||||||
return "Code required for button permission";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (perm.getLevel() == 1) {
|
|
||||||
perm.setParentId(null);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (perm.getLevel() == 2) {
|
|
||||||
if (perm.getParentId() == null) {
|
|
||||||
return "ParentId required for level 2";
|
|
||||||
}
|
|
||||||
SysPermission parent = sysPermissionService.getById(perm.getParentId());
|
|
||||||
if (parent == null) {
|
|
||||||
return "Parent not found";
|
|
||||||
}
|
|
||||||
if (parent.getLevel() == null || parent.getLevel() != 1) {
|
|
||||||
return "Parent must be level 1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<PermissionNode> buildTree(List<SysPermission> list) {
|
|
||||||
Map<Long, PermissionNode> map = new HashMap<>();
|
|
||||||
List<PermissionNode> roots = new ArrayList<>();
|
|
||||||
for (SysPermission p : list) {
|
|
||||||
PermissionNode node = toNode(p);
|
|
||||||
map.put(node.getPermId(), node);
|
|
||||||
}
|
|
||||||
for (PermissionNode node : map.values()) {
|
|
||||||
Long parentId = node.getParentId();
|
|
||||||
if (parentId != null && map.containsKey(parentId)) {
|
|
||||||
map.get(parentId).getChildren().add(node);
|
|
||||||
} else {
|
|
||||||
roots.add(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortTree(roots);
|
|
||||||
return roots;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sortTree(List<PermissionNode> nodes) {
|
|
||||||
nodes.sort(Comparator.comparingInt(n -> n.getSortOrder() == null ? 0 : n.getSortOrder()));
|
|
||||||
for (PermissionNode node : nodes) {
|
|
||||||
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
|
|
||||||
sortTree(node.getChildren());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PermissionNode toNode(SysPermission p) {
|
|
||||||
PermissionNode node = new PermissionNode();
|
|
||||||
node.setPermId(p.getPermId());
|
|
||||||
node.setParentId(p.getParentId());
|
|
||||||
node.setName(p.getName());
|
|
||||||
node.setCode(p.getCode());
|
|
||||||
node.setPermType(p.getPermType());
|
|
||||||
node.setLevel(p.getLevel());
|
|
||||||
node.setPath(p.getPath());
|
|
||||||
node.setComponent(p.getComponent());
|
|
||||||
node.setIcon(p.getIcon());
|
|
||||||
node.setSortOrder(p.getSortOrder());
|
|
||||||
node.setIsVisible(p.getIsVisible());
|
|
||||||
node.setStatus(p.getStatus());
|
|
||||||
node.setDescription(p.getDescription());
|
|
||||||
node.setMeta(p.getMeta());
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void invalidateRoleUsers(List<Long> roleIds) {
|
|
||||||
if (roleIds == null || roleIds.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (Long roleId : roleIds) {
|
|
||||||
if (roleId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
SysRole role = sysRoleService.getById(roleId);
|
|
||||||
if (role == null || role.getTenantId() == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<Long> userIds = sysUserRoleMapper.selectUserIdsByRoleId(roleId);
|
|
||||||
authVersionService.invalidateUsersTenantAuth(userIds, role.getTenantId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.dto.PlatformConfigVO;
|
|
||||||
import com.imeeting.entity.SysPlatformConfig;
|
|
||||||
import com.imeeting.service.SysPlatformConfigService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api")
|
|
||||||
public class PlatformConfigController {
|
|
||||||
|
|
||||||
private final SysPlatformConfigService platformConfigService;
|
|
||||||
|
|
||||||
public PlatformConfigController(SysPlatformConfigService platformConfigService) {
|
|
||||||
this.platformConfigService = platformConfigService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 公开配置接口 (用于登录页、favicon等)
|
|
||||||
*/
|
|
||||||
@GetMapping("/open/platform/config")
|
|
||||||
public ApiResponse<PlatformConfigVO> getOpenConfig() {
|
|
||||||
return ApiResponse.ok(platformConfigService.getConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取管理配置 (需要登录)
|
|
||||||
*/
|
|
||||||
@GetMapping("/admin/platform/config")
|
|
||||||
@PreAuthorize("isAuthenticated()")
|
|
||||||
public ApiResponse<PlatformConfigVO> getAdminConfig() {
|
|
||||||
return ApiResponse.ok(platformConfigService.getConfig());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新配置 (仅限平台管理员)
|
|
||||||
*/
|
|
||||||
@PutMapping("/admin/platform/config")
|
|
||||||
@PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')")
|
|
||||||
public ApiResponse<Boolean> updateConfig(@RequestBody SysPlatformConfig config) {
|
|
||||||
return ApiResponse.ok(platformConfigService.updateConfig(config));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传资源 (仅限平台管理员)
|
|
||||||
*/
|
|
||||||
@PostMapping("/admin/platform/config/upload")
|
|
||||||
@PreAuthorize("hasRole('ADMIN') or @ss.hasPermi('sys_platform:config:update')")
|
|
||||||
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) {
|
|
||||||
return ApiResponse.ok(platformConfigService.uploadAsset(file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,344 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.common.annotation.Log;
|
|
||||||
import com.imeeting.entity.SysRole;
|
|
||||||
import com.imeeting.entity.SysRolePermission;
|
|
||||||
import com.imeeting.entity.SysUser;
|
|
||||||
import com.imeeting.entity.SysUserRole;
|
|
||||||
import com.imeeting.mapper.SysRolePermissionMapper;
|
|
||||||
import com.imeeting.mapper.SysUserRoleMapper;
|
|
||||||
import com.imeeting.service.AuthScopeService;
|
|
||||||
import com.imeeting.service.AuthVersionService;
|
|
||||||
import com.imeeting.service.SysRoleService;
|
|
||||||
import com.imeeting.service.SysUserService;
|
|
||||||
import com.imeeting.service.SysPermissionService;
|
|
||||||
import com.imeeting.service.SysTenantUserService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/roles")
|
|
||||||
public class RoleController {
|
|
||||||
private final SysRoleService sysRoleService;
|
|
||||||
private final SysUserService sysUserService;
|
|
||||||
private final SysRolePermissionMapper sysRolePermissionMapper;
|
|
||||||
private final SysUserRoleMapper sysUserRoleMapper;
|
|
||||||
private final SysPermissionService sysPermissionService;
|
|
||||||
private final AuthScopeService authScopeService;
|
|
||||||
private final AuthVersionService authVersionService;
|
|
||||||
private final SysTenantUserService sysTenantUserService;
|
|
||||||
|
|
||||||
public RoleController(SysRoleService sysRoleService, SysUserService sysUserService,
|
|
||||||
SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper,
|
|
||||||
SysPermissionService sysPermissionService,
|
|
||||||
AuthScopeService authScopeService,
|
|
||||||
AuthVersionService authVersionService,
|
|
||||||
SysTenantUserService sysTenantUserService) {
|
|
||||||
this.sysRoleService = sysRoleService;
|
|
||||||
this.sysUserService = sysUserService;
|
|
||||||
this.sysRolePermissionMapper = sysRolePermissionMapper;
|
|
||||||
this.sysUserRoleMapper = sysUserRoleMapper;
|
|
||||||
this.sysPermissionService = sysPermissionService;
|
|
||||||
this.authScopeService = authScopeService;
|
|
||||||
this.authVersionService = authVersionService;
|
|
||||||
this.sysTenantUserService = sysTenantUserService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:list')")
|
|
||||||
public ApiResponse<List<SysRole>> list(@RequestParam(required = false) Long tenantId) {
|
|
||||||
QueryWrapper<SysRole> wrapper = new QueryWrapper<>();
|
|
||||||
|
|
||||||
if (authScopeService.isCurrentPlatformAdmin()) {
|
|
||||||
if (tenantId != null) {
|
|
||||||
wrapper.eq("tenant_id", tenantId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
wrapper.eq("tenant_id", currentTenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiResponse.ok(sysRoleService.list(wrapper));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}/users")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:query')")
|
|
||||||
public ApiResponse<List<SysUser>> listUsers(@PathVariable Long id) {
|
|
||||||
SysRole role = sysRoleService.getById(id);
|
|
||||||
if (role == null) {
|
|
||||||
return ApiResponse.error("角色不存在");
|
|
||||||
}
|
|
||||||
if (!canAccessTenant(role.getTenantId())) {
|
|
||||||
return ApiResponse.error("禁止跨租户查看角色用户");
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(sysUserService.listUsersByRoleId(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:query')")
|
|
||||||
public ApiResponse<SysRole> get(@PathVariable Long id) {
|
|
||||||
SysRole role = sysRoleService.getById(id);
|
|
||||||
if (role == null) {
|
|
||||||
return ApiResponse.error("角色不存在");
|
|
||||||
}
|
|
||||||
if (!canAccessTenant(role.getTenantId())) {
|
|
||||||
return ApiResponse.error("禁止跨租户查看角色");
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:create')")
|
|
||||||
@Log(value = "新增角色", type = "角色管理")
|
|
||||||
public ApiResponse<Boolean> create(@RequestBody SysRole role) {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
if (currentTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin()) {
|
|
||||||
role.setTenantId(currentTenantId);
|
|
||||||
} else if (role.getTenantId() == null) {
|
|
||||||
return ApiResponse.error("tenantId required for platform role creation");
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(sysRoleService.save(role));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:update')")
|
|
||||||
@Log(value = "修改角色", type = "角色管理")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysRole role) {
|
|
||||||
SysRole existing = sysRoleService.getById(id);
|
|
||||||
if (existing == null) {
|
|
||||||
return ApiResponse.error("角色不存在");
|
|
||||||
}
|
|
||||||
if (!canAccessTenant(existing.getTenantId())) {
|
|
||||||
return ApiResponse.error("禁止跨租户修改角色");
|
|
||||||
}
|
|
||||||
role.setRoleId(id);
|
|
||||||
role.setTenantId(existing.getTenantId());
|
|
||||||
return ApiResponse.ok(sysRoleService.updateById(role));
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:delete')")
|
|
||||||
@Log(value = "删除角色", type = "角色管理")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
SysRole existing = sysRoleService.getById(id);
|
|
||||||
if (existing == null) {
|
|
||||||
return ApiResponse.error("角色不存在");
|
|
||||||
}
|
|
||||||
if (!canAccessTenant(existing.getTenantId())) {
|
|
||||||
return ApiResponse.error("禁止跨租户删除角色");
|
|
||||||
}
|
|
||||||
if ("TENANT_ADMIN".equalsIgnoreCase(existing.getRoleCode()) && !authScopeService.isCurrentPlatformAdmin()) {
|
|
||||||
return ApiResponse.error("租户管理员角色只能由平台管理员删除");
|
|
||||||
}
|
|
||||||
List<Long> userIds = sysUserRoleMapper.selectUserIdsByRoleId(id);
|
|
||||||
boolean removed = sysRoleService.removeById(id);
|
|
||||||
if (removed) {
|
|
||||||
authVersionService.invalidateUsersTenantAuth(userIds, existing.getTenantId());
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}/permissions")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:permission:list')")
|
|
||||||
public ApiResponse<List<Long>> listRolePermissions(@PathVariable Long id) {
|
|
||||||
SysRole targetRole = sysRoleService.getById(id);
|
|
||||||
if (targetRole == null) {
|
|
||||||
return ApiResponse.error("角色不存在");
|
|
||||||
}
|
|
||||||
if (!canAccessTenant(targetRole.getTenantId())) {
|
|
||||||
return ApiResponse.error("禁止跨租户查看角色权限");
|
|
||||||
}
|
|
||||||
List<SysRolePermission> rows = sysRolePermissionMapper.selectList(
|
|
||||||
new QueryWrapper<SysRolePermission>().eq("role_id", id)
|
|
||||||
);
|
|
||||||
List<Long> permIds = new ArrayList<>();
|
|
||||||
for (SysRolePermission row : rows) {
|
|
||||||
if (row.getPermId() != null) {
|
|
||||||
permIds.add(row.getPermId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(permIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{id}/permissions")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:permission:save')")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public ApiResponse<Boolean> saveRolePermissions(@PathVariable Long id, @RequestBody PermissionBindingPayload payload) {
|
|
||||||
List<Long> permIds = payload == null ? null : payload.getPermIds();
|
|
||||||
|
|
||||||
// 权限越权校验
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
if (currentTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
SysRole targetRole = sysRoleService.getById(id);
|
|
||||||
if (targetRole == null) {
|
|
||||||
return ApiResponse.error("角色不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关键校验:只有平台管理员可以修改 TENANT_ADMIN 角色的权限
|
|
||||||
if ("TENANT_ADMIN".equalsIgnoreCase(targetRole.getRoleCode())) {
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin()) {
|
|
||||||
return ApiResponse.error("租户管理员角色的权限只能由平台管理员修改");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!canAccessTenant(targetRole.getTenantId())) {
|
|
||||||
return ApiResponse.error("禁止跨租户修改角色权限");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin()) {
|
|
||||||
List<com.imeeting.entity.SysPermission> myPerms = sysPermissionService.listByUserId(getCurrentUserId(), currentTenantId);
|
|
||||||
|
|
||||||
Set<Long> myPermIds = myPerms.stream()
|
|
||||||
.map(com.imeeting.entity.SysPermission::getPermId)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
if (permIds != null) {
|
|
||||||
for (Long pId : permIds) {
|
|
||||||
if (!myPermIds.contains(pId)) {
|
|
||||||
return ApiResponse.error("越权分配权限:" + pId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sysRolePermissionMapper.delete(new QueryWrapper<SysRolePermission>().eq("role_id", id));
|
|
||||||
if (permIds == null || permIds.isEmpty()) {
|
|
||||||
authVersionService.invalidateUsersTenantAuth(sysUserRoleMapper.selectUserIdsByRoleId(id), targetRole.getTenantId());
|
|
||||||
return ApiResponse.ok(true);
|
|
||||||
}
|
|
||||||
for (Long permId : permIds) {
|
|
||||||
if (permId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
SysRolePermission item = new SysRolePermission();
|
|
||||||
item.setRoleId(id);
|
|
||||||
item.setPermId(permId);
|
|
||||||
sysRolePermissionMapper.insert(item);
|
|
||||||
}
|
|
||||||
authVersionService.invalidateUsersTenantAuth(sysUserRoleMapper.selectUserIdsByRoleId(id), targetRole.getTenantId());
|
|
||||||
return ApiResponse.ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{id}/users")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:update')")
|
|
||||||
@Log(value = "角色关联用户", type = "角色管理")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public ApiResponse<Boolean> bindUsers(@PathVariable Long id, @RequestBody UserBindingPayload payload) {
|
|
||||||
if (payload == null || payload.getUserIds() == null) {
|
|
||||||
return ApiResponse.ok(true);
|
|
||||||
}
|
|
||||||
SysRole role = sysRoleService.getById(id);
|
|
||||||
if (role == null || role.getRoleId() == null || role.getTenantId() == null) {
|
|
||||||
return ApiResponse.error("角色不存在");
|
|
||||||
}
|
|
||||||
if (!canAccessTenant(role.getTenantId())) {
|
|
||||||
return ApiResponse.error("禁止跨租户绑定用户");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Long> toInsertUserIds = new ArrayList<>();
|
|
||||||
for (Long userId : payload.getUserIds()) {
|
|
||||||
if (userId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修复:处理逻辑删除导致的唯一键冲突
|
|
||||||
// 执行物理删除,彻底清除旧记录(包括已逻辑删除的)
|
|
||||||
sysUserRoleMapper.physicalDelete(id, userId, role.getTenantId());
|
|
||||||
|
|
||||||
// 确保该用户属于该租户
|
|
||||||
boolean hasMembership = sysTenantUserService.count(
|
|
||||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.imeeting.entity.SysTenantUser>()
|
|
||||||
.eq(com.imeeting.entity.SysTenantUser::getUserId, userId)
|
|
||||||
.eq(com.imeeting.entity.SysTenantUser::getTenantId, role.getTenantId())
|
|
||||||
) > 0;
|
|
||||||
if (!hasMembership) {
|
|
||||||
return ApiResponse.error("用户不属于角色所在租户:" + role.getTenantId());
|
|
||||||
}
|
|
||||||
toInsertUserIds.add(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Long userId : toInsertUserIds) {
|
|
||||||
SysUserRole ur = new SysUserRole();
|
|
||||||
ur.setTenantId(role.getTenantId());
|
|
||||||
ur.setRoleId(id);
|
|
||||||
ur.setUserId(userId);
|
|
||||||
sysUserRoleMapper.insert(ur);
|
|
||||||
authVersionService.invalidateUserTenantAuth(userId, role.getTenantId());
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}/users/{userId}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:role:update')")
|
|
||||||
@Log(value = "角色取消关联用户", type = "角色管理")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public ApiResponse<Boolean> unbindUser(@PathVariable Long id, @PathVariable Long userId) {
|
|
||||||
SysRole role = sysRoleService.getById(id);
|
|
||||||
if (role == null || role.getRoleId() == null || role.getTenantId() == null) {
|
|
||||||
return ApiResponse.error("角色不存在");
|
|
||||||
}
|
|
||||||
if (!canAccessTenant(role.getTenantId())) {
|
|
||||||
return ApiResponse.error("禁止跨租户解绑用户");
|
|
||||||
}
|
|
||||||
sysUserRoleMapper.physicalDelete(id, userId, role.getTenantId());
|
|
||||||
authVersionService.invalidateUserTenantAuth(userId, role.getTenantId());
|
|
||||||
return ApiResponse.ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getCurrentUserId() {
|
|
||||||
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
|
|
||||||
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getUserId();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getCurrentTenantId() {
|
|
||||||
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
|
|
||||||
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getTenantId();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canAccessTenant(Long targetTenantId) {
|
|
||||||
if (targetTenantId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (authScopeService.isCurrentPlatformAdmin()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
return currentTenantId != null && currentTenantId.equals(targetTenantId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UserBindingPayload {
|
|
||||||
private List<Long> userIds;
|
|
||||||
public List<Long> getUserIds() { return userIds; }
|
|
||||||
public void setUserIds(List<Long> userIds) { this.userIds = userIds; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class PermissionBindingPayload {
|
|
||||||
private List<Long> permIds;
|
|
||||||
|
|
||||||
public List<Long> getPermIds() {
|
|
||||||
return permIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPermIds(List<Long> permIds) {
|
|
||||||
this.permIds = permIds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.entity.SysLog;
|
|
||||||
import com.imeeting.security.LoginUser;
|
|
||||||
import com.imeeting.service.SysLogService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/logs")
|
|
||||||
public class SysLogController {
|
|
||||||
private final SysLogService sysLogService;
|
|
||||||
|
|
||||||
public SysLogController(SysLogService sysLogService) {
|
|
||||||
this.sysLogService = sysLogService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_log:list')")
|
|
||||||
public ApiResponse<IPage<SysLog>> list(
|
|
||||||
@RequestParam(defaultValue = "1") Integer current,
|
|
||||||
@RequestParam(defaultValue = "10") Integer size,
|
|
||||||
@RequestParam(required = false) String username,
|
|
||||||
@RequestParam(required = false) String logType,
|
|
||||||
@RequestParam(required = false) String operation,
|
|
||||||
@RequestParam(required = false) Integer status,
|
|
||||||
@RequestParam(required = false) String startDate,
|
|
||||||
@RequestParam(required = false) String endDate,
|
|
||||||
@RequestParam(required = false) String sortField,
|
|
||||||
@RequestParam(required = false) String sortOrder
|
|
||||||
) {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
LoginUser loginUser = (LoginUser) auth.getPrincipal();
|
|
||||||
|
|
||||||
// 判定平台管理员: isPlatformAdmin=true 且 tenantId=0
|
|
||||||
boolean isPlatformAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0).equals(loginUser.getTenantId());
|
|
||||||
|
|
||||||
QueryWrapper<SysLog> query = new QueryWrapper<>();
|
|
||||||
// 只有联表查询才需要前缀 'l.'
|
|
||||||
String prefix = isPlatformAdmin ? "l." : "";
|
|
||||||
|
|
||||||
if (logType != null && !logType.isEmpty()) {
|
|
||||||
query.eq(prefix + "log_type", logType);
|
|
||||||
}
|
|
||||||
if (username != null && !username.isEmpty()) {
|
|
||||||
query.like(prefix + "username", username);
|
|
||||||
}
|
|
||||||
if (operation != null && !operation.isEmpty()) {
|
|
||||||
query.like(prefix + "operation", operation);
|
|
||||||
}
|
|
||||||
if (status != null) {
|
|
||||||
query.eq(prefix + "status", status);
|
|
||||||
}
|
|
||||||
if (startDate != null && !startDate.isEmpty()) {
|
|
||||||
query.ge(prefix + "created_at", startDate + " 00:00:00");
|
|
||||||
}
|
|
||||||
if (endDate != null && !endDate.isEmpty()) {
|
|
||||||
query.le(prefix + "created_at", endDate + " 23:59:59");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态排序逻辑
|
|
||||||
if (sortField != null && !sortField.isEmpty()) {
|
|
||||||
String column = "created_at";
|
|
||||||
if ("duration".equals(sortField)) column = "duration";
|
|
||||||
|
|
||||||
if ("ascend".equals(sortOrder)) {
|
|
||||||
query.orderByAsc(prefix + column);
|
|
||||||
} else {
|
|
||||||
query.orderByDesc(prefix + column);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query.orderByDesc(prefix + "created_at");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlatformAdmin) {
|
|
||||||
return ApiResponse.ok(sysLogService.selectPageWithTenant(new Page<>(current, size), query));
|
|
||||||
} else {
|
|
||||||
return ApiResponse.ok(sysLogService.page(new Page<>(current, size), query));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.common.annotation.Log;
|
|
||||||
import com.imeeting.entity.SysOrg;
|
|
||||||
import com.imeeting.service.SysOrgService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/orgs")
|
|
||||||
public class SysOrgController {
|
|
||||||
private final SysOrgService sysOrgService;
|
|
||||||
|
|
||||||
public SysOrgController(SysOrgService sysOrgService) {
|
|
||||||
this.sysOrgService = sysOrgService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:org:list')")
|
|
||||||
public ApiResponse<List<SysOrg>> list(@RequestParam(required = false) Long tenantId) {
|
|
||||||
return ApiResponse.ok(sysOrgService.listTree(tenantId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:org:query')")
|
|
||||||
public ApiResponse<SysOrg> get(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(sysOrgService.getById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:org:create')")
|
|
||||||
@Log(value = "新增组织", type = "组织管理")
|
|
||||||
public ApiResponse<Boolean> create(@RequestBody SysOrg org) {
|
|
||||||
return ApiResponse.ok(sysOrgService.save(org));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:org:update')")
|
|
||||||
@Log(value = "修改组织", type = "组织管理")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysOrg org) {
|
|
||||||
org.setId(id);
|
|
||||||
return ApiResponse.ok(sysOrgService.updateById(org));
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:org:delete')")
|
|
||||||
@Log(value = "删除组织", type = "组织管理")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
// Check if has children
|
|
||||||
long count = sysOrgService.count(new LambdaQueryWrapper<SysOrg>().eq(SysOrg::getParentId, id));
|
|
||||||
if (count > 0) {
|
|
||||||
return ApiResponse.error("存在下级组织,无法删除");
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(sysOrgService.removeById(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.common.PageResult;
|
|
||||||
import com.imeeting.dto.SysParamQueryDTO;
|
|
||||||
import com.imeeting.dto.SysParamVO;
|
|
||||||
import com.imeeting.entity.SysParam;
|
|
||||||
import com.imeeting.service.SysParamService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/params")
|
|
||||||
public class SysParamController {
|
|
||||||
private final SysParamService sysParamService;
|
|
||||||
|
|
||||||
public SysParamController(SysParamService sysParamService) {
|
|
||||||
this.sysParamService = sysParamService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/page")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_param:list')")
|
|
||||||
public ApiResponse<PageResult<List<SysParamVO>>> page(SysParamQueryDTO query) {
|
|
||||||
return ApiResponse.ok(sysParamService.page(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_param:list')")
|
|
||||||
public ApiResponse<List<SysParamVO>> list() {
|
|
||||||
return ApiResponse.ok(sysParamService.list().stream().map(this::toVO).collect(Collectors.toList()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_param:query')")
|
|
||||||
public ApiResponse<SysParamVO> get(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(toVO(sysParamService.getById(id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_param:create')")
|
|
||||||
public ApiResponse<Boolean> create(@RequestBody SysParam param) {
|
|
||||||
boolean saved = sysParamService.save(param);
|
|
||||||
if (saved) {
|
|
||||||
sysParamService.syncParamToCache(param);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_param:update')")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysParam param) {
|
|
||||||
param.setParamId(id);
|
|
||||||
boolean updated = sysParamService.updateById(param);
|
|
||||||
if (updated) {
|
|
||||||
sysParamService.syncParamToCache(param);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_param:delete')")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
SysParam param = sysParamService.getById(id);
|
|
||||||
boolean removed = sysParamService.removeById(id);
|
|
||||||
if (removed && param != null) {
|
|
||||||
sysParamService.deleteParamCache(param.getParamKey());
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(removed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/value")
|
|
||||||
public ApiResponse<String> getValue(@RequestParam("key") String key,
|
|
||||||
@RequestParam(value = "defaultValue", required = false) String defaultValue) {
|
|
||||||
return ApiResponse.ok(sysParamService.getCachedParamValue(key, defaultValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
private SysParamVO toVO(SysParam entity) {
|
|
||||||
if (entity == null) return null;
|
|
||||||
SysParamVO vo = new SysParamVO();
|
|
||||||
vo.setParamId(entity.getParamId());
|
|
||||||
vo.setParamKey(entity.getParamKey());
|
|
||||||
vo.setParamValue(entity.getParamValue());
|
|
||||||
vo.setParamType(entity.getParamType());
|
|
||||||
vo.setIsSystem(entity.getIsSystem());
|
|
||||||
vo.setDescription(entity.getDescription());
|
|
||||||
vo.setStatus(entity.getStatus());
|
|
||||||
vo.setCreatedAt(entity.getCreatedAt());
|
|
||||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
|
||||||
return vo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.common.annotation.Log;
|
|
||||||
import com.imeeting.entity.SysTenant;
|
|
||||||
import com.imeeting.service.SysTenantService;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/tenants")
|
|
||||||
public class SysTenantController {
|
|
||||||
private final SysTenantService sysTenantService;
|
|
||||||
|
|
||||||
public SysTenantController(SysTenantService sysTenantService) {
|
|
||||||
this.sysTenantService = sysTenantService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_tenant:list')")
|
|
||||||
public ApiResponse<Page<SysTenant>> list(
|
|
||||||
@RequestParam(defaultValue = "1") Integer current,
|
|
||||||
@RequestParam(defaultValue = "10") Integer size,
|
|
||||||
@RequestParam(required = false) String name,
|
|
||||||
@RequestParam(required = false) String code
|
|
||||||
) {
|
|
||||||
LambdaQueryWrapper<SysTenant> query = new LambdaQueryWrapper<>();
|
|
||||||
if (name != null && !name.isEmpty()) {
|
|
||||||
query.like(SysTenant::getTenantName, name);
|
|
||||||
}
|
|
||||||
if (code != null && !code.isEmpty()) {
|
|
||||||
query.like(SysTenant::getTenantCode, code);
|
|
||||||
}
|
|
||||||
query.orderByDesc(SysTenant::getCreatedAt);
|
|
||||||
return ApiResponse.ok(sysTenantService.page(new Page<>(current, size), query));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_tenant:query')")
|
|
||||||
public ApiResponse<SysTenant> get(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(sysTenantService.getById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_tenant:create')")
|
|
||||||
@Log(value = "新增租户", type = "租户管理")
|
|
||||||
public ApiResponse<Long> create(@RequestBody com.imeeting.dto.CreateTenantDTO tenantDto) {
|
|
||||||
return ApiResponse.ok(sysTenantService.createTenantWithAdmin(tenantDto));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_tenant:update')")
|
|
||||||
@Log(value = "修改租户", type = "租户管理")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysTenant tenant) {
|
|
||||||
tenant.setId(id);
|
|
||||||
return ApiResponse.ok(sysTenantService.updateById(tenant));
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys_tenant:delete')")
|
|
||||||
@Log(value = "删除租户", type = "租户管理")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
return ApiResponse.ok(sysTenantService.removeById(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,371 +0,0 @@
|
||||||
package com.imeeting.controller;
|
|
||||||
|
|
||||||
import com.imeeting.common.ApiResponse;
|
|
||||||
import com.imeeting.dto.PasswordUpdateDTO;
|
|
||||||
import com.imeeting.dto.UserProfile;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
||||||
import com.imeeting.security.LoginUser;
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
||||||
import com.imeeting.entity.SysUser;
|
|
||||||
import com.imeeting.entity.SysUserRole;
|
|
||||||
import com.imeeting.mapper.SysUserRoleMapper;
|
|
||||||
import com.imeeting.service.AuthScopeService;
|
|
||||||
import com.imeeting.service.AuthVersionService;
|
|
||||||
import com.imeeting.service.SysUserService;
|
|
||||||
import com.imeeting.common.annotation.Log;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/users")
|
|
||||||
public class UserController {
|
|
||||||
private final SysUserService sysUserService;
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
private final SysUserRoleMapper sysUserRoleMapper;
|
|
||||||
private final com.imeeting.service.SysTenantUserService sysTenantUserService;
|
|
||||||
private final com.imeeting.service.SysRoleService sysRoleService;
|
|
||||||
private final AuthScopeService authScopeService;
|
|
||||||
private final AuthVersionService authVersionService;
|
|
||||||
|
|
||||||
public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder,
|
|
||||||
SysUserRoleMapper sysUserRoleMapper,
|
|
||||||
com.imeeting.service.SysTenantUserService sysTenantUserService,
|
|
||||||
com.imeeting.service.SysRoleService sysRoleService,
|
|
||||||
AuthScopeService authScopeService,
|
|
||||||
AuthVersionService authVersionService) {
|
|
||||||
this.sysUserService = sysUserService;
|
|
||||||
this.passwordEncoder = passwordEncoder;
|
|
||||||
this.sysUserRoleMapper = sysUserRoleMapper;
|
|
||||||
this.sysTenantUserService = sysTenantUserService;
|
|
||||||
this.sysRoleService = sysRoleService;
|
|
||||||
this.authScopeService = authScopeService;
|
|
||||||
this.authVersionService = authVersionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:user:list')")
|
|
||||||
public ApiResponse<List<SysUser>> list(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
List<SysUser> users;
|
|
||||||
Long targetTenantId = null;
|
|
||||||
|
|
||||||
if (Long.valueOf(0).equals(currentTenantId) && tenantId == null) {
|
|
||||||
users = sysUserService.list();
|
|
||||||
} else {
|
|
||||||
targetTenantId = tenantId != null ? tenantId : currentTenantId;
|
|
||||||
if (targetTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
users = sysUserService.listUsersByTenant(targetTenantId, orgId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (users != null && !users.isEmpty()) {
|
|
||||||
for (SysUser user : users) {
|
|
||||||
// 加载租户关系
|
|
||||||
user.setMemberships(sysTenantUserService.listByUserId(user.getUserId()));
|
|
||||||
|
|
||||||
// 加载角色信息
|
|
||||||
QueryWrapper<SysUserRole> roleQuery = new QueryWrapper<SysUserRole>().eq("user_id", user.getUserId());
|
|
||||||
if (targetTenantId != null) {
|
|
||||||
roleQuery.eq("tenant_id", targetTenantId);
|
|
||||||
}
|
|
||||||
List<SysUserRole> userRoles = sysUserRoleMapper.selectList(roleQuery);
|
|
||||||
if (userRoles != null && !userRoles.isEmpty()) {
|
|
||||||
List<Long> roleIds = userRoles.stream()
|
|
||||||
.map(SysUserRole::getRoleId)
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
user.setRoles(sysRoleService.listByIds(roleIds));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(users);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/me")
|
|
||||||
public ApiResponse<UserProfile> me() {
|
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser)) {
|
|
||||||
return ApiResponse.error("Unauthorized");
|
|
||||||
}
|
|
||||||
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
|
|
||||||
Long userId = loginUser.getUserId();
|
|
||||||
|
|
||||||
SysUser user = sysUserService.getByIdIgnoreTenant(userId);
|
|
||||||
if (user == null) {
|
|
||||||
return ApiResponse.error("User not found");
|
|
||||||
}
|
|
||||||
UserProfile profile = new UserProfile();
|
|
||||||
profile.setUserId(user.getUserId());
|
|
||||||
profile.setUsername(user.getUsername());
|
|
||||||
profile.setDisplayName(user.getDisplayName());
|
|
||||||
profile.setEmail(user.getEmail());
|
|
||||||
profile.setPhone(user.getPhone());
|
|
||||||
profile.setStatus(user.getStatus());
|
|
||||||
profile.setAdmin(userId == 1L);
|
|
||||||
profile.setIsPlatformAdmin(user.getIsPlatformAdmin());
|
|
||||||
profile.setIsTenantAdmin(loginUser.getIsTenantAdmin());
|
|
||||||
profile.setPwdResetRequired(user.getPwdResetRequired());
|
|
||||||
return ApiResponse.ok(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:user:query')")
|
|
||||||
public ApiResponse<SysUser> get(@PathVariable Long id) {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
if (currentTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
|
|
||||||
return ApiResponse.error("禁止跨租户查看用户");
|
|
||||||
}
|
|
||||||
SysUser user = sysUserService.getByIdIgnoreTenant(id);
|
|
||||||
if (user != null) {
|
|
||||||
user.setMemberships(sysTenantUserService.listByUserId(id));
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getCurrentTenantId() {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
|
|
||||||
return ((LoginUser) auth.getPrincipal()).getTenantId();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:user:create')")
|
|
||||||
@Log(value = "新增用户", type = "用户管理")
|
|
||||||
public ApiResponse<Boolean> create(@RequestBody SysUser user) {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
if (currentTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
// 非平台管理员强制设置为当前租户
|
|
||||||
if (!Long.valueOf(0).equals(currentTenantId)) {
|
|
||||||
if (user.getMemberships() != null && !user.getMemberships().isEmpty()) {
|
|
||||||
user.getMemberships().forEach(m -> m.setTenantId(currentTenantId));
|
|
||||||
} else {
|
|
||||||
// 如果没传身份,补齐当前租户身份
|
|
||||||
List<com.imeeting.entity.SysTenantUser> memberships = new java.util.ArrayList<>();
|
|
||||||
com.imeeting.entity.SysTenantUser m = new com.imeeting.entity.SysTenantUser();
|
|
||||||
m.setTenantId(currentTenantId);
|
|
||||||
memberships.add(m);
|
|
||||||
user.setMemberships(memberships);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
|
|
||||||
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
|
|
||||||
}
|
|
||||||
boolean saved = sysUserService.save(user);
|
|
||||||
if (saved) {
|
|
||||||
sysTenantUserService.syncMemberships(user.getUserId(), user.getMemberships());
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:user:update')")
|
|
||||||
@Log(value = "修改用户", type = "用户管理")
|
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysUser user) {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
if (currentTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
user.setUserId(id);
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
|
|
||||||
return ApiResponse.error("禁止跨租户修改用户");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 非平台管理员强制约束租户身份
|
|
||||||
if (!Long.valueOf(0).equals(currentTenantId)) {
|
|
||||||
if (user.getMemberships() != null) {
|
|
||||||
user.getMemberships().forEach(m -> m.setTenantId(currentTenantId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
|
|
||||||
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
|
|
||||||
}
|
|
||||||
boolean updated = sysUserService.updateById(user);
|
|
||||||
if (updated) {
|
|
||||||
sysTenantUserService.syncMemberships(id, user.getMemberships());
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:user:delete')")
|
|
||||||
@Log(value = "删除用户", type = "用户管理")
|
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
if (currentTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
|
|
||||||
return ApiResponse.error("禁止跨租户删除用户");
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(sysUserService.removeById(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/profile")
|
|
||||||
public ApiResponse<Boolean> updateProfile(@RequestBody SysUser user) {
|
|
||||||
Long userId = getCurrentUserId();
|
|
||||||
SysUser existing = sysUserService.getByIdIgnoreTenant(userId);
|
|
||||||
if (existing == null) return ApiResponse.error("用户不存在");
|
|
||||||
|
|
||||||
existing.setDisplayName(user.getDisplayName());
|
|
||||||
existing.setEmail(user.getEmail());
|
|
||||||
existing.setPhone(user.getPhone());
|
|
||||||
return ApiResponse.ok(sysUserService.updateById(existing));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/password")
|
|
||||||
public ApiResponse<Boolean> updatePassword(@RequestBody PasswordUpdateDTO dto) {
|
|
||||||
Long userId = getCurrentUserId();
|
|
||||||
SysUser user = sysUserService.getByIdIgnoreTenant(userId);
|
|
||||||
if (user == null) return ApiResponse.error("用户不存在");
|
|
||||||
|
|
||||||
if (!passwordEncoder.matches(dto.getOldPassword(), user.getPasswordHash())) {
|
|
||||||
return ApiResponse.error("旧密码不正确");
|
|
||||||
}
|
|
||||||
|
|
||||||
user.setPasswordHash(passwordEncoder.encode(dto.getNewPassword()));
|
|
||||||
user.setPwdResetRequired(0); // 重置标志位
|
|
||||||
return ApiResponse.ok(sysUserService.updateById(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Long getCurrentUserId() {
|
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (auth != null && auth.getPrincipal() instanceof LoginUser) {
|
|
||||||
return ((LoginUser) auth.getPrincipal()).getUserId();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/{id}/roles")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:user:role:list')")
|
|
||||||
public ApiResponse<List<Long>> listUserRoles(@PathVariable Long id) {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
if (currentTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
|
|
||||||
return ApiResponse.error("禁止跨租户查看用户角色");
|
|
||||||
}
|
|
||||||
QueryWrapper<SysUserRole> query = new QueryWrapper<SysUserRole>().eq("user_id", id);
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin()) {
|
|
||||||
query.eq("tenant_id", currentTenantId);
|
|
||||||
}
|
|
||||||
List<SysUserRole> rows = sysUserRoleMapper.selectList(query);
|
|
||||||
List<Long> roleIds = new ArrayList<>();
|
|
||||||
for (SysUserRole row : rows) {
|
|
||||||
if (row.getRoleId() != null) {
|
|
||||||
roleIds.add(row.getRoleId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(roleIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/{id}/roles")
|
|
||||||
@PreAuthorize("@ss.hasPermi('sys:user:role:save')")
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public ApiResponse<Boolean> saveUserRoles(@PathVariable Long id, @RequestBody RoleBindingPayload payload) {
|
|
||||||
Long currentTenantId = getCurrentTenantId();
|
|
||||||
if (currentTenantId == null) {
|
|
||||||
return ApiResponse.error("Tenant ID required");
|
|
||||||
}
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin() && !isUserInTenant(id, currentTenantId)) {
|
|
||||||
return ApiResponse.error("禁止跨租户分配角色");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Long> roleIds = payload == null ? null : payload.getRoleIds();
|
|
||||||
|
|
||||||
List<com.imeeting.entity.SysRole> rolesToBind = new ArrayList<>();
|
|
||||||
if (roleIds != null) {
|
|
||||||
for (Long roleId : roleIds) {
|
|
||||||
if (roleId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
com.imeeting.entity.SysRole role = sysRoleService.getById(roleId);
|
|
||||||
if (role == null || role.getRoleId() == null || role.getTenantId() == null) {
|
|
||||||
return ApiResponse.error("角色不存在:" + roleId);
|
|
||||||
}
|
|
||||||
Long roleTenantId = role.getTenantId();
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin() && !currentTenantId.equals(roleTenantId)) {
|
|
||||||
return ApiResponse.error("禁止跨租户分配角色:" + roleId);
|
|
||||||
}
|
|
||||||
boolean hasMembership = sysTenantUserService.count(
|
|
||||||
new LambdaQueryWrapper<com.imeeting.entity.SysTenantUser>()
|
|
||||||
.eq(com.imeeting.entity.SysTenantUser::getUserId, id)
|
|
||||||
.eq(com.imeeting.entity.SysTenantUser::getTenantId, roleTenantId)
|
|
||||||
) > 0;
|
|
||||||
if (!hasMembership) {
|
|
||||||
return ApiResponse.error("用户不属于角色所在租户:" + roleTenantId);
|
|
||||||
}
|
|
||||||
rolesToBind.add(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryWrapper<SysUserRole> scopeQuery = new QueryWrapper<SysUserRole>().eq("user_id", id);
|
|
||||||
if (!authScopeService.isCurrentPlatformAdmin()) {
|
|
||||||
scopeQuery.eq("tenant_id", currentTenantId);
|
|
||||||
}
|
|
||||||
List<SysUserRole> existingRows = sysUserRoleMapper.selectList(scopeQuery);
|
|
||||||
java.util.Set<Long> affectedTenantIds = new java.util.HashSet<>();
|
|
||||||
for (SysUserRole row : existingRows) {
|
|
||||||
if (row.getTenantId() != null) {
|
|
||||||
affectedTenantIds.add(row.getTenantId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (com.imeeting.entity.SysRole role : rolesToBind) {
|
|
||||||
if (role.getTenantId() != null) {
|
|
||||||
affectedTenantIds.add(role.getTenantId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sysUserRoleMapper.delete(scopeQuery);
|
|
||||||
for (com.imeeting.entity.SysRole role : rolesToBind) {
|
|
||||||
SysUserRole item = new SysUserRole();
|
|
||||||
item.setTenantId(role.getTenantId());
|
|
||||||
item.setUserId(id);
|
|
||||||
item.setRoleId(role.getRoleId());
|
|
||||||
sysUserRoleMapper.insert(item);
|
|
||||||
}
|
|
||||||
for (Long tenantId : affectedTenantIds) {
|
|
||||||
authVersionService.invalidateUserTenantAuth(id, tenantId);
|
|
||||||
}
|
|
||||||
return ApiResponse.ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isUserInTenant(Long userId, Long tenantId) {
|
|
||||||
if (userId == null || tenantId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return sysTenantUserService.count(
|
|
||||||
new LambdaQueryWrapper<com.imeeting.entity.SysTenantUser>()
|
|
||||||
.eq(com.imeeting.entity.SysTenantUser::getUserId, userId)
|
|
||||||
.eq(com.imeeting.entity.SysTenantUser::getTenantId, tenantId)
|
|
||||||
) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class RoleBindingPayload {
|
|
||||||
private List<Long> roleIds;
|
|
||||||
|
|
||||||
public List<Long> getRoleIds() {
|
|
||||||
return roleIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRoleIds(List<Long> roleIds) {
|
|
||||||
this.roleIds = roleIds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.AiModelDTO;
|
||||||
|
import com.imeeting.dto.biz.AiModelVO;
|
||||||
|
|
||||||
|
import com.imeeting.service.biz.AiModelService;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import com.unisbase.dto.PageResult;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/biz/aimodel")
|
||||||
|
public class AiModelController {
|
||||||
|
|
||||||
|
private final AiModelService aiModelService;
|
||||||
|
|
||||||
|
public AiModelController(AiModelService aiModelService) {
|
||||||
|
this.aiModelService = aiModelService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<AiModelVO> save(@RequestBody AiModelDTO dto) {
|
||||||
|
return ApiResponse.ok(aiModelService.saveModel(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<AiModelVO> update(@RequestBody AiModelDTO dto) {
|
||||||
|
if (dto.getId() == null) {
|
||||||
|
return ApiResponse.error("模型ID不能为空");
|
||||||
|
}
|
||||||
|
if (dto.getModelType() == null || dto.getModelType().isBlank()) {
|
||||||
|
return ApiResponse.error("模型类型不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
AiModelVO existing = aiModelService.getModelById(dto.getId(), dto.getModelType());
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("模型不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Long.valueOf(0L).equals(existing.getTenantId()) && !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||||
|
return ApiResponse.error("无权修改系统级模型");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(aiModelService.updateModel(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> delete(@PathVariable Long id, @RequestParam String type) {
|
||||||
|
if (type == null || type.isBlank()) {
|
||||||
|
return ApiResponse.error("模型类型不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
AiModelVO existing = aiModelService.getModelById(id, type);
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Long.valueOf(0L).equals(existing.getTenantId()) && !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||||
|
return ApiResponse.error("无权删除系统级模型");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(aiModelService.removeModelById(id, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<PageResult<List<AiModelVO>>> page(
|
||||||
|
@RequestParam(defaultValue = "1") Integer current,
|
||||||
|
@RequestParam(defaultValue = "10") Integer size,
|
||||||
|
@RequestParam(required = false) String name,
|
||||||
|
@RequestParam(required = false) String type) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
return ApiResponse.ok(aiModelService.pageModels(current, size, name, type, loginUser.getTenantId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/remote-list")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<List<String>> remoteList(
|
||||||
|
@RequestParam String provider,
|
||||||
|
@RequestParam String baseUrl,
|
||||||
|
@RequestParam(required = false) String apiKey) {
|
||||||
|
return ApiResponse.ok(aiModelService.fetchRemoteModels(provider, baseUrl, apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/default")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<AiModelVO> getDefault(@RequestParam String type) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
return ApiResponse.ok(aiModelService.getDefaultModel(type, loginUser.getTenantId()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
|
||||||
|
import com.imeeting.service.biz.MeetingService;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/biz/dashboard")
|
||||||
|
public class DashboardController {
|
||||||
|
|
||||||
|
private final MeetingService meetingService;
|
||||||
|
|
||||||
|
public DashboardController(MeetingService meetingService) {
|
||||||
|
this.meetingService = meetingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stats")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Map<String, Object>> getStats() {
|
||||||
|
LoginUser user = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
boolean isAdmin = Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin());
|
||||||
|
return ApiResponse.ok(meetingService.getDashboardStats(user.getTenantId(), user.getUserId(), isAdmin));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/recent")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<List<MeetingVO>> getRecent() {
|
||||||
|
LoginUser user = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
boolean isAdmin = Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin());
|
||||||
|
return ApiResponse.ok(meetingService.getRecentMeetings(user.getTenantId(), user.getUserId(), isAdmin, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.HotWordDTO;
|
||||||
|
import com.imeeting.dto.biz.HotWordVO;
|
||||||
|
import com.imeeting.entity.biz.HotWord;
|
||||||
|
|
||||||
|
import com.imeeting.service.biz.HotWordService;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import com.unisbase.dto.PageResult;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/biz/hotword")
|
||||||
|
public class HotWordController {
|
||||||
|
|
||||||
|
private final HotWordService hotWordService;
|
||||||
|
|
||||||
|
public HotWordController(HotWordService hotWordService) {
|
||||||
|
this.hotWordService = hotWordService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前用户是否具备管理员权限 (平台管理员或租户管理员)
|
||||||
|
*/
|
||||||
|
private boolean isCurrentUserAdmin(LoginUser user) {
|
||||||
|
return Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<HotWordVO> save(@RequestBody HotWordDTO hotWordDTO) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
|
||||||
|
// 核心校验:只有管理员可以创建公开热词
|
||||||
|
if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !isCurrentUserAdmin(loginUser)) {
|
||||||
|
return ApiResponse.error("无权创建租户公开热词,请设为个人私有");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<HotWordVO> update(@RequestBody HotWordDTO hotWordDTO) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
HotWord existing = hotWordService.getById(hotWordDTO.getId());
|
||||||
|
if (existing == null) return ApiResponse.error("热词不存在");
|
||||||
|
|
||||||
|
boolean isAdmin = isCurrentUserAdmin(loginUser);
|
||||||
|
|
||||||
|
// 核心校验逻辑:
|
||||||
|
// 1. 如果用户尝试将热词设为公开,必须具备管理员权限
|
||||||
|
if (Integer.valueOf(1).equals(hotWordDTO.getIsPublic()) && !isAdmin) {
|
||||||
|
return ApiResponse.error("无权将热词设为公开");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 如果是公开热词,只有管理员能改
|
||||||
|
if (Integer.valueOf(1).equals(existing.getIsPublic())) {
|
||||||
|
if (!isAdmin) return ApiResponse.error("无权修改公开热词");
|
||||||
|
} else {
|
||||||
|
// 3. 如果是私有热词,本人或管理员能改
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId()) && !isAdmin) {
|
||||||
|
return ApiResponse.error("无权修改他人私有热词");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(hotWordService.updateHotWord(hotWordDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
HotWord existing = hotWordService.getById(id);
|
||||||
|
if (existing == null) return ApiResponse.ok(true);
|
||||||
|
|
||||||
|
boolean isAdmin = isCurrentUserAdmin(loginUser);
|
||||||
|
|
||||||
|
// 权限校验:公开热词管理员可删,私有热词本人或管理员可删
|
||||||
|
if (Integer.valueOf(1).equals(existing.getIsPublic())) {
|
||||||
|
if (!isAdmin) return ApiResponse.error("无权删除公开热词");
|
||||||
|
} else {
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId()) && !isAdmin) {
|
||||||
|
return ApiResponse.error("无权删除他人私有热词");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(hotWordService.removeById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<PageResult<List<HotWordVO>>> page(
|
||||||
|
@RequestParam(defaultValue = "1") Integer current,
|
||||||
|
@RequestParam(defaultValue = "10") Integer size,
|
||||||
|
@RequestParam(required = false) String word,
|
||||||
|
@RequestParam(required = false) String category,
|
||||||
|
@RequestParam(required = false) Integer isPublic) {
|
||||||
|
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
boolean isAdmin = isCurrentUserAdmin(loginUser);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<HotWord> wrapper = new LambdaQueryWrapper<HotWord>()
|
||||||
|
.eq(HotWord::getTenantId, loginUser.getTenantId());
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
// 普通用户:只能看到“已公开”的,或者“自己创建”的
|
||||||
|
wrapper.and(w -> w.eq(HotWord::getIsPublic, 1).or().eq(HotWord::getCreatorId, loginUser.getUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加类型过滤
|
||||||
|
if (isPublic != null) {
|
||||||
|
wrapper.eq(HotWord::getIsPublic, isPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.like(word != null && !word.isEmpty(), HotWord::getWord, word)
|
||||||
|
.eq(category != null && !category.isEmpty(), HotWord::getCategory, category)
|
||||||
|
.orderByDesc(HotWord::getIsPublic)
|
||||||
|
.orderByDesc(HotWord::getCreatedAt);
|
||||||
|
|
||||||
|
Page<HotWord> page = hotWordService.page(new Page<>(current, size), wrapper);
|
||||||
|
List<HotWordVO> vos = page.getRecords().stream().map(this::toVO).collect(Collectors.toList());
|
||||||
|
|
||||||
|
PageResult<List<HotWordVO>> result = new PageResult<>();
|
||||||
|
result.setTotal(page.getTotal());
|
||||||
|
result.setRecords(vos);
|
||||||
|
return ApiResponse.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pinyin")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<List<String>> getPinyin(@RequestParam String word) {
|
||||||
|
return ApiResponse.ok(hotWordService.generatePinyin(word));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HotWordVO toVO(HotWord entity) {
|
||||||
|
HotWordVO vo = new HotWordVO();
|
||||||
|
vo.setId(entity.getId());
|
||||||
|
vo.setWord(entity.getWord());
|
||||||
|
vo.setPinyinList(entity.getPinyinList());
|
||||||
|
vo.setMatchStrategy(entity.getMatchStrategy());
|
||||||
|
vo.setCategory(entity.getCategory());
|
||||||
|
vo.setWeight(entity.getWeight());
|
||||||
|
vo.setStatus(entity.getStatus());
|
||||||
|
vo.setIsPublic(entity.getIsPublic());
|
||||||
|
vo.setCreatorId(entity.getCreatorId());
|
||||||
|
vo.setIsSynced(entity.getIsSynced());
|
||||||
|
vo.setRemark(entity.getRemark());
|
||||||
|
vo.setCreatedAt(entity.getCreatedAt());
|
||||||
|
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,865 @@
|
||||||
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.dto.biz.MeetingDTO;
|
||||||
|
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
||||||
|
import com.imeeting.dto.biz.MeetingVO;
|
||||||
|
import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO;
|
||||||
|
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
|
||||||
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
|
import com.imeeting.service.biz.MeetingService;
|
||||||
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import com.unisbase.dto.PageResult;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import org.apache.fontbox.ttf.TrueTypeCollection;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/biz/meeting")
|
||||||
|
public class MeetingController {
|
||||||
|
|
||||||
|
private final MeetingService meetingService;
|
||||||
|
private final AiTaskService aiTaskService;
|
||||||
|
private final PromptTemplateService promptTemplateService;
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final String uploadPath;
|
||||||
|
private final String resourcePrefix;
|
||||||
|
|
||||||
|
public MeetingController(MeetingService meetingService,
|
||||||
|
AiTaskService aiTaskService,
|
||||||
|
PromptTemplateService promptTemplateService,
|
||||||
|
StringRedisTemplate redisTemplate,
|
||||||
|
@Value("${unisbase.app.upload-path}") String uploadPath,
|
||||||
|
@Value("${unisbase.app.resource-prefix}") String resourcePrefix) {
|
||||||
|
this.meetingService = meetingService;
|
||||||
|
this.aiTaskService = aiTaskService;
|
||||||
|
this.promptTemplateService = promptTemplateService;
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
this.uploadPath = uploadPath;
|
||||||
|
this.resourcePrefix = resourcePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/progress")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Map<String, Object>> getProgress(@PathVariable Long id) {
|
||||||
|
String key = RedisKeys.meetingProgressKey(id);
|
||||||
|
String json = redisTemplate.opsForValue().get(key);
|
||||||
|
if (json != null) {
|
||||||
|
try {
|
||||||
|
return ApiResponse.ok(new com.fasterxml.jackson.databind.ObjectMapper().readValue(json, Map.class));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ApiResponse.error("解析进度异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Meeting m = meetingService.getById(id);
|
||||||
|
Map<String, Object> fallback = new HashMap<>();
|
||||||
|
if (m != null) {
|
||||||
|
if (m.getStatus() == 3) {
|
||||||
|
fallback.put("percent", 100);
|
||||||
|
fallback.put("message", "分析已完成");
|
||||||
|
} else if (m.getStatus() == 4) {
|
||||||
|
fallback.put("percent", -1);
|
||||||
|
fallback.put("message", "分析失败");
|
||||||
|
} else {
|
||||||
|
fallback.put("percent", 0);
|
||||||
|
fallback.put("message", "等待处理...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
|
||||||
|
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||||
|
String uploadDir = basePath + "audio/";
|
||||||
|
File dir = new File(uploadDir);
|
||||||
|
if (!dir.exists()) dir.mkdirs();
|
||||||
|
|
||||||
|
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
|
||||||
|
file.transferTo(new File(uploadDir + fileName));
|
||||||
|
String baseResourcePrefix = resourcePrefix.endsWith("/") ? resourcePrefix : resourcePrefix + "/";
|
||||||
|
return ApiResponse.ok(baseResourcePrefix+"audio/" + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<MeetingVO> create(@RequestBody MeetingDTO dto) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
if (dto.getPromptId() != null) {
|
||||||
|
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||||
|
dto.getPromptId(),
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()
|
||||||
|
);
|
||||||
|
if (!enabled) {
|
||||||
|
return ApiResponse.error("总结模板不可用或已被你禁用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dto.setTenantId(loginUser.getTenantId());
|
||||||
|
dto.setCreatorId(loginUser.getUserId());
|
||||||
|
dto.setCreatorName(loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername());
|
||||||
|
return ApiResponse.ok(meetingService.createMeeting(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/realtime/start")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<MeetingVO> createRealtime(@RequestBody MeetingDTO dto) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
if (dto.getPromptId() != null) {
|
||||||
|
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||||
|
dto.getPromptId(),
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()
|
||||||
|
);
|
||||||
|
if (!enabled) {
|
||||||
|
return ApiResponse.error("总结模板不可用或已被你禁用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dto.setTenantId(loginUser.getTenantId());
|
||||||
|
dto.setCreatorId(loginUser.getUserId());
|
||||||
|
dto.setCreatorName(loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername());
|
||||||
|
return ApiResponse.ok(meetingService.createRealtimeMeeting(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<PageResult<List<MeetingVO>>> page(
|
||||||
|
@RequestParam(defaultValue = "1") Integer current,
|
||||||
|
@RequestParam(defaultValue = "10") Integer size,
|
||||||
|
@RequestParam(required = false) String title,
|
||||||
|
@RequestParam(defaultValue = "all") String viewType) {
|
||||||
|
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||||
|
|
||||||
|
return ApiResponse.ok(meetingService.pageMeetings(current, size, title,
|
||||||
|
loginUser.getTenantId(), loginUser.getUserId(),
|
||||||
|
loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername(),
|
||||||
|
viewType, isAdmin));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/detail/{id}")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<MeetingVO> getDetail(@PathVariable Long id) {
|
||||||
|
return ApiResponse.ok(meetingService.getDetail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/summary/export")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ResponseEntity<byte[]> exportSummary(@PathVariable Long id, @RequestParam(defaultValue = "pdf") String format) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Meeting meetingEntity = meetingService.getById(id);
|
||||||
|
if (meetingEntity == null) {
|
||||||
|
throw new RuntimeException("数据未找到,请刷新后重试");
|
||||||
|
}
|
||||||
|
if (!canAccessMeeting(meetingEntity, loginUser)) {
|
||||||
|
throw new RuntimeException("无权下载此会议总结");
|
||||||
|
}
|
||||||
|
|
||||||
|
MeetingVO meeting = meetingService.getDetail(id);
|
||||||
|
if (meeting == null) {
|
||||||
|
throw new RuntimeException("数据未找到,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
AiTask latestSummaryTask = findLatestSummaryTask(meetingEntity);
|
||||||
|
if (latestSummaryTask == null || latestSummaryTask.getResultFilePath() == null || latestSummaryTask.getResultFilePath().isBlank()) {
|
||||||
|
throw new RuntimeException(" AI总结为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||||
|
Path summarySourcePath = Paths.get(basePath, latestSummaryTask.getResultFilePath().replace("\\", "/"));
|
||||||
|
if (!Files.exists(summarySourcePath)) {
|
||||||
|
throw new RuntimeException("总结源文件不存在,请重新总结后再试");
|
||||||
|
}
|
||||||
|
|
||||||
|
String safeTitle = (meeting.getTitle() == null || meeting.getTitle().trim().isEmpty())
|
||||||
|
? "meeting-summary-" + id
|
||||||
|
: meeting.getTitle().replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_");
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] bytes;
|
||||||
|
String ext;
|
||||||
|
String contentType;
|
||||||
|
Path exportDir = Paths.get(basePath, "meetings", String.valueOf(id), "exports");
|
||||||
|
Files.createDirectories(exportDir);
|
||||||
|
|
||||||
|
if ("word".equalsIgnoreCase(format) || "docx".equalsIgnoreCase(format)) {
|
||||||
|
ext = "docx";
|
||||||
|
contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||||
|
} else if ("pdf".equalsIgnoreCase(format)) {
|
||||||
|
ext = "pdf";
|
||||||
|
contentType = MediaType.APPLICATION_PDF_VALUE;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("格式化失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path exportPath = exportDir.resolve(latestSummaryTask.getId() + "." + ext);
|
||||||
|
boolean needRegenerate = !Files.exists(exportPath) ||
|
||||||
|
Files.getLastModifiedTime(exportPath).toMillis() < Files.getLastModifiedTime(summarySourcePath).toMillis();
|
||||||
|
|
||||||
|
if (needRegenerate) {
|
||||||
|
String markdown = Files.readString(summarySourcePath, StandardCharsets.UTF_8);
|
||||||
|
meeting.setSummaryContent(stripFrontMatter(markdown));
|
||||||
|
bytes = "docx".equals(ext) ? buildWordBytes(meeting) : buildPdfBytes(meeting);
|
||||||
|
Files.write(exportPath, bytes);
|
||||||
|
} else {
|
||||||
|
bytes = Files.readAllBytes(exportPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = safeTitle + "-AI-总结." + ext;
|
||||||
|
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replace("+", "%20");
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
|
||||||
|
.contentType(MediaType.parseMediaType(contentType))
|
||||||
|
.body(bytes);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("导出失败 " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiTask findLatestSummaryTask(Meeting meeting) {
|
||||||
|
if (meeting.getLatestSummaryTaskId() != null) {
|
||||||
|
AiTask task = aiTaskService.getById(meeting.getLatestSummaryTaskId());
|
||||||
|
if (task != null && "SUMMARY".equals(task.getTaskType()) && Integer.valueOf(2).equals(task.getStatus())
|
||||||
|
&& task.getResultFilePath() != null && !task.getResultFilePath().isBlank()) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getMeetingId, meeting.getId())
|
||||||
|
.eq(AiTask::getTaskType, "SUMMARY")
|
||||||
|
.eq(AiTask::getStatus, 2)
|
||||||
|
.isNotNull(AiTask::getResultFilePath)
|
||||||
|
.orderByDesc(AiTask::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canAccessMeeting(Meeting meeting, LoginUser user) {
|
||||||
|
if (Boolean.TRUE.equals(user.getIsPlatformAdmin()) || Boolean.TRUE.equals(user.getIsTenantAdmin())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (meeting.getCreatorId() != null && meeting.getCreatorId().equals(user.getUserId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (meeting.getParticipants() == null || meeting.getParticipants().isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String target = "," + user.getUserId() + ",";
|
||||||
|
return ("," + meeting.getParticipants() + ",").contains(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripFrontMatter(String markdown) {
|
||||||
|
if (markdown == null || markdown.isBlank()) {
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
if (!markdown.startsWith("---")) {
|
||||||
|
return unwrapMarkdownFence(markdown);
|
||||||
|
}
|
||||||
|
int second = markdown.indexOf("\n---", 3);
|
||||||
|
if (second < 0) {
|
||||||
|
return unwrapMarkdownFence(markdown);
|
||||||
|
}
|
||||||
|
int contentStart = second + 4;
|
||||||
|
if (contentStart < markdown.length() && markdown.charAt(contentStart) == '\n') {
|
||||||
|
contentStart++;
|
||||||
|
}
|
||||||
|
return unwrapMarkdownFence(markdown.substring(contentStart).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String unwrapMarkdownFence(String markdown) {
|
||||||
|
if (markdown == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = markdown.trim();
|
||||||
|
if (!normalized.startsWith("```")) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
int firstLineEnd = normalized.indexOf('\n');
|
||||||
|
if (firstLineEnd < 0) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
String firstLine = normalized.substring(0, firstLineEnd).trim().toLowerCase();
|
||||||
|
if (!"```".equals(firstLine) && !"```markdown".equals(firstLine) && !"```md".equals(firstLine)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
int lastFence = normalized.lastIndexOf("\n```");
|
||||||
|
if (lastFence <= firstLineEnd) {
|
||||||
|
return normalized.substring(firstLineEnd + 1).trim();
|
||||||
|
}
|
||||||
|
return normalized.substring(firstLineEnd + 1, lastFence).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/transcripts/{id}")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<List<MeetingTranscriptVO>> getTranscripts(@PathVariable Long id) {
|
||||||
|
return ApiResponse.ok(meetingService.getTranscripts(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/realtime/transcripts")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> appendRealtimeTranscripts(@PathVariable Long id, @RequestBody List<RealtimeTranscriptItemDTO> items) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Meeting existing = meetingService.getById(id);
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("会议不存在");
|
||||||
|
}
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
return ApiResponse.error("无权写入此会议的实时转录");
|
||||||
|
}
|
||||||
|
meetingService.appendRealtimeTranscripts(id, items);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/realtime/complete")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id, @RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Meeting existing = meetingService.getById(id);
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("会议不存在");
|
||||||
|
}
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
return ApiResponse.error("无权结束此实时会议");
|
||||||
|
}
|
||||||
|
meetingService.completeRealtimeMeeting(id, dto != null ? dto.getAudioUrl() : null);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/speaker")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> updateSpeaker(@RequestBody Map<String, Object> params) {
|
||||||
|
Long meetingId = Long.valueOf(params.get("meetingId").toString());
|
||||||
|
String speakerId = params.get("speakerId").toString();
|
||||||
|
String newName = params.get("newName") != null ? params.get("newName").toString() : null;
|
||||||
|
String label = params.get("label") != null ? params.get("label").toString() : null;
|
||||||
|
|
||||||
|
meetingService.updateSpeakerInfo(meetingId, speakerId, newName, label);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/participants")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> updateParticipants(@RequestBody Map<String, Object> params) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Long meetingId = Long.valueOf(params.get("meetingId").toString());
|
||||||
|
String participants = params.get("participants") != null ? params.get("participants").toString() : "";
|
||||||
|
|
||||||
|
Meeting existing = meetingService.getById(meetingId);
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("会议不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
return ApiResponse.error("无权修改此会议参会人");
|
||||||
|
}
|
||||||
|
|
||||||
|
meetingService.updateMeetingParticipants(meetingId, participants);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/re-summary")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> reSummary(@RequestBody Map<String, Object> params) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Long meetingId = Long.valueOf(params.get("meetingId").toString());
|
||||||
|
Long summaryModelId = Long.valueOf(params.get("summaryModelId").toString());
|
||||||
|
Long promptId = Long.valueOf(params.get("promptId").toString());
|
||||||
|
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||||
|
promptId,
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()
|
||||||
|
);
|
||||||
|
if (!enabled) {
|
||||||
|
return ApiResponse.error("总结模板不可用或已被你禁用");
|
||||||
|
}
|
||||||
|
|
||||||
|
meetingService.reSummary(meetingId, summaryModelId, promptId);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> update(@RequestBody Meeting meeting) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Meeting existing = meetingService.getById(meeting.getId());
|
||||||
|
if (existing == null) return ApiResponse.error("会议不存在");
|
||||||
|
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
return ApiResponse.error("无权修改此会议信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meeting.getSummaryContent() != null) {
|
||||||
|
meetingService.updateSummaryContent(meeting.getId(), meeting.getSummaryContent());
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(meetingService.updateById(meeting));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
Meeting existing = meetingService.getById(id);
|
||||||
|
if (existing == null) return ApiResponse.ok(true);
|
||||||
|
|
||||||
|
if (!existing.getCreatorId().equals(loginUser.getUserId())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())
|
||||||
|
&& !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
return ApiResponse.error("无权删除此会议");
|
||||||
|
}
|
||||||
|
|
||||||
|
meetingService.deleteMeeting(id);
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildWordBytes(MeetingVO meeting) throws IOException {
|
||||||
|
try (XWPFDocument document = new XWPFDocument();
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||||
|
XWPFParagraph title = document.createParagraph();
|
||||||
|
XWPFRun titleRun = title.createRun();
|
||||||
|
titleRun.setBold(true);
|
||||||
|
titleRun.setFontSize(16);
|
||||||
|
titleRun.setText((meeting.getTitle() == null ? "Meeting" : meeting.getTitle()) + " - AI 总结");
|
||||||
|
|
||||||
|
XWPFParagraph timeP = document.createParagraph();
|
||||||
|
timeP.createRun().setText("Meeting Time: " + String.valueOf(meeting.getMeetingTime()));
|
||||||
|
|
||||||
|
XWPFParagraph participantsP = document.createParagraph();
|
||||||
|
participantsP.createRun().setText("Participants: " + (meeting.getParticipants() == null ? "" : meeting.getParticipants()));
|
||||||
|
|
||||||
|
document.createParagraph();
|
||||||
|
|
||||||
|
for (MdBlock block : parseMarkdownBlocks(meeting.getSummaryContent())) {
|
||||||
|
XWPFParagraph p = document.createParagraph();
|
||||||
|
if (block.type == MdType.HEADING) {
|
||||||
|
int size = Math.max(12, 18 - (block.level - 1) * 2);
|
||||||
|
appendMarkdownRuns(p, block.text, true, size);
|
||||||
|
} else if (block.type == MdType.LIST) {
|
||||||
|
p.setIndentationLeft(360);
|
||||||
|
XWPFRun bullet = p.createRun();
|
||||||
|
bullet.setFontSize(12);
|
||||||
|
bullet.setText("- ");
|
||||||
|
appendMarkdownRuns(p, block.text, false, 12);
|
||||||
|
} else {
|
||||||
|
appendMarkdownRuns(p, block.text, false, 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.write(out);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] buildPdfBytes(MeetingVO meeting) throws IOException {
|
||||||
|
Parser parser = Parser.builder().build();
|
||||||
|
String markdown = meeting.getSummaryContent() == null ? "" : meeting.getSummaryContent();
|
||||||
|
Node document = parser.parse(markdown);
|
||||||
|
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||||
|
String htmlBody = renderer.render(document);
|
||||||
|
|
||||||
|
String title = meeting.getTitle() == null ? "Meeting" : meeting.getTitle();
|
||||||
|
String time = meeting.getMeetingTime() == null ? "" : meeting.getMeetingTime().toString();
|
||||||
|
String participants = meeting.getParticipants() == null ? "未指定" : meeting.getParticipants();
|
||||||
|
|
||||||
|
String html = "<html><head><style>" +
|
||||||
|
"body { font-family: 'NotoSansSC', 'SimSun', sans-serif; padding: 20px; line-height: 1.6; color: #333; }" +
|
||||||
|
"h1, h2, h3 { color: #1890ff; border-bottom: 1px solid #eee; padding-bottom: 5px; }" +
|
||||||
|
"table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }" +
|
||||||
|
"table, th, td { border: 1px solid #ddd; }" +
|
||||||
|
"th, td { padding: 8px 12px; text-align: left; }" +
|
||||||
|
"th { background-color: #f5f5f5; font-weight: bold; }" +
|
||||||
|
"blockquote { padding: 8px 16px; color: #666; border-left: 4px solid #1890ff; background: #f0f7ff; margin: 0 0 16px 0; }" +
|
||||||
|
"</style></head><body>" +
|
||||||
|
"<div style='text-align:center; margin-bottom:30px; border-bottom: 2px solid #1890ff; padding-bottom:20px;'>" +
|
||||||
|
"<h1 style='font-size:28px; margin-bottom:12px; color:#000; border:none;'>" + title + "</h1>" +
|
||||||
|
"<div style='font-size:14px; color:#666;'>" +
|
||||||
|
"<span>会议时间:" + time + "</span>" +
|
||||||
|
"<span style='margin: 0 20px;'>|</span>" +
|
||||||
|
"<span>参会人:" + participants + "</span>" +
|
||||||
|
"</div></div>" +
|
||||||
|
"<div class='markdown-body'>" + htmlBody + "</div>" +
|
||||||
|
"<div style='margin-top: 40px; text-align: right; font-size: 12px; color: #999; border-top: 1px dashed #eee; padding-top: 10px;'>" +
|
||||||
|
"由 iMeeting 智能助手生成" +
|
||||||
|
"</div>" +
|
||||||
|
"</body></html>";
|
||||||
|
|
||||||
|
org.jsoup.nodes.Document jsoupDoc = Jsoup.parse(html);
|
||||||
|
jsoupDoc.outputSettings().syntax(org.jsoup.nodes.Document.OutputSettings.Syntax.xml);
|
||||||
|
String xhtml = jsoupDoc.html();
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||||
|
PdfRendererBuilder builder = new PdfRendererBuilder();
|
||||||
|
builder.useFastMode();
|
||||||
|
|
||||||
|
// Register fonts from classpath
|
||||||
|
try {
|
||||||
|
java.io.InputStream fontStream = getClass().getResourceAsStream("/fonts/simsunb.ttf");
|
||||||
|
if (fontStream != null) {
|
||||||
|
File tempFont = File.createTempFile("simsunb", ".ttf");
|
||||||
|
tempFont.deleteOnExit();
|
||||||
|
java.nio.file.Files.copy(fontStream, tempFont.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
builder.useFont(tempFont, "SimSun");
|
||||||
|
fontStream.close();
|
||||||
|
} else {
|
||||||
|
System.out.println("Warning: simsunb.ttf not found in classpath (/fonts/simsunb.ttf).");
|
||||||
|
}
|
||||||
|
|
||||||
|
java.io.InputStream notoStream = getClass().getResourceAsStream("/fonts/NotoSansSC-VF.ttf");
|
||||||
|
if (notoStream != null) {
|
||||||
|
File tempNoto = File.createTempFile("notosans", ".ttf");
|
||||||
|
tempNoto.deleteOnExit();
|
||||||
|
java.nio.file.Files.copy(notoStream, tempNoto.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
builder.useFont(tempNoto, "NotoSansSC");
|
||||||
|
notoStream.close();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("Error loading font from classpath: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.withHtmlContent(xhtml, null);
|
||||||
|
builder.toStream(out);
|
||||||
|
builder.run();
|
||||||
|
return out.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException("PDF generation failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendMarkdownRuns(XWPFParagraph p, String text, boolean defaultBold, int size) {
|
||||||
|
String input = text == null ? "" : text;
|
||||||
|
Matcher m = Pattern.compile("\\*\\*(.+?)\\*\\*").matcher(input);
|
||||||
|
int start = 0;
|
||||||
|
while (m.find()) {
|
||||||
|
String normal = toPlainInline(input.substring(start, m.start()));
|
||||||
|
if (!normal.isEmpty()) {
|
||||||
|
XWPFRun run = p.createRun();
|
||||||
|
run.setBold(defaultBold);
|
||||||
|
run.setFontSize(size);
|
||||||
|
run.setText(normal);
|
||||||
|
}
|
||||||
|
String boldText = toPlainInline(m.group(1));
|
||||||
|
if (!boldText.isEmpty()) {
|
||||||
|
XWPFRun run = p.createRun();
|
||||||
|
run.setBold(true);
|
||||||
|
run.setFontSize(size);
|
||||||
|
run.setText(boldText);
|
||||||
|
}
|
||||||
|
start = m.end();
|
||||||
|
}
|
||||||
|
String tail = toPlainInline(input.substring(start));
|
||||||
|
if (!tail.isEmpty()) {
|
||||||
|
XWPFRun run = p.createRun();
|
||||||
|
run.setBold(defaultBold);
|
||||||
|
run.setFontSize(size);
|
||||||
|
run.setText(tail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PdfCtx writeWrappedPdf(PDDocument document, PdfCtx ctx, String text, List<PDFont> fonts, float fontSize,
|
||||||
|
float lineHeight, float margin, float maxWidth) throws IOException {
|
||||||
|
for (String line : wrapByWidth(text, maxWidth, fonts, fontSize)) {
|
||||||
|
if (ctx.y < margin + lineHeight) {
|
||||||
|
ctx.content.close();
|
||||||
|
ctx = newPdfPage(document, margin);
|
||||||
|
}
|
||||||
|
ctx.content.beginText();
|
||||||
|
ctx.content.newLineAtOffset(margin, ctx.y);
|
||||||
|
writeLineWithFontFallback(ctx.content, line, fonts, fontSize);
|
||||||
|
ctx.content.endText();
|
||||||
|
ctx.y -= lineHeight;
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> wrapByWidth(String text, float maxWidth, List<PDFont> fonts, float fontSize) throws IOException {
|
||||||
|
String content = text == null ? "" : text;
|
||||||
|
if (content.isEmpty()) return List.of("");
|
||||||
|
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
StringBuilder current = new StringBuilder();
|
||||||
|
float currentWidth = 0f;
|
||||||
|
for (int i = 0; i < content.length(); ) {
|
||||||
|
int codePoint = content.codePointAt(i);
|
||||||
|
String ch = normalizePdfChar(codePoint);
|
||||||
|
PDFont font = pickFontForChar(ch, fonts);
|
||||||
|
if (font == null) {
|
||||||
|
ch = "?";
|
||||||
|
font = pickFontForChar(ch, fonts);
|
||||||
|
}
|
||||||
|
if (font == null) {
|
||||||
|
i += Character.charCount(codePoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float charWidth = getCharWidth(font, ch, fontSize);
|
||||||
|
|
||||||
|
if (currentWidth + charWidth <= maxWidth || current.length() == 0) {
|
||||||
|
current.append(ch);
|
||||||
|
currentWidth += charWidth;
|
||||||
|
} else {
|
||||||
|
lines.add(current.toString());
|
||||||
|
current.setLength(0);
|
||||||
|
current.append(ch);
|
||||||
|
currentWidth = charWidth;
|
||||||
|
}
|
||||||
|
i += Character.charCount(codePoint);
|
||||||
|
}
|
||||||
|
if (current.length() > 0) {
|
||||||
|
lines.add(current.toString());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PDFont> loadPdfFonts(PDDocument document) {
|
||||||
|
List<PDFont> fonts = new ArrayList<>();
|
||||||
|
String[] candidates = new String[]{
|
||||||
|
"C:/Windows/Fonts/msyh.ttf",
|
||||||
|
"C:/Windows/Fonts/simhei.ttf",
|
||||||
|
"C:/Windows/Fonts/simsun.ttc",
|
||||||
|
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttf",
|
||||||
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||||
|
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttf"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String path : candidates) {
|
||||||
|
try {
|
||||||
|
File file = new File(path);
|
||||||
|
if (!file.exists()) continue;
|
||||||
|
if (path.toLowerCase().endsWith(".ttc")) {
|
||||||
|
try (TrueTypeCollection ttc = new TrueTypeCollection(file)) {
|
||||||
|
ttc.processAllFonts(font -> {
|
||||||
|
try {
|
||||||
|
fonts.add(PDType0Font.load(document, font, true));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fonts.add(PDType0Font.load(document, file));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fonts.isEmpty()) {
|
||||||
|
fonts.add(PDType1Font.HELVETICA);
|
||||||
|
}
|
||||||
|
return fonts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MdBlock> parseMarkdownBlocks(String markdown) {
|
||||||
|
List<MdBlock> blocks = new ArrayList<>();
|
||||||
|
if (markdown == null || markdown.trim().isEmpty()) {
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] lines = markdown.replace("\r\n", "\n").split("\n");
|
||||||
|
StringBuilder paragraph = new StringBuilder();
|
||||||
|
|
||||||
|
for (String raw : lines) {
|
||||||
|
String line = raw == null ? "" : raw.trim();
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
flushParagraph(blocks, paragraph);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith("#")) {
|
||||||
|
flushParagraph(blocks, paragraph);
|
||||||
|
int level = 0;
|
||||||
|
while (level < line.length() && line.charAt(level) == '#') {
|
||||||
|
level++;
|
||||||
|
}
|
||||||
|
level = Math.min(level, 6);
|
||||||
|
String text = line.substring(level).trim();
|
||||||
|
blocks.add(new MdBlock(MdType.HEADING, level, text));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith("- ") || line.startsWith("* ")) {
|
||||||
|
flushParagraph(blocks, paragraph);
|
||||||
|
blocks.add(new MdBlock(MdType.LIST, 0, line.substring(2).trim()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Matcher ordered = Pattern.compile("^\\d+\\.\\s+(.*)$").matcher(line);
|
||||||
|
if (ordered.find()) {
|
||||||
|
flushParagraph(blocks, paragraph);
|
||||||
|
blocks.add(new MdBlock(MdType.LIST, 0, ordered.group(1).trim()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paragraph.length() > 0) paragraph.append(' ');
|
||||||
|
paragraph.append(line);
|
||||||
|
}
|
||||||
|
flushParagraph(blocks, paragraph);
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushParagraph(List<MdBlock> blocks, StringBuilder paragraph) {
|
||||||
|
if (paragraph.length() > 0) {
|
||||||
|
blocks.add(new MdBlock(MdType.PARAGRAPH, 0, paragraph.toString()));
|
||||||
|
paragraph.setLength(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toPlainInline(String input) {
|
||||||
|
if (input == null) return "";
|
||||||
|
return input
|
||||||
|
.replaceAll("`([^`]+)`", "$1")
|
||||||
|
.replaceAll("\\*\\*(.*?)\\*\\*", "$1")
|
||||||
|
.replaceAll("\\*(.*?)\\*", "$1")
|
||||||
|
.replaceAll("\\[(.*?)]\\((.*?)\\)", "$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLineWithFontFallback(PDPageContentStream content, String line, List<PDFont> fonts, float fontSize) throws IOException {
|
||||||
|
if (line == null || line.isEmpty()) return;
|
||||||
|
PDFont currentFont = null;
|
||||||
|
StringBuilder segment = new StringBuilder();
|
||||||
|
for (int i = 0; i < line.length(); ) {
|
||||||
|
int codePoint = line.codePointAt(i);
|
||||||
|
String ch = normalizePdfChar(codePoint);
|
||||||
|
PDFont font = pickFontForChar(ch, fonts);
|
||||||
|
if (font == null) {
|
||||||
|
ch = "?";
|
||||||
|
font = pickFontForChar(ch, fonts);
|
||||||
|
}
|
||||||
|
if (font == null) {
|
||||||
|
i += Character.charCount(codePoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentFont == null) {
|
||||||
|
currentFont = font;
|
||||||
|
}
|
||||||
|
if (font != currentFont) {
|
||||||
|
content.setFont(currentFont, fontSize);
|
||||||
|
content.showText(segment.toString());
|
||||||
|
segment.setLength(0);
|
||||||
|
currentFont = font;
|
||||||
|
}
|
||||||
|
segment.append(ch);
|
||||||
|
i += Character.charCount(codePoint);
|
||||||
|
}
|
||||||
|
if (segment.length() > 0) {
|
||||||
|
content.setFont(currentFont, fontSize);
|
||||||
|
content.showText(segment.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PDFont pickFontForChar(String ch, List<PDFont> fonts) {
|
||||||
|
for (PDFont font : fonts) {
|
||||||
|
try {
|
||||||
|
font.encode(ch);
|
||||||
|
return font;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float getCharWidth(PDFont font, String ch, float fontSize) {
|
||||||
|
try {
|
||||||
|
return font.getStringWidth(ch) / 1000f * fontSize;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizePdfChar(int codePoint) {
|
||||||
|
if (codePoint == 0x2022) return "-";
|
||||||
|
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) return " ";
|
||||||
|
return new String(Character.toChars(codePoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PdfCtx newPdfPage(PDDocument document, float margin) throws IOException {
|
||||||
|
PDPage page = new PDPage(PDRectangle.A4);
|
||||||
|
document.addPage(page);
|
||||||
|
PDPageContentStream content = new PDPageContentStream(document, page);
|
||||||
|
float y = page.getMediaBox().getHeight() - margin;
|
||||||
|
return new PdfCtx(content, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum MdType {
|
||||||
|
HEADING,
|
||||||
|
LIST,
|
||||||
|
PARAGRAPH
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MdBlock {
|
||||||
|
private final MdType type;
|
||||||
|
private final int level;
|
||||||
|
private final String text;
|
||||||
|
|
||||||
|
private MdBlock(MdType type, int level, String text) {
|
||||||
|
this.type = type;
|
||||||
|
this.level = level;
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PdfCtx {
|
||||||
|
private final PDPageContentStream content;
|
||||||
|
private float y;
|
||||||
|
|
||||||
|
private PdfCtx(PDPageContentStream content, float y) {
|
||||||
|
this.content = content;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.PromptTemplateDTO;
|
||||||
|
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||||
|
import com.imeeting.entity.biz.PromptTemplate;
|
||||||
|
|
||||||
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import com.unisbase.dto.PageResult;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/biz/prompt")
|
||||||
|
public class PromptTemplateController {
|
||||||
|
|
||||||
|
private final PromptTemplateService promptTemplateService;
|
||||||
|
|
||||||
|
public PromptTemplateController(PromptTemplateService promptTemplateService) {
|
||||||
|
this.promptTemplateService = promptTemplateService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<PromptTemplateVO> save(@RequestBody PromptTemplateDTO dto) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
|
||||||
|
if (Integer.valueOf(1).equals(dto.getIsSystem())) {
|
||||||
|
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
return ApiResponse.error("No permission to create public template");
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||||
|
dto.setTenantId(loginUser.getTenantId());
|
||||||
|
} else if (dto.getTenantId() == null) {
|
||||||
|
dto.setTenantId(0L);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dto.setTenantId(loginUser.getTenantId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(promptTemplateService.saveTemplate(dto, loginUser.getUserId(), loginUser.getTenantId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<PromptTemplateVO> update(@RequestBody PromptTemplateDTO dto) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
PromptTemplate existing = promptTemplateService.getById(dto.getId());
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("Template not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canModify = false;
|
||||||
|
if (Integer.valueOf(0).equals(existing.getIsSystem())) {
|
||||||
|
canModify = existing.getCreatorId().equals(loginUser.getUserId());
|
||||||
|
} else {
|
||||||
|
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||||
|
canModify = existing.getTenantId() == 0L;
|
||||||
|
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
canModify = existing.getTenantId().equals(loginUser.getTenantId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canModify) {
|
||||||
|
return ApiResponse.error("No permission to modify this template");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(promptTemplateService.updateTemplate(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/status")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
PromptTemplate existing = promptTemplateService.getById(id);
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("Template not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canGlobalModify = false;
|
||||||
|
if (Integer.valueOf(1).equals(existing.getIsSystem())) {
|
||||||
|
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(existing.getTenantId())) {
|
||||||
|
canGlobalModify = true;
|
||||||
|
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin()) && existing.getTenantId().equals(loginUser.getTenantId())) {
|
||||||
|
canGlobalModify = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canGlobalModify) {
|
||||||
|
existing.setStatus(status);
|
||||||
|
return ApiResponse.ok(promptTemplateService.updateById(existing));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean success = promptTemplateService.updateUserTemplateStatus(
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
return ApiResponse.error("Template not found or no permission");
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
PromptTemplate existing = promptTemplateService.getById(id);
|
||||||
|
if (existing == null) {
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canModify = false;
|
||||||
|
if (Integer.valueOf(0).equals(existing.getIsSystem())) {
|
||||||
|
canModify = existing.getCreatorId().equals(loginUser.getUserId());
|
||||||
|
} else {
|
||||||
|
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||||
|
canModify = existing.getTenantId() == 0L;
|
||||||
|
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
|
canModify = existing.getTenantId().equals(loginUser.getTenantId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canModify) {
|
||||||
|
return ApiResponse.error("No permission to delete this template");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(promptTemplateService.removeById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<PageResult<List<PromptTemplateVO>>> page(
|
||||||
|
@RequestParam(defaultValue = "1") Integer current,
|
||||||
|
@RequestParam(defaultValue = "10") Integer size,
|
||||||
|
@RequestParam(required = false) String name,
|
||||||
|
@RequestParam(required = false) String category) {
|
||||||
|
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
return ApiResponse.ok(promptTemplateService.pageTemplates(
|
||||||
|
current,
|
||||||
|
size,
|
||||||
|
name,
|
||||||
|
category,
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.imeeting.controller.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
|
||||||
|
import com.imeeting.dto.biz.SpeakerRegisterDTO;
|
||||||
|
import com.imeeting.dto.biz.SpeakerVO;
|
||||||
|
import com.imeeting.entity.biz.Speaker;
|
||||||
|
|
||||||
|
import com.imeeting.service.biz.SpeakerService;
|
||||||
|
import com.unisbase.common.ApiResponse;
|
||||||
|
import com.unisbase.security.LoginUser;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/biz/speaker")
|
||||||
|
public class SpeakerController {
|
||||||
|
|
||||||
|
private final SpeakerService speakerService;
|
||||||
|
|
||||||
|
public SpeakerController(SpeakerService speakerService) {
|
||||||
|
this.speakerService = speakerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<SpeakerVO> register(@ModelAttribute SpeakerRegisterDTO registerDTO) {
|
||||||
|
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
if (!(principal instanceof LoginUser)) {
|
||||||
|
return ApiResponse.error("未获取到用户信息");
|
||||||
|
}
|
||||||
|
LoginUser loginUser = (LoginUser) principal;
|
||||||
|
registerDTO.setUserId(loginUser.getUserId());
|
||||||
|
|
||||||
|
// 自动取当前登录人姓名,如果没有,可以用登录名兜底
|
||||||
|
registerDTO.setName(loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername());
|
||||||
|
|
||||||
|
return ApiResponse.ok(speakerService.register(registerDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ApiResponse<List<SpeakerVO>> list() {
|
||||||
|
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
if (!(principal instanceof LoginUser)) {
|
||||||
|
return ApiResponse.error("未获取到用户信息");
|
||||||
|
}
|
||||||
|
LoginUser loginUser = (LoginUser) principal;
|
||||||
|
if (loginUser.getUserId() == null) {
|
||||||
|
return ApiResponse.error("无效的用户ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Speaker> list = speakerService.list(new LambdaQueryWrapper<Speaker>()
|
||||||
|
.eq(Speaker::getUserId, loginUser.getUserId())
|
||||||
|
.orderByDesc(Speaker::getUpdatedAt));
|
||||||
|
|
||||||
|
List<SpeakerVO> vos = list.stream().map(this::toVO).collect(Collectors.toList());
|
||||||
|
return ApiResponse.ok(vos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SpeakerVO toVO(Speaker speaker) {
|
||||||
|
SpeakerVO vo = new SpeakerVO();
|
||||||
|
vo.setId(speaker.getId());
|
||||||
|
vo.setName(speaker.getName());
|
||||||
|
vo.setUserId(speaker.getUserId());
|
||||||
|
vo.setVoicePath(speaker.getVoicePath());
|
||||||
|
vo.setVoiceExt(speaker.getVoiceExt());
|
||||||
|
vo.setVoiceSize(speaker.getVoiceSize());
|
||||||
|
vo.setStatus(speaker.getStatus());
|
||||||
|
vo.setRemark(speaker.getRemark());
|
||||||
|
vo.setCreatedAt(speaker.getCreatedAt());
|
||||||
|
vo.setUpdatedAt(speaker.getUpdatedAt());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
package com.imeeting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class CreateTenantDTO {
|
|
||||||
private String tenantCode;
|
|
||||||
private String tenantName;
|
|
||||||
private String contactName;
|
|
||||||
private String contactPhone;
|
|
||||||
private String remark;
|
|
||||||
private LocalDateTime expireTime;
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.imeeting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class PasswordUpdateDTO {
|
|
||||||
private String oldPassword;
|
|
||||||
private String newPassword;
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package com.imeeting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class PermissionNode {
|
|
||||||
private Long permId;
|
|
||||||
private Long parentId;
|
|
||||||
private String name;
|
|
||||||
private String code;
|
|
||||||
private String permType;
|
|
||||||
private Integer level;
|
|
||||||
private String path;
|
|
||||||
private String component;
|
|
||||||
private String icon;
|
|
||||||
private Integer sortOrder;
|
|
||||||
private Integer isVisible;
|
|
||||||
private Integer status;
|
|
||||||
private String description;
|
|
||||||
private String meta;
|
|
||||||
private List<PermissionNode> children = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package com.imeeting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class PlatformConfigVO {
|
|
||||||
private String projectName;
|
|
||||||
private String logoUrl;
|
|
||||||
private String iconUrl;
|
|
||||||
private String loginBgUrl;
|
|
||||||
private String icpInfo;
|
|
||||||
private String copyrightInfo;
|
|
||||||
private String systemDescription;
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package com.imeeting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class SysParamQueryDTO {
|
|
||||||
private String paramKey;
|
|
||||||
private String paramType;
|
|
||||||
private String description;
|
|
||||||
private Integer pageNum = 1;
|
|
||||||
private Integer pageSize = 10;
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package com.imeeting.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class SysParamVO {
|
|
||||||
private Long paramId;
|
|
||||||
private String paramKey;
|
|
||||||
private String paramValue;
|
|
||||||
private String paramType;
|
|
||||||
private Integer isSystem;
|
|
||||||
private String description;
|
|
||||||
private Integer status;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package com.imeeting.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class UserProfile {
|
|
||||||
private Long userId;
|
|
||||||
private String username;
|
|
||||||
private String displayName;
|
|
||||||
private String email;
|
|
||||||
private String phone;
|
|
||||||
private Integer status;
|
|
||||||
@JsonProperty("isAdmin")
|
|
||||||
private boolean isAdmin;
|
|
||||||
private Boolean isPlatformAdmin;
|
|
||||||
private Boolean isTenantAdmin;
|
|
||||||
private Integer pwdResetRequired;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AiModelDTO {
|
||||||
|
private Long id;
|
||||||
|
private String modelType;
|
||||||
|
private String modelName;
|
||||||
|
private String provider;
|
||||||
|
private String baseUrl;
|
||||||
|
private String apiPath;
|
||||||
|
private String apiKey;
|
||||||
|
private String modelCode;
|
||||||
|
private String wsUrl;
|
||||||
|
private BigDecimal temperature;
|
||||||
|
private BigDecimal topP;
|
||||||
|
private Map<String, Object> mediaConfig;
|
||||||
|
private Integer isDefault;
|
||||||
|
private Integer status;
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AiModelVO {
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String modelType;
|
||||||
|
private String modelName;
|
||||||
|
private String provider;
|
||||||
|
private String baseUrl;
|
||||||
|
private String apiPath;
|
||||||
|
private String apiKey; // Will be masked in actual implementation
|
||||||
|
private String modelCode;
|
||||||
|
private String wsUrl;
|
||||||
|
private BigDecimal temperature;
|
||||||
|
private BigDecimal topP;
|
||||||
|
private Map<String, Object> mediaConfig;
|
||||||
|
private Integer isDefault;
|
||||||
|
private Integer status;
|
||||||
|
private String remark;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class HotWordDTO {
|
||||||
|
private Long id;
|
||||||
|
private String word;
|
||||||
|
private List<String> pinyinList;
|
||||||
|
private Integer matchStrategy;
|
||||||
|
private String category;
|
||||||
|
private Integer weight;
|
||||||
|
private Integer status;
|
||||||
|
private Integer isPublic;
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class HotWordVO {
|
||||||
|
private Long id;
|
||||||
|
private String word;
|
||||||
|
private List<String> pinyinList;
|
||||||
|
private Integer isPublic;
|
||||||
|
private Long creatorId;
|
||||||
|
private Integer matchStrategy;
|
||||||
|
private String category;
|
||||||
|
private Integer weight;
|
||||||
|
private Integer status;
|
||||||
|
private Integer isSynced;
|
||||||
|
private String remark;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MeetingDTO {
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime meetingTime;
|
||||||
|
|
||||||
|
private String participants;
|
||||||
|
private String tags;
|
||||||
|
private String audioUrl;
|
||||||
|
private Long creatorId;
|
||||||
|
private String creatorName;
|
||||||
|
private Long asrModelId;
|
||||||
|
private Long summaryModelId;
|
||||||
|
private Long promptId;
|
||||||
|
private Integer useSpkId;
|
||||||
|
private List<String> hotWords;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MeetingTranscriptVO {
|
||||||
|
private Long id;
|
||||||
|
private String speakerId;
|
||||||
|
private String speakerName;
|
||||||
|
private String speakerLabel;
|
||||||
|
private String content;
|
||||||
|
private Integer startTime;
|
||||||
|
private Integer endTime;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class MeetingVO {
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long creatorId;
|
||||||
|
private String creatorName;
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime meetingTime;
|
||||||
|
|
||||||
|
private String participants;
|
||||||
|
private List<Long> participantIds;
|
||||||
|
private String tags;
|
||||||
|
private String audioUrl;
|
||||||
|
private Integer duration;
|
||||||
|
private String summaryContent;
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PromptTemplateDTO {
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private String templateName;
|
||||||
|
private String category;
|
||||||
|
private Integer isSystem;
|
||||||
|
private java.util.List<String> tags;
|
||||||
|
private String promptContent;
|
||||||
|
private Integer status;
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PromptTemplateVO {
|
||||||
|
private Long id;
|
||||||
|
private Long tenantId;
|
||||||
|
private Long creatorId;
|
||||||
|
private String templateName;
|
||||||
|
private String category;
|
||||||
|
private Integer isSystem;
|
||||||
|
private java.util.List<String> tags;
|
||||||
|
private Integer usageCount;
|
||||||
|
private String promptContent;
|
||||||
|
private Integer status;
|
||||||
|
private String remark;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SpeakerRegisterDTO {
|
||||||
|
private String name;
|
||||||
|
private Long userId;
|
||||||
|
private String remark;
|
||||||
|
private MultipartFile file;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SpeakerVO {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private Long userId;
|
||||||
|
private String voicePath;
|
||||||
|
private String voiceExt;
|
||||||
|
private Long voiceSize;
|
||||||
|
private Integer status;
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class BaseEntity {
|
|
||||||
private Long tenantId;
|
|
||||||
private Integer status;
|
|
||||||
|
|
||||||
@TableLogic(value = "0", delval = "1")
|
|
||||||
private Integer isDeleted;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("device_info")
|
|
||||||
public class Device extends BaseEntity {
|
|
||||||
@TableId(value = "device_id", type = IdType.AUTO)
|
|
||||||
private Long deviceId;
|
|
||||||
private Long userId;
|
|
||||||
private String deviceCode;
|
|
||||||
private String deviceName;
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("sys_dict_item")
|
|
||||||
public class SysDictItem extends BaseEntity {
|
|
||||||
@TableId(value = "dict_item_id", type = IdType.AUTO)
|
|
||||||
private Long dictItemId;
|
|
||||||
private String typeCode;
|
|
||||||
private String itemLabel;
|
|
||||||
private String itemValue;
|
|
||||||
private Integer sortOrder;
|
|
||||||
private String remark;
|
|
||||||
|
|
||||||
@TableField(exist = false)
|
|
||||||
private Long tenantId;
|
|
||||||
@TableField(exist = false)
|
|
||||||
private Integer isDeleted;
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("sys_dict_type")
|
|
||||||
public class SysDictType extends BaseEntity {
|
|
||||||
@TableId(value = "dict_type_id", type = IdType.AUTO)
|
|
||||||
private Long dictTypeId;
|
|
||||||
private String typeCode;
|
|
||||||
private String typeName;
|
|
||||||
private String remark;
|
|
||||||
|
|
||||||
@TableField(exist = false)
|
|
||||||
private Long tenantId;
|
|
||||||
@TableField(exist = false)
|
|
||||||
private Integer isDeleted;
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("sys_log")
|
|
||||||
public class SysLog {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private Long tenantId;
|
|
||||||
private Long userId;
|
|
||||||
private String username;
|
|
||||||
private String logType; // LOGIN, OPERATION
|
|
||||||
private String operation;
|
|
||||||
private String method;
|
|
||||||
private String params;
|
|
||||||
private Integer status;
|
|
||||||
private String ip;
|
|
||||||
private Long duration;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
@TableField(exist = false)
|
|
||||||
private String tenantName;
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("sys_org")
|
|
||||||
public class SysOrg extends BaseEntity {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
private Long tenantId;
|
|
||||||
private Long parentId;
|
|
||||||
private String orgName;
|
|
||||||
private String orgCode;
|
|
||||||
private String orgPath;
|
|
||||||
private Integer sortOrder;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("sys_param")
|
|
||||||
public class SysParam extends BaseEntity {
|
|
||||||
@TableId(value = "param_id", type = IdType.AUTO)
|
|
||||||
private Long paramId;
|
|
||||||
private String paramKey;
|
|
||||||
private String paramValue;
|
|
||||||
private String paramType;
|
|
||||||
private Integer isSystem;
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
@TableField(exist = false)
|
|
||||||
private Long tenantId;
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.*;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("sys_permission")
|
|
||||||
public class SysPermission {
|
|
||||||
@TableId(value = "perm_id", type = IdType.AUTO)
|
|
||||||
private Long permId;
|
|
||||||
private Long parentId;
|
|
||||||
private String name;
|
|
||||||
private String code;
|
|
||||||
private String permType;
|
|
||||||
private Integer level;
|
|
||||||
private String path;
|
|
||||||
private String component;
|
|
||||||
private String icon;
|
|
||||||
private Integer sortOrder;
|
|
||||||
private Integer isVisible;
|
|
||||||
private String description;
|
|
||||||
private String meta;
|
|
||||||
private Integer status;
|
|
||||||
|
|
||||||
@TableLogic
|
|
||||||
private Boolean isDeleted;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
|
|
||||||
@TableName("sys_platform_config")
|
|
||||||
public class SysPlatformConfig {
|
|
||||||
@TableId
|
|
||||||
private Long id;
|
|
||||||
private String projectName;
|
|
||||||
private String logoUrl;
|
|
||||||
private String iconUrl;
|
|
||||||
private String loginBgUrl;
|
|
||||||
private String icpInfo;
|
|
||||||
private String copyrightInfo;
|
|
||||||
private String systemDescription;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("sys_role")
|
|
||||||
public class SysRole extends BaseEntity {
|
|
||||||
@TableId(value = "role_id", type = IdType.AUTO)
|
|
||||||
private Long roleId;
|
|
||||||
private String roleCode;
|
|
||||||
private String roleName;
|
|
||||||
private String remark;
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("sys_role_permission")
|
|
||||||
public class SysRolePermission {
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private Long roleId;
|
|
||||||
private Long permId;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("sys_tenant")
|
|
||||||
public class SysTenant extends BaseEntity {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private String tenantCode;
|
|
||||||
private String tenantName;
|
|
||||||
private LocalDateTime expireTime;
|
|
||||||
private String contactName;
|
|
||||||
private String contactPhone;
|
|
||||||
private String remark;
|
|
||||||
|
|
||||||
@TableField(exist = false)
|
|
||||||
private Long tenantId;
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("sys_tenant_user")
|
|
||||||
public class SysTenantUser extends BaseEntity {
|
|
||||||
@TableId(type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private Long userId;
|
|
||||||
private Long tenantId;
|
|
||||||
private Long orgId;
|
|
||||||
|
|
||||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
|
||||||
private String orgName;
|
|
||||||
|
|
||||||
@com.baomidou.mybatisplus.annotation.TableLogic(value = "0", delval = "0")
|
|
||||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
|
||||||
private Integer isDeleted;
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("sys_user")
|
|
||||||
public class SysUser extends BaseEntity {
|
|
||||||
@TableId(value = "user_id", type = IdType.AUTO)
|
|
||||||
private Long userId;
|
|
||||||
private String username;
|
|
||||||
private String displayName;
|
|
||||||
private String email;
|
|
||||||
private String phone;
|
|
||||||
private String passwordHash;
|
|
||||||
private Integer pwdResetRequired;
|
|
||||||
|
|
||||||
private Boolean isPlatformAdmin;
|
|
||||||
|
|
||||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
|
||||||
private Long tenantId;
|
|
||||||
|
|
||||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
|
||||||
private Long orgId;
|
|
||||||
|
|
||||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
|
||||||
private java.util.List<SysTenantUser> memberships;
|
|
||||||
|
|
||||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
|
||||||
private java.util.List<SysRole> roles;
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package com.imeeting.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@TableName("sys_user_role")
|
|
||||||
public class SysUserRole {
|
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
|
||||||
private Long id;
|
|
||||||
private Long tenantId;
|
|
||||||
private Long userId;
|
|
||||||
private Long roleId;
|
|
||||||
@TableLogic(value = "0", delval = "1")
|
|
||||||
private Integer isDeleted;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName(value = "biz_ai_tasks", autoResultMap = true)
|
||||||
|
public class AiTask {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long meetingId;
|
||||||
|
|
||||||
|
private String taskType;
|
||||||
|
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private Map<String, Object> requestData;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private Map<String, Object> responseData;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private Map<String, Object> taskConfig;
|
||||||
|
|
||||||
|
private String resultFilePath;
|
||||||
|
|
||||||
|
private String errorMsg;
|
||||||
|
|
||||||
|
private LocalDateTime startedAt;
|
||||||
|
|
||||||
|
private LocalDateTime completedAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName(value = "biz_asr_models", autoResultMap = true)
|
||||||
|
public class AsrModel extends BaseEntity {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
private String baseUrl;
|
||||||
|
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
private String modelCode;
|
||||||
|
|
||||||
|
private String wsUrl;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private Map<String, Object> mediaConfig;
|
||||||
|
|
||||||
|
private Integer isDefault;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName(value = "biz_hot_words", autoResultMap = true)
|
||||||
|
public class HotWord extends BaseEntity {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String word;
|
||||||
|
|
||||||
|
private Integer isPublic;
|
||||||
|
|
||||||
|
private Long creatorId;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private List<String> pinyinList;
|
||||||
|
|
||||||
|
private Integer matchStrategy;
|
||||||
|
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
private Integer weight;
|
||||||
|
|
||||||
|
private Integer isSynced;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("biz_llm_models")
|
||||||
|
public class LlmModel extends BaseEntity {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
private String provider;
|
||||||
|
|
||||||
|
private String baseUrl;
|
||||||
|
|
||||||
|
private String apiPath;
|
||||||
|
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
private String modelCode;
|
||||||
|
|
||||||
|
private BigDecimal temperature;
|
||||||
|
|
||||||
|
private BigDecimal topP;
|
||||||
|
|
||||||
|
private Integer isDefault;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName(value = "biz_meetings", autoResultMap = true)
|
||||||
|
public class Meeting extends BaseEntity {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
private LocalDateTime meetingTime;
|
||||||
|
|
||||||
|
private String participants;
|
||||||
|
|
||||||
|
private String tags;
|
||||||
|
|
||||||
|
private String audioUrl;
|
||||||
|
|
||||||
|
private Long creatorId;
|
||||||
|
|
||||||
|
private String creatorName;
|
||||||
|
|
||||||
|
private Long latestSummaryTaskId;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String summaryContent;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("biz_meeting_transcripts")
|
||||||
|
public class MeetingTranscript {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long meetingId;
|
||||||
|
|
||||||
|
private String speakerId;
|
||||||
|
|
||||||
|
private String speakerName;
|
||||||
|
|
||||||
|
private String speakerLabel;
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private Integer startTime;
|
||||||
|
|
||||||
|
private Integer endTime;
|
||||||
|
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName(value = "biz_prompt_templates", autoResultMap = true)
|
||||||
|
public class PromptTemplate extends BaseEntity {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String templateName;
|
||||||
|
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
private Integer isSystem;
|
||||||
|
|
||||||
|
private Long creatorId;
|
||||||
|
|
||||||
|
@com.baomidou.mybatisplus.annotation.TableField(typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
|
||||||
|
private java.util.List<String> tags;
|
||||||
|
|
||||||
|
private Integer usageCount;
|
||||||
|
|
||||||
|
private String promptContent;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("biz_prompt_template_user_config")
|
||||||
|
public class PromptTemplateUserConfig extends BaseEntity {
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
private Long templateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.imeeting.entity.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.unisbase.entity.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@TableName("biz_speakers")
|
||||||
|
public class Speaker extends BaseEntity {
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String voicePath;
|
||||||
|
|
||||||
|
private String voiceExt;
|
||||||
|
|
||||||
|
private Long voiceSize;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
// Note: status, createdAt, updatedAt, isDeleted are in BaseEntity
|
||||||
|
// embedding is reserved for future pgvector use
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.imeeting.event;
|
||||||
|
|
||||||
|
public class MeetingCreatedEvent {
|
||||||
|
private final Long meetingId;
|
||||||
|
|
||||||
|
public MeetingCreatedEvent(Long meetingId) {
|
||||||
|
this.meetingId = meetingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getMeetingId() {
|
||||||
|
return meetingId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.imeeting.listener;
|
||||||
|
|
||||||
|
import com.imeeting.event.MeetingCreatedEvent;
|
||||||
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.event.TransactionPhase;
|
||||||
|
import org.springframework.transaction.event.TransactionalEventListener;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MeetingTaskDispatchListener {
|
||||||
|
|
||||||
|
private final AiTaskService aiTaskService;
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||||
|
public void onMeetingCreated(MeetingCreatedEvent event) {
|
||||||
|
aiTaskService.dispatchTasks(event.getMeetingId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
package com.imeeting.listener;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.entity.biz.Meeting;
|
||||||
|
import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务自愈监听器:在系统重启后自动恢复挂起的 AI 任务
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MeetingTaskRecoveryListener implements ApplicationRunner {
|
||||||
|
|
||||||
|
private final MeetingMapper meetingMapper;
|
||||||
|
private final AiTaskService aiTaskService;
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
log.info("Starting meeting task self-healing check...");
|
||||||
|
|
||||||
|
// 1. 查询状态为 1(识别中) 或 2(总结中) 且未删除的会议
|
||||||
|
List<Meeting> pendingMeetings = meetingMapper.selectList(new LambdaQueryWrapper<Meeting>()
|
||||||
|
.in(Meeting::getStatus, 1, 2)
|
||||||
|
.eq(Meeting::getIsDeleted, 0));
|
||||||
|
|
||||||
|
if (pendingMeetings.isEmpty()) {
|
||||||
|
log.info("No pending tasks found. Recovery check completed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
|
||||||
|
|
||||||
|
for (Meeting meeting : pendingMeetings) {
|
||||||
|
try {
|
||||||
|
// 处理 1 小时以上的死任务
|
||||||
|
if (meeting.getUpdatedAt() != null && meeting.getUpdatedAt().isBefore(oneHourAgo)) {
|
||||||
|
log.warn("Meeting {} is stuck for more than 1 hour, marking as failed", meeting.getId());
|
||||||
|
markAsFailed(meeting, "任务因系统维护超时,请重新发起分析");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 清理旧的 Redis 锁和进度缓存,确保恢复线程能拿到控制权
|
||||||
|
redisTemplate.delete(RedisKeys.meetingPollingLockKey(meeting.getId()));
|
||||||
|
|
||||||
|
// 3. 根据状态重新派发任务 (平滑拉起)
|
||||||
|
if (meeting.getStatus() == 1) {
|
||||||
|
log.info("Resuming ASR task for meeting {}", meeting.getId());
|
||||||
|
aiTaskService.dispatchTasks(meeting.getId());
|
||||||
|
} else if (meeting.getStatus() == 2) {
|
||||||
|
log.info("Resuming Summary task for meeting {}", meeting.getId());
|
||||||
|
aiTaskService.dispatchSummaryTask(meeting.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加小延迟防止惊群效应
|
||||||
|
TimeUnit.MILLISECONDS.sleep(200);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to recover meeting task {}", meeting.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Successfully processed {} pending tasks for recovery.", pendingMeetings.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markAsFailed(Meeting m, String reason) {
|
||||||
|
Meeting update = new Meeting();
|
||||||
|
update.setId(m.getId());
|
||||||
|
update.setStatus(4); // 失败
|
||||||
|
meetingMapper.updateById(update);
|
||||||
|
|
||||||
|
// 同步 Redis 进度为失败
|
||||||
|
String progressKey = RedisKeys.meetingProgressKey(m.getId());
|
||||||
|
redisTemplate.opsForValue().set(progressKey,
|
||||||
|
"{\"percent\":-1, \"message\":\"" + reason + "\", \"updateAt\":" + System.currentTimeMillis() + "}",
|
||||||
|
1, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.Device;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface DeviceMapper extends BaseMapper<Device> {}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysDictItem;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysDictItemMapper extends BaseMapper<SysDictItem> {
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysDictType;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysDictTypeMapper extends BaseMapper<SysDictType> {
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
|
||||||
import com.imeeting.entity.SysLog;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysLogMapper extends BaseMapper<SysLog> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@InterceptorIgnore(tenantLine = "true")
|
|
||||||
int insert(SysLog entity);
|
|
||||||
|
|
||||||
@Select("SELECT l.*, t.tenant_name FROM sys_log l " +
|
|
||||||
"LEFT JOIN sys_tenant t ON l.tenant_id = t.id " +
|
|
||||||
"${ew.customSqlSegment}")
|
|
||||||
IPage<SysLog> selectPageWithTenant(IPage<SysLog> page, @Param(Constants.WRAPPER) Wrapper<SysLog> queryWrapper);
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysOrg;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysOrgMapper extends BaseMapper<SysOrg> {
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysParam;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysParamMapper extends BaseMapper<SysParam> {}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysPermission;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysPermissionMapper extends BaseMapper<SysPermission> {
|
|
||||||
@com.baomidou.mybatisplus.annotation.InterceptorIgnore(tenantLine = "true")
|
|
||||||
@Select("""
|
|
||||||
SELECT DISTINCT p.*
|
|
||||||
FROM sys_permission p
|
|
||||||
JOIN sys_role_permission rp ON rp.perm_id = p.perm_id
|
|
||||||
JOIN sys_role r ON r.role_id = rp.role_id
|
|
||||||
JOIN sys_user_role ur ON ur.role_id = r.role_id
|
|
||||||
WHERE p.is_deleted = 0
|
|
||||||
AND r.is_deleted = 0
|
|
||||||
AND ur.is_deleted = 0
|
|
||||||
AND ur.user_id = #{userId}
|
|
||||||
AND r.tenant_id = #{tenantId}
|
|
||||||
AND (ur.tenant_id = #{tenantId} OR ur.tenant_id IS NULL)
|
|
||||||
""")
|
|
||||||
List<SysPermission> selectByUserId(@Param("userId") Long userId, @Param("tenantId") Long tenantId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysPlatformConfig;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysPlatformConfigMapper extends BaseMapper<SysPlatformConfig> {
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysRole;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysRoleMapper extends BaseMapper<SysRole> {}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysRolePermission;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysRolePermissionMapper extends BaseMapper<SysRolePermission> {
|
|
||||||
@Select("""
|
|
||||||
SELECT DISTINCT role_id
|
|
||||||
FROM sys_role_permission
|
|
||||||
WHERE perm_id = #{permId}
|
|
||||||
""")
|
|
||||||
List<Long> selectRoleIdsByPermId(@Param("permId") Long permId);
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysTenant;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysTenantMapper extends BaseMapper<SysTenant> {
|
|
||||||
@InterceptorIgnore(tenantLine = "true")
|
|
||||||
@Select("SELECT * FROM sys_tenant WHERE id = #{id}")
|
|
||||||
SysTenant selectByIdIgnoreTenant(@Param("id") Long id);
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysTenantUser;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysTenantUserMapper extends BaseMapper<SysTenantUser> {
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysUser;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysUserMapper extends BaseMapper<SysUser> {
|
|
||||||
@Select("""
|
|
||||||
SELECT u.*
|
|
||||||
FROM sys_user u
|
|
||||||
JOIN sys_user_role ur ON u.user_id = ur.user_id
|
|
||||||
WHERE ur.role_id = #{roleId}
|
|
||||||
AND ur.is_deleted = 0
|
|
||||||
AND u.is_deleted = 0
|
|
||||||
""")
|
|
||||||
List<SysUser> selectUsersByRoleId(@Param("roleId") Long roleId);
|
|
||||||
|
|
||||||
@InterceptorIgnore(tenantLine = "true")
|
|
||||||
@Select("SELECT * FROM sys_user WHERE username = #{username} AND is_deleted = 0")
|
|
||||||
SysUser selectByUsernameIgnoreTenant(@Param("username") String username);
|
|
||||||
|
|
||||||
@InterceptorIgnore(tenantLine = "true")
|
|
||||||
@Select("SELECT * FROM sys_user WHERE user_id = #{userId} AND is_deleted = 0")
|
|
||||||
SysUser selectByIdIgnoreTenant(@Param("userId") Long userId);
|
|
||||||
|
|
||||||
@InterceptorIgnore(tenantLine = "true")
|
|
||||||
@Select("<script>" +
|
|
||||||
"SELECT u.*, tu.org_id as orgId, tu.tenant_id as tenantId " +
|
|
||||||
"FROM sys_user u " +
|
|
||||||
"JOIN sys_tenant_user tu ON u.user_id = tu.user_id " +
|
|
||||||
"WHERE tu.tenant_id = #{tenantId} " +
|
|
||||||
"<if test='orgId != null'> AND tu.org_id = #{orgId} </if> " +
|
|
||||||
"AND u.is_deleted = 0 AND tu.is_deleted = 0" +
|
|
||||||
"</script>")
|
|
||||||
List<SysUser> selectUsersByTenant(@Param("tenantId") Long tenantId, @Param("orgId") Long orgId);
|
|
||||||
|
|
||||||
@InterceptorIgnore(tenantLine = "true")
|
|
||||||
@Select("""
|
|
||||||
SELECT t.id as tenantId, t.tenant_code as tenantCode, t.tenant_name as tenantName
|
|
||||||
FROM sys_tenant t
|
|
||||||
JOIN sys_tenant_user tu ON t.id = tu.tenant_id
|
|
||||||
JOIN sys_user u ON u.user_id = tu.user_id
|
|
||||||
WHERE u.username = #{username} AND u.is_deleted = 0 AND t.is_deleted = 0
|
|
||||||
ORDER BY t.id ASC
|
|
||||||
""")
|
|
||||||
List<com.imeeting.auth.dto.TokenResponse.TenantInfo> selectTenantsByUsername(@Param("username") String username);
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
package com.imeeting.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.imeeting.entity.SysUserRole;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
import org.apache.ibatis.annotations.Delete;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface SysUserRoleMapper extends BaseMapper<SysUserRole> {
|
|
||||||
@Delete("""
|
|
||||||
DELETE FROM sys_user_role
|
|
||||||
WHERE role_id = #{roleId} AND user_id = #{userId} AND tenant_id = #{tenantId}
|
|
||||||
""")
|
|
||||||
int physicalDelete(@Param("roleId") Long roleId, @Param("userId") Long userId, @Param("tenantId") Long tenantId);
|
|
||||||
|
|
||||||
@Select("""
|
|
||||||
SELECT COUNT(1)
|
|
||||||
FROM sys_user_role ur
|
|
||||||
JOIN sys_role r ON r.role_id = ur.role_id
|
|
||||||
WHERE ur.user_id = #{userId}
|
|
||||||
AND (ur.tenant_id = #{tenantId} OR ur.tenant_id IS NULL)
|
|
||||||
AND ur.is_deleted = 0
|
|
||||||
AND r.is_deleted = 0
|
|
||||||
AND r.tenant_id = #{tenantId}
|
|
||||||
AND r.role_code = 'TENANT_ADMIN'
|
|
||||||
""")
|
|
||||||
Long countTenantAdminRole(@Param("userId") Long userId, @Param("tenantId") Long tenantId);
|
|
||||||
|
|
||||||
@Select("""
|
|
||||||
SELECT DISTINCT ur.user_id
|
|
||||||
FROM sys_user_role ur
|
|
||||||
WHERE ur.role_id = #{roleId}
|
|
||||||
AND ur.is_deleted = 0
|
|
||||||
""")
|
|
||||||
List<Long> selectUserIdsByRoleId(@Param("roleId") Long roleId);
|
|
||||||
|
|
||||||
@Select("""
|
|
||||||
SELECT ur.role_id
|
|
||||||
FROM sys_user_role ur
|
|
||||||
WHERE ur.user_id = #{userId}
|
|
||||||
AND ur.tenant_id = #{tenantId}
|
|
||||||
AND ur.is_deleted = 0
|
|
||||||
""")
|
|
||||||
List<Long> selectRoleIdsByUserIdAndTenantId(@Param("userId") Long userId, @Param("tenantId") Long tenantId);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.imeeting.mapper.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AiTaskMapper extends BaseMapper<AiTask> {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.imeeting.mapper.biz;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.imeeting.entity.biz.AsrModel;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AsrModelMapper extends BaseMapper<AsrModel> {
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue