feat(auth): return Sa-Token on login and add tenant/user list APIs

This commit is contained in:
2026-07-07 15:06:13 +08:00
parent f50a3fd0c9
commit dfce1be9b2
12 changed files with 199 additions and 49 deletions

View File

@@ -3,15 +3,19 @@ package com.sino.mci.admin.controller;
import com.sino.mci.admin.dto.CreateTenantRequest;
import com.sino.mci.admin.dto.CreateUserRequest;
import com.sino.mci.admin.dto.LoginRequest;
import com.sino.mci.admin.dto.LoginResponse;
import com.sino.mci.admin.dto.PlatformUserResponse;
import com.sino.mci.admin.dto.TokenResponse;
import com.sino.mci.admin.dto.UserResponse;
import com.sino.mci.admin.entity.Tenant;
import com.sino.mci.admin.security.AuthContext;
import com.sino.mci.admin.service.AccountService;
import com.sino.mci.shared.web.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/account")
@RequiredArgsConstructor
@@ -24,6 +28,11 @@ public class AccountController {
return ApiResponse.ok(accountService.createTenant(request));
}
@GetMapping("/tenant/list")
public ApiResponse<List<Tenant>> listTenants() {
return ApiResponse.ok(accountService.listTenants());
}
@PostMapping("/user")
public ApiResponse<UserResponse> createUser(
@RequestHeader("X-Tenant-Code") String tenantCode,
@@ -31,10 +40,20 @@ public class AccountController {
return ApiResponse.ok(accountService.createUser(tenantCode, request));
}
@GetMapping("/user/list")
public ApiResponse<List<PlatformUserResponse>> listUsers() {
return ApiResponse.ok(accountService.listUsers(AuthContext.currentTenantCode()));
}
@PostMapping("/login")
public ApiResponse<LoginResponse> login(
public ApiResponse<TokenResponse> login(
@RequestHeader("X-Tenant-Code") String tenantCode,
@Valid @RequestBody LoginRequest request) {
return ApiResponse.ok(accountService.login(tenantCode, request.getUsername(), request.getPassword()));
}
@GetMapping("/me")
public ApiResponse<AuthContext.AuthUser> me() {
return ApiResponse.ok(AuthContext.get());
}
}

View File

@@ -0,0 +1,18 @@
package com.sino.mci.admin.dto;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class PlatformUserResponse {
private Long id;
private String tenantCode;
private String username;
private String realName;
private String phone;
private Integer status;
private List<String> roleCodes;
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,13 @@
package com.sino.mci.admin.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TokenResponse {
private String token;
private LoginResponse userInfo;
}

View File

@@ -13,7 +13,10 @@ public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String path = request.getRequestURI();
if (path.contains("/api/v1/account/login")) {
String method = request.getMethod();
if (path.contains("/api/v1/account/login")
|| ("POST".equals(method) && isExactPath(path, "/api/v1/account/tenant"))
|| ("POST".equals(method) && isExactPath(path, "/api/v1/account/user"))) {
return true;
}
@@ -25,4 +28,11 @@ public class AuthInterceptor implements HandlerInterceptor {
}
return true;
}
private boolean isExactPath(String requestUri, String endpointPath) {
String contextPath = "/msg-platform";
return requestUri.equals(endpointPath)
|| requestUri.equals(contextPath + endpointPath)
|| requestUri.endsWith(endpointPath) && requestUri.length() == contextPath.length() + endpointPath.length();
}
}

View File

@@ -1,9 +1,12 @@
package com.sino.mci.admin.service;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sino.mci.admin.dto.CreateTenantRequest;
import com.sino.mci.admin.dto.CreateUserRequest;
import com.sino.mci.admin.dto.LoginResponse;
import com.sino.mci.admin.dto.PlatformUserResponse;
import com.sino.mci.admin.dto.TokenResponse;
import com.sino.mci.admin.dto.UserResponse;
import com.sino.mci.admin.entity.PlatformRole;
import com.sino.mci.admin.entity.PlatformUser;
@@ -92,7 +95,7 @@ public class AccountService {
return toUserResponse(user);
}
public LoginResponse login(String tenantCode, String username, String password) {
public TokenResponse login(String tenantCode, String username, String password) {
PlatformUser user = userMapper.selectOne(
new LambdaQueryWrapper<PlatformUser>()
.eq(PlatformUser::getTenantCode, tenantCode)
@@ -102,14 +105,30 @@ public class AccountService {
throw new UnauthorizedException("用户名或密码错误");
}
LoginResponse response = new LoginResponse();
response.setId(user.getId());
response.setTenantCode(user.getTenantCode());
response.setUsername(user.getUsername());
response.setRealName(user.getRealName());
response.setPhone(user.getPhone());
response.setRoleCodes(loadRoleCodes(user.getId()));
return response;
LoginResponse userInfo = new LoginResponse();
userInfo.setId(user.getId());
userInfo.setTenantCode(user.getTenantCode());
userInfo.setUsername(user.getUsername());
userInfo.setRealName(user.getRealName());
userInfo.setPhone(user.getPhone());
userInfo.setRoleCodes(loadRoleCodes(user.getId()));
StpUtil.login(user.getId());
StpUtil.getSession().set("tenantCode", user.getTenantCode());
StpUtil.getSession().set("username", user.getUsername());
StpUtil.getSession().set("roleCodes", userInfo.getRoleCodes());
return new TokenResponse(StpUtil.getTokenValue(), userInfo);
}
public List<Tenant> listTenants() {
return tenantMapper.selectList(null);
}
public List<PlatformUserResponse> listUsers(String tenantCode) {
List<PlatformUser> users = userMapper.selectList(
new LambdaQueryWrapper<PlatformUser>().eq(PlatformUser::getTenantCode, tenantCode));
return users.stream().map(this::toPlatformUserResponse).collect(Collectors.toList());
}
private UserResponse toUserResponse(PlatformUser user) {
@@ -127,6 +146,19 @@ public class AccountService {
return response;
}
private PlatformUserResponse toPlatformUserResponse(PlatformUser user) {
PlatformUserResponse response = new PlatformUserResponse();
response.setId(user.getId());
response.setTenantCode(user.getTenantCode());
response.setUsername(user.getUsername());
response.setRealName(user.getRealName());
response.setPhone(user.getPhone());
response.setStatus(user.getStatus());
response.setRoleCodes(loadRoleCodes(user.getId()));
response.setCreateTime(user.getCreateTime());
return response;
}
private List<String> loadRoleCodes(Long userId) {
return userRoleMapper.selectList(
new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, userId))

View File

@@ -16,6 +16,9 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/msg-platform/api/v1/**", "/api/v1/**")
.excludePathPatterns("/msg-platform/api/v1/account/login", "/api/v1/account/login");
.excludePathPatterns(
"/msg-platform/api/v1/account/login", "/api/v1/account/login",
"/msg-platform/api/v1/account/tenant", "/api/v1/account/tenant",
"/msg-platform/api/v1/account/user", "/api/v1/account/user");
}
}

View File

@@ -0,0 +1,2 @@
INSERT IGNORE INTO mci_platform_role (role_code, role_name, permissions) VALUES
('platform_admin', '平台管理员', '["tenant:manage", "user:manage", "*"]');

View File

@@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import static com.sino.mci.test.AuthTestHelper.createTenantAndLogin;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -26,17 +27,12 @@ public class ChannelControllerTest {
@Test
void shouldCreateChannelSubjectAndAccount() throws Exception {
String tenantCode = "channel-" + UUID.randomUUID().toString().substring(0, 8);
String tenantPayload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"渠道测试\"}", tenantCode);
mockMvc.perform(post("/msg-platform/api/v1/account/tenant")
.contextPath("/msg-platform")
.contentType(MediaType.APPLICATION_JSON)
.content(tenantPayload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
String authToken = createTenantAndLogin(mockMvc, tenantCode, "渠道测试");
String subjectPayload = "{\"channelType\":\"wecom\",\"subjectCode\":\"sino\",\"subjectName\":\"Sino\"}";
mockMvc.perform(post("/msg-platform/api/v1/channel-subject")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(subjectPayload))
@@ -47,6 +43,7 @@ public class ChannelControllerTest {
String accountPayload = "{\"accountType\":1,\"channelType\":\"wecom\",\"accountId\":\"ZhangSan\",\"accountName\":\"张三\"}";
mockMvc.perform(post("/msg-platform/api/v1/channel-account")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(accountPayload))

View File

@@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import static com.sino.mci.test.AuthTestHelper.createTenantAndLogin;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -26,17 +27,12 @@ public class CrmControllerTest {
@Test
void shouldCreatePersonTagAndConversation() throws Exception {
String tenantCode = "crm-" + UUID.randomUUID().toString().substring(0, 8);
String tenantPayload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"CRM测试\"}", tenantCode);
mockMvc.perform(post("/msg-platform/api/v1/account/tenant")
.contextPath("/msg-platform")
.contentType(MediaType.APPLICATION_JSON)
.content(tenantPayload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
String authToken = createTenantAndLogin(mockMvc, tenantCode, "CRM测试");
String personPayload = "{\"personCode\":\"P001\",\"personName\":\"张三\",\"phone\":\"13800138000\",\"personType\":3}";
mockMvc.perform(post("/msg-platform/api/v1/person")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(personPayload))
@@ -47,6 +43,7 @@ public class CrmControllerTest {
String tagPayload = "{\"parentId\":0,\"tagName\":\"客户\"}";
mockMvc.perform(post("/msg-platform/api/v1/tag")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(tagPayload))
@@ -57,6 +54,7 @@ public class CrmControllerTest {
String conversationPayload = "{\"conversationType\":2,\"channelType\":\"wecom\",\"channelConversationId\":\"roomid_123\",\"conversationName\":\"测试群\"}";
mockMvc.perform(post("/msg-platform/api/v1/conversation")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(conversationPayload))
@@ -66,6 +64,7 @@ public class CrmControllerTest {
mockMvc.perform(get("/msg-platform/api/v1/tag/list")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))

View File

@@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import static com.sino.mci.test.AuthTestHelper.createTenantAndLogin;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -26,17 +27,12 @@ public class InboundFlowControllerTest {
@Test
void shouldCreateFlowAndReceiveMessage() throws Exception {
String tenantCode = "flow-" + UUID.randomUUID().toString().substring(0, 8);
String tenantPayload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"流程测试\",\"inboundWebhookUrl\":\"http://localhost:9999/webhook\"}", tenantCode);
mockMvc.perform(post("/msg-platform/api/v1/account/tenant")
.contextPath("/msg-platform")
.contentType(MediaType.APPLICATION_JSON)
.content(tenantPayload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
String authToken = createTenantAndLogin(mockMvc, tenantCode, "流程测试");
String flowPayload = "{\"flowCode\":\"inbound_default\",\"flowName\":\"默认入站流程\",\"flowType\":\"inbound\",\"definitionJson\":{\"nodes\":[{\"id\":\"receive\",\"type\":\"receive\"},{\"id\":\"intent\",\"type\":\"intent\"},{\"id\":\"webhook\",\"type\":\"webhook\"}]}}";
mockMvc.perform(post("/msg-platform/api/v1/inbound/flow")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(flowPayload))
@@ -45,12 +41,14 @@ public class InboundFlowControllerTest {
mockMvc.perform(post("/msg-platform/api/v1/inbound/flow/inbound_default/publish")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
mockMvc.perform(get("/msg-platform/api/v1/inbound/flow/inbound_default")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
@@ -62,6 +60,7 @@ public class InboundFlowControllerTest {
eventId);
mockMvc.perform(post("/msg-platform/api/v1/inbound/receive")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(messagePayload))

View File

@@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import static com.sino.mci.test.AuthTestHelper.createTenantAndLogin;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -26,18 +27,12 @@ public class SendControllerTest {
@Test
void shouldTestSendToConversationAndQueryRecords() throws Exception {
String tenantCode = "send-" + UUID.randomUUID().toString().substring(0, 8);
String tenantPayload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"发送测试\"}", tenantCode);
mockMvc.perform(post("/msg-platform/api/v1/account/tenant")
.contextPath("/msg-platform")
.contentType(MediaType.APPLICATION_JSON)
.content(tenantPayload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
String authToken = createTenantAndLogin(mockMvc, tenantCode, "发送测试");
String accountPayload = "{\"accountType\":1,\"channelType\":\"wecom\",\"accountId\":\"ZhangSan\",\"accountName\":\"张三\"}";
String accountResponse = mockMvc.perform(post("/msg-platform/api/v1/channel-account")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(accountPayload))
@@ -49,6 +44,7 @@ public class SendControllerTest {
String conversationPayload = "{\"conversationType\":2,\"channelType\":\"wecom\",\"channelConversationId\":\"room-001\",\"conversationName\":\"测试群\"}";
String conversationResponse = mockMvc.perform(post("/msg-platform/api/v1/conversation")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(conversationPayload))
@@ -60,6 +56,7 @@ public class SendControllerTest {
String bindPayload = String.format("{\"accountId\":%d,\"bindingType\":1,\"sortOrder\":1}", accountId);
mockMvc.perform(post("/msg-platform/api/v1/conversation/" + conversationId + "/bind-account")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(bindPayload))
@@ -71,6 +68,7 @@ public class SendControllerTest {
conversationId);
String sendResponse = mockMvc.perform(post("/msg-platform/api/v1/send/test")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(sendPayload))
@@ -83,6 +81,7 @@ public class SendControllerTest {
mockMvc.perform(get("/msg-platform/api/v1/send/" + taskId + "/records")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
@@ -94,17 +93,12 @@ public class SendControllerTest {
@Test
void shouldRejectUnsupportedTargetType() throws Exception {
String tenantCode = "send-" + UUID.randomUUID().toString().substring(0, 8);
String tenantPayload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"发送测试\"}", tenantCode);
mockMvc.perform(post("/msg-platform/api/v1/account/tenant")
.contextPath("/msg-platform")
.contentType(MediaType.APPLICATION_JSON)
.content(tenantPayload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
String authToken = createTenantAndLogin(mockMvc, tenantCode, "发送测试");
String sendPayload = "{\"targetType\":\"rule\",\"targetValue\":{},\"contentType\":\"text\",\"content\":{\"text\":\"hello\"}}";
mockMvc.perform(post("/msg-platform/api/v1/send/test")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(sendPayload))

View File

@@ -0,0 +1,64 @@
package com.sino.mci.test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.util.UUID;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public class AuthTestHelper {
public static final String ADMIN_USERNAME = "admin";
public static final String ADMIN_PASSWORD = "admin123";
public static String createTenantAndLogin(MockMvc mockMvc) throws Exception {
String tenantCode = "test-" + UUID.randomUUID().toString().substring(0, 8);
return createTenantAndLogin(mockMvc, tenantCode, "测试租户");
}
public static String createTenantAndLogin(MockMvc mockMvc, String tenantCode, String tenantName) throws Exception {
createTenant(mockMvc, tenantCode, tenantName);
createAdminUser(mockMvc, tenantCode);
return login(mockMvc, tenantCode);
}
public static void createTenant(MockMvc mockMvc, String tenantCode, String tenantName) throws Exception {
String payload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"%s\"}", tenantCode, tenantName);
mockMvc.perform(post("/msg-platform/api/v1/account/tenant")
.contextPath("/msg-platform")
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
}
public static void createAdminUser(MockMvc mockMvc, String tenantCode) throws Exception {
String payload = String.format(
"{\"username\":\"%s\",\"password\":\"%s\",\"realName\":\"管理员\",\"phone\":\"13800138000\",\"roleCodes\":[\"platform_admin\"]}",
ADMIN_USERNAME, ADMIN_PASSWORD);
mockMvc.perform(post("/msg-platform/api/v1/account/user")
.contextPath("/msg-platform")
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
}
public static String login(MockMvc mockMvc, String tenantCode) throws Exception {
String payload = String.format("{\"username\":\"%s\",\"password\":\"%s\"}", ADMIN_USERNAME, ADMIN_PASSWORD);
MvcResult result = mockMvc.perform(post("/msg-platform/api/v1/account/login")
.contextPath("/msg-platform")
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andReturn();
return "Bearer " + com.jayway.jsonpath.JsonPath.read(result.getResponse().getContentAsString(), "$.data.token");
}
}