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

41 KiB
Raw Blame History

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 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:

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
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 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
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
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:

cd /Users/marsal/Projects/sino-project/sino-mci/backend
mvn -pl mci-shared compile

Expected: BUILD SUCCESS

  • Step 5: Commit
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 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: 创建启动类
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:

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
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

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
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
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
#!/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:

cd /Users/marsal/Projects/sino-project/sino-mci
chmod +x scripts/local-up.sh
./scripts/local-up.sh

Expected: 四个容器启动成功MySQL 可连接。

  • Step 6: Commit
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

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
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

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:

cd /Users/marsal/Projects/sino-project/sino-mci/backend
mvn -pl mci-server spring-boot:run

在另一个终端验证:

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
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 实体

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 实体
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 实体
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 实体
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 内容类似:

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
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

用于自动填充 createTimeupdateTime

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:

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
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

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
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
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
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
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: 创建集成测试
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: 运行测试

确保基础设施已启动:

cd /Users/marsal/Projects/sino-project/sino-mci
./scripts/local-up.sh

Run:

cd /Users/marsal/Projects/sino-project/sino-mci/backend
mvn -pl mci-server test -Dtest=AccountControllerTest

Expected: BUILD SUCCESS测试通过。

  • Step 8: Commit
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_subjectmci_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?