feat: 添加旧版Android API支持和日志配置
- 添加 `ApiResponseSuccessCodeAdvice` 以处理旧版成功代码 - 添加 `LegacyMeetingCreateRequest`, `LegacyApiResponse` 和相关控制器 - 添加 `GrpcExceptionLoggingInterceptor` 以增强gRPC异常日志记录 - 更新 `application.yml`,移除 `/api/android/**` 的安全配置 - 更新前端API和组件,修复字段名称和代理配置 - 添加日志配置文件 `logback-spring.xml` 以支持日志滚动和格式化dev_na
parent
5f895bfe26
commit
dffd33206a
|
|
@ -0,0 +1,35 @@
|
|||
package com.imeeting.config;
|
||||
|
||||
import com.unisbase.common.ApiResponse;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class ApiResponseSuccessCodeAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private static final String LEGACY_SUCCESS_CODE = "0";
|
||||
private static final String SUCCESS_CODE = "200";
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body,
|
||||
MethodParameter returnType,
|
||||
MediaType selectedContentType,
|
||||
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
ServerHttpRequest request,
|
||||
ServerHttpResponse response) {
|
||||
if (body instanceof ApiResponse<?> apiResponse && LEGACY_SUCCESS_CODE.equals(apiResponse.getCode())) {
|
||||
apiResponse.setCode(SUCCESS_CODE);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.imeeting.config.grpc;
|
||||
|
||||
import io.grpc.ForwardingServerCallListener;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.ServerCall;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.Status;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class GrpcExceptionLoggingInterceptor implements ServerInterceptor {
|
||||
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
|
||||
Metadata headers,
|
||||
ServerCallHandler<ReqT, RespT> next) {
|
||||
String methodName = call.getMethodDescriptor().getFullMethodName();
|
||||
AtomicBoolean closed = new AtomicBoolean(false);
|
||||
ServerCall.Listener<ReqT> delegate;
|
||||
try {
|
||||
delegate = next.startCall(call, headers);
|
||||
} catch (RuntimeException ex) {
|
||||
log.error("gRPC startCall failed, method={}", methodName, ex);
|
||||
closeCall(call, closed, ex);
|
||||
return new ServerCall.Listener<>() {
|
||||
};
|
||||
}
|
||||
|
||||
return new ForwardingServerCallListener.SimpleForwardingServerCallListener<>(delegate) {
|
||||
@Override
|
||||
public void onMessage(ReqT message) {
|
||||
runSafely("onMessage", () -> super.onMessage(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHalfClose() {
|
||||
runSafely("onHalfClose", super::onHalfClose);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
runSafely("onCancel", super::onCancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
runSafely("onComplete", super::onComplete);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReady() {
|
||||
runSafely("onReady", super::onReady);
|
||||
}
|
||||
|
||||
private void runSafely(String phase, Runnable action) {
|
||||
try {
|
||||
action.run();
|
||||
} catch (RuntimeException ex) {
|
||||
log.error("gRPC request handling failed, method={}, phase={}", methodName, phase, ex);
|
||||
closeCall(call, closed, ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private <ReqT, RespT> void closeCall(ServerCall<ReqT, RespT> call, AtomicBoolean closed, RuntimeException ex) {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
call.close(Status.UNKNOWN.withDescription("Application error processing RPC").withCause(ex), new Metadata());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyLlmModelItemResponse;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/llm-models")
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyLlmModelController {
|
||||
|
||||
private final AiModelService aiModelService;
|
||||
|
||||
@GetMapping("/active")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<List<LegacyLlmModelItemResponse>> activeModels() {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
PageResult<List<AiModelVO>> result = aiModelService.pageModels(1, 1000, null, "LLM", loginUser.getTenantId());
|
||||
List<AiModelVO> enabledModels = result.getRecords() == null
|
||||
? List.of()
|
||||
: result.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||
.toList();
|
||||
boolean hasExplicitDefault = enabledModels.stream().anyMatch(item -> Integer.valueOf(1).equals(item.getIsDefault()));
|
||||
Long fallbackDefaultId = enabledModels.isEmpty() ? null : enabledModels.get(0).getId();
|
||||
List<LegacyLlmModelItemResponse> models = enabledModels.stream()
|
||||
.map(item -> LegacyLlmModelItemResponse.from(
|
||||
item,
|
||||
Integer.valueOf(1).equals(item.getIsDefault())
|
||||
|| (!hasExplicitDefault && Objects.equals(item.getId(), fallbackDefaultId))
|
||||
))
|
||||
.toList();
|
||||
return LegacyApiResponse.ok(models);
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingCreateResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingItemResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingListResponse;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/meetings")
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyMeetingController {
|
||||
|
||||
private final LegacyMeetingAdapterService legacyMeetingAdapterService;
|
||||
private final MeetingQueryService meetingQueryService;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final MeetingCommandService meetingCommandService;
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyMeetingCreateResponse> create(@RequestBody LegacyMeetingCreateRequest request) {
|
||||
MeetingVO meeting = legacyMeetingAdapterService.createMeeting(request, currentLoginUser());
|
||||
return LegacyApiResponse.ok(new LegacyMeetingCreateResponse(meeting.getId()));
|
||||
}
|
||||
|
||||
@PostMapping("/upload-audio")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<Void> uploadAudio(@RequestParam("meeting_id") Long meetingId,
|
||||
@RequestParam(value = "prompt_id", required = false) Long promptId,
|
||||
@RequestParam(value = "model_code", required = false) String modelCode,
|
||||
@RequestParam(value = "force_replace", defaultValue = "false") boolean forceReplace,
|
||||
@RequestParam("audio_file") MultipartFile audioFile) throws IOException {
|
||||
legacyMeetingAdapterService.uploadAndTriggerOfflineProcess(
|
||||
meetingId,
|
||||
promptId,
|
||||
modelCode,
|
||||
forceReplace,
|
||||
audioFile,
|
||||
currentLoginUser()
|
||||
);
|
||||
return LegacyApiResponse.ok("上传成功", null);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyMeetingListResponse> list(@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) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
boolean isAdmin = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) || Boolean.TRUE.equals(loginUser.getIsTenantAdmin());
|
||||
PageResult<List<MeetingVO>> result = meetingQueryService.pageMeetings(
|
||||
page,
|
||||
pageSize,
|
||||
title,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
"all",
|
||||
isAdmin
|
||||
);
|
||||
|
||||
LegacyMeetingListResponse data = new LegacyMeetingListResponse();
|
||||
data.setPage(page);
|
||||
data.setPageSize(pageSize);
|
||||
data.setTotal(result.getTotal());
|
||||
data.setTotalPages(pageSize == null || pageSize <= 0 ? 0 : (result.getTotal() + pageSize - 1) / pageSize);
|
||||
data.setHasMore(page != null && page < data.getTotalPages());
|
||||
data.setMeetings(result.getRecords() == null
|
||||
? List.of()
|
||||
: result.getRecords().stream().map(LegacyMeetingItemResponse::from).toList());
|
||||
return LegacyApiResponse.ok(data);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{meetingId}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<Void> delete(@PathVariable Long meetingId) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
meetingCommandService.deleteMeeting(meetingId);
|
||||
return LegacyApiResponse.ok("删除成功", null);
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
|
||||
private String resolveCreatorName(LoginUser loginUser) {
|
||||
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.imeeting.controller.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyApiResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyPromptItemResponse;
|
||||
import com.imeeting.dto.android.legacy.LegacyPromptListResponse;
|
||||
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.unisbase.dto.PageResult;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
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.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/prompts")
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyPromptController {
|
||||
|
||||
private static final String LEGACY_MEETING_SCENE = "MEETING_TASK";
|
||||
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
|
||||
@GetMapping("/active/{scene}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public LegacyApiResponse<LegacyPromptListResponse> activePrompts(@PathVariable String scene) {
|
||||
if (!LEGACY_MEETING_SCENE.equals(scene)) {
|
||||
return LegacyApiResponse.error("400", "scene only supports MEETING_TASK");
|
||||
}
|
||||
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
PageResult<List<PromptTemplateVO>> result = promptTemplateService.pageTemplates(
|
||||
1,
|
||||
1000,
|
||||
null,
|
||||
null,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin()
|
||||
);
|
||||
List<PromptTemplateVO> enabledTemplates = result.getRecords() == null
|
||||
? List.of()
|
||||
: result.getRecords().stream()
|
||||
.filter(item -> Integer.valueOf(1).equals(item.getStatus()))
|
||||
.toList();
|
||||
Long defaultTemplateId = enabledTemplates.isEmpty() ? null : enabledTemplates.get(0).getId();
|
||||
List<LegacyPromptItemResponse> prompts = enabledTemplates.stream()
|
||||
.map(item -> LegacyPromptItemResponse.from(item, Objects.equals(item.getId(), defaultTemplateId)))
|
||||
.toList();
|
||||
return LegacyApiResponse.ok(new LegacyPromptListResponse(prompts));
|
||||
}
|
||||
|
||||
private LoginUser currentLoginUser() {
|
||||
return (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LegacyApiResponse<T> {
|
||||
private String code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> LegacyApiResponse<T> ok(T data) {
|
||||
return new LegacyApiResponse<>("200", "success", data);
|
||||
}
|
||||
|
||||
public static <T> LegacyApiResponse<T> ok(String message, T data) {
|
||||
return new LegacyApiResponse<>("200", message, data);
|
||||
}
|
||||
|
||||
public static <T> LegacyApiResponse<T> error(String code, String message) {
|
||||
return new LegacyApiResponse<>(code, message, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LegacyLlmModelItemResponse {
|
||||
@JsonProperty("model_code")
|
||||
private String modelCode;
|
||||
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
|
||||
private String provider;
|
||||
|
||||
@JsonProperty("is_default")
|
||||
private Integer isDefault;
|
||||
|
||||
public static LegacyLlmModelItemResponse from(AiModelVO source, boolean defaultItem) {
|
||||
LegacyLlmModelItemResponse response = new LegacyLlmModelItemResponse();
|
||||
response.setModelCode(source.getModelCode());
|
||||
response.setModelName(source.getModelName());
|
||||
response.setProvider(source.getProvider());
|
||||
response.setIsDefault(defaultItem ? 1 : 0);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class LegacyMeetingCreateRequest {
|
||||
@JsonProperty("user_id")
|
||||
private Long userId;
|
||||
|
||||
private String title;
|
||||
|
||||
@JsonProperty("meeting_time")
|
||||
private String meetingTime;
|
||||
|
||||
private Object tags;
|
||||
|
||||
@JsonProperty("attendee_ids")
|
||||
private List<Long> attendeeIds;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LegacyMeetingCreateResponse {
|
||||
@JsonProperty("meeting_id")
|
||||
private Long meetingId;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LegacyMeetingItemResponse {
|
||||
@JsonProperty("meeting_id")
|
||||
private Long meetingId;
|
||||
|
||||
private String title;
|
||||
|
||||
@JsonProperty("meeting_time")
|
||||
private String meetingTime;
|
||||
|
||||
public static LegacyMeetingItemResponse from(MeetingVO source) {
|
||||
LegacyMeetingItemResponse response = new LegacyMeetingItemResponse();
|
||||
response.setMeetingId(source.getId());
|
||||
response.setTitle(source.getTitle());
|
||||
response.setMeetingTime(source.getMeetingTime() == null ? null : source.getMeetingTime().toString());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class LegacyMeetingListResponse {
|
||||
private List<LegacyMeetingItemResponse> meetings;
|
||||
private long total;
|
||||
private int page;
|
||||
|
||||
@JsonProperty("page_size")
|
||||
private int pageSize;
|
||||
|
||||
@JsonProperty("total_pages")
|
||||
private long totalPages;
|
||||
|
||||
@JsonProperty("has_more")
|
||||
private boolean hasMore;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.imeeting.dto.biz.PromptTemplateVO;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class LegacyPromptItemResponse {
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
@JsonProperty("is_default")
|
||||
private Integer isDefault;
|
||||
|
||||
public static LegacyPromptItemResponse from(PromptTemplateVO source, boolean defaultItem) {
|
||||
LegacyPromptItemResponse response = new LegacyPromptItemResponse();
|
||||
response.setId(source.getId());
|
||||
response.setName(source.getTemplateName());
|
||||
response.setIsDefault(defaultItem ? 1 : 0);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LegacyPromptListResponse {
|
||||
private List<LegacyPromptItemResponse> prompts;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.imeeting.dto.android.legacy;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LegacyUploadAudioResponse {
|
||||
@JsonProperty("meeting_id")
|
||||
private Long meetingId;
|
||||
|
||||
@JsonProperty("audio_url")
|
||||
private String audioUrl;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.imeeting.service.android.legacy;
|
||||
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
|
||||
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface LegacyMeetingAdapterService {
|
||||
MeetingVO createMeeting(LegacyMeetingCreateRequest request, LoginUser loginUser);
|
||||
|
||||
LegacyUploadAudioResponse uploadAndTriggerOfflineProcess(Long meetingId,
|
||||
Long promptId,
|
||||
String modelCode,
|
||||
boolean forceReplace,
|
||||
MultipartFile audioFile,
|
||||
LoginUser loginUser) throws IOException;
|
||||
}
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
package com.imeeting.service.android.legacy.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.imeeting.dto.android.legacy.LegacyMeetingCreateRequest;
|
||||
import com.imeeting.dto.android.legacy.LegacyUploadAudioResponse;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.RealtimeMeetingRuntimeProfile;
|
||||
import com.imeeting.entity.biz.AiTask;
|
||||
import com.imeeting.entity.biz.LlmModel;
|
||||
import com.imeeting.entity.biz.Meeting;
|
||||
import com.imeeting.entity.biz.MeetingTranscript;
|
||||
import com.imeeting.entity.biz.PromptTemplate;
|
||||
import com.imeeting.mapper.biz.LlmModelMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.service.android.legacy.LegacyMeetingAdapterService;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.MeetingAccessService;
|
||||
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||
import com.imeeting.service.biz.MeetingService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.service.biz.impl.MeetingDomainSupport;
|
||||
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
||||
import com.unisbase.security.LoginUser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyMeetingAdapterServiceImpl implements LegacyMeetingAdapterService {
|
||||
|
||||
private final MeetingService meetingService;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final MeetingDomainSupport meetingDomainSupport;
|
||||
private final MeetingRuntimeProfileResolver runtimeProfileResolver;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final AiTaskService aiTaskService;
|
||||
private final MeetingTranscriptMapper transcriptMapper;
|
||||
private final LlmModelMapper llmModelMapper;
|
||||
|
||||
@Value("${unisbase.app.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
@Value("${unisbase.app.resource-prefix:/api/static/}")
|
||||
private String resourcePrefix;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MeetingVO createMeeting(LegacyMeetingCreateRequest request, LoginUser loginUser) {
|
||||
if (request == null || request.getTitle() == null || request.getTitle().isBlank()) {
|
||||
throw new RuntimeException("会议标题不能为空");
|
||||
}
|
||||
LocalDateTime meetingTime = parseMeetingTime(request.getMeetingTime());
|
||||
if (meetingTime == null) {
|
||||
throw new RuntimeException("会议时间不能为空");
|
||||
}
|
||||
|
||||
Meeting meeting = meetingDomainSupport.initMeeting(
|
||||
request.getTitle().trim(),
|
||||
meetingTime,
|
||||
joinIds(request.getAttendeeIds()),
|
||||
normalizeTags(request.getTags()),
|
||||
null,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
loginUser.getUserId(),
|
||||
resolveCreatorName(loginUser),
|
||||
0
|
||||
);
|
||||
meetingService.save(meeting);
|
||||
|
||||
MeetingVO vo = new MeetingVO();
|
||||
meetingDomainSupport.fillMeetingVO(meeting, vo, false);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LegacyUploadAudioResponse uploadAndTriggerOfflineProcess(Long meetingId,
|
||||
Long promptId,
|
||||
String modelCode,
|
||||
boolean forceReplace,
|
||||
MultipartFile audioFile,
|
||||
LoginUser loginUser) throws IOException {
|
||||
if (meetingId == null) {
|
||||
throw new RuntimeException("meeting_id 不能为空");
|
||||
}
|
||||
if (audioFile == null || audioFile.isEmpty()) {
|
||||
throw new RuntimeException("audio_file 不能为空");
|
||||
}
|
||||
|
||||
Meeting meeting = meetingAccessService.requireMeeting(meetingId);
|
||||
meetingAccessService.assertCanEditMeeting(meeting, loginUser);
|
||||
|
||||
if (!forceReplace && meeting.getAudioUrl() != null && !meeting.getAudioUrl().isBlank()) {
|
||||
throw new RuntimeException("会议已有音频,如需替换请设置 force_replace=true");
|
||||
}
|
||||
long transcriptCount = transcriptMapper.selectCount(new LambdaQueryWrapper<MeetingTranscript>()
|
||||
.eq(MeetingTranscript::getMeetingId, meetingId));
|
||||
if (transcriptCount > 0) {
|
||||
throw new RuntimeException("当前会议已有转录内容,旧安卓接口暂不支持替换已生成转录");
|
||||
}
|
||||
if (promptId != null && !promptTemplateService.isTemplateEnabledForUser(
|
||||
promptId,
|
||||
loginUser.getTenantId(),
|
||||
loginUser.getUserId(),
|
||||
loginUser.getIsPlatformAdmin(),
|
||||
loginUser.getIsTenantAdmin())) {
|
||||
throw new RuntimeException("Summary template unavailable");
|
||||
}
|
||||
|
||||
RealtimeMeetingRuntimeProfile profile = runtimeProfileResolver.resolve(
|
||||
loginUser.getTenantId(),
|
||||
null,
|
||||
resolveSummaryModelId(modelCode, loginUser.getTenantId()),
|
||||
promptId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
);
|
||||
|
||||
String stagingUrl = storeStagingAudio(audioFile);
|
||||
String relocatedUrl = meetingDomainSupport.relocateAudioUrl(meetingId, stagingUrl);
|
||||
meeting.setAudioUrl(relocatedUrl);
|
||||
meeting.setAudioSaveStatus(RealtimeMeetingAudioStorageService.STATUS_SUCCESS);
|
||||
meeting.setAudioSaveMessage(null);
|
||||
meeting.setStatus(1);
|
||||
meetingService.updateById(meeting);
|
||||
|
||||
resetOrCreateAsrTask(meetingId, profile);
|
||||
resetOrCreateSummaryTask(meetingId, profile);
|
||||
dispatchTasksAfterCommit(meetingId, meeting.getTenantId(), meeting.getCreatorId());
|
||||
|
||||
return new LegacyUploadAudioResponse(meetingId, relocatedUrl);
|
||||
}
|
||||
|
||||
private String joinIds(List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return ids.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
private LocalDateTime parseMeetingTime(String rawValue) {
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String value = rawValue.trim();
|
||||
try {
|
||||
return OffsetDateTime.parse(value).toLocalDateTime();
|
||||
} catch (DateTimeParseException ignored) {
|
||||
// 旧安卓可能发送带时区 ISO,也可能发送当前后端本地时间格式。
|
||||
}
|
||||
try {
|
||||
return LocalDateTime.parse(value);
|
||||
} catch (DateTimeParseException ignored) {
|
||||
// 继续兼容 yyyy-MM-dd HH:mm:ss。
|
||||
}
|
||||
try {
|
||||
return LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
} catch (DateTimeParseException ex) {
|
||||
throw new RuntimeException("会议时间格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeTags(Object rawTags) {
|
||||
if (rawTags == null) {
|
||||
return null;
|
||||
}
|
||||
if (rawTags instanceof Iterable<?> items) {
|
||||
return joinValues(items);
|
||||
}
|
||||
return String.valueOf(rawTags).trim();
|
||||
}
|
||||
|
||||
private String joinValues(Iterable<?> items) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Object item : items) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String value = String.valueOf(item).trim();
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(value);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private Long resolveSummaryModelId(String modelCode, Long tenantId) {
|
||||
if (modelCode == null || modelCode.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
LlmModel model = llmModelMapper.selectOne(new LambdaQueryWrapper<LlmModel>()
|
||||
.eq(LlmModel::getModelCode, modelCode.trim())
|
||||
.eq(LlmModel::getStatus, 1)
|
||||
.and(wrapper -> wrapper.eq(LlmModel::getTenantId, tenantId).or().eq(LlmModel::getTenantId, 0L))
|
||||
.orderByDesc(LlmModel::getTenantId)
|
||||
.last("LIMIT 1"));
|
||||
if (model == null) {
|
||||
throw new RuntimeException("LLM 模型不存在或未启用: " + modelCode);
|
||||
}
|
||||
return model.getId();
|
||||
}
|
||||
|
||||
private String storeStagingAudio(MultipartFile audioFile) throws IOException {
|
||||
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||
Path uploadDir = Paths.get(basePath, "audio");
|
||||
Files.createDirectories(uploadDir);
|
||||
|
||||
String originalName = sanitizeFileName(audioFile.getOriginalFilename());
|
||||
Path target = uploadDir.resolve(UUID.randomUUID() + "_" + originalName);
|
||||
Files.copy(audioFile.getInputStream(), target, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
String baseResourcePrefix = resourcePrefix.endsWith("/") ? resourcePrefix : resourcePrefix + "/";
|
||||
return baseResourcePrefix + "audio/" + target.getFileName();
|
||||
}
|
||||
|
||||
private String sanitizeFileName(String fileName) {
|
||||
String value = fileName == null || fileName.isBlank() ? "audio" : fileName;
|
||||
value = value.replace('\\', '/');
|
||||
int slashIndex = value.lastIndexOf('/');
|
||||
if (slashIndex >= 0) {
|
||||
value = value.substring(slashIndex + 1);
|
||||
}
|
||||
value = value.replaceAll("[^A-Za-z0-9._-]", "_");
|
||||
return value.isBlank() ? "audio" : value;
|
||||
}
|
||||
|
||||
private void resetOrCreateAsrTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) {
|
||||
AiTask task = findLatestTask(meetingId, "ASR");
|
||||
Map<String, Object> taskConfig = new HashMap<>();
|
||||
taskConfig.put("asrModelId", profile.getResolvedAsrModelId());
|
||||
taskConfig.put("useSpkId", profile.getResolvedUseSpkId());
|
||||
taskConfig.put("enableTextRefine", profile.getResolvedEnableTextRefine());
|
||||
taskConfig.put("hotWords", profile.getResolvedHotWords());
|
||||
resetOrCreateTask(task, meetingId, "ASR", taskConfig);
|
||||
}
|
||||
|
||||
private void resetOrCreateSummaryTask(Long meetingId, RealtimeMeetingRuntimeProfile profile) {
|
||||
AiTask task = findLatestTask(meetingId, "SUMMARY");
|
||||
Map<String, Object> taskConfig = new HashMap<>();
|
||||
taskConfig.put("summaryModelId", profile.getResolvedSummaryModelId());
|
||||
PromptTemplate template = promptTemplateService.getById(profile.getResolvedPromptId());
|
||||
if (template != null) {
|
||||
taskConfig.put("promptContent", template.getPromptContent());
|
||||
}
|
||||
resetOrCreateTask(task, meetingId, "SUMMARY", taskConfig);
|
||||
}
|
||||
|
||||
private AiTask findLatestTask(Long meetingId, String taskType) {
|
||||
return aiTaskService.getOne(new LambdaQueryWrapper<AiTask>()
|
||||
.eq(AiTask::getMeetingId, meetingId)
|
||||
.eq(AiTask::getTaskType, taskType)
|
||||
.orderByDesc(AiTask::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private void resetOrCreateTask(AiTask task, Long meetingId, String taskType, Map<String, Object> taskConfig) {
|
||||
if (task == null) {
|
||||
task = new AiTask();
|
||||
task.setMeetingId(meetingId);
|
||||
task.setTaskType(taskType);
|
||||
}
|
||||
task.setStatus(0);
|
||||
task.setTaskConfig(taskConfig);
|
||||
task.setRequestData(null);
|
||||
task.setResponseData(null);
|
||||
task.setResultFilePath(null);
|
||||
task.setErrorMsg(null);
|
||||
task.setStartedAt(null);
|
||||
task.setCompletedAt(null);
|
||||
if (task.getId() == null) {
|
||||
aiTaskService.save(task);
|
||||
} else {
|
||||
aiTaskService.updateById(task);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchTasksAfterCommit(Long meetingId, Long tenantId, Long userId) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
aiTaskService.dispatchTasks(meetingId, tenantId, userId);
|
||||
return;
|
||||
}
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
aiTaskService.dispatchTasks(meetingId, tenantId, userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String resolveCreatorName(LoginUser loginUser) {
|
||||
return loginUser.getDisplayName() != null ? loginUser.getDisplayName() : loginUser.getUsername();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.imeeting.support;
|
||||
|
||||
import com.unisbase.security.LoginUser;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Component
|
||||
public class TaskSecurityContextRunner {
|
||||
|
||||
public void runAsTenantUser(Long tenantId, Long userId, Runnable action) {
|
||||
runWithUser(buildTenantUser(tenantId, userId), () -> {
|
||||
action.run();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public <T> T callAsTenantUser(Long tenantId, Long userId, Supplier<T> supplier) {
|
||||
return runWithUser(buildTenantUser(tenantId, userId), supplier);
|
||||
}
|
||||
|
||||
public <T> T callAsPlatformAdmin(Supplier<T> supplier) {
|
||||
return runWithUser(buildPlatformAdmin(), supplier);
|
||||
}
|
||||
|
||||
private <T> T runWithUser(LoginUser loginUser, Supplier<T> supplier) {
|
||||
SecurityContext previousContext = SecurityContextHolder.getContext();
|
||||
try {
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()));
|
||||
SecurityContextHolder.setContext(context);
|
||||
return supplier.get();
|
||||
} finally {
|
||||
SecurityContextHolder.clearContext();
|
||||
SecurityContextHolder.setContext(previousContext);
|
||||
}
|
||||
}
|
||||
|
||||
private LoginUser buildTenantUser(Long tenantId, Long userId) {
|
||||
LoginUser loginUser = new LoginUser(
|
||||
userId,
|
||||
tenantId,
|
||||
userId == null ? "async-user" : "async-user-" + userId,
|
||||
false,
|
||||
false,
|
||||
Collections.emptySet()
|
||||
);
|
||||
loginUser.setDisplayName(loginUser.getUsername());
|
||||
return loginUser;
|
||||
}
|
||||
|
||||
private LoginUser buildPlatformAdmin() {
|
||||
LoginUser loginUser = new LoginUser(
|
||||
0L,
|
||||
0L,
|
||||
"async-platform-admin",
|
||||
true,
|
||||
false,
|
||||
Collections.emptySet()
|
||||
);
|
||||
loginUser.setDisplayName("async-platform-admin");
|
||||
return loginUser;
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,6 @@ unisbase:
|
|||
- /actuator/health
|
||||
- /api/static/**
|
||||
- /ws/**
|
||||
- /api/android/**
|
||||
internal-auth:
|
||||
enabled: true
|
||||
header-name: X-Internal-Secret
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration scan="true" scanPeriod="60 seconds">
|
||||
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="imeeting-backend"/>
|
||||
<springProperty scope="context" name="APP_LOG_PATH" source="logging.file.path" defaultValue="./logs"/>
|
||||
|
||||
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n"/>
|
||||
<property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{64} - %msg%n"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${APP_LOG_PATH}/${APP_NAME}.log</file>
|
||||
<encoder>
|
||||
<pattern>${FILE_LOG_PATTERN}</pattern>
|
||||
<charset>UTF-8</charset>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${APP_LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<maxFileSize>${LOG_MAX_FILE_SIZE:-100MB}</maxFileSize>
|
||||
<maxHistory>${LOG_MAX_HISTORY:-30}</maxHistory>
|
||||
<totalSizeCap>${LOG_TOTAL_SIZE_CAP:-3GB}</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<springProfile name="dev">
|
||||
<logger name="io.grpc" level="DEBUG"/>
|
||||
<logger name="io.grpc.netty.shaded.io.grpc.netty" level="DEBUG"/>
|
||||
<logger name="com.imeeting.config.grpc" level="DEBUG"/>
|
||||
<logger name="com.imeeting.grpc" level="DEBUG"/>
|
||||
<logger name="com.imeeting.service.realtime.impl.RealtimeMeetingGrpcSessionServiceImpl" level="DEBUG"/>
|
||||
<logger name="com.imeeting.service.realtime.impl.AsrUpstreamBridgeServiceImpl" level="DEBUG"/>
|
||||
</springProfile>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
|
|
@ -55,10 +55,10 @@ function resolveMenuIcon(icon?: string): ReactNode {
|
|||
type PermissionMenuNode = SysPermission & {
|
||||
children?: PermissionMenuNode[];
|
||||
};
|
||||
type CachedUserProfile = { displayName?: string; username?: string; avatar_url?: string };
|
||||
type CachedUserProfile = { displayName?: string; username?: string; avatarUrl?: string };
|
||||
|
||||
function getAvatarUrl(profile?: CachedUserProfile | null) {
|
||||
return profile?.avatar_url?.trim() || "";
|
||||
return profile?.avatarUrl?.trim() || "";
|
||||
}
|
||||
export default function AppLayout() {
|
||||
const { message } = App.useApp();
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ export default function Users() {
|
|||
try {
|
||||
setAvatarUploading(true);
|
||||
const url = await uploadPlatformAsset(file);
|
||||
form.setFieldValue("avatar_url", url);
|
||||
form.setFieldValue("avatarUrl", url);
|
||||
message.success(t("common.success"));
|
||||
} finally {
|
||||
setAvatarUploading(false);
|
||||
|
|
@ -251,7 +251,7 @@ export default function Users() {
|
|||
displayName: values.displayName,
|
||||
email: values.email,
|
||||
phone: values.phone,
|
||||
avatar_url: values.avatar_url,
|
||||
avatarUrl: values.avatarUrl,
|
||||
status: values.status,
|
||||
isPlatformAdmin: values.isPlatformAdmin
|
||||
};
|
||||
|
|
@ -294,7 +294,7 @@ export default function Users() {
|
|||
key: "user",
|
||||
render: (_: any, record: SysUser) => (
|
||||
<Space>
|
||||
<Avatar className="user-avatar-placeholder" size={40} src={record.avatar_url || undefined} icon={record.avatar_url ? undefined : <UserOutlined />} />
|
||||
<Avatar className="user-avatar-placeholder" size={40} src={record.avatarUrl || undefined} icon={record.avatarUrl ? undefined : <UserOutlined />} />
|
||||
<div>
|
||||
<Space size={4}>
|
||||
<div className="user-display-name">{record.displayName}</div>
|
||||
|
|
@ -415,7 +415,7 @@ export default function Users() {
|
|||
<Col span={12}><Form.Item label={t("users.email")} name="email"><Input placeholder={t("usersExt.emailPlaceholder")} className="tabular-nums" /></Form.Item></Col>
|
||||
<Col span={12}><Form.Item label={t("users.phone")} name="phone"><Input placeholder={t("users.phone")} className="tabular-nums" /></Form.Item></Col>
|
||||
</Row>
|
||||
<Form.Item label={t("profile.avatarUrl")} name="avatar_url"><Input placeholder={t("profile.avatarUrlPlaceholder")} /></Form.Item>
|
||||
<Form.Item label={t("profile.avatarUrl")} name="avatarUrl"><Input placeholder={t("profile.avatarUrlPlaceholder")} /></Form.Item>
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={handleAvatarUpload}>
|
||||
<Button icon={<UploadOutlined />} loading={avatarUploading} style={{ marginBottom: 16 }}>{t("profile.uploadAvatar")}</Button>
|
||||
</Upload>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default function Profile() {
|
|||
try {
|
||||
setAvatarUploading(true);
|
||||
const url = await uploadPlatformAsset(file);
|
||||
profileForm.setFieldValue("avatar_url", url);
|
||||
profileForm.setFieldValue("avatarUrl", url);
|
||||
message.success(t("common.success"));
|
||||
} finally {
|
||||
setAvatarUploading(false);
|
||||
|
|
@ -96,7 +96,7 @@ export default function Profile() {
|
|||
};
|
||||
|
||||
const renderValue = (value?: string) => value || "-";
|
||||
const avatarUrlValue = Form.useWatch("avatar_url", profileForm) as string | undefined;
|
||||
const avatarUrlValue = Form.useWatch("avatarUrl", profileForm) as string | undefined;
|
||||
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
||||
|
||||
return (
|
||||
|
|
@ -137,7 +137,7 @@ export default function Profile() {
|
|||
<Form.Item label={t("users.phone")} name="phone">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("profile.avatarUrl")} name="avatar_url">
|
||||
<Form.Item label={t("profile.avatarUrl")} name="avatarUrl">
|
||||
<Input placeholder={t("profile.avatarUrlPlaceholder")} />
|
||||
</Form.Item>
|
||||
<Upload accept="image/*" showUploadList={false} beforeUpload={handleAvatarUpload}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface SysUser extends BaseEntity {
|
|||
displayName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
avatarUrl?: string;
|
||||
password?: string;
|
||||
passwordHash?: string;
|
||||
tenantId: number;
|
||||
|
|
@ -28,6 +29,7 @@ export interface UserProfile {
|
|||
displayName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
avatarUrl?: string;
|
||||
status?: number;
|
||||
isAdmin: boolean;
|
||||
isPlatformAdmin?: boolean;
|
||||
|
|
|
|||
Loading…
Reference in New Issue