基础库(internal/pkg)¶
internal/pkg 放无业务依赖、可被任何层复用的基础能力。这一页是速查手册,详细行为以源码注释为准。
1. apperr — 业务错误码¶
return apperr.New(apperr.CodeConflict, "角色编码已存在")
return apperr.Newf(apperr.CodeLocked, "账号锁定 %d 分钟", 15)
return apperr.Wrap(err, apperr.CodeInternal, "文件保存失败")
| 常量 | 值 | HTTP | 含义 |
|---|---|---|---|
CodeOK |
0 | 200/201 | 成功 |
CodeInvalidArgument |
40001 | 400 | 参数错误 |
CodeUnauthorized |
40101 | 401 | 未登录 |
CodeTokenExpired |
40102 | 401 | 登录失效 |
CodeForbidden |
40301 | 403 | 无权限 |
CodeNotFound |
40401 | 404 | 不存在 |
CodeConflict |
40901 | 409 | 冲突 |
CodeLocked |
42301 | 423 | 账号锁定 |
CodeTooManyRequests |
42901 | 429 | 限流 |
CodeInternal |
50001 | 500 | 内部错误 |
apperr.From(err)提取业务错误;未知错误统一包装为50001,不泄露内部信息。apperr.Wrap的cause只用于服务端日志,不返回客户端。- 预置
ErrUnauthorized、ErrForbidden、ErrNotFound。
2. response — 统一响应¶
response.OK(c, data) // 200
response.Created(c, data) // 201
response.Page(c, list, total, page, pageSize)
response.NoContent(c) // 204
response.Error(c, err) // 按 apperr 映射状态码
信封:
分页 data:{list,total,page,page_size}。response.Error 会:
- 把
50001的消息改为「服务器内部错误」。 - 把错误摘要写入
c.Errors供审计。 - 5xx 写结构化错误日志(含 request_id、方法、路径、原始错误)。
3. pagination — 分页¶
type Query struct {
Page int `form:"page"`
PageSize int `form:"page_size"`
Keyword string `form:"keyword"`
}
Normalize():页码至少 1,页大小默认 20,上限 200。Offset():(page-1)*pageSize。- 列表过滤器内嵌
pagination.Query,handler 里调用f.Normalize(),仓储里用paginate。
4. authn — 请求身份¶
type Principal struct {
UserID, Username, Nickname, SessionVersion, SessionID
Super bool
Permissions map[string]struct{}
}
p.Has("system:user:list") // super_admin 恒为 true
authn.With(ctx, p)
authn.FromContext(ctx)
handler 里用 CurrentUserID(c) / CurrentPrincipal(c) / CurrentSessionID(c)(internal/handler/helpers.go)。
5. cache — 缓存抽象¶
type Store interface {
Set(ctx, key, value string, ttl time.Duration) error
Get(ctx, key string) (string, error) // 不存在返回 ErrNotFound
Take(ctx, key string) (string, error) // 原子读+删
Del(ctx, keys ...string) error
Exists(ctx, key string) (bool, error)
Keys(ctx, prefix string) ([]string, error) // 只返回未过期键
SetNX(ctx, key, value string, ttl time.Duration) (bool, error)
Incr(ctx, key string, ttl time.Duration) (int64, error)
Ping(ctx) error
Close() error
}
cache.NewRedis(client):生产默认。cache.NewMemory():并发安全,进程内;重启丢失 refresh/会话/验证码,仅开发或单副本。Incr首次写入时设置 TTL,用于固定窗口限流。- 应用内键前缀:
refresh:、session:、login:fail:、unlock:fail:、mfa:*、captcha:、job:lock:、passkey:*。
6. captcha — 图形验证码¶
gen := captcha.New(store, 5*time.Minute)
challenge, _ := gen.Generate(ctx)
ok := gen.Verify(ctx, challenge.ID, answer)
- 默认 4 位字符,去掉
I O 0 1等易混淆字符。 - 返回 SVG data URL,无需图片服务。
- 答案存
captcha:<id>,校验一次即删除,失败也会消费,防重放。 - 可通过
WithReader/WithRand/WithClock/WithIDFunc/WithLength注入测试替身。
7. security — JWT 与密码¶
7.1 TokenManager¶
tokens := security.NewTokenManager(secret, issuer, accessTTL)
access, expiresAt, _ := tokens.Issue(userID, username, sessionVersion, sessionID)
claims, err := tokens.Parse(raw)
- HS256,解析时校验签名、issuer、exp。
Claims:uid、username、sv、sid、iss、exp、jti。- 另有
IssueFileAccess/ParseFileAccess用于/uploadsCookie;Cookie 名x_admin_files。
7.2 随机串与哈希¶
token, _ := security.RandomToken() // 32 字节 URL-safe
hash := security.HashToken(token) // SHA-256 hex
refresh token、分享令牌、会话 ID 都用这套。
7.3 密码¶
hasher := security.NewBcryptHasher(cost)
hash, _ := hasher.Hash(plain)
ok := hasher.Verify(hash, plain)
PasswordHasher 是接口,测试可用低成本实现。
8. storage — 文件存储¶
type Store interface {
Save(ctx, filename string, r io.Reader) (Info, error)
Open(ctx, key string) (io.ReadCloser, error)
Delete(ctx, key string) error
URL(key string) string
}
NewLocal(root, urlPrefix):布局YYYY/MM/<random>.<ext>,扩展名清洗,路径穿越校验。- 测试可用
WithClock/WithIDFunc固定时间与文件名。 - 替换对象存储时实现该接口并在
provideStorage注入。
9. excel — Excel 读写¶
9.1 小数据量¶
data, err := excel.Write("用户", headers, rows) // [][]string
rows, err := excel.Read(reader) // [][]string,跳过空行
9.2 流式导出¶
book, _ := excel.NewStreamBook("用户列表", headers)
defer book.Close()
for rows.Next() {
_ = book.WriteRow([]any{"张三", 18, time.Now()})
}
_ = book.SaveTo(writer)
- 基于 excelize
StreamWriter,内存占用与总行数无关。 - 单表写满自动追加新工作表,表头重复。
Rows()返回累计数据行数。
10. sysinfo — 主机与运行时¶
包含 Host / CPU / Memory / Swap / Load / Disks / Network / Process,以及 DatabaseStats 类型(连接池指标)。监控服务在此之上补充 Go runtime 与缓存探测。容器内读到的是容器视角。
11. totp — 双重验证¶
secret, _ := totp.GenerateSecret()
url := totp.ProvisioningURL(issuer, account, secret)
qr, _ := totp.QRCodeDataURL(url)
ok := totp.Validate(secret, code, time.Now())
ok = totp.ValidateSkew(secret, code, time.Now(), 1)
codes, _ := totp.GenerateRecoveryCodes()
hash := totp.HashRecoveryCode(code)
固定 SHA1 / 6 位 / 30 秒;Validate 允许 ±1 步偏差。恢复码只存 SHA-256 摘要。
12. tree — 扁平列表转树¶
nodes := tree.Build(items, tree.Options[*Node]{
ID: func(n *Node) uint64 { return n.ID },
ParentID: func(n *Node) uint64 { return n.ParentID },
SetChildren: func(n *Node, kids []*Node) { n.Children = kids },
})
- 保持输入顺序。
- 孤儿、自引用、环都会被提升为根节点,保证数据不丢失。
- 菜单树、部门树都用它。
13. requestid — 请求 ID¶
- 中间件优先复用请求头
X-Request-ID,否则生成。 - 响应体
request_id与访问日志、错误日志、操作日志共用同一个值。
14. 使用约定¶
pkg不 importinternal/service/internal/model/ Gin(response除外,它本身就是 HTTP 响应封装)。- 需要配置的能力通过参数传入,不要在 pkg 里读全局配置。
- 需要测试替换的依赖(时钟、随机源、ID)用
Option注入。 - 新增 pkg 时先问:它是否与业务无关?会被两个以上模块复用?否则放到使用它的 service 里。