# pure-goweb **Repository Path**: pwginfo/pure-goweb ## Basic Information - **Project Name**: pure-goweb - **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-09-23 - **Last Updated**: 2026-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # pure-goweb 基于 Gin + GORM + SQLite 的 Go Web 项目骨架,目标是作为 Web 项目的框架起点。 > **当前状态:骨架可用(`go run .` 可直接启动),P0 已全部完成;纯骨架不含业务代码与业务表。** > 剩余待办与已知限制见文末「已知限制」及 [docs/TODO.md](./docs/TODO.md)。 ## 技术栈 | 层 | 选型 | 说明 | | --- | --- | --- | | HTTP | `github.com/gin-gonic/gin` v1.12.0 | | | ORM | `gorm.io/gorm` v1.31.2 | 开启了 `TranslateError` | | 数据库 | `github.com/glebarez/sqlite` | 纯 Go 实现(modernc 系),**不依赖 CGO**,Windows 下无需 MinGW/MSVC | | 迁移 | `github.com/golang-migrate/migrate/v4` | 与 GORM **共用同一个连接池** | | 日志 | `github.com/sirupsen/logrus` | | | 缓存 | `github.com/patrickmn/go-cache` | 仅内存,未接 Redis | | 配置 | `github.com/goccy/go-yaml` | 根目录 `config.yaml` | | Go | 1.26.4 | | ## 快速开始 ```bash go run . ``` 启动后监听 `config.yaml` 中 `server.port`(默认 `10808`)。 首次启动会自动完成: 1. 加载根目录 `config.yaml`(缺失字段回落到 `pkg/configs/config.go` 默认值); 2. 创建数据目录(默认 `data/`,不存在时); 3. 打开 SQLite 连接池(WAL 模式); 4. 执行迁移目录(默认 `pkg/db/migration/`)下所有未应用的迁移;**迁移目录为空时正常启动,不报错**; 5. 把 `*gorm.DB` 交给 `route.Setup()` 装配路由。 重复启动时迁移已全部应用(`migrate.ErrNoChange`),会直接跳过。探活:`curl -sS http://127.0.0.1:10808/api/v1/healthz`。 ### 接入业务(3 步) 骨架不提供业务代码,按下列顺序接入: 1. **加表**:在 `pkg/db/migration/` 从 `000001` 起新建 `000001_{描述}.up.sql`(表名 `t_{模块}_{实体}`); 2. **建模块**:在 `internal/modules/<模块>/<实体>/` 写 `model.go` / `repository.go` / `service.go` / `handler.go` / `router.go`(详见下方「分层约定」); 3. **注册路由**:在根目录 `route/router.go` 的 `authed` / `public` 组挂载模块的 `RegisterRoutes`。 ### 配置文件 根目录 `config.yaml`: ```yaml server: port: 10808 database: data_dir: data file_name: pure-goweb.db migration_dir: pkg/db/migration jwt: # ⚠ 部署前必须替换为高强度随机值;当前为示例值,泄露即可伪造 token secret: devops-center-jwt-secret-change-me token_ttl_hours: 24 issuer: pure-goweb # JWT iss;缺省回落 pure-goweb rate_limit: enabled: false # 默认不启用限流 max_requests: 100 # 窗口内最大请求数 window_seconds: 60 # 窗口秒数 cors: allow_origins: [] # Origin 白名单;[] = 不回写 ACAO;可填 ["*"] 或具体源 log: level: info # debug | info | warn | error format: text # text | json ``` 配置结构体与加载逻辑见 `pkg/configs/config.go`;启动时 `main.go` 调用 `configs.Load("config.yaml")`,之后通过全局 `configs.App` 读取。 **部署前必须**:替换 `jwt.secret`(仓库内仍是示例值 `devops-center-jwt-secret-change-me`),否则任何拿到该值的人都可伪造合法 JWT。 ### 环境注意事项 - 数据库路径由 `database.data_dir` + `file_name` 拼接(相对**进程工作目录**解析),因此需要在项目根目录启动。 - 若构建时报 `~/.cache/go-build ... read-only file system`,是构建缓存目录不可写,可临时指定: ```bash GOCACHE=/tmp/gocache go build ./... ``` ## 目录结构 ``` . ├── main.go # 入口:加载配置 → DB → 装配 → 听信号优雅关停 ├── config.yaml # 应用配置(端口 / 数据库 / JWT) ├── route/ │ └── router.go # 路由装配:中间件挂载 / 业务模块注册 ├── pkg/ # 基建(可复用、与业务解耦) │ ├── configs/config.go # Config 结构体 + Load(goccy/go-yaml)+ 默认值 │ ├── db/ │ │ ├── database.go # InitDatabase:建目录、开连接池、跑迁移、返回 *gorm.DB │ │ ├── migrate_sqlite.go # 自定义 golang-migrate 的 database.Driver(复用连接池) │ │ └── migration/ # 版本化迁移脚本(骨架可为空;接入方加表时再写) │ ├── base/ # 统一响应封装(唯一 JSON 出口)+ 业务错误 + 分页 │ ├── middleware/ # 中间件 │ ├── jwt/ # JWT 签发/解析/白名单(secret、TTL 读配置) │ └── cache/ # 内存缓存(main.go 调用 Init) ├── internal/modules/ # 业务模块(internal/modules/<模块>/<实体>/;当前尚无示例) └── data/ # 运行时数据(SQLite,已 gitignore) ``` 分层约定:**装配在根(`route/`)、基建在 `pkg/`、业务在 `internal/`**。新代码按此归位,避免基建与业务混放。 ## 分层约定 业务模块统一放在 `internal/modules/<模块>/<实体>/`,每个模块五个文件: | 文件 | 职责 | | --- | --- | | `model.go` | GORM 实体定义,字段与迁移脚本列名保持一致 | | `repository.go` | 只做数据存取,不写业务规则 | | `service.go` | 业务逻辑,返回 `error`(未来返回统一业务错误码) | | `handler.go` | HTTP 出入参绑定与响应,不写业务逻辑 | | `router.go` | 暴露 `RegisterRoutes(r *gin.RouterGroup, db *gorm.DB)`,在根目录 `route/router.go` 中挂载 | **表名 ↔ 模块**:新表强制 `t_{模块}_{实体}`,例 `t_sys_user` → `internal/modules/sys/user/`。 **新增一个业务模块的标准流程**:写迁移 → 建目录 → 写 model → repository → service → handler → 在根目录 `route/router.go` 注册。 ## 数据库与迁移 - 迁移脚本命名:`{版本号}_{描述}.{up|down}.sql`,版本号补零到 6 位(如 `000001_create_t_sys_user.up.sql`)。 - **骨架不提供业务表**;历史 `users` 迁移已删除。接入方按需从 `000001` 起新增脚本。 - 新表名规则:`t_{模块}_{实体}`(如 `t_sys_user`),对应模块目录 `internal/modules/<模块>/<实体>/`。 - 从旧骨架升级:删除本地 `data/*.db` 后再 `go run .`,避免 `schema_migrations` 与空迁移目录不一致。 - 一个迁移文件内可以包含多条 SQL,`pkg/db/migrate_sqlite.go` 会把它整体包在一个事务里执行。 - 迁移状态表为 `schema_migrations`,由 `pkg/db/migrate_sqlite.go` 自动创建。 ### 为什么自己实现 migrate 的 SQLite 驱动 `pkg/db/migrate_sqlite.go` 不是多余的封装,它绕开了两个坑: - `migrate/database/sqlite` 会 import `modernc.org/sqlite` 并注册驱动名 `sqlite`,与本项目 GORM 侧 `glebarez/go-sqlite` 的**同名注册冲突**,两者同时链接会在进程启动时 panic:`sql: Register called twice for driver sqlite`; - `migrate/database/sqlite3` 基于 `mattn/go-sqlite3`,需要 CGO 和本地 C 工具链。 因此按 migrate 官方推荐的 "WithInstance —— 复用已有连接" 模式自实现驱动,行为与官方 `database/sqlite` 保持一致(相同的版本表结构、事务包裹方式、进程内锁语义)。 **不要**为了省事改回官方驱动,会 panic。 ### 连接池参数说明 `sqliteDSN()` 中的参数都有明确目的,调整前请先理解: - `journal_mode(WAL)`:写事务不阻塞读,读不阻塞写; - `_txlock=immediate`:`BEGIN` 即拿写锁,避免两个事务中途升级写锁互相报 `SQLITE_BUSY`; - `SetMaxOpenConns(1)`:SQLite 是单文件库,单连接串行访问从根本上避免多连接写冲突。**注意这意味着写操作是串行的**,高并发写入场景需要重新评估。 ## 已知限制 **完整 P0/P1 待办见 [docs/TODO.md](./docs/TODO.md)**(下列为摘要,完成后请同步改该文档与本节)。 - **鉴权闭环已接通(P0-1)**:`middleware.Auth()` 调用 `jwt.Parse`;登录态 = **全局缓存白名单**(`jwt.Generate` 写入 `cache.C`,登出/失效删除)。骨架不提供登录业务 API(接入方自行实现)。 - **无示例业务模块(有意)**:`internal/modules/` 为空;pure-goweb 不提供业务代码。 - **中间件**:`permission.go` 有意悬空;限流已实现且默认关闭;CORS 按 `cors.allow_origins` 白名单(默认空)。 - **已具备基建**:`GET /api/v1/healthz`(public)、`X-Request-Id` 紧凑日志(text 不定宽 message;本机环回显示 `127.0.0.1`)、`log.level`/`log.format`、优雅关停。 - **无业务迁移(有意)**:`pkg/db/migration/` 可为空,`migrateUp` 已处理空目录并正常启动;旧 `users` 已删除;接入方从 `000001` 起新建。 - **统一响应未全面接入**:`base/response.go` / `base/errors.go` / `base/page.go`(分页)已就绪,骨架无业务 handler,由接入方自然使用。 - **无自动化测试**:`*_test.go` 数量为 0。 - **无 LICENSE / CI / lint 配置**。 - **`jwt.secret` 仍为示例值**:部署前必须替换(见「配置文件」)。 ## 如何跑通 ```bash go run . # 应监听 config.yaml 中的 server.port(默认 10808) ``` 首次启动会创建 `data/`、执行 `pkg/db/migration/` 下未应用的迁移(目录为空也可启动);重复启动迁移已是最新则直接跳过。接入业务按上文「接入业务(3 步)」操作。