AGENTS.md 15 KB

cfc-backend

Spring Boot 2.7.18 后端服务,MyBatis-Plus ORM,JWT 认证。

WHERE TO LOOK

Task Location Notes
REST API 端点 src/main/java/com/etotem/cfc/controller/ 按功能分组: admin/, auth/, assessment/, family/, guide/, task/, reward/
业务逻辑 src/main/java/com/etotem/cfc/service/ Service 命名: XxxService.java
数据实体 src/main/java/com/etotem/cfc/entity/ 使用 MyBatis-Plus 注解
MyBatis Mapper src/main/java/com/etotem/cfc/mapper/ 接口文件,与XML映射对应
SQL 映射文件 src/main/resources/mapper/ MyBatis XML 文件
配置类 src/main/java/com/etotem/cfc/config/ JWT, Security, Swagger, DatabaseInitializer
DTO传输对象 src/main/java/com/etotem/cfc/dto/ 请求/响应数据结构
数据迁移 src/main/java/com/etotem/cfc/service/DataMigrationService.java sfms → cfc

CODE MAP (关键符号)

Symbol Type Location Role
AuthController Controller controller/auth/AuthController.java 微信登录、JWT 认证
TaskService Service service/TaskService.java 任务业务逻辑核心
DanAssessmentController Controller controller/DanAssessmentController.java DAN测评全链路(订单/预约/支付/结果)
AssessmentAppointmentController Controller controller/assessment/AssessmentAppointmentController.java 测评预约管理
DataMigrationService Service service/DataMigrationService.java sfms → cfc 全量数据迁移
User Entity entity/User.java 用户实体 (parent/child/teacher)
Task Entity entity/Task.java 任务实体
DanAssessmentResult Entity entity/DanAssessmentResult.java DAN测评结果(含孩子快照字段)
DatabaseInitializer Config config/DatabaseInitializer.java 数据库迁移和初始化
SfmsDataSourceConfig Config config/SfmsDataSourceConfig.java sfms 多数据源配置

