# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

> 个人开发项目。优先帮我把活干完、踩过的坑别再踩，**不要**纠结协作规范、提交模板、验证清单、影响面汇报这些东西。

## 项目概览

- `Loan Assistant` / `借贷助手` 客户端
- 技术栈：Expo 55、React Native 0.83、React 19、TypeScript（strict）、`expo-router`（typedRoutes + reactCompiler）、NativeWind 4、`@ant-design/react-native` 5
- 目标平台：iOS、Android、Web（部署在 `/h5` 子路径下）
- 包管理器：**pnpm**（仓库以 `pnpm-lock.yaml` 为准）

## 命令
- 使用 pnpm
- `package.json` 已经列出常规命令。
- 验证以 `pnpm lint` + 真机/模拟器为准。

## 大图架构

### 启动流程（[src/app/_layout.tsx](src/app/_layout.tsx)）
`RootLayout` 渲染 `<Stack>` 之前按顺序：
1. 加载 antd 图标字体（`useFonts({ antoutline })`）
2. `POST common/check_version` 检查原生版本，必要时弹 Modal 引导跳商店/下载链接（`type: 'apk' | 'url'`，`force` 控制是否可取消）。最近一次检查时间存在 MMKV 的 `last_update_app`，15 分钟内不再重复
3. `expo-updates` 拉热更新（`__DEV__` 下完全跳过）
4. Provider 嵌套顺序固定：`ThemeProvider`(react-navigation) → antd `Provider`(theme=`antdTheme`) → `AuthProvider` → `Stack`

新增全局 Provider 一律加在这里，不要散落到子页面。`Stack.screenOptions` 已设置了 iOS 风格透明模糊 header，新页面默认继承，特殊页（如 `sign-in`）通过 `Stack.Screen` 单独覆盖。

### 路由（[src/app/](src/app/)）
- `expo-router` + `experiments.typedRoutes`，新增页面文件即新增路由，不需要中央路由表
- 底部 Tab 在 [src/app/(tabs)/](src/app/(tabs)/)（`index/customer/analytics/reports/profile`），改 Tab 结构同时改 `(tabs)/_layout.tsx`
- 业务子流程目录：`credit/`、`customer/`；登录注册：`sign-in.tsx`、`sign-up.tsx`；Web 专属入口 `web.tsx` 和 `+html.tsx`
- `app.json -> experiments.baseUrl: "/h5"`，Web 部署到 `/h5`，本地 dev server 也带这个前缀

### 网络层（[src/utils/api.ts](src/utils/api.ts)）
唯一的 axios client：
- `baseURL` 来自 [src/config.json](src/config.json)（`api.url`），不要绕开它硬编码
- 自动注入 header `x-app-name`、`x-app-version`（拼了 `app.json` version + 平台 versionCode/buildNumber + `config.json` 的 `jsVersion`）、`x-app-platform`
- access token 是模块级单例（`setAccessToken` / `getAccessToken`），由 `AuthProvider` 在登录/启动时注入；过期判断带 30 秒余量，过期会自动清空
- 响应约定：HTTP 200 + `data.code === "1"` 才算成功，其它一律抛 `ApiError`（`HttpError` 是父类，提供 `is4xx/is5xx` 等静态判断）
- `validateStatus` 放行 200–499，5xx 才走 axios reject 路径
- 文件上传走 `uploadFile`（用 `expo/fetch`，避开 RN 的 multipart 兼容问题）
- `__DEV__` 下挂了请求/响应拦截器打 console；生产构建由 `metro.config.js` 的 `drop_console: true` 整体剔除

### 鉴权（[src/utils/auth.tsx](src/utils/auth.tsx)）
- `AuthProvider` 启动时从 MMKV `access_token` 读 token，调 `setAccessToken` 注入 api client
- `signIn` / `smsSignIn` / `signUp` / `signOut` 调完接口后由调用方 `setToken(...)` 写回，Provider 同步落 MMKV 并切 `authStatus`
- token 落盘用 `getGlobalStorage().setObject(key, value, ttl)`，TTL 用接口返回的 `expires_in`（秒）

