当前位置: 首页 > news >正文

账户维护、登出与多模态文件独立接口

1)登出:Header 或 Query 传 Token

@PostMapping("/logout")

public Result<Void> logout(

@RequestHeader(value = "Authorization", required = false) String authorization,

@RequestParam(value = "token", required = false) String token

) {

return loginSevice.logout(authorization, token);

}

2)注册:username+ 用当前请求拼publicBaseUrl(头像 URL)

@PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public Result<AuthResponse> register(

@RequestParam("username") String username,

@RequestParam("phone") String phone,

@RequestParam("password") String password,

@RequestParam(value = "avatar", required = false) MultipartFile avatar,

HttpServletRequest servletRequest

) {

RegisterRequest request = new RegisterRequest(username, phone, password);

String publicBaseUrl = ServletUriComponentsBuilder.fromRequestUri(servletRequest)

.replacePath(servletRequest.getContextPath())

.replaceQuery(null)

.build()

.toUriString();

return loginSevice.register(request, avatar, publicBaseUrl);

}

3)更新当前用户:昵称 / 头像 multipart,校验后save

String normalizedUsername = trimToNull(username);

boolean hasUsername = StringUtils.hasText(normalizedUsername);

boolean hasAvatar = avatar != null && !avatar.isEmpty();

if (!hasUsername && !hasAvatar) {

return failure("A0440", "username or avatar is required");

}

if (hasUsername && normalizedUsername.length() > 50) {

return failure("A0441", "username length must be <= 50");

}

UserInfoEntity user = authResult.getData();

if (hasUsername) {

user.setUsername(normalizedUsername);

}

if (hasAvatar) {

String extension = resolveAvatarExtension(avatar);

if (!StringUtils.hasText(extension)) {

return failure("A0413", "avatar format is invalid, only jpg/jpeg/png/gif/webp is supported");

}

String publicBaseUrl = ServletUriComponentsBuilder.fromRequestUri(servletRequest)

.replacePath(servletRequest.getContextPath())

.replaceQuery(null)

.build()

.toUriString();

try {

user.setAvatarUrl(saveAvatarFile(avatar, extension, publicBaseUrl));

} catch (IOException ex) {

return failure("A0414", "avatar upload failed");

}

}

user = userInfoRepository.save(user);

4)健康档案 GET:loadOrInitHealthProfile无则创建

@GetMapping("/health-profile")

public Result<HealthProfileResponse> getHealthProfile(

@RequestHeader(value = "Authorization", required = false) String authorization,

@RequestParam(value = "token", required = false) String tokenParam

) {

Result<UserInfoEntity> authResult = authenticate(authorization, tokenParam);

if (authResult.isFail()) {

return failure(authResult.getCode(), authResult.getMessage());

}

UserInfoEntity user = authResult.getData();

HealthProfileEntity profile = loadOrInitHealthProfile(user.getUserId());

return success(toHealthProfileResponse(profile), "get health profile success");

}

private HealthProfileEntity loadOrInitHealthProfile(Long userId) {

Optional<HealthProfileEntity> optionalProfile = healthProfileRepository.findByUserId(userId);

if (optionalProfile.isPresent()) {

return optionalProfile.get();

}

HealthProfileEntity profile = new HealthProfileEntity();

profile.setUserId(userId);

return healthProfileRepository.save(profile);

}

5)独立上传:fileType白名单 + 可选messageId+ 写FileIndex

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public Result<FileUploadResponse> upload(

@RequestHeader(value = "Authorization", required = false) String authorization,

@RequestParam(value = "token", required = false) String tokenParam,

@RequestParam("fileType") String fileType,

@RequestParam("file") MultipartFile file,

@RequestParam(value = "messageId", required = false) Long messageId,

HttpServletRequest servletRequest

) {

Result<UserInfoEntity> authResult = authenticate(authorization, tokenParam);

if (authResult.isFail()) {

return failure(authResult.getCode(), authResult.getMessage());

}

String normalizedFileType = trimToNull(fileType);

if (!StringUtils.hasText(normalizedFileType)) {

return failure("A0430", "fileType is required");

}

normalizedFileType = normalizedFileType.toUpperCase(Locale.ROOT);

if (!ALLOWED_FILE_TYPES.contains(normalizedFileType)) {

return failure("A0430", "fileType must be SYMPTOM_PIC, VOICE or PRESCRIPTION");

}

if (file == null || file.isEmpty()) {

return failure("A0431", "file is required");

}

String fileId = UUID.randomUUID().toString().replace("-", "");

String extension = resolveExtension(file.getOriginalFilename());

String storedName = StringUtils.hasText(extension) ? fileId + "." + extension : fileId;

Path rootPath = Paths.get(uploadRootDir).toAbsolutePath().normalize();

Path fileDirPath = rootPath.resolve(FILE_SUB_DIR).normalize();

Path targetPath = fileDirPath.resolve(storedName).normalize();

if (!targetPath.startsWith(fileDirPath)) {

return failure("A0434", "invalid file path");

}

try {

Files.createDirectories(fileDirPath);

file.transferTo(targetPath.toFile());

} catch (IOException ex) {

return failure("A0435", "save file failed");

}

String baseUrl = ServletUriComponentsBuilder.fromRequestUri(servletRequest)

.replacePath(servletRequest.getContextPath())

.replaceQuery(null)

.build()

.toUriString();

String objectUrl = baseUrl + PREFIX_UPLOADS + FILE_SUB_DIR + "/" + storedName;

FileIndexEntity entity = new FileIndexEntity();

entity.setFileId(fileId);

entity.setUserId(authResult.getData().getUserId());

entity.setMessageId(messageId);

entity.setFileType(normalizedFileType);

entity.setObjectUrl(objectUrl);

entity.setFileSize((int) Math.max(1, Math.ceil(file.getSize() / 1024.0)));

entity = fileIndexRepository.save(entity);

6)下载:按fileId+userId查归属,路径规范化防穿越

