From 2bbeea19fa2922a7aceae6cae3090c6f8347973f Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Thu, 2 Jul 2026 17:31:40 +0800 Subject: [PATCH] 20260702173140 --- code/app/CODE_STYLE.md | 338 ++++++++++++++++++++++++++++++++++ code/app/MEAL_ORDER_DESIGN.md | 45 ++--- 2 files changed, 350 insertions(+), 33 deletions(-) create mode 100644 code/app/CODE_STYLE.md diff --git a/code/app/CODE_STYLE.md b/code/app/CODE_STYLE.md new file mode 100644 index 00000000..ab8e3f48 --- /dev/null +++ b/code/app/CODE_STYLE.md @@ -0,0 +1,338 @@ +# 代码开发规范 + +本文档供 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`。 diff --git a/code/app/MEAL_ORDER_DESIGN.md b/code/app/MEAL_ORDER_DESIGN.md index 8c64c3a2..6018425c 100644 --- a/code/app/MEAL_ORDER_DESIGN.md +++ b/code/app/MEAL_ORDER_DESIGN.md @@ -112,14 +112,7 @@ - 加菜(type=2)和留下备注(type=4)两个动作需要触发家庭动态,调用 `recordFeed()`;其余操作不触发。 - 备注(type=4)仅允许 `create_by` 本人删除,不可编辑;操作日志(type≠4)不暴露删除入口。 - 活动日志按 `create_time` 升序展示,形成每顿饭的互动时间轴。 -- 餐食安排通过 `source_type` 区分来源:`1` 在家做饭,`2` 外出就餐;后续如需外卖或跳过某餐,再扩展枚举。 -- 外出就餐时,`location` 字段可填餐厅或地点名称,选填。 -- 外出就餐时,`cook_by`(做饭人)字段隐藏不展示。 -- 餐食明细统一保存到 `b_meal_plan_item`,不为外出就餐单独建明细表。 -- 在家做饭时,`b_meal_plan_item.dish_id` 必填,菜名和图片来自菜品库。 -- 外出就餐可以只记录地点,不强制填写具体菜品;如果填写外出菜品明细,则 `b_meal_plan_item.dish_name` 必填,`dish_id` 为空,`image_file_key` 选填。 -- 在家做饭时,`dish_id` 可以为空,即发起一顿饭但不指定菜品,等待其他家庭成员添加。 -- 第一版不单独建餐厅表。只有后续需要复用餐厅、收藏餐厅、统计常去餐厅、人均消费或地图定位时,再新增 `b_meal_restaurant` 并从 `b_meal_plan` 关联。 +- 餐食安排统一视为在家做饭,`dish_id` 可以为空,即发起一顿饭但不指定菜品,等待其他家庭成员添加。 ## 5.2 简化操作原则 @@ -290,8 +283,6 @@ - family_id - plan_date - meal_slot_id -- source_type -- location - remark - cook_by - create_time @@ -304,10 +295,8 @@ - `plan_date`:餐食日期,例如今天、明天或用户选择的日期。 - `meal_slot_id`:餐段 ID,指向 `b_meal_slot`。 - 同一个家庭、同一天、同一个餐段建议只有一条餐食安排。 -- `source_type`:餐食来源,1=在家做饭(默认),2=外出就餐。 -- `location`:餐厅或地点名称,选填,仅外出就餐时有意义,例如「海底捞」「外婆家」。 - `remark`:这顿饭的备注,选填。 -- `cook_by`:做饭人,选填,仅在家就餐时展示,指向家庭成员用户。 +- `cook_by`:做饭人,选填,指向家庭成员用户。 ### 餐段 b_meal_slot @@ -340,8 +329,6 @@ - family_id - plan_id - dish_id -- dish_name -- image_file_key - quantity - remark - sort_number @@ -352,9 +339,8 @@ 说明: -- 统一保存一顿饭的菜品明细,不区分在家或外出明细表。 -- 在家做饭时,`dish_id` 必填,指向 `b_meal_dish`,图片和菜名从菜品库取;`dish_name` 和 `image_file_key` 可以为空。 -- 外出就餐明细可选;如果填写明细,则 `dish_id` 为空,`dish_name` 必填,自由文本,例如「红烧肉」「海底捞牛肉卷」;`image_file_key` 选填,拍照上传,走现有 S3 规则。 +- 统一保存一顿饭的菜品明细。 +- `dish_id` 必填,指向 `b_meal_dish`,菜名和图片从菜品库取。 - `quantity` 第一版表示份数或数量,默认 1。 - 明细独立保存,方便一顿饭安排多道菜,也方便从餐段区块移除单道菜。 @@ -414,8 +400,8 @@ 1. 进入「今日」页。 2. 默认选中今天。 3. 页面按家庭启用餐段展示餐食区块,每个区块底部展示该餐的活动时间轴(操作记录 + 备注)。 -4. 点击某个区块的加菜,默认按在家做饭处理。 -5. 从菜品库选择一道或多道菜;如果切换为外出就餐,则填写餐厅或地点,并可选填写临时菜名。 +4. 点击某个区块的加菜。 +5. 从菜品库选择一道或多道菜。 6. 直接保存到该日期和餐段。 7. 保存成功后:向 `b_meal_plan_log` 写入 type=2 记录(每道菜一条);调用 `recordFeed()` 触发家庭动态。 8. 可选设置做饭人;设置后写入 type=5 记录。 @@ -423,11 +409,9 @@ 校验规则: -- 在家做饭时至少选择 1 道菜。 +- 至少选择 1 道菜。 - 日期默认今天,也可以快捷切换明天。 - 餐段必填,来自家庭自定义餐段。 -- 来源默认在家做饭;外出就餐时不展示做饭人。 -- 外出就餐时至少填写餐厅或地点、临时菜名二者之一;如果填写临时菜名,每道菜名不能为空。 - 数量默认 1,不强制用户填写。 - 保存时生成或更新 `b_meal_plan` 和 `b_meal_plan_item`;同步写入 `b_meal_plan_log`。 @@ -481,7 +465,7 @@ - 家庭自定义餐段。 - 餐食安排支持一次选择多道菜。 - 餐食安排支持设置做饭人。 -- 餐食安排支持记录外出就餐,外出就餐不单独维护餐厅档案。 +- 餐食安排支持设置做饭人。 - 今日/明日餐食视图。 - 餐食安排提供快速记账按钮。 @@ -512,7 +496,7 @@ - 今日/明日餐食可以用 `loadDataBySql` 做菜品、餐食安排、明细的聚合查询;第一版也可以前端分别查询后组合。 - 删除菜品时优先软删除,避免破坏历史餐食安排。 - 保存餐食安排建议在前端用一次 `saveData` 批量保存 `b_meal_plan` 和 `b_meal_plan_item`。 -- 外出就餐先作为 `b_meal_plan.source_type = 2` 处理,不新增餐厅表;后续确实需要餐厅复用和统计时,再加 `b_meal_restaurant`。 +- 餐食安排统一视为在家做饭。 - 如果后续要求状态流转、想吃列表或权限校验,再单独设计,不放入第一版。 - 图片上传复用现有 S3 上传接口。 - 前端状态可以新增 `meal` store,负责菜品、分类、标签、餐食安排缓存。 @@ -566,8 +550,8 @@ - [ ] 设计 `b_meal_tag` 表结构,包含 `parent_id` 支持父子标签。 - [ ] 设计 `b_meal_dish_tag` 表结构。 - [ ] 设计 `b_meal_slot` 表结构,支持家庭自定义餐段。 -- [ ] 设计 `b_meal_plan` 表结构,包含 `plan_date`、`meal_slot_id`、`cook_by`、`source_type`、`location`。 -- [ ] 设计 `b_meal_plan_item` 表结构,统一保存餐食明细;在家做饭用 `dish_id`,外出就餐用临时 `dish_name` 和可选 `image_file_key`。 +- [ ] 设计 `b_meal_plan` 表结构,包含 `plan_date`、`meal_slot_id`、`cook_by`。 +- [ ] 设计 `b_meal_plan_item` 表结构,`dish_id` 指向菜品库。 - [ ] 设计 `b_meal_plan_log` 表结构,包含 `plan_id`、`type`、`content`、`ref_id`,统一记录操作和备注。 - [ ] 补充索引设计,例如 `family_id/plan_date/meal_slot_id/sort_number`。 - [ ] 生成 SQL 文件并确认命名、字段类型、默认值。 @@ -704,10 +688,8 @@ - [ ] 新增餐食安排编辑页或 BottomSheet。 - [ ] 支持今天/明天快捷选择。 - [ ] 支持选择家庭自定义餐段。 -- [ ] 支持选择餐食来源:在家做饭或外出就餐。 - [ ] 支持填写每道菜数量。 - [ ] 支持选择做饭人。 -- [ ] 外出就餐时支持填写餐厅或地点名称,并支持录入临时菜名。 - [ ] 支持填写餐食备注。 - [ ] 提交后创建或更新餐食安排和餐食安排明细。 - [ ] 安排餐食时写入 type=1 日志记录。 @@ -723,8 +705,7 @@ 验收标准: -- 在家做饭时至少选择一道菜才能提交。 -- 外出就餐时至少填写餐厅或地点、临时菜名二者之一;如果填写多道临时菜名,则每道菜名不能为空。 +- 至少选择一道菜才能提交。 - 数量必须大于 0。 - 保存中有 LoadingOverlay。 @@ -736,7 +717,6 @@ - [ ] 支持打开日期选择器查看其他日期。 - [ ] 支持按家庭自定义餐段分组展示。 - [ ] 支持在每个餐段区块直接加菜。 -- [ ] 支持把某个餐段切换为外出就餐,并展示餐厅或地点名称。 - [ ] 支持在每个餐段区块移除菜。 - [ ] 支持从历史餐食安排再次安排同样的菜。 - [ ] 每个餐段区块下方展示活动时间轴,按 `create_time` 升序列出操作记录和备注。 @@ -753,7 +733,6 @@ - 今天和明天餐食安排能按家庭自定义餐段清晰展示。 - 切换日期后数据正确刷新。 - 再次安排能复用原菜品生成新的餐食安排明细。 -- 外出就餐能在同一餐段区块展示,不需要进入独立餐厅或外出明细页面。 ### 阶段 8:历史餐食与复用