16 KiB
CLAUDE.md
目录结构与职责(硬性约束)
biz/是泛化占位名,不是固定目录命名。表格中biz/代表「业务模块目录」,各项目必须按自身业务命名替换(本项目即styleagent/,位于server/styleagent/),禁止新项目照抄biz/;ui-src/、data/、workspace/亦为本项目目录名,各项目按自身命名。以下表格路径均以server/为根(前端另见app-uni/)。
| 目录 | 职责 | 强约束 |
|---|---|---|
| common/ | 通用层:HTTP 服务与鉴权中间件、RouteRegister 反射路由、DAO 基类、查询缓存、锁与协程池封装、DB 组访问器、文件存储 | 不得依赖业务模块包;新增跨模块通用能力放这里 |
| styleagent/consts/ | 常量集中地:表名(table_name.go)、状态(status.go)、内容类型、默认参数与各协程池默认大小 | 业务常量一律在此集中,禁止散落 magic number;新增池默认大小在此定义 |
| styleagent/model/ | entity(表结构,与 DAO 一一对应)、dto(请求/响应结构,g.Meta 内嵌定义路由)、domain(领域模型:跨表聚合与服务层组装值,可被 dto/entity 引用) |
entity 只做表映射,不带业务逻辑;dto 是 controller 与 HTTP 的唯一出入口;domain 收纳不属于 dto 也不属于 entity 的类型(见下) |
| styleagent/dao/ | 单表数据访问,每表一个文件 | 无业务逻辑;查询经 base_dao 缓存 |
| styleagent/service/ | 业务逻辑:规则校验、文件读写、事务、跨表组装、调用 dao、LLM 编排 | 不直接写 HTTP 响应(例外见下);并行任务走 common 协程池 |
| styleagent/controller/ | 接口层:接收参数、调用 service、原样返回 service 结果 | 见「分层职责规范」;禁止调用 dao |
| 前端目录 | 前台 app-uni/ 为 uni-app (Vue 3) 多端工程(Android/iOS/微信小程序/H5);后台管理端未建 |
H5 构建产物由 server 托管(main.go WebStaticDir);多端工程差异见技术设计.md |
| 运行时数据目录 | SQLite 库、上传/生成文件(本项目 server/data/ server/workspace/) |
不提交 git;删除即丢失数据,改动前先确认 |
分层职责规范(硬性要求)
严格分层 controller → service → dao,禁止跨层调用(controller 禁止直接调 dao)。
| 层 | 目录 | 职责 | 禁止 |
|---|---|---|---|
| controller | server/styleagent/controller | 接收 dto 请求参数(依赖 DTO v tag 自动校验)调用 service,原样返回 service 结果(返回类型与 service 一致,即 dto);传参方式:整个 *dto.XxxReq 直接传给 service,禁止从 dto 拆出多个属性逐个传参 |
直接调用 dao;任何组装/映射/字段搬运;手写业务规则校验(库表依赖/跨字段,应下沉 service);文件 IO;状态流转;跨表数据组装 |
| service | server/styleagent/service | 业务逻辑:规则校验、文件读写、事务、跨表组装、调用 dao;只允许返回 dto 类型(返回与 controller 输出一致的 *dto.XxxRes),派生值(如 scene_name/node_count)在 service 用 dto 组装 |
直接写 HTTP 响应(例外见下);返回裸 gdb.Record 或任何非 dto 类型 |
| dao | server/styleagent/dao | 构建 SQL 并执行;行→结构体转换在 dao 内部用 GoFrame 自带方法(Record.Struct / Result.Structs,按 orm tag)完成,对外只允许返回 entity(或单值如 int/map) |
业务逻辑;返回裸 gdb.Record——裸 gdb.Record 不允许作为任何分层方法的返回值(含 service 内事务读),转换只发生在 dao 内部,不外泄 |
例外:SSE 流式响应、HTML/文件导出等"直接写响应体"的场景由 controller 完成——这是"值返回"的流式形式,事件序列化、心跳属 HTTP 协议职责,保留在 controller。
分层锚定原则(controller 反向锚定,防跑偏):controller 只透传 ⇒ 接口返回类型以 dto 为准 ⇒ service 返回类型被 dto 锁死 ⇒ dao 输出被 entity 锁死。任何一层若出现"为下一层做数据搬运"(controller 映射 service 结果、service 逐键取裸 Record 字段),即违反本原则,应向上收敛:转换在 dao 内部(Record→entity)、组装在 service(entity→dto)、透传在 controller。
教训(此前偏离原因,开发时引以为戒):
- 自定义中转层不得替代 dto:domain 只收"不进 HTTP 出入参"的纯领域值(如结算输入 SettleState/结算结果 FinalSettle);凡出现在接口出入参中的类型一律用 dto,由 service 直接产出——否则 controller 被迫承担映射,违反"controller 只透传"
- dao 必须完成行→结构体转换:裸 gdb.Record 的 string 键取值是魔法值,拼错列名编译期不报错,且会把键取值扩散到 service/controller;GoFrame 自带
Record.Struct/Result.Structs(按ormtag)即转换手段 - 新分层先对齐框架惯例:GoFrame 原生分层即 dao 转 entity / service 返回 dto / controller 薄透传,自定义设计前先核对框架默认范式
分层文件对齐与代码模式(硬性要求)
- 每张业务表对应一组
entity / dao / service / controller / dto文件,数量严格对齐(核验方式:每层目录文件数 = 分层表数,分层表数 = 总表数 − 豁免表数);虚拟表(向量 vec0 / FTS5)与流水/记录类表(如 point_log)豁免分层对齐:不建任何独立分层文件(含 entity/dao),建表由主表 dao 统一管理(同虚拟表模式),由使用方 service 事务内直写,禁止为只写不读的审计表造分层门面;无 HTTP 出入口的表(纯内部配置、聚合数据、状态聚合等)豁免 controller/dto 分层:entity/dao/service 保留,数据经所属主表端点透出(如详情页聚合子表数据),禁止造无路由的空壳 controller/dto 门面;无任何读写引用的死表连表带分层整套删除,启动时 DROP 库内残留表与代码保持一致 - 非表文件一律不进业务分层目录:路由注册与中间件装配写在
main.go(公共中间件与 RouteRegister 在common/http.go);建表在 daoinit()(CREATE TABLE IF NOT EXISTS + 索引 + 迁移),死表 DROP 由启动时与代码对齐;鉴权等跨模块通用能力放common/;跨表业务流程归入所属表文件(如闯关 Choose 属 level 表)——分层目录出现非表文件即违反对齐,禁止以非表名开独立分层文件 - 不建 parser/rag 等技术目录:纯技术能力(文档解析、中文分词、向量序列化)平铺在
common/;业务编排(分块、检索、工作流)归入对应 service 文件 - entity:每文件一张表,
orm标签与列名一致,时间字段用*gtime.Time,只做表映射 - dto:请求/响应结构,
g.Meta内嵌定义路由;只描述 HTTP 出入参,不承载领域逻辑 - domain(目录
biz/model/domain/,package domain):仅收纳不进 HTTP 出入参的纯领域值(如结算输入 SettleState/结算结果 FinalSettle,service 内部流转 + 单测使用);判断标准:类型是否出现在接口出入参中——出现即用 dto。entity 对表、dto 对 HTTP、domain 对纯领域;service 返回 dto(允许 import dto),dao 返回 entity(禁止外泄 gdb.Record) - dao:单例
var Xxx = &xxxDao{},init()内CREATE TABLE IF NOT EXISTS+ 索引 + 迁移;通用 CRUD 复用common/base_dao.go(InsertAndReturnId / GetOneByPk / UpdateByPk) - controller:路由反射注册(
common.RouteRegister,按 controller 结构体名转 kebab-case 组前缀),path/method 唯一来源为 dto 的g.Meta(携带 path/method/summary);新增接口只需写 dto + controller 方法,禁止在 main.go 手动逐条注册;跨组方法以H5前缀命名(h5 组只注册 H5 开头方法,admin 组跳过);直接写响应体的例外场景(如支付回调裸文本 "success")注册在 controller 内并注释说明 - 接口只允许 GET / POST:写操作传 JSON body(或 multipart),读操作走 query params;无 PUT/DELETE
- dao 查询缓存:查询用
gdb.CacheOption(TTL 来自配置),写操作后必须清对应缓存,否则出现"库里已改、查询还是旧值"
错误处理规范(硬性要求)
- 所有可能失败的调用必须显式处理返回的 error:向上返回(保留上下文用
gerror.Wrap/Newf)或记录日志,禁止_, _ =静默丢弃——吞错会掩盖故障根因,修复问题必须先定位错误路径,不得以忽略 error 换取编译通过 - defer 关闭等无法向上返回的资源清理错误,用
defer func() { _ = x.Close() }()显式声明忽略意图,禁止裸defer x.Close()隐式吞错
并发规范
- 可并行的场景:纯 IO 任务——读查询、LLM/Embedding 调用、文件读取。SQLite 写一律回主 goroutine 串行(无 WAL 时并发写会
database is locked,锁定风险归零,并发只赢在 IO 等待上) - 新增并行点的固定三处:common 加池封装(grpool)→
biz/consts加默认大小 →config.yml加key: 并发度(缺失或非法时回退默认值) - 禁止直接用裸
go启动并行工作负载,一律走common的池 - 防死锁:等待链单向「主 → A池 → B池」,被等待池的任务内不得再等待任何池(会饿死 worker);池无 Wait 方法,等待用调用方
sync.WaitGroup,任务结果经 buffered channel 回主 goroutine - 共享状态安全(内存):池内任务并发执行,共享实例(service 单例、model 句柄等)只允许只读访问;可变字段必须在提交池之前由主 goroutine 一次性预置,任务内禁止写共享字段——Go map 并发写直接
fatal error: concurrent map writes,无锁、无降级、不可恢复,只能崩溃重启。需要可变共享状态时按优先级:① 无共享(任务内新建、buffered channel 传递结果)② 锁(sync.Mutex/RWMutex,锁内只做内存操作,LLM/DB 等 IO 放锁外)③sync/atomic(仅限 int 类标量计数/标志,如atomic.AddInt64,并发计数禁用普通++;复合结构不要用 atomic,指针 CAS 属例外) - 锁的使用:互斥场景唯一入口是
common.WithLock[T any](ctx, key, expire, retries, retryInterval, fn func() (T, error)) (T, error)——泛型回调,业务返回值经 T 原样透出给下游;内部按 config.yml 自动选择锁实现(配置了redis节点 → redis 锁,跨实例互斥,SET NX EX + token 对比删除防误删他人锁;未配置 → gcache 内存锁,单实例互斥),禁止直接用 gcache/gredis 自己实现加锁。拿不到锁(被占用,ErrLockHeld)最多重试retries次、每次间隔retryInterval(retries=0立即失败;ctx 取消/超时同样终止等待);中间件故障不重试直接返回。锁自动释放:无论 fn 成功、失败还是 panic,defer 释放。expire必须 > 0(进程崩溃兜底不死锁),fn 耗时必须在 expire 前完成,fn 内禁止长耗时 IO(LLM/DB 调用);锁粒度按业务唯一键尽量小 - 裸
go允许的例外:go func(){ wg.Wait(); close(ch) }()收尾惯用法、SSE 心跳、流式管道(Stream 读写)等长生命周期/非工作负载协程
文档职责(三文档体系)
| 文档 | 职责 | 何时补充/更新 |
|---|---|---|
| CLAUDE.md(本文件) | 公司通用开发规范:分层职责、代码模式、并发/事务/缓存约束、流程 | 规范变化时 |
| README.md | 项目功能介绍:架构、数据流、表清单、功能模块、API 清单、使用说明 | 功能增减时 |
| 技术设计.md | 实现细节与技术决策:DDL、检索参数、风险与备选方案 | 关键技术决策/参数变化时 |
开发流程(文档驱动,硬性要求)
永远以文档驱动开发:用户提出开发需求 → 先给出实现方案(技术选型、影响面、改动清单) → 用户确认后先补充文档再动手写代码。补充哪个文档取决于内容性质:规范 → 本文件,功能 → README,实现细节/技术决策 → 技术设计.md。禁止未经确认直接开发,禁止先写代码后补文档。
数据访问规范(硬性要求)
- 事务:涉及多张表的增删改操作必须包数据库事务,禁止逐表裸调用。事务必须在 service 层(
g.DB().Transaction包裹与事务内写方法如XxxInTx,service 持有 tx 句柄编排多表),dao 层只做单表无状态 CRUD,不持事务;tx.Begin后必须用defer防护已提交后的二次 Rollback - SQL 单表约束:每个 SQL 只允许访问一张表,禁止 JOIN 与跨表子查询(
IN (SELECT ...)/EXISTS);跨表数据一律拆为多条单表 SQL + 应用层内存组装——先取外键 id 列表,再对目标表IN查询;IN参数须按 ≤100 分批(SQLite 变量数上限 999) - 禁止 N+1 查询:禁止在循环中逐条查库。循环场景一律改为批处理——一次
ListByXxx取回后按外键在内存分组 - 缓存一致性:DAO 查询走缓存(TTL 来自
database.cache.ttl),写操作后必须清对应缓存 - 批处理 SQL:批量写入用
InsertAll类方法,批量删除用IN子句,禁止循环单条 INSERT/DELETE - 配置即使用:config.yml 中出现 redis / mq 等中间件配置时,代码必须实际接入使用,禁止"配置了但代码不用"或"代码写死但配置缺失"
- 消息/回调幂等:接入 MQ / Webhook 时,消费与回调处理必须幂等——MQ 至少一次语义、webhook 失败重试都可能重复投递同一事件,禁止依赖"只投一次"假设。幂等手段:以业务唯一键(如
任务ID + 事件类型)先查重或建唯一约束再落库,重复事件直接忽略;重试与补偿逻辑同样要防重复执行
运维部署规范(硬性要求)
- 部署形态:Docker 单镜像部署,前后端一体单端口(8080);运行时数据目录挂载持久化,容器重建不丢数据。部署文件为
server/Dockerfile,启动与使用见 README - 数据即文件:文件型存储,备份 = 打包「数据库目录(小而关键)+ 上传文件目录(大而可重建)」两个目录;迁移 = 拷贝到新机器即可
- 运行时数据与代码分离:数据目录不提交 git;删除即丢数据,改动前先确认
- 配置即文件:项目配置文件为唯一配置入口(监听端口、并发度等),环境变量覆盖无效
金额单位规范(硬性要求)
- 金额一律以「分」为单位存储与传输:所有金额字段(零售价、进价、单价、小计、总金额)在数据库与接口中使用整数分(entity/dto 字段类型
int64),禁止浮点元 - 展示转换在前端:前端展示 ÷100 转元保留两位小数,提交 ×100 转分;禁止后端做元↔分转换,后端只处理整数分
- 金额计算:
数量(浮点克) × 单价(分)后必须common.RoundInt四舍五入到整数分,禁止裸浮点累加 - 量纲区分:数量/库存/用量为浮点克(REAL),金额为整数分;混算时必须显式转换
- 存量库迁移以
PRAGMA user_version版本化标记,禁止重复执行
约定
- controller 方法签名固定为
(ctx, *dto.XxxReq) (*dto.XxxRes, error),实例注册模式var Xxx = &xxx{} - 参数校验优先用 GoFrame DTO 校验:请求结构体用
vtag(required / regex / in 等)声明,框架自动校验并返回错误,controller 不手写校验;仅 DTO 表达不了的业务规则(跨字段依赖、查库校验如重名、取值范围依赖配置)放 service。JSON 格式解析可留在 controller 或下沉 service,但须保持与调用点一致 - 响应组装(实体 → DTO 字段映射)在 service 进行,controller 只透传
- service 方法签名 ctx 开头,错误统一用
gerror - 编译验证:
go build ./... - H5 客户侧文案合规(硬性):H5 页面与微信模板消息面向终端消费者,软件方无医疗资质,禁止出现 诊所/开方/处方/药方/医嘱/药品/服药/诊疗 等医疗行为用语——暗示诊疗即违规;一律用中性话术:「健康打卡」「调理」「饮食禁忌提醒」「联系服务机构」等;登录页与浏览器标题等可被搜索引擎收录的对外表面保持中性品牌名