Files
sino-mci/docs/superpowers/plans/2026-07-06-sino-mci-master-implementation-plan.md
marsal 24a70eb530 fix(task12): 修复前端登录租户编码透传、补充 tenant_admin 角色迁移与外部 API 测试脚本
- 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 测试计划
2026-07-07 16:08:20 +08:00

1316 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:** 采用单仓库 monorepoJava 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?**