CONVENTIONS

  • 分层架构: Controller → Service → Mapper,禁止跨层调用
  • ORM: MyBatis-Plus,实体使用 @TableName@TableId(type = IdType.AUTO)
  • 响应包装: 统一 Result<T> 返回,包含 code/message/data
  • 认证: JWT Token,Header Authorization: Bearer {token}
  • 命名: Controller 以 Controller 结尾,Service 以 Service 结尾
  • 控制器分组: 按功能模块分为 admin, auth, assessment, family, guide, task, reward 等子目录
  • DI注解: 使用 @Resource 代替 @Autowired,字段名必须与类型默认 Bean Name 一致(如 PaymentService 的字段名必须为 paymentService
  • 接口方法: 所有接口统一使用 @PostMapping(POST 方法),禁止使用 @GetMapping/@PutMapping/@DeleteMapping
  • 新增接口前: 必须查阅 docs/superpowers/api/API_REFERENCE.md,确认:
    1. 是否有已具备该功能的接口 → 直接使用
    2. 是否有相似接口需简单修改 → 评估修改成本
    3. 实在没有 → 才新增,并在 docs/superpowers/api/API_REFERENCE.md 同步记录
  • 废弃接口处理: 标记 @Deprecated + 返回 Result.error(410, "该接口已废弃,请使用 xxx"),待确认无调用后清理

    grep -rn '@GetMapping\|@PostMapping\|@PutMapping\|@DeleteMapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
    
  • Bean名冲突检查: 新增 Controller/Service 时,确保类名不与其他包中的类重名(Spring 默认 Bean Name 为类名首字母小写)

  • 身份标识: userId=登录账号(users.id)、memberId/familyMemberId/currentMemberId=家庭成员(family_members.id)、childId=遗留表(children.id);账号自身/交易/权限判断用 userId,成员/孩子数据(任务/健康/成长/关系/五维能量)用 memberId;JwtInterceptoruserId+familyId 自动反查 currentMemberId(始终是登录账号自己),切换视角不改变 currentMemberId,靠前端显式传 memberId 参数覆盖;新功能禁止新增 childId 引用(详见 docs/architecture/userid-vs-memberid-usage-guide.md

ANTI-PATTERNS (本项目禁止)

  • DO NOT 在 Controller 直接操作 Mapper
  • DO NOT 跳过 Service 层直接调用 Mapper
  • DO NOT 硬编码数据库连接,使用 application.yml
  • NEVER 在代码中硬编码 JWT 密钥
  • NEVER 使用 @ts-ignoreas any 绕过类型检查
  • NEVER 跳过 JWT 认证直接调用需登录接口
  • NEVER 使用 @Resource(name="xxx") 与字段名不一致的注入方式,保持字段名与类型默认 Bean Name 一致
  • NEVER 使用 @GetMapping/@PutMapping/@DeleteMapping — 统一使用 @PostMapping
  • NEVER 直接 new XxxMapper() — Mapper 必须通过 @Resource 注入
  • NEVER 使用 awk '!seen[$0]++' 对 Java 源文件去重 — 会删除合法的重复行(如多个方法的 } 闭合括号)

UNIQUE STYLES

  • 双角色系统:家长端/孩子端,登录后根据 role 字段分流,家长端可切孩子端,切换过来的孩子端可退出切换,即时出需要密码。直接登录的孩子端不能切换到家长 端
  • 成长规划师角色系统:成长规划师是个单独的角色,经过认证的成长规划师可以以成长规划师身份登录,成长规划师可切换自己作为家长端
  • 积分机制:完成任务加分、超时扣分,逻辑在 PointsService
  • 心愿审批流程:孩子创建 → 家长审批 → 扣减积分
  • 测评订单:家长申请 → 选择规划师 → 孩子信息快照 → 支付 → 规划师录入结果
  • 街道地址匹配:四级地址结构(省市区街道),区域回退匹配逻辑
  • 孩子快照:每次测评时保存孩子身高/体重/学校等动态信息到 DanAssessmentResult
  • 多数据源:cfc 主库 spring.datasource.* + sfms 源库 sfms.datasource.*(只读)

DATABASE MIGRATION WORKFLOW

当出现 Unknown column 'xxx' in 'field list' 等字段不存在错误,或需要进行表结构变更时,按以下流程处理:

工作流程

错误日志定位 → 查找实体类 → 检查 schema.sql → 添加迁移 → 同步 schema.sql → 编译验证

Step 1: 诊断错误

后端日志中出现类似以下错误:

Unknown column 'xxx' in 'field list'
Table 'xxx' doesn't exist

从错误信息中提取:表名、列名/对象名,并从异常堆栈找到触发点(哪个 Service/Controller)。

Step 2: 查找实体与建表 SQL

  • entity/ 目录找到对应实体类,确认字段已定义(@TableField 或成员变量)
  • src/main/resources/schema.sql 中找到该表的 CREATE TABLE 语句,确认列是否已包含

Step 3: 判断修复位置

情况 修复方式
schema.sql 建表语句有该列,但生产库没有(旧表) 只需在 DatabaseInitializer.runMigrations() 添加 ALTER TABLE
schema.sql 没有该列,实体也没有 在实体添加字段 + schema.sql 建表语句补列 + DatabaseInitializer 加迁移
全新表 schema.sql 添加 CREATE TABLE + DatabaseInitializer 的 runMigrations() 加迁移
仅调整约束/类型/默认值 DatabaseInitializer.runMigrations() 加 MODIFY COLUMN

Step 4: 在 DatabaseInitializer 添加迁移

DatabaseInitializer.javarunMigrations() 方法是所有 DDL 迁移的唯一入口。

添加列(首选 ensureColumn 辅助方法)

ensureColumn(table, column, definition) 是已有的辅助方法(第 4099 行),自动处理重复列异常:

// 迁移N: xxx表添加yyy字段
ensureColumn("table_name", "column_name", "VARCHAR(50) COMMENT '字段说明'");

ensureColumn 不可用时(如需要 MODIFY / 多个列在同一个 try 块),使用标准模式:

// 迁移N: xxx表添加yyy字段(原因描述/需求编号)
try {
    jdbcTemplate.execute("ALTER TABLE xxx ADD COLUMN yyy VARCHAR(50) COMMENT '字段说明'");
    log.info("已添加yyy列到xxx表");
} catch (Exception e) {
    // 列已存在,忽略错误
}

创建新表

// 迁移N: 创建 xxx 表(功能描述)
try {
    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS xxx (" +
            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
            "...字段定义..." +
            "INDEX idx_xxx (column)" +
            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表注释'");
    log.info("已创建xxx表");
} catch (Exception e) {
    // 表已存在,忽略错误
}

添加索引

try {
    jdbcTemplate.execute("ALTER TABLE xxx ADD INDEX idx_xxx (column)");
    log.info("已添加idx_xxx索引到xxx表");
} catch (Exception e) {
    // 索引已存在,忽略错误
}

修改字段

try {
    jdbcTemplate.execute("ALTER TABLE xxx MODIFY COLUMN yyy VARCHAR(100) COMMENT '新注释'");
    log.info("已修改xxx.yyy字段");
} catch (Exception e) {
    // 修改失败,忽略错误
}

Step 5: 同步 schema.sql

schema.sql 中保持该表最新的完整定义:

  • 新增列:在 schema.sql 对应表的 CREATE TABLE 语句中也加上该列定义(保持建表即包含所有列)
  • 新建表:在 schema.sql 追加该表的 CREATE TABLE IF NOT EXISTS 语句
  • 仅修改约束/默认值:无需更新 schema.sql
  • 迁移中 CREATE TABLE IF NOT EXISTS 创建的表:也追加到 schema.sql(保持 schema.sql 为完整定义快照)

Step 6: 验证

mvn clean compile   # 编译验证通过即可

迁移代码是幂等的(try-catch 忽略已存在的列/表/索引),无需运行时验证。

规范要点

  • 迁移编号:按现有顺序递增(当前最大编号 迁移N,在文件末尾搜索 // 迁移 确认最新编号)
  • 注释格式// 迁移N: 表名 操作说明(原因/需求编号)
  • 日志:每个成功的 ALTER/CREATE 后加 log.info(),失败统一用空 catch 或 // 忽略
  • 幂等性:所有迁移必须可重复执行而不报错(try-catch 包裹 + 列存在检查)
  • 辅助方法:优先使用 ensureColumn() 添加列,它自动处理重复列异常;原始 try-catch 模式用于索引/表/非标准操作
  • 同步原则对 schema.sql 的每一条 DDL 变更,必须在 DatabaseInitializer 中有对应的迁移,反之亦然——两者保持最终一致

COMMANDS

mvn clean compile        # 编译验证(唯一验证方式)
mvn spring-boot:run      # 启动开发服务器 (localhost:9082)
mvn test                 # 运行测试 (目前只有1个测试类)

AI 服务规范(统一走 LangGraph)

  • 所有 AI 能力(对话/问卷/画像/推荐/解析)基于 cfc-langgraph Python 服务(FastAPI + LangGraph + ChromaDB RAG,端口 9000,部署到 ai.etotem.com.cn)。
  • Java 侧统一通过 AiGateway 调用(熔断 + 超时 + Fallback),禁止绕过 AiGateway 直接对接第三方 LLM
  • 新增 AI 功能优先以 LangGraph graph 形式实现(cfc-langgraph 的 app/graphs/src/ 模块)。
  • LangGraph 配置文件:application.ymllanggraph.base-url(报告解析)/ python.base-url(通用 AI)。

Python LangGraph 服务(部署到 ai.etotem.com.cn)

cd cfc-langgraph && pip install -r requirements.txt && uvicorn src.app:app --port 9000


## 工作流程

1. 修改代码后执行 `mvn clean compile` 验证编译通过
2. 编译通过即视为验证完成,不做运行时/启动验证

## NOTES

- 数据库:MySQL 8.0,**测试库**地址 `192.168.16.251:3306`(库名可能为 `zxyj`,参见根 AGENTS.md;该地址只是测试环境,**不是生产库**,生产连接信息需向运维确认)
- 后端端口:9082,JWT 密钥在 `application.yml`
- 街道数据:使用静态种子数据,街道匹配有回退机制
- API冲突已修复:检查所有Controller确保无重复路径映射
- sfms 源库连接信息在 `application.yml` 的 `sfms.datasource.*`
- 数据迁移 POST `/api/migration/run`

## 后端接口规范

- 后台/api的接口规范都用post方式

## 家庭成员列表接口规范

### 统一接口:`POST /api/family/member/list`

**唯一入口**,所有获取家庭成员信息的需求必须调用此接口。

**替代(已废弃,请勿新增调用):**
- `POST /api/family/user/family-members` — 返回 Map 格式,信息分散在 parents + children 两块
- `POST /api/family/user/members/visible` — 只返回 `show_to_family=1` 的部分
- `POST /api/family/user/children/list` — 只返回孩子,不包含家长
- `POST /api/user/children/list` — 同上,重复路由

**请求:**

json // 可选的过滤参数 { "visibleOnly": true, // 仅返回 show_to_family=1 的成员(默认false) "includeFamily": true // 是否包含家庭信息(familyId/name/inviteCode,默认false) }


**返回:** `Result<List<FamilyMemberVO>>`

**`FamilyMemberVO` 字段说明:**

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | Long | family_members.id |
| `source` | String | `family_member` 或 `parent` |
| `userId` | Long | 关联的用户ID |
| `nickname` | String | 昵称 |
| `phone` | String | 手机号 |
| `avatar` | String | 头像 |
| `gender` | String | male/female |
| `age` | Integer | 年龄(由 birthday 实时计算) |
| `effectiveRole` | String | parent/child/teacher |
| `roleLabel` | String | 关系标签(如"爸爸""妈妈""爷爷""奶奶") |
| `generation` | Integer | 辈分值 |
| `isSpouse` | Boolean | 是否配偶 |
| `isAdmin` | Boolean | 是否家庭管理员 |
| `isSelf` | Boolean | 是否当前用户自己 |
| `totalPoints` | Integer | 总积分 |
| `showToFamily` | Integer | 对家庭成员可见性(1=可见, 0=不可见) |
| `trustScore` | BigDecimal | 信任度 0-100 |
| `intimacyScore` | BigDecimal | 亲密度 0-100 |
| `communicationScore` | BigDecimal | 沟通质量 0-100 |
| `memberType` | String | 成员类型(parent/child) |

**后端实现:**
- Controller: `FamilyMembersController.listMembers()` → `FamilyMemberService.listMembers()`
- 传入 `visibleOnly=true` 时,调用 `FamilyMemberService.listMembers()` 后按 `show_to_family` 过滤
- 不传 `familyMemberId` 参数,根据 JWT 的 userId 自动获取用户的 `familyId`

**前端调用:**

js import { getFamilyMemberList } from '@/utils/api.js' // 已封装为:request('/api/family/member/list', 'POST', { visibleOnly: true }) ```

新增成员相关接口: | 功能 | 路径 | 说明 | |------|------|------| | 添加成员 | POST /api/family/member/add | 创建新家庭成员 | | 更新成员 | POST /api/family/member/update | 修改成员信息 | | 踢出成员 | POST /api/family/member/kick | 删除成员 | | 切换视角 | POST /api/family/member/switch | 切换到指定成员视图 | | 检查编辑 | POST /api/family/member/editable | 检查成员是否可编辑 | | 变更日志 | POST /api/family/member/logs | 成员变更历史 |