feat: 重构会议和任务模型,优化ASR和总结任务处理逻辑
- 移除 `Meeting` 实体中的 `asrModelId`, `summaryModelId`, `promptContent`, `useSpkId`, `hotWords`, `summaryContent` 字段,新增 `latestSummaryTaskId` 字段 - 更新数据库表结构,移除相关字段并添加 `latest_summary_task_id` 字段 - 在 `AiTask` 实体中新增 `task_config` 和 `result_file_path` 字段 - 重构 `AiTaskServiceImpl` 中的 ASR 和总结任务处理逻辑,支持任务配置和结果文件路径 - 重构 `MeetingServiceImpl` 中的会议创建和更新逻辑,分离 ASR 和总结任务 - 优化 `PromptTemplateServiceImpl`,新增用户模板状态管理功能dev_na
parent
d554849e8e
commit
86b3616786
|
|
@ -316,10 +316,8 @@
|
||||||
| id | BIGSERIAL | PK | 主键ID |
|
| id | BIGSERIAL | PK | 主键ID |
|
||||||
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
| tenant_id | BIGINT | NOT NULL | 租户ID |
|
||||||
| title | VARCHAR(200) | NOT NULL | 会议标题 |
|
| title | VARCHAR(200) | NOT NULL | 会议标题 |
|
||||||
| asr_model_id | BIGINT | | 使用的 ASR 模型 |
|
| audio_url | VARCHAR(500) | | 专属音频路径 |
|
||||||
| summary_model_id | BIGINT | | 使用的 LLM 模型 |
|
| latest_summary_task_id | BIGINT | | 最新成功的总结任务ID |
|
||||||
| prompt_content | TEXT | | **[快照]** 发起任务时的提示词模板内容 |
|
|
||||||
| summary_content | TEXT | | **[固化]** 最终生成的 Markdown 总结内容 |
|
|
||||||
| status | SMALLINT | DEFAULT 0 | 0:待处理, 1:识别中, 2:总结中, 3:已完成, 4:失败 |
|
| status | SMALLINT | DEFAULT 0 | 0:待处理, 1:识别中, 2:总结中, 3:已完成, 4:失败 |
|
||||||
|
|
||||||
### 5.6 `biz_meeting_transcripts`(转录明细表)
|
### 5.6 `biz_meeting_transcripts`(转录明细表)
|
||||||
|
|
@ -335,8 +333,11 @@
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
| 字段 | 类型 | 约束 | 说明 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| id | BIGSERIAL | PK | 主键ID |
|
| id | BIGSERIAL | PK | 主键ID |
|
||||||
|
| meeting_id | BIGINT | NOT NULL | 关联会议ID |
|
||||||
| task_type | VARCHAR(20) | | ASR / SUMMARY |
|
| task_type | VARCHAR(20) | | ASR / SUMMARY |
|
||||||
| request_data | JSONB | | 请求原始数据 |
|
| request_data | JSONB | | 请求原始数据 |
|
||||||
| response_data | JSONB | | 响应原始数据 |
|
| response_data | JSONB | | 响应原始数据 |
|
||||||
|
| task_config | TEXT | | **[快照]** 任务配置(模型ID、提示词模板等) |
|
||||||
|
| result_file_path | VARCHAR(500) | | 结果文件相对路径 (如MD总结文件) |
|
||||||
| status | SMALLINT | | 0:排队, 1:处理中, 2:成功, 3:失败 |
|
| status | SMALLINT | | 0:排队, 1:处理中, 2:成功, 3:失败 |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -341,12 +341,7 @@ CREATE TABLE biz_meetings (
|
||||||
audio_url VARCHAR(500),
|
audio_url VARCHAR(500),
|
||||||
creator_id BIGINT, -- 发起人ID
|
creator_id BIGINT, -- 发起人ID
|
||||||
creator_name VARCHAR(100), -- 发起人姓名
|
creator_name VARCHAR(100), -- 发起人姓名
|
||||||
asr_model_id BIGINT, -- ASR模型ID
|
latest_summary_task_id BIGINT, -- 最新成功总结任务ID
|
||||||
summary_model_id BIGINT, -- LLM模型ID
|
|
||||||
prompt_content TEXT, -- 发起任务时的提示词模板快照
|
|
||||||
use_spk_id SMALLINT DEFAULT 1, -- 是否开启声纹识别 (1:开启, 0:关闭)
|
|
||||||
hot_words text, -- 任务发起时的热词快照
|
|
||||||
summary_content TEXT, -- Markdown 总结结果
|
|
||||||
status SMALLINT DEFAULT 0, -- 0:待处理, 1:处理中, 2:成功, 3:失败
|
status SMALLINT DEFAULT 0, -- 0:待处理, 1:处理中, 2:成功, 3:失败
|
||||||
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(),
|
||||||
|
|
@ -381,6 +376,8 @@ CREATE TABLE biz_ai_tasks (
|
||||||
status SMALLINT DEFAULT 0, -- 0:排队, 1:执行中, 2:成功, 3:失败
|
status SMALLINT DEFAULT 0, -- 0:排队, 1:执行中, 2:成功, 3:失败
|
||||||
request_data text, -- 请求三方原始JSON
|
request_data text, -- 请求三方原始JSON
|
||||||
response_data text, -- 三方返回原始JSON
|
response_data text, -- 三方返回原始JSON
|
||||||
|
task_config text, -- 任务配置参数快照
|
||||||
|
result_file_path VARCHAR(500), -- 结果文件路径
|
||||||
error_msg TEXT, -- 错误堆栈
|
error_msg TEXT, -- 错误堆栈
|
||||||
started_at TIMESTAMP(6),
|
started_at TIMESTAMP(6),
|
||||||
completed_at TIMESTAMP(6)
|
completed_at TIMESTAMP(6)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.imeeting.dto.biz.MeetingVO;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.security.LoginUser;
|
import com.imeeting.security.LoginUser;
|
||||||
import com.imeeting.service.biz.MeetingService;
|
import com.imeeting.service.biz.MeetingService;
|
||||||
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
import org.apache.fontbox.ttf.TrueTypeCollection;
|
import org.apache.fontbox.ttf.TrueTypeCollection;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
|
@ -54,13 +55,16 @@ import java.util.regex.Pattern;
|
||||||
public class MeetingController {
|
public class MeetingController {
|
||||||
|
|
||||||
private final MeetingService meetingService;
|
private final MeetingService meetingService;
|
||||||
|
private final PromptTemplateService promptTemplateService;
|
||||||
private final StringRedisTemplate redisTemplate;
|
private final StringRedisTemplate redisTemplate;
|
||||||
private final String uploadPath;
|
private final String uploadPath;
|
||||||
|
|
||||||
public MeetingController(MeetingService meetingService,
|
public MeetingController(MeetingService meetingService,
|
||||||
|
PromptTemplateService promptTemplateService,
|
||||||
StringRedisTemplate redisTemplate,
|
StringRedisTemplate redisTemplate,
|
||||||
@Value("${app.upload-path}") String uploadPath) {
|
@Value("${app.upload-path}") String uploadPath) {
|
||||||
this.meetingService = meetingService;
|
this.meetingService = meetingService;
|
||||||
|
this.promptTemplateService = promptTemplateService;
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
this.uploadPath = uploadPath;
|
this.uploadPath = uploadPath;
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +117,18 @@ public class MeetingController {
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public ApiResponse<MeetingVO> create(@RequestBody MeetingDTO dto) {
|
public ApiResponse<MeetingVO> create(@RequestBody MeetingDTO dto) {
|
||||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
if (dto.getPromptId() != null) {
|
||||||
|
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||||
|
dto.getPromptId(),
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()
|
||||||
|
);
|
||||||
|
if (!enabled) {
|
||||||
|
return ApiResponse.error("总结模板不可用或已被你禁用");
|
||||||
|
}
|
||||||
|
}
|
||||||
dto.setTenantId(loginUser.getTenantId());
|
dto.setTenantId(loginUser.getTenantId());
|
||||||
dto.setCreatorId(loginUser.getUserId());
|
dto.setCreatorId(loginUser.getUserId());
|
||||||
dto.setCreatorName(loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername());
|
dto.setCreatorName(loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername());
|
||||||
|
|
@ -205,9 +221,20 @@ public class MeetingController {
|
||||||
@PostMapping("/re-summary")
|
@PostMapping("/re-summary")
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public ApiResponse<Boolean> reSummary(@RequestBody Map<String, Object> params) {
|
public ApiResponse<Boolean> reSummary(@RequestBody Map<String, Object> params) {
|
||||||
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
Long meetingId = Long.valueOf(params.get("meetingId").toString());
|
Long meetingId = Long.valueOf(params.get("meetingId").toString());
|
||||||
Long summaryModelId = Long.valueOf(params.get("summaryModelId").toString());
|
Long summaryModelId = Long.valueOf(params.get("summaryModelId").toString());
|
||||||
Long promptId = Long.valueOf(params.get("promptId").toString());
|
Long promptId = Long.valueOf(params.get("promptId").toString());
|
||||||
|
boolean enabled = promptTemplateService.isTemplateEnabledForUser(
|
||||||
|
promptId,
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()
|
||||||
|
);
|
||||||
|
if (!enabled) {
|
||||||
|
return ApiResponse.error("总结模板不可用或已被你禁用");
|
||||||
|
}
|
||||||
|
|
||||||
meetingService.reSummary(meetingId, summaryModelId, promptId);
|
meetingService.reSummary(meetingId, summaryModelId, promptId);
|
||||||
return ApiResponse.ok(true);
|
return ApiResponse.ok(true);
|
||||||
|
|
|
||||||
|
|
@ -28,23 +28,16 @@ public class PromptTemplateController {
|
||||||
public ApiResponse<PromptTemplateVO> save(@RequestBody PromptTemplateDTO dto) {
|
public ApiResponse<PromptTemplateVO> save(@RequestBody PromptTemplateDTO dto) {
|
||||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
|
||||||
// 权限校验逻辑
|
|
||||||
if (Integer.valueOf(1).equals(dto.getIsSystem())) {
|
if (Integer.valueOf(1).equals(dto.getIsSystem())) {
|
||||||
// 只有平台管理员能创建平台级模板(tenantId=0)
|
|
||||||
// 只有租户管理员能创建租户级模板(tenantId>0)
|
|
||||||
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && !Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
return ApiResponse.error("无权创建系统模板");
|
return ApiResponse.error("No permission to create public template");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是租户管理员创建系统模板,确保 tenantId 是其所属租户
|
|
||||||
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
if (!Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||||
dto.setTenantId(loginUser.getTenantId());
|
dto.setTenantId(loginUser.getTenantId());
|
||||||
} else {
|
} else if (dto.getTenantId() == null) {
|
||||||
// 平台管理员:如果 DTO 没传 tenantId,默认设为 0
|
dto.setTenantId(0L);
|
||||||
if (dto.getTenantId() == null) dto.setTenantId(0L);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 普通模板
|
|
||||||
dto.setTenantId(loginUser.getTenantId());
|
dto.setTenantId(loginUser.getTenantId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,24 +50,22 @@ public class PromptTemplateController {
|
||||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
PromptTemplate existing = promptTemplateService.getById(dto.getId());
|
PromptTemplate existing = promptTemplateService.getById(dto.getId());
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
return ApiResponse.error("模板不存在");
|
return ApiResponse.error("Template not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 核心权限判定
|
|
||||||
boolean canModify = false;
|
boolean canModify = false;
|
||||||
|
if (Integer.valueOf(0).equals(existing.getIsSystem())) {
|
||||||
|
canModify = existing.getCreatorId().equals(loginUser.getUserId());
|
||||||
|
} else {
|
||||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||||
// 平台管理员只能修改平台级模板 (tenantId = 0)
|
|
||||||
canModify = existing.getTenantId() == 0L;
|
canModify = existing.getTenantId() == 0L;
|
||||||
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
// 租户管理员可以修改本租户的所有模板 (租户预置 + 个人模板)
|
|
||||||
canModify = existing.getTenantId().equals(loginUser.getTenantId());
|
canModify = existing.getTenantId().equals(loginUser.getTenantId());
|
||||||
} else {
|
}
|
||||||
// 普通用户仅限自己的个人模板
|
|
||||||
canModify = existing.getCreatorId().equals(loginUser.getUserId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canModify) {
|
if (!canModify) {
|
||||||
return ApiResponse.error("无权修改此模板");
|
return ApiResponse.error("No permission to modify this template");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiResponse.ok(promptTemplateService.updateTemplate(dto));
|
return ApiResponse.ok(promptTemplateService.updateTemplate(dto));
|
||||||
|
|
@ -85,25 +76,38 @@ public class PromptTemplateController {
|
||||||
public ApiResponse<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
public ApiResponse<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
PromptTemplate existing = promptTemplateService.getById(id);
|
PromptTemplate existing = promptTemplateService.getById(id);
|
||||||
if (existing == null) return ApiResponse.error("模板不存在");
|
if (existing == null) {
|
||||||
|
return ApiResponse.error("Template not found");
|
||||||
boolean canModify = false;
|
|
||||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
|
||||||
canModify = existing.getTenantId() == 0L;
|
|
||||||
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
|
||||||
canModify = existing.getTenantId().equals(loginUser.getTenantId());
|
|
||||||
} else {
|
|
||||||
canModify = existing.getCreatorId().equals(loginUser.getUserId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canModify) {
|
boolean canGlobalModify = false;
|
||||||
return ApiResponse.error("无权修改此模板");
|
if (Integer.valueOf(1).equals(existing.getIsSystem())) {
|
||||||
|
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) && Long.valueOf(0L).equals(existing.getTenantId())) {
|
||||||
|
canGlobalModify = true;
|
||||||
|
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin()) && existing.getTenantId().equals(loginUser.getTenantId())) {
|
||||||
|
canGlobalModify = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (canGlobalModify) {
|
||||||
existing.setStatus(status);
|
existing.setStatus(status);
|
||||||
return ApiResponse.ok(promptTemplateService.updateById(existing));
|
return ApiResponse.ok(promptTemplateService.updateById(existing));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean success = promptTemplateService.updateUserTemplateStatus(
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
return ApiResponse.error("Template not found or no permission");
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||||
|
|
@ -114,16 +118,18 @@ public class PromptTemplateController {
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean canModify = false;
|
boolean canModify = false;
|
||||||
|
if (Integer.valueOf(0).equals(existing.getIsSystem())) {
|
||||||
|
canModify = existing.getCreatorId().equals(loginUser.getUserId());
|
||||||
|
} else {
|
||||||
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
|
||||||
canModify = existing.getTenantId() == 0L;
|
canModify = existing.getTenantId() == 0L;
|
||||||
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
|
||||||
canModify = existing.getTenantId().equals(loginUser.getTenantId());
|
canModify = existing.getTenantId().equals(loginUser.getTenantId());
|
||||||
} else {
|
}
|
||||||
canModify = existing.getCreatorId().equals(loginUser.getUserId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canModify) {
|
if (!canModify) {
|
||||||
return ApiResponse.error("无权删除此模板");
|
return ApiResponse.error("No permission to delete this template");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiResponse.ok(promptTemplateService.removeById(id));
|
return ApiResponse.ok(promptTemplateService.removeById(id));
|
||||||
|
|
@ -139,8 +145,13 @@ public class PromptTemplateController {
|
||||||
|
|
||||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
return ApiResponse.ok(promptTemplateService.pageTemplates(
|
return ApiResponse.ok(promptTemplateService.pageTemplates(
|
||||||
current, size, name, category,
|
current,
|
||||||
loginUser.getTenantId(), loginUser.getUserId(),
|
size,
|
||||||
loginUser.getIsPlatformAdmin(), loginUser.getIsTenantAdmin()));
|
name,
|
||||||
|
category,
|
||||||
|
loginUser.getTenantId(),
|
||||||
|
loginUser.getUserId(),
|
||||||
|
loginUser.getIsPlatformAdmin(),
|
||||||
|
loginUser.getIsTenantAdmin()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ public class MeetingVO {
|
||||||
|
|
||||||
private String participants;
|
private String participants;
|
||||||
private String tags;
|
private String tags;
|
||||||
private Integer useSpkId;
|
|
||||||
private String audioUrl;
|
private String audioUrl;
|
||||||
private String summaryContent;
|
private String summaryContent;
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ public class AiTask {
|
||||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
private Map<String, Object> responseData;
|
private Map<String, Object> responseData;
|
||||||
|
|
||||||
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
|
private Map<String, Object> taskConfig;
|
||||||
|
|
||||||
|
private String resultFilePath;
|
||||||
|
|
||||||
private String errorMsg;
|
private String errorMsg;
|
||||||
|
|
||||||
private LocalDateTime startedAt;
|
private LocalDateTime startedAt;
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,5 @@ public class Meeting extends BaseEntity {
|
||||||
|
|
||||||
private String creatorName;
|
private String creatorName;
|
||||||
|
|
||||||
private Long asrModelId;
|
private Long latestSummaryTaskId;
|
||||||
|
|
||||||
private Long summaryModelId;
|
|
||||||
|
|
||||||
private String promptContent;
|
|
||||||
|
|
||||||
private Integer useSpkId;
|
|
||||||
|
|
||||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
|
||||||
private List<String> hotWords;
|
|
||||||
|
|
||||||
private String summaryContent;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.imeeting.dto.biz.PromptTemplateDTO;
|
import com.imeeting.dto.biz.PromptTemplateDTO;
|
||||||
import com.imeeting.dto.biz.PromptTemplateVO;
|
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||||
import com.imeeting.entity.biz.PromptTemplate;
|
import com.imeeting.entity.biz.PromptTemplate;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
||||||
import com.imeeting.common.PageResult;
|
import com.imeeting.common.PageResult;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -14,4 +13,6 @@ public interface PromptTemplateService extends IService<PromptTemplate> {
|
||||||
PromptTemplateVO updateTemplate(PromptTemplateDTO dto);
|
PromptTemplateVO updateTemplate(PromptTemplateDTO dto);
|
||||||
PageResult<List<PromptTemplateVO>> pageTemplates(Integer current, Integer size, String name, String category,
|
PageResult<List<PromptTemplateVO>> pageTemplates(Integer current, Integer size, String name, String category,
|
||||||
Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
||||||
|
boolean updateUserTemplateStatus(Long templateId, Integer status, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
||||||
|
boolean isTemplateEnabledForUser(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
@ -54,6 +57,9 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
@Value("${app.server-base-url}")
|
@Value("${app.server-base-url}")
|
||||||
private String serverBaseUrl;
|
private String serverBaseUrl;
|
||||||
|
|
||||||
|
@Value("${app.upload-path}")
|
||||||
|
private String uploadPath;
|
||||||
|
|
||||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -72,11 +78,34 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
Meeting meeting = meetingMapper.selectById(meetingId);
|
Meeting meeting = meetingMapper.selectById(meetingId);
|
||||||
if (meeting == null) return;
|
if (meeting == null) return;
|
||||||
|
|
||||||
// 1. 执行 ASR 识别 (含接管逻辑)
|
AiTask asrTask = this.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
String asrText = processAsrTask(meeting);
|
.eq(AiTask::getMeetingId, meetingId)
|
||||||
|
.eq(AiTask::getTaskType, "ASR")
|
||||||
|
.orderByDesc(AiTask::getId)
|
||||||
|
.last("limit 1"));
|
||||||
|
|
||||||
// 2. 执行 LLM 总结
|
String asrText = "";
|
||||||
processSummaryTask(meeting, asrText);
|
if (asrTask != null && asrTask.getStatus() == 0) {
|
||||||
|
asrText = processAsrTask(meeting, asrTask);
|
||||||
|
} else {
|
||||||
|
List<MeetingTranscript> transcripts = transcriptMapper.selectList(new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
|
.eq(MeetingTranscript::getMeetingId, meetingId)
|
||||||
|
.orderByAsc(MeetingTranscript::getStartTime));
|
||||||
|
asrText = transcripts.stream()
|
||||||
|
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
AiTask sumTask = this.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getMeetingId, meetingId)
|
||||||
|
.eq(AiTask::getTaskType, "SUMMARY")
|
||||||
|
.orderByDesc(AiTask::getId)
|
||||||
|
.last("limit 1"));
|
||||||
|
if (sumTask != null && sumTask.getStatus() == 0) {
|
||||||
|
processSummaryTask(meeting, asrText, sumTask);
|
||||||
|
} else if (meeting.getStatus() != 3) {
|
||||||
|
updateMeetingStatus(meetingId, 3);
|
||||||
|
}
|
||||||
|
|
||||||
redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId));
|
redisTemplate.delete(RedisKeys.meetingProgressKey(meetingId));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
@ -106,7 +135,14 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
||||||
.collect(Collectors.joining("\n"));
|
.collect(Collectors.joining("\n"));
|
||||||
|
|
||||||
processSummaryTask(meeting, asrText);
|
AiTask sumTask = this.getOne(new LambdaQueryWrapper<AiTask>()
|
||||||
|
.eq(AiTask::getMeetingId, meetingId)
|
||||||
|
.eq(AiTask::getTaskType, "SUMMARY")
|
||||||
|
.orderByDesc(AiTask::getId)
|
||||||
|
.last("limit 1"));
|
||||||
|
if (sumTask != null && sumTask.getStatus() == 0) {
|
||||||
|
processSummaryTask(meeting, asrText, sumTask);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Re-summary failed for meeting {}", meetingId, e);
|
log.error("Re-summary failed for meeting {}", meetingId, e);
|
||||||
updateMeetingStatus(meetingId, 4);
|
updateMeetingStatus(meetingId, 4);
|
||||||
|
|
@ -114,49 +150,24 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String processAsrTask(Meeting meeting) throws Exception {
|
private String processAsrTask(Meeting meeting, AiTask taskRecord) throws Exception {
|
||||||
updateMeetingStatus(meeting.getId(), 1);
|
updateMeetingStatus(meeting.getId(), 1);
|
||||||
|
|
||||||
AiModel asrModel = aiModelService.getById(meeting.getAsrModelId());
|
taskRecord.setStatus(1);
|
||||||
|
taskRecord.setStartedAt(LocalDateTime.now());
|
||||||
|
this.updateById(taskRecord);
|
||||||
|
|
||||||
|
Long asrModelId = Long.valueOf(taskRecord.getTaskConfig().get("asrModelId").toString());
|
||||||
|
AiModel asrModel = aiModelService.getById(asrModelId);
|
||||||
if (asrModel == null) throw new RuntimeException("ASR模型配置不存在");
|
if (asrModel == null) throw new RuntimeException("ASR模型配置不存在");
|
||||||
|
|
||||||
String submitUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/recognition" : asrModel.getBaseUrl() + "/api/tasks/recognition";
|
String submitUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/recognition" : asrModel.getBaseUrl() + "/api/tasks/recognition";
|
||||||
String taskId = null;
|
String taskId = null;
|
||||||
AiTask taskRecord = null;
|
|
||||||
|
|
||||||
// --- 核心:接管逻辑 ---
|
|
||||||
List<AiTask> existingTasks = this.list(new LambdaQueryWrapper<AiTask>()
|
|
||||||
.eq(AiTask::getMeetingId, meeting.getId())
|
|
||||||
.eq(AiTask::getTaskType, "ASR")
|
|
||||||
.orderByDesc(AiTask::getStartedAt));
|
|
||||||
|
|
||||||
if (!existingTasks.isEmpty()) {
|
|
||||||
AiTask lastTask = existingTasks.get(0);
|
|
||||||
if (lastTask.getResponseData() != null && lastTask.getResponseData().get("task_id") != null) {
|
|
||||||
String potentialTaskId = lastTask.getResponseData().get("task_id").toString();
|
|
||||||
log.info("Attempting to resume ASR task {} for meeting {}", potentialTaskId, meeting.getId());
|
|
||||||
|
|
||||||
// 验证旧 taskId 是否依然有效
|
|
||||||
String checkUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + potentialTaskId : asrModel.getBaseUrl() + "/api/tasks/" + potentialTaskId;
|
|
||||||
try {
|
|
||||||
String checkResp = get(checkUrl);
|
|
||||||
JsonNode node = objectMapper.readTree(checkResp);
|
|
||||||
if (node.path("code").asInt() == 200) {
|
|
||||||
taskId = potentialTaskId;
|
|
||||||
taskRecord = lastTask;
|
|
||||||
log.info("Successfully resumed ASR task {}", taskId);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Old task ID {} is invalid, will start a new one", potentialTaskId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有可接管的任务,则发起新任务
|
|
||||||
if (taskId == null) {
|
|
||||||
updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0);
|
updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0);
|
||||||
Map<String, Object> req = buildAsrRequest(meeting);
|
Map<String, Object> req = buildAsrRequest(meeting, taskRecord);
|
||||||
taskRecord = createAiTask(meeting.getId(), "ASR", req);
|
taskRecord.setRequestData(req);
|
||||||
|
this.updateById(taskRecord);
|
||||||
|
|
||||||
String respBody = postJson(submitUrl, req);
|
String respBody = postJson(submitUrl, req);
|
||||||
JsonNode submitNode = objectMapper.readTree(respBody);
|
JsonNode submitNode = objectMapper.readTree(respBody);
|
||||||
|
|
@ -167,7 +178,6 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
taskId = submitNode.path("data").path("task_id").asText();
|
taskId = submitNode.path("data").path("task_id").asText();
|
||||||
taskRecord.setResponseData(Map.of("task_id", taskId));
|
taskRecord.setResponseData(Map.of("task_id", taskId));
|
||||||
this.updateById(taskRecord);
|
this.updateById(taskRecord);
|
||||||
}
|
|
||||||
|
|
||||||
String queryUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + taskId : asrModel.getBaseUrl() + "/api/tasks/" + taskId;
|
String queryUrl = asrModel.getBaseUrl().endsWith("/") ? asrModel.getBaseUrl() + "api/tasks/" + taskId : asrModel.getBaseUrl() + "/api/tasks/" + taskId;
|
||||||
|
|
||||||
|
|
@ -210,7 +220,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
return saveTranscripts(meeting, resultNode);
|
return saveTranscripts(meeting, resultNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildAsrRequest(Meeting meeting) {
|
private Map<String, Object> buildAsrRequest(Meeting meeting, AiTask taskRecord) {
|
||||||
Map<String, Object> req = new HashMap<>();
|
Map<String, Object> req = new HashMap<>();
|
||||||
String rawAudioUrl = meeting.getAudioUrl();
|
String rawAudioUrl = meeting.getAudioUrl();
|
||||||
String encodedAudioUrl = Arrays.stream(rawAudioUrl.split("/"))
|
String encodedAudioUrl = Arrays.stream(rawAudioUrl.split("/"))
|
||||||
|
|
@ -220,17 +230,24 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
})
|
})
|
||||||
.collect(Collectors.joining("/"));
|
.collect(Collectors.joining("/"));
|
||||||
req.put("file_path", serverBaseUrl + (encodedAudioUrl.startsWith("/") ? "" : "/") + encodedAudioUrl);
|
req.put("file_path", serverBaseUrl + (encodedAudioUrl.startsWith("/") ? "" : "/") + encodedAudioUrl);
|
||||||
req.put("use_spk_id", meeting.getUseSpkId() != null && meeting.getUseSpkId() == 1);
|
|
||||||
|
Object useSpkObj = taskRecord.getTaskConfig().get("useSpkId");
|
||||||
|
boolean useSpk = useSpkObj != null && useSpkObj.toString().equals("1");
|
||||||
|
req.put("use_spk_id", useSpk);
|
||||||
|
|
||||||
List<Map<String, Object>> hotwords = new ArrayList<>();
|
List<Map<String, Object>> hotwords = new ArrayList<>();
|
||||||
if (meeting.getHotWords() != null && !meeting.getHotWords().isEmpty()) {
|
Object hotWordsObj = taskRecord.getTaskConfig().get("hotWords");
|
||||||
|
if (hotWordsObj instanceof List) {
|
||||||
|
List<String> words = (List<String>) hotWordsObj;
|
||||||
|
if (!words.isEmpty()) {
|
||||||
List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||||
.eq(HotWord::getTenantId, meeting.getTenantId()).in(HotWord::getWord, meeting.getHotWords()));
|
.eq(HotWord::getTenantId, meeting.getTenantId()).in(HotWord::getWord, words));
|
||||||
Map<String, Integer> weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1));
|
Map<String, Integer> weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1));
|
||||||
for (String w : meeting.getHotWords()) {
|
for (String w : words) {
|
||||||
hotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0));
|
hotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
req.put("hotwords", hotwords);
|
req.put("hotwords", hotwords);
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
@ -272,22 +289,32 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processSummaryTask(Meeting meeting, String asrText) throws Exception {
|
private void processSummaryTask(Meeting meeting, String asrText, AiTask taskRecord) throws Exception {
|
||||||
updateMeetingStatus(meeting.getId(), 2);
|
updateMeetingStatus(meeting.getId(), 2);
|
||||||
updateProgress(meeting.getId(), 90, "正在生成智能总结纪要...", 0);
|
updateProgress(meeting.getId(), 90, "正在生成智能总结纪要...", 0);
|
||||||
|
|
||||||
AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId());
|
taskRecord.setStatus(1);
|
||||||
|
taskRecord.setStartedAt(LocalDateTime.now());
|
||||||
|
this.updateById(taskRecord);
|
||||||
|
|
||||||
|
Long summaryModelId = Long.valueOf(taskRecord.getTaskConfig().get("summaryModelId").toString());
|
||||||
|
AiModel llmModel = aiModelService.getById(summaryModelId);
|
||||||
if (llmModel == null) return;
|
if (llmModel == null) return;
|
||||||
|
|
||||||
|
String promptContent = taskRecord.getTaskConfig().get("promptContent") != null ?
|
||||||
|
taskRecord.getTaskConfig().get("promptContent").toString() : "";
|
||||||
|
|
||||||
Map<String, Object> req = new HashMap<>();
|
Map<String, Object> req = new HashMap<>();
|
||||||
req.put("model", llmModel.getModelCode());
|
req.put("model", llmModel.getModelCode());
|
||||||
req.put("temperature", llmModel.getTemperature());
|
req.put("temperature", llmModel.getTemperature());
|
||||||
req.put("messages", List.of(
|
req.put("messages", List.of(
|
||||||
Map.of("role", "system", "content", meeting.getPromptContent()),
|
Map.of("role", "system", "content", promptContent),
|
||||||
Map.of("role", "user", "content", "请总结以下会议内容:\n" + asrText)
|
Map.of("role", "user", "content", "请总结以下会议内容:\n" + asrText)
|
||||||
));
|
));
|
||||||
|
|
||||||
AiTask taskRecord = createAiTask(meeting.getId(), "SUMMARY", req);
|
taskRecord.setRequestData(req);
|
||||||
|
this.updateById(taskRecord);
|
||||||
|
|
||||||
String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
|
String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
|
@ -302,10 +329,28 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
||||||
|
|
||||||
if (response.statusCode() == 200 && respNode.has("choices")) {
|
if (response.statusCode() == 200 && respNode.has("choices")) {
|
||||||
String content = respNode.path("choices").path(0).path("message").path("content").asText();
|
String content = respNode.path("choices").path(0).path("message").path("content").asText();
|
||||||
meeting.setSummaryContent(content);
|
|
||||||
|
// Save to File
|
||||||
|
String timestamp = java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||||
|
String fileName = "summary_" + timestamp + ".md";
|
||||||
|
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||||
|
Path targetDir = Paths.get(basePath, "meetings", String.valueOf(meeting.getId()), "summaries");
|
||||||
|
Files.createDirectories(targetDir);
|
||||||
|
Path filePath = targetDir.resolve(fileName);
|
||||||
|
|
||||||
|
String frontMatter = "---\n" +
|
||||||
|
"生成时间: " + LocalDateTime.now() + "\n" +
|
||||||
|
"使用模型: " + llmModel.getModelName() + "\n" +
|
||||||
|
"---\n\n";
|
||||||
|
Files.writeString(filePath, frontMatter + content, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
taskRecord.setResultFilePath("meetings/" + meeting.getId() + "/summaries/" + fileName);
|
||||||
|
updateAiTaskSuccess(taskRecord, respNode);
|
||||||
|
|
||||||
|
meeting.setLatestSummaryTaskId(taskRecord.getId());
|
||||||
meeting.setStatus(3);
|
meeting.setStatus(3);
|
||||||
meetingMapper.updateById(meeting);
|
meetingMapper.updateById(meeting);
|
||||||
updateAiTaskSuccess(taskRecord, respNode);
|
|
||||||
updateProgress(meeting.getId(), 100, "全流程分析完成", 0);
|
updateProgress(meeting.getId(), 100, "全流程分析完成", 0);
|
||||||
} else {
|
} else {
|
||||||
updateAiTaskFail(taskRecord, "LLM Summary failed: " + response.body());
|
updateAiTaskFail(taskRecord, "LLM Summary failed: " + response.body());
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.imeeting.dto.biz.MeetingVO;
|
||||||
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.entity.biz.AiModel;
|
import com.imeeting.entity.biz.AiModel;
|
||||||
|
import com.imeeting.entity.biz.AiTask;
|
||||||
import com.imeeting.entity.biz.PromptTemplate;
|
import com.imeeting.entity.biz.PromptTemplate;
|
||||||
import com.imeeting.entity.biz.MeetingTranscript;
|
import com.imeeting.entity.biz.MeetingTranscript;
|
||||||
import com.imeeting.entity.biz.HotWord;
|
import com.imeeting.entity.biz.HotWord;
|
||||||
|
|
@ -24,16 +25,27 @@ import com.imeeting.service.biz.PromptTemplateService;
|
||||||
import com.imeeting.service.biz.AiTaskService;
|
import com.imeeting.service.biz.AiTaskService;
|
||||||
import com.imeeting.service.biz.HotWordService;
|
import com.imeeting.service.biz.HotWordService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.FileSystemUtils;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> implements MeetingService {
|
public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> implements MeetingService {
|
||||||
|
|
@ -46,6 +58,9 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
private final SysUserMapper sysUserMapper;
|
private final SysUserMapper sysUserMapper;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
@Value("${app.upload-path}")
|
||||||
|
private String uploadPath;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public MeetingVO createMeeting(MeetingDTO dto) {
|
public MeetingVO createMeeting(MeetingDTO dto) {
|
||||||
|
|
@ -54,31 +69,80 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
meeting.setMeetingTime(dto.getMeetingTime());
|
meeting.setMeetingTime(dto.getMeetingTime());
|
||||||
meeting.setParticipants(dto.getParticipants());
|
meeting.setParticipants(dto.getParticipants());
|
||||||
meeting.setTags(dto.getTags());
|
meeting.setTags(dto.getTags());
|
||||||
meeting.setAudioUrl(dto.getAudioUrl());
|
|
||||||
meeting.setAsrModelId(dto.getAsrModelId());
|
|
||||||
meeting.setSummaryModelId(dto.getSummaryModelId());
|
|
||||||
meeting.setUseSpkId(dto.getUseSpkId() != null ? dto.getUseSpkId() : 1);
|
|
||||||
meeting.setCreatorId(dto.getCreatorId());
|
meeting.setCreatorId(dto.getCreatorId());
|
||||||
meeting.setCreatorName(dto.getCreatorName());
|
meeting.setCreatorName(dto.getCreatorName());
|
||||||
|
meeting.setTenantId(dto.getTenantId() != null ? dto.getTenantId() : 0L);
|
||||||
|
meeting.setStatus(0);
|
||||||
|
|
||||||
if (dto.getPromptId() != null) {
|
this.save(meeting); // Save to get meeting ID
|
||||||
PromptTemplate template = promptTemplateService.getById(dto.getPromptId());
|
|
||||||
if (template != null) {
|
// File moving logic
|
||||||
meeting.setPromptContent(template.getPromptContent());
|
String audioUrl = dto.getAudioUrl();
|
||||||
|
if (audioUrl != null && audioUrl.startsWith("/api/static/audio/")) {
|
||||||
|
try {
|
||||||
|
String fileName = audioUrl.substring(audioUrl.lastIndexOf("/") + 1);
|
||||||
|
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||||
|
Path sourcePath = Paths.get(basePath, "audio", fileName);
|
||||||
|
|
||||||
|
if (Files.exists(sourcePath)) {
|
||||||
|
String ext = "";
|
||||||
|
int dotIdx = fileName.lastIndexOf('.');
|
||||||
|
if (dotIdx > 0) {
|
||||||
|
ext = fileName.substring(dotIdx);
|
||||||
|
}
|
||||||
|
String targetDir = basePath + "meetings/" + meeting.getId();
|
||||||
|
Files.createDirectories(Paths.get(targetDir));
|
||||||
|
Path targetPath = Paths.get(targetDir, "source_audio" + ext);
|
||||||
|
Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
audioUrl = "/api/static/meetings/" + meeting.getId() + "/source_audio" + ext;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to move audio file for meeting {}", meeting.getId(), e);
|
||||||
|
throw new RuntimeException("文件处理失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
meeting.setAudioUrl(audioUrl);
|
||||||
|
this.updateById(meeting);
|
||||||
|
|
||||||
|
// ASR Task
|
||||||
|
AiTask asrTask = new AiTask();
|
||||||
|
asrTask.setMeetingId(meeting.getId());
|
||||||
|
asrTask.setTaskType("ASR");
|
||||||
|
asrTask.setStatus(0);
|
||||||
|
|
||||||
|
Map<String, Object> asrConfig = new HashMap<>();
|
||||||
|
asrConfig.put("asrModelId", dto.getAsrModelId());
|
||||||
|
asrConfig.put("useSpkId", dto.getUseSpkId() != null ? dto.getUseSpkId() : 1);
|
||||||
|
|
||||||
List<String> finalHotWords = dto.getHotWords();
|
List<String> finalHotWords = dto.getHotWords();
|
||||||
if (finalHotWords == null || finalHotWords.isEmpty()) {
|
if (finalHotWords == null || finalHotWords.isEmpty()) {
|
||||||
finalHotWords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
finalHotWords = hotWordService.list(new LambdaQueryWrapper<HotWord>()
|
||||||
.eq(HotWord::getTenantId, dto.getTenantId())
|
.eq(HotWord::getTenantId, meeting.getTenantId())
|
||||||
.eq(HotWord::getStatus, 1))
|
.eq(HotWord::getStatus, 1))
|
||||||
.stream().map(HotWord::getWord).collect(Collectors.toList());
|
.stream().map(HotWord::getWord).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
meeting.setHotWords(finalHotWords);
|
asrConfig.put("hotWords", finalHotWords);
|
||||||
|
asrTask.setTaskConfig(asrConfig);
|
||||||
|
aiTaskService.save(asrTask);
|
||||||
|
|
||||||
|
// SUMMARY Task
|
||||||
|
AiTask sumTask = new AiTask();
|
||||||
|
sumTask.setMeetingId(meeting.getId());
|
||||||
|
sumTask.setTaskType("SUMMARY");
|
||||||
|
sumTask.setStatus(0);
|
||||||
|
|
||||||
|
Map<String, Object> sumConfig = new HashMap<>();
|
||||||
|
sumConfig.put("summaryModelId", dto.getSummaryModelId());
|
||||||
|
if (dto.getPromptId() != null) {
|
||||||
|
PromptTemplate template = promptTemplateService.getById(dto.getPromptId());
|
||||||
|
if (template != null) {
|
||||||
|
sumConfig.put("promptContent", template.getPromptContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sumTask.setTaskConfig(sumConfig);
|
||||||
|
aiTaskService.save(sumTask);
|
||||||
|
|
||||||
meeting.setStatus(0);
|
|
||||||
this.save(meeting);
|
|
||||||
eventPublisher.publishEvent(new MeetingCreatedEvent(meeting.getId()));
|
eventPublisher.publishEvent(new MeetingCreatedEvent(meeting.getId()));
|
||||||
return toVO(meeting);
|
return toVO(meeting);
|
||||||
}
|
}
|
||||||
|
|
@ -165,13 +229,21 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
Meeting meeting = this.getById(meetingId);
|
Meeting meeting = this.getById(meetingId);
|
||||||
if (meeting == null) throw new RuntimeException("Meeting not found");
|
if (meeting == null) throw new RuntimeException("Meeting not found");
|
||||||
|
|
||||||
meeting.setSummaryModelId(summaryModelId);
|
AiTask sumTask = new AiTask();
|
||||||
|
sumTask.setMeetingId(meetingId);
|
||||||
|
sumTask.setTaskType("SUMMARY");
|
||||||
|
sumTask.setStatus(0);
|
||||||
|
|
||||||
|
Map<String, Object> sumConfig = new HashMap<>();
|
||||||
|
sumConfig.put("summaryModelId", summaryModelId);
|
||||||
if (promptId != null) {
|
if (promptId != null) {
|
||||||
PromptTemplate template = promptTemplateService.getById(promptId);
|
PromptTemplate template = promptTemplateService.getById(promptId);
|
||||||
if (template != null) {
|
if (template != null) {
|
||||||
meeting.setPromptContent(template.getPromptContent());
|
sumConfig.put("promptContent", template.getPromptContent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sumTask.setTaskConfig(sumConfig);
|
||||||
|
aiTaskService.save(sumTask);
|
||||||
|
|
||||||
meeting.setStatus(2);
|
meeting.setStatus(2);
|
||||||
this.updateById(meeting);
|
this.updateById(meeting);
|
||||||
|
|
@ -216,10 +288,8 @@ public class MeetingServiceImpl extends ServiceImpl<MeetingMapper, Meeting> impl
|
||||||
vo.setTitle(meeting.getTitle());
|
vo.setTitle(meeting.getTitle());
|
||||||
vo.setMeetingTime(meeting.getMeetingTime());
|
vo.setMeetingTime(meeting.getMeetingTime());
|
||||||
vo.setTags(meeting.getTags());
|
vo.setTags(meeting.getTags());
|
||||||
vo.setUseSpkId(meeting.getUseSpkId());
|
|
||||||
vo.setAudioUrl(meeting.getAudioUrl());
|
vo.setAudioUrl(meeting.getAudioUrl());
|
||||||
vo.setStatus(meeting.getStatus());
|
vo.setStatus(meeting.getStatus());
|
||||||
vo.setSummaryContent(meeting.getSummaryContent());
|
|
||||||
vo.setCreatedAt(meeting.getCreatedAt());
|
vo.setCreatedAt(meeting.getCreatedAt());
|
||||||
|
|
||||||
if (meeting.getParticipants() != null && !meeting.getParticipants().isEmpty()) {
|
if (meeting.getParticipants() != null && !meeting.getParticipants().isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,25 @@ import com.imeeting.common.PageResult;
|
||||||
import com.imeeting.dto.biz.PromptTemplateDTO;
|
import com.imeeting.dto.biz.PromptTemplateDTO;
|
||||||
import com.imeeting.dto.biz.PromptTemplateVO;
|
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||||
import com.imeeting.entity.biz.PromptTemplate;
|
import com.imeeting.entity.biz.PromptTemplate;
|
||||||
|
import com.imeeting.entity.biz.PromptTemplateUserConfig;
|
||||||
import com.imeeting.mapper.biz.PromptTemplateMapper;
|
import com.imeeting.mapper.biz.PromptTemplateMapper;
|
||||||
|
import com.imeeting.mapper.biz.PromptTemplateUserConfigMapper;
|
||||||
import com.imeeting.service.biz.PromptTemplateService;
|
import com.imeeting.service.biz.PromptTemplateService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper, PromptTemplate> implements PromptTemplateService {
|
public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper, PromptTemplate> implements PromptTemplateService {
|
||||||
|
|
||||||
|
private final PromptTemplateUserConfigMapper userConfigMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public PromptTemplateVO saveTemplate(PromptTemplateDTO dto, Long userId, Long tenantId) {
|
public PromptTemplateVO saveTemplate(PromptTemplateDTO dto, Long userId, Long tenantId) {
|
||||||
|
|
@ -26,7 +34,6 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
|
|
||||||
entity.setCreatorId(userId);
|
entity.setCreatorId(userId);
|
||||||
|
|
||||||
// 逻辑纠偏:如果是平台管理员设置了 tenantId 为 0,则设为 0;否则强制设为用户当前租户
|
|
||||||
if (dto.getTenantId() != null && dto.getTenantId() == 0L) {
|
if (dto.getTenantId() != null && dto.getTenantId() == 0L) {
|
||||||
entity.setTenantId(0L);
|
entity.setTenantId(0L);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -35,10 +42,9 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
|
|
||||||
entity.setUsageCount(0);
|
entity.setUsageCount(0);
|
||||||
this.save(entity);
|
this.save(entity);
|
||||||
return toVO(entity);
|
return toVO(entity, entity.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public PromptTemplateVO updateTemplate(PromptTemplateDTO dto) {
|
public PromptTemplateVO updateTemplate(PromptTemplateDTO dto) {
|
||||||
|
|
@ -48,47 +54,26 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
}
|
}
|
||||||
copyProperties(dto, entity);
|
copyProperties(dto, entity);
|
||||||
this.updateById(entity);
|
this.updateById(entity);
|
||||||
return toVO(entity);
|
return toVO(entity, entity.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<List<PromptTemplateVO>> pageTemplates(Integer current, Integer size, String name, String category,
|
public PageResult<List<PromptTemplateVO>> pageTemplates(Integer current, Integer size, String name, String category,
|
||||||
Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||||
|
|
||||||
LambdaQueryWrapper<PromptTemplate> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<PromptTemplate> wrapper = buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin);
|
||||||
|
|
||||||
// 核心过滤逻辑:分层可见性 (精细化处理)
|
|
||||||
if (Boolean.TRUE.equals(isPlatformAdmin)) {
|
|
||||||
// 平台管理员:可以看到所有平台级模板 (tenantId=0)
|
|
||||||
wrapper.eq(PromptTemplate::getTenantId, 0L);
|
|
||||||
} else if (Boolean.TRUE.equals(isTenantAdmin)) {
|
|
||||||
// 租户管理员:
|
|
||||||
// 1. 本租户所有模板 (tenantId=currentTenantId)
|
|
||||||
// 2. 平台预置 (tenantId=0 & isSystem=1)
|
|
||||||
wrapper.and(w -> w
|
|
||||||
.eq(PromptTemplate::getTenantId, tenantId)
|
|
||||||
.or(sw -> sw.eq(PromptTemplate::getTenantId, 0L).eq(PromptTemplate::getIsSystem, 1))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// 普通个人用户:
|
|
||||||
// 1. 个人创建 (creatorId=currentUserId)
|
|
||||||
// 2. 平台预置 (tenantId=0 & isSystem=1)
|
|
||||||
// 3. 租户预置 (tenantId=currentTenantId & isSystem=1)
|
|
||||||
wrapper.and(w -> w
|
|
||||||
.eq(PromptTemplate::getCreatorId, userId)
|
|
||||||
.or(sw -> sw.eq(PromptTemplate::getTenantId, 0L).eq(PromptTemplate::getIsSystem, 1))
|
|
||||||
.or(sw -> sw.eq(PromptTemplate::getTenantId, tenantId).eq(PromptTemplate::getIsSystem, 1))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通用过滤条件
|
|
||||||
wrapper.like(name != null && !name.isEmpty(), PromptTemplate::getTemplateName, name)
|
wrapper.like(name != null && !name.isEmpty(), PromptTemplate::getTemplateName, name)
|
||||||
.eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category)
|
.eq(category != null && !category.isEmpty(), PromptTemplate::getCategory, category)
|
||||||
.orderByDesc(PromptTemplate::getIsSystem)
|
.orderByDesc(PromptTemplate::getIsSystem)
|
||||||
.orderByDesc(PromptTemplate::getCreatedAt);
|
.orderByDesc(PromptTemplate::getCreatedAt);
|
||||||
|
|
||||||
Page<PromptTemplate> page = this.page(new Page<>(current, size), wrapper);
|
Page<PromptTemplate> page = this.page(new Page<>(current, size), wrapper);
|
||||||
List<PromptTemplateVO> vos = page.getRecords().stream().map(this::toVO).collect(Collectors.toList());
|
List<PromptTemplate> records = page.getRecords();
|
||||||
|
Map<Long, Integer> userStatusMap = queryUserStatusMap(tenantId, userId, records.stream().map(PromptTemplate::getId).collect(Collectors.toList()));
|
||||||
|
List<PromptTemplateVO> vos = records.stream()
|
||||||
|
.map(template -> toVO(template, effectiveStatus(template.getStatus(), userStatusMap.get(template.getId()))))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
PageResult<List<PromptTemplateVO>> result = new PageResult<>();
|
PageResult<List<PromptTemplateVO>> result = new PageResult<>();
|
||||||
result.setTotal(page.getTotal());
|
result.setTotal(page.getTotal());
|
||||||
|
|
@ -96,6 +81,86 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean updateUserTemplateStatus(Long templateId, Integer status, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||||
|
PromptTemplate template = this.getOne(buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin)
|
||||||
|
.eq(PromptTemplate::getId, templateId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (template == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptTemplateUserConfig existing = userConfigMapper.selectOne(new LambdaQueryWrapper<PromptTemplateUserConfig>()
|
||||||
|
.eq(PromptTemplateUserConfig::getTenantId, tenantId)
|
||||||
|
.eq(PromptTemplateUserConfig::getUserId, userId)
|
||||||
|
.eq(PromptTemplateUserConfig::getTemplateId, templateId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
|
||||||
|
if (existing != null) {
|
||||||
|
existing.setStatus(status);
|
||||||
|
return userConfigMapper.updateById(existing) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptTemplateUserConfig entity = new PromptTemplateUserConfig();
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setUserId(userId);
|
||||||
|
entity.setTemplateId(templateId);
|
||||||
|
entity.setStatus(status);
|
||||||
|
return userConfigMapper.insert(entity) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTemplateEnabledForUser(Long templateId, Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||||
|
PromptTemplate template = this.getOne(buildVisibilityWrapper(tenantId, userId, isPlatformAdmin, isTenantAdmin)
|
||||||
|
.eq(PromptTemplate::getId, templateId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (template == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PromptTemplateUserConfig config = userConfigMapper.selectOne(new LambdaQueryWrapper<PromptTemplateUserConfig>()
|
||||||
|
.eq(PromptTemplateUserConfig::getTenantId, tenantId)
|
||||||
|
.eq(PromptTemplateUserConfig::getUserId, userId)
|
||||||
|
.eq(PromptTemplateUserConfig::getTemplateId, templateId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
Integer userStatus = config == null ? null : config.getStatus();
|
||||||
|
return effectiveStatus(template.getStatus(), userStatus) == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LambdaQueryWrapper<PromptTemplate> buildVisibilityWrapper(Long tenantId, Long userId, Boolean isPlatformAdmin, Boolean isTenantAdmin) {
|
||||||
|
LambdaQueryWrapper<PromptTemplate> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.and(w -> w
|
||||||
|
.eq(PromptTemplate::getCreatorId, userId)
|
||||||
|
.or(sw -> sw.eq(PromptTemplate::getTenantId, 0L).eq(PromptTemplate::getIsSystem, 1))
|
||||||
|
.or(sw -> sw.eq(PromptTemplate::getTenantId, tenantId).eq(PromptTemplate::getIsSystem, 1))
|
||||||
|
);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, Integer> queryUserStatusMap(Long tenantId, Long userId, List<Long> templateIds) {
|
||||||
|
if (templateIds == null || templateIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
List<PromptTemplateUserConfig> configs = userConfigMapper.selectList(new LambdaQueryWrapper<PromptTemplateUserConfig>()
|
||||||
|
.eq(PromptTemplateUserConfig::getTenantId, tenantId)
|
||||||
|
.eq(PromptTemplateUserConfig::getUserId, userId)
|
||||||
|
.in(PromptTemplateUserConfig::getTemplateId, templateIds));
|
||||||
|
|
||||||
|
Map<Long, Integer> statusMap = new HashMap<>();
|
||||||
|
for (PromptTemplateUserConfig config : configs) {
|
||||||
|
statusMap.put(config.getTemplateId(), config.getStatus());
|
||||||
|
}
|
||||||
|
return statusMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer effectiveStatus(Integer templateStatus, Integer userStatus) {
|
||||||
|
if (userStatus != null) {
|
||||||
|
return userStatus;
|
||||||
|
}
|
||||||
|
return templateStatus == null ? 1 : templateStatus;
|
||||||
|
}
|
||||||
|
|
||||||
private void copyProperties(PromptTemplateDTO dto, PromptTemplate entity) {
|
private void copyProperties(PromptTemplateDTO dto, PromptTemplate entity) {
|
||||||
entity.setTemplateName(dto.getTemplateName());
|
entity.setTemplateName(dto.getTemplateName());
|
||||||
entity.setCategory(dto.getCategory());
|
entity.setCategory(dto.getCategory());
|
||||||
|
|
@ -107,7 +172,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
entity.setRemark(dto.getRemark());
|
entity.setRemark(dto.getRemark());
|
||||||
}
|
}
|
||||||
|
|
||||||
private PromptTemplateVO toVO(PromptTemplate entity) {
|
private PromptTemplateVO toVO(PromptTemplate entity, Integer status) {
|
||||||
PromptTemplateVO vo = new PromptTemplateVO();
|
PromptTemplateVO vo = new PromptTemplateVO();
|
||||||
vo.setId(entity.getId());
|
vo.setId(entity.getId());
|
||||||
vo.setTenantId(entity.getTenantId());
|
vo.setTenantId(entity.getTenantId());
|
||||||
|
|
@ -118,7 +183,7 @@ public class PromptTemplateServiceImpl extends ServiceImpl<PromptTemplateMapper,
|
||||||
vo.setTags(entity.getTags());
|
vo.setTags(entity.getTags());
|
||||||
vo.setUsageCount(entity.getUsageCount());
|
vo.setUsageCount(entity.getUsageCount());
|
||||||
vo.setPromptContent(entity.getPromptContent());
|
vo.setPromptContent(entity.getPromptContent());
|
||||||
vo.setStatus(entity.getStatus());
|
vo.setStatus(status);
|
||||||
vo.setRemark(entity.getRemark());
|
vo.setRemark(entity.getRemark());
|
||||||
vo.setCreatedAt(entity.getCreatedAt());
|
vo.setCreatedAt(entity.getCreatedAt());
|
||||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||||
|
|
|
||||||
|
|
@ -1,152 +1,152 @@
|
||||||
package com.imeeting.biz;
|
//package com.imeeting.biz;
|
||||||
|
//
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
//import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
//import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
//import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.imeeting.entity.biz.AiModel;
|
//import com.imeeting.entity.biz.AiModel;
|
||||||
import com.imeeting.entity.biz.Meeting;
|
//import com.imeeting.entity.biz.Meeting;
|
||||||
import com.imeeting.entity.biz.MeetingTranscript;
|
//import com.imeeting.entity.biz.MeetingTranscript;
|
||||||
import com.imeeting.mapper.biz.MeetingMapper;
|
//import com.imeeting.mapper.biz.MeetingMapper;
|
||||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
//import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||||
import com.imeeting.service.biz.AiModelService;
|
//import com.imeeting.service.biz.AiModelService;
|
||||||
import org.junit.jupiter.api.Test;
|
//import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
//import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
//import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
//
|
||||||
import java.net.URI;
|
//import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
//import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
//import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
//import java.net.http.HttpResponse;
|
||||||
import java.time.Duration;
|
//import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
//import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
//import java.util.HashMap;
|
||||||
import java.util.List;
|
//import java.util.List;
|
||||||
import java.util.Map;
|
//import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
//import java.util.stream.Collectors;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* 总结模块分步分析测试类 - 真实数据版
|
// * 总结模块分步分析测试类 - 真实数据版
|
||||||
*/
|
// */
|
||||||
@SpringBootTest
|
//@SpringBootTest
|
||||||
public class SummaryTest {
|
//public class SummaryTest {
|
||||||
|
//
|
||||||
@Autowired
|
// @Autowired
|
||||||
private MeetingMapper meetingMapper;
|
// private MeetingMapper meetingMapper;
|
||||||
|
//
|
||||||
@Autowired
|
// @Autowired
|
||||||
private MeetingTranscriptMapper transcriptMapper;
|
// private MeetingTranscriptMapper transcriptMapper;
|
||||||
|
//
|
||||||
@Autowired
|
// @Autowired
|
||||||
private AiModelService aiModelService;
|
// private AiModelService aiModelService;
|
||||||
|
//
|
||||||
@Autowired
|
// @Autowired
|
||||||
private ObjectMapper objectMapper;
|
// private ObjectMapper objectMapper;
|
||||||
|
//
|
||||||
@Test
|
// @Test
|
||||||
public void testManualSummary() throws Exception {
|
// public void testManualSummary() throws Exception {
|
||||||
// --- 步骤 1: 准备测试数据 ---
|
// // --- 步骤 1: 准备测试数据 ---
|
||||||
// 请替换为您数据库中真实的 meetingId
|
// // 请替换为您数据库中真实的 meetingId
|
||||||
Long testMeetingId = 3L;
|
// Long testMeetingId = 3L;
|
||||||
|
//
|
||||||
Meeting meeting = meetingMapper.selectById(testMeetingId);
|
// Meeting meeting = meetingMapper.selectById(testMeetingId);
|
||||||
if (meeting == null) {
|
// if (meeting == null) {
|
||||||
System.out.println("❌ 错误:未找到 ID 为 " + testMeetingId + " 的会议记录");
|
// System.out.println("❌ 错误:未找到 ID 为 " + testMeetingId + " 的会议记录");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取真实的 ASR 转录数据
|
|
||||||
List<MeetingTranscript> transcripts = transcriptMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<MeetingTranscript>()
|
|
||||||
.eq(MeetingTranscript::getMeetingId, testMeetingId)
|
|
||||||
.orderByAsc(MeetingTranscript::getStartTime)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (transcripts.isEmpty()) {
|
|
||||||
System.out.println("⚠️ 警告:该会议暂无转录明细数据 (MeetingTranscript)");
|
|
||||||
// 如果没明细,您可以选择是否继续,或者手动造一点
|
|
||||||
// return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
String realAsrText = transcripts.stream()
|
// // 获取真实的 ASR 转录数据
|
||||||
.map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
// List<MeetingTranscript> transcripts = transcriptMapper.selectList(
|
||||||
.collect(Collectors.joining("\n"));
|
// new LambdaQueryWrapper<MeetingTranscript>()
|
||||||
|
// .eq(MeetingTranscript::getMeetingId, testMeetingId)
|
||||||
System.out.println("\n--- [DEBUG] 提取到的真实转录文本 ---");
|
// .orderByAsc(MeetingTranscript::getStartTime)
|
||||||
System.out.println(realAsrText);
|
// );
|
||||||
|
//
|
||||||
AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId());
|
// if (transcripts.isEmpty()) {
|
||||||
if (llmModel == null) {
|
// System.out.println("⚠️ 警告:该会议暂无转录明细数据 (MeetingTranscript)");
|
||||||
System.out.println("❌ 错误:该会议未绑定总结模型配置");
|
// // 如果没明细,您可以选择是否继续,或者手动造一点
|
||||||
return;
|
// // return;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
System.out.println("\n✅ 基础数据加载成功");
|
// String realAsrText = transcripts.stream()
|
||||||
System.out.println(" 模型名称: " + llmModel.getModelName());
|
// .map(t -> (t.getSpeakerName() != null ? t.getSpeakerName() : t.getSpeakerId()) + ": " + t.getContent())
|
||||||
System.out.println(" 提示词模板快照: " + (meeting.getPromptContent() != null && meeting.getPromptContent().length() > 50
|
// .collect(Collectors.joining("\n"));
|
||||||
? meeting.getPromptContent().substring(0, 50) + "..."
|
//
|
||||||
: meeting.getPromptContent()));
|
// System.out.println("\n--- [DEBUG] 提取到的真实转录文本 ---");
|
||||||
|
// System.out.println(realAsrText);
|
||||||
// --- 步骤 2: 构造请求 Payload ---
|
//
|
||||||
Map<String, Object> req = new HashMap<>();
|
// AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId());
|
||||||
req.put("model", llmModel.getModelCode());
|
// if (llmModel == null) {
|
||||||
req.put("temperature", llmModel.getTemperature());
|
// System.out.println("❌ 错误:该会议未绑定总结模型配置");
|
||||||
|
// return;
|
||||||
List<Map<String, String>> messages = new ArrayList<>();
|
// }
|
||||||
// 系统角色注入 Prompt
|
//
|
||||||
messages.add(Map.of("role", "system", "content", meeting.getPromptContent() != null ? meeting.getPromptContent() : "请总结以下会议内容"));
|
// System.out.println("\n✅ 基础数据加载成功");
|
||||||
// 用户角色注入 真实的 ASR 文本
|
// System.out.println(" 模型名称: " + llmModel.getModelName());
|
||||||
messages.add(Map.of("role", "user", "content", "以下是会议转录全文:\n" + realAsrText));
|
// System.out.println(" 提示词模板快照: " + (meeting.getPromptContent() != null && meeting.getPromptContent().length() > 50
|
||||||
req.put("messages", messages);
|
// ? meeting.getPromptContent().substring(0, 50) + "..."
|
||||||
|
// : meeting.getPromptContent()));
|
||||||
String jsonPayload = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(req);
|
//
|
||||||
System.out.println("\n--- [DEBUG] 发送给 AI 的请求 JSON ---");
|
// // --- 步骤 2: 构造请求 Payload ---
|
||||||
System.out.println(jsonPayload);
|
// Map<String, Object> req = new HashMap<>();
|
||||||
|
// req.put("model", llmModel.getModelCode());
|
||||||
// --- 步骤 3: 发起网络请求 ---
|
// req.put("temperature", llmModel.getTemperature());
|
||||||
String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
|
//
|
||||||
System.out.println("\n--- [DEBUG] 目标 URL: " + url);
|
// List<Map<String, String>> messages = new ArrayList<>();
|
||||||
|
// // 系统角色注入 Prompt
|
||||||
HttpClient client = HttpClient.newBuilder()
|
// messages.add(Map.of("role", "system", "content", meeting.getPromptContent() != null ? meeting.getPromptContent() : "请总结以下会议内容"));
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
// // 用户角色注入 真实的 ASR 文本
|
||||||
.build();
|
// messages.add(Map.of("role", "user", "content", "以下是会议转录全文:\n" + realAsrText));
|
||||||
|
// req.put("messages", messages);
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
//
|
||||||
.uri(URI.create(url))
|
// String jsonPayload = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(req);
|
||||||
.header("Content-Type", "application/json")
|
// System.out.println("\n--- [DEBUG] 发送给 AI 的请求 JSON ---");
|
||||||
.header("Authorization", "Bearer " + llmModel.getApiKey())
|
// System.out.println(jsonPayload);
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
//
|
||||||
.build();
|
// // --- 步骤 3: 发起网络请求 ---
|
||||||
|
// String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
|
||||||
System.out.println("⏳ 正在请求第三方 AI 接口...");
|
// System.out.println("\n--- [DEBUG] 目标 URL: " + url);
|
||||||
try {
|
//
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
// HttpClient client = HttpClient.newBuilder()
|
||||||
|
// .connectTimeout(Duration.ofSeconds(10))
|
||||||
System.out.println("\n--- [DEBUG] 接口返回状态码: " + response.statusCode());
|
// .build();
|
||||||
System.out.println("--- [DEBUG] 接口返回 Raw Body ---");
|
//
|
||||||
System.out.println(response.body());
|
// HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
// .uri(URI.create(url))
|
||||||
// --- 步骤 4: 解析结果 ---
|
// .header("Content-Type", "application/json")
|
||||||
if (response.statusCode() == 200) {
|
// .header("Authorization", "Bearer " + llmModel.getApiKey())
|
||||||
JsonNode respNode = objectMapper.readTree(response.body());
|
// .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
|
||||||
if (respNode.has("choices")) {
|
// .build();
|
||||||
String finalContent = respNode.get("choices").get(0).get("message").get("content").asText();
|
//
|
||||||
System.out.println("\n✨ 总结生成成功!结果如下:");
|
// System.out.println("⏳ 正在请求第三方 AI 接口...");
|
||||||
System.out.println("------------------------------------");
|
// try {
|
||||||
System.out.println(finalContent);
|
// HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
System.out.println("------------------------------------");
|
//
|
||||||
|
// System.out.println("\n--- [DEBUG] 接口返回状态码: " + response.statusCode());
|
||||||
// 可选:将结果更新回数据库以便前端查看
|
// System.out.println("--- [DEBUG] 接口返回 Raw Body ---");
|
||||||
// meeting.setSummaryContent(finalContent);
|
// System.out.println(response.body());
|
||||||
// meetingMapper.updateById(meeting);
|
//
|
||||||
} else {
|
// // --- 步骤 4: 解析结果 ---
|
||||||
System.out.println("❌ 错误:返回结果中不包含 'choices' 字段,请检查厂商 API 适配。");
|
// if (response.statusCode() == 200) {
|
||||||
}
|
// JsonNode respNode = objectMapper.readTree(response.body());
|
||||||
} else {
|
// if (respNode.has("choices")) {
|
||||||
System.out.println("❌ 接口请求失败,请检查 BaseUrl 和 ApiKey 是否正确。");
|
// String finalContent = respNode.get("choices").get(0).get("message").get("content").asText();
|
||||||
}
|
// System.out.println("\n✨ 总结生成成功!结果如下:");
|
||||||
} catch (Exception e) {
|
// System.out.println("------------------------------------");
|
||||||
System.out.println("❌ 网络异常:" + e.getMessage());
|
// System.out.println(finalContent);
|
||||||
e.printStackTrace();
|
// System.out.println("------------------------------------");
|
||||||
}
|
//
|
||||||
}
|
// // 可选:将结果更新回数据库以便前端查看
|
||||||
}
|
// // meeting.setSummaryContent(finalContent);
|
||||||
|
// // meetingMapper.updateById(meeting);
|
||||||
|
// } else {
|
||||||
|
// System.out.println("❌ 错误:返回结果中不包含 'choices' 字段,请检查厂商 API 适配。");
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// System.out.println("❌ 接口请求失败,请检查 BaseUrl 和 ApiKey 是否正确。");
|
||||||
|
// }
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// System.out.println("❌ 网络异常:" + e.getMessage());
|
||||||
|
// e.printStackTrace();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -95,17 +95,19 @@ const PromptTemplates: React.FC = () => {
|
||||||
});
|
});
|
||||||
setPreviewContent(record.promptContent);
|
setPreviewContent(record.promptContent);
|
||||||
} else {
|
} else {
|
||||||
const isPlatformLevel = Number(record.tenantId) === 0;
|
const isPlatformLevel = Number(record.tenantId) === 0 && Number(record.isSystem) === 1;
|
||||||
const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1;
|
const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1;
|
||||||
|
|
||||||
// 权限判定逻辑
|
// 权限判定逻辑
|
||||||
let canEdit = false;
|
let canEdit = false;
|
||||||
if (isPlatformAdmin) {
|
if (Number(record.isSystem) === 0) {
|
||||||
|
canEdit = Number(record.creatorId) === currentUserId;
|
||||||
|
} else if (isPlatformAdmin) {
|
||||||
canEdit = isPlatformLevel;
|
canEdit = isPlatformLevel;
|
||||||
} else if (isTenantAdmin) {
|
} else if (isTenantAdmin) {
|
||||||
canEdit = Number(record.tenantId) === activeTenantId;
|
canEdit = Number(record.tenantId) === activeTenantId;
|
||||||
} else {
|
} else {
|
||||||
canEdit = Number(record.creatorId) === currentUserId;
|
canEdit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
|
|
@ -191,15 +193,18 @@ const PromptTemplates: React.FC = () => {
|
||||||
let canEdit = false;
|
let canEdit = false;
|
||||||
const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1;
|
const currentUserId = userProfile.userId ? Number(userProfile.userId) : -1;
|
||||||
|
|
||||||
if (isPlatformAdmin) {
|
if (isPersonalLevel) {
|
||||||
// 平台管理员管理平台下的所有 (tenantId = 0)
|
// 个人模板仅本人可编辑
|
||||||
|
canEdit = Number(item.creatorId) === currentUserId;
|
||||||
|
} else if (isPlatformAdmin) {
|
||||||
|
// 平台管理员管理平台公开模板 (tenantId = 0)
|
||||||
canEdit = Number(item.tenantId) === 0;
|
canEdit = Number(item.tenantId) === 0;
|
||||||
} else if (isTenantAdmin) {
|
} else if (isTenantAdmin) {
|
||||||
// 租户管理员管理本租户所有模板
|
// 租户管理员管理本租户公开模板
|
||||||
canEdit = Number(item.tenantId) === activeTenantId;
|
canEdit = Number(item.tenantId) === activeTenantId;
|
||||||
} else {
|
} else {
|
||||||
// 普通用户仅限自己的个人模板
|
// 普通用户不可编辑公开模板
|
||||||
canEdit = Number(item.creatorId) === currentUserId;
|
canEdit = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标签颜色与文字
|
// 标签颜色与文字
|
||||||
|
|
@ -236,15 +241,15 @@ const PromptTemplates: React.FC = () => {
|
||||||
size="small"
|
size="small"
|
||||||
checked={item.status === 1}
|
checked={item.status === 1}
|
||||||
onChange={(checked) => handleStatusChange(item.id, checked)}
|
onChange={(checked) => handleStatusChange(item.id, checked)}
|
||||||
disabled={!canEdit}
|
disabled={false}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*<div style={{ marginBottom: 12 }}>*/}
|
<div style={{ marginBottom: 12 }}>
|
||||||
{/* <Text strong style={{ fontSize: 16, display: 'block' }} ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text>*/}
|
<Text strong style={{ fontSize: 16, display: 'block' }} ellipsis={{ tooltip: item.templateName }}>{item.templateName}</Text>
|
||||||
{/*<Text type="secondary" style={{ fontSize: 12 }}>使用次数: {item.usageCount || 0}</Text>*/}
|
{/*<Text type="secondary" style={{ fontSize: 12 }}>使用次数: {item.usageCount || 0}</Text>*/}
|
||||||
{/*</div>*/}
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20, height: 22, overflow: 'hidden' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 20, height: 22, overflow: 'hidden' }}>
|
||||||
{item.tags?.map(tag => {
|
{item.tags?.map(tag => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue