feat: 添加旧版Android API支持和会议预览功能

- 添加 `LegacyLoginResponse`, `LegacyRefreshTokenResponse`, `LegacyLoginUserResponse` 和 `LegacyMeetingTagResponse` DTO
- 添加 `LegacyAuthController` 以处理旧版登录和刷新令牌请求
- 更新 `MeetingAccessServiceImpl` 中的异常信息为英文
- 在前端 `MeetingPreview.tsx` 中添加多语言文本和分享功能
dev_na
chenhao 2026-04-16 09:41:22 +08:00
parent db310fc803
commit 017e1d2ded
12 changed files with 818 additions and 324 deletions

View File

@ -0,0 +1,96 @@
package com.imeeting.controller.android.legacy;
import com.imeeting.dto.android.legacy.LegacyApiResponse;
import com.imeeting.dto.android.legacy.LegacyLoginResponse;
import com.imeeting.dto.android.legacy.LegacyLoginUserResponse;
import com.imeeting.dto.android.legacy.LegacyRefreshTokenResponse;
import com.unisbase.dto.LoginRequest;
import com.unisbase.dto.RefreshRequest;
import com.unisbase.dto.SysRoleDTO;
import com.unisbase.dto.SysUserDTO;
import com.unisbase.dto.TokenResponse;
import com.unisbase.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.util.StringUtils;
import java.util.List;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class LegacyAuthController {
private final AuthService authService;
@PostMapping("/login")
public LegacyApiResponse<LegacyLoginResponse> login(@Valid @RequestBody LoginRequest request) {
TokenResponse tokenResponse = authService.login(request, true);
return LegacyApiResponse.ok(new LegacyLoginResponse(
tokenResponse.getAccessToken(),
tokenResponse.getRefreshToken(),
toLegacyUser(tokenResponse.getUser())
));
}
@PostMapping("/refresh")
public LegacyApiResponse<LegacyRefreshTokenResponse> refresh(@RequestBody(required = false) RefreshRequest request,
@RequestHeader(value = "Authorization", required = false) String authorization,
@RequestHeader(value = "X-Android-Access-Token", required = false) String androidAccessToken) {
TokenResponse tokenResponse = authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken));
return LegacyApiResponse.ok(new LegacyRefreshTokenResponse(tokenResponse.getAccessToken(),tokenResponse.getRefreshToken()));
}
private LegacyLoginUserResponse toLegacyUser(SysUserDTO user) {
if (user == null) {
return null;
}
SysRoleDTO primaryRole = resolvePrimaryRole(user);
return new LegacyLoginUserResponse(
user.getUserId(),
user.getUsername(),
user.getDisplayName(),
user.getAvatarUrl(),
user.getEmail(),
primaryRole == null ? null : primaryRole.getRoleId(),
primaryRole == null ? null : primaryRole.getRoleName(),
user.getCreatedAt()
);
}
private SysRoleDTO resolvePrimaryRole(SysUserDTO user) {
List<SysRoleDTO> roles = user.getRoles();
if (roles != null && !roles.isEmpty()) {
return roles.get(0);
}
List<Long> roleIds = user.getRoleIds();
if (roleIds != null && !roleIds.isEmpty()) {
SysRoleDTO role = new SysRoleDTO();
role.setRoleId(roleIds.get(0));
return role;
}
return null;
}
private String resolveRefreshToken(RefreshRequest request, String authorization, String androidAccessToken) {
if (request != null && StringUtils.hasText(request.getRefreshToken())) {
return request.getRefreshToken().trim();
}
if (StringUtils.hasText(androidAccessToken)) {
return androidAccessToken.trim();
}
if (StringUtils.hasText(authorization)) {
String value = authorization.trim();
if (value.startsWith("Bearer ")) {
return value.substring(7).trim();
}
return value;
}
throw new IllegalArgumentException("refreshToken不能为空");
}
}

View File

@ -0,0 +1,53 @@
package com.imeeting.controller.biz;
import com.imeeting.dto.biz.MeetingPreviewAccessVO;
import com.imeeting.dto.biz.PublicMeetingPreviewVO;
import com.imeeting.entity.biz.Meeting;
import com.imeeting.service.biz.MeetingAccessService;
import com.imeeting.service.biz.MeetingQueryService;
import com.unisbase.common.ApiResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/public/meetings")
public class MeetingPublicPreviewController {
private final MeetingQueryService meetingQueryService;
private final MeetingAccessService meetingAccessService;
public MeetingPublicPreviewController(MeetingQueryService meetingQueryService,
MeetingAccessService meetingAccessService) {
this.meetingQueryService = meetingQueryService;
this.meetingAccessService = meetingAccessService;
}
@GetMapping("/{id}/preview/access")
public ApiResponse<MeetingPreviewAccessVO> getPreviewAccess(@PathVariable Long id) {
try {
Meeting meeting = meetingAccessService.requireMeeting(id);
return ApiResponse.ok(new MeetingPreviewAccessVO(meetingAccessService.isPreviewPasswordRequired(meeting)));
} catch (RuntimeException ex) {
return ApiResponse.error(ex.getMessage());
}
}
@GetMapping("/{id}/preview")
public ApiResponse<PublicMeetingPreviewVO> getPreview(@PathVariable Long id,
@RequestParam(required = false) String accessPassword) {
try {
Meeting meeting = meetingAccessService.requireMeeting(id);
meetingAccessService.assertCanPreviewMeeting(meeting, accessPassword);
PublicMeetingPreviewVO data = new PublicMeetingPreviewVO();
data.setMeeting(meetingQueryService.getDetail(id));
data.setTranscripts(meetingQueryService.getTranscripts(id));
return ApiResponse.ok(data);
} catch (RuntimeException ex) {
return ApiResponse.error(ex.getMessage());
}
}
}

