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