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