feat: 公众端个人资料更新接口 DTO 校验与前端对齐
Made-with: Cursor
This commit is contained in:
parent
9913b3453b
commit
92f7bf5419
@ -6,11 +6,13 @@ import com.lesingle.common.util.SecurityUtil;
|
|||||||
import com.lesingle.modules.biz.contest.entity.BizContestRegistration;
|
import com.lesingle.modules.biz.contest.entity.BizContestRegistration;
|
||||||
import com.lesingle.modules.pub.service.PublicActivityService;
|
import com.lesingle.modules.pub.service.PublicActivityService;
|
||||||
import com.lesingle.modules.pub.service.PublicInteractionService;
|
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.PublicProfileService;
|
||||||
import com.lesingle.modules.pub.service.PublicUserWorkService;
|
import com.lesingle.modules.pub.service.PublicUserWorkService;
|
||||||
import com.lesingle.modules.ugc.entity.UgcWork;
|
import com.lesingle.modules.ugc.entity.UgcWork;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@ -34,11 +36,10 @@ public class PublicProfileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/profile")
|
@PutMapping("/profile")
|
||||||
@Operation(summary = "更新个人资料")
|
@Operation(summary = "更新个人资料", description = "请求体 JSON:nickname(必填)、city、gender(male/female)、avatar(可选,不传则不改头像)")
|
||||||
public Result<Void> updateProfile(@RequestBody Map<String, String> body) {
|
public Result<Void> updateProfile(@Valid @RequestBody PublicProfileUpdateDto dto) {
|
||||||
Long userId = SecurityUtil.getCurrentUserId();
|
Long userId = SecurityUtil.getCurrentUserId();
|
||||||
publicProfileService.updateProfile(userId,
|
publicProfileService.updateProfile(userId, dto);
|
||||||
body.get("nickname"), body.get("city"), body.get("avatar"), body.get("gender"));
|
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -4,10 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.lesingle.common.constants.TenantConstants;
|
import com.lesingle.common.constants.TenantConstants;
|
||||||
import com.lesingle.common.enums.CommonStatus;
|
import com.lesingle.common.enums.CommonStatus;
|
||||||
|
import com.lesingle.common.enums.ErrorCode;
|
||||||
import com.lesingle.common.enums.UserSource;
|
import com.lesingle.common.enums.UserSource;
|
||||||
import com.lesingle.common.enums.UserType;
|
import com.lesingle.common.enums.UserType;
|
||||||
import com.lesingle.common.exception.BusinessException;
|
import com.lesingle.common.exception.BusinessException;
|
||||||
import com.lesingle.modules.pub.dto.CreateChildDto;
|
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.SysUser;
|
||||||
import com.lesingle.modules.sys.entity.SysTenant;
|
import com.lesingle.modules.sys.entity.SysTenant;
|
||||||
import com.lesingle.modules.sys.mapper.SysUserMapper;
|
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.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
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<>();
|
LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<>();
|
||||||
wrapper.eq(SysUser::getId, userId);
|
wrapper.eq(SysUser::getId, userId);
|
||||||
if (nickname != null) wrapper.set(SysUser::getNickname, nickname);
|
wrapper.set(SysUser::getNickname, dto.getNickname().trim());
|
||||||
if (city != null) wrapper.set(SysUser::getCity, city);
|
wrapper.set(SysUser::getCity, StringUtils.hasText(dto.getCity()) ? dto.getCity().trim() : null);
|
||||||
if (avatar != null) wrapper.set(SysUser::getAvatar, avatar);
|
wrapper.set(SysUser::getGender, StringUtils.hasText(g) ? g.trim() : null);
|
||||||
if (gender != null) wrapper.set(SysUser::getGender, gender);
|
if (dto.getAvatar() != null) {
|
||||||
|
wrapper.set(SysUser::getAvatar, StringUtils.hasText(dto.getAvatar()) ? dto.getAvatar().trim() : null);
|
||||||
|
}
|
||||||
sysUserMapper.update(null, wrapper);
|
sysUserMapper.update(null, wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,8 @@ export interface PublicUser {
|
|||||||
phone: string | null;
|
phone: string | null;
|
||||||
city: string | null;
|
city: string | null;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
|
/** 与后端 SysUser.gender 一致:male / female */
|
||||||
|
gender?: string | null;
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
tenantCode: string;
|
tenantCode: string;
|
||||||
userSource: string;
|
userSource: string;
|
||||||
@ -118,6 +120,14 @@ export interface PublicUser {
|
|||||||
childrenCount?: number;
|
childrenCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** PUT /public/mine/profile 请求体,与后端 PublicProfileUpdateDto 对齐 */
|
||||||
|
export interface PublicProfileUpdatePayload {
|
||||||
|
nickname: string;
|
||||||
|
city?: string;
|
||||||
|
gender?: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
token: string;
|
token: string;
|
||||||
user: PublicUser;
|
user: PublicUser;
|
||||||
@ -144,12 +154,8 @@ export const publicAuthApi = {
|
|||||||
export const publicProfileApi = {
|
export const publicProfileApi = {
|
||||||
getProfile: (): Promise<PublicUser> => publicApi.get("/public/mine/profile"),
|
getProfile: (): Promise<PublicUser> => publicApi.get("/public/mine/profile"),
|
||||||
|
|
||||||
updateProfile: (data: {
|
updateProfile: (data: PublicProfileUpdatePayload) =>
|
||||||
nickname?: string;
|
publicApi.put("/public/mine/profile", data),
|
||||||
city?: string;
|
|
||||||
avatar?: string;
|
|
||||||
gender?: string;
|
|
||||||
}) => publicApi.put("/public/mine/profile", data),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==================== 子女管理 ====================
|
// ==================== 子女管理 ====================
|
||||||
|
|||||||
@ -70,17 +70,27 @@
|
|||||||
|
|
||||||
<!-- 编辑个人信息弹窗 -->
|
<!-- 编辑个人信息弹窗 -->
|
||||||
<a-modal v-model:open="showEditModal" title="编辑个人信息" :footer="null" :width="400" centered>
|
<a-modal v-model:open="showEditModal" title="编辑个人信息" :footer="null" :width="400" centered>
|
||||||
<a-form layout="vertical" @finish="handleSaveProfile" style="margin-top: 16px;">
|
<a-form
|
||||||
<a-form-item label="昵称" :rules="[{ required: true, message: '请输入昵称' }]">
|
: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-input v-model:value="editForm.nickname" placeholder="你的昵称" :maxlength="20" />
|
||||||
</a-form-item>
|
</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 v-model:value="editForm.gender" placeholder="选择性别" allow-clear>
|
||||||
<a-select-option value="male">男</a-select-option>
|
<a-select-option value="male">男</a-select-option>
|
||||||
<a-select-option value="female">女</a-select-option>
|
<a-select-option value="female">女</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</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-input v-model:value="editForm.city" placeholder="你所在的城市" :maxlength="50" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-button type="primary" html-type="submit" block :loading="editLoading"
|
<a-button type="primary" html-type="submit" block :loading="editLoading"
|
||||||
@ -104,7 +114,7 @@ import {
|
|||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
} from "@ant-design/icons-vue"
|
} from "@ant-design/icons-vue"
|
||||||
import { publicProfileApi, publicMineApi } from "@/api/public"
|
import { publicProfileApi, publicMineApi, type PublicProfileUpdatePayload } from "@/api/public"
|
||||||
import { useAicreateStore } from "@/stores/aicreate"
|
import { useAicreateStore } from "@/stores/aicreate"
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -149,12 +159,39 @@ const childrenDesc = computed(() => {
|
|||||||
return count > 0 ? `已创建 ${count} 个子女账号` : "为子女创建独立账号"
|
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 () => {
|
const fetchProfile = async () => {
|
||||||
try {
|
try {
|
||||||
user.value = await publicProfileApi.getProfile()
|
user.value = await publicProfileApi.getProfile()
|
||||||
editForm.nickname = user.value?.nickname || ""
|
editForm.nickname = user.value?.nickname || ""
|
||||||
editForm.city = user.value?.city || ""
|
editForm.city = user.value?.city || ""
|
||||||
editForm.gender = user.value?.gender || ""
|
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 {
|
} catch {
|
||||||
message.error("获取个人信息失败,请重新登录")
|
message.error("获取个人信息失败,请重新登录")
|
||||||
handleLogout()
|
handleLogout()
|
||||||
@ -172,17 +209,21 @@ const fetchCounts = async () => {
|
|||||||
|
|
||||||
const handleSaveProfile = async () => {
|
const handleSaveProfile = async () => {
|
||||||
editLoading.value = true
|
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 {
|
try {
|
||||||
await publicProfileApi.updateProfile({
|
await publicProfileApi.updateProfile(payload)
|
||||||
nickname: editForm.nickname,
|
|
||||||
city: editForm.city || undefined,
|
|
||||||
gender: editForm.gender || undefined,
|
|
||||||
})
|
|
||||||
message.success("保存成功")
|
message.success("保存成功")
|
||||||
showEditModal.value = false
|
showEditModal.value = false
|
||||||
fetchProfile()
|
await fetchProfile()
|
||||||
} catch {
|
} catch (e: unknown) {
|
||||||
message.error("保存失败")
|
const msg =
|
||||||
|
(e as { response?: { data?: { message?: string } } })?.response?.data?.message || "保存失败"
|
||||||
|
message.error(msg)
|
||||||
} finally {
|
} finally {
|
||||||
editLoading.value = false
|
editLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user