- 3.1 bcrypt 移出事务 (Register): repository.HashPassword 前移到 db.Transaction 之前,消除连接池占用。
- 3.2 Login 消除用户枚举 + 限流 + timing 抹平: pkg/errors 加 ErrInvalidCredential
/ErrTooManyLoginAttempts; 用户不存在/密码错/密码空 三路径统一返回同一错误;
mobile 5次/ip 20次 per 15min 限流 (Redis, fail-open 降级); user-not-found 走
dummy bcrypt 抹平 ~100ms 时序差,完全消除枚举侧信道;空密码分支已核实无时序 leak。
- 3.3 MQ streams adapter 停用 → stub: 0 业务调用方, 新 stub EventProducer.Publish no-op;
pkg/mq/mq.go Init 不再装配 streams; 全仓 grep 验证 11 处硬编码
'gallery'/'default' 集中到 pkg/queue/consts (值不变, 仅消漂移)。
- 3.5 JWT 密钥治理: pkg/jwt MustInit fail-fast + atomic.Value (见上一个 commit 293c7b1)。
- 3.6 aiChat 健壮性: SaveContext 用 persona.ID(非 req.PersonaId); Redis/memory 错误
记 WARN 不静默; Dify err 映射稳定用户文案,原始 err 仅服务端日志。
- 3.7 statistic.Client 重构: TrackEvent 改 buffered channel (cap 1024) + dispatchLoop
worker; 失败 ERROR 日志带字段; drop 记 WARN; Close 可重复调用。
- 3.8 网关聚合: StarCache (60s TTL, singleflight) 替换 5+ 处 GetFanIdentities 链式调用;
DeleteAccount 改网关直调 userService.DeleteAccount(避免改 hand-written triple.go
风险,见报告 §5 proto 风险复盘); 铸造双写改异步 channel+consumer (3 retry)。
- 大量单测: 各子项 TDD (RED→GREEN), 关键并发 race_test (50 goroutine)。
Co-Authored-By: Claude <noreply@anthropic.com>
110 lines
3.6 KiB
Go
110 lines
3.6 KiB
Go
package consts
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestQueueNames_HaveExpectedValues 锁死字面量值 — 改名 = 丢消息(regression guard)。
|
|
func TestQueueNames_HaveExpectedValues(t *testing.T) {
|
|
if QueueGallery != "gallery" {
|
|
t.Fatalf("QueueGallery changed to %q — 改名会丢消息!", QueueGallery)
|
|
}
|
|
if QueueDefault != "default" {
|
|
t.Fatalf("QueueDefault changed to %q — 改名会丢消息!", QueueDefault)
|
|
}
|
|
}
|
|
|
|
// TestServiceFilesUseConstants 验证 galleryService / taskService 已用 consts 引用字面量,
|
|
// 不再硬编码 "gallery"/"default"。此测试是字面量集中化的回归保护。
|
|
//
|
|
// 允许保留字面量的位置:
|
|
// - consts.go 本文件(定义)
|
|
// - 注释 / doc 字符串
|
|
// - pkg/mq/asynq 的默认值("default" 作为 asynq client 端的语义保留)
|
|
func TestServiceFilesUseConstants(t *testing.T) {
|
|
// 项目根 = 后端目录
|
|
_, thisFile, _, _ := runtime.Caller(0)
|
|
root := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(thisFile))))
|
|
// thisFile = .../pkg/queue/consts/consts_test.go
|
|
// 走 4 层: consts -> queue -> pkg -> backend
|
|
|
|
targets := []string{
|
|
filepath.Join(root, "services", "galleryService", "main.go"),
|
|
filepath.Join(root, "services", "galleryService", "mq", "producer.go"),
|
|
filepath.Join(root, "services", "galleryService", "mq", "consumer.go"),
|
|
filepath.Join(root, "services", "taskService", "main.go"),
|
|
filepath.Join(root, "services", "taskService", "mq", "consumer.go"),
|
|
}
|
|
|
|
for _, p := range targets {
|
|
body, err := os.ReadFile(p)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", p, err)
|
|
}
|
|
text := string(body)
|
|
// 抽掉所有 // / /* */ 注释,再扫字面量
|
|
stripped := stripComments(text)
|
|
// 找 `Queue: "gallery"` / `Queue: "default"` / `Queues: map[...]string{...}` 等
|
|
// 实际只关心:
|
|
// - "gallery" 出现在 Queue 上下文字面量位置(Queue: "gallery" 或 "gallery": N)
|
|
// - "default" 同上
|
|
assertNoQueueLiteral(t, p, stripped, `"gallery"`, "QueueGallery")
|
|
assertNoQueueLiteral(t, p, stripped, `"default"`, "QueueDefault")
|
|
}
|
|
}
|
|
|
|
func assertNoQueueLiteral(t *testing.T, file, body, literal, constName string) {
|
|
t.Helper()
|
|
// 行号定位以便排查
|
|
for i, line := range rangeLines(body) {
|
|
// 跳过 consts 导入行(import 块里就是 consts. 引用,不算字面量)
|
|
// 但我们看的是 stripped 文本,import 也还在;不影响,只看 Queue/Queues 上下文
|
|
if !strings.Contains(line, literal) {
|
|
continue
|
|
}
|
|
// 判定: 此行是否在 Queue: / Queues: 上下文里
|
|
isQueueContext := strings.Contains(line, "Queue:") ||
|
|
strings.Contains(line, "Queues:") ||
|
|
strings.Contains(line, "Queue =") ||
|
|
strings.Contains(line, `Queues =`)
|
|
if isQueueContext {
|
|
t.Errorf("%s:%d 仍硬编码 %s — 应改为 consts.%s\n line: %s",
|
|
file, i+1, literal, constName, strings.TrimSpace(line))
|
|
}
|
|
}
|
|
}
|
|
|
|
// stripComments 删掉 // / /* */ 注释。简化版:用 // 拆行处理 // 注释,/ *...* / 整段删。
|
|
func stripComments(s string) string {
|
|
var b strings.Builder
|
|
// 先删 /* ... */
|
|
for {
|
|
start := strings.Index(s, "/*")
|
|
if start < 0 {
|
|
break
|
|
}
|
|
end := strings.Index(s[start:], "*/")
|
|
if end < 0 {
|
|
s = s[:start]
|
|
break
|
|
}
|
|
s = s[:start] + s[start+end+2:]
|
|
}
|
|
// 再按行删 // 注释
|
|
for _, line := range strings.Split(s, "\n") {
|
|
if i := strings.Index(line, "//"); i >= 0 {
|
|
line = line[:i]
|
|
}
|
|
b.WriteString(line)
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// rangeLines 把字符串按行返回(不分配 index/element 配对,只给测试用)。
|
|
func rangeLines(s string) []string { return strings.Split(s, "\n") }
|