From 1e7a8ad83c00bd152cc1f6c317ac4925052d2ad9 Mon Sep 17 00:00:00 2001 From: chenhao Date: Wed, 10 Jun 2026 16:00:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20Android=20?= =?UTF-8?q?=E4=BC=9A=E8=AE=AE=E6=8E=A7=E5=88=B6=E5=99=A8=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20ASR=20=E9=87=8D=E8=AF=95=E5=92=8C=E6=80=BB=E7=BB=93?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `retryTranscription` 和 `retrySummary` 方法,支持重试 ASR 识别和会议总结 - 更新 `create` 方法,返回 `AndroidMeetingCreateResponse` 对象 - 更新 `list` 方法,返回 `AndroidMeetingListItemVO` 列表 - 添加辅助方法 `buildAndroidMeetingCreateResponse` 和 `buildAndroidMeetingListPage` 用于构建响应对象 - 引入 `h5BaseUrl` 配置项,用于生成会议预览 URL --- .../android/AndroidMeetingController.java | 134 ++++++++++++++++-- 1 file changed, 122 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java index 2bbe7c5..70199f4 100644 --- a/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java +++ b/backend/src/main/java/com/imeeting/controller/android/AndroidMeetingController.java @@ -6,7 +6,9 @@ import com.imeeting.common.MeetingConstants; import com.imeeting.common.SysParamKeys; import com.imeeting.common.exception.ExistingOfflineMeetingException; import com.imeeting.dto.android.AndroidAuthContext; +import com.imeeting.dto.android.AndroidMeetingCreateResponse; import com.imeeting.dto.android.AndroidMeetingConfigVo; +import com.imeeting.dto.android.AndroidMeetingListItemVO; import com.imeeting.dto.android.AndroidOfflineMeetingConflictVO; import com.imeeting.dto.android.AndroidOfflineMeetingFinishRequest; import com.imeeting.dto.android.AndroidUnifiedMeetingStatusRequest; @@ -51,6 +53,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -65,7 +70,9 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; @@ -84,6 +91,9 @@ public class AndroidMeetingController { private static final String STAGE_SUMMARY_GENERATION = "summary_generation"; private static final String STAGE_COMPLETED = "completed"; + @Value("${imeeting.h5.base-url:}") + private String h5BaseUrl; + private final AndroidAuthService androidAuthService; private final AndroidChunkUploadService androidChunkUploadService; private final LegacyMeetingAdapterService legacyMeetingAdapterService; @@ -138,7 +148,7 @@ public class AndroidMeetingController { @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "200", description = "返回新创建的会议详情", - content = @Content(schema = @Schema(implementation = MeetingVO.class)) + content = @Content(schema = @Schema(implementation = AndroidMeetingCreateResponse.class)) ) }) @PostMapping("/create") @@ -153,7 +163,8 @@ public class AndroidMeetingController { // if (existingMeeting != null) { // return new ApiResponse<>("409", "设备端已有会议", meetingQueryService.getDetailIgnoreTenant(existingMeeting.getId())); // } - return ApiResponse.ok(legacyMeetingAdapterService.createMeeting(command, authContext, loginUser)); + MeetingVO meeting = legacyMeetingAdapterService.createMeeting(command, authContext, loginUser); + return ApiResponse.ok(buildAndroidMeetingCreateResponse(meeting)); } catch (ExistingOfflineMeetingException ex) { return new ApiResponse<>("409", "有未结束会议", new AndroidOfflineMeetingConflictVO(ex.getMeetingId())); } @@ -247,11 +258,11 @@ public class AndroidMeetingController { ) }) @GetMapping - public ApiResponse>> list(HttpServletRequest request, - @RequestParam(value = "user_id", required = false) Long ignoredUserId, - @RequestParam(defaultValue = "1") Integer page, - @RequestParam(value = "page_size", defaultValue = "10") Integer pageSize, - @RequestParam(required = false) String title) { + public ApiResponse>> list(HttpServletRequest request, + @RequestParam(value = "user_id", required = false) Long ignoredUserId, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(value = "page_size", defaultValue = "10") Integer pageSize, + @RequestParam(required = false) String title) { AndroidRequestLogHelper.logRequest(log, "Android会议", "分页查询会议接口", "userId", ignoredUserId, "page", page, @@ -259,7 +270,7 @@ public class AndroidMeetingController { "title", title); AndroidAuthContext authContext = androidAuthService.authenticateHttp(request); LoginUser loginUser = AndroidLoginUserSupport.requireLoginUser(authContext); - return ApiResponse.ok(meetingQueryService.pageMeetings( + PageResult> result = meetingQueryService.pageMeetings( page, pageSize, title, @@ -269,7 +280,8 @@ public class AndroidMeetingController { "all", null, AndroidLoginUserSupport.isAdmin(authContext) - )); + ); + return ApiResponse.ok(buildAndroidMeetingListPage(result)); } @Operation(summary = "查询Android会议预览数据") @@ -323,14 +335,51 @@ public class AndroidMeetingController { .build()); } - @Operation(summary = "更新Android会议访问密码") + @Operation(summary = "重试 Android 会议 ASR 识别") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "200", - description = "返回更新后的会议访问密码,传空时表示清空访问密码", - content = @Content(schema = @Schema(implementation = String.class)) + description = "重试成功返回 true", + content = @Content(schema = @Schema(implementation = Boolean.class)) ) }) + @PostMapping("/{meetingId}/transcripts/retry") + @Anonymous + public ApiResponse retryTranscription(HttpServletRequest request, @PathVariable Long meetingId) { + AndroidRequestLogHelper.logRequest(log, "Android会议", "重试会议 ASR 识别接口", "meetingId", meetingId); + AndroidAuthContext authContext = androidAuthService.authenticateHttp(request); + LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext); + requireOperableOfflineMeeting(meetingId, authContext, loginUser); + meetingCommandService.retryTranscription(meetingId); + return ApiResponse.ok(true); + } + + @Operation(summary = "重试 Android 会议总结") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "重试成功返回 true", + content = @Content(schema = @Schema(implementation = Boolean.class)) + ) + }) + @PostMapping("/{meetingId}/summary/retry") + @Anonymous + public ApiResponse retrySummary(HttpServletRequest request, @PathVariable Long meetingId) { + AndroidRequestLogHelper.logRequest(log, "Android会议", "重试会议总结接口", "meetingId", meetingId); + AndroidAuthContext authContext = androidAuthService.authenticateHttp(request); + LoginUser loginUser = authContext.isAnonymous() ? null : AndroidLoginUserSupport.requireLoginUser(authContext); + requireOperableOfflineMeeting(meetingId, authContext, loginUser); + meetingCommandService.retrySummary(meetingId); + return ApiResponse.ok(true); + } + @Operation(summary = "更新Android会议访问密码") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "返回更新后的会议访问密码,传空时表示清空访问密码", + content = @Content(schema = @Schema(implementation = String.class)) + ) + }) @PutMapping("/{meetingId}/access-password") @Log(value = "修改Android会议访问密码", type = "Android会议管理") public ApiResponse updateAccessPassword(HttpServletRequest request, @@ -419,6 +468,67 @@ public class AndroidMeetingController { return ApiResponse.ok(resultVo); } + private AndroidMeetingCreateResponse buildAndroidMeetingCreateResponse(MeetingVO meeting) { + AndroidMeetingCreateResponse response = new AndroidMeetingCreateResponse(); + if (meeting == null) { + return response; + } + BeanUtils.copyProperties(meeting, response); + response.setPreviewUrl(buildPreviewUrl(meeting.getId())); + return response; + } + + private PageResult> buildAndroidMeetingListPage(PageResult> source) { + PageResult> result = new PageResult<>(); + if (source == null) { + result.setTotal(0L); + result.setRecords(List.of()); + return result; + } + result.setTotal(source.getTotal()); + result.setRecords(source.getRecords() == null + ? List.of() + : source.getRecords().stream().map(this::buildAndroidMeetingListItem).toList()); + return result; + } + + private AndroidMeetingListItemVO buildAndroidMeetingListItem(MeetingVO meeting) { + AndroidMeetingListItemVO item = new AndroidMeetingListItemVO(); + if (meeting == null) { + return item; + } + BeanUtils.copyProperties(meeting, item); + item.setPreviewUrl(buildPreviewUrl(meeting.getId())); + item.setDayOffset(resolveDayOffset(meeting.getMeetingTime())); + return item; + } + + private String buildPreviewUrl(Long meetingId) { + if (meetingId == null) { + return null; + } + String baseUrl = normalizeH5BaseUrl(); + if (!StringUtils.hasText(baseUrl)) { + return null; + } + return baseUrl + "/meetings/" + meetingId + "/preview"; + } + + private String normalizeH5BaseUrl() { + String baseUrl = StringUtils.hasText(h5BaseUrl) ? h5BaseUrl.trim() : ""; + if (!StringUtils.hasText(baseUrl)) { + return ""; + } + return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + } + + private Long resolveDayOffset(LocalDateTime meetingTime) { + if (meetingTime == null) { + return null; + } + return ChronoUnit.DAYS.between(LocalDate.now(), meetingTime.toLocalDate()); + } + private MeetingVO requireOperableOfflineMeeting(Long meetingId, AndroidAuthContext authContext, LoginUser loginUser) { MeetingVO meeting = meetingQueryService.getDetailIgnoreTenant(meetingId); if (meeting == null) {