import dayjs from 'dayjs' import crypto from 'crypto' // 密码加密 key(与原 PHP 保持一致) const PASSWORD_KEY = process.env.PASSWORD_KEY || 'WVImV8mIMnpY9Lrmh3yoaJ2yRLNACBfg' /** * 格式化时间 */ export function time(date) { return dayjs(date).format('YYYY-MM-DD HH:mm:ss') } /** * 当前 Unix 时间戳(秒) */ export function unixTime() { return Math.floor(Date.now() / 1000) } /** * md5 加密(兼容 PHP md5()) */ export function md5(str) { return crypto.createHash('md5').update(str).digest('hex') } /** * 对密码做 md5+key 加密(与原 PHP PASSWORD_KEY 逻辑一致) */ export function encryptPassword(password) { return md5(password + PASSWORD_KEY) } /** * 生成随机验证码 * @param {number} length 长度 * @param {'NUMBER'|'CHAR'|'ALL'} type 类型 */ export function generateCode(length = 6, type = 'NUMBER') { const chars = { NUMBER: '0123456789', CHAR: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ALL: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', } const pool = chars[type] || chars.NUMBER let code = '' for (let i = 0; i < length; i++) { code += pool[Math.floor(Math.random() * pool.length)] } return code } /** * 获取客户端真实 IP(支持代理) */ export function getClientIp(ctx) { return ( ctx.request.headers['x-forwarded-for']?.split(',')[0]?.trim() || ctx.request.headers['x-real-ip'] || ctx.ip ) } /** * 校验账号格式(6-16位字母数字下划线) */ export function isValidAccount(account) { return /^[a-zA-Z0-9_]{6,16}$/.test(account) } /** * 校验邮箱格式 */ export function isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) } /** * 从 User-Agent 解析设备/系统/浏览器信息(与 PHP function.php 保持一致) * @param {string} ua * @returns {{ device: string, os: string, browse: string }} */ export function getDeviceInfo(ua = '') { const uaLower = ua.toLowerCase() // 设备类型 let device = 'pc' if (/mobile|android|iphone|ipad|ipod|windows phone/i.test(ua)) device = 'mobile' // 操作系统 let os = 'Other' if (/windows nt 10/i.test(ua)) os = 'Windows 10' else if (/windows nt 6\.3/i.test(ua)) os = 'Windows 8.1' else if (/windows nt 6\.2/i.test(ua)) os = 'Windows 8' else if (/windows nt 6\.1/i.test(ua)) os = 'Windows 7' else if (/windows nt 6\.0/i.test(ua)) os = 'Windows Vista' else if (/windows nt 5\.1/i.test(ua)) os = 'Windows XP' else if (/windows/i.test(ua)) os = 'Windows' else if (/android (\d+\.\d+)/i.test(ua)) os = 'Android ' + ua.match(/android (\d+\.\d+)/i)[1] else if (/iphone os (\d+_\d+)/i.test(ua)) os = 'iOS ' + ua.match(/iphone os (\d+_\d+)/i)[1].replace('_', '.') else if (/ipad.*os (\d+_\d+)/i.test(ua)) os = 'iPadOS ' + ua.match(/ipad.*os (\d+_\d+)/i)[1].replace('_', '.') else if (/mac os x/i.test(ua)) os = 'macOS' else if (/linux/i.test(ua)) os = 'Linux' // 浏览器 let browse = 'Other' if (/edg\//i.test(ua)) browse = 'Edge' else if (/opr\//i.test(ua) || /opera/i.test(ua)) browse = 'Opera' else if (/chrome\/(\d+)/i.test(ua)) browse = 'Chrome ' + ua.match(/chrome\/(\d+)/i)[1] else if (/firefox\/(\d+)/i.test(ua)) browse = 'Firefox ' + ua.match(/firefox\/(\d+)/i)[1] else if (/safari\/(\d+)/i.test(ua) && !/chrome/i.test(ua)) browse = 'Safari' else if (/micromessenger/i.test(ua)) browse = '微信' else if (/mqqbrowser/i.test(ua)) browse = 'QQ浏览器' return { device, os, browse } }