11 KiB
代码开发规范
本文档供 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。 - 使用注释块分隔函数区域:
// LoginQq ======================== QQ 登录 ======================== // ==================== data ====================
2.4 导入规范
内部包排第一组,第三方包排第二组,两组之间空一行:
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 数据库表要求
所有业务表必须包含以下审计字段:
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),在页面中调用:
// 查询
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:
const familyId = useFamilyStore((s) => s.getFamilyId());
const userId = useUserStore((s) => s.getUserId());
3.5 状态管理
- 使用 Zustand 创建 store,持久化存储使用
persist中间件 +AsyncStorage。 - Store 接口中同时包含状态和方法定义。
- 持久化 store 使用
partialize选择要持久化的字段。 - 子应用(如 finance)内部使用内存 store,不持久化。
Store 模式:
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/文件结构。
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 布尔值。
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:
const { loading, refreshing, runInitial, runRefresh, runLoadMore } = useListLoading();
3.8 家庭成员动态记录
某些操作(创建/加入家庭、记账、收藏等)必须调用 recordFeed(),使用 {nickname} 占位符(不硬编码昵称):
import { recordFeed } from "@/helpers/feed";
recordFeed({
family_id: familyId,
type: "bill",
content: "{nickname}记录了账单",
});
4. 数据库
- 数据库:PostgreSQL 18.1,数据库名
allapp,schemapublic。 - 所有业务表使用雪花 ID(通过
getUniqueIdApi获取)。 - SQL 查询中谨慎使用参数占位符
?(后端支持$1风格)。
5. 文件上传
上传使用 s3UploadFileApi(files[])(自动构建 FormData)。删除使用 deleteS3FileQuietly(),不在页面中重复实现 S3 key 校验逻辑。
文件键格式:YYYY/MM/DD/{uuid}.ext。