本地工作区快照:恢复 .git 仓库(合并远端 main 前基线)

This commit is contained in:
chenxing
2026-08-21 10:46:47 +08:00
commit f182fe44ce
269 changed files with 22450 additions and 0 deletions
@@ -0,0 +1,233 @@
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.validation.ValidationGroups;
import com.yunhai.userbase.config.Config;
import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem;
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.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 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());
}
}