View File

@ -0,0 +1,14 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyLoginResponse {
private String token;
private String refreshToken;
private LegacyLoginUserResponse user;
}

View File

@ -0,0 +1,21 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyLoginUserResponse {
private Long user_id;
private String username;
private String caption;
private String avatar_url;
private String email;
private Long role_id;
private String role_name;
private LocalDateTime created_at;
}

View File

@ -0,0 +1,13 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyMeetingTagResponse {
private Long id;
private String name;
}

View File

@ -0,0 +1,13 @@
package com.imeeting.dto.android.legacy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LegacyRefreshTokenResponse {
private String token;
private String refreshToken;
}

View File

@ -0,0 +1,10 @@
package com.imeeting.dto.biz;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class MeetingPreviewAccessVO {
private boolean passwordRequired;
}

View File

@ -0,0 +1,11 @@
package com.imeeting.dto.biz;
import lombok.Data;
import java.util.List;
@Data
public class PublicMeetingPreviewVO {
private MeetingVO meeting;
private List<MeetingTranscriptVO> transcripts;
}

View File

@ -17,7 +17,7 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
public Meeting requireMeeting(Long meetingId) {
Meeting meeting = meetingMapper.selectById(meetingId);
if (meeting == null) {
throw new RuntimeException("会议不存在");
throw new RuntimeException("Meeting not found");
}
return meeting;
}
@ -35,10 +35,10 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
}
String providedPassword = normalizePreviewPassword(accessPassword);
if (providedPassword == null) {
throw new RuntimeException("该会议需要访问密码");
throw new RuntimeException("Access password is required");
}
if (!expectedPassword.equals(providedPassword)) {
throw new RuntimeException("访问密码错误");
throw new RuntimeException("Access password is incorrect");
}
}
@ -48,7 +48,7 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
return;
}
if (!isSameTenant(meeting, loginUser)) {
throw new RuntimeException("无权查看此会议");
throw new RuntimeException("No permission to view this meeting");
}
if (isTenantAdmin(loginUser)) {
return;
@ -56,7 +56,7 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
if (isCreator(meeting, loginUser) || isParticipant(meeting, loginUser)) {
return;
}
throw new RuntimeException("无权查看此会议");
throw new RuntimeException("No permission to view this meeting");
}
@Override
@ -65,12 +65,12 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
return;
}
if (!isSameTenant(meeting, loginUser)) {
throw new RuntimeException("无权修改此会议");
throw new RuntimeException("No permission to edit this meeting");
}
if (isTenantAdmin(loginUser) || isCreator(meeting, loginUser)) {
return;
}
throw new RuntimeException("无权修改此会议");
throw new RuntimeException("No permission to edit this meeting");
}
@Override
@ -79,12 +79,12 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
return;
}
if (!isSameTenant(meeting, loginUser)) {
throw new RuntimeException("无权操作此实时会议");
throw new RuntimeException("No permission to manage this realtime meeting");
}
if (isTenantAdmin(loginUser) || isCreator(meeting, loginUser)) {
return;
}
throw new RuntimeException("无权操作此实时会议");
throw new RuntimeException("No permission to manage this realtime meeting");
}
@Override
@ -93,12 +93,12 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
return;
}
if (!isSameTenant(meeting, loginUser)) {
throw new RuntimeException("无权导出此会议");
throw new RuntimeException("No permission to export this meeting");
}
if (isTenantAdmin(loginUser) || isCreator(meeting, loginUser) || isParticipant(meeting, loginUser)) {
return;
}
throw new RuntimeException("无权导出此会议");
throw new RuntimeException("No permission to export this meeting");
}
private boolean isPlatformAdmin(LoginUser loginUser) {

View File

@ -50,6 +50,7 @@ unisbase:
token-prefix: "Bearer "
permit-all-urls:
- /actuator/health
- /api/auth/**
- /api/static/**
- /api/public/meetings/**
- /ws/**

View File

@ -151,6 +151,23 @@
letter-spacing: -0.04em;
}
.meeting-preview-title-number {
font-family: "Segoe UI", "SF Pro Display", "Helvetica Neue", sans-serif;
font-variant-numeric: lining-nums proportional-nums;
letter-spacing: -0.02em;
}
.meeting-preview-hero-toolbar {
display: grid;
gap: 18px;
}
.meeting-preview-hero-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.meeting-preview-subtitle {
margin: 14px 0 0;
color: var(--preview-muted);
@ -193,6 +210,19 @@
margin-top: 18px;
}
.meeting-preview-page-tabs .ant-tabs-nav {
margin-bottom: 18px;
}
.meeting-preview-page-tabs .ant-tabs-tab {
font-weight: 600;
}
.meeting-preview-tab-panel {
display: grid;
gap: 18px;
}
.meeting-preview-section {
padding: 22px 18px;
}
@ -501,10 +531,17 @@
}
.meeting-preview-transcript-item.is-active {
transform: translateY(-2px);
border-color: rgba(184, 100, 50, 0.28);
box-shadow: 0 12px 26px rgba(105, 72, 40, 0.08);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 244, 235, 0.92));
}
.meeting-preview-transcript-item.is-active .meeting-preview-transcript-copy {
color: #5a3113;
font-weight: 600;
}
.meeting-preview-transcript-head {
display: flex;
align-items: center;
@ -589,3 +626,10 @@
gap: 22px;
}
}
@media (max-width: 767px) {
.meeting-preview-hero-actions {
display: grid;
grid-template-columns: 1fr;
}
}

File diff suppressed because it is too large Load Diff