# 精油配方计算器 - 部署文档

## 架构

- **前端**: Vue 3 + Vite + Pinia + Vue Router（构建为静态文件，由 FastAPI serve）
- **后端**: FastAPI + SQLite，端口 8000
- **部署**: Kubernetes (k3s) on `oci.euphon.net`
- **域名**: https://oil.oci.euphon.net
- **TLS**: Traefik + Let's Encrypt (自动)

## 目录结构

```
├── backend/
│   ├── main.py          # FastAPI 应用
│   ├── database.py      # SQLite 数据库操作
│   ├── defaults.json    # 默认精油和配方数据（首次启动时 seed）
│   └── requirements.txt
├── frontend/            # Vue 3 + Vite 项目
│   ├── package.json
│   ├── vite.config.js
│   ├── index.html
│   ├── public/          # 静态资源（favicon、PWA icons）
│   └── src/
│       ├── main.js          # 入口文件
│       ├── App.vue          # 根组件
│       ├── router/          # Vue Router 路由配置
│       ├── stores/          # Pinia 状态管理
│       │   ├── auth.js      # 认证/用户
│       │   ├── oils.js      # 精油价格
│       │   ├── recipes.js   # 配方/标签/收藏
│       │   ├── diary.js     # 个人配方日记
│       │   └── ui.js        # UI 状态
│       ├── composables/     # 组合式函数
│       │   ├── useApi.js    # API 请求封装
│       │   ├── useDialog.js # 自定义对话框
│       │   ├── useSmartPaste.js    # 智能粘贴解析
│       │   └── useOilTranslation.js # 精油中英翻译
│       ├── components/      # 共享组件
│       │   ├── RecipeCard.vue
│       │   ├── RecipeDetailOverlay.vue
│       │   ├── TagPicker.vue
│       │   ├── LoginModal.vue
│       │   ├── UserMenu.vue
│       │   └── CustomDialog.vue
│       ├── views/           # 页面组件
│       │   ├── RecipeSearch.vue     # 配方查询
│       │   ├── RecipeManager.vue    # 管理配方
│       │   ├── Inventory.vue        # 个人库存
│       │   ├── OilReference.vue     # 精油价目
│       │   ├── Projects.vue         # 商业核算
│       │   ├── MyDiary.vue          # 我的（日记/品牌/账号）
│       │   ├── AuditLog.vue         # 操作日志
│       │   ├── BugTracker.vue       # Bug 追踪
│       │   └── UserManagement.vue   # 用户管��
│       └── assets/
│           └── styles.css   # 全局样式
├── deploy/
│   ├── namespace.yaml
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── pvc.yaml         # 1Gi 持久卷，存放 SQLite 数据库
│   ├── ingress.yaml
│   ├── setup-kubeconfig.sh
│   └── kubeconfig
├── Dockerfile           # 多阶段构建（Node → Python）
└── doc/deploy.md
```

## 本地开发

```bash
# 前端开发（热更新）
cd frontend
npm install
npm run dev    # 默认 http://localhost:5173，自动代理 /api 到 :8000

# 后端开发
cd backend
pip install -r requirements.txt
uvicorn backend.main:app --reload --port 8000
```

## 构建

```bash
# 前端构建
cd frontend
npm run build  # 输出到 frontend/dist/

# Docker 构建（多阶段：先构建前端，再打包后端）
docker build -t oil-calculator .
```

## 首次部署

已完成，以下为记录。

```bash
# 1. SSH 到服务器
ssh fam@oci.euphon.net

# 2. 上传项目
scp oil-calc.tar.gz fam@oci.euphon.net:/tmp/
ssh fam@oci.euphon.net "mkdir -p ~/oil-calculator && cd ~/oil-calculator && tar xzf /tmp/oil-calc.tar.gz"

# 3. 构建并推送镜像
cd ~/oil-calculator
docker build -t registry.oci.euphon.net/oil-calculator:latest .
docker push registry.oci.euphon.net/oil-calculator:latest

# 4. 部署 k8s 资源
cd deploy
kubectl apply -f namespace.yaml
kubectl apply -f pvc.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f ingress.yaml

# 5. 需要 regcred 才能拉取私有镜像（从已有 ns 复制）
kubectl get secret regcred -n guitar -o yaml | sed 's/namespace: guitar/namespace: oil-calculator/' | kubectl apply -f -
```

## 更新部署

```bash
# 在本地打包上传
cd /path/to/oil
tar czf /tmp/oil-calc.tar.gz Dockerfile backend/ frontend/ deploy/ --exclude='frontend/node_modules' --exclude='frontend/dist'
scp /tmp/oil-calc.tar.gz fam@oci.euphon.net:/tmp/

# SSH 到服务器构建并重启
ssh fam@oci.euphon.net
cd ~/oil-calculator && tar xzf /tmp/oil-calc.tar.gz
docker build -t registry.oci.euphon.net/oil-calculator:latest .
docker push registry.oci.euphon.net/oil-calculator:latest
kubectl rollout restart deploy/oil-calculator -n oil-calculator
```

或者使用受限 kubeconfig（仅重启，不含构建）：

```bash
KUBECONFIG=deploy/kubeconfig kubectl rollout restart deploy/oil-calculator
```

## 受限 Kubeconfig

文件位于 `deploy/kubeconfig`，权限范围：
- **Namespace**: `oil-calculator` only
- **ServiceAccount**: `oil-calculator-deployer`
- **权限**: pods, services, deployments, replicasets, ingresses, PVC, configmaps, secrets, pods/log 的完整 CRUD
- **无法**访问其他 namespace 或集群级资源

重新生成：`bash deploy/setup-kubeconfig.sh`

## K8s 配置要点

- **Ingress class**: `traefik`
- **TLS annotation**: `traefik.ingress.kubernetes.io/router.tls.certresolver: le`
- **镜像仓库**: `registry.oci.euphon.net`（需要 `regcred` secret）
- **数据持久化**: PVC 挂载到 `/data`，SQLite 数据库在 `/data/oil_calculator.db`

## API 端点

| Method | Path | 说明 |
|--------|------|------|
| GET | /api/oils | 所有精油列表 |
| POST | /api/oils | 添加/更新精油 |
| DELETE | /api/oils/{name} | 删除精油 |
| GET | /api/recipes | 所有配方列表 |
| POST | /api/recipes | 新增配方 |
| PUT | /api/recipes/{id} | 更新配方 |
| DELETE | /api/recipes/{id} | 删除配方 |
| GET | /api/tags | 所有标签 |
| POST | /api/tags | 新增标签 |
| DELETE | /api/tags/{name} | 删除标签 |
| GET | /api/me | 当前用户信息 |
| POST | /api/login | 登录 |
| POST | /api/register | 注册 |
| GET | /api/diary | 个人配方列表 |
| POST | /api/diary | 创建个人配方 |
| PUT | /api/diary/{id} | 更新个人配方 |
| DELETE | /api/diary/{id} | 删除个人配方 |
| GET | /api/favorites | 收藏列表 |
| POST | /api/favorites/{id} | 添加收藏 |
| DELETE | /api/favorites/{id} | 取消收藏 |
| GET | /api/inventory | 个人库存 |
| POST | /api/inventory | 更新库存 |
| GET | /api/projects | 商业项目列表 |
| GET | /api/audit-log | 操作日志 |
| GET | /api/users | 用户列表（管理员）|
| GET | /api/bug-reports | Bug 列表 |
| GET | /api/notifications | 通知列表 |
| GET | /api/version | 服务器版本 |