Optional<FileIndexEntity> optionalFile = fileIndexRepository.findByFileIdAndUserId(normalizedFileId, authResult.getData().getUserId());

if (optionalFile.isEmpty()) {

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(failure("A0432", "file not found"));

}

FileIndexEntity entity = optionalFile.get();

Path filePath = resolveStoredFilePath(entity.getObjectUrl());

if (filePath == null || !Files.exists(filePath)) {

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(failure("A0433", "file resource not found"));

}

try {

Resource resource = new UrlResource(filePath.toUri());

String contentType = Files.probeContentType(filePath);

if (!StringUtils.hasText(contentType)) {

contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;

}

return ResponseEntity.ok()

.contentType(MediaType.parseMediaType(contentType))

.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filePath.getFileName() + "\"")

.body(resource);

} catch (MalformedURLException ex) {

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failure("A0436", "file resource open failed"));

} catch (IOException ex) {

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(failure("A0437", "file content type detect failed"));

}

}

private Path resolveStoredFilePath(String objectUrl) {

String normalizedPath = trimToNull(objectUrl);

if (!StringUtils.hasText(normalizedPath)) {

return null;

}

if (normalizedPath.startsWith("http://") || normalizedPath.startsWith("https://")) {

normalizedPath = URI.create(normalizedPath).getPath();

}

if (!normalizedPath.startsWith(PREFIX_UPLOADS)) {

return null;

}

String relativePath = normalizedPath.substring(PREFIX_UPLOADS.length());

Path rootPath = Paths.get(uploadRootDir).toAbsolutePath().normalize();

Path filePath = rootPath.resolve(relativePath).normalize();

if (!filePath.startsWith(rootPath)) {

return null;

}

return filePath;

}

http://www.jsqmd.com/news/779092/

相关文章:

  • 嵌入式安全关键系统开发:形式化需求验证工具STIMULUS的核心价值与实践
  • 好用的WMS解决方案哪家好
  • 2026年4月行业内热门的调节阀供应商推荐,电站阀/止回阀/水力控制阀/铜阀门/闸阀/调节阀/截止阀,调节阀实力厂家推荐 - 品牌推荐师
  • 告别低效采集!用MaixHub+K210+Mx_yolov3打造端到端物体识别项目(附数据集处理技巧)
  • VSCode 插件安装失败显示 ECONNRESET 如何处理?
  • 搞网络安全的,谁还没几个压箱底绝活?可AI来了以后呢?
  • 2026 外贸财税 | 电商税务机构排行榜:专业 + 技术 + 避坑全解析,这两家上海机构凭实力领跑 - 速递信息
  • 【数据分析】基于哈里斯鹰优化算法优化ANFIS参数进行鸢尾花分类附Matlab代码
  • 因为太贵、太拉、抢不到,我才试了 DeepSeek V4,结果真香了
  • 上饶AI搜索优化服务商评测:专业度核心维度对比 - 奔跑123
  • 物联网的本质回归:从技术堆栈到务实应用的设计哲学
  • 格排障顺序 + perf为主 + bcc辅助 + 结果验证在最后”。我给你一份标准生产级 SOP - 小镇
  • 【数据分析】基于 AHP-EW 聚类融合的煤矿顶板风险预警模型附Matlab代码
  • 2026年CE认证|EAC认证|ROHS测试机构排行榜单推荐:专业视角下的检测认证服务商深度解析 - 速递信息
  • 手把手教你用Nginx给NPS管理后台加SSL证书(含免费证书申请与配置全流程)
  • 速看|营销智脑 V6 本周上线,四大维度焕新,解锁全域营销新玩法
  • Git Worktree Manager:高效管理多分支并行开发的Git增强工具
  • CodeAlive MCP:基于GraphRAG的AI编码助手深度上下文引擎实战
  • 70 岁吕良伟分享科学养生:逆龄状态来自 16+8 轻断食与营养均衡实践
  • MAX2140 SDARS接收器架构与射频前端设计解析
  • 基于DE-SARSA强化学习的跳频通信系统智能抗干扰策略matlab仿真
  • Productivity 的核心不是任务管理:拆解 Claude 的 L1/L2 记忆缓存
  • 解放双手的碧蓝航线全自动脚本:Alas让你的游戏时间更有价值
  • Day16-Java
  • 开源项目蓝图:从TypeScript到Vite的工程化实践与自动化流程
  • 斯坦福大学造了一个“AI医生考场“,结果最强的AI也只考了46分
  • Hugging Face 发布 Reachy Mini 机器人智能体工具包,支持自然语言开发;OpenAI 首款 AI 手机有望明年上半年量产丨日报
  • 模具工装全生命周期智能化管理,工业Agent驱动的落地方法详解
  • CPA学习软件怎么选?揽星会计CPA一个APP搞定全周期备考 - 速递信息
  • AI应用开发之特征值与SVD分解详解