# 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 4.0.0 org.springframework.boot spring-boot-starter-parent 4.1.0 com.sino sino-mci-parent 0.0.1-SNAPSHOT pom sino-mci-parent Message & Customer Interaction Platform 21 21 21 UTF-8 3.5.7 mci-shared mci-server mci-channel-common mci-channel-wecom mci-channel-dingtalk mci-channel-feishu mci-channel-ivr com.sino mci-shared ${project.version} com.sino mci-channel-common ${project.version} com.baomidou mybatis-plus-boot-starter ${mybatis-plus.version} com.baomidou mybatis-plus-annotation ${mybatis-plus.version} com.mysql mysql-connector-j 8.3.0 org.springframework.boot spring-boot-maven-plugin 4.1.0 ``` - [ ] **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 4.0.0 com.sino sino-mci-parent 0.0.1-SNAPSHOT mci-shared mci-shared com.baomidou mybatis-plus-annotation ${mybatis-plus.version} org.projectlombok lombok true ``` - [ ] **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 { 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 ApiResponse ok(T data) { ApiResponse response = new ApiResponse<>(); response.setCode(SUCCESS_CODE); response.setMessage(SUCCESS_MESSAGE); response.setData(data); return response; } public static ApiResponse fail(int code, String message) { ApiResponse 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 4.0.0 com.sino sino-mci-parent 0.0.1-SNAPSHOT mci-server mci-server com.sino mci-shared org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-amqp com.baomidou mybatis-plus-boot-starter ${mybatis-plus.version} com.mysql mysql-connector-j org.flywaydb flyway-mysql org.springframework.security spring-security-crypto org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin ``` - [ ] **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 { } ``` 其余三个:`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 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().eq(PlatformRole::getRoleCode, roleCode)); if (role != null) { UserRole userRole = new UserRole(); userRole.setUserId(user.getId()); userRole.setRoleId(role.getId()); userRoleMapper.insert(userRole); } } } return user; } public PlatformUser login(String tenantCode, String username, String password) { PlatformUser user = userMapper.selectOne( new LambdaQueryWrapper() .eq(PlatformUser::getTenantCode, tenantCode) .eq(PlatformUser::getUsername, username) .eq(PlatformUser::getStatus, 1)); if (user == null || !BCrypt.checkpw(password, user.getPassword())) { throw new RuntimeException("用户名或密码错误"); } return user; } } ``` - [ ] **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?**