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("上传内容为空");
}