--- name: allapp-conventions description: > Allapp 项目开发规范和数据库配置指南。该 skill 应在以下场景使用:(1) 在 Allapp 项目中新增功能、 创建数据库表、或调用后端 API 时;(2) 需要了解项目通用 API 接口用法、数据库连接信息、 表结构约定时;(3) 需要遵循项目的代码规范时。触发词包括:allapp、记账、app-go、app-rn、 数据库连接、API 接口、建表、收藏功能等。 agent_created: true --- # Allapp 项目开发规范 ## 项目概览 Allapp 是一个家庭/个人空间型 mini 程序平台: - **后端** `app-go/`:Go + Fiber v3 + PostgreSQL (pgx) + JWT - **前端** `app-rn/`:React Native + Expo 55 + Expo Router + Zustand + Alova - **数据库** PostgreSQL,库名 `allapp`,schema `public` 数据库连接信息见 `references/db-config.md`。 ## 项目结构速查 ``` app-go/ # Go 后端 ├── configs/ # YAML 配置(config.yaml / config.dev.yaml) ├── internal/ │ ├── bootstrap/ # 启动入口 │ ├── config/ # 配置加载 │ ├── handle/ # 请求处理(auth / data / data_save / data_list / s3) │ ├── httpx/ # 请求/响应工具 │ ├── middleware/ # 中间件(auth / logger) │ ├── router/ # 路由注册 │ └── types/ # 请求/响应类型 ├── pkg/ │ ├── db/ # 数据库操作(insert / update / meta_field) │ └── jwtx/ # JWT 工具 └── main.go app-rn/ # React Native 前端 ├── app.json ├── package.json # ⚠️ 写页面必须先读这个 ├── tailwind.config.ts ├── src/ │ ├── app/ # Expo Router 页面 │ │ ├── _layout.tsx # 根布局 │ │ ├── (main)/ # 主应用(route group,不影响 URL) │ │ │ ├── (tabs)/ # 主 Tab(home / chat / my) │ │ │ ├── auth/ # 登录页 │ │ │ ├── family/ # 家庭管理 │ │ │ ├── tools/ # 工具广场 │ │ │ └── activities/ # 动态列表 │ │ └── finance/ # 记账子模块(独立于 main) │ ├── components/ # 通用组件(layout/view / AZIndex 等) │ ├── configs/ # pages.ts(子应用列表配置) │ ├── layouts/ # 布局(ModuleLayout 等) │ ├── request/ # API 封装(index.ts / api.ts) │ ├── store/ # Zustand 状态管理 │ └── types/ # TypeScript 类型定义 ``` ## 前端关键依赖库 写页面/组件时必须优先使用项目已安装的库,不要引入新包: | 库 | 用途 | 导入方式 | |----|------|----------| | `heroui-native` | UI 组件库(PressableFeedback、SearchField、Button 等) | `import { PressableFeedback, SearchField } from "heroui-native"` | | `@legendapp/list` | 高性能长列表 | `import { LegendList } from "@legendapp/list"` | | `lucide-react-native` | 图标库 | `import { Star, ... } from "lucide-react-native"` | | `zustand` | 状态管理 | `import { create } from "zustand"` | | `alova` + `@alova/adapter-axios` | 请求库 | `import { createAlova } from "alova"` | | `dayjs` | 日期处理 | `import dayjs from "dayjs"` | | `expo-router` | 路由 | `import { useRouter } from "expo-router"` | | `react-native-reanimated` | 动画 | `import Animated from "react-native-reanimated"` | | `react-native-gesture-handler` | 手势 | 与 reanimated 配合 | | `react-native-safe-area-context` | 安全区域 | `import { useSafeAreaInsets } from "react-native-safe-area-context"` | | `tailwindcss` + `tailwind-merge` | 样式 | className 方式,`import { twMerge } from "tailwind-merge"` | | `sonner-native` | Toast 提示 | `import { toast } from "sonner-native"` | | `@gorhom/bottom-sheet` | 底部弹出面板 | | | `expo-image` | 图片组件 | `import { Image } from "expo-image"` | **样式约定**:项目使用 NativeWind(Tailwind CSS),className 方式写样式,比如 `className="bg-white rounded-xl shadow-lg"`。不要写内联 `style={{}}` 除非必要。 ## 核心开发规范 ### 规则 0:写页面前必须先读 package.json **每次写新页面或新组件之前**,必须先用 Read 工具读取 `app-rn/package.json`,了解项目已有的依赖库,确保: - 使用项目已有的组件库和工具库,不要引入未安装的包 - 了解可用的第三方组件(如 heroui-native、@legendapp/list 等),优先复用 ### 规则 1:禁止新增专用 API 接口 项目已有 4 个通用数据 API,所有 CRUD 操作必须复用它们,**不得再创建专用 REST 端点**: | 接口 | 路径 | 用途 | |------|------|------| | loadData | `POST /data/loadData` | 查询:传入 `view_name`(表/视图名)、`search_condition`(WHERE)、`order_by`、`search_columns`、`args` | | saveData | `POST /data/saveData` | 增删改:传入 `[{table_name, key_field, inserts, updates, deletes}]` 数组 | | loadDataBySql | `POST /data/loadDataBySql` | 原生 SQL:传入 `sql` 和 `args` | | getUniqueId | `POST /data/getUniqueId` | 获取雪花 ID:传入 `count`(1-100) | **新增接口必须满足两个条件:** 1. 先向用户确认是否允许新增接口 2. 新接口必须是**通用接口**(类似上面四种),不能专属于某个功能 前端调用示例: ```ts // 查询 import { loadDataApi, saveDataApi, getUniqueIdApi } from "@/request/api"; const res = await loadDataApi({ view_name: "b_user", search_condition: "id = ?", search_columns: [], order_by: "", args: [userId], }); // 插入 const idRes = await getUniqueIdApi(1); await saveDataApi([{ table_name: "b_user", key_field: "id", inserts: [{ id: idRes.data[0], nickname: "test", ... }], updates: [], deletes: [], }]); ``` ### 规则 2:数据库表必须包含审计字段 所有业务表的**必需列**: ```sql id BIGINT PRIMARY KEY, -- 雪花 ID create_time TIMESTAMPTZ NOT NULL, -- 创建时间 update_time TIMESTAMPTZ NOT NULL, -- 更新时间 create_by BIGINT, -- 创建者 user_id update_by BIGINT -- 更新者 user_id ``` 后端 `pkg/db/meta_field.go` 中的 `applyMetaFields` 函数会为 insert/update 自动注入这些字段。 只有 `b_user` 和 `b_user_oauth` 两类系统表在 `auditExcludeTables` 中豁免,**其他新表一律不要加豁免**。 ID 获取方式: - 前端:调用 `getUniqueIdApi(count)` 获取 - 后端:调用 `uniqueid.NextId()` 获取 ### 规则 3:不要重复造轮子 - **前端 API 请求**:统一使用 `@/request/api` 中已有的方法,不要创建新的 API 封装 - **前端状态管理**:优先复用已有的 Zustand store(`@/store/`),新 Store 只用通用 API - **前端组件**: - UI 组件优先用 `heroui-native`(PressableFeedback、SearchField、Button、Spinner 等) - 长列表用 `@legendapp/list`(LegendList),不要用 FlatList/ScrollView 处理大量数据 - 图标统一用 `lucide-react-native` - Toast 用 `sonner-native` - 底部面板用 `@gorhom/bottom-sheet` - 样式用 NativeWind className,避免内联 style - **后端**:不要创建新的 handler 和路由,除非是通用基础设施 ### 规则 4:家庭/空间为中心的设计 系统以家庭(Family)为核心实体。所有业务数据(记账、收藏等)都跟 `family_id` 关联。 用户通过 `family_member` 表关联到家庭。 ## 关键文件索引 | 文件 | 说明 | 写页面必读 | |------|------|:---:| | `app-rn/package.json` | 前端依赖清单,了解可用库 | ⭐ | | `PROJECT_CONTEXT.md` | 完整项目文档 | | | `db.local.yml` | 本地数据库连接备忘 | | | `app-go/internal/router/router.go` | 后端路由定义 | | | `app-go/pkg/db/meta_field.go` | 审计字段自动注入逻辑 | | | `app-rn/src/request/api.ts` | 前端 API 方法 | | | `app-rn/src/store/` | Zustand 状态管理 | | | `app-rn/src/configs/pages.ts` | 子应用列表配置 | | | `app-rn/src/components/` | 通用组件 | | ## 当前数据库表 ``` b_user - 用户 b_user_oauth - 第三方登录绑定 b_family - 家庭/空间 b_family_member - 家庭成员 b_finance - 记账记录 b_finance_category - 记账分类 b_finance_category_default - 默认分类模板 b_app_favorite - 子应用收藏 b_family_feed - 家庭动态 ``` 视图:`v_family_member`(join b_family_member + b_user) ## 家庭动态(b_family_feed)记录规范 以下场景**必须**调用 `recordFeed()` 写入动态。 **type 简码对照**:`join`(加入/创建)、`rename`(改名)、`bill`(记账)、`fav`(收藏) | 触发场景 | type | content 模板 | 触发位置 | |---------|------|-------------|---------| | 创建家庭 | `join` | `{nickname} 创建了家庭「{名称}」` | `family/entry.tsx` handleCreate | | 加入家庭 | `join` | `{nickname} 加入了家庭` | `family/entry.tsx` handleJoin | | 修改昵称 | `rename` | `{nickname} 修改了昵称为「{新昵称}」` | `family/user.tsx` handleRename | | 记账 | `bill` | `{nickname} 记了一笔「{备注}」¥{金额}` | `finance/add/` 保存成功 | | 收藏应用 | `fav` | `{nickname} 收藏了「{应用名}」` | `tools/index.tsx` 收藏操作 | **⚠️ content 自包含原则**:所有展示信息直接写在 content 字符串中,不依赖 meta 字段。`recordFeed()` 不接收 meta 参数,`loadFeeds()` 只做 `{nickname}` 替换。meta 列保留但始终为 NULL。 **content 占位符规则**:content 存储模板字符串,用户昵称用 `{nickname}` 占位,**不在写入时拼接**。`loadFeeds()` 会自动将 `{nickname}` 替换为 `b_family_member.nickname` 的最新值,确保改名后历史动态也显示最新昵称。 ```ts import { recordFeed } from "@/helpers/feed"; // 正确写法 — 用 {nickname} 占位符,不拼接昵称 recordFeed(familyId, userId, "join", "{nickname} 加入了家庭"); // 错误写法 — 禁止拼接昵称写死 recordFeed(familyId, userId, "join", `${nick} 加入了家庭`); // ❌ ``` **修改昵称时同步更新**:`family/user.tsx` 的 `handleRename` 除了更新 `b_user.name`,还必须同步更新 `b_family_member.nickname`: ```ts saveDataApi([ { table_name: "b_user", updates: [{ id: uid, name: newName }] }, { table_name: "b_family_member", updates: [{ id: memberId, nickname: newName }] }, ]); ``` ### 前端:拉取数据 ```ts import { loadDataApi } from "@/request/api"; // 查当前家庭的收藏 const res = await loadDataApi({ view_name: "b_app_favorite", search_condition: "family_id = ?", order_by: "id ASC", search_columns: [], args: [familyId], }); if (res.isSuccess) { const list = res.data; // any[] } ``` ### 前端:保存数据(新增) ```ts import { saveDataApi, getUniqueIdApi } from "@/request/api"; const idRes = await getUniqueIdApi(1); await saveDataApi([{ table_name: "b_app_favorite", key_field: "id", inserts: [{ id: idRes.data[0], family_id: familyId, user_id: userId, app_id: "finance", }], updates: [], deletes: [], }]); ``` ### 前端:保存数据(删除) ```ts await saveDataApi([{ table_name: "b_app_favorite", key_field: "id", inserts: [], updates: [], deletes: [{ id: recordId }], }]); ``` ### Python:连接数据库 ```python import psycopg2 conn = psycopg2.connect( 'postgres://postgres:@117.72.182.135:5432/allapp?sslmode=disable' ) # 具体密码见 references/db-config.md ```