docs: 注释/文档一致性审查修复(A/B/C 三档共 23 项)

对 60 个非测试源文件做文档/注释/代码一致性审查,修复 23 项。全部为注释/文档
改动 + 1 处死代码删除,无运行逻辑变更,无 breaking change。

A 档·实质错误(10 项,会误导下游):
- app.go: closeResources 三连契约注释统一修正--failAfterInit 走
  rollbackReplacedResources 不走 closeResources,closeResources 仅供 doShutdown 复用
- database/manager.go: RandomPicker 注释从过时的 rand.Intn/issue 54899 改为 crypto/rand
- model/base.go: datetime(0) 与“亚秒精度”矛盾,改为 datetime(6)
- cache/keybuilder.go: 三处示例下划线分隔符改为冒号(与默认分隔符 : 一致)
- repository/repository.go: Delete 接口注释从绝对“软删除”改为条件性;
  UpdateFields 示例移除把字段名当 WHERE 条件的误用写法
- middleware/csrf.go: CSRFToken 注释从“用于 API 模式”改为 Cookie/双重提交模式说明
- examples/full/main.go: POST /users 补上“需 Authorization”标注;删除 _ = app 死代码
- utils/random.go: RandInt/RandInt64 补 min==max 边界说明(对齐 RandIntSecure 写法)

B 档·导出符号注释缺失/不完整(6 项):
- ws/ws.go: 5 个 Type* 常量、4 个 Err* 变量补注释
- console/console.go: 5 个 Level 常量补注释
- jwt/jwt.go: 2 个 Blacklist 常量补注释
- router/router.go: 包级 GroupWithMiddlewareGroup 补注释
- middleware/ratelimit.go: NewRateLimiter 补 panic 条件与 Stop 生命周期说明

C 档·描述不完整(7 项):
- utils/strings.go Substr、convert.go CalcPageCount/CalcOffset、datetime.go
  StartOfMonth/EndOfMonth、file.go CopyFile、validator.go IsChinese、crypto.go HashFile
  补全边界/默认行为/返回值语义
- response/error.go: WithDetail 补 nil 接收者行为说明

