同步平台业务模块能力

This commit is contained in:
baiyanyun
2026-07-13 09:13:30 +08:00
parent e612f5e434
commit a1a38f62d8
224 changed files with 22331 additions and 1224 deletions

View File

@@ -142,6 +142,12 @@ public class TenantConfigDto implements Serializable {
private String mpAppId;
private String mpAppSecret;
@Schema(description = "公众号 appId微信内 H5 JSAPI / OAuth")
private String oaAppId;
@Schema(description = "公众号 secret")
private String oaAppSecret;
private String sandboxConfig;
private boolean enabledSandbox;
private Boolean supportCustomDomain;

View File

@@ -34,4 +34,6 @@ public interface AuthService {
String newTicket(UserDto userDto, String token);
String getTokenByTicket(String ticket);
String newEcoToken(String tenantClientId, String tenantSecret, UserDto user);
}

View File

@@ -31,6 +31,11 @@ public interface SpaceApplicationService {
*/
List<SpaceDto> queryListByUserId(Long userId);
/**
* 查询用户的个人空间ID
*/
Long getPersonalSpaceId(Long userId);
/**
* 查询空间用户列表
*/

View File

@@ -76,6 +76,11 @@ public interface SysGroupApplicationService {
*/
List<SysGroup> getEffectiveGroupListByUserId(Long userId);
/**
* 查询用户当前系统订阅套餐关联的用户组(不含 sys_user_group 直接绑定,仅启用状态)
*/
List<SysGroup> getSubscriptionGroupListByUserId(Long userId);
/**
* 查询用户可用于系统登录的已授权用户组(启用、授权开启、未过期)
*/
@@ -123,4 +128,3 @@ public interface SysGroupApplicationService {
}

View File

@@ -17,6 +17,7 @@ import com.xspaceagi.system.spec.utils.I18nUtil;
import com.xspaceagi.system.spec.utils.JwtUtils;
import com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -25,6 +26,7 @@ import org.springframework.util.Assert;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class AuthServiceImpl implements AuthService {
@@ -271,4 +273,18 @@ public class AuthServiceImpl implements AuthService {
}
return null;
}
@Override
public String newEcoToken(String tenantClientId, String tenantSecret, UserDto user) {
TenantConfigDto tenantConfig = (TenantConfigDto) RequestContext.get().getTenantConfig();
Map<String, String> data = Map.of(
"clientId", tenantClientId,
"clientSiteUrl", tenantConfig.getSiteUrl(),
"userId", user.getId().toString(),
"role", user.getRole().name()
);
log.info("newEcoToken: {}, userId {}, userName {}", data, user.getId(), user.getUserName());
return JwtUtils.createJwt(String.valueOf(user.getId()), "user" + user.getId(), tenantSecret, 86400, data);
}
}

View File

@@ -178,7 +178,7 @@ public class PermissionImportServiceImpl implements PermissionImportService {
}
/**
* 基于差异 JSON 导入:对差异中的新增 / 修改记录做 upsert不处理删除
* 基于差异 JSON 导入:对差异中的新增/修改记录做 upsert对 removed 记录做物理删除
*/
@SuppressWarnings("unchecked")
private void doImportDiff(Tenant tenant, String version) {
@@ -243,6 +243,16 @@ public class PermissionImportServiceImpl implements PermissionImportService {
}
}
}
List<Map<String, Object>> removedResources = (List<Map<String, Object>>) resources.get("removed");
if (CollectionUtils.isNotEmpty(removedResources)) {
for (Map<String, Object> m : removedResources) {
String code = (String) m.get("code");
if (StringUtils.isBlank(code)) continue;
sysResourceService.remove(Wrappers.<SysResource>lambdaQuery()
.eq(SysResource::getTenantId, tenantId)
.eq(SysResource::getCode, code));
}
}
}
// 2. Menu
@@ -272,6 +282,16 @@ public class PermissionImportServiceImpl implements PermissionImportService {
}
}
}
List<Map<String, Object>> removedMenus = (List<Map<String, Object>>) menus.get("removed");
if (CollectionUtils.isNotEmpty(removedMenus)) {
for (Map<String, Object> m : removedMenus) {
String code = (String) m.get("code");
if (StringUtils.isBlank(code)) continue;
sysMenuService.remove(Wrappers.<SysMenu>lambdaQuery()
.eq(SysMenu::getTenantId, tenantId)
.eq(SysMenu::getCode, code));
}
}
}
// 3. Role
@@ -301,6 +321,16 @@ public class PermissionImportServiceImpl implements PermissionImportService {
}
}
}
List<Map<String, Object>> removedRoles = (List<Map<String, Object>>) roles.get("removed");
if (CollectionUtils.isNotEmpty(removedRoles)) {
for (Map<String, Object> m : removedRoles) {
String code = (String) m.get("code");
if (StringUtils.isBlank(code)) continue;
sysRoleService.remove(Wrappers.<SysRole>lambdaQuery()
.eq(SysRole::getTenantId, tenantId)
.eq(SysRole::getCode, code));
}
}
}
// 4. Group
@@ -330,6 +360,16 @@ public class PermissionImportServiceImpl implements PermissionImportService {
}
}
}
List<Map<String, Object>> removedGroups = (List<Map<String, Object>>) groups.get("removed");
if (CollectionUtils.isNotEmpty(removedGroups)) {
for (Map<String, Object> m : removedGroups) {
String code = (String) m.get("code");
if (StringUtils.isBlank(code)) continue;
sysGroupService.remove(Wrappers.<SysGroup>lambdaQuery()
.eq(SysGroup::getTenantId, tenantId)
.eq(SysGroup::getCode, code));
}
}
}
// 5. MenuResource
@@ -363,6 +403,19 @@ public class PermissionImportServiceImpl implements PermissionImportService {
resolveOrCreateMenuResource(tenantId, menuId, resourceId, dto);
}
}
List<Map<String, Object>> removedMenuResources = (List<Map<String, Object>>) menuResources.get("removed");
if (CollectionUtils.isNotEmpty(removedMenuResources)) {
for (Map<String, Object> m : removedMenuResources) {
MenuResourceExportDto mrDto = JsonSerializeUtil.parseObject(JsonSerializeUtil.toJSONString(m), MenuResourceExportDto.class);
Long menuId = menuCodeToId.get(mrDto.getMenuCode());
Long resourceId = resourceCodeToId.get(mrDto.getResourceCode());
if (menuId == null || resourceId == null) continue;
sysMenuResourceService.remove(Wrappers.<SysMenuResource>lambdaQuery()
.eq(SysMenuResource::getTenantId, tenantId)
.eq(SysMenuResource::getMenuId, menuId)
.eq(SysMenuResource::getResourceId, resourceId));
}
}
}
// 6. RoleMenu
@@ -398,6 +451,19 @@ public class PermissionImportServiceImpl implements PermissionImportService {
resolveOrCreateRoleMenu(tenantId, roleId, menuId, dto, resourceTreeJson);
}
}
List<Map<String, Object>> removedRoleMenus = (List<Map<String, Object>>) roleMenus.get("removed");
if (CollectionUtils.isNotEmpty(removedRoleMenus)) {
for (Map<String, Object> m : removedRoleMenus) {
RoleMenuExportDto rmDto = JsonSerializeUtil.parseObject(JsonSerializeUtil.toJSONString(m), RoleMenuExportDto.class);
Long roleId = roleCodeToId.get(rmDto.getRoleCode());
Long menuId = menuCodeToId.get(rmDto.getMenuCode());
if (roleId == null || menuId == null) continue;
sysRoleMenuService.remove(Wrappers.<SysRoleMenu>lambdaQuery()
.eq(SysRoleMenu::getTenantId, tenantId)
.eq(SysRoleMenu::getRoleId, roleId)
.eq(SysRoleMenu::getMenuId, menuId));
}
}
}
// 7. GroupMenu
@@ -433,6 +499,19 @@ public class PermissionImportServiceImpl implements PermissionImportService {
resolveOrCreateGroupMenu(tenantId, groupId, menuId, dto, resourceTreeJson);
}
}
List<Map<String, Object>> removedGroupMenus = (List<Map<String, Object>>) groupMenus.get("removed");
if (CollectionUtils.isNotEmpty(removedGroupMenus)) {
for (Map<String, Object> m : removedGroupMenus) {
GroupMenuExportDto gmDto = JsonSerializeUtil.parseObject(JsonSerializeUtil.toJSONString(m), GroupMenuExportDto.class);
Long groupId = groupCodeToId.get(gmDto.getGroupCode());
Long menuId = menuCodeToId.get(gmDto.getMenuCode());
if (groupId == null || menuId == null) continue;
sysGroupMenuService.remove(Wrappers.<SysGroupMenu>lambdaQuery()
.eq(SysGroupMenu::getTenantId, tenantId)
.eq(SysGroupMenu::getGroupId, groupId)
.eq(SysGroupMenu::getMenuId, menuId));
}
}
}
// 8. DataPermission
@@ -464,6 +543,18 @@ public class PermissionImportServiceImpl implements PermissionImportService {
resolveOrCreateDataPermission(tenantId, dto, targetId);
}
}
List<Map<String, Object>> removedDataPermissions = (List<Map<String, Object>>) dataPermissions.get("removed");
if (CollectionUtils.isNotEmpty(removedDataPermissions)) {
for (Map<String, Object> m : removedDataPermissions) {
DataPermissionExportDto dpDto = JsonSerializeUtil.parseObject(JsonSerializeUtil.toJSONString(m), DataPermissionExportDto.class);
Long targetId = resolveTargetId(dpDto, roleCodeToId, groupCodeToId);
if (targetId == null) continue;
sysDataPermissionService.remove(Wrappers.<SysDataPermission>lambdaQuery()
.eq(SysDataPermission::getTenantId, tenantId)
.eq(SysDataPermission::getTargetType, dpDto.getTargetType())
.eq(SysDataPermission::getTargetId, targetId));
}
}
}
log.info("权限差异导入完成租户ID{},版本:{}", tenantId, version);

View File

