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 全绿;二进制端到端冒烟通过
160 lines
3.1 KiB
Go
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:])
|
|
}
|