311 lines
15 KiB
Java
311 lines
15 KiB
Java
package com.yunhai.userbase.controller.oapi;
|
||
|
||
import com.yunhai.platform.appauth.AppAuthFilter;
|
||
import com.yunhai.platform.utils.exception.DException;
|
||
import com.yunhai.platform.utils.model.BoaGroup;
|
||
import com.yunhai.platform.utils.model.BoaModule;
|
||
import com.yunhai.platform.utils.model.BoaRoleInfo;
|
||
import com.yunhai.platform.utils.model.BoaUser;
|
||
import com.yunhai.userbase.common.annotation.OapiLog;
|
||
import com.yunhai.userbase.common.validation.ValidationGroups;
|
||
import com.yunhai.userbase.config.Config;
|
||
import com.yunhai.userbase.entity.ApplicationInfo;
|
||
import com.yunhai.userbase.model.sysuser.BatchChangeStatusRequest;
|
||
import com.yunhai.userbase.model.sysuser.SysUserChangeStatusRequest;
|
||
import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem;
|
||
import com.yunhai.userbase.model.sysuser.SysUserGroupStateRequest;
|
||
import com.yunhai.userbase.model.sysuser.SysUserMoveRequest;
|
||
import com.yunhai.userbase.model.sysuser.SysUserPasswordRequest;
|
||
import com.yunhai.userbase.model.sysuser.SysUserRequest;
|
||
import com.yunhai.userbase.model.sysuser.SysUserTransferSchoolRequest;
|
||
import com.yunhai.userbase.repository.ApplicationInfoRepository;
|
||
import com.yunhai.userbase.service.IGroupAuthzService;
|
||
import com.yunhai.userbase.service.ISysUserService;
|
||
import com.yunhai.userbase.service.impl.UserCenterService;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.validation.annotation.Validated;
|
||
import org.springframework.web.bind.annotation.GetMapping;
|
||
import org.springframework.web.bind.annotation.PathVariable;
|
||
import org.springframework.web.bind.annotation.PostMapping;
|
||
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;
|
||
import java.util.Collections;
|
||
import java.util.List;
|
||
|
||
/**
|
||
* 用户中心开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/user/...})。<br>
|
||
* <br>
|
||
* 路径复用 {@link Config#OPEN_API_PATH} 统一前缀,与基础平台 {@code DefaultUserCenterClient}
|
||
* 硬编码的契约严格一致:
|
||
* {@code GET /oapi/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}、
|
||
* {@code /oapi/user/modules?userId=&schId=}。<br>
|
||
* 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求,
|
||
* 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。
|
||
*/
|
||
@RestController
|
||
@RequestMapping(Config.OPEN_API_PATH + "user/")
|
||
public class UserController {
|
||
|
||
private static final Logger log = LoggerFactory.getLogger(UserController.class);
|
||
|
||
@Autowired
|
||
private UserCenterService userCenterService;
|
||
|
||
@Autowired
|
||
private ISysUserService sysUserService;
|
||
|
||
@Autowired
|
||
private IGroupAuthzService groupAuthzService;
|
||
|
||
@Autowired
|
||
private ApplicationInfoRepository applicationInfoRepository;
|
||
|
||
@GetMapping("byId")
|
||
public BoaUser byId(@RequestParam("id") Integer id) {
|
||
return userCenterService.getUserInfoWithDeptById(id);
|
||
}
|
||
|
||
@GetMapping("byLoginName")
|
||
public BoaUser byLoginName(@RequestParam("loginName") String loginName) {
|
||
if (loginName == null || loginName.trim().isEmpty()) {
|
||
return null;
|
||
}
|
||
return userCenterService.getUserInfoWithDeptByAccount(loginName);
|
||
}
|
||
|
||
@GetMapping("byPhone")
|
||
public BoaUser byPhone(@RequestParam("phone") String phone) {
|
||
if (phone == null || phone.trim().isEmpty()) {
|
||
return null;
|
||
}
|
||
return userCenterService.getUserInfoWithDeptByPhone(phone);
|
||
}
|
||
|
||
/**
|
||
* 查询用户在<b>调用方应用</b>下、指定学校(上下文)里的已授权模块列表。<br>
|
||
* <br>
|
||
* 与管理端「权限一览」返回<b>全应用聚合</b>不同,本方法严格按调用方应用维度过滤:<br>
|
||
* ① 通过 {@link AppAuthFilter#APP_AUTH_ATTR} 拿到验签时的 appKey(RequireAppSignFilter
|
||
* 已保证非空);<br>
|
||
* ② 查 application_info 映射到具体 appId;<br>
|
||
* ③ 调 Service 专用方法:所有 SQL/集合构建均限定在该 appId 范围内(数据库层面过滤)。<br>
|
||
* <br>
|
||
* 边界:调用方 appKey 查不到对应应用 → 直接返回空列表(安全优先,禁止意外返回其他应用的模块)。<br>
|
||
*
|
||
* @param request 用于取 AppAuthFilter 设置的 appKey
|
||
* @param userId 用户ID(必填)
|
||
* @param schId 学校ID/组ID(可选;用于限定「学校上下文」:非空时仅取该学校子树内的真实归属组参与判定,
|
||
* 为空时取用户全部真实归属组,与权限一览一致)
|
||
* @return 该应用下用户已授权模块(扁平化;无权限时返回空列表)
|
||
*/
|
||
@GetMapping("modules")
|
||
public List<BoaModule> modules(HttpServletRequest request,
|
||
@RequestParam("userId") Integer userId,
|
||
@RequestParam(value = "schId", required = false) Integer schId) {
|
||
if (userId == null) {
|
||
return Collections.emptyList();
|
||
}
|
||
Integer appId = resolveCallerAppId(request);
|
||
if (appId == null) {
|
||
return Collections.emptyList();
|
||
}
|
||
return groupAuthzService.findUserModulesForApp(appId, schId, userId);
|
||
}
|
||
|
||
/**
|
||
* 从请求 attribute 中解析调用方 appKey,并映射为 application_info 的主键 id。<br>
|
||
* 映射失败返回 null(Controller 层统一返回空列表;异常只打 warn 日志,不给前端 500)。
|
||
*/
|
||
private Integer resolveCallerAppId(HttpServletRequest request) {
|
||
Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR);
|
||
if (attr == null) {
|
||
log.warn("[oapi:user:modules] no AppAuth attr (should be blocked by RequireAppSignFilter), uri={}",
|
||
request.getRequestURI());
|
||
return null;
|
||
}
|
||
String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr);
|
||
if (appKeyStr.trim().isEmpty()) {
|
||
log.warn("[oapi:user:modules] AppAuth attr is blank, uri={}", request.getRequestURI());
|
||
return null;
|
||
}
|
||
Integer appKey;
|
||
try {
|
||
appKey = Integer.parseInt(appKeyStr.trim());
|
||
} catch (NumberFormatException e) {
|
||
log.warn("[oapi:user:modules] invalid appKey format={}, uri={}", appKeyStr, request.getRequestURI());
|
||
return null;
|
||
}
|
||
ApplicationInfo app = applicationInfoRepository.findByAppKey(appKey);
|
||
if (app == null) {
|
||
log.warn("[oapi:user:modules] appKey={} not found in application_info, uri={}", appKey,
|
||
request.getRequestURI());
|
||
return null;
|
||
}
|
||
return app.getId();
|
||
}
|
||
|
||
|
||
/**
|
||
* 新增用户:走完整校验(账号唯一/手机号/身份证/密码强度),BCrypt 加密并写入组关联;
|
||
* 返回创建后的用户(含 id 与组织链)。<br>
|
||
* 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名;此处不记操作日志(避免明文密码写入 request_params)。
|
||
*/
|
||
@PostMapping("add")
|
||
public BoaUser add(@RequestBody @Validated(ValidationGroups.Create.class) SysUserRequest request) {
|
||
Integer id = sysUserService.add(request);
|
||
return userCenterService.getUserInfoWithDeptById(id);
|
||
}
|
||
|
||
/**
|
||
* 编辑用户:密码为空时不修改密码;返回更新后的用户(含 id 与组织链)。
|
||
*/
|
||
@PostMapping("edit/{id}")
|
||
public BoaUser edit(@PathVariable Integer id,
|
||
@RequestBody @Validated(ValidationGroups.Update.class) SysUserRequest request) {
|
||
sysUserService.edit(id, request);
|
||
return userCenterService.getUserInfoWithDeptById(id);
|
||
}
|
||
|
||
/**
|
||
* 删除用户(按组解绑,语义同管理端「从组中删除用户」):仅解除该用户与指定组的关联;<br>
|
||
* 仅当用户已无任何组关联时,才级联删除 sys_user 及其角色绑定。<br>
|
||
* 这样多校用户只会被退出指定学校,而不会被整体误删。<br>
|
||
* 新增 {@code schoolId} 边界:服务端强制 groupId 属于该校子树且不等于 schoolId,避免跨校/整校移除。
|
||
*/
|
||
@PostMapping("delete/{id}/{groupId}/{schoolId}")
|
||
public void delete(@PathVariable Integer id, @PathVariable Integer groupId, @PathVariable Integer schoolId) {
|
||
sysUserService.deleteFromGroup(id, groupId, schoolId);
|
||
}
|
||
|
||
/**
|
||
* 批量删除用户-组关系:请求体为 {@code SysUserGroupDeleteItem} 数组,每条携带自身 (userId, groupId, schoolId);<br>
|
||
* 服务端逐条校验学校边界后解绑,语义与单条 delete 一致。
|
||
*/
|
||
@PostMapping("deleteUsers")
|
||
public void deleteUsers(@RequestBody List<SysUserGroupDeleteItem> items) {
|
||
sysUserService.deleteUsers(items);
|
||
}
|
||
|
||
/**
|
||
* 批量新增用户:逐条走完整校验(含密码强度,每条需携带 password + confirmPassword);
|
||
* 返回创建后的用户列表(顺序与入参一致,均含 id 与组织链)。
|
||
*/
|
||
@PostMapping("batchAdd")
|
||
public List<BoaUser> batchAdd(@RequestBody List<SysUserRequest> requests) {
|
||
List<Integer> ids = sysUserService.batchAdd(requests);
|
||
List<BoaUser> result = new ArrayList<>();
|
||
for (Integer id : ids) {
|
||
result.add(userCenterService.getUserInfoWithDeptById(id));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 移动用户到指定组:仅允许在同一学校(组)范围内移动——用户当前必须已在该学校子树内,
|
||
* 且目标组也必须在该学校子树内,否则拒绝(禁止跨校移动)。<br>
|
||
* 语义同后台「调整组」——覆盖式把用户组归属替换为目标组(目标设为主组)。返回移动后的用户(含新的组织链)。
|
||
*/
|
||
@PostMapping("moveToGroup")
|
||
public BoaUser moveToGroup(@RequestBody @Validated SysUserMoveRequest request) {
|
||
sysUserService.moveUserWithinSchool(request.getUserId(), request.getGroupId(), request.getSchoolId());
|
||
return userCenterService.getUserInfoWithDeptById(request.getUserId());
|
||
}
|
||
|
||
/**
|
||
* 查询用户的业务角色身份列表(角色 + 主归属学校组上下文)。<br>
|
||
* 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。
|
||
*/
|
||
@GetMapping("roles")
|
||
public List<BoaRoleInfo> roles(@RequestParam("userId") Integer userId) {
|
||
return sysUserService.findUserRoles(userId);
|
||
}
|
||
|
||
/**
|
||
* 修改用户密码:先校验旧密码,再写入新密码(BCrypt)。<br>
|
||
* 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名;此处不记操作日志(避免明文密码写入 request_params)。
|
||
*/
|
||
@PostMapping("password")
|
||
public void password(@RequestBody @Validated SysUserPasswordRequest request) {
|
||
sysUserService.changePassword(request.getUserId(), request.getOldPassword(), request.getNewPassword());
|
||
}
|
||
|
||
/**
|
||
* 修改用户头像:校验图片(非空/≤20MB/扩展名白名单)→ 压缩 → 存储并写入 profile_picture,<br>
|
||
* 返回新头像完整 Web URL。multipart 直传(form 字段 userId + 文件 part file)。<br>
|
||
* 与修改密码一致,仅由过滤器层强制应用签名;POST 自动记 OapiLog(文件内容不落库,仅记文件名元数据)。
|
||
*/
|
||
@PostMapping("profilePicture")
|
||
public String profilePicture(@RequestParam("userId") Integer userId,
|
||
@RequestParam("file") MultipartFile file) {
|
||
return sysUserService.changeProfilePicture(userId, file);
|
||
}
|
||
|
||
/**
|
||
* 设置用户在指定组上的状态(转出/恢复)。<br>
|
||
* state=1 转出、state=0 恢复正常;仅作关系标记,不影响成员查询、组织链与下游推送。
|
||
* 仅允许在同一学校(组)范围内操作——groupId 必须属于 schoolId 子树且不等于 schoolId。<br>
|
||
* 鉴权由过滤器层强制签名;POST 自动记 OapiLog。
|
||
*/
|
||
@PostMapping("groupState")
|
||
public void groupState(@RequestBody @Validated SysUserGroupStateRequest request) {
|
||
sysUserService.changeUserGroupState(request.getUserId(), request.getGroupId(),
|
||
request.getSchoolId(), request.getState());
|
||
}
|
||
|
||
/**
|
||
* 查询用户在指定组上的状态:0=正常,1=转出。<br>
|
||
* 边界与设置接口一致(groupId 必须属于 schoolId 子树且不等于 schoolId);
|
||
* 用户不在此组时返回业务错误(code=5)。
|
||
*/
|
||
@OapiLog(value = "查询用户组状态", saveResponse = false)
|
||
@GetMapping("groupState")
|
||
public Integer groupState(@RequestParam("userId") Integer userId,
|
||
@RequestParam("groupId") Integer groupId,
|
||
@RequestParam("schoolId") Integer schoolId) {
|
||
return sysUserService.getUserGroupState(userId, groupId, schoolId);
|
||
}
|
||
|
||
/**
|
||
* 跨校转入(转出后迁移到其他学校),同时支持同校转组(原学校与目标学校相同时,从原组转到另一组)。<br>
|
||
* 前置:用户已在原学校(fromSchoolId 子树内)存在至少一条「转出」状态的组关系(先经 groupState 置转出);<br>
|
||
* 原组即该转出状态组关系,仅当原学校与原组都与目标一致(fromSchoolId==schoolId 且转出组==groupId)才拒绝。<br>
|
||
* 操作成功后:同校转组删除原组关系、新增目标组关系(主归属继承原组,其他组与他校关系保留);
|
||
* 跨校转入删除原学校范围内全部组关系(其他学校保留),在目标学校建立目标组关系(主归属、状态恢复正常),并推送编辑用户订阅。<br>
|
||
* 鉴权由过滤器层强制签名;POST 自动记 OapiLog。
|
||
*/
|
||
@PostMapping("transferToSchool")
|
||
public BoaUser transferToSchool(@RequestBody @Validated SysUserTransferSchoolRequest request) {
|
||
sysUserService.transferUserToSchool(request.getUserId(), request.getFromSchoolId(),
|
||
request.getSchoolId(), request.getGroupId());
|
||
return userCenterService.getUserInfoWithDeptById(request.getUserId());
|
||
}
|
||
|
||
/**
|
||
* 设置单个用户状态(启用/禁用账号,语义同管理端 sysuser/Enable、sysuser/Disable 的合并版)。<br>
|
||
* disabled=true 禁用、false 启用;变更后自动推送「编辑用户」订阅。<br>
|
||
* 鉴权由过滤器层强制签名;POST 自动记 OapiLog。
|
||
*/
|
||
@PostMapping("changeStatus")
|
||
public void changeStatus(@RequestBody @Validated SysUserChangeStatusRequest request) {
|
||
sysUserService.changeStatus(request.getUserId(), request.getDisabled());
|
||
}
|
||
|
||
/**
|
||
* 批量设置用户状态(启用/禁用,语义同管理端 sysuser/BatchChangeStatus)。<br>
|
||
* 任一 userId 无效则整体拒绝;变更后自动推送「编辑用户」订阅。<br>
|
||
* 鉴权由过滤器层强制签名;POST 自动记 OapiLog。
|
||
*/
|
||
@PostMapping("batchChangeStatus")
|
||
public void batchChangeStatus(@RequestBody @Validated BatchChangeStatusRequest request) {
|
||
sysUserService.batchChangeStatus(request.getUserIds(), request.getDisabled());
|
||
}
|
||
}
|