@@ -23,6 +23,7 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@@ -108,17 +109,39 @@ public class SpaceApplicationServiceImpl implements SpaceApplicationService, Spa
@Override
public List<SpaceDto> queryListByUserId(Long userId) {
List<Space> spaceList = spaceDomainService.queryListByIds(spaceDomainService.querySpaceUserListByUserId(userId).stream().map(SpaceUser::getSpaceId).toList());
List<SpaceUser> spaceUsers = spaceDomainService.querySpaceUserListByUserId(userId);
Map<Long, SpaceUser> spaceUserMap = spaceUsers.stream().collect(Collectors.toMap(SpaceUser::getSpaceId, (s1) -> s1, (s1, s2) -> s1));
List<Long> ids = spaceUsers.stream().map(SpaceUser::getSpaceId).toList();
List<Space> spaceList = spaceDomainService.queryListByIds(ids);
if (spaceList != null) {
return spaceList.stream().map(space -> {
SpaceDto spaceDto = new SpaceDto();
BeanUtils.copyProperties(space, spaceDto);
if (spaceUserMap.get(spaceDto.getId()) != null) {
spaceDto.setCurrentUserRole(spaceUserMap.get(spaceDto.getId()).getRole());
}
return spaceDto;
}).collect(Collectors.toList());
}
return new ArrayList<>();
}
@Override
public Long getPersonalSpaceId(Long userId) {
Space personalSpace = null;
List<Space> spaceList = spaceDomainService.queryListByIds(spaceDomainService.querySpaceUserListByUserId(userId).stream().map(SpaceUser::getSpaceId).toList());
if (spaceList != null) {
personalSpace = spaceList.stream()
.filter(s -> s.getType() == Space.Type.Personal)
.findFirst()
.orElse(null);
}
if (personalSpace == null) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.agentUserNoPersonalSpace);
}
return personalSpace.getId();
}
@Override
public List<SpaceUserDto> querySpaceUserList(Long spaceId) {
List<SpaceUser> spaceUserList = spaceDomainService.querySpaceUserList(spaceId);

View File

@@ -144,6 +144,22 @@ public class SysGroupApplicationServiceImpl implements SysGroupApplicationServic
: groupList.stream().filter(g -> StatusEnum.isEnabled(g.getStatus())).toList();
}
@Override
public List<SysGroup> getSubscriptionGroupListByUserId(Long userId) {
if (userId == null) {
return List.of();
}
List<Long> subscriptionGroupIds = getSubscriptionPlanGroupIds(userId);
if (CollectionUtils.isEmpty(subscriptionGroupIds)) {
return List.of();
}
List<SysGroup> groups = listGroupsByIds(subscriptionGroupIds);
if (CollectionUtils.isEmpty(groups)) {
return List.of();
}
return groups.stream().filter(g -> StatusEnum.isEnabled(g.getStatus())).toList();
}
@Override
public List<SysGroup> getAuthorizedGroupListForLogin(Long userId) {
return filterAuthorizedGroupsForLogin(getEffectiveGroupListByUserId(userId));

View File

@@ -153,6 +153,14 @@ public class SysUserPermissionServiceImpl implements SysUserPermissionService {
MenuNode menuNode = menuNodes.get(0);
menuNode.setChildren(null);
// 多个角色/用户组时取最大权限ALL(1) > PART(2) > NONE(0)
Integer mergedMenuBindType = menuNodes.stream()
.map(MenuNode::getMenuBindType)
.filter(Objects::nonNull)
.max(Comparator.comparingInt(SysUserPermissionServiceImpl::menuBindTypePriority))
.orElse(BindTypeEnum.NONE.getCode());
menuNode.setMenuBindType(mergedMenuBindType);
// 合并资源(先打平再按 resourceId 分组)
List<ResourceNode> flatResources = menuNodes.stream()
.filter(n -> CollectionUtils.isNotEmpty(n.getResourceTree()))
@@ -193,6 +201,25 @@ public class SysUserPermissionServiceImpl implements SysUserPermissionService {
return result;
}
/**
* 菜单绑定类型优先级ALL(1) > PART(2) > NONE(0)
*/
private static int menuBindTypePriority(Integer bindType) {
if (bindType == null) {
return 0;
}
if (BindTypeEnum.ALL.getCode().equals(bindType)) {
return 3;
}
if (BindTypeEnum.PART.getCode().equals(bindType)) {
return 2;
}
if (BindTypeEnum.NONE.getCode().equals(bindType)) {
return 1;
}
return 0;
}
/**
* 资源绑定类型优先级ALL(1) > PART(2) > NONE(0)
*/

View File

@@ -2,6 +2,8 @@ package com.xspaceagi.system.application.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.xspaceagi.eco.market.sdk.constant.EcoMarketRegisterTaskConstant;
import com.xspaceagi.eco.market.sdk.model.ClientSecretDTO;
import com.xspaceagi.eco.market.sdk.service.IEcoMarketSecretRpcService;
import com.xspaceagi.pay.spec.gateway.PayGatewayOutboundCacheEvictSupport;
import com.xspaceagi.pay.spec.gateway.PayGatewayOutboundCacheEvictor;
@@ -13,11 +15,14 @@ import com.xspaceagi.system.infra.dao.entity.TenantConfig;
import com.xspaceagi.system.infra.dao.entity.User;
import com.xspaceagi.system.infra.dao.service.TenantConfigService;
import com.xspaceagi.system.infra.dao.service.TenantService;
import com.xspaceagi.system.sdk.service.ScheduleTaskApiService;
import com.xspaceagi.system.sdk.service.dto.ScheduleTaskDto;
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 com.xspaceagi.system.spec.utils.RedisUtil;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
@@ -30,6 +35,7 @@ import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Date;
import java.util.stream.Collectors;
@Slf4j
@@ -49,17 +55,40 @@ public class TenantConfigApplicationServiceImpl implements TenantConfigApplicati
private UserApplicationService userApplicationService;
@Resource
private IEcoMarketSecretRpcService ecoMarketSecretRpcService;
private ScheduleTaskApiService scheduleTaskApiService;
@Autowired(required = false)
private PayGatewayOutboundCacheEvictor payGatewayOutboundCacheEvictor;
@Resource
private IEcoMarketSecretRpcService iEcoMarketSecretRpcService;
@Value("${app.version:1.0.0}")
private String newVersion;
@Value("${license:}")
private String license;
@Value("${eco-market.server.enabled:false}")
private boolean ecoMarketServerEnabled;
@PostConstruct
private void startUpRegisterEco() {
if (!ecoMarketServerEnabled) {
log.info("Eco market remote server disabled, skip startup registration task");
return;
}
ClientSecretDTO secretDTO = iEcoMarketSecretRpcService.getByTenantId(1L);
if (secretDTO != null) {
return;
}
Tenant t = new Tenant();
t.setId(1L);
t.setName("Qiming AgentOS");
t.setDescription("Qiming AgentOS");
scheduleEcoMarketRegisterTask(t);
}
@Override
public List<TenantConfigItemDto> getTenantConfigList() {
String lockKey = "lock-for-check-tenant-config:" + RequestContext.get().getTenantId();
@@ -207,9 +236,36 @@ public class TenantConfigApplicationServiceImpl implements TenantConfigApplicati
RequestContext.setThreadTenantId(tenant.getId());
userApplicationService.add(userDto);
// 注册生态市场客户端,注册成功后初始化租户信息
ecoMarketSecretRpcService.registerClient(tenant.getId(), tenant.getName(), tenant.getDescription());
log.info("注册生态市场客户端成功: tenantId={}", tenant.getId());
// 注册任务
scheduleEcoMarketRegisterTask(tenant);
}
private void scheduleEcoMarketRegisterTask(Tenant tenant) {
if (!ecoMarketServerEnabled) {
log.info("Eco market remote server disabled, skip register task scheduling: tenantId={}", tenant.getId());
return;
}
try {
Map<String, Object> params = new HashMap<>();
params.put("tenantId", tenant.getId());
params.put("name", tenant.getName());
params.put("description", tenant.getDescription() != null ? tenant.getDescription() : "");
Long taskId = scheduleTaskApiService.start(ScheduleTaskDto.builder()
.tenantId(tenant.getId())
.taskId(EcoMarketRegisterTaskConstant.taskIdForTenant(tenant.getId()))
.beanId(EcoMarketRegisterTaskConstant.BEAN_ID)
.taskName("注册生态市场客户端")
.targetType("Tenant")
.targetId(String.valueOf(tenant.getId()))
.maxExecTimes(Long.MAX_VALUE)
.cron(ScheduleTaskDto.Cron.EVERY_MINUTE.getCron())
.lockTime(new Date())
.params(params)
.build());
log.info("已调度生态市场注册任务: tenantId={}, scheduleTaskId={}", tenant.getId(), taskId);
} catch (Exception e) {
log.warn("调度生态市场注册任务失败,租户已创建,可稍后通过生态市场功能触发懒注册: tenantId={}", tenant.getId(), e);
}
}
@Override

View File

@@ -24,7 +24,7 @@ public class DefaultIconUrlUtil {
}
public static String setDefaultIconUrl(String originalIcon, String name, String type) {
if (StringUtils.isBlank(name)) {
if (StringUtils.isBlank(name) || RequestContext.get() == null) {
return originalIcon;
}
//避免名字太长引起url超长

View File

@@ -0,0 +1,112 @@
[
{
"type": "System",
"side": "Mobile",
"module": "Chat",
"dataId": "-1",
"lang": "en-US",
"fieldKey": "Mobile.Chat.callLimitLabel",
"fieldValue": "Available Calls",
"remark": null
},
{
"type": "System",
"side": "Mobile",
"module": "Chat",
"dataId": "-1",
"lang": "en-US",
"fieldKey": "Mobile.Chat.subscriptionModalTitle",
"fieldValue": "Select Subscription Plan",
"remark": null
},
{
"type": "System",
"side": "PC",
"module": "Pages",
"dataId": "-1",
"lang": "en-US",
"fieldKey": "PC.Pages.MorePage.MyEarnings.confirmWithdrawDesc",
"fieldValue": "The withdrawal amount is {0} Yuan, the platform service fee is {1} Yuan, and the actual amount received is {2} Yuan.",
"remark": null
},
{
"type": "System",
"side": "Mobile",
"module": "Chat",
"dataId": "-1",
"lang": "zh-CN",
"fieldKey": "Mobile.Chat.callLimitLabel",
"fieldValue": "可调用次数",
"remark": null
},
{
"type": "System",
"side": "Mobile",
"module": "Chat",
"dataId": "-1",
"lang": "zh-CN",
"fieldKey": "Mobile.Chat.subscriptionModalTitle",
"fieldValue": "选择订阅套餐",
"remark": null
},
{
"type": "System",
"side": "Mobile",
"module": "Chat",
"dataId": "-1",
"lang": "zh-CN",
"fieldKey": "Mobile.Chat.unlimited",
"fieldValue": "不限制",
"remark": null
},
{
"type": "System",
"side": "PC",
"module": "Pages",
"dataId": "-1",
"lang": "zh-CN",
"fieldKey": "PC.Pages.MorePage.MyEarnings.confirmWithdrawDesc",
"fieldValue": "本次提现金额为 {0} 元,平台服务费 {1} 元,实际到账 {2} 元。",
"remark": null
},
{
"type": "System",
"side": "Mobile",
"module": "Chat",
"dataId": "-1",
"lang": "zh-TW",
"fieldKey": "Mobile.Chat.callLimitLabel",
"fieldValue": "可調用次數",
"remark": null
},
{
"type": "System",
"side": "Mobile",
"module": "Chat",
"dataId": "-1",
"lang": "zh-TW",
"fieldKey": "Mobile.Chat.subscriptionModalTitle",
"fieldValue": "選擇訂閱套餐",
"remark": null
},
{
"type": "System",
"side": "Mobile",
"module": "Chat",
"dataId": "-1",
"lang": "zh-TW",
"fieldKey": "Mobile.Chat.unlimited",
"fieldValue": "不限制",
"remark": null
},
{
"type": "System",
"side": "PC",
"module": "Pages",
"dataId": "-1",
"lang": "zh-TW",
"fieldKey": "PC.Pages.MorePage.MyEarnings.confirmWithdrawDesc",
"fieldValue": "本次提現金額為 {0} 元,平台服務費 {1} 元,實際到賬 {2} 元。",
"remark": null
}
]

View File

@@ -1,5 +1,5 @@
{
"version": "1.4",
"version": "1.6",
"resources": [
{
"code": "space",
@@ -225,6 +225,15 @@
"sortIndex": 25,
"status": 1
},
{
"code": "display_recommend_manage",
"name": "展示推荐模块",
"description": "展示推荐模块",
"source": 1,
"type": 1,
"sortIndex": 26,
"status": 1
},
{
"code": "category_config_query",
"name": "查询",
@@ -865,6 +874,16 @@
"sortIndex": 1,
"status": 1
},
{
"code": "display_recommend_query",
"name": "查询",
"description": "查询",
"source": 1,
"type": 2,
"parentCode": "display_recommend_manage",
"sortIndex": 1,
"status": 1
},
{
"code": "i18n_lang_query",
"name": "查询",
@@ -994,6 +1013,16 @@
"sortIndex": 2,
"status": 1
},
{
"code": "display_recommend_save",
"name": "保存",
"description": "保存",
"source": 1,
"type": 2,
"parentCode": "display_recommend_manage",
"sortIndex": 2,
"status": 1
},
{
"code": "i18n_lang_add",
"name": "新增",
@@ -1103,6 +1132,16 @@
"sortIndex": 3,
"status": 1
},
{
"code": "display_recommend_delete",
"name": "删除",
"description": "删除",
"source": 1,
"type": 2,
"parentCode": "display_recommend_manage",
"sortIndex": 3,
"status": 1
},
{
"code": "i18n_lang_modify",
"name": "编辑",
@@ -2279,7 +2318,7 @@
"name": "生态市场",
"description": "生态市场",
"source": 1,
"path": "/ecosystem/mcp",
"path": "%siteUrl%/api/eco/redirect",
"openType": 1,
"icon": "",
"sortIndex": 5,
@@ -2414,7 +2453,7 @@
"parentCode": "workspace",
"code": "component_lib_dev",
"name": "组件资源",
"description": "组件",
"description": "组件资源",
"source": 1,
"path": "/space/:spaceId/library",
"openType": 1,
@@ -2537,6 +2576,7 @@
"source": 1,
"path": "/space/:spaceId/resource-pricing",
"openType": 1,
"icon": "",
"sortIndex": 6,
"status": 1
},
@@ -2551,6 +2591,17 @@
"sortIndex": 6,
"status": 1
},
{
"parentCode": "more_page",
"code": "history_conversation",
"name": "历史会话",
"source": 1,
"path": "/more-page/history-conversation",
"openType": 1,
"icon": "",
"sortIndex": 7,
"status": 1
},
{
"parentCode": "workspace",
"code": "im_channel",
@@ -2600,9 +2651,9 @@
},
{
"parentCode": "system_manage",
"code": "permission_manage",
"name": "菜单权限",
"description": "菜单权限",
"code": "recommend_manage",
"name": "推荐管理",
"description": "推荐管理",
"source": 1,
"openType": 1,
"icon": "",
@@ -2622,24 +2673,23 @@
"status": 1
},
{
"parentCode": "workspace",
"code": "space_square",
"name": "空间广场",
"description": "空间广场",
"parentCode": "system_manage",
"code": "permission_manage",
"name": "菜单权限",
"description": "菜单权限",
"source": 1,
"path": "/space/:spaceId/space-square?activeKey=Agent",
"openType": 1,
"icon": "",
"sortIndex": 10,
"status": 1
},
{
"parentCode": "system_manage",
"code": "system_task_manage",
"name": "任务管理",
"description": "任务管理",
"parentCode": "workspace",
"code": "space_square",
"name": "空间广场",
"description": "空间广场",
"source": 1,
"path": "/system/task-manage",
"path": "/space/:spaceId/space-square?activeKey=Agent",
"openType": 1,
"icon": "",
"sortIndex": 10,
@@ -2656,6 +2706,18 @@
"sortIndex": 11,
"status": 1
},
{
"parentCode": "system_manage",
"code": "system_task_manage",
"name": "任务管理",
"description": "任务管理",
"source": 1,
"path": "/system/task-manage",
"openType": 1,
"icon": "",
"sortIndex": 11,
"status": 1
},
{
"parentCode": "system_manage",
"code": "system_log_query",
@@ -2664,7 +2726,7 @@
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 11,
"sortIndex": 12,
"status": 1
},
{
@@ -2675,7 +2737,7 @@
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 12,
"sortIndex": 13,
"status": 1
},
{
@@ -2716,11 +2778,11 @@
},
{
"parentCode": "component_lib_dev",
"code": "plugin_and_workflow",
"name": "插件与工作流",
"description": "插件与工作流",
"code": "plugin",
"name": "插件",
"description": "插件",
"source": 1,
"path": "/space/:spaceId/plugin-workflow",
"path": "/space/:spaceId/plugin",
"openType": 1,
"icon": "",
"sortIndex": 1,
@@ -2786,18 +2848,6 @@
"sortIndex": 2,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "knowledge_and_datatable",
"name": "知识与数据存储",
"description": "知识与数据存储",
"source": 1,
"path": "/space/:spaceId/knowledge-storage",
"openType": 1,
"icon": "",
"sortIndex": 2,
"status": 1
},
{
"parentCode": "pay_and_earnings",
"code": "merchant_onboarding",
@@ -2822,6 +2872,18 @@
"sortIndex": 2,
"status": 1
},
{
"parentCode": "recommend_manage",
"code": "recommend_official",
"name": "官方推荐",
"description": "官方推荐",
"source": 1,
"path": "/system/recommend-manage/official",
"openType": 1,
"icon": "",
"sortIndex": 2,
"status": 1
},
{
"parentCode": "subscription_and_points",
"code": "subscription_packages",
@@ -2858,6 +2920,18 @@
"sortIndex": 2,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "workflow",
"name": "工作流",
"description": "工作流",
"source": 1,
"path": "/space/:spaceId/workflow",
"openType": 1,
"icon": "",
"sortIndex": 2,
"status": 1
},
{
"parentCode": "content_manage",
"code": "content_page_app",
@@ -2882,6 +2956,18 @@
"sortIndex": 3,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "knowledge",
"name": "知识库",
"description": "知识库",
"source": 1,
"path": "/space/:spaceId/knowledge",
"openType": 1,
"icon": "",
"sortIndex": 3,
"status": 1
},
{
"parentCode": "permission_manage",
"code": "menu_manage",
@@ -2919,24 +3005,24 @@
"status": 1
},
{
"parentCode": "system_config",
"code": "sandbox_config",
"name": "沙盒配置",
"description": "沙盒配置",
"parentCode": "recommend_manage",
"code": "recommend_chatbox",
"name": "对话框智能体",
"description": "对话框智能体",
"source": 1,
"path": "/system/config/sandbox",
"path": "/system/recommend-manage/chatbox",
"openType": 1,
"icon": "",
"sortIndex": 3,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "space_model_manage",
"name": "模型管理",
"description": "模型管理",
"parentCode": "system_config",
"code": "sandbox_config",
"name": "沙盒配置",
"description": "沙盒配置",
"source": 1,
"path": "/space/:spaceId/model-manage",
"path": "/system/config/sandbox",
"openType": 1,
"icon": "",
"sortIndex": 3,
@@ -2966,6 +3052,18 @@
"sortIndex": 4,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "datatable",
"name": "数据表",
"description": "数据表",
"source": 1,
"path": "/space/:spaceId/storage",
"openType": 1,
"icon": "",
"sortIndex": 4,
"status": 1
},
{
"parentCode": "pay_and_earnings",
"code": "developer_earnings_stats",
@@ -3038,6 +3136,18 @@
"sortIndex": 5,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "space_model_manage",
"name": "模型管理",
"description": "模型管理",
"source": 1,
"path": "/space/:spaceId/model-manage",
"openType": 1,
"icon": "",
"sortIndex": 5,
"status": 1
},
{
"parentCode": "subscription_and_points",
"code": "business_orders",
@@ -3382,6 +3492,11 @@
"menuCode": "model_config",
"resourceCode": "model_manage",
"resourceBindType": 1
},
{
"menuCode": "recommend_manage",
"resourceCode": "display_recommend_manage",
"resourceBindType": 1
}
],
"roleMenus": [
@@ -3438,13 +3553,25 @@
},
{
"roleCode": "super_admin",
"menuCode": "plugin_and_workflow",
"menuCode": "plugin",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "knowledge_and_datatable",
"menuCode": "workflow",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "knowledge",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "datatable",
"menuBindType": 1,
"resourceTree": []
},
@@ -3875,6 +4002,29 @@
}
]
},
{
"roleCode": "super_admin",
"menuCode": "recommend_manage",
"menuBindType": 1,
"resourceTree": [
{
"code": "display_recommend_manage",
"resourceBindType": 1
}
]
},
{
"roleCode": "super_admin",
"menuCode": "recommend_official",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "recommend_chatbox",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "permission_manage",
@@ -4086,6 +4236,12 @@
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "model_permissions",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "api_key",
@@ -4094,7 +4250,7 @@
},
{
"roleCode": "super_admin",
"menuCode": "model_permissions",
"menuCode": "history_conversation",
"menuBindType": 1,
"resourceTree": []
}
@@ -4153,13 +4309,25 @@
},
{
"groupCode": "default_group",
"menuCode": "plugin_and_workflow",
"menuCode": "plugin",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "knowledge_and_datatable",
"menuCode": "workflow",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "knowledge",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "datatable",
"menuBindType": 1,
"resourceTree": []
},
@@ -4253,6 +4421,12 @@
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "eco_market",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "documents",
@@ -4312,6 +4486,12 @@
"menuCode": "api_key",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "history_conversation",
"menuBindType": 1,
"resourceTree": []
}
],
"dataPermissions": [

View File

@@ -0,0 +1,249 @@
{
"fromVersion": "1.4",
"toVersion": "1.5",
"resources": {
"added": [],
"removed": [],
"modified": []
},
"menus": {
"added": [
{
"parentCode": "component_lib_dev",
"code": "plugin",
"name": "插件",
"description": "插件",
"source": 1,
"path": "/space/:spaceId/plugin",
"openType": 1,
"icon": "",
"sortIndex": 1,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "workflow",
"name": "工作流",
"description": "工作流",
"source": 1,
"path": "/space/:spaceId/workflow",
"openType": 1,
"icon": "",
"sortIndex": 2,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "knowledge",
"name": "知识库",
"description": "知识库",
"source": 1,
"path": "/space/:spaceId/knowledge",
"openType": 1,
"icon": "",
"sortIndex": 3,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "datatable",
"name": "数据表",
"description": "数据表",
"source": 1,
"path": "/space/:spaceId/storage",
"openType": 1,
"icon": "",
"sortIndex": 4,
"status": 1
}
],
"removed": [
{
"parentCode": "component_lib_dev",
"code": "plugin_and_workflow",
"name": "插件与工作流",
"description": "插件与工作流",
"source": 1,
"path": "/space/:spaceId/plugin-workflow",
"openType": 1,
"icon": "",
"sortIndex": 1,
"status": 1
},
{
"parentCode": "component_lib_dev",
"code": "knowledge_and_datatable",
"name": "知识与数据存储",
"description": "知识与数据存储",
"source": 1,
"path": "/space/:spaceId/knowledge-storage",
"openType": 1,
"icon": "",
"sortIndex": 2,
"status": 1
}
],
"modified": [
{
"key": "component_lib_dev",
"old": {
"parentCode": "workspace",
"code": "component_lib_dev",
"name": "组件资源",
"description": "组件库",
"source": 1,
"path": "/space/:spaceId/library",
"openType": 1,
"icon": "",
"sortIndex": 3,
"status": 1
},
"new": {
"parentCode": "workspace",
"code": "component_lib_dev",
"name": "组件资源",
"description": "组件资源",
"source": 1,
"path": "/space/:spaceId/library",
"openType": 1,
"icon": "",
"sortIndex": 3,
"status": 1
}
},
{
"key": "space_model_manage",
"old": {
"parentCode": "component_lib_dev",
"code": "space_model_manage",
"name": "模型管理",
"description": "模型管理",
"source": 1,
"path": "/space/:spaceId/model-manage",
"openType": 1,
"icon": "",
"sortIndex": 3,
"status": 1
},
"new": {
"parentCode": "component_lib_dev",
"code": "space_model_manage",
"name": "模型管理",
"description": "模型管理",
"source": 1,
"path": "/space/:spaceId/model-manage",
"openType": 1,
"icon": "",
"sortIndex": 5,
"status": 1
}
}
]
},
"roles": {
"added": [],
"removed": [],
"modified": []
},
"groups": {
"added": [],
"removed": [],
"modified": []
},
"menuResources": {
"added": [],
"removed": [],
"modified": []
},
"roleMenus": {
"added": [
{
"roleCode": "super_admin",
"menuCode": "plugin",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "workflow",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "knowledge",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "datatable",
"menuBindType": 1,
"resourceTree": []
}
],
"removed": [
{
"roleCode": "super_admin",
"menuCode": "plugin_and_workflow",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "knowledge_and_datatable",
"menuBindType": 1,
"resourceTree": []
}
],
"modified": []
},
"groupMenus": {
"added": [
{
"groupCode": "default_group",
"menuCode": "plugin",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "workflow",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "knowledge",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "datatable",
"menuBindType": 1,
"resourceTree": []
}
],
"removed": [
{
"groupCode": "default_group",
"menuCode": "plugin_and_workflow",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "knowledge_and_datatable",
"menuBindType": 1,
"resourceTree": []
}
],
"modified": []
},
"dataPermissions": {
"added": [],
"removed": [],
"modified": []
}
}

View File

@@ -0,0 +1,308 @@
{
"fromVersion": "1.5",
"toVersion": "1.6",
"resources": {
"added": [
{
"code": "display_recommend_manage",
"name": "展示推荐模块",
"description": "展示推荐模块",
"source": 1,
"type": 1,
"sortIndex": 26,
"status": 1
},
{
"code": "display_recommend_query",
"name": "查询",
"description": "查询",
"source": 1,
"type": 2,
"parentCode": "display_recommend_manage",
"sortIndex": 1,
"status": 1
},
{
"code": "display_recommend_save",
"name": "保存",
"description": "保存",
"source": 1,
"type": 2,
"parentCode": "display_recommend_manage",
"sortIndex": 2,
"status": 1
},
{
"code": "display_recommend_delete",
"name": "删除",
"description": "删除",
"source": 1,
"type": 2,
"parentCode": "display_recommend_manage",
"sortIndex": 3,
"status": 1
}
],
"removed": [],
"modified": []
},
"menus": {
"added": [
{
"parentCode": "more_page",
"code": "history_conversation",
"name": "历史会话",
"source": 1,
"path": "/more-page/history-conversation",
"openType": 1,
"icon": "",
"sortIndex": 7,
"status": 1
},
{
"parentCode": "system_manage",
"code": "recommend_manage",
"name": "推荐管理",
"description": "推荐管理",
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 9,
"status": 1
},
{
"parentCode": "recommend_manage",
"code": "recommend_official",
"name": "官方推荐",
"description": "官方推荐",
"source": 1,
"path": "/system/recommend-manage/official",
"openType": 1,
"icon": "",
"sortIndex": 2,
"status": 1
},
{
"parentCode": "recommend_manage",
"code": "recommend_chatbox",
"name": "对话框智能体",
"description": "对话框智能体",
"source": 1,
"path": "/system/recommend-manage/chatbox",
"openType": 1,
"icon": "",
"sortIndex": 3,
"status": 1
}
],
"removed": [],
"modified": [
{
"key": "eco_market",
"old": {
"code": "eco_market",
"name": "生态市场",
"description": "生态市场",
"source": 1,
"path": "/ecosystem/mcp",
"openType": 1,
"icon": "",
"sortIndex": 5,
"status": 1
},
"new": {
"code": "eco_market",
"name": "生态市场",
"description": "生态市场",
"source": 1,
"path": "%siteUrl%/api/eco/redirect",
"openType": 1,
"icon": "",
"sortIndex": 5,
"status": 1
}
},
{
"key": "permission_manage",
"old": {
"parentCode": "system_manage",
"code": "permission_manage",
"name": "菜单权限",
"description": "菜单权限",
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 9,
"status": 1
},
"new": {
"parentCode": "system_manage",
"code": "permission_manage",
"name": "菜单权限",
"description": "菜单权限",
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 10,
"status": 1
}
},
{
"key": "system_task_manage",
"old": {
"parentCode": "system_manage",
"code": "system_task_manage",
"name": "任务管理",
"description": "任务管理",
"source": 1,
"path": "/system/task-manage",
"openType": 1,
"icon": "",
"sortIndex": 10,
"status": 1
},
"new": {
"parentCode": "system_manage",
"code": "system_task_manage",
"name": "任务管理",
"description": "任务管理",
"source": 1,
"path": "/system/task-manage",
"openType": 1,
"icon": "",
"sortIndex": 11,
"status": 1
}
},
{
"key": "system_log_query",
"old": {
"parentCode": "system_manage",
"code": "system_log_query",
"name": "日志查询",
"description": "日志查询",
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 11,
"status": 1
},
"new": {
"parentCode": "system_manage",
"code": "system_log_query",
"name": "日志查询",
"description": "日志查询",
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 12,
"status": 1
}
},
{
"key": "system_config",
"old": {
"parentCode": "system_manage",
"code": "system_config",
"name": "系统配置",
"description": "系统配置",
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 12,
"status": 1
},
"new": {
"parentCode": "system_manage",
"code": "system_config",
"name": "系统配置",
"description": "系统配置",
"source": 1,
"openType": 1,
"icon": "",
"sortIndex": 13,
"status": 1
}
}
]
},
"roles": {
"added": [],
"removed": [],
"modified": []
},
"groups": {
"added": [],
"removed": [],
"modified": []
},
"menuResources": {
"added": [
{
"menuCode": "recommend_manage",
"resourceCode": "display_recommend_manage",
"resourceBindType": 1
}
],
"removed": [],
"modified": []
},
"roleMenus": {
"added": [
{
"roleCode": "super_admin",
"menuCode": "recommend_manage",
"menuBindType": 1,
"resourceTree": [
{
"code": "display_recommend_manage",
"resourceBindType": 1
}
]
},
{
"roleCode": "super_admin",
"menuCode": "recommend_official",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "recommend_chatbox",
"menuBindType": 1,
"resourceTree": []
},
{
"roleCode": "super_admin",
"menuCode": "history_conversation",
"menuBindType": 1,
"resourceTree": []
}
],
"removed": [],
"modified": []
},
"groupMenus": {
"added": [
{
"groupCode": "default_group",
"menuCode": "eco_market",
"menuBindType": 1,
"resourceTree": []
},
{
"groupCode": "default_group",
"menuCode": "history_conversation",
"menuBindType": 1,
"resourceTree": []
}
],
"removed": [],
"modified": []
},
"dataPermissions": {
"added": [],
"removed": [],
"modified": []
}
}

View File

@@ -102,12 +102,66 @@ public class MenuBindResourceHelper {
if (BindTypeEnum.ALL.getCode().equals(bindType)) {
propagateMenuBindType(menuId, menuChildrenMap, menuBindTypeMap,
processedMenuIds, BindTypeEnum.ALL.getCode());
} else if (BindTypeEnum.NONE.getCode().equals(bindType)) {
propagateMenuBindType(menuId, menuChildrenMap, menuBindTypeMap,
processedMenuIds, BindTypeEnum.NONE.getCode());
}
// NONE 不向下传播子菜单可单独授权root=NONE 与显式子菜单 ALL 不应互相覆盖
}
validateRootMenuBindTypeConsistency(menuBindTypeMap);
return menuBindTypeMap;
}
/**
* 校验 root 与子菜单 bindType 一致性(保存时使用)。
*/
public static void validateRootMenuBindTypeConsistency(Map<Long, Integer> menuBindTypeMap) {
if (menuBindTypeMap == null || menuBindTypeMap.isEmpty()) {
return;
}
Integer rootType = menuBindTypeMap.get(0L);
if (rootType == null) {
return;
}
if (BindTypeEnum.ALL.getCode().equals(rootType)) {
for (Map.Entry<Long, Integer> entry : menuBindTypeMap.entrySet()) {
Long menuId = entry.getKey();
if (menuId == null || menuId == 0L) {
continue;
}
Integer bindType = entry.getValue();
if (bindType == null
|| BindTypeEnum.NONE.getCode().equals(bindType)
|| BindTypeEnum.PART.getCode().equals(bindType)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM,
BizExceptionCodeEnum.systemMenuBindRootAllRequiresAllChildren);
}
}
return;
}
if (BindTypeEnum.NONE.getCode().equals(rootType)) {
for (Map.Entry<Long, Integer> entry : menuBindTypeMap.entrySet()) {
Long menuId = entry.getKey();
if (menuId == null || menuId == 0L) {
continue;
}
Integer bindType = entry.getValue();
if (bindType != null
&& (BindTypeEnum.ALL.getCode().equals(bindType)
|| BindTypeEnum.PART.getCode().equals(bindType))) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM,
BizExceptionCodeEnum.systemMenuBindRootNoneForbidsChildGrant);
}
}
return;
}
if (BindTypeEnum.PART.getCode().equals(rootType)) {
boolean anyChildGranted = menuBindTypeMap.entrySet().stream()
.anyMatch(e -> e.getKey() != null && e.getKey() != 0L
&& e.getValue() != null
&& !BindTypeEnum.NONE.getCode().equals(e.getValue()));
if (!anyChildGranted) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM,
BizExceptionCodeEnum.systemMenuBindRootPartRequiresChildGrant);
}
}
return menuBindTypeMap;
}
/**
@@ -513,29 +567,7 @@ public class MenuBindResourceHelper {
if (menuId != null && explicitlyBoundMenuIds.contains(menuId)) {
return;
}
boolean allNone = true;
boolean allAll = true;
boolean hasNonNone = false;
for (MenuNode child : children) {
Integer bindType = child.getMenuBindType();
if (bindType == null || BindTypeEnum.NONE.getCode().equals(bindType)) {
allAll = false;
} else if (BindTypeEnum.ALL.getCode().equals(bindType)) {
hasNonNone = true;
allNone = false;
} else {
hasNonNone = true;
allNone = false;
allAll = false;
}
}
if (allNone) {
node.setMenuBindType(BindTypeEnum.NONE.getCode());
} else if (allAll && hasNonNone) {
node.setMenuBindType(BindTypeEnum.ALL.getCode());
} else {
node.setMenuBindType(BindTypeEnum.PART.getCode());
}
node.setMenuBindType(synthesizeMenuBindTypeFromMenuNodes(children));
} else {
Long menuId = node.getId();
if (menuId == null || explicitlyBoundMenuIds.contains(menuId)) {
@@ -548,17 +580,63 @@ public class MenuBindResourceHelper {
}
/**
* 针对 root 节点自上而下强制传播 ALL/NONE 到所有子菜单
* 根据子菜单 bindType 合成父菜单 bindType全 NONE→NONE全 ALL→ALL否则 PART。
*/
public static Integer synthesizeMenuBindTypeFromMenuNodes(List<MenuNode> children) {
if (CollectionUtils.isEmpty(children)) {
return BindTypeEnum.NONE.getCode();
}
boolean allNone = true;
boolean allAll = true;
boolean hasNonNone = false;
for (MenuNode child : children) {
Integer bindType = child.getMenuBindType();
if (bindType == null || BindTypeEnum.NONE.getCode().equals(bindType)) {
allAll = false;
} else if (BindTypeEnum.ALL.getCode().equals(bindType)) {
hasNonNone = true;
allNone = false;
} else {
hasNonNone = true;
allNone = false;
allAll = false;
}
}
if (allNone) {
return BindTypeEnum.NONE.getCode();
}
if (allAll && hasNonNone) {
return BindTypeEnum.ALL.getCode();
}
return BindTypeEnum.PART.getCode();
}
/**
* 校正 root bindType子菜单存在非 NONE 时root 不应为 NONE读路径处理历史脏数据
*/
public static void reconcileRootMenuBindType(MenuNode root) {
if (root == null || CollectionUtils.isEmpty(root.getChildren())) {
return;
}
Integer rootType = root.getMenuBindType();
if (rootType == null || !BindTypeEnum.NONE.getCode().equals(rootType)) {
return;
}
Integer synthesized = synthesizeMenuBindTypeFromMenuNodes(root.getChildren());
if (synthesized != null && !BindTypeEnum.NONE.getCode().equals(synthesized)) {
root.setMenuBindType(synthesized);
}
}
/**
* root 为 ALL 时自上而下传播NONE/PART 不传播,避免覆盖子菜单显式授权。
*/
public static void propagateRootMenuBindType(MenuNode root) {
if (root == null) {
return;
}
Integer bindType = root.getMenuBindType();
if (bindType == null || BindTypeEnum.PART.getCode().equals(bindType)) {
return;
}
if (!BindTypeEnum.ALL.getCode().equals(bindType) && !BindTypeEnum.NONE.getCode().equals(bindType)) {
if (!BindTypeEnum.ALL.getCode().equals(bindType)) {
return;
}
if (CollectionUtils.isNotEmpty(root.getChildren())) {

View File

@@ -741,7 +741,9 @@ public class SysGroupDomainServiceImpl implements SysGroupDomainService {
MenuBindResourceHelper.adjustMenuBindTypeBottomUp(rootNode, explicitlyBoundMenuIds);
// 如果 root 的 menuBindType 为 ALL 或 NONE则按规则将该类型自上而下强制传播到所有子菜单
MenuBindResourceHelper.reconcileRootMenuBindType(rootNode);
// root 为 ALL 时自上而下传播到所有子菜单
MenuBindResourceHelper.propagateRootMenuBindType(rootNode);
List<MenuNode> result = new ArrayList<>();

View File

@@ -831,7 +831,9 @@ public class SysRoleDomainServiceImpl implements SysRoleDomainService {
MenuBindResourceHelper.adjustMenuBindTypeBottomUp(rootNode, explicitlyBoundMenuIds);
// 如果 root 的 menuBindType 为 ALL 或 NONE则按规则将该类型自上而下强制传播到所有子菜单
MenuBindResourceHelper.reconcileRootMenuBindType(rootNode);
// root 为 ALL 时自上而下传播到所有子菜单
MenuBindResourceHelper.propagateRootMenuBindType(rootNode);
List<MenuNode> result = new ArrayList<>();

View File

@@ -100,7 +100,7 @@ public class TenantConfigServiceImpl extends ServiceImpl<TenantConfigMapper, Ten
tenantConfigDefaultList.add(TenantConfig.builder()
.name("faviconUrl")
.description("浏览器地址栏显示图标")
.value("https://agent-1251073634.cos.ap-chengdu.myqcloud.com/store/961a8939eb384eafb102008ad045f5de.ico")
.value("https://agent-statics-tc.qiming.com/store/961a8939eb384eafb102008ad045f5de.ico")
.notice("")
.placeholder("请上传浏览器地址栏显示图标")
.category(TenantConfig.ConfigCategory.BaseConfig)
@@ -609,6 +609,34 @@ public class TenantConfigServiceImpl extends ServiceImpl<TenantConfigMapper, Ten
.sort(90)
.build());
tenantConfigDefaultList.add(TenantConfig.builder()
.name("oaAppId")
.description("公众号 appId微信内 H5 JSAPI / OAuth未使用时忽略")
.value("")
.notice("公众号 appId")
.placeholder("请输入公众号 appId")
.category(TenantConfig.ConfigCategory.BaseConfig)
.inputType(TenantConfig.InputType.Input)
.dataType(TenantConfig.DataType.String)
.minHeight(100)
.required(false)
.sort(91)
.build());
tenantConfigDefaultList.add(TenantConfig.builder()
.name("oaAppSecret")
.description("公众号 secret微信内 H5 JSAPI / OAuth未使用时忽略")
.value("")
.notice("公众号 secret")
.placeholder("请输入公众号 secret")
.category(TenantConfig.ConfigCategory.BaseConfig)
.inputType(TenantConfig.InputType.Input)
.dataType(TenantConfig.DataType.String)
.minHeight(100)
.required(false)
.sort(91)
.build());
tenantConfigDefaultList.add(TenantConfig.builder()
.name("pageFooterText")
.description("登录页底部展示信息")
@@ -1094,4 +1122,4 @@ public class TenantConfigServiceImpl extends ServiceImpl<TenantConfigMapper, Ten
update(updateWrapper);
}
}
}
}

View File

@@ -13,13 +13,17 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.springframework.util.StringUtils;
@Slf4j
@Service
public class WeChatMpService {
private static final String WECHAT_MP_URL = "https://api.weixin.qq.com/cgi-bin/";
private static final String WECHAT_JSCODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
private static final String WECHAT_MP_APP_ID_KEY = "mpAppId";
private static final String WECHAT_MP_APP_SECRET_KEY = "mpAppSecret";
@@ -32,18 +36,45 @@ public class WeChatMpService {
@Resource
private RedisUtil redisUtil;
public String getAccessToken() {
List<TenantConfig> tenantConfigList = tenantConfigService.getTenantConfigList();
String mpAppId = "";
String mpAppSecret = "";
for (TenantConfig tenantConfig : tenantConfigList) {
if (WECHAT_MP_APP_ID_KEY.equals(tenantConfig.getName())) {
mpAppId = tenantConfig.getValue().toString();
}
if (WECHAT_MP_APP_SECRET_KEY.equals(tenantConfig.getName())) {
mpAppSecret = tenantConfig.getValue().toString();
}
/**
* 小程序登录码 换取 openId
*/
public String getOpenIdByLoginCode(String jsCode) {
if (!StringUtils.hasText(jsCode)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
}
MpCredentials credentials = resolveMpCredentials();
String url = WECHAT_JSCODE2SESSION_URL
+ "?appid=" + credentials.appId()
+ "&secret=" + credentials.appSecret()
+ "&js_code=" + URLEncoder.encode(jsCode.trim(), StandardCharsets.UTF_8)
+ "&grant_type=authorization_code";
String response = httpClient.get(url);
if (!JSON.isValid(response)) {
log.warn("微信 jscode2session 响应非 JSON");
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
}
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") != 0) {
log.warn("微信 jscode2session 失败 errcode={} errmsg={}", jsonObject.getInteger("errcode"), jsonObject.getString("errmsg"));
throw BizException.of(
ErrorCodeEnum.ERROR_REQUEST,
BizExceptionCodeEnum.systemWeChatApiReturnedError,
jsonObject.getString("errmsg"));
}
String openId = jsonObject.getString("openid");
if (!StringUtils.hasText(openId)) {
log.warn("微信 jscode2session 未返回 openid");
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
}
log.info("微信 jscode2session 成功 openid={}", maskOpenId(openId));
return openId;
}
public String getAccessToken() {
MpCredentials credentials = resolveMpCredentials();
String mpAppId = credentials.appId();
String mpAppSecret = credentials.appSecret();
Object accessToken = redisUtil.get("mp.accessToken");
if (accessToken != null) {
return accessToken.toString();
@@ -70,12 +101,14 @@ public class WeChatMpService {
if (JSON.isValid(response)) {
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") != 0) {
redisUtil.expire("mp.accessToken", 0);
if (jsonObject.getInteger("errcode") == 40001) {
redisUtil.expire("mp.accessToken", -1);
if (tryAgainWhenExpired) {
return getPhoneNumber(code, false);
}
}
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatApiReturnedError,
jsonObject.getString("errmsg"));
}
@@ -88,4 +121,31 @@ public class WeChatMpService {
}
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatPhoneNumberFetchFailed);
}
private MpCredentials resolveMpCredentials() {
List<TenantConfig> tenantConfigList = tenantConfigService.getTenantConfigList();
String mpAppId = "";
String mpAppSecret = "";
for (TenantConfig tenantConfig : tenantConfigList) {
if (WECHAT_MP_APP_ID_KEY.equals(tenantConfig.getName())) {
mpAppId = tenantConfig.getValue().toString();
}
if (WECHAT_MP_APP_SECRET_KEY.equals(tenantConfig.getName())) {
mpAppSecret = tenantConfig.getValue().toString();
}
}
if (!StringUtils.hasText(mpAppId) || !StringUtils.hasText(mpAppSecret)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatMpNotConfigured);
}
return new MpCredentials(mpAppId, mpAppSecret);
}
private record MpCredentials(String appId, String appSecret) {}
private static String maskOpenId(String openId) {
if (!StringUtils.hasText(openId) || openId.length() <= 8) {
return "***";
}
return openId.substring(0, 4) + "***" + openId.substring(openId.length() - 4);
}
}

View File

@@ -0,0 +1,149 @@
package com.xspaceagi.system.infra.rpc;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.xspaceagi.system.infra.dao.entity.TenantConfig;
import com.xspaceagi.system.infra.dao.service.TenantConfigService;
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 com.xspaceagi.system.spec.utils.HttpClient;
import jakarta.annotation.Resource;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** 微信公众号 OAuth 与 JSAPI 支付凭证(按租户 oaAppId / oaAppSecret。 */
@Slf4j
@Service
public class WeChatOaService {
private static final String WECHAT_OAUTH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
private static final String WECHAT_OAUTH_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
@Resource
private TenantConfigService tenantConfigService;
@Resource
private HttpClient httpClient;
/**
* 公众号网页授权 code 换取 openIdsnsapi_base
*/
public String getOpenIdByOAuthCode(String code, long tenantId) {
if (!StringUtils.hasText(code)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
}
OaCredentials credentials = resolveOaCredentials(tenantId);
String url = WECHAT_OAUTH_ACCESS_TOKEN_URL
+ "?appid=" + credentials.appId()
+ "&secret=" + credentials.appSecret()
+ "&code=" + URLEncoder.encode(code.trim(), StandardCharsets.UTF_8)
+ "&grant_type=authorization_code";
String response = httpClient.get(url);
if (!JSON.isValid(response)) {
log.warn("微信 OAuth access_token 响应非 JSON tenantId={}", tenantId);
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
}
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") != 0) {
log.warn(
"微信 OAuth access_token 失败 tenantId={} errcode={} errmsg={}",
tenantId,
jsonObject.getInteger("errcode"),
jsonObject.getString("errmsg"));
throw BizException.of(
ErrorCodeEnum.ERROR_REQUEST,
BizExceptionCodeEnum.systemWeChatApiReturnedError,
jsonObject.getString("errmsg"));
}
String openId = jsonObject.getString("openid");
if (!StringUtils.hasText(openId)) {
log.warn("微信 OAuth access_token 未返回 openid tenantId={}", tenantId);
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
}
log.info("微信 OAuth 成功 tenantId={} openid={}", tenantId, maskOpenId(openId));
return openId;
}
/** 构建 snsapi_base 授权页 URL供微信内 H5 跳转。 */
public String buildOAuthAuthorizeUrl(String redirectUri, String state, long tenantId) {
if (!StringUtils.hasText(redirectUri)) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "redirectUri cannot be blank");
}
OaCredentials credentials = resolveOaCredentials(tenantId);
String normalizedRedirectUri = redirectUri.trim();
String encodedRedirect = URLEncoder.encode(normalizedRedirectUri, StandardCharsets.UTF_8);
String stateParam = StringUtils.hasText(state) ? state.trim() : "";
String redirectUriHost = extractHost(normalizedRedirectUri);
log.info(
"build wechat oauth authorize url tenantId={} appId={} redirectUriHost={} redirectUri={} state={}",
tenantId,
credentials.appId(),
redirectUriHost,
normalizedRedirectUri,
stateParam);
return WECHAT_OAUTH_AUTHORIZE_URL
+ "?appid=" + credentials.appId()
+ "&redirect_uri=" + encodedRedirect
+ "&response_type=code"
+ "&scope=snsapi_base"
+ "&state=" + URLEncoder.encode(stateParam, StandardCharsets.UTF_8)
+ "#wechat_redirect";
}
private static String extractHost(String uriText) {
try {
URI uri = URI.create(uriText);
return StringUtils.hasText(uri.getHost()) ? uri.getHost() : "";
} catch (Exception ignored) {
return "";
}
}
public String resolveOaAppId(long tenantId) {
return resolveOaCredentials(tenantId).appId();
}
private OaCredentials resolveOaCredentials(long tenantId) {
boolean nullCtx = RequestContext.get() == null;
try {
if (nullCtx) {
RequestContext.setThreadTenantId(tenantId);
}
List<TenantConfig> tenantConfigList = tenantConfigService.getTenantConfigList();
String oaAppId = "";
String oaAppSecret = "";
for (TenantConfig tenantConfig : tenantConfigList) {
if ("oaAppId".equals(tenantConfig.getName()) && tenantConfig.getValue() != null) {
oaAppId = tenantConfig.getValue().toString();
}
if ("oaAppSecret".equals(tenantConfig.getName()) && tenantConfig.getValue() != null) {
oaAppSecret = tenantConfig.getValue().toString();
}
}
if (!StringUtils.hasText(oaAppId) || !StringUtils.hasText(oaAppSecret)) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatOaNotConfigured);
}
return new OaCredentials(oaAppId.trim(), oaAppSecret.trim());
} finally {
if (nullCtx) {
RequestContext.remove();
}
}
}
private record OaCredentials(String appId, String appSecret) {}
private static String maskOpenId(String openId) {
if (!StringUtils.hasText(openId) || openId.length() <= 8) {
return "***";
}
return openId.substring(0, 4) + "***" + openId.substring(openId.length() - 4);
}
}

