Compare commits
249 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
d5e1616c01 | |
|
|
22aefae6cf | |
|
|
e3c688b9ce | |
|
|
1e1ca71939 | |
|
|
093438e73a | |
|
|
85ed5e37fd | |
|
|
3df2e753c5 | |
|
|
8703b8bdc7 | |
|
|
c375390d52 | |
|
|
5df4f3d840 | |
|
|
66f84af96c | |
|
|
c8df0d9604 | |
|
|
cd4b5365d6 | |
|
|
58674642e7 | |
|
|
0aaa3a1cde | |
|
|
9e12e03481 | |
|
|
e39f1405b3 | |
|
|
7671f94fcd | |
|
|
77879f4e0e | |
|
|
6476b23bb6 | |
|
|
788a98f28f | |
|
|
c0b4c0d319 | |
|
|
b06b7585d8 | |
|
|
9d0edbfa00 | |
|
|
93900308e0 | |
|
|
ec8bb466da | |
|
|
76410071e7 | |
|
|
ae75ea4e2c | |
|
|
2bc0b55e60 | |
|
|
37696b13a9 | |
|
|
0d6acd1056 | |
|
|
31d7a8a59f | |
|
|
91ab0eb7f0 | |
|
|
ba52e21437 | |
|
|
cc43fe2e01 | |
|
|
17dbeefdf8 | |
|
|
f5a6a22eb1 | |
|
|
e954e775c0 | |
|
|
3337285135 | |
|
|
b0a3dedb99 | |
|
|
90b1f60955 | |
|
|
0a33ff8e56 | |
|
|
4097e719a9 | |
|
|
4bc3f65a10 | |
|
|
cfb5aeff71 | |
|
|
69e6eff9d9 | |
|
|
92c8b126f5 | |
|
|
202ce057fd | |
|
|
c89e508091 | |
|
|
1ab6e37fe1 | |
|
|
2a95eca102 | |
|
|
d36beec479 | |
|
|
d5525496ea | |
|
|
065708497a | |
|
|
fc034b8326 | |
|
|
0593e0c23d | |
|
|
6c970536b2 | |
|
|
08d9024401 | |
|
|
57f343ddfd | |
|
|
982425e21e | |
|
|
06c4f5d74e | |
|
|
9c98e670e1 | |
|
|
c0cc4b1c27 | |
|
|
255e268138 | |
|
|
21d356bb58 | |
|
|
bf40b13383 | |
|
|
77fe6d4e77 | |
|
|
ba62c9e0c0 | |
|
|
033ffaacc9 | |
|
|
a036c14673 | |
|
|
31a4c5c767 | |
|
|
2bab042ca0 | |
|
|
97065c68a6 | |
|
|
d38acf5ccc | |
|
|
95f7f766c5 | |
|
|
11b726017d | |
|
|
5ff2ddc388 | |
|
|
b34efa5eb0 | |
|
|
33caa1201f | |
|
|
4499e6265b | |
|
|
9585033303 | |
|
|
97ee737f8c | |
|
|
7b0e100f6c | |
|
|
d5738ca35d | |
|
|
8c771abbb8 | |
|
|
bdc91f4f8b | |
|
|
194a05cbe0 | |
|
|
ee1e75eda2 | |
|
|
7233f13598 | |
|
|
f787f867bb | |
|
|
8d4a31e043 | |
|
|
443e067b30 | |
|
|
3c299aaf3b | |
|
|
4572d15bea | |
|
|
c0d2dcce3d | |
|
|
236176d5ff | |
|
|
877a4a0654 | |
|
|
fee7f2a0b3 | |
|
|
c64c8b5690 | |
|
|
fd9ef5c885 | |
|
|
146b31b809 | |
|
|
6e4d10427a | |
|
|
e330edc965 | |
|
|
2e05a25e63 | |
|
|
178b920581 | |
|
|
b07bbe90a0 | |
|
|
1e7a8ad83c | |
|
|
11c3ee1b09 | |
|
|
1813b90c6a | |
|
|
82dc485dcf | |
|
|
1cce0aeabb | |
|
|
d47a66febd | |
|
|
e1e321a86d | |
|
|
30414bd41f | |
|
|
1b41693597 | |
|
|
2e20799b4b | |
|
|
40bf049a0e | |
|
|
e7659b1e31 | |
|
|
8716608afa | |
|
|
7c3b65624e | |
|
|
fb5c4b545e | |
|
|
3860ba3e00 | |
|
|
8a082732df | |
|
|
92a12c4c81 | |
|
|
7f9c080bf7 | |
|
|
47ebeade20 | |
|
|
384494d9ff | |
|
|
892275bc65 | |
|
|
a046ecf05b | |
|
|
188809305e | |
|
|
7989b6aa11 | |
|
|
7d08234919 | |
|
|
f7480df565 | |
|
|
51190f330c | |
|
|
3469884bca | |
|
|
a49888587f | |
|
|
b9ec41095f | |
|
|
ccb408ade5 | |
|
|
1877c64cc2 | |
|
|
beab0edabe | |
|
|
38edf9dad6 | |
|
|
a34885111c | |
|
|
9b63a1ec4e | |
|
|
eba6bf105e | |
|
|
c0e973e5a9 | |
|
|
5b4304a4b2 | |
|
|
aed87e8ad3 | |
|
|
6445d429f8 | |
|
|
f20be9bcc6 | |
|
|
4904526e09 | |
|
|
a8b93a46f8 | |
|
|
35698287de | |
|
|
aaa2624fe2 | |
|
|
6600d37757 | |
|
|
5aefcf8d7d | |
|
|
e6580beaa8 | |
|
|
2f80c6c55e | |
|
|
a295a3b15b | |
|
|
f6ffaddae1 | |
|
|
99f5fd1cbd | |
|
|
0b8014d1af | |
|
|
8cdac8ad9f | |
|
|
4e38580258 | |
|
|
8d0ef246f3 | |
|
|
29551dfbe2 | |
|
|
b36a08adc7 | |
|
|
2d788bac75 | |
|
|
324e283f41 | |
|
|
6a08fb1a3b | |
|
|
940cc8a939 | |
|
|
ce4743c5ea | |
|
|
900f092d5e | |
|
|
6107e611f4 | |
|
|
b1fd9de87d | |
|
|
9480d0e4bf | |
|
|
ddd97e0514 | |
|
|
27ae0a3def | |
|
|
d4424a157b | |
|
|
0960e625f9 | |
|
|
48dd7d1150 | |
|
|
29a4be912d | |
|
|
4194b9afe6 | |
|
|
276f9aa188 | |
|
|
5a5f71d465 | |
|
|
7c64cdf7a2 | |
|
|
712d31d911 | |
|
|
caf2e22df0 | |
|
|
017e1d2ded | |
|
|
db310fc803 | |
|
|
2b30744d2e | |
|
|
6d46998abe | |
|
|
658b7e6b59 | |
|
|
5972182519 | |
|
|
b2430abe73 | |
|
|
3b7ba2c47a | |
|
|
dffd33206a | |
|
|
5f895bfe26 | |
|
|
53ff2292a8 | |
|
|
b9593324a5 | |
|
|
1c82365e97 | |
|
|
b2e2f2c46a | |
|
|
e83a0ece32 | |
|
|
c802f63ada | |
|
|
21c38355c3 | |
|
|
135203b9f6 | |
|
|
24c3835b79 | |
|
|
ff47c34349 | |
|
|
3cd1c48bce | |
|
|
d780278da4 | |
|
|
4f42fb50ad | |
|
|
5da9a97d55 | |
|
|
578359a0d3 | |
|
|
f0d63c97a3 | |
|
|
3a7baa0341 | |
|
|
a611ac2b61 | |
|
|
552e2255bd | |
|
|
f9c0d31b87 | |
|
|
9d1a8710af | |
|
|
60754bbd26 | |
|
|
ffc19fa572 | |
|
|
8dbed4c8e6 | |
|
|
12c79cdf26 | |
|
|
4ee7a620b9 | |
|
|
92e6b9fd4d | |
|
|
653a9f7ef4 | |
|
|
2b1d7b8a2e | |
|
|
af5c658bd9 | |
|
|
d8bfdb21fa | |
|
|
eaed89c9ec | |
|
|
364e49b3df | |
|
|
86b3616786 | |
|
|
d554849e8e | |
|
|
c60db64578 | |
|
|
430459c331 | |
|
|
61da050438 | |
|
|
0ccf0aa87d | |
|
|
11ab76f2ed | |
|
|
423327c61d | |
|
|
5c47b07ec6 | |
|
|
afff8a8d07 | |
|
|
35396104a0 | |
|
|
37025d3f02 | |
|
|
80a4682757 | |
|
|
eaadc4ee51 | |
|
|
5e4a2aa2d1 | |
|
|
70e739818a | |
|
|
f6ba04ebd8 | |
|
|
21b3ab3afc | |
|
|
1a392d96b9 |
|
|
@ -0,0 +1,45 @@
|
|||
# Local environment files
|
||||
.env
|
||||
.env.*
|
||||
backend/.env
|
||||
backend/.env.*
|
||||
backend/src/main/resources/application-local.yml
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
!backend/.env.example
|
||||
.omx/
|
||||
/backend/target/
|
||||
/.idea
|
||||
/.vscode
|
||||
/.codegraph
|
||||
/.agents
|
||||
/backend/.env.example
|
||||
/backend/.mvn-settings-ali.xml
|
||||
/backend/.mvn-settings-codex.xml
|
||||
/backend/imeeting-backend.iml
|
||||
/backend/lombok.config
|
||||
/database/
|
||||
/deploy/
|
||||
/docs/
|
||||
/rebel.xml
|
||||
/.editorconfig
|
||||
/frontend/design/
|
||||
/components/
|
||||
/bat/
|
||||
/.agents/
|
||||
/.codex/
|
||||
/.gemini/
|
||||
/.idea/
|
||||
/.m2-temp/
|
||||
/.m2-test/
|
||||
/.omx/
|
||||
/APP_LOG_PATH_IS_UNDEFINED/
|
||||
/backend/.m2repo/
|
||||
/backend/m2repo_local/
|
||||
/backend/src/test/
|
||||
/backend/target/
|
||||
/.claude
|
||||
/web-fe/
|
||||
/.idea/
|
||||
/.idea/*
|
||||
|
|
@ -1,223 +1,480 @@
|
|||
# 数据库结构文档(PostgreSQL)
|
||||
|
||||
本文档根据 `backend/design/db_schema_pgsql.sql` 生成,描述当前核心表结构、字段、约束与索引。
|
||||
|
||||
## 0. 租户与组织
|
||||
|
||||
### 0.1 `sys_tenant`(租户表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
# 鏁版嵁搴撶粨鏋勬枃妗o紙PostgreSQL锛?
|
||||
鏈枃妗f牴鎹?`backend/design/db_schema_pgsql.sql` 鐢熸垚锛屾弿杩板綋鍓嶆牳蹇冭〃缁撴瀯銆佸瓧娈点€佺害鏉熶笌绱㈠紩銆?
|
||||
## 0. 绉熸埛涓庣粍缁?
|
||||
### 0.1 `sys_tenant`锛堢鎴疯〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 租户ID |
|
||||
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 租户编码 |
|
||||
| tenant_name | VARCHAR(128) | NOT NULL | 租户名称 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
||||
| expire_time | TIMESTAMP(6) | | 过期时间 |
|
||||
| contact_name | VARCHAR(64) | | 联系人 |
|
||||
| contact_phone | VARCHAR(32) | | 联系电话 |
|
||||
| remark | VARCHAR(255) | | 备注 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
| id | BIGSERIAL | PK | 绉熸埛ID |
|
||||
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 绉熸埛缂栫爜 |
|
||||
| tenant_name | VARCHAR(128) | NOT NULL | 绉熸埛鍚嶇О |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
|
||||
| expire_time | TIMESTAMP(6) | | 杩囨湡鏃堕棿 |
|
||||
| contact_name | VARCHAR(64) | | 鑱旂郴浜?|
|
||||
| contact_phone | VARCHAR(32) | | 鑱旂郴鐢佃瘽 |
|
||||
| remark | VARCHAR(255) | | 澶囨敞 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
|
||||
索引:
|
||||
- `uk_tenant_code`:`UNIQUE (tenant_code) WHERE is_deleted = FALSE`
|
||||
绱㈠紩锛?- `uk_tenant_code`锛歚UNIQUE (tenant_code) WHERE is_deleted = FALSE`
|
||||
|
||||
### 0.2 `sys_org`(组织架构表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
### 0.2 `sys_org`锛堢粍缁囨灦鏋勮〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 组织ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||
| parent_id | BIGINT | | 父级组织ID |
|
||||
| org_name | VARCHAR(128) | NOT NULL | 组织名称 |
|
||||
| org_code | VARCHAR(64) | | 组织编码 |
|
||||
| org_path | VARCHAR(512) | | 组织路径 |
|
||||
| sort_order | INTEGER | DEFAULT 0 | 排序 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
| id | BIGSERIAL | PK | 缁勭粐ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||
| parent_id | BIGINT | | 鐖剁骇缁勭粐ID |
|
||||
| org_name | VARCHAR(128) | NOT NULL | 缁勭粐鍚嶇О |
|
||||
| org_code | VARCHAR(64) | | 缁勭粐缂栫爜 |
|
||||
| org_path | VARCHAR(512) | | 缁勭粐璺緞 |
|
||||
| sort_order | INTEGER | DEFAULT 0 | 鎺掑簭 |
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
|
||||
外键:
|
||||
- `fk_org_parent`:`parent_id -> sys_org(id)`
|
||||
- `fk_org_tenant`:`tenant_id -> sys_tenant(id)`
|
||||
澶栭敭锛?- `fk_org_parent`锛歚parent_id -> sys_org(id)`
|
||||
- `fk_org_tenant`锛歚tenant_id -> sys_tenant(id)`
|
||||
|
||||
索引:
|
||||
- `idx_org_tenant`:`(tenant_id)`
|
||||
绱㈠紩锛?- `idx_org_tenant`锛歚(tenant_id)`
|
||||
|
||||
## 1. 用户与角色
|
||||
|
||||
### 1.1 `sys_user`(用户表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
## 1. 鐢ㄦ埛涓庤鑹?
|
||||
### 1.1 `sys_user`锛堢敤鎴疯〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| user_id | BIGSERIAL | PK | 用户ID |
|
||||
| username | VARCHAR(50) | NOT NULL, UNIQUE | 登录名 |
|
||||
| display_name | VARCHAR(50) | NOT NULL | 显示名 |
|
||||
| email | VARCHAR(100) | | 邮箱 |
|
||||
| phone | VARCHAR(30) | UNIQUE | 手机号 |
|
||||
| password_hash | VARCHAR(255) | NOT NULL | 密码哈希 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
||||
| pwd_reset_required | SMALLINT | DEFAULT 1 | 首次登录是否需改密 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| is_platform_admin | BOOLEAN | DEFAULT false | 是否平台管理员 |
|
||||
| user_id | BIGSERIAL | PK | 鐢ㄦ埛ID |
|
||||
| username | VARCHAR(50) | NOT NULL, UNIQUE | 鐧诲綍鍚?|
|
||||
| display_name | VARCHAR(50) | NOT NULL | 鏄剧ず鍚?|
|
||||
| email | VARCHAR(100) | | 閭 |
|
||||
| phone | VARCHAR(30) | UNIQUE | 鎵嬫満鍙?|
|
||||
| password_hash | VARCHAR(255) | NOT NULL | 瀵嗙爜鍝堝笇 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
|
||||
| pwd_reset_required | SMALLINT | DEFAULT 1 | 棣栨鐧诲綍鏄惁闇€鏀瑰瘑 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||
| is_platform_admin | BOOLEAN | DEFAULT false | 鏄惁骞冲彴绠$悊鍛?|
|
||||
|
||||
索引:
|
||||
- `uk_user_username`:`UNIQUE (username) WHERE is_deleted = FALSE`
|
||||
绱㈠紩锛?- `uk_user_username`锛歚UNIQUE (username) WHERE is_deleted = FALSE`
|
||||
|
||||
### 1.2 `sys_role`(角色表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
### 1.2 `sys_role`锛堣鑹茶〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| role_id | BIGSERIAL | PK | 角色ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||
| role_code | VARCHAR(50) | NOT NULL | 角色编码(租户内唯一) |
|
||||
| role_name | VARCHAR(50) | NOT NULL | 角色名称 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
||||
| remark | TEXT | | 备注 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| role_id | BIGSERIAL | PK | 瑙掕壊ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||
| role_code | VARCHAR(50) | NOT NULL | 瑙掕壊缂栫爜锛堢鎴峰唴鍞竴锛?|
|
||||
| role_name | VARCHAR(50) | NOT NULL | 瑙掕壊鍚嶇О |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
|
||||
| remark | TEXT | | 澶囨敞 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||
|
||||
索引:
|
||||
- `idx_sys_role_tenant`:`(tenant_id)`
|
||||
- `uk_role_code`:`UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE`
|
||||
绱㈠紩锛?- `idx_sys_role_tenant`锛歚(tenant_id)`
|
||||
- `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 |
|
||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
||||
| role_id | BIGINT | NOT NULL | 角色ID |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| id | BIGSERIAL | PK | 鍏宠仈ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||
| user_id | BIGINT | NOT NULL | 鐢ㄦ埛ID |
|
||||
| role_id | BIGINT | NOT NULL | 瑙掕壊ID |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||
|
||||
唯一约束:
|
||||
- `UNIQUE (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 |
|
||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||
| org_id | BIGINT | | 组织ID |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| id | BIGSERIAL | PK | 鍏宠仈ID |
|
||||
| user_id | BIGINT | NOT NULL | 鐢ㄦ埛ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||
| org_id | BIGINT | | 缁勭粐ID |
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||
|
||||
索引:
|
||||
- `uk_tenant_user`:`UNIQUE (user_id, tenant_id) WHERE is_deleted = 0`
|
||||
绱㈠紩锛?- `uk_tenant_user`锛歚UNIQUE (user_id, tenant_id) WHERE is_deleted = 0`
|
||||
|
||||
## 2. 权限/字典/参数(全局共享)
|
||||
|
||||
### 2.1 `sys_permission`(权限表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
## 2. 鏉冮檺/瀛楀吀/鍙傛暟锛堝叏灞€鍏变韩锛?
|
||||
### 2.1 `sys_permission`锛堟潈闄愯〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| perm_id | BIGSERIAL | PK | 权限ID |
|
||||
| parent_id | BIGINT | | 父级权限ID |
|
||||
| name | VARCHAR(100) | NOT NULL | 权限名称 |
|
||||
| code | VARCHAR(100) | NOT NULL, UNIQUE | 权限编码 |
|
||||
| perm_type | VARCHAR(20) | NOT NULL | 权限类型 |
|
||||
| level | INTEGER | NOT NULL | 层级 |
|
||||
| path | VARCHAR(255) | | 路径 |
|
||||
| component | VARCHAR(255) | | 组件 |
|
||||
| icon | VARCHAR(100) | | 图标 |
|
||||
| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 排序 |
|
||||
| is_visible | SMALLINT | NOT NULL, DEFAULT 1 | 是否可见 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 状态 |
|
||||
| description | TEXT | | 描述 |
|
||||
| meta | JSONB | | 扩展信息 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 逻辑删除标记 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 创建时间 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 更新时间 |
|
||||
| perm_id | BIGSERIAL | PK | 鏉冮檺ID |
|
||||
| parent_id | BIGINT | | 鐖剁骇鏉冮檺ID |
|
||||
| name | VARCHAR(100) | NOT NULL | 鏉冮檺鍚嶇О |
|
||||
| code | VARCHAR(100) | NOT NULL, UNIQUE | 鏉冮檺缂栫爜 |
|
||||
| perm_type | VARCHAR(20) | NOT NULL | 鏉冮檺绫诲瀷 |
|
||||
| level | INTEGER | NOT NULL | 灞傜骇 |
|
||||
| path | VARCHAR(255) | | 璺緞 |
|
||||
| component | VARCHAR(255) | | 缁勪欢 |
|
||||
| icon | VARCHAR(100) | | 鍥炬爣 |
|
||||
| sort_order | INTEGER | NOT NULL, DEFAULT 0 | 鎺掑簭 |
|
||||
| is_visible | SMALLINT | NOT NULL, DEFAULT 1 | 鏄惁鍙 |
|
||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 鐘舵€?|
|
||||
| description | TEXT | | 鎻忚堪 |
|
||||
| meta | JSONB | | 鎵╁睍淇℃伅 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||
|
||||
### 2.2 `sys_dict_type`(字典类型表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
### 2.2 `sys_dict_type`锛堝瓧鍏哥被鍨嬭〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| dict_type_id | BIGSERIAL | PK | 类型ID |
|
||||
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 类型编码 |
|
||||
| type_name | VARCHAR(50) | NOT NULL | 类型名称 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| remark | TEXT | | 备注 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
| dict_type_id | BIGSERIAL | PK | 绫诲瀷ID |
|
||||
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 绫诲瀷缂栫爜 |
|
||||
| type_name | VARCHAR(50) | NOT NULL | 绫诲瀷鍚嶇О |
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||
| remark | TEXT | | 澶囨敞 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鏇存柊鏃堕棿 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
|
||||
**初始化数据:**
|
||||
- `sys_common_status`: 通用状态 (启用/禁用)
|
||||
- `sys_permission_type`: 权限类型 (目录/菜单/按钮)
|
||||
- `sys_common_visibility`: 可见性 (显示/隐藏)
|
||||
- `sys_permission_level`: 权限层级 (1, 2, 3)
|
||||
- `sys_log_type`: 日志类型 (LOGIN/OPERATION)
|
||||
- `sys_param_type`: 参数类型 (String/Number/Boolean/JSON)
|
||||
- `sys_log_status`: 操作状态 (成功/失败)
|
||||
**鍒濆鍖栨暟鎹細**
|
||||
- `sys_common_status`: 閫氱敤鐘舵€?(鍚敤/绂佺敤)
|
||||
- `sys_permission_type`: 鏉冮檺绫诲瀷 (鐩綍/鑿滃崟/鎸夐挳)
|
||||
- `sys_common_visibility`: 鍙鎬?(鏄剧ず/闅愯棌)
|
||||
- `sys_permission_level`: 鏉冮檺灞傜骇 (1, 2, 3)
|
||||
- `sys_log_type`: 鏃ュ織绫诲瀷 (LOGIN/OPERATION)
|
||||
- `sys_param_type`: 鍙傛暟绫诲瀷 (String/Number/Boolean/JSON)
|
||||
- `sys_log_status`: 鎿嶄綔鐘舵€?(鎴愬姛/澶辫触)
|
||||
|
||||
### 2.3 `sys_dict_item`(字典项表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
### 2.3 `sys_dict_item`锛堝瓧鍏搁」琛級
|
||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| dict_item_id | BIGSERIAL | PK | 字典项ID |
|
||||
| type_code | VARCHAR(50) | NOT NULL | 字典类型编码 |
|
||||
| item_label | VARCHAR(100) | NOT NULL | 展示文本 |
|
||||
| item_value | VARCHAR(100) | NOT NULL | 存储值 |
|
||||
| sort_order | INT | DEFAULT 0 | 排序 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 更新时间 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除标记 |
|
||||
| dict_item_id | BIGSERIAL | PK | 瀛楀吀椤笽D |
|
||||
| type_code | VARCHAR(50) | NOT NULL | 瀛楀吀绫诲瀷缂栫爜 |
|
||||
| item_label | VARCHAR(100) | NOT NULL | 灞曠ず鏂囨湰 |
|
||||
| item_value | VARCHAR(100) | NOT NULL | 瀛樺偍鍊?|
|
||||
| sort_order | INT | DEFAULT 0 | 鎺掑簭 |
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鏇存柊鏃堕棿 |
|
||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||
|
||||
索引:
|
||||
- `idx_dict_item_type`:`(type_code)`
|
||||
- `uk_dict_item_value`:`UNIQUE (type_code, item_value)`
|
||||
绱㈠紩锛?- `idx_dict_item_type`锛歚(type_code)`
|
||||
- `uk_dict_item_value`锛歚UNIQUE (type_code, item_value)`
|
||||
|
||||
### 2.4 `sys_param`(系统参数表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
### 2.4 `sys_param`锛堢郴缁熷弬鏁拌〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 参数ID |
|
||||
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 参数键 |
|
||||
| param_value | TEXT | NOT NULL | 参数值 |
|
||||
| param_type | VARCHAR(20) | NOT NULL | 参数类型 |
|
||||
| is_system | SMALLINT | DEFAULT 0 | 是否系统内置 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| description | TEXT | | 描述 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
| id | BIGSERIAL | PK | 鍙傛暟ID |
|
||||
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 鍙傛暟閿?|
|
||||
| param_value | TEXT | NOT NULL | 鍙傛暟鍊?|
|
||||
| param_type | VARCHAR(20) | NOT NULL | 鍙傛暟绫诲瀷 |
|
||||
| is_system | SMALLINT | DEFAULT 0 | 鏄惁绯荤粺鍐呯疆 |
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||
| description | TEXT | | 鎻忚堪 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
||||
|
||||
## 3. 日志(租户隔离)
|
||||
## 3. 鏃ュ織锛堢鎴烽殧绂伙級
|
||||
|
||||
### 3.1 `sys_log`(系统日志表)
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
### 3.1 `sys_log`锛堢郴缁熸棩蹇楄〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 日志ID |
|
||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 租户ID |
|
||||
| user_id | BIGINT | | 用户ID |
|
||||
| username | VARCHAR(50) | | 用户名 |
|
||||
| log_type | VARCHAR(20) | | 日志类型(如 LOGIN、OPERATION) |
|
||||
| operation | VARCHAR(100) | NOT NULL | 操作描述 |
|
||||
| method | VARCHAR(200) | | 方法 |
|
||||
| params | TEXT | | 请求参数 |
|
||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
||||
| id | BIGSERIAL | PK | 鏃ュ織ID |
|
||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID |
|
||||
| user_id | BIGINT | | 鐢ㄦ埛ID |
|
||||
| username | VARCHAR(50) | | 鐢ㄦ埛鍚?|
|
||||
| log_type | VARCHAR(20) | | 鏃ュ織绫诲瀷锛堝 LOGIN銆丱PERATION锛?|
|
||||
| operation | VARCHAR(100) | NOT NULL | 鎿嶄綔鎻忚堪 |
|
||||
| method | VARCHAR(200) | | 鏂规硶 |
|
||||
| params | TEXT | | 璇锋眰鍙傛暟 |
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?|
|
||||
| ip | VARCHAR(50) | | IP |
|
||||
| duration | BIGINT | | 耗时(ms) |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
| duration | BIGINT | | 鑰楁椂锛坢s锛?|
|
||||
| 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 |
|
||||
| project_name | VARCHAR(128) | NOT NULL | 项目名称 |
|
||||
| id | BIGINT | PK | 鍥哄畾涓?1 |
|
||||
| project_name | VARCHAR(128) | NOT NULL | 椤圭洰鍚嶇О |
|
||||
| logo_url | VARCHAR(512) | | Logo URL |
|
||||
| icon_url | VARCHAR(512) | | Icon URL |
|
||||
| login_bg_url | VARCHAR(512) | | 登录页背景 |
|
||||
| icp_info | VARCHAR(128) | | 备案信息 |
|
||||
| copyright_info | VARCHAR(255) | | 版权信息 |
|
||||
| system_description | TEXT | | 系统描述 |
|
||||
| created_at | TIMESTAMP | NOT NULL | 创建时间 |
|
||||
| updated_at | TIMESTAMP | NOT NULL | 更新时间 |
|
||||
| login_bg_url | VARCHAR(512) | | 鐧诲綍椤佃儗鏅?|
|
||||
| icp_info | VARCHAR(128) | | 澶囨淇℃伅 |
|
||||
| copyright_info | VARCHAR(255) | | 鐗堟潈淇℃伅 |
|
||||
| system_description | TEXT | | 绯荤粺鎻忚堪 |
|
||||
| created_at | TIMESTAMP | NOT NULL | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP | NOT NULL | 鏇存柊鏃堕棿 |
|
||||
|
||||
## 5. 涓氬姟妯″潡
|
||||
|
||||
### 5.1 `biz_speakers`锛堝0绾瑰彂瑷€浜鸿〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||
| creator_id | BIGINT | NOT NULL | 鍒涘缓浜篒D锛岀敤浜庡0绾瑰簱褰掑睘 |
|
||||
| user_id | BIGINT | | 鍏宠仈绯荤粺鐢ㄦ埛ID |
|
||||
| external_speaker_id | VARCHAR(100) | | 绗笁鏂瑰0绾瑰簱涓殑浜哄憳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) | | 澹扮汗鐗瑰緛鍚戦噺 |
|
||||
| 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 | 閫昏緫鍒犻櫎 |
|
||||
|
||||
绱㈠紩锛?- `idx_speaker_tenant`锛歚(tenant_id) WHERE is_deleted = 0`
|
||||
- `idx_speaker_creator`锛歚(creator_id) WHERE is_deleted = 0`
|
||||
- `idx_speaker_user`锛歚(user_id) WHERE is_deleted = 0`
|
||||
- `idx_speaker_external`锛歚(external_speaker_id) WHERE is_deleted = 0`
|
||||
- `uk_speaker_tenant_name`锛歚UNIQUE (tenant_id, name) WHERE is_deleted = 0`
|
||||
|
||||
### 5.2 `biz_hot_word_groups`锛堢儹璇嶇粍琛級
|
||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||
| group_name | VARCHAR(100) | NOT NULL | 鐑瘝缁勫悕绉?|
|
||||
| creator_id | BIGINT | | 鍒涘缓浜篒D |
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€侊紙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 | 閫昏緫鍒犻櫎 |
|
||||
|
||||
绱㈠紩锛?- `idx_hot_word_group_tenant`锛歚(tenant_id) WHERE is_deleted = 0`
|
||||
- `uk_hot_word_group_name_scope`锛歚UNIQUE (tenant_id, group_name) WHERE is_deleted = 0`
|
||||
|
||||
### 5.3 `biz_hot_words`锛堢儹璇嶇鐞嗚〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||
| word | VARCHAR(100) | NOT NULL | 鐑瘝鍘熸枃 |
|
||||
| is_public | SMALLINT | DEFAULT 0 | 鏄惁绉熸埛鍏紑锛?:鍏紑锛?:涓汉绉佹湁锛?|
|
||||
| creator_id | BIGINT | | 鍒涘缓鑰匢D |
|
||||
| pinyin_list | TEXT | | 鎷奸煶鏁扮粍 |
|
||||
| match_strategy | SMALLINT | DEFAULT 1 | 鍖归厤绛栫暐锛?:绮剧‘鍖归厤锛?:鎷奸煶妯$硦鍖归厤锛?|
|
||||
| category | VARCHAR(50) | | 绫诲埆锛堜汉鍚嶃€佹湳璇€佸湴鍚嶏級 |
|
||||
| hot_word_group_id | BIGINT | | 鎵€灞炵儹璇嶇粍ID |
|
||||
| weight | INTEGER | DEFAULT 10 | 鏉冮噸锛?-100锛?|
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€侊紙1:鍚敤锛?:绂佺敤锛?|
|
||||
| is_synced | SMALLINT | DEFAULT 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 | 閫昏緫鍒犻櫎 |
|
||||
|
||||
绱㈠紩锛?- `idx_hotword_tenant`锛歚(tenant_id)`
|
||||
- `idx_hotword_word`锛歚(word) WHERE is_deleted = 0`
|
||||
- `idx_hotword_group`锛歚(hot_word_group_id) WHERE is_deleted = 0`
|
||||
|
||||
### 5.4 `biz_prompt_templates`锛堟彁绀鸿瘝妯℃澘琛級
|
||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID锛? 涓虹郴缁熺骇锛?|
|
||||
| template_name | VARCHAR(100) | NOT NULL | 妯℃澘鍚嶇О |
|
||||
| description | VARCHAR(255) | | 妯℃澘鎻忚堪 |
|
||||
| category | VARCHAR(20) | | 鍒嗙被锛堝瓧鍏革細`biz_prompt_category`锛?|
|
||||
| is_system | SMALLINT | DEFAULT 0 | 鏄惁绯荤粺棰勭疆锛?:鏄紝0:鍚︼級 |
|
||||
| creator_id | BIGINT | | 鍒涘缓浜篒D |
|
||||
| tags | TEXT | | 鏍囩鏁扮粍 |
|
||||
| hot_word_group_id | BIGINT | | 缁戝畾鐑瘝缁処D |
|
||||
| usage_count | INTEGER | DEFAULT 0 | 浣跨敤娆℃暟 |
|
||||
| prompt_content | TEXT | NOT NULL | 鎻愮ず璇嶅唴瀹?|
|
||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€侊紙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 | 閫昏緫鍒犻櫎 |
|
||||
|
||||
绱㈠紩锛?- `idx_prompt_tenant`锛歚(tenant_id)`
|
||||
- `idx_prompt_system`锛歚(is_system) WHERE is_deleted = 0`
|
||||
- `idx_prompt_group`锛歚(hot_word_group_id) WHERE is_deleted = 0`
|
||||
|
||||
### 5.5 `biz_asr_models`锛圓SR 妯″瀷閰嶇疆琛級
|
||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID |
|
||||
| model_name | VARCHAR(100) | NOT NULL | 妯″瀷鏄剧ず鍚嶇О |
|
||||
| provider | VARCHAR(50) | | 鎻愪緵鍟?|
|
||||
| base_url | VARCHAR(255) | | 鎺ュ彛鍩虹鍦板潃 |
|
||||
| api_key | VARCHAR(255) | | API 瀵嗛挜 |
|
||||
| model_code | VARCHAR(100) | | 妯″瀷浠g爜 |
|
||||
| ws_url | VARCHAR(255) | | WebSocket 鍦板潃 |
|
||||
| media_config | TEXT | | 濯掍綋鍙傛暟 |
|
||||
| is_default | SMALLINT | DEFAULT 0 | 榛樿妯″瀷鏍囪 |
|
||||
| sort_order | INTEGER | NOT NULL, 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 | 閫昏緫鍒犻櫎 |
|
||||
|
||||
绱㈠紩锛?- `idx_asr_model_tenant`锛歚(tenant_id)`
|
||||
- `idx_asr_model_default`锛歚(is_default) WHERE is_deleted = 0`
|
||||
- `idx_asr_model_sort_order`锛歚(tenant_id, is_default, sort_order) WHERE is_deleted = 0`
|
||||
- `uk_asr_model_default_enabled_tenant`锛歚(tenant_id) WHERE is_deleted = 0 AND status = 1 AND is_default = 1`
|
||||
|
||||
### 5.6 `biz_llm_models`锛圠LM 妯″瀷閰嶇疆琛級
|
||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛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) | | 妯″瀷浠g爜 |
|
||||
| temperature | DECIMAL(3,2) | DEFAULT 0.7 | 娓╁害鍙傛暟 |
|
||||
| top_p | DECIMAL(3,2) | DEFAULT 0.9 | Top P 鍙傛暟 |
|
||||
| is_default | SMALLINT | DEFAULT 0 | 榛樿妯″瀷鏍囪 |
|
||||
| sort_order | INTEGER | NOT NULL, 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 | 閫昏緫鍒犻櫎 |
|
||||
|
||||
绱㈠紩锛?- `idx_llm_model_tenant`锛歚(tenant_id)`
|
||||
- `idx_llm_model_default`锛歚(is_default) WHERE is_deleted = 0`
|
||||
- `idx_llm_model_sort_order`锛歚(tenant_id, is_default, sort_order) WHERE is_deleted = 0`
|
||||
- `uk_llm_model_default_enabled_tenant`锛歚(tenant_id) WHERE is_deleted = 0 AND status = 1 AND is_default = 1`
|
||||
|
||||
### 5.7 `biz_meetings`锛堜細璁富琛級
|
||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID |
|
||||
| title | VARCHAR(200) | NOT NULL | 浼氳鏍囬 |
|
||||
| meeting_time | TIMESTAMP(6) | | 浼氳鏃堕棿 |
|
||||
| participants | TEXT | | 鍙備細浜轰俊鎭?|
|
||||
| tags | VARCHAR(255) | | 鏍囩 |
|
||||
| audio_url | VARCHAR(500) | | 涓撳睘闊抽璺緞 |
|
||||
| meeting_type | VARCHAR(32) | | 会议类型:OFFLINE / REALTIME |
|
||||
| meeting_source | VARCHAR(32) | | 会议来源平台:WEB / ANDROID |
|
||||
| creator_id | BIGINT | | 鍙戣捣浜篒D |
|
||||
| creator_name | VARCHAR(100) | | 鍙戣捣浜哄鍚?|
|
||||
| latest_summary_task_id | BIGINT | | 鏈€鏂版垚鍔熸€荤粨浠诲姟ID |
|
||||
| status | SMALLINT | DEFAULT 0 | 鐘舵€侊紙0:寰呭鐞嗭紝1:澶勭悊涓紝2:鎴愬姛锛?:澶辫触锛?|
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
||||
|
||||
绱㈠紩锛?- `idx_meeting_tenant`锛歚(tenant_id)`
|
||||
|
||||
### 5.8 `biz_meeting_transcripts`锛堣浆褰曟槑缁嗚〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
|
||||
| speaker_id | VARCHAR(50) | | ASR 杩斿洖鐨勫彂瑷€浜烘爣璇?|
|
||||
| speaker_name | VARCHAR(100) | | 淇敼鍚庣殑鍙戣█浜哄鍚?|
|
||||
| speaker_label | VARCHAR(50) | | 鍙戣█浜烘爣绛?|
|
||||
| content | TEXT | | 杞綍鍐呭 |
|
||||
| start_time | INTEGER | | 寮€濮嬫椂闂达紙ms锛?|
|
||||
| end_time | INTEGER | | 缁撴潫鏃堕棿锛坢s锛?|
|
||||
| sort_order | INTEGER | | 鎺掑簭 |
|
||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||
|
||||
绱㈠紩锛?- `idx_transcript_meeting`锛歚(meeting_id)`
|
||||
|
||||
### 5.9 `biz_ai_tasks`锛圓I 浠诲姟娴佹按琛級
|
||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
## 6. 会议积分模式增量
|
||||
|
||||
### 6.1 `biz_meetings` 增量字段
|
||||
- `effective_audio_duration_seconds`
|
||||
- 类型:`INTEGER`
|
||||
- 说明:会议最终有效录音时长(秒),作为会议统计与积分计费统一口径。
|
||||
|
||||
### 6.2 `biz_meeting_points_accounts`
|
||||
- 用途:当前版本按租户维护统一积分余额与累计消耗。
|
||||
- 关键字段:
|
||||
- `tenant_id`
|
||||
- `user_id`
|
||||
- `0` 表示公共账户
|
||||
- 非 `0` 表示个人账户
|
||||
- `current_balance`
|
||||
- `total_points_used`
|
||||
- `total_asr_points_used`
|
||||
- `total_llm_points_used`
|
||||
- 关键索引:
|
||||
- `uk_biz_meeting_points_accounts_tenant_user`
|
||||
|
||||
### 6.3 `biz_meeting_summary_charge_records`
|
||||
- 用途:每次 SUMMARY 任务保留一条计费快照记录,并按 ASR / LLM 成功节点累计实际扣费。
|
||||
- 关键字段:
|
||||
- `meeting_id`
|
||||
- `summary_task_id`
|
||||
- `user_id`
|
||||
- 记录所属会议 owner / 创建人
|
||||
- `audio_duration_seconds`
|
||||
- `charged_minutes`
|
||||
- `billing_units`
|
||||
- `unit_minutes_snapshot`
|
||||
- `cost_per_unit_snapshot`
|
||||
- `total_points`
|
||||
- 当前记录应计总积分;重新总结场景仅记录 LLM 应计积分
|
||||
- `charged_total_points`
|
||||
- `asr_points`
|
||||
- `charged_asr_points`
|
||||
- `llm_points`
|
||||
- `charged_llm_points`
|
||||
- `asr_ratio_snapshot`
|
||||
- `llm_ratio_snapshot`
|
||||
- `balance_before`
|
||||
- `balance_after`
|
||||
- `points_delta`
|
||||
- `charge_trigger_type`
|
||||
- `summary_status`
|
||||
- `points_mode_enabled`
|
||||
- `failure_reason`
|
||||
- `charged_at`
|
||||
- `asr_charged_at`
|
||||
- `llm_charged_at`
|
||||
- 关键索引:
|
||||
- `idx_biz_meeting_summary_charge_records_meeting`
|
||||
- `idx_biz_meeting_summary_charge_records_user`
|
||||
- `idx_biz_meeting_summary_charge_records_task`
|
||||
|
||||
### 6.5 `biz_meeting_points_ledgers`
|
||||
- 用途:实际发生积分变化时记录 ASR / LLM / INIT / RECHARGE 流水。
|
||||
- 关键字段:
|
||||
- `user_id`
|
||||
- `0` 表示公共账户
|
||||
- 非 `0` 表示个人账户
|
||||
- `meeting_id`
|
||||
- `summary_task_id`
|
||||
- `charge_record_id`
|
||||
- `points_delta`
|
||||
- `points_type`
|
||||
- `balance_before`
|
||||
- `balance_after`
|
||||
- `remark`
|
||||
|
||||
### 6.6 当前计费口径
|
||||
- 当前支持两种扣费账户:
|
||||
- `PUBLIC`:租户公共账户
|
||||
- `PERSONAL`:会议 owner / 创建人的个人账户
|
||||
- 当前优先扣费账户由系统参数 `meeting.points.account_mode` 控制:
|
||||
- `PUBLIC`
|
||||
- `PERSONAL`
|
||||
- 自动总结:
|
||||
- `ASR` 成功后扣减 `ASR` 比例积分
|
||||
- `LLM / SUMMARY` 成功后扣减 `LLM` 比例积分
|
||||
- 重新总结:
|
||||
- 只在 `LLM / SUMMARY` 成功后扣减 `LLM` 比例积分
|
||||
- 失败不扣费:
|
||||
- `ASR` 失败不扣 `ASR`
|
||||
- `SUMMARY` 失败不扣 `LLM`
|
||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
|
||||
| task_type | VARCHAR(20) | | 浠诲姟绫诲瀷锛圓SR / SUMMARY锛?|
|
||||
| status | SMALLINT | DEFAULT 0 | 鐘舵€侊紙0:鎺掗槦锛?:鎵ц涓紝2:鎴愬姛锛?:澶辫触锛?|
|
||||
| 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) | | 瀹屾垚鏃堕棿 |
|
||||
|
||||
绱㈠紩锛?- `idx_aitask_meeting`锛歚(meeting_id)`
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
-- PostgreSQL Database Schema for iMeeting (Multi-tenant)
|
||||
-- PostgreSQL Database Schema for iMeeting (Multi-tenant)
|
||||
-- 0 为系统预留租户 ID
|
||||
|
||||
-- ----------------------------
|
||||
-- 0. 租户与组织
|
||||
-- ----------------------------
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
-- 租户表
|
||||
CREATE TABLE sys_tenant (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
|
@ -19,7 +19,7 @@ CREATE TABLE sys_tenant (
|
|||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
is_deleted SMALLINT DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX uk_tenant_code ON sys_tenant (tenant_code) WHERE is_deleted = 0;
|
||||
CREATE INDEX uk_tenant_code ON sys_tenant (tenant_code) WHERE is_deleted = 0;
|
||||
|
||||
-- 组织架构表
|
||||
DROP TABLE IF EXISTS sys_org CASCADE;
|
||||
|
|
@ -55,6 +55,7 @@ CREATE TABLE sys_user (
|
|||
user_id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
display_name VARCHAR(50) NOT NULL,
|
||||
avatar_url VARCHAR(500),
|
||||
email VARCHAR(100),
|
||||
phone VARCHAR(30) UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
|
|
@ -75,6 +76,7 @@ CREATE TABLE sys_role (
|
|||
tenant_id BIGINT NOT NULL,
|
||||
role_code VARCHAR(50) NOT NULL,
|
||||
role_name VARCHAR(50) NOT NULL,
|
||||
data_scope_type VARCHAR(32) NOT NULL DEFAULT 'SELF',
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
remark TEXT,
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
|
|
@ -83,7 +85,20 @@ CREATE TABLE sys_role (
|
|||
);
|
||||
|
||||
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;
|
||||
|
||||
DROP TABLE IF EXISTS sys_role_data_scope_org CASCADE;
|
||||
|
||||
CREATE TABLE sys_role_data_scope_org (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
org_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_role_data_scope_role_id ON sys_role_data_scope_org (role_id);
|
||||
CREATE INDEX idx_role_data_scope_org_id ON sys_role_data_scope_org (org_id);
|
||||
|
||||
-- 用户-角色关联表 (按 tenant_id 强约束,避免跨租户角色污染)
|
||||
DROP TABLE IF EXISTS sys_user_role CASCADE;
|
||||
|
|
@ -109,7 +124,7 @@ CREATE TABLE sys_permission (
|
|||
perm_id BIGSERIAL PRIMARY KEY,
|
||||
parent_id BIGINT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
code VARCHAR(100) NOT NULL UNIQUE,
|
||||
code VARCHAR(100) NOT NULL ,
|
||||
perm_type VARCHAR(20) NOT NULL,
|
||||
level INTEGER NOT NULL,
|
||||
path VARCHAR(255),
|
||||
|
|
@ -138,7 +153,7 @@ CREATE TABLE sys_tenant_user (
|
|||
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)
|
||||
WHERE is_deleted = 0;
|
||||
CREATE TABLE sys_dict_type (
|
||||
|
|
@ -161,13 +176,14 @@ CREATE TABLE sys_dict_item (
|
|||
status SMALLINT DEFAULT 1,
|
||||
created_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 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 (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
param_id BIGSERIAL PRIMARY KEY,
|
||||
param_key VARCHAR(100) UNIQUE NOT NULL,
|
||||
param_value TEXT NOT NULL,
|
||||
param_type VARCHAR(20) NOT NULL,
|
||||
|
|
@ -179,6 +195,36 @@ CREATE TABLE sys_param (
|
|||
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()
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS sys_bot_credential CASCADE;
|
||||
|
||||
CREATE TABLE sys_bot_credential (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
bot_id VARCHAR(64) NOT NULL,
|
||||
secret_hash VARCHAR(64) NOT NULL,
|
||||
secret_salt VARCHAR(32),
|
||||
user_id BIGINT NOT NULL,
|
||||
status CHAR(1) NOT NULL DEFAULT '0',
|
||||
expire_time TIMESTAMP(6),
|
||||
last_access_time TIMESTAMP(6),
|
||||
last_access_ip VARCHAR(128),
|
||||
remark VARCHAR(500),
|
||||
create_by VARCHAR(64),
|
||||
create_time TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64),
|
||||
update_time TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_sys_bot_credential_bot_id ON sys_bot_credential (bot_id);
|
||||
CREATE INDEX idx_sys_bot_credential_user_id ON sys_bot_credential (user_id);
|
||||
-- ----------------------------
|
||||
-- 3. 日志 (租户隔离)
|
||||
-- ----------------------------
|
||||
|
|
@ -188,7 +234,10 @@ CREATE TABLE sys_log (
|
|||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT,
|
||||
username VARCHAR(50),
|
||||
source_system VARCHAR(64),
|
||||
log_type VARCHAR(20), -- LOGIN, OPERATION
|
||||
module_name VARCHAR(100),
|
||||
action_name VARCHAR(100),
|
||||
operation VARCHAR(100) NOT NULL,
|
||||
method VARCHAR(200),
|
||||
params TEXT,
|
||||
|
|
@ -198,6 +247,9 @@ CREATE TABLE sys_log (
|
|||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_log_tenant_type ON sys_log (tenant_id, log_type, created_at);
|
||||
CREATE INDEX idx_log_tenant_type_module_time ON sys_log (tenant_id, log_type, module_name, created_at);
|
||||
CREATE INDEX idx_log_tenant_type_user_time ON sys_log (tenant_id, log_type, username, created_at);
|
||||
CREATE INDEX idx_log_tenant_type_source_time ON sys_log (tenant_id, log_type, source_system, created_at);
|
||||
|
||||
-- ----------------------------
|
||||
-- 4. 平台配置 (系统品牌化)
|
||||
|
|
@ -217,49 +269,478 @@ CREATE TABLE sys_platform_config (
|
|||
is_deleted SMALLINT DEFAULT 0
|
||||
);
|
||||
|
||||
INSERT INTO sys_platform_config (id, project_name, copyright_info)
|
||||
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,
|
||||
tenant_id BIGINT NOT NULL, -- 租户ID
|
||||
creator_id BIGINT NOT NULL, -- 创建人ID,用于声纹库管理归属
|
||||
user_id BIGINT, -- 关联系统用户ID,可为空
|
||||
external_speaker_id VARCHAR(100), -- 第三方声纹库中的人员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_tenant ON biz_speakers (tenant_id) WHERE is_deleted = 0;
|
||||
CREATE INDEX idx_speaker_creator ON biz_speakers (creator_id) WHERE is_deleted = 0;
|
||||
CREATE INDEX idx_speaker_user ON biz_speakers (user_id) WHERE is_deleted = 0;
|
||||
CREATE INDEX idx_speaker_external ON biz_speakers (external_speaker_id) WHERE is_deleted = 0;
|
||||
CREATE UNIQUE INDEX uk_speaker_tenant_name ON biz_speakers (tenant_id, name) WHERE is_deleted = 0;
|
||||
|
||||
COMMENT ON TABLE biz_speakers IS '声纹发言人基础信息表 (声纹库资源)';
|
||||
|
||||
-- ----------------------------
|
||||
-- 7. 业务模块 - 热词管理
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS biz_hot_word_groups CASCADE;
|
||||
CREATE TABLE biz_hot_word_groups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL, -- 租户ID
|
||||
group_name VARCHAR(100) NOT NULL, -- 热词组名称
|
||||
creator_id BIGINT, -- 创建人ID
|
||||
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_hot_word_group_tenant ON biz_hot_word_groups (tenant_id) WHERE is_deleted = 0;
|
||||
CREATE UNIQUE INDEX uk_hot_word_group_name_scope ON biz_hot_word_groups (tenant_id, group_name) WHERE is_deleted = 0;
|
||||
|
||||
COMMENT ON TABLE biz_hot_word_groups IS '热词组表';
|
||||
|
||||
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), -- 类别 (人名、术语、地名)
|
||||
hot_word_group_id BIGINT, -- 所属热词组 ID
|
||||
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;
|
||||
CREATE INDEX idx_hotword_group ON biz_hot_words (hot_word_group_id) 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, -- 模板名称
|
||||
description VARCHAR(255), -- 模板描述
|
||||
category VARCHAR(20), -- 分类 (字典: biz_prompt_category)
|
||||
is_system SMALLINT DEFAULT 0, -- 是否系统预置 (1:是, 0:否)
|
||||
creator_id BIGINT, -- 创建人ID
|
||||
tags text, -- 标签数组 (JSONB)
|
||||
hot_word_group_id BIGINT, -- 绑定热词组 ID
|
||||
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;
|
||||
CREATE INDEX idx_prompt_group ON biz_prompt_templates (hot_word_group_id) 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,
|
||||
sort_order INTEGER NOT NULL 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,
|
||||
max_tokens BIGINT NOT NULL DEFAULT 30000,
|
||||
is_default SMALLINT DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL 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_asr_model_sort_order ON biz_asr_models (tenant_id, is_default, sort_order) WHERE is_deleted = 0;
|
||||
CREATE UNIQUE INDEX uk_asr_model_default_enabled_tenant ON biz_asr_models (tenant_id) WHERE is_deleted = 0 AND status = 1 AND is_default = 1;
|
||||
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;
|
||||
CREATE INDEX idx_llm_model_sort_order ON biz_llm_models (tenant_id, is_default, sort_order) WHERE is_deleted = 0;
|
||||
CREATE UNIQUE INDEX uk_llm_model_default_enabled_tenant ON biz_llm_models (tenant_id) WHERE is_deleted = 0 AND status = 1 AND is_default = 1;
|
||||
|
||||
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),
|
||||
meeting_type VARCHAR(32), -- OFFLINE / REALTIME
|
||||
meeting_source VARCHAR(32), -- WEB / ANDROID
|
||||
creator_id BIGINT, -- 发起人ID
|
||||
creator_name VARCHAR(100), -- 发起人姓名
|
||||
host_user_id BIGINT, -- 主持人用户ID
|
||||
host_name VARCHAR(100), -- 主持人展示名称
|
||||
access_password VARCHAR(128), -- 兼容旧版安卓预览访问的会议访问密码
|
||||
latest_summary_task_id BIGINT, -- 最新成功总结任务ID
|
||||
audio_save_status VARCHAR(20) DEFAULT 'NONE', -- 实时音频保存状态:NONE/SUCCESS/FAILED
|
||||
audio_save_message VARCHAR(500), -- 实时音频保存失败提示信息
|
||||
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 任务流水日志表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 13. 会议积分模式增量结构
|
||||
-- ----------------------------
|
||||
ALTER TABLE biz_meetings
|
||||
ADD COLUMN IF NOT EXISTS effective_audio_duration_seconds INTEGER;
|
||||
|
||||
COMMENT ON COLUMN biz_meetings.effective_audio_duration_seconds IS '会议最终有效录音时长(秒),用于统计与计费口径';
|
||||
|
||||
DROP TABLE IF EXISTS biz_meeting_points_accounts CASCADE;
|
||||
CREATE TABLE biz_meeting_points_accounts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
user_id BIGINT NOT NULL,
|
||||
current_balance BIGINT NOT NULL DEFAULT 0,
|
||||
total_points_used BIGINT NOT NULL DEFAULT 0,
|
||||
total_asr_points_used BIGINT NOT NULL DEFAULT 0,
|
||||
total_llm_points_used BIGINT NOT NULL DEFAULT 0,
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_biz_meeting_points_accounts_tenant_user
|
||||
ON biz_meeting_points_accounts (tenant_id, user_id);
|
||||
|
||||
COMMENT ON TABLE biz_meeting_points_accounts IS '会议积分账户表';
|
||||
COMMENT ON COLUMN biz_meeting_points_accounts.user_id IS '0表示公共账户,非0表示个人账户';
|
||||
|
||||
DROP TABLE IF EXISTS biz_meeting_summary_charge_records CASCADE;
|
||||
CREATE TABLE biz_meeting_summary_charge_records (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
meeting_id BIGINT NOT NULL,
|
||||
summary_task_id BIGINT,
|
||||
user_id BIGINT NOT NULL,
|
||||
audio_duration_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
charged_minutes INTEGER NOT NULL DEFAULT 0,
|
||||
billing_units INTEGER NOT NULL DEFAULT 0,
|
||||
unit_minutes_snapshot INTEGER NOT NULL DEFAULT 1,
|
||||
cost_per_unit_snapshot INTEGER NOT NULL DEFAULT 0,
|
||||
total_points BIGINT NOT NULL DEFAULT 0,
|
||||
asr_points BIGINT NOT NULL DEFAULT 0,
|
||||
llm_points BIGINT NOT NULL DEFAULT 0,
|
||||
asr_ratio_snapshot INTEGER NOT NULL DEFAULT 0,
|
||||
llm_ratio_snapshot INTEGER NOT NULL DEFAULT 0,
|
||||
balance_before BIGINT,
|
||||
balance_after BIGINT,
|
||||
points_delta BIGINT NOT NULL DEFAULT 0,
|
||||
charge_trigger_type VARCHAR(32) NOT NULL,
|
||||
summary_status VARCHAR(32) NOT NULL DEFAULT 'CREATED',
|
||||
points_mode_enabled SMALLINT NOT NULL DEFAULT 0,
|
||||
blocked_reason VARCHAR(64),
|
||||
failure_reason VARCHAR(500),
|
||||
charged_at TIMESTAMP(6),
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_biz_meeting_summary_charge_records_meeting
|
||||
ON biz_meeting_summary_charge_records (meeting_id);
|
||||
|
||||
CREATE INDEX idx_biz_meeting_summary_charge_records_user
|
||||
ON biz_meeting_summary_charge_records (user_id);
|
||||
|
||||
CREATE INDEX idx_biz_meeting_summary_charge_records_task
|
||||
ON biz_meeting_summary_charge_records (summary_task_id);
|
||||
|
||||
COMMENT ON TABLE biz_meeting_summary_charge_records IS '会议总结消耗记录表';
|
||||
COMMENT ON COLUMN biz_meeting_summary_charge_records.total_points IS '本次记录应计总积分,重新总结场景仅记录LLM应计积分';
|
||||
|
||||
DROP TABLE IF EXISTS biz_meeting_points_ledgers CASCADE;
|
||||
CREATE TABLE biz_meeting_points_ledgers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
user_id BIGINT NOT NULL,
|
||||
meeting_id BIGINT,
|
||||
summary_task_id BIGINT,
|
||||
charge_record_id BIGINT,
|
||||
points_delta BIGINT NOT NULL,
|
||||
points_type VARCHAR(32) NOT NULL,
|
||||
balance_before BIGINT NOT NULL,
|
||||
balance_after BIGINT NOT NULL,
|
||||
remark VARCHAR(500),
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_biz_meeting_points_ledgers_user
|
||||
ON biz_meeting_points_ledgers (user_id);
|
||||
|
||||
CREATE INDEX idx_biz_meeting_points_ledgers_meeting
|
||||
ON biz_meeting_points_ledgers (meeting_id);
|
||||
|
||||
COMMENT ON TABLE biz_meeting_points_ledgers IS '会议积分流水表';
|
||||
COMMENT ON COLUMN biz_meeting_points_ledgers.user_id IS '0表示公共账户,非0表示个人账户';
|
||||
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
|
||||
);
|
||||
|
||||
-- ----------------------------
|
||||
-- 13. 业务模块 - 旧版安卓兼容
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS biz_client_downloads CASCADE;
|
||||
CREATE TABLE biz_client_downloads (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
platform_type VARCHAR(32), -- 平台类型
|
||||
platform_name VARCHAR(64), -- 平台显示名称
|
||||
platform_code VARCHAR(64) NOT NULL, -- 平台编码,如 android / ios / windows
|
||||
version VARCHAR(64) NOT NULL, -- 版本名称
|
||||
version_code BIGINT, -- 版本号
|
||||
download_url VARCHAR(512) NOT NULL, -- 下载地址
|
||||
file_size BIGINT, -- 文件大小
|
||||
release_notes TEXT, -- 发布说明
|
||||
is_latest SMALLINT NOT NULL DEFAULT 0, -- 是否当前平台最新版本:1-是,0-否
|
||||
min_system_version VARCHAR(64), -- 最低系统版本要求
|
||||
created_by BIGINT, -- 创建人
|
||||
status SMALLINT NOT NULL 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_client_downloads_platform_code
|
||||
ON biz_client_downloads (platform_code);
|
||||
|
||||
CREATE INDEX idx_client_downloads_latest
|
||||
ON biz_client_downloads (platform_code, is_latest)
|
||||
WHERE is_deleted = 0;
|
||||
|
||||
COMMENT ON TABLE biz_client_downloads IS '旧版安卓客户端版本兼容表';
|
||||
|
||||
DROP TABLE IF EXISTS biz_external_apps CASCADE;
|
||||
CREATE TABLE biz_external_apps (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
app_name VARCHAR(128) NOT NULL, -- 应用名称
|
||||
app_type VARCHAR(32) NOT NULL, -- 应用类型:native / web
|
||||
app_info JSONB, -- 应用附加信息,如 web_url / package_name / apk_url
|
||||
icon_url VARCHAR(512), -- 图标地址
|
||||
description VARCHAR(255), -- 描述
|
||||
sort_order INTEGER NOT NULL DEFAULT 0, -- 排序值
|
||||
created_by BIGINT, -- 创建人
|
||||
status SMALLINT NOT NULL 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_external_apps_status_sort
|
||||
ON biz_external_apps (status, sort_order);
|
||||
|
||||
COMMENT ON TABLE biz_external_apps IS '旧版安卓首页外部应用兼容表';
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- 5. 基础初始化数据
|
||||
-- ----------------------------
|
||||
|
||||
-- 字典初始化数据
|
||||
-- sys_common_status
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_common_status', '通用状态', '0=禁用, 1=启用');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_status', '启用', '1', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_status', '禁用', '0', 2);
|
||||
|
||||
-- sys_permission_type
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_permission_type', '权限类型', 'directory=目录, menu=菜单, button=按钮');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '目录', 'directory', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '菜单', 'menu', 2);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_type', '按钮', 'button', 3);
|
||||
-- ----------------------------
|
||||
-- 6. 屏保模块
|
||||
-- ----------------------------
|
||||
|
||||
-- sys_common_visibility
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_common_visibility', '可见性', '0=隐藏, 1=显示');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_visibility', '显示', '1', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_common_visibility', '隐藏', '0', 2);
|
||||
CREATE TABLE biz_screen_savers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
scope_type VARCHAR(32) NOT NULL DEFAULT 'PLATFORM',
|
||||
owner_user_id BIGINT,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
image_url VARCHAR(512) NOT NULL,
|
||||
description VARCHAR(255),
|
||||
display_duration_sec INTEGER NOT NULL DEFAULT 15,
|
||||
image_width INTEGER,
|
||||
image_height INTEGER,
|
||||
image_format VARCHAR(16),
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_by BIGINT,
|
||||
status SMALLINT NOT NULL 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
|
||||
);
|
||||
|
||||
-- sys_permission_level
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_permission_level', '权限层级', '1=一级入口, 2=二级子项, 3=三级按钮');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '一级入口', '1', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '二级子项', '2', 2);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_permission_level', '三级按钮', '3', 3);
|
||||
CREATE INDEX idx_screen_savers_status_sort
|
||||
ON biz_screen_savers (status, sort_order);
|
||||
|
||||
-- sys_log_type
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_type', '日志类型', 'LOGIN=登录, OPERATION=操作');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_type', '登录', 'LOGIN', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_type', '操作', 'OPERATION', 2);
|
||||
CREATE INDEX idx_screen_savers_scope_owner_status_sort
|
||||
ON biz_screen_savers (scope_type, owner_user_id, status, sort_order);
|
||||
|
||||
-- sys_param_type
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_param_type', '参数类型', 'String, Number, Boolean, JSON');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'String', 'String', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'Number', 'Number', 2);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'Boolean', 'Boolean', 3);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_param_type', 'JSON', 'JSON', 4);
|
||||
CREATE TABLE biz_screen_saver_user_config (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT NOT NULL,
|
||||
screen_saver_id BIGINT NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_screen_saver_user_cfg_user_item
|
||||
ON biz_screen_saver_user_config (tenant_id, user_id, screen_saver_id)
|
||||
WHERE is_deleted = 0;
|
||||
|
||||
CREATE INDEX idx_screen_saver_user_cfg_item
|
||||
ON biz_screen_saver_user_config (screen_saver_id)
|
||||
WHERE is_deleted = 0;
|
||||
|
||||
CREATE TABLE biz_screen_saver_user_settings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT NOT NULL,
|
||||
display_duration_sec INTEGER NOT NULL DEFAULT 15,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||
is_deleted SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_screen_saver_user_settings_user
|
||||
ON biz_screen_saver_user_settings (tenant_id, user_id)
|
||||
WHERE is_deleted = 0;
|
||||
|
||||
-- sys_log_status
|
||||
INSERT INTO sys_dict_type (type_code, type_name, remark) VALUES ('sys_log_status', '操作状态', '1=成功, 0=失败');
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '成功', '1', 1);
|
||||
INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order) VALUES ('sys_log_status', '失败', '0', 2);
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
|
||||
126
backend/pom.xml
126
backend/pom.xml
|
|
@ -1,4 +1,4 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
|
@ -20,6 +20,10 @@
|
|||
<mybatis-plus.version>3.5.6</mybatis-plus.version>
|
||||
<jjwt.version>0.11.5</jjwt.version>
|
||||
<easycaptcha.version>1.6.2</easycaptcha.version>
|
||||
<grpc.version>1.76.1</grpc.version>
|
||||
<protobuf.version>3.25.8</protobuf.version>
|
||||
<protobuf.plugin.version>0.6.1</protobuf.plugin.version>
|
||||
<os.maven.plugin.version>1.7.1</os.maven.plugin.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
|
@ -27,6 +31,10 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
|
|
@ -47,6 +55,10 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
|
|
@ -79,6 +91,38 @@
|
|||
<artifactId>easy-captcha</artifactId>
|
||||
<version>${easycaptcha.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.38</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.belerweb</groupId>
|
||||
<artifactId>pinyin4j</artifactId>
|
||||
<version>2.5.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-services</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
|
|
@ -89,10 +133,90 @@
|
|||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
<!-- Source: https://mvnrepository.com/artifact/com.tencentcloudapi/tencentcloud-speech-sdk-java -->
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-speech-sdk-java</artifactId>
|
||||
<version>1.0.67</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-asr</artifactId>
|
||||
<version>3.1.1470</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>${os.maven.plugin.version}</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>${protobuf.plugin.version}</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>compile-custom</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package com.imeeting;
|
|||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class ImeetingApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ImeetingApplication.class, args);
|
||||
|
|
|
|||
|
|
@ -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("系统异常");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.imeeting.common;
|
||||
|
||||
public final class LicenseConstants {
|
||||
private LicenseConstants() {
|
||||
}
|
||||
|
||||
public static final String TEMP_SERIAL_PREFIX = "SN";
|
||||
public static final String IMPORT_BATCH_PREFIX = "LIC";
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.imeeting.common;
|
||||
|
||||
public final class MeetingConstants {
|
||||
public static final String TYPE_OFFLINE = "OFFLINE";
|
||||
public static final String TYPE_REALTIME = "REALTIME";
|
||||
|
||||
public static final String SOURCE_WEB = "WEB";
|
||||
public static final String SOURCE_ANDROID = "ANDROID";
|
||||
|
||||
public static final String DEVICE_MODE_PUBLIC = "PUBLIC";
|
||||
public static final String DEVICE_MODE_PRIVATE = "PRIVATE";
|
||||
|
||||
public static final String DEVICE_DELIVERY_NONE = "NONE";
|
||||
public static final String DEVICE_DELIVERY_PENDING = "PENDING";
|
||||
public static final String DEVICE_DELIVERY_ACKED = "ACKED";
|
||||
public static final String DEVICE_DELIVERY_EXPIRED = "EXPIRED";
|
||||
public static final String DEVICE_DELIVERY_CANCELLED = "CANCELLED";
|
||||
|
||||
public static final String OFFLINE_RECORDING_ACTIVE = "ACTIVE";
|
||||
public static final String OFFLINE_RECORDING_PRE_END = "PRE_END";
|
||||
public static final String OFFLINE_RECORDING_UPLOAD_FINISHED = "UPLOAD_FINISHED";
|
||||
|
||||
public static final String SUMMARY_DETAIL_DETAILED = "DETAILED";
|
||||
public static final String SUMMARY_DETAIL_STANDARD = "STANDARD";
|
||||
public static final String SUMMARY_DETAIL_BRIEF = "BRIEF";
|
||||
|
||||
private MeetingConstants() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.imeeting.common;
|
||||
|
||||
public enum MeetingProgressStage {
|
||||
QUEUED("queued", 10, false),
|
||||
ASR_SUBMITTED("asr_submitted", 20, false),
|
||||
ASR_RUNNING("asr_running", 30, false),
|
||||
ASR_COMPLETED("asr_completed", 40, false),
|
||||
CHAPTER_RUNNING("chapter_running", 50, false),
|
||||
SUMMARY_RUNNING("summary_running", 60, false),
|
||||
COMPLETED("completed", 100, true),
|
||||
FAILED("failed", 100, true);
|
||||
|
||||
private final String code;
|
||||
private final int order;
|
||||
private final boolean terminal;
|
||||
|
||||
MeetingProgressStage(String code, int order, boolean terminal) {
|
||||
this.code = code;
|
||||
this.order = order;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public int getOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public boolean isTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,113 @@ public final class RedisKeys {
|
|||
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 String meetingSummaryLockKey(Long meetingId) {
|
||||
return "biz:meeting:summary:lock:" + meetingId;
|
||||
}
|
||||
|
||||
public static String meetingAsrScheduleLockKey() {
|
||||
return "biz:meeting:asr:schedule:lock";
|
||||
}
|
||||
|
||||
public static String meetingAsrPermitSetKey(String queueKey) {
|
||||
return "biz:meeting:asr:permit:set:" + normalizeQueueKey(queueKey);
|
||||
}
|
||||
|
||||
public static String meetingAsrPermitQueueKey(Long meetingId) {
|
||||
return "biz:meeting:asr:permit:meeting:" + meetingId;
|
||||
}
|
||||
|
||||
public static String meetingAsrPermitSyncLockKey() {
|
||||
return "biz:meeting:asr:permit:sync:lock";
|
||||
}
|
||||
|
||||
public static String meetingAsrRefillLockKey() {
|
||||
return "biz:meeting:asr:refill:lock";
|
||||
}
|
||||
|
||||
public static String realtimeMeetingSocketSessionKey(String sessionToken) {
|
||||
return "biz:meeting:realtime:socket:" + sessionToken;
|
||||
}
|
||||
|
||||
public static String realtimeMeetingSessionStateKey(Long meetingId) {
|
||||
return "biz:meeting:realtime:state:" + meetingId;
|
||||
}
|
||||
|
||||
public static String realtimeMeetingTranscriptCacheKey(Long meetingId) {
|
||||
return "biz:meeting:realtime:transcript-cache:" + meetingId;
|
||||
}
|
||||
|
||||
public static String realtimeMeetingResumeTimeoutKey(Long meetingId) {
|
||||
return realtimeMeetingResumeTimeoutPrefix() + meetingId;
|
||||
}
|
||||
|
||||
public static String realtimeMeetingEmptyTimeoutKey(Long meetingId) {
|
||||
return realtimeMeetingEmptyTimeoutPrefix() + meetingId;
|
||||
}
|
||||
|
||||
public static String realtimeMeetingTimeoutLockKey(Long meetingId) {
|
||||
return "biz:meeting:realtime:timeout:lock:" + meetingId;
|
||||
}
|
||||
|
||||
public static String realtimeMeetingResumeTimeoutPrefix() {
|
||||
return "biz:meeting:realtime:resume-timeout:";
|
||||
}
|
||||
|
||||
public static String realtimeMeetingEmptyTimeoutPrefix() {
|
||||
return "biz:meeting:realtime:empty-timeout:";
|
||||
}
|
||||
|
||||
public static String androidDeviceOnlineKey(String deviceId) {
|
||||
return "biz:android:device:online:" + deviceId;
|
||||
}
|
||||
|
||||
public static String androidDeviceActiveConnectionKey(String deviceId) {
|
||||
return "biz:android:device:active-conn:" + deviceId;
|
||||
}
|
||||
|
||||
public static String androidDeviceConnectionKey(String connectionId) {
|
||||
return "biz:android:device:conn:" + connectionId;
|
||||
}
|
||||
|
||||
public static String androidDeviceTopicsKey(String deviceId) {
|
||||
return "biz:android:device:topics:" + deviceId;
|
||||
}
|
||||
|
||||
public static String realtimeMeetingEventSeqKey(Long meetingId) {
|
||||
return "biz:meeting:realtime:event-seq:" + meetingId;
|
||||
}
|
||||
|
||||
public static String publicMeetingSessionKey(String sessionId) {
|
||||
return "biz:meeting:public-session:" + sessionId;
|
||||
}
|
||||
|
||||
public static String androidChunkUploadSessionKey(Long meetingId) {
|
||||
return "biz:meeting:android:chunk-upload:" + meetingId;
|
||||
}
|
||||
|
||||
public static String androidPendingMeetingDraftKey(Long meetingId) {
|
||||
return "biz:meeting:android:draft:" + meetingId;
|
||||
}
|
||||
|
||||
public static String androidDeviceWeatherKey(String cityName) {
|
||||
return "biz:android:device:weather:" + cityName;
|
||||
}
|
||||
|
||||
private static String normalizeQueueKey(String queueKey) {
|
||||
if (queueKey == null || queueKey.isBlank()) {
|
||||
return "unknown";
|
||||
}
|
||||
return queueKey.trim();
|
||||
}
|
||||
|
||||
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_TYPE = "type";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,71 @@
|
|||
package com.imeeting.common;
|
||||
|
||||
/**
|
||||
* 系统参数 Key 常量定义。
|
||||
*/
|
||||
public final class SysParamKeys {
|
||||
private SysParamKeys() {}
|
||||
private SysParamKeys() {
|
||||
}
|
||||
|
||||
public static final String CAPTCHA_ENABLED = "security.captcha.enabled";
|
||||
// 会议总结系统提示词模板,控制总结任务的角色设定和输出约束
|
||||
public static final String MEETING_SUMMARY_SYSTEM_PROMPT = "meeting.summary.system_prompt";
|
||||
// 会议章节系统提示词模板,控制章节切分任务的角色设定和输出约束
|
||||
public static final String MEETING_CHAPTER_SYSTEM_PROMPT = "meeting.chapter.prompt_template";
|
||||
// 会议总结用户提示词模板,承载会议字段和业务占位符
|
||||
public static final String MEETING_SUMMARY_USER_TEMPLATE = "meeting.summary.user_template";
|
||||
// 会议章节用户提示词模板,承载转录分段占位符
|
||||
public static final String MEETING_CHAPTER_USER_TEMPLATE = "meeting.chapter.user_template";
|
||||
/** 是否启用 AI 目录。 */
|
||||
public static final String MEETING_AI_CATALOG_ENABLED = "meeting.ai_catalog.enabled";
|
||||
/** 会议总结派发模式:PARALLEL / SERIAL。 */
|
||||
public static final String MEETING_SUMMARY_DISPATCH_MODE = "meeting.summary.dispatch_mode";
|
||||
/** 离线会议音频上传大小上限,单位 MB。 */
|
||||
public static final String MEETING_OFFLINE_AUDIO_MAX_SIZE_MB = "meeting.offline_audio.max_size_mb";
|
||||
/** 是否允许创建离线会议。 */
|
||||
public static final String MEETING_CREATE_OFFLINE_ENABLED = "meeting.create.offline_enabled";
|
||||
/** 是否允许创建实时会议。 */
|
||||
public static final String MEETING_CREATE_REALTIME_ENABLED = "meeting.create.realtime_enabled";
|
||||
/** ASR 任务最大并发数。 */
|
||||
public static final String MEETING_ASR_MAX_CONCURRENT = "meeting.asr.max_concurrent";
|
||||
/** 会议暂停最长时长,单位秒。 */
|
||||
public static final String MEETING_MAX_PAUSE_DURATION = "meeting.max_pause_duration";
|
||||
/** 单场会议最大时长,单位分钟。 */
|
||||
public static final String MEETING_MAX_MEETING_DURATION = "meeting.max_meeting_duration";
|
||||
/**
|
||||
* 单场会议最小时长,单位秒。
|
||||
*/
|
||||
public static final String MEETING_MIN_MEETING_DURATION = "meeting.min_meeting_duration";
|
||||
/** 会议音频传输丢包率配置值。 */
|
||||
public static final String MEETING_PACKET_LOSS_RATE = "meeting.packet_loss_rate";
|
||||
/** 安卓端是否启用音频分片上传。 */
|
||||
public static final String MEETING_ANDROID_AUDIO_CHUNK_UPLOAD_ENABLED = "meeting.android.audio.chunk_upload_enabled";
|
||||
/** 安卓端音频分片上传时每片时长,单位秒。 */
|
||||
public static final String MEETING_ANDROID_AUDIO_CHUNK_DURATION_SECONDS = "meeting.android.audio.chunk_duration_seconds";
|
||||
/** 会议积分功能总开关。 */
|
||||
public static final String MEETING_POINTS_ENABLED = "meeting.points.enabled";
|
||||
/** 积分计费单位时长,单位分钟。 */
|
||||
public static final String MEETING_POINTS_UNIT_MINUTES = "meeting.points.unit_minutes";
|
||||
/** 每个计费单位消耗的积分数。 */
|
||||
public static final String MEETING_POINTS_COST_PER_UNIT = "meeting.points.cost_per_unit";
|
||||
/** 积分拆分时分配给 ASR 的比例。 */
|
||||
public static final String MEETING_POINTS_ASR_RATIO = "meeting.points.asr_ratio";
|
||||
/** 积分拆分时分配给 LLM 的比例。 */
|
||||
public static final String MEETING_POINTS_LLM_RATIO = "meeting.points.llm_ratio";
|
||||
/** 租户初始化公共积分账户时的初始积分余额。 */
|
||||
public static final String MEETING_POINTS_INITIAL_BALANCE = "meeting.points.initial_balance";
|
||||
/** 会议积分账户模式:PUBLIC / PERSONAL / BOTH。 */
|
||||
public static final String MEETING_POINTS_ACCOUNT_MODE = "meeting.points.account_mode";
|
||||
/** 会议积分扣费优先级:PERSONAL_FIRST / PUBLIC_FIRST。 */
|
||||
public static final String MEETING_POINTS_CHARGE_PRIORITY = "meeting.points.charge_priority";
|
||||
/** 临时授权默认下发数量。 */
|
||||
public static final String LICENSE_TEMP_DEFAULT_COUNT = "license.temp.default.count";
|
||||
/** 临时授权默认有效期,单位月。 */
|
||||
public static final String LICENSE_TEMP_DEFAULT_EXPIRE_MONTHS = "license.temp.default.expire.months";
|
||||
/** 默认授权对应的产品编码。 */
|
||||
public static final String LICENSE_DEFAULT_PRODUCT_CODE = "license.default.product.code";
|
||||
/** 和风天气接口基础地址。 */
|
||||
public static final String DEVICE_WEATHER_QWEATHER_BASE_URL = "device.weather.qweather.base_url";
|
||||
/** 和风天气接口访问 Key。 */
|
||||
public static final String DEVICE_WEATHER_QWEATHER_KEY = "device.weather.qweather.key";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.unisbase.config.properties.UnisBaseProperties;
|
||||
import com.unisbase.entity.SysTenant;
|
||||
import com.unisbase.mapper.SysTenantMapper;
|
||||
import com.unisbase.security.SpringSecurityTenantProvider;
|
||||
import com.unisbase.spi.UnisBaseTenantProvider;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
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 org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@Configuration
|
||||
public class AndroidTenantProviderConfig {
|
||||
|
||||
private static final String TENANT_ID_HEADER = "X-Tenant-Id";
|
||||
private static final String TENANT_CODE_HEADER = "X-Tenant-Code";
|
||||
|
||||
@Bean
|
||||
public UnisBaseTenantProvider unisBaseTenantProvider(UnisBaseProperties properties,
|
||||
ObjectProvider<SysTenantMapper> sysTenantMapperProvider) {
|
||||
return new AndroidTenantProvider(properties, sysTenantMapperProvider);
|
||||
}
|
||||
|
||||
private static final class AndroidTenantProvider extends SpringSecurityTenantProvider {
|
||||
private final UnisBaseProperties properties;
|
||||
private final ObjectProvider<SysTenantMapper> sysTenantMapperProvider;
|
||||
|
||||
private AndroidTenantProvider(UnisBaseProperties properties, ObjectProvider<SysTenantMapper> sysTenantMapperProvider) {
|
||||
super(properties);
|
||||
this.properties = properties;
|
||||
this.sysTenantMapperProvider = sysTenantMapperProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getCurrentTenantId() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.getPrincipal() instanceof com.unisbase.security.LoginUser user && user.getTenantId() != null) {
|
||||
return user.getTenantId();
|
||||
}
|
||||
|
||||
Long tenantId = parseTenantIdHeader();
|
||||
if (tenantId != null) {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
String tenantCode = header(TENANT_CODE_HEADER);
|
||||
if (StringUtils.hasText(tenantCode)) {
|
||||
Long resolvedTenantId = resolveTenantIdByCode(tenantCode.trim());
|
||||
if (resolvedTenantId != null) {
|
||||
return resolvedTenantId;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSingleTenantMode()) {
|
||||
Long defaultTenantId = properties.getTenant().getDefaultTenantId();
|
||||
return defaultTenantId == null || defaultTenantId <= 0 ? 1L : defaultTenantId;
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
private Long parseTenantIdHeader() {
|
||||
String value = header(TENANT_ID_HEADER);
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(value.trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Long resolveTenantIdByCode(String tenantCode) {
|
||||
SysTenantMapper sysTenantMapper = sysTenantMapperProvider.getIfAvailable();
|
||||
if (sysTenantMapper == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
SysTenant tenant = sysTenantMapper.selectOne(new LambdaQueryWrapper<SysTenant>()
|
||||
.eq(SysTenant::getTenantCode, tenantCode)
|
||||
.eq(SysTenant::getIsDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
return tenant == null ? null : tenant.getId();
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String header(String name) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
return request == null ? null : request.getHeader(name);
|
||||
}
|
||||
|
||||
private boolean isSingleTenantMode() {
|
||||
if (properties == null || properties.getTenant() == null) {
|
||||
return false;
|
||||
}
|
||||
String mode = properties.getTenant().getMode();
|
||||
if (mode != null && !mode.isBlank()) {
|
||||
return "single".equalsIgnoreCase(mode.trim());
|
||||
}
|
||||
return !properties.getTenant().isEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class ApiResponseSuccessCodeAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private static final String LEGACY_SUCCESS_CODE = "0";
|
||||
private static final String SUCCESS_CODE = "200";
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body,
|
||||
MethodParameter returnType,
|
||||
MediaType selectedContentType,
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
ServerHttpRequest request,
|
||||
ServerHttpResponse response) {
|
||||
if (body instanceof ApiResponse<?> apiResponse && LEGACY_SUCCESS_CODE.equals(apiResponse.getCode())) {
|
||||
apiResponse.setCode(SUCCESS_CODE);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.api.StatefulRedisConnection;
|
||||
import io.lettuce.core.codec.StringCodec;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Configuration
|
||||
public class LettuceRedisConfig {
|
||||
|
||||
@Bean(destroyMethod = "shutdown")
|
||||
public RedisClient redisClient(RedisProperties redisProperties) {
|
||||
RedisURI.Builder builder = RedisURI.builder()
|
||||
.withHost(redisProperties.getHost())
|
||||
.withPort(redisProperties.getPort())
|
||||
.withDatabase(redisProperties.getDatabase());
|
||||
|
||||
if (StringUtils.hasText(redisProperties.getUsername())) {
|
||||
builder.withAuthentication(redisProperties.getUsername(), redisProperties.getPassword());
|
||||
} else if (StringUtils.hasText(redisProperties.getPassword())) {
|
||||
builder.withPassword(redisProperties.getPassword().toCharArray());
|
||||
}
|
||||
|
||||
Duration timeout = redisProperties.getTimeout();
|
||||
if (timeout != null && !timeout.isZero() && !timeout.isNegative()) {
|
||||
builder.withTimeout(timeout);
|
||||
}
|
||||
|
||||
return RedisClient.create(builder.build());
|
||||
}
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
public StatefulRedisConnection<String, String> redisConnection(RedisClient redisClient) {
|
||||
return redisClient.connect(StringCodec.UTF8);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
@Configuration
|
||||
public class MeetingAsyncExecutorConfig {
|
||||
|
||||
|
||||
|
||||
@Bean("asrTaskExecutor")
|
||||
public Executor asrTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(4);
|
||||
executor.setMaxPoolSize(8);
|
||||
executor.setQueueCapacity(20);
|
||||
executor.setThreadNamePrefix("imeeting-asr-worker-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
|
||||
@Bean("summaryTaskExecutor")
|
||||
public Executor summaryTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(8);
|
||||
executor.setMaxPoolSize(16);
|
||||
executor.setQueueCapacity(20);
|
||||
executor.setThreadNamePrefix("imeeting-summary-worker-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
@Bean("chunkMergeExecutor")
|
||||
public Executor chunkMergeExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setMaxPoolSize(4);
|
||||
executor.setQueueCapacity(10);
|
||||
executor.setThreadNamePrefix("imeeting-chunk-merge-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme.In;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@SecurityScheme(
|
||||
name = OpenApiConfig.BEARER_AUTH_SCHEME,
|
||||
type = SecuritySchemeType.HTTP,
|
||||
scheme = "bearer",
|
||||
bearerFormat = "JWT",
|
||||
in = SecuritySchemeIn.HEADER
|
||||
)
|
||||
public class OpenApiConfig {
|
||||
|
||||
public static final String BEARER_AUTH_SCHEME = "bearerAuth";
|
||||
|
||||
@Bean
|
||||
public OpenAPI imeetingOpenApi() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("iMeeting 接口文档")
|
||||
.description("iMeeting 后端 REST 接口与兼容接口文档")
|
||||
.version("v0.1.0"))
|
||||
.addSecurityItem(new SecurityRequirement().addList(BEARER_AUTH_SCHEME))
|
||||
.schemaRequirement(BEARER_AUTH_SCHEME, new io.swagger.v3.oas.models.security.SecurityScheme()
|
||||
.type(Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.in(In.HEADER));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import com.imeeting.websocket.RealtimeMeetingProxyWebSocketHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
@RequiredArgsConstructor
|
||||
public class RealtimeMeetingWebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
private final RealtimeMeetingProxyWebSocketHandler realtimeMeetingProxyWebSocketHandler;
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(realtimeMeetingProxyWebSocketHandler, "/ws/meeting/realtime")
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 Tomcat 内置的 WebSocket keepalive ping 检测(sessionIdleTimeout)。
|
||||
* <p>
|
||||
* Tomcat 默认会对 WebSocket session 设置 sessionIdleTimeout(-1 表示无限,但某些版本默认非 -1),
|
||||
* 并通过后台线程定期发送 Ping 帧,若在超时内未收到 Pong 响应,触发
|
||||
* code=1011 "keepalive ping timeout" 强制断开。
|
||||
* 实时 ASR 场景中,客户端持续发送音频帧,由前端心跳保活,
|
||||
* 因此显式将 sessionIdleTimeout 设为 -1(无限)。
|
||||
* </p>
|
||||
*/
|
||||
@Bean
|
||||
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> wsSessionIdleTimeoutCustomizer() {
|
||||
return factory -> factory.addConnectorCustomizers(connector -> {
|
||||
// 通过系统属性通知 Tomcat WS 容器关闭 sessionIdleTimeout 检查
|
||||
// org.apache.tomcat.websocket.DEFAULT_SESSION_IDLE_TIMEOUT=-1
|
||||
if (System.getProperty("org.apache.tomcat.websocket.DEFAULT_SESSION_IDLE_TIMEOUT") == null) {
|
||||
System.setProperty("org.apache.tomcat.websocket.DEFAULT_SESSION_IDLE_TIMEOUT", "-1");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
|
||||
@Configuration
|
||||
public class RedisKeyExpirationConfig {
|
||||
|
||||
@Bean
|
||||
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(connectionFactory);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package com.imeeting.config;
|
|||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
|
|
@ -10,12 +12,17 @@ import java.io.File;
|
|||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value("${app.upload-path}")
|
||||
@Value("${unisbase.app.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
@Value("${app.resource-prefix}")
|
||||
@Value("${unisbase.app.resource-prefix}")
|
||||
private String resourcePrefix;
|
||||
|
||||
@Override
|
||||
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
|
||||
configurer.mediaType("m4a", MediaType.parseMediaType("audio/mp4"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 确保目录存在
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.imeeting.config.grpc;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "imeeting.grpc.auth")
|
||||
public class AndroidGrpcAuthProperties {
|
||||
|
||||
private boolean enabled = false;
|
||||
private boolean allowAnonymous = true;
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.imeeting.config.grpc;
|
||||
|
||||
import io.grpc.ForwardingServerCallListener;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.ServerCall;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.Status;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GrpcExceptionLoggingInterceptor implements ServerInterceptor {
|
||||
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
|
||||
Metadata headers,
|
||||
ServerCallHandler<ReqT, RespT> next) {
|
||||
String methodName = call.getMethodDescriptor().getFullMethodName();
|
||||
AtomicBoolean closed = new AtomicBoolean(false);
|
||||
ServerCall.Listener<ReqT> delegate;
|
||||
try {
|
||||
delegate = next.startCall(call, headers);
|
||||
} catch (RuntimeException ex) {
|
||||
log.error("gRPC startCall failed, method={}", methodName, ex);
|
||||
closeCall(call, closed, ex);
|
||||
return new ServerCall.Listener<>() {
|
||||
};
|
||||
}
|
||||
|
||||
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(delegate) {
|
||||
@Override
|
||||
public void onMessage(ReqT message) {
|
||||
runSafely("onMessage", () -> super.onMessage(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHalfClose() {
|
||||
runSafely("onHalfClose", super::onHalfClose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
runSafely("onCancel", super::onCancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
runSafely("onComplete", super::onComplete);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReady() {
|
||||
runSafely("onReady", super::onReady);
|
||||
}
|
||||
|
||||
private void runSafely(String phase, Runnable action) {
|
||||
try {
|
||||
action.run();
|
||||
} catch (RuntimeException ex) {
|
||||
log.error("gRPC request handling failed, method={}, phase={}", methodName, phase, ex);
|
||||
closeCall(call, closed, ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private <ReqT, RespT> void closeCall(ServerCall<ReqT, RespT> call, AtomicBoolean closed, RuntimeException ex) {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
call.close(Status.UNKNOWN.withDescription("应用处理 RPC 时发生异常").withCause(ex), new Metadata());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.imeeting.config.grpc;
|
||||
|
||||
import io.grpc.BindableService;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.protobuf.services.ProtoReflectionService;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@EnableConfigurationProperties({GrpcServerProperties.class, AndroidGrpcAuthProperties.class})
|
||||
public class GrpcServerLifecycle {
|
||||
|
||||
private final GrpcServerProperties properties;
|
||||
private final GrpcExceptionLoggingInterceptor grpcExceptionLoggingInterceptor;
|
||||
private final List<BindableService> bindableServices;
|
||||
private Server server;
|
||||
|
||||
@PostConstruct
|
||||
public void start() throws IOException {
|
||||
if (!properties.isEnabled()) {
|
||||
log.info("gRPC server is disabled by configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
NettyServerBuilder builder = NettyServerBuilder.forPort(properties.getPort())
|
||||
.maxInboundMessageSize(properties.getMaxInboundMessageSize())
|
||||
.intercept(grpcExceptionLoggingInterceptor);
|
||||
bindableServices.forEach(builder::addService);
|
||||
if (properties.isReflectionEnabled()) {
|
||||
builder.addService(ProtoReflectionService.newInstance());
|
||||
}
|
||||
|
||||
server = builder.build();
|
||||
server.start();
|
||||
log.info("gRPC server started on port {} with {} services", properties.getPort(), bindableServices.size());
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
log.info("Stopping gRPC server");
|
||||
server.shutdown();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.imeeting.config.grpc;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "imeeting.grpc")
|
||||
public class GrpcServerProperties {
|
||||
|
||||
private boolean enabled = true;
|
||||
private int port = 19090;
|
||||
private int maxInboundMessageSize = 4194304;
|
||||
private boolean reflectionEnabled = true;
|
||||
private Gateway gateway = new Gateway();
|
||||
private Realtime realtime = new Realtime();
|
||||
|
||||
@Data
|
||||
public static class Gateway {
|
||||
private long heartbeatIntervalSeconds = 15;
|
||||
private long heartbeatTimeoutSeconds = 45;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Realtime {
|
||||
private long sessionTtlSeconds = 600;
|
||||
private int sampleRate = 16000;
|
||||
private int channels = 1;
|
||||
private String encoding = "PCM16LE";
|
||||
private long connectionTtlSeconds = 1800;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,143 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.service.android.AndroidDeviceBindingService;
|
||||
import com.imeeting.service.android.AndroidDeviceRegistrationService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.auth.JwtTokenProvider;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.exception.UnAuthException;
|
||||
import com.unisbase.dto.LoginRequest;
|
||||
import com.unisbase.dto.RefreshRequest;
|
||||
import com.unisbase.dto.TokenResponse;
|
||||
import com.unisbase.service.AuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
|
||||
@Tag(name = "Android认证接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidAuthController {
|
||||
private final AuthService authService;
|
||||
private final AndroidDeviceBindingService androidDeviceBindingService;
|
||||
private final AndroidDeviceRegistrationService androidDeviceRegistrationService;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Operation(summary = "Android登录")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回登录成功后的访问令牌、刷新令牌和当前用户信息",
|
||||
content = @Content(schema = @Schema(implementation = TokenResponse.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/login")
|
||||
public ApiResponse<TokenResponse> login(@Valid @RequestBody LoginRequest request,
|
||||
@RequestHeader(value = "X-Android-Device-Id", required = false) String deviceId,
|
||||
@RequestHeader(value = "X-Android-App-Version", required = false) String appVersion,
|
||||
@RequestHeader(value = "X-Android-Platform", required = false) String platform) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android认证", "登录接口",
|
||||
"request", request,
|
||||
"deviceId", deviceId,
|
||||
"appVersion", appVersion,
|
||||
"platform", platform);
|
||||
if (!StringUtils.hasText(deviceId)) {
|
||||
throw new IllegalArgumentException("X-Android-Device-Id不能为空");
|
||||
}
|
||||
androidDeviceRegistrationService.requireRegistered(deviceId.trim());
|
||||
TokenResponse response = authService.login(request, true);
|
||||
if (response != null && response.getUser() != null && response.getCurrentTenantId() != null && StringUtils.hasText(deviceId)) {
|
||||
androidDeviceBindingService.recordLogin(
|
||||
deviceId.trim(),
|
||||
response.getCurrentTenantId(),
|
||||
response.getUser().getUserId()
|
||||
);
|
||||
}
|
||||
return ApiResponse.ok(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "Android刷新令牌")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回刷新后的访问令牌、刷新令牌和当前用户信息",
|
||||
content = @Content(schema = @Schema(implementation = TokenResponse.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/refresh")
|
||||
public ApiResponse<TokenResponse> refresh(@RequestBody(required = false) RefreshRequest request,
|
||||
@RequestHeader(value = "Authorization", required = false) String authorization,
|
||||
@RequestHeader(value = "X-Android-Access-Token", required = false) String androidAccessToken) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android认证", "刷新令牌接口",
|
||||
"request", request,
|
||||
"authorization", authorization,
|
||||
"androidAccessToken", androidAccessToken);
|
||||
TokenResponse refresh = null;
|
||||
try {
|
||||
refresh = authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(e.getMessage());
|
||||
}
|
||||
return ApiResponse.ok(refresh);
|
||||
}
|
||||
|
||||
@Operation(summary = "Android退出登录")
|
||||
@PostMapping("/logout")
|
||||
public ApiResponse<Void> logout(HttpServletRequest request,
|
||||
@RequestHeader(value = "Authorization", required = false) String authorization,
|
||||
@RequestHeader(value = "X-Android-Device-Id", required = false) String deviceId) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android认证", "退出登录接口",
|
||||
"authorization", authorization,
|
||||
"deviceId", deviceId);
|
||||
String token = extractToken(authorization);
|
||||
var claims = jwtTokenProvider.parseToken(token);
|
||||
Long userId = claims.get("userId", Long.class);
|
||||
Long tenantId = claims.get("tenantId", Long.class);
|
||||
String sessionId = claims.get("sessionId", String.class);
|
||||
authService.logout(userId, tenantId, sessionId);
|
||||
if (StringUtils.hasText(deviceId)) {
|
||||
androidDeviceBindingService.unbindPrivateDevice(deviceId.trim());
|
||||
}
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
private String resolveRefreshToken(RefreshRequest request, String authorization, String androidAccessToken) {
|
||||
if (request != null && StringUtils.hasText(request.getRefreshToken())) {
|
||||
return request.getRefreshToken().trim();
|
||||
}
|
||||
if (StringUtils.hasText(androidAccessToken)) {
|
||||
return androidAccessToken.trim();
|
||||
}
|
||||
if (StringUtils.hasText(authorization)) {
|
||||
String value = authorization.trim();
|
||||
if (value.startsWith("Bearer ")) {
|
||||
return value.substring(7).trim();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
throw new IllegalArgumentException("refreshToken不能为空");
|
||||
}
|
||||
|
||||
private String extractToken(String authorization) {
|
||||
if (!StringUtils.hasText(authorization)) {
|
||||
throw new IllegalArgumentException("Authorization不能为空");
|
||||
}
|
||||
String value = authorization.trim();
|
||||
return value.startsWith("Bearer ") ? value.substring(7).trim() : value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.imeeting.entity.biz.ClientDownload;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.ClientDownloadService;
|
||||
import com.unisbase.annotation.Anonymous;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "Android客户端接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/clients")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidClientController {
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final ClientDownloadService clientDownloadService;
|
||||
|
||||
@Operation(summary = "查询平台最新客户端")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回指定平台当前生效的最新客户端安装包信息",
|
||||
content = @Content(schema = @Schema(implementation = ClientDownload.class))
|
||||
)
|
||||
})
|
||||
@GetMapping("/latest/by-platform")
|
||||
@Anonymous
|
||||
public ApiResponse<ClientDownload> latestByPlatform(HttpServletRequest request,
|
||||
@RequestParam(value = "platform_code", required = false) String platformCode,
|
||||
@RequestParam(value = "platform_type", required = false) String platformType,
|
||||
@RequestParam(value = "platform_name", required = false) String platformName) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android客户端", "查询平台最新客户端接口",
|
||||
"platformCode", platformCode,
|
||||
"platformType", platformType,
|
||||
"platformName", platformName);
|
||||
androidAuthService.authenticateHttp(request);
|
||||
if ((platformCode == null || platformCode.isBlank())
|
||||
&& ((platformType == null || platformType.isBlank()) || (platformName == null || platformName.isBlank()))) {
|
||||
return ApiResponse.error("请提供 platform_code 参数");
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<ClientDownload> wrapper = new LambdaQueryWrapper<ClientDownload>()
|
||||
.eq(ClientDownload::getStatus, 1)
|
||||
.eq(ClientDownload::getIsLatest, 1);
|
||||
if (platformCode != null && !platformCode.isBlank()) {
|
||||
wrapper.apply("LOWER(platform_code) = {0}", platformCode.trim().toLowerCase());
|
||||
} else {
|
||||
wrapper.apply("LOWER(platform_type) = {0}", platformType.trim().toLowerCase())
|
||||
.apply("LOWER(platform_name) = {0}", platformName.trim().toLowerCase());
|
||||
}
|
||||
wrapper.orderByDesc(ClientDownload::getVersionCode)
|
||||
.orderByDesc(ClientDownload::getId)
|
||||
.last("LIMIT 1");
|
||||
|
||||
ClientDownload clientDownload = clientDownloadService.getOne(wrapper);
|
||||
if (clientDownload == null) {
|
||||
return ApiResponse.error("暂无最新客户端");
|
||||
}
|
||||
return ApiResponse.ok(clientDownload);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.AndroidDeviceHomeStatsVO;
|
||||
import com.imeeting.dto.android.AndroidDeviceRegisterRequest;
|
||||
import com.imeeting.dto.android.AndroidDeviceRegisterResponse;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.android.AndroidDeviceService;
|
||||
import com.imeeting.service.android.AndroidDeviceRegistrationService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.annotation.Anonymous;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "Android设备接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/devices")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidDeviceController {
|
||||
private static final String TENANT_CODE_HEADER = "X-Tenant-Code";
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final AndroidDeviceRegistrationService androidDeviceRegistrationService;
|
||||
private final AndroidDeviceService androidDeviceService;
|
||||
|
||||
@Operation(summary = "设备自注册")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回设备注册后的基础信息",
|
||||
content = @Content(schema = @Schema(implementation = AndroidDeviceRegisterResponse.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/register")
|
||||
@Anonymous
|
||||
public ApiResponse<AndroidDeviceRegisterResponse> register(HttpServletRequest request,
|
||||
@RequestBody(required = false) AndroidDeviceRegisterRequest command) {
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("注册请求不能为空");
|
||||
}
|
||||
String tenantCode = resolveTenantCode(request, command);
|
||||
AndroidRequestLogHelper.logRequest(log, "Android设备", "设备自注册", "request", command, "tenantCode", tenantCode);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttpIgnoreToken(request, false);
|
||||
AndroidDeviceRegisterResponse response = androidDeviceRegistrationService.register(
|
||||
tenantCode,
|
||||
authContext.getDeviceId(),
|
||||
command.getDeviceName(),
|
||||
command.getTerminalType() == null ? authContext.getPlatform() : command.getTerminalType(),
|
||||
command.getTerminalVersion() == null ? authContext.getAppVersion() : command.getTerminalVersion()
|
||||
);
|
||||
return ApiResponse.ok(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新设备")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "更新设备信息",
|
||||
content = @Content(schema = @Schema(implementation = AndroidDeviceRegisterResponse.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/update")
|
||||
@Anonymous
|
||||
public ApiResponse update(HttpServletRequest request,
|
||||
@RequestBody(required = false) AndroidDeviceRegisterRequest command) {
|
||||
if (command == null) {
|
||||
throw new IllegalArgumentException("更新设备请求参数不能为空");
|
||||
}
|
||||
String tenantCode = request == null ? null : request.getHeader(TENANT_CODE_HEADER);
|
||||
AndroidRequestLogHelper.logRequest(log, "Android设备", "更新设备", "request", command, "tenantCode", tenantCode);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request, false);
|
||||
androidDeviceService.updateDevice(
|
||||
tenantCode,
|
||||
authContext.getDeviceId(),
|
||||
command.getDeviceName(),
|
||||
command.getTerminalType() == null ? authContext.getPlatform() : command.getTerminalType(),
|
||||
command.getTerminalVersion() == null ? authContext.getAppVersion() : command.getTerminalVersion()
|
||||
);
|
||||
return ApiResponse.ok(null);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询设备首页统计")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回设备首页统计信息",
|
||||
content = @Content(schema = @Schema(implementation = AndroidDeviceHomeStatsVO.class))
|
||||
)
|
||||
})
|
||||
@GetMapping("/home")
|
||||
@Anonymous
|
||||
public ApiResponse<AndroidDeviceHomeStatsVO> home(HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android设备", "查询设备首页统计");
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request, true, true);
|
||||
return ApiResponse.ok(androidDeviceService.getHomeStats(authContext));
|
||||
}
|
||||
|
||||
private String resolveTenantCode(HttpServletRequest request, AndroidDeviceRegisterRequest command) {
|
||||
if (command != null && StringUtils.hasText(command.getTenantCode())) {
|
||||
return command.getTenantCode().trim();
|
||||
}
|
||||
String tenantCodeFromHeader = request == null ? null : request.getHeader(TENANT_CODE_HEADER);
|
||||
if (StringUtils.hasText(tenantCodeFromHeader)) {
|
||||
return tenantCodeFromHeader.trim();
|
||||
}
|
||||
throw new IllegalArgumentException("tenantCode不能为空");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.entity.biz.ExternalApp;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.ExternalAppService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "Android外部应用接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/external-apps")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidExternalAppController {
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final ExternalAppService externalAppService;
|
||||
|
||||
@Operation(summary = "查询启用的外部应用")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回当前启用的外部应用列表",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ExternalApp.class)))
|
||||
)
|
||||
})
|
||||
@GetMapping("/active")
|
||||
public ApiResponse<List<ExternalApp>> active(HttpServletRequest request,
|
||||
@RequestParam(value = "is_active", required = false) Integer ignoredIsActive) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android外部应用", "查询启用外部应用接口", "isActive", ignoredIsActive);
|
||||
androidAuthService.authenticateHttp(request);
|
||||
List<ExternalApp> apps = externalAppService.list(new LambdaQueryWrapper<ExternalApp>()
|
||||
.eq(ExternalApp::getStatus, 1)
|
||||
.orderByAsc(ExternalApp::getSortOrder)
|
||||
.orderByDesc(ExternalApp::getCreatedAt));
|
||||
return ApiResponse.ok(apps);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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;
|
||||
|
||||
@Tag(name = "Android模型接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/llm-models")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidLlmModelController {
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final AiModelService aiModelService;
|
||||
|
||||
@Operation(summary = "查询启用的大模型列表")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回当前租户下启用的大语言模型列表",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AiModelVO.class)))
|
||||
)
|
||||
})
|
||||
@GetMapping("/active")
|
||||
public ApiResponse<List<AiModelVO>> activeModels(HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android模型", "查询启用大模型列表接口");
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
PageResult<List<AiModelVO>> result = aiModelService.pageModels(1, 1000, null, "LLM", loginUser.getTenantId(), false);
|
||||
List<AiModelVO> enabledModels = result.getRecords() == null
|
||||
? List.of()
|
||||
: result.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||
.toList();
|
||||
return ApiResponse.ok(enabledModels);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.unisbase.security.LoginUser;
|
||||
|
||||
final class AndroidLoginUserSupport {
|
||||
|
||||
private AndroidLoginUserSupport() {
|
||||
}
|
||||
|
||||
static LoginUser requireLoginUser(AndroidAuthContext authContext) {
|
||||
LoginUser loginUser = toLoginUser(authContext);
|
||||
if (loginUser == null) {
|
||||
throw new RuntimeException("Android用户未登录或认证无效");
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
static LoginUser toLoginUser(AndroidAuthContext authContext) {
|
||||
if (authContext == null || authContext.isAnonymous()
|
||||
|| authContext.getUserId() == null || authContext.getTenantId() == null) {
|
||||
return null;
|
||||
}
|
||||
LoginUser loginUser = new LoginUser(
|
||||
authContext.getUserId(),
|
||||
authContext.getTenantId(),
|
||||
authContext.getUsername(),
|
||||
authContext.getPlatformAdmin(),
|
||||
authContext.getTenantAdmin(),
|
||||
authContext.getPermissions()
|
||||
);
|
||||
loginUser.setDisplayName(authContext.getDisplayName());
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
static boolean isAdmin(AndroidAuthContext authContext) {
|
||||
return authContext != null
|
||||
&& (Boolean.TRUE.equals(authContext.getPlatformAdmin())
|
||||
|| Boolean.TRUE.equals(authContext.getTenantAdmin()));
|
||||
}
|
||||
|
||||
static String resolveDisplayName(AndroidAuthContext authContext) {
|
||||
if (authContext == null) {
|
||||
return null;
|
||||
}
|
||||
if (authContext.getDisplayName() != null && !authContext.getDisplayName().isBlank()) {
|
||||
return authContext.getDisplayName().trim();
|
||||
}
|
||||
if (authContext.getUsername() != null && !authContext.getUsername().isBlank()) {
|
||||
return authContext.getUsername().trim();
|
||||
}
|
||||
return authContext.getDeviceId();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.android.AndroidChunkUploadService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.annotation.Anonymous;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Tag(name = "Android会议分片上传接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/meetings/upload-audio")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidMeetingChunkUploadController {
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final AndroidChunkUploadService androidChunkUploadService;
|
||||
|
||||
@Operation(summary = "上传会议音频分片")
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "分片上传成功返回 true",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/chunk")
|
||||
@Anonymous
|
||||
public ApiResponse<Boolean> uploadChunk(HttpServletRequest request,
|
||||
@RequestParam("meeting_id") Long meetingId,
|
||||
@RequestParam("chunk_index") Integer chunkIndex,
|
||||
@RequestParam("chunk_file") MultipartFile chunkFile) throws IOException {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "上传会议音频分片",
|
||||
"meetingId", meetingId,
|
||||
"chunkIndex", chunkIndex,
|
||||
"chunkFile", chunkFile);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttpIgnoreToken(request,true);
|
||||
androidChunkUploadService.saveChunk(meetingId, chunkIndex, chunkFile, authContext);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "完成分片上传并触发会议音频处理")
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回上传后的会议 ID 和音频地址",
|
||||
content = @Content(schema = @Schema(implementation = LegacyUploadAudioResponse.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/complete")
|
||||
@Anonymous
|
||||
public ApiResponse<LegacyUploadAudioResponse> completeUpload(HttpServletRequest request,
|
||||
@RequestParam("meeting_id") Long meetingId,
|
||||
@RequestParam("total_chunks") Integer totalChunks) throws IOException {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "完成分片上传",
|
||||
"meetingId", meetingId,
|
||||
"totalChunks", totalChunks);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
androidChunkUploadService.completeUploadAsync(meetingId, totalChunks, authContext);
|
||||
return ApiResponse.ok(new LegacyUploadAudioResponse(meetingId, null, "后台合并上传中"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,585 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.AndroidOfflineMeetingCreateCommand;
|
||||
import com.imeeting.dto.android.AndroidMeetingCreateResponse;
|
||||
import com.imeeting.dto.android.AndroidMeetingConfigVo;
|
||||
import com.imeeting.dto.android.AndroidMeetingListItemVO;
|
||||
import com.imeeting.dto.android.AndroidOfflineMeetingConflictVO;
|
||||
import com.imeeting.dto.android.AndroidOfflineMeetingFinishRequest;
|
||||
import com.imeeting.dto.android.AndroidUnifiedMeetingStatusRequest;
|
||||
import com.imeeting.dto.android.AndroidUnifiedMeetingStatusResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||
import com.imeeting.dto.biz.*;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.entity.biz.PromptTemplate;
|
||||
import com.imeeting.enums.BusinessErrorCodeEnum;
|
||||
import com.imeeting.enums.MeetingStatusEnum;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.android.AndroidChunkUploadService;
|
||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||
import com.imeeting.service.android.AndroidMeetingPushService;
|
||||
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.imeeting.service.biz.*;
|
||||
import com.unisbase.annotation.Anonymous;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.common.exception.BusinessException;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.entity.SysTenant;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysTenantMapper;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import com.unisbase.service.SysDictItemService;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import com.unisbase.service.impl.SysDictItemServiceImpl;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Tag(name = "Android会议接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/meetings")
|
||||
@Slf4j
|
||||
public class AndroidMeetingController {
|
||||
|
||||
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
|
||||
private static final String STAGE_AUDIO_TRANSCRIPTION = "audio_transcription";
|
||||
private static final String STAGE_SUMMARY_GENERATION = "summary_generation";
|
||||
private static final String STAGE_COMPLETED = "completed";
|
||||
private static final String TENANT_CODE_HEADER = "X-Tenant-Code";
|
||||
|
||||
@Value("${imeeting.h5.base-url:}")
|
||||
private String h5BaseUrl;
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final AndroidMeetingPushService androidMeetingPushService;
|
||||
private final AndroidChunkUploadService androidChunkUploadService;
|
||||
private final LegacyMeetingAdapterService legacyMeetingAdapterService;
|
||||
private final MeetingQueryService meetingQueryService;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
private final MeetingService meetingService;
|
||||
private final AiTaskService aiTaskService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final SysTenantMapper sysTenantMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final AiModelService aiModelService;
|
||||
private final HotWordGroupService hotWordGroupService;
|
||||
private final SysDictItemService dictItemService;
|
||||
private final SysParamService paramService;
|
||||
private final MeetingProgressService meetingProgressService;
|
||||
private final MeetingUnifiedStatusService meetingUnifiedStatusService;
|
||||
|
||||
@Autowired
|
||||
public AndroidMeetingController(AndroidAuthService androidAuthService,
|
||||
AndroidChunkUploadService androidChunkUploadService,
|
||||
LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||
MeetingQueryService meetingQueryService,
|
||||
MeetingAccessService meetingAccessService,
|
||||
MeetingCommandService meetingCommandService,
|
||||
MeetingService meetingService,
|
||||
AiTaskService aiTaskService,
|
||||
PromptTemplateService promptTemplateService,
|
||||
SysTenantMapper sysTenantMapper,
|
||||
SysUserMapper sysUserMapper,
|
||||
AiModelService aiModelService,
|
||||
HotWordGroupService hotWordGroupService,
|
||||
SysDictItemService dictItemService,
|
||||
SysParamService paramService,
|
||||
MeetingProgressService meetingProgressService,
|
||||
AndroidMeetingPushService androidMeetingPushService,
|
||||
MeetingUnifiedStatusService meetingUnifiedStatusService) {
|
||||
this.androidAuthService = androidAuthService;
|
||||
this.androidChunkUploadService = androidChunkUploadService;
|
||||
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
|
||||
this.meetingQueryService = meetingQueryService;
|
||||
this.meetingAccessService = meetingAccessService;
|
||||
this.meetingCommandService = meetingCommandService;
|
||||
this.meetingService = meetingService;
|
||||
this.aiTaskService = aiTaskService;
|
||||
this.promptTemplateService = promptTemplateService;
|
||||
this.sysTenantMapper = sysTenantMapper;
|
||||
this.sysUserMapper = sysUserMapper;
|
||||
this.meetingProgressService = meetingProgressService;
|
||||
this.aiModelService = aiModelService;
|
||||
this.hotWordGroupService = hotWordGroupService;
|
||||
this.paramService = paramService;
|
||||
this.dictItemService = dictItemService;
|
||||
this.meetingUnifiedStatusService = meetingUnifiedStatusService;
|
||||
this.androidMeetingPushService = androidMeetingPushService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建Android离线会议")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回新创建的会议详情",
|
||||
content = @Content(schema = @Schema(implementation = AndroidMeetingCreateResponse.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/create")
|
||||
@Anonymous
|
||||
@Log(value = "新增Android会议", type = "Android会议管理")
|
||||
public ApiResponse<Object> create(HttpServletRequest request,
|
||||
@RequestBody AndroidOfflineMeetingCreateCommand command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "创建离线会议接口", "request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
resolvePublicDeviceTenantId(request, command, authContext);
|
||||
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
|
||||
// Meeting existingMeeting = findLatestUnfinishedMeetingByDevice(authContext.getDeviceId());
|
||||
// if (existingMeeting != null) {
|
||||
// return new ApiResponse<>("409", "设备端已有会议", meetingQueryService.getDetailIgnoreTenant(existingMeeting.getId()));
|
||||
// }
|
||||
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(command, authContext, loginUser);
|
||||
return ApiResponse.ok(buildAndroidMeetingCreateResponse(meeting));
|
||||
}
|
||||
|
||||
@Operation(summary = "上传Android会议音频")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回上传后的会议 ID 和音频地址",
|
||||
content = @Content(schema = @Schema(implementation = LegacyUploadAudioResponse.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/upload-audio")
|
||||
@Anonymous
|
||||
public ApiResponse<LegacyUploadAudioResponse> uploadAudio(HttpServletRequest request,
|
||||
@RequestParam("id") Long meetingId,
|
||||
@RequestParam(value = "prompt_id", required = false) Long promptId,
|
||||
@RequestParam(value = "model_code", required = false) String modelCode,
|
||||
@RequestParam(value = "force_replace", defaultValue = "false") boolean forceReplace,
|
||||
@RequestParam("audio_file") MultipartFile audioFile) throws IOException {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "上传会议音频接口",
|
||||
"meetingId", meetingId,
|
||||
"promptId", promptId,
|
||||
"modelCode", modelCode,
|
||||
"forceReplace", forceReplace,
|
||||
"audioFile", audioFile);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
if (authContext.isAnonymous()) {
|
||||
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcessForPublicDevice(
|
||||
meetingId,
|
||||
promptId,
|
||||
modelCode,
|
||||
forceReplace,
|
||||
audioFile,
|
||||
authContext
|
||||
));
|
||||
}
|
||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
return ApiResponse.ok(legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
|
||||
meetingId,
|
||||
promptId,
|
||||
modelCode,
|
||||
forceReplace,
|
||||
audioFile,
|
||||
authContext,
|
||||
loginUser
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "结束 Android 离线会议录音阶段")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "结束成功返回 true",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/{meetingId}/finish")
|
||||
@Anonymous
|
||||
public ApiResponse<Object> finishOfflineMeeting(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@RequestBody(required = false) AndroidOfflineMeetingFinishRequest command) throws IOException {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "结束离线会议录音阶段",
|
||||
"meetingId", meetingId,
|
||||
"request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttpIgnoreToken(request, true);
|
||||
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
|
||||
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId);
|
||||
LegacyUploadAudioResponse uploadResult = new LegacyUploadAudioResponse();
|
||||
if (isUploadFinishedStage(command)) {
|
||||
androidChunkUploadService.completeUploadAsync(
|
||||
meeting.getId(),
|
||||
command.getTotalChunks(),
|
||||
authContext
|
||||
);
|
||||
// if (uploadResult == null) {
|
||||
// throw new RuntimeException("分片上传完成后未生成结果");
|
||||
// }
|
||||
uploadResult.setMeetingId(meetingId);
|
||||
}
|
||||
meetingCommandService.finishOfflineMeeting(meeting.getId(), command == null ? null : command.getFinishStage());
|
||||
return ApiResponse.ok(uploadResult);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询Android会议")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回当前用户可见的会议分页结果",
|
||||
content = @Content(schema = @Schema(implementation = PageResult.class))
|
||||
)
|
||||
})
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<List<AndroidMeetingListItemVO>>> list(HttpServletRequest request,
|
||||
@RequestParam(value = "user_id", required = false) Long ignoredUserId,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "page_size", defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String title) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "分页查询会议接口",
|
||||
"userId", ignoredUserId,
|
||||
"page", page,
|
||||
"pageSize", pageSize,
|
||||
"title", title);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
PageResult<List<MeetingVO>> result = meetingQueryService.pageMeetings(
|
||||
page,
|
||||
pageSize,
|
||||
title,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
AndroidLoginUserSupport.resolveDisplayName(authContext),
|
||||
"all",
|
||||
null,
|
||||
AndroidLoginUserSupport.isAdmin(authContext)
|
||||
);
|
||||
return ApiResponse.ok(buildAndroidMeetingListPage(result));
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "查询Android会议统一状态")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回统一状态及可选内容",
|
||||
content = @Content(schema = @Schema(implementation = AndroidUnifiedMeetingStatusResponse.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/{meetingId}/status")
|
||||
@Anonymous
|
||||
public ApiResponse<AndroidUnifiedMeetingStatusResponse> getUnifiedStatus(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@RequestBody(required = false) AndroidUnifiedMeetingStatusRequest command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "查询会议统一状态",
|
||||
"meetingId", meetingId,
|
||||
"request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
|
||||
MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId,false);
|
||||
UnifiedMeetingStatusVO status = meetingUnifiedStatusService.resolve(meetingId);
|
||||
boolean includeTranscript = Boolean.TRUE.equals(command == null ? null : command.getIncludeTranscript());
|
||||
boolean includeSummary = Boolean.TRUE.equals(command == null ? null : command.getIncludeSummary());
|
||||
List<MeetingTranscriptVO> transcripts = includeTranscript ? meetingQueryService.getTranscripts(meetingId) : null;
|
||||
String summaryContent = includeSummary ? meeting.getSummaryContent() : null;
|
||||
AndroidUnifiedMeetingStatusResponse build = AndroidUnifiedMeetingStatusResponse.builder()
|
||||
.meetingId(meetingId)
|
||||
.status(status)
|
||||
.meeting(meeting)
|
||||
.includesTranscript(includeTranscript)
|
||||
.transcripts(transcripts)
|
||||
.includesSummary(includeSummary)
|
||||
.summaryContent(summaryContent)
|
||||
.build();
|
||||
log.info("[{}]{}.返回数据:[{}]", "Android会议", "查询会议统一状态", build);
|
||||
return ApiResponse.ok(build);
|
||||
}
|
||||
|
||||
@Operation(summary = "重试 Android 会议 ASR 识别")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "重试成功返回 true",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/{meetingId}/transcripts/retry")
|
||||
@Anonymous
|
||||
public ApiResponse<Boolean> retryTranscription(HttpServletRequest request, @PathVariable Long meetingId) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "重试会议 ASR 识别接口", "meetingId", meetingId);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
|
||||
meetingCommandService.retryTranscription(meetingId);
|
||||
androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.TRANSCRIBING.getCode());
|
||||
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "重试 Android 会议总结")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "重试成功返回 true",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/{meetingId}/summary/retry")
|
||||
@Anonymous
|
||||
public ApiResponse<Boolean> retrySummary(HttpServletRequest request, @PathVariable Long meetingId) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "重试会议总结接口", "meetingId", meetingId);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
requireOperableOfflineMeeting(meetingId, authContext, loginUser);
|
||||
meetingCommandService.retrySummary(meetingId);
|
||||
androidMeetingPushService.pushMeetingStatusChanged(meetingId, UnifiedMeetingStatusStage.SUMMARIZING.getCode());
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新Android会议访问密码")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回更新后的会议访问密码,传空时表示清空访问密码",
|
||||
content = @Content(schema = @Schema(implementation = String.class))
|
||||
)
|
||||
})
|
||||
@PutMapping("/{meetingId}/access-password")
|
||||
@Log(value = "修改Android会议访问密码", type = "Android会议管理")
|
||||
public ApiResponse<String> updateAccessPassword(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@RequestBody(required = false) LegacyMeetingAccessPasswordRequest command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "更新会议访问密码接口",
|
||||
"meetingId", meetingId,
|
||||
"request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
if (!Objects.equals(meeting.getCreatorId(), loginUser.getUserId())) {
|
||||
return ApiResponse.error("仅会议创建人可设置访问密码");
|
||||
}
|
||||
String password = normalizePassword(command == null ? null : command.getPassword());
|
||||
meetingService.update(new LambdaUpdateWrapper<Meeting>()
|
||||
.eq(Meeting::getId, meeting.getId())
|
||||
.set(Meeting::getAccessPassword, password));
|
||||
return ApiResponse.ok(password);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除Android会议")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回是否删除成功",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@DeleteMapping("/{meetingId}")
|
||||
@Log(value = "删除Android会议", type = "Android会议管理")
|
||||
public ApiResponse<Boolean> delete(HttpServletRequest request, @PathVariable Long meetingId) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "删除会议接口", "meetingId", meetingId);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.deleteMeeting(meetingId);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@GetMapping("/config")
|
||||
@Log(value = "获取会议配置", type = "Android会议管理")
|
||||
@Operation(summary = "获取会议配置")
|
||||
@Anonymous
|
||||
public ApiResponse<AndroidMeetingConfigVo> config(HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android会议", "获取会议配置接口");
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = AndroidLoginUserSupport.toLoginUser(authContext);
|
||||
Long tenantId = loginUser != null ? loginUser.getTenantId() : authContext.getTenantId();
|
||||
Long userId = loginUser != null ? loginUser.getUserId() : null;
|
||||
boolean isPlatformAdmin = loginUser != null && Boolean.TRUE.equals(loginUser.getIsPlatformAdmin());
|
||||
boolean isTenantAdmin = loginUser != null && Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||
AndroidMeetingConfigVo resultVo = new AndroidMeetingConfigVo();
|
||||
PageResult<List<PromptTemplateVO>> promptTemplateList = promptTemplateService.pageTemplates(
|
||||
1,
|
||||
1000,
|
||||
null,
|
||||
null,
|
||||
tenantId,
|
||||
userId,
|
||||
isPlatformAdmin,
|
||||
isTenantAdmin
|
||||
);
|
||||
List<PromptTemplateVO> enabledTemplates = promptTemplateList.getRecords() == null
|
||||
? List.of()
|
||||
: promptTemplateList.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||
.toList();
|
||||
resultVo.setTemplateList(enabledTemplates);
|
||||
PageResult<List<AiModelVO>> modelList = aiModelService.pageModels(1, 1000, null, "LLM", tenantId, false);
|
||||
List<AiModelVO> enabledModels = modelList.getRecords() == null
|
||||
? List.of()
|
||||
: modelList.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()) && Integer.valueOf(1).equals(item.getTenantEnabled()))
|
||||
.toList();
|
||||
resultVo.setModelsList(enabledModels);
|
||||
resultVo.setHotWordGroupList(hotWordGroupService.listVisibleOptions(tenantId));
|
||||
resultVo.setSummaryDegreeOfDetail(dictItemService.getItemsByTypeCode("summary_degree_detail"));
|
||||
resultVo.setMaxMeetingDuration(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_MAX_MEETING_DURATION, "30")));
|
||||
resultVo.setMinMeetingDuration(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_MIN_MEETING_DURATION, "10")));
|
||||
resultVo.setMaxPauseDuration(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_MAX_PAUSE_DURATION, String.valueOf(60 * 4))));
|
||||
BigDecimal bigDecimal = new BigDecimal(paramService.getParamValue(SysParamKeys.MEETING_MAX_PAUSE_DURATION, "99"));
|
||||
bigDecimal = bigDecimal.setScale(2, RoundingMode.HALF_UP);
|
||||
resultVo.setPacketLossRate(bigDecimal);
|
||||
resultVo.setChunkUploadEnabled(Boolean.parseBoolean(paramService.getParamValue(SysParamKeys.MEETING_ANDROID_AUDIO_CHUNK_UPLOAD_ENABLED, "false")));
|
||||
resultVo.setChunkDurationSeconds(Integer.valueOf(paramService.getParamValue(SysParamKeys.MEETING_ANDROID_AUDIO_CHUNK_DURATION_SECONDS, "60")));
|
||||
|
||||
return ApiResponse.ok(resultVo);
|
||||
}
|
||||
|
||||
private void resolvePublicDeviceTenantId(HttpServletRequest request,
|
||||
AndroidOfflineMeetingCreateCommand command,
|
||||
AndroidAuthContext authContext) {
|
||||
if (command == null || command.getTenantId() != null || authContext == null || !authContext.isAnonymous()) {
|
||||
return;
|
||||
}
|
||||
String tenantCode = request == null ? null : request.getHeader(TENANT_CODE_HEADER);
|
||||
if (!StringUtils.hasText(tenantCode)) {
|
||||
throw new IllegalArgumentException("tenantCode不能为空");
|
||||
}
|
||||
SysTenant tenant = sysTenantMapper.selectOne(new LambdaQueryWrapper<SysTenant>()
|
||||
.eq(SysTenant::getTenantCode, tenantCode.trim())
|
||||
.eq(SysTenant::getIsDeleted, 0)
|
||||
.last("LIMIT 1"));
|
||||
if (tenant == null || tenant.getId() == null) {
|
||||
throw new IllegalArgumentException("tenantCode无效,无法获取tenantId");
|
||||
}
|
||||
command.setTenantId(tenant.getId());
|
||||
}
|
||||
|
||||
private AndroidMeetingCreateResponse buildAndroidMeetingCreateResponse(MeetingVO meeting) {
|
||||
AndroidMeetingCreateResponse response = new AndroidMeetingCreateResponse();
|
||||
if (meeting == null) {
|
||||
return response;
|
||||
}
|
||||
BeanUtils.copyProperties(meeting, response);
|
||||
response.setPreviewUrl(buildPreviewUrl(meeting.getId()));
|
||||
return response;
|
||||
}
|
||||
|
||||
private PageResult<List<AndroidMeetingListItemVO>> buildAndroidMeetingListPage(PageResult<List<MeetingVO>> source) {
|
||||
PageResult<List<AndroidMeetingListItemVO>> result = new PageResult<>();
|
||||
if (source == null) {
|
||||
result.setTotal(0L);
|
||||
result.setRecords(List.of());
|
||||
return result;
|
||||
}
|
||||
result.setTotal(source.getTotal());
|
||||
result.setRecords(source.getRecords() == null
|
||||
? List.of()
|
||||
: source.getRecords().stream().map(this::buildAndroidMeetingListItem).toList());
|
||||
return result;
|
||||
}
|
||||
|
||||
private AndroidMeetingListItemVO buildAndroidMeetingListItem(MeetingVO meeting) {
|
||||
AndroidMeetingListItemVO item = new AndroidMeetingListItemVO();
|
||||
if (meeting == null) {
|
||||
return item;
|
||||
}
|
||||
BeanUtils.copyProperties(meeting, item);
|
||||
item.setPreviewUrl(buildPreviewUrl(meeting.getId()));
|
||||
item.setDayOffset(resolveDayOffset(meeting.getMeetingTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private String buildPreviewUrl(Long meetingId) {
|
||||
if (meetingId == null) {
|
||||
return null;
|
||||
}
|
||||
String baseUrl = normalizeH5BaseUrl();
|
||||
if (!StringUtils.hasText(baseUrl)) {
|
||||
return null;
|
||||
}
|
||||
return baseUrl + "/meetings/" + meetingId + "/preview";
|
||||
}
|
||||
|
||||
private String normalizeH5BaseUrl() {
|
||||
String baseUrl = StringUtils.hasText(h5BaseUrl) ? h5BaseUrl.trim() : "";
|
||||
if (!StringUtils.hasText(baseUrl)) {
|
||||
return "";
|
||||
}
|
||||
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
}
|
||||
|
||||
private Long resolveDayOffset(LocalDateTime meetingTime) {
|
||||
if (meetingTime == null) {
|
||||
return null;
|
||||
}
|
||||
return ChronoUnit.DAYS.between(LocalDate.now(), meetingTime.toLocalDate());
|
||||
}
|
||||
|
||||
private Meeting requireOperableOfflineMeeting(Long meetingId, AndroidAuthContext authContext, LoginUser loginUser) {
|
||||
Meeting meeting = meetingAccessService.requireMeetingIgnoreTenant(meetingId);
|
||||
if (!MeetingConstants.TYPE_OFFLINE.equals(meeting.getMeetingType())) {
|
||||
throw new RuntimeException("当前会议不是离线会议");
|
||||
}
|
||||
if (authContext == null || authContext.getDeviceId() == null || authContext.getDeviceId().isBlank()) {
|
||||
throw new RuntimeException("设备ID不能为空");
|
||||
}
|
||||
return meeting;
|
||||
}
|
||||
|
||||
private boolean isUploadFinishedStage(AndroidOfflineMeetingFinishRequest command) {
|
||||
return command != null
|
||||
&& MeetingConstants.OFFLINE_RECORDING_UPLOAD_FINISHED.equalsIgnoreCase(command.getFinishStage());
|
||||
}
|
||||
|
||||
private String normalizePassword(String password) {
|
||||
if (password == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = password.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.config.grpc.GrpcServerProperties;
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.AndroidCreateRealtimeMeetingCommand;
|
||||
import com.imeeting.dto.android.AndroidCreateRealtimeMeetingVO;
|
||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingAuthorizationService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "Android实时会议")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/meeting")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidMeetingRealtimeController {
|
||||
|
||||
private static final DateTimeFormatter TITLE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final MeetingAuthorizationService meetingAuthorizationService;
|
||||
private final MeetingQueryService meetingQueryService;
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver;
|
||||
private final GrpcServerProperties grpcServerProperties;
|
||||
|
||||
@Operation(summary = "创建 Android 实时会议")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回实时会议创建结果和当前生效的运行时参数",
|
||||
content = @Content(schema = @Schema(implementation = AndroidCreateRealtimeMeetingVO.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/realtime/create")
|
||||
@Log(value = "新增Android实时会议", type = "Android实时会议")
|
||||
public ApiResponse<AndroidCreateRealtimeMeetingVO> createRealtimeMeeting(HttpServletRequest request,
|
||||
@RequestBody(required = false) AndroidCreateRealtimeMeetingCommand command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android实时会议", "创建实时会议接口", "command", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
meetingAuthorizationService.assertCanCreateMeeting(authContext);
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile = meetingRuntimeProfileResolver.resolve(
|
||||
authContext.getTenantId(),
|
||||
command == null ? null : command.getAsrModelId(),
|
||||
command == null ? null : command.getSummaryModelId(),
|
||||
command == null ? null : command.getPromptId(),
|
||||
command == null ? null : command.getMode(),
|
||||
command == null ? null : command.getLanguage(),
|
||||
command == null ? null : command.getUseSpkId(),
|
||||
command == null ? null : command.getEnablePunctuation(),
|
||||
command == null ? null : command.getEnableItn(),
|
||||
command == null ? null : command.getEnableTextRefine(),
|
||||
command == null ? null : command.getSaveAudio(),
|
||||
command == null ? null : command.getHotWordGroupId(),
|
||||
command == null ? null : command.getHotWords()
|
||||
);
|
||||
CreateRealtimeMeetingCommand createCommand = buildCreateCommand(command, authContext, runtimeProfile);
|
||||
MeetingVO meeting = meetingCommandService.createRealtimeMeeting(
|
||||
createCommand,
|
||||
authContext.getTenantId(),
|
||||
authContext.getUserId(),
|
||||
resolveCreatorName(authContext),
|
||||
MeetingConstants.SOURCE_ANDROID
|
||||
);
|
||||
|
||||
RealtimeMeetingSessionStatusVO status = realtimeMeetingSessionStateService.getStatus(meeting.getId());
|
||||
AndroidCreateRealtimeMeetingVO vo = new AndroidCreateRealtimeMeetingVO();
|
||||
vo.setMeetingId(meeting.getId());
|
||||
vo.setTitle(meeting.getTitle());
|
||||
vo.setHostUserId(meeting.getHostUserId());
|
||||
vo.setHostName(meeting.getHostName());
|
||||
vo.setSampleRate(grpcServerProperties.getRealtime().getSampleRate());
|
||||
vo.setChannels(grpcServerProperties.getRealtime().getChannels());
|
||||
vo.setEncoding(grpcServerProperties.getRealtime().getEncoding());
|
||||
vo.setResolvedAsrModelId(runtimeProfile.getResolvedAsrModelId());
|
||||
vo.setResolvedAsrModelName(runtimeProfile.getResolvedAsrModelName());
|
||||
vo.setResolvedSummaryModelId(runtimeProfile.getResolvedSummaryModelId());
|
||||
vo.setResolvedSummaryModelName(runtimeProfile.getResolvedSummaryModelName());
|
||||
vo.setResolvedPromptId(runtimeProfile.getResolvedPromptId());
|
||||
vo.setResolvedPromptName(runtimeProfile.getResolvedPromptName());
|
||||
vo.setResumeConfig(status == null ? null : status.getResumeConfig());
|
||||
vo.setStatus(status);
|
||||
return ApiResponse.ok(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询 Android 实时会议状态")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回实时会议当前状态和恢复信息",
|
||||
content = @Content(schema = @Schema(implementation = RealtimeMeetingSessionStatusVO.class))
|
||||
)
|
||||
})
|
||||
@GetMapping("/{id}/realtime/session-status")
|
||||
public ApiResponse<RealtimeMeetingSessionStatusVO> getRealtimeSessionStatus(@PathVariable Long id, HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android实时会议", "查询实时会议状态接口", "meetingId", id);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAuthorizationService.assertCanManageRealtimeMeeting(meeting, authContext);
|
||||
return ApiResponse.ok(realtimeMeetingSessionStateService.getStatus(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询 Android 会议转写")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回会议转写记录列表",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = MeetingTranscriptVO.class)))
|
||||
)
|
||||
})
|
||||
@GetMapping("/{id}/transcripts")
|
||||
public ApiResponse<List<MeetingTranscriptVO>> getTranscripts(@PathVariable Long id, HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android实时会议", "查询会议转写接口", "meetingId", id);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAuthorizationService.assertCanViewMeeting(meeting, authContext);
|
||||
return ApiResponse.ok(meetingQueryService.getTranscripts(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "暂停 Android 实时会议")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回暂停后的实时会议状态",
|
||||
content = @Content(schema = @Schema(implementation = RealtimeMeetingSessionStatusVO.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/{id}/realtime/pause")
|
||||
public ApiResponse<RealtimeMeetingSessionStatusVO> pauseRealtimeMeeting(@PathVariable Long id, HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android实时会议", "暂停实时会议接口", "meetingId", id);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAuthorizationService.assertCanControlRealtimeMeeting(meeting, authContext, MeetingConstants.SOURCE_ANDROID);
|
||||
return ApiResponse.ok(realtimeMeetingSessionStateService.pause(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成 Android 实时会议")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回实时会议完成是否成功",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/{id}/realtime/complete")
|
||||
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id,
|
||||
HttpServletRequest request,
|
||||
@RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android实时会议", "完成实时会议接口", "meetingId", id, "request", dto);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAuthorizationService.assertCanControlRealtimeMeeting(meeting, authContext, MeetingConstants.SOURCE_ANDROID);
|
||||
meetingCommandService.completeRealtimeMeeting(
|
||||
id,
|
||||
dto != null ? dto.getAudioUrl() : null,
|
||||
dto != null && Boolean.TRUE.equals(dto.getOverwriteAudio())
|
||||
);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
private CreateRealtimeMeetingCommand buildCreateCommand(AndroidCreateRealtimeMeetingCommand command,
|
||||
AndroidAuthContext authContext,
|
||||
RealtimeMeetingRuntimeProfile runtimeProfile) {
|
||||
CreateRealtimeMeetingCommand createCommand = new CreateRealtimeMeetingCommand();
|
||||
LocalDateTime meetingTime = command != null && command.getMeetingTime() != null ? command.getMeetingTime() : LocalDateTime.now();
|
||||
createCommand.setTitle(resolveMeetingTitle(command, meetingTime));
|
||||
createCommand.setMeetingTime(meetingTime);
|
||||
createCommand.setParticipants(command == null ? "" : normalize(command.getParticipants(), ""));
|
||||
createCommand.setTags(command == null ? "" : normalize(command.getTags()));
|
||||
createCommand.setHostUserId(resolveHostUserId(command, authContext));
|
||||
createCommand.setHostName(resolveHostName(command, authContext, createCommand.getHostUserId()));
|
||||
createCommand.setAsrModelId(runtimeProfile.getResolvedAsrModelId());
|
||||
createCommand.setSummaryModelId(runtimeProfile.getResolvedSummaryModelId());
|
||||
createCommand.setPromptId(runtimeProfile.getResolvedPromptId());
|
||||
createCommand.setHotWordGroupId(runtimeProfile.getResolvedHotWordGroupId());
|
||||
createCommand.setSummaryDetailLevel(command == null ? null : command.getSummaryDetailLevel());
|
||||
createCommand.setMode(runtimeProfile.getResolvedMode());
|
||||
createCommand.setLanguage(runtimeProfile.getResolvedLanguage());
|
||||
createCommand.setUseSpkId(runtimeProfile.getResolvedUseSpkId());
|
||||
createCommand.setEnablePunctuation(runtimeProfile.getResolvedEnablePunctuation());
|
||||
createCommand.setEnableItn(runtimeProfile.getResolvedEnableItn());
|
||||
createCommand.setEnableTextRefine(runtimeProfile.getResolvedEnableTextRefine());
|
||||
createCommand.setSaveAudio(runtimeProfile.getResolvedSaveAudio());
|
||||
createCommand.setHotWords(runtimeProfile.getResolvedHotWords());
|
||||
return createCommand;
|
||||
}
|
||||
|
||||
private String resolveMeetingTitle(AndroidCreateRealtimeMeetingCommand command, LocalDateTime meetingTime) {
|
||||
String title = command == null ? null : normalize(command.getTitle());
|
||||
if (title != null && !title.isBlank()) {
|
||||
return title;
|
||||
}
|
||||
return "Android-Realtime-Meeting-" + TITLE_TIME_FORMATTER.format(meetingTime);
|
||||
}
|
||||
|
||||
private Long resolveHostUserId(AndroidCreateRealtimeMeetingCommand command, AndroidAuthContext authContext) {
|
||||
if (command != null && command.getHostUserId() != null) {
|
||||
return command.getHostUserId();
|
||||
}
|
||||
return authContext.getUserId();
|
||||
}
|
||||
|
||||
private String resolveHostName(AndroidCreateRealtimeMeetingCommand command, AndroidAuthContext authContext, Long hostUserId) {
|
||||
if (command != null && command.getHostName() != null && !command.getHostName().isBlank()) {
|
||||
return command.getHostName().trim();
|
||||
}
|
||||
if (hostUserId != null && hostUserId.equals(authContext.getUserId())) {
|
||||
return resolveCreatorName(authContext);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveCreatorName(AndroidAuthContext authContext) {
|
||||
if (authContext == null) {
|
||||
return "android";
|
||||
}
|
||||
if (authContext.getDisplayName() != null && !authContext.getDisplayName().isBlank()) {
|
||||
return authContext.getDisplayName().trim();
|
||||
}
|
||||
if (authContext.getUsername() != null && !authContext.getUsername().isBlank()) {
|
||||
return authContext.getUsername().trim();
|
||||
}
|
||||
return authContext.getDeviceId() == null || authContext.getDeviceId().isBlank()
|
||||
? "android"
|
||||
: "android:" + authContext.getDeviceId().trim();
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return normalize(value, null);
|
||||
}
|
||||
|
||||
private String normalize(String value, String defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
String normalized = value.trim();
|
||||
return normalized.isEmpty() ? defaultValue : normalized;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "Android提示词接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/prompts")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidPromptController {
|
||||
|
||||
private static final String LEGACY_MEETING_SCENE = "MEETING_TASK";
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
|
||||
@Operation(summary = "查询场景提示词")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回指定场景下当前用户可用且启用的提示词模板列表",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PromptTemplateVO.class)))
|
||||
)
|
||||
})
|
||||
@GetMapping("/active/{scene}")
|
||||
public ApiResponse<List<PromptTemplateVO>> activePrompts(HttpServletRequest request, @PathVariable String scene) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android提示词", "查询场景提示词接口", "scene", scene);
|
||||
if (!LEGACY_MEETING_SCENE.equals(scene)) {
|
||||
return ApiResponse.error("scene 仅支持 MEETING_TASK");
|
||||
}
|
||||
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext);
|
||||
PageResult<List<PromptTemplateVO>> result = promptTemplateService.pageTemplates(
|
||||
1,
|
||||
1000,
|
||||
null,
|
||||
null,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin()
|
||||
);
|
||||
List<PromptTemplateVO> enabledTemplates = result.getRecords() == null
|
||||
? List.of()
|
||||
: result.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||
.toList();
|
||||
return ApiResponse.ok(enabledTemplates);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.AndroidPushMessageVO;
|
||||
import com.imeeting.dto.android.AndroidPublicMeetingSessionResultVO;
|
||||
import com.imeeting.dto.android.AndroidPublicMeetingSessionRequest;
|
||||
import com.imeeting.entity.biz.AndroidPushMessage;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.enums.MeetingStatusEnum;
|
||||
import com.imeeting.enums.MeetingPushTypeEnum;
|
||||
import com.imeeting.mapper.DeviceInfoMapper;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.android.AndroidPushMessageService;
|
||||
import com.imeeting.service.android.AndroidPublicMeetingSessionService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.MeetingService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.annotation.Anonymous;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "Android公有设备会议接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/public-meetings")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidPublicMeetingController {
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final AndroidPublicMeetingSessionService androidPublicMeetingSessionService;
|
||||
private final AndroidPushMessageService androidPushMessageService;
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
private final MeetingService meetingService;
|
||||
private final DeviceInfoMapper deviceInfoMapper;
|
||||
|
||||
@Operation(summary = "创建公有设备扫码发会会话")
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "优先返回待处理扫码消息,否则返回扫码会话二维码",
|
||||
content = @Content(schema = @Schema(implementation = AndroidPublicMeetingSessionResultVO.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/session")
|
||||
@Anonymous
|
||||
public ApiResponse<AndroidPublicMeetingSessionResultVO> createSession(HttpServletRequest request,
|
||||
@RequestBody(required = false) AndroidPublicMeetingSessionRequest command) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android公有会议", "创建扫码发会会话",
|
||||
"request", command);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
assertPublicDevice(authContext.getDeviceId());
|
||||
AndroidPushMessage pendingMessage = androidPushMessageService.findLatestPendingMessage(
|
||||
authContext.getDeviceId(),
|
||||
MeetingPushTypeEnum.PUBLIC_MEETING_LOGIN_CONFIRM.getCode()
|
||||
);
|
||||
AndroidPublicMeetingSessionResultVO result = new AndroidPublicMeetingSessionResultVO();
|
||||
if (pendingMessage != null) {
|
||||
result.setMode("PENDING_MESSAGE");
|
||||
result.setMessage(androidPushMessageService.toPushMessageVO(pendingMessage));
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
result.setMode("QR_CODE");
|
||||
result.setQrCode(androidPublicMeetingSessionService.create(
|
||||
authContext.getDeviceId(),
|
||||
command == null ? null : command.getTitle()
|
||||
));
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "主动拉取未确认扫码消息")
|
||||
@GetMapping("/pending-login-message")
|
||||
@Anonymous
|
||||
public ApiResponse<AndroidPushMessageVO> pullPendingLoginMessage(HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android公有会议", "主动拉取未确认扫码消息");
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
assertPublicDevice(authContext.getDeviceId());
|
||||
AndroidPushMessage pendingMessage = androidPushMessageService.findLatestPendingMessage(
|
||||
authContext.getDeviceId(),
|
||||
MeetingPushTypeEnum.PUBLIC_MEETING_LOGIN_CONFIRM.getCode()
|
||||
);
|
||||
return ApiResponse.ok(androidPushMessageService.toPushMessageVO(pendingMessage));
|
||||
}
|
||||
|
||||
@Operation(summary = "公有设备删除未开始会议")
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "删除成功返回 true",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@DeleteMapping("/{meetingId}")
|
||||
@Anonymous
|
||||
public ApiResponse<Boolean> deletePendingMeeting(HttpServletRequest request, @PathVariable Long meetingId) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android公有会议", "删除未开始会议", "meetingId", meetingId);
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
assertPublicDevice(authContext.getDeviceId());
|
||||
Meeting meeting = meetingService.getById(meetingId);
|
||||
if (meeting == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
}
|
||||
if (meeting.getSourceDeviceCode() == null || !meeting.getSourceDeviceCode().equals(authContext.getDeviceId())) {
|
||||
throw new RuntimeException("当前会议不属于该设备");
|
||||
}
|
||||
if (MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED)
|
||||
|| MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.FAILED)) {
|
||||
throw new RuntimeException("当前会议状态不允许删除");
|
||||
}
|
||||
meetingCommandService.deleteMeeting(meetingId);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
private void assertPublicDevice(String deviceId) {
|
||||
if (deviceId == null || deviceId.isBlank()) {
|
||||
throw new RuntimeException("设备ID不能为空");
|
||||
}
|
||||
var device = deviceInfoMapper.selectByDeviceCodeIgnoreTenant(deviceId);
|
||||
if (device == null) {
|
||||
throw new RuntimeException("设备未注册,请先完成设备注册");
|
||||
}
|
||||
if (device != null && device.getUserId() != null) {
|
||||
throw new RuntimeException("当前设备为私有设备,请走私有设备发会流程");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.imeeting.controller.android;
|
||||
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.AndroidScreenSaverCatalogVO;
|
||||
import com.imeeting.dto.android.AndroidScreenSaverItemVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverSelectionResult;
|
||||
import com.imeeting.service.android.AndroidAuthService;
|
||||
import com.imeeting.service.biz.ScreenSaverService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.imeeting.support.TaskSecurityContextRunner;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "Android屏保")
|
||||
@RestController
|
||||
@RequestMapping("/api/android/screensavers")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AndroidScreenSaverController {
|
||||
|
||||
private final AndroidAuthService androidAuthService;
|
||||
private final ScreenSaverService screenSaverService;
|
||||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||
@Value("${imeeting.h5.base-url:}")
|
||||
private String h5BaseUrl;
|
||||
@Operation(summary = "获取当前生效屏保")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回当前生效的屏保配置和轮播项列表",
|
||||
content = @Content(schema = @Schema(implementation = AndroidScreenSaverCatalogVO.class))
|
||||
)
|
||||
})
|
||||
@GetMapping("/active")
|
||||
public ApiResponse<AndroidScreenSaverCatalogVO> active(HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "Android屏保", "获取当前生效屏保接口");
|
||||
AndroidAuthContext authContext = androidAuthService.authenticateHttp(request);
|
||||
ScreenSaverSelectionResult selection = querySelection(authContext);
|
||||
AndroidScreenSaverCatalogVO vo = new AndroidScreenSaverCatalogVO();
|
||||
vo.setRefreshIntervalSec(300);
|
||||
vo.setH5BaseUrl(StringUtils.hasText(h5BaseUrl) ? h5BaseUrl.trim() : null);
|
||||
vo.setPlayMode("SEQUENTIAL");
|
||||
vo.setDisplayDurationSec(selection.getDisplayDurationSec());
|
||||
vo.setSourceScope(selection.getSourceScope());
|
||||
vo.setItems(selection.getItems().stream().map(item -> {
|
||||
AndroidScreenSaverItemVO child = new AndroidScreenSaverItemVO();
|
||||
child.setId(item.getId());
|
||||
child.setName(item.getName());
|
||||
child.setImageUrl(item.getImageUrl());
|
||||
child.setDescription(item.getDescription());
|
||||
child.setSortOrder(item.getSortOrder());
|
||||
child.setUpdatedAt(item.getUpdatedAt());
|
||||
return child;
|
||||
}).toList());
|
||||
return ApiResponse.ok(vo);
|
||||
}
|
||||
|
||||
private ScreenSaverSelectionResult querySelection(AndroidAuthContext authContext) {
|
||||
if (authContext == null || authContext.isAnonymous()
|
||||
|| authContext.getUserId() == null || authContext.getTenantId() == null) {
|
||||
return screenSaverService.getActiveSelection(null);
|
||||
}
|
||||
return taskSecurityContextRunner.callAsTenantUser(
|
||||
authContext.getTenantId(),
|
||||
authContext.getUserId(),
|
||||
() -> screenSaverService.getActiveSelection(authContext.getUserId())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.google.protobuf.ServiceException;
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyLoginResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyLoginUserResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyRefreshTokenResponse;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.dto.LoginRequest;
|
||||
import com.unisbase.dto.RefreshRequest;
|
||||
import com.unisbase.dto.SysRoleDTO;
|
||||
import com.unisbase.dto.SysUserDTO;
|
||||
import com.unisbase.dto.TokenResponse;
|
||||
import com.unisbase.service.AuthService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "兼容认证接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LegacyAuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@Operation(summary = "兼容登录")
|
||||
@PostMapping("/login")
|
||||
public LegacyApiResponse<LegacyLoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容认证", "登录接口", "request", request);
|
||||
TokenResponse tokenResponse = null;
|
||||
try {
|
||||
tokenResponse = authService.login(request, true);
|
||||
} catch (Exception e) {
|
||||
return LegacyApiResponse.error("400",e.getMessage());
|
||||
}
|
||||
try {
|
||||
tokenResponse.getUser().setTenantId(tokenResponse.getAvailableTenants().stream().findFirst().orElseThrow(()-> new ServiceException("未绑定租户")).getTenantId());
|
||||
} catch (ServiceException e) {
|
||||
return LegacyApiResponse.error("400",e.getMessage());
|
||||
}
|
||||
return LegacyApiResponse.ok(new LegacyLoginResponse(
|
||||
tokenResponse.getAccessToken(),
|
||||
tokenResponse.getRefreshToken(),
|
||||
toLegacyUser(tokenResponse.getUser())
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容刷新令牌")
|
||||
@PostMapping("/refresh")
|
||||
public LegacyApiResponse<LegacyRefreshTokenResponse> refresh(@RequestBody(required = false) RefreshRequest request,
|
||||
@RequestHeader(value = "Authorization", required = false) String authorization,
|
||||
@RequestHeader(value = "X-Android-Access-Token", required = false) String androidAccessToken) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容认证", "刷新令牌接口",
|
||||
"request", request,
|
||||
"authorization", authorization,
|
||||
"androidAccessToken", androidAccessToken);
|
||||
TokenResponse tokenResponse = null;
|
||||
try {
|
||||
tokenResponse = authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken));
|
||||
} catch (Exception e) {
|
||||
return LegacyApiResponse.error("400",e.getMessage());
|
||||
}
|
||||
return LegacyApiResponse.ok(new LegacyRefreshTokenResponse(tokenResponse.getAccessToken(),tokenResponse.getRefreshToken()));
|
||||
}
|
||||
|
||||
private LegacyLoginUserResponse toLegacyUser(SysUserDTO user) {
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
SysRoleDTO primaryRole = resolvePrimaryRole(user);
|
||||
return new LegacyLoginUserResponse(
|
||||
user.getUserId(),
|
||||
user.getTenantId(),
|
||||
user.getUsername(),
|
||||
user.getDisplayName(),
|
||||
user.getAvatarUrl(),
|
||||
user.getEmail(),
|
||||
primaryRole == null ? null : primaryRole.getRoleId(),
|
||||
primaryRole == null ? null : primaryRole.getRoleName(),
|
||||
user.getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private SysRoleDTO resolvePrimaryRole(SysUserDTO user) {
|
||||
List<SysRoleDTO> roles = user.getRoles();
|
||||
if (roles != null && !roles.isEmpty()) {
|
||||
return roles.get(0);
|
||||
}
|
||||
List<Long> roleIds = user.getRoleIds();
|
||||
if (roleIds != null && !roleIds.isEmpty()) {
|
||||
SysRoleDTO role = new SysRoleDTO();
|
||||
role.setRoleId(roleIds.get(0));
|
||||
return role;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveRefreshToken(RefreshRequest request, String authorization, String androidAccessToken) {
|
||||
if (request != null && StringUtils.hasText(request.getRefreshToken())) {
|
||||
return request.getRefreshToken().trim();
|
||||
}
|
||||
if (StringUtils.hasText(androidAccessToken)) {
|
||||
return androidAccessToken.trim();
|
||||
}
|
||||
if (StringUtils.hasText(authorization)) {
|
||||
String value = authorization.trim();
|
||||
if (value.startsWith("Bearer ")) {
|
||||
return value.substring(7).trim();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
throw new IllegalArgumentException("refreshToken不能为空");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyClientDownloadResponse;
|
||||
import com.imeeting.service.android.legacy.LegacyCatalogAdapterService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "兼容客户端下载接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/clients")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LegacyClientController {
|
||||
|
||||
private final LegacyCatalogAdapterService legacyCatalogAdapterService;
|
||||
|
||||
@Operation(summary = "查询平台最新客户端")
|
||||
@GetMapping("/latest/by-platform")
|
||||
public LegacyApiResponse<LegacyClientDownloadResponse> latestByPlatform(@RequestParam(value = "platform_code", required = false) String platformCode,
|
||||
@RequestParam(value = "platform_type", required = false) String platformType,
|
||||
@RequestParam(value = "platform_name", required = false) String platformName) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容客户端", "查询平台最新客户端接口",
|
||||
"platformCode", platformCode,
|
||||
"platformType", platformType,
|
||||
"platformName", platformName);
|
||||
if ((platformCode == null || platformCode.isBlank())
|
||||
&& ((platformType == null || platformType.isBlank()) || (platformName == null || platformName.isBlank()))) {
|
||||
return LegacyApiResponse.error("400", "请提供 platform_code 参数");
|
||||
}
|
||||
|
||||
LegacyClientDownloadResponse response = legacyCatalogAdapterService.getLatestClient(platformCode, platformType, platformName);
|
||||
if (response == null) {
|
||||
return LegacyApiResponse.error("404", "暂无最新客户端");
|
||||
}
|
||||
return LegacyApiResponse.ok(response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyExternalAppItemResponse;
|
||||
import com.imeeting.service.android.legacy.LegacyCatalogAdapterService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "兼容外部应用接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/external-apps")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LegacyExternalAppController {
|
||||
|
||||
private final LegacyCatalogAdapterService legacyCatalogAdapterService;
|
||||
|
||||
@Operation(summary = "查询启用的外部应用")
|
||||
@GetMapping("/active")
|
||||
public LegacyApiResponse<List<LegacyExternalAppItemResponse>> active(@RequestParam(value = "is_active", required = false) Integer ignoredIsActive) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容外部应用", "查询启用外部应用接口", "isActive", ignoredIsActive);
|
||||
return LegacyApiResponse.ok(legacyCatalogAdapterService.listActiveExternalApps());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyLlmModelItemResponse;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.Objects;
|
||||
|
||||
@Tag(name = "兼容模型接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/llm-models")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LegacyLlmModelController {
|
||||
|
||||
private final AiModelService aiModelService;
|
||||
|
||||
@Operation(summary = "查询启用的大模型列表")
|
||||
@GetMapping("/active")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<List<LegacyLlmModelItemResponse>> activeModels() {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容模型", "查询启用大模型列表接口");
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
PageResult<List<AiModelVO>> result = aiModelService.pageModels(1, 1000, null, "LLM", loginUser.getTenantId(), false);
|
||||
List<AiModelVO> enabledModels = result.getRecords() == null
|
||||
? List.of()
|
||||
: result.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||
.toList();
|
||||
boolean hasExplicitDefault = enabledModels.stream().anyMatch(item -> Integer.valueOf(1).equals(item.getIsDefault()));
|
||||
Long fallbackDefaultId = enabledModels.isEmpty() ? null : enabledModels.get(0).getId();
|
||||
List<LegacyLlmModelItemResponse> models = enabledModels.stream()
|
||||
.map(item -> LegacyLlmModelItemResponse.from(
|
||||
item,
|
||||
Integer.valueOf(1).equals(item.getIsDefault())
|
||||
|| (!hasExplicitDefault && Objects.equals(item.getId(), fallbackDefaultId))
|
||||
))
|
||||
.toList();
|
||||
return LegacyApiResponse.ok(models);
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,644 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.AndroidAuthContext;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordRequest;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingAccessPasswordResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingAttendeeResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingCreateResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingItemResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingListResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewDataResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingPreviewResult;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingProcessingStatusResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingTagResponse;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.entity.biz.PromptTemplate;
|
||||
import com.imeeting.enums.MeetingStatusEnum;
|
||||
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.MeetingProgressService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.imeeting.service.biz.MeetingService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Tag(name = "兼容会议接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/meetings")
|
||||
@Slf4j
|
||||
public class LegacyMeetingController {
|
||||
|
||||
private static final String STAGE_DATA_INITIALIZATION = "data_initialization";
|
||||
private static final String STAGE_AUDIO_TRANSCRIPTION = "audio_transcription";
|
||||
private static final String STAGE_SUMMARY_GENERATION = "summary_generation";
|
||||
private static final String STAGE_COMPLETED = "completed";
|
||||
|
||||
private final LegacyMeetingAdapterService legacyMeetingAdapterService;
|
||||
private final MeetingQueryService meetingQueryService;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
private final MeetingService meetingService;
|
||||
private final AiTaskService aiTaskService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final MeetingTranscriptMapper meetingTranscriptMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final MeetingProgressService meetingProgressService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||
MeetingQueryService meetingQueryService,
|
||||
MeetingAccessService meetingAccessService,
|
||||
MeetingCommandService meetingCommandService,
|
||||
MeetingService meetingService,
|
||||
AiTaskService aiTaskService,
|
||||
PromptTemplateService promptTemplateService,
|
||||
MeetingTranscriptMapper meetingTranscriptMapper,
|
||||
SysUserMapper sysUserMapper) {
|
||||
this(legacyMeetingAdapterService,
|
||||
meetingQueryService,
|
||||
meetingAccessService,
|
||||
meetingCommandService,
|
||||
meetingService,
|
||||
aiTaskService,
|
||||
promptTemplateService,
|
||||
meetingTranscriptMapper,
|
||||
sysUserMapper,
|
||||
(MeetingProgressService) null,
|
||||
new ObjectMapper());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public LegacyMeetingController(LegacyMeetingAdapterService legacyMeetingAdapterService,
|
||||
MeetingQueryService meetingQueryService,
|
||||
MeetingAccessService meetingAccessService,
|
||||
MeetingCommandService meetingCommandService,
|
||||
MeetingService meetingService,
|
||||
AiTaskService aiTaskService,
|
||||
PromptTemplateService promptTemplateService,
|
||||
MeetingTranscriptMapper meetingTranscriptMapper,
|
||||
SysUserMapper sysUserMapper,
|
||||
MeetingProgressService meetingProgressService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.legacyMeetingAdapterService = legacyMeetingAdapterService;
|
||||
this.meetingQueryService = meetingQueryService;
|
||||
this.meetingAccessService = meetingAccessService;
|
||||
this.meetingCommandService = meetingCommandService;
|
||||
this.meetingService = meetingService;
|
||||
this.aiTaskService = aiTaskService;
|
||||
this.promptTemplateService = promptTemplateService;
|
||||
this.meetingTranscriptMapper = meetingTranscriptMapper;
|
||||
this.sysUserMapper = sysUserMapper;
|
||||
this.meetingProgressService = meetingProgressService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容创建会议")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增兼容会议", type = "兼容会议管理")
|
||||
public LegacyApiResponse<LegacyMeetingCreateResponse> create(@RequestBody LegacyMeetingCreateRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容会议", "创建会议接口", "request", request);
|
||||
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(request, buildLegacyAuthContext(), currentLoginUser());
|
||||
return LegacyApiResponse.ok(new LegacyMeetingCreateResponse(meeting.getId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容上传会议音频")
|
||||
@PostMapping("/upload-audio")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<Void> uploadAudio(@RequestParam("meeting_id") Long meetingId,
|
||||
@RequestParam(value = "prompt_id", required = false) Long promptId,
|
||||
@RequestParam(value = "model_code", required = false) String modelCode,
|
||||
@RequestParam(value = "force_replace", defaultValue = "false") boolean forceReplace,
|
||||
@RequestParam("audio_file") MultipartFile audioFile) throws IOException {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容会议", "上传会议音频接口",
|
||||
"meetingId", meetingId,
|
||||
"promptId", promptId,
|
||||
"modelCode", modelCode,
|
||||
"forceReplace", forceReplace,
|
||||
"audioFile", audioFile);
|
||||
legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
|
||||
meetingId,
|
||||
promptId,
|
||||
modelCode,
|
||||
forceReplace,
|
||||
audioFile,
|
||||
buildLegacyAuthContext(),
|
||||
currentLoginUser()
|
||||
);
|
||||
return LegacyApiResponse.ok("上传成功", null);
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容分页查询会议")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyMeetingListResponse> list(@RequestParam(value = "user_id", required = false) Long ignoredUserId,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "page_size", defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) String title) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容会议", "分页查询会议接口",
|
||||
"userId", ignoredUserId,
|
||||
"page", page,
|
||||
"pageSize", pageSize,
|
||||
"title", title);
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||
PageResult<List<MeetingVO>> result = meetingQueryService.pageMeetings(
|
||||
page,
|
||||
pageSize,
|
||||
title,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
"all",
|
||||
null,
|
||||
isAdmin
|
||||
);
|
||||
|
||||
LegacyMeetingListResponse data = new LegacyMeetingListResponse();
|
||||
data.setPage(page);
|
||||
data.setPageSize(pageSize);
|
||||
data.setTotal(result.getTotal());
|
||||
data.setTotalPages(pageSize == null || pageSize <= 0 ? 0 : (result.getTotal() + pageSize - 1) / pageSize);
|
||||
data.setHasMore(page != null && page < data.getTotalPages());
|
||||
data.setMeetings(result.getRecords() == null
|
||||
? List.of()
|
||||
: result.getRecords().stream().map(this::buildListItem).toList());
|
||||
return LegacyApiResponse.ok(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容查询会议预览数据")
|
||||
@GetMapping("/{meetingId}/preview-data")
|
||||
public LegacyApiResponse<?> previewData(@PathVariable Long meetingId) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容会议", "查询会议预览数据接口", "meetingId", meetingId);
|
||||
LegacyMeetingPreviewResult result = buildPreviewResult(meetingId);
|
||||
return new LegacyApiResponse<>(result.getCode(), result.getMessage(), result.getData());
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容更新会议访问密码")
|
||||
@PutMapping("/{meetingId}/access-password")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改兼容会议访问密码", type = "兼容会议管理")
|
||||
public LegacyApiResponse<LegacyMeetingAccessPasswordResponse> updateAccessPassword(@PathVariable Long meetingId,
|
||||
@RequestBody(required = false) LegacyMeetingAccessPasswordRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容会议", "更新会议访问密码接口",
|
||||
"meetingId", meetingId,
|
||||
"request", request);
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
if (!Objects.equals(meeting.getCreatorId(), loginUser.getUserId())) {
|
||||
return LegacyApiResponse.error("403", "仅会议创建人可设置访问密码");
|
||||
}
|
||||
String password = normalizePassword(request == null ? null : request.getPassword());
|
||||
meetingService.update(new LambdaUpdateWrapper<Meeting>()
|
||||
.eq(Meeting::getId,meeting.getId())
|
||||
.set(Meeting::getAccessPassword, password));
|
||||
return LegacyApiResponse.ok(new LegacyMeetingAccessPasswordResponse(password));
|
||||
}
|
||||
|
||||
@Operation(summary = "兼容删除会议")
|
||||
@DeleteMapping("/{meetingId}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除兼容会议", type = "兼容会议管理")
|
||||
public LegacyApiResponse<Void> delete(@PathVariable Long meetingId) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容会议", "删除会议接口", "meetingId", meetingId);
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.deleteMeeting(meetingId);
|
||||
return LegacyApiResponse.ok("删除成功", null);
|
||||
}
|
||||
|
||||
private LegacyMeetingPreviewResult buildPreviewResult(Long meetingId) {
|
||||
Meeting meeting = meetingService.getById(meetingId);
|
||||
if (meeting == null) {
|
||||
return new LegacyMeetingPreviewResult("404", "会议不存在", null);
|
||||
}
|
||||
|
||||
AiTask asrTask = findLatestTask(meetingId, "ASR");
|
||||
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
|
||||
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
|
||||
MeetingVO detail = (MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED) || summaryCompleted)
|
||||
? meetingQueryService.getDetail(meetingId)
|
||||
: null;
|
||||
boolean hasSummary = detail != null && detail.getSummaryContent() != null && !detail.getSummaryContent().isBlank();
|
||||
|
||||
if (hasSummary) {
|
||||
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask));
|
||||
}
|
||||
if (summaryCompleted) {
|
||||
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, detail, summaryTask));
|
||||
}
|
||||
// if (summaryCompleted) {
|
||||
// return new LegacyMeetingPreviewResult(
|
||||
// "504",
|
||||
// "处理已完成,但摘要尚未同步,请稍后重试",
|
||||
// buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED))
|
||||
// );
|
||||
// }
|
||||
if (isFailed(asrTask)) {
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"503",
|
||||
buildFailureMessage(asrTask, "转译"),
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 50, STAGE_AUDIO_TRANSCRIPTION))
|
||||
);
|
||||
}
|
||||
if (isFailed(summaryTask)) {
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"503",
|
||||
buildFailureMessage(summaryTask, "总结"),
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("转译或总结失败", 75, STAGE_SUMMARY_GENERATION))
|
||||
);
|
||||
}
|
||||
|
||||
Integer realtimeProgress = resolveRealtimeProgress(meetingId);
|
||||
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus()) && realtimeProgress != null && realtimeProgress <= 0) {
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"400",
|
||||
"会议正在处理中",
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
|
||||
);
|
||||
}
|
||||
if (realtimeProgress != null) {
|
||||
if (realtimeProgress >= 100) {
|
||||
MeetingVO completedDetail = detail != null ? detail : meetingQueryService.getDetail(meetingId);
|
||||
boolean completedHasSummary = completedDetail != null
|
||||
&& completedDetail.getSummaryContent() != null
|
||||
&& !completedDetail.getSummaryContent().isBlank();
|
||||
if (completedHasSummary) {
|
||||
return new LegacyMeetingPreviewResult("200", "success", buildCompletedPreview(meeting, completedDetail, summaryTask));
|
||||
}
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"504",
|
||||
"处理已完成,但摘要尚未同步,请稍后重试",
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED))
|
||||
);
|
||||
}
|
||||
if (realtimeProgress < 90) {
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"400",
|
||||
"会议正在处理中",
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("姝e湪杞瘧闊抽", 50, STAGE_AUDIO_TRANSCRIPTION))
|
||||
);
|
||||
}
|
||||
if (realtimeProgress >= 90) {
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"400",
|
||||
"会议正在处理中",
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("姝e湪鐢熸垚鎬荤粨", 75, STAGE_SUMMARY_GENERATION))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
|
||||
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
|
||||
|
||||
if (!isAsrStage && !isSummaryStage) {
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"400",
|
||||
"会议正在处理中",
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("会议数据准备中", 25, STAGE_DATA_INITIALIZATION))
|
||||
);
|
||||
}
|
||||
if (!isSummaryStage) {
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"400",
|
||||
"会议正在处理中",
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("正在转译音频", 50, STAGE_AUDIO_TRANSCRIPTION))
|
||||
);
|
||||
}
|
||||
return new LegacyMeetingPreviewResult(
|
||||
"400",
|
||||
"会议正在处理中",
|
||||
buildProcessingPreview(meeting, summaryTask, processingStatus("正在生成总结", 75, STAGE_SUMMARY_GENERATION))
|
||||
);
|
||||
}
|
||||
|
||||
private LegacyMeetingPreviewDataResponse buildCompletedPreview(Meeting meeting, MeetingVO detail, AiTask summaryTask) {
|
||||
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();
|
||||
data.setMeetingId(meeting.getId());
|
||||
data.setTitle(meeting.getTitle());
|
||||
data.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
|
||||
data.setSummary(detail.getSummaryContent());
|
||||
data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
|
||||
Long promptId = resolvePromptId(summaryTask);
|
||||
data.setPromptId(promptId);
|
||||
data.setPromptName(resolvePromptName(promptId));
|
||||
List<LegacyMeetingAttendeeResponse> attendees = buildAttendees(meeting.getParticipants());
|
||||
data.setAttendees(attendees);
|
||||
data.setAttendeesCount(attendees.size());
|
||||
data.setHasPassword(meeting.getAccessPassword() != null && !meeting.getAccessPassword().isBlank());
|
||||
data.setProcessingStatus(processingStatus("摘要已生成,可扫码查看", 100, STAGE_COMPLETED));
|
||||
return data;
|
||||
}
|
||||
|
||||
private LegacyMeetingItemResponse buildListItem(MeetingVO meeting) {
|
||||
LegacyMeetingItemResponse item = new LegacyMeetingItemResponse();
|
||||
item.setMeetingId(meeting.getId());
|
||||
item.setTitle(meeting.getTitle());
|
||||
item.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
|
||||
item.setCreatedAt(formatDateTime(meeting.getCreatedAt()));
|
||||
item.setCreatorId(meeting.getCreatorId());
|
||||
item.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
|
||||
item.setAudioFilePath(meeting.getAudioUrl());
|
||||
item.setAudioDuration(meeting.getDuration());
|
||||
item.setAccessPassword(resolveAccessPassword(meeting.getId()));
|
||||
|
||||
List<Long> attendeeIds = meeting.getParticipantIds() == null ? List.of() : meeting.getParticipantIds();
|
||||
item.setAttendeeIds(attendeeIds);
|
||||
item.setAttendees(buildAttendees(attendeeIds));
|
||||
item.setTags(buildTags(meeting.getTags()));
|
||||
item.setSummary(resolveListSummary(meeting.getId()));
|
||||
|
||||
LegacyMeetingProcessingStatusResponse status = buildListStatus(meeting);
|
||||
item.setOverallStatus(status.getOverallStatus());
|
||||
item.setOverallProgress(status.getOverallProgress());
|
||||
item.setCurrentStage(translateListStage(status.getCurrentStage()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private LegacyMeetingPreviewDataResponse buildProcessingPreview(Meeting meeting,
|
||||
AiTask summaryTask,
|
||||
LegacyMeetingProcessingStatusResponse status) {
|
||||
LegacyMeetingPreviewDataResponse data = new LegacyMeetingPreviewDataResponse();
|
||||
data.setMeetingId(meeting.getId());
|
||||
data.setTitle(meeting.getTitle());
|
||||
data.setMeetingTime(formatDateTime(meeting.getMeetingTime()));
|
||||
data.setCreatorUsername(resolveCreatorDisplayName(meeting.getCreatorId(), meeting.getCreatorName()));
|
||||
Long promptId = resolvePromptId(summaryTask);
|
||||
data.setPromptId(promptId);
|
||||
data.setPromptName(resolvePromptName(promptId));
|
||||
data.setHasPassword(meeting.getAccessPassword() != null && !meeting.getAccessPassword().isBlank());
|
||||
data.setProcessingStatus(status);
|
||||
return data;
|
||||
}
|
||||
|
||||
private LegacyMeetingProcessingStatusResponse processingStatus(String overallStatus, int overallProgress, String currentStage) {
|
||||
return new LegacyMeetingProcessingStatusResponse(overallStatus, overallProgress, currentStage);
|
||||
}
|
||||
|
||||
private Integer resolveRealtimeProgress(Long meetingId) {
|
||||
if (meetingProgressService == null) {
|
||||
return null;
|
||||
}
|
||||
return meetingProgressService.resolvePercent(meetingId);
|
||||
}
|
||||
|
||||
private LegacyMeetingProcessingStatusResponse buildListStatus(MeetingVO meeting) {
|
||||
Long meetingId = meeting.getId();
|
||||
AiTask asrTask = findLatestTask(meetingId, "ASR");
|
||||
AiTask summaryTask = findLatestTask(meetingId, "SUMMARY");
|
||||
boolean summaryCompleted = summaryTask != null && Integer.valueOf(2).equals(summaryTask.getStatus());
|
||||
|
||||
if (MeetingStatusEnum.isCode(meeting.getStatus(), MeetingStatusEnum.COMPLETED) || summaryCompleted) {
|
||||
return new LegacyMeetingProcessingStatusResponse("completed", 100, STAGE_COMPLETED);
|
||||
}
|
||||
if (isFailed(asrTask)) {
|
||||
return new LegacyMeetingProcessingStatusResponse("failed", 50, STAGE_AUDIO_TRANSCRIPTION);
|
||||
}
|
||||
if (isFailed(summaryTask)) {
|
||||
return new LegacyMeetingProcessingStatusResponse("failed", 75, STAGE_SUMMARY_GENERATION);
|
||||
}
|
||||
|
||||
boolean isSummaryStage = isSummaryStage(meeting.getStatus(), summaryTask);
|
||||
boolean isAsrStage = isAsrStage(meeting.getStatus(), asrTask, hasAudio(meeting), isSummaryStage);
|
||||
|
||||
if (!isAsrStage && !isSummaryStage) {
|
||||
return new LegacyMeetingProcessingStatusResponse("pending", 0, STAGE_DATA_INITIALIZATION);
|
||||
}
|
||||
if (isSummaryStage) {
|
||||
return new LegacyMeetingProcessingStatusResponse("summarizing", 75, STAGE_SUMMARY_GENERATION);
|
||||
}
|
||||
return new LegacyMeetingProcessingStatusResponse("transcribing", 50, STAGE_AUDIO_TRANSCRIPTION);
|
||||
}
|
||||
|
||||
private String buildFailureMessage(AiTask failedTask, String stageName) {
|
||||
String error = failedTask == null || failedTask.getErrorMsg() == null || failedTask.getErrorMsg().isBlank()
|
||||
? "处理失败"
|
||||
: failedTask.getErrorMsg();
|
||||
return "会议" + stageName + "失败: " + error;
|
||||
}
|
||||
|
||||
private boolean isRunningAsr(AiTask task) {
|
||||
return task != null && Integer.valueOf(1).equals(task.getStatus());
|
||||
}
|
||||
|
||||
private boolean isRunningSummary(AiTask task) {
|
||||
return task != null && Integer.valueOf(1).equals(task.getStatus());
|
||||
}
|
||||
|
||||
private boolean isFailed(AiTask task) {
|
||||
return task != null && Integer.valueOf(3).equals(task.getStatus());
|
||||
}
|
||||
|
||||
private AiTask findLatestTask(Long meetingId, String taskType) {
|
||||
return aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
.eq(AiTask::getTaskType, taskType)
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private Long resolvePromptId(AiTask summaryTask) {
|
||||
if (summaryTask == null || summaryTask.getTaskConfig() == null) {
|
||||
return null;
|
||||
}
|
||||
Object rawPromptId = summaryTask.getTaskConfig().get("promptId");
|
||||
if (rawPromptId == null) {
|
||||
return null;
|
||||
}
|
||||
if (rawPromptId instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
String value = String.valueOf(rawPromptId).trim();
|
||||
if (value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String resolvePromptName(Long promptId) {
|
||||
if (promptId == null) {
|
||||
return null;
|
||||
}
|
||||
PromptTemplate template = promptTemplateService.getById(promptId);
|
||||
return template == null ? null : template.getTemplateName();
|
||||
}
|
||||
|
||||
private List<LegacyMeetingAttendeeResponse> buildAttendees(String participants) {
|
||||
return buildAttendees(parseParticipantIds(participants));
|
||||
}
|
||||
|
||||
private List<LegacyMeetingAttendeeResponse> buildAttendees(List<Long> participantIds) {
|
||||
if (participantIds == null || participantIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, SysUser> userMap = sysUserMapper.selectBatchIds(participantIds).stream()
|
||||
.collect(Collectors.toMap(SysUser::getUserId, user -> user, (left, right) -> left, LinkedHashMap::new));
|
||||
|
||||
return participantIds.stream()
|
||||
.map(userId -> {
|
||||
SysUser user = userMap.get(userId);
|
||||
String caption = user == null
|
||||
? String.valueOf(userId)
|
||||
: (user.getDisplayName() != null ? user.getDisplayName() : user.getUsername());
|
||||
String username = user == null ? null : user.getUsername();
|
||||
return new LegacyMeetingAttendeeResponse(userId, username, caption);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<LegacyMeetingTagResponse> buildTags(String rawTags) {
|
||||
if (rawTags == null || rawTags.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.stream(rawTags.split(","))
|
||||
.map(String::trim)
|
||||
.filter(value -> !value.isEmpty())
|
||||
.map(value -> new LegacyMeetingTagResponse(null, value))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Long> parseParticipantIds(String participants) {
|
||||
if (participants == null || participants.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.stream(participants.split(","))
|
||||
.map(String::trim)
|
||||
.filter(value -> !value.isEmpty())
|
||||
.map(value -> {
|
||||
try {
|
||||
return Long.parseLong(value);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String normalizePassword(String password) {
|
||||
if (password == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = password.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
private String resolveListSummary(Long meetingId) {
|
||||
MeetingVO detail = meetingQueryService.getDetail(meetingId);
|
||||
if (detail == null || detail.getSummaryContent() == null || detail.getSummaryContent().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String summary = detail.getSummaryContent().trim();
|
||||
return summary.length() <= 240 ? summary : summary.substring(0, 240);
|
||||
}
|
||||
|
||||
private String resolveAccessPassword(Long meetingId) {
|
||||
Meeting meeting = meetingService.getById(meetingId);
|
||||
return meeting == null ? null : normalizePassword(meeting.getAccessPassword());
|
||||
}
|
||||
|
||||
private String resolveCreatorDisplayName(Long creatorId, String fallbackName) {
|
||||
if (creatorId == null) {
|
||||
return fallbackName;
|
||||
}
|
||||
SysUser creator = sysUserMapper.selectById(creatorId);
|
||||
if (creator == null) {
|
||||
return fallbackName;
|
||||
}
|
||||
if (creator.getDisplayName() != null && !creator.getDisplayName().isBlank()) {
|
||||
return creator.getDisplayName();
|
||||
}
|
||||
if (creator.getUsername() != null && !creator.getUsername().isBlank()) {
|
||||
return creator.getUsername();
|
||||
}
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
private boolean hasAudio(Meeting meeting) {
|
||||
return meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank();
|
||||
}
|
||||
|
||||
private boolean hasAudio(MeetingVO meeting) {
|
||||
return meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank();
|
||||
}
|
||||
|
||||
private boolean isSummaryStage(Integer meetingStatus, AiTask summaryTask) {
|
||||
return Integer.valueOf(2).equals(meetingStatus) || isRunningSummary(summaryTask);
|
||||
}
|
||||
|
||||
private boolean isAsrStage(Integer meetingStatus, AiTask asrTask, boolean hasAudio, boolean isSummaryStage) {
|
||||
return (Integer.valueOf(1).equals(meetingStatus) && (asrTask == null || !Integer.valueOf(0).equals(asrTask.getStatus())))
|
||||
|| isRunningAsr(asrTask)
|
||||
|| (asrTask == null && hasAudio && !isSummaryStage);
|
||||
}
|
||||
|
||||
private String formatDateTime(LocalDateTime value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
private String translateListStage(String stage) {
|
||||
if (STAGE_SUMMARY_GENERATION.equals(stage)) {
|
||||
return "llm";
|
||||
}
|
||||
if (STAGE_COMPLETED.equals(stage)) {
|
||||
return "completed";
|
||||
}
|
||||
return "transcription";
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
|
||||
private String resolveCreatorName(LoginUser loginUser) {
|
||||
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
||||
}
|
||||
private AndroidAuthContext buildLegacyAuthContext() {
|
||||
return new AndroidAuthContext();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyPromptItemResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyPromptListResponse;
|
||||
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Tag(name = "兼容提示词接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/prompts")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LegacyPromptController {
|
||||
|
||||
private static final String LEGACY_MEETING_SCENE = "MEETING_TASK";
|
||||
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
|
||||
@Operation(summary = "查询场景提示词")
|
||||
@GetMapping("/active/{scene}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyPromptListResponse> activePrompts(@PathVariable String scene) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容提示词", "查询场景提示词接口", "scene", scene);
|
||||
if (!LEGACY_MEETING_SCENE.equals(scene)) {
|
||||
return LegacyApiResponse.error("400", "scene 仅支持 MEETING_TASK");
|
||||
}
|
||||
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
PageResult<List<PromptTemplateVO>> result = promptTemplateService.pageTemplates(
|
||||
1,
|
||||
1000,
|
||||
null,
|
||||
null,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin()
|
||||
);
|
||||
List<PromptTemplateVO> enabledTemplates = result.getRecords() == null
|
||||
? List.of()
|
||||
: result.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||
.toList();
|
||||
Long defaultTemplateId = enabledTemplates.isEmpty() ? null : enabledTemplates.get(0).getId();
|
||||
List<LegacyPromptItemResponse> prompts = enabledTemplates.stream()
|
||||
.map(item -> LegacyPromptItemResponse.from(item, Objects.equals(item.getId(), defaultTemplateId)))
|
||||
.toList();
|
||||
return LegacyApiResponse.ok(new LegacyPromptListResponse(prompts));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyScreenSaverCatalogResponse;
|
||||
import com.imeeting.service.android.legacy.LegacyScreenSaverAdapterService;
|
||||
import com.imeeting.support.AndroidRequestLogHelper;
|
||||
import com.imeeting.support.TaskSecurityContextRunner;
|
||||
import com.unisbase.dto.InternalAuthCheckResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import com.unisbase.service.TokenValidationService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "兼容屏保接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/screensavers")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LegacyScreenSaverController {
|
||||
|
||||
private static final String HEADER_AUTHORIZATION = "Authorization";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final LegacyScreenSaverAdapterService legacyScreenSaverAdapterService;
|
||||
private final TokenValidationService tokenValidationService;
|
||||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||
|
||||
@Operation(summary = "查询启用的屏保列表")
|
||||
@GetMapping("/active")
|
||||
public LegacyApiResponse<LegacyScreenSaverCatalogResponse> active(HttpServletRequest request) {
|
||||
AndroidRequestLogHelper.logRequest(log, "兼容屏保", "查询启用屏保列表接口");
|
||||
LoginUser loginUser = resolveLoginUser(request);
|
||||
return LegacyApiResponse.ok(queryActive(loginUser));
|
||||
}
|
||||
|
||||
private LegacyScreenSaverCatalogResponse queryActive(LoginUser loginUser) {
|
||||
if (loginUser == null || loginUser.getUserId() == null || loginUser.getTenantId() == null) {
|
||||
return legacyScreenSaverAdapterService.getActiveScreenSavers(null);
|
||||
}
|
||||
return taskSecurityContextRunner.callAsTenantUser(
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
() -> legacyScreenSaverAdapterService.getActiveScreenSavers(loginUser.getUserId())
|
||||
);
|
||||
}
|
||||
|
||||
private LoginUser resolveLoginUser(HttpServletRequest request) {
|
||||
LoginUser loginUser = currentLoginUserFromContext();
|
||||
if (loginUser != null) {
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
String token = resolveBearerToken(request);
|
||||
if (!StringUtils.hasText(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InternalAuthCheckResponse authResult = tokenValidationService.validateAccessToken(token);
|
||||
if (authResult == null || !authResult.isValid()
|
||||
|| authResult.getUserId() == null || authResult.getTenantId() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LoginUser resolved = new LoginUser(
|
||||
authResult.getUserId(),
|
||||
authResult.getTenantId(),
|
||||
authResult.getUsername(),
|
||||
authResult.getPlatformAdmin(),
|
||||
authResult.getTenantAdmin(),
|
||||
authResult.getPermissions()
|
||||
);
|
||||
resolved.setDisplayName(authResult.getDisplayName());
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUserFromContext() {
|
||||
if (SecurityContextHolder.getContext().getAuthentication() == null
|
||||
|| !(SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof LoginUser loginUser)) {
|
||||
return null;
|
||||
}
|
||||
if (loginUser.getUserId() == null || loginUser.getTenantId() == null) {
|
||||
return null;
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
private String resolveBearerToken(HttpServletRequest request) {
|
||||
String authorization = request.getHeader(HEADER_AUTHORIZATION);
|
||||
if (!StringUtils.hasText(authorization) || !authorization.startsWith(BEARER_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
String token = authorization.substring(BEARER_PREFIX.length()).trim();
|
||||
return token.isEmpty() ? null : token;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.AiLocalProfileVO;
|
||||
import com.imeeting.dto.biz.AiModelDTO;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.dto.biz.PlatformAsrStatusUpdateCommand;
|
||||
import com.imeeting.dto.biz.TenantModelDefaultCommand;
|
||||
import com.imeeting.enums.ModelProviderEnum;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "AI模型管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/aimodel")
|
||||
public class AiModelController {
|
||||
|
||||
private final AiModelService aiModelService;
|
||||
|
||||
public AiModelController(AiModelService aiModelService) {
|
||||
this.aiModelService = aiModelService;
|
||||
}
|
||||
|
||||
@Operation(summary = "新增AI模型")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增AI模型", type = "AI模型管理")
|
||||
public ApiResponse<AiModelVO> save(@RequestBody AiModelDTO dto) {
|
||||
return ApiResponse.ok(aiModelService.saveModel(dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新AI模型")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改AI模型", type = "AI模型管理")
|
||||
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));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除AI模型")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除AI模型", type = "AI模型管理")
|
||||
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));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询AI模型")
|
||||
@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,
|
||||
@RequestParam(required = false, defaultValue = "false") Boolean tenantEnabledOnly) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
boolean platformAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin());
|
||||
return ApiResponse.ok(aiModelService.pageModels(current, size, name, type, loginUser.getTenantId(), platformAdmin, Boolean.TRUE.equals(tenantEnabledOnly)));
|
||||
}
|
||||
|
||||
@Operation(summary = "拉取远程模型列表")
|
||||
@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));
|
||||
}
|
||||
|
||||
@Operation(summary = "测试本地模型连通性")
|
||||
@PostMapping("/local-connectivity-test")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<AiLocalProfileVO> testLocalConnectivity(@RequestBody AiModelDTO dto) {
|
||||
if (dto.getBaseUrl() == null || dto.getBaseUrl().isBlank()) {
|
||||
return ApiResponse.error("基础地址不能为空");
|
||||
}
|
||||
// if (dto.getApiKey() == null || dto.getApiKey().isBlank()) {
|
||||
// return ApiResponse.error("API 密钥不能为空");
|
||||
// }
|
||||
return ApiResponse.ok(aiModelService.testLocalConnectivity(dto.getBaseUrl(), dto.getApiKey()));
|
||||
}
|
||||
|
||||
@Operation(summary = "测试LLM模型连通性")
|
||||
@PostMapping("/llm-connectivity-test")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> testLlmConnectivity(@RequestBody AiModelDTO dto) {
|
||||
if (ModelProviderEnum.LOCAL.getCode().equalsIgnoreCase(dto.getProvider()) && (dto.getBaseUrl() == null || dto.getBaseUrl().isBlank())) {
|
||||
return ApiResponse.error("基础地址不能为空");
|
||||
}
|
||||
if (dto.getModelCode() == null || dto.getModelCode().isBlank()) {
|
||||
return ApiResponse.error("模型名称不能为空");
|
||||
}
|
||||
aiModelService.testLlmConnectivity(dto);
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取默认AI模型")
|
||||
@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()));
|
||||
}
|
||||
|
||||
@Operation(summary = "租户启用 ASR")
|
||||
@PostMapping("/{id}/tenant-enable")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> tenantEnable(@PathVariable Long id,
|
||||
@RequestParam(required = false, defaultValue = "ASR") String type) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getTenantId() == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
aiModelService.enableModelForTenant(type, loginUser.getTenantId(), id);
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Operation(summary = "租户关闭 ASR")
|
||||
@PostMapping("/{id}/tenant-disable")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> tenantDisable(@PathVariable Long id,
|
||||
@RequestParam(required = false, defaultValue = "ASR") String type) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getTenantId() == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
aiModelService.disableModelForTenant(type, loginUser.getTenantId(), id);
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Operation(summary = "平台更新 ASR 状态")
|
||||
@PostMapping("/{id}/platform-status")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> updatePlatformStatus(@PathVariable Long id,
|
||||
@RequestBody PlatformAsrStatusUpdateCommand command,
|
||||
@RequestParam(required = false, defaultValue = "ASR") String type) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
aiModelService.updatePlatformModelStatus(type, id, command.getStatus(), Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()));
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Operation(summary = "同步当前 ASR 声纹")
|
||||
@PostMapping("/current/sync-speakers")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> syncCurrentSpeakers() {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getTenantId() == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
aiModelService.syncCurrentTenantActiveAsrSpeakers(loginUser.getTenantId());
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Operation(summary = "绉熸埛璁剧疆榛樿妯″瀷")
|
||||
@PostMapping("/{id}/tenant-default")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> setTenantDefault(@PathVariable Long id,
|
||||
@RequestBody TenantModelDefaultCommand command) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getTenantId() == null) {
|
||||
return ApiResponse.error("鏈幏鍙栧埌鐢ㄦ埛淇℃伅");
|
||||
}
|
||||
aiModelService.setDefaultModelForTenant(command.getModelType(), loginUser.getTenantId(), id);
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
private LoginUser getLoginUser() {
|
||||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
if (principal instanceof LoginUser loginUser) {
|
||||
return loginUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.ClientDownloadDTO;
|
||||
import com.imeeting.entity.biz.ClientDownload;
|
||||
import com.imeeting.service.biz.ClientDownloadService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "客户端下载管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/clients")
|
||||
@RequiredArgsConstructor
|
||||
public class ClientDownloadController {
|
||||
|
||||
private final ClientDownloadService clientDownloadService;
|
||||
|
||||
@Operation(summary = "查询客户端下载包列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> list(@RequestParam(value = "platformCode", required = false) String platformCode,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||
@RequestParam(value = "size", defaultValue = "50") Integer size) {
|
||||
List<ClientDownload> clients = clientDownloadService.listForAdmin(currentLoginUser(), platformCode, status);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("clients", clients);
|
||||
data.put("total", clients.size());
|
||||
data.put("page", page);
|
||||
data.put("size", size);
|
||||
return ApiResponse.ok(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "新增客户端下载包")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增客户端下载包", type = "客户端下载管理")
|
||||
public ApiResponse<ClientDownload> create(@RequestBody ClientDownloadDTO dto) {
|
||||
return ApiResponse.ok(clientDownloadService.create(dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改客户端下载包")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改客户端下载包", type = "客户端下载管理")
|
||||
public ApiResponse<ClientDownload> update(@PathVariable Long id, @RequestBody ClientDownloadDTO dto) {
|
||||
return ApiResponse.ok(clientDownloadService.update(id, dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除客户端下载包")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除客户端下载包", type = "客户端下载管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
clientDownloadService.removeClient(id, currentLoginUser());
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "上传客户端安装包")
|
||||
@PostMapping("/upload")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> upload(@RequestParam("platformCode") String platformCode,
|
||||
@RequestParam("file") MultipartFile file) throws IOException {
|
||||
return ApiResponse.ok(clientDownloadService.uploadPackage(platformCode, file));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.imeeting.controller.biz;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
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;
|
||||
|
||||
@Tag(name = "工作台")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/dashboard")
|
||||
public class DashboardController {
|
||||
|
||||
private final MeetingQueryService meetingQueryService;
|
||||
|
||||
public DashboardController(MeetingQueryService meetingQueryService) {
|
||||
this.meetingQueryService = meetingQueryService;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取工作台统计数据")
|
||||
@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(meetingQueryService.getDashboardStats(user.getTenantId(), user.getUserId(), isAdmin));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取最近会议列表")
|
||||
@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(meetingQueryService.getRecentMeetings(user.getTenantId(), user.getUserId(), isAdmin, 10));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.DeviceAdminUpdateCommand;
|
||||
import com.imeeting.dto.biz.DeviceOnlineAdminVO;
|
||||
import com.imeeting.service.biz.DeviceOnlineManagementService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "设备在线管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/devices")
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceManagementController {
|
||||
|
||||
private final DeviceOnlineManagementService deviceOnlineManagementService;
|
||||
|
||||
@Operation(summary = "查询设备在线管理列表")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "返回设备在线管理列表",
|
||||
content = @Content(array = @ArraySchema(schema = @Schema(implementation = DeviceOnlineAdminVO.class)))
|
||||
)
|
||||
})
|
||||
@GetMapping
|
||||
public ApiResponse<List<DeviceOnlineAdminVO>> list() {
|
||||
return ApiResponse.ok(deviceOnlineManagementService.listForAdmin(currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新设备管理信息")
|
||||
@PutMapping("/{id}")
|
||||
@Log(value = "修改设备管理信息", type = "设备在线管理")
|
||||
public ApiResponse<DeviceOnlineAdminVO> update(@PathVariable Long id, @RequestBody DeviceAdminUpdateCommand command) {
|
||||
return ApiResponse.ok(deviceOnlineManagementService.update(id, command, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "踢下线设备")
|
||||
@PostMapping("/{id}/kick")
|
||||
public ApiResponse<Boolean> kick(@PathVariable Long id) {
|
||||
return ApiResponse.ok(deviceOnlineManagementService.kick(id, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除设备并解绑授权")
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
return ApiResponse.ok(deviceOnlineManagementService.delete(id, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "重置设备首页统计")
|
||||
@PostMapping("/{id}/reset")
|
||||
public ApiResponse<Boolean> reset(@PathVariable Long id) {
|
||||
return ApiResponse.ok(deviceOnlineManagementService.resetStats(id, currentLoginUser()));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.ExternalAppDTO;
|
||||
import com.imeeting.entity.biz.ExternalApp;
|
||||
import com.imeeting.service.biz.ExternalAppService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "外部应用管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/external-apps")
|
||||
@RequiredArgsConstructor
|
||||
public class ExternalAppController {
|
||||
|
||||
private final ExternalAppService externalAppService;
|
||||
|
||||
@Operation(summary = "查询外部应用列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<Map<String, Object>>> list(@RequestParam(value = "appType", required = false) String appType,
|
||||
@RequestParam(value = "status", required = false) Integer status) {
|
||||
return ApiResponse.ok(externalAppService.listForAdmin(currentLoginUser(), appType, status));
|
||||
}
|
||||
|
||||
@Operation(summary = "新增外部应用")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增外部应用", type = "外部应用管理")
|
||||
public ApiResponse<ExternalApp> create(@RequestBody ExternalAppDTO dto) {
|
||||
return ApiResponse.ok(externalAppService.create(dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改外部应用")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改外部应用", type = "外部应用管理")
|
||||
public ApiResponse<ExternalApp> update(@PathVariable Long id, @RequestBody ExternalAppDTO dto) {
|
||||
return ApiResponse.ok(externalAppService.update(id, dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除外部应用")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除外部应用", type = "外部应用管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
externalAppService.removeApp(id, currentLoginUser());
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "上传外部应用APK")
|
||||
@PostMapping("/upload-apk")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> uploadApk(@RequestParam("apkFile") MultipartFile apkFile) throws IOException {
|
||||
return ApiResponse.ok(externalAppService.uploadApk(apkFile));
|
||||
}
|
||||
|
||||
@Operation(summary = "上传外部应用图标")
|
||||
@PostMapping("/upload-icon")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> uploadIcon(@RequestParam("iconFile") MultipartFile iconFile) throws IOException {
|
||||
return ApiResponse.ok(externalAppService.uploadIcon(iconFile));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
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.HotWordBatchGroupDTO;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
import com.imeeting.entity.biz.HotWordGroup;
|
||||
import com.imeeting.service.biz.HotWordGroupService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
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;
|
||||
|
||||
@Tag(name = "热词管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/hotword")
|
||||
public class HotWordController {
|
||||
|
||||
private final HotWordService hotWordService;
|
||||
private final HotWordGroupService hotWordGroupService;
|
||||
|
||||
public HotWordController(HotWordService hotWordService, HotWordGroupService hotWordGroupService) {
|
||||
this.hotWordService = hotWordService;
|
||||
this.hotWordGroupService = hotWordGroupService;
|
||||
}
|
||||
|
||||
private Long resolveTargetTenantId(LoginUser loginUser, Long tenantId) {
|
||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(tenantId)) {
|
||||
return 0L;
|
||||
}
|
||||
return loginUser.getTenantId();
|
||||
}
|
||||
|
||||
@Operation(summary = "新增热词")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增热词", type = "热词管理")
|
||||
public ApiResponse<HotWordVO> save(@RequestBody HotWordDTO hotWordDTO) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, hotWordDTO.getTenantId());
|
||||
return ApiResponse.ok(hotWordService.saveHotWord(hotWordDTO, loginUser.getUserId(), targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改热词")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改热词", type = "热词管理")
|
||||
public ApiResponse<HotWordVO> update(@RequestBody HotWordDTO hotWordDTO) {
|
||||
HotWord existing = hotWordService.getById(hotWordDTO.getId());
|
||||
if (existing == null) {
|
||||
return ApiResponse.error("热词不存在");
|
||||
}
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
return ApiResponse.ok(hotWordService.updateHotWord(hotWordDTO, loginUser.getUserId(), existing.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量修改热词分组")
|
||||
@PutMapping("/group/batch")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "批量修改热词分组", type = "热词管理")
|
||||
public ApiResponse<Integer> updateGroupBatch(@RequestBody HotWordBatchGroupDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
return ApiResponse.ok(hotWordService.updateHotWordGroupBatch(dto.getIds(), dto.getHotWordGroupId(), targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除热词")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除热词", type = "热词管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
HotWord existing = hotWordService.getById(id);
|
||||
if (existing == null) {
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
return ApiResponse.ok(hotWordService.removeById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询热词")
|
||||
@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) Long hotWordGroupId,
|
||||
@RequestParam(required = false) Boolean ungrouped,
|
||||
@RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) ? tenantId : null;
|
||||
|
||||
LambdaQueryWrapper<HotWord> wrapper = new LambdaQueryWrapper<HotWord>()
|
||||
.like(word != null && !word.isEmpty(), HotWord::getWord, word)
|
||||
.eq(category != null && !category.isEmpty(), HotWord::getCategory, category)
|
||||
.eq(hotWordGroupId != null, HotWord::getHotWordGroupId, hotWordGroupId)
|
||||
.isNull(Boolean.TRUE.equals(ungrouped), HotWord::getHotWordGroupId)
|
||||
.orderByDesc(HotWord::getCreatedAt);
|
||||
wrapper.eq(targetTenantId != null, HotWord::getTenantId, targetTenantId);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Operation(summary = "生成热词拼音")
|
||||
@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.setHotWordGroupId(entity.getHotWordGroupId());
|
||||
vo.setWeight(entity.getWeight());
|
||||
vo.setStatus(entity.getStatus());
|
||||
vo.setIsPublic(1);
|
||||
vo.setCreatorId(entity.getCreatorId());
|
||||
vo.setIsSynced(entity.getIsSynced());
|
||||
vo.setRemark(entity.getRemark());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
if (entity.getHotWordGroupId() != null) {
|
||||
HotWordGroup group = hotWordGroupService.getById(entity.getHotWordGroupId());
|
||||
vo.setHotWordGroupName(group == null ? null : group.getGroupName());
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.HotWordGroupDTO;
|
||||
import com.imeeting.dto.biz.HotWordGroupVO;
|
||||
import com.imeeting.service.biz.HotWordGroupService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "热词组管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/hotword-group")
|
||||
public class HotWordGroupController {
|
||||
|
||||
private final HotWordGroupService hotWordGroupService;
|
||||
|
||||
public HotWordGroupController(HotWordGroupService hotWordGroupService) {
|
||||
this.hotWordGroupService = hotWordGroupService;
|
||||
}
|
||||
|
||||
private Long resolveTargetTenantId(LoginUser loginUser, Long tenantId) {
|
||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(tenantId)) {
|
||||
return 0L;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Operation(summary = "新增热词组")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增热词组", type = "热词组管理")
|
||||
public ApiResponse<HotWordGroupVO> save(@RequestBody HotWordGroupDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
return ApiResponse.ok(hotWordGroupService.saveGroup(dto, loginUser.getUserId(), targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改热词组")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改热词组", type = "热词组管理")
|
||||
public ApiResponse<HotWordGroupVO> update(@RequestBody HotWordGroupDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
HotWordGroupVO existing = hotWordGroupService.listVisibleOptions(targetTenantId).stream()
|
||||
.filter(item -> item.getId().equals(dto.getId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (existing == null) {
|
||||
return ApiResponse.error("热词组不存在");
|
||||
}
|
||||
dto.setTenantId(targetTenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.updateGroup(dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除热词组")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除热词组", type = "热词组管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id, @RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.removeGroupById(id, targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询热词组")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<HotWordGroupVO>>> page(
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "10") Integer size,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.pageGroups(current, size, name, status, targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询热词组选项")
|
||||
@GetMapping("/options")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<HotWordGroupVO>> options(@RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
|
||||
return ApiResponse.ok(hotWordGroupService.listVisibleOptions(targetTenantId));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.LicenseImportResultVO;
|
||||
import com.imeeting.dto.biz.LicenseVO;
|
||||
import com.imeeting.service.biz.LicenseService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "租户授权管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/licenses")
|
||||
@RequiredArgsConstructor
|
||||
public class LicenseManagementController {
|
||||
|
||||
private final LicenseService licenseService;
|
||||
|
||||
@Operation(summary = "查询当前租户授权列表")
|
||||
@GetMapping
|
||||
public ApiResponse<List<LicenseVO>> list() {
|
||||
return ApiResponse.ok(licenseService.listCurrentTenantLicenses(currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "导入当前租户正式授权")
|
||||
@PostMapping("/import")
|
||||
public ApiResponse<LicenseImportResultVO> importLicenses(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
return ApiResponse.ok(licenseService.importFormalLicenses(file, currentLoginUser()));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,644 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.common.MeetingConstants;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.dto.biz.CreateMeetingCommand;
|
||||
import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
||||
import com.imeeting.dto.biz.MeetingCreateConfigVO;
|
||||
import com.imeeting.dto.biz.MeetingResummaryDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryOrchestrationTriggerResultVO;
|
||||
import com.imeeting.dto.biz.MeetingSpeakerUpdateDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryExportResult;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptExportResult;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.OpenRealtimeSocketSessionCommand;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingCompleteDTO;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||
import com.imeeting.dto.biz.RealtimeSocketSessionVO;
|
||||
import com.imeeting.dto.biz.RealtimeTranscriptItemDTO;
|
||||
import com.imeeting.dto.biz.UpdateMeetingBasicCommand;
|
||||
import com.imeeting.dto.biz.UpdateMeetingParticipantsCommand;
|
||||
import com.imeeting.dto.biz.UpdateMeetingSummaryCommand;
|
||||
import com.imeeting.dto.biz.UpdateMeetingTranscriptCommand;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.MeetingExportService;
|
||||
import com.imeeting.service.biz.MeetingProgressService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||
import com.imeeting.service.biz.MeetingUnifiedStatusService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
||||
import com.imeeting.service.biz.impl.MeetingAudioUploadSupport;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "会议管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/meeting")
|
||||
public class MeetingController {
|
||||
|
||||
private final MeetingQueryService meetingQueryService;
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final MeetingExportService meetingExportService;
|
||||
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
private final MeetingAudioUploadSupport meetingAudioUploadSupport;
|
||||
private final MeetingProgressService meetingProgressService;
|
||||
private final SysParamService sysParamService;
|
||||
private final AiTaskService aiTaskService;
|
||||
private final MeetingUnifiedStatusService meetingUnifiedStatusService;
|
||||
|
||||
@Value("${imeeting.h5.base-url:}")
|
||||
private String h5BaseUrl;
|
||||
|
||||
@Autowired
|
||||
public MeetingController(MeetingQueryService meetingQueryService,
|
||||
MeetingCommandService meetingCommandService,
|
||||
MeetingAccessService meetingAccessService,
|
||||
MeetingExportService meetingExportService,
|
||||
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||
PromptTemplateService promptTemplateService,
|
||||
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
|
||||
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
||||
MeetingAudioUploadSupport meetingAudioUploadSupport,
|
||||
MeetingProgressService meetingProgressService,
|
||||
SysParamService sysParamService,
|
||||
AiTaskService aiTaskService,
|
||||
MeetingUnifiedStatusService meetingUnifiedStatusService) {
|
||||
this.meetingQueryService = meetingQueryService;
|
||||
this.meetingCommandService = meetingCommandService;
|
||||
this.meetingAccessService = meetingAccessService;
|
||||
this.meetingExportService = meetingExportService;
|
||||
this.meetingTranscriptFileService = meetingTranscriptFileService;
|
||||
this.promptTemplateService = promptTemplateService;
|
||||
this.realtimeMeetingSocketSessionService = realtimeMeetingSocketSessionService;
|
||||
this.realtimeMeetingSessionStateService = realtimeMeetingSessionStateService;
|
||||
this.meetingAudioUploadSupport = meetingAudioUploadSupport;
|
||||
this.meetingProgressService = meetingProgressService;
|
||||
this.sysParamService = sysParamService;
|
||||
this.aiTaskService = aiTaskService;
|
||||
this.meetingUnifiedStatusService = meetingUnifiedStatusService;
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议处理进度")
|
||||
@GetMapping("/{id}/progress")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, Object>> getProgress(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
||||
if ("Waiting...".equals(progress.get("message"))) {
|
||||
AiTask asrTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, id)
|
||||
.eq(AiTask::getTaskType, "ASR")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus())) {
|
||||
return ApiResponse.ok(Map.of("percent", 0, "message", "排队中,等待 ASR 执行名额..."));
|
||||
}
|
||||
if (asrTask != null && Integer.valueOf(1).equals(asrTask.getStatus())) {
|
||||
return ApiResponse.ok(Map.of("percent", 5, "message", "识别中,等待进度刷新..."));
|
||||
}
|
||||
}
|
||||
Map<String, Object> payload = new LinkedHashMap<>(progress);
|
||||
payload.put("unifiedStatus", meetingUnifiedStatusService.resolve(id));
|
||||
return ApiResponse.ok(payload);
|
||||
}
|
||||
|
||||
|
||||
@Operation(summary = "批量查询会议处理进度")
|
||||
@PostMapping("/progress/batch")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<Long, Map<String, Object>>> getProgressBatch(@RequestBody List<Long> ids) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Map<Long, Map<String, Object>> result = new LinkedHashMap<>();
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
for (Long id : ids) {
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||
Map<String, Object> progress = meetingProgressService.getProgressMap(id);
|
||||
if ("Waiting...".equals(progress.get("message"))) {
|
||||
AiTask asrTask = aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, id)
|
||||
.eq(AiTask::getTaskType, "ASR")
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
if (asrTask != null && Integer.valueOf(0).equals(asrTask.getStatus())) {
|
||||
progress = Map.of("percent", 0, "message", "排队中,等待 ASR 执行名额...");
|
||||
} else if (asrTask != null && Integer.valueOf(1).equals(asrTask.getStatus())) {
|
||||
progress = Map.of("percent", 5, "message", "识别中,等待进度刷新...");
|
||||
}
|
||||
}
|
||||
Map<String, Object> payload = new LinkedHashMap<>(progress);
|
||||
payload.put("unifiedStatus", meetingUnifiedStatusService.resolve(id));
|
||||
result.put(id, payload);
|
||||
} catch (RuntimeException ignored) {
|
||||
// Ignore inaccessible meetings in batch mode.
|
||||
}
|
||||
}
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "重新调度排队中的会议 ASR 任务")
|
||||
@PostMapping("/{id}/retry-schedule")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> retrySchedule(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
return ApiResponse.ok(aiTaskService.retryScheduleMeeting(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "上传会议音频")
|
||||
@PostMapping("/upload")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
return ApiResponse.ok(meetingAudioUploadSupport.storeUploadedAudio(file));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会议创建配置")
|
||||
@GetMapping("/create-config")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingCreateConfigVO> getCreateConfig() {
|
||||
MeetingCreateConfigVO vo = new MeetingCreateConfigVO();
|
||||
vo.setOfflineEnabled(resolveBooleanParam(SysParamKeys.MEETING_CREATE_OFFLINE_ENABLED, true));
|
||||
vo.setRealtimeEnabled(resolveBooleanParam(SysParamKeys.MEETING_CREATE_REALTIME_ENABLED, true));
|
||||
vo.setAiCatalogEnabled(resolveBooleanParam(SysParamKeys.MEETING_AI_CATALOG_ENABLED, false));
|
||||
vo.setOfflineAudioMaxSizeMb(resolveLongParam(SysParamKeys.MEETING_OFFLINE_AUDIO_MAX_SIZE_MB, 1024L));
|
||||
return ApiResponse.ok(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会议分享配置")
|
||||
@GetMapping("/share-config")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<String, String>> getShareConfig() {
|
||||
String baseUrl = StringUtils.hasText(h5BaseUrl) ? h5BaseUrl.trim() : "";
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
|
||||
}
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("h5BaseUrl", baseUrl);
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "创建离线会议")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增离线会议", type = "会议管理")
|
||||
public ApiResponse<MeetingVO> create(@Valid @RequestBody CreateMeetingCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
assertPromptAvailable(command.getPromptId(), loginUser);
|
||||
return ApiResponse.ok(meetingCommandService.createMeeting(
|
||||
command,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
MeetingConstants.SOURCE_WEB
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建实时会议")
|
||||
@PostMapping("/realtime/start")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增实时会议", type = "会议管理")
|
||||
public ApiResponse<MeetingVO> createRealtime(@Valid @RequestBody CreateRealtimeMeetingCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
assertPromptAvailable(command.getPromptId(), loginUser);
|
||||
return ApiResponse.ok(meetingCommandService.createRealtimeMeeting(
|
||||
command,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
MeetingConstants.SOURCE_WEB
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询会议")
|
||||
@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,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||
|
||||
return ApiResponse.ok(meetingQueryService.pageMeetings(
|
||||
current,
|
||||
size,
|
||||
title,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
viewType,
|
||||
status,
|
||||
isAdmin
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingVO> getDetail(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||
return ApiResponse.ok(meetingQueryService.getDetail(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "导出会议摘要")
|
||||
@GetMapping("/{id}/summary/export")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<byte[]> exportSummary(@PathVariable Long id, @RequestParam(defaultValue = "pdf") String format) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanExportMeeting(meeting, loginUser);
|
||||
|
||||
MeetingVO meetingDetail = meetingQueryService.getDetail(id);
|
||||
if (meetingDetail == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
}
|
||||
|
||||
MeetingSummaryExportResult exportResult = meetingExportService.exportSummary(meeting, meetingDetail, format, loginUser);
|
||||
String encodedFilename = URLEncoder.encode(exportResult.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
|
||||
.contentType(MediaType.parseMediaType(exportResult.getContentType()))
|
||||
.body(exportResult.getContent());
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议转写记录")
|
||||
@GetMapping("/{id}/transcripts")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<MeetingTranscriptVO>> getTranscripts(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||
return ApiResponse.ok(meetingQueryService.getTranscripts(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议章节")
|
||||
@GetMapping("/{id}/chapters")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<Map<String, Object>>> getChapters(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanViewMeeting(meeting, loginUser);
|
||||
return ApiResponse.ok(meetingQueryService.getChapters(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "下载会议转录 Markdown")
|
||||
@GetMapping("/{id}/transcripts/export")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<byte[]> exportTranscripts(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanExportMeeting(meeting, loginUser);
|
||||
|
||||
MeetingVO meetingDetail = meetingQueryService.getDetail(id);
|
||||
if (meetingDetail == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
}
|
||||
|
||||
MeetingTranscriptExportResult exportResult = meetingTranscriptFileService.exportTranscript(meeting, meetingDetail);
|
||||
String encodedFilename = URLEncoder.encode(exportResult.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
|
||||
.contentType(MediaType.parseMediaType(exportResult.getContentType()))
|
||||
.body(exportResult.getContent());
|
||||
}
|
||||
|
||||
@Operation(summary = "查询实时会议状态")
|
||||
@GetMapping("/{id}/realtime/session-status")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<RealtimeMeetingSessionStatusVO> getRealtimeSessionStatus(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanManageRealtimeMeeting(meeting, loginUser);
|
||||
return ApiResponse.ok(realtimeMeetingSessionStateService.getStatus(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量查询实时会议状态")
|
||||
@PostMapping("/realtime/session-status/batch")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Map<Long, RealtimeMeetingSessionStatusVO>> getRealtimeSessionStatuses(@RequestBody List<Long> ids) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Map<Long, RealtimeMeetingSessionStatusVO> result = new LinkedHashMap<>();
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
Map<Long, RealtimeMeetingSessionStatusVO> statuses = realtimeMeetingSessionStateService.getStatuses(ids);
|
||||
for (Long id : ids) {
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanManageRealtimeMeeting(meeting, loginUser);
|
||||
RealtimeMeetingSessionStatusVO status = statuses.get(id);
|
||||
if (status != null) {
|
||||
result.put(id, status);
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
// Preserve previous per-item fallback behavior for inaccessible meetings.
|
||||
}
|
||||
}
|
||||
return ApiResponse.ok(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "暂停实时会议")
|
||||
@PostMapping("/{id}/realtime/pause")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<RealtimeMeetingSessionStatusVO> pauseRealtimeMeeting(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingConstants.SOURCE_WEB);
|
||||
return ApiResponse.ok(realtimeMeetingSessionStateService.pause(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "打开实时会议Socket会话")
|
||||
@PostMapping("/{id}/realtime/socket-session")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<RealtimeSocketSessionVO> openRealtimeSocketSession(@PathVariable Long id,
|
||||
@RequestBody OpenRealtimeSocketSessionCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
return ApiResponse.ok(realtimeMeetingSocketSessionService.createSession(
|
||||
id,
|
||||
command.getAsrModelId(),
|
||||
command.getMode(),
|
||||
command.getLanguage(),
|
||||
command.getUseSpkId(),
|
||||
command.getEnablePunctuation(),
|
||||
command.getEnableItn(),
|
||||
command.getEnableTextRefine(),
|
||||
command.getSaveAudio(),
|
||||
command.getHotwords(),
|
||||
loginUser
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成实时会议")
|
||||
@PostMapping("/{id}/realtime/complete")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> completeRealtimeMeeting(@PathVariable Long id, @RequestBody(required = false) RealtimeMeetingCompleteDTO dto) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanControlRealtimeMeeting(meeting, loginUser, MeetingConstants.SOURCE_WEB);
|
||||
meetingCommandService.completeRealtimeMeeting(
|
||||
id,
|
||||
dto != null ? dto.getAudioUrl() : null,
|
||||
dto != null && Boolean.TRUE.equals(dto.getOverwriteAudio())
|
||||
);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会议讲话人")
|
||||
@PutMapping("/speaker")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改会议讲话人", type = "会议管理")
|
||||
public ApiResponse<Boolean> updateSpeaker(@RequestBody MeetingSpeakerUpdateDTO dto) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(dto.getMeetingId());
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.updateSpeakerInfo(dto.getMeetingId(), dto.getSpeakerId(), dto.getNewName(), dto.getLabel());
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会议转写")
|
||||
@PutMapping("/{id}/transcripts/{transcriptId}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改会议转写", type = "会议管理")
|
||||
public ApiResponse<Boolean> updateTranscript(@PathVariable Long id,
|
||||
@PathVariable Long transcriptId,
|
||||
@RequestBody UpdateMeetingTranscriptCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
command.setMeetingId(id);
|
||||
command.setTranscriptId(transcriptId);
|
||||
meetingCommandService.updateMeetingTranscript(command);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新参会人员")
|
||||
@PutMapping("/{id}/participants")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改会议参会人员", type = "会议管理")
|
||||
public ApiResponse<Boolean> updateParticipants(@PathVariable Long id,
|
||||
@RequestBody UpdateMeetingParticipantsCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
command.setMeetingId(id);
|
||||
meetingCommandService.updateMeetingParticipants(command.getMeetingId(), command.getParticipants());
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "重新生成会议摘要")
|
||||
@PostMapping("/{id}/summary/regenerate")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> reSummary(@PathVariable Long id, @Valid @RequestBody MeetingResummaryDTO dto) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
dto.setMeetingId(id);
|
||||
assertPromptAvailable(dto.getPromptId(), loginUser);
|
||||
meetingCommandService.reSummary(
|
||||
dto.getMeetingId(),
|
||||
dto.getSummaryModelId(),
|
||||
dto.getChapterModelId(),
|
||||
dto.getPromptId(),
|
||||
dto.getUserPrompt(),
|
||||
dto.getSummaryDetailLevel()
|
||||
);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "手动触发外部 n8n 总结编排")
|
||||
@PostMapping("/{id}/summary/orchestration/trigger")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingSummaryOrchestrationTriggerResultVO> triggerExternalSummaryOrchestration(@PathVariable Long id,
|
||||
@RequestParam(defaultValue = "false") boolean force) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
return ApiResponse.ok(meetingCommandService.triggerExternalSummaryOrchestration(id, force));
|
||||
}
|
||||
|
||||
@Operation(summary = "重试音频转写")
|
||||
@PostMapping("/{id}/transcripts/regenerate")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> retryTranscription(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.retryTranscription(id);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "重试 AI 目录")
|
||||
@PostMapping("/{id}/chapters/retry")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> retryChapter(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.retryChapter(id);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "重试会议总结")
|
||||
@PostMapping("/{id}/summary/retry")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> retrySummary(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.retrySummary(id);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会议基础信息")
|
||||
@PutMapping("/{id}/basic")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改会议基础信息", type = "会议管理")
|
||||
public ApiResponse<Boolean> updateBasic(@PathVariable Long id, @RequestBody UpdateMeetingBasicCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
assertPromptAvailable(command.getPromptId(), loginUser);
|
||||
command.setMeetingId(id);
|
||||
meetingCommandService.updateMeetingBasic(command);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会议摘要")
|
||||
@PutMapping("/{id}/summary")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改会议摘要", type = "会议管理")
|
||||
public ApiResponse<Boolean> updateSummary(@PathVariable Long id, @RequestBody UpdateMeetingSummaryCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
command.setMeetingId(id);
|
||||
meetingCommandService.updateSummaryContent(command.getMeetingId(), command.getSummaryContent());
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除会议")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除会议", type = "会议管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.deleteMeeting(id);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
|
||||
private void assertPromptAvailable(Long promptId, LoginUser loginUser) {
|
||||
if (promptId == null) {
|
||||
return;
|
||||
}
|
||||
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||
promptId,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin()
|
||||
);
|
||||
if (!enabled) {
|
||||
throw new RuntimeException("总结模板不可用");
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveCreatorName(LoginUser loginUser) {
|
||||
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
||||
}
|
||||
|
||||
private boolean resolveBooleanParam(String key, boolean defaultValue) {
|
||||
String rawValue = sysParamService.getCachedParamValue(key, String.valueOf(defaultValue));
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
String normalized = rawValue.trim().toLowerCase();
|
||||
if ("1".equals(normalized) || "true".equals(normalized) || "yes".equals(normalized) || "on".equals(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if ("0".equals(normalized) || "false".equals(normalized) || "no".equals(normalized) || "off".equals(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private long resolveLongParam(String key, long defaultValue) {
|
||||
String rawValue = sysParamService.getCachedParamValue(key, String.valueOf(defaultValue));
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
long parsed = Long.parseLong(rawValue.trim());
|
||||
return parsed > 0 ? parsed : defaultValue;
|
||||
} catch (NumberFormatException ex) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.android.AndroidGrpcConnectionSnapshotVO;
|
||||
import com.imeeting.dto.biz.MeetingExternalWorkflowFailureDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryFinalizeDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryPromptContextRequestDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryPromptContextVO;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportDTO;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptChapterImportResultVO;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
|
||||
import com.imeeting.service.android.AndroidGatewayPushService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.config.properties.UnisBaseProperties;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "会议内部编排接口")
|
||||
@RestController
|
||||
@RequestMapping("/sys/internal/meetings")
|
||||
public class MeetingInternalWorkflowController {
|
||||
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
private final MeetingQueryService meetingQueryService;
|
||||
private final AndroidGatewayPushService androidGatewayPushService;
|
||||
private final UnisBaseProperties unisBaseProperties;
|
||||
|
||||
@Value("${imeeting.summary-orchestration.mode:INTERNAL_BUILTIN}")
|
||||
private String summaryOrchestrationMode;
|
||||
|
||||
public MeetingInternalWorkflowController(MeetingCommandService meetingCommandService,
|
||||
MeetingQueryService meetingQueryService,
|
||||
AndroidGatewayPushService androidGatewayPushService,
|
||||
UnisBaseProperties unisBaseProperties) {
|
||||
this.meetingCommandService = meetingCommandService;
|
||||
this.meetingQueryService = meetingQueryService;
|
||||
this.androidGatewayPushService = androidGatewayPushService;
|
||||
this.unisBaseProperties = unisBaseProperties;
|
||||
}
|
||||
|
||||
@Operation(summary = "导入会议章节")
|
||||
@PostMapping("/{meetingId}/chapters/import")
|
||||
public ApiResponse<MeetingTranscriptChapterImportResultVO> importChapters(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@Valid @RequestBody MeetingTranscriptChapterImportDTO command) {
|
||||
if (!isExternalModeEnabled()) {
|
||||
return ApiResponse.error("External n8n summary orchestration is disabled");
|
||||
}
|
||||
if (!isInternalSecretValid(request)) {
|
||||
return ApiResponse.error("Invalid internal secret");
|
||||
}
|
||||
command.setMeetingId(meetingId);
|
||||
return ApiResponse.ok(meetingCommandService.importTranscriptChapters(command));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会议原始转录源")
|
||||
@GetMapping("/{meetingId}/transcript-source")
|
||||
public ApiResponse<MeetingTranscriptSourceVO> getTranscriptSource(HttpServletRequest request, @PathVariable Long meetingId) {
|
||||
if (!isExternalModeEnabled()) {
|
||||
return ApiResponse.error("External n8n summary orchestration is disabled");
|
||||
}
|
||||
if (!isInternalSecretValid(request)) {
|
||||
return ApiResponse.error("Invalid internal secret");
|
||||
}
|
||||
return ApiResponse.ok(meetingQueryService.getTranscriptSource(meetingId));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会议总结提示词上下文")
|
||||
@PostMapping("/{meetingId}/summary-prompt-context")
|
||||
public ApiResponse<MeetingSummaryPromptContextVO> getSummaryPromptContext(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@RequestBody(required = false) MeetingSummaryPromptContextRequestDTO requestDTO) {
|
||||
if (!isExternalModeEnabled()) {
|
||||
return ApiResponse.error("External n8n summary orchestration is disabled");
|
||||
}
|
||||
if (!isInternalSecretValid(request)) {
|
||||
return ApiResponse.error("Invalid internal secret");
|
||||
}
|
||||
return ApiResponse.ok(meetingQueryService.buildSummaryPromptContext(
|
||||
meetingId,
|
||||
requestDTO == null ? new MeetingSummaryPromptContextRequestDTO() : requestDTO
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "回填会议总结")
|
||||
@PostMapping("/{meetingId}/summary/finalize")
|
||||
public ApiResponse<Boolean> finalizeSummary(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@Valid @RequestBody MeetingSummaryFinalizeDTO command) {
|
||||
if (!isExternalModeEnabled()) {
|
||||
return ApiResponse.error("External n8n summary orchestration is disabled");
|
||||
}
|
||||
if (!isInternalSecretValid(request)) {
|
||||
return ApiResponse.error("Invalid internal secret");
|
||||
}
|
||||
command.setMeetingId(meetingId);
|
||||
meetingCommandService.finalizeSummary(command);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "回写外部编排失败")
|
||||
@PostMapping("/{meetingId}/summary/fail")
|
||||
public ApiResponse<Boolean> failSummary(HttpServletRequest request,
|
||||
@PathVariable Long meetingId,
|
||||
@Valid @RequestBody MeetingExternalWorkflowFailureDTO command) {
|
||||
if (!isExternalModeEnabled()) {
|
||||
return ApiResponse.error("External n8n summary orchestration is disabled");
|
||||
}
|
||||
if (!isInternalSecretValid(request)) {
|
||||
return ApiResponse.error("Invalid internal secret");
|
||||
}
|
||||
command.setMeetingId(meetingId);
|
||||
meetingCommandService.markExternalSummaryOrchestrationFailed(command);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询 Android gRPC 连接详情")
|
||||
@GetMapping("/grpc/connections")
|
||||
public ApiResponse<AndroidGrpcConnectionSnapshotVO> listGrpcConnections(HttpServletRequest request) {
|
||||
if (!isInternalSecretValid(request)) {
|
||||
return ApiResponse.error("Invalid internal secret");
|
||||
}
|
||||
return ApiResponse.ok(androidGatewayPushService.snapshotConnections());
|
||||
}
|
||||
|
||||
private boolean isExternalModeEnabled() {
|
||||
return "EXTERNAL_N8N".equalsIgnoreCase(summaryOrchestrationMode);
|
||||
}
|
||||
|
||||
private boolean isInternalSecretValid(HttpServletRequest request) {
|
||||
if (request == null || unisBaseProperties == null || unisBaseProperties.getInternalAuth() == null) {
|
||||
return false;
|
||||
}
|
||||
if (!unisBaseProperties.getInternalAuth().isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
String headerName = unisBaseProperties.getInternalAuth().getHeaderName();
|
||||
String expectedSecret = unisBaseProperties.getInternalAuth().getSecret();
|
||||
if (headerName == null || headerName.isBlank() || expectedSecret == null || expectedSecret.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String actual = request.getHeader(headerName);
|
||||
return expectedSecret.equals(actual);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.MeetingPointsBalanceVO;
|
||||
import com.imeeting.dto.biz.MeetingPointsTransferRequest;
|
||||
import com.imeeting.service.biz.MeetingPointsService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "会议积分")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/meeting-points")
|
||||
public class MeetingPointsController {
|
||||
private final MeetingPointsService meetingPointsService;
|
||||
|
||||
public MeetingPointsController(MeetingPointsService meetingPointsService) {
|
||||
this.meetingPointsService = meetingPointsService;
|
||||
}
|
||||
|
||||
@Operation(summary = "查看会议积分余额")
|
||||
@GetMapping("/balance")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingPointsBalanceVO> getBalance(@RequestParam(value = "userId", required = false) Long userId) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Long targetUserId = resolveTargetUserId(loginUser, userId);
|
||||
return ApiResponse.ok(meetingPointsService.getBalanceView(loginUser.getTenantId(), targetUserId));
|
||||
}
|
||||
|
||||
@Operation(summary = "从公共账户分配积分给个人账户")
|
||||
@PostMapping("/transfer")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> transferPublicPoints(@RequestBody MeetingPointsTransferRequest request) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
ensureAdmin(loginUser);
|
||||
if (request == null) {
|
||||
throw new RuntimeException("分配请求不能为空");
|
||||
}
|
||||
meetingPointsService.transferPublicPointsToUser(
|
||||
loginUser.getTenantId(),
|
||||
request.getTargetUserId(),
|
||||
request.getPoints(),
|
||||
request.getRemark()
|
||||
);
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
private Long resolveTargetUserId(LoginUser loginUser, Long requestedUserId) {
|
||||
if (requestedUserId == null || requestedUserId.equals(loginUser.getUserId())) {
|
||||
return loginUser.getUserId();
|
||||
}
|
||||
ensureAdmin(loginUser);
|
||||
return requestedUserId;
|
||||
}
|
||||
|
||||
private void ensureAdmin(LoginUser loginUser) {
|
||||
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||
if (!isAdmin) {
|
||||
throw new RuntimeException("无权限执行该操作");
|
||||
}
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.MeetingPointsLedgerDetailVO;
|
||||
import com.imeeting.dto.biz.MeetingPointsLedgerListItemVO;
|
||||
import com.imeeting.dto.biz.MeetingPointsOverviewVO;
|
||||
import com.imeeting.service.biz.MeetingPointsQueryService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "积分管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/meeting-points/management")
|
||||
public class MeetingPointsManagementController {
|
||||
private final MeetingPointsQueryService meetingPointsQueryService;
|
||||
|
||||
public MeetingPointsManagementController(MeetingPointsQueryService meetingPointsQueryService) {
|
||||
this.meetingPointsQueryService = meetingPointsQueryService;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取积分管理总览")
|
||||
@GetMapping("/overview")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingPointsOverviewVO> getOverview() {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||
return ApiResponse.ok(meetingPointsQueryService.getOverview(loginUser.getTenantId(), loginUser.getUserId(), isAdmin));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询积分消耗流水")
|
||||
@GetMapping("/ledgers")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<MeetingPointsLedgerListItemVO>>> pageLedgers(
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size,
|
||||
@RequestParam(required = false) String username,
|
||||
@RequestParam(required = false) String pointsType) {
|
||||
return ApiResponse.ok(meetingPointsQueryService.pageLedgers(currentLoginUser().getTenantId(), current, size, username, pointsType));
|
||||
}
|
||||
|
||||
@Operation(summary = "查看积分消耗流水详情")
|
||||
@GetMapping("/ledgers/{ledgerId}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<MeetingPointsLedgerDetailVO> getLedgerDetail(@PathVariable Long ledgerId) {
|
||||
return ApiResponse.ok(meetingPointsQueryService.getLedgerDetail(currentLoginUser().getTenantId(), ledgerId));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.MeetingPreviewAccessVO;
|
||||
import com.imeeting.dto.biz.PublicMeetingPreviewVO;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "会议公开预览")
|
||||
@RestController
|
||||
@RequestMapping("/api/public/meetings")
|
||||
public class MeetingPublicPreviewController {
|
||||
|
||||
private final MeetingQueryService meetingQueryService;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
|
||||
public MeetingPublicPreviewController(MeetingQueryService meetingQueryService,
|
||||
MeetingAccessService meetingAccessService) {
|
||||
this.meetingQueryService = meetingQueryService;
|
||||
this.meetingAccessService = meetingAccessService;
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会议预览访问要求")
|
||||
@GetMapping("/{id}/preview/access")
|
||||
public ApiResponse<MeetingPreviewAccessVO> getPreviewAccess(@PathVariable Long id) {
|
||||
try {
|
||||
Meeting meeting = meetingAccessService.requireMeetingIgnoreTenant(id);
|
||||
return ApiResponse.ok(new MeetingPreviewAccessVO(meetingAccessService.isPreviewPasswordRequired(meeting)));
|
||||
} catch (RuntimeException ex) {
|
||||
return ApiResponse.error(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会议公开预览内容")
|
||||
@GetMapping("/{id}/preview")
|
||||
public ApiResponse<PublicMeetingPreviewVO> getPreview(@PathVariable Long id,
|
||||
@RequestParam(required = false) String accessPassword) {
|
||||
try {
|
||||
Meeting meeting = meetingAccessService.requireMeetingIgnoreTenant(id);
|
||||
meetingAccessService.assertCanPreviewMeeting(meeting, accessPassword);
|
||||
|
||||
PublicMeetingPreviewVO data = new PublicMeetingPreviewVO();
|
||||
data.setMeeting(meetingQueryService.getDetailIgnoreTenant(id));
|
||||
if (data.getMeeting() != null) {
|
||||
data.getMeeting().setAccessPassword(null);
|
||||
}
|
||||
data.setTranscripts(meetingQueryService.getTranscripts(id));
|
||||
data.setChapters(meetingQueryService.getChaptersIgnoreTenant(id));
|
||||
return ApiResponse.ok(data);
|
||||
} catch (RuntimeException ex) {
|
||||
return ApiResponse.error(ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
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.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "提示词模板管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/prompt")
|
||||
public class PromptTemplateController {
|
||||
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
|
||||
public PromptTemplateController(PromptTemplateService promptTemplateService) {
|
||||
this.promptTemplateService = promptTemplateService;
|
||||
}
|
||||
|
||||
@Operation(summary = "新增提示词模板")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增提示词模板", type = "提示词模板管理")
|
||||
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("无权创建公共模板");
|
||||
}
|
||||
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()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改提示词模板")
|
||||
@PutMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改提示词模板", type = "提示词模板管理")
|
||||
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("模板不存在");
|
||||
}
|
||||
|
||||
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("无权修改该模板");
|
||||
}
|
||||
|
||||
return ApiResponse.ok(promptTemplateService.updateTemplate(dto, loginUser.getUserId(), loginUser.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新提示词模板状态")
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改提示词模板状态", type = "提示词模板管理")
|
||||
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("模板不存在");
|
||||
}
|
||||
|
||||
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("模板不存在或无权限访问");
|
||||
}
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除提示词模板")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除提示词模板", type = "提示词模板管理")
|
||||
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("无权删除该模板");
|
||||
}
|
||||
|
||||
return ApiResponse.ok(promptTemplateService.removeById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询提示词模板详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PromptTemplateVO> detail(@PathVariable Long id) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
return ApiResponse.ok(promptTemplateService.getTemplateDetail(
|
||||
id,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin()
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询提示词模板")
|
||||
@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,63 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.android.AndroidPublicLoginConfirmPayload;
|
||||
import com.imeeting.dto.android.AndroidPublicMeetingSessionState;
|
||||
import com.imeeting.service.android.AndroidMeetingPushService;
|
||||
import com.imeeting.service.android.AndroidPublicMeetingSessionService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "公有设备扫码建会接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/public-device-meetings")
|
||||
public class PublicDeviceMeetingController {
|
||||
private final AndroidPublicMeetingSessionService androidPublicMeetingSessionService;
|
||||
private final AndroidMeetingPushService androidMeetingPushService;
|
||||
|
||||
public PublicDeviceMeetingController(AndroidPublicMeetingSessionService androidPublicMeetingSessionService,
|
||||
AndroidMeetingPushService androidMeetingPushService) {
|
||||
this.androidPublicMeetingSessionService = androidPublicMeetingSessionService;
|
||||
this.androidMeetingPushService = androidMeetingPushService;
|
||||
}
|
||||
|
||||
@Operation(summary = "H5扫码确认公有设备登录")
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "确认成功返回 true,并向设备推送扫码用户信息",
|
||||
content = @Content(schema = @Schema(implementation = Boolean.class))
|
||||
)
|
||||
})
|
||||
@PostMapping("/sessions/{sessionId}/create")
|
||||
public ApiResponse<Boolean> createBySession(@PathVariable String sessionId) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
AndroidPublicMeetingSessionState session = androidPublicMeetingSessionService.require(sessionId);
|
||||
AndroidPublicLoginConfirmPayload payload = new AndroidPublicLoginConfirmPayload();
|
||||
payload.setSessionId(sessionId);
|
||||
payload.setTenantId(loginUser.getTenantId());
|
||||
payload.setUserId(loginUser.getUserId());
|
||||
payload.setUsername(loginUser.getUsername());
|
||||
payload.setDisplayName(loginUser.getDisplayName());
|
||||
androidMeetingPushService.pushPublicLoginConfirm(session.getDeviceId(), payload);
|
||||
androidPublicMeetingSessionService.invalidate(sessionId);
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof LoginUser loginUser)) {
|
||||
throw new RuntimeException("未获取到登录用户");
|
||||
}
|
||||
return loginUser;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.ScreenSaverAdminVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverDTO;
|
||||
import com.imeeting.dto.biz.ScreenSaverImageUploadVO;
|
||||
import com.imeeting.dto.biz.ScreenSaverUserSettingsDTO;
|
||||
import com.imeeting.dto.biz.ScreenSaverUserSettingsVO;
|
||||
import com.imeeting.entity.biz.ScreenSaver;
|
||||
import com.imeeting.service.biz.ScreenSaverService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "屏保管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/screen-savers")
|
||||
@RequiredArgsConstructor
|
||||
public class ScreenSaverController {
|
||||
|
||||
private final ScreenSaverService screenSaverService;
|
||||
|
||||
@Operation(summary = "查询屏保列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<ScreenSaverAdminVO>> list(@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "scopeType", required = false) String scopeType,
|
||||
@RequestParam(value = "ownerUserId", required = false) Long ownerUserId) {
|
||||
return ApiResponse.ok(screenSaverService.listForAdmin(currentLoginUser(), keyword, status, scopeType, ownerUserId));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询当前用户屏保播放设置")
|
||||
@GetMapping("/my-settings")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ScreenSaverUserSettingsVO> getMySettings() {
|
||||
return ApiResponse.ok(screenSaverService.getMySettings(currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新当前用户屏保播放设置")
|
||||
@PutMapping("/my-settings")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改个人屏保设置", type = "屏保管理")
|
||||
public ApiResponse<ScreenSaverUserSettingsVO> updateMySettings(@RequestBody ScreenSaverUserSettingsDTO dto) {
|
||||
return ApiResponse.ok(screenSaverService.updateMySettings(dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "新增屏保")
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增屏保", type = "屏保管理")
|
||||
public ApiResponse<ScreenSaver> create(@RequestBody ScreenSaverDTO dto) {
|
||||
return ApiResponse.ok(screenSaverService.create(dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改屏保")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改屏保", type = "屏保管理")
|
||||
public ApiResponse<ScreenSaver> update(@PathVariable Long id, @RequestBody ScreenSaverDTO dto) {
|
||||
return ApiResponse.ok(screenSaverService.update(id, dto, currentLoginUser()));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新屏保状态")
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "修改屏保状态", type = "屏保管理")
|
||||
public ApiResponse<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
boolean success = screenSaverService.updateStatus(id, status, currentLoginUser());
|
||||
if (!success) {
|
||||
return ApiResponse.error("屏保不存在或无权限访问");
|
||||
}
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除屏保")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除屏保", type = "屏保管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
screenSaverService.removeScreenSaver(id, currentLoginUser());
|
||||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
@Operation(summary = "上传屏保图片")
|
||||
@PostMapping("/upload-image")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<ScreenSaverImageUploadVO> uploadImage(@RequestParam("imageFile") MultipartFile imageFile) throws IOException {
|
||||
return ApiResponse.ok(screenSaverService.uploadImage(imageFile));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.SpeakerRegisterDTO;
|
||||
import com.imeeting.dto.biz.SpeakerVO;
|
||||
import com.imeeting.service.biz.SpeakerService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.common.annotation.Log;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "讲话人管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/speaker")
|
||||
public class SpeakerController {
|
||||
|
||||
private final SpeakerService speakerService;
|
||||
|
||||
public SpeakerController(SpeakerService speakerService) {
|
||||
this.speakerService = speakerService;
|
||||
}
|
||||
|
||||
@Operation(summary = "注册讲话人样本")
|
||||
@PostMapping("/register")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "新增讲话人样本", type = "讲话人管理")
|
||||
public ApiResponse<SpeakerVO> register(@ModelAttribute SpeakerRegisterDTO registerDTO) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserId() == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
registerDTO.setCreatorId(loginUser.getUserId());
|
||||
return ApiResponse.ok(speakerService.register(registerDTO, loginUser));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询讲话人")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<SpeakerVO>>> page(
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "8") Integer size,
|
||||
@RequestParam(required = false) String name) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserId() == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
return ApiResponse.ok(speakerService.pageVisible(current, size, name, loginUser));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询可见讲话人")
|
||||
@GetMapping("/list")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<List<SpeakerVO>> list() {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserId() == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
return ApiResponse.ok(speakerService.listVisible(loginUser));
|
||||
}
|
||||
|
||||
@Operation(summary = "同步当前 ASR 声纹")
|
||||
@PostMapping("/{id}/sync")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<Boolean> sync(@PathVariable Long id) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserId() == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
speakerService.syncCurrentAsr(id, loginUser);
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除讲话人")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "删除讲话人", type = "讲话人管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
LoginUser loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserId() == null) {
|
||||
return ApiResponse.error("未获取到用户信息");
|
||||
}
|
||||
speakerService.deleteSpeaker(id, loginUser);
|
||||
return ApiResponse.ok(Boolean.TRUE);
|
||||
}
|
||||
|
||||
private LoginUser getLoginUser() {
|
||||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
if (principal instanceof LoginUser loginUser) {
|
||||
return loginUser;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.imeeting.controller.biz;
|
||||
|
||||
import com.imeeting.dto.biz.TenantMeetingPointsSettingVO;
|
||||
import com.imeeting.dto.biz.UpdateTenantMeetingPointsBalanceCheckCommand;
|
||||
import com.imeeting.service.biz.TenantMeetingPointsManagementService;
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "租户积分余额校验")
|
||||
@RestController
|
||||
@RequestMapping("/api/biz/tenant-meeting-points/settings")
|
||||
public class TenantMeetingPointsSettingController {
|
||||
private final TenantMeetingPointsManagementService tenantMeetingPointsManagementService;
|
||||
|
||||
public TenantMeetingPointsSettingController(TenantMeetingPointsManagementService tenantMeetingPointsManagementService) {
|
||||
this.tenantMeetingPointsManagementService = tenantMeetingPointsManagementService;
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询租户积分余额校验配置")
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<PageResult<List<TenantMeetingPointsSettingVO>>> pageSettings(
|
||||
@RequestParam(defaultValue = "1") Integer current,
|
||||
@RequestParam(defaultValue = "20") Integer size,
|
||||
@RequestParam(required = false) String tenantName,
|
||||
@RequestParam(required = false) String tenantCode,
|
||||
@RequestParam(required = false) Boolean balanceCheckEnabled) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
ensurePlatformAdmin(loginUser);
|
||||
return ApiResponse.ok(tenantMeetingPointsManagementService.pageSettings(current, size, tenantName, tenantCode, balanceCheckEnabled));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取当前租户积分余额校验配置")
|
||||
@GetMapping("/current")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<TenantMeetingPointsSettingVO> getCurrentSetting() {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
ensureAdmin(loginUser);
|
||||
return ApiResponse.ok(tenantMeetingPointsManagementService.getCurrentTenantSetting(loginUser.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新租户积分余额校验开关")
|
||||
@PutMapping("/{tenantId}/balance-check")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ApiResponse<TenantMeetingPointsSettingVO> updateBalanceCheck(@PathVariable Long tenantId,
|
||||
@Valid @RequestBody UpdateTenantMeetingPointsBalanceCheckCommand command) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
ensureCanManageTenant(loginUser, tenantId);
|
||||
if (command == null || command.getBalanceCheckEnabled() == null) {
|
||||
throw new RuntimeException("余额校验开关不能为空");
|
||||
}
|
||||
return ApiResponse.ok(tenantMeetingPointsManagementService.updateBalanceCheck(
|
||||
tenantId,
|
||||
command.getBalanceCheckEnabled(),
|
||||
command.getRemark(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getDisplayName()
|
||||
));
|
||||
}
|
||||
|
||||
private void ensureCanManageTenant(LoginUser loginUser, Long tenantId) {
|
||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||
return;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||
throw new RuntimeException("无权限执行该操作");
|
||||
}
|
||||
if (tenantId == null || !tenantId.equals(loginUser.getTenantId())) {
|
||||
throw new RuntimeException("租户管理员只能操作当前租户");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureAdmin(LoginUser loginUser) {
|
||||
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||
throw new RuntimeException("无权限执行该操作");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePlatformAdmin(LoginUser loginUser) {
|
||||
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||
throw new RuntimeException("仅平台管理员可查看租户列表");
|
||||
}
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.android;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
public class AndroidAuthContext {
|
||||
private String authMode;
|
||||
private String deviceId;
|
||||
private Long tenantId;
|
||||
private String tenantCode;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private String displayName;
|
||||
private Boolean platformAdmin;
|
||||
private Boolean tenantAdmin;
|
||||
private Set<String> permissions;
|
||||
private String appId;
|
||||
private String appVersion;
|
||||
private String platform;
|
||||
private String accessToken;
|
||||
private boolean anonymous;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.imeeting.dto.android;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
@Data
|
||||
public class AndroidChunkUploadSessionState {
|
||||
private Long meetingId;
|
||||
private String deviceId;
|
||||
private Integer totalChunks;
|
||||
private String fileName;
|
||||
private String contentType;
|
||||
private Set<Integer> receivedChunks = new TreeSet<>();
|
||||
private Set<String> uploadedChunkFileNames = new TreeSet<>();
|
||||
private Map<Integer, String> chunkFileNames = new TreeMap<>();
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.imeeting.dto.android;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AndroidCreateRealtimeMeetingCommand {
|
||||
private String title;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime meetingTime;
|
||||
|
||||
private String participants;
|
||||
private String tags;
|
||||
private Long hostUserId;
|
||||
private String hostName;
|
||||
private Long asrModelId;
|
||||
private Long summaryModelId;
|
||||
private Long promptId;
|
||||
private Long hotWordGroupId;
|
||||
@Schema(description = "总结详细程度:DETAILED=详细,STANDARD=标准,BRIEF=简洁")
|
||||
private String summaryDetailLevel;
|
||||
private String mode;
|
||||
private String language;
|
||||
private Integer useSpkId;
|
||||
private Boolean enablePunctuation;
|
||||
private Boolean enableItn;
|
||||
private Boolean enableTextRefine;
|
||||
private Boolean saveAudio;
|
||||
private List<String> hotWords;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.imeeting.dto.android;
|
||||
|
||||
import com.imeeting.dto.biz.RealtimeMeetingResumeConfig;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingSessionStatusVO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "Android 实时会议创建结果")
|
||||
@Data
|
||||
public class AndroidCreateRealtimeMeetingVO {
|
||||
@Schema(description = "会议 ID")
|
||||
private Long meetingId;
|
||||
@Schema(description = "会议标题")
|
||||
private String title;
|
||||
@Schema(description = "主持人用户 ID")
|
||||
private Long hostUserId;
|
||||
@Schema(description = "主持人名称")
|
||||
private String hostName;
|
||||
@Schema(description = "实时音频采样率")
|
||||
private Integer sampleRate;
|
||||
@Schema(description = "音频通道数")
|
||||
private Integer channels;
|
||||
@Schema(description = "音频编码格式")
|
||||
private String encoding;
|
||||
@Schema(description = "最终生效的 ASR 模型 ID")
|
||||
private Long resolvedAsrModelId;
|
||||
@Schema(description = "最终生效的 ASR 模型名称")
|
||||
private String resolvedAsrModelName;
|
||||
@Schema(description = "最终生效的总结模型 ID")
|
||||
private Long resolvedSummaryModelId;
|
||||
@Schema(description = "最终生效的总结模型名称")
|
||||
private String resolvedSummaryModelName;
|
||||
@Schema(description = "最终生效的提示词模板 ID")
|
||||
private Long resolvedPromptId;
|
||||
@Schema(description = "最终生效的提示词模板名称")
|
||||
private String resolvedPromptName;
|
||||
@Schema(description = "恢复会议时使用的运行时参数")
|
||||
private RealtimeMeetingResumeConfig resumeConfig;
|
||||
@Schema(description = "当前实时会议状态")
|
||||
private RealtimeMeetingSessionStatusVO status;
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.imeeting.dto.android;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "Android 设备首页统计响应")
|
||||
public class AndroidDeviceHomeStatsVO {
|
||||
|
||||
@Schema(description = "设备名称")
|
||||
private String deviceName;
|
||||
|
||||
@Schema(description = "授权类型:1-临时,2-正式")
|
||||
private Integer licenseType;
|
||||
|
||||
@Schema(description = "剩余分钟数")
|
||||
private Long remainingMinutes;
|
||||
|
||||
@Schema(description = "会议数量")
|
||||
private Long meetingCount;
|
||||
|
||||
@Schema(description = "会议总时长,单位分钟")
|
||||
private Long meetingDurationMinutes;
|
||||
|
||||
@Schema(description = "登录用户数")
|
||||
private Long loginUserCount;
|
||||
|
||||
@Schema(description = "H5 地址")
|
||||
private String h5BaseUrl;
|
||||
|
||||
@Schema(description = "天气信息")
|
||||
private AndroidDeviceHomeWeatherVO weather;
|
||||
|
||||
@Schema(description = "是否已登录")
|
||||
private Boolean loggedIn;
|
||||
@Schema(description = "是否开启余额校验")
|
||||
private Boolean balanceCheckEnabled;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue