# 代码开发规范 本文档供 AI 开发工具(如 Claude Code)在本项目中开发时参考。记录后端(Go)和前端(React Native / TypeScript)的编码约定、模式及约束。 --- ## 1. 通用原则 - 编辑器使用 2 空格缩进(Go 使用 tab)。 - 避免过早抽象。三次重复的代码优于一个过早的通用抽象。 - 不写多余的注释。只有当"为什么这样做"不显而易见时才加注释——隐藏约束、微妙的不变量、特定 bug 的变通方案。 - 不加只为了"安全"的错误处理和校验。信任内部代码和框架的保证,只在系统边界(用户输入、外部 API)做校验。 - 所有引用路径使用项目别名(前端 `@/`,后端 `allapp-go/`),不使用相对路径 `../../`。 --- ## 2. Go 后端(`app-go/`) ### 2.1 项目结构 ``` app-go/ internal/ config/ 配置加载 errors/ 错误包装 handle/ HTTP handler(按领域分文件) httpx/ HTTP 请求/响应工具 middleware/ Fiber 中间件 router/ 路由注册 types/ DTO(按领域分文件) ws/ WebSocket pkg/ common/ 通用工具 db/ PostgreSQL 客户端(insert/select/update/delete/tx/validate) jwtx/ JWT 工具 logger/ 日志 requestx/ HTTP 请求工具 s3store/ S3 存储 uniqueid/ 雪花 ID 生成 wechat/ 微信登录 ``` ### 2.2 命名规范 | 成员 | 规范 | 示例 | |---|---|---| | 导出函数/类型 | `PascalCase` | `LoadData`, `LoginQqReq` | | 非导出函数 | `camelCase` | `handleThirdLogin`, `toInt64` | | 局部变量 | `camelCase` | `dbClient`, `token`, `vo` | | 包名 | 单个小写单词 | `package handle`, `package db` | | 文件名 | `snake_case.go` | `meta_field.go`, `data_save.go` | | JSON 标签 | `snake_case` | `json:"family_id"`, `json:"search_condition"` | | 验证标签 | `validate:"required"` | 使用 `go-playground/validator` 库 | | 枚举/常量 | `PascalCase` | `CtxUserIDKey` | ### 2.3 文件组织 - **每个 handler 文件**按领域拆分:`auth.go`、`data_list.go`、`data_save.go`、`s3.go`。 - **每个 types 文件**按对应 handler 拆分:`auth.go`、`data_list.go`、`data_save.go`、`s3.go`。 - 使用注释块分隔函数区域: ```go // LoginQq ======================== QQ 登录 ======================== // ==================== data ==================== ``` ### 2.4 导入规范 内部包排第一组,第三方包排第二组,两组之间空一行: ```go import ( "allapp-go/internal/httpx" "allapp-go/internal/types" "github.com/gofiber/fiber/v3" ) ``` ### 2.5 错误处理 - 来自数据库、JWT、S3 等外部调用的错误,用 `errors.WithStack(err)` 包装后向上返回。 - 业务逻辑错误直接调用 `httpx.Fail(c, "用户可读消息")` 或 `httpx.Unauthorized(c, "消息")` 返回 JSON 响应。 - 数据库事务使用闭包模式:`dbClient.WithTx(c.Context(), func(tx *db.Client) error { ... })`。 ### 2.6 CRUD 约束 **绝不创建专用 REST 端点。** 所有 CRUD 必须复用 5 个通用数据 API: | 端点 | 用途 | |---|---| | `POST /app/data/loadData` | 按条件查询 | | `POST /app/data/loadDataPage` | 分页查询 | | `POST /app/data/loadDataBySql` | 原生 SQL 查询 | | `POST /app/data/saveData` | 批量增/改/删 | | `POST /app/data/getUniqueId` | 获取雪花 ID | 如果认为需要新端点,**必须先询问用户**。即使新增,端点也必须是通用型的。 ### 2.7 数据库表要求 所有业务表必须包含以下审计字段: ```sql id BIGINT PRIMARY KEY, -- 雪花 ID create_time TIMESTAMPTZ, update_time TIMESTAMPTZ, create_by BIGINT, update_by BIGINT ``` 审计字段由 `pkg/db/meta_field.go` 自动注入。不要将表加入 `auditExcludeTables`(只有 `b_user` 和 `b_user_oauth` 豁免)。 ### 2.8 数据模型约束 - 所有业务数据与 `family_id` 关联。 - 查询时必须按当前用户的 `family_id` 过滤。 - 用户通过 `b_family_member` 与家庭关联。 --- ## 3. React Native / TypeScript 前端(`app-rn/`) ### 3.1 项目结构 ``` app-rn/src/ app/ Expo Router 页面(文件即路由) components/ 可复用组件 configs/ 配置定义 helpers/ 业务辅助函数 hooks/ 自定义 Hooks layouts/ 布局组件 request/ API 实例 + 封装 store/ Zustand 状态 types/ TypeScript 类型 utils/ 通用工具函数 ``` ### 3.2 命名规范 | 成员 | 规范 | 示例 | |---|---|---| | 组件函数 | `PascalCase` | `Home`, `PageLayout`, `LoadingOverlay` | | 普通函数 | `camelCase` | `formatMoney`, `loadBills`, `createDefaultFilter` | | hooks | `camelCase` 以 `use` 开头 | `useLoading`, `useFamilyStore` | | API 函数 | `camelCase` 以 `Api` 结尾 | `loginQqApi`, `loadDataApi`, `saveDataApi` | | 接口/类型 | `PascalCase` | `FinanceBill`, `UserState`, `FilterState` | | 枚举 | `PascalCase`,成员 `PascalCase` | `enum FinanceType { Expense = 0, Income = 1 }` | | 文件名 | `camelCase.ts` / `camelCase.tsx` | `useLoading.ts`, `finance.ts` | | 目录名 | 小写单数 | `hooks/`, `store/`, `types/` | ### 3.3 TypeScript 类型 - 优先使用 `interface` 而不是 `type`。 - 全局类型定义在 `src/types/global.d.ts` 中,使用 `declare global {}` 扩充。 - 领域类型按文件拆分:`auth.ts`、`finance.ts`、`chat.ts` 等。 - 每个文件保持单一责任原则。 ### 3.4 API 调用模式 **所有数据操作经过 5 个通用 API 封装函数**(定义在 `src/request/api.ts`),在页面中调用: ```typescript // 查询 const res = await loadDataApi({ view_name: "b_finance", search_condition: "family_id = ? and create_by = ?", order_by: "create_time desc", search_columns: ["id", "amount", "remark"], args: [familyId, userId], }); // 分页查询 const res = await loadDataPageApi({ view_name: "b_finance", search_condition: "family_id = ?", order_by: "create_time desc", search_columns: ["id", "amount", "remark"], args: [familyId], page: 1, page_size: 20, }); // 原生 SQL 查询 const res = await loadDataBySqlApi({ sql: "select * from b_finance where family_id = ?", args: [familyId], }); // 增/改/删 const res = await saveDataApi([ { table_name: "b_finance", key_field: "id", inserts: [{ id, family_id, category_id, amount, record_time }], updates: [{ id, amount }], deletes: [{ id }], }, ]); ``` 响应包含 `res.isSuccess` 布尔值判断结果。 数据访问必须传递正确的 `family_id` 和 `user_id`: ```typescript const familyId = useFamilyStore((s) => s.getFamilyId()); const userId = useUserStore((s) => s.getUserId()); ``` ### 3.5 状态管理 - 使用 Zustand 创建 store,持久化存储使用 `persist` 中间件 + `AsyncStorage`。 - Store 接口中同时包含状态和方法定义。 - 持久化 store 使用 `partialize` 选择要持久化的字段。 - 子应用(如 finance)内部使用内存 store,不持久化。 Store 模式: ```typescript import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; interface MyState { items: Item[]; setItems: (items: Item[]) => void; loadItems: () => Promise; } export const useMyStore = create()( persist( (set) => ({ items: [], setItems: (items) => set({ items }), loadItems: async () => { const res = await loadDataApi({ ... }); if (res.isSuccess) set({ items: res.data as Item[] }); }, }), { name: "my-storage", storage: createJSONStorage(() => AsyncStorage), partialize: (state) => ({ items: state.items }), }, ), ); ``` ### 3.6 组件模式 - 使用 NativeWind `className` 进行样式,不写内联 `style={}`。 - 页面组件使用 `PageLayout` / `ModuleLayout` / `AppLayout` 包裹。 - 长列表使用 `@legendapp/list` 优化性能。 - 图标使用 `lucide-react-native` 或自定义 IconFont 组件。 - UI 组件优先使用 HeroUI Native(`heroui-native`)。 - 使用 `expo-router` 进行导航,路由基于 `src/app/` 文件结构。 ### 3.6a 搜索条件 BottomSheet 搜索/筛选条件弹窗统一使用项目封装的 `BottomSheet` 组件(`@/components/BottomSheet`),遵循以下规范: - **标题居中**:`BottomSheet.Content` 内的标题文案(如"筛选记账")使用 `text-center` + `font-semibold` 居中显示。 - **重置按钮在右侧**:标题栏右侧放置重置按钮,使用 `flex-row items-center justify-between` 布局:左侧空 `flex-1`,中间标题,右侧重置按钮。 - **限制高度可滚动**:筛选选项放在 `ScrollView` 中,设置 `maxHeight: Dimensions.get("window").height * 0.55`,`showsVerticalScrollIndicator={false}`,底部按钮固定在 ScrollView 外部。 - **布局参考**:参照 `finance/(tabs)/home/index.tsx` 中的筛选 BottomSheet 实现: - 日期范围用 `TouchableOpacity` 模拟输入框 - 分类/成员选择使用标签按钮(`TouchableOpacity` + `border` + `rounded-full`)布局,`flex-row flex-wrap gap-2` - 筛选标签分区域展示,用 `Text` 小标题分隔(如"支出"/"收入"分组) - 使用 `useCallback` 包裹打开/关闭/重置/应用回调。 ### 3.6b 表单页面(键盘遮挡处理) ### 3.7 异步操作与加载状态 **所有异步保存/更新/删除操作必须使用 `useLoading` + `LoadingOverlay` 模式**,不得手动管理多个 loading 布尔值。 ```typescript const { isLoading, startLoading, stopLoading } = useLoading(); // 操作开始 startLoading("删除中..."); try { const res = await saveDataApi([...]); if (res.isSuccess) { stopLoading({ type: "success", message: "已删除" }); } else { stopLoading({ type: "error", message: res.message || "删除失败" }); } } catch { stopLoading({ type: "error", message: "删除失败" }); } // 在 JSX 底部添加 LoadingOverlay return ( {/* 页面内容 */} ); ``` 列表加载使用 `useListLoading`: ```typescript const { loading, refreshing, runInitial, runRefresh, runLoadMore } = useListLoading(); ``` ### 3.8 家庭成员动态记录 某些操作(创建/加入家庭、记账、收藏等)必须调用 `recordFeed()`,使用 `{nickname}` 占位符(不硬编码昵称): ```typescript import { recordFeed } from "@/helpers/feed"; recordFeed({ family_id: familyId, type: "bill", content: "{nickname}记录了账单", }); ``` --- ## 4. 数据库 - 数据库:PostgreSQL 18.1,数据库名 `allapp`,schema `public`。 - 所有业务表使用雪花 ID(通过 `getUniqueIdApi` 获取)。 - SQL 查询中谨慎使用参数占位符 `?`(后端支持 `$1` 风格)。 --- ## 5. 文件上传 上传使用 `s3UploadFileApi(files[])`(自动构建 `FormData`)。删除使用 `deleteS3FileQuietly()`,不在页面中重复实现 S3 key 校验逻辑。 文件键格式:`YYYY/MM/DD/{uuid}.ext`。