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
chenhao 2026-03-09 14:44:24 +08:00
parent d554849e8e
commit 86b3616786
13 changed files with 575 additions and 360 deletions

View File

@ -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:失败 |

View File

@ -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)

View File

@ -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);

View File

@ -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 (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { if (Integer.valueOf(0).equals(existing.getIsSystem())) {
// 平台管理员只能修改平台级模板 (tenantId = 0)
canModify = existing.getTenantId() == 0L;
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
// 租户管理员可以修改本租户的所有模板 (租户预置 + 个人模板)
canModify = existing.getTenantId().equals(loginUser.getTenantId());
} else {
// 普通用户仅限自己的个人模板
canModify = existing.getCreatorId().equals(loginUser.getUserId()); canModify = existing.getCreatorId().equals(loginUser.getUserId());
} else {
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
canModify = existing.getTenantId() == 0L;
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
canModify = existing.getTenantId().equals(loginUser.getTenantId());
}
} }
if (!canModify) { 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,23 +76,36 @@ 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;
}
} }
existing.setStatus(status); if (canGlobalModify) {
return ApiResponse.ok(promptTemplateService.updateById(existing)); existing.setStatus(status);
return ApiResponse.ok(promptTemplateService.updateById(existing));
}
boolean success = promptTemplateService.updateUserTemplateStatus(
id,
status,
loginUser.getTenantId(),
loginUser.getUserId(),
loginUser.getIsPlatformAdmin(),
loginUser.getIsTenantAdmin()
);
if (!success) {
return ApiResponse.error("Template not found or no permission");
}
return ApiResponse.ok(true);
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@ -114,16 +118,18 @@ public class PromptTemplateController {
} }
boolean canModify = false; boolean canModify = false;
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) { if (Integer.valueOf(0).equals(existing.getIsSystem())) {
canModify = existing.getTenantId() == 0L;
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
canModify = existing.getTenantId().equals(loginUser.getTenantId());
} else {
canModify = existing.getCreatorId().equals(loginUser.getUserId()); canModify = existing.getCreatorId().equals(loginUser.getUserId());
} else {
if (Boolean.TRUE.equals(loginUser.getIsPlatformAdmin())) {
canModify = existing.getTenantId() == 0L;
} else if (Boolean.TRUE.equals(loginUser.getIsTenantAdmin())) {
canModify = existing.getTenantId().equals(loginUser.getTenantId());
}
} }
if (!canModify) { 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()));
} }
} }

View File

@ -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;

View File

@ -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;

View File

@ -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;
} }

View File

@ -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);
} }

View File

@ -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,60 +150,34 @@ 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;
// --- 核心:接管逻辑 --- updateProgress(meeting.getId(), 5, "正在提交识别请求...", 0);
List<AiTask> existingTasks = this.list(new LambdaQueryWrapper<AiTask>() Map<String, Object> req = buildAsrRequest(meeting, taskRecord);
.eq(AiTask::getMeetingId, meeting.getId()) taskRecord.setRequestData(req);
.eq(AiTask::getTaskType, "ASR") this.updateById(taskRecord);
.orderByDesc(AiTask::getStartedAt));
if (!existingTasks.isEmpty()) { String respBody = postJson(submitUrl, req);
AiTask lastTask = existingTasks.get(0); JsonNode submitNode = objectMapper.readTree(respBody);
if (lastTask.getResponseData() != null && lastTask.getResponseData().get("task_id") != null) { if (submitNode.path("code").asInt() != 200) {
String potentialTaskId = lastTask.getResponseData().get("task_id").toString(); updateAiTaskFail(taskRecord, "Submission Failed: " + respBody);
log.info("Attempting to resume ASR task {} for meeting {}", potentialTaskId, meeting.getId()); throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText());
// 验证旧 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);
Map<String, Object> req = buildAsrRequest(meeting);
taskRecord = createAiTask(meeting.getId(), "ASR", req);
String respBody = postJson(submitUrl, req);
JsonNode submitNode = objectMapper.readTree(respBody);
if (submitNode.path("code").asInt() != 200) {
updateAiTaskFail(taskRecord, "Submission Failed: " + respBody);
throw new RuntimeException("ASR引擎拒绝请求: " + submitNode.path("msg").asText());
}
taskId = submitNode.path("data").path("task_id").asText();
taskRecord.setResponseData(Map.of("task_id", taskId));
this.updateById(taskRecord);
} }
taskId = submitNode.path("data").path("task_id").asText();
taskRecord.setResponseData(Map.of("task_id", taskId));
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,15 +230,22 @@ 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");
List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>() if (hotWordsObj instanceof List) {
.eq(HotWord::getTenantId, meeting.getTenantId()).in(HotWord::getWord, meeting.getHotWords())); List<String> words = (List<String>) hotWordsObj;
Map<String, Integer> weightMap = entities.stream().collect(Collectors.toMap(HotWord::getWord, HotWord::getWeight, (v1, v2) -> v1)); if (!words.isEmpty()) {
for (String w : meeting.getHotWords()) { List<HotWord> entities = hotWordService.list(new LambdaQueryWrapper<HotWord>()
hotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0)); .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));
for (String w : words) {
hotwords.add(Map.of("hotword", w, "weight", weightMap.getOrDefault(w, 10) / 10.0));
}
} }
} }
req.put("hotwords", hotwords); req.put("hotwords", hotwords);
@ -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());