验证:go build / go vet / go test -race 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
杭州明婳科技
2026-07-12 18:47:12 +08:00
parent 6730f12cd6
commit ffcb1a77b9
21 changed files with 302 additions and 40 deletions
+14 -5
View File
@@ -459,8 +459,13 @@ func (a *App) Init() error {
// failAfterInit 在 doInit 失败时执行资源回滚(M1)。
// 顺序:标记 stateStopping(阻新 Go)→ cancel rootCtx → wg.Wait(让 a.Go 启动的探活等
// goroutine 先退出)→ closeResources。先停 goroutine 再关资源,避免“关 DB 时探活 goroutine
// 仍在用”的竞态。回滚错误与原 initErr 合并上抛,不吞
// goroutine 先退出10s 超时)→ stopCron(5s) → rollbackReplacedResources。
// 先停 goroutine 再回滚资源,避免“回滚 DB 时探活 goroutine 仍在用”的竞态。
//
// 回滚走 rollbackReplacedResources(恢复 Init 前默认 manager 再关新建 manager),
// 而非 closeResources(直接关闭,仅供 doShutdown 复用)。Init 失败需恢复默认 manager
// 而非直接关,故两者不复用同一路径。cron 停止预算固定 5s,由本路径单独停止。
// 回滚错误与原 initErr 合并上抛,不吞。
func (a *App) failAfterInit() {
a.lifecycleMu.Lock()
a.state = stateStopping
@@ -493,8 +498,10 @@ func (a *App) failAfterInit() {
}
// closeResources 关闭 db→redis→loggerM1)。各均为幂等 no-op(未 init 时安全),
// 供 failAfterInit 与 doShutdown 复用。cron 因停止预算随调用方不同(Init 失败 5s /
// Shutdown 剩余预算),由各调用方单独停止,不在此处。返回聚合错误。
// 供 doShutdown 复用。cron 因停止预算随调用方不同(Init 失败 5s / Shutdown 剩余
// 预算),由各调用方单独停止,不在此处。返回聚合错误。
// 注意:Init 失败的回滚不经本函数,而走 failAfterInit 的 rollbackReplacedResources
// (需恢复 Init 前默认 manager,而非直接关闭)。
func (a *App) closeResources() error {
var errs []error
if a.initializedMySQL {
@@ -1050,7 +1057,9 @@ func (a *App) doShutdown(wasInitialized bool) error {
logger.Info("停止限流器...")
middleware.StopRateLimiters()
// db/redis/logger 经 closeResources 统一关闭(与 failAfterInit 复用,M1)。
// db/redis/logger 经 closeResources 统一关闭(M1)。注意:closeResources 仅供
// Shutdown 路径使用;Init 失败的回滚走 failAfterInit 的 rollbackReplacedResources
// 两者不复用同一路径(Init 失败需恢复默认 managerShutdown 直接关闭)。
// 先记最后一条 "应用已优雅关闭",再 Close(关闭后写日志会 fall back 到 nop)。
logger.Info("关闭数据库连接/Redis/日志...")
logger.Info("应用已优雅关闭")
+3 -3
View File
@@ -44,7 +44,7 @@ func WithSeparator(separator string) KeyBuilderOption {
}
// WithCacheType 设置缓存类型标识
// 示例: WithCacheType("session") -> "session_site_a_user:1"
// 示例: WithCacheType("session") -> "session:site_a:user:1"
func WithCacheType(cacheType string) KeyBuilderOption {
return func(kb *KeyBuilder) {
if kb == nil {
@@ -72,7 +72,7 @@ func NewKeyBuilder(opts ...KeyBuilderOption) *KeyBuilder {
// Build 构建完整键名(CK2 修复:mu 读保护)。
// 格式: {cacheType}{separator}{prefix}{separator}{key}
// 示例: kb.Build("user:1") -> "cache_site_a_user:1"
// 示例: kb.Build("user:1") -> "cache:site_a:user:1"
func (kb *KeyBuilder) Build(key string) string {
kb.mu.Lock()
defer kb.mu.Unlock()
@@ -153,7 +153,7 @@ func (kb *KeyBuilder) BuildSession(key string) string {
}
// BuildPattern 构建匹配模式(用于 SCAN/Keys)(CK2 修复:mu 读保护)。
// 示例: kb.BuildPattern("user:*") -> "cache_site_a_user:*"
// 示例: kb.BuildPattern("user:*") -> "cache:site_a:user:*"
func (kb *KeyBuilder) BuildPattern(pattern string) string {
return kb.Build(pattern)
}
+5
View File
@@ -21,10 +21,15 @@ var writeMu sync.Mutex
type Level int32
const (
// LevelDebug 调试级别(最低)。
LevelDebug Level = iota
// LevelInfo 普通信息。
LevelInfo
// LevelSuccess 成功信息。
LevelSuccess
// LevelWarn 警告。
LevelWarn
// LevelError 错误(最高常规级别)。
LevelError
// LevelSilent 完全静默:所有调用都不输出
+2 -2
View File
@@ -63,8 +63,8 @@ func (p *RoundRobinPicker) Pick(replicas []*gorm.DB) *gorm.DB {
// RandomPicker 随机选择从库。
//
// D2 注释:rand.Intn 自 Go 1.20 起(go.dev/issue/54899)内部使用 per-goroutine
// 随机源,并发安全无需额外同步。本模块 go.mod 声明 go 1.25.0,满足此要求
// 使用 crypto/rand 生成随机索引,并发安全且不可预测。len(replicas)<=0 返回 nil
// crypto/rand 失败(极罕见,如熵池耗尽)时回退到 replicas[0],保证可用性
type RandomPicker struct{}
// Pick 随机选择一个从库
+1 -4
View File
@@ -8,7 +8,7 @@
//
// POST /api/v1/login {"username":"alice","password":"secret"} → 返回 token
// GET /api/v1/users/:id (需 Authorization: Bearer <token>
// POST /api/v1/users (创建用户)
// POST /api/v1/users 需 Authorization: Bearer <token>创建用户)
//
// 示例会在启动时初始化 alice/secret,密码以 bcrypt 哈希保存。
//
@@ -59,9 +59,6 @@ func main() {
}),
)
// 初始化 user repositoryApp.Init 之后 master DB 才可用,这里在 registerRoutes 里延迟拿)
_ = app
if err := app.Run(); err != nil {
fmt.Printf("启动失败: %v\n", err)
os.Exit(1)
+235
View File
@@ -0,0 +1,235 @@
# xlgo 框架文档/注释/代码一致性审查报告
> 审查日期:2026-07-12
> 审查范围:60 个非测试 `.go` 源文件,25 个包目录
> 方法:每条结论均亲自读源码确认;对初筛标"无问题"的文件做抽查复审;专门核查跨函数契约。不转述初筛结论。
## 关于本次审查
本次审查对每一条结论都亲自读取了源码确认,并对标"无问题"的文件做了抽查复审,还专门查了跨函数契约。下方为经得起复核的结果。
**真实数字:23 项**,分三档,每档标了置信度。
---
## 一、整体审查概览
- **审查文件总数**:60(25 个包目录,全部非测试源文件)
- **亲自读源码确认的问题**:23 项
- **A 档·实质错误(误导下游)**:10 项 - 注释与代码逻辑矛盾,会让人按注释写错代码
- **B 档·导出符号注释缺失/不完整**:6 项 - 不致命但违反 godoc 规范或同块内风格不一
- **C 档·描述不完整**:7 项 - 边界/默认行为没写全,不算错但该补
- **代码健康度评分****7.5 / 10**
**整体评价**:xlgo 注释质量**两极分化**。核心安全/并发路径(`tls.go``validate.go``recover.go``repository.go` 的 H6 修复块、`logger` 的 H7 块)注释极其详尽且与代码逐条对应,是全仓库最好的;问题集中在两类边缘:① 历史修复后注释没同步(RandomPicker、closeResources 三连、model/base);② 工具函数/utils 的边界行为和导出常量注释缺失。**本轮主要发现是注释/文档问题,未确认 CRITICAL/HIGH 代码缺陷**`storage.go` 超限错误类型是唯一存疑点,见第四节)。
---
## 二、A 档·实质错误(10 项,高置信,该修)
这些全部亲自读了源码,证据确凿。
### 1. app.go:462 / 495 / 1053 - closeResources 三连契约错误(跨函数)
这是最严重的一组,**同一个契约错误散布在三处注释,互相印证却全错**:
- **【位置 1】第 462 行 `failAfterInit` 注释**
```go
// 顺序:... -> closeResources。先停 goroutine 再关资源...
```
实际第 483-488 行调的是 `stopCron` + `rollbackReplacedResources`**不调 `closeResources`**。
- **【位置 2】第 495 行 `closeResources` 注释**
```go
// closeResources 关闭 db->redis->loggerM1)。...供 failAfterInit 与 doShutdown 复用。
```
grep 确认 `closeResources` 只被 `doShutdown`(第 1057 行)调用,`failAfterInit` 不调它。**"供 failAfterInit 复用"是假的**。
- **【位置 3】第 1053 行 `doShutdown` 注释**
```go
// db/redis/logger 经 closeResources 统一关闭(与 failAfterInit 复用,M1)。
```
同样错--`failAfterInit` 走 `rollbackReplacedResources`,不复用 `closeResources`。
**建议**:把三处注释统一修正--`failAfterInit` 回滚走 `rollbackReplacedResources`(恢复 Init 前默认 manager 再关新建 manager),`closeResources` 仅供 `doShutdown` 复用。`failAfterInit` 选 `rollback` 而非 `close` 是有意的(Init 失败要恢复默认 manager 不直接关),注释应点明这个区别。
### 2. database/manager.go:64-67 - RandomPicker 注释引用了已废弃的 rand.Intn
```go
// RandomPicker 随机选择从库。
//
// D2 注释:rand.Intn 自 Go 1.20 起(go.dev/issue/54899)内部使用 per-goroutine
// 随机源,并发安全无需额外同步。本模块 go.mod 声明 go 1.25.0,满足此要求。
```
实际第 75 行:`cryptorand.Int(cryptorand.Reader, big.NewInt(...))` - 已改用 `crypto/rand`,与 `math/rand.Intn`、issue 54899、per-goroutine 随机源**全部无关**。并发安全的原因现在是 `crypto/rand` 本身线程安全。注释整段过时。
**建议修改为**
```go
// RandomPicker 随机选择从库。
//
// 使用 crypto/rand 生成随机索引,并发安全且不可预测。len(replicas)<=0 返回 nil
// crypto/rand 失败(极罕见,如熵池耗尽)时回退到 replicas[0],保证可用性。
```
### 3. model/base.go:9-10 - 注释内部自相矛盾
```go
// 时间列类型(MySQL 通常为 datetime(0) 或 timestamp,保留亚秒精度)。
```
`datetime(0)` = 0 位小数秒(秒级精度),与"保留亚秒精度"直接冲突。GORM MySQL 驱动默认是 `datetime(6)`。应为 `datetime(6)`。
**建议修改为**
```go
// BaseModel 基础模型。CreatedAt/UpdatedAt 不显式指定 type,由 GORM 按驱动选默认
// 时间列类型(MySQL 通常为 datetime(6),保留亚秒精度)。
```
### 4. cache/keybuilder.go:47 / 75 / 156 - 三处示例分隔符全错
默认分隔符是 `:`(第 17、62 行),但三处示例都用下划线:
- 第 47 行:`WithCacheType("session") -> "session_site_a_user:1"` -> 应为 `session:site_a:user:1`
- 第 75 行:`kb.Build("user:1") -> "cache_site_a_user:1"` -> 应为 `cache:site_a:user:1`(且与同注释内格式说明 `{separator}` 矛盾)
- 第 156 行:`kb.BuildPattern("user:*") -> "cache_site_a_user:*"` -> 应为 `cache:site_a:user:*`
同文件第 44 行 `WithSeparator` 示例却正确用 `:`,说明这是历史遗留笔误。
### 5. repository/repository.go:37 - Delete 接口注释绝对化
```go
// Delete 删除记录(软删除)
Delete(ctx context.Context, id uint) error
```
实现 `BaseRepo.Delete`(第 156-160 行)契约:"若 T 内嵌 gorm.DeletedAt(或 gorm.Model)为软删除;否则为硬删除"。接口注释说死"软删除",下游按接口理解会误判。
**建议修改为**
```go
// Delete 删除记录(T 含软删除字段时为软删除,否则为硬删除;详见 BaseRepo.Delete
Delete(ctx context.Context, id uint) error
```
### 6. repository/repository.go:144 - UpdateFields 示例语义错误
```go
// repo.UpdateFields(ctx, &User{Name: "new"}, "name") // 仅更新 name
```
函数第 150-152 行 `db.Where(conds[0], conds[1:]...)` 把 `"name"` 当 WHERE 条件(`WHERE name`),不是"仅更新 name 字段"的选择器。struct 模式 `Updates(&User{Name:"new"})` 本就只更新非零字段,无需传 `"name"`。示例会误导人把字段名当条件传。第二行(map + `"id = ?"`)正确。
**建议修改为**
```go
// 示例:
//
// repo.UpdateFields(ctx, &User{Name: "new"}) // struct:仅更新非零字段 name
// repo.UpdateFields(ctx, map[string]any{"status": 0}, "id = ?", id) // map:显式置零,带条件
```
### 7. middleware/csrf.go:260 - CSRFToken 注释"用于 API 模式"误导
```go
// CSRFToken 返回 CSRF Token 的处理器(用于 API 模式)
```
实现第 262-274 行:取 gin context 的 "csrf_token"Cookie 模式中间件写入),取不到就地生成一次性 token 直接返回,**不写入 `apiTokens`**。而 API 模式可校验 token 由 `GenerateAPIToken`(第 345 行)颁发。所以 `CSRFToken` 产出的 token **不能被 `CSRFForAPI` 消费**,注释会让人误以为它产出 API 模式可校验 token。
**建议修改为**
```go
// CSRFToken 返回当前请求上下文中的 CSRF Token;若上下文无 Token 则现场生成一个返回。
// 适用于 Cookie/双重提交模式(与 CSRF/DoubleSubmitCookie 中间件配合),
// 不与 CSRFForAPI 配套--API 模式的可校验 Token 请用 GenerateAPIToken 颁发。
```
### 8. examples/full/main.go:11 - POST /users 认证标注缺失
```go
// GET /api/v1/users/:id (需 Authorization: Bearer <token>
// POST /api/v1/users (创建用户)
```
实际第 82-83 行:`auth := api.Group("/", middleware.AuthRequired())``auth.POST("/users", createUser)` 在 auth 组内,**同样需 token**。注释对 GET 标了认证,POST 没标,会让人以为 POST 是公开接口。
**建议修改为**
```go
// POST /api/v1/login {"username":"alice","password":"secret"} -> 返回 token(公开)
// GET /api/v1/users/:id (需 Authorization: Bearer <token>
// POST /api/v1/users (需 Authorization: Bearer <token>,创建用户)
```
### 9. utils/random.go:39 - RandInt 与 RandInt64 的 min==max 边界与区间注释矛盾
```go
// RandInt 返回 [min, max) 范围内的随机整数。
func RandInt(min, max int) int {
if min == max {
return min // 等于 max,与半开区间 [min,max) 矛盾
}
```
`[min, max)` 在 `min==max` 时应为空集,但代码返回 `min`(即 max)。`RandIntSecure`(第 112 行)与 `RandInt64Secure`(第 128 行)注释均已明确说明此例外("min==max 返回 minmax<min 自动交换"),但 `RandInt`(第 39 行)与 `RandInt64`(第 55 行)没说。同问题影响 2 个函数。
**建议修改为**
```go
// RandInt 返回 [min, max) 范围内的随机整数。min==max 时返回 minmax<min 自动交换。
```
### 10. examples/full/main.go:63 - `_ = app` 死代码 + 误导注释
```go
// 初始化 user repositoryApp.Init 之后 master DB 才可用,这里在 registerRoutes 里延迟拿)
_ = app
```
`_ = app` 是无意义空赋值(`app` 在 49 行声明、65 行 `app.Run()` 已用),注释紧贴它让人误以为二者相关。实际延迟初始化在 73 行 `registerRoutes` 内。建议删掉 `_ = app`,把注释移至 `registerRoutes` 内 repo 初始化处。
---
## 三、B 档·导出符号注释缺失/不完整(6 项,中置信,该补)
这些是导出符号(const/var 块常量及部分函数)注释缺失或不完整,不误导但违反 godoc 规范或同块内风格不一。
| 文件 | 位置 | 符号 | 说明 |
|---|---|---|---|
| ws/ws.go | 94-100 | `TypeText`/`TypeBinary`/`TypePing`/`TypePong`/`TypeClose` | 5 个导出常量无注释,类型 `MessageType` 有 |
| ws/ws.go | 352-357 | `ErrHubNotRunning`/`ErrHubStopped`/`ErrHubQueueFull`/`ErrNilConnection` | 4 个导出错误变量无注释 |
| console/console.go | 23-32 | `LevelDebug`/`LevelInfo`/`LevelSuccess`/`LevelWarn`/`LevelError` | 5 个级别常量无注释(仅 `LevelSilent` 有) |
| jwt/jwt.go | 126-129 | `BlacklistFailOpen`/`BlacklistFailClosed` | 2 个导出常量无注释,类型 `BlacklistPolicy` 有 |
| router/router.go | 680 | 包级 `GroupWithMiddlewareGroup` | 方法版(672 行)有详细注释,包级版无注释 |
| middleware/ratelimit.go | 37 | `NewRateLimiter` | 已有简短注释("创建速率限制器(内存版)"),但未说明 `rate<=0`/`window<=0` panic(已确认 `mustValidRateLimit` 第 57-62 行真 panic)及必须 `Stop()` 释放 goroutine |
---
## 四、C 档·描述不完整(7 项,低置信,可选)
边界/默认行为没写全,注释本身不算错,建议补但不急。
| 文件 | 位置 | 符号 | 缺什么 |
|---|---|---|---|
| utils/strings.go | 105 | `Substr` | 未说 `length<=0` 返回空、`start` 越界返回空 |
| utils/convert.go | 89/97 | `CalcPageCount`/`CalcOffset` | 未说非法入参返回 0 / 默认值(1、20) |
| utils/datetime.go | 76/81 | `StartOfMonth`/`EndOfMonth` | 未说具体时刻(1日00:00 / 末日23:59:59.999999999 |
| utils/file.go | 79 | `CopyFile` | 未说自动建目录、覆盖、参数顺序 dst,src |
| utils/validator.go | 49 | `IsChinese` | 未说空串返回 false |
| utils/crypto.go | 52 | `HashFile` | 未说返回 hex 编码、`newHash` 须返回新实例 |
| response/error.go | 90 | `WithDetail` | 未文档化 nil 接收者行为(返回 `&Error{Detail:detail}``Code=0`/`Message=""`,不 panic |
### 唯一存疑的代码问题(非纯文档)
**storage/storage.go:461 与 :664** - `LocalStorage.Get`/`OSSStorage.Get` 读取超 `maxReadBytes` 时返回包装 `ErrInvalidPath` 的错误。"超过最大读取限制"用 `ErrInvalidPath`(路径无效)语义不贴切,建议改 `ErrUploadTooLarge` 或新增专用错误。**这是唯一可能涉及代码(非注释)的问题**,但属错误类型选用,非功能 bug,标存疑待定夺。
---
## 五、关于"漏看/错看"的说明
本次审查纠正了初筛阶段的若干问题:
1. **虚高数字已修正**:初筛报出 55 项,其中大量为"可以写得更详细"类软问题。剔除噪声后真实硬伤 10 项。
2. **漏报已补**:初筛只报了 `failAfterInit` 一处,本次亲自 grep 确认了 `closeResources` 三连错误(第 462/495/1053 行),补上了漏掉的 2 处。这是典型的"局部对全局错"--三处注释单独看都像对,拼起来契约对不上。
3. **"无问题"文件复审**:抽查了 `cache.go`、`validation`hash+password)、`compress`、`logger`+field)、`test`、`uuid`、`examples/minimal`、`database/tls`、`config/validate`、`middleware`cors/recover/timeout)。这些文件初筛标"无问题"**整体可信**--尤其 `tls.go`、`validate.go`、`recover.go` 注释质量很高。抽查中只在 `cache.go:264` `Init()` 注释发现一处边界(标"初始化全局缓存实例"但实际不启 Redis,依赖 `database.InitRedis`),属"注释过度承诺"边界,未列入硬伤,供参考。
4. **方法局限坦白**:未对全部 60 个文件逐行重读,而是对初筛报的所有问题源码逐条复核 + 抽查"无问题"文件覆盖各类情况 + 专门查跨函数契约。**仍可能存在的盲区**:① 某些没被重点提及、也没抽查的文件里可能藏着问题;② 跨文件断言只深挖了 closeResources 和 repository 路由两处,其他跨文件引用没全查。
---
## 附:审查覆盖
- **A 档 10 项**:全部亲自 Read 源码确认,含 7 项二次交叉复核(keybuilder、model/base、RandomPicker、repository Delete/UpdateFields、csrf CSRFToken、examples/full)。
- **B 档 6 项**:全部亲自 Read 确认注释缺失或不完整(含 `NewRateLimiter` 的 panic/Stop 语义缺口经 `mustValidRateLimit` 源码确认)。
- **C 档 7 项**:全部亲自 Read 确认边界行为。
- **跨函数契约**`closeResources`/`rollbackReplacedResources` 经 grep 全仓库调用点确认;`repository` 经 Read 确认接入 `database.GetDBFromContext`H6 修复属实)。
- **无问题文件**:抽查 10+ 个覆盖接口密集/注释最详尽/中间件/示例/工具各类型,初筛"无问题"裁定整体可信。
+2
View File
@@ -124,7 +124,9 @@ const blacklistOpTimeout = 1 * time.Second
type BlacklistPolicy int
const (
// BlacklistFailOpen 黑名单查询出错时放行 Token(默认;兼容未启用 Redis 的存量部署)。
BlacklistFailOpen BlacklistPolicy = iota
// BlacklistFailClosed 黑名单查询出错时拒绝 Token(安全敏感路由使用)。
BlacklistFailClosed
)
+3 -1
View File
@@ -257,7 +257,9 @@ func GetCSRFToken(c *gin.Context) string {
return s
}
// CSRFToken 返回 CSRF Token 的处理器(用于 API 模式)
// CSRFToken 返回当前请求上下文中的 CSRF Token;若上下文无 Token 则现场生成一个返回。
// 适用于 Cookie/双重提交模式(与 CSRF/DoubleSubmitCookie 中间件配合),
// 不与 CSRFForAPI 配套--API 模式的可校验 Token 请用 GenerateAPIToken 颁发。
func CSRFToken(c *gin.Context) {
token := GetCSRFToken(c)
if token == "" {
+3 -1
View File
@@ -34,7 +34,9 @@ type visitor struct {
count int
}
// NewRateLimiter 创建速率限制器(内存版)
// NewRateLimiter 创建速率限制器(内存版)
// rate<=0 或 window<=0 时 panic。内部启动 cleanup goroutine,调用方负责在不再使用时
// 调用 (*RateLimiter).Stop 释放,否则 goroutine 泄漏。
func NewRateLimiter(rate int, window time.Duration) *RateLimiter {
mustValidRateLimit(rate, window)
ctx, cancel := context.WithCancel(context.Background())
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
// BaseModel 基础模型。CreatedAt/UpdatedAt 不显式指定 type,由 GORM 按驱动选默认
// 时间列类型(MySQL 通常为 datetime(0) 或 timestamp,保留亚秒精度)。
// 时间列类型(MySQL 通常为 datetime(6),保留亚秒精度)。
type BaseModel struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
+3 -3
View File
@@ -34,7 +34,7 @@ type BaseRepository[T any] interface {
Create(ctx context.Context, model *T) error
// Update 更新记录
Update(ctx context.Context, model *T) error
// Delete 删除记录(软删除
// Delete 删除记录(T 含软删除字段时为软删除,否则为硬删除;详见 BaseRepo.Delete
Delete(ctx context.Context, id uint) error
// FindByIDs 批量查询
FindByIDs(ctx context.Context, ids []uint) ([]T, error)
@@ -143,8 +143,8 @@ func (r *BaseRepo[T]) Update(ctx context.Context, model *T) error {
//
// 示例:
//
// repo.UpdateFields(ctx, &User{Name: "new"}, "name") // 仅更新 name
// repo.UpdateFields(ctx, map[string]any{"status": 0}, "id = ?", id) // 显式置零
// repo.UpdateFields(ctx, &User{Name: "new"}) // struct:仅更新非零字段 name
// repo.UpdateFields(ctx, map[string]any{"status": 0}, "id = ?", id) // map:显式置零,带条件
func (r *BaseRepo[T]) UpdateFields(ctx context.Context, model any, conds ...any) error {
db := r.writeConn(ctx).Model(new(T))
if len(conds) > 0 {
+3
View File
@@ -91,6 +91,9 @@ func (e *Error) Error() string {
//
// H-10 修复:返回新 *Error 拷贝,不 mutate 接收者。预定义 Err*(如 ErrNotFound)是
// 包级共享指针,原实现 e.Detail = detail 会污染全局且并发调用存在数据竞争。
//
// nil 接收者:返回 &Error{Detail: detail}Code=0/Message=""),不 panic;调用方应避免
// 在 nil 上调用本方法以拿到无 Code/Message 的半成品错误。
func (e *Error) WithDetail(detail string) *Error {
if e == nil {
return &Error{Detail: detail}
+1
View File
@@ -677,6 +677,7 @@ func (r *Registry) GroupWithMiddlewareGroup(engine *gin.Engine, path string, gro
return engine.Group(path, r.GetMiddlewareGroup(groupName)...)
}
// GroupWithMiddlewareGroup 使用中间件分组创建路由组(全局方式,代理到默认 Registry)。
func GroupWithMiddlewareGroup(engine *gin.Engine, path string, groupName string) *gin.RouterGroup {
return ensureRegistry().GroupWithMiddlewareGroup(engine, path, groupName)
}
+2 -2
View File
@@ -86,7 +86,7 @@ func ToString64(n int64) string {
return strconv.FormatInt(n, 10)
}
// CalcPageCount 计算总页数
// CalcPageCount 计算总页数(向上取整)。total<=0 或 pageSize<=0 时返回 0。
func CalcPageCount(total, pageSize int64) int64 {
if total <= 0 || pageSize <= 0 {
return 0
@@ -94,7 +94,7 @@ func CalcPageCount(total, pageSize int64) int64 {
return (total + pageSize - 1) / pageSize
}
// CalcOffset 计算分页偏移量
// CalcOffset 计算分页偏移量 (page-1)*pageSize。page<=0 时按 1 处理,pageSize<=0 时按 20 处理。
func CalcOffset(page, pageSize int) int {
if page <= 0 {
page = 1
+5 -4
View File
@@ -49,11 +49,12 @@ func SHA256Bytes(data []byte) string {
return hex.EncodeToString(h.Sum(nil))
}
// HashFile 计算文件的哈希值。
// HashFile 流式计算文件的哈希值,返回 hex 编码字符串
//
// M-F 修复:改为流式 io.Copy(h, f),不再经 ReadFile 把整文件读入内存——原实现大文件 OOM
// 流式哈希内存占用恒定、可处理任意大小文件,与 hash.Hash 的 io.Writer 接口契合。
// 调用方负责选择哈希算法(如 sha256.New)。
// newHash 须返回全新的 hash.Hash 实例(如 sha256.New),本函数不复用它
// Open 或读取失败时返回错误。M-F 修复:改为流式 io.Copy(h, f),不再经 ReadFile 把整文件
// 读入内存——原实现大文件 OOM。流式哈希内存占用恒定、可处理任意大小文件,与 hash.Hash
// 的 io.Writer 接口契合。调用方负责选择哈希算法(如 sha256.New)。
func HashFile(path string, newHash func() hash.Hash) (string, error) {
// #nosec G304 -- generic hashing helper intentionally opens caller-provided trusted local paths.
f, err := os.Open(path)
+2 -2
View File
@@ -73,12 +73,12 @@ func StartOfWeek(t time.Time) time.Time {
return StartOfDay(monday)
}
// StartOfMonth 返回指定时间当月的开始时间
// StartOfMonth 返回指定时间当月 1 日 00:00:00(保留原时区)。
func StartOfMonth(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
}
// EndOfMonth 返回指定时间当月的结束时间
// EndOfMonth 返回指定时间当月最后一天 23:59:59.999999999(保留原时区)。
func EndOfMonth(t time.Time) time.Time {
return time.Date(t.Year(), t.Month()+1, 0, 23, 59, 59, 999999999, t.Location())
}
+1 -1
View File
@@ -76,7 +76,7 @@ func AppendFile(path string, data []byte) error {
return err
}
// CopyFile 复制文件
// CopyFile 将 src 复制到 dst(覆盖),自动创建目标目录。src 不存在时返回其 Open 错误。
func CopyFile(dst, src string) error {
// #nosec G304 -- generic filesystem helper intentionally opens caller-provided trusted source paths.
srcFile, err := os.Open(src)
+2 -2
View File
@@ -36,7 +36,7 @@ const (
// ErrRandInvalidLength 长度过大(超过 1<<20,保护熵池)。n<=0 时返回空串而非此错误。
var ErrRandInvalidLength = errors.New("invalid random length")
// RandInt 返回 [min, max) 范围内的随机整数。
// RandInt 返回 [min, max) 范围内的随机整数。min==max 时返回 minmax<min 自动交换。
//
// 非密码学安全(math/rand + sync.Pool),仅用于非安全场景(负载均衡、游戏、A/B 分桶等);
// 不适用于 token/OTP 等安全场景。
@@ -52,7 +52,7 @@ func RandInt(min, max int) int {
return min + r.Intn(max-min)
}
// RandInt64 返回 [min, max) 范围内的随机 int64(非密码学安全)。
// RandInt64 返回 [min, max) 范围内的随机 int64(非密码学安全)。min==max 返回 minmax<min 自动交换。
func RandInt64(min, max int64) int64 {
if min == max {
return min
+2 -1
View File
@@ -102,7 +102,8 @@ func trimRightFunc(s string, f func(rune) bool) string {
return ""
}
// Substr 截取子字符串(按 Unicode 字符计数)
// Substr 截取子字符串(按 Unicode 字符计数)
// start 支持负数(从末尾计算);start 越界或 length<=0 返回空串;length 超出末尾自动截断。
// 参数: start 起始位置(支持负数从末尾计算),length 截取长度
func Substr(s string, start, length int) string {
runes := []rune(s)
+1 -1
View File
@@ -46,7 +46,7 @@ func IsIDCard(id string) bool {
return idCardRE.MatchString(id)
}
// IsChinese 检查字符串是否全部为中文字符
// IsChinese 检查字符串是否非空且全部为中文字符CJK 基本区 U+4E00..U+9FFF)。空串返回 false。
func IsChinese(s string) bool {
for _, r := range s {
if r < 0x4E00 || r > 0x9FFF {
+11 -7
View File
@@ -92,11 +92,11 @@ const (
type MessageType string
const (
TypeText MessageType = "text"
TypeBinary MessageType = "binary"
TypePing MessageType = "ping"
TypePong MessageType = "pong"
TypeClose MessageType = "close"
TypeText MessageType = "text" // 文本消息
TypeBinary MessageType = "binary" // 二进制消息
TypePing MessageType = "ping" // 心跳 ping
TypePong MessageType = "pong" // 心跳 pong
TypeClose MessageType = "close" // 关闭消息
)
// Message WebSocket 消息
@@ -350,9 +350,13 @@ func SetCheckOrigin(fn func(r *http.Request) bool) {
}
var (
// ErrHubNotRunning 在 Hub 未启动 Run 即调用 Try* 方法时返回。
ErrHubNotRunning = errors.New("websocket hub not running")
ErrHubStopped = errors.New("websocket hub stopped")
ErrHubQueueFull = errors.New("websocket hub queue full")
// ErrHubStopped 在 Hub 已 Stop 后调用 Try* 方法时返回。
ErrHubStopped = errors.New("websocket hub stopped")
// ErrHubQueueFull 在 register/unregister/broadcast channel 缓冲满时返回。
ErrHubQueueFull = errors.New("websocket hub queue full")
// ErrNilConnection 在向 Hub 注册/注销 nil 连接时返回。
ErrNilConnection = errors.New("websocket connection is nil")
)