feat: 公众端个人资料更新接口 DTO 校验与前端对齐

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-16 17:41:54 +08:00
parent 9913b3453b
commit 92f7bf5419
5 changed files with 117 additions and 29 deletions

View File

@ -6,11 +6,13 @@ import com.lesingle.common.util.SecurityUtil;
import com.lesingle.modules.biz.contest.entity.BizContestRegistration;
import com.lesingle.modules.pub.service.PublicActivityService;
import com.lesingle.modules.pub.service.PublicInteractionService;
import com.lesingle.modules.pub.dto.PublicProfileUpdateDto;
import com.lesingle.modules.pub.service.PublicProfileService;
import com.lesingle.modules.pub.service.PublicUserWorkService;
import com.lesingle.modules.ugc.entity.UgcWork;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -34,11 +36,10 @@ public class PublicProfileController {
}
@PutMapping("/profile")
@Operation(summary = "更新个人资料")
public Result<Void> updateProfile(@RequestBody Map<String, String> body) {
@Operation(summary = "更新个人资料", description = "请求体 JSONnickname必填、city、gendermale/female、avatar可选不传则不改头像")
public Result<Void> updateProfile(@Valid @RequestBody PublicProfileUpdateDto dto) {
Long userId = SecurityUtil.getCurrentUserId();
publicProfileService.updateProfile(userId,
body.get("nickname"), body.get("city"), body.get("avatar"), body.get("gender"));
publicProfileService.updateProfile(userId, dto);
return Result.success();
}

View File

@ -0,0 +1,30 @@
package com.lesingle.modules.pub.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 公众端我的个人资料更新与前端 PUT /public/mine/profile 请求体一致camelCase
*/
@Data
@Schema(description = "公众端更新个人资料")
public class PublicProfileUpdateDto {
@NotBlank(message = "昵称不能为空")
@Size(min = 1, max = 20, message = "昵称长度为1-20个字符")
@Schema(description = "昵称")
private String nickname;
@Size(max = 50, message = "城市长度不超过50个字符")
@Schema(description = "城市,可空表示清空")
private String city;
@Schema(description = "性别male / female可空表示清空")
private String gender;
@Size(max = 512, message = "头像地址过长")
@Schema(description = "头像 URL不传则不修改")
private String avatar;
}

View File

@ -4,10 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.lesingle.common.constants.TenantConstants;
import com.lesingle.common.enums.CommonStatus;
import com.lesingle.common.enums.ErrorCode;
import com.lesingle.common.enums.UserSource;
import com.lesingle.common.enums.UserType;
import com.lesingle.common.exception.BusinessException;
import com.lesingle.modules.pub.dto.CreateChildDto;
import com.lesingle.modules.pub.dto.PublicProfileUpdateDto;
import com.lesingle.modules.sys.entity.SysUser;
import com.lesingle.modules.sys.entity.SysTenant;
import com.lesingle.modules.sys.mapper.SysUserMapper;
@ -21,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
@ -62,15 +65,22 @@ public class PublicProfileService {
}
/**
* 更新个人资料
* 更新个人资料 {@link PublicProfileUpdateDto} 字段一致
*/
public void updateProfile(Long userId, String nickname, String city, String avatar, String gender) {
public void updateProfile(Long userId, PublicProfileUpdateDto dto) {
String g = dto.getGender();
if (StringUtils.hasText(g) && !"male".equals(g) && !"female".equals(g)) {
throw BusinessException.of(ErrorCode.BAD_REQUEST, "性别须为 male 或 female");
}
LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SysUser::getId, userId);
if (nickname != null) wrapper.set(SysUser::getNickname, nickname);
if (city != null) wrapper.set(SysUser::getCity, city);
if (avatar != null) wrapper.set(SysUser::getAvatar, avatar);
if (gender != null) wrapper.set(SysUser::getGender, gender);
wrapper.set(SysUser::getNickname, dto.getNickname().trim());
wrapper.set(SysUser::getCity, StringUtils.hasText(dto.getCity()) ? dto.getCity().trim() : null);
wrapper.set(SysUser::getGender, StringUtils.hasText(g) ? g.trim() : null);
if (dto.getAvatar() != null) {
wrapper.set(SysUser::getAvatar, StringUtils.hasText(dto.getAvatar()) ? dto.getAvatar().trim() : null);
}
sysUserMapper.update(null, wrapper);
}

View File

@ -107,6 +107,8 @@ export interface PublicUser {
phone: string | null;
city: string | null;
avatar: string | null;
/** 与后端 SysUser.gender 一致male / female */
gender?: string | null;
tenantId: number;
tenantCode: string;
userSource: string;
@ -118,6 +120,14 @@ export interface PublicUser {
childrenCount?: number;
}
/** PUT /public/mine/profile 请求体,与后端 PublicProfileUpdateDto 对齐 */
export interface PublicProfileUpdatePayload {
nickname: string;
city?: string;
gender?: string;
avatar?: string;
}
export interface LoginResponse {
token: string;
user: PublicUser;
@ -144,12 +154,8 @@ export const publicAuthApi = {
export const publicProfileApi = {
getProfile: (): Promise<PublicUser> => publicApi.get("/public/mine/profile"),
updateProfile: (data: {
nickname?: string;
city?: string;
avatar?: string;
gender?: string;
}) => publicApi.put("/public/mine/profile", data),
updateProfile: (data: PublicProfileUpdatePayload) =>
publicApi.put("/public/mine/profile", data),
};
// ==================== 子女管理 ====================

View File

@ -70,17 +70,27 @@
<!-- 编辑个人信息弹窗 -->
<a-modal v-model:open="showEditModal" title="编辑个人信息" :footer="null" :width="400" centered>
<a-form layout="vertical" @finish="handleSaveProfile" style="margin-top: 16px;">
<a-form-item label="昵称" :rules="[{ required: true, message: '请输入昵称' }]">
<a-form
:model="editForm"
layout="vertical"
autocomplete="off"
@finish="handleSaveProfile"
style="margin-top: 16px;"
>
<a-form-item
label="昵称"
name="nickname"
:rules="[{ required: true, message: '请输入昵称' }, { max: 20, message: '最多20个字符' }]"
>
<a-input v-model:value="editForm.nickname" placeholder="你的昵称" :maxlength="20" />
</a-form-item>
<a-form-item label="性别">
<a-form-item label="性别" name="gender">
<a-select v-model:value="editForm.gender" placeholder="选择性别" allow-clear>
<a-select-option value="male"></a-select-option>
<a-select-option value="female"></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="城市">
<a-form-item label="城市" name="city" :rules="[{ max: 50, message: '最多50个字符' }]">
<a-input v-model:value="editForm.city" placeholder="你所在的城市" :maxlength="50" />
</a-form-item>
<a-button type="primary" html-type="submit" block :loading="editLoading"
@ -104,7 +114,7 @@ import {
LogoutOutlined,
EditOutlined,
} from "@ant-design/icons-vue"
import { publicProfileApi, publicMineApi } from "@/api/public"
import { publicProfileApi, publicMineApi, type PublicProfileUpdatePayload } from "@/api/public"
import { useAicreateStore } from "@/stores/aicreate"
const router = useRouter()
@ -149,12 +159,39 @@ const childrenDesc = computed(() => {
return count > 0 ? `已创建 ${count} 个子女账号` : "为子女创建独立账号"
})
/** 与登录态 localStorage `public_user` 合并,避免改资料后其它页仍显示旧昵称 */
const syncPublicUserCache = (patch: { nickname?: string; city?: string | null; gender?: string | null; avatar?: string | null }) => {
try {
const raw = localStorage.getItem("public_user")
if (!raw) return
const prev = JSON.parse(raw) as Record<string, unknown>
localStorage.setItem(
"public_user",
JSON.stringify({
...prev,
...(patch.nickname !== undefined && { nickname: patch.nickname }),
...(patch.city !== undefined && { city: patch.city }),
...(patch.gender !== undefined && { gender: patch.gender }),
...(patch.avatar !== undefined && { avatar: patch.avatar }),
}),
)
} catch {
/* ignore */
}
}
const fetchProfile = async () => {
try {
user.value = await publicProfileApi.getProfile()
editForm.nickname = user.value?.nickname || ""
editForm.city = user.value?.city || ""
editForm.gender = user.value?.gender || ""
syncPublicUserCache({
nickname: user.value?.nickname,
city: user.value?.city ?? null,
gender: user.value?.gender ?? null,
avatar: user.value?.avatar ?? null,
})
} catch {
message.error("获取个人信息失败,请重新登录")
handleLogout()
@ -172,17 +209,21 @@ const fetchCounts = async () => {
const handleSaveProfile = async () => {
editLoading.value = true
const nickname = editForm.nickname.trim()
const payload: PublicProfileUpdatePayload = {
nickname,
...(editForm.city?.trim() ? { city: editForm.city.trim() } : { city: "" }),
...(editForm.gender ? { gender: editForm.gender } : { gender: "" }),
}
try {
await publicProfileApi.updateProfile({
nickname: editForm.nickname,
city: editForm.city || undefined,
gender: editForm.gender || undefined,
})
await publicProfileApi.updateProfile(payload)
message.success("保存成功")
showEditModal.value = false
fetchProfile()
} catch {
message.error("保存失败")
await fetchProfile()
} catch (e: unknown) {
const msg =
(e as { response?: { data?: { message?: string } } })?.response?.data?.message || "保存失败"
message.error(msg)
} finally {
editLoading.value = false
}