同步平台业务模块能力
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user