Files
SKYMirror 7f060dd0e4 26.9(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务:

- 安全审计全部修复(docs/security-audit-2026-09-05.md):
  bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、
  全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、
  管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、
  会话默认 7 天、限流缓存故障降级、robots.txt 端点等
- 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固
- Redis 支持库号(FCB_REDIS_DB / redis://…/db URL)
- 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、
  提码 5 位起、chunk 32MiB 上限、admin 审计动作等)

验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
2026-09-05 04:22:41 +08:00

160 lines
3.1 KiB
Go

package cache
import (
"context"
"sync"
"time"
)
// memoryItem 内存缓存条目。
type memoryItem struct {
value string
expiresAt time.Time // 零值表示不过期
}
// MemoryCache 进程内存缓存实现(单机、无持久化)。
type MemoryCache struct {
mu sync.RWMutex
items map[string]memoryItem
done chan struct{}
}
// NewMemory 构造内存缓存,并启动后台过期清理。
func NewMemory() *MemoryCache {
m := &MemoryCache{
items: make(map[string]memoryItem),
done: make(chan struct{}),
}
go m.gcLoop()
return m
}
// gcLoop 每分钟清理一次过期键,避免长期运行内存膨胀。
func (m *MemoryCache) gcLoop() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-m.done:
return
case now := <-ticker.C:
m.mu.Lock()
for k, item := range m.items {
if !item.expiresAt.IsZero() && now.After(item.expiresAt) {
delete(m.items, k)
}
}
m.mu.Unlock()
}
}
}
// Get 读取键值。
func (m *MemoryCache) Get(_ context.Context, key string) (string, error) {
m.mu.RLock()
item, ok := m.items[key]
m.mu.RUnlock()
if !ok {
return "", ErrNotFound
}
if !item.expiresAt.IsZero() && time.Now().After(item.expiresAt) {
return "", ErrNotFound
}
return item.value, nil
}
// Set 写入键值。
func (m *MemoryCache) Set(_ context.Context, key, value string, ttl time.Duration) error {
item := memoryItem{value: value}
if ttl > 0 {
item.expiresAt = time.Now().Add(ttl)
}
m.mu.Lock()
m.items[key] = item
m.mu.Unlock()
return nil
}
// Delete 删除键。
func (m *MemoryCache) Delete(_ context.Context, keys ...string) error {
m.mu.Lock()
for _, k := range keys {
delete(m.items, k)
}
m.mu.Unlock()
return nil
}
// Exists 判断键是否存在。
func (m *MemoryCache) Exists(_ context.Context, key string) (bool, error) {
_, err := m.Get(context.Background(), key)
return err == nil, nil
}
// Incr 原子自增;首次创建时记录窗口起点(以过期时间体现)。
func (m *MemoryCache) Incr(_ context.Context, key string, ttl time.Duration) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
now := time.Now()
item, ok := m.items[key]
if ok && !item.expiresAt.IsZero() && now.After(item.expiresAt) {
// 窗口已过期,重新计数
ok = false
}
var n int64
if !ok {
n = 1
newItem := memoryItem{value: "1"}
if ttl > 0 {
newItem.expiresAt = now.Add(ttl)
}
m.items[key] = newItem
return n, nil
}
// 解析现有值
for _, c := range item.value {
if c < '0' || c > '9' {
n = 0
break
}
n = n*10 + int64(c-'0')
}
n++
newItem := memoryItem{value: itoa(n), expiresAt: item.expiresAt}
m.items[key] = newItem
return n, nil
}
// Close 停止清理协程。
func (m *MemoryCache) Close() error {
select {
case <-m.done:
default:
close(m.done)
}
return nil
}
// itoa 简单整数转字符串,避免在锁内依赖 strconv 的额外开销(数值都很小)。
func itoa(n int64) string {
if n == 0 {
return "0"
}
var buf [20]byte
i := len(buf)
neg := n < 0
if neg {
n = -n
}
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}