View File

@ -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()) {

View File

@ -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());

View File

@ -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; // return;
} // }
//
// 获取真实的 ASR 转录数据 // // 获取真实的 ASR 转录数据
List<MeetingTranscript> transcripts = transcriptMapper.selectList( // List<MeetingTranscript> transcripts = transcriptMapper.selectList(
new LambdaQueryWrapper<MeetingTranscript>() // new LambdaQueryWrapper<MeetingTranscript>()
.eq(MeetingTranscript::getMeetingId, testMeetingId) // .eq(MeetingTranscript::getMeetingId, testMeetingId)
.orderByAsc(MeetingTranscript::getStartTime) // .orderByAsc(MeetingTranscript::getStartTime)
); // );
//
if (transcripts.isEmpty()) { // if (transcripts.isEmpty()) {
System.out.println("⚠️ 警告:该会议暂无转录明细数据 (MeetingTranscript)"); // System.out.println("⚠️ 警告:该会议暂无转录明细数据 (MeetingTranscript)");
// 如果没明细,您可以选择是否继续,或者手动造一点 // // 如果没明细,您可以选择是否继续,或者手动造一点
// return; // // return;
} // }
//
String realAsrText = transcripts.stream() // String realAsrText = transcripts.stream()
.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"));
//
System.out.println("\n--- [DEBUG] 提取到的真实转录文本 ---"); // System.out.println("\n--- [DEBUG] 提取到的真实转录文本 ---");
System.out.println(realAsrText); // System.out.println(realAsrText);
//
AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId()); // AiModel llmModel = aiModelService.getById(meeting.getSummaryModelId());
if (llmModel == null) { // if (llmModel == null) {
System.out.println("❌ 错误:该会议未绑定总结模型配置"); // System.out.println("❌ 错误:该会议未绑定总结模型配置");
return; // return;
} // }
//
System.out.println("\n✅ 基础数据加载成功"); // System.out.println("\n✅ 基础数据加载成功");
System.out.println(" 模型名称: " + llmModel.getModelName()); // System.out.println(" 模型名称: " + llmModel.getModelName());
System.out.println(" 提示词模板快照: " + (meeting.getPromptContent() != null && meeting.getPromptContent().length() > 50 // System.out.println(" 提示词模板快照: " + (meeting.getPromptContent() != null && meeting.getPromptContent().length() > 50
? meeting.getPromptContent().substring(0, 50) + "..." // ? meeting.getPromptContent().substring(0, 50) + "..."
: meeting.getPromptContent())); // : meeting.getPromptContent()));
//
// --- 步骤 2: 构造请求 Payload --- // // --- 步骤 2: 构造请求 Payload ---
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());
//
List<Map<String, String>> messages = new ArrayList<>(); // List<Map<String, String>> messages = new ArrayList<>();
// 系统角色注入 Prompt // // 系统角色注入 Prompt
messages.add(Map.of("role", "system", "content", meeting.getPromptContent() != null ? meeting.getPromptContent() : "请总结以下会议内容")); // messages.add(Map.of("role", "system", "content", meeting.getPromptContent() != null ? meeting.getPromptContent() : "请总结以下会议内容"));
// 用户角色注入 真实的 ASR 文本 // // 用户角色注入 真实的 ASR 文本
messages.add(Map.of("role", "user", "content", "以下是会议转录全文:\n" + realAsrText)); // messages.add(Map.of("role", "user", "content", "以下是会议转录全文:\n" + realAsrText));
req.put("messages", messages); // req.put("messages", messages);
//
String jsonPayload = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(req); // String jsonPayload = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(req);
System.out.println("\n--- [DEBUG] 发送给 AI 的请求 JSON ---"); // System.out.println("\n--- [DEBUG] 发送给 AI 的请求 JSON ---");
System.out.println(jsonPayload); // System.out.println(jsonPayload);
//
// --- 步骤 3: 发起网络请求 --- // // --- 步骤 3: 发起网络请求 ---
String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions"); // String url = llmModel.getBaseUrl() + (llmModel.getApiPath() != null ? llmModel.getApiPath() : "/v1/chat/completions");
System.out.println("\n--- [DEBUG] 目标 URL: " + url); // System.out.println("\n--- [DEBUG] 目标 URL: " + url);
//
HttpClient client = HttpClient.newBuilder() // HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) // .connectTimeout(Duration.ofSeconds(10))
.build(); // .build();
//
HttpRequest request = HttpRequest.newBuilder() // HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url)) // .uri(URI.create(url))
.header("Content-Type", "application/json") // .header("Content-Type", "application/json")
.header("Authorization", "Bearer " + llmModel.getApiKey()) // .header("Authorization", "Bearer " + llmModel.getApiKey())
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) // .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build(); // .build();
//
System.out.println("⏳ 正在请求第三方 AI 接口..."); // System.out.println("⏳ 正在请求第三方 AI 接口...");
try { // try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); // HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
//
System.out.println("\n--- [DEBUG] 接口返回状态码: " + response.statusCode()); // System.out.println("\n--- [DEBUG] 接口返回状态码: " + response.statusCode());
System.out.println("--- [DEBUG] 接口返回 Raw Body ---"); // System.out.println("--- [DEBUG] 接口返回 Raw Body ---");
System.out.println(response.body()); // System.out.println(response.body());
//
// --- 步骤 4: 解析结果 --- // // --- 步骤 4: 解析结果 ---
if (response.statusCode() == 200) { // if (response.statusCode() == 200) {
JsonNode respNode = objectMapper.readTree(response.body()); // JsonNode respNode = objectMapper.readTree(response.body());
if (respNode.has("choices")) { // if (respNode.has("choices")) {
String finalContent = respNode.get("choices").get(0).get("message").get("content").asText(); // String finalContent = respNode.get("choices").get(0).get("message").get("content").asText();
System.out.println("\n✨ 总结生成成功!结果如下:"); // System.out.println("\n✨ 总结生成成功!结果如下:");
System.out.println("------------------------------------"); // System.out.println("------------------------------------");
System.out.println(finalContent); // System.out.println(finalContent);
System.out.println("------------------------------------"); // System.out.println("------------------------------------");
//
// 可选:将结果更新回数据库以便前端查看 // // 可选:将结果更新回数据库以便前端查看
// meeting.setSummaryContent(finalContent); // // meeting.setSummaryContent(finalContent);
// meetingMapper.updateById(meeting); // // meetingMapper.updateById(meeting);
} else { // } else {
System.out.println("❌ 错误:返回结果中不包含 'choices' 字段,请检查厂商 API 适配。"); // System.out.println("❌ 错误:返回结果中不包含 'choices' 字段,请检查厂商 API 适配。");
} // }
} else { // } else {
System.out.println("❌ 接口请求失败,请检查 BaseUrl 和 ApiKey 是否正确。"); // System.out.println("❌ 接口请求失败,请检查 BaseUrl 和 ApiKey 是否正确。");
} // }
} catch (Exception e) { // } catch (Exception e) {
System.out.println("❌ 网络异常:" + e.getMessage()); // System.out.println("❌ 网络异常:" + e.getMessage());
e.printStackTrace(); // e.printStackTrace();
} // }
} // }
} //}

View File

@ -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 => {