feat: 添加实时会议状态处理和转录内容检查
- 在 `AiTaskServiceImpl` 中添加 `buildTranscriptText` 和 `failPendingSummaryTask` 方法,用于构建转录文本和处理失败的摘要任务 - 更新 `doDispatchSummaryTask` 和 `dispatchTasks` 方法,以在转录内容为空时处理失败情况 - 在前端 `Meetings.tsx` 中添加实时会议状态处理逻辑,支持实时会议的暂停、进行中和待开始状态 - 更新测试类 `AiTaskServiceImplTest` 以包含新的测试用例,验证转录内容为空时的任务处理逻辑dev_na
parent
8d0ef246f3
commit
4e38580258
|
|
@ -1,11 +1,7 @@
|
||||||
# 鏁版嵁搴撶粨鏋勬枃妗o紙PostgreSQL锛?
|
# 鏁版嵁搴撶粨鏋勬枃妗o紙PostgreSQL锛?
|
||||||
|
|
||||||
鏈枃妗f牴鎹?`backend/design/db_schema_pgsql.sql` 鐢熸垚锛屾弿杩板綋鍓嶆牳蹇冭〃缁撴瀯銆佸瓧娈点€佺害鏉熶笌绱㈠紩銆?
|
鏈枃妗f牴鎹?`backend/design/db_schema_pgsql.sql` 鐢熸垚锛屾弿杩板綋鍓嶆牳蹇冭〃缁撴瀯銆佸瓧娈点€佺害鏉熶笌绱㈠紩銆?
|
||||||
|
|
||||||
## 0. 绉熸埛涓庣粍缁?
|
## 0. 绉熸埛涓庣粍缁?
|
||||||
|
### 0.1 `sys_tenant`锛堢鎴疯〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
### 0.1 `sys_tenant`锛堢鎴疯〃锛?
|
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 绉熸埛ID |
|
| id | BIGSERIAL | PK | 绉熸埛ID |
|
||||||
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 绉熸埛缂栫爜 |
|
| tenant_code | VARCHAR(64) | NOT NULL, UNIQUE | 绉熸埛缂栫爜 |
|
||||||
|
|
@ -19,11 +15,9 @@
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `uk_tenant_code`锛歚UNIQUE (tenant_code) WHERE is_deleted = FALSE`
|
||||||
- `uk_tenant_code`锛歚UNIQUE (tenant_code) WHERE is_deleted = FALSE`
|
|
||||||
|
|
||||||
### 0.2 `sys_org`锛堢粍缁囨灦鏋勮〃锛?
|
### 0.2 `sys_org`锛堢粍缁囨灦鏋勮〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 缁勭粐ID |
|
| id | BIGSERIAL | PK | 缁勭粐ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
|
|
@ -37,17 +31,13 @@
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
|
|
||||||
澶栭敭锛?
|
澶栭敭锛?- `fk_org_parent`锛歚parent_id -> sys_org(id)`
|
||||||
- `fk_org_parent`锛歚parent_id -> sys_org(id)`
|
|
||||||
- `fk_org_tenant`锛歚tenant_id -> sys_tenant(id)`
|
- `fk_org_tenant`锛歚tenant_id -> sys_tenant(id)`
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `idx_org_tenant`锛歚(tenant_id)`
|
||||||
- `idx_org_tenant`锛歚(tenant_id)`
|
|
||||||
|
|
||||||
## 1. 鐢ㄦ埛涓庤鑹?
|
## 1. 鐢ㄦ埛涓庤鑹?
|
||||||
|
### 1.1 `sys_user`锛堢敤鎴疯〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
### 1.1 `sys_user`锛堢敤鎴疯〃锛?
|
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| user_id | BIGSERIAL | PK | 鐢ㄦ埛ID |
|
| user_id | BIGSERIAL | PK | 鐢ㄦ埛ID |
|
||||||
| username | VARCHAR(50) | NOT NULL, UNIQUE | 鐧诲綍鍚?|
|
| username | VARCHAR(50) | NOT NULL, UNIQUE | 鐧诲綍鍚?|
|
||||||
|
|
@ -62,11 +52,9 @@
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
| is_platform_admin | BOOLEAN | DEFAULT false | 鏄惁骞冲彴绠$悊鍛?|
|
| is_platform_admin | BOOLEAN | DEFAULT false | 鏄惁骞冲彴绠$悊鍛?|
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `uk_user_username`锛歚UNIQUE (username) WHERE is_deleted = FALSE`
|
||||||
- `uk_user_username`锛歚UNIQUE (username) WHERE is_deleted = FALSE`
|
|
||||||
|
|
||||||
### 1.2 `sys_role`锛堣鑹茶〃锛?
|
### 1.2 `sys_role`锛堣鑹茶〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| role_id | BIGSERIAL | PK | 瑙掕壊ID |
|
| role_id | BIGSERIAL | PK | 瑙掕壊ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
|
|
@ -78,8 +66,7 @@
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `idx_sys_role_tenant`锛歚(tenant_id)`
|
||||||
- `idx_sys_role_tenant`锛歚(tenant_id)`
|
|
||||||
- `uk_role_code`锛歚UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE`
|
- `uk_role_code`锛歚UNIQUE (tenant_id, role_code) WHERE is_deleted = FALSE`
|
||||||
|
|
||||||
### 1.3 `sys_user_role`锛堢敤鎴?瑙掕壊鍏宠仈琛紝绉熸埛寮虹害鏉燂級
|
### 1.3 `sys_user_role`锛堢敤鎴?瑙掕壊鍏宠仈琛紝绉熸埛寮虹害鏉燂級
|
||||||
|
|
@ -93,11 +80,9 @@
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
|
||||||
鍞竴绾︽潫锛?
|
鍞竴绾︽潫锛?- `UNIQUE (tenant_id, user_id, role_id) WHERE is_deleted = 0`
|
||||||
- `UNIQUE (tenant_id, user_id, role_id) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
### 1.4 `sys_tenant_user`锛堢鎴锋垚鍛樺叧鑱旇〃锛?
|
### 1.4 `sys_tenant_user`锛堢鎴锋垚鍛樺叧鑱旇〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 鍏宠仈ID |
|
| id | BIGSERIAL | PK | 鍏宠仈ID |
|
||||||
| user_id | BIGINT | NOT NULL | 鐢ㄦ埛ID |
|
| user_id | BIGINT | NOT NULL | 鐢ㄦ埛ID |
|
||||||
|
|
@ -108,13 +93,10 @@
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `uk_tenant_user`锛歚UNIQUE (user_id, tenant_id) WHERE is_deleted = 0`
|
||||||
- `uk_tenant_user`锛歚UNIQUE (user_id, tenant_id) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
## 2. 鏉冮檺/瀛楀吀/鍙傛暟锛堝叏灞€鍏变韩锛?
|
## 2. 鏉冮檺/瀛楀吀/鍙傛暟锛堝叏灞€鍏变韩锛?
|
||||||
|
### 2.1 `sys_permission`锛堟潈闄愯〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
### 2.1 `sys_permission`锛堟潈闄愯〃锛?
|
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| perm_id | BIGSERIAL | PK | 鏉冮檺ID |
|
| perm_id | BIGSERIAL | PK | 鏉冮檺ID |
|
||||||
| parent_id | BIGINT | | 鐖剁骇鏉冮檺ID |
|
| parent_id | BIGINT | | 鐖剁骇鏉冮檺ID |
|
||||||
|
|
@ -134,8 +116,7 @@
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
|
|
||||||
### 2.2 `sys_dict_type`锛堝瓧鍏哥被鍨嬭〃锛?
|
### 2.2 `sys_dict_type`锛堝瓧鍏哥被鍨嬭〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| dict_type_id | BIGSERIAL | PK | 绫诲瀷ID |
|
| dict_type_id | BIGSERIAL | PK | 绫诲瀷ID |
|
||||||
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 绫诲瀷缂栫爜 |
|
| type_code | VARCHAR(50) | NOT NULL, UNIQUE | 绫诲瀷缂栫爜 |
|
||||||
|
|
@ -168,12 +149,10 @@
|
||||||
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎鏍囪 |
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `idx_dict_item_type`锛歚(type_code)`
|
||||||
- `idx_dict_item_type`锛歚(type_code)`
|
|
||||||
- `uk_dict_item_value`锛歚UNIQUE (type_code, item_value)`
|
- `uk_dict_item_value`锛歚UNIQUE (type_code, item_value)`
|
||||||
|
|
||||||
### 2.4 `sys_param`锛堢郴缁熷弬鏁拌〃锛?
|
### 2.4 `sys_param`锛堢郴缁熷弬鏁拌〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 鍙傛暟ID |
|
| id | BIGSERIAL | PK | 鍙傛暟ID |
|
||||||
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 鍙傛暟閿?|
|
| param_key | VARCHAR(100) | NOT NULL, UNIQUE | 鍙傛暟閿?|
|
||||||
|
|
@ -186,8 +165,7 @@
|
||||||
|
|
||||||
## 3. 鏃ュ織锛堢鎴烽殧绂伙級
|
## 3. 鏃ュ織锛堢鎴烽殧绂伙級
|
||||||
|
|
||||||
### 3.1 `sys_log`锛堢郴缁熸棩蹇楄〃锛?
|
### 3.1 `sys_log`锛堢郴缁熸棩蹇楄〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 鏃ュ織ID |
|
| id | BIGSERIAL | PK | 鏃ュ織ID |
|
||||||
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID |
|
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID |
|
||||||
|
|
@ -202,13 +180,11 @@
|
||||||
| duration | BIGINT | | 鑰楁椂锛坢s锛?|
|
| duration | BIGINT | | 鑰楁椂锛坢s锛?|
|
||||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 鍒涘缓鏃堕棿 |
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `idx_log_tenant_type`锛歚(tenant_id, log_type, created_at)`
|
||||||
- `idx_log_tenant_type`锛歚(tenant_id, log_type, created_at)`
|
|
||||||
|
|
||||||
## 4. 骞冲彴閰嶇疆
|
## 4. 骞冲彴閰嶇疆
|
||||||
|
|
||||||
### 4.1 `sys_platform_config`锛堝钩鍙扮鐞嗚〃锛?
|
### 4.1 `sys_platform_config`锛堝钩鍙扮鐞嗚〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGINT | PK | 鍥哄畾涓?1 |
|
| id | BIGINT | PK | 鍥哄畾涓?1 |
|
||||||
| project_name | VARCHAR(128) | NOT NULL | 椤圭洰鍚嶇О |
|
| project_name | VARCHAR(128) | NOT NULL | 椤圭洰鍚嶇О |
|
||||||
|
|
@ -223,225 +199,192 @@
|
||||||
|
|
||||||
## 5. 涓氬姟妯″潡
|
## 5. 涓氬姟妯″潡
|
||||||
|
|
||||||
### 5.1 `biz_speakers`锛堝0绾瑰彂瑷€浜鸿〃锛?
|
### 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 |
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
| user_id | BIGINT | | 鍏宠仈绯荤粺鐢ㄦ埛ID |
|
| group_name | VARCHAR(100) | NOT NULL | 鐑瘝缁勫悕绉?|
|
||||||
| name | VARCHAR(100) | NOT NULL | 鍙戣█浜哄鍚?|
|
| creator_id | BIGINT | | 鍒涘缓浜篒D |
|
||||||
| voice_path | VARCHAR(512) | | 鍘熷鏂囦欢璺緞 |
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€侊紙1:鍚敤锛?:绂佺敤锛?|
|
||||||
| voice_ext | VARCHAR(10) | | 鏂囦欢鍚庣紑 |
|
| remark | VARCHAR(255) | | 澶囨敞 |
|
||||||
| voice_size | BIGINT | | 鏂囦欢澶у皬 |
|
|
||||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:宸蹭繚瀛? 2:娉ㄥ唽涓? 3:宸叉敞鍐? |
|
|
||||||
| embedding | VECTOR | | 澹扮汗鐗瑰緛鍚戦噺 |
|
|
||||||
| remark | TEXT | | 澶囨敞 |
|
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `idx_hot_word_group_tenant`锛歚(tenant_id) WHERE is_deleted = 0`
|
||||||
- `idx_speaker_tenant`: `(tenant_id)`
|
- `uk_hot_word_group_name_scope`锛歚UNIQUE (tenant_id, group_name) WHERE is_deleted = 0`
|
||||||
- `idx_speaker_user`: `(user_id) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
### 5.2 `biz_hot_words`锛堢儹璇嶇鐞嗚〃锛?
|
### 5.3 `biz_hot_words`锛堢儹璇嶇鐞嗚〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
||||||
| word | VARCHAR(100) | NOT NULL | 鐑瘝鍘熸枃 |
|
| word | VARCHAR(100) | NOT NULL | 鐑瘝鍘熸枃 |
|
||||||
| pinyin_list | JSONB | | 鎷奸煶鏁扮粍 |
|
| is_public | SMALLINT | DEFAULT 0 | 鏄惁绉熸埛鍏紑锛?:鍏紑锛?:涓汉绉佹湁锛?|
|
||||||
| match_strategy | SMALLINT | DEFAULT 1 | 鍖归厤绛栫暐 (1:绮剧‘, 2:妯$硦) |
|
| creator_id | BIGINT | | 鍒涘缓鑰匢D |
|
||||||
| category | VARCHAR(50) | | 绫诲埆 (浜哄悕銆佹湳璇瓑) |
|
| pinyin_list | TEXT | | 鎷奸煶鏁扮粍 |
|
||||||
| hot_word_group_id | BIGINT | | 所属热词组 ID |
|
| match_strategy | SMALLINT | DEFAULT 1 | 鍖归厤绛栫暐锛?:绮剧‘鍖归厤锛?:鎷奸煶妯$硦鍖归厤锛?|
|
||||||
| weight | INTEGER | DEFAULT 10 | 鏉冮噸 (1-100) |
|
| category | VARCHAR(50) | | 绫诲埆锛堜汉鍚嶃€佹湳璇€佸湴鍚嶏級 |
|
||||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:鍚敤, 0:绂佺敤) |
|
| hot_word_group_id | BIGINT | | 鎵€灞炵儹璇嶇粍ID |
|
||||||
| is_synced | SMALLINT | DEFAULT 0 | 宸插悓姝ョ涓夋柟鏍囪 |
|
| weight | INTEGER | DEFAULT 10 | 鏉冮噸锛?-100锛?|
|
||||||
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€侊紙1:鍚敤锛?:绂佺敤锛?|
|
||||||
|
| is_synced | SMALLINT | DEFAULT 0 | 鏄惁宸插悓姝ョ涓夋柟寮曟搸锛?:鏈悓姝ワ紝1:宸插悓姝ワ級 |
|
||||||
| remark | TEXT | | 澶囨敞 |
|
| remark | TEXT | | 澶囨敞 |
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `idx_hotword_tenant`锛歚(tenant_id)`
|
||||||
- `idx_hotword_tenant`: `(tenant_id)`
|
- `idx_hotword_word`锛歚(word) WHERE is_deleted = 0`
|
||||||
- `idx_hotword_word`: `(word) WHERE is_deleted = 0`
|
- `idx_hotword_group`锛歚(hot_word_group_id) WHERE is_deleted = 0`
|
||||||
- `idx_hotword_group`: `(hot_word_group_id) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
### 5.3 `biz_hot_word_groups`(热词组表)
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| id | BIGSERIAL | PK | 主键ID |
|
|
||||||
| 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 | DEFAULT 0 | 逻辑删除 |
|
|
||||||
|
|
||||||
索引:
|
|
||||||
- `idx_hot_word_group_tenant`: `(tenant_id) WHERE is_deleted = 0`
|
|
||||||
- `uk_hot_word_group_name_scope`: `(tenant_id, group_name) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
### 5.4 `biz_prompt_templates`锛堟彁绀鸿瘝妯℃澘琛級
|
### 5.4 `biz_prompt_templates`锛堟彁绀鸿瘝妯℃澘琛級
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛ID |
|
| tenant_id | BIGINT | NOT NULL, DEFAULT 0 | 绉熸埛ID锛? 涓虹郴缁熺骇锛?|
|
||||||
| template_name | VARCHAR(100) | NOT NULL | 妯℃澘鍚嶇О |
|
| template_name | VARCHAR(100) | NOT NULL | 妯℃澘鍚嶇О |
|
||||||
| category | VARCHAR(20) | | 鍒嗙被 (瀛楀吀: biz_prompt_category) |
|
| description | VARCHAR(255) | | 妯℃澘鎻忚堪 |
|
||||||
| is_system | SMALLINT | DEFAULT 0 | 鏄惁棰勭疆 (1:鏄? 0:鍚? |
|
| category | VARCHAR(20) | | 鍒嗙被锛堝瓧鍏革細`biz_prompt_category`锛?|
|
||||||
|
| is_system | SMALLINT | DEFAULT 0 | 鏄惁绯荤粺棰勭疆锛?:鏄紝0:鍚︼級 |
|
||||||
| creator_id | BIGINT | | 鍒涘缓浜篒D |
|
| creator_id | BIGINT | | 鍒涘缓浜篒D |
|
||||||
| tags | JSONB | | 鏍囩鏁扮粍 |
|
| tags | TEXT | | 鏍囩鏁扮粍 |
|
||||||
| hot_word_group_id | BIGINT | | 绑定热词组 ID |
|
| hot_word_group_id | BIGINT | | 缁戝畾鐑瘝缁処D |
|
||||||
| usage_count | INTEGER | DEFAULT 0 | 浣跨敤娆℃暟 |
|
| usage_count | INTEGER | DEFAULT 0 | 浣跨敤娆℃暟 |
|
||||||
| prompt_content | TEXT | NOT NULL | 鎻愮ず璇嶅唴瀹?|
|
| prompt_content | TEXT | NOT NULL | 鎻愮ず璇嶅唴瀹?|
|
||||||
| status | SMALLINT | DEFAULT 1 | 鐘舵€?(1:鍚敤, 0:绂佺敤) |
|
| status | SMALLINT | DEFAULT 1 | 鐘舵€侊紙1:鍚敤锛?:绂佺敤锛?|
|
||||||
| remark | VARCHAR(255) | | 澶囨敞 |
|
| remark | VARCHAR(255) | | 澶囨敞 |
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
| updated_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鏇存柊鏃堕棿 |
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
| is_deleted | SMALLINT | NOT NULL, DEFAULT 0 | 閫昏緫鍒犻櫎 |
|
||||||
|
|
||||||
绱㈠紩锛?
|
绱㈠紩锛?- `idx_prompt_tenant`锛歚(tenant_id)`
|
||||||
- `idx_prompt_tenant`: `(tenant_id)`
|
- `idx_prompt_system`锛歚(is_system) WHERE is_deleted = 0`
|
||||||
- `idx_prompt_system`: `(is_system) WHERE is_deleted = 0`
|
- `idx_prompt_group`锛歚(hot_word_group_id) WHERE is_deleted = 0`
|
||||||
- `idx_prompt_group`: `(hot_word_group_id) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
### 5.5 `biz_asr_models`(ASR 模型管理表)
|
### 5.5 `biz_asr_models`锛圓SR 妯″瀷閰嶇疆琛級
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| id | BIGSERIAL | PK | 主键ID |
|
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
|
||||||
| model_name | VARCHAR(100) | NOT NULL | 模型显示名称 |
|
|
||||||
| provider | VARCHAR(50) | | 提供商 |
|
|
||||||
| base_url | VARCHAR(255) | | 接口基础地址 |
|
|
||||||
| api_key | VARCHAR(255) | | API 密钥 |
|
|
||||||
| model_code | VARCHAR(100) | | 模型代码 |
|
|
||||||
| ws_url | VARCHAR(255) | | WebSocket 地址 |
|
|
||||||
| media_config | JSON/TEXT | | 媒体参数 |
|
|
||||||
| is_default | SMALLINT | DEFAULT 0 | 默认模型标记 |
|
|
||||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
|
||||||
| remark | VARCHAR(255) | | 备注 |
|
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
|
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
|
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
|
||||||
|
|
||||||
索引:
|
|
||||||
- `idx_asr_model_tenant`: `(tenant_id)`
|
|
||||||
- `idx_asr_model_default`: `(is_default) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
### 5.5 `biz_llm_models`(LLM 模型管理表)
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| id | BIGSERIAL | PK | 主键ID |
|
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
|
||||||
| model_name | VARCHAR(100) | NOT NULL | 模型显示名称 |
|
|
||||||
| provider | VARCHAR(50) | | 提供商 |
|
|
||||||
| base_url | VARCHAR(255) | | 接口基础地址 |
|
|
||||||
| api_path | VARCHAR(100) | | API 路径 |
|
|
||||||
| api_key | VARCHAR(255) | | API 密钥 |
|
|
||||||
| model_code | VARCHAR(100) | | 模型代码 |
|
|
||||||
| temperature | DECIMAL | DEFAULT 0.7 | 随机性 |
|
|
||||||
| top_p | DECIMAL | DEFAULT 0.9 | 核采样 |
|
|
||||||
| is_default | SMALLINT | DEFAULT 0 | 默认模型标记 |
|
|
||||||
| status | SMALLINT | DEFAULT 1 | 状态 |
|
|
||||||
| remark | VARCHAR(255) | | 备注 |
|
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
|
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
|
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
|
||||||
|
|
||||||
索引:
|
|
||||||
- `idx_llm_model_tenant`: `(tenant_id)`
|
|
||||||
- `idx_llm_model_default`: `(is_default) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
### 5.6 `biz_meetings`锛堜細璁富琛級
|
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 绉熸埛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 | 浼氳鏍囬 |
|
| title | VARCHAR(200) | NOT NULL | 浼氳鏍囬 |
|
||||||
| audio_url | VARCHAR(500) | | 涓撳睘闊抽璺緞 |
|
| meeting_time | TIMESTAMP(6) | | 浼氳鏃堕棿 |
|
||||||
| latest_summary_task_id | BIGINT | | 鏈€鏂版垚鍔熺殑鎬荤粨浠诲姟ID |
|
| participants | TEXT | | 鍙備細浜轰俊鎭?|
|
||||||
| status | SMALLINT | DEFAULT 0 | 0:寰呭鐞? 1:璇嗗埆涓? 2:鎬荤粨涓? 3:宸插畬鎴? 4:澶辫触 |
|
| tags | VARCHAR(255) | | 鏍囩 |
|
||||||
|
| audio_url | VARCHAR(500) | | 涓撳睘闊抽璺緞 |
|
||||||
|
| 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 | 閫昏緫鍒犻櫎 |
|
||||||
|
|
||||||
### 5.6 `biz_meeting_transcripts`锛堣浆褰曟槑缁嗚〃锛?
|
绱㈠紩锛?- `idx_meeting_tenant`锛歚(tenant_id)`
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
|
||||||
|
### 5.8 `biz_meeting_transcripts`锛堣浆褰曟槑缁嗚〃锛?| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
|
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
|
||||||
|
| speaker_id | VARCHAR(50) | | ASR 杩斿洖鐨勫彂瑷€浜烘爣璇?|
|
||||||
|
| speaker_name | VARCHAR(100) | | 淇敼鍚庣殑鍙戣█浜哄鍚?|
|
||||||
| speaker_label | VARCHAR(50) | | 鍙戣█浜烘爣绛?|
|
| speaker_label | VARCHAR(50) | | 鍙戣█浜烘爣绛?|
|
||||||
| content | TEXT | | 杞綍鏂囧瓧 |
|
| content | TEXT | | 杞綍鍐呭 |
|
||||||
| start_time | INTEGER | | 寮€濮嬫椂闂?(ms) |
|
| start_time | INTEGER | | 寮€濮嬫椂闂达紙ms锛?|
|
||||||
|
| end_time | INTEGER | | 缁撴潫鏃堕棿锛坢s锛?|
|
||||||
|
| sort_order | INTEGER | | 鎺掑簭 |
|
||||||
|
| created_at | TIMESTAMP(6) | NOT NULL, DEFAULT now() | 鍒涘缓鏃堕棿 |
|
||||||
|
|
||||||
### 5.7 `biz_ai_tasks`锛圓I 浠诲姟娴佹按琛級
|
绱㈠紩锛?- `idx_transcript_meeting`锛歚(meeting_id)`
|
||||||
|
|
||||||
|
### 5.9 `biz_ai_tasks`锛圓I 浠诲姟娴佹按琛級
|
||||||
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
| 瀛楁 | 绫诲瀷 | 绾︽潫 | 璇存槑 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 涓婚敭ID |
|
| id | BIGSERIAL | PK | 涓婚敭ID |
|
||||||
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
|
| meeting_id | BIGINT | NOT NULL | 鍏宠仈浼氳ID |
|
||||||
| task_type | VARCHAR(20) | | ASR / SUMMARY |
|
| task_type | VARCHAR(20) | | 浠诲姟绫诲瀷锛圓SR / SUMMARY锛?|
|
||||||
| request_data | JSONB | | 璇锋眰鍘熷鏁版嵁 |
|
| status | SMALLINT | DEFAULT 0 | 鐘舵€侊紙0:鎺掗槦锛?:鎵ц涓紝2:鎴愬姛锛?:澶辫触锛?|
|
||||||
| response_data | JSONB | | 鍝嶅簲鍘熷鏁版嵁 |
|
| request_data | TEXT | | 璇锋眰涓夋柟鍘熷 JSON |
|
||||||
| task_config | TEXT | | **[蹇収]** 浠诲姟閰嶇疆(妯″瀷ID銆佹彁绀鸿瘝妯℃澘绛? |
|
| response_data | TEXT | | 涓夋柟杩斿洖鍘熷 JSON |
|
||||||
| result_file_path | VARCHAR(500) | | 缁撴灉鏂囦欢鐩稿璺緞 (濡侻D鎬荤粨鏂囦欢) |
|
| task_config | TEXT | | 浠诲姟閰嶇疆鍙傛暟蹇収 |
|
||||||
| status | SMALLINT | | 0:鎺掗槦, 1:澶勭悊涓? 2:鎴愬姛, 3:澶辫触 |
|
| result_file_path | VARCHAR(500) | | 缁撴灉鏂囦欢璺緞 |
|
||||||
|
| error_msg | TEXT | | 閿欒鍫嗘爤 |
|
||||||
## 6. 屏保模块
|
| started_at | TIMESTAMP(6) | | 寮€濮嬫椂闂?|
|
||||||
|
| completed_at | TIMESTAMP(6) | | 瀹屾垚鏃堕棿 |
|
||||||
### 6.1 `biz_screen_savers`(屏保素材表)
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| id | BIGSERIAL | PK | 屏保ID |
|
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
|
||||||
| scope_type | VARCHAR(32) | NOT NULL, DEFAULT 'PLATFORM' | 作用域:PLATFORM / USER |
|
|
||||||
| owner_user_id | BIGINT | | 当作用域为 USER 时的归属用户 |
|
|
||||||
| 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 | | 创建人ID |
|
|
||||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 启用状态 |
|
|
||||||
| remark | VARCHAR(255) | | 备注 |
|
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
|
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
|
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
|
||||||
|
|
||||||
索引:
|
|
||||||
- `idx_screen_savers_status_sort`: `(status, sort_order)`
|
|
||||||
- `idx_screen_savers_scope_owner_status_sort`: `(scope_type, owner_user_id, status, sort_order)`
|
|
||||||
|
|
||||||
### 6.2 `biz_screen_saver_user_config`(屏保用户状态覆盖表)
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| id | BIGSERIAL | PK | 配置ID |
|
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
|
||||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
|
||||||
| screen_saver_id | BIGINT | NOT NULL | 屏保素材ID |
|
|
||||||
| status | SMALLINT | NOT NULL, DEFAULT 1 | 用户覆盖启停状态 |
|
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
|
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
|
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
|
||||||
|
|
||||||
索引:
|
|
||||||
- `uk_screen_saver_user_cfg_user_item`: `UNIQUE (tenant_id, user_id, screen_saver_id) WHERE is_deleted = 0`
|
|
||||||
- `idx_screen_saver_user_cfg_item`: `(screen_saver_id) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
### 6.3 `biz_screen_saver_user_settings`(屏保用户播放设置表)
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| id | BIGSERIAL | PK | 设置ID |
|
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
|
||||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
|
||||||
| display_duration_sec | INTEGER | NOT NULL, DEFAULT 15 | 当前用户统一屏保展示时长(秒) |
|
|
||||||
| created_at | TIMESTAMP(6) | NOT NULL | 创建时间 |
|
|
||||||
| updated_at | TIMESTAMP(6) | NOT NULL | 更新时间 |
|
|
||||||
| is_deleted | SMALLINT | DEFAULT 0 | 逻辑删除 |
|
|
||||||
|
|
||||||
索引:
|
|
||||||
- `uk_screen_saver_user_settings_user`: `UNIQUE (tenant_id, user_id) WHERE is_deleted = 0`
|
|
||||||
|
|
||||||
|
|
||||||
|
绱㈠紩锛?- `idx_aitask_meeting`锛歚(meeting_id)`
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,7 @@ CREATE TABLE biz_asr_models (
|
||||||
ws_url VARCHAR(255),
|
ws_url VARCHAR(255),
|
||||||
media_config text,
|
media_config text,
|
||||||
is_default SMALLINT DEFAULT 0,
|
is_default SMALLINT DEFAULT 0,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
status SMALLINT DEFAULT 1,
|
status SMALLINT DEFAULT 1,
|
||||||
remark VARCHAR(255),
|
remark VARCHAR(255),
|
||||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
|
@ -370,6 +371,7 @@ CREATE TABLE biz_llm_models (
|
||||||
temperature DECIMAL(3,2) DEFAULT 0.7,
|
temperature DECIMAL(3,2) DEFAULT 0.7,
|
||||||
top_p DECIMAL(3,2) DEFAULT 0.9,
|
top_p DECIMAL(3,2) DEFAULT 0.9,
|
||||||
is_default SMALLINT DEFAULT 0,
|
is_default SMALLINT DEFAULT 0,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
status SMALLINT DEFAULT 1,
|
status SMALLINT DEFAULT 1,
|
||||||
remark VARCHAR(255),
|
remark VARCHAR(255),
|
||||||
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
created_at TIMESTAMP(6) NOT NULL DEFAULT now(),
|
||||||
|
|
@ -379,8 +381,12 @@ CREATE TABLE biz_llm_models (
|
||||||
|
|
||||||
CREATE INDEX idx_asr_model_tenant ON biz_asr_models (tenant_id);
|
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_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_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_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_asr_models IS 'ASR 模型配置表';
|
||||||
COMMENT ON TABLE biz_llm_models IS 'LLM 模型配置表';
|
COMMENT ON TABLE biz_llm_models IS 'LLM 模型配置表';
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,16 @@ public class LegacyLlmModelItemResponse {
|
||||||
@JsonProperty("is_default")
|
@JsonProperty("is_default")
|
||||||
private Integer isDefault;
|
private Integer isDefault;
|
||||||
|
|
||||||
|
@JsonProperty("sort_order")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
public static LegacyLlmModelItemResponse from(AiModelVO source, boolean defaultItem) {
|
public static LegacyLlmModelItemResponse from(AiModelVO source, boolean defaultItem) {
|
||||||
LegacyLlmModelItemResponse response = new LegacyLlmModelItemResponse();
|
LegacyLlmModelItemResponse response = new LegacyLlmModelItemResponse();
|
||||||
response.setModelCode(source.getModelCode());
|
response.setModelCode(source.getModelCode());
|
||||||
response.setModelName(source.getModelName());
|
response.setModelName(source.getModelName());
|
||||||
response.setProvider(source.getProvider());
|
response.setProvider(source.getProvider());
|
||||||
response.setIsDefault(defaultItem ? 1 : 0);
|
response.setIsDefault(defaultItem ? 1 : 0);
|
||||||
|
response.setSortOrder(source.getSortOrder());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,45 @@
|
||||||
package com.imeeting.dto.biz;
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Schema(description = "AI 模型配置请求")
|
||||||
@Data
|
@Data
|
||||||
public class AiModelDTO {
|
public class AiModelDTO {
|
||||||
|
@Schema(description = "模型 ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
@Schema(description = "模型类型")
|
||||||
private String modelType;
|
private String modelType;
|
||||||
|
@Schema(description = "模型显示名称")
|
||||||
private String modelName;
|
private String modelName;
|
||||||
|
@Schema(description = "提供商")
|
||||||
private String provider;
|
private String provider;
|
||||||
|
@Schema(description = "基础地址")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
@Schema(description = "接口路径")
|
||||||
private String apiPath;
|
private String apiPath;
|
||||||
|
@Schema(description = "接口密钥")
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
@Schema(description = "连通性测试消息")
|
||||||
private String testMessage;
|
private String testMessage;
|
||||||
|
@Schema(description = "模型编码")
|
||||||
private String modelCode;
|
private String modelCode;
|
||||||
|
@Schema(description = "WebSocket 地址")
|
||||||
private String wsUrl;
|
private String wsUrl;
|
||||||
|
@Schema(description = "温度参数")
|
||||||
private BigDecimal temperature;
|
private BigDecimal temperature;
|
||||||
|
@Schema(description = "TopP 参数")
|
||||||
private BigDecimal topP;
|
private BigDecimal topP;
|
||||||
|
@Schema(description = "媒体配置")
|
||||||
private Map<String, Object> mediaConfig;
|
private Map<String, Object> mediaConfig;
|
||||||
|
@Schema(description = "是否默认")
|
||||||
private Integer isDefault;
|
private Integer isDefault;
|
||||||
|
@Schema(description = "状态")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
@Schema(description = "排序值,越小越靠前")
|
||||||
|
private Integer sortOrder;
|
||||||
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ public class AiModelVO {
|
||||||
private Integer isDefault;
|
private Integer isDefault;
|
||||||
@Schema(description = "启用状态")
|
@Schema(description = "启用状态")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
@Schema(description = "排序值,越小越靠前")
|
||||||
|
private Integer sortOrder;
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
@Schema(description = "创建时间")
|
@Schema(description = "创建时间")
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ public class AsrModel extends BaseEntity {
|
||||||
@Schema(description = "是否默认模型")
|
@Schema(description = "是否默认模型")
|
||||||
private Integer isDefault;
|
private Integer isDefault;
|
||||||
|
|
||||||
|
@Schema(description = "排序值,越小越靠前")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ public class LlmModel extends BaseEntity {
|
||||||
@Schema(description = "是否默认模型")
|
@Schema(description = "是否默认模型")
|
||||||
private Integer isDefault;
|
private Integer isDefault;
|
||||||
|
|
||||||
|
@Schema(description = "排序值,越小越靠前")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
|
|
||||||
private static final String TYPE_ASR = "ASR";
|
private static final String TYPE_ASR = "ASR";
|
||||||
private static final String TYPE_LLM = "LLM";
|
private static final String TYPE_LLM = "LLM";
|
||||||
|
private static final int DEFAULT_SORT_ORDER = 0;
|
||||||
private static final String DEFAULT_LLM_API_PATH = "/v1/chat/completions";
|
private static final String DEFAULT_LLM_API_PATH = "/v1/chat/completions";
|
||||||
private static final String DEFAULT_ANTHROPIC_API_PATH = "/messages";
|
private static final String DEFAULT_ANTHROPIC_API_PATH = "/messages";
|
||||||
private static final String CONNECTIVITY_TEST_SYSTEM_PROMPT = """
|
private static final String CONNECTIVITY_TEST_SYSTEM_PROMPT = """
|
||||||
|
|
@ -126,6 +127,8 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
LambdaQueryWrapper<AsrModel> wrapper = new LambdaQueryWrapper<AsrModel>()
|
LambdaQueryWrapper<AsrModel> wrapper = new LambdaQueryWrapper<AsrModel>()
|
||||||
.and(w -> w.eq(AsrModel::getTenantId, tenantId).or().eq(AsrModel::getTenantId, 0L))
|
.and(w -> w.eq(AsrModel::getTenantId, tenantId).or().eq(AsrModel::getTenantId, 0L))
|
||||||
.like(name != null && !name.isBlank(), AsrModel::getModelName, name)
|
.like(name != null && !name.isBlank(), AsrModel::getModelName, name)
|
||||||
|
.orderByDesc(AsrModel::getIsDefault)
|
||||||
|
.orderByAsc(AsrModel::getSortOrder)
|
||||||
.orderByDesc(AsrModel::getTenantId)
|
.orderByDesc(AsrModel::getTenantId)
|
||||||
.orderByDesc(AsrModel::getCreatedAt);
|
.orderByDesc(AsrModel::getCreatedAt);
|
||||||
Page<AsrModel> resultPage = asrModelMapper.selectPage(page, wrapper);
|
Page<AsrModel> resultPage = asrModelMapper.selectPage(page, wrapper);
|
||||||
|
|
@ -143,6 +146,8 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
LambdaQueryWrapper<LlmModel> wrapper = new LambdaQueryWrapper<LlmModel>()
|
LambdaQueryWrapper<LlmModel> wrapper = new LambdaQueryWrapper<LlmModel>()
|
||||||
.and(w -> w.eq(LlmModel::getTenantId, tenantId).or().eq(LlmModel::getTenantId, 0L))
|
.and(w -> w.eq(LlmModel::getTenantId, tenantId).or().eq(LlmModel::getTenantId, 0L))
|
||||||
.like(name != null && !name.isBlank(), LlmModel::getModelName, name)
|
.like(name != null && !name.isBlank(), LlmModel::getModelName, name)
|
||||||
|
.orderByDesc(LlmModel::getIsDefault)
|
||||||
|
.orderByAsc(LlmModel::getSortOrder)
|
||||||
.orderByDesc(LlmModel::getTenantId)
|
.orderByDesc(LlmModel::getTenantId)
|
||||||
.orderByDesc(LlmModel::getCreatedAt);
|
.orderByDesc(LlmModel::getCreatedAt);
|
||||||
Page<LlmModel> resultPage = llmModelMapper.selectPage(page, wrapper);
|
Page<LlmModel> resultPage = llmModelMapper.selectPage(page, wrapper);
|
||||||
|
|
@ -438,10 +443,10 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
throw new RuntimeException("闂佸搫鐗滈崜娆忥耿閺夋嚦鐔煎灳瀹曞洠鍋撻悜鑺ョ厐鐎广儱娲ㄩ弸鍌毲庨崶銊х畵闁宦板妽瀵板嫭娼忛銉? HTTP " + response.statusCode());
|
throw new RuntimeException("本地模型配置保存失败: HTTP " + response.statusCode());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("闂佸搫鐗滈崜娆忥耿閺夋嚦鐔煎灳瀹曞洠鍋撻悜鑺ョ厐鐎广儱娲ㄩ弸鍌毲庨崶銊х畵闁宦板妽瀵板嫭娼忛銉? " + e.getMessage(), e);
|
throw new RuntimeException("本地模型配置保存失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -564,6 +569,21 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
return chatCompletionContent;
|
return chatCompletionContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String chatCompletionReasoning = root.path("choices").path(0).path("message").path("reasoning").asText(null);
|
||||||
|
if (chatCompletionReasoning != null && !chatCompletionReasoning.isBlank()) {
|
||||||
|
return chatCompletionReasoning;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode reasoningContentNode = root.path("choices").path(0).path("message").path("reasoning_content");
|
||||||
|
if (reasoningContentNode.isArray()) {
|
||||||
|
for (JsonNode reasoningItem : reasoningContentNode) {
|
||||||
|
String text = reasoningItem.path("text").asText(null);
|
||||||
|
if (text != null && !text.isBlank()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String textCompletionContent = root.path("choices").path(0).path("text").asText(null);
|
String textCompletionContent = root.path("choices").path(0).path("text").asText(null);
|
||||||
if (textCompletionContent != null && !textCompletionContent.isBlank()) {
|
if (textCompletionContent != null && !textCompletionContent.isBlank()) {
|
||||||
return textCompletionContent;
|
return textCompletionContent;
|
||||||
|
|
@ -720,6 +740,9 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
if (dto == null) {
|
if (dto == null) {
|
||||||
throw new RuntimeException("模型配置不能为空");
|
throw new RuntimeException("模型配置不能为空");
|
||||||
}
|
}
|
||||||
|
if (Integer.valueOf(1).equals(dto.getIsDefault()) && !Integer.valueOf(1).equals(dto.getStatus())) {
|
||||||
|
throw new RuntimeException("默认模型必须为启用状态");
|
||||||
|
}
|
||||||
if ("custom".equals(normalizeProvider(dto.getProvider()))) {
|
if ("custom".equals(normalizeProvider(dto.getProvider()))) {
|
||||||
if (TYPE_ASR.equals(normalizeType(dto.getModelType()))) {
|
if (TYPE_ASR.equals(normalizeType(dto.getModelType()))) {
|
||||||
Map<String, Object> mediaConfig = dto.getMediaConfig() == null ? Collections.emptyMap() : dto.getMediaConfig();
|
Map<String, Object> mediaConfig = dto.getMediaConfig() == null ? Collections.emptyMap() : dto.getMediaConfig();
|
||||||
|
|
@ -738,17 +761,23 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
String resolvedType = normalizeType(type);
|
String resolvedType = normalizeType(type);
|
||||||
if (TYPE_ASR.equals(resolvedType)) {
|
if (TYPE_ASR.equals(resolvedType)) {
|
||||||
AsrModel model = asrModelMapper.selectOne(new LambdaQueryWrapper<AsrModel>()
|
AsrModel model = asrModelMapper.selectOne(new LambdaQueryWrapper<AsrModel>()
|
||||||
|
.eq(AsrModel::getStatus, 1)
|
||||||
.eq(AsrModel::getIsDefault, 1)
|
.eq(AsrModel::getIsDefault, 1)
|
||||||
.and(w -> w.eq(AsrModel::getTenantId, tenantId).or().eq(AsrModel::getTenantId, 0L))
|
.and(w -> w.eq(AsrModel::getTenantId, tenantId).or().eq(AsrModel::getTenantId, 0L))
|
||||||
.orderByDesc(AsrModel::getTenantId)
|
.orderByDesc(AsrModel::getTenantId)
|
||||||
|
.orderByAsc(AsrModel::getSortOrder)
|
||||||
|
.orderByDesc(AsrModel::getCreatedAt)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
return model == null ? null : toAsrVO(model);
|
return model == null ? null : toAsrVO(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
LlmModel model = llmModelMapper.selectOne(new LambdaQueryWrapper<LlmModel>()
|
LlmModel model = llmModelMapper.selectOne(new LambdaQueryWrapper<LlmModel>()
|
||||||
|
.eq(LlmModel::getStatus, 1)
|
||||||
.eq(LlmModel::getIsDefault, 1)
|
.eq(LlmModel::getIsDefault, 1)
|
||||||
.and(w -> w.eq(LlmModel::getTenantId, tenantId).or().eq(LlmModel::getTenantId, 0L))
|
.and(w -> w.eq(LlmModel::getTenantId, tenantId).or().eq(LlmModel::getTenantId, 0L))
|
||||||
.orderByDesc(LlmModel::getTenantId)
|
.orderByDesc(LlmModel::getTenantId)
|
||||||
|
.orderByAsc(LlmModel::getSortOrder)
|
||||||
|
.orderByDesc(LlmModel::getCreatedAt)
|
||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
return model == null ? null : toLlmVO(model);
|
return model == null ? null : toLlmVO(model);
|
||||||
}
|
}
|
||||||
|
|
@ -855,6 +884,7 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
entity.setMediaConfig(dto.getMediaConfig());
|
entity.setMediaConfig(dto.getMediaConfig());
|
||||||
entity.setIsDefault(dto.getIsDefault());
|
entity.setIsDefault(dto.getIsDefault());
|
||||||
entity.setStatus(dto.getStatus());
|
entity.setStatus(dto.getStatus());
|
||||||
|
entity.setSortOrder(normalizeSortOrder(dto.getSortOrder()));
|
||||||
entity.setRemark(dto.getRemark());
|
entity.setRemark(dto.getRemark());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -914,6 +944,7 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
entity.setTopP(dto.getTopP() == null ? BigDecimal.valueOf(0.9) : dto.getTopP());
|
entity.setTopP(dto.getTopP() == null ? BigDecimal.valueOf(0.9) : dto.getTopP());
|
||||||
entity.setIsDefault(dto.getIsDefault());
|
entity.setIsDefault(dto.getIsDefault());
|
||||||
entity.setStatus(dto.getStatus());
|
entity.setStatus(dto.getStatus());
|
||||||
|
entity.setSortOrder(normalizeSortOrder(dto.getSortOrder()));
|
||||||
entity.setRemark(dto.getRemark());
|
entity.setRemark(dto.getRemark());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -931,6 +962,7 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
vo.setMediaConfig(entity.getMediaConfig());
|
vo.setMediaConfig(entity.getMediaConfig());
|
||||||
vo.setIsDefault(entity.getIsDefault());
|
vo.setIsDefault(entity.getIsDefault());
|
||||||
vo.setStatus(entity.getStatus());
|
vo.setStatus(entity.getStatus());
|
||||||
|
vo.setSortOrder(entity.getSortOrder());
|
||||||
vo.setRemark(entity.getRemark());
|
vo.setRemark(entity.getRemark());
|
||||||
vo.setCreatedAt(entity.getCreatedAt());
|
vo.setCreatedAt(entity.getCreatedAt());
|
||||||
return vo;
|
return vo;
|
||||||
|
|
@ -951,11 +983,16 @@ public class AiModelServiceImpl implements AiModelService {
|
||||||
vo.setTopP(entity.getTopP());
|
vo.setTopP(entity.getTopP());
|
||||||
vo.setIsDefault(entity.getIsDefault());
|
vo.setIsDefault(entity.getIsDefault());
|
||||||
vo.setStatus(entity.getStatus());
|
vo.setStatus(entity.getStatus());
|
||||||
|
vo.setSortOrder(entity.getSortOrder());
|
||||||
vo.setRemark(entity.getRemark());
|
vo.setRemark(entity.getRemark());
|
||||||
vo.setCreatedAt(entity.getCreatedAt());
|
vo.setCreatedAt(entity.getCreatedAt());
|
||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Integer normalizeSortOrder(Integer sortOrder) {
|
||||||
|
return sortOrder == null ? DEFAULT_SORT_ORDER : sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeType(String type) {
|
private String normalizeType(String type) {
|
||||||
if (type == null || type.isBlank()) {
|
if (type == null || type.isBlank()) {
|
||||||
return TYPE_ASR;
|
return TYPE_ASR;
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
if (template == null) {
|
if (template == null) {
|
||||||
throw new IllegalArgumentException("模板不存在");
|
throw new IllegalArgumentException("模板不存在");
|
||||||
}
|
}
|
||||||
Map<Long, HotWordGroup> hotWordGroupMap = queryHotWordGroupMap(List.of(template.getHotWordGroupId()));
|
Map<Long, HotWordGroup> hotWordGroupMap = queryHotWordGroupMap(java.util.Collections.singletonList(template.getHotWordGroupId()));
|
||||||
PromptTemplateVO vo = toVO(template, template.getStatus(), hotWordGroupMap);
|
PromptTemplateVO vo = toVO(template, template.getStatus(), hotWordGroupMap);
|
||||||
vo.setHotWords(resolveHotWords(template.getHotWordGroupId()));
|
vo.setHotWords(resolveHotWords(template.getHotWordGroupId()));
|
||||||
return vo;
|
return vo;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ unisbase:
|
||||||
- /api/auth/**
|
- /api/auth/**
|
||||||
- /api/static/**
|
- /api/static/**
|
||||||
- /api/public/meetings/**
|
- /api/public/meetings/**
|
||||||
|
- /api/android/auth/login
|
||||||
|
- /api/android/auth/refresh
|
||||||
- /api/android/screensavers/active
|
- /api/android/screensavers/active
|
||||||
- /api/screensavers/active
|
- /api/screensavers/active
|
||||||
- /v3/api-docs/**
|
- /v3/api-docs/**
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
@ -67,7 +68,7 @@ class AiModelServiceImplTest {
|
||||||
dto.setApiPath("/v1/chat/completions");
|
dto.setApiPath("/v1/chat/completions");
|
||||||
dto.setApiKey("test-key");
|
dto.setApiKey("test-key");
|
||||||
dto.setModelCode("gpt-test");
|
dto.setModelCode("gpt-test");
|
||||||
dto.setTestMessage("璇峰洖澶嶏細杩炴帴姝e父");
|
dto.setTestMessage("请回复:连接正常");
|
||||||
|
|
||||||
service.testLlmConnectivity(dto);
|
service.testLlmConnectivity(dto);
|
||||||
|
|
||||||
|
|
@ -84,7 +85,7 @@ class AiModelServiceImplTest {
|
||||||
.orElse("")
|
.orElse("")
|
||||||
.trim()
|
.trim()
|
||||||
);
|
);
|
||||||
assertEquals("璇峰洖澶嶏細杩炴帴姝e父", requestJson.path("messages").path(1).path("content").asText());
|
assertEquals("请回复:连接正常", requestJson.path("messages").path(1).path("content").asText());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -137,6 +138,41 @@ class AiModelServiceImplTest {
|
||||||
service.testLlmConnectivity(dto);
|
service.testLlmConnectivity(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLlmConnectivityShouldAcceptReasoningWhenContentMissing() throws Exception {
|
||||||
|
server = HttpServer.create(new InetSocketAddress(0), 0);
|
||||||
|
server.createContext("/v1/chat/completions", exchange -> {
|
||||||
|
captureRequest(exchange, new AtomicReference<>(), new AtomicReference<>(), new AtomicReference<>());
|
||||||
|
writeJson(exchange, 200, """
|
||||||
|
{
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"content": null,
|
||||||
|
"reasoning": "The model produced reasoning text before emitting the final answer."
|
||||||
|
},
|
||||||
|
"finish_reason": "length"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
});
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||||
|
objectMapper,
|
||||||
|
mock(AsrModelMapper.class),
|
||||||
|
mock(LlmModelMapper.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
AiModelDTO dto = new AiModelDTO();
|
||||||
|
dto.setBaseUrl("http://127.0.0.1:" + server.getAddress().getPort());
|
||||||
|
dto.setApiPath("/v1/chat/completions");
|
||||||
|
dto.setModelCode("gpt-test");
|
||||||
|
|
||||||
|
service.testLlmConnectivity(dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLlmConnectivityShouldUseHttp11ClientForCompatibility() throws Exception {
|
void testLlmConnectivityShouldUseHttp11ClientForCompatibility() throws Exception {
|
||||||
AiModelServiceImpl service = new AiModelServiceImpl(
|
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||||
|
|
@ -181,6 +217,57 @@ class AiModelServiceImplTest {
|
||||||
assertNull(captor.getValue().getApiKey());
|
assertNull(captor.getValue().getApiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveModelShouldPersistSortOrderForLlm() {
|
||||||
|
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
|
||||||
|
LlmModelMapper llmModelMapper = mock(LlmModelMapper.class);
|
||||||
|
when(llmModelMapper.insert(any(LlmModel.class))).thenReturn(1);
|
||||||
|
|
||||||
|
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||||
|
objectMapper,
|
||||||
|
asrModelMapper,
|
||||||
|
llmModelMapper
|
||||||
|
);
|
||||||
|
|
||||||
|
AiModelDTO dto = new AiModelDTO();
|
||||||
|
dto.setModelType("LLM");
|
||||||
|
dto.setModelName("ordered-llm");
|
||||||
|
dto.setProvider("openai");
|
||||||
|
dto.setBaseUrl("http://127.0.0.1:9000");
|
||||||
|
dto.setApiPath("/v1/chat/completions");
|
||||||
|
dto.setModelCode("gpt-test");
|
||||||
|
dto.setIsDefault(0);
|
||||||
|
dto.setStatus(1);
|
||||||
|
dto.setSortOrder(7);
|
||||||
|
|
||||||
|
service.saveModel(dto);
|
||||||
|
|
||||||
|
ArgumentCaptor<LlmModel> captor = ArgumentCaptor.forClass(LlmModel.class);
|
||||||
|
verify(llmModelMapper, times(1)).insert(captor.capture());
|
||||||
|
assertEquals(7, captor.getValue().getSortOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveModelShouldRejectDisabledDefaultModel() {
|
||||||
|
AiModelServiceImpl service = new AiModelServiceImpl(
|
||||||
|
objectMapper,
|
||||||
|
mock(AsrModelMapper.class),
|
||||||
|
mock(LlmModelMapper.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
AiModelDTO dto = new AiModelDTO();
|
||||||
|
dto.setModelType("LLM");
|
||||||
|
dto.setModelName("invalid-default");
|
||||||
|
dto.setProvider("openai");
|
||||||
|
dto.setBaseUrl("http://127.0.0.1:9000");
|
||||||
|
dto.setApiPath("/v1/chat/completions");
|
||||||
|
dto.setModelCode("gpt-test");
|
||||||
|
dto.setIsDefault(1);
|
||||||
|
dto.setStatus(0);
|
||||||
|
|
||||||
|
assertThrows(RuntimeException.class, () -> service.saveModel(dto));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void saveModelShouldAllowCustomAsrWithoutApiKeyAndSkipSync() {
|
void saveModelShouldAllowCustomAsrWithoutApiKeyAndSkipSync() {
|
||||||
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
|
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ package com.imeeting.service.biz.impl;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.imeeting.dto.biz.AiModelVO;
|
import com.imeeting.dto.biz.AiModelVO;
|
||||||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||||
|
import com.imeeting.entity.biz.AsrModel;
|
||||||
import com.imeeting.entity.biz.HotWord;
|
import com.imeeting.entity.biz.HotWord;
|
||||||
|
import com.imeeting.entity.biz.LlmModel;
|
||||||
import com.imeeting.entity.biz.PromptTemplate;
|
import com.imeeting.entity.biz.PromptTemplate;
|
||||||
import com.imeeting.mapper.biz.AsrModelMapper;
|
import com.imeeting.mapper.biz.AsrModelMapper;
|
||||||
import com.imeeting.mapper.biz.LlmModelMapper;
|
import com.imeeting.mapper.biz.LlmModelMapper;
|
||||||
|
|
@ -18,6 +20,7 @@ import java.util.List;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
|
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
|
@ -146,6 +149,48 @@ class MeetingRuntimeProfileResolverImplTest {
|
||||||
assertIterableEquals(List.of("OpenAI", "Codex"), profile.getResolvedHotWords());
|
assertIterableEquals(List.of("OpenAI", "Codex"), profile.getResolvedHotWords());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveShouldFallbackToFirstEnabledModelUsingSortOrder() {
|
||||||
|
AiModelService aiModelService = mock(AiModelService.class);
|
||||||
|
PromptTemplateService promptTemplateService = mock(PromptTemplateService.class);
|
||||||
|
HotWordService hotWordService = mock(HotWordService.class);
|
||||||
|
AsrModelMapper asrModelMapper = mock(AsrModelMapper.class);
|
||||||
|
LlmModelMapper llmModelMapper = mock(LlmModelMapper.class);
|
||||||
|
MeetingRuntimeProfileResolverImpl resolver = new MeetingRuntimeProfileResolverImpl(
|
||||||
|
aiModelService,
|
||||||
|
promptTemplateService,
|
||||||
|
hotWordService,
|
||||||
|
asrModelMapper,
|
||||||
|
llmModelMapper
|
||||||
|
);
|
||||||
|
|
||||||
|
when(aiModelService.getDefaultModel("ASR", 1L)).thenReturn(null);
|
||||||
|
when(aiModelService.getDefaultModel("LLM", 1L)).thenReturn(null);
|
||||||
|
when(asrModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(asrEntity(11L));
|
||||||
|
when(llmModelMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(llmEntity(22L));
|
||||||
|
when(aiModelService.getModelById(11L, "ASR")).thenReturn(enabledModel(11L, 1L, "ASR-Model"));
|
||||||
|
when(aiModelService.getModelById(22L, "LLM")).thenReturn(enabledModel(22L, 1L, "LLM-Model"));
|
||||||
|
when(promptTemplateService.getOne(any(LambdaQueryWrapper.class))).thenReturn(enabledPrompt(33L, 1L, "Default Prompt"));
|
||||||
|
|
||||||
|
RealtimeMeetingRuntimeProfile profile = resolver.resolve(
|
||||||
|
1L,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(11L, profile.getResolvedAsrModelId());
|
||||||
|
assertEquals(22L, profile.getResolvedSummaryModelId());
|
||||||
|
}
|
||||||
|
|
||||||
private AiModelVO enabledModel(Long id, Long tenantId, String name) {
|
private AiModelVO enabledModel(Long id, Long tenantId, String name) {
|
||||||
AiModelVO model = new AiModelVO();
|
AiModelVO model = new AiModelVO();
|
||||||
model.setId(id);
|
model.setId(id);
|
||||||
|
|
@ -163,4 +208,18 @@ class MeetingRuntimeProfileResolverImplTest {
|
||||||
template.setStatus(1);
|
template.setStatus(1);
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AsrModel asrEntity(Long id) {
|
||||||
|
AsrModel entity = new AsrModel();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setStatus(1);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LlmModel llmEntity(Long id) {
|
||||||
|
LlmModel entity = new LlmModel();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setStatus(1);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export interface AiModelVO {
|
||||||
mediaConfig?: Record<string, any>;
|
mediaConfig?: Record<string, any>;
|
||||||
isDefault: number;
|
isDefault: number;
|
||||||
status: number;
|
status: number;
|
||||||
|
sortOrder?: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +46,7 @@ export interface AiModelDTO {
|
||||||
mediaConfig?: Record<string, any>;
|
mediaConfig?: Record<string, any>;
|
||||||
isDefault: number;
|
isDefault: number;
|
||||||
status: number;
|
status: number;
|
||||||
|
sortOrder?: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import http from "../http";
|
||||||
|
|
||||||
|
export interface HotWordGroupVO {
|
||||||
|
id: number;
|
||||||
|
tenantId: number;
|
||||||
|
groupName: string;
|
||||||
|
creatorId: number;
|
||||||
|
status: number;
|
||||||
|
hotWordCount: number;
|
||||||
|
remark?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HotWordGroupDTO {
|
||||||
|
id?: number;
|
||||||
|
tenantId?: number;
|
||||||
|
groupName: string;
|
||||||
|
status: number;
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHotWordGroupPage = (params: {
|
||||||
|
current: number;
|
||||||
|
size: number;
|
||||||
|
name?: string;
|
||||||
|
tenantId?: number;
|
||||||
|
}) => {
|
||||||
|
return http.get<{ code: string; data: { records: HotWordGroupVO[]; total: number }; msg: string }>(
|
||||||
|
"/api/biz/hotword-group/page",
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHotWordGroupOptions = (tenantId?: number) => {
|
||||||
|
return http.get<{ code: string; data: HotWordGroupVO[]; msg: string }>(
|
||||||
|
"/api/biz/hotword-group/options",
|
||||||
|
{ params: { tenantId } }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveHotWordGroup = (data: HotWordGroupDTO) => {
|
||||||
|
return http.post<{ code: string; data: HotWordGroupVO; msg: string }>(
|
||||||
|
"/api/biz/hotword-group",
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateHotWordGroup = (data: HotWordGroupDTO) => {
|
||||||
|
return http.put<{ code: string; data: HotWordGroupVO; msg: string }>(
|
||||||
|
"/api/biz/hotword-group",
|
||||||
|
data
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteHotWordGroup = (id: number, tenantId?: number) => {
|
||||||
|
return http.delete<{ code: string; data: boolean; msg: string }>(
|
||||||
|
`/api/biz/hotword-group/${id}`,
|
||||||
|
{ params: { tenantId } }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"total": "Total {{total}} items",
|
"total": "Total {{total}} items",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"view": "View"
|
"view": "View",
|
||||||
|
"maxLength": "exceeds the maximum length the maximum length is{{length}}"
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
|
|
@ -170,6 +171,7 @@
|
||||||
"drawerTitleEdit": "Edit Tenant",
|
"drawerTitleEdit": "Edit Tenant",
|
||||||
"tenantName": "Tenant Name",
|
"tenantName": "Tenant Name",
|
||||||
"tenantCode": "Tenant Code",
|
"tenantCode": "Tenant Code",
|
||||||
|
"defaultAdminUsername": "Default Admin Username",
|
||||||
"contactName": "Contact Name",
|
"contactName": "Contact Name",
|
||||||
"contactPhone": "Phone"
|
"contactPhone": "Phone"
|
||||||
},
|
},
|
||||||
|
|
@ -231,7 +233,8 @@
|
||||||
"copyright": "Copyright",
|
"copyright": "Copyright",
|
||||||
"desc": "System Description",
|
"desc": "System Description",
|
||||||
"uploadHint": "Click or drag to upload",
|
"uploadHint": "Click or drag to upload",
|
||||||
"uploadLimit": "Recommend 1:1 ratio, max 2MB",
|
"uploadLimit": "Recommend 1:1 ratio, max {{size}}",
|
||||||
|
"uploadTooLarge": "Image size must not exceed {{size}}",
|
||||||
"basicInfo": "Basic Information",
|
"basicInfo": "Basic Information",
|
||||||
"brandAssets": "Brand Assets",
|
"brandAssets": "Brand Assets",
|
||||||
"complianceFooter": "Compliance and Footer",
|
"complianceFooter": "Compliance and Footer",
|
||||||
|
|
@ -261,13 +264,13 @@
|
||||||
"updatePassword": "Update Password",
|
"updatePassword": "Update Password",
|
||||||
"passwordsDoNotMatch": "Passwords do not match.",
|
"passwordsDoNotMatch": "Passwords do not match.",
|
||||||
"standardUser": "Standard User",
|
"standardUser": "Standard User",
|
||||||
|
"avatarUrl": "Avatar URL",
|
||||||
|
"avatarUrlPlaceholder": "Enter avatar image URL",
|
||||||
|
"uploadAvatar": "Upload Avatar",
|
||||||
"botCredentialTab": "Bot Credential",
|
"botCredentialTab": "Bot Credential",
|
||||||
"botCredentialHint": "Use this credential pair to access /mcp with X-Bot-Id and X-Bot-Secret.",
|
"botCredentialHint": "Use this credential pair to access /mcp with X-Bot-Id and X-Bot-Secret.",
|
||||||
"botCredentialHintDesc": "The secret is shown only after generation. Store it securely after copying.",
|
"botCredentialHintDesc": "The secret is shown only after generation. Store it securely after copying.",
|
||||||
"botBindStatus": "Binding Status",
|
"botBindStatus": "Binding Status",
|
||||||
"avatarUrl": "Avatar URL",
|
|
||||||
"avatarUrlPlaceholder": "Enter avatar image URL",
|
|
||||||
"uploadAvatar": "Upload Avatar",
|
|
||||||
"botBound": "Bound",
|
"botBound": "Bound",
|
||||||
"botUnbound": "Not Generated",
|
"botUnbound": "Not Generated",
|
||||||
"botSecretHidden": "Hidden. Generate or reset to get a new secret.",
|
"botSecretHidden": "Hidden. Generate or reset to get a new secret.",
|
||||||
|
|
@ -310,7 +313,11 @@
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"platform": "Platform",
|
"platform": "Platform",
|
||||||
"close": "Close"
|
"close": "Close",
|
||||||
|
"cleanCurrent": "Clear {{type}}",
|
||||||
|
"cleanConfirmTitle": "Clear {{type}}?",
|
||||||
|
"cleanConfirmDescription": "This will remove all records of the current log type and ignores the active filters.",
|
||||||
|
"cleanSuccess": "{{type}} cleared"
|
||||||
},
|
},
|
||||||
"devicesExt": {
|
"devicesExt": {
|
||||||
"operationSucceeded": "Operation succeeded",
|
"operationSucceeded": "Operation succeeded",
|
||||||
|
|
@ -358,6 +365,9 @@
|
||||||
"expired": "Expired",
|
"expired": "Expired",
|
||||||
"tenantNamePlaceholder": "Example: Cloud Intelligence",
|
"tenantNamePlaceholder": "Example: Cloud Intelligence",
|
||||||
"tenantCodePlaceholder": "Example: UNIS",
|
"tenantCodePlaceholder": "Example: UNIS",
|
||||||
|
"defaultAdminUsernamePlaceholder": "Default: amdin@tenantcode",
|
||||||
|
"defaultAdminUsernameTip": "The default value is generated from the lowercase tenant code and can be changed manually. Username uniqueness is validated on save.",
|
||||||
|
"defaultAdminUsernameFormatTip": "Username may contain only lowercase letters, numbers, @, and _.",
|
||||||
"contactNamePlaceholder": "Contact person",
|
"contactNamePlaceholder": "Contact person",
|
||||||
"contactPhonePlaceholder": "Mobile or phone",
|
"contactPhonePlaceholder": "Mobile or phone",
|
||||||
"remarkPlaceholder": "Optional tenant description",
|
"remarkPlaceholder": "Optional tenant description",
|
||||||
|
|
@ -374,6 +384,8 @@
|
||||||
"emailPlaceholder": "example@domain.com",
|
"emailPlaceholder": "example@domain.com",
|
||||||
"passwordKeepPlaceholder": "Leave blank to keep current password",
|
"passwordKeepPlaceholder": "Leave blank to keep current password",
|
||||||
"passwordInitPlaceholder": "Set initial password",
|
"passwordInitPlaceholder": "Set initial password",
|
||||||
|
"confirmPassword": "Confirm Password",
|
||||||
|
"confirmPasswordPlaceholder": "Please confirm the password",
|
||||||
"selectOrgPlaceholder": "Select organization or department",
|
"selectOrgPlaceholder": "Select organization or department",
|
||||||
"membershipsTitle": "Tenant Memberships",
|
"membershipsTitle": "Tenant Memberships",
|
||||||
"membershipTitle": "Membership #{{index}}",
|
"membershipTitle": "Membership #{{index}}",
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,10 @@
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"success": "操作成功",
|
"success": "操作成功",
|
||||||
"error": "操作失败",
|
"error": "操作失败",
|
||||||
"total": "共{{total}}条数据",
|
"total": "共 {{total}} 条数据",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"view": "查看"
|
"view": "查看",
|
||||||
|
"maxLength": "超出最大长度,最大长度为{{length}}"
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"profile": "个人信息",
|
"profile": "个人信息",
|
||||||
|
|
@ -170,6 +171,7 @@
|
||||||
"drawerTitleEdit": "编辑租户信息",
|
"drawerTitleEdit": "编辑租户信息",
|
||||||
"tenantName": "租户名称",
|
"tenantName": "租户名称",
|
||||||
"tenantCode": "租户编码",
|
"tenantCode": "租户编码",
|
||||||
|
"defaultAdminUsername": "默认管理员用户名",
|
||||||
"contactName": "联系人姓名",
|
"contactName": "联系人姓名",
|
||||||
"contactPhone": "联系电话"
|
"contactPhone": "联系电话"
|
||||||
},
|
},
|
||||||
|
|
@ -231,7 +233,8 @@
|
||||||
"copyright": "版权信息",
|
"copyright": "版权信息",
|
||||||
"desc": "系统描述",
|
"desc": "系统描述",
|
||||||
"uploadHint": "点击或拖拽上传图片",
|
"uploadHint": "点击或拖拽上传图片",
|
||||||
"uploadLimit": "建议比例 1:1,大小不超过 2MB",
|
"uploadLimit": "建议比例 1:1,大小不超过 {{size}}",
|
||||||
|
"uploadTooLarge": "上传图片不能超过 {{size}}",
|
||||||
"basicInfo": "基础信息",
|
"basicInfo": "基础信息",
|
||||||
"brandAssets": "品牌资源",
|
"brandAssets": "品牌资源",
|
||||||
"complianceFooter": "合规与页脚",
|
"complianceFooter": "合规与页脚",
|
||||||
|
|
@ -260,15 +263,14 @@
|
||||||
"saveChanges": "保存修改",
|
"saveChanges": "保存修改",
|
||||||
"updatePassword": "更新密码",
|
"updatePassword": "更新密码",
|
||||||
"passwordsDoNotMatch": "两次输入的密码不一致。",
|
"passwordsDoNotMatch": "两次输入的密码不一致。",
|
||||||
"standardUser": "普通用户",
|
"standardUser": "\u666e\u901a\u7528\u6237",
|
||||||
"botCredentialTab": "Bot 凭证",
|
|
||||||
"botCredentialHint": "使用这组凭证通过 X-Bot-Id 和 X-Bot-Secret 访问 /mcp。",
|
|
||||||
"botCredentialHintDesc": "Secret 只会在生成后显示一次,请复制后妥善保管。",
|
|
||||||
"botBindStatus": "绑定状态",
|
|
||||||
"avatarUrl": "\u5934\u50cf URL",
|
"avatarUrl": "\u5934\u50cf URL",
|
||||||
"avatarUrlPlaceholder": "\u8bf7\u8f93\u5165\u5934\u50cf\u56fe\u7247\u5730\u5740",
|
"avatarUrlPlaceholder": "\u8bf7\u8f93\u5165\u5934\u50cf\u56fe\u7247\u5730\u5740",
|
||||||
"uploadAvatar": "\u4e0a\u4f20\u5934\u50cf",
|
"uploadAvatar": "\u4e0a\u4f20\u5934\u50cf",
|
||||||
|
"botCredentialTab": "Bot \u51ed\u8bc1",
|
||||||
|
"botCredentialHint": "使用这组凭证通过 X-Bot-Id 和 X-Bot-Secret 访问 /mcp。",
|
||||||
|
"botCredentialHintDesc": "Secret 只会在生成后显示一次,请复制后妥善保管。",
|
||||||
|
"botBindStatus": "绑定状态",
|
||||||
"botBound": "已绑定",
|
"botBound": "已绑定",
|
||||||
"botUnbound": "未生成",
|
"botUnbound": "未生成",
|
||||||
"botSecretHidden": "已隐藏。如需查看新的 Secret,请重新生成。",
|
"botSecretHidden": "已隐藏。如需查看新的 Secret,请重新生成。",
|
||||||
|
|
@ -311,7 +313,11 @@
|
||||||
"failed": "失败",
|
"failed": "失败",
|
||||||
"system": "系统",
|
"system": "系统",
|
||||||
"platform": "平台",
|
"platform": "平台",
|
||||||
"close": "关闭"
|
"close": "关闭",
|
||||||
|
"cleanCurrent": "清空{{type}}",
|
||||||
|
"cleanConfirmTitle": "确认清空{{type}}?",
|
||||||
|
"cleanConfirmDescription": "将清空当前日志类型的全部记录,不受当前筛选条件影响。",
|
||||||
|
"cleanSuccess": "{{type}}已清空"
|
||||||
},
|
},
|
||||||
"devicesExt": {
|
"devicesExt": {
|
||||||
"operationSucceeded": "操作成功",
|
"operationSucceeded": "操作成功",
|
||||||
|
|
@ -359,6 +365,9 @@
|
||||||
"expired": "已过期",
|
"expired": "已过期",
|
||||||
"tenantNamePlaceholder": "例如:云智协同",
|
"tenantNamePlaceholder": "例如:云智协同",
|
||||||
"tenantCodePlaceholder": "例如:UNIS",
|
"tenantCodePlaceholder": "例如:UNIS",
|
||||||
|
"defaultAdminUsernamePlaceholder": "默认:amdin@租户编码小写",
|
||||||
|
"defaultAdminUsernameTip": "默认值会根据租户编码自动生成,并且可以手动修改;保存时会校验用户名是否重复。",
|
||||||
|
"defaultAdminUsernameFormatTip": "用户名只能输入小写字母、数字、@ 和 _。",
|
||||||
"contactNamePlaceholder": "请输入联系人姓名",
|
"contactNamePlaceholder": "请输入联系人姓名",
|
||||||
"contactPhonePlaceholder": "请输入手机号或座机号",
|
"contactPhonePlaceholder": "请输入手机号或座机号",
|
||||||
"remarkPlaceholder": "选填,填写租户说明",
|
"remarkPlaceholder": "选填,填写租户说明",
|
||||||
|
|
@ -375,6 +384,8 @@
|
||||||
"emailPlaceholder": "example@domain.com",
|
"emailPlaceholder": "example@domain.com",
|
||||||
"passwordKeepPlaceholder": "不填写则保留当前密码",
|
"passwordKeepPlaceholder": "不填写则保留当前密码",
|
||||||
"passwordInitPlaceholder": "请设置初始密码",
|
"passwordInitPlaceholder": "请设置初始密码",
|
||||||
|
"confirmPassword": "确认密码",
|
||||||
|
"confirmPasswordPlaceholder": "请再次输入密码",
|
||||||
"selectOrgPlaceholder": "请选择组织或部门",
|
"selectOrgPlaceholder": "请选择组织或部门",
|
||||||
"membershipsTitle": "租户归属",
|
"membershipsTitle": "租户归属",
|
||||||
"membershipTitle": "归属 #{{index}}",
|
"membershipTitle": "归属 #{{index}}",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
.users-page {
|
.users-page {
|
||||||
padding: 24px;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.users-header {
|
.users-header {
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.users-table-toolbar {
|
.users-table-toolbar {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.users-search-input {
|
.users-search-input {
|
||||||
|
|
|
||||||
|
|
@ -397,9 +397,18 @@ export default function Users() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
||||||
<div className="flex-1 min-h-0 h-full">
|
<Table
|
||||||
<Table rowKey="userId" columns={columns} dataSource={filteredData} loading={loading} size="middle" scroll={{ y: "calc(100vh - 420px)" }} pagination={getStandardPagination(filteredData.length, current, pageSize, (page, size) => { setCurrent(page); setPageSize(size); })} />
|
rowKey="userId"
|
||||||
</div>
|
columns={columns}
|
||||||
|
dataSource={filteredData}
|
||||||
|
loading={loading}
|
||||||
|
size="middle"
|
||||||
|
scroll={{ y: "calc(100vh - 380px)" }}
|
||||||
|
pagination={getStandardPagination(filteredData.length, current, pageSize, (page, size) => {
|
||||||
|
setCurrent(page);
|
||||||
|
setPageSize(size);
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Drawer title={<div className="user-drawer-title"><UserOutlined className="mr-2" aria-hidden="true" />{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
<Drawer title={<div className="user-drawer-title"><UserOutlined className="mr-2" aria-hidden="true" />{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ export default function RolePermissionBinding() {
|
||||||
title: t("common.status"),
|
title: t("common.status"),
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (value: number) => (value === 1 ? <Tag color="green" className="m-0">鍚敤</Tag> : <Tag className="m-0">绂佺敤</Tag>)
|
render: (value: number) => (value === 1 ? <Tag color="green" className="m-0">启用</Tag> : <Tag className="m-0">禁用</Tag>)
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
@ -266,7 +266,7 @@ export default function RolePermissionBinding() {
|
||||||
}}
|
}}
|
||||||
defaultExpandAll
|
defaultExpandAll
|
||||||
/>
|
/>
|
||||||
{!permissions.length && !loadingPerms ? <Empty description="鏆傛棤鏉冮檺鏁版嵁" /> : null}
|
{!permissions.length && !loadingPerms ? <Empty description="暂无权限数据" /> : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200">
|
<div className="flex flex-col items-center justify-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200">
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ const AiModels: React.FC = () => {
|
||||||
const localProfileLoadedRef = useRef(false);
|
const localProfileLoadedRef = useRef(false);
|
||||||
|
|
||||||
const provider = Form.useWatch("provider", form);
|
const provider = Form.useWatch("provider", form);
|
||||||
|
const isDefaultChecked = Form.useWatch("isDefaultChecked", form);
|
||||||
const isLocalProvider = String(provider || "").toLowerCase() === "custom";
|
const isLocalProvider = String(provider || "").toLowerCase() === "custom";
|
||||||
|
|
||||||
const isPlatformAdmin = useMemo(() => {
|
const isPlatformAdmin = useMemo(() => {
|
||||||
|
|
@ -152,6 +153,7 @@ const AiModels: React.FC = () => {
|
||||||
modelType: activeType,
|
modelType: activeType,
|
||||||
isDefaultChecked: false,
|
isDefaultChecked: false,
|
||||||
statusChecked: true,
|
statusChecked: true,
|
||||||
|
sortOrder: 0,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
topP: 0.9,
|
topP: 0.9,
|
||||||
apiPath: "/v1/chat/completions",
|
apiPath: "/v1/chat/completions",
|
||||||
|
|
@ -239,6 +241,10 @@ const AiModels: React.FC = () => {
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
if (values.isDefaultChecked && !values.statusChecked) {
|
||||||
|
message.warning("默认模型必须保持启用状态");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const payload: AiModelDTO = {
|
const payload: AiModelDTO = {
|
||||||
id: editingId ?? undefined,
|
id: editingId ?? undefined,
|
||||||
modelType: values.modelType,
|
modelType: values.modelType,
|
||||||
|
|
@ -260,6 +266,7 @@ const AiModels: React.FC = () => {
|
||||||
topP: values.topP,
|
topP: values.topP,
|
||||||
isDefault: values.isDefaultChecked ? 1 : 0,
|
isDefault: values.isDefaultChecked ? 1 : 0,
|
||||||
status: values.statusChecked ? 1 : 0,
|
status: values.statusChecked ? 1 : 0,
|
||||||
|
sortOrder: values.sortOrder ?? 0,
|
||||||
remark: values.remark,
|
remark: values.remark,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -360,6 +367,12 @@ const AiModels: React.FC = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ title: "模型名称(code)", dataIndex: "modelCode", key: "modelCode" },
|
{ title: "模型名称(code)", dataIndex: "modelCode", key: "modelCode" },
|
||||||
|
{
|
||||||
|
title: "排序",
|
||||||
|
dataIndex: "sortOrder",
|
||||||
|
key: "sortOrder",
|
||||||
|
render: (value: number | undefined) => value ?? 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "状态",
|
title: "状态",
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
|
|
@ -504,6 +517,14 @@ const AiModels: React.FC = () => {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item name="sortOrder" label="排序值">
|
||||||
|
<InputNumber min={0} style={{ width: "100%" }} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Form.Item name="baseUrl" label="Base URL" rules={[{ required: true, message: "请输入 Base URL" }]}>
|
<Form.Item name="baseUrl" label="Base URL" rules={[{ required: true, message: "请输入 Base URL" }]}>
|
||||||
<Input placeholder="https://api.example.com/v1" />
|
<Input placeholder="https://api.example.com/v1" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
@ -618,12 +639,20 @@ const AiModels: React.FC = () => {
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name="isDefaultChecked" label="设为默认" valuePropName="checked">
|
<Form.Item name="isDefaultChecked" label="设为默认" valuePropName="checked">
|
||||||
<Switch checkedChildren="是" unCheckedChildren="否" />
|
<Switch
|
||||||
|
checkedChildren="是"
|
||||||
|
unCheckedChildren="否"
|
||||||
|
onChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
form.setFieldValue("statusChecked", true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Form.Item name="statusChecked" label="状态" valuePropName="checked">
|
<Form.Item name="statusChecked" label="状态" valuePropName="checked">
|
||||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
<Switch checkedChildren="启用" unCheckedChildren="禁用" disabled={Boolean(isDefaultChecked)} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
|
List,
|
||||||
Modal,
|
Modal,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Row,
|
Row,
|
||||||
|
|
@ -20,9 +21,7 @@ import {
|
||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
FolderOpenOutlined,
|
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SearchOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useDict } from "../../hooks/useDict";
|
import { useDict } from "../../hooks/useDict";
|
||||||
|
|
@ -43,6 +42,7 @@ import {
|
||||||
type HotWordGroupVO,
|
type HotWordGroupVO,
|
||||||
} from "../../api/business/hotwordGroup";
|
} from "../../api/business/hotwordGroup";
|
||||||
import AppPagination from "../../components/shared/AppPagination";
|
import AppPagination from "../../components/shared/AppPagination";
|
||||||
|
import ListActionBar from "../../components/shared/ListActionBar/ListActionBar";
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
@ -90,13 +90,14 @@ const HotWords: React.FC = () => {
|
||||||
const [submitLoading, setSubmitLoading] = useState(false);
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||||||
|
|
||||||
const [groupOptions, setGroupOptions] = useState<HotWordGroupVO[]>([]);
|
const [groupOptions, setGroupOptions] = useState<HotWordGroupVO[]>([]);
|
||||||
const [groupManageVisible, setGroupManageVisible] = useState(false);
|
|
||||||
const [groupEditorVisible, setGroupEditorVisible] = useState(false);
|
const [groupEditorVisible, setGroupEditorVisible] = useState(false);
|
||||||
const [groupLoading, setGroupLoading] = useState(false);
|
const [groupLoading, setGroupLoading] = useState(false);
|
||||||
const [groupSubmitLoading, setGroupSubmitLoading] = useState(false);
|
const [groupSubmitLoading, setGroupSubmitLoading] = useState(false);
|
||||||
const [groupData, setGroupData] = useState<HotWordGroupVO[]>([]);
|
const [groupData, setGroupData] = useState<HotWordGroupVO[]>([]);
|
||||||
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
|
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const [filterVisible, setFilterVisible] = useState(false);
|
||||||
|
|
||||||
const groupNameMap = useMemo(
|
const groupNameMap = useMemo(
|
||||||
() => Object.fromEntries(groupOptions.map((item) => [item.id, item.groupName])) as Record<number, string>,
|
() => Object.fromEntries(groupOptions.map((item) => [item.id, item.groupName])) as Record<number, string>,
|
||||||
[groupOptions]
|
[groupOptions]
|
||||||
|
|
@ -104,10 +105,11 @@ const HotWords: React.FC = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchData();
|
void fetchData();
|
||||||
}, [current, searchCategory, searchGroupId, searchWord, size]);
|
}, [current, searchCategory, searchGroupId, size]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadGroupOptions();
|
void loadGroupOptions();
|
||||||
|
void loadGroupPage();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
|
|
@ -165,7 +167,7 @@ const HotWords: React.FC = () => {
|
||||||
} else {
|
} else {
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
form.setFieldsValue({ weight: 2, status: 1 });
|
form.setFieldsValue({ weight: 2, status: 1, hotWordGroupId: searchGroupId });
|
||||||
}
|
}
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
@ -174,6 +176,7 @@ const HotWords: React.FC = () => {
|
||||||
await deleteHotWord(id);
|
await deleteHotWord(id);
|
||||||
message.success("删除成功");
|
message.success("删除成功");
|
||||||
await fetchData();
|
await fetchData();
|
||||||
|
await loadGroupPage();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
|
@ -194,7 +197,7 @@ const HotWords: React.FC = () => {
|
||||||
message.success("新增成功");
|
message.success("新增成功");
|
||||||
}
|
}
|
||||||
setModalVisible(false);
|
setModalVisible(false);
|
||||||
await Promise.all([fetchData(), loadGroupOptions(), groupManageVisible ? loadGroupPage() : Promise.resolve()]);
|
await Promise.all([fetchData(), loadGroupOptions(), loadGroupPage()]);
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitLoading(false);
|
setSubmitLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -216,12 +219,8 @@ const HotWords: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openGroupManager = async () => {
|
const openGroupEditor = (record?: HotWordGroupVO, e?: React.MouseEvent) => {
|
||||||
setGroupManageVisible(true);
|
e?.stopPropagation();
|
||||||
await loadGroupPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openGroupEditor = (record?: HotWordGroupVO) => {
|
|
||||||
if (record) {
|
if (record) {
|
||||||
setEditingGroupId(record.id);
|
setEditingGroupId(record.id);
|
||||||
groupForm.setFieldsValue({
|
groupForm.setFieldsValue({
|
||||||
|
|
@ -255,9 +254,13 @@ const HotWords: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteGroup = async (id: number) => {
|
const handleDeleteGroup = async (id: number, e?: React.MouseEvent) => {
|
||||||
|
e?.stopPropagation();
|
||||||
await deleteHotWordGroup(id, isPlatformAdmin ? activeTenantId : undefined);
|
await deleteHotWordGroup(id, isPlatformAdmin ? activeTenantId : undefined);
|
||||||
message.success("热词组删除成功");
|
message.success("热词组删除成功");
|
||||||
|
if (searchGroupId === id) {
|
||||||
|
setSearchGroupId(undefined);
|
||||||
|
}
|
||||||
await Promise.all([loadGroupOptions(), loadGroupPage(), fetchData()]);
|
await Promise.all([loadGroupOptions(), loadGroupPage(), fetchData()]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -325,121 +328,178 @@ const HotWords: React.FC = () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const groupColumns = [
|
const filterContent = (
|
||||||
{
|
<div style={{ width: 200 }}>
|
||||||
title: "热词组名称",
|
<div style={{ marginBottom: 8 }}>分类筛选</div>
|
||||||
dataIndex: "groupName",
|
<Select
|
||||||
key: "groupName",
|
placeholder="按分类筛选"
|
||||||
render: (value: string) => <Text strong>{value}</Text>,
|
style={{ width: '100%' }}
|
||||||
},
|
allowClear
|
||||||
{
|
value={searchCategory}
|
||||||
title: "热词数量",
|
onChange={(value) => {
|
||||||
dataIndex: "hotWordCount",
|
setSearchCategory(value);
|
||||||
key: "hotWordCount",
|
setCurrent(1);
|
||||||
render: (value: number) => <Tag color={value >= 200 ? "red" : "processing"}>{value}/200</Tag>,
|
setFilterVisible(false);
|
||||||
},
|
}}
|
||||||
{
|
>
|
||||||
title: "状态",
|
{categories.map((item) => (
|
||||||
dataIndex: "status",
|
<Option key={item.itemValue} value={item.itemValue}>
|
||||||
key: "status",
|
{item.itemLabel}
|
||||||
render: (value: number) => value === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />,
|
</Option>
|
||||||
},
|
))}
|
||||||
{
|
</Select>
|
||||||
title: "备注",
|
</div>
|
||||||
dataIndex: "remark",
|
);
|
||||||
key: "remark",
|
|
||||||
render: (value?: string) => value || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
key: "action",
|
|
||||||
render: (_: unknown, record: HotWordGroupVO) => (
|
|
||||||
<Space>
|
|
||||||
<Button type="link" size="small" onClick={() => openGroupEditor(record)}>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Popconfirm
|
|
||||||
title="确定删除这个热词组吗?"
|
|
||||||
description="删除前必须先解除模板引用并清空组内热词。"
|
|
||||||
onConfirm={() => handleDeleteGroup(record.id)}
|
|
||||||
okText={t("common.confirm")}
|
|
||||||
cancelText={t("common.cancel")}
|
|
||||||
>
|
|
||||||
<Button type="link" size="small" danger>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<div className="app-page" style={{ padding: '16px', display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
<Card
|
<div style={{ display: 'flex', gap: '16px', flex: 1, minHeight: 0 }}>
|
||||||
className="app-page__content-card shadow-sm"
|
{/* Left Panel: Hotword Groups */}
|
||||||
style={{ flex: 1, minHeight: 0 }}
|
<Card
|
||||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
className="shadow-sm"
|
||||||
title="热词管理"
|
title="热词组"
|
||||||
extra={
|
style={{ width: '25%', display: 'flex', flexDirection: 'column', minWidth: 280 }}
|
||||||
<Space wrap>
|
styles={{ body: { padding: 0, flex: 1, overflow: 'auto' } }}
|
||||||
<Select
|
extra={
|
||||||
placeholder="按分类筛选"
|
<Button
|
||||||
style={{ width: 150 }}
|
type="primary"
|
||||||
allowClear
|
icon={<PlusOutlined />}
|
||||||
onChange={(value) => {
|
onClick={() => openGroupEditor()}
|
||||||
setSearchCategory(value);
|
size="small"
|
||||||
setCurrent(1);
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
loading={groupLoading}
|
||||||
|
dataSource={[{ id: undefined, groupName: '全部热词' } as any, ...groupData]}
|
||||||
|
renderItem={(item) => {
|
||||||
|
const isSelected = searchGroupId === item.id;
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
if (item.id) {
|
||||||
|
actions.push(
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={(e) => openGroupEditor(item, e)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
actions.push(
|
||||||
|
<Popconfirm
|
||||||
|
title="确定删除这个热词组吗?"
|
||||||
|
description="删除前必须先解除模板引用并清空组内热词。"
|
||||||
|
onConfirm={(e) => handleDeleteGroup(item.id, e)}
|
||||||
|
onCancel={e => e?.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Popconfirm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
onClick={() => {
|
||||||
|
setSearchGroupId(item.id);
|
||||||
|
setCurrent(1);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '12px 24px',
|
||||||
|
background: isSelected ? 'var(--ant-color-primary-bg, #e6f4ff)' : 'transparent',
|
||||||
|
borderBottom: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
|
||||||
|
transition: 'background 0.3s'
|
||||||
|
}}
|
||||||
|
actions={actions}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={
|
||||||
|
<Text strong style={{ color: isSelected ? 'var(--ant-color-primary, #1677ff)' : 'inherit' }}>
|
||||||
|
{item.groupName}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
item.id
|
||||||
|
? <><Tag color={item.hotWordCount >= 200 ? "red" : "processing"}>{item.hotWordCount}/200</Tag> {item.remark}</>
|
||||||
|
: '查看所有热词'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Right Panel: Hotwords */}
|
||||||
|
<Card
|
||||||
|
className="shadow-sm"
|
||||||
|
title={searchGroupId ? groupData.find(g => g.id === searchGroupId)?.groupName || '热词列表' : '全部热词'}
|
||||||
|
style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}
|
||||||
|
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '16px 24px 0' }}>
|
||||||
|
<ListActionBar
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
key: 'add',
|
||||||
|
label: '新增热词',
|
||||||
|
type: 'primary',
|
||||||
|
icon: <PlusOutlined />,
|
||||||
|
onClick: () => handleOpenModal()
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
search={{
|
||||||
|
placeholder: '搜索热词原文',
|
||||||
|
value: searchWord,
|
||||||
|
onChange: (val) => setSearchWord(val),
|
||||||
|
onSearch: () => {
|
||||||
|
setCurrent(1);
|
||||||
|
void fetchData();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
filter={{
|
||||||
{categories.map((item) => (
|
content: filterContent,
|
||||||
<Option key={item.itemValue} value={item.itemValue}>
|
title: '高级筛选',
|
||||||
{item.itemLabel}
|
visible: filterVisible,
|
||||||
</Option>
|
onVisibleChange: setFilterVisible,
|
||||||
))}
|
isActive: !!searchCategory,
|
||||||
</Select>
|
selectedLabel: searchCategory ? categories.find(c => c.itemValue === searchCategory)?.itemLabel : '筛选'
|
||||||
<Select
|
}}
|
||||||
placeholder="按热词组筛选"
|
showRefresh
|
||||||
style={{ width: 180 }}
|
onRefresh={() => {
|
||||||
allowClear
|
|
||||||
options={groupOptions.map((item) => ({ label: item.groupName, value: item.id }))}
|
|
||||||
onChange={(value) => {
|
|
||||||
setSearchGroupId(value);
|
|
||||||
setCurrent(1);
|
setCurrent(1);
|
||||||
|
void fetchData();
|
||||||
|
void loadGroupPage();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
</div>
|
||||||
placeholder="搜索热词原文"
|
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "16px 24px 0" }}>
|
||||||
prefix={<SearchOutlined />}
|
<Table
|
||||||
allowClear
|
columns={columns}
|
||||||
onPressEnter={(e) => {
|
dataSource={data}
|
||||||
setSearchWord((e.target as HTMLInputElement).value);
|
rowKey="id"
|
||||||
setCurrent(1);
|
loading={loading}
|
||||||
}}
|
scroll={{ x: "max-content" }}
|
||||||
style={{ width: 200 }}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
<Button icon={<FolderOpenOutlined />} onClick={() => void openGroupManager()}>
|
</div>
|
||||||
热词组管理
|
<AppPagination
|
||||||
</Button>
|
current={current}
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
|
pageSize={size}
|
||||||
新增热词
|
total={total}
|
||||||
</Button>
|
onChange={(page, pageSize) => {
|
||||||
</Space>
|
setCurrent(page);
|
||||||
}
|
setSize(pageSize);
|
||||||
>
|
}}
|
||||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "24px 24px 0" }}>
|
/>
|
||||||
<Table columns={columns} dataSource={data} rowKey="id" loading={loading} scroll={{ x: "max-content" }} pagination={false} />
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<AppPagination
|
|
||||||
current={current}
|
|
||||||
pageSize={size}
|
|
||||||
total={total}
|
|
||||||
onChange={(page, pageSize) => {
|
|
||||||
setCurrent(page);
|
|
||||||
setSize(pageSize);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={editingId ? "编辑热词" : "新增热词"}
|
title={editingId ? "编辑热词" : "新增热词"}
|
||||||
|
|
@ -500,22 +560,6 @@ const HotWords: React.FC = () => {
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
|
||||||
title="热词组管理"
|
|
||||||
open={groupManageVisible}
|
|
||||||
onCancel={() => setGroupManageVisible(false)}
|
|
||||||
footer={null}
|
|
||||||
width={900}
|
|
||||||
destroyOnHidden
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: 16, display: "flex", justifyContent: "flex-end" }}>
|
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openGroupEditor()}>
|
|
||||||
新增热词组
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Table rowKey="id" columns={groupColumns} dataSource={groupData} loading={groupLoading} pagination={false} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={editingGroupId ? "编辑热词组" : "新增热词组"}
|
title={editingGroupId ? "编辑热词组" : "新增热词组"}
|
||||||
open={groupEditorVisible}
|
open={groupEditorVisible}
|
||||||
|
|
|
||||||
|
|
@ -154,10 +154,10 @@ export default function Tenants() {
|
||||||
<Card className="app-page__filter-card border-0" style={{ borderRadius: "12px" }} styles={{ body: { padding: "16px" } }}>
|
<Card className="app-page__filter-card border-0" style={{ borderRadius: "12px" }} styles={{ body: { padding: "16px" } }}>
|
||||||
<Space wrap size="middle" className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
<Space wrap size="middle" className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
||||||
<Space wrap size="middle" className="app-page__toolbar">
|
<Space wrap size="middle" className="app-page__toolbar">
|
||||||
<Input placeholder={t("tenants.tenantName")} prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />} style={{ width: 220, borderRadius: "6px" }} value={params.name} onChange={(event) => setParams({ ...params, name: event.target.value })} allowClear />
|
<Input placeholder={t("tenants.tenantName")} prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />} style={{ width: 220, borderRadius: "6px" }} value={params.name} onChange={(event) => setParams({ ...params, name: event.target.value })} allowClear />
|
||||||
<Input placeholder={t("tenants.tenantCode")} style={{ width: 180, borderRadius: "6px" }} value={params.code} onChange={(event) => setParams({ ...params, code: event.target.value })} allowClear />
|
<Input placeholder={t("tenants.tenantCode")} style={{ width: 180, borderRadius: "6px" }} value={params.code} onChange={(event) => setParams({ ...params, code: event.target.value })} allowClear />
|
||||||
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch} style={{ borderRadius: "6px" }}>{t("common.search")}</Button>
|
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch} style={{ borderRadius: "6px" }}>{t("common.search")}</Button>
|
||||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset} style={{ borderRadius: "6px" }}>{t("common.reset")}</Button>
|
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset} style={{ borderRadius: "6px" }}>{t("common.reset")}</Button>
|
||||||
</Space>
|
</Space>
|
||||||
{can("sys_tenant:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate} style={{ borderRadius: "6px" }}>{t("common.create")}</Button>}
|
{can("sys_tenant:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate} style={{ borderRadius: "6px" }}>{t("common.create")}</Button>}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
@ -202,23 +202,23 @@ export default function Tenants() {
|
||||||
</Row>
|
</Row>
|
||||||
{!editing && (
|
{!editing && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("tenants.defaultAdminUsername", { defaultValue: "默认管理员账户" })}
|
label={t("tenants.defaultAdminUsername")}
|
||||||
name="defaultAdminUsername"
|
name="defaultAdminUsername"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: t("tenants.defaultAdminUsername", { defaultValue: "默认管理员账户" }) },
|
{ required: true, message: t("tenants.defaultAdminUsername") },
|
||||||
{
|
{
|
||||||
pattern: LOGIN_NAME_PATTERN,
|
pattern: LOGIN_NAME_PATTERN,
|
||||||
message: t("tenantsExt.defaultAdminUsernameFormatTip", { defaultValue: "登录名只能输入数字、小写英文、@ 和 _" })
|
message: t("tenantsExt.defaultAdminUsernameFormatTip")
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
getValueFromEvent={(event) => {
|
getValueFromEvent={(event) => {
|
||||||
setAdminAccountTouched(true);
|
setAdminAccountTouched(true);
|
||||||
return sanitizeLoginName(event?.target?.value);
|
return sanitizeLoginName(event?.target?.value);
|
||||||
}}
|
}}
|
||||||
extra={t("tenantsExt.defaultAdminUsernameTip", { defaultValue: "默认值会根据租户编码自动生成,可手动修改;保存时会校验登录名是否重复。登录名只能输入数字、小写英文、@ 和 _。" })}
|
extra={t("tenantsExt.defaultAdminUsernameTip")}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("tenantsExt.defaultAdminUsernamePlaceholder", { defaultValue: "默认:admin@租户编码小写" })}
|
placeholder={t("tenantsExt.defaultAdminUsernamePlaceholder")}
|
||||||
className="tabular-nums"
|
className="tabular-nums"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue