chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
package com.xspaceagi.system.web.aspect;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.SysUserPermissionCacheService;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.enums.ResourceEnum;
import com.xspaceagi.system.spec.exception.ResourcePermissionException;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* 资源权限切面
* 支持方法级别和类级别的 @RequireResource 注解,方法上的注解优先于类上的
*/
@Slf4j
@Aspect
@Component
public class ResourcePermissionAspect {
@Resource
private SysUserPermissionCacheService sysUserPermissionCacheService;
/**
* 环绕通知,拦截带有 @RequireResource 注解的方法或类
* 方法级别:仅该方法受注解限制;类级别:该类的所有方法都受注解限制
*/
@Around("@annotation(com.xspaceagi.system.spec.annotation.RequireResource) || @within(com.xspaceagi.system.spec.annotation.RequireResource)")
public Object checkResourcePermission(ProceedingJoinPoint joinPoint) throws Throwable {
RequireResource requireResource = resolveRequireResource(joinPoint);
if (requireResource == null) {
return joinPoint.proceed();
}
// 如果允许跳过权限检查,直接执行方法
if (requireResource.skipCheck()) {
log.debug("跳过资源权限检查: {}", joinPoint.getSignature().getName());
return joinPoint.proceed();
}
try {
UserDto currentUser = (UserDto) RequestContext.get().getUser();
if (currentUser == null) {
log.warn("User not logged in, access denied: {}", joinPoint.getSignature().getName());
throw new ResourcePermissionException("用户未登录");
}
List<String> resourceCodes = collectResourceCodes(requireResource);
if (CollectionUtils.isEmpty(resourceCodes)) {
log.warn("资源编码为空,拒绝访问: {}", joinPoint.getSignature().getName());
throw new ResourcePermissionException("资源编码不能为空");
}
sysUserPermissionCacheService.checkResourcePermissionAny(currentUser.getId(), resourceCodes);
log.debug("资源权限验证通过,用户[{}]访问资源[{}]: {}",
currentUser.getId(), resourceCodes, joinPoint.getSignature().getName());
return joinPoint.proceed();
} catch (ResourcePermissionException e) {
log.warn("资源Permission check failed: {}", e.getMessage());
throw e;
} catch (Throwable e) {
throw e;
}
}
/**
* 解析 @RequireResource 注解:优先取方法上的,其次取类上的
*/
private RequireResource resolveRequireResource(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequireResource requireResource = AnnotationUtils.findAnnotation(method, RequireResource.class);
if (requireResource != null) {
return requireResource;
}
return AnnotationUtils.findAnnotation(method.getDeclaringClass(), RequireResource.class);
}
/**
* 从注解中收集所有资源码value 枚举 + resourceCodes
*/
@SuppressWarnings("deprecation")
private List<String> collectResourceCodes(RequireResource requireResource) {
List<String> allCodes = new ArrayList<>();
if (requireResource.value() != null && requireResource.value().length > 0) {
for (ResourceEnum e : requireResource.value()) {
if (e != null && e.getCode() != null) {
allCodes.add(e.getCode());
}
}
}
if (requireResource.codes() != null && requireResource.codes().length > 0) {
for (String s : requireResource.codes()) {
if (StringUtils.isNotBlank(s)) {
allCodes.add(s.trim());
}
}
}
return allCodes;
}
}

View File

