feat: 添加旧版Android API支持和会议预览功能
- 添加 `LegacyLoginResponse`, `LegacyRefreshTokenResponse`, `LegacyLoginUserResponse` 和 `LegacyMeetingTagResponse` DTO - 添加 `LegacyAuthController` 以处理旧版登录和刷新令牌请求 - 更新 `MeetingAccessServiceImpl` 中的异常信息为英文 - 在前端 `MeetingPreview.tsx` 中添加多语言文本和分享功能dev_na
parent
db310fc803
commit
017e1d2ded
|
|
@ -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不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.imeeting.dto.biz;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MeetingPreviewAccessVO {
|
||||||
|
private boolean passwordRequired;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,7 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
|
||||||
public Meeting requireMeeting(Long meetingId) {
|
public Meeting requireMeeting(Long meetingId) {
|
||||||
Meeting meeting = meetingMapper.selectById(meetingId);
|
Meeting meeting = meetingMapper.selectById(meetingId);
|
||||||
if (meeting == null) {
|
if (meeting == null) {
|
||||||
throw new RuntimeException("会议不存在");
|
throw new RuntimeException("Meeting not found");
|
||||||
}
|
}
|
||||||
return meeting;
|
return meeting;
|
||||||
}
|
}
|
||||||
|
|
@ -35,10 +35,10 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
|
||||||
}
|
}
|
||||||
String providedPassword = normalizePreviewPassword(accessPassword);
|
String providedPassword = normalizePreviewPassword(accessPassword);
|
||||||
if (providedPassword == null) {
|
if (providedPassword == null) {
|
||||||
throw new RuntimeException("该会议需要访问密码");
|
throw new RuntimeException("Access password is required");
|
||||||
}
|
}
|
||||||
if (!expectedPassword.equals(providedPassword)) {
|
if (!expectedPassword.equals(providedPassword)) {
|
||||||
throw new RuntimeException("访问密码错误");
|
throw new RuntimeException("Access password is incorrect");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isSameTenant(meeting, loginUser)) {
|
if (!isSameTenant(meeting, loginUser)) {
|
||||||
throw new RuntimeException("无权查看此会议");
|
throw new RuntimeException("No permission to view this meeting");
|
||||||
}
|
}
|
||||||
if (isTenantAdmin(loginUser)) {
|
if (isTenantAdmin(loginUser)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -56,7 +56,7 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
|
||||||
if (isCreator(meeting, loginUser) || isParticipant(meeting, loginUser)) {
|
if (isCreator(meeting, loginUser) || isParticipant(meeting, loginUser)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new RuntimeException("无权查看此会议");
|
throw new RuntimeException("No permission to view this meeting");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -65,12 +65,12 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isSameTenant(meeting, loginUser)) {
|
if (!isSameTenant(meeting, loginUser)) {
|
||||||
throw new RuntimeException("无权修改此会议");
|
throw new RuntimeException("No permission to edit this meeting");
|
||||||
}
|
}
|
||||||
if (isTenantAdmin(loginUser) || isCreator(meeting, loginUser)) {
|
if (isTenantAdmin(loginUser) || isCreator(meeting, loginUser)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new RuntimeException("无权修改此会议");
|
throw new RuntimeException("No permission to edit this meeting");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -79,12 +79,12 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isSameTenant(meeting, loginUser)) {
|
if (!isSameTenant(meeting, loginUser)) {
|
||||||
throw new RuntimeException("无权操作此实时会议");
|
throw new RuntimeException("No permission to manage this realtime meeting");
|
||||||
}
|
}
|
||||||
if (isTenantAdmin(loginUser) || isCreator(meeting, loginUser)) {
|
if (isTenantAdmin(loginUser) || isCreator(meeting, loginUser)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new RuntimeException("无权操作此实时会议");
|
throw new RuntimeException("No permission to manage this realtime meeting");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -93,12 +93,12 @@ public class MeetingAccessServiceImpl implements MeetingAccessService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isSameTenant(meeting, loginUser)) {
|
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)) {
|
if (isTenantAdmin(loginUser) || isCreator(meeting, loginUser) || isParticipant(meeting, loginUser)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new RuntimeException("无权导出此会议");
|
throw new RuntimeException("No permission to export this meeting");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPlatformAdmin(LoginUser loginUser) {
|
private boolean isPlatformAdmin(LoginUser loginUser) {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ unisbase:
|
||||||
token-prefix: "Bearer "
|
token-prefix: "Bearer "
|
||||||
permit-all-urls:
|
permit-all-urls:
|
||||||
- /actuator/health
|
- /actuator/health
|
||||||
|
- /api/auth/**
|
||||||
- /api/static/**
|
- /api/static/**
|
||||||
- /api/public/meetings/**
|
- /api/public/meetings/**
|
||||||
- /ws/**
|
- /ws/**
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,23 @@
|
||||||
letter-spacing: -0.04em;
|
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 {
|
.meeting-preview-subtitle {
|
||||||
margin: 14px 0 0;
|
margin: 14px 0 0;
|
||||||
color: var(--preview-muted);
|
color: var(--preview-muted);
|
||||||
|
|
@ -193,6 +210,19 @@
|
||||||
margin-top: 18px;
|
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 {
|
.meeting-preview-section {
|
||||||
padding: 22px 18px;
|
padding: 22px 18px;
|
||||||
}
|
}
|
||||||
|
|
@ -501,10 +531,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.meeting-preview-transcript-item.is-active {
|
.meeting-preview-transcript-item.is-active {
|
||||||
|
transform: translateY(-2px);
|
||||||
border-color: rgba(184, 100, 50, 0.28);
|
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));
|
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 {
|
.meeting-preview-transcript-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -589,3 +626,10 @@
|
||||||
gap: 22px;
|
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
Loading…
Reference in New Issue