Files
offerpai_python_ai/.kiro/steering/代码开发风格文档.md
T
2026-07-01 17:47:03 +08:00

10 KiB
Raw Blame History

inclusion
inclusion
manual

代码开发风格文档

本项目为 FastAPI + SQLAlchemy (asyncio) 的 Python 3.12 后端项目,应用主目录为 app/

项目结构

  • app/config/ — 配置层:Pydantic Settings 统一配置
  • app/core/ — 核心基础设施:数据库、Redis、鉴权、中间件、异常处理、日志、统一响应
  • app/ai/ — AI 能力层:LLM 模型枚举与实例创建
  • app/api/ — 路由层:REST API 接口定义
  • app/models/ — ORM 模型层:SQLAlchemy 声明式映射
  • app/services/ — 业务逻辑层:Service 类
  • app/core/schemas/ — 公共 Schema:统一响应模型等

命名约定

文件命名

  • 全部小写,下划线分隔,如 func_permission_service.pyuser_func_usage_log.py
  • 路由文件以业务名命名,如 health.pyresume.py
  • ORM 模型文件与表名对应(去掉 bg_ 前缀),如 func_permission.py 对应 bg_func_permission

类命名

  • Service 以 Service 结尾,如 FuncPermissionService
  • ORM 模型用 PascalCase 业务名,无后缀,如 FuncPermissionUserFuncUsageLog
  • Pydantic Schema 按用途命名:请求参数以 Param 结尾,响应以 Dto 结尾,如 ResumeParamResumeDto
  • 枚举类以大写命名,如 LLM

变量与函数命名

  • 函数和变量使用 snake_case,如 check_and_deductuser_id
  • 私有函数以单下划线开头,如 _insert_usage_log
  • 常量使用全大写下划线,如 _FRIENDLY_MESSAGES_SKIP_PATHS

类型注解

  • 所有函数参数和返回值必须有类型注解
  • ORM 模型字段使用 Mapped[T] + mapped_column() 声明
  • Pydantic 模型字段使用标准类型注解 + Field()
  • 可选字段使用 Optional[T]T | None
  • 集合类型使用 list[T]dict[K, V]Python 3.12 内置泛型)

注释规范

  • 模块级注释使用文件顶部的 docstring,说明模块用途和使用示例
  • 类注释使用 docstring,说明对应的表名和用途
  • 方法注释使用 docstring,简洁描述功能
  • 复杂逻辑用行内注释 # 说明

ORM 模型类注释

  • 类 docstring 说明对应的表名和用途
  • 特殊字段通过 comment 参数说明含义,如 comment="状态 1=启用 0=禁用"

Service 类注释

  • 模块级 docstring 说明该服务的主要功能、依赖服务、使用的表
  • 格式示例:
    """功能权限 Service
    
    校验用户功能权限并扣减库存,业务异常时回退。
    逻辑与 Java 端 FuncPermissionService 完全一致。
    """
    
  • 每个方法用 docstring 简要说明逻辑流程,复杂方法可分步骤描述

分包规则