@@ -0,0 +1,68 @@
package com.xspaceagi.system.web.aspect;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.annotation.SaasAdmin;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class SaasAdminAspect {
private static final Long SAAS_ADMIN_TENANT_ID = 1L;
@Around("@annotation(com.xspaceagi.system.spec.annotation.SaasAdmin) || @within(com.xspaceagi.system.spec.annotation.SaasAdmin)")
public Object checkSaasAdminPermission(ProceedingJoinPoint joinPoint) throws Throwable {
SaasAdmin saasAdmin = resolveSaasAdmin(joinPoint);
if (saasAdmin == null) {
return joinPoint.proceed();
}
if (saasAdmin.skipCheck()) {
log.debug("跳过 SaaS 管理员权限检查: {}", joinPoint.getSignature().getName());
return joinPoint.proceed();
}
if (RequestContext.get() == null || RequestContext.get().getTenantId() == null) {
log.warn("上下文或租户ID为空拒绝访问: {}", joinPoint.getSignature().getName());
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
if (!SAAS_ADMIN_TENANT_ID.equals(RequestContext.get().getTenantId())) {
log.warn("拒绝访问: tenantId={}, method={}", RequestContext.get().getTenantId(), joinPoint.getSignature().getName());
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
UserDto userDto = (UserDto) RequestContext.get().getUser();
if (userDto == null || userDto.getRole() != User.Role.Admin) {
log.warn("拒绝访问: userId={}, method={}", userDto != null ? userDto.getId() : null, joinPoint.getSignature().getName());
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
log.debug("SaaS Admin check passed, user [{}] accessing: {}", userDto.getId(), joinPoint.getSignature().getName());
return joinPoint.proceed();
}
private SaasAdmin resolveSaasAdmin(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SaasAdmin saasAdmin = AnnotationUtils.findAnnotation(method, SaasAdmin.class);
if (saasAdmin != null) {
return saasAdmin;
}
return AnnotationUtils.findAnnotation(method.getDeclaringClass(), SaasAdmin.class);
}
}

View File

@@ -0,0 +1,101 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.application.dto.CategoryCreateDto;
import com.xspaceagi.system.application.dto.CategoryUpdateDto;
import com.xspaceagi.system.application.service.CategoryApplicationService;
import com.xspaceagi.system.infra.dao.entity.Category;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.web.controller.base.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
/**
* 分类管理控制器
*/
@Slf4j
@Tag(name = "分类管理", description = "分类管理相关接口")
@RestController
@RequestMapping("/api/system/category")
public class CategoryController extends BaseController {
@Resource
private CategoryApplicationService categoryApplicationService;
/**
* 创建分类
*/
@RequireResource(CATEGORY_CONFIG_ADD)
@Operation(summary = "创建分类")
@PostMapping(value = "/create", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Category> create(@RequestBody CategoryCreateDto dto) {
log.info("[create] Create category, name={}, code={}, type={}", dto.getName(), dto.getCode(), dto.getType());
Assert.notNull(dto.getName(), "Name is required");
Assert.notNull(dto.getCode(), "Code is required");
Assert.notNull(dto.getType(), "Type is required");
Category category = new Category();
category.setName(dto.getName());
category.setCode(dto.getCode());
category.setType(dto.getType());
category.setDescription(dto.getDescription());
return categoryApplicationService.create(category, getUser());
}
/**
* 更新分类
*/
@RequireResource(CATEGORY_CONFIG_MODIFY)
@Operation(summary = "更新分类")
@PostMapping(value = "/update", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Category> update(@RequestBody CategoryUpdateDto dto) {
log.info("[update] Update category, id={}, name={}, code={}, type={}",
dto.getId(), dto.getName(), dto.getCode(), dto.getType());
Assert.notNull(dto.getId(), "ID is required");
Category category = new Category();
category.setId(dto.getId());
category.setName(dto.getName());
category.setCode(dto.getCode());
category.setType(dto.getType());
category.setDescription(dto.getDescription());
return categoryApplicationService.update(category, getUser());
}
/**
* 删除分类
*/
@RequireResource(CATEGORY_CONFIG_DELETE)
@Operation(summary = "删除分类")
@PostMapping(value = "/delete/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> delete(@PathVariable("id") Long id) {
log.info("[delete] Delete category, id={}", id);
Assert.notNull(id, "ID不能为空");
return categoryApplicationService.delete(id, getUser());
}
/**
* 根据类型查询分类列表
*/
@RequireResource(CATEGORY_CONFIG_QUERY)
@Operation(summary = "根据类型查询分类列表")
@GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<List<Category>> listByType(@RequestParam("type") String type) {
log.info("[listByType] 查询分类列表, type={}", type);
List<Category> list = categoryApplicationService.listByType(type);
I18nUtil.replaceSystemMessage(list);
return ReqResult.success(list);
}
}

View File

@@ -0,0 +1,58 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.sdk.service.UserAccessKeyApiService;
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.enums.HttpStatusEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
public class ChatKeyCheck {
public static void check(HttpServletRequest request, UserAccessKeyApiService userAccessKeyApiService) {
String fragment = request.getHeader("Fragment");
String referer = request.getHeader("Referer");
String chatKey = null;
if (referer != null && referer.contains("chat-temp")) {
try {
URL url = new URL(referer);
chatKey = url.getPath().replace("chat-temp", "").replace("/", "");
} catch (Exception e) {
//
log.warn("referer error", e);
}
} else if (StringUtils.isNotBlank(fragment)) {
//fragmentchatKey=ck-1ce5a050c8f64f5a8b555d06b4818038
//用正则获取chatKey的值其中 chatKey后面为-或其他字符串
chatKey = extractChatKey(fragment);
}
if (StringUtils.isNotBlank(chatKey)) {
UserAccessKeyDto userAccessKeyDto = userAccessKeyApiService.queryAccessKey(chatKey);
if (userAccessKeyDto == null) {
throw BizException.of(HttpStatusEnum.UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED,
BizExceptionCodeEnum.systemUnauthorizedOrSessionExpired);
}
}
}
private static String extractChatKey(String fragment) {
// 正则表达式提取chatKey
String regex = "chatKey=([^&]+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(fragment);
if (matcher.find()) {
return matcher.group(1);
} else {
return null;
}
}
}

View File

@@ -0,0 +1,120 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.application.constant.I18nLangTagConstraints;
import com.xspaceagi.system.application.constant.SupportedLocaleConstants;
import com.xspaceagi.system.application.dto.I18nLangDto;
import com.xspaceagi.system.application.dto.SupportedLocaleOptionDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.I18nApplicationService;
import com.xspaceagi.system.application.service.I18nLangApplicationService;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 国际化管理器
*/
@Slf4j
@Tag(name = "多语言查询", description = "国际化相关接口(语言管理、多语言配置)")
@RestController
@RequestMapping("/api/i18n")
public class I18nController {
@Resource
private I18nApplicationService i18nApplicationService;
@Resource
private I18nLangApplicationService i18nLangApplicationService;
@Resource
private UserApplicationService userApplicationService;
/**
* 查询系统语言 Map
*/
@Operation(summary = "查询指定语言信息")
@GetMapping(value = "/query", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Map<String, String>> querySystemLangMap(@RequestParam(value = "side", required = false) String side,
@RequestParam(value = "lang", required = false) String lang,
HttpServletResponse response) {
log.info("[querySystemLangMap] 查询系统语言 Map, lang={}", lang);
String resolved = StringUtils.isBlank(lang) ? RequestContext.get().getLang() : lang.trim();
lang = StringUtils.isBlank(resolved) ? resolved
: I18nLangTagConstraints.tryNormalizeToStoredForm(resolved).orElse(resolved);
Map<String, String> map = i18nApplicationService.querySystemLangMap(RequestContext.get().getTenantId(), side, lang);
if (StringUtils.isBlank(lang)) {
return ReqResult.success(map);
}
if (RequestContext.get().isLogin()) {
UserDto update = new UserDto();
update.setLang(lang);
update.setId(RequestContext.get().getUserId());
userApplicationService.update(update);
UserDto userDto = (UserDto) RequestContext.get().getUser();
if (userDto.getRole() != User.Role.Admin) {
removeSystemManageMessage(map);
}
} else {
removeSystemManageMessage(map);
}
Cookie cookie = new Cookie("lang", lang);
cookie.setMaxAge(86400 * 365);
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);
return ReqResult.success(map);
}
private void removeSystemManageMessage(Map<String, String> map) {
if (map == null || map.isEmpty()) {
return;
}
// 先收集需要删除的 key避免在遍历过程中直接修改 map
List<String> keysToRemove = map.keySet().stream()
.filter(key -> key.startsWith("PC.System")) // TODO 确认管理端的 key前缀
.toList();
// 批量删除
keysToRemove.forEach(map::remove);
}
@Operation(summary = "查询语言列表(用户侧:不展示租户默认,仅标当前会话语言)")
@GetMapping(value = "/lang/list", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<List<I18nLangDto>> queryLangList() {
List<I18nLangDto> i18nLangs = i18nLangApplicationService.queryAll();
String currentLang = RequestContext.get().getLang();
if (StringUtils.isNotBlank(currentLang)) {
//不把租户默认当默认展示,统一先置为非默认
i18nLangs.forEach(l -> l.setIsDefault(0));
i18nLangs.stream()
.filter(l -> l.getLang() != null && l.getLang().equalsIgnoreCase(currentLang))
.findFirst()
.ifPresent(l -> l.setIsDefault(1));
}
return ReqResult.success(i18nLangs);
}
}

View File

@@ -0,0 +1,523 @@
package com.xspaceagi.system.web.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.system.application.constant.SupportedLocaleConstants;
import com.xspaceagi.system.application.dto.*;
import com.xspaceagi.system.application.constant.I18nLangTagConstraints;
import com.xspaceagi.system.application.dto.permission.export.I18nConfigExportDto;
import com.xspaceagi.system.application.service.I18nApplicationService;
import com.xspaceagi.system.application.service.I18nConfigDiffService;
import com.xspaceagi.system.application.service.I18nExportService;
import com.xspaceagi.system.application.service.I18nImportService;
import com.xspaceagi.system.application.service.I18nLangApplicationService;
import com.xspaceagi.system.infra.dao.entity.Tenant;
import com.xspaceagi.system.infra.dao.service.TenantService;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.annotation.SaasAdmin;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.constants.I18nSyncConstants;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.I18nSideEnum;
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
/**
* 国际化管理器
*/
@Slf4j
@Tag(name = "多语言管理", description = "国际化相关接口(语言管理、多语言配置)")
@RestController
@RequestMapping("/api/system/i18n")
public class I18nManageController {
@Resource
private I18nLangApplicationService i18nLangApplicationService;
@Resource
private I18nApplicationService i18nApplicationService;
@Resource
private I18nExportService i18nExportService;
@Resource
private I18nImportService i18nImportService;
@Resource
private I18nConfigDiffService i18nConfigDiffService;
@Resource
private TenantService tenantService;
private final long TRANSLATE_BATCH_SIZE = 50L;
// ==================== I18nLang 相关接口 ====================
@Operation(summary = "可选语言列表(用于前端初始化;与 Accept-Language 匹配项排在第 1 位)")
@GetMapping(value = "/locale/bootstrap", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<List<SupportedLocaleOptionDto>> localeBootstrap(HttpServletRequest request) {
List<SupportedLocaleOptionDto> options = SupportedLocaleConstants.DEFAULT_LOCALE_TAG_WHITELIST.stream()
.map(SupportedLocaleOptionDto::fromTag)
.collect(Collectors.toList());
List<Locale> supportedLocales = SupportedLocaleConstants.DEFAULT_LOCALE_TAG_WHITELIST.stream()
.map(Locale::forLanguageTag)
.toList();
String acceptLanguage = request.getHeader("Accept-Language");
if (StringUtils.isBlank(acceptLanguage)) {
return ReqResult.success(options);
}
try {
List<Locale.LanguageRange> ranges = Locale.LanguageRange.parse(acceptLanguage);
Locale matched = Locale.lookup(ranges, supportedLocales);
if (matched == null) {
return ReqResult.success(options);
}
String matchedTag = matched.toLanguageTag();
Optional<SupportedLocaleOptionDto> hit = options.stream()
.filter(o -> matchedTag.equalsIgnoreCase(o.getTag()))
.findFirst();
if (hit.isEmpty()) {
return ReqResult.success(options);
}
List<SupportedLocaleOptionDto> reordered = new ArrayList<>(options.size());
reordered.add(hit.get());
options.stream()
.filter(o -> !matchedTag.equalsIgnoreCase(o.getTag()))
.forEach(reordered::add);
return ReqResult.success(reordered);
} catch (IllegalArgumentException e) {
log.debug("Accept-Language 解析失败: {}", acceptLanguage, e);
return ReqResult.success(options);
}
}
/**
* 新增语言
*/
@RequireResource(I18N_LANG_ADD)
@Operation(summary = "新增语言")
@PostMapping(value = "/lang/add", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Long> addLang(@RequestBody @Valid I18nLangAddDto addDto) {
log.info("[addLang] 新增语言name={}, lang={}", addDto.getName(), addDto.getLang());
Assert.notNull(addDto.getName(), "Parameter 'name' cannot be left blank.");
Assert.notNull(addDto.getLang(), "Parameter 'lang' cannot be left blank.");
Long id = i18nLangApplicationService.add(addDto);
return ReqResult.success(id);
}
/**
* 删除语言
*/
@RequireResource(I18N_LANG_DELETE)
@Operation(summary = "删除语言")
@PostMapping(value = "/lang/delete/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> deleteLang(@PathVariable("id") Long id) {
log.info("[deleteLang] 删除语言id={}", id);
Assert.notNull(id, "Parameter 'id' cannot be left blank.");
i18nLangApplicationService.delete(id);
return ReqResult.success();
}
/**
* 更新语言
*/
@RequireResource(I18N_LANG_MODIFY)
@Operation(summary = "更新语言")
@PostMapping(value = "/lang/update", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> updateLang(@RequestBody @Valid I18nLangUpdateDto updateDto) {
log.info("[updateLang] 更新语言id={}, name={}, status={}",
updateDto.getId(), updateDto.getName(), updateDto.getStatus());
Assert.notNull(updateDto.getId(), "Parameter 'id' cannot be left blank.");
i18nLangApplicationService.update(updateDto);
return ReqResult.success();
}
/**
* 设置为默认语言
*/
@RequireResource(I18N_LANG_MODIFY)
@Operation(summary = "设置为默认语言")
@PostMapping(value = "/lang/setDefault/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> setDefaultLang(@PathVariable("id") Long id) {
log.info("[setDefaultLang] 设置为默认语言id={}", id);
i18nLangApplicationService.setDefault(id);
return ReqResult.success();
}
/**
* 查询全部语言
*/
@RequireResource(I18N_LANG_QUERY)
@Operation(summary = "查询全部语言")
@GetMapping(value = "/lang/list", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<List<I18nLangDto>> queryAllLang() {
log.info("[queryAllLang] 查询全部语言");
List<I18nLangDto> list = i18nLangApplicationService.queryAll();
return ReqResult.success(list);
}
/**
* 批量更新语言排序
*/
@RequireResource(I18N_LANG_MODIFY)
@Operation(summary = "批量更新语言排序")
@PostMapping(value = "/lang/sort", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> updateLangSort(@RequestBody @Valid List<I18nLangDto> sortList) {
log.info("[updateLangSort] 批量更新语言排序size={}", sortList.size());
i18nLangApplicationService.updateSort(sortList);
return ReqResult.success();
}
// ==================== I18nConfig 相关接口 ====================
@RequireResource(I18N_LANG_QUERY)
@Operation(summary = "查询多语言端列表")
@GetMapping(value = "/side/list", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<List<String>> moduleList() {
return ReqResult.success(Arrays.stream(I18nSideEnum.values()).map(I18nSideEnum::getSide).toList());
}
/**
* 分页查询多语言配置条件side、lang、module 精确匹配key 对 fieldKey 模糊匹配)
*/
@RequireResource(I18N_LANG_QUERY)
@Operation(summary = "分页查询多语言配置列表")
@PostMapping(value = "/config/list", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<IPage<I18nConfigDto>> queryI18nConfigList(@RequestBody(required = false) I18nConfigQueryDto query) {
if (query == null) {
query = new I18nConfigQueryDto();
}
log.info("[queryI18nConfigList] 查询多语言配置, query={}", query);
IPage<I18nConfigDto> page = i18nApplicationService.queryI18nConfigPage(RequestContext.get().getTenantId(), query);
return ReqResult.success(page);
}
/**
* 条件与 {@link #queryI18nConfigList} 一致,忽略分页字段,全量导出为 JSON 文件(结构与内置 i18n-config 单行一致)。
*/
@RequireResource(I18N_LANG_QUERY)
@Operation(summary = "按条件导出多语言配置为 JSON 文件")
@PostMapping(value = "/config/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public byte[] exportI18nConfig(@RequestBody(required = false) I18nConfigQueryDto query,
HttpServletResponse response) {
if (query == null) {
query = new I18nConfigQueryDto();
}
log.info("[exportI18nConfig] 按条件导出多语言配置, query={}", query);
I18nConfigQueryExportDto data = i18nApplicationService.exportI18nConfig(RequestContext.get().getTenantId(), query);
String json = JsonSerializeUtil.toJSONString(data);
byte[] body = json.getBytes(StandardCharsets.UTF_8);
String lang = "";
if (StringUtils.isNotBlank(query.getLang())) {
lang = I18nLangTagConstraints.tryNormalizeToStoredForm(query.getLang().trim()).orElse(query.getLang().trim());
}
String fileName = "i18n-config-" + lang + ".json";
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
return body;
}
// @RequireResource(I18N_LANG_TRANSLATE)
// @Operation(summary = "翻译全部配置(不为空的不会翻译)")
// @GetMapping(value = "/config/translateAll", produces = MediaType.APPLICATION_JSON_VALUE)
// public Flux<ReqResult<I18nConfigDto>> translateI18nConfigList(@RequestParam(value = "lang", required = false) String lang) {
// List<I18nConfigDto> list = i18nApplicationService.queryI18nConfigList(RequestContext.get().getTenantId(), "System", null, null, null, lang);
// Flux<ReqResult<I18nConfigDto>> flux = Flux.create(listFluxSink -> {
// list.forEach(item -> {
// if (StringUtils.isNotBlank(item.getValue())) {
// return;
// }
// try {
// listFluxSink.next(ReqResult.success(i18nApplicationService.translateMessage(item)));
// } catch (Exception e) {
// log.warn("翻译异常", e);
// listFluxSink.next(ReqResult.error(I18nMessage.ALERT_MSG_098.getKey()));
// }
// });
// listFluxSink.complete();
// });
// return flux.publishOn(Schedulers.boundedElastic());
// }
// @RequireResource(I18N_LANG_TRANSLATE)
// @Operation(summary = "翻译指定配置(会覆盖原有的)")
// @PostMapping(value = "/config/translate", produces = MediaType.APPLICATION_JSON_VALUE)
// public ReqResult<I18nConfigDto> translateI18nConfig(@RequestBody I18nConfigDto i18nConfigDto) {
// return ReqResult.success(i18nApplicationService.translateMessage(i18nConfigDto));
// }
@RequireResource(I18N_LANG_TRANSLATE)
@Operation(summary = "翻译单个key")
@PostMapping(value = "/config/translateKey", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> translateForKey(@RequestBody I18nConfigDto i18nConfigDto,
@RequestParam("sourceLang") String sourceLang,
@RequestParam("targetLang") String targetLang) {
Assert.hasText(sourceLang, "The source language cannot be empty.");
Assert.hasText(targetLang, "The target language cannot be empty.");
String sourceCanon = I18nLangTagConstraints.tryNormalizeToStoredForm(sourceLang)
.orElseThrow(() -> new IllegalArgumentException(I18nLangTagConstraints.LANG_TAG_MESSAGE_EN));
String targetCanon = I18nLangTagConstraints.tryNormalizeToStoredForm(targetLang)
.orElseThrow(() -> new IllegalArgumentException(I18nLangTagConstraints.LANG_TAG_MESSAGE_EN));
Assert.isTrue(!I18nLangTagConstraints.sameLanguageTag(sourceCanon, targetCanon),
"The source language and target language cannot be the same.");
Long tenantId = RequestContext.get().getTenantId();
i18nApplicationService.translateForKey(tenantId, i18nConfigDto, sourceCanon, targetCanon);
return ReqResult.success();
}
@RequireResource(I18N_LANG_TRANSLATE)
@Operation(summary = "批量翻译指定 key 列表")
@PostMapping(value = "/config/translateKeys", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> translateForKeys(@RequestBody @Valid I18nBatchTranslateDto request) {
String sourceCanon = I18nLangTagConstraints.tryNormalizeToStoredForm(request.getSourceLang())
.orElseThrow(() -> new IllegalArgumentException(I18nLangTagConstraints.LANG_TAG_MESSAGE_EN));
String targetCanon = I18nLangTagConstraints.tryNormalizeToStoredForm(request.getTargetLang())
.orElseThrow(() -> new IllegalArgumentException(I18nLangTagConstraints.LANG_TAG_MESSAGE_EN));
Assert.isTrue(!I18nLangTagConstraints.sameLanguageTag(sourceCanon, targetCanon),
"The source language and target language cannot be the same.");
Long tenantId = RequestContext.get().getTenantId();
i18nApplicationService.translateForKeys(tenantId, request.getKeys(), sourceCanon, targetCanon);
return ReqResult.success();
}
@RequireResource(I18N_LANG_TRANSLATE)
@Operation(summary = "翻译所有key")
@PostMapping(value = "/config/translateAll", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<ReqResult<Map<String, Object>>>> translateForAll(@RequestParam("sourceLang") String sourceLang, @RequestParam("targetLang") String targetLang) {
Assert.hasText(sourceLang, "The source language cannot be empty.");
Assert.hasText(targetLang, "The target language cannot be empty.");
String sourceCanon = I18nLangTagConstraints.tryNormalizeToStoredForm(sourceLang)
.orElseThrow(() -> new IllegalArgumentException(I18nLangTagConstraints.LANG_TAG_MESSAGE_EN));
String targetCanon = I18nLangTagConstraints.tryNormalizeToStoredForm(targetLang)
.orElseThrow(() -> new IllegalArgumentException(I18nLangTagConstraints.LANG_TAG_MESSAGE_EN));
Assert.isTrue(!I18nLangTagConstraints.sameLanguageTag(sourceCanon, targetCanon),
"The source language and target language cannot be the same.");
Long tenantId = RequestContext.get().getTenantId();
List<I18nConfigDto> sourceAll = i18nApplicationService.queryI18nConfigList(tenantId, "System", null, null, null, sourceCanon);
Map<String, I18nConfigDto> sourceMap = sourceAll.stream()
.filter(item -> StringUtils.isNotBlank(item.getKey()) && StringUtils.isNotBlank(item.getValue()))
.collect(Collectors.toMap(I18nConfigDto::getKey, item -> item, (a, b) -> a, LinkedHashMap::new));
List<I18nConfigDto> targetAll = i18nApplicationService.queryI18nConfigList(tenantId, "System", null, null, null, targetCanon);
List<I18nConfigDto> blankTargetList = targetAll.stream()
.filter(item -> StringUtils.isBlank(item.getValue()))
.toList();
long total = blankTargetList.size();
int totalPages = total == 0 ? 0 : (int) ((total + TRANSLATE_BATCH_SIZE - 1) / TRANSLATE_BATCH_SIZE);
List<Map<String, Object>> failedItems = Collections.synchronizedList(new ArrayList<>());
Flux<ServerSentEvent<ReqResult<Map<String, Object>>>> progressStream = Flux
.range(1, totalPages)
.concatMap(pageNo -> Mono.fromCallable(() -> {
RequestContext.setThreadTenantId(tenantId);
try {
int fromIndex = Math.toIntExact((long) (pageNo - 1) * TRANSLATE_BATCH_SIZE);
int toIndex = Math.min(fromIndex + Math.toIntExact(TRANSLATE_BATCH_SIZE), blankTargetList.size());
List<I18nConfigDto> targetChunk = blankTargetList.subList(fromIndex, toIndex);
List<I18nConfigDto> sourceChunk = targetChunk.stream()
.map(item -> sourceMap.get(item.getKey()))
.filter(Objects::nonNull)
.toList();
try {
if (!sourceChunk.isEmpty()) {
i18nApplicationService.translateForKeysBatch(tenantId, sourceChunk, sourceCanon, targetCanon);
}
} catch (Exception e) {
log.warn("translateAll 单页翻译失败, pageNo={}, sourceLang={}, targetLang={}",
pageNo, sourceCanon, targetCanon, e);
Map<String, Object> failed = new HashMap<>();
failed.put("pageNo", pageNo);
failed.put("error", e.getMessage());
failed.put("count", targetChunk.size());
failed.put("keys", targetChunk.stream().map(I18nConfigDto::getKey).toList());
failedItems.add(failed);
}
long completed = Math.min((long) pageNo * TRANSLATE_BATCH_SIZE, total);
Map<String, Object> payload = new java.util.HashMap<>();
payload.put("phase", "progress");
payload.put("total", total);
payload.put("completed", completed);
payload.put("percent", total == 0 ? 100 : (int) (completed * 100 / total));
return ServerSentEvent.<ReqResult<Map<String, Object>>>builder()
.event("progress")
.data(ReqResult.success(payload))
.build();
} catch (Exception e) {
log.warn("translateAll 单页处理失败, pageNo={}, sourceLang={}, targetLang={}",
pageNo, sourceCanon, targetCanon, e);
Map<String, Object> failed = new HashMap<>();
failed.put("pageNo", pageNo);
failed.put("error", e.getMessage());
failed.put("count", 0);
failed.put("keys", List.of());
failedItems.add(failed);
long completed = Math.min((long) pageNo * TRANSLATE_BATCH_SIZE, total);
Map<String, Object> payload = new HashMap<>();
payload.put("phase", "progress");
payload.put("total", total);
payload.put("completed", completed);
payload.put("percent", total == 0 ? 100 : (int) (completed * 100 / total));
return ServerSentEvent.<ReqResult<Map<String, Object>>>builder()
.event("progress")
.data(ReqResult.success(payload))
.build();
} finally {
RequestContext.remove();
}
}).subscribeOn(Schedulers.boundedElastic()));
Flux<ServerSentEvent<ReqResult<Map<String, Object>>>> doneStream = Mono.fromCallable(() -> {
Map<String, Object> payload = new HashMap<>();
payload.put("phase", "done");
payload.put("success", failedItems.isEmpty());
payload.put("total", total);
payload.put("completed", total);
payload.put("percent", 100);
payload.put("failedCount", failedItems.size());
payload.put("failedItems", failedItems);
return ServerSentEvent.<ReqResult<Map<String, Object>>>builder()
.event("done")
.data(ReqResult.success(payload))
.build();
}).flux();
Flux<ServerSentEvent<ReqResult<Map<String, Object>>>> businessStream = progressStream.concatWith(doneStream);
Flux<ServerSentEvent<ReqResult<Map<String, Object>>>> heartbeatStream = Flux.interval(Duration.ofSeconds(10))
.map(seq -> ServerSentEvent.<ReqResult<Map<String, Object>>>builder()
.event("ping")
.data(ReqResult.success(Map.of("phase", "ping", "ts", System.currentTimeMillis())))
.build())
.takeUntilOther(businessStream.ignoreElements());
return Flux.merge(businessStream, heartbeatStream);
}
/**
* 新增或更新多语言配置åå
*/
@RequireResource(I18N_LANG_MODIFY)
@Operation(summary = "新增或更新多语言配置")
@PostMapping(value = "/config/addOrUpdate", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> addOrUpdateI18nConfig(@RequestBody @Valid I18nConfigDto dto) {
log.info("[addOrUpdateI18nConfig] 新增或更新多语言配置type={}, module={}, fieldKey={}",
dto.getType(), dto.getModule(), dto.getKey());
i18nApplicationService.addOrUpdateI18nConfig(dto);
return ReqResult.success();
}
@RequireResource(I18N_LANG_MODIFY)
@Operation(summary = "批量新增或更新多语言配置")
@PostMapping(value = "/config/batchAddOrUpdate", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> batchAddOrUpdateI18nConfig(@RequestBody @Valid List<I18nConfigDto> configs) {
Assert.notEmpty(configs, "The configuration list cannot be empty.");
Assert.isTrue(configs.size() <= 10000, "Supports up to 10,000 configurations per batch.");
i18nApplicationService.batchAddOrUpdateI18nConfig(configs);
return ReqResult.success();
}
@RequireResource(I18N_LANG_DELETE)
@Operation(summary = "批量删除多语言配置")
@PostMapping(value = "/config/batchDelete", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> batchDeleteI18nConfig(@RequestBody List<I18nConfigDto> configs) {
Assert.notEmpty(configs, "The configuration list cannot be empty.");
i18nApplicationService.batchDeleteI18nConfig(configs);
return ReqResult.success();
}
@SaasAdmin
@Operation(hidden = true, summary = "比对两个版本的 i18n 配置并生成新增/变更差异文件")
@GetMapping(value = "/config/diff", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<String> diffConfig(@RequestParam String fromVersion, @RequestParam String toVersion) {
try {
var split = i18nConfigDiffService.generateDiff(fromVersion, toVersion);
String baseDir = "./" + I18nSyncConstants.I18N_JSON_EXPORT_BASE_PATH + "/";
String addFileName = I18nSyncConstants.buildI18nConfigAddFileName(toVersion);
writeDiffJsonFile(baseDir + addFileName, split.getAddRows());
String updateFileName = I18nSyncConstants.buildI18nConfigUpdateFileName(toVersion);
writeDiffJsonFile(baseDir + updateFileName, split.getUpdateRows());
return ReqResult.success("已生成差异文件: " + addFileName + "(新增 " + split.getAddRows().size()
+ " 条), " + updateFileName + "(变更 " + split.getUpdateRows().size() + " 条)");
} catch (IOException e) {
return ReqResult.error(e.getMessage());
}
}
private void writeDiffJsonFile(String saveToPath, Object rows) throws IOException {
Path path = Paths.get(saveToPath).toAbsolutePath();
Files.createDirectories(path.getParent());
Files.writeString(path, JsonSerializeUtil.toJSONString(rows), StandardCharsets.UTF_8);
}
@SaasAdmin
@Operation(hidden = true, summary = "将指定版本的新增/变更差异配置导入到租户")
@GetMapping(value = "/config/import-diff", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<String> importDiffConfig(@RequestParam Long tenantId, @RequestParam String version) {
Tenant tenant = tenantService.getById(tenantId);
if (tenant == null) {
return ReqResult.error("租户不存在");
}
i18nImportService.addConfigToTenant(tenant, version);
i18nImportService.updateConfigToTenant(tenant, version);
return ReqResult.success("差异配置导入成功(新增 + 变更)");
}
@SaasAdmin
@Operation(hidden = true)
@GetMapping(value = "/exportToFile", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Object> exportToFile(@RequestParam String version, @RequestParam List<String> langs) {
try {
I18nConfigExportDto dto = i18nExportService.exportConfig(version, langs);
String json = JsonSerializeUtil.toJSONString(dto.getConfigs());
String saveToPath = "./" + I18nSyncConstants.I18N_JSON_EXPORT_BASE_PATH + "/"
+ I18nSyncConstants.buildI18nConfigExportFileName(version);
Path path = Paths.get(saveToPath).toAbsolutePath();
Files.createDirectories(path.getParent());
Files.writeString(path, json, StandardCharsets.UTF_8);
return ReqResult.success("已导出");
} catch (IOException e) {
return ReqResult.error(e.getMessage());
}
}
@SaasAdmin
@Operation(hidden = true)
@GetMapping(value = "/importFromFile", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<String> importFromFile(@RequestParam Long tenantId, @RequestParam String version) {
Tenant tenant = tenantService.getById(tenantId);
if (tenant == null) {
return ReqResult.error("租户不存在");
}
i18nImportService.importLangToTenant(tenant, version);
i18nImportService.importConfigToTenant(tenant, version);
return ReqResult.success("导入成功");
}
}

View File

@@ -0,0 +1,121 @@
package com.xspaceagi.system.web.controller;
import cn.hutool.core.codec.Base64;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.xspaceagi.file.sdk.IFileAccessService;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.enums.HttpStatusEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.web.emoj.IconGenerator;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
@Tag(name = "文件上传")
@Slf4j
@RestController
public class IconGenerateController {
private static final Color[] rgbs = new Color[]{
new Color(117, 189, 108),
new Color(77, 87, 224),
new Color(63, 118, 247),
new Color(238, 193, 79),
new Color(160, 109, 237)
};
private static final Map<String, Color> componentColorMap = Map.of(
"workflow", new Color(96, 183, 93),
"plugin", new Color(75, 64, 222),
"knowledge", new Color(228, 148, 51),
"table", new Color(255, 187, 0)
);
@Value("${file.uploadFolder:}")
private String uploadFolder;
@Resource
private IFileAccessService iFileAccessService;
private final IconGenerator iconGenerator = new IconGenerator();
@GetMapping("/api/file/**")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
if (!RequestContext.get().isLogin()) {
try {
iFileAccessService.checkFileUrlAk(request.getRequestURI(), request.getParameter("ak"));
} catch (Exception e) {
throw BizException.of(HttpStatusEnum.UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED,
BizExceptionCodeEnum.systemUnauthorizedOrSessionExpired);
}
}
try {
String key = request.getRequestURI().substring("/api/file/".length());
String path0 = uploadFolder.endsWith("/") ? uploadFolder + key : uploadFolder + "/" + key;
Path path = Paths.get(path0);
if (Files.exists(path)) {
//根据key的后缀设置contentType
String contentType = Files.probeContentType(path);
response.setContentType(contentType);
Files.copy(path, response.getOutputStream());
}
} catch (IOException e) {
log.error("文件下载失败", e);
}
}
@GetMapping(path = "/api/logo/**", produces = "image/png")
public byte[] defaultLogo(HttpServletRequest request) throws IOException {
String key = request.getRequestURI().substring("/api/logo/".length());
String text = URLDecoder.decode(key, StandardCharsets.UTF_8);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedImage image = iconGenerator.generateIcon(text, 200);
ImageIO.write(image, "png", baos);
return baos.toByteArray();
}
@GetMapping(path = "/api/logo/{type}/**", produces = "image/png")
public byte[] defaultLogo0(@PathVariable String type, HttpServletRequest request) throws IOException {
String key = request.getRequestURI().substring(("/api/logo/" + type).length());
String text = URLDecoder.decode(key, StandardCharsets.UTF_8);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedImage image = iconGenerator.generateIcon(text, 200);
ImageIO.write(image, "png", baos);
return baos.toByteArray();
}
@GetMapping("/api/qr/{base64text}")
public void generateQrCode(@PathVariable("base64text") String base64text, HttpServletResponse response) {
byte[] decode = Base64.decode(base64text);
try {
BitMatrix matrix = new MultiFormatWriter().encode(new String(decode), BarcodeFormat.QR_CODE, 300, 300);
MatrixToImageWriter.writeToStream(matrix, "PNG", response.getOutputStream());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,154 @@
package com.xspaceagi.system.web.controller;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.xspaceagi.eco.market.sdk.reponse.ClientSecretResponse;
import com.xspaceagi.system.application.dto.*;
import com.xspaceagi.system.application.service.AuthService;
import com.xspaceagi.system.application.service.NotifyMessageApplicationService;
import com.xspaceagi.system.infra.dao.entity.NotifyMessage;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.infra.rpc.ClientSecretRpcService;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.utils.HttpClient;
import com.xspaceagi.system.web.dto.EventRespDto;
import com.xspaceagi.system.web.dto.PullMessageAckDto;
import com.xspaceagi.system.web.dto.PullMessageRequestDto;
import com.xspaceagi.system.web.dto.PullMessageResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Tag(name = "消息通知相关接口")
@RestController
@RequestMapping("/api/notify")
public class NotifyMessageController {
@Resource
private NotifyMessageApplicationService notifyMessageApplicationService;
@Resource
private AuthService authService;
@Resource
private HttpClient httpClient;
@Value("${installation-source}")
private String installationSource;
@Value("${app.version}")
private String version;
@Value("${eco-market.server.base-url}")
private String cloudApi;
@Resource
private ClientSecretRpcService clientSecretRpcService;
@Operation(summary = "查询用户消息列表")
@RequestMapping(path = "/message/list", method = RequestMethod.POST)
public ReqResult<List<NotifyMessageDto>> listQuery(@RequestBody NotifyMessageQueryDto messageQueryDto) {
messageQueryDto.setUserId(RequestContext.get().getUserId());
return ReqResult.success(notifyMessageApplicationService.queryNotifyMessageList(messageQueryDto));
}
private void pullMessage() {
UserDto userDto = (UserDto) RequestContext.get().getUser();
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
if (userDto.getRole() == User.Role.Admin) {
// 管理员拉取云端消息
ClientSecretResponse clientSecretResponse = clientSecretRpcService.queryClientSecret(tenantConfigDto.getTenantId());
if (clientSecretResponse != null) {
PullMessageRequestDto pullMessageRequestDto = new PullMessageRequestDto();
pullMessageRequestDto.setTenantName(tenantConfigDto.getSiteName());
pullMessageRequestDto.setSiteUrl(tenantConfigDto.getSiteUrl());
pullMessageRequestDto.setClientId(clientSecretResponse.getClientId());
pullMessageRequestDto.setClientSecret(clientSecretResponse.getClientSecret());
pullMessageRequestDto.setInstallationSource(installationSource);
pullMessageRequestDto.setVersion(version);
pullMessageRequestDto.setUser(userDto.getNickName() != null ? userDto.getNickName() : userDto.getUserName());
pullMessageRequestDto.setUid(userDto.getUid());
String content = httpClient.post(cloudApi + "/api/message/pull", JSON.toJSONString(pullMessageRequestDto), Map.of("Authorization", "Bearer " + clientSecretResponse.getClientSecret()));
ReqResult<List<PullMessageResponseDto>> pullMessageResponseDtos = JSON.parseObject(content, new TypeReference<ReqResult<List<PullMessageResponseDto>>>() {
});
if (pullMessageResponseDtos != null && CollectionUtils.isNotEmpty(pullMessageResponseDtos.getData())) {
pullMessageResponseDtos.getData().forEach(pullMessageResponseDto -> notifyMessageApplicationService.sendNotifyMessage(SendNotifyMessageDto.builder()
.scope(NotifyMessage.MessageScope.System)
.content(pullMessageResponseDto.getContent())
.senderId(RequestContext.get().getUserId())
.userIds(Arrays.asList(RequestContext.get().getUserId()))
.build()));
PullMessageAckDto pullMessageAckDto = new PullMessageAckDto();
pullMessageAckDto.setClientId(clientSecretResponse.getClientId());
pullMessageAckDto.setClientSecret(clientSecretResponse.getClientSecret());
pullMessageAckDto.setUid(userDto.getUid());
pullMessageAckDto.setMessageIds(pullMessageResponseDtos.getData().stream().map(PullMessageResponseDto::getMessageId).collect(Collectors.toList()));
httpClient.post(cloudApi + "/api/message/ack", JSON.toJSONString(pullMessageAckDto), Map.of("Authorization", "Bearer " + clientSecretResponse.getClientSecret()));
}
}
}
}
@Operation(summary = "查询用户未读消息数量")
@RequestMapping(path = "/message/unread/count", method = RequestMethod.GET)
public ReqResult<Long> unReadCount() {
return ReqResult.success(notifyMessageApplicationService.countUnreadNotifyMessage(RequestContext.get().getUserId()));
}
@Operation(summary = "清除所有未读消息")
@RequestMapping(path = "/message/unread/clear", method = RequestMethod.GET)
public ReqResult<Void> unReadClear() {
notifyMessageApplicationService.updateAllUnreadNotifyMessage(RequestContext.get().getUserId());
return ReqResult.success();
}
@Operation(summary = "更新指定未读消息为已读", description = "消息ID列表作为POST参数例如 [1,2,4]")
@RequestMapping(path = "/message/read", method = RequestMethod.POST)
public ReqResult<Void> read(@RequestBody @Schema(description = "消息ID列表") List<Long> ids) {
notifyMessageApplicationService.updateReadStatus(RequestContext.get().getUserId(), ids);
return ReqResult.success();
}
@Operation(summary = "事件通知查询接口")
@RequestMapping(path = "/event/collect/batch", method = RequestMethod.GET)
public ReqResult<EventRespDto> collectEvent() {
String clientId = authService.getClientId(RequestContext.get().getUserId(), RequestContext.get().getToken());
List<EventDto<?>> eventDtos = notifyMessageApplicationService.collectEventList(RequestContext.get().getUserId(), clientId);
EventRespDto eventRespDto = new EventRespDto();
eventRespDto.setHasEvent(!eventDtos.isEmpty());
eventRespDto.setEventList(eventDtos);
eventRespDto.setVersion(version);
try {
// 之前的逻辑
pullMessage();
} catch (Exception e) {
log.error("触发消息拉取事件失败", e);
}
return ReqResult.success(eventRespDto);
}
@Operation(summary = "事件通知清除接口")
@RequestMapping(path = "/event/clear", method = RequestMethod.GET)
public ReqResult<Void> clearEvent() {
String clientId = authService.getClientId(RequestContext.get().getUserId(), RequestContext.get().getToken());
notifyMessageApplicationService.clearEventList(RequestContext.get().getUserId(), clientId);
return ReqResult.success();
}
}

View File

@@ -0,0 +1,88 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.infra.dao.entity.OpenApiDefinition;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.infra.dao.service.OpenApiDefinitionService;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.PermissionTargetTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Tag(name = "开放API", description = "开放API相关接口")
@RestController
@RequestMapping("/api/system/open-api")
public class OpenApiController {
@Resource
private OpenApiDefinitionService openApiDefinitionService;
@Operation(summary = "查询开放API列表")
@GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<List<OpenApiDefinition>> queryAll(@RequestParam("targetType") Integer targetType) {
if (PermissionTargetTypeEnum.isInValid(targetType)) {
return ReqResult.error("参数targetType错误");
}
List<OpenApiDefinition> list = openApiDefinitionService.queryAll();
PermissionTargetTypeEnum targetTypeEnum = PermissionTargetTypeEnum.getByCode(targetType);
if (targetTypeEnum == PermissionTargetTypeEnum.GROUP) {
List<OpenApiDefinition> filteredList = filterUserRoleApis(list);
log.info("[queryAll] List open APIs, targetType={}, size={}", targetType, filteredList.size());
return ReqResult.success(filteredList);
}
log.info("[queryAll] List open APIs, targetType={}, size={}", targetType, list == null ? 0 : list.size());
return ReqResult.success(list);
}
private List<OpenApiDefinition> filterUserRoleApis(List<OpenApiDefinition> list) {
List<OpenApiDefinition> result = new ArrayList<>();
if (list == null) {
return result;
}
for (OpenApiDefinition item : list) {
OpenApiDefinition copied = copyAndFilter(item);
if (copied != null) {
result.add(copied);
}
}
return result;
}
private OpenApiDefinition copyAndFilter(OpenApiDefinition item) {
if (item == null) {
return null;
}
User.Role role = item.getRole();
if (role != null && role != User.Role.User) {
return null;
}
OpenApiDefinition copied = new OpenApiDefinition();
copied.setName(item.getName());
copied.setKey(item.getKey());
copied.setRole(item.getRole());
copied.setPath(item.getPath());
if (item.getApiList() != null) {
List<OpenApiDefinition> children = new ArrayList<>();
for (OpenApiDefinition child : item.getApiList()) {
OpenApiDefinition copiedChild = copyAndFilter(child);
if (copiedChild != null) {
children.add(copiedChild);
}
}
copied.setApiList(children);
}
return copied;
}
}

View File

@@ -0,0 +1,228 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.application.dto.SpaceDto;
import com.xspaceagi.system.application.dto.SpaceUserDto;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.SpaceApplicationService;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.infra.dao.entity.Space;
import com.xspaceagi.system.infra.dao.entity.SpaceUser;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService;
import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.web.dto.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@Tag(name = "工作空间相关接口")
@RestController
@RequestMapping("/api/space")
@Slf4j
public class SpaceController {
@Resource
private SpaceApplicationService spaceApplicationService;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
private IUserDataPermissionRpcService userDataPermissionRpcService;
@Resource
private UserApplicationService userApplicationService;
@RequireResource(SPACE_CREATE)
@Operation(summary = "创建团队空间接口")
@RequestMapping(path = "/add", method = RequestMethod.POST)
public ReqResult<Long> add(@RequestBody @Valid SpaceAddDto spaceAddDto) {
// 创建团队空间数量限制
UserDataPermissionDto userDataPermission = userDataPermissionRpcService.getUserDataPermission(RequestContext.get().getUserId());
userDataPermission.checkMaxSpaceCount(spaceApplicationService.countUserCreatedTeamSpaces(RequestContext.get().getUserId()).intValue());
SpaceDto spaceDto = SpaceDto.builder()
.name(spaceAddDto.getName())
.description(spaceAddDto.getDescription())
.icon(spaceAddDto.getIcon())
.creatorId(RequestContext.get().getUserId())
.type(Space.Type.Team)
.build();
return ReqResult.success(spaceApplicationService.add(spaceDto));
}
@RequireResource(SPACE_MODIFY)
@Operation(summary = "更新团队空间接口")
@RequestMapping(path = "/update", method = RequestMethod.POST)
public ReqResult<Void> update(@RequestBody @Valid SpaceUpdateDto spaceUpdateDto) {
spacePermissionService.checkSpaceAdminPermission(spaceUpdateDto.getId());
spaceApplicationService.update(SpaceDto.builder().id(spaceUpdateDto.getId()).icon(spaceUpdateDto.getIcon())
.name(spaceUpdateDto.getName()).description(spaceUpdateDto.getDescription())
.receivePublish(spaceUpdateDto.getReceivePublish()).allowDevelop(spaceUpdateDto.getAllowDevelop()).build());
return ReqResult.success();
}
@RequireResource(SPACE_DELETE)
@Operation(summary = "删除空间接口")
@RequestMapping(path = "/delete/{spaceId}", method = RequestMethod.POST)
public ReqResult<Void> delete(@PathVariable Long spaceId) {
SpaceDto spaceDto = spaceApplicationService.queryById(spaceId);
if (spaceDto != null && spaceDto.getType() == Space.Type.Personal) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemPersonalSpaceDeleteForbidden);
}
spacePermissionService.checkSpaceOwnerPermission(spaceId);
spaceApplicationService.delete(spaceId);
return ReqResult.success();
}
//@RequireResource(SPACE_QUERY_DETAIL)
@Operation(summary = "查询指定空间信息")
@RequestMapping(path = "/get/{spaceId}", method = RequestMethod.GET)
public ReqResult<SpaceDto> getOne(@PathVariable Long spaceId) {
spacePermissionService.checkSpaceUserPermission(spaceId);
SpaceDto spaceDto = spaceApplicationService.queryById(spaceId);
SpaceUserDto spaceUserDto = spaceApplicationService.querySpaceUser(spaceId, RequestContext.get().getUserId());
if (spaceUserDto != null) {
spaceDto.setCurrentUserRole(spaceUserDto.getRole());
}
UserDto userDto = userApplicationService.queryById(spaceDto.getCreatorId());
if (userDto != null) {
spaceDto.setCreatorName(userDto.getUserName());
}
I18nUtil.replaceSystemMessage(spaceDto);
return ReqResult.success(spaceDto);
}
@Operation(summary = "跳转到用户空间页面")
@RequestMapping(path = "/redirect", method = RequestMethod.GET)
public void redirect(HttpServletResponse response) throws IOException {
SpaceDto spaceDto = spaceApplicationService.queryListByUserId(RequestContext.get().getUserId()).stream().filter(spaceDto1 -> spaceDto1.getType() == Space.Type.Personal).findFirst().orElse(null);
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
if (spaceDto == null) {
response.sendRedirect(tenantConfigDto.getCasClientHostUrl());
}
String redirectUrl = tenantConfigDto.getCasClientHostUrl().endsWith("/") ? tenantConfigDto.getCasClientHostUrl() + "space/" + spaceDto.getId() + "/develop" :
tenantConfigDto.getCasClientHostUrl() + "/space/" + spaceDto.getId() + "/develop";
response.sendRedirect(redirectUrl);
}
//@RequireResource(SPACE_QUERY_LIST)
@Operation(summary = "查询用户空间列表")
@RequestMapping(path = "/list", method = RequestMethod.GET)
public ReqResult<List<SpaceDto>> list() {
List<SpaceDto> spaceDtoList = spaceApplicationService.queryListByUserId(RequestContext.get().getUserId());
spaceDtoList.forEach(spaceDto -> {
SpaceUserDto spaceUserDto = spaceApplicationService.querySpaceUser(spaceDto.getId(), RequestContext.get().getUserId());
if (spaceUserDto != null) {
spaceDto.setCurrentUserRole(spaceUserDto.getRole());
}
});
I18nUtil.replaceSystemMessage(spaceDtoList);
return ReqResult.success(spaceDtoList);
}
@RequireResource(SPACE_ADD_USER)
@Operation(summary = "增加团队成员接口")
@RequestMapping(path = "/user/add", method = RequestMethod.POST)
public ReqResult<Void> addSpaceUser(@RequestBody @Valid List<SpaceUserAddDto> spaceUserAddDtos) {
if (CollectionUtils.isEmpty(spaceUserAddDtos)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemParamRequired);
}
spaceUserAddDtos.forEach(spaceUserAddDto -> {
if (spaceUserAddDto.getRole() == SpaceUser.Role.Owner) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemCannotAddOwner);
}
spacePermissionService.checkSpaceAdminPermission(spaceUserAddDto.getSpaceId());
});
spaceUserAddDtos.forEach(spaceUserAddDto -> {
SpaceDto spaceDto = spaceApplicationService.queryById(spaceUserAddDto.getSpaceId());
// 团队空间才允许添加成员
if (spaceDto != null && spaceDto.getType() != Space.Type.Personal) {
SpaceUserDto spaceUserDto = new SpaceUserDto();
spaceUserDto.setUserId(spaceUserAddDto.getUserId());
spaceUserDto.setRole(spaceUserAddDto.getRole());
spaceUserDto.setSpaceId(spaceUserAddDto.getSpaceId());
spaceApplicationService.addSpaceUser(spaceUserDto);
}
});
return ReqResult.success();
}
@Operation(summary = "更新团队成员角色接口")
@RequestMapping(path = "/user/role/update", method = RequestMethod.POST)
public ReqResult<Void> updateSpaceUserRole(@RequestBody @Valid SpaceUserRoleUpdateDto spaceUserRoleUpdateDto) {
if (spaceUserRoleUpdateDto.getRole() == SpaceUser.Role.Owner) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemCannotUpdateToOwner);
}
spacePermissionService.checkSpaceAdminPermission(spaceUserRoleUpdateDto.getSpaceId());
spaceApplicationService.updateSpaceUserRole(spaceUserRoleUpdateDto.getSpaceId(), spaceUserRoleUpdateDto.getUserId(), spaceUserRoleUpdateDto.getRole());
return ReqResult.success();
}
@RequireResource(SPACE_DELETE_USER)
@Operation(summary = "删除团队成员接口")
@RequestMapping(path = "/user/delete", method = RequestMethod.POST)
public ReqResult<Void> deleteUser(@RequestBody @Valid SpaceUserDeleteDto spaceUserDeleteDto) {
spacePermissionService.checkSpaceAdminPermission(spaceUserDeleteDto.getSpaceId());
if (spaceUserDeleteDto.getUserId().equals(RequestContext.get().getUserId())) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemCannotDeleteSelf);
}
SpaceDto spaceDto = spaceApplicationService.queryById(spaceUserDeleteDto.getSpaceId());
if (spaceDto == null) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemSpaceNotFound);
}
if (spaceDto.getCreatorId().equals(spaceUserDeleteDto.getUserId())) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemCannotDeleteSpaceCreator);
}
spaceApplicationService.deleteSpaceUser(spaceUserDeleteDto.getSpaceId(), spaceUserDeleteDto.getUserId());
return ReqResult.success();
}
@RequireResource(SPACE_QUERY_USER_LIST)
@Operation(summary = "查询团队成员列表接口")
@RequestMapping(path = "/user/list", method = RequestMethod.POST)
public ReqResult<List<SpaceUserDto>> getTeamUserList(@RequestBody @Valid SpaceUserQueryDto spaceUserQueryDto) {
spacePermissionService.checkSpaceUserPermission(spaceUserQueryDto.getSpaceId());
List<SpaceUserDto> teamUserDtoList = spaceApplicationService.querySpaceUserList(spaceUserQueryDto.getSpaceId());
teamUserDtoList = teamUserDtoList.stream().filter(spaceUserDto -> {
boolean flag = true;
if (StringUtils.isNotBlank(spaceUserQueryDto.getKw())) {
if (!spaceUserDto.getUserName().contains(spaceUserQueryDto.getKw()) && !spaceUserDto.getNickName().contains(spaceUserQueryDto.getKw())) {
flag = false;
}
}
if (spaceUserQueryDto.getRole() != null && spaceUserDto.getRole() != spaceUserQueryDto.getRole()) {
flag = false;
}
return flag;
}).toList();
return ReqResult.success(teamUserDtoList);
}
@RequireResource(SPACE_TRANSFER)
@Operation(summary = "空间转让接口")
@RequestMapping(path = "/transfer", method = RequestMethod.POST)
public ReqResult<Void> transfer(@RequestBody @Valid SpaceTransferDto spaceTransferDto) {
spacePermissionService.checkSpaceOwnerPermission(spaceTransferDto.getSpaceId());
spaceApplicationService.transfer(spaceTransferDto.getSpaceId(), spaceTransferDto.getTargetUserId());
return ReqResult.success();
}
}

View File

@@ -0,0 +1,100 @@
package com.xspaceagi.system.web.controller;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.xspaceagi.eco.market.sdk.reponse.ClientSecretResponse;
import com.xspaceagi.system.infra.rpc.ClientSecretRpcService;
import com.xspaceagi.system.sdk.service.UserAccessKeyApiService;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.web.dto.SttResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;
@Tag(name = "语音转换相关接口")
@Slf4j
@RestController
public class SpeechApiController {
static {
System.setProperty("jdk.httpclient.keepalive.timeout", "0");
}
@Value("${stt.api.base-url:}")
private String baseUrl;
@Resource
private UserAccessKeyApiService userAccessKeyApiService;
@Resource
private ClientSecretRpcService clientSecretRpcService;
private final HttpClient client = HttpClient.newBuilder().connectTimeout(java.time.Duration.ofSeconds(10)).version(HttpClient.Version.HTTP_1_1).build();
@Operation(summary = "语音转文本", description = "上传语音文件,转换为文本")
@PostMapping("/api/audio/stt")
public ReqResult<SttResult> stt(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
if (!RequestContext.get().isLogin()) {
ChatKeyCheck.check(request, userAccessKeyApiService);
}
if (file.isEmpty()) {
return ReqResult.error("Please select a file to upload");
}
try {
ClientSecretResponse clientSecretResponse = clientSecretRpcService.queryClientSecret(RequestContext.get().getTenantId());
if (clientSecretResponse == null) {
return ReqResult.error("Client secret not found");
}
String content = uploadFile(baseUrl, file.getBytes(), clientSecretResponse.getClientSecret());
ReqResult result = JSON.parseObject(content, new TypeReference<ReqResult<SttResult>>() {
});
return result;
} catch (Exception e) {
log.error("File upload failed", e);
return ReqResult.error("File upload failed");
}
}
public String uploadFile(String baseUrl, byte[] fileBytes, String clientSecret) throws Exception {
String boundary = UUID.randomUUID().toString();
String header = "--" + boundary + "\r\n" +
"Content-Disposition: form-data; name=\"audio\"; filename=\"" + "audio.wav" + "\"\r\n" +
"Content-Type: application/octet-stream\r\n\r\n";
String footer = "\r\n--" + boundary + "--\r\n";
byte[] headerBytes = header.getBytes();
byte[] footerBytes = footer.getBytes();
byte[] requestBody = new byte[headerBytes.length + fileBytes.length + footerBytes.length];
System.arraycopy(headerBytes, 0, requestBody, 0, headerBytes.length);
System.arraycopy(fileBytes, 0, requestBody, headerBytes.length, fileBytes.length);
System.arraycopy(footerBytes, 0, requestBody, headerBytes.length + fileBytes.length, footerBytes.length);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/transcribe"))
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.header("Authorization", "Bearer " + clientSecret)
.header("Request-Id", UUID.randomUUID().toString())
.timeout(java.time.Duration.ofSeconds(60))
.POST(HttpRequest.BodyPublishers.ofByteArray(requestBody))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
}
}

View File

@@ -0,0 +1,47 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.application.service.SysApplicationService;
import com.xspaceagi.system.application.service.SysUserPermissionCacheService;
import com.xspaceagi.system.spec.dto.ReqResult;
import jakarta.annotation.Resource;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/api/sys")
public class SysController {
private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Resource
private SysApplicationService sysApplicationService;
@Resource
private SysUserPermissionCacheService sysUserPermissionCacheService;
/**
* 获取系统启动时间,排查问题使用
*/
@GetMapping(value = "/v", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<String> startTime() {
LocalDateTime serverStartupTime = sysApplicationService.getServerStartupTime();
return ReqResult.success(serverStartupTime != null ? serverStartupTime.format(FORMAT) : null);
}
@GetMapping(value = "/permission-cache-time", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<String> permissionCacheTime() {
Long timestamp = sysUserPermissionCacheService.getPermissionLatestCacheTime();
if (timestamp == null) {
return ReqResult.success(null);
}
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
return ReqResult.success(dateTime.format(FORMAT));
}
}

View File

@@ -0,0 +1,137 @@
package com.xspaceagi.system.web.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.system.infra.dao.model.OperatorLogModel;
import com.xspaceagi.system.infra.repository.ISysOperatorLogRepository;
import com.xspaceagi.system.infra.service.QueryVoListDelegateService;
import com.xspaceagi.system.sdk.operate.ActionType;
import com.xspaceagi.system.sdk.operate.OperationLogReporter;
import com.xspaceagi.system.sdk.operate.SystemEnum;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.page.PageQueryParamVo;
import com.xspaceagi.system.spec.page.PageQueryVo;
import com.xspaceagi.system.spec.page.SuperPage;
import com.xspaceagi.system.web.dto.operatorlog.EnumOptionDto;
import com.xspaceagi.system.web.dto.operatorlog.OperatorLogDto;
import com.xspaceagi.system.web.dto.operatorlog.OperatorLogQueryDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestBody;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "系统配置-操作日志")
@RestController
@RequestMapping("/api/sys/operator/log")
@Slf4j
public class SysOperatorLogController {
@Resource
private QueryVoListDelegateService queryVoListDelegateService;
@Resource
private ISysOperatorLogRepository sysOperatorLogRepository;
@OperationLogReporter(actionType = ActionType.QUERY,
action = "操作日志页面列表查询", objectName = "操作日志", systemCode = SystemEnum.SYSTEM)
@Operation(summary = "操作日志页面列表查询")
@ApiResponses(
@ApiResponse(responseCode = "0000", description = "成功")
)
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<IPage<OperatorLogDto>> list(@RequestBody PageQueryVo<OperatorLogQueryDto> pageQueryVo) {
OperatorLogQueryDto queryFilter = pageQueryVo.getQueryFilter();
// 时间戳转换为 LocalDateTime复用现有时间范围查询机制
if (queryFilter != null) {
if (queryFilter.getCreateTimeGt() != null || queryFilter.getCreateTimeLt() != null) {
List<LocalDateTime> created = new ArrayList<>(2);
if (queryFilter.getCreateTimeGt() != null) {
created.add(LocalDateTime.ofInstant(
Instant.ofEpochMilli(queryFilter.getCreateTimeGt()),
ZoneId.systemDefault()));
}
if (queryFilter.getCreateTimeLt() != null) {
created.add(LocalDateTime.ofInstant(
Instant.ofEpochMilli(queryFilter.getCreateTimeLt()),
ZoneId.systemDefault()));
}
queryFilter.setCreated(created);
}
}
PageQueryParamVo pageQueryParamVo = new PageQueryParamVo(pageQueryVo);
SuperPage<OperatorLogModel> superPage = this.queryVoListDelegateService.queryVoList(this.sysOperatorLogRepository,
pageQueryParamVo, null);
var userModelList = superPage.getRecords();
//类型转换
List<OperatorLogDto> dtoList = userModelList.stream().map(OperatorLogDto::convertToDto).toList();
SuperPage<OperatorLogDto> iPage = SuperPage.build(superPage, dtoList);
iPage.setCurrent(pageQueryVo.getCurrent());
iPage.setSize(pageQueryVo.getPageSize());
return ReqResult.success(iPage);
}
@OperationLogReporter(actionType = ActionType.QUERY,
action = "操作日志详情查看", objectName = "操作日志", systemCode = SystemEnum.SYSTEM)
@Operation(summary = "操作日志详情查看")
@ApiResponses(
@ApiResponse(responseCode = "0000", description = "成功")
)
@RequestMapping(path = "/queryById", method = RequestMethod.GET)
public ReqResult<OperatorLogDto> queryById(Long id) {
var model = this.sysOperatorLogRepository.queryOneInfoById(id);
OperatorLogDto dto = OperatorLogDto.convertToDto(model);
return ReqResult.success(dto);
}
@OperationLogReporter(actionType = ActionType.QUERY,
action = "获取系统编码选项", objectName = "系统编码", systemCode = SystemEnum.SYSTEM)
@Operation(summary = "获取系统编码选项列表")
@ApiResponses(@ApiResponse(responseCode = "0000", description = "成功"))
@RequestMapping(path = "/systemCode/options", method = RequestMethod.GET)
public ReqResult<List<EnumOptionDto>> getSystemCodeOptions() {
List<EnumOptionDto> options = Arrays.stream(SystemEnum.values())
.map(e -> EnumOptionDto.builder()
.label(e.getDesc())
.value(e.name())
.build())
.collect(Collectors.toList());
return ReqResult.success(options);
}
@OperationLogReporter(actionType = ActionType.QUERY,
action = "获取操作类型选项", objectName = "操作类型", systemCode = SystemEnum.SYSTEM)
@Operation(summary = "获取操作类型选项列表")
@ApiResponses(@ApiResponse(responseCode = "0000", description = "成功"))
@RequestMapping(path = "/actionType/options", method = RequestMethod.GET)
public ReqResult<List<EnumOptionDto>> getActionTypeOptions() {
List<EnumOptionDto> options = Arrays.stream(ActionType.values())
.map(e -> EnumOptionDto.builder()
.label(e.getName())
.value(e.name())
.build())
.collect(Collectors.toList());
return ReqResult.success(options);
}
}

View File

@@ -0,0 +1,70 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.TenantConfigItemDto;
import com.xspaceagi.system.application.service.TenantConfigApplicationService;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.spec.utils.RedisUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@Tag(name = "系统管理-系统配置相关接口")
@RestController
@RequestMapping("/api/system/config")
public class TenantConfigManageController {
@Resource
private TenantConfigApplicationService tenantConfigApplicationService;
@Resource
private RedisUtil redisUtil;
@RequireResource({SYSTEM_SETTING_BASIC})
@Operation(summary = "查询配置列表")
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<List<TenantConfigItemDto>> listQuery() {
List<TenantConfigItemDto> tenantConfigList = tenantConfigApplicationService.getTenantConfigList();
I18nUtil.replaceSystemMessage(tenantConfigList);
redisUtil.expire("tenant_config:" + RequestContext.get().getTenantId(), 0);
return ReqResult.success(tenantConfigList);
}
@RequireResource({SYSTEM_SETTING_SAVE})
@Operation(summary = "更新配置信息")
@RequestMapping(path = "/add", method = RequestMethod.POST)
public ReqResult<Void> update(@RequestBody TenantConfigDto tenantConfigDto) {
tenantConfigDto.setTenantId(RequestContext.get().getTenantId());
tenantConfigApplicationService.updateConfig(tenantConfigDto);
return ReqResult.success();
}
@RequireResource({SYSTEM_SETTING_SAVE})
@Operation(summary = "更新配置信息")
@RequestMapping(path = "/save", method = RequestMethod.POST)
public ReqResult<Void> save(@RequestBody TenantConfigDto tenantConfigDto) {
tenantConfigDto.setTenantId(RequestContext.get().getTenantId());
tenantConfigApplicationService.updateConfig(tenantConfigDto);
return ReqResult.success();
}
@RequireResource({SYSTEM_THEME_CONFIG_SAVE})
@Operation(summary = "更新主题配置")
@RequestMapping(path = "/update-theme", method = RequestMethod.POST)
public ReqResult<Void> updateTheme(@RequestBody TenantConfigDto tenantConfigDto) {
tenantConfigDto.setTenantId(RequestContext.get().getTenantId());
tenantConfigApplicationService.updateConfig(tenantConfigDto);
return ReqResult.success();
}
}

View File

@@ -0,0 +1,112 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.file.sdk.IFileAccessService;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.utils.I18nUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "租户信息查询接口")
@RestController
@RequestMapping("/api/tenant")
public class TenantController {
@Value("${app.version:1.0.0}")
private String version;
@Resource
private IFileAccessService iFileAccessService;
@Value("${supportCustomDomain:false}")
private String supportCustomDomain;
@Value("${spring.servlet.multipart.max-file-size:100MB}")
private String maxFileSize;
@Value("${license:}")
private String license;
@Value("${model-api-proxy.base-api-url:}")
private String baseApiUrl;
@Operation(summary = "租户配置信息查询接口")
@RequestMapping(path = "/config", method = RequestMethod.GET)
public ReqResult<TenantConfigDto> getConfig() {
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
tenantConfigDto.setDefaultAgentIds(null);
tenantConfigDto.setDomainNames(null);
tenantConfigDto.setDefaultSuggestModelId(null);
tenantConfigDto.setDefaultSummaryModelId(null);
tenantConfigDto.setRecommendAgentIds(null);
tenantConfigDto.setCodeSafeCheckPrompt(null);
tenantConfigDto.setGlobalSystemPrompt(null);
tenantConfigDto.setUserWhiteList(null);
tenantConfigDto.setSmtpHost(null);
tenantConfigDto.setSmtpPassword(null);
tenantConfigDto.setSmtpUsername(null);
tenantConfigDto.setSmtpPort(null);
tenantConfigDto.setSmsAccessKeyId(null);
tenantConfigDto.setSmsAccessKeySecret(null);
tenantConfigDto.setSmsSignName(null);
tenantConfigDto.setSmsTemplateCode(null);
tenantConfigDto.setCaptchaAccessKeyId(null);
tenantConfigDto.setCaptchaAccessKeySecret(null);
tenantConfigDto.setMpAppId(null);
tenantConfigDto.setMpAppSecret(null);
tenantConfigDto.setSandboxConfig(null);
tenantConfigDto.setVersion(version);
tenantConfigDto.setSupportCustomDomain(StringUtils.equals(supportCustomDomain, "true"));
tenantConfigDto.setEnabledSandbox(true);
tenantConfigDto.setSiteLogo(iFileAccessService.getFileUrlWithAk(tenantConfigDto.getSiteLogo(), true));
tenantConfigDto.setFaviconUrl(iFileAccessService.getFileUrlWithAk(tenantConfigDto.getFaviconUrl(), true));
tenantConfigDto.setSquareBanner(iFileAccessService.getFileUrlWithAk(tenantConfigDto.getSquareBanner(), true));
tenantConfigDto.setMaxFileSize(maxFileSize);
if (StringUtils.isBlank(tenantConfigDto.getTemplateConfig()) || tenantConfigDto.getTemplateConfig().equals("{}")) {
tenantConfigDto.setTemplateConfig("{\"primaryColor\":\"#5147ff\",\"backgroundId\":\"bg-variant-1\",\"antdTheme\":\"light\",\"layoutStyle\":\"light\",\"navigationStyle\":\"style1\",\"timestamp\":1757425328082}");
}
String userName = "";
if (RequestContext.get().isLogin()) {
UserDto user = (UserDto) RequestContext.get().getUser();
if (StringUtils.isNotBlank(user.getNickName())) {
userName = user.getNickName();
} else {
userName = user.getUserName();
}
}
if (StringUtils.isBlank(tenantConfigDto.getHomeSlogan())) {
tenantConfigDto.setHomeSlogan("Hi {{USER_NAME}}, how can I help you today?");
}
if (StringUtils.isNotBlank(tenantConfigDto.getHomeSlogan())) {
tenantConfigDto.setHomeSlogan(tenantConfigDto.getHomeSlogan().replace("{{USER_NAME}}", userName));
}
if (StringUtils.isBlank(tenantConfigDto.getLoginPageText())) {
tenantConfigDto.setLoginPageText("Easily build and deploy your<br>private Agentic AI solution");
}
if (StringUtils.isBlank(tenantConfigDto.getLoginPageSubText())) {
tenantConfigDto.setLoginPageSubText("Comprehensive workflow and plugin development, RAG knowledge base and data table storage, MCP integration and open capabilities");
}
if (StringUtils.isNotBlank(license)) {
tenantConfigDto.setCommercialEdition(true);
}
String siteUrl = tenantConfigDto.getSiteUrl();
if (StringUtils.isNotBlank(baseApiUrl)) {
siteUrl = baseApiUrl;
}
if (siteUrl.endsWith("/")) {
siteUrl = siteUrl.substring(0, siteUrl.length() - 1);
}
tenantConfigDto.setBaseModelApiUrl(siteUrl + "/api/proxy/model");
I18nUtil.replaceSystemMessage(tenantConfigDto);
return ReqResult.success(tenantConfigDto);
}
}

View File

@@ -0,0 +1,65 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.application.dto.TenantAddDto;
import com.xspaceagi.system.application.dto.TenantDto;
import com.xspaceagi.system.application.service.TenantConfigApplicationService;
import com.xspaceagi.system.infra.dao.entity.Tenant;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "系统管理-租户管理相关接口")
@RestController
@RequestMapping("/api/v1/tenant")
public class TenantManageController {
@Resource
private TenantConfigApplicationService tenantConfigApplicationService;
@Operation(summary = "查询租户列表")
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<List<TenantDto>> listQuery() {
checkPermission();
return ReqResult.success(tenantConfigApplicationService.getTenantList());
}
@Operation(summary = "新增租户")
@RequestMapping(path = "/add", method = RequestMethod.POST)
public ReqResult<Void> add(@RequestBody @Valid TenantAddDto tenant) {
checkPermission();
if (tenantConfigApplicationService.queryTenantIdByDomainName(tenant.getDomain()) != null) {
return ReqResult.error("域名`" + tenant.getDomain() + "`已被占用");
}
tenant.setDomain(tenant.getDomain().toLowerCase());
tenantConfigApplicationService.addTenant(tenant);
return ReqResult.success();
}
@Operation(summary = "更新租户信息")
@RequestMapping(path = "/update", method = RequestMethod.POST)
public ReqResult<Void> update(@RequestBody Tenant tenant) {
checkPermission();
tenantConfigApplicationService.updateTenant(tenant);
return ReqResult.success();
}
private void checkPermission() {
//租户ID为1的管理员拥有所有租户的管理权限
if (RequestContext.get().getTenantId() != 1) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
}

View File

@@ -0,0 +1,95 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.agent.core.sdk.IModelRpcService;
import com.xspaceagi.system.application.dto.UpdateApiKeyDto;
import com.xspaceagi.system.application.service.UserApiKeyApplicationService;
import com.xspaceagi.system.infra.dao.entity.OpenApiDefinition;
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.web.dto.ApiInvokeStatDto;
import com.xspaceagi.system.web.dto.ApiKeyCreateDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Tag(name = "用户APIKEY管理", description = "用户APIKEY管理相关接口")
@RestController
@RequestMapping("/api/user/api-key")
public class UserApiKeyController {
@Resource
private UserApiKeyApplicationService userApiKeyApplicationService;
@Resource
private IModelRpcService iModelRpcService;
@Operation(summary = "新增创建APIKEY")
@PostMapping("/create")
public ReqResult<UserAccessKeyDto> create(@RequestBody ApiKeyCreateDto apiKeyCreateDto) {
UserAccessKeyDto userApiKey = userApiKeyApplicationService.createUserApiKey(RequestContext.get().getUserId(), apiKeyCreateDto.getName(), apiKeyCreateDto.getExpire());
return ReqResult.success(userApiKey);
}
@Operation(summary = "更新APIKEY")
@PostMapping("/update")
public ReqResult<Void> update(@RequestBody UpdateApiKeyDto updateApiKeyDto) {
updateApiKeyDto.setUserId(RequestContext.get().getUserId());
userApiKeyApplicationService.updateUserApiKey(updateApiKeyDto);
return ReqResult.success();
}
@Operation(summary = "删除APIKEY")
@PostMapping("/delete/{apiKey}")
public ReqResult<Void> delete(@PathVariable String apiKey) {
userApiKeyApplicationService.deleteUserApiKey(RequestContext.get().getUserId(), apiKey);
return ReqResult.success();
}
@Operation(summary = "查询用户APIKEY列表")
@GetMapping("/list")
public ReqResult<List<UserAccessKeyDto>> list() {
List<UserAccessKeyDto> userApiKeys = userApiKeyApplicationService.getUserApiKeys(RequestContext.get().getUserId());
return ReqResult.success(userApiKeys);
}
@Operation(summary = "获取用户有权限的API列表")
@GetMapping("/open-api-definitions")
public ReqResult<List<OpenApiDefinition>> openApiDefinitions() {
List<OpenApiDefinition> openApiDefinitions = userApiKeyApplicationService.queryOpenApiDefinitions(RequestContext.get().getUserId());
return ReqResult.success(openApiDefinitions);
}
//TODO 完善
@Operation(summary = "APIKEY对应的接口调用统计")
@GetMapping("/stats")
public ReqResult<List<ApiInvokeStatDto>> stats() {
List<OpenApiDefinition> openApiDefinitions = userApiKeyApplicationService.queryOpenApiDefinitions(RequestContext.get().getUserId());
List<ApiInvokeStatDto> apiInvokeStats = new ArrayList<>();
openApiDefinitions.forEach(openApiDefinition -> {
if (CollectionUtils.isNotEmpty(openApiDefinition.getApiList())) {
openApiDefinition.getApiList().forEach(apiDefinition -> {
ApiInvokeStatDto apiInvokeStat = new ApiInvokeStatDto();
apiInvokeStat.setName(apiDefinition.getName());
apiInvokeStat.setPath(apiDefinition.getPath());
apiInvokeStat.setKey(apiDefinition.getKey());
apiInvokeStat.setTotal(ApiInvokeStatDto.InvokeCount.builder().failCount(0L).successCount(100L).totalCount(20L).build());
apiInvokeStat.setToday(ApiInvokeStatDto.InvokeCount.builder().failCount(0L).successCount(100L).totalCount(20L).build());
apiInvokeStat.setYesterday(ApiInvokeStatDto.InvokeCount.builder().failCount(0L).successCount(100L).totalCount(20L).build());
apiInvokeStat.setWeek(ApiInvokeStatDto.InvokeCount.builder().failCount(0L).successCount(100L).totalCount(20L).build());
apiInvokeStat.setMonth(ApiInvokeStatDto.InvokeCount.builder().failCount(0L).successCount(100L).totalCount(20L).build());
apiInvokeStats.add(apiInvokeStat);
});
}
});
return ReqResult.success(apiInvokeStats);
}
}

View File

@@ -0,0 +1,383 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.dto.permission.MenuNodeDto;
import com.xspaceagi.system.application.service.AuthService;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.application.service.impl.SysUserPermissionCacheServiceImpl;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.infra.verify.VerifyCodeSendAndCheckService;
import com.xspaceagi.system.infra.verify.captcha.CaptchaConfig;
import com.xspaceagi.system.infra.verify.email.SmtpConfig;
import com.xspaceagi.system.infra.verify.sms.SmsConfig;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.PageQueryVo;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.CodeTypeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.web.dto.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Date;
import java.util.List;
@Slf4j
@Tag(name = "用户相关接口", description = "错误码4010为未认证")
@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
private UserApplicationService userApplicationService;
@Resource
private VerifyCodeSendAndCheckService verifyCodeSendAndCheckService;
@Resource
private SysUserPermissionCacheServiceImpl sysUserAuthCacheService;
@Resource
private SysUserPermissionCacheServiceImpl sysUserPermissionCacheService;
@Resource
private AuthService authService;
@Operation(summary = "密码登录接口")
@RequestMapping(path = "/passwordLogin", method = RequestMethod.POST)
public ReqResult<LoginResDto> passwordLogin(@RequestBody PasswordLoginDto loginDto, HttpServletRequest request, HttpServletResponse response) {
verifyCodeSendAndCheckService.checkCaptchaVerifyParam(buildCaptchaConfig(), loginDto.getCaptchaVerifyParam());
String token = authService.loginWithPassword(StringUtils.isBlank(loginDto.getPhone()) ? loginDto.getPhoneOrEmail() : loginDto.getPhone(), loginDto.getPassword());
return ReqResult.success(buildLoginResDto(token, request, response));
}
@Operation(summary = "验证码登录/注册接口")
@RequestMapping(path = "/codeLogin", method = RequestMethod.POST)
public ReqResult<LoginResDto> codeLogin(@RequestBody CodeLoginDto loginDto, HttpServletRequest request, HttpServletResponse response) {
String token = authService.loginWithCode(StringUtils.isBlank(loginDto.getPhone()) ? loginDto.getPhoneOrEmail() : loginDto.getPhone(), loginDto.getCode());
return ReqResult.success(buildLoginResDto(token, request, response));
}
@Operation(summary = "小程序code登录")
@RequestMapping(path = "/mp/codeLogin", method = RequestMethod.POST)
public ReqResult<LoginResDto> mpCodeLogin(@RequestBody MPCodeLoginDto loginDto, HttpServletRequest request, HttpServletResponse response) {
String token = authService.loginWithMpCode(loginDto.getCode());
return ReqResult.success(buildLoginResDto(token, request, response));
}
@Operation(summary = "退出登录接口")
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public ReqResult<Void> logout(HttpServletResponse response) {
authService.expireToken(RequestContext.get().getToken());
Cookie cookie = new Cookie("ticket", "");
cookie.setMaxAge(-1);
cookie.setPath("/");
response.addCookie(cookie);
return ReqResult.success();
}
@Operation(summary = "发送验证码")
@RequestMapping(path = "/code/send", method = RequestMethod.POST)
public ReqResult<Void> sendCode(@RequestBody CodeSendDto codeSendDto) {
if (!RequestContext.get().isLogin()) {
verifyCodeSendAndCheckService.checkCaptchaVerifyParam(buildCaptchaConfig(), codeSendDto.getCaptchaVerifyParam());
}
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
if (CodeSendDto.Type.RESET_PASSWORD == codeSendDto.getType()) {
UserDto curUserDto = (UserDto) RequestContext.get().getUser();
//3 为邮箱登录
if (tenantConfigDto.getAuthType() == TenantConfigDto.AuthTypeEnum.EMAIL.getCode()) {
codeSendDto.setEmail(curUserDto.getEmail());
}
if (tenantConfigDto.getAuthType() == TenantConfigDto.AuthTypeEnum.PHONE.getCode()) {
codeSendDto.setPhone(curUserDto.getPhone());
} else {
codeSendDto.setEmail(curUserDto.getEmail());
}
}
String code = null;
if (StringUtils.isNotBlank(codeSendDto.getPhone())) {
SmsConfig smsConfig = new SmsConfig();
smsConfig.setSmsAccessKeyId(tenantConfigDto.getSmsAccessKeyId());
smsConfig.setSmsAccessKeySecret(tenantConfigDto.getSmsAccessKeySecret());
smsConfig.setSmsSignName(tenantConfigDto.getSmsSignName());
smsConfig.setSmsTemplateCode(tenantConfigDto.getSmsTemplateCode());
code = verifyCodeSendAndCheckService.sendPhoneCode(smsConfig, CodeTypeEnum.valueOf(codeSendDto.getType().name()), codeSendDto.getPhone());
}
if (StringUtils.isNotBlank(codeSendDto.getEmail())) {
SmtpConfig stmpConfig = new SmtpConfig();
stmpConfig.setHost(tenantConfigDto.getSmtpHost());
stmpConfig.setPort(tenantConfigDto.getSmtpPort());
stmpConfig.setUsername(tenantConfigDto.getSmtpUsername());
stmpConfig.setPassword(tenantConfigDto.getSmtpPassword());
stmpConfig.setSiteName(tenantConfigDto.getSiteName());
code = verifyCodeSendAndCheckService.sendEmailCode(stmpConfig, CodeTypeEnum.valueOf(codeSendDto.getType().name()), codeSendDto.getEmail());
}
log.info("发送验证码:{},手机号:{},邮箱:{}", code, codeSendDto.getPhone(), codeSendDto.getEmail());
if (tenantConfigDto.getAuthType() == TenantConfigDto.AuthTypeEnum.PHONE.getCode() && StringUtils.isBlank(tenantConfigDto.getSmsAccessKeyId())) {
throw new BizException(I18nUtil.systemMessage("Backend.User.SendCode.SmsServiceNotConfigured", code));
}
if (tenantConfigDto.getAuthType() == TenantConfigDto.AuthTypeEnum.EMAIL.getCode() && StringUtils.isBlank(tenantConfigDto.getSmtpUsername())) {
throw new BizException(I18nUtil.systemMessage("Backend.User.SendCode.EmailServiceNotConfigured", code));
}
return ReqResult.success();
}
@Operation(summary = "检测邮箱是否被占用")
@RequestMapping(path = "/email/check/{email}", method = RequestMethod.POST)
public ReqResult<Boolean> checkEmail(@PathVariable String email) {
//验证邮箱格式,邮箱用户名支持 .
if (!email.matches("^[a-zA-Z0-9._-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$")) {
throw new BizException(I18nUtil.systemMessage("Backend.User.Email.InvalidFormat"));
}
boolean isUsed = null != userApplicationService.queryUserByEmail(email);
return ReqResult.success(isUsed);
}
@Operation(summary = "绑定邮箱")
@RequestMapping(path = "/email/bind", method = RequestMethod.POST)
public ReqResult<Void> bindEmail(@RequestBody BindEmailDto bindEmailDto) {
if (StringUtils.isNotBlank(bindEmailDto.getEmail())) {
//验证邮箱格式
if (!bindEmailDto.getEmail().matches("^[a-zA-Z0-9._-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$")) {
throw new BizException(I18nUtil.systemMessage("Backend.User.Email.InvalidFormat"));
}
verifyCodeSendAndCheckService.checkEmailCode(CodeTypeEnum.BIND_EMAIL, bindEmailDto.getEmail(), bindEmailDto.getCode());
boolean isUsed = null != userApplicationService.queryUserByEmail(bindEmailDto.getEmail());
if (isUsed) {
throw new BizException(I18nUtil.systemMessage("Backend.User.Email.Occupied"));
}
} else if (StringUtils.isNotBlank(bindEmailDto.getPhone())) {
verifyCodeSendAndCheckService.checkPhoneCode(CodeTypeEnum.BIND_EMAIL, bindEmailDto.getPhone(), bindEmailDto.getCode());
boolean isUsed = null != userApplicationService.queryUserByPhone(bindEmailDto.getPhone());
if (isUsed) {
throw new BizException(I18nUtil.systemMessage("Backend.User.Phone.Occupied"));
}
}
UserDto userDto = (UserDto) RequestContext.get().getUser();
UserDto userUpdate = new UserDto();
userUpdate.setId(userDto.getId());
userUpdate.setEmail(bindEmailDto.getEmail());
userUpdate.setPhone(bindEmailDto.getPhone());
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@Operation(summary = "重置密码")
@RequestMapping(path = "/password/reset", method = RequestMethod.POST)
public ReqResult<Void> resetPassword(@RequestBody ResetPasswordDto resetPasswordDto) {
TenantConfigDto tenantConfigDto = (TenantConfigDto) RequestContext.get().getTenantConfig();
UserDto curUserDto = (UserDto) RequestContext.get().getUser();
if ((tenantConfigDto.getAuthType() == 1 && StringUtils.isNotBlank(tenantConfigDto.getSmsAccessKeyId())) || tenantConfigDto.getAuthType() == 3 && StringUtils.isNotBlank(tenantConfigDto.getSmtpUsername())) {
try {
verifyCodeSendAndCheckService.checkPhoneCode(CodeTypeEnum.RESET_PASSWORD, curUserDto.getPhone(), resetPasswordDto.getCode());
} catch (Exception e) {
verifyCodeSendAndCheckService.checkEmailCode(CodeTypeEnum.RESET_PASSWORD, curUserDto.getEmail(), resetPasswordDto.getCode());
}
}
UserDto userDto = (UserDto) RequestContext.get().getUser();
UserDto userUpdate = new UserDto();
userUpdate.setId(userDto.getId());
userUpdate.setPassword(resetPasswordDto.getNewPassword());
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@Operation(summary = "首次登录设置密码")
@RequestMapping(path = "/password/set", method = RequestMethod.POST)
public ReqResult<Void> setPassword(@RequestBody SetPasswordDto setPasswordDto) {
UserDto userDto = (UserDto) RequestContext.get().getUser();
if (userDto.getResetPass() == 0) {
UserDto userUpdate = new UserDto();
userUpdate.setId(userDto.getId());
userUpdate.setPassword(setPasswordDto.getPassword());
userUpdate.setResetPass(1);
userApplicationService.update(userUpdate);
}
return ReqResult.success();
}
@Operation(summary = "更新用户信息")
@RequestMapping(path = "/update", method = RequestMethod.POST)
public ReqResult<Void> update(@RequestBody UserUpdateDto userUpdateDto) {
UserDto userDto = (UserDto) RequestContext.get().getUser();
UserDto userUpdate = new UserDto();
userUpdate.setId(userDto.getId());
userUpdate.setAvatar(userUpdateDto.getAvatar());
userUpdate.setNickName(userUpdateDto.getNickName());
userUpdate.setUserName(userUpdateDto.getUserName());
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@Operation(summary = "查询当前登录用户信息")
@RequestMapping(path = "/getLoginInfo", method = RequestMethod.GET)
public ReqResult<UserDto> getLoginUserInfo() {
UserDto user = (UserDto) RequestContext.get().getUser();
user.setLangMap(null);
return ReqResult.success(user);
}
@Operation(summary = "获取当前登录用户的动态认证码")
@RequestMapping(path = "/dynamicCode", method = RequestMethod.GET)
public ReqResult<String> dynamicCode() {
return ReqResult.success(userApplicationService.getUserDynamicCode(((UserDto) RequestContext.get().getUser()).getId()));
}
@Operation(summary = "查询用户信息")
@RequestMapping(path = "/query", method = RequestMethod.POST)
public ReqResult<UserDto> queryUser(@RequestBody UserInfoQueryDto userInfoQueryDto) {
UserDto userDto = null;
if (StringUtils.isNotBlank(userInfoQueryDto.getUserName())) {
userDto = userApplicationService.queryUserByUserName(userInfoQueryDto.getUserName());
} else if (StringUtils.isNotBlank(userInfoQueryDto.getEmail())) {
userDto = userApplicationService.queryUserByEmail(userInfoQueryDto.getEmail());
} else if (StringUtils.isNotBlank(userInfoQueryDto.getPhone())) {
userDto = userApplicationService.queryUserByPhone(userInfoQueryDto.getPhone());
}
// 返回用户信息时,不返回密码、手机号、邮箱、状态、角色
if (userDto != null) {
userDto.setPassword(null);
userDto.setPhone(null);
userDto.setEmail(null);
userDto.setStatus(null);
userDto.setRole(null);
if (userDto.getStatus() == User.Status.Deleted) {
return ReqResult.success(null);
}
}
return ReqResult.success(userDto);
}
@Operation(summary = "创建临时ticket")
@RequestMapping(path = "/ticket/create", method = RequestMethod.POST)
public ReqResult<String> createTicket(@RequestBody TicketCreateDto ticketCreateDto) {
UserDto loginUserInfo = authService.getLoginUserInfo(ticketCreateDto.getToken());
if (loginUserInfo != null) {
return ReqResult.success(authService.newTicket(loginUserInfo, ticketCreateDto.getToken()));
}
return ReqResult.error("error auth token");
}
@Operation(summary = "根据关键字搜索用户信息")
@RequestMapping(path = "/search", method = RequestMethod.POST)
public ReqResult<List<UserDto>> search(@RequestBody UserQueryDto userQueryDto) {
PageQueryVo<com.xspaceagi.system.application.dto.UserQueryDto> pageQueryVo = new PageQueryVo<>();
pageQueryVo.setPageNo(1L);
pageQueryVo.setPageSize(1L);
com.xspaceagi.system.application.dto.UserQueryDto userQueryDto1 = new com.xspaceagi.system.application.dto.UserQueryDto();
userQueryDto1.setUserName(userQueryDto.getKw());
pageQueryVo.setQueryFilter(userQueryDto1);
List<UserDto> records = userApplicationService.listQuery(pageQueryVo).getRecords();
records.forEach(userDto -> {
if (StringUtils.isNotBlank(userDto.getNickName())) {
userDto.setUserName(userDto.getNickName() + "(" + userDto.getUserName() + ")");
}
userDto.setPassword(null);
userDto.setPhone(null);
userDto.setEmail(null);
userDto.setStatus(null);
//此处需要返回role用于限制用户是否能绑定角色
//userDto.setRole(null);
});
//records大于五条时只返回前五条
if (records.size() > 5) {
records = records.subList(0, 5);
}
return ReqResult.success(records);
}
@Operation(summary = "查询用户的菜单权限(树形结构)")
@GetMapping("/list-menu")
public ReqResult<List<MenuNodeDto>> getMenuList() {
Long userId = ((UserDto) RequestContext.get().getUser()).getId();
if (userId == null) {
return ReqResult.error("用户未登录");
}
List<MenuNodeDto> menuTree = sysUserAuthCacheService.getUserMenuTree(userId);
I18nUtil.replaceSystemMessage(menuTree);
return ReqResult.success(menuTree);
}
@GetMapping("/refresh-permission")
public ReqResult<Void> refreshPermission() {
Long userId = ((UserDto) RequestContext.get().getUser()).getId();
if (userId == null) {
return ReqResult.error("用户未登录");
}
sysUserPermissionCacheService.clearCacheByUserIds(List.of(userId));
return ReqResult.success();
}
@GetMapping("/lang")
public ReqResult<Void> lang(HttpServletRequest request) {
Long userId = ((UserDto) RequestContext.get().getUser()).getId();
if (userId == null) {
return ReqResult.error("用户未登录");
}
sysUserPermissionCacheService.clearCacheByUserIds(List.of(userId));
return ReqResult.success();
}
private LoginResDto buildLoginResDto(String token, HttpServletRequest request, HttpServletResponse response) {
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
int expire = tenantConfig.getAuthExpire() == null ? 86400 * 30 : tenantConfig.getAuthExpire().intValue() * 60;
Cookie cookie = new Cookie("ticket", token);
cookie.setMaxAge(expire);
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);
UserDto userDto = authService.getLoginUserInfo(token);
String referer = request.getHeader("Referer");
String redirect = extractRedirect(referer);
String header = request.getHeader("X-Client-Type");
if (!log.isDebugEnabled() && header == null) {
token = "";
}
return LoginResDto.builder().resetPass(userDto.getResetPass()).redirect(redirect).expireDate(new Date(System.currentTimeMillis() + expire * 1000L)).token(token).build();
}
private CaptchaConfig buildCaptchaConfig() {
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
CaptchaConfig captchaConfig = new CaptchaConfig();
captchaConfig.setCaptchaAccessKeyId(tenantConfig.getCaptchaAccessKeyId());
captchaConfig.setCaptchaAccessKeySecret(tenantConfig.getCaptchaAccessKeySecret());
captchaConfig.setOpenCaptcha(tenantConfig.getOpenCaptcha());
return captchaConfig;
}
private static String extractRedirect(String referer) {
String redirect = null;
if (StringUtils.isNotBlank(referer)) {
try {
String[] parts = referer.split("\\?");
if (parts.length > 1) {
String queryString = parts[1];
String[] params = queryString.split("&");
for (String param : params) {
String[] keyValue = param.split("=");
if (keyValue.length == 2 && "redirect".equals(keyValue[0])) {
redirect = URLDecoder.decode(keyValue[1], "UTF-8");
break;
}
}
}
} catch (UnsupportedEncodingException e) {
}
}
return redirect;
}
}

View File

@@ -0,0 +1,176 @@
package com.xspaceagi.system.web.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.system.application.dto.SendNotifyMessageDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.dto.UserQueryDto;
import com.xspaceagi.system.application.service.NotifyMessageApplicationService;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.infra.dao.entity.NotifyMessage;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.infra.dao.mapper.UserMapper;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.PageQueryVo;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.web.dto.UserAddDto;
import com.xspaceagi.system.web.dto.UserStatsDto;
import com.xspaceagi.system.web.dto.UserUpdateDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@Tag(name = "系统管理-用户管理相关接口")
@RestController
@RequestMapping("/api/system/user")
public class UserManageController {
@Resource
private UserApplicationService userApplicationService;
@Resource
private NotifyMessageApplicationService notifyMessageApplicationService;
@Resource
private UserMapper userMapper;
@RequireResource(USER_MANAGE_QUERY)
@Operation(summary = "查询用户列表")
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<IPage<UserDto>> listQuery(@RequestBody PageQueryVo<UserQueryDto> pageQueryVo) {
checkAdmin();
return ReqResult.success(userApplicationService.listQuery(pageQueryVo));
}
@RequireResource(SYSTEM_DASHBOARD)
@Operation(summary = "获取用户统计")
@RequestMapping(path = "/stats", method = RequestMethod.GET)
public ReqResult<UserStatsDto> getUserStats() {
checkAdmin();
Long totalUserCount = userMapper.countTotalUsers();
Long todayNewUserCount = userMapper.countTodayNewUsers();
List<UserStatsDto.TrendItem> last7DaysTrend = userMapper.getLast7DaysNewUserTrend().stream()
.map(UserStatsDto::fromMap)
.collect(Collectors.toList());
List<UserStatsDto.TrendItem> last30DaysTrend = userMapper.getLast30DaysNewUserTrend().stream()
.map(UserStatsDto::fromMap)
.collect(Collectors.toList());
List<UserStatsDto.TrendItem> monthlyTrend = userMapper.getMonthlyNewUserTrend().stream()
.map(UserStatsDto::fromMap)
.collect(Collectors.toList());
UserStatsDto stats = UserStatsDto.builder()
.totalUserCount(totalUserCount != null ? totalUserCount : 0L)
.todayNewUserCount(todayNewUserCount != null ? todayNewUserCount : 0L)
.last7DaysTrend(last7DaysTrend)
.last30DaysTrend(last30DaysTrend)
.monthlyTrend(monthlyTrend)
.build();
return ReqResult.success(stats);
}
@RequireResource(USER_MANAGE_ADD)
@Operation(summary = "添加用户")
@RequestMapping(path = "/add", method = RequestMethod.POST)
public ReqResult<Void> addUser(@RequestBody UserAddDto userAddDto) {
checkAdmin();
UserDto userDto = new UserDto();
BeanUtils.copyProperties(userAddDto, userDto);
if (StringUtils.isNotBlank(userAddDto.getPassword())) {
userDto.setResetPass(1);
}
userApplicationService.add(userDto);
return ReqResult.success();
}
@RequireResource(USER_MANAGE_MODIFY)
@Operation(summary = "更新用户信息")
@RequestMapping(path = "/updateById/{id}", method = RequestMethod.POST)
public ReqResult<Void> updateUserById(@PathVariable Long id, @RequestBody UserUpdateDto userUpdateDto) {
checkAdmin();
UserDto userUpdate = new UserDto();
userUpdate.setId(id);
userUpdate.setAvatar(userUpdateDto.getAvatar());
userUpdate.setNickName(userUpdateDto.getNickName());
userUpdate.setUserName(userUpdateDto.getUserName());
userUpdate.setPhone(userUpdateDto.getPhone());
userUpdate.setEmail(userUpdateDto.getEmail());
userUpdate.setRole(userUpdateDto.getRole());
userUpdate.setPassword(userUpdateDto.getPassword());
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@RequireResource(USER_MANAGE_DISABLE)
@Operation(summary = "禁用用户")
@RequestMapping(path = "/disable/{id}", method = RequestMethod.POST)
public ReqResult<Void> disableUserById(@PathVariable Long id) {
checkAdmin();
UserDto userUpdate = new UserDto();
userUpdate.setId(id);
userUpdate.setStatus(User.Status.Disabled);
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@RequireResource(USER_MANAGE_ENABLE)
@Operation(summary = "启用用户")
@RequestMapping(path = "/enable/{id}", method = RequestMethod.POST)
public ReqResult<Void> enableUserById(@PathVariable Long id) {
checkAdmin();
UserDto userUpdate = new UserDto();
userUpdate.setId(id);
userUpdate.setStatus(User.Status.Enabled);
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@RequireResource(USER_MANAGE_DISABLE)
@Operation(summary = "删除用户")
@RequestMapping(path = "/delete/{id}", method = RequestMethod.POST)
public ReqResult<Void> deleteUserById(@PathVariable Long id) {
checkAdmin();
userApplicationService.logicDelete(id);
return ReqResult.success();
}
@RequireResource(USER_MANAGE_SEND_MESSAGE)
@Operation(summary = "发送通知消息")
@RequestMapping(path = "/notify/message/send", method = RequestMethod.POST)
public ReqResult<Void> notify(@RequestBody SendNotifyMessageDto notifyMessageDto) {
notifyMessageDto.setSenderId(RequestContext.get().getUserId());
if (CollectionUtils.isNotEmpty(notifyMessageDto.getUserIds())) {
notifyMessageDto.setScope(NotifyMessage.MessageScope.System);
} else {
notifyMessageDto.setScope(NotifyMessage.MessageScope.Broadcast);
}
notifyMessageApplicationService.sendNotifyMessage(notifyMessageDto);
return ReqResult.success();
}
/**
* 当前简单的在商家范围支持普通用户和管理员两种角色;后续再迭代完整的权限角色
*/
private void checkAdmin() {
if (((UserDto) RequestContext.get().getUser()).getRole() != User.Role.Admin) {
throw BizException.of(ErrorCodeEnum.PERMISSION_DENIED, BizExceptionCodeEnum.permissionDenied);
}
}
}

View File

@@ -0,0 +1,80 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.application.dto.UsageDto;
import com.xspaceagi.system.application.dto.UserMetricDto;
import com.xspaceagi.system.application.service.UserMetricApplicationService;
import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService;
import com.xspaceagi.system.sdk.service.dto.BizType;
import com.xspaceagi.system.sdk.service.dto.PeriodType;
import com.xspaceagi.system.sdk.service.dto.PeriodUtils;
import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.PeriodTypeEnum;
import com.xspaceagi.system.spec.utils.I18nUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Tag(name = "用户计量统计", description = "用户计量相关接口")
@RestController
@RequestMapping("/api/user/metric")
public class UserMetricController {
@Resource
private UserMetricApplicationService userMetricApplicationService;
@Resource
private IUserDataPermissionRpcService userDataPermissionRpcService;
@Operation(summary = "查询用户各项已使用情况")
@GetMapping("/usage")
public ReqResult<UsageDto> usage() {
UserDataPermissionDto userDataPermission = userDataPermissionRpcService.getUserDataPermission(RequestContext.get().getUserId());
UsageDto usageDto = new UsageDto();
List<UserMetricDto> userMetricDtos = userMetricApplicationService.queryByUserId(RequestContext.get().getUserId(), PeriodUtils.getCurrentPeriod(PeriodTypeEnum.DAY.getCode()));
Map<String, BigInteger> bizMap = userMetricDtos.stream().collect(Collectors.toMap(userMetricDto -> userMetricDto.getBizType() + "-" + userMetricDto.getPeriodType(), val -> val.getValue().toBigInteger(), (v1, v2) -> v1));
Object todayTokenUsage = bizMap.get(BizType.TOKEN_USAGE.name() + "-" + PeriodType.DAY);
if (todayTokenUsage == null) {
todayTokenUsage = "0";
}
UsageDto.Usage tokenUsage = UsageDto.Usage.builder().usage(todayTokenUsage.toString()).limit(userDataPermission.getTokenLimit().getLimitPerDay() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getTokenLimit().getLimitPerDay().toString()).build();
tokenUsage.setDescription(tokenUsage.getUsage() + "/" + tokenUsage.getLimit());
usageDto.setTodayTokenUsage(tokenUsage);
Object generalAgentChatUsage = bizMap.get(BizType.GENERAL_AGENT_CHAT.name() + "-" + PeriodType.DAY);
if (generalAgentChatUsage == null) {
generalAgentChatUsage = "0";
}
usageDto.setTodayAgentPromptUsage(UsageDto.Usage.builder().usage(generalAgentChatUsage.toString()).limit(userDataPermission.getAgentDailyPromptLimit() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getAgentDailyPromptLimit().toString()).build());
usageDto.getTodayAgentPromptUsage().setDescription(usageDto.getTodayAgentPromptUsage().getUsage() + "/" + usageDto.getTodayAgentPromptUsage().getLimit());
Object appDevChatUsage = bizMap.get(BizType.APP_DEV_CHAT.name() + "-" + PeriodType.DAY);
if (appDevChatUsage == null) {
appDevChatUsage = 0;
}
usageDto.setTodayPageAppPromptUsage(UsageDto.Usage.builder().usage(appDevChatUsage.toString()).limit(userDataPermission.getPageDailyPromptLimit() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getPageDailyPromptLimit().toString()).build());
usageDto.getTodayPageAppPromptUsage().setDescription(usageDto.getTodayPageAppPromptUsage().getUsage() + "/" + usageDto.getTodayPageAppPromptUsage().getLimit());
usageDto.setNewAgentUsage(UsageDto.Usage.builder().description(userDataPermission.getMaxAgentCount() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getMaxAgentCount().toString()).build());
usageDto.setNewTableUsage(UsageDto.Usage.builder().description(userDataPermission.getMaxDataTableCount() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getMaxDataTableCount().toString()).build());
usageDto.setNewTaskUsage(UsageDto.Usage.builder().description(userDataPermission.getMaxScheduledTaskCount() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getMaxScheduledTaskCount().toString()).build());
usageDto.setNewKnowledgeBaseUsage(UsageDto.Usage.builder().description(userDataPermission.getMaxKnowledgeCount() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getMaxKnowledgeCount().toString()).build());
usageDto.setKnowledgeBaseStorageUsage(UsageDto.Usage.builder().description(userDataPermission.getKnowledgeStorageLimitGb().compareTo(BigDecimal.valueOf(-1L)) == 0 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getKnowledgeStorageLimitGb().toBigInteger() + "GB").build());
usageDto.setNewWorkspaceUsage(UsageDto.Usage.builder().description(userDataPermission.getMaxSpaceCount() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getMaxSpaceCount().toString()).build());
usageDto.setSandboxMemoryLimit(UsageDto.Usage.builder().description(userDataPermission.getAgentComputerMemoryGb() == null ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getAgentComputerMemoryGb() + "GB").build());
usageDto.setNewPageAppUsage(UsageDto.Usage.builder().description(userDataPermission.getMaxPageAppCount() == -1 ? I18nUtil.systemMessage("Backend.UserMetric.Unlimited") : userDataPermission.getMaxPageAppCount().toString()).build());
return ReqResult.success(usageDto);
}
}

View File

@@ -0,0 +1,103 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.infra.dao.mapper.UserReqMapper;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.web.dto.UserAccessStatsDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.SYSTEM_DASHBOARD;
@Tag(name = "用户请求统计查询")
@RestController
@RequestMapping("/api/system/request")
public class UserRequestController {
@Resource
private UserReqMapper userReqMapper;
@RequireResource(SYSTEM_DASHBOARD)
@Operation(summary = "执行统计SQL")
@RequestMapping(path = "/sql", method = RequestMethod.POST)
public ReqResult<List<Map<String, Object>>> sqlExecute(@RequestBody Map<String, Object> params) throws JSQLParserException {
Assert.notNull(params.get("sql"), "SQL cannot be left blank.");
Assert.isTrue(params.get("sql").toString().toLowerCase().trim().startsWith("select"), "SQL must be start with select");
//确保sql只能是查询
Statement statement = CCJSqlParserUtil.parse(params.get("sql").toString());
Select select = (Select) statement;
PlainSelect plainSelect = select.getPlainSelect();
if (plainSelect != null) {
// 替换所有表名
replaceTableName(plainSelect.getFromItem(), "user_req");
if (plainSelect.getJoins() != null) {
for (Join join : plainSelect.getJoins()) {
replaceTableName(join.getRightItem(), "user_req");
}
}
// 检查是否有 LIMIT如果没有则加上 LIMIT 1000
if (plainSelect.getLimit() == null) {
var limit = new Limit().withRowCount(new LongValue(1000));
plainSelect.setLimit(limit);
}
}
return ReqResult.success(userReqMapper.select(statement.toString()));
}
@RequireResource(SYSTEM_DASHBOARD)
@Operation(summary = "获取用户访问统计")
@RequestMapping(path = "/stats", method = RequestMethod.GET)
public ReqResult<UserAccessStatsDto> getAccessStats() {
Long todayUserCount = userReqMapper.countTodayUsers();
Long last7DaysUserCount = userReqMapper.countLast7DaysUsers();
Long last30DaysUserCount = userReqMapper.countLast30DaysUsers();
List<UserAccessStatsDto.TrendItem> last7DaysTrend = userReqMapper.getLast7DaysTrend().stream()
.map(UserAccessStatsDto::fromMap)
.collect(Collectors.toList());
UserAccessStatsDto stats = UserAccessStatsDto.builder()
.todayUserCount(todayUserCount != null ? todayUserCount : 0L)
.last7DaysUserCount(last7DaysUserCount != null ? last7DaysUserCount : 0L)
.last30DaysUserCount(last30DaysUserCount != null ? last30DaysUserCount : 0L)
.last7DaysTrend(last7DaysTrend)
.build();
return ReqResult.success(stats);
}
private static void replaceTableName(FromItem fromItem, String newTableName) {
if (fromItem instanceof Table table) {
table.setName(newTableName);
} else if (fromItem instanceof LateralSubSelect lateral) {
var select = lateral.getSelect();
if (select instanceof PlainSelect subPlainSelect) {
replaceTableName(subPlainSelect.getFromItem(), newTableName);
if (subPlainSelect.getJoins() != null) {
for (Join join : subPlainSelect.getJoins()) {
replaceTableName(join.getRightItem(), newTableName);
}
}
}
} else if (fromItem instanceof ParenthesedFromItem parenthesed) {
replaceTableName(parenthesed.getFromItem(), newTableName);
}
}
}

View File

@@ -0,0 +1,52 @@
package com.xspaceagi.system.web.controller.api;
import com.xspaceagi.system.application.dto.TenantConfigDto;
import com.xspaceagi.system.application.service.AuthService;
import com.xspaceagi.system.spec.common.RequestContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@Tag(name = "开放API-用户认证跳转")
@RestController
@RequestMapping("/api")
public class RedirectApiController {
@Resource
private AuthService authService;
@Operation(summary = "根据ticket认证校验并跳转")
@RequestMapping(path = "/ticket/redirect", method = RequestMethod.GET)
public void redirect(@RequestParam(name = "ticket") String ticket, @RequestParam(name = "redirectUrl") String redirectUrl,
HttpServletResponse response) {
String token = authService.getTokenByTicket(ticket);
if (token == null) {
throw new IllegalArgumentException("Error ticket");
}
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
int expire = tenantConfig.getAuthExpire() == null ? 86400 * 30 : tenantConfig.getAuthExpire().intValue() * 60;
Cookie cookie = new Cookie("ticket", token);
cookie.setHttpOnly(true);
cookie.setMaxAge(expire);
cookie.setPath("/");
// iframe SameSite=None; Secure
cookie.setSecure(true);
cookie.setAttribute("SameSite","None");
response.addCookie(cookie);
try {
response.sendRedirect(URLDecoder.decode(redirectUrl, StandardCharsets.UTF_8));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,92 @@
package com.xspaceagi.system.web.controller.api;
import com.xspaceagi.system.application.dto.SpaceDto;
import com.xspaceagi.system.application.dto.SpaceUserDto;
import com.xspaceagi.system.application.service.SpaceApplicationService;
import com.xspaceagi.system.infra.dao.entity.Space;
import com.xspaceagi.system.sdk.permission.SpacePermissionService;
import com.xspaceagi.system.sdk.permission.IUserDataPermissionRpcService;
import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.web.dto.SpaceAddDto;
import com.xspaceagi.system.web.dto.SpaceUpdateDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "开放API-工作空间相关接口")
@RestController
@RequestMapping("/api/v1/space")
@Slf4j
public class SpaceApiController {
@Resource
private SpaceApplicationService spaceApplicationService;
@Resource
private SpacePermissionService spacePermissionService;
@Resource
private IUserDataPermissionRpcService userDataPermissionRpcService;
@Operation(summary = "创建团队空间接口")
@RequestMapping(path = "/add", method = RequestMethod.POST)
public ReqResult<Long> add(@RequestBody @Valid SpaceAddDto spaceAddDto) {
// 创建团队空间数量限制
UserDataPermissionDto userDataPermission = userDataPermissionRpcService.getUserDataPermission(RequestContext.get().getUserId());
userDataPermission.checkMaxSpaceCount(spaceApplicationService.countUserCreatedTeamSpaces(RequestContext.get().getUserId()).intValue());
SpaceDto spaceDto = SpaceDto.builder()
.name(spaceAddDto.getName())
.description(spaceAddDto.getDescription())
.icon(spaceAddDto.getIcon())
.creatorId(RequestContext.get().getUserId())
.type(Space.Type.Team)
.build();
return ReqResult.success(spaceApplicationService.add(spaceDto));
}
@Operation(summary = "更新团队空间接口")
@RequestMapping(path = "/{id}/update", method = RequestMethod.POST)
public ReqResult<Void> update(@PathVariable Long id, @RequestBody @Valid SpaceUpdateDto spaceUpdateDto) {
spaceUpdateDto.setId(id);
spacePermissionService.checkSpaceAdminPermission(spaceUpdateDto.getId());
spaceApplicationService.update(SpaceDto.builder().id(spaceUpdateDto.getId()).icon(spaceUpdateDto.getIcon())
.name(spaceUpdateDto.getName()).description(spaceUpdateDto.getDescription())
.receivePublish(spaceUpdateDto.getReceivePublish()).allowDevelop(spaceUpdateDto.getAllowDevelop()).build());
return ReqResult.success();
}
@Operation(summary = "删除空间接口")
@RequestMapping(path = "/{id}/delete", method = RequestMethod.POST)
public ReqResult<Void> delete(@PathVariable Long id) {
SpaceDto spaceDto = spaceApplicationService.queryById(id);
if (spaceDto != null && spaceDto.getType() == Space.Type.Personal) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemPersonalSpaceDeleteForbidden);
}
spacePermissionService.checkSpaceOwnerPermission(id);
spaceApplicationService.delete(id);
return ReqResult.success();
}
@Operation(summary = "查询用户空间列表")
@RequestMapping(path = "/list", method = RequestMethod.GET)
public ReqResult<List<SpaceDto>> list() {
List<SpaceDto> spaceDtoList = spaceApplicationService.queryListByUserId(RequestContext.get().getUserId());
spaceDtoList.forEach(spaceDto -> {
SpaceUserDto spaceUserDto = spaceApplicationService.querySpaceUser(spaceDto.getId(), RequestContext.get().getUserId());
if (spaceUserDto != null) {
spaceDto.setCurrentUserRole(spaceUserDto.getRole());
}
});
return ReqResult.success(spaceDtoList);
}
}

View File

@@ -0,0 +1,156 @@
package com.xspaceagi.system.web.controller.api;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.xspaceagi.system.application.dto.SpaceDto;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.dto.UserQueryDto;
import com.xspaceagi.system.application.service.AuthService;
import com.xspaceagi.system.application.service.SpaceApplicationService;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.dto.PageQueryVo;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.web.dto.UserAddDto;
import com.xspaceagi.system.web.dto.UserUpdateDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@Tag(name = "开放API-用户管理相关接口")
@RestController
@RequestMapping("/api/v1/system/user")
public class UserManApiController {
@Resource
private UserApplicationService userApplicationService;
@Resource
private SpaceApplicationService spaceApplicationService;
@Resource
private AuthService authService;
@Operation(summary = "分页查询用户列表")
@RequestMapping(path = "/list", method = RequestMethod.POST)
public ReqResult<IPage<UserDto>> listQuery(@RequestBody PageQueryVo<UserQueryDto> pageQueryVo) {
return ReqResult.success(userApplicationService.listQuery(pageQueryVo));
}
@Operation(summary = "查询用户详细信息")
@RequestMapping(path = "/detail", method = RequestMethod.GET)
public ReqResult<UserDto> detailQuery(@RequestParam(name = "uid", required = false) String uid,
@RequestParam(name = "id", required = false) Long id,
@RequestParam(name = "username", required = false) String username,
@RequestParam(name = "email", required = false) String email,
@RequestParam(name = "phone", required = false) String phone) {
UserDto userDto = null;
if (id != null) {
userDto = userApplicationService.queryById(id);
}
if (userDto == null && StringUtils.isNotBlank(uid)) {
userDto = userApplicationService.queryUserByUid(uid);
}
if (userDto == null && StringUtils.isNotBlank(username)) {
userDto = userApplicationService.queryUserByUserName(username);
}
if (userDto == null && StringUtils.isNotBlank(email)) {
userDto = userApplicationService.queryUserByEmail(email);
}
if (userDto == null && StringUtils.isNotBlank(phone)) {
userDto = userApplicationService.queryUserByPhone(phone);
}
if (userDto == null) {
throw new IllegalArgumentException("Invalid param");
}
return ReqResult.success(userDto);
}
@Operation(summary = "添加用户")
@RequestMapping(path = "/add", method = RequestMethod.POST)
public ReqResult<Long> addUser(@RequestBody UserAddDto userAddDto) {
Assert.notNull(userAddDto, "Invalid param");
Assert.isTrue(StringUtils.isNotBlank(userAddDto.getPhone()) || StringUtils.isNotBlank(userAddDto.getEmail()), "email and phone cannot be null at the same time");
UserDto userDto = new UserDto();
BeanUtils.copyProperties(userAddDto, userDto);
if (StringUtils.isNotBlank(userAddDto.getPassword())) {
userDto.setResetPass(1);
}
userApplicationService.add(userDto);
return ReqResult.success(userDto.getId());
}
@Operation(summary = "更新用户信息")
@RequestMapping(path = "/{id}/update", method = RequestMethod.POST)
public ReqResult<Void> updateUserById(@PathVariable Long id, @RequestBody UserUpdateDto userUpdateDto) {
UserDto userUpdate = new UserDto();
userUpdate.setId(id);
userUpdate.setAvatar(userUpdateDto.getAvatar());
userUpdate.setUserName(userUpdateDto.getUserName());
userUpdate.setNickName(userUpdateDto.getNickName());
userUpdate.setPhone(userUpdateDto.getPhone());
userUpdate.setEmail(userUpdateDto.getEmail());
userUpdate.setRole(userUpdateDto.getRole());
userUpdate.setPassword(userUpdateDto.getPassword());
//写一个验证,以上字段不能同时为空
Assert.isTrue(StringUtils.isNotBlank(userUpdateDto.getNickName()) || StringUtils.isNotBlank(userUpdateDto.getUserName())
|| StringUtils.isNotBlank(userUpdateDto.getEmail()) || StringUtils.isNotBlank(userUpdateDto.getPhone())
|| StringUtils.isNotBlank(userUpdateDto.getAvatar()) || userUpdateDto.getRole() != null
|| StringUtils.isNotBlank(userUpdateDto.getPassword()), "nickName, userName, email, phone, avatar, role, password cannot be null at the same time");
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@Operation(summary = "禁用用户")
@RequestMapping(path = "/{id}/disable", method = RequestMethod.POST)
public ReqResult<Void> disableUserById(@PathVariable Long id) {
UserDto userUpdate = new UserDto();
userUpdate.setId(id);
userUpdate.setStatus(User.Status.Disabled);
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@Operation(summary = "启用用户")
@RequestMapping(path = "/{id}/enable", method = RequestMethod.POST)
public ReqResult<Void> enableUserById(@PathVariable Long id) {
UserDto userUpdate = new UserDto();
userUpdate.setId(id);
userUpdate.setStatus(User.Status.Enabled);
userApplicationService.update(userUpdate);
return ReqResult.success();
}
@Operation(summary = "查询用户空间列表")
@RequestMapping(path = "/{id}/space/list", method = RequestMethod.GET)
public ReqResult<List<SpaceDto>> spaceList(@PathVariable Long id) {
List<SpaceDto> spaceDtoList = spaceApplicationService.queryListByUserId(id);
return ReqResult.success(spaceDtoList);
}
@Operation(summary = "创建指定用户的认证ticket")
@RequestMapping(path = "/{id}/ticket/create", method = RequestMethod.POST)
public ReqResult<String> createTicket(@PathVariable Long id) {
UserDto userDto = userApplicationService.queryById(id);
if (userDto == null) {
throw new IllegalArgumentException("Error user id");
}
String token = authService.createToken(userDto, "open-api-" + UUID.randomUUID().toString().replace("-", ""));
String ticket = authService.newTicket(userDto, token);
return ReqResult.success(ticket);
}
@Operation(summary = "清除用户通过ticket获得的认证")
@RequestMapping(path = "/{id}/ticket/clear", method = RequestMethod.POST)
public ReqResult<Void> clearAll(@PathVariable Long id) {
authService.expireUserAllToken(id, "open-api-");
return ReqResult.success();
}
}

View File

@@ -0,0 +1,66 @@
package com.xspaceagi.system.web.controller.base;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.common.RequestContext;
import com.xspaceagi.system.spec.common.UserContext;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import lombok.extern.slf4j.Slf4j;
/**
* 公共逻辑处理
*
* @author soddy
*/
@Slf4j
public abstract class BaseController {
// @Resource
// private LoginWrapper loginWrapper;
/**
* 获取当前登录用户信息
*/
public UserContext getUser() {
var userDto = (UserDto) RequestContext.get().getUser();
if (userDto == null) {
throw BizException.of(ErrorCodeEnum.UNAUTHORIZED, BizExceptionCodeEnum.systemUserNotLoggedInWeb);
}
return UserContext.builder()
.userId(userDto.getId())
.userName(userDto.getUserName())
.nickName(userDto.getNickName())
.avatar(userDto.getAvatar())
.email(userDto.getEmail())
.phone(userDto.getPhone())
.status(userDto.getStatus() == User.Status.Enabled ? 1 : -1)
.tenantId(userDto.getTenantId())
.tenantName(null)
.orgId(null)
.orgName(null)
.roleType(userDto.getRole() == User.Role.Admin ? 1 : 2)
.build();
}
// /**
// * 获取当前用户信息
// */
// public UserContext getUserInfo() {
// var user = loginWrapper.getUser();
// if (Objects.isNull(user)) {
// user = UserContext.builder().build();
// }
// return user;
// }
//
// /**
// * 获取当前用户id
// */
// public Long getUserId() {
// return loginWrapper.getUserId();
// }
}

View File

@@ -0,0 +1,98 @@
package com.xspaceagi.system.web.controller.permission;
import com.xspaceagi.system.application.dto.permission.export.PermissionExportDto;
import com.xspaceagi.system.application.service.PermissionDiffService;
import com.xspaceagi.system.application.service.PermissionExportService;
import com.xspaceagi.system.application.service.PermissionImportService;
import com.xspaceagi.system.infra.dao.entity.Tenant;
import com.xspaceagi.system.infra.dao.service.TenantService;
import com.xspaceagi.system.spec.annotation.SaasAdmin;
import com.xspaceagi.system.spec.constants.PermissionSyncConstants;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
import jakarta.annotation.Resource;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
@RequestMapping("/api/system/permission")
public class PermissionSyncController {
@Resource
private PermissionExportService permissionExportService;
@Resource
private PermissionImportService permissionImportService;
@Resource
private PermissionDiffService permissionDiffService;
@Resource
private TenantService tenantService;
@SaasAdmin
@GetMapping(value = "/export", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Object> export(@RequestParam String version) {
try {
PermissionExportDto dto = permissionExportService.exportConfig(version);
String json = JsonSerializeUtil.toJSONString(dto);
String saveToPath = "./" + PermissionSyncConstants.PERMISSION_JSON_EXPORT_BASE_PATH + "/" + PermissionSyncConstants.buildExportFileName(version);
Path path = Paths.get(saveToPath).toAbsolutePath();
Files.createDirectories(path.getParent());
Files.writeString(path, json, StandardCharsets.UTF_8);
return ReqResult.success("已导出");
} catch (IOException e) {
return ReqResult.error(e.getMessage());
}
}
@SaasAdmin
@GetMapping(value = "/import", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<String> importPermission(@RequestParam Long tenantId, @RequestParam String version) {
Tenant tenant = tenantService.getById(tenantId);
if (tenant == null) {
return ReqResult.error("租户不存在");
}
permissionImportService.importToTenant(tenant, version);
return ReqResult.success("导入成功");
}
@SaasAdmin
@GetMapping(value = "/diff", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<String> diff(@RequestParam String fromVersion, @RequestParam String toVersion) {
try {
var diff = permissionDiffService.generateDiff(fromVersion, toVersion);
String json = JsonSerializeUtil.toJSONString(diff);
String fileName = PermissionSyncConstants.buildDiffFileName(toVersion);
String saveToPath = "./" + PermissionSyncConstants.PERMISSION_JSON_EXPORT_BASE_PATH + "/" + fileName;
Path path = Paths.get(saveToPath).toAbsolutePath();
Files.createDirectories(path.getParent());
Files.writeString(path, json, StandardCharsets.UTF_8);
return ReqResult.success("已生成差异文件: " + fileName);
} catch (IOException e) {
return ReqResult.error(e.getMessage());
}
}
@SaasAdmin
@GetMapping(value = "/import-diff", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<String> importDiff(@RequestParam Long tenantId, @RequestParam String version) {
Tenant tenant = tenantService.getById(tenantId);
if (tenant == null) {
return ReqResult.error("租户不存在");
}
permissionImportService.importDiffToTenant(tenant, version);
return ReqResult.success("差异导入成功");
}
}

View File

@@ -0,0 +1,376 @@
package com.xspaceagi.system.web.controller.permission;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.system.application.converter.GroupBindMenuModelConverter;
import com.xspaceagi.system.application.converter.MenuNodeConverter;
import com.xspaceagi.system.application.converter.SysDataPermissionConverter;
import com.xspaceagi.system.application.dto.permission.*;
import com.xspaceagi.system.application.service.SysDataPermissionApplicationService;
import com.xspaceagi.system.application.service.SysGroupApplicationService;
import com.xspaceagi.system.application.service.SysSubjectPermissionApplicationService;
import com.xspaceagi.system.domain.model.GroupBindMenuModel;
import com.xspaceagi.system.domain.model.MenuNode;
import com.xspaceagi.system.domain.model.SortIndex;
import com.xspaceagi.system.infra.dao.entity.SysDataPermission;
import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.PageQueryVo;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.*;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.web.controller.base.BaseController;
import com.fasterxml.jackson.core.type.TypeReference;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@Slf4j
@Tag(name = "权限管理-用户组", description = "用户组相关接口")
@RestController
@RequestMapping("/api/system/group")
public class SysGroupController extends BaseController {
@Resource
private SysGroupApplicationService sysGroupApplicationService;
@Resource
private SysDataPermissionApplicationService sysDataPermissionApplicationService;
@Resource
private SysSubjectPermissionApplicationService sysSubjectPermissionApplicationService;
@RequireResource(USER_GROUP_MANAGE_ADD)
@Operation(summary = "添加组")
@PostMapping(value = "/add", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> addGroup(@RequestBody SysGroupAddDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
if (dto.getSource() != null && SourceEnum.isInValid(dto.getSource())) {
return ReqResult.error("参数source错误");
}
if (dto.getStatus() != null && StatusEnum.isInValid(dto.getStatus())) {
return ReqResult.error("参数status错误");
}
SysGroup group = new SysGroup();
BeanUtils.copyProperties(dto, group);
sysGroupApplicationService.addGroup(group, getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_MODIFY)
@Operation(summary = "更新组")
@PostMapping("/update")
public ReqResult<Void> updateGroup(@RequestBody SysGroupUpdateDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
if (dto.getSource() != null && SourceEnum.isInValid(dto.getSource())) {
return ReqResult.error("参数source错误");
}
if (dto.getStatus() != null && StatusEnum.isInValid(dto.getStatus())) {
return ReqResult.error("参数status错误");
}
SysGroup group = new SysGroup();
BeanUtils.copyProperties(dto, group);
group.setCode(null); // code不允许更新
sysGroupApplicationService.updateGroup(group, getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_DELETE)
@Operation(summary = "删除组")
@PostMapping("/delete/{groupId}")
public ReqResult<Void> deleteGroup(@PathVariable Long groupId) {
if (groupId == null) {
return ReqResult.error("参数不能为空");
}
sysGroupApplicationService.deleteGroup(groupId, getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_QUERY)
@Operation(summary = "根据ID查询组")
@GetMapping("/{groupId}")
public ReqResult<SysGroupDto> getGroupById(@PathVariable Long groupId) {
if (groupId == null) {
return ReqResult.error("参数不能为空");
}
SysGroup group = sysGroupApplicationService.getGroupById(groupId);
if (group == null) {
return ReqResult.error("用户组不存在");
}
SysGroupDto groupDto = new SysGroupDto();
BeanUtils.copyProperties(group, groupDto);
// SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.GROUP, group.getId());
// if (dataPermission != null) {
// groupDto.setModelIds(dataPermission.getModelIds());
// groupDto.setTokenLimit(dataPermission.getTokenLimit());
// }
I18nUtil.replaceSystemMessage(groupDto);
return ReqResult.success(groupDto);
}
@RequireResource(USER_GROUP_MANAGE_QUERY)
@Operation(summary = "根据编码查询组")
@GetMapping("/code/{groupCode}")
public ReqResult<SysGroupDto> getGroupByCode(@PathVariable String groupCode) {
if (StringUtils.isBlank(groupCode)) {
return ReqResult.error("参数不能为空");
}
SysGroup group = sysGroupApplicationService.getGroupByCode(groupCode);
if (group == null) {
return ReqResult.error("用户组不存在");
}
SysGroupDto groupDto = new SysGroupDto();
BeanUtils.copyProperties(group, groupDto);
// SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.GROUP, group.getId());
// if (dataPermission != null) {
// groupDto.setModelIds(dataPermission.getModelIds());
// groupDto.setTokenLimit(dataPermission.getTokenLimit());
// }
I18nUtil.replaceSystemMessage(groupDto);
return ReqResult.success(groupDto);
}
@RequireResource(USER_GROUP_MANAGE_MODIFY)
@Operation(summary = "调整用户组顺序")
@PostMapping("/update-sort")
public ReqResult<Void> updateSortIndex(@RequestBody SortIndexUpdateDto dto) {
if (dto == null || CollectionUtils.isEmpty(dto.getItems())) {
return ReqResult.error("参数不能为空");
}
List<SortIndex> sortIndexList = dto.getItems().stream()
.map(item -> {
SortIndex model = new SortIndex();
model.setId(item.getId());
model.setParentId(item.getParentId());
model.setSortIndex(item.getSortIndex());
return model;
})
.collect(Collectors.toList());
sysGroupApplicationService.batchUpdateGroupSort(sortIndexList, getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_QUERY)
@Operation(summary = "根据条件查询组列表")
@GetMapping("/list")
public ReqResult<List<SysGroupDto>> getGroupList(SysGroupQueryDto dto) {
SysGroup sysGroup = new SysGroup();
if (dto != null) {
BeanUtils.copyProperties(dto, sysGroup);
}
List<SysGroup> groupList = sysGroupApplicationService.getGroupList(sysGroup);
if (CollectionUtils.isEmpty(groupList)) {
return ReqResult.success();
}
// List<Long> groupIds = groupList.stream().map(SysGroup::getId).toList();
// List<SysDataPermission> dataPermissionList = sysDataPermissionApplicationService.getByTargetList(PermissionTargetTypeEnum.GROUP, groupIds);
// Map<Long, SysDataPermission> permissionMap = dataPermissionList.stream()
// .collect(Collectors.toMap(SysDataPermission::getTargetId, p -> p, (a, b) -> a));
List<SysGroupDto> dtoList = groupList.stream().map(r -> {
SysGroupDto groupDto = new SysGroupDto();
BeanUtils.copyProperties(r, groupDto);
// SysDataPermission dataPermission = permissionMap.get(r.getId());
// if (dataPermission != null) {
// groupDto.setModelIds(dataPermission.getModelIds());
// groupDto.setTokenLimit(dataPermission.getTokenLimit());
// }
return groupDto;
}).collect(Collectors.toList());
I18nUtil.replaceSystemMessage(dtoList);
return ReqResult.success(dtoList);
}
@RequireResource(USER_GROUP_MANAGE_BIND_USER)
@Operation(summary = "分页查询组已绑定的用户列表支持按userName模糊筛选")
@PostMapping("/list-user")
public ReqResult<IPage<SysUserDto>> getUserListByGroupId(@RequestBody PageQueryVo<SysGroupUserQueryDto> pageQueryVo) {
if (pageQueryVo == null || pageQueryVo.getQueryFilter() == null || pageQueryVo.getQueryFilter().getGroupId() == null) {
return ReqResult.error("用户组ID不能为空");
}
SysGroupUserQueryDto queryDto = pageQueryVo.getQueryFilter();
Long groupId = queryDto.getGroupId();
String userName = queryDto.getUserName();
long pageNo = pageQueryVo.getPageNo() != null ? pageQueryVo.getPageNo() : 1L;
long pageSize = pageQueryVo.getPageSize() != null ? pageQueryVo.getPageSize() : 10L;
IPage<User> userPage = sysGroupApplicationService.getUserPageByGroupId(groupId, userName, pageNo, pageSize);
List<SysUserDto> dtoList = userPage.getRecords().stream().map(user -> {
SysUserDto userDto = new SysUserDto();
BeanUtils.copyProperties(user, userDto);
userDto.setUserId(user.getId());
return userDto;
}).collect(Collectors.toList());
IPage<SysUserDto> resultPage = new Page<>(userPage.getCurrent(), userPage.getSize(), userPage.getTotal());
resultPage.setRecords(dtoList);
return ReqResult.success(resultPage);
}
@RequireResource(USER_GROUP_MANAGE_BIND_DATA)
@Operation(summary = "查询用户组数据权限")
@GetMapping("/data-permission/{groupId}")
public ReqResult<SysDataPermissionBindDto> getGroupDataPermission(@PathVariable Long groupId) {
if (groupId == null) {
return ReqResult.error("参数不能为空");
}
SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.GROUP, groupId);
SysDataPermissionBindDto result = SysDataPermissionConverter.toDto(dataPermission);
result = result == null ? new SysDataPermissionBindDto() : result;
result.setModelIds(sysSubjectPermissionApplicationService.listSubjectIdsByTarget(
PermissionTargetTypeEnum.GROUP, groupId, PermissionSubjectTypeEnum.MODEL));
result.setAgentIds(sysSubjectPermissionApplicationService.listSubjectIdsByTarget(
PermissionTargetTypeEnum.GROUP, groupId, PermissionSubjectTypeEnum.AGENT));
result.setPageAgentIds(sysSubjectPermissionApplicationService.listSubjectIdsByTarget(
PermissionTargetTypeEnum.GROUP, groupId, PermissionSubjectTypeEnum.PAGE));
Map<String, String> openApiConfigMap = sysSubjectPermissionApplicationService.listSubjectKeyConfigByTarget(
PermissionTargetTypeEnum.GROUP, groupId, PermissionSubjectTypeEnum.OPEN_API);
List<SysDataPermissionBindDto.OpenApiConfig> openApiConfigs = openApiConfigMap.entrySet().stream()
.map(entry -> {
SysDataPermissionBindDto.OpenApiConfig config = new SysDataPermissionBindDto.OpenApiConfig();
config.setKey(entry.getKey());
if (StringUtils.isNotBlank(entry.getValue())) {
try {
Map<String, Integer> valueMap = JsonSerializeUtil.parseObject(entry.getValue(), new TypeReference<Map<String, Integer>>() {});
config.setRpm(valueMap == null ? null : valueMap.get("rpm"));
config.setRpd(valueMap == null ? null : valueMap.get("rpd"));
} catch (Exception ignore) {
config.setRpm(null);
config.setRpd(null);
}
}
return config;
})
.toList();
result.setOpenApiConfigs(openApiConfigs);
result.setKnowledgeIds(sysSubjectPermissionApplicationService.listSubjectIdsByTarget(
PermissionTargetTypeEnum.GROUP, groupId, PermissionSubjectTypeEnum.KNOWLEDGE));
return ReqResult.success(result);
}
@RequireResource(USER_GROUP_MANAGE_BIND_DATA)
@Operation(summary = "组绑定数据权限(全量覆盖)")
@PostMapping("/bind-data-permission")
public ReqResult<Void> bindDataPermission(@RequestBody SysGroupBindDataPermissionDto dto) {
if (dto == null || dto.getGroupId() == null) {
return ReqResult.error("用户组ID不能为空");
}
SysDataPermissionBindDto bindDto = dto.getDataPermission();
SysDataPermission dataPermission = SysDataPermissionConverter.toEntity(bindDto);
sysGroupApplicationService.bindDataPermission(dto.getGroupId(), dataPermission, getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_BIND_USER)
@Operation(summary = "组绑定用户(全量覆盖)")
@PostMapping("/bind-user")
public ReqResult<Void> bindUser(@RequestBody SysGroupBindUserDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
sysGroupApplicationService.groupBindUser(dto.getGroupId(), dto.getUserIds(), getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_BIND_USER)
@Operation(summary = "用户组添加用户")
@PostMapping("/add-user")
public ReqResult<Void> addUser(@RequestBody SysGroupBindUserSingleDto dto) {
if (dto == null || dto.getGroupId() == null || dto.getUserId() == null) {
return ReqResult.error("用户组ID和用户ID不能为空");
}
sysGroupApplicationService.groupAddUser(dto.getGroupId(), dto.getUserId(), getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_BIND_USER)
@Operation(summary = "用户组移除用户")
@PostMapping("/remove-user")
public ReqResult<Void> removeUser(@RequestBody SysGroupBindUserSingleDto dto) {
if (dto == null || dto.getGroupId() == null || dto.getUserId() == null) {
return ReqResult.error("用户组ID和用户ID不能为空");
}
sysGroupApplicationService.groupRemoveUser(dto.getGroupId(), dto.getUserId(), getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_BIND_MENU)
@Operation(summary = "组绑定菜单(全量覆盖)")
@PostMapping("/bind-menu")
public ReqResult<Void> bindMenu(@RequestBody SysGroupBindMenuDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
// 用户组不允许绑定 生态市场/系统管理
checkForbiddenMenuCodes(dto.getMenuTree());
GroupBindMenuModel model = GroupBindMenuModelConverter.convertToModel(dto);
sysGroupApplicationService.bindMenu(model, getUser());
return ReqResult.success();
}
@RequireResource(USER_GROUP_MANAGE_BIND_MENU)
@Operation(summary = "查询组已绑定的菜单(树形结构)")
@GetMapping("/list-menu/{groupId}")
public ReqResult<List<MenuNodeDto>> getMenuListByGroupId(@PathVariable Long groupId) {
if (groupId == null) {
return ReqResult.error("参数不能为空");
}
// 获取处理好的菜单树(已包含资源详细信息)
List<MenuNode> menuNodeList = sysGroupApplicationService.getMenuTreeByGroupId(groupId);
// 转换为DTO
List<MenuNodeDto> menuDtoList = MenuNodeConverter.convertMenuTreeToDtoTree(menuNodeList);
I18nUtil.replaceSystemMessage(menuDtoList);
return ReqResult.success(menuDtoList);
}
private void checkForbiddenMenuCodes(List<MenuNodeDto> nodes) {
if (CollectionUtils.isEmpty(nodes)) {
return;
}
for (MenuNodeDto node : nodes) {
if (StringUtils.isNotBlank(node.getCode())) {
if (node.getCode().equals(MenuEnum.ECO_MARKET.getCode()) && node.getMenuBindType() > 0) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupCannotBindForbiddenMenu,
MenuEnum.ECO_MARKET.getName());
}
if (node.getCode().equals(MenuEnum.SYSTEM_MANAGE.getCode()) && node.getMenuBindType() > 0) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupCannotBindForbiddenMenu,
MenuEnum.SYSTEM_MANAGE.getName());
}
}
if (CollectionUtils.isNotEmpty(node.getChildren())) {
checkForbiddenMenuCodes(node.getChildren());
}
}
}
}

View File

@@ -0,0 +1,441 @@
package com.xspaceagi.system.web.controller.permission;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.xspaceagi.system.application.dto.permission.*;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.utils.I18nUtil;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
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.RestController;
import com.xspaceagi.system.application.service.SysMenuApplicationService;
import com.xspaceagi.system.application.service.SysResourceApplicationService;
import com.xspaceagi.system.domain.model.MenuNode;
import com.xspaceagi.system.domain.model.SortIndex;
import com.xspaceagi.system.infra.dao.entity.SysMenu;
import com.xspaceagi.system.infra.dao.entity.SysMenuResource;
import com.xspaceagi.system.infra.dao.entity.SysResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.BindTypeEnum;
import com.xspaceagi.system.spec.enums.OpenTypeEnum;
import com.xspaceagi.system.spec.enums.SourceEnum;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
import com.xspaceagi.system.web.controller.base.BaseController;
import com.xspaceagi.system.application.converter.MenuBindResourceModelConverter;
import com.xspaceagi.system.application.converter.MenuTreeUtil;
import com.xspaceagi.system.application.converter.ResourceTreeUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@Slf4j
@Tag(name = "权限管理-菜单", description = "菜单相关接口")
@RestController
@RequestMapping("/api/system/menu")
public class SysMenuController extends BaseController {
@Resource
private SysMenuApplicationService sysMenuApplicationService;
@Resource
private SysResourceApplicationService sysResourceApplicationService;
@RequireResource(MENU_MANAGE_ADD)
@Operation(summary = "添加菜单")
@PostMapping(value = "/add",produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> addMenu(@RequestBody SysMenuAddDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
if (dto.getSource() != null && SourceEnum.isInValid(dto.getSource())) {
return ReqResult.error("参数source错误");
}
if (dto.getOpenType() != null && OpenTypeEnum.isInValid(dto.getOpenType())) {
return ReqResult.error("参数openType错误");
}
if (dto.getStatus() != null && YesOrNoEnum.isInValid(dto.getStatus())) {
return ReqResult.error("参数status错误");
}
SysMenu menu = new SysMenu();
BeanUtils.copyProperties(dto, menu);
MenuNode menuNode = null;
if (CollectionUtils.isNotEmpty(dto.getResourceTree())) {
SysMenuBindResourceDto bindResourceDto = new SysMenuBindResourceDto();
bindResourceDto.setResourceTree(dto.getResourceTree());
menuNode = MenuBindResourceModelConverter.convertToMenuNode(bindResourceDto);
}
sysMenuApplicationService.addMenu(menu, menuNode, SourceEnum.CUSTOM.getCode(), getUser());
return ReqResult.success();
}
@RequireResource(MENU_MANAGE_MODIFY)
@Operation(summary = "更新菜单")
@PostMapping("/update")
public ReqResult<Void> updateMenu(@RequestBody SysMenuUpdateDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
if (dto.getSource() != null && SourceEnum.isInValid(dto.getSource())) {
return ReqResult.error("参数source错误");
}
if (dto.getOpenType() != null && OpenTypeEnum.isInValid(dto.getOpenType())) {
return ReqResult.error("参数openType错误");
}
if (dto.getStatus() != null && YesOrNoEnum.isInValid(dto.getStatus())) {
return ReqResult.error("参数status错误");
}
SysMenu menu = new SysMenu();
BeanUtils.copyProperties(dto, menu);
MenuNode menuNode = null;
if (CollectionUtils.isNotEmpty(dto.getResourceTree())) {
SysMenuBindResourceDto bindResourceDto = new SysMenuBindResourceDto();
bindResourceDto.setResourceTree(dto.getResourceTree());
menuNode = MenuBindResourceModelConverter.convertToMenuNode(bindResourceDto);
}
menu.setCode(null); // code不允许更新
sysMenuApplicationService.updateMenu(menu, menuNode, SourceEnum.CUSTOM.getCode(), getUser());
return ReqResult.success();
}
@RequireResource(MENU_MANAGE_DELETE)
@Operation(summary = "删除菜单")
@PostMapping("/delete/{menuId}")
public ReqResult<Void> deleteMenu(@PathVariable Long menuId) {
if (menuId == null) {
return ReqResult.error("参数不能为空");
}
sysMenuApplicationService.deleteMenu(menuId, getUser());
return ReqResult.success();
}
@RequireResource(MENU_MANAGE_QUERY)
@Operation(summary = "根据ID查询菜单")
@GetMapping("/{menuId}")
public ReqResult<MenuNodeDto> getMenuById(@PathVariable Long menuId) {
if (menuId == null) {
return ReqResult.error("参数不能为空");
}
SysMenu menu = sysMenuApplicationService.getMenuById(menuId);
if (menu == null) {
return ReqResult.error("菜单不存在");
}
MenuNodeDto dto = new MenuNodeDto();
BeanUtils.copyProperties(menu, dto);
ReqResult<List<ResourceNodeDto>> resourceListByMenuId = getResourceListByMenuId(menuId);
dto.setResourceTree(resourceListByMenuId.getData());
I18nUtil.replaceSystemMessage(dto);
return ReqResult.success(dto);
}
@RequireResource(MENU_MANAGE_QUERY)
@Operation(summary = "根据编码查询菜单")
@GetMapping("/code/{menuCode}")
public ReqResult<MenuNodeDto> getMenuByCode(@PathVariable String menuCode) {
if (StringUtils.isBlank(menuCode)) {
return ReqResult.error("参数不能为空");
}
SysMenu menu = sysMenuApplicationService.getMenuByCode(menuCode);
if (menu == null) {
return ReqResult.error("菜单不存在");
}
MenuNodeDto dto = new MenuNodeDto();
BeanUtils.copyProperties(menu, dto);
ReqResult<List<ResourceNodeDto>> resourceListByMenuId = getResourceListByMenuId(menu.getId());
dto.setResourceTree(resourceListByMenuId.getData());
I18nUtil.replaceSystemMessage(dto);
return ReqResult.success(dto);
}
@RequireResource(MENU_MANAGE_QUERY)
@Operation(summary = "根据条件查询菜单列表(树形结构)")
@GetMapping("/list")
public ReqResult<List<MenuNodeDto>> getMenuList(SysMenuQueryDto queryDto) {
SysMenu sysMenu = new SysMenu();
if (queryDto != null) {
BeanUtils.copyProperties(queryDto, sysMenu);
}
List<SysMenu> menuList = sysMenuApplicationService.getMenuList(sysMenu);
List<MenuNodeDto> menuDtoList = menuList.stream().map(menu -> {
MenuNodeDto dto = new MenuNodeDto();
BeanUtils.copyProperties(menu, dto);
return dto;
}).collect(Collectors.toList());
List<MenuNodeDto> treeList = MenuTreeUtil.buildMenuTree(menuDtoList);
// 统一处理:如果 parentId 为空,则设置为 0
fillMenuParentIdIfNull(treeList);
// 如果 queryDto 为 null 或所有字段都为空(查询完整菜单树),需要添加根节点
if (isMenuQueryEmpty(queryDto)) {
MenuNodeDto rootNode = getMenuRoot();
rootNode.setChildren(treeList);
List<MenuNodeDto> result = new ArrayList<>();
result.add(rootNode);
I18nUtil.replaceSystemMessage(treeList);
return ReqResult.success(result);
}
I18nUtil.replaceSystemMessage(treeList);
return ReqResult.success(treeList);
}
@RequireResource(MENU_MANAGE_MODIFY)
@Operation(summary = "批量调整菜单顺序")
@PostMapping("/update-sort")
public ReqResult<Void> updateMenuSort(@RequestBody SortIndexUpdateDto dto) {
if (dto == null || CollectionUtils.isEmpty(dto.getItems())) {
return ReqResult.error("参数不能为空");
}
List<SortIndex> sortIndexList = dto.getItems().stream()
.map(item -> {
SortIndex model = new SortIndex();
model.setId(item.getId());
model.setParentId(item.getParentId());
model.setSortIndex(item.getSortIndex());
return model;
})
.collect(Collectors.toList());
sysMenuApplicationService.batchUpdateMenuSort(sortIndexList, getUser());
return ReqResult.success();
}
@RequireResource(MENU_MANAGE_MODIFY)
@Operation(summary = "菜单绑定资源")
@PostMapping("/bind-resource")
public ReqResult<Void> bindResource(@RequestBody SysMenuBindResourceDto dto) {
if (dto == null || dto.getMenuId() == null) {
return ReqResult.error("参数不能为空");
}
MenuNode menuNode = MenuBindResourceModelConverter.convertToMenuNode(dto);
sysMenuApplicationService.bindResource(menuNode, getUser());
return ReqResult.success();
}
@RequireResource(MENU_MANAGE_QUERY)
@Operation(summary = "查询菜单绑定的资源(树形结构)")
@GetMapping("/list-resource/{menuId}")
public ReqResult<List<ResourceNodeDto>> getResourceListByMenuId(@PathVariable Long menuId) {
if (menuId == null) {
return ReqResult.error("参数不能为空");
}
// 查询菜单绑定的资源列表(可能为空)
List<SysMenuResource> menuResourceList = sysMenuApplicationService.getResourceListByMenuId(menuId);
// 查询所有资源列表(构建完整资源树)
List<SysResource> resourceList = sysResourceApplicationService.getResourceList(null);
// 构建资源父子关系映射
Map<Long, List<SysResource>> resourceChildrenMap = new HashMap<>();
for (SysResource resource : resourceList) {
Long parentId = resource.getParentId();
if (parentId != null && parentId != 0L) {
resourceChildrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(resource);
}
}
// 提取 资源ID->绑定类型 映射(初始映射,只包含数据库中存储的资源)
Map<Long, Integer> resourceBindTypeMap = CollectionUtils.isEmpty(menuResourceList)
? new HashMap<>()
: menuResourceList.stream()
.filter(mr -> mr.getResourceId() != null)
.collect(Collectors.toMap(
SysMenuResource::getResourceId,
SysMenuResource::getResourceBindType,
(existing, replacement) -> existing // 如果有重复,保留第一个
));
// 对于绑定类型为 ALL 的资源,需要将其所有子资源也标记为 ALL
// 注意ALL 类型的资源会强制覆盖子资源的绑定类型
Set<Long> allBindResourceIds = new HashSet<>();
for (SysMenuResource menuResource : menuResourceList) {
if (menuResource.getResourceId() == null) {
continue;
}
Integer bindType = menuResource.getResourceBindType();
if (BindTypeEnum.ALL.getCode().equals(bindType)) {
allBindResourceIds.add(menuResource.getResourceId());
}
}
// 对于 ALL 类型的资源,递归收集所有子资源,并强制设置为 ALL
Set<Long> processedResourceIds = new HashSet<>();
for (Long allBindResourceId : allBindResourceIds) {
collectChildrenAndSetBindType(allBindResourceId, resourceChildrenMap,
resourceBindTypeMap, processedResourceIds, BindTypeEnum.ALL.getCode());
}
// 构建资源树(完整资源树)
// 注意:对于 PART 类型的资源,子节点会根据 resourceBindTypeMap 中的具体值来标记
// 如果子节点不在 resourceBindTypeMap 中,则标记为 NONE未绑定
List<ResourceNodeDto> resourceTree = ResourceTreeUtil.buildResourceTree(resourceList, resourceBindTypeMap);
// 检查资源树中是否已经有 root 根节点
boolean hasRootNode = resourceTree.stream()
.anyMatch(node -> "root".equals(node.getCode()) && node.getId() != null && node.getId() == 0L);
// 如果没有 root 根节点,则添加
if (!hasRootNode) {
ResourceNodeDto rootNode = getResourceRoot();
// 根据业务逻辑设置根节点的绑定类型
Integer rootBindType;
if (CollectionUtils.isEmpty(menuResourceList)) {
// 如果菜单绑定的资源列表为空,设置为 NONE
rootBindType = BindTypeEnum.NONE.getCode();
} else if (CollectionUtils.isEmpty(resourceTree)) {
// 如果资源树为空,设置为 NONE
rootBindType = BindTypeEnum.NONE.getCode();
} else {
// 检查所有根节点的直接子节点parentId=0或null的绑定类型是否都是 ALL
// 从原始资源列表中找出所有根节点parentId=0或null检查它们的绑定类型
List<SysResource> rootResources = resourceList.stream()
.filter(resource -> resource.getParentId() == null || resource.getParentId() == 0L)
.collect(Collectors.toList());
if (rootResources.isEmpty()) {
rootBindType = BindTypeEnum.NONE.getCode();
} else {
boolean allChildrenAreAll = rootResources.stream()
.allMatch(resource -> {
Integer bindType = resourceBindTypeMap.get(resource.getId());
return bindType != null && BindTypeEnum.ALL.getCode().equals(bindType);
});
if (allChildrenAreAll) {
rootBindType = BindTypeEnum.ALL.getCode();
} else {
rootBindType = BindTypeEnum.PART.getCode();
}
}
}
rootNode.setResourceBindType(rootBindType);
rootNode.setChildren(resourceTree);
List<ResourceNodeDto> result = new ArrayList<>();
result.add(rootNode);
I18nUtil.replaceSystemMessage(result);
return ReqResult.success(result);
}
// 如果已经有 root 根节点,直接返回资源树
I18nUtil.replaceSystemMessage(resourceTree);
return ReqResult.success(resourceTree);
}
/**
* 递归收集某个资源的所有子资源,并强制设置绑定类型
* 注意:此方法用于处理 ALL 类型的资源,会强制覆盖子资源的绑定类型
*/
private void collectChildrenAndSetBindType(Long resourceId,
Map<Long, List<SysResource>> resourceChildrenMap,
Map<Long, Integer> resourceBindTypeMap,
Set<Long> processedResourceIds,
Integer bindType) {
if (processedResourceIds.contains(resourceId)) {
return; // 避免重复处理
}
processedResourceIds.add(resourceId);
List<SysResource> children = resourceChildrenMap.get(resourceId);
if (CollectionUtils.isEmpty(children)) {
return;
}
for (SysResource child : children) {
if (child.getId() == null) {
continue;
}
// 强制设置子资源的绑定类型ALL 类型会覆盖子资源的原有绑定类型)
resourceBindTypeMap.put(child.getId(), bindType);
// 递归处理子资源的子资源
collectChildrenAndSetBindType(child.getId(), resourceChildrenMap, resourceBindTypeMap,
processedResourceIds, bindType);
}
}
/**
* 判断菜单查询条件是否为空
* @param queryDto 查询条件
* @return true表示查询条件为空null或所有字段都为null/空值)
*/
private boolean isMenuQueryEmpty(SysMenuQueryDto queryDto) {
if (queryDto == null) {
return true;
}
return StringUtils.isBlank(queryDto.getCode())
&& StringUtils.isBlank(queryDto.getName())
&& queryDto.getSource() == null
&& queryDto.getParentId() == null
&& queryDto.getStatus() == null;
}
/**
* 递归处理菜单树,如果 parentId 为空则设置为 0
*/
private void fillMenuParentIdIfNull(List<MenuNodeDto> list) {
if (CollectionUtils.isEmpty(list)) {
return;
}
for (MenuNodeDto node : list) {
if (node.getParentId() == null) {
node.setParentId(0L);
}
if (CollectionUtils.isNotEmpty(node.getChildren())) {
fillMenuParentIdIfNull(node.getChildren());
}
}
}
private MenuNodeDto getMenuRoot() {
MenuNodeDto rootNode = new MenuNodeDto();
rootNode.setId(0L);
rootNode.setCode("root");
rootNode.setName("根节点");
rootNode.setParentId(null);
rootNode.setDescription("根节点");
rootNode.setSource(SourceEnum.SYSTEM.getCode());
rootNode.setOpenType(OpenTypeEnum.CURRENT_TAB.getCode());
rootNode.setStatus(YesOrNoEnum.Y.getKey());
return rootNode;
}
private ResourceNodeDto getResourceRoot() {
ResourceNodeDto rootNode = new ResourceNodeDto();
rootNode.setId(0L);
rootNode.setCode("root");
rootNode.setName("根节点");
rootNode.setDescription("根节点");
rootNode.setParentId(null);
rootNode.setSource(SourceEnum.SYSTEM.getCode());
rootNode.setStatus(YesOrNoEnum.Y.getKey());
return rootNode;
}
}

View File

@@ -0,0 +1,201 @@
package com.xspaceagi.system.web.controller.permission;
import com.xspaceagi.system.application.dto.permission.*;
import com.xspaceagi.system.application.service.SysResourceApplicationService;
import com.xspaceagi.system.domain.model.SortIndex;
import com.xspaceagi.system.infra.dao.entity.SysResource;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.ResourceTypeEnum;
import com.xspaceagi.system.spec.enums.SourceEnum;
import com.xspaceagi.system.spec.enums.YesOrNoEnum;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.web.controller.base.BaseController;
import com.xspaceagi.system.application.converter.ResourceTreeUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@Slf4j
@Tag(name = "权限管理-资源", description = "资源相关接口")
@RestController
@RequestMapping("/api/system/resource")
public class SysResourceController extends BaseController {
@Resource
private SysResourceApplicationService sysResourceApplicationService;
@RequireResource(RESOURCE_MANAGE_ADD)
@Operation(summary = "添加资源")
@PostMapping(value = "/add",produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> addResource(@RequestBody SysResourceAddDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
if (dto.getSource() != null && SourceEnum.isInValid(dto.getSource())) {
return ReqResult.error("参数source错误");
}
if (dto.getStatus() != null && YesOrNoEnum.isInValid(dto.getStatus())) {
return ReqResult.error("参数status错误");
}
if (dto.getType() != null && ResourceTypeEnum.isInValid(dto.getType())) {
return ReqResult.error("参数type错误");
}
SysResource resource = new SysResource();
BeanUtils.copyProperties(dto, resource);
sysResourceApplicationService.addResource(resource, getUser());
return ReqResult.success();
}
@RequireResource(RESOURCE_MANAGE_MODIFY)
@Operation(summary = "更新资源")
@PostMapping("/update")
public ReqResult<Void> updateResource(@RequestBody SysResourceUpdateDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
if (dto.getSource() != null && SourceEnum.isInValid(dto.getSource())) {
return ReqResult.error("参数source错误");
}
if (dto.getStatus() != null && YesOrNoEnum.isInValid(dto.getStatus())) {
return ReqResult.error("参数status错误");
}
if (dto.getType() != null && ResourceTypeEnum.isInValid(dto.getType())) {
return ReqResult.error("参数type错误");
}
SysResource resource = new SysResource();
BeanUtils.copyProperties(dto, resource);
resource.setCode(null); // code不允许更新
sysResourceApplicationService.updateResource(resource, getUser());
return ReqResult.success();
}
@RequireResource(RESOURCE_MANAGE_DELETE)
@Operation(summary = "删除资源")
@PostMapping("/delete/{resourceId}")
public ReqResult<Void> deleteResource(@PathVariable Long resourceId) {
if (resourceId == null) {
return ReqResult.error("参数不能为空");
}
sysResourceApplicationService.deleteResource(resourceId, getUser());
return ReqResult.success();
}
@RequireResource(RESOURCE_MANAGE_QUERY)
@Operation(summary = "根据ID查询资源")
@GetMapping("/{resourceId}")
public ReqResult<ResourceNodeDto> getResourceById(@PathVariable Long resourceId) {
if (resourceId == null) {
return ReqResult.error("参数不能为空");
}
SysResource resource = sysResourceApplicationService.getResourceById(resourceId);
if (resource == null) {
return ReqResult.error("资源不存在");
}
ResourceNodeDto dto = new ResourceNodeDto();
BeanUtils.copyProperties(resource, dto);
I18nUtil.replaceSystemMessage(dto);
return ReqResult.success(dto);
}
@RequireResource(RESOURCE_MANAGE_QUERY)
@Operation(summary = "根据编码查询资源")
@GetMapping("/code/{resourceCode}")
public ReqResult<ResourceNodeDto> getResourceByCode(@PathVariable String resourceCode) {
if (StringUtils.isBlank(resourceCode)) {
return ReqResult.error("参数不能为空");
}
SysResource resource = sysResourceApplicationService.getResourceByCode(resourceCode);
if (resource == null) {
return ReqResult.error("资源不存在");
}
ResourceNodeDto dto = new ResourceNodeDto();
BeanUtils.copyProperties(resource, dto);
I18nUtil.replaceSystemMessage(dto);
return ReqResult.success(dto);
}
@RequireResource(RESOURCE_MANAGE_MODIFY)
@Operation(summary = "调整资源顺序")
@PostMapping("/update-sort")
public ReqResult<Void> updateSortIndex(@RequestBody SortIndexUpdateDto dto) {
if (dto == null || CollectionUtils.isEmpty(dto.getItems())) {
return ReqResult.error("参数不能为空");
}
List<SortIndex> sortIndexList = dto.getItems().stream()
.map(item -> {
SortIndex model = new SortIndex();
model.setId(item.getId());
model.setParentId(item.getParentId());
model.setSortIndex(item.getSortIndex());
return model;
})
.collect(Collectors.toList());
sysResourceApplicationService.batchUpdateResourceSort(sortIndexList, getUser());
return ReqResult.success();
}
@RequireResource(RESOURCE_MANAGE_QUERY)
@Operation(summary = "根据条件查询资源列表(树形结构)")
@GetMapping("/list")
public ReqResult<List<ResourceNodeDto>> getResourceList(SysResourceQueryDto queryDto) {
SysResource sysResource = new SysResource();
if (queryDto != null) {
BeanUtils.copyProperties(queryDto, sysResource);
}
List<SysResource> resourceList = sysResourceApplicationService.getResourceList(sysResource);
// 构建资源树
List<ResourceNodeDto> tree = ResourceTreeUtil.buildResourceTree(resourceList);
// 如果 queryDto 为 null 或所有字段都为空(查询完整资源树),需要添加根节点
if (isQueryEmpty(queryDto)) {
ResourceNodeDto rootNode = new ResourceNodeDto();
rootNode.setId(0L);
rootNode.setCode("root");
rootNode.setName("根节点");
rootNode.setParentId(null);
rootNode.setChildren(tree);
List<ResourceNodeDto> result = new ArrayList<>();
result.add(rootNode);
I18nUtil.replaceSystemMessage(result);
return ReqResult.success(result);
}
I18nUtil.replaceSystemMessage(tree);
return ReqResult.success(tree);
}
/**
* 判断查询条件是否为空
*/
private boolean isQueryEmpty(SysResourceQueryDto queryDto) {
if (queryDto == null) {
return true;
}
return StringUtils.isBlank(queryDto.getCode())
&& StringUtils.isBlank(queryDto.getName())
&& queryDto.getSource() == null
&& queryDto.getType() == null
&& queryDto.getParentId() == null
&& queryDto.getStatus() == null;
}
}

View File

@@ -0,0 +1,360 @@
package com.xspaceagi.system.web.controller.permission;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xspaceagi.system.application.converter.MenuNodeConverter;
import com.xspaceagi.system.application.converter.RoleBindMenuModelConverter;
import com.xspaceagi.system.application.converter.SysDataPermissionConverter;
import com.xspaceagi.system.application.dto.permission.*;
import com.xspaceagi.system.application.service.SysDataPermissionApplicationService;
import com.xspaceagi.system.application.service.SysRoleApplicationService;
import com.xspaceagi.system.application.service.SysSubjectPermissionApplicationService;
import com.xspaceagi.system.domain.model.MenuNode;
import com.xspaceagi.system.domain.model.RoleBindMenuModel;
import com.xspaceagi.system.domain.model.SortIndex;
import com.xspaceagi.system.infra.dao.entity.SysDataPermission;
import com.xspaceagi.system.infra.dao.entity.SysRole;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.enums.PermissionSubjectTypeEnum;
import com.xspaceagi.system.spec.enums.PermissionTargetTypeEnum;
import com.xspaceagi.system.spec.enums.SourceEnum;
import com.xspaceagi.system.spec.enums.StatusEnum;
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.web.controller.base.BaseController;
import com.fasterxml.jackson.core.type.TypeReference;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import com.xspaceagi.system.spec.dto.PageQueryVo;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@Slf4j
@Tag(name = "权限管理-角色", description = "角色相关接口")
@RestController
@RequestMapping("/api/system/role")
public class SysRoleController extends BaseController {
@Resource
private SysRoleApplicationService sysRoleApplicationService;
@Autowired
private SysDataPermissionApplicationService sysDataPermissionApplicationService;
@Autowired
private SysSubjectPermissionApplicationService sysSubjectPermissionApplicationService;
@RequireResource(ROLE_MANAGE_ADD)
@Operation(summary = "添加角色")
@PostMapping(value = "/add", produces = MediaType.APPLICATION_JSON_VALUE)
public ReqResult<Void> addRole(@RequestBody SysRoleAddDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
if (dto.getSource() != null && SourceEnum.isInValid(dto.getSource())) {
return ReqResult.error("参数source错误");
}
if (dto.getStatus() != null && StatusEnum.isInValid(dto.getStatus())) {
return ReqResult.error("参数status错误");
}
SysRole role = new SysRole();
BeanUtils.copyProperties(dto, role);
sysRoleApplicationService.addRole(role, getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_MODIFY)
@Operation(summary = "更新角色")
@PostMapping("/update")
public ReqResult<Void> updateRole(@RequestBody SysRoleUpdateDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
if (dto.getSource() != null && SourceEnum.isInValid(dto.getSource())) {
return ReqResult.error("参数source错误");
}
if (dto.getStatus() != null && StatusEnum.isInValid(dto.getStatus())) {
return ReqResult.error("参数status错误");
}
SysRole role = new SysRole();
BeanUtils.copyProperties(dto, role);
role.setCode(null); // code不允许更新
sysRoleApplicationService.updateRole(role, getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_DELETE)
@Operation(summary = "删除角色")
@PostMapping("/delete/{roleId}")
public ReqResult<Void> deleteRole(@PathVariable Long roleId) {
if (roleId == null) {
return ReqResult.error("参数不能为空");
}
sysRoleApplicationService.deleteRole(roleId, getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_QUERY)
@Operation(summary = "根据ID查询角色")
@GetMapping("/{roleId}")
public ReqResult<SysRoleDto> getRoleById(@PathVariable Long roleId) {
if (roleId == null) {
return ReqResult.error("参数不能为空");
}
SysRole role = sysRoleApplicationService.getRoleById(roleId);
if (role == null) {
return ReqResult.error("角色不存在");
}
SysRoleDto roleDto = new SysRoleDto();
BeanUtils.copyProperties(role, roleDto);
// SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.ROLE, role.getId());
// if (dataPermission != null) {
// roleDto.setModelIds(dataPermission.getModelIds());
// roleDto.setTokenLimit(dataPermission.getTokenLimit());
// }
I18nUtil.replaceSystemMessage(roleDto);
return ReqResult.success(roleDto);
}
@RequireResource(ROLE_MANAGE_QUERY)
@Operation(summary = "根据编码查询角色")
@GetMapping("/code/{roleCode}")
public ReqResult<SysRoleDto> getRoleByCode(@PathVariable String roleCode) {
if (StringUtils.isBlank(roleCode)) {
return ReqResult.error("参数不能为空");
}
SysRole role = sysRoleApplicationService.getRoleByCode(roleCode);
if (role == null) {
return ReqResult.error("角色不存在");
}
SysRoleDto roleDto = new SysRoleDto();
BeanUtils.copyProperties(role, roleDto);
// SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.ROLE, role.getId());
// if (dataPermission != null) {
// roleDto.setModelIds(dataPermission.getModelIds());
// roleDto.setTokenLimit(dataPermission.getTokenLimit());
// }
I18nUtil.replaceSystemMessage(roleDto);
return ReqResult.success(roleDto);
}
@RequireResource(ROLE_MANAGE_MODIFY)
@Operation(summary = "调整角色顺序")
@PostMapping("/update-sort")
public ReqResult<Void> updateSortIndex(@RequestBody SortIndexUpdateDto dto) {
if (dto == null || CollectionUtils.isEmpty(dto.getItems())) {
return ReqResult.error("参数不能为空");
}
List<SortIndex> sortIndexList = dto.getItems().stream()
.map(item -> {
SortIndex model = new SortIndex();
model.setId(item.getId());
model.setParentId(item.getParentId());
model.setSortIndex(item.getSortIndex());
return model;
})
.collect(Collectors.toList());
sysRoleApplicationService.batchUpdateRoleSort(sortIndexList, getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_QUERY)
@Operation(summary = "根据条件查询角色")
@GetMapping("/list")
public ReqResult<List<SysRoleDto>> getRoleList(SysRoleQueryDto dto) {
SysRole sysRole = new SysRole();
if (dto != null) {
BeanUtils.copyProperties(dto, sysRole);
}
List<SysRole> roleList = sysRoleApplicationService.getRoleList(sysRole);
if (CollectionUtils.isEmpty(roleList)) {
return ReqResult.success();
}
// List<Long> roleIds = roleList.stream().map(SysRole::getId).toList();
// List<SysDataPermission> dataPermissionList = sysDataPermissionApplicationService.getByTargetList(PermissionTargetTypeEnum.ROLE, roleIds);
// Map<Long, SysDataPermission> permissionMap = dataPermissionList.stream()
// .collect(Collectors.toMap(SysDataPermission::getTargetId, p -> p, (a, b) -> a));
List<SysRoleDto> dtoList = roleList.stream().map(r -> {
SysRoleDto roleDto = new SysRoleDto();
BeanUtils.copyProperties(r, roleDto);
// SysDataPermission dataPermission = permissionMap.get(r.getId());
// if (dataPermission != null) {
// roleDto.setModelIds(dataPermission.getModelIds());
// roleDto.setTokenLimit(dataPermission.getTokenLimit());
// }
return roleDto;
}).collect(Collectors.toList());
I18nUtil.replaceSystemMessage(dtoList);
return ReqResult.success(dtoList);
}
@RequireResource(ROLE_MANAGE_BIND_USER)
@Operation(summary = "分页查询角色已绑定的用户支持按userName模糊筛选")
@PostMapping("/list-user")
public ReqResult<IPage<SysUserDto>> getUserListByRoleId(@RequestBody PageQueryVo<SysRoleUserQueryDto> pageQueryVo) {
if (pageQueryVo == null || pageQueryVo.getQueryFilter() == null || pageQueryVo.getQueryFilter().getRoleId() == null) {
return ReqResult.error("角色ID不能为空");
}
SysRoleUserQueryDto queryDto = pageQueryVo.getQueryFilter();
Long roleId = queryDto.getRoleId();
String userName = queryDto.getUserName();
long pageNo = pageQueryVo.getPageNo() != null ? pageQueryVo.getPageNo() : 1L;
long pageSize = pageQueryVo.getPageSize() != null ? pageQueryVo.getPageSize() : 10L;
IPage<User> userPage = sysRoleApplicationService.getUserPageByRoleId(roleId, userName, pageNo, pageSize);
List<SysUserDto> dtoList = userPage.getRecords().stream().map(user -> {
SysUserDto userDto = new SysUserDto();
BeanUtils.copyProperties(user, userDto);
userDto.setUserId(user.getId());
return userDto;
}).collect(Collectors.toList());
IPage<SysUserDto> resultPage = new Page<>(userPage.getCurrent(), userPage.getSize(), userPage.getTotal());
resultPage.setRecords(dtoList);
return ReqResult.success(resultPage);
}
@RequireResource(ROLE_MANAGE_BIND_USER)
@Operation(summary = "角色绑定用户(全量覆盖)")
@PostMapping("/bind-user")
public ReqResult<Void> bindUser(@RequestBody SysRoleBindUserDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
sysRoleApplicationService.roleBindUser(dto.getRoleId(), dto.getUserIds(), getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_BIND_USER)
@Operation(summary = "角色添加用户")
@PostMapping("/add-user")
public ReqResult<Void> addUser(@RequestBody SysRoleBindUserSingleDto dto) {
if (dto == null || dto.getRoleId() == null || dto.getUserId() == null) {
return ReqResult.error("角色ID和用户ID不能为空");
}
sysRoleApplicationService.roleAddUser(dto.getRoleId(), dto.getUserId(), getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_BIND_USER)
@Operation(summary = "角色移除用户")
@PostMapping("/remove-user")
public ReqResult<Void> removeUser(@RequestBody SysRoleBindUserSingleDto dto) {
if (dto == null || dto.getRoleId() == null || dto.getUserId() == null) {
return ReqResult.error("角色ID和用户ID不能为空");
}
sysRoleApplicationService.roleRemoveUser(dto.getRoleId(), dto.getUserId(), getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_BIND_DATA)
@Operation(summary = "查询角色数据权限")
@GetMapping("/data-permission/{roleId}")
public ReqResult<SysDataPermissionBindDto> getRoleDataPermission(@PathVariable Long roleId) {
if (roleId == null) {
return ReqResult.error("参数不能为空");
}
SysDataPermission dataPermission = sysDataPermissionApplicationService.getByTarget(PermissionTargetTypeEnum.ROLE, roleId);
SysDataPermissionBindDto result = SysDataPermissionConverter.toDto(dataPermission);
result = result == null ? new SysDataPermissionBindDto() : result;
result.setModelIds(sysSubjectPermissionApplicationService.listSubjectIdsByTarget(
PermissionTargetTypeEnum.ROLE, roleId, PermissionSubjectTypeEnum.MODEL));
result.setAgentIds(sysSubjectPermissionApplicationService.listSubjectIdsByTarget(
PermissionTargetTypeEnum.ROLE, roleId, PermissionSubjectTypeEnum.AGENT));
result.setPageAgentIds(sysSubjectPermissionApplicationService.listSubjectIdsByTarget(
PermissionTargetTypeEnum.ROLE, roleId, PermissionSubjectTypeEnum.PAGE));
Map<String, String> openApiConfigMap = sysSubjectPermissionApplicationService.listSubjectKeyConfigByTarget(
PermissionTargetTypeEnum.ROLE, roleId, PermissionSubjectTypeEnum.OPEN_API);
List<SysDataPermissionBindDto.OpenApiConfig> openApiConfigs = openApiConfigMap.entrySet().stream()
.map(entry -> {
SysDataPermissionBindDto.OpenApiConfig config = new SysDataPermissionBindDto.OpenApiConfig();
config.setKey(entry.getKey());
if (StringUtils.isNotBlank(entry.getValue())) {
try {
Map<String, Integer> valueMap = JsonSerializeUtil.parseObject(entry.getValue(), new TypeReference<Map<String, Integer>>() {});
config.setRpm(valueMap == null ? null : valueMap.get("rpm"));
config.setRpd(valueMap == null ? null : valueMap.get("rpd"));
} catch (Exception ignore) {
config.setRpm(null);
config.setRpd(null);
}
}
return config;
})
.toList();
result.setOpenApiConfigs(openApiConfigs);
result.setKnowledgeIds(sysSubjectPermissionApplicationService.listSubjectIdsByTarget(
PermissionTargetTypeEnum.ROLE, roleId, PermissionSubjectTypeEnum.KNOWLEDGE));
return ReqResult.success(result);
}
@RequireResource(ROLE_MANAGE_BIND_DATA)
@Operation(summary = "角色绑定数据权限(全量覆盖)")
@PostMapping("/bind-data-permission")
public ReqResult<Void> bindDataPermission(@RequestBody SysRoleBindDataPermissionDto dto) {
if (dto == null || dto.getRoleId() == null) {
return ReqResult.error("角色ID不能为空");
}
SysDataPermissionBindDto bindDto = dto.getDataPermission();
SysDataPermission dataPermission = SysDataPermissionConverter.toEntity(bindDto);
sysRoleApplicationService.bindDataPermission(dto.getRoleId(), dataPermission, getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_BIND_MENU)
@Operation(summary = "角色绑定菜单(全量覆盖)")
@PostMapping("/bind-menu")
public ReqResult<Void> bindMenu(@RequestBody SysRoleBindMenuDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
RoleBindMenuModel model = RoleBindMenuModelConverter.convertToModel(dto);
sysRoleApplicationService.bindMenu(model, getUser());
return ReqResult.success();
}
@RequireResource(ROLE_MANAGE_BIND_MENU)
@Operation(summary = "查询角色已绑定的菜单(树形结构)")
@GetMapping("/list-menu/{roleId}")
public ReqResult<List<MenuNodeDto>> getMenuListByRoleId(@PathVariable Long roleId) {
if (roleId == null) {
return ReqResult.error("参数不能为空");
}
// 获取处理好的菜单树(已包含资源详细信息)
List<MenuNode> menuNodeList = sysRoleApplicationService.getMenuTreeByRoleId(roleId);
// 转换为DTO
List<MenuNodeDto> menuDtoList = MenuNodeConverter.convertMenuTreeToDtoTree(menuNodeList);
I18nUtil.replaceSystemMessage(menuDtoList);
return ReqResult.success(menuDtoList);
}
}

View File

@@ -0,0 +1,151 @@
package com.xspaceagi.system.web.controller.permission;
import com.xspaceagi.system.application.dto.permission.*;
import com.xspaceagi.system.application.service.SysDataPermissionApplicationService;
import com.xspaceagi.system.application.service.SysGroupApplicationService;
import com.xspaceagi.system.application.service.SysRoleApplicationService;
import com.xspaceagi.system.application.service.impl.SysUserPermissionCacheServiceImpl;
import com.xspaceagi.system.infra.dao.entity.SysGroup;
import com.xspaceagi.system.infra.dao.entity.SysRole;
import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.annotation.SaasAdmin;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.web.controller.base.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
import static com.xspaceagi.system.spec.enums.ResourceEnum.*;
@Slf4j
@Tag(name = "权限管理-用户", description = "用户权限相关接口")
@RestController
@RequestMapping("/api/system/user")
public class SysUserController extends BaseController {
@Resource
private SysRoleApplicationService sysRoleApplicationService;
@Resource
private SysGroupApplicationService sysGroupApplicationService;
@Resource
private SysDataPermissionApplicationService sysDataPermissionApplicationService;
@Resource
private SysUserPermissionCacheServiceImpl sysUserPermissionCacheService;
@RequireResource(USER_MANAGE_BIND_ROLE)
@Operation(summary = "查询用户绑定的角色列表")
@GetMapping("/list-role/{userId}")
public ReqResult<List<SysRoleDto>> getRoleListByUserId(@PathVariable Long userId) {
if (userId == null) {
return ReqResult.error("参数不能为空");
}
List<SysRole> roleList = sysRoleApplicationService.getRoleListByUserId(userId);
if (CollectionUtils.isEmpty(roleList)) {
return ReqResult.success();
}
List<SysRoleDto> dtoList = roleList.stream().map(role -> {
SysRoleDto roleDto = new SysRoleDto();
BeanUtils.copyProperties(role, roleDto);
return roleDto;
}).collect(Collectors.toList());
I18nUtil.replaceSystemMessage(dtoList);
return ReqResult.success(dtoList);
}
@RequireResource(USER_MANAGE_BIND_ROLE)
@Operation(summary = "用户绑定角色(全量覆盖)")
@PostMapping("/bind-role")
public ReqResult<Void> bindUser(@RequestBody SysUserBindRoleDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
sysRoleApplicationService.userBindRole(dto.getUserId(), dto.getRoleIds(), getUser());
return ReqResult.success();
}
@RequireResource(USER_MANAGE_BIND_GROUP)
@Operation(summary = "查询用户绑定的组列表")
@GetMapping("/list-group/{userId}")
public ReqResult<List<SysGroupDto>> getGroupListByUserId(@PathVariable Long userId) {
if (userId == null) {
return ReqResult.error("参数不能为空");
}
List<SysGroup> groupList = sysGroupApplicationService.getGroupListByUserId(userId);
if (CollectionUtils.isEmpty(groupList)) {
return ReqResult.success();
}
List<SysGroupDto> dtoList = groupList.stream().map(group -> {
SysGroupDto groupDto = new SysGroupDto();
BeanUtils.copyProperties(group, groupDto);
return groupDto;
}).collect(Collectors.toList());
I18nUtil.replaceSystemMessage(dtoList);
return ReqResult.success(dtoList);
}
@RequireResource(USER_MANAGE_BIND_GROUP)
@Operation(summary = "用户绑定组(全量覆盖)")
@PostMapping("/bind-group")
public ReqResult<Void> bindGroup(@RequestBody SysUserBindGroupDto dto) {
if (dto == null) {
return ReqResult.error("参数不能为空");
}
sysGroupApplicationService.userBindGroup(dto.getUserId(), dto.getGroupIds(), getUser());
return ReqResult.success();
}
@RequireResource(USER_MANAGE_QUERY_MENU_PERMISSION)
@Operation(summary = "查询用户的菜单权限(树形结构)")
@GetMapping("/list-menu/{userId}")
public ReqResult<List<MenuNodeDto>> getMenuListByUserId(@PathVariable Long userId) {
if (userId == null) {
return ReqResult.error("参数不能为空");
}
List<MenuNodeDto> menuTree = sysUserPermissionCacheService.getUserMenuTree(userId);
I18nUtil.replaceSystemMessage(menuTree);
return ReqResult.success(menuTree);
}
@RequireResource(USER_MANAGE_QUERY_DATA_PERMISSION)
@Operation(summary = "查询用户数据权限")
@GetMapping("/data-permission/{userId}")
public ReqResult<UserDataPermissionDto> getUserDataPermission(@PathVariable Long userId) {
if (userId == null) {
return ReqResult.error("参数不能为空");
}
UserDataPermissionDto dataPermission = sysDataPermissionApplicationService.getUserDataPermission(userId);
return ReqResult.success(dataPermission);
}
@SaasAdmin
@Operation(summary = "清除指定用户权限缓存")
@GetMapping("/cache/clear-user")
public ReqResult<Void> clearUserCache(Long tenantId, Long userId) {
if (userId == null) {
return ReqResult.error("参数不能为空");
}
sysUserPermissionCacheService.clearCacheByTenantAndUserIds(tenantId, List.of(userId));
return ReqResult.success();
}
@SaasAdmin
@Operation(summary = "清除所有用户权限缓存")
@GetMapping("/cache/clear-all")
public ReqResult<Void> clearAllUserCache(Long tenantId) {
sysUserPermissionCacheService.clearCacheAllByTenant(tenantId);
return ReqResult.success();
}
}

View File

@@ -0,0 +1,39 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
public class ApiInvokeStatDto {
@Schema(description = "接口名称")
private String name;
@Schema(description = "接口路径")
private String path;
@Schema(description = "接口标识查询调用记录时from参数传改值")
private String key;
@Schema(description = "接口调用总次数")
private InvokeCount total;
@Schema(description = "接口调用今日次数")
private InvokeCount today;
@Schema(description = "接口调用昨日次数")
private InvokeCount yesterday;
@Schema(description = "接口调用本周次数")
private InvokeCount week;
@Schema(description = "接口调用本月次数")
private InvokeCount month;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class InvokeCount {
private Long totalCount;
private Long successCount;
private Long failCount;
}
}

View File

@@ -0,0 +1,15 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
public class ApiKeyCreateDto {
@Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED)
private String name;
@Schema(description = "过期时间")
private Date expire;
}

View File

@@ -0,0 +1,19 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class BindEmailDto implements Serializable {
@Schema(description = "邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED)
private String email;
@Schema(description = "手机号码", requiredMode = Schema.RequiredMode.REQUIRED)
private String phone;
@Schema(description = "验证码", requiredMode = Schema.RequiredMode.REQUIRED)
private String code;
}

View File

@@ -0,0 +1,22 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Schema(description = "验证码登录")
@Data
public class CodeLoginDto implements Serializable {
@Schema(description = "完整的手机号码,例如 8613888888888")
private String phone;
@Schema(description = "完整的邮箱地址,与手机号码二选一,例如 xxx@xx.com")
private String phoneOrEmail;
@NotNull
@Schema(description = "验证码", requiredMode = Schema.RequiredMode.REQUIRED)
private String code;
}

View File

@@ -0,0 +1,28 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class CodeSendDto implements Serializable {
@Schema(description = "验证码类型", requiredMode = Schema.RequiredMode.REQUIRED)
private Type type;
@Schema(description = "完整的手机号码,例如 8613888888888")
private String phone;
@Schema(description = "邮箱地址,与手机号码二选一")
private String email;
@Schema(description = "验证码参数")
private String captchaVerifyParam;
public enum Type {
LOGIN_OR_REGISTER,
RESET_PASSWORD,
BIND_EMAIL
}
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.system.web.dto;
import com.xspaceagi.system.application.dto.EventDto;
import lombok.Data;
import java.util.List;
@Data
public class EventRespDto {
private String version;
private boolean hasEvent;
private List<EventDto<?>> eventList;
}

View File

@@ -0,0 +1,31 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class LoginResDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "认证token认证方式header中传Authorization:Bearer token")
private String token;
@Schema(description = "token过期时间")
private Date expireDate;
@Schema(description = "判断用户是否设置过密码,如果未设置过,需要弹出密码设置框让用户设置密码")
private Integer resetPass;
@Schema(description = "登录成功后跳转的页面")
private String redirect;
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Schema(description = "小程序code登录")
@Data
public class MPCodeLoginDto implements Serializable {
@NotNull
@Schema(description = "小程序端拿到的code", requiredMode = Schema.RequiredMode.REQUIRED)
private String code;
}

View File

@@ -0,0 +1,25 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Schema(description = "密码登录")
@Data
public class PasswordLoginDto implements Serializable {
@Schema(description = "完整的手机号码,例如 8613888888888", requiredMode = Schema.RequiredMode.REQUIRED)
private String phone;
@Schema(description = "完整的邮箱地址,与手机号码二选一,例如 xxx@xx.com")
private String phoneOrEmail;
@NotNull
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
private String password;
@Schema(description = "验证码参数")
private String captchaVerifyParam;
}

View File

@@ -0,0 +1,14 @@
package com.xspaceagi.system.web.dto;
import lombok.Data;
import java.util.List;
@Data
public class PullMessageAckDto {
private String clientId;
private String clientSecret;
private List<String> messageIds;
private String uid;
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.system.web.dto;
import lombok.Data;
@Data
public class PullMessageRequestDto {
private String tenantName;
private String siteUrl;
private String clientId;
private String clientSecret;
private String installationSource;
private String version;
private String user;
private String uid;
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.system.web.dto;
import lombok.Data;
@Data
public class PullMessageResponseDto {
private String messageId;
private String content;
}

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class ResetPasswordDto implements Serializable {
@Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED)
private String newPassword;
@Schema(description = "验证码", requiredMode = Schema.RequiredMode.REQUIRED)
private String code;
}

View File

@@ -0,0 +1,13 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class SetPasswordDto implements Serializable {
@Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED)
private String password;
}

View File

@@ -0,0 +1,22 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class SpaceAddDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "空间名称", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Space name is required")
private String name;
@Schema(description = "空间类型")
private String description;
@Schema(description = "空间图标")
private String icon;
}

View File

@@ -0,0 +1,20 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class SpaceTransferDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "空间ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Space ID is required")
private Long spaceId;
@Schema(description = "目标用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Target user ID is required")
private Long targetUserId;
}

View File

@@ -0,0 +1,31 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class SpaceUpdateDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "空间ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Space ID is required")
private Long id;
@Schema(description = "空间名称")
private String name;
@Schema(description = "空间类型")
private String description;
@Schema(description = "空间图标")
private String icon;
@Schema(description = "空间是否接收来自外部的发布")
private Integer receivePublish;
@Schema(description = "空间是否开启开发功能")
private Integer allowDevelop;
}

View File

@@ -0,0 +1,24 @@
package com.xspaceagi.system.web.dto;
import com.xspaceagi.system.infra.dao.entity.SpaceUser;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class SpaceUserAddDto implements Serializable {
@Schema(description = "空间ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Space ID is required")
private Long spaceId;
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "User ID is required")
private Long userId;
@Schema(description = "用户角色", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "User role is required")
private SpaceUser.Role role;
}

View File

@@ -0,0 +1,19 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class SpaceUserDeleteDto implements Serializable {
@Schema(description = "空间ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Space ID is required")
private Long spaceId;
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "User ID is required")
private Long userId;
}

View File

@@ -0,0 +1,22 @@
package com.xspaceagi.system.web.dto;
import com.xspaceagi.system.infra.dao.entity.SpaceUser;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class SpaceUserQueryDto implements Serializable {
@Schema(description = "空间ID", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "Space ID is required")
private Long spaceId;
@Schema(description = "关键字", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String kw;
@Schema(description = "角色", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private SpaceUser.Role role;
}

View File

@@ -0,0 +1,20 @@
package com.xspaceagi.system.web.dto;
import com.xspaceagi.system.infra.dao.entity.SpaceUser;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class SpaceUserRoleUpdateDto implements Serializable {
@Schema(description = "空间ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long spaceId;
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long userId;
@Schema(description = "用户角色", requiredMode = Schema.RequiredMode.REQUIRED)
private SpaceUser.Role role;
}

View File

@@ -0,0 +1,9 @@
package com.xspaceagi.system.web.dto;
import lombok.Data;
@Data
public class SttResult {
private String text;
}

View File

@@ -0,0 +1,11 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class TicketCreateDto {
@Schema(description = "认证Token")
private String token;
}

View File

@@ -0,0 +1,33 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Schema(description = "文件上传结果")
@Data
public class UploadResultDto implements Serializable {
@Schema(description = "文件完整的网络地址")
private String url;
@Schema(description = "文件唯一标识")
private String key;
@Schema(description = "文件名称")
private String fileName;
@Schema(description = "文件类型")
private String mimeType;
@Schema(description = "文件大小")
private int size;
@Schema(description = "图片宽度")
private int width;
@Schema(description = "图片高度")
private int height;
}

View File

@@ -0,0 +1,50 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "用户访问统计响应")
public class UserAccessStatsDto {
@Schema(description = "今日访问用户数")
private Long todayUserCount;
@Schema(description = "七日访问用户数")
private Long last7DaysUserCount;
@Schema(description = "30日访问用户数")
private Long last30DaysUserCount;
@Schema(description = "七日访问趋势")
private List<TrendItem> last7DaysTrend;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "趋势数据项")
public static class TrendItem {
@Schema(description = "日期/月份")
private String date;
@Schema(description = "用户数")
private Long userCount;
}
public static TrendItem fromMap(Map<String, Object> map) {
return TrendItem.builder()
.date(map.get("date") != null ? map.get("date").toString() : map.get("month").toString())
.userCount(Long.valueOf(map.get("user_count").toString()))
.build();
}
}

View File

@@ -0,0 +1,35 @@
package com.xspaceagi.system.web.dto;
import com.xspaceagi.system.infra.dao.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserAddDto implements Serializable {
@Schema(description = "用户唯一标识,可以记录三方账号系统唯一标识")
private String uid;
@Schema(description = "手机号码")
private String phone;
@Schema(description = "密码")
private String password;
@Schema(description = "姓名")
private String userName;
@Schema(description = "手机号码")
private String nickName;
@Schema(description = "头像")
private String avatar;
@Schema(description = "邮箱")
private String email;
@Schema(description = "角色")
private User.Role role;
}

View File

@@ -0,0 +1,20 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserInfoQueryDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "用户名(用户名、邮箱、手机号三选一)")
private String userName;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号码")
private String phone;
}

View File

@@ -0,0 +1,30 @@
package com.xspaceagi.system.web.dto;
import com.xspaceagi.system.spec.enums.PeriodTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(description = "用户计量更新请求")
public class UserMetricUpdateDto {
@Schema(description = "用户ID", required = true)
private Long userId;
@Schema(description = "业务类型", required = true)
private String bizType;
@Schema(description = "时段类型", required = true)
private String periodType;
@Schema(description = "时段值", required = true)
private String period;
@Schema(description = "计量值", required = true)
private BigDecimal value;
@Schema(description = "操作类型add-增加值set-设置值", required = true)
private String operationType;
}

View File

@@ -0,0 +1,11 @@
package com.xspaceagi.system.web.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserQueryDto implements Serializable {
private String kw;
}

View File

@@ -0,0 +1,53 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "用户统计响应")
public class UserStatsDto {
@Schema(description = "用户总数")
private Long totalUserCount;
@Schema(description = "今日新增用户数")
private Long todayNewUserCount;
@Schema(description = "7日新增趋势")
private List<TrendItem> last7DaysTrend;
@Schema(description = "30日新增趋势")
private List<TrendItem> last30DaysTrend;
@Schema(description = "按月新增趋势")
private List<TrendItem> monthlyTrend;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "趋势数据项")
public static class TrendItem {
@Schema(description = "日期/月份")
private String date;
@Schema(description = "新增用户数")
private Long userCount;
}
public static TrendItem fromMap(Map<String, Object> map) {
return TrendItem.builder()
.date(map.get("date") != null ? map.get("date").toString() : map.get("month").toString())
.userCount(Long.valueOf(map.get("user_count").toString()))
.build();
}
}

View File

@@ -0,0 +1,32 @@
package com.xspaceagi.system.web.dto;
import com.xspaceagi.system.infra.dao.entity.User;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class UserUpdateDto implements Serializable {
@Schema(description = "用户名")
private String userName;
@Schema(description = "用户昵称")
private String nickName;
@Schema(description = "用户头像地址")
private String avatar;
@Schema(description = "角色")
private User.Role role;
@Schema(description = "管理员邮箱")
private String email;
@Schema(description = "手机号码")
private String phone;
@Schema(description = "密码")
private String password;
}

View File

@@ -0,0 +1,21 @@
package com.xspaceagi.system.web.dto.operatorlog;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Schema(description = "枚举选项")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EnumOptionDto {
@Schema(description = "显示文本")
private String label;
@Schema(description = "实际值")
private String value;
}

View File

@@ -0,0 +1,107 @@
package com.xspaceagi.system.web.dto.operatorlog;
import com.xspaceagi.system.infra.dao.model.OperatorLogModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* 操作日志(SysOperatorLog)实体类
*
* @author p1
* @since 2024-11-01 11:16:02
*/
@Schema(description = "操作日志")
@Getter
@Setter
public class OperatorLogDto {
/**
* 自增主键
*/
@Schema(description = "自增主键", example = "1")
private Long id;
/**
* 1:操作类型;2:访问日志
*/
@Schema(description = "操作类型1:操作类型;2:访问日志", example = "1")
private Long operateType;
/**
* 系统编码
*/
@Schema(description = "系统编码", example = "SYS001")
private String systemCode;
/**
* 系统名称
*/
@Schema(description = "系统名称", example = "系统管理")
private String systemName;
/**
* 操作对象,比如:用户表,角色表,菜单表
*/
@Schema(description = "操作对象,比如:用户表,角色表,菜单表", example = "用户表")
private String object;
/**
* 操作动作,比如:新增,删除,修改,查看
*/
@Schema(description = "操作动作,比如:新增,删除,修改,查看", example = "新增")
private String action;
/**
* 操作内容,比如评估页面
*/
@Schema(description = "操作内容,比如评估页面", example = "评估页面")
private String operateContent;
/**
* 额外的操作内容信息记录,比如:更新提交的数据内容
*/
@Schema(description = "额外的操作内容信息记录,比如:更新提交的数据内容", example = "更新了用户信息")
private String extraContent;
/**
* 操作人所属机构id
*/
@Schema(description = "操作人所属机构id", example = "1")
private Long orgId;
/**
* 操作人所属机构名称
*/
@Schema(description = "操作人所属机构名称", example = "技术部")
private String orgName;
/**
* 创建人id
*/
@Schema(description = "创建人id", example = "1")
private Long creatorId;
/**
* 创建人名称
*/
@Schema(description = "创建人名称", example = "张三")
private String creator;
/**
* 创建时间
*/
@Schema(description = "创建时间", example = "2024-11-01T11:16:03")
private LocalDateTime created;
public static OperatorLogDto convertToDto(OperatorLogModel model) {
OperatorLogDto operatorLogDto = new OperatorLogDto();
operatorLogDto.setId(model.getId());
operatorLogDto.setOperateType(model.getOperateType());
operatorLogDto.setSystemCode(model.getSystemCode());
operatorLogDto.setSystemName(model.getSystemName());
operatorLogDto.setObject(model.getObjectOp());
operatorLogDto.setAction(model.getAction());
operatorLogDto.setOperateContent(model.getOperateContent());
operatorLogDto.setExtraContent(model.getExtraContent());
operatorLogDto.setOrgId(model.getOrgId());
operatorLogDto.setOrgName(model.getOrgName());
operatorLogDto.setCreatorId(model.getCreatorId());
operatorLogDto.setCreator(model.getCreator());
operatorLogDto.setCreated(model.getCreated());
return operatorLogDto;
}
}

View File

@@ -0,0 +1,60 @@
package com.xspaceagi.system.web.dto.operatorlog;
import com.xspaceagi.system.sdk.operate.OperateTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "操作日志查询条件")
@Getter
@Setter
public class OperatorLogQueryDto implements Serializable {
@Schema(description = "系统名称")
private List<String> systemName;
@Schema(description = "系统编码")
private List<String> systemCode;
@Schema(description = "操作动作,比如:新增,删除,修改,查看")
private List<String> action;
@Schema(description = "创建人ID")
private List<Long> creatorId;
/**
* @see OperateTypeEnum
*/
@Schema(description = "操作日志类型")
private Integer operateType;
@Schema(description = "操作方式")
private String actionType;
@Schema(description = "对象名称")
private String object;
@Schema(description = "操作内容")
private String operateContent;
@Schema(description = "额外内容(请求参数)")
private String extraContent;
@Schema(description = "创建人名称")
private String creator;
@Schema(description = "创建时间-开始(毫秒时间戳)")
private Long createTimeGt;
@Schema(description = "创建时间-结束(毫秒时间戳)")
private Long createTimeLt;
@Schema(description = "创建时间范围(内部使用)")
private List<LocalDateTime> created;
}

View File

@@ -0,0 +1,103 @@
package com.xspaceagi.system.web.emoj;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
public class IconGenerator {
private static final Color[] rgbs = new Color[]{
new Color(117, 189, 108),
new Color(77, 87, 224),
new Color(63, 118, 247),
new Color(238, 193, 79),
new Color(160, 109, 237)
};
public enum IconStyle {
CLASSIC, MODERN, MINIMAL, COLORFUL
}
public enum IconFormat {
PNG, JPG, SVG, ICO
}
private Map<String, Color> colorSchemes;
public IconGenerator() {
initColorSchemes();
}
private void initColorSchemes() {
colorSchemes = new HashMap<>();
colorSchemes.put("java_blue", new Color(0, 115, 150));
colorSchemes.put("java_orange", new Color(255, 140, 0));
colorSchemes.put("dark", new Color(45, 45, 45));
colorSchemes.put("light", new Color(245, 245, 245));
}
public BufferedImage generateIcon(String text, int size) {
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
// 设置渲染质量
setupGraphics(g2d);
drawClassicIcon(g2d, text, size);
g2d.dispose();
return image;
}
public static String getFirstNonSpecialChar(String str) {
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isLetterOrDigit(c)) {
return String.valueOf(c).toUpperCase(); // 将字符转为字符串返回
}
}
return "A"; // 如果没有找到符合条件的字符,返回空字符串
}
private void setupGraphics(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
private void drawClassicIcon(Graphics2D g2d, String text, int size) {
try {
String decode = URLDecoder.decode(text, "UTF-8");
text = getFirstNonSpecialChar(decode);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
int index = Math.abs(text.hashCode()) % rgbs.length;
Color bgColor = rgbs[index];
RadialGradientPaint radialGradient = new RadialGradientPaint(
new Point2D.Double(size * 0.3, size * 0.3), // 中心点偏左上
size * 0.6f, // 半径
new float[]{0.0f, 0.6f, 1.0f},
new Color[]{
new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), 250), // 中心较亮
new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), 200), // 中等亮度
new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), 150) // 边缘透明
},
MultipleGradientPaint.CycleMethod.NO_CYCLE
);
g2d.setPaint(radialGradient);
g2d.fillRoundRect(0, 0, size, size, size / 8, size / 8);
// 绘制文字
g2d.setColor(Color.WHITE);
Font font = new Font("Arial", Font.BOLD, size * 2 / 5);
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics();
int x = (size - fm.stringWidth(text)) / 2;
int y = (size - fm.getHeight()) / 2 + fm.getAscent();
g2d.drawString(text, x, y);
}
}

View File

@@ -0,0 +1,197 @@
package com.xspaceagi.system.web.i18n;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.xspaceagi.agent.core.adapter.application.ModelApplicationService;
import com.xspaceagi.system.application.service.I18nLlmTranslator;
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
import com.xspaceagi.system.spec.exception.BizException;
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.ai.retry.NonTransientAiException;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 通过租户默认对话模型翻译({@link ModelApplicationService#queryDefaultModelConfig()}
*/
@Slf4j
@Component
public class I18nLlmTranslatorImpl implements I18nLlmTranslator {
private static final int MAX_CHUNK = 6000;
@Resource
private ModelApplicationService modelApplicationService;
@Override
public String translate(String text, String sourceLangTag, String targetLangTag) {
if (StringUtils.isBlank(text)) {
return text;
}
if (sourceLangTag != null && targetLangTag != null && sourceLangTag.equalsIgnoreCase(targetLangTag)) {
return text;
}
StringBuilder merged = new StringBuilder();
for (int start = 0; start < text.length(); start += MAX_CHUNK) {
int end = Math.min(start + MAX_CHUNK, text.length());
if (end < text.length()) {
int lineBreak = text.lastIndexOf('\n', end - 1);
if (lineBreak > start) {
end = lineBreak + 1;
}
}
String chunk = text.substring(start, end);
merged.append(translateChunk(chunk, sourceLangTag, targetLangTag));
}
return merged.toString();
}
@Override
public Map<String, String> translateBatch(Map<String, String> textByKey, String sourceLangTag, String targetLangTag) {
if (textByKey == null || textByKey.isEmpty()) {
return Map.of();
}
try {
String prompt = buildBatchPrompt(textByKey, sourceLangTag, targetLangTag);
String raw = modelApplicationService.call(prompt);
String cleaned = sanitizeModelOutput(raw);
String jsonText = extractJsonObject(cleaned);
if (!JSON.isValidObject(jsonText)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemI18nBatchTranslationNotJson,
StringUtils.abbreviate(cleaned, 200));
}
JSONObject jsonObject = JSON.parseObject(jsonText);
Map<String, String> result = new LinkedHashMap<>();
textByKey.keySet().forEach(key -> {
String v = jsonObject.getString(key);
if (StringUtils.isBlank(v)) {
String sourceText = textByKey.get(key);
if (StringUtils.isBlank(sourceText)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemI18nBatchTranslationKeyOrValueMissing, key);
}
log.warn("批量翻译缺少 key 或值为空,回退单条翻译: {}", key);
String fallback = translate(sourceText, sourceLangTag, targetLangTag);
if (StringUtils.isBlank(fallback)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemI18nBatchTranslationKeyOrValueMissing, key);
}
v = fallback;
}
result.put(key, v);
});
return result;
} catch (Exception e) {
log.warn("大模型批量翻译失败 {} -> {}", sourceLangTag, targetLangTag, e);
throw toBizException(e);
}
}
private String translateChunk(String chunk, String sourceLangTag, String targetLangTag) {
try {
String prompt = buildPrompt(chunk, sourceLangTag, targetLangTag);
String raw = modelApplicationService.call(prompt);
String cleaned = sanitizeModelOutput(raw);
if (StringUtils.isBlank(cleaned)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemI18nTranslationEmpty);
}
return cleaned;
} catch (Exception e) {
log.warn("大模型翻译失败 {} -> {}", sourceLangTag, targetLangTag, e);
throw toBizException(e);
}
}
private BizException toBizException(Exception e) {
if (e instanceof BizException bizException) {
return bizException;
}
if (e instanceof NonTransientAiException && containsAuthFailure(e)) {
return BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemI18nTranslationModelAuthFailed);
}
return BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemI18nTranslationFailedWithReason,
e.getMessage() != null ? e.getMessage() : "");
}
private boolean containsAuthFailure(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
String msg = current.getMessage();
if (StringUtils.containsIgnoreCase(msg, "401")
|| StringUtils.contains(msg, "身份验证失败")
|| StringUtils.containsIgnoreCase(msg, "authentication")) {
return true;
}
current = current.getCause();
}
return false;
}
private static String buildPrompt(String sourceText, String sourceLangTag, String targetLangTag) {
return """
You are a professional UI localization translator.
Translate the text below from language "%s" to language "%s".
Rules:
- Output ONLY the translated text. No quotes, no markdown fences, no explanations.
- Preserve placeholders and formatting exactly: %%s, %%d, {0}, {{name}}, HTML tags, etc.
- Preserve line breaks and meaningful whitespace.
Text to translate:
""".formatted(sourceLangTag, targetLangTag)
+ sourceText;
}
private static String buildBatchPrompt(Map<String, String> textByKey, String sourceLangTag, String targetLangTag) {
String json = JSON.toJSONString(textByKey);
return """
You are a professional UI localization translator.
Translate all values in the given JSON object from language "%s" to language "%s".
Keep the keys exactly unchanged.
Rules:
- Output ONLY one JSON object.
- Keep the same keys, no added/removed keys.
- Translate only values.
- Preserve placeholders and formatting exactly: %%s, %%d, {0}, {{name}}, HTML tags, etc.
Input JSON:
%s
""".formatted(sourceLangTag, targetLangTag, json);
}
private static String extractJsonObject(String text) {
if (StringUtils.isBlank(text)) {
return "";
}
int start = text.indexOf('{');
int end = text.lastIndexOf('}');
if (start >= 0 && end > start) {
return text.substring(start, end + 1);
}
return text;
}
private static String sanitizeModelOutput(String raw) {
if (raw == null) {
return "";
}
String t = raw.trim();
if (t.startsWith("```")) {
int firstNl = t.indexOf('\n');
if (firstNl > 0) {
t = t.substring(firstNl + 1);
}
int fence = t.lastIndexOf("```");
if (fence > 0) {
t = t.substring(0, fence);
}
}
return t.trim();
}
}

View File

@@ -0,0 +1,91 @@
package com.xspaceagi.system.web.service;
import com.xspaceagi.system.application.service.CategoryApplicationService;
import com.xspaceagi.system.infra.dao.entity.Category;
import com.xspaceagi.system.infra.dao.service.CategoryService;
import com.xspaceagi.system.sdk.service.CategoryApiService;
import com.xspaceagi.system.sdk.service.dto.CategoryDto;
import com.xspaceagi.system.spec.tenant.thread.TenantFunctions;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.util.List;
import java.util.stream.Collectors;
/**
* 分类API服务实现
*/
@Service
public class CategoryApiServiceImpl implements CategoryApiService {
@Resource
private CategoryApplicationService categoryApplicationService;
@Resource
private CategoryService categoryService;
@Override
public CategoryDto insert(CategoryDto categoryDto) {
Assert.notNull(categoryDto, "categoryDto不能为空");
Assert.hasText(categoryDto.getName(), "名称不能为空");
Assert.hasText(categoryDto.getCode(), "编码不能为空");
Assert.hasText(categoryDto.getType(), "类型不能为空");
Assert.notNull(categoryDto.getTenantId(), "租户ID不能为空");
Category category = new Category();
BeanUtils.copyProperties(categoryDto, category);
TenantFunctions.runWithIgnoreCheck(() -> categoryService.save(category));
return convertToDto(category);
}
@Override
public CategoryDto getById(Long id) {
Category category = categoryApplicationService.getById(id);
return convertToDto(category);
}
@Override
public List<CategoryDto> listByType(String type) {
List<Category> categories = categoryApplicationService.listByType(type);
return categories.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
@Override
public List<CategoryDto> listByTenantId(Long tenantId) {
List<Category> categories = categoryApplicationService.listByTenantId(tenantId);
return categories.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
@Override
public List<CategoryDto> listByTypeAndTenantId(String type, Long tenantId) {
Assert.hasText(type, "type cannot be null");
Assert.notNull(tenantId, "tenantId cannot be null");
List<Category> categories = TenantFunctions.callWithIgnoreCheck(() -> categoryApplicationService.listByTypeAndTenantId(type, tenantId));
return categories.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
@Override
public CategoryDto getByCode(String code) {
Category category = categoryApplicationService.getByCode(code);
return convertToDto(category);
}
/**
* 将Category实体转换为CategoryDto
*/
private CategoryDto convertToDto(Category category) {
if (category == null) {
return null;
}
CategoryDto dto = new CategoryDto();
BeanUtils.copyProperties(category, dto);
return dto;
}
}

View File

@@ -0,0 +1,22 @@
package com.xspaceagi.system.web.service;
import com.xspaceagi.system.sdk.retry.dto.RetrySubmission;
import com.xspaceagi.system.sdk.retry.server.IRetrySubmitService;
import com.xspaceagi.system.spec.dto.ReqResult;
import com.xspaceagi.system.application.service.RetryApplicationService;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
@Service
public class RetrySubmitService implements IRetrySubmitService {
@Resource
private RetryApplicationService retryApplicationService;
@Override
public ReqResult<Void> submitRetryData(RetrySubmission retrySubmission) {
retryApplicationService.submitRetryData(retrySubmission);
return ReqResult.success();
}
}

View File

@@ -0,0 +1,217 @@
package com.xspaceagi.system.web.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.xspaceagi.system.application.dto.UserDto;
import com.xspaceagi.system.application.service.UserApplicationService;
import com.xspaceagi.system.infra.dao.entity.UserAccessKey;
import com.xspaceagi.system.infra.dao.service.UserAccessKeyService;
import com.xspaceagi.system.sdk.service.UserAccessKeyApiService;
import com.xspaceagi.system.sdk.service.dto.UserAccessKeyDto;
import com.xspaceagi.system.spec.jackson.JsonSerializeUtil;
import jakarta.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class UserAccessKeyApiServiceImpl implements UserAccessKeyApiService {
@Resource
private UserAccessKeyService userAccessKeyService;
@Resource
private UserApplicationService userApplicationService;
@Override
public UserAccessKeyDto newAccessKey(Long userId, UserAccessKeyDto.AKTargetType targetType, String targetId) {
return newAccessKey(userId, targetType, targetId, "ak-" + UUID.randomUUID().toString().replace("-", ""));
}
@Override
public UserAccessKeyDto newAccessKey(Long userId, UserAccessKeyDto.AKTargetType targetType, String targetId, String accessKey) {
UserAccessKey userAccessKey = new UserAccessKey();
userAccessKey.setUserId(userId);
userAccessKey.setTargetType(targetType);
userAccessKey.setTargetId(targetId);
userAccessKey.setAccessKey(accessKey);
userAccessKey.setConfig(UserAccessKeyDto.UserAccessKeyConfig.builder().isDevMode(0).enabled(true).build());
userAccessKeyService.save(userAccessKey);
UserAccessKeyDto userAccessKeyDto = new UserAccessKeyDto();
BeanUtils.copyProperties(userAccessKey, userAccessKeyDto);
return userAccessKeyDto;
}
@Override
public UserAccessKeyDto newAccessKey(Long tenantId, Long userId, UserAccessKeyDto.AKTargetType targetType, String targetId, UserAccessKeyDto.UserAccessKeyConfig userAccessKeyConfig) {
UserAccessKey userAccessKey = new UserAccessKey();
userAccessKey.setTenantId(tenantId);
userAccessKey.setUserId(userId);
userAccessKey.setTargetType(targetType);
userAccessKey.setTargetId(targetId);
userAccessKey.setAccessKey("ak-" + UUID.randomUUID().toString().replace("-", ""));
userAccessKey.setConfig(userAccessKeyConfig);
userAccessKeyService.save(userAccessKey);
UserAccessKeyDto userAccessKeyDto = new UserAccessKeyDto();
BeanUtils.copyProperties(userAccessKey, userAccessKeyDto);
return userAccessKeyDto;
}
@Override
public void deleteAccessKey(Long userId, String accessKey) {
userAccessKeyService.remove(new QueryWrapper<UserAccessKey>().eq("access_key", accessKey).eq("user_id", userId));
}
@Override
public void deleteAccessKeyWithAgentId(Long agentId, String accessKey) {
userAccessKeyService.remove(new LambdaQueryWrapper<UserAccessKey>().eq(UserAccessKey::getAccessKey, accessKey)
.eq(UserAccessKey::getTargetType, UserAccessKeyDto.AKTargetType.Agent)
.eq(UserAccessKey::getTargetId, agentId)
);
}
@Override
public List<UserAccessKeyDto> queryAccessKeyList(Long userId, UserAccessKeyDto.AKTargetType targetType, String targetId) {
LambdaQueryWrapper<UserAccessKey> queryWrapper = new LambdaQueryWrapper<>();
if (userId != null) {
queryWrapper.eq(UserAccessKey::getUserId, userId);
}
if (targetType != null) {
queryWrapper.eq(UserAccessKey::getTargetType, targetType);
}
if (targetId != null) {
queryWrapper.eq(UserAccessKey::getTargetId, targetId);
}
List<UserAccessKey> userAccessKeys = userAccessKeyService.list(queryWrapper);
//userIds
List<Long> userIds = userAccessKeys.stream().map(UserAccessKey::getUserId).distinct().collect(Collectors.toList());
Map<Long, UserDto> userMap = new HashMap<>();
if (CollectionUtils.isNotEmpty(userIds)) {
List<UserDto> users = userApplicationService.queryUserListByIds(userIds);
users.forEach(userDto -> userMap.put(userDto.getId(), userDto));
}
return userAccessKeys.stream().map(userAccessKey -> {
UserDto userDto = userMap.get(userAccessKey.getUserId());
UserAccessKeyDto.Creator creator = null;
if (userDto != null) {
creator = new UserAccessKeyDto.Creator();
creator.setUserName(userDto.getNickName() == null ? userDto.getUserName() : userDto.getNickName());
creator.setUserId(userDto.getId());
}
return UserAccessKeyDto.builder()
.id(userAccessKey.getId())
.tenantId(userAccessKey.getTenantId())
.userId(userAccessKey.getUserId())
.targetType(userAccessKey.getTargetType())
.targetId(userAccessKey.getTargetId())
.accessKey(userAccessKey.getAccessKey())
.config(userAccessKey.getConfig())
.created(userAccessKey.getCreated())
.status(userAccessKey.getStatus())
.expire(userAccessKey.getExpire())
.name(userAccessKey.getName())
.creator(creator)
.build();
}
).collect(Collectors.toList());
}
@Override
public UserAccessKeyDto queryAccessKey(Long userId, UserAccessKeyDto.AKTargetType targetType, String targetId) {
UserAccessKey userAccessKey = userAccessKeyService.getOne(new QueryWrapper<UserAccessKey>()
.eq("user_id", userId)
.eq("target_type", targetType)
.eq("target_id", targetId), false
);
if (userAccessKey == null) {
return null;
}
return UserAccessKeyDto.builder()
.id(userAccessKey.getId())
.tenantId(userAccessKey.getTenantId())
.userId(userAccessKey.getUserId())
.targetType(userAccessKey.getTargetType())
.targetId(userAccessKey.getTargetId())
.accessKey(userAccessKey.getAccessKey())
.config(userAccessKey.getConfig())
.created(userAccessKey.getCreated())
.build();
}
@Override
public UserAccessKeyDto queryAccessKey(String accessKey) {
if (accessKey == null) {
return null;
}
UserAccessKey userAccessKey = userAccessKeyService.getOne(new QueryWrapper<UserAccessKey>().eq("access_key", accessKey));
if (userAccessKey == null) {
return null;
}
return UserAccessKeyDto.builder()
.id(userAccessKey.getId())
.tenantId(userAccessKey.getTenantId())
.name(userAccessKey.getName())
.userId(userAccessKey.getUserId())
.targetType(userAccessKey.getTargetType())
.targetId(userAccessKey.getTargetId())
.accessKey(userAccessKey.getAccessKey())
.config(userAccessKey.getConfig())
.created(userAccessKey.getCreated())
.expire(userAccessKey.getExpire())
.status(userAccessKey.getStatus())
.build();
}
@Override
public UserAccessKeyDto refreshAccessKey(Long id) {
UserAccessKey userAccessKeyUpdate = new UserAccessKey();
userAccessKeyUpdate.setId(id);
userAccessKeyUpdate.setAccessKey(UUID.randomUUID().toString().replace("-", ""));
userAccessKeyService.updateById(userAccessKeyUpdate);
UserAccessKey userAccessKey = userAccessKeyService.getById(id);
if (userAccessKey == null) {
return null;
}
return UserAccessKeyDto.builder()
.id(userAccessKey.getId())
.tenantId(userAccessKey.getTenantId())
.userId(userAccessKey.getUserId())
.targetType(userAccessKey.getTargetType())
.targetId(userAccessKey.getTargetId())
.accessKey(userAccessKey.getAccessKey())
.config(userAccessKey.getConfig())
.build();
}
@Override
public void updateAgentDevMode(Long userId, Long agentId, String accessKey, Integer devMode) {
if (devMode == null || !devMode.equals(1)) {
devMode = 0;
}
LambdaUpdateWrapper<UserAccessKey> updateWrapper = new LambdaUpdateWrapper<>();
if (userId != null) {
updateWrapper.eq(UserAccessKey::getUserId, userId);
}
updateWrapper.eq(UserAccessKey::getTargetId, agentId);
updateWrapper.eq(UserAccessKey::getTargetType, UserAccessKeyDto.AKTargetType.Agent);
updateWrapper.eq(UserAccessKey::getAccessKey, accessKey);
updateWrapper.set(UserAccessKey::getConfig, JsonSerializeUtil.toJSONStringGeneric(UserAccessKeyDto.UserAccessKeyConfig.builder().isDevMode(devMode).build()));
userAccessKeyService.update(updateWrapper);
}
@Override
public void updateUserAccessKeyConfig(Long id, UserAccessKeyDto.UserAccessKeyConfig userAccessKeyConfig) {
LambdaUpdateWrapper<UserAccessKey> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(UserAccessKey::getId, id);
updateWrapper.set(UserAccessKey::getConfig, JsonSerializeUtil.toJSONStringGeneric(userAccessKeyConfig));
userAccessKeyService.update(updateWrapper);
}
}

View File

@@ -0,0 +1,32 @@
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
<id>repack</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<!--
重新打包需要包含的jar包,这个过程会解压所有的jar包并按照jar的规则重新组合成一个jar包
这里需要包含当前这个工程 ,格式是{groupId}:{artifactId}
-->
<includes>
<!-- 包含项目自己 -->
<include>com.xspaceagi:system-web</include>
<include>com.xspaceagi:system-application</include>
<include>com.xspaceagi:system-domain</include>
<include>com.xspaceagi:system-infra</include>
<include>com.xspaceagi:system-spec</include>
</includes>
<!-- 打包结果目录:/ 表示target/ -->
<outputDirectory>/</outputDirectory>
<!-- 是否解压依赖 -->
<unpack>true</unpack>
<!-- 打包的scope -->
<!-- <scope>system</scope>-->
</dependencySet>
</dependencySets>
</assembly>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>system-web</artifactId>
<groupId>com.xspaceagi</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>system-web</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>