diff --git a/admin-web/src/api/account.ts b/admin-web/src/api/account.ts
index 122361e..6cd6004 100644
--- a/admin-web/src/api/account.ts
+++ b/admin-web/src/api/account.ts
@@ -1,7 +1,9 @@
import client from './client'
export function login(data: { tenantCode: string; username: string; password: string }) {
- return client.post('/api/v1/account/login', data)
+ return client.post('/api/v1/account/login', data, {
+ headers: { 'X-Tenant-Code': data.tenantCode },
+ })
}
export function createTenant(data: { tenantCode: string; tenantName: string }) {
diff --git a/admin-web/src/api/client.ts b/admin-web/src/api/client.ts
index 151953b..fccfa25 100644
--- a/admin-web/src/api/client.ts
+++ b/admin-web/src/api/client.ts
@@ -13,8 +13,10 @@ client.interceptors.request.use((config) => {
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
- const tenantCode = localStorage.getItem('mci-tenant-code') || 'default'
- config.headers['X-Tenant-Code'] = tenantCode
+ if (!config.headers['X-Tenant-Code']) {
+ const tenantCode = localStorage.getItem('mci-tenant-code') || 'default'
+ config.headers['X-Tenant-Code'] = tenantCode
+ }
return config
})
diff --git a/backend/mci-server/src/main/resources/db/migration/V10__add_tenant_admin_role.sql b/backend/mci-server/src/main/resources/db/migration/V10__add_tenant_admin_role.sql
new file mode 100644
index 0000000..ee4b3fa
--- /dev/null
+++ b/backend/mci-server/src/main/resources/db/migration/V10__add_tenant_admin_role.sql
@@ -0,0 +1,2 @@
+INSERT IGNORE INTO mci_platform_role (role_code, role_name, permissions) VALUES
+('tenant_admin', '租户管理员', '["*"]');
diff --git a/docs/superpowers/plans/2026-07-06-sino-mci-master-implementation-plan.md b/docs/superpowers/plans/2026-07-06-sino-mci-master-implementation-plan.md
new file mode 100644
index 0000000..75b3018
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-06-sino-mci-master-implementation-plan.md
@@ -0,0 +1,1315 @@
+# 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?**
diff --git a/scripts/test-external-api.sh b/scripts/test-external-api.sh
index 38e1793..8a11175 100755
--- a/scripts/test-external-api.sh
+++ b/scripts/test-external-api.sh
@@ -55,60 +55,74 @@ TENANT_RESPONSE=$(make_request POST "$BASE_URL/api/v1/account/tenant" \
'{"tenantCode":"'"$TENANT_CODE"'","tenantName":"External API Test"}')
assert_code_zero "Create tenant" "$TENANT_RESPONSE"
-# 2. Create channel subject
-echo "==> 2. Create channel subject"
+# 2. Create admin user and login
+echo "==> 2. Create admin user and login"
+USER_RESPONSE=$(make_request POST "$BASE_URL/api/v1/account/user" \
+ '{"username":"admin","password":"admin123","realName":"Admin","roleCodes":["tenant_admin"]}' \
+ -H "X-Tenant-Code: $TENANT_CODE")
+assert_code_zero "Create admin user" "$USER_RESPONSE"
+
+LOGIN_RESPONSE=$(make_request POST "$BASE_URL/api/v1/account/login" \
+ '{"username":"admin","password":"admin123"}' \
+ -H "X-Tenant-Code: $TENANT_CODE")
+assert_code_zero "Login" "$LOGIN_RESPONSE"
+TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.data.token')
+AUTH_HEADER="Authorization: Bearer $TOKEN"
+
+# 3. Create channel subject
+echo "==> 3. Create channel subject"
SUBJECT_RESPONSE=$(make_request POST "$BASE_URL/api/v1/channel-subject" \
'{"channelType":"wecom","subjectCode":"sino","subjectName":"Sino Test"}' \
- -H "X-Tenant-Code: $TENANT_CODE")
+ -H "$AUTH_HEADER" -H "X-Tenant-Code: $TENANT_CODE")
assert_code_zero "Create channel subject" "$SUBJECT_RESPONSE"
-# 3. Create channel account
-echo "==> 3. Create channel account"
+# 4. Create channel account
+echo "==> 4. Create channel account"
ACCOUNT_RESPONSE=$(make_request POST "$BASE_URL/api/v1/channel-account" \
'{"accountType":1,"channelType":"wecom","accountId":"ZhangSan","accountName":"张三"}' \
- -H "X-Tenant-Code: $TENANT_CODE")
+ -H "$AUTH_HEADER" -H "X-Tenant-Code: $TENANT_CODE")
assert_code_zero "Create channel account" "$ACCOUNT_RESPONSE"
ACCOUNT_ID=$(echo "$ACCOUNT_RESPONSE" | jq -r '.data.id')
-# 4. Create conversation
-echo "==> 4. Create conversation"
+# 5. Create conversation
+echo "==> 5. Create conversation"
CONVERSATION_RESPONSE=$(make_request POST "$BASE_URL/api/v1/conversation" \
'{"conversationType":2,"channelType":"wecom","channelConversationId":"room-test-'"$(date +%s)"'","conversationName":"Test Group"}' \
- -H "X-Tenant-Code: $TENANT_CODE")
+ -H "$AUTH_HEADER" -H "X-Tenant-Code: $TENANT_CODE")
assert_code_zero "Create conversation" "$CONVERSATION_RESPONSE"
CONVERSATION_ID=$(echo "$CONVERSATION_RESPONSE" | jq -r '.data.id')
-# 5. Bind account to conversation
-echo "==> 5. Bind account to conversation"
+# 6. Bind account to conversation
+echo "==> 6. Bind account to conversation"
BIND_RESPONSE=$(make_request POST "$BASE_URL/api/v1/conversation/$CONVERSATION_ID/bind-account" \
"{\"accountId\":$ACCOUNT_ID,\"bindingType\":1,\"sortOrder\":1}" \
- -H "X-Tenant-Code: $TENANT_CODE")
+ -H "$AUTH_HEADER" -H "X-Tenant-Code: $TENANT_CODE")
assert_code_zero "Bind account to conversation" "$BIND_RESPONSE"
-# 6. Test send
-echo "==> 6. Test send"
+# 7. Test send
+echo "==> 7. Test send"
SEND_RESPONSE=$(make_request POST "$BASE_URL/api/v1/send/test" \
"{\"targetType\":\"conversation\",\"targetValue\":{\"conversation_id\":$CONVERSATION_ID},\"contentType\":\"text\",\"content\":{\"text\":\"hello from external api test\"},\"channelStrategy\":{\"channel_type\":\"wecom\",\"strategy\":\"primary\"}}" \
- -H "X-Tenant-Code: $TENANT_CODE")
+ -H "$AUTH_HEADER" -H "X-Tenant-Code: $TENANT_CODE")
assert_code_zero "Test send" "$SEND_RESPONSE"
TASK_ID=$(echo "$SEND_RESPONSE" | jq -r '.data.taskId')
-# 7. Query send records
-echo "==> 7. Query send records"
+# 8. Query send records
+echo "==> 8. Query send records"
RECORDS_RESPONSE=$(make_request GET "$BASE_URL/api/v1/send/$TASK_ID/records" "" \
- -H "X-Tenant-Code: $TENANT_CODE")
+ -H "$AUTH_HEADER" -H "X-Tenant-Code: $TENANT_CODE")
assert_code_zero "Query send records" "$RECORDS_RESPONSE"
-# 8. Receive inbound message
-echo "==> 8. Receive inbound message"
+# 9. Receive inbound message
+echo "==> 9. Receive inbound message"
EVENT_ID="evt-$(date +%s)-$RANDOM"
INBOUND_RESPONSE=$(make_request POST "$BASE_URL/api/v1/inbound/receive" \
"{\"eventId\":\"$EVENT_ID\",\"channelType\":\"wecom\",\"channelSubjectId\":1,\"eventType\":\"message\",\"messageSource\":\"realtime\",\"conversationId\":$CONVERSATION_ID,\"accountId\":$ACCOUNT_ID,\"content\":{\"text\":\"hello inbound\"},\"rawPayload\":{},\"businessContext\":{}}" \
- -H "X-Tenant-Code: $TENANT_CODE")
+ -H "$AUTH_HEADER" -H "X-Tenant-Code: $TENANT_CODE")
assert_code_zero "Receive inbound message" "$INBOUND_RESPONSE"
-# 9. Channel-service health
-echo "==> 9. Channel-service health"
+# 10. Channel-service health
+echo "==> 10. Channel-service health"
HEALTH_RESPONSE=$(curl -s -S "$CHANNEL_URL/health")
if echo "$HEALTH_RESPONSE" | jq -e '.status == "ok"' >/dev/null 2>&1; then
echo "[PASS] Channel-service health"
@@ -119,8 +133,8 @@ else
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
-# 10. WeChat personal login start
-echo "==> 10. WeChat personal login start"
+# 11. WeChat personal login start
+echo "==> 11. WeChat personal login start"
LOGIN_RESPONSE=$(make_request POST "$CHANNEL_URL/v1/wechat-personal/login/start" \
'{"account_id":"bot-test-'"$(date +%s)"'","account_name":"Test Bot"}')
if echo "$LOGIN_RESPONSE" | jq -e '.login_id' >/dev/null 2>&1; then
@@ -132,8 +146,8 @@ else
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
-# 11. WeChat personal send text (after confirming a login)
-echo "==> 11. WeChat personal send text"
+# 12. WeChat personal send text (after confirming a login)
+echo "==> 12. WeChat personal send text"
BOT_ID="bot-send-$(date +%s)"
make_request POST "$CHANNEL_URL/v1/wechat-personal/login/start" \
'{"account_id":"'"$BOT_ID"'"}' >/dev/null