API 路由(app/api/

  • 每个业务模块一个路由文件,如 health.pyresume.py
  • 使用 APIRouter(prefix="/xxx", tags=["xxx"]) 定义路由前缀和标签
  • app/main.py 中注册路由

Serviceapp/services/

  • 每个业务模块一个 Service 文件
  • Service 类通过构造函数接收 AsyncSession,如 def __init__(self, session: AsyncSession)
  • 不使用全局 Service 实例,每次请求通过依赖注入创建

ORM 模型(app/models/

  • 每个表一个模型文件
  • 所有模型继承 app.core.database.Base
  • 表名通过 __tablename__ 指定

Pydantic Schemaapp/core/schemas/

  • 公共 Schema 放在 app/core/schemas/ 下,如 responses.py
  • 业务相关的请求/响应 Schema 放在对应的 app/api/app/services/ 同级目录,或集中在 app/core/schemas/{功能模块}/

获取当前登录用户

  • 通过 RequestContext.user_id.get() 获取当前登录用户 ID
  • 或通过依赖注入 Depends(require_login) 获取并校验
  • 需要功能权限校验时使用 Depends(func_permission("func_code"))

接口规范

  • Router 只负责参数接收和调用 Service,不写业务逻辑
  • 白名单路径(无需鉴权)在 settings.auth_whitelist 中配置
  • POST 用 @router.post()GET 用 @router.get()
  • 复杂参数使用 Pydantic 模型 + Body(),简单参数使用 Query()Path()
  • 路由方法直接返回业务数据,由 ResponseWrapMiddleware 自动包装为 StandardResponse

异常处理

核心约定

  • 统一异常与全局处理器都集中在 app/core/exceptions.pymain.py 只调用一次 register_exception_handlers(app)
  • 自定义异常一律 HTTP 500,真实业务含义靠响应体里的 code 区分(前端拦截器读 code
  • 抛异常时传一句具体原因(detail)即可,不用记错误码;不传就用类别名兜底
    • raise BizError("不支持的商品") → msg = 业务异常[不支持的商品]
    • raise BizError() → msg = 业务异常
  • 不要 catch 后吞掉异常,交由全局异常处理器统一处理

自定义异常类(app/core/exceptions.py

业务代码优先抛下列语义化异常,不要直接 raise HTTPException

异常类 code 类别名 典型场景
ParamError 4000 参数异常 入参缺失/非法、模型档位不支持
AssertError 4100 断言异常 业务前置条件不满足(由 Assert 工具抛出)
BizError 4200 业务异常 资源不存在、限频、验证码、越权、会员校验
CreditError 4300 扣费异常 余额不足、扣费失败(前端引导充值)
SysError 5000 系统异常 已知内部错误 / 上游模型调用失败(主动抛)
  • 所有自定义异常继承 AppError,处理器只在基类上注册,靠继承链自动覆盖全部子类
  • 流式(SSE)场景用 exc.to_event() 输出 error 事件;上游异常包装用 SysError.from_exc(exc)
  • 兜底:未建模的异常由 global_exception_handler 统一按系统异常(code=5000)返回,dev 环境暴露细节,生产只回通用提示
from app.core.exceptions import BizError, CreditError, ParamError

if not resume:
    raise BizError("简历不存在")
if balance < cost:
    raise CreditError("积分不足")
if model not in ALLOWED:
    raise ParamError(f"不支持的模型档位: {model}")

业务断言工具(app/core/asserts.py

前置条件校验优先用 Assert 工具类(风格参考 Spring Assert),条件不满足时抛 AssertError(code=4100)不要再手写 if not ... raise 或裸 assert

from app.core.asserts import Assert

Assert.not_none(user, "用户不存在")
Assert.has_text(func_code, "功能编码不能为空")
Assert.not_empty(items, "列表不能为空")
Assert.gt(days, 0, "天数必须大于0")
Assert.eq(status, 1, "状态不可用")

常用方法:is_true / is_false / not_none / is_none / has_text / not_empty / gt / gte / lt / lte / eq / ne

HTTPException

  • 仅在需要返回特定 HTTP 状态码的场景(如框架层、鉴权 401/403)使用 raise HTTPException(status_code=xxx, detail="描述")
  • 业务拒绝一律用上面的自定义异常,不要用 HTTPException 表达业务错误

Redis 使用规范

  • 通过 app.core.redis.redis_client 或依赖注入 Depends(get_redis) 获取客户端
  • key 命名与 Java 端保持一致,如 login:token:{userId}
  • 值统一 JSON 序列化(json.dumps / json.loads
  • 设置 TTL 时使用 ex 参数(秒)

数据库设计风格

  • 与 Java 端共享同一数据库,表结构由 Java 端管理
  • 表名以 bg_ 前缀,下划线命名,如 bg_func_permission
  • 主键 id,类型 BigInteger
  • 时间字段使用 DateTime 类型,包含 create_timeupdate_time
  • 逻辑删除字段 is_delete,类型 BigInteger0=正常,非0=删除
  • 状态字段用 Integer0/1 表示,通过 comment 说明含义
  • 查询使用 SQLAlchemy select() + where() 构建条件
  • 更新使用 update() + where() + values()
  • 会话通过 get_db() 依赖注入获取,自动 commit/rollback/close

异步规范

  • 所有数据库操作、Redis 操作、HTTP 请求使用 async/await
  • Service 方法统一使用 async def
  • 路由处理函数统一使用 async def
  • 避免在异步上下文中使用同步阻塞操作

AI 调用规范

  • 业务代码不直接使用 LLM 枚举,而是从 app.ai.model_config 中引用对应模块的场景配置类
  • model_config.py 中每个模块一个 class,每个场景一个类属性,属性值为预创建的 ChatOpenAI 实例
  • 修改模型或调整参数只需改 model_config.py 一个文件,业务代码不动
  • AI 调用应做好异常捕获和容错,单次失败不应影响整体流程
  • 长耗时 AI 调用考虑异步执行

模型引用示例

from app.ai.model_config import SkillGapModel, NovaChatModel

# chain 中直接使用配置类属性(已经是 ChatOpenAI 实例)
_plan_chain = (
    ChatPromptTemplate.from_messages([...])
    | SkillGapModel.AGENT_PLAN
    | StrOutputParser()
)

# 非 chain 场景直接 await 调用
result = await NovaChatModel.CHAT.ainvoke(messages)

新增 AI 场景步骤

  1. app/ai/model_config.py 对应模块的 class 中新增一个类属性,指定模型和参数
  2. 在业务代码中 from app.ai.model_config import XxxModel,引用该属性
  3. 如需新增模块,在 model_config.py 中新建一个 class

AI 输出 JSON 解析

  • LLM 返回的 JSON 经常被 markdown 代码块(```json ... ```)包裹,禁止直接使用 LangChain 的 JsonOutputParser
  • 统一使用 app.tool.json_helper.parse_llm_json 解析 AI 输出的 JSON 文本
  • parse_llm_json 会自动剥离 markdown 代码块标记,并通过 json_repair 做容错修复
  • 不要在各模块中自行编写 JSON 清洗/解析逻辑,统一复用 parse_llm_json

代码格式规范

紧凑风格

  • 避免过度换行,保持代码紧凑易读
  • 链式调用尽量写在一行,除非超过 120 字符
  • 方法参数列表较多时,可适当换行但保持紧凑
  • f-string 拼接优先写在一行

示例

推荐(紧凑风格):

# 查询语句一行
result = await session.execute(select(FuncPermission).where(FuncPermission.func_code == func_code, FuncPermission.status == 1))

# 链式操作一行
perm = result.scalar_one_or_none()

# f-string 拼接一行
log.info(f"功能权限校验 userId:{user_id} funcCode:{func_code}")

# 方法参数紧凑排列
async def check_and_deduct(self, user_id: int, func_code: str) -> int:

# 多条件 where 紧凑排列
result = await self.session.execute(select(UserFuncPermissionStock).where(
    UserFuncPermissionStock.user_id == user_id, UserFuncPermissionStock.func_code == func_code))