feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全
- 面包屑导航组件,集成至TopNav header - 侧边栏菜单分组间距增大,分组间分隔线 - 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计 - 修复Policies.tsx民主程序推进bug(字段名/API路径/参数) - 用工文本模板变量名英文转中文显示 - 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY) - 通知示例数据补充 - h2标题统一为text-sm font-medium - 新增run.md
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 验证码存储服务
|
||||
* 支持两种模式:
|
||||
* 1. Redis(生产环境,REDIS_URL 环境变量存在时自动启用)
|
||||
* 2. 内存 Map(开发环境 fallback)
|
||||
*
|
||||
* 功能:存储、验证、过期清理、失败次数限制
|
||||
*/
|
||||
|
||||
interface CodeEntry {
|
||||
code: string
|
||||
expiresAt: number
|
||||
failCount: number
|
||||
lastSentAt: number
|
||||
}
|
||||
|
||||
// 内存存储 fallback
|
||||
const memoryStore = new Map<string, CodeEntry>()
|
||||
|
||||
// 是否启用 Redis
|
||||
const REDIS_URL = process.env.REDIS_URL || ''
|
||||
const useRedis = !!REDIS_URL
|
||||
|
||||
// Redis 客户端(懒加载)
|
||||
let redisClient: any = null
|
||||
async function getRedis() {
|
||||
if (!redisClient && useRedis) {
|
||||
try {
|
||||
const { createClient } = await import('redis')
|
||||
redisClient = createClient({ url: REDIS_URL })
|
||||
redisClient.on('error', (err: any) => console.error('[Redis] error:', err))
|
||||
await redisClient.connect()
|
||||
console.log('[Redis] 验证码存储已连接')
|
||||
} catch (err) {
|
||||
console.warn('[Redis] 连接失败,降级为内存存储:', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
return redisClient
|
||||
}
|
||||
|
||||
const KEY_PREFIX = 'vcode:'
|
||||
const TTL_SECONDS = 300 // 5 分钟
|
||||
|
||||
/**
|
||||
* 存储验证码
|
||||
*/
|
||||
export async function setCode(key: string, code: string, ttlMs: number = 5 * 60 * 1000): Promise<void> {
|
||||
const entry: CodeEntry = {
|
||||
code,
|
||||
expiresAt: Date.now() + ttlMs,
|
||||
failCount: 0,
|
||||
lastSentAt: Date.now(),
|
||||
}
|
||||
|
||||
const redis = await getRedis()
|
||||
if (redis) {
|
||||
try {
|
||||
await redis.set(`${KEY_PREFIX}${key}`, JSON.stringify(entry), { PX: ttlMs })
|
||||
return
|
||||
} catch (err) {
|
||||
console.warn('[Redis] set 失败,降级内存:', err)
|
||||
}
|
||||
}
|
||||
memoryStore.set(key, entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码条目
|
||||
*/
|
||||
export async function getCode(key: string): Promise<CodeEntry | null> {
|
||||
const redis = await getRedis()
|
||||
if (redis) {
|
||||
try {
|
||||
const raw = await redis.get(`${KEY_PREFIX}${key}`)
|
||||
if (!raw) return null
|
||||
const entry = JSON.parse(raw) as CodeEntry
|
||||
if (entry.expiresAt < Date.now()) {
|
||||
await redis.del(`${KEY_PREFIX}${key}`)
|
||||
return null
|
||||
}
|
||||
return entry
|
||||
} catch (err) {
|
||||
console.warn('[Redis] get 失败,降级内存:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const entry = memoryStore.get(key)
|
||||
if (!entry) return null
|
||||
if (entry.expiresAt < Date.now()) {
|
||||
memoryStore.delete(key)
|
||||
return null
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新验证码条目(如递增失败次数)
|
||||
*/
|
||||
export async function updateCode(key: string, updates: Partial<CodeEntry>): Promise<void> {
|
||||
const redis = await getRedis()
|
||||
if (redis) {
|
||||
try {
|
||||
const raw = await redis.get(`${KEY_PREFIX}${key}`)
|
||||
if (raw) {
|
||||
const entry = { ...JSON.parse(raw), ...updates }
|
||||
await redis.set(`${KEY_PREFIX}${key}`, JSON.stringify(entry), { PX: TTL_SECONDS * 1000 })
|
||||
}
|
||||
return
|
||||
} catch (err) {
|
||||
console.warn('[Redis] update 失败,降级内存:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const entry = memoryStore.get(key)
|
||||
if (entry) {
|
||||
Object.assign(entry, updates)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除验证码
|
||||
*/
|
||||
export async function deleteCode(key: string): Promise<void> {
|
||||
const redis = await getRedis()
|
||||
if (redis) {
|
||||
try {
|
||||
await redis.del(`${KEY_PREFIX}${key}`)
|
||||
return
|
||||
} catch (err) {
|
||||
console.warn('[Redis] del 失败,降级内存:', err)
|
||||
}
|
||||
}
|
||||
memoryStore.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查发送频率限制(60秒内不可重复发送)
|
||||
*/
|
||||
export async function checkRateLimit(key: string, intervalMs: number = 60 * 1000): Promise<boolean> {
|
||||
const entry = await getCode(key)
|
||||
if (entry && entry.lastSentAt && Date.now() - entry.lastSentAt < intervalMs) {
|
||||
return false // 被限流
|
||||
}
|
||||
return true // 允许发送
|
||||
}
|
||||
Reference in New Issue
Block a user