✨ 揭秘 Spring Boot 代码中的“身份转换”:getVipIdByStock
深度解析 ✨
大家好!👋 在日常的 Spring Boot 项目开发中,我们经常会遇到一些看似简单却蕴含深意的代码行。今天,我们就来深入探讨这样一行代码:
adminId = adminCommonService.getVipIdByStock(adminId);
这行代码通常出现在 Controller 层处理请求的方法中,尤其是在涉及多用户、多角色、分层权限管理的系统中。它到底做了什么?为什么需要它?不加行不行?🤔 带着这些疑问,让我们一起层层剥茧,结合具体的代码实例,彻底搞懂它的来龙去脉!
❓ 问题背景:共享资源下的身份难题
想象一个场景:你正在开发一个库存管理系统。系统中有两种主要用户:
- 公司老板 (Company Super Admin): 拥有整个公司的库存数据,可能是系统服务的购买者 (VIP)。
- 仓库管理员 (Store Manager): 由老板指派,负责具体管理库存,但他本身并不拥有这些库存数据的所有权。
当仓库管理员登录系统想要查看“我的库存”列表时,我们应该展示谁的库存呢?显然,应该展示他所属公司的库存,也就是老板账户下的库存。如果直接用仓库管理员的 ID 去查询,很可能查不到数据,或者逻辑上就是错误的。
这时,adminId = adminCommonService.getVipIdByStock(adminId);
这行代码就应运而生了,它的核心目的就是解决在处理共享资源(如库存)时,如何将操作者的身份 标准化 为资源所有者或权限主体的身份。
📊 核心功能概览
功能项 | 描述 | 涉及组件 (主要) | 输入 | 输出 |
---|---|---|---|---|
身份标准化 | 检查当前用户角色,如果是特定操作者(如库存管理员),则查找并返回其上级(公司超管)ID;否则返回自身 ID。 🎯 | AdminCommonService , AdminService , RoleCacheService , AdminCacheService | 当前登录用户 adminId | 用于后续操作的 adminId |
核心判断 | 是否为 ROLE_STORE (库存管理人) 角色? | AdminCommonService.hasRole , RoleCacheService | 用户 adminId | 布尔值 (True/False) |
上级查找 | 若是 ROLE_STORE ,则递归向上查找 pid (父级ID),直到找到 ROLE_COMPANY_SUPER (公司超管) 的用户。 🧗♂️ | AdminService.findCompanySuperId , AdminCacheService | 用户 adminId | 公司超管 adminId |
性能优化 | 大量使用缓存 (@Cacheable ) 提高用户、角色信息的查询效率。 🚀 | AdminCacheService , RoleCacheService | 查询参数 | 缓存的数据或数据库数据 |
数据基础 | 依赖数据库中的 admin (含 pid ), role , admin_role_mapping 表结构。 💾 | AdminRepository , RoleRepository , AdminRoleMappingRepository | - | 数据库记录 |
🗺️ 决策流程:Flowchart (Mermaid)
让我们用流程图来看看 getVipIdByStock
方法内部的决策逻辑:
流程解读:
- 输入当前用户 ID (
nowId
)。 - 检查该用户是否具有
ROLE_STORE
(库存管理人)角色。 - 如果是,则调用
findCompanySuperId
方法,该方法会递归地通过用户的pid
(父级 ID)向上查找,直到找到拥有ROLE_COMPANY_SUPER
(公司超管)角色的用户,并返回其 ID。 - 如果不是,则直接返回用户自身的 ID (
nowId
)。
⏱️ 时序交互:Sequence Diagram (Mermaid)
当一个 仓库管理员 调用 list
方法查看库存列表时,系统内部的交互过程大致如下:
时序图解读:
这个图清晰地展示了从 Controller 接收请求开始,如何调用 AdminCommonService
,再通过 AdminService
进行递归查找(涉及到用户缓存、角色缓存以及最终的数据库查询),最终获得公司超管 ID 并用其进行后续数据查询的完整过程。
🔬 代码层层解析:从 Controller 到 Repository
让我们深入代码,看看这个机制是如何在不同层级实现的:
1. Controller 层 (StockInventoryController
)
@GetMapping("admin/list")
@ApiOperation("我的库存分页列表")
public BaseResult list(PageWithSearch basePage, @ApiIgnore @SessionAttribute(Constants.ADMIN_ID) Integer adminId) {
checkParam(basePage.getField(), basePage.getValue(), Arrays.asList(searchParamValidArray));
// ===> 就是这里!在调用核心业务逻辑前进行身份标准化 <===
adminId = adminCommonService.getVipIdByStock(adminId);
return BaseResult.success(stockInventoryApiService.findPage(adminId, basePage));
}
在 Controller 的 list
方法中,获取到 Session 中的 adminId
后,立即调用 adminCommonService.getVipIdByStock
进行处理,然后再将可能被转换后的 adminId
传递给 stockInventoryApiService
进行实际的数据查询。
2. Service 层 (AdminCommonService
)
@Service
public class AdminCommonService {
// ... 注入 AdminService, AdminCacheService, RoleCacheService ...
/**
* 获取商品管理人的上级vip id
* 当操作者为库存管理人时获取上级的vip id
* 否则返回自身
*/
public Integer getVipIdByStock(Integer nowId) {
// 核心判断:是否为库存管理人?
return hasRole(nowId, Admin.ROLE_STORE)
// 是:查找公司超管 ID
? findCompanySuperId(nowId)
// 否:返回自身 ID
: nowId;
}
// 判断用户是否有指定角色 (利用 RoleCacheService)
public boolean hasRole(Integer adminId, String role) {
List<String> roleStrList = roleCacheService.findRolesByAdminId(adminId).stream().map(Role::getName).collect(Collectors.toList());
return roleStrList.contains(role);
}
// 查询公司超管 ID (委托给 AdminService)
public Integer findCompanySuperId(Integer adminId) {
return adminService.findCompanySuperId(adminId);
}
// ... 其他辅助方法 ...
}
AdminCommonService
负责编排逻辑:调用 hasRole
判断角色,如果需要,再调用 findCompanySuperId
获取上级 ID。
3. Service 层 (AdminService
) - 递归查找核心
@Service
public class AdminService {
// ... 注入 AdminRepository, AdminCacheService, RoleCacheService ...
// 入口方法:根据 ID 查找超管
public Integer findCompanySuperId(Integer adminId) {
// 使用缓存获取 Admin 对象
Admin admin = adminCacheService.findById(adminId).orElseThrow(() -> new RuntimeException("未查到管理员信息"));
return findCompanySuperId(admin); // 调用重载方法
}
// 重载方法:处理 Admin 对象
public Integer findCompanySuperId(Admin admin) {
// 获取当前用户的角色
List<Role> roles = roleCacheService.findRolesByAdminId(admin.getId());
for (Role role : roles) {
// 如果当前用户就是公司超管,直接返回其 ID
if (Admin.ROLE_COMPANY_SUPER.equals(role.getName())) {
return admin.getId();
}
}
// 如果不是超管,调用递归方法查找上级
return findCompanySuperIdNext(admin);
}
// 递归查找方法
private Integer findCompanySuperIdNext(Admin admin) {
// 获取父级 ID (pid)
Integer parentId = admin.getPid();
if (parentId == null) { // 如果没有父级了还没找到,可能数据有问题或已到顶层非超管
throw new RuntimeException("未找到公司超级管理员"); // 或者根据业务返回特定值
}
// 获取父级 Admin 对象 (使用缓存)
Admin parentAdmin = adminCacheService.findById(parentId).orElseThrow(() -> new RuntimeException("未找到上级用户信息"));
// 获取父级的角色
List<Role> parentRoles = roleCacheService.findRolesByAdminId(parentAdmin.getId());
for (Role role : parentRoles) {
// 如果父级是公司超管,返回父级 ID
if (Admin.ROLE_COMPANY_SUPER.equals(role.getName())) {
return parentAdmin.getId();
}
}
// 如果父级还不是超管,继续向上递归查找
return findCompanySuperIdNext(parentAdmin);
}
// ... 其他方法 ...
}
AdminService
是查找公司超管的核心。它首先检查当前用户是否为超管,如果不是,则通过 admin.getPid()
获取父级 ID,然后递归调用自身,直到找到具有 ROLE_COMPANY_SUPER
角色的用户为止。
4. Caching 层 (AdminCacheService
, RoleCacheService
)
// AdminCacheService
@Service
public class AdminCacheService {
@Autowired private AdminRepository adminRepository;
@Cacheable(value = "findAdminById", key = "#id") // 对 findById 结果进行缓存
public Optional<Admin> findById(Integer id) {
return adminRepository.findById(id);
}
// ... 其他缓存相关方法 (save, delete 时需要 @CacheEvict 清除缓存) ...
}
// RoleCacheService
@Service
public class RoleCacheService {
@Autowired private RoleRepository roleRepository;
// ... 其他注入 ...
@Cacheable(value = "findRoleByAdminId", key = "#adminId") // 对用户角色列表进行缓存
public List<Role> findRolesByAdminId(Integer adminId) {
return roleRepository.findRolesByAdminId(adminId);
}
// ... 其他缓存相关方法 ...
}
缓存层通过 Spring Cache 的注解 (@Cacheable
, @CacheEvict
) 极大地提高了性能。频繁调用的 findById
和 findRolesByAdminId
的结果会被缓存起来,避免了每次都查询数据库。
5. Repository 层 (AdminRepository
, RoleRepository
)
// AdminRepository
public interface AdminRepository extends JpaRepository<Admin, Integer> {
// JpaRepository 自动提供了 findById 方法
// 实体类 Admin 中应该包含 pid 字段
}
// RoleRepository
public interface RoleRepository extends JpaRepository<Role, Integer> {
@Query(value = "select r.* from role r join admin_role_mapping arm on r.id = arm.role_id and arm.admin_id = ?1", nativeQuery = true)
List<Role> findRolesByAdminId(Integer adminId); // 查询用户角色
}
// AdminRoleMappingRepository (虽然没直接用在查找逻辑,但支撑了 admin_role_mapping 表)
public interface AdminRoleMappingRepository extends JpaRepository<AdminRoleMapping, Integer> {
// ...
}
Repository 层定义了与数据库交互的接口。AdminRepository
提供了基础的 findById
(继承自 JpaRepository
) 来获取 Admin
对象(包含 pid
),RoleRepository
则通过原生 SQL 查询关联 admin_role_mapping
表来获取用户的角色列表。
🤔 为什么这行代码如此重要?(The “Aha!” Moment)💡
现在,我们回到最初的问题:为什么要有 adminId = adminCommonService.getVipIdByStock(adminId);
这行代码?
- 确保数据归属正确性: 库存数据是公司的,不是仓库管理员个人的。通过将 ID 转换为公司超管 ID,保证了后续查询 (
findPage
) 是基于正确的数据主体进行的。 - 统一权限和订阅校验: 系统的某些功能(比如增删改库存、查看某些高级报表)可能与公司超管账户的订阅状态 (VIP) 或权限绑定。使用转换后的 ID 可以确保后续的权限检查(如代码中其他地方可能存在的
subscribeService.isVIP(adminId)
)是基于公司超管账户进行的。 - 简化下游服务逻辑:
StockInventoryApiService
等下游服务不需要再关心传入的adminId
到底是哪个角色的 ID,它们只需要假设这个 ID 就是拥有数据和权限的那个 ID,从而简化了它们的内部逻辑。Controller 层承担了身份标准化的职责。 - 支持组织架构和代理操作: 这种机制天然地支持了“员工代理老板操作”的业务场景,使得系统的组织架构设计更加灵活。
🎯 一言以蔽之:这行代码扮演了一个“守门员”和“翻译官”的角色,确保了在处理公司级资源时,系统始终使用代表公司的“法人” ID,屏蔽了底层员工身份的差异,保证了业务逻辑的一致性和正确性。
✅ 总结
通过对 StockInventoryController
, AdminCommonService
, AdminService
, *CacheService
, *Repository
等多个层级代码的深入分析,我们彻底理解了 adminId = adminCommonService.getVipIdByStock(adminId);
这行代码的精妙之处。它不仅仅是一次简单的赋值,而是:
- 一个基于 角色 的条件判断。
- 一个利用
pid
实现的 递归向上查找 机制。 - 一个结合了 缓存 优化的实践。
- 一个保证 数据归属 和 权限统一 的关键设计。
希望这篇博客能帮助你理解这类代码设计的意图和实现方式。下次在你的项目中遇到类似场景时,就能更加得心应手了!💪
🧠 思维导图 (Markdown)
希望这篇博客对你有帮助!😊