feat(profile): 新增C端个人资料与设备相册功能

- 新增个人资料模块,支持获取、修改昵称和个性签名
- 实现头像上传与校验,支持JPEG/PNG格式及尺寸限制
- 添加app_user表signature字段的可重复执行SQL脚本
- 新增设备相册控制器,支持读取、收藏和删除相册图片
- 编写AppProfileService和DeviceAlbumController单元测试
This commit is contained in:
tongtong committed 2026-11-09 09:23:43 +08:00
1 parent 8d61c63c8b
commit 4dd7d98ce9
9 files changed
+382

No files matched your search

@@ -0,0 +1,64 @@
package com.youlai.boot.app.profile;
import com.youlai.boot.app.profile.mapper.AppProfileMapper;
import com.youlai.boot.app.profile.model.AppProfileForm;
import com.youlai.boot.app.profile.model.AppProfileVO;
import com.youlai.boot.app.profile.service.AppProfileService;
import com.youlai.boot.common.config.StorageDir;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.support.file.model.FileInfo;
import com.youlai.boot.support.file.service.FileService;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class AppProfileServiceTest {
private final AppProfileMapper mapper = mock(AppProfileMapper.class);
private final FileService files = mock(FileService.class);
private final AppProfileService service = new AppProfileService(mapper, files);
@Test
void updatesOnlyCurrentUsersTextFields() {
AppProfileVO current = new AppProfileVO();
current.setId(7L);
when(mapper.findCurrent(7L)).thenReturn(current);
AppProfileForm form = new AppProfileForm();
form.setNickname(" 家人 ");
form.setSignature(" 你好 ");
service.update(7L, form);
verify(mapper).updateProfile(7L, "家人", "你好");
verify(mapper, never()).updateAvatar(anyLong(), anyString());
verifyNoInteractions(files);
}
@Test
void rejectsInvalidAvatarBeforeUploading() {
when(mapper.findCurrent(7L)).thenReturn(new AppProfileVO());
MockMultipartFile invalid = new MockMultipartFile(
"file", "avatar.png", "image/png", "invalid".getBytes());
assertThrows(BusinessException.class, () -> service.uploadAvatar(7L, invalid));
verifyNoInteractions(files);
}
@Test
void removesNewUploadWhenUserCannotBeUpdated() {
when(mapper.findCurrent(7L)).thenReturn(new AppProfileVO());
byte[] png = Base64.getDecoder().decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGP4DwQACfsD/fteaysAAAAASUVORK5CYII=");
MockMultipartFile image = new MockMultipartFile("file", "avatar.png", "image/png", png);
FileInfo uploaded = new FileInfo();
uploaded.setUrl("https://files.example/app/avatar/new.png");
when(files.uploadFile(image, StorageDir.AVATAR)).thenReturn(uploaded);
when(mapper.updateAvatar(7L, uploaded.getUrl())).thenReturn(0);
assertThrows(BusinessException.class, () -> service.uploadAvatar(7L, image));
verify(files).deleteFile(uploaded.getUrl());
}
}
@@ -0,0 +1,54 @@
package com.youlai.boot.device;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.device.controller.DeviceAlbumController;
import com.youlai.boot.device.model.vo.DevicePictureVO;
import com.youlai.boot.device.service.DevicePictureService;
import jakarta.validation.Validation;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class DeviceAlbumControllerTest {
private final DevicePictureService pictures = mock(DevicePictureService.class);
private final DeviceAlbumController controller = new DeviceAlbumController(pictures);
@Test
void listUsesAuthenticatedDeviceAndRetainsFavoriteState() {
DevicePictureVO photo = new DevicePictureVO();
photo.setUrl("https://example.com/photo.jpg");
photo.setFavorite(1);
when(pictures.listPictures("device-a")).thenReturn(List.of(photo));
assertThat(controller.list("device-a").getData()).containsExactly(photo);
verify(pictures).listPictures("device-a");
verifyNoMoreInteractions(pictures);
}
@Test
void favoriteAndDeleteUseAuthenticatedDeviceAndExactUrl() {
String url = "https://example.com/photo.jpg?key=a%2Bb";
when(pictures.updateFavorite("device-a", url, true)).thenReturn(true);
assertThat(controller.favorite("device-a", url, 1).getCode()).isEqualTo(ResultCode.SUCCESS.getCode());
controller.delete("device-a", url);
verify(pictures).updateFavorite("device-a", url, true);
verify(pictures).deletePicture("device-a", url);
verifyNoMoreInteractions(pictures);
}
@Test
void missingPictureDoesNotReportFavoriteSuccess() {
assertThat(controller.favorite("device-a", "missing", 0).getCode()).isNotEqualTo(ResultCode.SUCCESS.getCode());
}
@Test
void mutationRejectsBlankIdentityAndInvalidFavorite() throws Exception {
try (var factory = Validation.buildDefaultValidatorFactory()) {
var validator = factory.getValidator().forExecutables();
var method = DeviceAlbumController.class.getMethod("favorite", String.class, String.class, Integer.class);
assertThat(validator.validateParameters(controller, method, new Object[]{"device-a", "url", 2})).isNotEmpty();
assertThat(validator.validateParameters(controller, method, new Object[]{"", " ", 1})).hasSize(2);
assertThat(validator.validateParameters(controller, method, new Object[]{"device-a", "url", 0})).isEmpty();
}
}
}