From 53b2fedb7a5055c3622397223b69d9fc03b2fe26 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 2 Sep 2026 10:23:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=A4=B4=E5=83=8F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yunhai/userbase/aspect/OapiLogAspect.java | 13 +++++++ .../userbase/config/MyWebAppConfigurer.java | 2 + .../controller/oapi/UserController.java | 12 ++++++ .../userbase/service/ISysUserService.java | 10 +++++ .../userbase/service/impl/SysUserService.java | 37 +++++++++++++++++++ .../com/yunhai/userbase/utils/FileUtil.java | 2 +- 6 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java b/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java index c204f7b..b9027a6 100644 --- a/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java +++ b/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java @@ -24,13 +24,16 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * 开放接口(/oapi/**)调用日志切面。
@@ -199,6 +202,16 @@ public class OapiLogAspect { || arg instanceof HttpSession) { continue; } + // multipart 文件不序列化内容(避免读文件字节/写爆 request_params),仅记文件名元数据 + if (arg instanceof MultipartFile) { + MultipartFile mf = (MultipartFile) arg; + Map meta = new LinkedHashMap(); + meta.put("fileName", mf.getOriginalFilename()); + meta.put("contentType", mf.getContentType()); + meta.put("size", mf.getSize()); + filtered.add(meta); + continue; + } filtered.add(arg); } if (filtered.isEmpty()) { diff --git a/src/main/java/com/yunhai/userbase/config/MyWebAppConfigurer.java b/src/main/java/com/yunhai/userbase/config/MyWebAppConfigurer.java index c439d9d..6223b5f 100644 --- a/src/main/java/com/yunhai/userbase/config/MyWebAppConfigurer.java +++ b/src/main/java/com/yunhai/userbase/config/MyWebAppConfigurer.java @@ -18,11 +18,13 @@ public class MyWebAppConfigurer implements WebMvcConfigurer { String ICON_FILE_WEB_PATH = "/upload/icon/"; String WORD_FILE_WEB_PATH = "/upload/word/"; String HTML_FILE_WEB_PATH = "/upload/html/"; + String AVATAR_FILE_WEB_PATH = "/upload/avatar/"; registry.addResourceHandler(IMG_FILE_WEB_PATH + "**").addResourceLocations("file:" + FILE_PATH + "/image/"); registry.addResourceHandler(File_FILE_WEB_PATH + "**").addResourceLocations("file:" + FILE_PATH + "/file/"); registry.addResourceHandler(ICON_FILE_WEB_PATH+"**").addResourceLocations("file:"+FILE_PATH+"/icon/"); registry.addResourceHandler(WORD_FILE_WEB_PATH+"**").addResourceLocations("file:"+FILE_PATH+"/word/"); registry.addResourceHandler(HTML_FILE_WEB_PATH+"**").addResourceLocations("file:"+FILE_PATH+"/html/"); + registry.addResourceHandler(AVATAR_FILE_WEB_PATH+"**").addResourceLocations("file:"+FILE_PATH+"/avatar/"); } diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java index 59c178b..50047c1 100644 --- a/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java +++ b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java @@ -33,6 +33,7 @@ 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 javax.servlet.http.HttpServletRequest; import java.util.ArrayList; @@ -236,6 +237,17 @@ public class UserController { sysUserService.changePassword(request.getUserId(), request.getOldPassword(), request.getNewPassword()); } + /** + * 修改用户头像:校验图片(非空/≤20MB/扩展名白名单)→ 压缩 → 存储并写入 profile_picture,
+ * 返回新头像完整 Web URL。multipart 直传(form 字段 userId + 文件 part file)。
+ * 与修改密码一致,仅由过滤器层强制应用签名;POST 自动记 OapiLog(文件内容不落库,仅记文件名元数据)。 + */ + @PostMapping("profilePicture") + public String profilePicture(@RequestParam("userId") Integer userId, + @RequestParam("file") MultipartFile file) { + return sysUserService.changeProfilePicture(userId, file); + } + /** * 设置用户在指定组上的状态(转出/恢复)。
* state=1 转出、state=0 恢复正常;仅作关系标记,不影响成员查询、组织链与下游推送。 diff --git a/src/main/java/com/yunhai/userbase/service/ISysUserService.java b/src/main/java/com/yunhai/userbase/service/ISysUserService.java index 95e1aaf..d36fc0a 100644 --- a/src/main/java/com/yunhai/userbase/service/ISysUserService.java +++ b/src/main/java/com/yunhai/userbase/service/ISysUserService.java @@ -197,6 +197,16 @@ public interface ISysUserService { */ void changePassword(Integer userId, String oldPassword, String newPassword); + /** + * 修改用户头像:校验图片(非空/≤20MB/扩展名白名单)→ 压缩 → 存储至 avatar 子目录 → + * 写入 profile_picture。oapi 开放接口专用;返回新头像完整 Web URL。 + * + * @param userId 用户ID + * @param file 头像图片文件(multipart) + * @return 新头像完整可访问 URL + */ + String changeProfilePicture(Integer userId, MultipartFile file); + /** * 开放接口专用:设置用户在指定组上的状态(转出/恢复)。
* 仅作关系标记,不影响成员查询、组织链与下游推送;同值重复调用幂等。
diff --git a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java index 67f7a9d..ffedd89 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java @@ -40,6 +40,8 @@ import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; import com.yunhai.userbase.model.sysuser.ExcelSysUser; import com.yunhai.userbase.utils.EasyPoiUtils; +import com.yunhai.userbase.utils.FileUtil; +import com.yunhai.userbase.utils.ImageCompressUtil; import com.yunhai.userbase.utils.ValidatorUtil; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; @@ -1223,6 +1225,41 @@ public class SysUserService implements ISysUserService { sysUserRepository.save(entity); } + /** 头像大小上限(与前端限制一致:20MB) */ + private static final long AVATAR_MAX_SIZE = 20L * 1024 * 1024; + /** 头像扩展名白名单(webp 由压缩工具原样兜底存储) */ + private static final Set AVATAR_ALLOWED_EXT = new HashSet<>( + Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp")); + + @Override + @Transactional + public String changeProfilePicture(Integer userId, MultipartFile file) { + SysUser entity = sysUserRepository.findById(userId) + .orElseThrow(() -> new DException("系统用户不存在")); + if (file == null || file.isEmpty()) { + throw new DException("请选择上传文件"); + } + if (file.getSize() > AVATAR_MAX_SIZE) { + throw new DException("图片大小不能超过20MB"); + } + String ext = StringUtils.lowerCase(cn.hutool.core.io.FileUtil.extName(file.getOriginalFilename())); + if (!AVATAR_ALLOWED_EXT.contains(ext)) { + throw new DException("仅支持 jpg/jpeg/png/gif/bmp/webp 格式图片"); + } + byte[] raw; + try { + raw = file.getBytes(); + } catch (IOException e) { + throw new DException("读取上传文件失败"); + } + byte[] data = ImageCompressUtil.compressAvatar(raw, ext); + String url = FileUtil.uploadBytes(data, "avatar", "." + ext); + entity.setProfilePicture(url); + entity.setUpdateUser(SYSTEM_USER_ID); + sysUserRepository.save(entity); + return url; + } + /** 校验手机号与身份证(有值才校验)。 */ private void validateProfile(SysUserRequest request) { if (StringUtils.isNotBlank(request.getPhone()) && !ValidatorUtil.isPhone(request.getPhone())) { diff --git a/src/main/java/com/yunhai/userbase/utils/FileUtil.java b/src/main/java/com/yunhai/userbase/utils/FileUtil.java index 4efe128..6a8cf09 100644 --- a/src/main/java/com/yunhai/userbase/utils/FileUtil.java +++ b/src/main/java/com/yunhai/userbase/utils/FileUtil.java @@ -354,7 +354,7 @@ public class FileUtil { * @param ext 固定后缀(带点) * @return 完整可访问的 Web URL */ - private static String uploadBytes(byte[] data, String subDir, String ext) { + public static String uploadBytes(byte[] data, String subDir, String ext) { if (data == null || data.length == 0 || subDir == null || subDir.trim().length() == 0) { throw new DException("上传内容为空"); }