feat(oapi): 本人联系方式修改端点 POST /oapi/user/self/contact——只写 sys_user phone/email 两字段(空白=不修改),手机号格式/唯一查重+邮箱格式校验,UserChangedEvent 驱逐 boaUser 缓存(含旧 phone: key 提示);附带修复 BoaUserLoader 过时置空:email 列早已存在(实体/管理端/导入均读写),回读 BoaUser 须携带 email(任务B·UserBase 侧)
build / build-push (push) Successful in 2m8s

This commit is contained in:
chenxing
2026-10-02 08:53:30 +08:00
parent f0eaebd186
commit 90c08a5f14
5 changed files with 116 additions and 2 deletions
@@ -12,6 +12,7 @@ import com.yunhai.userbase.common.validation.ValidationGroups;
import com.yunhai.userbase.config.Config;
import com.yunhai.userbase.model.sysuser.BatchChangeStatusRequest;
import com.yunhai.userbase.model.sysuser.SysUserChangeStatusRequest;
import com.yunhai.userbase.model.sysuser.SysUserContactRequest;
import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem;
import com.yunhai.userbase.model.sysuser.SysUserGroupStateRequest;
import com.yunhai.userbase.model.sysuser.SysUserMoveRequest;
@@ -240,6 +241,19 @@ public class UserController {
sysUserService.changePassword(request.getUserId(), request.getOldPassword(), request.getNewPassword());
}
/**
* 本人联系方式修改(手机号/邮箱):body {userId, phone, email},phone/email 至少一项非空,
* 空白=不修改该字段;只更新 sys_user 的 phone/email 两字段(其余一律不动),
* 手机号/邮箱格式与长度校验、手机号唯一查重在 Service 层。返回更新后的用户
* (getUserInfoWithDeptById,事务提交后缓存已被 UserChangedEvent 驱逐,回读即新值)。<br>
* 鉴权由过滤器层强制签名;POST 自动记 OapiLog。
*/
@PostMapping("self/contact")
public BoaUser selfContact(@RequestBody @Validated SysUserContactRequest request) {
sysUserService.updateSelfContact(request.getUserId(), request.getPhone(), request.getEmail());
return userCenterService.getUserInfoWithDeptById(request.getUserId());
}
/**
* 修改用户头像:校验图片(非空/≤20MB/扩展名白名单)→ 压缩 → 存储并写入 profile_picture,<br>
* 返回新头像完整 Web URL。multipart 直传(form 字段 userId + 文件 part file)。<br>
@@ -0,0 +1,35 @@
package com.yunhai.userbase.model.sysuser;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 本人联系方式修改请求(oapi {@code /oapi/user/self/contact} 专用)。<br>
* 手机号/邮箱至少一项非空(Service 层校验);字段级格式、长度与唯一性校验同在 Service 层
* (手机号口径与 {@code validateProfile} 一致,另做邮箱格式 + 手机号查重)。
*
* @author chenx
*/
@Data
public class SysUserContactRequest {
/**
* 用户ID(必填)。
*/
@NotNull
private Integer userId;
/**
* 手机号(可选;非空时走 1[3-9] 段位格式校验 + 全库唯一查重)。
*/
@Size(max = 20, message = "手机号长度不能超过20字符")
private String phone;
/**
* 邮箱(可选;非空时走邮箱格式校验)。
*/
@Size(max = 100, message = "邮箱长度不能超过100字符")
private String email;
}
@@ -218,6 +218,19 @@ public interface ISysUserService {
*/
void changePassword(Integer userId, String oldPassword, String newPassword);
/**
* 修改本人联系方式(手机号/邮箱,oapi 开放接口 {@code /oapi/user/self/contact} 专用)。<br>
* 只写 sys_user 的 phone/email 两字段,其余字段一律不动;phone/email 至少一项非空,
* 空白字段视为「不修改该字段」(不写库不清空);手机号改值时做全库唯一查重(phone 是
* byPhone 身份维度,重复会破坏手机号找人)。更新后发 UserChangedEvent 精确驱逐 boaUser
* 缓存(含旧 phone: 维度 key 提示),调用方紧跟着的 byId 回读即新值。
*
* @param userId 用户ID(目标用户本人)
* @param phone 新手机号(可空=不改)
* @param email 新邮箱(可空=不改)
*/
void updateSelfContact(Integer userId, String phone, String email);
/**
* 修改用户头像:校验图片(非空/≤20MB/扩展名白名单)→ 压缩 → 存储至 avatar 子目录 →
* 写入 profile_picture。oapi 开放接口专用;返回新头像完整 Web URL。
@@ -92,8 +92,10 @@ public class BoaUserLoader {
boa.setIdcard(user.getIdcard());
boa.setPhone(user.getPhone());
boa.setProfilePicture(user.getProfilePicture());
// 用户表无 email / gender 列,置 null
boa.setEmail(null);
// email 列已存在(SysUser.email,管理端编辑/批量导入均已读写):
// 本人联系方式修改(oapi self/contact)后回读 BoaUser 须携带新 email,旧注释「用户表无 email 列」已过时;
// gender 列虽在实体上,但当前无写入来源,保持置 null
boa.setEmail(user.getEmail());
boa.setGender(null);
boa.setType(user.getType() != null ? user.getType().getKey() : null);
// 状态语义对齐:本表 disabled(NORMAL=0 正常 / DISABLED=1 禁用) -> BoaUser(1 正常 / 0 禁用)
@@ -96,6 +96,10 @@ public class SysUserService implements ISysUserService {
private static final Set<String> SUPER_ADMIN_ACCOUNTS =
new HashSet<>(Arrays.asList("yunhaisch", "yunhaisch2", "yunhaisch3"));
/** 邮箱格式(本人联系方式修改用):本地段常规字符 + 点分域名,总长另由请求 @Size 限制 */
private static final java.util.regex.Pattern CONTACT_EMAIL =
java.util.regex.Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$");
@Autowired
private SysUserRepository sysUserRepository;
@@ -289,6 +293,52 @@ public class SysUserService implements ISysUserService {
sysUserRepository.save(entity);
}
/**
* 修改本人联系方式(oapi /oapi/user/self/contact):只写 phone/email 两字段,其余不动。<br>
* 空白入参=「不修改该字段」(不清空——清空语义不在本接口范围);手机号是 byPhone 身份维度,
* 改值前全库查重;更新后发 UPDATE_USER 推送 + UserChangedEvent(带旧 phone: key 提示)驱逐
* boaUser 缓存,控制器紧跟着的 getUserInfoWithDeptById 回读即新值(AFTER_COMMIT 后于回读执行)。
*/
@Override
@Transactional
public void updateSelfContact(Integer userId, String phone, String email) {
SysUser entity = sysUserRepository.findById(userId)
.orElseThrow(() -> new DException("系统用户不存在"));
String newPhone = StringUtils.trimToNull(phone);
String newEmail = StringUtils.trimToNull(email);
if (newPhone == null && newEmail == null) {
throw new DException("手机号与邮箱至少填写一项");
}
if (newPhone != null && !ValidatorUtil.isPhone(newPhone)) {
throw new DException("手机号格式不正确");
}
if (newEmail != null && !CONTACT_EMAIL.matcher(newEmail).matches()) {
throw new DException("邮箱格式不正确");
}
if (newPhone != null && !newPhone.equals(entity.getPhone())) {
SysUser samePhone = sysUserRepository.findByPhone(newPhone);
if (samePhone != null && !samePhone.getId().equals(userId)) {
throw new DException("手机号已被其他用户使用");
}
}
// 改手机号后旧 phone: 维度缓存 key 无处回查,须随事件显式驱逐(同 editInternal 口径);
// 仅改邮箱时手机号未变,不加提示(避免无谓多驱逐一个 key)
Set<String> staleKeyHints = new HashSet<>();
if (newPhone != null && StringUtils.isNotBlank(entity.getPhone())
&& !entity.getPhone().equals(newPhone)) {
staleKeyHints.add("phone:" + entity.getPhone());
}
if (newPhone != null) {
entity.setPhone(newPhone);
}
if (newEmail != null) {
entity.setEmail(newEmail);
}
entity.setUpdateUser(SYSTEM_USER_ID);
sysUserRepository.save(entity);
publishUserEvent(PushInterfaceType.UPDATE_USER, userId, staleKeyHints);
}
/**
* 开放接口的用户身份写入(追加语义,只加不删):<br>
* 学校范围 = 显式 schoolId,缺省按 request.groupId 解析(与新增口同一约定,调用方不感知解析逻辑);