diff --git a/backend/mci-server/pom.xml b/backend/mci-server/pom.xml
index f0eb930..8b8b8e4 100644
--- a/backend/mci-server/pom.xml
+++ b/backend/mci-server/pom.xml
@@ -36,6 +36,11 @@
com.baomidou
mybatis-plus-boot-starter
+
+ org.mybatis
+ mybatis-spring
+ 3.0.4
+
com.mysql
mysql-connector-j
@@ -58,6 +63,12 @@
spring-boot-starter-test
test
+
+ org.springframework.boot
+ spring-boot-webmvc-test
+ 4.1.0
+ test
+
diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java b/backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java
new file mode 100644
index 0000000..4e51ff6
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java
@@ -0,0 +1,37 @@
+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.service.AccountService;
+import com.sino.mci.shared.web.ApiResponse;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/v1/account")
+@RequiredArgsConstructor
+public class AccountController {
+
+ private final AccountService accountService;
+
+ @PostMapping("/tenant")
+ public ApiResponse> createTenant(@Valid @RequestBody CreateTenantRequest request) {
+ return ApiResponse.ok(accountService.createTenant(request));
+ }
+
+ @PostMapping("/user")
+ public ApiResponse> createUser(
+ @RequestHeader("X-Tenant-Code") String tenantCode,
+ @Valid @RequestBody CreateUserRequest request) {
+ return ApiResponse.ok(accountService.createUser(tenantCode, request));
+ }
+
+ @PostMapping("/login")
+ public ApiResponse> login(
+ @RequestHeader("X-Tenant-Code") String tenantCode,
+ @Valid @RequestBody LoginRequest request) {
+ return ApiResponse.ok(accountService.login(tenantCode, request.getUsername(), request.getPassword()));
+ }
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/dto/CreateTenantRequest.java b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/CreateTenantRequest.java
new file mode 100644
index 0000000..a3cf092
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/CreateTenantRequest.java
@@ -0,0 +1,16 @@
+package com.sino.mci.admin.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class CreateTenantRequest {
+
+ @NotBlank
+ private String tenantCode;
+
+ @NotBlank
+ private String tenantName;
+
+ private String inboundWebhookUrl;
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/dto/CreateUserRequest.java b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/CreateUserRequest.java
new file mode 100644
index 0000000..1ed3340
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/CreateUserRequest.java
@@ -0,0 +1,20 @@
+package com.sino.mci.admin.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CreateUserRequest {
+
+ @NotBlank
+ private String username;
+
+ @NotBlank
+ private String password;
+
+ private String realName;
+ private String phone;
+ private List roleCodes;
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginRequest.java b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginRequest.java
new file mode 100644
index 0000000..e0c20dd
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginRequest.java
@@ -0,0 +1,14 @@
+package com.sino.mci.admin.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class LoginRequest {
+
+ @NotBlank
+ private String username;
+
+ @NotBlank
+ private String password;
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java b/backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java
new file mode 100644
index 0000000..d50154f
--- /dev/null
+++ b/backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java
@@ -0,0 +1,79 @@
+package com.sino.mci.admin.service;
+
+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.entity.PlatformRole;
+import com.sino.mci.admin.entity.PlatformUser;
+import com.sino.mci.admin.entity.Tenant;
+import com.sino.mci.admin.entity.UserRole;
+import com.sino.mci.admin.repository.PlatformRoleMapper;
+import com.sino.mci.admin.repository.PlatformUserMapper;
+import com.sino.mci.admin.repository.TenantMapper;
+import com.sino.mci.admin.repository.UserRoleMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.crypto.bcrypt.BCrypt;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class AccountService {
+
+ private final TenantMapper tenantMapper;
+ private final PlatformUserMapper userMapper;
+ private final PlatformRoleMapper roleMapper;
+ private final UserRoleMapper userRoleMapper;
+
+ @Transactional
+ public Tenant createTenant(CreateTenantRequest request) {
+ Tenant tenant = new Tenant();
+ tenant.setTenantCode(request.getTenantCode());
+ tenant.setTenantName(request.getTenantName());
+ tenant.setInboundWebhookUrl(request.getInboundWebhookUrl());
+ tenant.setStatus(1);
+ tenantMapper.insert(tenant);
+ return tenant;
+ }
+
+ @Transactional
+ public PlatformUser createUser(String tenantCode, CreateUserRequest request) {
+ PlatformUser user = new PlatformUser();
+ user.setTenantCode(tenantCode);
+ user.setUsername(request.getUsername());
+ user.setPassword(BCrypt.hashpw(request.getPassword(), BCrypt.gensalt()));
+ user.setRealName(request.getRealName());
+ user.setPhone(request.getPhone());
+ user.setStatus(1);
+ userMapper.insert(user);
+
+ if (!CollectionUtils.isEmpty(request.getRoleCodes())) {
+ for (String roleCode : request.getRoleCodes()) {
+ PlatformRole role = roleMapper.selectOne(
+ new LambdaQueryWrapper().eq(PlatformRole::getRoleCode, roleCode));
+ if (role != null) {
+ UserRole userRole = new UserRole();
+ userRole.setUserId(user.getId());
+ userRole.setRoleId(role.getId());
+ userRoleMapper.insert(userRole);
+ }
+ }
+ }
+ return user;
+ }
+
+ public PlatformUser login(String tenantCode, String username, String password) {
+ PlatformUser user = userMapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(PlatformUser::getTenantCode, tenantCode)
+ .eq(PlatformUser::getUsername, username)
+ .eq(PlatformUser::getStatus, 1));
+ if (user == null || !BCrypt.checkpw(password, user.getPassword())) {
+ throw new RuntimeException("用户名或密码错误");
+ }
+ return user;
+ }
+}
diff --git a/backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusConfig.java b/backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusConfig.java
index a8ee337..100f710 100644
--- a/backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusConfig.java
+++ b/backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusConfig.java
@@ -1,9 +1,28 @@
package com.sino.mci.config;
+import com.baomidou.mybatisplus.core.config.GlobalConfig;
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import javax.sql.DataSource;
+
@Configuration
@MapperScan("com.sino.mci.admin.repository")
public class MybatisPlusConfig {
+
+ @Bean
+ public SqlSessionFactory sqlSessionFactory(DataSource dataSource, MetaObjectHandler metaObjectHandler) throws Exception {
+ MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
+ factoryBean.setDataSource(dataSource);
+
+ GlobalConfig globalConfig = new GlobalConfig();
+ globalConfig.setMetaObjectHandler(metaObjectHandler);
+ factoryBean.setGlobalConfig(globalConfig);
+
+ return factoryBean.getObject();
+ }
}
diff --git a/backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java b/backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java
new file mode 100644
index 0000000..ec7e7ea
--- /dev/null
+++ b/backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java
@@ -0,0 +1,35 @@
+package com.sino.mci.admin.controller;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+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;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class AccountControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void shouldCreateTenant() throws Exception {
+ String tenantCode = "test-" + UUID.randomUUID().toString().substring(0, 8);
+ String payload = String.format("{\"tenantCode\":\"%s\",\"tenantName\":\"测试租户\"}", tenantCode);
+ 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))
+ .andExpect(jsonPath("$.data.tenantCode").value(tenantCode));
+ }
+}
diff --git a/backend/mci-server/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/AutoConfigureMockMvc.java b/backend/mci-server/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/AutoConfigureMockMvc.java
new file mode 100644
index 0000000..b4efd8c
--- /dev/null
+++ b/backend/mci-server/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/AutoConfigureMockMvc.java
@@ -0,0 +1,22 @@
+package org.springframework.boot.test.autoconfigure.web.servlet;
+
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@ImportAutoConfiguration({
+ org.springframework.boot.webmvc.test.autoconfigure.MockMvcAutoConfiguration.class,
+ org.springframework.boot.webmvc.test.autoconfigure.MockMvcWebClientAutoConfiguration.class,
+ org.springframework.boot.webmvc.test.autoconfigure.MockMvcWebDriverAutoConfiguration.class
+})
+public @interface AutoConfigureMockMvc {
+}