Files
workspace/code/app/CODE_STYLE.md
T
2026-07-02 21:54:26 +08:00

345 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 代码开发规范
本文档供 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<void>;
}
export const useMyStore = create<MyState>()(
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/` 文件结构。
- **图标颜色使用主题色**:通过 `useThemeColor("accent")` 获取主题色后用 `color={accentColor}` 设置,不使用 `className="text-accent"`(对图标不生效)。
- **详情页布局**:避免一个页面使用多个卡片(`Card`),改为平铺布局——大图 + 信息区块(标签式 label-value)+ 操作按钮。参考 `clothes/detail/[id]/index.tsx`。
### 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.6c 表单页面(键盘遮挡处理)
### 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 (
<PageLayout>
{/* 页面内容 */}
<LoadingOverlay visible={isLoading} />
</PageLayout>
);
```
列表加载使用 `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`。