Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 121928733b | |||
| 987b16fd41 |
382
INTEGRATION.md
382
INTEGRATION.md
@@ -1,366 +1,192 @@
|
|||||||
# GoCommon 业务项目对接操作手册
|
# GoCommon 业务项目对接手册
|
||||||
|
|
||||||
本文档供**引用 GoCommon 的业务项目**使用。按本手册对接即可,无需重复沟通基础设施实现方式。
|
|
||||||
|
|
||||||
模块路径:`git.toowon.com/jimmy/go-common`
|
模块路径:`git.toowon.com/jimmy/go-common`
|
||||||
|
|
||||||
> 本文档只描述**目标架构**,重构时一步到位,**不提供过渡期用法,不考虑向后兼容**。
|
> 目标架构一步到位,不考虑向后兼容。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 对接原则
|
## 1. 原则
|
||||||
|
|
||||||
| 原则 | 说明 |
|
| 原则 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Factory 只是入口 | 启动时初始化一次,通过 getter 获取各模块**对象** |
|
| Factory 只是入口 | 启动初始化一次,getter 取模块对象 |
|
||||||
| 能力在模块自身 | 使用 `log.Info()`、`db.Find()`、`store.Upload()`,不在 Factory 上堆透传方法 |
|
| 能力在模块自身 | `log.Info()`、`db.Find()`、`store.Upload()`,不在 Factory 透传 |
|
||||||
| 按需引用 | 用什么模块取什么对象;无状态工具直接 `import tools` |
|
| 按需配置 | 只用到的模块写进 `config.json` |
|
||||||
| 不重写基础设施 | 数据库连接、Redis 客户端、统一 HTTP 出参、中间件链由 GoCommon 提供 |
|
| 不重写基础设施 | 连接池、HTTP 出参、中间件链由本库提供 |
|
||||||
| 业务只管业务 | Service 返回数据;Handler 交给 `http.Handler` 出参;Migration 只写 SQL 文件 |
|
| 业务只管业务 | Service 返回数据;出参用 `http.Handler`;迁移只写 SQL |
|
||||||
|
|
||||||
|
登录鉴权、参数校验、出站 HTTP Client 等由**业务项目自行实现**,不在本库范围。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 环境准备
|
## 2. 安装
|
||||||
|
|
||||||
### 2.1 配置私有模块
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go env -w GOPRIVATE=git.toowon.com
|
go env -w GOPRIVATE=git.toowon.com
|
||||||
go env -w GONOPROXY=git.toowon.com
|
go env -w GONOPROXY=git.toowon.com
|
||||||
go env -w GONOSUMDB=git.toowon.com
|
go env -w GONOSUMDB=git.toowon.com
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Git 认证(SSH 推荐)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git config --global url."git@git.toowon.com:".insteadOf "https://git.toowon.com/"
|
git config --global url."git@git.toowon.com:".insteadOf "https://git.toowon.com/"
|
||||||
|
|
||||||
|
go get git.toowon.com/jimmy/go-common@v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.3 安装依赖
|
配置示例:[`config/example.json`](./config/example.json)。
|
||||||
|
|
||||||
```bash
|
|
||||||
go get git.toowon.com/jimmy/go-common@v2.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
在 `go.mod` 中:
|
|
||||||
|
|
||||||
```go
|
|
||||||
require git.toowon.com/jimmy/go-common v2.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
配置示例见 [`config/example.json`](./config/example.json)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 推荐项目结构
|
## 3. 推荐结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
your-project/
|
your-project/
|
||||||
├── config.json
|
├── config.json
|
||||||
├── cmd/
|
├── cmd/server/main.go # 复制 templates/server/main.go
|
||||||
│ ├── server/main.go
|
├── cmd/migrate/main.go # 复制 templates/migrate/main.go
|
||||||
│ └── migrate/main.go # 从 templates/migrate/main.go 复制
|
|
||||||
├── migrations/
|
├── migrations/
|
||||||
│ └── 20240101000001_create_users.sql
|
├── locales/ # 可选
|
||||||
├── locales/ # i18n(可选)
|
├── internal/handler/
|
||||||
│ ├── zh-CN.json
|
├── internal/service/
|
||||||
│ └── en-US.json
|
|
||||||
├── internal/
|
|
||||||
│ ├── handler/
|
|
||||||
│ └── service/
|
|
||||||
└── go.mod
|
└── go.mod
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 启动初始化(只做一次)
|
## 4. 启动(只做一次)
|
||||||
|
|
||||||
```go
|
```bash
|
||||||
package main
|
cp templates/server/main.go cmd/server/main.go
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.toowon.com/jimmy/go-common/factory"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if err := factory.Init("config.json"); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
app := factory.Default()
|
|
||||||
chain := app.MiddlewareChain()
|
|
||||||
chain.Append(yourAuthMiddleware)
|
|
||||||
|
|
||||||
userSvc, err := NewUserService(app)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Handle("/api/users", chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
listUsers(w, r, userSvc, app)
|
|
||||||
}))
|
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**禁止**在每个 Handler 里 `NewFromFile` / `Init`。全局只初始化一次,通过 `factory.Default()` 或注入 `*factory.Factory`。
|
模板默认已包含:`MustInit` + `buildOptions`(Option 挂载点)+ `MustLogger` + `Warmup` + `MiddlewareChain` + `NewHandler` + `Close`。
|
||||||
|
|
||||||
|
复制后主要改三处:
|
||||||
|
|
||||||
|
| 函数 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `buildOptions()` | `WithLogger` / `WithStorage` 等自定义注入 |
|
||||||
|
| `setupMiddleware()` | `chain.Append(业务鉴权…)` |
|
||||||
|
| `registerRoutes()` | 注册业务路由 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/server -config config.json -addr :8080
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 获取模块对象(Factory getter)
|
## 5. 获取模块对象
|
||||||
|
|
||||||
连接与客户端由 Factory **lazy 初始化并缓存**。业务侧**不要**自行 `gorm.Open` / `redis.NewClient`。
|
连接由 Factory lazy 缓存。业务侧不要自行 `gorm.Open` / `redis.NewClient`。
|
||||||
|
|
||||||
| 模块 | API | 用法 |
|
| 模块 | API | 说明 |
|
||||||
|------|-----|------|
|
|------|-----|------|
|
||||||
| 配置 | `app.Config()` | 读原始配置 |
|
| 配置 | `Config()` | 原始配置 |
|
||||||
| 数据库 | `app.Database()` | `db.Find(&users)`、`db.Transaction(...)` |
|
| 数据库 | `Database()` / `MustDatabase()` | `*gorm.DB` |
|
||||||
| Redis | `app.Redis()` | `rds.Set(ctx, key, val, ttl)` |
|
| Redis | `Redis()` / `MustRedis()` | `*redis.Client` |
|
||||||
| 日志 | `app.Logger()` | `log.Info(...)`,退出前 `log.Close()` |
|
| 日志 | `Logger()` / `MustLogger()` | 退出用 `Close()` |
|
||||||
| 存储 | `app.Storage()` | `store.Upload(ctx, key, reader)` |
|
| 存储 | `Storage()` / `MustStorage()` | Upload / GetURL |
|
||||||
| 邮件 | `app.Email()` | `mail.SendEmail(...)` |
|
| 邮件 | `Email()` / `MustEmail()` | SendEmail / SendEmailAsync |
|
||||||
| 短信 | `app.SMS()` | `sms.SendSMS(...)` |
|
| 短信 | `SMS()` / `MustSMS()` | SendSMS / SendSMSAsync |
|
||||||
| Excel | `app.Excel()` | `ex.ExportToFile(...)` |
|
| Excel | `Excel()` | 每次新建导出器 |
|
||||||
| 国际化 | `app.I18n()` | 供 `http.Handler` 注入;或 `i18n.GetMessage(...)` |
|
| 国际化 | `I18n()` / `MustI18n()` | 一般由 `NewHandler` 注入 |
|
||||||
| 中间件链 | `app.MiddlewareChain()` | `chain.Append(...).ThenFunc(...)` |
|
| HTTP 出参 | `NewHandler(w, r)` | `Success` / `Error` / `ErrorData` |
|
||||||
| 迁移 | `app.Migrator("migrations")` | `m.Up()` / `m.Status()` / `m.Down()` |
|
| 中间件 | `MiddlewareChain()` | `Append` / `ThenFunc` |
|
||||||
|
| 迁移 | `Migrator(dir)` | Up / Down / Status |
|
||||||
|
| 生命周期 | `Close()` / `Warmup(...)` | 收口 / 启动预热 |
|
||||||
|
|
||||||
### 注入 Service 示例
|
`MustXxx` 仅用于 **main / 启动注入**(失败 panic)。
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type UserService struct {
|
func NewUserService(app *factory.Factory) *UserService {
|
||||||
db *gorm.DB
|
return &UserService{db: app.MustDatabase(), rds: app.MustRedis()}
|
||||||
rds *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserService(app *factory.Factory) (*UserService, error) {
|
|
||||||
db, err := app.Database()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rds, err := app.Redis()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &UserService{db: db, rds: rds}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *UserService) List(page, size int) (users []User, total int64, err error) {
|
|
||||||
s.db.Model(&User{}).Count(&total)
|
|
||||||
err = s.db.Offset((page-1)*size).Limit(size).Find(&users).Error
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`config.json` 未配置的模块,调用 getter 会返回错误——**只配置使用的模块即可**。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. HTTP 统一出参
|
## 6. HTTP 统一出参
|
||||||
|
|
||||||
### 6.1 职责划分
|
| 层级 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| Service | 返回数据或 error |
|
||||||
|
| Handler | 解析、调 Service、`h.Success` / `h.Error` |
|
||||||
|
| `http.Handler` | 统一 JSON 信封、i18n、timestamp |
|
||||||
|
|
||||||
| 层级 | 做什么 |
|
响应格式(HTTP 恒 200):
|
||||||
|------|--------|
|
|
||||||
| Service | 返回 `[]User`、`total`、业务 error |
|
|
||||||
| Handler | 解析请求、调 Service、通过 `http.Handler` 出参 |
|
|
||||||
| `http.Handler` | PageData → Response → JSON(结构 / 编码 / 语种 / 时间统一) |
|
|
||||||
|
|
||||||
**禁止**在 Service 拼 JSON,**禁止**在业务项目自定义 `Response` 结构,**禁止**经 Factory 出参(无 `app.Success` / `app.Error`)。
|
|
||||||
|
|
||||||
### 6.2 标准响应格式
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{ "code": 0, "message": "success", "timestamp": 1704067200, "data": {} }
|
||||||
"code": 0,
|
|
||||||
"message": "success",
|
|
||||||
"timestamp": 1704067200,
|
|
||||||
"data": {}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
分页时 `data`:
|
分页 `data`:`{ "list", "total", "page", "pageSize" }`。
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"list": [],
|
|
||||||
"total": 100,
|
|
||||||
"page": 1,
|
|
||||||
"pageSize": 20
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
类型定义在 `http` 包:`http.Response`、`http.PageData`。
|
|
||||||
|
|
||||||
### 6.3 Handler 用法(唯一方式)
|
|
||||||
|
|
||||||
中间件链须包含 `Language`、`Timezone`(`MiddlewareChain()` 已默认组装)。
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import commonhttp "git.toowon.com/jimmy/go-common/http"
|
h := app.NewHandler(w, r) // 自动注入已配置的 i18n
|
||||||
|
|
||||||
func listUsers(w http.ResponseWriter, r *http.Request, svc *UserService, app *factory.Factory) {
|
|
||||||
i18n, _ := app.I18n()
|
|
||||||
h := commonhttp.NewHandler(w, r, commonhttp.WithI18n(i18n))
|
|
||||||
|
|
||||||
var req ListUserRequest
|
var req ListUserRequest
|
||||||
if err := h.ParseJSON(&req); err != nil {
|
if err := h.ParseJSON(&req); err != nil {
|
||||||
h.Error("common.invalid_request")
|
h.Error("common.invalid_request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
users, total, err := svc.List(h.Pagination().GetPage(), h.Pagination().GetPageSize())
|
||||||
p := h.Pagination()
|
|
||||||
users, total, err := svc.List(p.GetPage(), p.GetPageSize())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.Error("user.list_failed")
|
h.Error("user.list_failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.SuccessPage(users, total)
|
h.SuccessPage(users, total)
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`http.Handler` 统一负责:
|
请求头:`Accept-Language`(语种)、`X-Timezone`(默认 `Asia/Shanghai`)。
|
||||||
|
|
||||||
- `Content-Type: application/json; charset=utf-8`
|
|
||||||
- 从 context 读取语种、时区
|
|
||||||
- 消息码(如 `user.not_found`)经 i18n 转为文案与业务 code
|
|
||||||
- `timestamp` 按统一时区策略写入
|
|
||||||
|
|
||||||
### 6.4 请求头约定
|
|
||||||
|
|
||||||
| Header | 用途 |
|
|
||||||
|--------|------|
|
|
||||||
| `Accept-Language` | 响应消息语种 |
|
|
||||||
| `X-Timezone` | 时区(默认 `Asia/Shanghai`) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 数据库迁移
|
## 7. 数据库迁移
|
||||||
|
|
||||||
执行框架已封装,业务**只写 SQL 文件**。
|
|
||||||
|
|
||||||
### 7.1 推荐:独立 migrate 命令(与 Web 解耦)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp templates/migrate/main.go cmd/migrate/main.go
|
cp templates/migrate/main.go cmd/migrate/main.go
|
||||||
go build -o bin/migrate cmd/migrate/main.go
|
go build -o bin/migrate cmd/migrate/main.go
|
||||||
|
./bin/migrate up|status|down
|
||||||
./bin/migrate up
|
|
||||||
./bin/migrate status
|
|
||||||
./bin/migrate down
|
|
||||||
./bin/migrate up -config config.json -dir migrations
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
模板默认走 Factory:`MustInit` + 日志 + `Warmup(Database)` + `Migrator` + `Close`;同样可用 `buildOptions()` 注入自定义 DB/Logger。
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- migrations/20240101000001_create_users.sql
|
-- migrations/20240101000001_create_users.sql
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(255) NOT NULL);
|
||||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
username VARCHAR(255) NOT NULL
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- migrations/20240101000001_create_users.down.sql
|
-- migrations/20240101000001_create_users.down.sql
|
||||||
DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
```
|
```
|
||||||
|
|
||||||
独立 CLI 内部调用 `migration.RunMigrationsFromConfigWithCommand`,无需业务实现连接逻辑。
|
|
||||||
|
|
||||||
### 7.2 可选:经 Factory(开发 / 小项目)
|
|
||||||
|
|
||||||
```go
|
|
||||||
m, err := app.Migrator("migrations")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := m.Up(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 Docker
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
command: sh -c "./bin/migrate up && ./bin/server"
|
|
||||||
volumes:
|
|
||||||
- ./config.json:/app/config.json:ro
|
|
||||||
- ./migrations:/app/migrations:ro
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 各模块按需对接
|
## 8. 模块速查
|
||||||
|
|
||||||
### 8.1 日志
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
log, _ := app.Logger()
|
// 日志
|
||||||
defer log.Close()
|
app.MustLogger().Info("服务启动", nil)
|
||||||
log.Info("服务启动")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 存储
|
// 存储
|
||||||
|
|
||||||
```go
|
|
||||||
store, _ := app.Storage()
|
store, _ := app.Storage()
|
||||||
store.Upload(ctx, "images/a.jpg", fileReader, "image/jpeg")
|
_ = store.Upload(ctx, "images/a.jpg", reader, "image/jpeg")
|
||||||
url, _ := store.GetURL("images/a.jpg", 3600)
|
|
||||||
```
|
|
||||||
|
|
||||||
不经 Factory 时:`storage.NewStorage(storage.StorageTypeLocal, cfg)`。
|
// 邮件 / 短信(HTTP 通知用 Async;验证码用同步)
|
||||||
|
app.MustEmail().SendEmailAsync(ctx, []string{"a@b.com"}, "主题", "正文")
|
||||||
|
app.MustSMS().SendSMSAsync(ctx, []string{"13800138000"}, map[string]string{"code": "123456"})
|
||||||
|
|
||||||
### 8.3 邮件 / 短信
|
// Excel
|
||||||
|
app.Excel().ExportToFile("users.xlsx", "用户列表", columns, users)
|
||||||
|
|
||||||
```go
|
// i18n(locales 目录;出参经 NewHandler 自动用)
|
||||||
mail, _ := app.Email()
|
app.MustI18n().GetMessage("zh-CN", "user.not_found")
|
||||||
defer mail.Close()
|
|
||||||
|
|
||||||
// HTTP 通知类:异步,不阻塞请求
|
|
||||||
mail.SendEmailAsync(r.Context(), []string{"a@b.com"}, "主题", "正文")
|
|
||||||
|
|
||||||
// 验证码等需等待结果:同步
|
|
||||||
mail.SendEmail([]string{"a@b.com"}, "主题", "正文")
|
|
||||||
|
|
||||||
sms, _ := app.SMS()
|
|
||||||
defer sms.Close()
|
|
||||||
sms.SendSMSAsync(r.Context(), []string{"13800138000"}, map[string]string{"code": "123456"})
|
|
||||||
sms.SendSMS([]string{"13800138000"}, map[string]string{"code": "123456"})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.4 Excel
|
|
||||||
|
|
||||||
```go
|
|
||||||
ex := app.Excel()
|
|
||||||
ex.ExportToFile("users.xlsx", "用户列表", columns, users)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.5 国际化
|
|
||||||
|
|
||||||
```go
|
|
||||||
i18n, _ := app.I18n()
|
|
||||||
i18n.LoadFromDir("locales")
|
|
||||||
msg := i18n.GetMessage("zh-CN", "user.not_found")
|
|
||||||
```
|
|
||||||
|
|
||||||
HTTP 出参的 i18n 通过 `NewHandler(..., WithI18n(i18n))` 注入,Handler 内用消息码调用 `h.Error("user.not_found")`。
|
|
||||||
|
|
||||||
### 8.6 无状态工具(不经 Factory)
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "git.toowon.com/jimmy/go-common/tools"
|
|
||||||
|
|
||||||
|
// tools(不经 Factory)
|
||||||
tools.Now()
|
tools.Now()
|
||||||
tools.MD5("text")
|
tools.MD5("text")
|
||||||
tools.YuanToCents(100.5)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. config.json 最小示例
|
## 9. 最小 config.json
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -389,41 +215,23 @@ tools.YuanToCents(100.5)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 反模式(禁止)
|
## 10. 反模式
|
||||||
|
|
||||||
- 每个 Handler 内 `factory.Init` / `NewFromFile`
|
- Handler 内重复 `Init`
|
||||||
- 业务 Service 内写 HTTP JSON 响应
|
- Service 拼 HTTP JSON / 自定义 Response
|
||||||
- 自行 `gorm.Open` / `redis.NewClient`
|
- 自行 `gorm.Open` / `redis.NewClient`
|
||||||
- 使用 Factory 透传:`LogInfo`、`RedisSet`、`Success`、`Error`、`Now`、`MD5` 等
|
- Factory 透传(`LogInfo`、`Success`、`Now` 等)
|
||||||
- 直接调用 `http.Success(w, ...)` 包级函数(应使用 `http.Handler`)
|
- 请求路径滥用 `MustXxx`
|
||||||
- 在业务项目复制 GoCommon 的 Response / 中间件实现
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. 故障排除
|
## 11. 故障排除
|
||||||
|
|
||||||
**无法下载模块:**
|
| 问题 | 处理 |
|
||||||
|
|
||||||
```bash
|
|
||||||
go env -w GOPRIVATE=git.toowon.com
|
|
||||||
git config --global url."git@git.toowon.com:".insteadOf "https://git.toowon.com/"
|
|
||||||
go get git.toowon.com/jimmy/go-common@latest
|
|
||||||
```
|
|
||||||
|
|
||||||
**依赖错误:** `go clean -modcache && go mod tidy`
|
|
||||||
|
|
||||||
**getter 报 config is nil:** 补充 `config.json` 对应段,或不调用该 getter
|
|
||||||
|
|
||||||
**迁移找不到文件:** 检查 `-dir` 与 SQL 文件命名
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. 文档与版本
|
|
||||||
|
|
||||||
| 文档 | 用途 |
|
|
||||||
|------|------|
|
|------|------|
|
||||||
| 本文档 | 业务项目对接 |
|
| 无法下载模块 | 检查 `GOPRIVATE` 与 SSH insteadOf |
|
||||||
| [README.md](./README.md) | 库概述 |
|
| 依赖异常 | `go clean -modcache && go mod tidy` |
|
||||||
| [VERSION.md](./VERSION.md) | 版本发布 |
|
| getter 报 config is nil | 补配置段,或不调用该模块 |
|
||||||
|
| 迁移找不到文件 | 检查 `-dir` 与 SQL 命名 |
|
||||||
|
|
||||||
版本升级见 [VERSION.md](./VERSION.md)。
|
版本发布见 [VERSION.md](./VERSION.md)。
|
||||||
|
|||||||
65
README.md
65
README.md
@@ -1,56 +1,45 @@
|
|||||||
# GoCommon - Go 通用工具类库
|
# GoCommon
|
||||||
|
|
||||||
供其他 Go 项目引用的通用工具集合。业务项目对接请直接阅读:
|
供其他 Go 项目引用的通用工具库。对接请看:**[INTEGRATION.md](./INTEGRATION.md)**
|
||||||
|
|
||||||
**[业务项目对接操作手册(INTEGRATION.md)](./INTEGRATION.md)**
|
|
||||||
|
|
||||||
## 模块路径
|
|
||||||
|
|
||||||
```
|
```
|
||||||
git.toowon.com/jimmy/go-common
|
git.toowon.com/jimmy/go-common
|
||||||
```
|
```
|
||||||
|
|
||||||
## 安装
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go env -w GOPRIVATE=git.toowon.com
|
go env -w GOPRIVATE=git.toowon.com
|
||||||
go get git.toowon.com/jimmy/go-common@v2.0.0
|
go get git.toowon.com/jimmy/go-common@v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## 设计概要
|
## 设计
|
||||||
|
|
||||||
- **Factory**:入口,启动时初始化一次,按需 getter 获取模块对象(DB、Redis、Logger 等)
|
- **Factory**:启动初始化一次,getter 取模块对象
|
||||||
- **各模块包**:能力在对象方法上(`log.Info()`、`store.Upload()`)
|
- **模块包**:`object.Method()`(如 `log.Info`、`store.Upload`)
|
||||||
- **http 包**:统一 HTTP 出参(Response / PageData / Handler)
|
- **http**:统一出参(`Handler`)
|
||||||
- **migration**:独立 CLI 或 Factory 执行 SQL 迁移
|
- **tools / migration**:无状态或独立 CLI,不经 Factory 透传
|
||||||
- **tools**:无状态工具函数,直接 import
|
|
||||||
|
|
||||||
## 功能模块
|
## 模块
|
||||||
|
|
||||||
| 模块 | 包路径 | 说明 |
|
| 包 | 说明 |
|
||||||
|------|--------|------|
|
|----|------|
|
||||||
| 配置 | `config` | JSON 配置加载 |
|
| `config` | JSON 配置 |
|
||||||
| 工厂 | `factory` | 统一入口与 lazy getter |
|
| `factory` | 入口与 lazy getter |
|
||||||
| HTTP | `http` | 请求解析、统一响应 |
|
| `http` | 请求解析、统一响应 |
|
||||||
| 中间件 | `middleware` | CORS、日志、Recovery、限流、语种、时区 |
|
| `middleware` | CORS、日志、Recovery、限流、语种、时区 |
|
||||||
| 工具 | `tools` | 时间、加密、金额、类型转换 |
|
| `logger` | 异步日志 |
|
||||||
| 日志 | `logger` | 异步日志 |
|
| `storage` | Local / OSS / MinIO |
|
||||||
| 存储 | `storage` | Local / OSS / MinIO |
|
| `email` / `sms` | SMTP、阿里云短信 |
|
||||||
| 邮件 / 短信 | `email` / `sms` | SMTP、阿里云短信 |
|
| `excel` | 导出 |
|
||||||
| Excel | `excel` | 数据导出 |
|
| `i18n` | 多语言消息 |
|
||||||
| 国际化 | `i18n` | 多语言消息 |
|
| `migration` | SQL 迁移 |
|
||||||
| 迁移 | `migration` | SQL 版本管理 |
|
| `tools` | 时间、加密、金额等 |
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
| 文档 | 说明 |
|
| 文档 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [INTEGRATION.md](./INTEGRATION.md) | 业务项目对接操作手册 |
|
| [INTEGRATION.md](./INTEGRATION.md) | 对接手册 |
|
||||||
| [VERSION.md](./VERSION.md) | 版本管理与发布 |
|
| [VERSION.md](./VERSION.md) | 版本与发布 |
|
||||||
| [templates/](./templates/) | migrate 等脚手架模板 |
|
| [templates/](./templates/) | server / migrate 等脚手架 |
|
||||||
| [config/example.json](./config/example.json) | 配置文件示例 |
|
| [config/example.json](./config/example.json) | 配置示例 |
|
||||||
| [examples/](./examples/) | 代码示例 |
|
| [examples/](./examples/) | 示例代码 |
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|||||||
146
VERSION.md
146
VERSION.md
@@ -1,143 +1,33 @@
|
|||||||
# 版本管理说明
|
# 版本管理
|
||||||
|
|
||||||
## 版本号规则
|
遵循 [语义化版本](https://semver.org/lang/zh-CN/):`v主.次.修订`。
|
||||||
|
|
||||||
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/) 规范:
|
## 发布
|
||||||
|
|
||||||
- **主版本号(MAJOR)**:当你做了不兼容的 API 修改
|
|
||||||
- **次版本号(MINOR)**:当你做了向下兼容的功能性新增
|
|
||||||
- **修订号(PATCH)**:当你做了向下兼容的问题修正
|
|
||||||
|
|
||||||
版本格式:`v主版本号.次版本号.修订号`,例如:`v1.0.0`、`v1.1.0`、`v2.0.0`
|
|
||||||
|
|
||||||
## 发布新版本
|
|
||||||
|
|
||||||
### 方式1:使用发布脚本(推荐)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 使用发布脚本(会自动验证版本格式、检查未提交更改等)
|
# 推荐:脚本校验并打标签
|
||||||
./scripts/release.sh v1.0.0 "Release version 1.0.0"
|
./scripts/release.sh v1.2.0 "Release version 1.2.0"
|
||||||
|
|
||||||
|
# 或手动
|
||||||
|
git tag -a v1.2.0 -m "Release version 1.2.0"
|
||||||
|
git push origin v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
### 方式2:手动创建标签
|
发布前:代码已提交;同步更新本文档与 `README.md` 中的版本号。
|
||||||
|
|
||||||
|
## 消费方引用
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 确保所有更改已提交
|
go get git.toowon.com/jimmy/go-common@v1.2.0 # 生产固定版本
|
||||||
git add .
|
go get git.toowon.com/jimmy/go-common@latest # 开发
|
||||||
git commit -m "Prepare for release v1.0.0"
|
|
||||||
|
|
||||||
# 2. 创建版本标签
|
|
||||||
git tag -a v1.0.0 -m "Release version 1.0.0"
|
|
||||||
|
|
||||||
# 3. 推送标签到远程仓库
|
|
||||||
git push origin v1.0.0
|
|
||||||
|
|
||||||
# 或者一次性推送所有标签
|
|
||||||
git push origin --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
### 验证标签
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看所有标签
|
|
||||||
git tag -l
|
|
||||||
|
|
||||||
# 查看标签详情
|
|
||||||
git show v1.0.0
|
|
||||||
|
|
||||||
# 查看标签列表(带注释)
|
|
||||||
git tag -l -n
|
|
||||||
```
|
|
||||||
|
|
||||||
## 调用方如何使用版本
|
|
||||||
|
|
||||||
### 方式1:使用最新版本(推荐用于开发)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get git.toowon.com/jimmy/go-common@latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方式2:使用特定版本(推荐用于生产)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 使用具体版本号
|
|
||||||
go get git.toowon.com/jimmy/go-common@v1.0.0
|
|
||||||
|
|
||||||
# 使用版本范围(自动选择最新版本)
|
|
||||||
go get git.toowon.com/jimmy/go-common@v1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方式3:在 go.mod 中指定版本
|
|
||||||
|
|
||||||
```go
|
|
||||||
module your-project
|
|
||||||
|
|
||||||
require (
|
|
||||||
git.toowon.com/jimmy/go-common v1.0.0
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
然后运行:
|
|
||||||
```bash
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方式4:更新到最新版本
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 更新到最新版本
|
|
||||||
go get -u git.toowon.com/jimmy/go-common@latest
|
|
||||||
|
|
||||||
# 更新到最新补丁版本(如从 v1.0.0 更新到 v1.0.1)
|
|
||||||
go get -u=patch git.toowon.com/jimmy/go-common
|
|
||||||
|
|
||||||
# 更新到最新次版本(如从 v1.0.0 更新到 v1.1.0)
|
|
||||||
go get -u=minor git.toowon.com/jimmy/go-common
|
|
||||||
```
|
|
||||||
|
|
||||||
## 版本发布流程
|
|
||||||
|
|
||||||
1. **开发完成**:确保所有功能已实现并通过测试
|
|
||||||
2. **更新版本号**:在 `README.md` 和 `VERSION.md` 中更新版本号
|
|
||||||
3. **提交代码**:提交所有更改到 Git
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "Prepare for release v1.0.0"
|
|
||||||
```
|
|
||||||
4. **创建并推送标签**:
|
|
||||||
```bash
|
|
||||||
# 使用脚本(推荐)
|
|
||||||
./scripts/release.sh v1.0.0 "Release version 1.0.0"
|
|
||||||
|
|
||||||
# 或手动创建
|
|
||||||
git tag -a v1.0.0 -m "Release version 1.0.0"
|
|
||||||
git push origin v1.0.0
|
|
||||||
```
|
|
||||||
5. **验证**:在其他项目中测试是否能正确获取该版本
|
|
||||||
```bash
|
|
||||||
# 在新项目中测试
|
|
||||||
go get git.toowon.com/jimmy/go-common@v1.0.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 当前版本
|
## 当前版本
|
||||||
|
|
||||||
当前版本:**v2.0.0**
|
**v1.2.0**
|
||||||
|
|
||||||
## 版本历史
|
## 版本历史
|
||||||
|
|
||||||
- **v2.0.0** (当前版本,Breaking)
|
- **v1.2.0** — DX:`MustInit` / `Warmup` / `MustXxx` / `Close` / `NewHandler`;Option 注入;Excel 每次新建;`templates/server`;`ErrorData`;文档精简
|
||||||
- Factory 精简为 `Init` / `Default()` + lazy getter,删除全部透传方法
|
- **v1.1.0** — 同上 DX 能力首次合入(见上)
|
||||||
- HTTP 出参统一由 `http.Handler` 负责,删除包级 `Success` / `SystemError`
|
- **v1.0.0** — 初始:Factory lazy getter、http.Handler 统一出参、logger / email / sms / storage / middleware / migration / tools / i18n / excel
|
||||||
- Logger API 精简为 `Debug/Info/Error(msg, fields)`,新增 Request ID + `FromContext`
|
|
||||||
- email / sms 新增异步队列 + `Close()`
|
|
||||||
- 中间件链默认顺序:Recovery → RequestID → Logging → …
|
|
||||||
|
|
||||||
- **v1.0.0**
|
|
||||||
- 初始版本
|
|
||||||
- 包含所有基础工具类:migration、datetime、http、middleware、config、storage、email、sms、factory、logger
|
|
||||||
|
|
||||||
- **v1.1.0** (未发布)
|
|
||||||
- storage:新增本地文件夹存储(LocalStorage)
|
|
||||||
- config:新增 `localStorage` 配置段
|
|
||||||
- factory:支持 Local/MinIO/OSS 自动选择
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ func main() {
|
|||||||
|
|
||||||
func listUsers(w http.ResponseWriter, r *http.Request) {
|
func listUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
app := factory.Default()
|
app := factory.Default()
|
||||||
i18n, _ := app.I18n()
|
h := app.NewHandler(w, r)
|
||||||
h := commonhttp.NewHandler(w, r, commonhttp.WithI18n(i18n))
|
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Keyword string `json:"keyword"`
|
Keyword string `json:"keyword"`
|
||||||
@@ -42,11 +41,9 @@ func listUsers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p := h.Pagination()
|
|
||||||
users := []User{
|
users := []User{
|
||||||
{ID: 1, Name: "User1", Email: "user1@example.com"},
|
{ID: 1, Name: "User1", Email: "user1@example.com"},
|
||||||
{ID: 2, Name: "User2", Email: "user2@example.com"},
|
{ID: 2, Name: "User2", Email: "user2@example.com"},
|
||||||
}
|
}
|
||||||
h.SuccessPage(users, 100)
|
h.SuccessPage(users, 100)
|
||||||
_ = p
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,18 @@
|
|||||||
{
|
{
|
||||||
|
"common.success": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "success"
|
||||||
|
},
|
||||||
|
"common.invalid_request": {
|
||||||
|
"code": 100001,
|
||||||
|
"message": "Invalid request"
|
||||||
|
},
|
||||||
"user.not_found": {
|
"user.not_found": {
|
||||||
"code": 100002,
|
"code": 100002,
|
||||||
"message": "User not found"
|
"message": "User not found"
|
||||||
},
|
},
|
||||||
"user.login_success": {
|
"system.internal_error": {
|
||||||
"code": 0,
|
|
||||||
"message": "Login successful"
|
|
||||||
},
|
|
||||||
"user.welcome": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Welcome, %s"
|
|
||||||
},
|
|
||||||
"user.logout": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Logout"
|
|
||||||
},
|
|
||||||
"error.invalid_params": {
|
|
||||||
"code": 100001,
|
|
||||||
"message": "Invalid parameters"
|
|
||||||
},
|
|
||||||
"error.server_error": {
|
|
||||||
"code": 100003,
|
"code": 100003,
|
||||||
"message": "Server error"
|
"message": "Internal server error"
|
||||||
},
|
|
||||||
"order.created": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Order created successfully"
|
|
||||||
},
|
|
||||||
"order.paid": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Order paid successfully"
|
|
||||||
},
|
|
||||||
"message.count": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "You have %d new messages"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,18 @@
|
|||||||
{
|
{
|
||||||
|
"common.success": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "成功"
|
||||||
|
},
|
||||||
|
"common.invalid_request": {
|
||||||
|
"code": 100001,
|
||||||
|
"message": "请求无效"
|
||||||
|
},
|
||||||
"user.not_found": {
|
"user.not_found": {
|
||||||
"code": 100002,
|
"code": 100002,
|
||||||
"message": "用户不存在"
|
"message": "用户不存在"
|
||||||
},
|
},
|
||||||
"user.login_success": {
|
"system.internal_error": {
|
||||||
"code": 0,
|
|
||||||
"message": "登录成功"
|
|
||||||
},
|
|
||||||
"user.welcome": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "欢迎,%s"
|
|
||||||
},
|
|
||||||
"user.logout": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "退出登录"
|
|
||||||
},
|
|
||||||
"error.invalid_params": {
|
|
||||||
"code": 100001,
|
|
||||||
"message": "参数无效"
|
|
||||||
},
|
|
||||||
"error.server_error": {
|
|
||||||
"code": 100003,
|
"code": 100003,
|
||||||
"message": "服务器错误"
|
"message": "服务器错误"
|
||||||
},
|
|
||||||
"order.created": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "订单创建成功"
|
|
||||||
},
|
|
||||||
"order.paid": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "订单支付成功"
|
|
||||||
},
|
|
||||||
"message.count": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "您有 %d 条新消息"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,9 +87,12 @@ type ExportData interface {
|
|||||||
//
|
//
|
||||||
// excel.ExportToWriter(w, "用户列表", columns, users)
|
// excel.ExportToWriter(w, "用户列表", columns, users)
|
||||||
func (e *Excel) ExportToWriter(w io.Writer, sheetName string, columns []ExportColumn, data interface{}) error {
|
func (e *Excel) ExportToWriter(w io.Writer, sheetName string, columns []ExportColumn, data interface{}) error {
|
||||||
if e.file == nil {
|
// 每次导出使用新文件,避免同实例多次/并发导出互相污染
|
||||||
e.file = excelize.NewFile()
|
e.file = excelize.NewFile()
|
||||||
}
|
defer func() {
|
||||||
|
_ = e.file.Close()
|
||||||
|
e.file = nil
|
||||||
|
}()
|
||||||
|
|
||||||
// 设置工作表名称
|
// 设置工作表名称
|
||||||
if sheetName == "" {
|
if sheetName == "" {
|
||||||
|
|||||||
35
excel/excel_test.go
Normal file
35
excel/excel_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package excel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExportToWriterResetsFile(t *testing.T) {
|
||||||
|
ex := NewExcel()
|
||||||
|
cols := []ExportColumn{
|
||||||
|
{Header: "ID", Field: "ID"},
|
||||||
|
{Header: "Name", Field: "Name"},
|
||||||
|
}
|
||||||
|
data := []row{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}}
|
||||||
|
|
||||||
|
var buf1, buf2 bytes.Buffer
|
||||||
|
if err := ex.ExportToWriter(&buf1, "S1", cols, data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ex.ExportToWriter(&buf2, "S1", cols, data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if buf1.Len() == 0 || buf2.Len() == 0 {
|
||||||
|
t.Fatal("expected non-empty xlsx output")
|
||||||
|
}
|
||||||
|
// 导出后不应残留内部文件句柄
|
||||||
|
if ex.file != nil {
|
||||||
|
t.Fatal("internal file should be cleared after ExportToWriter")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"git.toowon.com/jimmy/go-common/config"
|
"git.toowon.com/jimmy/go-common/config"
|
||||||
"git.toowon.com/jimmy/go-common/email"
|
"git.toowon.com/jimmy/go-common/email"
|
||||||
"git.toowon.com/jimmy/go-common/excel"
|
"git.toowon.com/jimmy/go-common/excel"
|
||||||
|
commonhttp "git.toowon.com/jimmy/go-common/http"
|
||||||
"git.toowon.com/jimmy/go-common/i18n"
|
"git.toowon.com/jimmy/go-common/i18n"
|
||||||
"git.toowon.com/jimmy/go-common/logger"
|
"git.toowon.com/jimmy/go-common/logger"
|
||||||
"git.toowon.com/jimmy/go-common/middleware"
|
"git.toowon.com/jimmy/go-common/middleware"
|
||||||
@@ -38,7 +39,6 @@ type Factory struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
i18n *i18n.I18n
|
i18n *i18n.I18n
|
||||||
excel *excel.Excel
|
|
||||||
chain *middleware.Chain
|
chain *middleware.Chain
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -47,13 +47,6 @@ type Factory struct {
|
|||||||
// Option Factory 可选项(支持重载模块实现)
|
// Option Factory 可选项(支持重载模块实现)
|
||||||
type Option func(*Factory)
|
type Option func(*Factory)
|
||||||
|
|
||||||
// WithStorage 注入自定义存储实现
|
|
||||||
func WithStorage(s storage.Storage) Option {
|
|
||||||
return func(f *Factory) {
|
|
||||||
f.storage = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init 从配置文件初始化全局 Factory(启动时调用一次)
|
// Init 从配置文件初始化全局 Factory(启动时调用一次)
|
||||||
func Init(filePath string, opts ...Option) error {
|
func Init(filePath string, opts ...Option) error {
|
||||||
cfg, err := config.LoadFromFile(filePath)
|
cfg, err := config.LoadFromFile(filePath)
|
||||||
@@ -86,6 +79,15 @@ func (f *Factory) Config() *config.Config {
|
|||||||
return f.cfg
|
return f.cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewHandler 创建 HTTP 出参处理器,并自动注入已配置的 i18n(若可用)
|
||||||
|
// 能力仍在 *http.Handler 上(Success / Error),Factory 不透传出参方法。
|
||||||
|
func (f *Factory) NewHandler(w http.ResponseWriter, r *http.Request, opts ...commonhttp.HandlerOption) *commonhttp.Handler {
|
||||||
|
if i18nInst, err := f.getI18n(); err == nil && i18nInst != nil {
|
||||||
|
opts = append([]commonhttp.HandlerOption{commonhttp.WithI18n(i18nInst)}, opts...)
|
||||||
|
}
|
||||||
|
return commonhttp.NewHandler(w, r, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Factory) getLogger() (*logger.Logger, error) {
|
func (f *Factory) getLogger() (*logger.Logger, error) {
|
||||||
if f.logger != nil {
|
if f.logger != nil {
|
||||||
return f.logger, nil
|
return f.logger, nil
|
||||||
@@ -107,7 +109,7 @@ func (f *Factory) getLogger() (*logger.Logger, error) {
|
|||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger 获取日志对象
|
// Logger 获取日志对象(无 logger 配置时使用默认配置)
|
||||||
func (f *Factory) Logger() (*logger.Logger, error) {
|
func (f *Factory) Logger() (*logger.Logger, error) {
|
||||||
return f.getLogger()
|
return f.getLogger()
|
||||||
}
|
}
|
||||||
@@ -312,21 +314,9 @@ func (f *Factory) I18n() (*i18n.I18n, error) {
|
|||||||
return f.getI18n()
|
return f.getI18n()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Factory) getExcel() *excel.Excel {
|
// Excel 创建 Excel 导出器(每次新建,避免共享可变文件状态)
|
||||||
if f.excel != nil {
|
|
||||||
return f.excel
|
|
||||||
}
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
if f.excel == nil {
|
|
||||||
f.excel = excel.NewExcel()
|
|
||||||
}
|
|
||||||
return f.excel
|
|
||||||
}
|
|
||||||
|
|
||||||
// Excel 获取 Excel 导出器
|
|
||||||
func (f *Factory) Excel() *excel.Excel {
|
func (f *Factory) Excel() *excel.Excel {
|
||||||
return f.getExcel()
|
return excel.NewExcel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MiddlewareChain 获取默认中间件链
|
// MiddlewareChain 获取默认中间件链
|
||||||
@@ -341,8 +331,11 @@ func (f *Factory) MiddlewareChain() *middleware.Chain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var mws []func(http.Handler) http.Handler
|
var mws []func(http.Handler) http.Handler
|
||||||
l, _ := f.getLogger()
|
|
||||||
i18nInst, _ := f.getI18n()
|
// logger 无配置时有默认实现;失败则降级为 nil(Recovery/Logging 可处理)
|
||||||
|
l, _ := f.getLoggerUnlocked()
|
||||||
|
// i18n 为可选:未配置时 Recovery 使用默认文案
|
||||||
|
i18nInst, _ := f.getI18nUnlocked()
|
||||||
|
|
||||||
mws = append(mws, middleware.Recovery(&middleware.RecoveryConfig{
|
mws = append(mws, middleware.Recovery(&middleware.RecoveryConfig{
|
||||||
Logger: l,
|
Logger: l,
|
||||||
@@ -384,6 +377,41 @@ func (f *Factory) MiddlewareChain() *middleware.Chain {
|
|||||||
return f.chain
|
return f.chain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getLoggerUnlocked 在已持有 f.mu 时初始化 logger(供 MiddlewareChain 使用)
|
||||||
|
func (f *Factory) getLoggerUnlocked() (*logger.Logger, error) {
|
||||||
|
if f.logger != nil {
|
||||||
|
return f.logger, nil
|
||||||
|
}
|
||||||
|
var cfg *config.LoggerConfig
|
||||||
|
if f.cfg != nil {
|
||||||
|
cfg = f.cfg.Logger
|
||||||
|
}
|
||||||
|
l, err := logger.NewLogger(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f.logger = l
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getI18nUnlocked 在已持有 f.mu 时初始化 i18n(供 MiddlewareChain 使用)
|
||||||
|
func (f *Factory) getI18nUnlocked() (*i18n.I18n, error) {
|
||||||
|
if f.i18n != nil {
|
||||||
|
return f.i18n, nil
|
||||||
|
}
|
||||||
|
if f.cfg == nil || f.cfg.I18n == nil {
|
||||||
|
return nil, fmt.Errorf("i18n config is nil")
|
||||||
|
}
|
||||||
|
i := i18n.NewI18n(f.cfg.I18n.DefaultLang)
|
||||||
|
if f.cfg.I18n.LocalesDir != "" {
|
||||||
|
if err := i.LoadFromDir(f.cfg.I18n.LocalesDir); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.i18n = i
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Migrator 创建迁移器并加载指定目录下的 SQL 文件
|
// Migrator 创建迁移器并加载指定目录下的 SQL 文件
|
||||||
func (f *Factory) Migrator(migrationsDir string) (*migration.Migrator, error) {
|
func (f *Factory) Migrator(migrationsDir string) (*migration.Migrator, error) {
|
||||||
db, err := f.getDatabase()
|
db, err := f.getDatabase()
|
||||||
|
|||||||
121
factory/factory_test.go
Normal file
121
factory/factory_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.toowon.com/jimmy/go-common/config"
|
||||||
|
"git.toowon.com/jimmy/go-common/i18n"
|
||||||
|
"git.toowon.com/jimmy/go-common/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExcelReturnsFreshInstance(t *testing.T) {
|
||||||
|
app := New(nil)
|
||||||
|
a := app.Excel()
|
||||||
|
b := app.Excel()
|
||||||
|
if a == nil || b == nil {
|
||||||
|
t.Fatal("Excel() returned nil")
|
||||||
|
}
|
||||||
|
if a == b {
|
||||||
|
t.Fatal("Excel() should return a new instance each call")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithLoggerOption(t *testing.T) {
|
||||||
|
custom, err := logger.NewLogger(&config.LoggerConfig{
|
||||||
|
Level: "error",
|
||||||
|
Output: "stdout",
|
||||||
|
Async: config.BoolPtr(false),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
app := New(nil, WithLogger(custom))
|
||||||
|
got, err := app.Logger()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != custom {
|
||||||
|
t.Fatal("WithLogger should inject the provided logger")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithI18nAndNewHandler(t *testing.T) {
|
||||||
|
i := i18n.NewI18n("zh-CN")
|
||||||
|
i.LoadFromMap("zh-CN", map[string]i18n.MessageInfo{
|
||||||
|
"common.success": {Code: 0, Message: "成功"},
|
||||||
|
})
|
||||||
|
app := New(nil, WithI18n(i))
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
||||||
|
h := app.NewHandler(rec, req)
|
||||||
|
h.Success(map[string]string{"ok": "1"})
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", rec.Code)
|
||||||
|
}
|
||||||
|
body := rec.Body.String()
|
||||||
|
if body == "" || !strings.Contains(body, "成功") {
|
||||||
|
t.Fatalf("response body missing i18n message: %s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHandlerWithoutI18n(t *testing.T) {
|
||||||
|
app := New(nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
h := app.NewHandler(rec, req)
|
||||||
|
h.Success(nil)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarmupLogger(t *testing.T) {
|
||||||
|
app := New(nil)
|
||||||
|
if err := app.Warmup(ModuleLogger); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarmupMissingDatabase(t *testing.T) {
|
||||||
|
app := New(nil)
|
||||||
|
err := app.Warmup(ModuleDatabase)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected database warmup to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarmupUnknownModule(t *testing.T) {
|
||||||
|
app := New(nil)
|
||||||
|
err := app.Warmup(Module("unknown"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected unknown module error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloseLogger(t *testing.T) {
|
||||||
|
app := New(nil)
|
||||||
|
if _, err := app.Logger(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := app.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Close 后再取 logger 应可重新创建
|
||||||
|
if _, err := app.Logger(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_ = app.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMustLogger(t *testing.T) {
|
||||||
|
app := New(nil)
|
||||||
|
l := app.MustLogger()
|
||||||
|
if l == nil {
|
||||||
|
t.Fatal("MustLogger returned nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
181
factory/lifecycle.go
Normal file
181
factory/lifecycle.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.toowon.com/jimmy/go-common/email"
|
||||||
|
"git.toowon.com/jimmy/go-common/i18n"
|
||||||
|
"git.toowon.com/jimmy/go-common/logger"
|
||||||
|
"git.toowon.com/jimmy/go-common/sms"
|
||||||
|
"git.toowon.com/jimmy/go-common/storage"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Module 可预热的模块标识(用于 Warmup)
|
||||||
|
type Module string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModuleLogger Module = "logger"
|
||||||
|
ModuleDatabase Module = "database"
|
||||||
|
ModuleRedis Module = "redis"
|
||||||
|
ModuleStorage Module = "storage"
|
||||||
|
ModuleEmail Module = "email"
|
||||||
|
ModuleSMS Module = "sms"
|
||||||
|
ModuleI18n Module = "i18n"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MustInit 从配置文件初始化全局 Factory;失败则 panic(适合 main 启动)
|
||||||
|
func MustInit(filePath string, opts ...Option) *Factory {
|
||||||
|
if err := Init(filePath, opts...); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return Default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warmup 按需预热模块:配置缺失或连接失败时立即返回错误(适合启动期 fail-fast)
|
||||||
|
func (f *Factory) Warmup(modules ...Module) error {
|
||||||
|
if f == nil {
|
||||||
|
return fmt.Errorf("factory is nil")
|
||||||
|
}
|
||||||
|
for _, m := range modules {
|
||||||
|
var err error
|
||||||
|
switch m {
|
||||||
|
case ModuleLogger:
|
||||||
|
_, err = f.Logger()
|
||||||
|
case ModuleDatabase:
|
||||||
|
_, err = f.Database()
|
||||||
|
case ModuleRedis:
|
||||||
|
_, err = f.Redis()
|
||||||
|
case ModuleStorage:
|
||||||
|
_, err = f.Storage()
|
||||||
|
case ModuleEmail:
|
||||||
|
_, err = f.Email()
|
||||||
|
case ModuleSMS:
|
||||||
|
_, err = f.SMS()
|
||||||
|
case ModuleI18n:
|
||||||
|
_, err = f.I18n()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown module: %s", m)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("warmup %s: %w", m, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭已创建的有状态模块(email / sms / redis / database / logger)
|
||||||
|
// 可重复调用;未初始化的模块会被跳过。
|
||||||
|
func (f *Factory) Close() error {
|
||||||
|
if f == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if f.email != nil {
|
||||||
|
if err := f.email.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("email: %w", err))
|
||||||
|
}
|
||||||
|
f.email = nil
|
||||||
|
}
|
||||||
|
if f.sms != nil {
|
||||||
|
if err := f.sms.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("sms: %w", err))
|
||||||
|
}
|
||||||
|
f.sms = nil
|
||||||
|
}
|
||||||
|
if f.redis != nil {
|
||||||
|
if err := f.redis.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("redis: %w", err))
|
||||||
|
}
|
||||||
|
f.redis = nil
|
||||||
|
}
|
||||||
|
if f.db != nil {
|
||||||
|
if sqlDB, err := f.db.DB(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("database: %w", err))
|
||||||
|
} else if err := sqlDB.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("database: %w", err))
|
||||||
|
}
|
||||||
|
f.db = nil
|
||||||
|
}
|
||||||
|
if f.logger != nil {
|
||||||
|
if err := f.logger.Close(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("logger: %w", err))
|
||||||
|
}
|
||||||
|
f.logger = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中间件链依赖 logger/i18n,关闭后允许下次重新组装
|
||||||
|
f.chain = nil
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustLogger 获取日志对象;失败则 panic(适合启动期注入)
|
||||||
|
func (f *Factory) MustLogger() *logger.Logger {
|
||||||
|
l, err := f.Logger()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustDatabase 获取数据库连接;失败则 panic(适合启动期注入)
|
||||||
|
func (f *Factory) MustDatabase() *gorm.DB {
|
||||||
|
db, err := f.Database()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustRedis 获取 Redis 客户端;失败则 panic(适合启动期注入)
|
||||||
|
func (f *Factory) MustRedis() *redis.Client {
|
||||||
|
c, err := f.Redis()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustStorage 获取存储对象;失败则 panic(适合启动期注入)
|
||||||
|
func (f *Factory) MustStorage() storage.Storage {
|
||||||
|
s, err := f.Storage()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustEmail 获取邮件客户端;失败则 panic(适合启动期注入)
|
||||||
|
func (f *Factory) MustEmail() *email.Email {
|
||||||
|
e, err := f.Email()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustSMS 获取短信客户端;失败则 panic(适合启动期注入)
|
||||||
|
func (f *Factory) MustSMS() *sms.SMS {
|
||||||
|
s, err := f.SMS()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustI18n 获取国际化对象;失败则 panic(适合启动期注入)
|
||||||
|
func (f *Factory) MustI18n() *i18n.I18n {
|
||||||
|
i, err := f.I18n()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
68
factory/options.go
Normal file
68
factory/options.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.toowon.com/jimmy/go-common/email"
|
||||||
|
"git.toowon.com/jimmy/go-common/i18n"
|
||||||
|
"git.toowon.com/jimmy/go-common/logger"
|
||||||
|
"git.toowon.com/jimmy/go-common/middleware"
|
||||||
|
"git.toowon.com/jimmy/go-common/sms"
|
||||||
|
"git.toowon.com/jimmy/go-common/storage"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithStorage 注入自定义存储实现
|
||||||
|
func WithStorage(s storage.Storage) Option {
|
||||||
|
return func(f *Factory) {
|
||||||
|
f.storage = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger 注入自定义日志对象
|
||||||
|
func WithLogger(l *logger.Logger) Option {
|
||||||
|
return func(f *Factory) {
|
||||||
|
f.logger = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDatabase 注入自定义数据库连接
|
||||||
|
func WithDatabase(db *gorm.DB) Option {
|
||||||
|
return func(f *Factory) {
|
||||||
|
f.db = db
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRedis 注入自定义 Redis 客户端
|
||||||
|
func WithRedis(c *redis.Client) Option {
|
||||||
|
return func(f *Factory) {
|
||||||
|
f.redis = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEmail 注入自定义邮件客户端
|
||||||
|
func WithEmail(e *email.Email) Option {
|
||||||
|
return func(f *Factory) {
|
||||||
|
f.email = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSMS 注入自定义短信客户端
|
||||||
|
func WithSMS(s *sms.SMS) Option {
|
||||||
|
return func(f *Factory) {
|
||||||
|
f.sms = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithI18n 注入自定义国际化对象
|
||||||
|
func WithI18n(i *i18n.I18n) Option {
|
||||||
|
return func(f *Factory) {
|
||||||
|
f.i18n = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMiddlewareChain 注入自定义中间件链
|
||||||
|
func WithMiddlewareChain(c *middleware.Chain) Option {
|
||||||
|
return func(f *Factory) {
|
||||||
|
f.chain = c
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,8 +82,13 @@ func (h *Handler) SuccessPage(list interface{}, total int64) {
|
|||||||
h.Success(pageData)
|
h.Success(pageData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error 失败响应(messageCode 为 i18n 消息码)
|
// Error 失败响应(messageCode 为 i18n 消息码,data 为 null)
|
||||||
func (h *Handler) Error(messageCode string, args ...interface{}) {
|
func (h *Handler) Error(messageCode string, args ...interface{}) {
|
||||||
|
h.ErrorData(messageCode, nil, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorData 失败响应并附带 data
|
||||||
|
func (h *Handler) ErrorData(messageCode string, data interface{}, args ...interface{}) {
|
||||||
code := 0
|
code := 0
|
||||||
message := messageCode
|
message := messageCode
|
||||||
if h.i18n != nil {
|
if h.i18n != nil {
|
||||||
@@ -91,5 +96,5 @@ func (h *Handler) Error(messageCode string, args ...interface{}) {
|
|||||||
code = info.Code
|
code = info.Code
|
||||||
message = info.Message
|
message = info.Message
|
||||||
}
|
}
|
||||||
writeResponse(h.w, code, message, nil)
|
writeResponse(h.w, code, message, data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,17 @@
|
|||||||
# 模板文件
|
# 模板
|
||||||
|
|
||||||
这个目录包含了可以直接复制到你项目中使用的模板文件。
|
复制到业务项目后,按注释中的挂载点扩展即可。
|
||||||
|
|
||||||
## 包含的模板
|
| 文件 | 默认已接好 | 主要改哪里 |
|
||||||
|
|------|------------|------------|
|
||||||
- `migrate/main.go` - 数据库迁移工具模板 ⭐
|
| `server/main.go` | MustInit、日志、Warmup、默认中间件链、NewHandler、Close;`-config`/`-addr` | `buildOptions` / `setupMiddleware` / `registerRoutes` |
|
||||||
- `Dockerfile.example` - Docker 构建示例
|
| `migrate/main.go` | MustInit、日志、Database Warmup、Migrator、Close;`-config`/`-dir` | `buildOptions`;SQL 放 `migrations/` |
|
||||||
- `docker-compose.example.yml` - Docker Compose 示例
|
| `Dockerfile.example` 等 | 镜像 / Compose / Makefile 示例 | 按部署环境改 |
|
||||||
- `Makefile.example` - Makefile 常用命令示例
|
|
||||||
|
|
||||||
## 快速使用
|
|
||||||
|
|
||||||
### 迁移工具模板
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 复制到你的项目
|
mkdir -p cmd/server cmd/migrate
|
||||||
mkdir -p cmd/migrate
|
cp templates/server/main.go cmd/server/main.go
|
||||||
cp templates/migrate/main.go cmd/migrate/
|
cp templates/migrate/main.go cmd/migrate/main.go
|
||||||
|
|
||||||
# 2. 编译
|
|
||||||
go build -o bin/migrate cmd/migrate/main.go
|
|
||||||
|
|
||||||
# 3. 使用
|
|
||||||
./bin/migrate up
|
|
||||||
./bin/migrate -help
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker 模板
|
对接说明:[INTEGRATION.md](../INTEGRATION.md)。
|
||||||
|
|
||||||
```bash
|
|
||||||
# 复制到你的项目根目录
|
|
||||||
cp templates/Dockerfile.example Dockerfile
|
|
||||||
cp templates/docker-compose.example.yml docker-compose.yml
|
|
||||||
cp templates/Makefile.example Makefile
|
|
||||||
```
|
|
||||||
|
|
||||||
## 完整文档
|
|
||||||
|
|
||||||
详细使用说明请查看:[INTEGRATION.md](../INTEGRATION.md) 第 7 节「数据库迁移」
|
|
||||||
|
|||||||
@@ -5,145 +5,153 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"git.toowon.com/jimmy/go-common/factory"
|
||||||
"git.toowon.com/jimmy/go-common/migration"
|
"git.toowon.com/jimmy/go-common/migration"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 数据库迁移工具(黑盒模式)
|
// 数据库迁移 CLI 脚手架。
|
||||||
//
|
//
|
||||||
// 工作原理:
|
// mkdir -p cmd/migrate
|
||||||
// 此工具调用 migration.RunMigrationsFromConfigWithCommand() 方法,
|
// cp templates/migrate/main.go cmd/migrate/main.go
|
||||||
// 内部自动处理配置加载、数据库连接、迁移执行等所有细节。
|
// go build -o bin/migrate cmd/migrate/main.go
|
||||||
// 你只需要提供配置文件和SQL迁移文件即可。
|
// ./bin/migrate up|status|down
|
||||||
//
|
//
|
||||||
// 使用方式:
|
// 默认已接好:Factory 初始化、日志、Database Warmup、Migrator、Close 收口。
|
||||||
// 基本用法:
|
// 配置优先级:-config/-dir > 环境变量 CONFIG_FILE/MIGRATIONS_DIR > 默认值。
|
||||||
// ./migrate up # 使用默认配置
|
|
||||||
// ./migrate up -config /path/to/config.json # 指定配置文件
|
|
||||||
// ./migrate up -config config.json -dir db/migrations # 指定配置和迁移目录
|
|
||||||
// ./migrate status # 查看迁移状态
|
|
||||||
// ./migrate down # 回滚最后一个迁移
|
|
||||||
//
|
|
||||||
// Docker 中使用:
|
|
||||||
// # 方式1:挂载配置文件(推荐)
|
|
||||||
// docker run -v /host/config.json:/app/config.json myapp ./migrate up
|
|
||||||
//
|
|
||||||
// # 方式2:使用环境变量指定配置文件路径
|
|
||||||
// docker run -e CONFIG_FILE=/etc/app/config.json myapp ./migrate up
|
|
||||||
//
|
|
||||||
// # 方式3:指定容器内的配置文件路径
|
|
||||||
// docker run myapp ./migrate up -config /etc/app/config.json
|
|
||||||
//
|
|
||||||
// 支持的命令:
|
|
||||||
// up - 执行所有待执行的迁移
|
|
||||||
// down - 回滚最后一个迁移
|
|
||||||
// status - 查看迁移状态
|
|
||||||
//
|
|
||||||
// 配置优先级(从高到低):
|
|
||||||
// 1. 命令行参数 -config 和 -dir
|
|
||||||
// 2. 环境变量 CONFIG_FILE 和 MIGRATIONS_DIR
|
|
||||||
// 3. 默认值(config.json 和 migrations)
|
|
||||||
|
|
||||||
|
func main() {
|
||||||
var (
|
var (
|
||||||
configFile string
|
configFile string
|
||||||
migrationsDir string
|
migrationsDir string
|
||||||
showHelp bool
|
showHelp bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
flag.StringVar(&configFile, "config", "", "配置文件路径(默认 config.json)")
|
||||||
flag.StringVar(&configFile, "config", "", "配置文件路径(默认:config.json 或环境变量 CONFIG_FILE)")
|
|
||||||
flag.StringVar(&configFile, "c", "", "配置文件路径(简写)")
|
flag.StringVar(&configFile, "c", "", "配置文件路径(简写)")
|
||||||
flag.StringVar(&migrationsDir, "dir", "", "迁移文件目录(默认:migrations 或环境变量 MIGRATIONS_DIR)")
|
flag.StringVar(&migrationsDir, "dir", "", "迁移目录(默认 migrations)")
|
||||||
flag.StringVar(&migrationsDir, "d", "", "迁移文件目录(简写)")
|
flag.StringVar(&migrationsDir, "d", "", "迁移目录(简写)")
|
||||||
flag.BoolVar(&showHelp, "help", false, "显示帮助信息")
|
flag.BoolVar(&showHelp, "help", false, "显示帮助")
|
||||||
flag.BoolVar(&showHelp, "h", false, "显示帮助信息(简写)")
|
flag.BoolVar(&showHelp, "h", false, "显示帮助(简写)")
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// 显示帮助
|
|
||||||
if showHelp {
|
if showHelp {
|
||||||
printHelp()
|
printHelp()
|
||||||
os.Exit(0)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取命令(默认up)
|
|
||||||
// 支持两种方式:
|
|
||||||
// 1. 位置参数:./migrate up
|
|
||||||
// 2. 标志参数:./migrate -cmd=up(向后兼容)
|
|
||||||
command := "up"
|
command := "up"
|
||||||
args := flag.Args()
|
if args := flag.Args(); len(args) > 0 {
|
||||||
if len(args) > 0 {
|
|
||||||
command = args[0]
|
command = args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证命令
|
|
||||||
if command != "up" && command != "down" && command != "status" {
|
if command != "up" && command != "down" && command != "status" {
|
||||||
fmt.Fprintf(os.Stderr, "错误:未知命令 '%s'\n\n", command)
|
fmt.Fprintf(os.Stderr, "未知命令: %s\n\n", command)
|
||||||
printHelp()
|
printHelp()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取配置文件路径(优先级:命令行 > 环境变量 > 默认值)
|
|
||||||
// 如果未指定,RunMigrationsFromConfigWithCommand 会自动查找
|
|
||||||
if configFile == "" {
|
if configFile == "" {
|
||||||
configFile = getEnv("CONFIG_FILE", "")
|
configFile = envOr("CONFIG_FILE", "config.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取迁移目录(优先级:命令行 > 环境变量 > 默认值)
|
|
||||||
// 如果未指定,RunMigrationsFromConfigWithCommand 会使用默认值 "migrations"
|
|
||||||
if migrationsDir == "" {
|
if migrationsDir == "" {
|
||||||
migrationsDir = getEnv("MIGRATIONS_DIR", "")
|
migrationsDir = envOr("MIGRATIONS_DIR", "migrations")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行迁移(黑盒模式:内部自动处理所有细节)
|
app := factory.MustInit(configFile, buildOptions()...)
|
||||||
if err := migration.RunMigrationsFromConfigWithCommand(configFile, migrationsDir, command); err != nil {
|
defer app.Close()
|
||||||
fmt.Fprintf(os.Stderr, "错误: %v\n", err)
|
|
||||||
|
log := app.MustLogger()
|
||||||
|
|
||||||
|
if err := app.Warmup(factory.ModuleLogger, factory.ModuleDatabase); err != nil {
|
||||||
|
log.Error("warmup failed", map[string]any{"error": err.Error()})
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m, err := app.Migrator(migrationsDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("create migrator failed", map[string]any{"error": err.Error()})
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnv(key, defaultValue string) string {
|
switch command {
|
||||||
if value := os.Getenv(key); value != "" {
|
case "up":
|
||||||
return value
|
if err := m.Up(); err != nil {
|
||||||
|
log.Error("migrate up failed", map[string]any{"error": err.Error()})
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return defaultValue
|
log.Info("migrate up ok", map[string]any{"dir": migrationsDir})
|
||||||
|
fmt.Println("✓ 迁移执行成功")
|
||||||
|
|
||||||
|
case "down":
|
||||||
|
if err := m.Down(); err != nil {
|
||||||
|
log.Error("migrate down failed", map[string]any{"error": err.Error()})
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
log.Info("migrate down ok", map[string]any{"dir": migrationsDir})
|
||||||
|
fmt.Println("✓ 迁移回滚成功")
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
status, err := m.Status()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("migrate status failed", map[string]any{"error": err.Error()})
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
printStatus(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOptions 组装 Factory Option(自定义 DB / Logger 等在此注入)
|
||||||
|
func buildOptions() []factory.Option {
|
||||||
|
// 默认走 config.json。需要替换时例如:
|
||||||
|
//
|
||||||
|
// return []factory.Option{
|
||||||
|
// factory.WithLogger(customLogger),
|
||||||
|
// factory.WithDatabase(db),
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatus(status []migration.MigrationStatus) {
|
||||||
|
if len(status) == 0 {
|
||||||
|
fmt.Println("没有找到迁移")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("\n迁移状态:")
|
||||||
|
fmt.Printf("%-20s %-40s %-10s\n", "版本", "描述", "状态")
|
||||||
|
for _, s := range status {
|
||||||
|
st := "待执行"
|
||||||
|
if s.Applied {
|
||||||
|
st = "已应用"
|
||||||
|
}
|
||||||
|
fmt.Printf("%-20s %-40s %-10s\n", s.Version, s.Description, st)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOr(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func printHelp() {
|
func printHelp() {
|
||||||
fmt.Println("数据库迁移工具")
|
fmt.Println(`数据库迁移工具
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("用法:")
|
用法:
|
||||||
fmt.Println(" migrate [命令] [选项]")
|
migrate [命令] [选项]
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("命令:")
|
命令:
|
||||||
fmt.Println(" up 执行所有待执行的迁移(默认)")
|
up 执行待执行迁移(默认)
|
||||||
fmt.Println(" down 回滚最后一个迁移")
|
down 回滚最后一个迁移
|
||||||
fmt.Println(" status 查看迁移状态")
|
status 查看状态
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("选项:")
|
选项:
|
||||||
fmt.Println(" -config, -c 配置文件路径(默认: config.json)")
|
-config, -c 配置文件(默认: config.json,可用 CONFIG_FILE)
|
||||||
fmt.Println(" -dir, -d 迁移文件目录(默认: migrations)")
|
-dir, -d 迁移目录(默认: migrations,可用 MIGRATIONS_DIR)
|
||||||
fmt.Println(" -help, -h 显示帮助信息")
|
-help, -h 帮助
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("示例:")
|
示例:
|
||||||
fmt.Println(" # 使用默认配置")
|
migrate up
|
||||||
fmt.Println(" migrate up")
|
migrate up -config /etc/app/config.json -dir db/migrations
|
||||||
fmt.Println()
|
CONFIG_FILE=config.json migrate status`)
|
||||||
fmt.Println(" # 指定配置文件")
|
|
||||||
fmt.Println(" migrate up -config /etc/app/config.json")
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println(" # 指定配置和迁移目录")
|
|
||||||
fmt.Println(" migrate up -c config.json -d db/migrations")
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println(" # 使用环境变量指定配置文件路径")
|
|
||||||
fmt.Println(" CONFIG_FILE=/etc/app/config.json migrate up")
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println(" # Docker 中使用(挂载配置文件)")
|
|
||||||
fmt.Println(" docker run -v /host/config.json:/app/config.json myapp migrate up")
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("配置优先级(从高到低):")
|
|
||||||
fmt.Println(" 1. 命令行参数 -config 和 -dir")
|
|
||||||
fmt.Println(" 2. 环境变量 CONFIG_FILE 和 MIGRATIONS_DIR")
|
|
||||||
fmt.Println(" 3. 默认值(config.json 和 migrations)")
|
|
||||||
}
|
}
|
||||||
|
|||||||
100
templates/server/main.go
Normal file
100
templates/server/main.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.toowon.com/jimmy/go-common/factory"
|
||||||
|
"git.toowon.com/jimmy/go-common/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务 HTTP 服务脚手架。
|
||||||
|
//
|
||||||
|
// mkdir -p cmd/server
|
||||||
|
// cp templates/server/main.go cmd/server/main.go
|
||||||
|
//
|
||||||
|
// 默认已接好:Factory 初始化、日志、默认中间件链、NewHandler 出参、Close 收口。
|
||||||
|
// 按需改:buildOptions / setupMiddleware / registerRoutes / Warmup 模块列表。
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configPath := flag.String("config", envOr("CONFIG_FILE", "config.json"), "配置文件路径")
|
||||||
|
addr := flag.String("addr", envOr("HTTP_ADDR", ":8080"), "监听地址")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
app := factory.MustInit(*configPath, buildOptions()...)
|
||||||
|
defer app.Close()
|
||||||
|
|
||||||
|
log := app.MustLogger()
|
||||||
|
|
||||||
|
// 启动期预热:按业务实际使用的模块增删(未配置的模块 Warmup 会失败)
|
||||||
|
if err := app.Warmup(
|
||||||
|
factory.ModuleLogger,
|
||||||
|
// factory.ModuleI18n,
|
||||||
|
// factory.ModuleDatabase,
|
||||||
|
// factory.ModuleRedis,
|
||||||
|
); err != nil {
|
||||||
|
log.Error("warmup failed", map[string]any{"error": err.Error()})
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
chain := setupMiddleware(app)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
registerRoutes(mux, chain, app)
|
||||||
|
|
||||||
|
log.Info("server starting", map[string]any{"addr": *addr, "config": *configPath})
|
||||||
|
if err := http.ListenAndServe(*addr, mux); err != nil {
|
||||||
|
log.Error("server stopped", map[string]any{"error": err.Error()})
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOptions 组装 Factory Option(自定义实现 / 测试替身在此注入)
|
||||||
|
func buildOptions() []factory.Option {
|
||||||
|
// 默认不注入,全部走 config.json lazy 初始化。
|
||||||
|
// 需要替换实现时取消注释,例如:
|
||||||
|
//
|
||||||
|
// return []factory.Option{
|
||||||
|
// factory.WithLogger(customLogger),
|
||||||
|
// factory.WithStorage(customStorage),
|
||||||
|
// factory.WithDatabase(db),
|
||||||
|
// factory.WithRedis(rds),
|
||||||
|
// factory.WithI18n(i18nInst),
|
||||||
|
// factory.WithMiddlewareChain(customChain),
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupMiddleware 基于 Factory 默认链追加业务中间件
|
||||||
|
// 默认链:Recovery → RequestID → Logging → RateLimit? → CORS? → Language → Timezone
|
||||||
|
func setupMiddleware(app *factory.Factory) *middleware.Chain {
|
||||||
|
chain := app.MiddlewareChain()
|
||||||
|
|
||||||
|
// 业务鉴权等自行挂载(本库不提供登录体系)
|
||||||
|
// chain.Append(authMiddleware)
|
||||||
|
// chain.Append(permissionMiddleware)
|
||||||
|
|
||||||
|
return chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerRoutes 注册路由;出参统一用 app.NewHandler
|
||||||
|
func registerRoutes(mux *http.ServeMux, chain *middleware.Chain, app *factory.Factory) {
|
||||||
|
mux.Handle("/api/ping", chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h := app.NewHandler(w, r)
|
||||||
|
h.Success(map[string]string{"status": "ok"})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 示例:业务路由
|
||||||
|
// mux.Handle("/api/users", chain.ThenFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// h := app.NewHandler(w, r)
|
||||||
|
// // ...
|
||||||
|
// h.Success(nil)
|
||||||
|
// }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOr(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user