# todos **Repository Path**: ifercarly/todos ## Basic Information - **Project Name**: todos - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-30 - **Last Updated**: 2026-10-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Marker 清单 一个 TodoMVC 风格的待办清单,前后端完全分离: - `backend/`:Go + Gin + GORM,提供 `/v1` JSON API,数据存 MySQL 8 - `frontend/`:Vite + Vue 3 + TailwindCSS,构建产物是纯静态文件 前后端通过「同源 + 反向代理」通信(开发用 Vite 代理,上线用 Nginx 反代 `/v1`), 所以后端没有 CORS 配置也不会跨域。 各自更细的说明见 [`backend/README.md`](backend/README.md) 和 [`frontend/README.md`](frontend/README.md)。 ## 项目结构 ``` gogogo/ ├── compose.yaml # mysql + api + web 全套容器编排 ├── backend/ # 后端:只提供 /v1 JSON 接口 │ ├── conf/config.ini # 本地配置(含密码,不入库) │ ├── Dockerfile # 只构建应用镜像,不含数据库 │ └── main.go └── frontend/ # 前端:纯静态 SPA ├── docker/nginx.conf.template # 容器内 Nginx:静态托管 + 反代 /v1 ├── Dockerfile # Node 构建 + Nginx 运行 ├── vite.config.js # 开发代理 /v1 → 127.0.0.1:9000 └── src/ ``` ## 前置条件 | 用途 | 版本要求 | | --- | --- | | 后端运行 | Go 1.26+ | | 数据库 | MySQL 8.x(本地或容器均可) | | 前端开发 | Node.js >= 20.19 | | 容器化部署 | Docker + Docker Compose v2 | ## 本地开发 ### 1. 准备数据库 ```sql CREATE DATABASE marker DEFAULT CHARSET=utf8mb4; ``` 表结构由程序启动时的 `AutoMigrate` 自动创建,不需要手动建表。 ### 2. 配置后端 `backend/conf/config.ini` 已被 `.gitignore` 忽略,首次拉代码从模板复制: ```bash cd backend cp conf/config.example.ini conf/config.ini ``` 按本地实际情况修改数据库账号密码(默认 `127.0.0.1:3306`,root/123456): ```ini port = 9000 release = false [mysql] user = root password = 你的数据库密码 host = 127.0.0.1 port = 3306 db = marker ``` ### 3. 启动后端 ```bash cd backend go run . # 等价于 go run . conf/config.ini ``` 启动后可验证: - `GET http://127.0.0.1:9000/` —— 服务信息与接口列表 - `GET http://127.0.0.1:9000/healthz` —— 健康检查,会 ping 一次数据库, 数据库不可用时返回 `503` ### 4. 启动前端 ```bash cd frontend npm install npm run dev # http://localhost:5173 ``` 开发服务器把 `/v1` 代理到 `http://127.0.0.1:9000`,所以**先起后端再起前端**。 代理目标可用环境变量 `VITE_API_TARGET` 改(见 `frontend/.env.example`)。 ## 部署 部署要分两块,**两块都必须做,缺一不可**: - **后端** = API + MySQL - **前端** = 静态站点(并且要能把 `/v1` 反向代理到后端) 下面的 A / B / C 是三种「前后端组合方式」,任选**一种组合**即可; 选定之后,该组合里的后端、前端步骤都要执行完。 | 组合 | 后端怎么跑 | 前端怎么跑 | 适合场景 | | --- | --- | --- | --- | | A(推荐) | 根目录 compose:`mysql` + `api` + `web` | 同左,前端容器内 Nginx 托管 | 单机部署,想一条命令起全套 | | B | 同一份 compose 只起 `mysql` + `api` | 前端容器(Nginx)或任意静态托管 | 前后端分开维护,前端放 CDN / 另一台机器 | | C | 裸机 `go build` + 系统 Nginx | `npm run build` + 系统 Nginx | 服务器上已有 Nginx 和 MySQL,不装 Docker | > 只跑后端不会有页面(后端只提供 JSON API,访问 9000 根路径只会看到接口列表); > 只跑前端也只是一张空页面(`/v1` 反代不到后端,列表加载会报错)。 ### 方式 A:一条命令起全套(推荐) 仓库根目录的 `compose.yaml` 已经把三个服务编好了:`mysql`(`mysql:8.3`,数据存命名卷 `marker-mysql-data`)、`api`(`backend/Dockerfile`,多阶段构建、非 root 运行)、 `web`(`frontend/Dockerfile`,容器内 Nginx 托管静态页并把 `/v1` 反代到 `api:9000`)。 在根目录执行(密码通过环境变量传入,见下文「敏感信息放哪」): ```bash docker compose up -d --build docker compose ps # 三个服务都是 running / healthy 即可 ``` 访问 **http://localhost:8080** 就是完整应用(前端页面 + 后端接口一起打通)。 - 库和账号自动创建:`mysql` 容器会建 `marker` 库,并建一个**只对 marker 库有权限**的应用账号 (环境变量 `MARKER_DB_USER` / `MARKER_DB_PASSWORD`),`api` 用的就是它,不是 root - 没提供这两个变量时会用开发默认值(root `123456`,应用账号 `marker` / `marker_dev_password`), **上线必须改**:在平台的环境变量面板里设置,或先 `export MARKER_DB_PASSWORD=...` 再 `up` - 启动顺序由 `depends_on: service_healthy` 保证,程序侧还有 15 秒左右的 指数退避重试兜底 - `api` 默认只在 compose 内网暴露;要在宿主机直接 curl 接口,取消 `compose.yaml` 里 `api.ports` 的注释(`9000:9000`) - 数据库同理默认不暴露,要直连调试就取消 `mysql.ports` 的注释(映射到宿主机 3306) > 如果你之前已经用旧配置跑过,`marker-mysql-data` 卷里只有 root 账号,新建的 > `marker` 用户不会补建,`api` 会报 `Access denied`。二选一: > 进容器手动 `CREATE USER` + `GRANT SELECT,INSERT,UPDATE,DELETE ON marker.* TO ...`, > 或 `docker compose down -v` 重建(**会删数据**)。 ### 方式 B:后端和前端分开部署 #### B-1 后端(+ MySQL) 还是用根目录这份 compose,只起后端两个服务: ```bash docker compose up -d --build api # 会连带拉起 mysql(depends_on) docker compose ps # mysql healthy、api running 即就绪 ``` 想在宿主机验证接口,先把 `compose.yaml` 里 `api.ports` 取消注释,再: ```bash curl http://127.0.0.1:9000/healthz # {"status":"ok"} ``` 常用命令(都在根目录执行): ```bash docker compose logs -f api # 后端日志 docker compose restart api # 改完环境变量 / 代码后重建用 up -d --build api docker compose down # 停止,数据保留 docker compose down -v # 停止并删数据,慎用 ``` #### B-2 前端(静态站) 前端有两种跑法,选一种,都要把 `/v1` 反代到 B-1 里起的后端: **跑法 1:本地构建 + 任意静态服务器** ```bash cd frontend npm ci npm run build # 产物在 frontend/dist npm run preview # 本地预览构建产物,http://localhost:4173 ``` `dist` 交给 Nginx / Caddy / 对象存储托管,反向代理部分参考下面方式 C 里的 Nginx 配置。 **跑法 2:前端容器(容器内 Nginx 同时负责静态托管和反代 `/v1`)** ```bash cd frontend docker build -t marker-web . docker run -d --name marker-web -p 8080:80 \ -e API_UPSTREAM=http://host.docker.internal:9000 \ marker-web # 打开 http://localhost:8080 ``` - `API_UPSTREAM`:后端地址,默认 `http://host.docker.internal:9000`(后端跑在宿主机) - Linux 访问宿主机需额外加 `--add-host=host.docker.internal:host-gateway` - 如果后端是 B-1 里的 compose 容器,它默认不映射 9000 端口,所以要么先取消 `compose.yaml` 里 `api.ports` 的注释,要么让前端容器加入同一个网络: `docker run --network gogogo_default -e API_UPSTREAM=http://api:9000 ...` (网络名一般是 `<目录名>_default`,用 `docker network ls` 确认) ### 方式 C:裸机 + 系统 Nginx(后端 + 前端) 后端(MySQL 和 `conf/config.ini` 先按[本地开发](#本地开发)准备好): ```bash cd backend go build -o marker ./marker conf/config.ini # 或用 systemd / supervisor 托管 ``` 前端: ```bash cd frontend && npm run build sudo cp -r dist/* /var/www/marker/ ``` Nginx 配置(同域部署,关键是把 `/v1` 反代到后端,并用 `try_files` 兜住 history 路由): ```nginx server { listen 80; server_name todo.example.com; root /var/www/marker; index index.html; location / { try_files $uri $uri/ /index.html; # 少了这行,刷新 /active 会 404 } location /v1/ { proxy_pass http://127.0.0.1:9000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` ## 配置与环境变量 后端:先读配置文件,同名环境变量覆盖它;配置文件不存在时只用环境变量 (容器里的行为),必填项缺失会直接报错退出。 **上线时不需要配置文件**:把下表的 7 个环境变量注入进程环境,就能完全覆盖 `config.ini`。 镜像里本来也不含配置。如果所在平台只能改文件,可以挂载到 `/conf/config.ini` (容器工作目录是 `/`),或把路径作为启动参数传进去 —— 详见 [`COURSE.md`](COURSE.md) 附录 F。 | 配置项 | 配置文件 | 环境变量 | 必填 | | --- | --- | --- | --- | | API 端口 | `port` | `PORT` | 是 | | Gin release 模式 | `release` | `RELEASE` | 否 | | MySQL 地址 | `[mysql] host` | `MYSQL_HOST` | 是 | | MySQL 端口 | `[mysql] port` | `MYSQL_PORT` | 是 | | MySQL 用户 | `[mysql] user` | `MYSQL_USER` | 是 | | MySQL 密码 | `[mysql] password` | `MYSQL_PASSWORD` | 否(允许空) | | 数据库名 | `[mysql] db` | `MYSQL_DB` | 是 | 前端: | 变量 | 作用 | 默认值 | | --- | --- | --- | | `VITE_API_BASE_URL` | Axios 的 baseURL,一般留空(同源),需要绝对地址时才填 | 空 | | `VITE_API_TARGET` | 开发 / 预览服务器把 `/v1` 代理到哪 | `http://127.0.0.1:9000` | | `API_UPSTREAM` | 前端**容器**内 Nginx 反代的后端地址 | `http://host.docker.internal:9000` | ## 敏感信息放哪 密码这类东西**不要写进会入库的文件**,放哪取决于你在哪个场景跑: | 场景 | 放哪里 | 说明 | | --- | --- | --- | | 本机 `go run .` | `backend/conf/config.ini`(或直接设环境变量) | 已在 `.gitignore`,只提交 `config.example.ini` | | `docker compose` | 平台的环境变量面板,或 shell 里 `export` | compose 用 `${VAR}` 插值后注入容器;仓库里不托管密码文件 | | 生产 / 多实例 | 密钥管理:Docker secret、k8s Secret、Vault、云厂商 Secrets Manager | 需要轮换、审计时用;本地文件和面板都不算密钥系统 | 几个容易误解的点: - **明文文件之间没有谁更安全**:`config.ini`、`.env` 都是明文。安全性来自「不进仓库、不进镜像、 运行时注入、最小权限账号」,而不是文件后缀。 - **本项目不提供 `.env` 文件**:Go 进程只认真实环境变量,多一份 `.env` 就多一层 「谁覆盖谁」的心智负担。有些平台的界面上把它叫「.env 编辑框」, 本质仍然是把 `KEY=VALUE` 注进容器环境,效果一样,不需要额外文件。 - 临时 `MARKER_DB_PASSWORD=xxx docker compose up` 会把密码留在 shell 历史里, 长期部署用平台面板或 secret 更合适。 - 应用账号用**最小权限**:compose 里 `MYSQL_USER` 只对 `marker` 库有权限, 不要图省事让应用拿 root。 - 交付镜像里不含任何配置(`backend/Dockerfile` 没有 `COPY conf/`), 密码只在运行时通过环境变量注入。 ## API | 方法 | 路径 | 请求体 | 说明 | | --- | --- | --- | --- | | GET | `/v1/todo` | - | 全部待办 | | POST | `/v1/todo` | `{"title":"买牛奶"}` | 新增待办 | | PUT | `/v1/todo/:id` | `{"status":true}` / `{"title":"..."}` | 更新状态或标题 | | DELETE | `/v1/todo/:id` | - | 删除待办 | ## 常见问题 - **刷新 `/active` 或 `/completed` 404**:服务器没配 `try_files $uri $uri/ /index.html`, history 路由必须把未命中的路径回退到 `index.html`。 - **页面能打开但列表一直加载失败**:只部署了前端,`/v1` 后面的后端没起或反代地址不对, 检查 `API_UPSTREAM`(容器)或 Nginx 的 `proxy_pass`。 - **`/healthz` 返回 503**:HTTP 服务活着但 MySQL 连不上,先看数据库地址、账号密码和网络连通性。 - **部署在子路径(如 `/todo/`)**:构建时也要指定同样的 base(`vite build --base=/todo/`), 否则静态资源和路由都会错位。