diff --git a/docs/superpowers/plans/2026-07-07-admin-auth-flow-testing-plan.md b/docs/superpowers/plans/2026-07-07-admin-auth-flow-testing-plan.md new file mode 100644 index 0000000..64dadd0 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-admin-auth-flow-testing-plan.md @@ -0,0 +1,2455 @@ +# admin-web 补全与登录/租户管理实施计划 + +> **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:** 为 sino-mci 补齐登录鉴权、多租户管理、管理后台列表与绑定、入站流程图、消息测试模块,使管理后台达到可交付状态。 + +**Architecture:** 后端使用 JWT + Spring Interceptor 实现无状态鉴权,前端通过 localStorage 存储 token 并做路由守卫;管理后台各页面通过新增列表接口加载真实数据;入站流程使用 Vue Flow 可视化编排并记录执行轨迹;消息测试模块统一处理出站测试与入站模拟。 + +**Tech Stack:** Java 21, Spring Boot 4.1, JJWT, Vue 3, Element Plus, Vue Flow, MyBatis-Plus, MySQL + +--- + +## 功能点清单 + +### 后端功能点 + +1. JWT Token 生成与解析 +2. 鉴权拦截器与登录上下文 +3. 登录接口改造(返回 token) +4. 当前登录用户信息接口 +5. 租户列表接口 +6. 用户列表接口 +7. 租户隔离校验 +8. 渠道主体列表接口 +9. 渠道账号列表接口 +10. 会话列表接口 +11. 联系人列表接口 +12. 发送记录列表接口 +13. Dashboard 统计接口 +14. 入站流程执行轨迹记录与查询 +15. Webhook 测试接收端点 + +### 前端功能点 + +1. 登录页 +2. 前端路由守卫 +3. API Token 注入 +4. 租户管理页 +5. 用户管理页 +6. Dashboard 真实统计 +7. 渠道主体页补充字段与列表 +8. 渠道账号页补充字段与列表 +9. 会话/群聊页绑定账号 +10. 联系人页列表 +11. 发送记录页 UX 优化 +12. 标签层级展示 +13. 入站流程可视化编辑器 +14. 入站流程执行追踪 +15. 消息测试模块(出站 + 入站) + +--- + +## 文件结构 + +### 后端新增/修改文件 + +| 文件 | 职责 | +|---|---| +| `backend/mci-server/pom.xml` | 添加 jjwt 依赖 | +| `backend/mci-server/src/main/java/com/sino/mci/admin/security/JwtTokenProvider.java` | JWT 生成与解析 | +| `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java` | 当前登录用户 ThreadLocal | +| `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java` | 鉴权拦截器 | +| `backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java` | 注册拦截器 | +| `backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginResponse.java` | 改造为包含 token | +| `backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java` | 新增 me/tenant list/user list 接口,改造 login | +| `backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java` | 新增 list 方法 | +| `backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelSubjectController.java` | 新增 list 接口 | +| `backend/mci-server/src/main/java/com/sino/mci/channel/service/ChannelSubjectService.java` | 新增 list 方法 | +| `backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelAccountController.java` | 新增 list 接口 | +| `backend/mci-server/src/main/java/com/sino/mci/channel/service/ChannelAccountService.java` | 新增 list 方法 | +| `backend/mci-server/src/main/java/com/sino/mci/channel/controller/ConversationController.java` | 新增 list 接口 | +| `backend/mci-server/src/main/java/com/sino/mci/channel/service/ConversationService.java` | 新增 list 方法 | +| `backend/mci-server/src/main/java/com/sino/mci/crm/controller/PersonController.java` | 新增 list 接口 | +| `backend/mci-server/src/main/java/com/sino/mci/crm/service/PersonService.java` | 新增 list 方法 | +| `backend/mci-server/src/main/java/com/sino/mci/send/controller/SendController.java` | 新增 list 接口 | +| `backend/mci-server/src/main/java/com/sino/mci/send/service/SendService.java` | 新增 list 方法 | +| `backend/mci-server/src/main/java/com/sino/mci/admin/controller/DashboardController.java` | Dashboard 统计 | +| `backend/mci-server/src/main/java/com/sino/mci/inbound/entity/FlowExecutionTrace.java` | 执行轨迹实体 | +| `backend/mci-server/src/main/java/com/sino/mci/inbound/repository/FlowExecutionTraceMapper.java` | 轨迹 Mapper | +| `backend/mci-server/src/main/java/com/sino/mci/inbound/controller/InboundMessageController.java` | 新增 trace 接口 | +| `backend/mci-server/src/main/java/com/sino/mci/inbound/engine/FlowEngine.java` | 记录执行轨迹 | +| `backend/mci-server/src/main/resources/db/migration/V8__add_flow_trace.sql` | 轨迹表迁移 | +| `backend/mci-server/src/main/resources/db/migration/V9__add_user_role_platform_admin.sql` | 平台管理员角色 | + +### 前端新增/修改文件 + +| 文件 | 职责 | +|---|---| +| `admin-web/src/api/client.ts` | 注入 Authorization token | +| `admin-web/src/api/account.ts` | 登录/租户/用户 API | +| `admin-web/src/api/channel.ts` | 列表 API | +| `admin-web/src/api/send.ts` | 发送列表 API | +| `admin-web/src/api/dashboard.ts` | 统计 API | +| `admin-web/src/api/inbound.ts` | 入站/轨迹 API | +| `admin-web/src/stores/auth.ts` | Pinia 登录状态 | +| `admin-web/src/views/Login.vue` | 登录页 | +| `admin-web/src/views/TenantManagement.vue` | 租户管理 | +| `admin-web/src/views/UserManagement.vue` | 用户管理 | +| `admin-web/src/views/Dashboard.vue` | 真实统计 | +| `admin-web/src/views/ChannelSubject.vue` | 列表 + 补充字段 | +| `admin-web/src/views/ChannelAccount.vue` | 列表 + 补充字段 | +| `admin-web/src/views/Conversation.vue` | 列表 + 绑定账号 | +| `admin-web/src/views/Person.vue` | 列表 | +| `admin-web/src/views/SendRecord.vue` | UX 优化 | +| `admin-web/src/views/Tag.vue` | 层级展示 | +| `admin-web/src/views/InboundFlow.vue` | Vue Flow 编辑器 | +| `admin-web/src/views/FlowTrace.vue` | 流程追踪 | +| `admin-web/src/views/MessageTest.vue` | 消息测试 | +| `admin-web/src/router/index.ts` | 登录路由 + 守卫 | +| `admin-web/src/components/Layout.vue` | 顶部显示当前用户/退出 | +| `admin-web/package.json` | 添加 @vue-flow/core、pinia | + +--- + +## Task 1: 后端 JWT 与鉴权基础设施 + +**Files:** +- Modify: `backend/mci-server/pom.xml` +- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/security/JwtTokenProvider.java` +- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java` +- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java` +- Create: `backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java` + +- [ ] **Step 1: 添加 jjwt 依赖** + +修改 `backend/mci-server/pom.xml` 的 `` 节点,添加: + +```xml + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + +``` + +- [ ] **Step 2: 创建 JwtTokenProvider** + +创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/JwtTokenProvider.java`: + +```java +package com.sino.mci.admin.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.List; + +@Component +public class JwtTokenProvider { + + @Value("${mci.jwt.secret:sino-mci-default-secret-key-at-least-32-bytes}") + private String secret; + + @Value("${mci.jwt.expiration-ms:86400000}") + private long expirationMs; + + private SecretKey getKey() { + return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String tenantCode, Long userId, String username, List roleCodes) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + expirationMs); + return Jwts.builder() + .subject(String.valueOf(userId)) + .claim("tenantCode", tenantCode) + .claim("username", username) + .claim("roleCodes", roleCodes) + .issuedAt(now) + .expiration(expiry) + .signWith(getKey()) + .compact(); + } + + public Claims parseToken(String token) { + return Jwts.parser() + .verifyWith(getKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public boolean validateToken(String token) { + try { + parseToken(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } +} +``` + +- [ ] **Step 3: 创建 AuthContext** + +创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthContext.java`: + +```java +package com.sino.mci.admin.security; + +import lombok.Data; + +public class AuthContext { + + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + @Data + public static class AuthUser { + private Long userId; + private String tenantCode; + private String username; + private java.util.List roleCodes; + } + + public static void set(AuthUser user) { + CURRENT.set(user); + } + + public static AuthUser get() { + return CURRENT.get(); + } + + public static void clear() { + CURRENT.remove(); + } + + public static String currentTenantCode() { + AuthUser user = get(); + return user == null ? null : user.getTenantCode(); + } +} +``` + +- [ ] **Step 4: 创建 AuthInterceptor** + +创建 `backend/mci-server/src/main/java/com/sino/mci/admin/security/AuthInterceptor.java`: + +```java +package com.sino.mci.admin.security; + +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class AuthInterceptor implements HandlerInterceptor { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String path = request.getRequestURI(); + if (path.contains("/api/v1/account/login") || path.contains("/api/v1/account/tenant")) { + return true; + } + + String header = request.getHeader("Authorization"); + if (header == null || !header.startsWith("Bearer ")) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + String token = header.substring(7); + if (!jwtTokenProvider.validateToken(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + Claims claims = jwtTokenProvider.parseToken(token); + AuthContext.AuthUser user = new AuthContext.AuthUser(); + user.setUserId(Long.valueOf(claims.getSubject())); + user.setTenantCode(claims.get("tenantCode", String.class)); + user.setUsername(claims.get("username", String.class)); + user.setRoleCodes(claims.get("roleCodes", List.class)); + AuthContext.set(user); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + AuthContext.clear(); + } +} +``` + +- [ ] **Step 5: 注册拦截器** + +创建 `backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java`: + +```java +package com.sino.mci.config; + +import com.sino.mci.admin.security.AuthInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final AuthInterceptor authInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor) + .addPathPatterns("/msg-platform/api/v1/**", "/api/v1/**") + .excludePathPatterns("/msg-platform/api/v1/account/login", "/api/v1/account/login"); + } +} +``` + +- [ ] **Step 6: 编译验证** + +Run: +```bash +cd /Users/marsal/Projects/sino-project/sino-mci/backend +JAVA_HOME=/opt/homebrew/opt/openjdk@21 mvn -f mci-server/pom.xml compile -DskipTests +``` + +Expected: BUILD SUCCESS + +- [ ] **Step 7: 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/admin/security/ backend/mci-server/src/main/java/com/sino/mci/config/WebConfig.java +git commit -m "feat(auth): add JWT provider, auth context and interceptor" +``` + +--- + +## Task 2: 后端登录与账号管理 API 改造 + +**Files:** +- Modify: `backend/mci-server/src/main/java/com/sino/mci/admin/dto/LoginResponse.java` +- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/dto/TokenResponse.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java` +- Create: `backend/mci-server/src/main/resources/db/migration/V9__add_user_role_platform_admin.sql` + +- [ ] **Step 1: 创建 TokenResponse** + +创建 `backend/mci-server/src/main/java/com/sino/mci/admin/dto/TokenResponse.java`: + +```java +package com.sino.mci.admin.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TokenResponse { + private String token; + private LoginResponse userInfo; +} +``` + +- [ ] **Step 2: 改造 AccountService.login 返回 TokenResponse** + +修改 `backend/mci-server/src/main/java/com/sino/mci/admin/service/AccountService.java`: + +注入 `JwtTokenProvider`: + +```java +import com.sino.mci.admin.security.JwtTokenProvider; + +private final JwtTokenProvider jwtTokenProvider; +``` + +构造器已使用 `@RequiredArgsConstructor`,无需改动构造器。 + +改造 `login` 方法: + +```java +public TokenResponse 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 UnauthorizedException("用户名或密码错误"); + } + + LoginResponse userInfo = new LoginResponse(); + userInfo.setId(user.getId()); + userInfo.setTenantCode(user.getTenantCode()); + userInfo.setUsername(user.getUsername()); + userInfo.setRealName(user.getRealName()); + userInfo.setPhone(user.getPhone()); + userInfo.setRoleCodes(loadRoleCodes(user.getId())); + + String token = jwtTokenProvider.generateToken( + user.getTenantCode(), user.getId(), user.getUsername(), userInfo.getRoleCodes()); + return new TokenResponse(token, userInfo); +} +``` + +- [ ] **Step 3: 新增 list 方法到 AccountService** + +在 `AccountService.java` 末尾添加: + +```java +public List listTenants() { + return tenantMapper.selectList(new LambdaQueryWrapper().eq(Tenant::getStatus, 1)); +} + +public List listUsers(String tenantCode) { + return userMapper.selectList( + new LambdaQueryWrapper() + .eq(PlatformUser::getTenantCode, tenantCode) + .eq(PlatformUser::getStatus, 1)); +} +``` + +- [ ] **Step 4: 改造 AccountController** + +修改 `backend/mci-server/src/main/java/com/sino/mci/admin/controller/AccountController.java`: + +```java +package com.sino.mci.admin.controller; + +import com.sino.mci.admin.dto.*; +import com.sino.mci.admin.entity.Tenant; +import com.sino.mci.admin.security.AuthContext; +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.*; + +import java.util.List; + +@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)); + } + + @GetMapping("/tenant/list") + public ApiResponse> listTenants() { + return ApiResponse.ok(accountService.listTenants()); + } + + @PostMapping("/user") + public ApiResponse createUser( + @RequestHeader("X-Tenant-Code") String tenantCode, + @Valid @RequestBody CreateUserRequest request) { + return ApiResponse.ok(accountService.createUser(tenantCode, request)); + } + + @GetMapping("/user/list") + public ApiResponse> listUsers() { + return ApiResponse.ok(accountService.listUsers(AuthContext.currentTenantCode())); + } + + @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())); + } + + @GetMapping("/me") + public ApiResponse me() { + return ApiResponse.ok(AuthContext.get()); + } +} +``` + +- [ ] **Step 5: 新增 PlatformUserResponse** + +创建 `backend/mci-server/src/main/java/com/sino/mci/admin/dto/PlatformUserResponse.java`: + +```java +package com.sino.mci.admin.dto; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class PlatformUserResponse { + private Long id; + private String tenantCode; + private String username; + private String realName; + private String phone; + private Integer status; + private List roleCodes; + private LocalDateTime createTime; +} +``` + +并调整 `AccountService.listUsers` 返回 `List`,使用 `toUserResponse` 转换。 + +- [ ] **Step 6: 添加平台管理员角色种子数据** + +创建 `backend/mci-server/src/main/resources/db/migration/V9__add_user_role_platform_admin.sql`: + +```sql +INSERT IGNORE INTO mci_platform_role (role_code, role_name, permissions) VALUES +('platform_admin', '平台管理员', '["tenant:manage", "user:manage", "*"]'); +``` + +- [ ] **Step 7: 运行测试** + +Run: +```bash +cd /Users/marsal/Projects/sino-project/sino-mci/backend/mci-server +JAVA_HOME=/opt/homebrew/opt/openjdk@21 mvn test +``` + +Expected: 7 tests pass + +- [ ] **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/main/resources/db/migration/V9__add_user_role_platform_admin.sql +git commit -m "feat(auth): return JWT on login and add tenant/user list APIs" +``` + +--- + +## Task 3: 后端列表接口与 Dashboard 统计 + +**Files:** +- Modify: `backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelSubjectController.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/channel/service/ChannelSubjectService.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/channel/controller/ChannelAccountController.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/channel/service/ChannelAccountService.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/channel/controller/ConversationController.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/channel/service/ConversationService.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/crm/controller/PersonController.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/crm/service/PersonService.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/send/controller/SendController.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/send/service/SendService.java` +- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/controller/DashboardController.java` +- Create: `backend/mci-server/src/main/java/com/sino/mci/admin/dto/DashboardStatsResponse.java` + +- [ ] **Step 1: ChannelSubject 列表** + +修改 `ChannelSubjectController.java`: + +```java +@GetMapping("/list") +public ApiResponse> listSubjects( + @RequestHeader("X-Tenant-Code") String tenantCode) { + return ApiResponse.ok(subjectService.listSubjects(tenantCode)); +} +``` + +修改 `ChannelSubjectService.java`: + +```java +public List listSubjects(String tenantCode) { + return subjectMapper.selectList( + new LambdaQueryWrapper() + .eq(ChannelSubject::getTenantCode, tenantCode) + .eq(ChannelSubject::getStatus, 1) + .orderByDesc(ChannelSubject::getId)); +} +``` + +- [ ] **Step 2: ChannelAccount 列表** + +修改 `ChannelAccountController.java`: + +```java +@GetMapping("/list") +public ApiResponse> listAccounts( + @RequestHeader("X-Tenant-Code") String tenantCode) { + return ApiResponse.ok(accountService.listAccounts(tenantCode)); +} +``` + +修改 `ChannelAccountService.java`: + +```java +public List listAccounts(String tenantCode) { + return accountMapper.selectList( + new LambdaQueryWrapper() + .eq(ChannelAccount::getTenantCode, tenantCode) + .eq(ChannelAccount::getStatus, 1) + .orderByDesc(ChannelAccount::getId)); +} +``` + +- [ ] **Step 3: Conversation 列表** + +修改 `ConversationController.java`: + +```java +@GetMapping("/list") +public ApiResponse> listConversations( + @RequestHeader("X-Tenant-Code") String tenantCode) { + return ApiResponse.ok(conversationService.listConversations(tenantCode)); +} +``` + +修改 `ConversationService.java`: + +```java +public List listConversations(String tenantCode) { + return conversationMapper.selectList( + new LambdaQueryWrapper() + .eq(Conversation::getTenantCode, tenantCode) + .eq(Conversation::getStatus, 1) + .orderByDesc(Conversation::getId)); +} +``` + +- [ ] **Step 4: Person 列表** + +修改 `PersonController.java`: + +```java +@GetMapping("/list") +public ApiResponse> listPersons( + @RequestHeader("X-Tenant-Code") String tenantCode) { + return ApiResponse.ok(personService.listPersons(tenantCode)); +} +``` + +修改 `PersonService.java`: + +```java +public List listPersons(String tenantCode) { + return personMapper.selectList( + new LambdaQueryWrapper() + .eq(Person::getTenantCode, tenantCode) + .eq(Person::getStatus, 1) + .orderByDesc(Person::getId)); +} +``` + +- [ ] **Step 5: SendRecord 列表** + +修改 `SendController.java`: + +```java +@GetMapping("/list") +public ApiResponse> listSendRecords( + @RequestHeader("X-Tenant-Code") String tenantCode) { + return ApiResponse.ok(sendService.listRecentRecords(tenantCode)); +} +``` + +修改 `SendService.java`: + +```java +public List listRecentRecords(String tenantCode) { + List records = sendRecordMapper.selectList( + new LambdaQueryWrapper() + .eq(SendRecord::getTenantCode, tenantCode) + .orderByDesc(SendRecord::getId) + .last("LIMIT 50")); + return records.stream().map(this::toResponse).collect(Collectors.toList()); +} +``` + +- [ ] **Step 6: Dashboard 统计** + +创建 `backend/mci-server/src/main/java/com/sino/mci/admin/dto/DashboardStatsResponse.java`: + +```java +package com.sino.mci.admin.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DashboardStatsResponse { + private long subjectCount; + private long accountCount; + private long conversationCount; + private long sentToday; +} +``` + +创建 `backend/mci-server/src/main/java/com/sino/mci/admin/controller/DashboardController.java`: + +```java +package com.sino.mci.admin.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.sino.mci.admin.dto.DashboardStatsResponse; +import com.sino.mci.admin.security.AuthContext; +import com.sino.mci.channel.entity.ChannelAccount; +import com.sino.mci.channel.entity.ChannelSubject; +import com.sino.mci.channel.entity.Conversation; +import com.sino.mci.channel.repository.ChannelAccountMapper; +import com.sino.mci.channel.repository.ChannelSubjectMapper; +import com.sino.mci.channel.repository.ConversationMapper; +import com.sino.mci.send.entity.SendRecord; +import com.sino.mci.send.repository.SendRecordMapper; +import com.sino.mci.shared.web.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@RestController +@RequestMapping("/api/v1/dashboard") +@RequiredArgsConstructor +public class DashboardController { + + private final ChannelSubjectMapper subjectMapper; + private final ChannelAccountMapper accountMapper; + private final ConversationMapper conversationMapper; + private final SendRecordMapper sendRecordMapper; + + @GetMapping("/stats") + public ApiResponse stats() { + String tenantCode = AuthContext.currentTenantCode(); + LocalDateTime startOfDay = LocalDateTime.of(LocalDate.now(), LocalTime.MIN); + LocalDateTime endOfDay = LocalDateTime.of(LocalDate.now(), LocalTime.MAX); + + long subjects = subjectMapper.selectCount( + new LambdaQueryWrapper() + .eq(ChannelSubject::getTenantCode, tenantCode) + .eq(ChannelSubject::getStatus, 1)); + long accounts = accountMapper.selectCount( + new LambdaQueryWrapper() + .eq(ChannelAccount::getTenantCode, tenantCode) + .eq(ChannelAccount::getStatus, 1)); + long conversations = conversationMapper.selectCount( + new LambdaQueryWrapper() + .eq(Conversation::getTenantCode, tenantCode) + .eq(Conversation::getStatus, 1)); + long sentToday = sendRecordMapper.selectCount( + new LambdaQueryWrapper() + .eq(SendRecord::getTenantCode, tenantCode) + .between(SendRecord::getSendTime, startOfDay, endOfDay)); + + return ApiResponse.ok(new DashboardStatsResponse(subjects, accounts, conversations, sentToday)); + } +} +``` + +- [ ] **Step 7: 运行测试** + +Run: +```bash +cd /Users/marsal/Projects/sino-project/sino-mci/backend/mci-server +JAVA_HOME=/opt/homebrew/opt/openjdk@21 mvn test +``` + +Expected: 7 tests pass + +- [ ] **Step 8: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add backend/mci-server/src/main/java/com/sino/mci/ +git commit -m "feat(api): add list endpoints and dashboard stats" +``` + +--- + +## Task 4: 入站流程执行轨迹 + +**Files:** +- Create: `backend/mci-server/src/main/resources/db/migration/V8__add_flow_trace.sql` +- Create: `backend/mci-server/src/main/java/com/sino/mci/inbound/entity/FlowExecutionTrace.java` +- Create: `backend/mci-server/src/main/java/com/sino/mci/inbound/repository/FlowExecutionTraceMapper.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/inbound/engine/FlowEngine.java` +- Modify: `backend/mci-server/src/main/java/com/sino/mci/inbound/controller/InboundMessageController.java` + +- [ ] **Step 1: 创建轨迹表迁移** + +创建 `backend/mci-server/src/main/resources/db/migration/V8__add_flow_trace.sql`: + +```sql +CREATE TABLE IF NOT EXISTS mci_flow_execution_trace ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + event_id VARCHAR(128) NOT NULL COMMENT '入站事件ID', + flow_definition_id BIGINT COMMENT '流程定义ID', + node_type VARCHAR(64) NOT NULL COMMENT '节点类型', + node_config JSON COMMENT '节点配置快照', + input TEXT COMMENT '节点输入', + output TEXT COMMENT '节点输出', + status TINYINT NOT NULL DEFAULT 1 COMMENT '1成功 2失败', + duration_ms INT COMMENT '执行耗时毫秒', + error_msg VARCHAR(512) COMMENT '错误信息', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY idx_event_id (event_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程执行轨迹表'; +``` + +- [ ] **Step 2: 创建实体与 Mapper** + +创建 `FlowExecutionTrace.java`: + +```java +package com.sino.mci.inbound.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("mci_flow_execution_trace") +public class FlowExecutionTrace { + private Long id; + private String eventId; + private Long flowDefinitionId; + private String nodeType; + private String nodeConfig; + private String input; + private String output; + private Integer status; + private Integer durationMs; + private String errorMsg; + private LocalDateTime createTime; +} +``` + +创建 `FlowExecutionTraceMapper.java`: + +```java +package com.sino.mci.inbound.repository; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.sino.mci.inbound.entity.FlowExecutionTrace; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FlowExecutionTraceMapper extends BaseMapper { +} +``` + +- [ ] **Step 3: FlowEngine 记录轨迹** + +修改 `FlowEngine.java`,注入 `FlowExecutionTraceMapper`。 + +当前 `FlowEngine.executeInboundFlow` 实际执行固定三步:意图识别、Webhook 推送、标记已处理。轨迹按以下固定节点记录: + +```java +private final FlowExecutionTraceMapper traceMapper; + +private void trace(String eventId, Long flowDefinitionId, String nodeType, + String input, String output, int status, long durationMs, String errorMsg) { + FlowExecutionTrace trace = new FlowExecutionTrace(); + trace.setEventId(eventId); + trace.setFlowDefinitionId(flowDefinitionId); + trace.setNodeType(nodeType); + trace.setInput(input); + trace.setOutput(output); + trace.setStatus(status); + trace.setDurationMs((int) durationMs); + trace.setErrorMsg(errorMsg); + traceMapper.insert(trace); +} +``` + +改造 `executeInboundFlow`: + +```java +public void executeInboundFlow(Long eventId) { + MessageEvent event = messageEventMapper.selectById(eventId); + if (event == null) { + log.warn("Message event not found: {}", eventId); + return; + } + + FlowDefinition flow = findPublishedFlow(event.getTenantCode()); + Long flowDefinitionId = flow == null ? null : flow.getId(); + long flowStart = System.currentTimeMillis(); + trace(event.getEventId(), flowDefinitionId, "flow_start", null, + "tenant=" + event.getTenantCode(), 1, 0, null); + + if (flow == null) { + log.info("No published inbound flow for tenant {}, skipping flow execution", event.getTenantCode()); + markProcessed(event, flowDefinitionId); + return; + } + + try { + long intentStart = System.currentTimeMillis(); + IntentRecognitionService.IntentResult intent = intentRecognitionService.recognize(event); + trace(event.getEventId(), flowDefinitionId, "intent_recognition", + event.getContent(), JsonUtils.toJson(intent), 1, + System.currentTimeMillis() - intentStart, null); + + long webhookStart = System.currentTimeMillis(); + webhookPushService.pushToBusiness(event.getTenantCode(), event, intent); + trace(event.getEventId(), flowDefinitionId, "webhook_push", + JsonUtils.toJson(intent), "pushed", 1, + System.currentTimeMillis() - webhookStart, null); + + markProcessed(event, flowDefinitionId); + } catch (Exception e) { + log.error("Flow execution failed for event {}", eventId, e); + trace(event.getEventId(), flowDefinitionId, "webhook_push", + event.getContent(), null, 2, System.currentTimeMillis() - flowStart, + e.getMessage()); + } +} + +private void markProcessed(MessageEvent event, Long flowDefinitionId) { + event.setIsProcessed(1); + messageEventMapper.updateById(event); + trace(event.getEventId(), flowDefinitionId, "mark_processed", null, "done", 1, 0, null); +} +``` + +同时在 `InboundMessageService.receiveMessage` 保存消息后、调用 `FlowEngine` 前记录 `receive` 节点: + +```java +private final FlowExecutionTraceMapper traceMapper; + +// 在 messageEventMapper.insert(event); 之后 +FlowExecutionTrace receiveTrace = new FlowExecutionTrace(); +receiveTrace.setEventId(event.getEventId()); +receiveTrace.setNodeType("receive"); +receiveTrace.setInput(JsonUtils.toJson(request)); +receiveTrace.setOutput("event_id=" + event.getId()); +receiveTrace.setStatus(1); +traceMapper.insert(receiveTrace); +``` + +这样单条事件轨迹为:`receive -> flow_start -> intent_recognition -> webhook_push -> mark_processed`。 + +- [ ] **Step 4: 新增 trace 查询接口** + +修改 `InboundMessageController.java`: + +```java +@GetMapping("/{eventId}/trace") +public ApiResponse> trace(@PathVariable String eventId) { + return ApiResponse.ok(inboundMessageService.listTraces(eventId)); +} +``` + +在 `InboundMessageService.java` 新增: + +```java +public List listTraces(String eventId) { + return traceMapper.selectList( + new LambdaQueryWrapper() + .eq(FlowExecutionTrace::getEventId, eventId) + .orderByAsc(FlowExecutionTrace::getId)); +} +``` + +- [ ] **Step 5: 运行测试** + +Run: +```bash +cd /Users/marsal/Projects/sino-project/sino-mci/backend/mci-server +JAVA_HOME=/opt/homebrew/opt/openjdk@21 mvn test +``` + +Expected: 7 tests pass + +- [ ] **Step 6: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add backend/mci-server/src/main/java/com/sino/mci/inbound/ backend/mci-server/src/main/resources/db/migration/V8__add_flow_trace.sql +git commit -m "feat(inbound): add flow execution trace recording and query" +``` + +--- + +## Task 5: 前端依赖与 API 层改造 + +**Files:** +- Modify: `admin-web/package.json` +- Modify: `admin-web/src/api/client.ts` +- Modify: `admin-web/src/api/account.ts` +- Modify: `admin-web/src/api/channel.ts` +- Modify: `admin-web/src/api/crm.ts` +- Modify: `admin-web/src/api/send.ts` +- Create: `admin-web/src/api/dashboard.ts` +- Create: `admin-web/src/api/inbound.ts` + +- [ ] **Step 1: 添加依赖** + +修改 `admin-web/package.json`: + +```json +"dependencies": { + "axios": "^1.18.1", + "element-plus": "^2.14.2", + "pinia": "^3.0.1", + "vue": "^3.5.39", + "vue-router": "^4.6.4", + "@vue-flow/core": "^1.42.1", + "@vue-flow/background": "^1.3.2", + "@vue-flow/controls": "^1.1.2" +} +``` + +Run: +```bash +cd /Users/marsal/Projects/sino-project/sino-mci/admin-web +npm install +``` + +- [ ] **Step 2: 改造 client.ts** + +修改 `admin-web/src/api/client.ts`: + +```typescript +import axios from 'axios' + +const client = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || '/msg-platform', + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}) + +client.interceptors.request.use((config) => { + const token = localStorage.getItem('mci-token') + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } + const tenantCode = localStorage.getItem('mci-tenant-code') + if (tenantCode) { + config.headers['X-Tenant-Code'] = tenantCode + } + return config +}) + +client.interceptors.response.use( + (response) => response.data, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('mci-token') + localStorage.removeItem('mci-tenant-code') + window.location.href = '/login' + } + const message = error.response?.data?.message || error.message || '请求失败' + return Promise.reject(new Error(message)) + } +) + +export default client +``` + +- [ ] **Step 3: 改造 account.ts** + +```typescript +import client from './client' + +export function login(data: { tenantCode: string; username: string; password: string }) { + return client.post('/api/v1/account/login', data) +} + +export function createTenant(data: { tenantCode: string; tenantName: string }) { + return client.post('/api/v1/account/tenant', data) +} + +export function listTenants() { + return client.get('/api/v1/account/tenant/list') +} + +export function createUser(data: { username: string; password: string; realName?: string; roleCodes?: string[] }) { + return client.post('/api/v1/account/user', data) +} + +export function listUsers() { + return client.get('/api/v1/account/user/list') +} + +export function me() { + return client.get('/api/v1/account/me') +} +``` + +- [ ] **Step 4: 改造 channel.ts** + +```typescript +import client from './client' + +export function createChannelSubject(data: any) { + return client.post('/api/v1/channel-subject', data) +} + +export function listChannelSubjects() { + return client.get('/api/v1/channel-subject/list') +} + +export function createChannelAccount(data: any) { + return client.post('/api/v1/channel-account', data) +} + +export function listChannelAccounts() { + return client.get('/api/v1/channel-account/list') +} + +export function initChannelAccount(data: any) { + return client.post('/api/v1/channel-account/init', data) +} + +export function createConversation(data: any) { + return client.post('/api/v1/conversation', data) +} + +export function listConversations() { + return client.get('/api/v1/conversation/list') +} + +export function bindConversationAccount(conversationId: number, data: any) { + return client.post(`/api/v1/conversation/${conversationId}/bind-account`, data) +} + +export function bindConversationTags(conversationId: number, data: any) { + return client.post(`/api/v1/conversation/${conversationId}/bind-tags`, data) +} +``` + +- [ ] **Step 5: 改造 crm.ts** + +```typescript +import client from './client' + +export function createPerson(data: any) { + return client.post('/api/v1/person', data) +} + +export function listPersons() { + return client.get('/api/v1/person/list') +} + +export function listTags() { + return client.get('/api/v1/tag/list') +} + +export function createTag(data: any) { + return client.post('/api/v1/tag', data) +} +``` + +- [ ] **Step 6: 改造 send.ts** + +```typescript +import client from './client' + +export function sendMessage(data: any) { + return client.post('/api/v1/send', data) +} + +export function testSendMessage(data: any) { + return client.post('/api/v1/send/test', data) +} + +export function listSendRecords(taskId?: string) { + if (taskId) { + return client.get(`/api/v1/send/${taskId}/records`) + } + return client.get('/api/v1/send/list') +} +``` + +- [ ] **Step 7: 创建 dashboard.ts 和 inbound.ts** + +`admin-web/src/api/dashboard.ts`: + +```typescript +import client from './client' + +export function getDashboardStats() { + return client.get('/api/v1/dashboard/stats') +} +``` + +`admin-web/src/api/inbound.ts`: + +```typescript +import client from './client' + +export function receiveMessage(data: any) { + return client.post('/api/v1/inbound/receive', data) +} + +export function getFlowTraces(eventId: string) { + return client.get(`/api/v1/inbound/${eventId}/trace`) +} +``` + +`admin-web/src/api/flow.ts`(供 Vue Flow 编辑器调用): + +```typescript +import client from './client' + +export function createFlowDefinition(data: any) { + return client.post('/api/v1/inbound/flow', data) +} + +export function listFlowDefinitions() { + return client.get('/api/v1/inbound/flow/list') +} +``` + +- [ ] **Step 8: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add admin-web/package.json admin-web/package-lock.json admin-web/src/api/ +git commit -m "feat(web): add pinia/vue-flow deps and update API layer" +``` + +--- + +## Task 6: 前端登录与路由守卫 + +**Files:** +- Create: `admin-web/src/stores/auth.ts` +- Create: `admin-web/src/views/Login.vue` +- Modify: `admin-web/src/router/index.ts` +- Modify: `admin-web/src/components/Layout.vue` + +- [ ] **Step 1: 创建 Pinia auth store** + +创建 `admin-web/src/stores/auth.ts`: + +```typescript +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('mci-token') || '') + const tenantCode = ref(localStorage.getItem('mci-tenant-code') || '') + const username = ref('') + const roleCodes = ref([]) + + const isLoggedIn = computed(() => !!token.value) + + function setAuth(t: string, tc: string, user: string, roles: string[]) { + token.value = t + tenantCode.value = tc + username.value = user + roleCodes.value = roles + localStorage.setItem('mci-token', t) + localStorage.setItem('mci-tenant-code', tc) + } + + function clearAuth() { + token.value = '' + tenantCode.value = '' + username.value = '' + roleCodes.value = [] + localStorage.removeItem('mci-token') + localStorage.removeItem('mci-tenant-code') + } + + return { token, tenantCode, username, roleCodes, isLoggedIn, setAuth, clearAuth } +}) +``` + +- [ ] **Step 2: 注册 Pinia** + +修改 `admin-web/src/main.ts`,在现有 Element Plus 注册基础上加入 Pinia: + +```typescript +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import './style.css' +import App from './App.vue' +import router from './router' + +const app = createApp(App) +app.use(createPinia()) +app.use(ElementPlus) +app.use(router) +app.mount('#app') +``` + +- [ ] **Step 3: 创建 Login.vue** + +创建 `admin-web/src/views/Login.vue`: + +```vue + + + + + +``` + +- [ ] **Step 4: 改造路由** + +修改 `admin-web/src/router/index.ts`: + +```typescript +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '@/stores/auth' +import Layout from '../components/Layout.vue' +import Dashboard from '../views/Dashboard.vue' +import ChannelSubject from '../views/ChannelSubject.vue' +import ChannelAccount from '../views/ChannelAccount.vue' +import Conversation from '../views/Conversation.vue' +import Person from '../views/Person.vue' +import Tag from '../views/Tag.vue' +import SendRecord from '../views/SendRecord.vue' +import InboundFlow from '../views/InboundFlow.vue' +import FlowTrace from '../views/FlowTrace.vue' +import MessageTest from '../views/MessageTest.vue' +import TenantManagement from '../views/TenantManagement.vue' +import UserManagement from '../views/UserManagement.vue' +import Login from '../views/Login.vue' + +const routes = [ + { path: '/login', component: Login, meta: { public: true } }, + { + path: '/', + component: Layout, + redirect: '/dashboard', + children: [ + { path: 'dashboard', component: Dashboard, meta: { title: '概览' } }, + { path: 'channel-subject', component: ChannelSubject, meta: { title: '渠道主体' } }, + { path: 'channel-account', component: ChannelAccount, meta: { title: '渠道账号' } }, + { path: 'conversation', component: Conversation, meta: { title: '会话/群聊' } }, + { path: 'person', component: Person, meta: { title: '联系人' } }, + { path: 'tag', component: Tag, meta: { title: '标签' } }, + { path: 'send-record', component: SendRecord, meta: { title: '发送记录' } }, + { path: 'inbound-flow', component: InboundFlow, meta: { title: '入站流程' } }, + { path: 'flow-trace', component: FlowTrace, meta: { title: '流程追踪' } }, + { path: 'message-test', component: MessageTest, meta: { title: '消息测试' } }, + { path: 'tenant-management', component: TenantManagement, meta: { title: '租户管理' } }, + { path: 'user-management', component: UserManagement, meta: { title: '用户管理' } }, + ], + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +router.beforeEach((to, from, next) => { + const authStore = useAuthStore() + if (!to.meta.public && !authStore.isLoggedIn) { + next('/login') + } else if (to.path === '/login' && authStore.isLoggedIn) { + next('/dashboard') + } else { + next() + } +}) + +export default router +``` + +- [ ] **Step 5: 改造 Layout.vue 显示用户和退出** + +修改 `admin-web/src/components/Layout.vue`(若不存在则创建): + +```vue + + + + + +``` + +- [ ] **Step 6: 构建验证** + +Run: +```bash +cd /Users/marsal/Projects/sino-project/sino-mci/admin-web +npm test +``` + +Expected: BUILD SUCCESS + +- [ ] **Step 7: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add admin-web/src/stores/ admin-web/src/views/Login.vue admin-web/src/router/index.ts admin-web/src/components/Layout.vue admin-web/src/main.ts +git commit -m "feat(web): add login, auth store and router guard" +``` + +--- + +## Task 7: 前端租户/用户管理页 + +**Files:** +- Create: `admin-web/src/views/TenantManagement.vue` +- Create: `admin-web/src/views/UserManagement.vue` + +- [ ] **Step 1: 创建 TenantManagement.vue** + +```vue + + + +``` + +- [ ] **Step 2: 创建 UserManagement.vue** + +```vue + + + +``` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add admin-web/src/views/TenantManagement.vue admin-web/src/views/UserManagement.vue +git commit -m "feat(web): add tenant and user management pages" +``` + +--- + +## Task 8: 前端 Dashboard 与各列表页 + +**Files:** +- Modify: `admin-web/src/views/Dashboard.vue` +- Modify: `admin-web/src/views/ChannelSubject.vue` +- Modify: `admin-web/src/views/ChannelAccount.vue` +- Modify: `admin-web/src/views/Conversation.vue` +- Modify: `admin-web/src/views/Person.vue` +- Modify: `admin-web/src/views/Tag.vue` + +- [ ] **Step 1: Dashboard.vue** + +```vue + + + + + +``` + +- [ ] **Step 2: ChannelSubject.vue 改造** + +补充表单字段并加载列表: + +```vue + + + +``` + +- [ ] **Step 3: ChannelAccount.vue 改造** + +补充 subjectId、sessionArchiveEnabled 并加载列表: + +```vue + + + +``` + +- [ ] **Step 4: Conversation.vue 改造** + +补充 ownerAccountId 和绑定账号弹窗: + +```vue + + + +``` + +- [ ] **Step 5: Person.vue 改造** + +加载人员列表: + +```vue + + + +``` + +- [ ] **Step 6: Tag.vue 改造** + +树形展示层级: + +```vue + + + +``` + +- [ ] **Step 7: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add admin-web/src/views/ +git commit -m "feat(web): load real data in dashboard and list pages" +``` + +--- + +## Task 9: 发送记录页 UX 优化 + +**Files:** +- Modify: `admin-web/src/views/SendRecord.vue` + +- [ ] **Step 1: 优化 SendRecord.vue** + +```vue + + + +``` + +- [ ] **Step 2: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add admin-web/src/views/SendRecord.vue +git commit -m "feat(web): improve send record UX with dropdowns" +``` + +--- + +## Task 10: 入站流程可视化编辑器与追踪 + +**Files:** +- Modify: `admin-web/src/views/InboundFlow.vue` +- Create: `admin-web/src/views/FlowTrace.vue` + +- [ ] **Step 1: 安装 Vue Flow** + +已在 Task 5 添加依赖,无需重复。 + +- [ ] **Step 2: 改造 InboundFlow.vue** + +使用 Vue Flow 实现流程图编辑: + +```vue + + + +``` + +说明:后端 `FlowDefinition.definitionJson` 为字符串字段,但在 `CreateFlowRequest` 中对应 `Map`,前端按 `{ nodes, edges }` 结构提交,由后端序列化为 JSON 字符串持久化。 + +- [ ] **Step 3: 创建 FlowTrace.vue** + +```vue + + + +``` + +- [ ] **Step 4: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add admin-web/src/views/InboundFlow.vue admin-web/src/views/FlowTrace.vue +git commit -m "feat(web): add Vue Flow editor and flow trace viewer" +``` + +--- + +## Task 11: 消息测试模块 + +**Files:** +- Create: `admin-web/src/views/MessageTest.vue` + +- [ ] **Step 1: 创建 MessageTest.vue** + +```vue + + + +``` + +- [ ] **Step 2: Commit** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add admin-web/src/views/MessageTest.vue +git commit -m "feat(web): add message test module" +``` + +--- + +## Task 12: 联调与验证 + +- [ ] **Step 1: 启动基础设施** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +./scripts/local-up.sh --with-apps +``` + +- [ ] **Step 2: 创建初始租户和管理员** + +```bash +curl -X POST http://localhost/msg-platform/api/v1/account/tenant \ + -H "Content-Type: application/json" \ + -d '{"tenantCode":"platform","tenantName":"平台管理"}' + +curl -X POST http://localhost/msg-platform/api/v1/account/user \ + -H "Content-Type: application/json" \ + -H "X-Tenant-Code: platform" \ + -d '{"username":"admin","password":"admin123","realName":"平台管理员","roleCodes":["platform_admin"]}' +``` + +- [ ] **Step 3: 登录验证** + +打开 `http://localhost/login`,使用 `platform / admin / admin123` 登录,确认进入 Dashboard。 + +- [ ] **Step 4: 运行测试** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci/backend/mci-server && JAVA_HOME=/opt/homebrew/opt/openjdk@21 mvn test +cd /Users/marsal/Projects/sino-project/sino-mci/channel-service && source .venv/bin/activate && pytest +cd /Users/marsal/Projects/sino-project/sino-mci/admin-web && npm test +bash /Users/marsal/Projects/sino-project/sino-mci/scripts/test-external-api.sh +``` + +Expected: 全部通过 + +- [ ] **Step 5: Commit any fixes** + +```bash +cd /Users/marsal/Projects/sino-project/sino-mci +git add . +git commit -m "fix: integration fixes for auth and admin-web" +``` + +--- + +## 计划自检 + +**Spec coverage:** +- 登录鉴权:Task 1-2 +- 多租户管理:Task 2、Task 7 +- 管理后台列表:Task 3、Task 8 +- 会话绑定:Task 8 +- Dashboard 统计:Task 3、Task 8 +- 入站流程图:Task 4、Task 10 +- 消息测试模块:Task 11 +- 前端路由守卫:Task 6 + +**Placeholder scan:** 无 TBD/TODO/实现 later。 + +**Type consistency:** `LoginResponse` 复用现有字段;`AuthContext.AuthUser` 与 Controller 返回一致;`SendRecordResponse` 复用现有转换逻辑。