Files
workspace/code/app/CODE_STYLE.md
T
2026-07-02 17:31:40 +08:00

11 KiB
Raw Blame History

代码开发规范

本文档供 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,schema public。
  • 所有业务表使用雪花 ID(通过 getUniqueIdApi 获取)。
  • SQL 查询中谨慎使用参数占位符 ?(后端支持 $1 风格)。

5. 文件上传

上传使用 s3UploadFileApi(files[])(自动构建 FormData)。删除使用 deleteS3FileQuietly(),不在页面中重复实现 S3 key 校验逻辑。

文件键格式:YYYY/MM/DD/{uuid}.ext。