feat(aihr): 重构运营后台与租户知识治理

- 重构运营总览、侧栏和内容运营工作台\n- 增加大喇叭、成果审核与运营统计链路\n- 补齐租户、知识空间、分类和知识维护闭环\n- 加固标签页租户上下文、停用租户写保护与迁移预检
This commit is contained in:
2026-07-23 01:24:22 +08:00
parent 333c365c6f
commit 163b08ecfe
94 changed files with 8610 additions and 1114 deletions
@@ -8,6 +8,7 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
@@ -147,10 +148,23 @@ public class SysTenantController extends BaseController {
@SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY)
@GetMapping("/dynamic/{tenantId}")
public R<Void> dynamicTenant(@NotBlank(message = "租户ID不能为空") @PathVariable String tenantId) {
SysTenantVo tenant = TenantHelper.ignore(() -> tenantService.queryByTenantId(tenantId));
if (tenant == null || !SystemConstants.NORMAL.equals(tenant.getStatus())) {
return R.fail("目标租户不存在或已停用");
}
TenantHelper.setDynamic(tenantId, true);
return R.ok();
}
/**
* 查询当前超级管理员的动态租户。
*/
@SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY)
@GetMapping("/dynamic")
public R<String> currentDynamicTenant() {
return R.ok(TenantHelper.getDynamic());
}
/**
* 清除动态租户
*/
@@ -25,6 +25,12 @@ public interface ISysTenantService {
*/
SysTenantVo queryByTenantId(String tenantId);
/**
* 在当前事务中锁定并查询租户,供需要与租户停用操作串行化的写入流程使用。
* 此查询绕过缓存;调用方必须已开启事务。
*/
SysTenantVo lockByTenantId(String tenantId);
/**
* 查询租户列表
*/
@@ -24,6 +24,7 @@ import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.entity.UploadResult;
import org.dromara.common.oss.enums.AccessPolicyType;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysOss;
import org.dromara.system.domain.SysOssExt;
import org.dromara.system.domain.bo.SysOssBo;
@@ -195,6 +196,13 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
if (ObjectUtil.isNull(file) || file.isEmpty()) {
throw new ServiceException("上传文件不能为空");
}
// Resolve and pin the request tenant before an irreversible object-store write.
// This rejects a stale page context before upload and keeps an in-flight upload
// associated with the tenant it was accepted under.
return TenantHelper.dynamic(TenantHelper.getTenantId(), () -> uploadMultipartFile(file));
}
private SysOssVo uploadMultipartFile(MultipartFile file) {
String originalfileName = file.getOriginalFilename();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
OssClient storage = OssFactory.instance();
@@ -222,6 +230,11 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
if (ObjectUtil.isNull(file) || !file.isFile() || file.length() <= 0) {
throw new ServiceException("上传文件不能为空");
}
// Keep non-HTTP/background imports on their resolved temporary tenant as well.
return TenantHelper.dynamic(TenantHelper.getTenantId(), () -> uploadLocalFile(file));
}
private SysOssVo uploadLocalFile(File file) {
String originalfileName = file.getName();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
OssClient storage = OssFactory.instance();
@@ -75,6 +75,16 @@ public class SysTenantServiceImpl implements ISysTenantService {
return baseMapper.selectVoOne(new LambdaQueryWrapper<SysTenant>().eq(SysTenant::getTenantId, tenantId));
}
/**
* 查询时持有租户行锁,避免租户停用与受保护写入在同一时刻交错。
*/
@Override
public SysTenantVo lockByTenantId(String tenantId) {
return baseMapper.selectVoOne(new LambdaQueryWrapper<SysTenant>()
.eq(SysTenant::getTenantId, tenantId)
.last("for update"));
}
/**
* 查询租户列表
*/