View File

@@ -49,8 +49,10 @@ public class TraceContext {
private Object log;
private TokenUsage tokenUsage;
private DurationUsage durationUsage;
private Long subscriptionId;
private String apiKey;
private boolean error;
private String errorCode;
@@ -73,10 +75,19 @@ public class TraceContext {
.billUserId(billUserId)
.enableSubscription(enableSubscription)
.conversationId(conversationId)
.apiKey(apiKey)
.traceTargets(nextTraceTargets)
.build();
}
public static String maskApiKey(String apiKey) {
//apiKey只取前面4位和后4位
if (apiKey != null && apiKey.length() >= 8) {
return apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4);
}
return apiKey;
}
@Data
@AllArgsConstructor
@@ -115,4 +126,12 @@ public class TraceContext {
public long inputTokens = 0;
public long outputTokens = 0;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class DurationUsage {
public long duration = 0;
}
}

View File

@@ -66,6 +66,7 @@ public class UserAccessKeyDto {
@Schema(description = "API 授权配置")
private List<ApiConfig> apiConfigs;
private List<Long> modelIds;
private Object ext;
}
@Data

View File

@@ -0,0 +1,20 @@
package com.xspaceagi.sandbox;
import jakarta.servlet.http.HttpServletRequest;
public class SandboxUtils {
private SandboxUtils() {
}
/**
* 判断当前请求是否来自沙箱环境
*/
public static boolean isSandboxRequest(HttpServletRequest request) {
if (request == null) {
return false;
}
Object src = request.getAttribute(SandboxRequestAttributes.REQUEST_SOURCE);
return SandboxRequestAttributes.SOURCE_SANDBOX.equals(src);
}
}

View File

@@ -436,7 +436,13 @@ public enum ResourceEnum {
PAY_EARNINGS_QUERY(ResourceTypeEnum.OPERATION, "pay_earnings_query", "查询", "pay_earnings"),
PAY_EARNINGS_MODIFY(ResourceTypeEnum.OPERATION, "pay_earnings_modify", "编辑", "pay_earnings"),
MERCHANT_ONBOARDING_AUDIT(ResourceTypeEnum.OPERATION, "merchant_onboarding_audit", "进件审核", "pay_earnings"),
WITHDRAW_AUDIT(ResourceTypeEnum.OPERATION, "withdraw_audit", "提现审核", "pay_earnings");
WITHDRAW_AUDIT(ResourceTypeEnum.OPERATION, "withdraw_audit", "提现审核", "pay_earnings"),
// ================== 展示推荐管理模块 ==================
DISPLAY_RECOMMEND_MANAGE(ResourceTypeEnum.MODULE, "display_recommend_manage", "展示推荐管理模块", "root"),
DISPLAY_RECOMMEND_QUERY(ResourceTypeEnum.OPERATION, "display_recommend_query", "查询", "display_recommend_manage"),
DISPLAY_RECOMMEND_SAVE(ResourceTypeEnum.OPERATION, "display_recommend_save", "保存", "display_recommend_manage"),
DISPLAY_RECOMMEND_DELETE(ResourceTypeEnum.OPERATION, "display_recommend_delete", "删除", "display_recommend_manage");
private final ResourceTypeEnum type;
private final String code;

View File

@@ -132,6 +132,9 @@ public enum BizExceptionCodeEnum implements IBizExceptionCodeEnum {
systemMenuBindResourceBindTypeInvalid("3132", "资源ID[%s]的绑定类型[%s]无效只能是0(NONE)、1(ALL)或2(PART)", "For resource ID [%s], bind type [%s] is invalid; must be 0 (NONE), 1 (ALL), or 2 (PART).", "菜单绑定"),
systemRbacResourceCodeLengthExceeded("3133", "资源码长度不能超过100", "Resource code length cannot exceed 100.", "RBAC"),
systemGroupCannotBindForbiddenMenu("3134", "用户组不能绑定「%s」菜单", "User group cannot bind menu \"%s\".", "RBAC"),
systemMenuBindRootAllRequiresAllChildren("3280", "根节点为全部绑定时,所有子菜单必须为全部绑定", "When root is fully bound, all child menus must be fully bound.", "菜单绑定"),
systemMenuBindRootNoneForbidsChildGrant("3281", "根节点为未绑定时,子菜单不能为全部绑定或部分绑定", "When root is unbound, child menus cannot be fully or partially bound.", "菜单绑定"),
systemMenuBindRootPartRequiresChildGrant("3282", "根节点为部分绑定时,至少需要一个子菜单有权限", "When root is partially bound, at least one child menu must be granted.", "菜单绑定"),
systemVerifyCodeIncorrect("3135", "验证码错误", "Incorrect verification code.", "验证码"),
systemVerifyParamInvalid("3136", "校验参数异常", "Invalid verification parameters.", "验证码"),
systemVerifyCodeCheckFailed("3137", "验证码校验失败", "Verification code check failed.", "验证码"),
@@ -143,6 +146,10 @@ public enum BizExceptionCodeEnum implements IBizExceptionCodeEnum {
systemWeChatAccessTokenFetchFailed("3143", "获取微信 access_token 失败", "Failed to obtain WeChat access_token.", "微信"),
systemWeChatApiReturnedError("3144", "%s", "%s", "微信"),
systemWeChatPhoneNumberFetchFailed("3145", "获取微信手机号失败", "Failed to obtain WeChat phone number.", "微信"),
systemWeChatOpenIdFetchFailed("3180", "获取微信 OpenID 失败", "Failed to obtain WeChat OpenID.", "微信"),
systemWeChatMpNotConfigured("3181", "微信小程序 AppId/AppSecret 未配置", "WeChat mini program AppId/AppSecret is not configured.", "微信"),
systemAlipayMiniPayNotConfigured("3182", "支付宝小程序支付授权未配置", "Alipay mini program payment auth is not configured.", "支付宝"),
systemWeChatOaNotConfigured("3186", "微信公众号 AppId/AppSecret 未配置", "WeChat official account AppId/AppSecret is not configured.", "微信"),
systemTenantDomainAlreadyInUse("3146", "域名%s已被占用", "Domain %s is already in use.", "租户"),
systemUserIdInvalid("3147", "错误的用户ID", "Invalid user ID.", "用户"),
systemMenuPermissionImportTenantInvalid("3148", "菜单权限导入失败,租户ID无效", "Menu permission import failed: invalid tenant ID.", "权限"),
@@ -232,7 +239,7 @@ public enum BizExceptionCodeEnum implements IBizExceptionCodeEnum {
customPagePathConfigExists("3241", "指定路径的配置已存在,无法添加", "Configuration for this path already exists; cannot add.", "自定义页面"),
customPagePathConfigNotFoundForEdit("3242", "指定路径的配置不存在,无法编辑", "Configuration for this path does not exist; cannot edit.", "自定义页面"),
customPagePathConfigNotFoundForDelete("3243", "指定路径的配置不存在,无法删除", "Configuration for this path does not exist; cannot delete.", "自定义页面"),
customPageProjectConfigNotFound("3244", "项目配置不存在", "Project configuration does not exist.", "自定义页面"),
customPageProjectConfigNotFound("3244", "项目不存在", "Project does not exist.", "自定义页面"),
customPageDuplicatePathConfig("3245", "存在重复的路径配置: %s:%s", "Duplicate path configuration: %s:%s", "自定义页面"),
customPageBackendListEmpty("3246", "后端地址列表不能为空: %s:%s", "Backend address list cannot be empty: %s:%s", "自定义页面"),
customPageBackendAddressEmpty("3247", "后端地址不能为空: %s:%s", "Backend address cannot be empty: %s:%s", "自定义页面"),
@@ -415,6 +422,7 @@ public enum BizExceptionCodeEnum implements IBizExceptionCodeEnum {
agentTemplateConfigInvalid("9224", "该模版配置异常,暂时不可用", "This template configuration is invalid and temporarily unavailable.", "模版"),
agentSandboxServerStoppedOrRemoved("9225", "关联的agent服务器已停止或已删除", "The associated agent server has stopped or been removed.", "沙箱/电脑"),
agentSandboxNotFound("9226", "未找到沙盒服务器", "Sandbox server not found.", "沙箱/电脑"),
agentSandboxConfigError("9226", "沙盒服务器配置错误", "Sandbox server config error.", "沙箱/电脑"),
agentPrivateComputerDeleteForbidden("9227", "私有电脑的智能体不允许在此删除", "Agents on a private computer cannot be deleted here.", "智能体"),
agentKnowledgeIdInvalid("9228", "知识ID错误", "Invalid knowledge ID.", "参数"),
agentPageIdInvalid("9229", "页面ID错误", "Invalid page ID.", "参数"),
@@ -542,6 +550,8 @@ public enum BizExceptionCodeEnum implements IBizExceptionCodeEnum {
agentOpenapiConversationIdInvalid("9363", "conversationId 错误", "Invalid conversationId.", "OpenAPI 英文"),
agentOpenapiPluginNotFound("9364", "Plugin不存在", "Plugin does not exist.", "英文资源名"),
agentOpenapiSkillNotFound("9366", "Skill不存在", "Skill does not exist.", "英文资源名"),
agentTemplateInitFailed("9367", "项目模板初始化失败 %s", "Failed to init project template. %s", "工作空间"),
agentPackageFailed("9367", "项目打包失败 %s", "Failed to package artifact %s", "工作空间"),
/** 沙箱 / 电脑连接与状态 */
agentComputerConnectFailed("9371", "智能体电脑连接失败", "Failed to connect to agent computer.", "沙箱"),
@@ -557,10 +567,15 @@ public enum BizExceptionCodeEnum implements IBizExceptionCodeEnum {
agentWorkflowLoopMaxCountInvalid("9379", "循环节点最大循环次数不能小于等于0", "Loop max iterations must be greater than 0.", "工作流"),
agentWorkflowConditionBranchesEmpty("9380", "条件分支列表不能为空", "Condition branch list cannot be empty.", "工作流"),
workspaceExecuteCommandFailed("9400", "命令执行失败 %s", "Failed to execute command. %s", "工作空间"),
pay_order_not_found("9381", "支付订单不存在", "Payment order not found", "支付"),
pay_developer_account_not_found("9382", "开发者账户信息不存在", "Developer account not found", "支付");
pay_developer_account_not_found("9382", "开发者账户信息不存在", "Developer account not found", "支付"),
pay_h5_not_allowed_in_app("9383", "App 内请使用原生支付,不支持 H5 支付", "H5 payment is not allowed in the app; use native SDK payment", "支付"),
pay_app_native_requires_app("9384", "App 原生支付仅限 App 内调用,手机浏览器请使用 H5 支付", "App native payment requires the app client; use H5 payment in mobile browser", "支付");
/**
* 错误码

View File

@@ -13,7 +13,7 @@ public class TenantFunctions {
try {
call = callable.call();
} catch (Exception e) {
log.error("callWithIgnoreCheck error", e);
log.warn("callWithIgnoreCheck error {}", e.getMessage());
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}

View File

@@ -17,7 +17,7 @@ import java.util.regex.Pattern;
@Slf4j
public class ChatKeyCheck {
public static void check(HttpServletRequest request, UserAccessKeyApiService userAccessKeyApiService) {
public static UserAccessKeyDto check(HttpServletRequest request, UserAccessKeyApiService userAccessKeyApiService) {
String fragment = request.getHeader("Fragment");
String referer = request.getHeader("Referer");
String chatKey = null;
@@ -40,7 +40,10 @@ public class ChatKeyCheck {
throw BizException.of(HttpStatusEnum.UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED,
BizExceptionCodeEnum.systemUnauthorizedOrSessionExpired);
}
return userAccessKeyDto;
}
throw BizException.of(HttpStatusEnum.UNAUTHORIZED, ErrorCodeEnum.UNAUTHORIZED,
BizExceptionCodeEnum.systemUnauthorizedOrSessionExpired);
}
private static String extractChatKey(String fragment) {

View File

@@ -80,6 +80,9 @@ public class IconGenerateController {
if (Files.exists(path)) {
//根据key的后缀设置contentType
String contentType = Files.probeContentType(path);
if (contentType == null && key.toLowerCase().endsWith(".svg")) {
contentType = "image/svg+xml";
}
response.setContentType(contentType);
Files.copy(path, response.getOutputStream());
}

View File

@@ -1,23 +1,40 @@
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.apache.commons.lang3.StringUtils;
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")
@@ -29,9 +46,28 @@ public class NotifyMessageController {
@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;
@Value("${eco-market.server.pull-message-enabled:false}")
private boolean cloudMessagePullEnabled;
@Value("${eco-market.server.cancelHB:}")
private String cancelHB;
@Resource
private ClientSecretRpcService clientSecretRpcService;
@Operation(summary = "查询用户消息列表")
@RequestMapping(path = "/message/list", method = RequestMethod.POST)
public ReqResult<List<NotifyMessageDto>> listQuery(@RequestBody NotifyMessageQueryDto messageQueryDto) {
@@ -39,6 +75,48 @@ public class NotifyMessageController {
return ReqResult.success(notifyMessageApplicationService.queryNotifyMessageList(messageQueryDto));
}
private void pullMessage() {
if (!cloudMessagePullEnabled || StringUtils.isBlank(cloudApi)) {
return;
}
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()));
if (StringUtils.isBlank(cancelHB)) {
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() {
@@ -68,6 +146,12 @@ public class NotifyMessageController {
eventRespDto.setHasEvent(!eventDtos.isEmpty());
eventRespDto.setEventList(eventDtos);
eventRespDto.setVersion(version);
try {
// 之前的逻辑
pullMessage();
} catch (Exception e) {
log.error("触发消息拉取事件失败", e);
}
return ReqResult.success(eventRespDto);
}

View File

@@ -8,8 +8,8 @@ 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.permission.SpacePermissionService;
import com.xspaceagi.system.sdk.service.dto.UserDataPermissionDto;
import com.xspaceagi.system.spec.annotation.RequireResource;
import com.xspaceagi.system.spec.common.RequestContext;
@@ -52,6 +52,11 @@ public class SpaceController {
@Resource
private UserApplicationService userApplicationService;
private static int compare(SpaceDto o1, SpaceDto o2) {
if (o1.getType() == Space.Type.Personal) return -1;
return 1;
}
@RequireResource(SPACE_CREATE)
@Operation(summary = "创建团队空间接口")
@RequestMapping(path = "/add", method = RequestMethod.POST)
@@ -135,6 +140,8 @@ public class SpaceController {
spaceDto.setCurrentUserRole(spaceUserDto.getRole());
}
});
//个人空间排到第一个去
spaceDtoList.sort(SpaceController::compare);
I18nUtil.replaceSystemMessage(spaceDtoList);
return ReqResult.success(spaceDtoList);
}

View File

@@ -46,7 +46,6 @@ public class TenantController {
tenantConfigDto.setDomainNames(null);
tenantConfigDto.setDefaultSuggestModelId(null);
tenantConfigDto.setDefaultSummaryModelId(null);
tenantConfigDto.setRecommendAgentIds(null);
tenantConfigDto.setCodeSafeCheckPrompt(null);
tenantConfigDto.setGlobalSystemPrompt(null);
tenantConfigDto.setUserWhiteList(null);

View File

@@ -0,0 +1,112 @@
package com.xspaceagi.system.web.controller;
import com.xspaceagi.system.infra.rpc.WeChatOaService;
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.web.dto.WeChatOaOAuthUrlResponse;
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 java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
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;
@Slf4j
@RestController
@RequestMapping("/api/wechat/oa")
@Tag(name = "微信公众号 OAuth")
public class WeChatOaController {
@Resource
private WeChatOaService weChatOaService;
@GetMapping("/oauth-url")
@Operation(
summary = "获取公众号 OAuth 授权页 URL",
description = "微信内 H5 支付前跳转 snsapi_base 授权redirectUri 须为 https 完整地址。"
+ "推荐传后端固定回调地址 /api/wechat/oa/callback携带 returnUrl由后端 302 回前端页面并透传 code/state")
public ReqResult<WeChatOaOAuthUrlResponse> getOAuthAuthorizeUrl(
@RequestParam String redirectUri, @RequestParam(required = false) String state) {
Assert.hasText(redirectUri, "redirectUri cannot be blank");
String normalizedRedirectUri = redirectUri.trim();
validateHttpsUrl(normalizedRedirectUri);
Long tenantId = RequestContext.get() != null ? RequestContext.get().getTenantId() : null;
if (tenantId == null) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "tenant context required");
}
URI redirectUriObj = URI.create(normalizedRedirectUri);
log.info(
"wechat oa oauth-url requested tenantId={} redirectUriHost={} redirectUri={} state={}",
tenantId,
redirectUriObj.getHost(),
normalizedRedirectUri,
state);
String authorizeUrl = weChatOaService.buildOAuthAuthorizeUrl(normalizedRedirectUri, state, tenantId);
return ReqResult.success(new WeChatOaOAuthUrlResponse(authorizeUrl));
}
@GetMapping("/callback")
@Operation(
summary = "公众号 OAuth 固定回调中转",
description = "接收微信 OAuth 回调 code/state并 302 跳转到 returnUrl同时附加 code/state 参数")
public void callbackRedirect(
@RequestParam String returnUrl,
@RequestParam(required = false) String code,
@RequestParam(required = false) String state,
HttpServletResponse response) {
String target = appendOAuthParams(validateHttpsUrlAndReturn(returnUrl.trim()), code, state);
log.info("wechat oa callback redirect targetUrl={} codePresent={} state={}",
target,
StringUtils.hasText(code),
state);
response.setStatus(HttpServletResponse.SC_FOUND);
response.setHeader("Location", target);
}
private static String appendOAuthParams(String returnUrl, String code, String state) {
StringBuilder sb = new StringBuilder(returnUrl);
char connector = returnUrl.contains("?") ? '&' : '?';
if (StringUtils.hasText(code)) {
sb.append(connector)
.append("code=")
.append(URLEncoder.encode(code.trim(), StandardCharsets.UTF_8));
connector = '&';
}
if (StringUtils.hasText(state)) {
sb.append(connector)
.append("state=")
.append(URLEncoder.encode(state.trim(), StandardCharsets.UTF_8));
}
return sb.toString();
}
private static void validateHttpsUrl(String url) {
validateHttpsUrlAndReturn(url);
}
private static String validateHttpsUrlAndReturn(String url) {
try {
URI uri = URI.create(url);
String scheme = uri.getScheme();
// if (!"https".equalsIgnoreCase(scheme)) {
// throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "redirectUri must use https");
// }
if (!StringUtils.hasText(uri.getHost())) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid redirectUri");
}
return url;
} catch (IllegalArgumentException e) {
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "Invalid redirectUri");
}
}
}

View File

@@ -347,7 +347,7 @@ public class SysGroupController extends BaseController {
return ReqResult.error("参数不能为空");
}
checkGroupManageScope(dto.getGroupId());
// 用户组不允许绑定生态市场和系统管理整棵子树。
// 用户组不允许绑定系统管理整棵子树。
checkForbiddenMenuCodes(dto.getMenuTree(), false);
GroupBindMenuModel model = GroupBindMenuModelConverter.convertToModel(dto);
@@ -355,6 +355,26 @@ public class SysGroupController extends BaseController {
return ReqResult.success();
}
@RequireResource(USER_MANAGE_BIND_GROUP)
@Operation(summary = "查询用户订阅套餐关联的用户组列表")
@GetMapping("/list-subscription-group/{userId}")
public ReqResult<List<SysGroupDto>> getSubscriptionGroupListByUserId(@PathVariable Long userId) {
if (userId == null) {
return ReqResult.error("参数不能为空");
}
List<SysGroup> groupList = sysGroupApplicationService.getSubscriptionGroupListByUserId(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_GROUP_MANAGE_BIND_MENU)
@Operation(summary = "查询组已绑定的菜单(树形结构)")
@GetMapping("/list-menu/{groupId}")
@@ -380,10 +400,6 @@ public class SysGroupController extends BaseController {
for (MenuNodeDto node : nodes) {
boolean currentForbidden = underForbiddenMenu;
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());
}
currentForbidden = currentForbidden || node.getCode().equals(MenuEnum.SYSTEM_MANAGE.getCode());
if (currentForbidden && node.getMenuBindType() > 0) {
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemGroupCannotBindForbiddenMenu,

View File

@@ -0,0 +1,16 @@
package com.xspaceagi.system.web.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "微信公众号 OAuth 授权页 URL")
public class WeChatOaOAuthUrlResponse {
@Schema(description = "微信 OAuth 授权页完整 URL微信内浏览器 location.href 跳转)")
private String authorizeUrl;
}