# ONE · 一个博客 一个能跑通全流程的博客 MVP:后台登录 → 写文章(长文 / 短文)→ 发布 → 前台按「07 融合 + Twitter 信息流」风格展示。 - 后端:Go(`net/http` + `database/sql`,SQLite / PostgreSQL 双支持),在 `backend/` - 前台 + 后台:Vue 3 + Vue Router + Vite,在 `frontend/`(pnpm 管理依赖) - Markdown:服务端用 `goldmark` 渲染入库,编辑器实时预览用 `marked` + `DOMPurify` ## 一条命令跑起来 ```bash make dev # 后端 8080 + 前端 3000(前端带热更新,已配好 /api 代理) ``` 打开 http://localhost:3000 是前台,http://localhost:3000/admin 是后台。 停掉:`make stop` 想要单端口(前端构建产物由 Go 直接托管,接近生产形态): ```bash make start # 先 pnpm run build,再起 Go,只开 8080 ``` 打开 http://localhost:8080 。 其它:`make web` 只构建前端、`make server` 只起后端、`make test` 跑后端测试、`make clean` 清构建产物、`make db-reset` 清空本地 SQLite 数据。 ## 后台登录 默认账号密码来自环境变量,未设置时是 `admin` / `admin`(启动日志会打印出来): ```bash ONE_ADMIN_USER=admin ONE_ADMIN_PASSWORD=换一个 make start ``` 登录态是服务端签发的 httpOnly cookie(HMAC-SHA256,7 天有效),同时支持 `Authorization: Bearer `,方便用 curl / 脚本写文章。 ## 环境变量 | 变量 | 默认 | 说明 | | --- | --- | --- | | `ONE_ADDR` | `:8080` | 监听地址 | | `ONE_DB_DRIVER` | `sqlite` | `sqlite` 或 `postgres` | | `ONE_DB_DSN` | `./data/one.db` | 数据库连接串(Postgres 示例:`postgres://user:pass@localhost/one?sslmode=disable`) | | `ONE_ADMIN_USER` | `admin` | 后台用户名 | | `ONE_ADMIN_PASSWORD` | `admin` | 后台密码 | | `ONE_SECRET` | 随机生成 | 会话签名密钥;不设的话重启后登录态失效 | | `ONE_SITE_URL` | `http://localhost:8080` | RSS 里的站点地址,部署时务必改成真实域名 | | `ONE_WEB_DIST` | `./frontend/dist` | 前端构建产物目录 | | `ONE_DATA_DIR` | `./data` | SQLite 数据目录 | ## 目录结构 ``` backend/ Go 后端(模块名 oneblog) main.go 路由装配、静态托管 SPA、优雅退出 internal/config 环境变量 internal/db SQLite / PostgreSQL 连接与 `?` → `$n` 占位符重写 internal/model 数据结构 internal/store schema 迁移 + 全部 SQL internal/render goldmark 渲染、阅读时长估算、摘要截取 internal/api 公开接口 + RSS(/api/*、/rss.xml) internal/admin 后台接口与登录鉴权(/api/admin/*) internal/httpx JSON 读写助手 frontend/ Vue 3 前端(pnpm) src/styles.css 设计 token 与正文排版(07 风格) src/views 前台:时间线 / 详情 / 归档 / 标签 / 关于 src/admin 后台:登录 / 列表 / 编辑器 / 标签 / 设置 src/components 左栏导航、右栏卡片、时间线行 ``` 原有那一版 Vue 前端(07 风格重写之前)保留在 git 历史的第一个 commit `edee708` 里, 需要对照或回滚:`git checkout edee708 -- frontend/src`。 ## 接口 公开(无需登录): ``` GET /api/site 站点设置 GET /api/posts?kind=&tag=&q=&page=&size= 已发布文章(kind: long|short) GET /api/posts/:slug 文章详情 GET /api/archive 按年 → 月分组 GET /api/tags 标签与计数 GET /rss.xml (/feed 同) RSS 2.0 ``` 后台(需登录): ``` POST /api/admin/login {username,password} → {token} POST /api/admin/logout GET /api/admin/me GET /api/admin/posts?status=&kind=&q=&page=&size= POST /api/admin/posts 新建 GET /api/admin/posts/:id PUT /api/admin/posts/:id 更新(编辑器自动保存走这里) DELETE /api/admin/posts/:id GET /api/admin/tags POST /api/admin/tags {name} PUT /api/admin/tags/:id {name} DELETE /api/admin/tags/:id GET /api/admin/settings PUT /api/admin/settings ``` `Post` 的关键字段:`kind`(`long` / `short`)、`title`、`slug`、`summary`、`content_md`、`content_html`、`status`、`published_at`、`reading_minutes`、`tags[]`。 短文没有可见标题,但 `title` 仍会由正文首句生成,供归档与标签列表索引。 ## 部署 生产形态是「一个二进制 + 一个静态目录」: ```bash make build # 产出 ./one-server,前端已打进 web/dist ONE_ADDR=:8080 \ ONE_SITE_URL=https://your.domain \ ONE_DB_DRIVER=postgres \ ONE_DB_DSN="postgres://user:pass@127.0.0.1/one?sslmode=disable" \ ONE_SECRET=一串随机长字符串 \ ONE_ADMIN_PASSWORD=强密码 \ ./one-server ``` 要点: - **静态资源**:Go 直接托管 `web/dist`,未知路径回落到 `index.html`,所以 `/post/xxx`、`/admin` 刷新都不会 404。 - **反向代理**:前面挂 Nginx / Caddy 时把 `/` 反代到 `ONE_ADDR` 即可;如果只在内网监听,可以不挂。 - **SQLite 部署**:把 `data/` 挂到持久卷;并发写很低的博客足够用(已开 WAL + busy_timeout)。 - **PostgreSQL 部署**:改 `ONE_DB_DRIVER` 与 `ONE_DB_DSN`,schema 会在启动时自动创建,无需手动迁移。 - **systemd 示例**: ```ini [Unit] Description=ONE blog After=network.target [Service] WorkingDirectory=/opt/one Environment=ONE_ADDR=:8080 ONE_SITE_URL=https://your.domain ONE_SECRET=xxx ONE_ADMIN_PASSWORD=xxx ExecStart=/opt/one/one-server Restart=always [Install] WantedBy=multi-user.target ``` - **备份**:SQLite 直接拷 `data/one.db`(停写更稳);Postgres 用 `pg_dump`。 ## 已验证 `make start` 后:后台登录 → 写长文与短文 → 发布 → 前台时间线(长文标题 + 摘要 / 短文正文铺开)、详情、归档、标签、RSS 全部可见; 编辑器改正文 1.5 秒后自动保存到服务端;桌面与移动宽度无横向滚动;SQLite 路径全流程跑通,PostgreSQL 的占位符重写有单元测试覆盖(本机没有 Postgres 实例,未做端到端验证)。 ## 待确认 - 仓库地址:本仓库是在本地 `/Users/cjun/Code/one` 新建的 git 仓库,还没有 remote。给个地址就能推上去并绑进工作区。 - 鉴权方式:目前是账号密码 + 签名 cookie。想改成纯 token 也可以,接口已经兼容 `Authorization: Bearer`。 - 图片上传:MVP 未做,正文里先用图片外链。