### 持久化（[src/utils/storage.ts](src/utils/storage.ts)）
三个独立 MMKV 实例，路径都落在 `expo-file-system` 的 `cacheDirectory`：
- `getGlobalStorage()` — 长期 KV（token、上次版本检查时间）
- `getCaches()` — 业务通用缓存
- `getApiCache()` — 接口结果缓存

扩展方法 `setObject(key, value, ttlSeconds)` / `getObject(key)` 把 TTL 编进 JSON；后台每 15 分钟扫一遍清理过期项。Web 端有 [storage.web.ts](src/utils/storage.web.ts) 同名导出，靠 Metro 平台后缀分流。

### 主题与样式
- Tailwind 配置在 [tailwind.config.js](tailwind.config.js)，扩展 token 来自仓库根的 [theme.js](theme.js)（注意是 `.js`，不在 `src/`）
- antd 主题在 [src/constants/antd-theme.ts](src/constants/antd-theme.ts)，react-navigation 的 `DefaultTheme` 在 `_layout.tsx` 基于它构造，三套主题共用同一组色板
- [babel.config.js](babel.config.js) 启用 `babel-plugin-import` 给 antd-rn 做按需引入；`jsxImportSource: "nativewind"` 让 `className` 直接生效
- 全局 CSS：[src/global.css](src/global.css)（NativeWind 入口，由 metro 注入）+ [src/global.web.css](src/global.web.css)（仅 Web）

### 平台分流
文件级用平台后缀：`*.web.tsx` 与 `*.tsx` 共存即可（如 `animated-icon.tsx` / `animated-icon.web.tsx`、`storage.ts` / `storage.web.ts`）。避免在单文件里堆过多 `Platform.OS` 分支。

## 目录约定

- `src/app/` — `expo-router` 路由 / layout / 页面级 screen
- `src/components/` — 跨页面复用组件；`src/components/ui/` 放更基础的 UI 积木
- `src/hooks/` — 自定义 hooks
- `src/utils/` — `api`、`auth`、`storage`、`cache` 等基础能力
- `src/constants/` — 主题与常量
- `assets/` — 运行时资源
- `docs/antd/` — 本地 antd-rn 参考文档（`llms-semantic.md` / `llms-full.txt` / `llms.txt`），新增/调整 antd 组件优先查阅
- `design/` — 设计稿，**不**作为运行时代码来源
- `android/`、`ios/` — 原生工程，JS 改动默认不动
- `dist/` — 构建产物，不要手工编辑

路径别名（[tsconfig.json](tsconfig.json)）：`@/*` → `src/*`，`@/assets/*` → `assets/*`。

## 踩坑提醒

- React 19 + `experiments.reactCompiler: true` 已开启，**别**为了"优化"到处补 `useMemo` / `useCallback`，编译器自己会处理
- TypeScript strict，新代码别用 `any` 糊过去
- 复用现有基础组件 / Tailwind token，别在页面里重复造按钮/输入框/弹窗/状态标签或散落魔法颜色值
- `airpush.sh <runtimeVersion>` 发热更新时，`runtimeVersion` 必须等于已安装客户端的 `app.json -> expo.version`（policy 是 `appVersion`），否则设备拉不到
- `pnpm reset-project` 会把 `app/` 移走，正常开发**绝对**别跑
- 不要引入重量级新依赖，除非现有方案明显不够用
- `app.json` / `babel.config.js` / `metro.config.js` / `tailwind.config.js` 改前先想清楚影响面

## 仓库与外部上下文

- 上一级目录 `Loan/` 下还有 `server/`（后端）、`expo-updates-server/`（热更新服务）、`ant-design-mobile-rn/`（被 fork 的 antd-rn）等同胞仓库；改接口/热更新协议可能需要联动它们，但默认不跨仓改
- 接口 base URL `https://loan.ewaga.com/api/v1/`，热更新服务 `https://updates-loan.ewaga.com/`
- iOS bundleId / Android package：`com.cdloan.assistant`，应用显示名"借贷助手" / "Loan Assistant"
- 代码签名证书在 [code-signing/](code-signing/)，`expo-updates` 用它校验热更新包，**别**提交私钥
