- admin-web/api/client.ts: 请求拦截器仅在未显式传入 X-Tenant-Code 时使用 localStorage 默认值 - admin-web/api/account.ts: login 显式携带租户编码请求头,解决浏览器登录 401 - db/migration/V10: 新增 tenant_admin 平台角色,供外部 API 测试创建租户管理员 - scripts/test-external-api.sh: 创建租户后自动创建管理员并登录,后续接口使用 Bearer Token - docs/superpowers/plans: 提交总体规划与 Task 12 测试计划
1316 lines
41 KiB
Markdown
1316 lines
41 KiB
Markdown
# sino-mci 消息与客户互动中台实施计划(总体规划)
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 在 `sa-server` 平级目录下新建独立项目 `sino-mci`,按 1851 首期需求搭建消息中台,支持渠道主体/账号管理、会话/标签管理、入站流程编排、发送记录与出站发送、Python pywechat 渠道服务及配套管理后台。
|
||
|
||
**Architecture:** 采用单仓库 monorepo,Java 21 + Spring Boot 4.1.x 主中台,Vue 3 + Element Plus 管理后台,Python 3.11 + FastAPI 渠道服务;数据用 MySQL 8 + Flyway 迁移,缓存/队列用 Redis + RabbitMQ,媒体文件用 SeaweedFS。
|
||
|
||
**Tech Stack:** Java 21, Spring Boot 4.1.x, Maven 4, Flyway, MySQL 8, Redis 7, RabbitMQ, Vue 3, Element Plus, Vue Flow, Python 3.11, FastAPI, pywechat, SeaweedFS, Docker.
|
||
|
||
---
|
||
|
||
## 范围与阶段划分
|
||
|
||
由于项目覆盖多个独立子系统,本实施计划拆分为 6 个阶段,每个阶段产出可独立运行的软件单元:
|
||
|
||
| 阶段 | 目标 | 可独立验证的产出 |
|
||
|------|------|----------------|
|
||
| **Phase 1** | 项目基础骨架 + 数据库 + 账号权限体系 | `mci-server` 可启动,账号 API 可调用,数据库表自动创建 |
|
||
| **Phase 2** | 渠道主体 + 企微渠道适配 + 渠道账号初始化 | 企微回调可接收,渠道账号可初始化,会话存档能力可配置 |
|
||
| **Phase 3** | 联系人 + 渠道身份 + 会话/群聊 + 标签 | 人员/会话/标签 CRUD 完整,群聊同步可用 |
|
||
| **Phase 4** | 入站流程编排 + LLM 意图识别 + 入站 webhook 推送 | 入站消息按流程节点执行,可推送到业务系统 webhook |
|
||
| **Phase 5** | 出站发送 + 发送记录 + 策略执行 + 测试发送 | 业务系统可调用发送 API,发送记录可查询,测试发送可用 |
|
||
| **Phase 6** | Python 渠道服务 + pywechat 个人号接入 | pywechat 可登录、发消息、心跳上报 |
|
||
| **Phase 7** | Vue 管理后台 + 基础设施部署 | 前端页面可访问,docker-compose 一键启动 |
|
||
|
||
各阶段依赖:
|
||
- Phase 2 依赖 Phase 1
|
||
- Phase 3 依赖 Phase 1
|
||
- Phase 4 依赖 Phase 2 + Phase 3
|
||
- Phase 5 依赖 Phase 1 + Phase 3
|
||
- Phase 6 依赖 Phase 1(与 Phase 2-5 可并行开发)
|
||
- Phase 7 前端依赖 Phase 1-6 的 API;基础设施贯穿始终
|
||
|
||
---
|
||
|
||
## 项目根目录约定
|
||
|
||
所有路径相对于:
|
||
|
||
```
|
||
/Users/marsal/Projects/sino-project/sino-mci/
|
||
```
|
||
|
||
目录结构见设计文档第六节:`docs/architecture/2026-07-06-message-customer-interaction-design.md`。
|
||
|
||
---
|
||
|
||
## Phase 1: 项目基础骨架 + 数据库 + 账号权限体系
|
||
|
||
### Phase 1 目标
|
||
|
||
完成可运行的 `mci-server`,具备:
|
||
1. Maven 4 多模块骨架;
|
||
2. Spring Boot 4.1.x 主服务;
|
||
3. MySQL + Redis + RabbitMQ 连接配置;
|
||
4. Flyway 数据库迁移;
|
||
5. 账号权限模型与 API(租户、用户、角色、登录)。
|
||
|
||
### Phase 1 文件清单
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `backend/pom.xml` | Maven 4 parent POM |
|
||
| `backend/mci-server/pom.xml` | 主服务模块 POM |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/MciServerApplication.java` | Spring Boot 启动类 |
|
||
| `backend/mci-server/src/main/resources/application.yml` | 主配置 |
|
||
| `backend/mci-server/src/main/resources/application-dev.yml` | 本地开发配置 |
|
||
| `backend/mci-server/src/main/resources/db/migration/V1__init_account.sql` | 账号权限初始化表 |
|
||
| `backend/mci-server/src/main/resources/db/migration/V2__seed_roles.sql` | 初始角色数据 |
|
||
| `backend/mci-shared/pom.xml` | 公共模块 POM |
|
||
| `backend/mci-shared/src/main/java/com/sino/mci/shared/entity/BaseEntity.java` | 公共实体基类 |
|
||
| `backend/mci-shared/src/main/java/com/sino/mci/shared/web/ApiResponse.java` | 统一响应包装 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/admin/entity/Tenant.java` | 租户实体 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/admin/entity/PlatformUser.java` | 用户实体 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/admin/entity/PlatformRole.java` | 角色实体 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/admin/entity/UserRole.java` | 用户角色关系实体 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/admin/repository/*` | MyBatis Plus Mapper 接口 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/admin/service/*` | 业务服务 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java` | 账号 API |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusConfig.java` | MyBatis Plus 配置 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/config/RedisConfig.java` | Redis 配置 |
|
||
| `backend/mci-server/src/main/java/com/sino/mci/config/RabbitMqConfig.java` | RabbitMQ 配置 |
|
||
| `infrastructure/docker-compose.yml` | 本地基础设施 |
|
||
| `scripts/local-up.sh` | 一键启动本地环境 |
|
||
|
||
---
|
||
|
||
### Task 1.1: 创建 Maven 4 parent POM
|
||
|
||
**Files:**
|
||
- Create: `backend/pom.xml`
|
||
|
||
- [ ] **Step 1: 创建 parent POM**
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||
<modelVersion>4.0.0</modelVersion>
|
||
|
||
<parent>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-parent</artifactId>
|
||
<version>4.1.0</version>
|
||
<relativePath/>
|
||
</parent>
|
||
|
||
<groupId>com.sino</groupId>
|
||
<artifactId>sino-mci-parent</artifactId>
|
||
<version>0.0.1-SNAPSHOT</version>
|
||
<packaging>pom</packaging>
|
||
<name>sino-mci-parent</name>
|
||
<description>Message & Customer Interaction Platform</description>
|
||
|
||
<properties>
|
||
<java.version>21</java.version>
|
||
<maven.compiler.source>21</maven.compiler.source>
|
||
<maven.compiler.target>21</maven.compiler.target>
|
||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||
</properties>
|
||
|
||
<modules>
|
||
<module>mci-shared</module>
|
||
<module>mci-server</module>
|
||
<module>mci-channel-common</module>
|
||
<module>mci-channel-wecom</module>
|
||
<module>mci-channel-dingtalk</module>
|
||
<module>mci-channel-feishu</module>
|
||
<module>mci-channel-ivr</module>
|
||
</modules>
|
||
|
||
<dependencyManagement>
|
||
<dependencies>
|
||
<dependency>
|
||
<groupId>com.sino</groupId>
|
||
<artifactId>mci-shared</artifactId>
|
||
<version>${project.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.sino</groupId>
|
||
<artifactId>mci-channel-common</artifactId>
|
||
<version>${project.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.baomidou</groupId>
|
||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||
<version>${mybatis-plus.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.baomidou</groupId>
|
||
<artifactId>mybatis-plus-annotation</artifactId>
|
||
<version>${mybatis-plus.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.mysql</groupId>
|
||
<artifactId>mysql-connector-j</artifactId>
|
||
<version>8.3.0</version>
|
||
</dependency>
|
||
</dependencies>
|
||
</dependencyManagement>
|
||
|
||
<build>
|
||
<pluginManagement>
|
||
<plugins>
|
||
<plugin>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||
<version>4.1.0</version>
|
||
</plugin>
|
||
</plugins>
|
||
</pluginManagement>
|
||
</build>
|
||
</project>
|
||
```
|
||
|
||
- [ ] **Step 2: 验证 XML 格式**
|
||
|
||
Run:
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci/backend
|
||
mvn help:evaluate -Dexpression=project.version -q -DforceStdout
|
||
```
|
||
|
||
Expected: `0.0.1-SNAPSHOT`
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
git add backend/pom.xml
|
||
git commit -m "chore: add Maven 4 parent POM for sino-mci"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.2: 创建 mci-shared 公共模块
|
||
|
||
**Files:**
|
||
- Create: `backend/mci-shared/pom.xml`
|
||
- Create: `backend/mci-shared/src/main/java/com/sino/mci/shared/entity/BaseEntity.java`
|
||
- Create: `backend/mci-shared/src/main/java/com/sino/mci/shared/web/ApiResponse.java`
|
||
|
||
- [ ] **Step 1: 创建 mci-shared POM**
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||
<modelVersion>4.0.0</modelVersion>
|
||
<parent>
|
||
<groupId>com.sino</groupId>
|
||
<artifactId>sino-mci-parent</artifactId>
|
||
<version>0.0.1-SNAPSHOT</version>
|
||
</parent>
|
||
<artifactId>mci-shared</artifactId>
|
||
<name>mci-shared</name>
|
||
|
||
<dependencies>
|
||
<dependency>
|
||
<groupId>com.baomidou</groupId>
|
||
<artifactId>mybatis-plus-annotation</artifactId>
|
||
<version>${mybatis-plus.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.projectlombok</groupId>
|
||
<artifactId>lombok</artifactId>
|
||
<optional>true</optional>
|
||
</dependency>
|
||
</dependencies>
|
||
</project>
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 BaseEntity**
|
||
|
||
```java
|
||
package com.sino.mci.shared.entity;
|
||
|
||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||
import com.baomidou.mybatisplus.annotation.IdType;
|
||
import com.baomidou.mybatisplus.annotation.TableField;
|
||
import com.baomidou.mybatisplus.annotation.TableId;
|
||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||
import lombok.Getter;
|
||
import lombok.Setter;
|
||
|
||
import java.io.Serializable;
|
||
import java.time.LocalDateTime;
|
||
|
||
@Getter
|
||
@Setter
|
||
public abstract class BaseEntity implements Serializable {
|
||
|
||
private static final long serialVersionUID = 1L;
|
||
|
||
@TableId(type = IdType.AUTO)
|
||
private Long id;
|
||
|
||
@TableField(fill = FieldFill.INSERT)
|
||
private LocalDateTime createTime;
|
||
|
||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||
private LocalDateTime updateTime;
|
||
|
||
@TableLogic
|
||
private Integer deleted;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 创建 ApiResponse**
|
||
|
||
```java
|
||
package com.sino.mci.shared.web;
|
||
|
||
import lombok.Data;
|
||
|
||
@Data
|
||
public class ApiResponse<T> {
|
||
|
||
private static final int SUCCESS_CODE = 0;
|
||
private static final String SUCCESS_MESSAGE = "ok";
|
||
|
||
private int code;
|
||
private String message;
|
||
private T data;
|
||
|
||
public static <T> ApiResponse<T> ok(T data) {
|
||
ApiResponse<T> response = new ApiResponse<>();
|
||
response.setCode(SUCCESS_CODE);
|
||
response.setMessage(SUCCESS_MESSAGE);
|
||
response.setData(data);
|
||
return response;
|
||
}
|
||
|
||
public static <T> ApiResponse<T> fail(int code, String message) {
|
||
ApiResponse<T> response = new ApiResponse<>();
|
||
response.setCode(code);
|
||
response.setMessage(message);
|
||
return response;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 编译验证**
|
||
|
||
Run:
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci/backend
|
||
mvn -pl mci-shared compile
|
||
```
|
||
|
||
Expected: BUILD SUCCESS
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
git add backend/mci-shared/
|
||
git commit -m "feat(shared): add BaseEntity and ApiResponse"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.3: 创建 mci-server 主服务模块
|
||
|
||
**Files:**
|
||
- Create: `backend/mci-server/pom.xml`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/MciServerApplication.java`
|
||
|
||
- [ ] **Step 1: 创建 mci-server POM**
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||
<modelVersion>4.0.0</modelVersion>
|
||
<parent>
|
||
<groupId>com.sino</groupId>
|
||
<artifactId>sino-mci-parent</artifactId>
|
||
<version>0.0.1-SNAPSHOT</version>
|
||
</parent>
|
||
<artifactId>mci-server</artifactId>
|
||
<name>mci-server</name>
|
||
|
||
<dependencies>
|
||
<dependency>
|
||
<groupId>com.sino</groupId>
|
||
<artifactId>mci-shared</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-web</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-validation</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.baomidou</groupId>
|
||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||
<version>${mybatis-plus.version}</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>com.mysql</groupId>
|
||
<artifactId>mysql-connector-j</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.flywaydb</groupId>
|
||
<artifactId>flyway-mysql</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.security</groupId>
|
||
<artifactId>spring-security-crypto</artifactId>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.projectlombok</groupId>
|
||
<artifactId>lombok</artifactId>
|
||
<optional>true</optional>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-starter-test</artifactId>
|
||
<scope>test</scope>
|
||
</dependency>
|
||
</dependencies>
|
||
|
||
<build>
|
||
<plugins>
|
||
<plugin>
|
||
<groupId>org.springframework.boot</groupId>
|
||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||
</plugin>
|
||
</plugins>
|
||
</build>
|
||
</project>
|
||
```
|
||
|
||
- [ ] **Step 2: 创建启动类**
|
||
|
||
```java
|
||
package com.sino.mci;
|
||
|
||
import org.springframework.boot.SpringApplication;
|
||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||
|
||
@SpringBootApplication
|
||
public class MciServerApplication {
|
||
|
||
public static void main(String[] args) {
|
||
SpringApplication.run(MciServerApplication.class, args);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 编译验证**
|
||
|
||
Because `backend/pom.xml` declares modules that do not yet exist, build the modules standalone instead of using the reactor:
|
||
|
||
Run:
|
||
```bash
|
||
export JAVA_HOME=/opt/homebrew/opt/openjdk@21
|
||
export PATH=$JAVA_HOME/bin:$PATH
|
||
cd /Users/marsal/Projects/sino-project/sino-mci/backend
|
||
mvn -N install
|
||
mvn -f mci-shared/pom.xml install
|
||
mvn -f mci-server/pom.xml compile
|
||
```
|
||
|
||
Expected: All three commands return BUILD SUCCESS
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
git add backend/mci-server/pom.xml backend/mci-server/src/main/java/com/sino/mci/MciServerApplication.java
|
||
git commit -m "feat(server): add mci-server module with Spring Boot 4.1"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.4: 配置 application.yml 与本地开发环境
|
||
|
||
**Files:**
|
||
- Create: `backend/mci-server/src/main/resources/application.yml`
|
||
- Create: `backend/mci-server/src/main/resources/application-dev.yml`
|
||
- Create: `infrastructure/docker-compose.yml`
|
||
- Create: `scripts/local-up.sh`
|
||
|
||
- [ ] **Step 1: 创建 application.yml**
|
||
|
||
```yaml
|
||
server:
|
||
port: 8080
|
||
servlet:
|
||
context-path: /msg-platform
|
||
|
||
spring:
|
||
application:
|
||
name: mci-server
|
||
profiles:
|
||
active: dev
|
||
jackson:
|
||
default-property-inclusion: non_null
|
||
|
||
mybatis-plus:
|
||
configuration:
|
||
map-underscore-to-camel-case: true
|
||
global-config:
|
||
db-config:
|
||
logic-delete-field: deleted
|
||
logic-delete-value: 1
|
||
logic-not-delete-value: 0
|
||
|
||
management:
|
||
endpoints:
|
||
web:
|
||
exposure:
|
||
include: health,info
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 application-dev.yml**
|
||
|
||
```yaml
|
||
spring:
|
||
datasource:
|
||
url: jdbc:mysql://localhost:3306/sino_mci?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||
username: root
|
||
password: root
|
||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||
data:
|
||
redis:
|
||
host: localhost
|
||
port: 6379
|
||
database: 0
|
||
rabbitmq:
|
||
host: localhost
|
||
port: 5672
|
||
username: guest
|
||
password: guest
|
||
flyway:
|
||
enabled: true
|
||
locations: classpath:db/migration
|
||
baseline-on-migrate: true
|
||
```
|
||
|
||
- [ ] **Step 3: 创建 docker-compose.yml**
|
||
|
||
```yaml
|
||
version: '3.8'
|
||
|
||
services:
|
||
mysql:
|
||
image: mysql:8.0.36
|
||
container_name: mci-mysql
|
||
environment:
|
||
MYSQL_ROOT_PASSWORD: root
|
||
MYSQL_DATABASE: sino_mci
|
||
ports:
|
||
- "3306:3306"
|
||
volumes:
|
||
- mci-mysql-data:/var/lib/mysql
|
||
|
||
redis:
|
||
image: redis:7.2
|
||
container_name: mci-redis
|
||
ports:
|
||
- "6379:6379"
|
||
|
||
rabbitmq:
|
||
image: rabbitmq:3.13-management
|
||
container_name: mci-rabbitmq
|
||
environment:
|
||
RABBITMQ_DEFAULT_USER: guest
|
||
RABBITMQ_DEFAULT_PASS: guest
|
||
ports:
|
||
- "5672:5672"
|
||
- "15672:15672"
|
||
|
||
seaweedfs:
|
||
image: chrislusf/seaweedfs:3.62
|
||
container_name: mci-seaweedfs
|
||
ports:
|
||
- "9333:9333"
|
||
- "8888:8888"
|
||
command: server -dir=/data
|
||
volumes:
|
||
- mci-seaweedfs-data:/data
|
||
|
||
volumes:
|
||
mci-mysql-data:
|
||
mci-seaweedfs-data:
|
||
```
|
||
|
||
- [ ] **Step 4: 创建 local-up.sh**
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
set -e
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
cd "$SCRIPT_DIR/.."
|
||
|
||
echo "Starting local infrastructure..."
|
||
docker compose -f infrastructure/docker-compose.yml up -d
|
||
|
||
echo "Waiting for MySQL..."
|
||
MAX_RETRIES=60
|
||
RETRY_COUNT=0
|
||
until docker exec mci-mysql mysql -uroot -proot -e "SELECT 1" > /dev/null 2>&1; do
|
||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||
if [ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]; then
|
||
echo "MySQL did not become ready within ${MAX_RETRIES} seconds."
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
echo "Waiting for Redis..."
|
||
RETRY_COUNT=0
|
||
until docker exec mci-redis redis-cli ping | grep -q "PONG"; do
|
||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||
if [ "$RETRY_COUNT" -ge 30 ]; then
|
||
echo "Redis did not become ready within 30 seconds."
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
echo "Waiting for RabbitMQ..."
|
||
RETRY_COUNT=0
|
||
until curl -s -o /dev/null -w "%{http_code}" http://guest:guest@localhost:15672/api/overview | grep -q "200"; do
|
||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||
if [ "$RETRY_COUNT" -ge 30 ]; then
|
||
echo "RabbitMQ did not become ready within 30 seconds."
|
||
exit 1
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
echo "Local infrastructure is ready."
|
||
echo "MySQL: jdbc:mysql://localhost:3306/sino_mci"
|
||
echo "Redis: localhost:6379"
|
||
echo "RabbitMQ: localhost:5672 / http://localhost:15672"
|
||
echo "SeaweedFS: http://localhost:9333"
|
||
```
|
||
|
||
- [ ] **Step 5: 启动基础设施并验证**
|
||
|
||
Run:
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
chmod +x scripts/local-up.sh
|
||
./scripts/local-up.sh
|
||
```
|
||
|
||
Expected: 四个容器启动成功,MySQL 可连接。
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
git add backend/mci-server/src/main/resources/ infrastructure/docker-compose.yml scripts/local-up.sh
|
||
git commit -m "chore: add dev configs and local infrastructure"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.5: 创建账号权限 Flyway 迁移脚本
|
||
|
||
**Files:**
|
||
- Create: `backend/mci-server/src/main/resources/db/migration/V1__init_account.sql`
|
||
- Create: `backend/mci-server/src/main/resources/db/migration/V2__seed_roles.sql`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/config/FlywayConfig.java`
|
||
|
||
- [ ] **Step 1: 创建 V1__init_account.sql**
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS mci_tenant (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
tenant_code VARCHAR(64) NOT NULL UNIQUE COMMENT '租户编码',
|
||
tenant_name VARCHAR(128) NOT NULL COMMENT '租户名称',
|
||
auth_config JSON COMMENT '认证配置',
|
||
inbound_webhook_url VARCHAR(512) COMMENT '入站消息推送地址',
|
||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
deleted TINYINT NOT NULL DEFAULT 0
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户/业务系统表';
|
||
|
||
CREATE TABLE IF NOT EXISTS mci_platform_role (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
role_code VARCHAR(64) NOT NULL UNIQUE COMMENT '角色编码',
|
||
role_name VARCHAR(128) NOT NULL COMMENT '角色名称',
|
||
permissions JSON COMMENT '权限列表',
|
||
status TINYINT NOT NULL DEFAULT 1,
|
||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
deleted TINYINT NOT NULL DEFAULT 0
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台角色表';
|
||
|
||
CREATE TABLE IF NOT EXISTS mci_user (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
tenant_code VARCHAR(64) NOT NULL COMMENT '所属租户',
|
||
username VARCHAR(64) NOT NULL COMMENT '用户名',
|
||
password VARCHAR(256) NOT NULL COMMENT '加密密码',
|
||
real_name VARCHAR(128) COMMENT '真实姓名',
|
||
phone VARCHAR(32) COMMENT '手机号',
|
||
email VARCHAR(128) COMMENT '邮箱',
|
||
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1启用',
|
||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
deleted TINYINT NOT NULL DEFAULT 0,
|
||
UNIQUE KEY uk_tenant_username (tenant_code, username, deleted)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='中台用户表';
|
||
|
||
CREATE TABLE IF NOT EXISTS mci_user_role (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id BIGINT NOT NULL COMMENT '用户ID',
|
||
role_id BIGINT NOT NULL COMMENT '角色ID',
|
||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uk_user_role (user_id, role_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关系表';
|
||
|
||
ALTER TABLE mci_user_role
|
||
ADD CONSTRAINT fk_user_role_user_id FOREIGN KEY (user_id) REFERENCES mci_user (id),
|
||
ADD CONSTRAINT fk_user_role_role_id FOREIGN KEY (role_id) REFERENCES mci_platform_role (id);
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 V2__seed_roles.sql**
|
||
|
||
```sql
|
||
INSERT IGNORE INTO mci_platform_role (role_code, role_name, permissions) VALUES
|
||
('admin', '租户管理员', '["*"]'),
|
||
('operator', '运营操作员', '["send:write", "channel:read", "channel:write", "record:read"]'),
|
||
('viewer', '只读用户', '["record:read", "channel:read"]');
|
||
```
|
||
|
||
- [ ] **Step 3: 创建 FlywayConfig.java**
|
||
|
||
Spring Boot 4.1.0 的 `spring-boot-autoconfigure` 不再包含 `FlywayAutoConfiguration`,需要显式提供 Flyway bean:
|
||
|
||
```java
|
||
package com.sino.mci.config;
|
||
|
||
import javax.sql.DataSource;
|
||
|
||
import org.flywaydb.core.Flyway;
|
||
import org.springframework.beans.factory.annotation.Value;
|
||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||
import org.springframework.context.annotation.Bean;
|
||
import org.springframework.context.annotation.Configuration;
|
||
|
||
@Configuration
|
||
@ConditionalOnProperty(prefix = "spring.flyway", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||
public class FlywayConfig {
|
||
|
||
@Bean(initMethod = "migrate")
|
||
public Flyway flyway(
|
||
DataSource dataSource,
|
||
@Value("${spring.flyway.locations:classpath:db/migration}") String[] locations,
|
||
@Value("${spring.flyway.baseline-on-migrate:true}") boolean baselineOnMigrate) {
|
||
return Flyway.configure()
|
||
.dataSource(dataSource)
|
||
.locations(locations)
|
||
.baselineOnMigrate(baselineOnMigrate)
|
||
.load();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 启动服务验证 Flyway 迁移**
|
||
|
||
Run:
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci/backend
|
||
mvn -pl mci-server spring-boot:run
|
||
```
|
||
|
||
在另一个终端验证:
|
||
```bash
|
||
docker exec mci-mysql mysql -uroot -proot sino_mci -e "SHOW TABLES;"
|
||
```
|
||
|
||
Expected: 输出 `mci_tenant`, `mci_platform_role`, `mci_user`, `mci_user_role`, `flyway_schema_history`。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
git add backend/mci-server/src/main/resources/db/migration/ backend/mci-server/src/main/java/com/sino/mci/config/FlywayConfig.java
|
||
git commit -m "feat(db): add account and role Flyway migrations"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.6: 实现账号权限实体与 Mapper
|
||
|
||
**Files:**
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/entity/Tenant.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/entity/PlatformUser.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/entity/PlatformRole.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/entity/UserRole.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/repository/TenantMapper.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/repository/PlatformUserMapper.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/repository/PlatformRoleMapper.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/repository/UserRoleMapper.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusConfig.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusMetaObjectHandler.java`
|
||
|
||
- [ ] **Step 1: 创建 Tenant 实体**
|
||
|
||
```java
|
||
package com.sino.mci.admin.entity;
|
||
|
||
import com.baomidou.mybatisplus.annotation.TableName;
|
||
import com.sino.mci.shared.entity.BaseEntity;
|
||
import lombok.Data;
|
||
import lombok.EqualsAndHashCode;
|
||
|
||
@Data
|
||
@EqualsAndHashCode(callSuper = true)
|
||
@TableName("mci_tenant")
|
||
public class Tenant extends BaseEntity {
|
||
|
||
private String tenantCode;
|
||
private String tenantName;
|
||
private String authConfig;
|
||
private String inboundWebhookUrl;
|
||
private Integer status;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 PlatformUser 实体**
|
||
|
||
```java
|
||
package com.sino.mci.admin.entity;
|
||
|
||
import com.baomidou.mybatisplus.annotation.TableName;
|
||
import com.sino.mci.shared.entity.BaseEntity;
|
||
import lombok.Data;
|
||
import lombok.EqualsAndHashCode;
|
||
|
||
@Data
|
||
@EqualsAndHashCode(callSuper = true)
|
||
@TableName("mci_user")
|
||
public class PlatformUser extends BaseEntity {
|
||
|
||
private String tenantCode;
|
||
private String username;
|
||
private String password;
|
||
private String realName;
|
||
private String phone;
|
||
private String email;
|
||
private Integer status;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 创建 PlatformRole 实体**
|
||
|
||
```java
|
||
package com.sino.mci.admin.entity;
|
||
|
||
import com.baomidou.mybatisplus.annotation.TableName;
|
||
import com.sino.mci.shared.entity.BaseEntity;
|
||
import lombok.Data;
|
||
import lombok.EqualsAndHashCode;
|
||
|
||
@Data
|
||
@EqualsAndHashCode(callSuper = true)
|
||
@TableName("mci_platform_role")
|
||
public class PlatformRole extends BaseEntity {
|
||
|
||
private String roleCode;
|
||
private String roleName;
|
||
private String permissions;
|
||
private Integer status;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 创建 UserRole 实体**
|
||
|
||
```java
|
||
package com.sino.mci.admin.entity;
|
||
|
||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||
import com.baomidou.mybatisplus.annotation.IdType;
|
||
import com.baomidou.mybatisplus.annotation.TableField;
|
||
import com.baomidou.mybatisplus.annotation.TableId;
|
||
import com.baomidou.mybatisplus.annotation.TableName;
|
||
import lombok.Data;
|
||
|
||
import java.time.LocalDateTime;
|
||
|
||
@Data
|
||
@TableName("mci_user_role")
|
||
public class UserRole {
|
||
|
||
@TableId(type = IdType.AUTO)
|
||
private Long id;
|
||
|
||
private Long userId;
|
||
private Long roleId;
|
||
|
||
@TableField(fill = FieldFill.INSERT)
|
||
private LocalDateTime createTime;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: 创建 Mapper 接口**
|
||
|
||
每个 Mapper 内容类似:
|
||
|
||
```java
|
||
package com.sino.mci.admin.repository;
|
||
|
||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||
import com.sino.mci.admin.entity.Tenant;
|
||
import org.apache.ibatis.annotations.Mapper;
|
||
|
||
@Mapper
|
||
public interface TenantMapper extends BaseMapper<Tenant> {
|
||
}
|
||
```
|
||
|
||
其余三个:`PlatformUserMapper`, `PlatformRoleMapper`, `UserRoleMapper`。
|
||
|
||
- [ ] **Step 6: 创建 MybatisPlusConfig**
|
||
|
||
```java
|
||
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();
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 7: 创建 MybatisPlusMetaObjectHandler**
|
||
|
||
用于自动填充 `createTime` 和 `updateTime`:
|
||
|
||
```java
|
||
package com.sino.mci.config;
|
||
|
||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||
import org.apache.ibatis.reflection.MetaObject;
|
||
import org.springframework.stereotype.Component;
|
||
|
||
import java.time.LocalDateTime;
|
||
|
||
@Component
|
||
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
|
||
|
||
@Override
|
||
public void insertFill(MetaObject metaObject) {
|
||
LocalDateTime now = LocalDateTime.now();
|
||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
|
||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
|
||
}
|
||
|
||
@Override
|
||
public void updateFill(MetaObject metaObject) {
|
||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 8: 编译验证**
|
||
|
||
Run:
|
||
```bash
|
||
export JAVA_HOME=/opt/homebrew/opt/openjdk@21
|
||
export PATH=$JAVA_HOME/bin:$PATH
|
||
cd /Users/marsal/Projects/sino-project/sino-mci/backend
|
||
mvn -N install
|
||
mvn -f mci-shared/pom.xml install
|
||
mvn -f mci-server/pom.xml compile
|
||
```
|
||
|
||
Expected: BUILD SUCCESS
|
||
|
||
- [ ] **Step 9: Commit**
|
||
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
git add backend/mci-server/src/main/java/com/sino/mci/admin/ backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusConfig.java backend/mci-server/src/main/java/com/sino/mci/config/MybatisPlusMetaObjectHandler.java
|
||
git commit -m "feat(admin): add account entities and MyBatis mappers"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 1.7: 实现账号管理 API
|
||
|
||
**Files:**
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/dto/CreateTenantRequest.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/dto/CreateUserRequest.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginRequest.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java`
|
||
- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java`
|
||
- Create: `backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java`
|
||
|
||
- [ ] **Step 1: 创建 CreateTenantRequest DTO**
|
||
|
||
```java
|
||
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;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 创建 CreateUserRequest DTO**
|
||
|
||
```java
|
||
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<String> roleCodes;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 创建 LoginRequest DTO**
|
||
|
||
```java
|
||
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;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 创建 AccountService**
|
||
|
||
```java
|
||
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<PlatformRole>().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<PlatformUser>()
|
||
.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;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: 创建 AccountController**
|
||
|
||
```java
|
||
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()));
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: 创建集成测试**
|
||
|
||
```java
|
||
package com.sino.mci.admin.controller;
|
||
|
||
import org.junit.jupiter.api.Test;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||
import org.springframework.boot.test.context.SpringBootTest;
|
||
import org.springframework.http.MediaType;
|
||
import org.springframework.test.web.servlet.MockMvc;
|
||
|
||
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-" + java.util.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));
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 7: 运行测试**
|
||
|
||
确保基础设施已启动:
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
./scripts/local-up.sh
|
||
```
|
||
|
||
Run:
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci/backend
|
||
mvn -pl mci-server test -Dtest=AccountControllerTest
|
||
```
|
||
|
||
Expected: BUILD SUCCESS,测试通过。
|
||
|
||
- [ ] **Step 8: Commit**
|
||
|
||
```bash
|
||
cd /Users/marsal/Projects/sino-project/sino-mci
|
||
git add backend/mci-server/src/main/java/com/sino/mci/admin/ backend/mci-server/src/test/java/com/sino/mci/admin/controller/AccountControllerTest.java
|
||
git commit -m "feat(admin): implement tenant/user/role account APIs"
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 1 验收标准
|
||
|
||
- [ ] `mvn -pl mci-server -am compile` 成功
|
||
- [ ] `./scripts/local-up.sh` 成功启动 MySQL/Redis/RabbitMQ/SeaweedFS
|
||
- [ ] `mvn -pl mci-server spring-boot:run` 启动后,Flyway 自动创建 4 张账号表
|
||
- [ ] `POST /msg-platform/api/v1/account/tenant` 可创建租户
|
||
- [ ] `POST /msg-platform/api/v1/account/user` 可创建用户
|
||
- [ ] `POST /msg-platform/api/v1/account/login` 可登录
|
||
- [ ] `AccountControllerTest` 通过
|
||
|
||
---
|
||
|
||
## 后续阶段概述
|
||
|
||
### Phase 2: 渠道主体 + 企微渠道适配
|
||
- 创建 `mci-channel-common` 模块,定义 `ChannelAdapter` SPI
|
||
- 创建 `mci-channel-wecom` 模块,集成 `weixin-java-cp`
|
||
- 实现 `mci_channel_subject` 和 `mci_channel_account` 表及 API
|
||
- 实现企微回调接收和会话存档配置
|
||
|
||
### Phase 3: 联系人 + 会话 + 标签
|
||
- 实现 `mci_person`, `mci_channel_identity`, `mci_conversation`, `mci_tag` 等表
|
||
- 实现人员/身份/会话/标签 CRUD API
|
||
- 实现账号-会话绑定和已有群聊同步
|
||
|
||
### Phase 4: 入站流程编排 + LLM
|
||
- 实现 `mci_flow_definition` 表和轻量流程执行器
|
||
- 前端 Vue Flow 画布
|
||
- 集成 Spring AI 做意图识别
|
||
- 实现入站 webhook 推送
|
||
|
||
### Phase 5: 出站发送 + 策略
|
||
- 实现 `mci_send_record` 表
|
||
- 实现 `/v1/send`, `/v1/send/test`, `/v1/send/preview` 等 API
|
||
- 实现精准定位、渠道选择、策略执行
|
||
|
||
### Phase 6: Python 渠道服务
|
||
- 搭建 `channel-service` FastAPI 项目
|
||
- 实现 pywechat 登录、发消息、心跳、状态上报
|
||
- 实现客户端监控 HTML + Web Components
|
||
|
||
### Phase 7: 前端 + 部署
|
||
- 搭建 `admin-web` Vue 3 + Element Plus 项目
|
||
- 实现 8 个管理页面
|
||
- 完善 docker-compose 和 K8s 部署配置
|
||
|
||
---
|
||
|
||
## Self-Review
|
||
|
||
1. **Spec coverage**: Phase 1 覆盖设计文档 2.3.1/2.3.16/2.3.17/2.3.18(租户/角色/用户)、5.9(账号权限 API)、6.2/6.3(项目结构)。后续阶段在后续计划中细化。
|
||
2. **Placeholder scan**: 无 TBD/TODO;代码片段完整;命令和预期输出明确。
|
||
3. **Type consistency**: 实体字段、DTO、Service 方法签名一致;表名与实体 `@TableName` 一致。
|
||
|
||
---
|
||
|
||
## Execution Handoff
|
||
|
||
**Plan complete and saved to `docs/superpowers/plans/2026-07-06-sino-mci-master-implementation-plan.md`. Two execution options:**
|
||
|
||
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
|
||
|
||
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
|
||
|
||
**Which approach?**
|