prepare('SELECT COUNT(*) FROM ' . tn('users') . ' WHERE reg_ip = ? AND DATE(created_at) = CURDATE()'); $stmt->execute([$ip]); return (int) $stmt->fetchColumn(); } catch (Throwable $e) { return 0; } } /** * 站点基础 URL(用于拼接验证链接)。自动适配是否部署在子目录。 */ function siteBaseUrl() { $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || ($_SERVER['SERVER_PORT'] ?? '') == '443' ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $dir = dirname($_SERVER['SCRIPT_NAME'] ?? '/index.php'); $dir = ($dir === '/' || $dir === '\\') ? '' : rtrim($dir, '/'); return $scheme . '://' . $host . $dir; } /** * 取昵称/用户名的首字(用于头像圆圈)。 */ function avatarText($name) { $name = trim((string) $name); if ($name === '') { return '?'; } return mb_strtoupper(mb_substr($name, 0, 1, 'UTF-8'), 'UTF-8'); } /** * 发送邮箱验证邮件:生成/更新 email_token 并投递。 * 返回 ['ok'=>bool, 'error'=>string, 'log'=>array] */ function sendVerifyMail($userId) { $stmt = db()->prepare('SELECT id, username, nickname, email FROM ' . tn('users') . ' WHERE id = ?'); $stmt->execute([$userId]); $user = $stmt->fetch(); if (!$user || $user['email'] === '') { return ['ok' => false, 'error' => '该账号没有可用邮箱', 'log' => []]; } $token = bin2hex(random_bytes(24)); db()->prepare('UPDATE ' . tn('users') . ' SET email_token = ? WHERE id = ?') ->execute([$token, $user['id']]); $url = siteBaseUrl() . '/verify.php?token=' . $token; $name = $user['nickname'] ?: $user['username']; $subject = '[' . SITE_NAME . '] 请验证你的邮箱'; $body = '
' . '

验证你的邮箱

' . '

你好 ' . h($name) . ',感谢注册 ' . h(SITE_NAME) . '!

' . '

请点击下方按钮完成邮箱验证:

' . '

验证邮箱

' . '

如按钮无法点击,请复制以下链接到浏览器打开:
' . h($url) . '

' . '
' . '

若非本人操作,请忽略此邮件。

' . '
'; return fnwSendMail($user['email'], $subject, $body); } /** * 当前登录用户(普通用户),未登录返回 null。 */ function currentUser($forceRefresh = false) { if (empty($_SESSION['user_id'])) { return null; } static $cache = null; if ($cache !== null && !$forceRefresh) { return $cache; } $cache = null; // 强制刷新时先清掉旧缓存 $pdo = db(); $stmt = $pdo->prepare('SELECT id, username, email, nickname, verified, is_admin, status, points, invite_code, invited_by, avatar FROM ' . tn('users') . ' WHERE id = ?'); $stmt->execute([$_SESSION['user_id']]); $row = $stmt->fetch(); // 账号被封禁则视为未登录,并清除会话 if (!$row || (int) $row['status'] !== 1) { unset($_SESSION['user_id']); $cache = null; return null; } $cache = $row; return $cache; } /** * 清除当前登录用户的请求级缓存(后台修改用户数据后调用,使下次 currentUser() 重新查库)。 */ function refreshCurrentUser() { currentUser(true); } function isLoggedIn() { return currentUser() !== null; } /** * 当前登录的管理员,未登录返回 null。 */ function currentAdmin() { if (empty($_SESSION['admin_id'])) { return null; } static $cache = null; if ($cache !== null) { return $cache; } $pdo = db(); $stmt = $pdo->prepare('SELECT id, username, email, status FROM ' . tn('users') . ' WHERE id = ? AND is_admin = 1'); $stmt->execute([$_SESSION['admin_id']]); $row = $stmt->fetch(); if (!$row || (int) $row['status'] !== 1) { unset($_SESSION['admin_id']); $cache = null; return null; } $cache = $row; return $cache; } /** * 要求登录,否则跳转到登录页并带回跳地址。 */ function requireLogin($next = '') { if (!isLoggedIn()) { $q = $next ? '?next=' . urlencode($next) : ''; redirect('login.php' . $q); } } /** * 要求管理员(is_admin=1)登录(后台用)。 * 注意:仅校验会话标志不够——客服(role=cs)同样持有 admin_id 会话, * 因此必须额外确认 is_admin=1,否则客服可通过直接访问 URL 进入管理员专属页面。 */ function requireAdmin() { if (empty($_SESSION['admin_id']) || !currentAdmin()) { redirect('login.php'); } } /** * 当前登录的「员工」账号(管理员或客服),未登录/无权限返回 null。 * 与 currentAdmin() 的区别:currentAdmin() 仅限 is_admin=1;本函数额外允许 role='cs' 的客服。 */ function currentStaff() { if (empty($_SESSION['admin_id'])) { return null; } static $cache = null; if ($cache !== null) { return $cache; } $pdo = db(); $stmt = $pdo->prepare( 'SELECT id, username, email, status, role, is_admin FROM ' . tn('users') . ' WHERE id = ? AND status = 1 AND role IN (\'admin\', \'cs\')' ); $stmt->execute([$_SESSION['admin_id']]); $row = $stmt->fetch(); if (!$row) { unset($_SESSION['admin_id']); $cache = null; return null; } $cache = $row; return $cache; } /** * 要求员工(管理员或客服)登录,否则跳转到后台登录页。 */ function requireStaff() { if (empty($_SESSION['admin_id']) || !currentStaff()) { redirect('login.php'); } } /** * 当前员工是否为管理员(拥有全部后台权限)。 */ function isAdminStaff() { $s = currentStaff(); return $s !== null && (int) $s['is_admin'] === 1; } /** * 生成/获取 CSRF 令牌(存入 session)。 */ function csrfToken() { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } /** * 输出 CSRF 隐藏字段。 */ function csrfField() { return ''; } /** * 校验 CSRF(仅 POST)。失败直接 403。 */ function verifyCsrf() { if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (empty($_POST['csrf']) || !hash_equals((string) $_SESSION['csrf'], (string) $_POST['csrf'])) { http_response_code(403); exit('CSRF 校验失败'); } } } /** * 校验 CSRF token 字符串(用于 AJAX/API 场景)。 * @param string $token 待校验的 token * @return bool */ function verifyCsrfToken($token) { return !empty($token) && !empty($_SESSION['csrf']) && hash_equals((string)$_SESSION['csrf'], (string)$token); } /** * 当前主题:浅色 / 深色。跟随 cookie,默认跟随系统(用 CSS 媒体查询兜底)。 */ function currentTheme() { return (!empty($_COOKIE['fnw_theme']) && $_COOKIE['fnw_theme'] === 'dark') ? 'dark' : 'light'; } /** * 订单状态中文映射。 */ function orderStatusLabel($status) { $map = [ 'pending' => '待处理', 'paid' => '已受理', 'shipped' => '已发货', 'completed' => '已完成', 'cancelled' => '已取消', ]; return $map[$status] ?? $status; } /** * 读取单个系统设置(settings 表)。 */ function getSetting($k, $default = '') { try { $stmt = db()->prepare('SELECT v FROM ' . tn('settings') . ' WHERE k = ?'); $stmt->execute([$k]); $v = $stmt->fetchColumn(); return $v === false ? $default : $v; } catch (Throwable $e) { return $default; } } /** * 保存单个系统设置(不存在则插入,存在则更新)。 */ function saveSetting($k, $v) { db()->prepare( 'INSERT INTO ' . tn('settings') . ' (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = ?' )->execute([$k, $v, $v]); } /** * 读取 SMTP 配置(带缓存)。 */ function smtpConfig() { static $c = null; if ($c !== null) { return $c; } $keys = ['smtp_host', 'smtp_port', 'smtp_enc', 'smtp_user', 'smtp_pass', 'smtp_from', 'smtp_fromname', 'notify_emails']; $map = []; try { $pdo = db(); $ph = implode(',', array_fill(0, count($keys), '?')); $stmt = $pdo->prepare('SELECT k, v FROM ' . tn('settings') . ' WHERE k IN (' . $ph . ')'); $stmt->execute($keys); foreach ($stmt->fetchAll() as $row) { $map[$row['k']] = $row['v']; } } catch (Throwable $e) { // 配置表尚不可用时返回空配置 } $c = []; foreach ($keys as $k) { $c[$k] = $map[$k] ?? ''; } return $c; } /** * 通过 SMTP 发送邮件(依赖 includes/mailer.php)。 * 返回 ['ok'=>true,'log'=>[...]] 或 ['ok'=>false,'error'=>string,'log'=>[...]] */ function fnwSendMail($to, $subject, $body) { $cfg = smtpConfig(); if (empty($cfg['smtp_host'])) { return ['ok' => false, 'error' => 'SMTP 未配置']; } require_once __DIR__ . '/mailer.php'; $mailer = new SmtpMailer($cfg['smtp_host'], $cfg['smtp_port'], $cfg['smtp_enc'], $cfg['smtp_user'], $cfg['smtp_pass']); $from = $cfg['smtp_from'] ?: $cfg['smtp_user']; $ok = $mailer->send($to, $subject, $body, $from, $cfg['smtp_fromname'] ?: SITE_NAME); if ($ok) { return ['ok' => true, 'log' => $mailer->log]; } return ['ok' => false, 'error' => $mailer->lastError, 'log' => $mailer->log]; } /** * 工单状态中文映射。 */ function ticketStatusLabel($status) { $map = [ 'open' => '待处理', 'pending' => '处理中', 'resolved' => '已解决', 'closed' => '已关闭', ]; return $map[$status] ?? $status; } /** * 工单优先级中文映射。 */ function ticketPriorityLabel($p) { $map = [1 => '低', 2 => '普通', 3 => '高']; return $map[(int) $p] ?? '普通'; } /** * 周期天数 → 中文标签。0 表示一次性/实物。 */ function periodLabel($days) { $days = (int) $days; if ($days <= 0) { return '一次性'; } if ($days % 365 === 0) { return ($days / 365) . ' 年'; } if ($days % 30 === 0) { return ($days / 30) . ' 个月'; } return $days . ' 天'; } /** * 工单类型中文映射。 */ function ticketTypeLabel($t) { $map = [ 'general' => '普通', 'renewal' => '续期申请', ]; return $map[$t] ?? '普通'; } /** * 到期时间 → 展示文案(null 表示无到期/永久)。 */ function expiryText($dt) { if ($dt === null || $dt === '') { return '—'; } return date('Y-m-d H:i', strtotime($dt)); } /** * 是否即将/已经到期(用于前台续期入口判断,由调用方按需使用)。 */ function isExpired($dt) { if ($dt === null || $dt === '') { return false; } return strtotime($dt) < time(); } /** * 到期状态分析:返回是否含到期时间、展示文案、剩余天数、等级(ok/warn/danger/expired/none)。 */ function expiryState($dt) { if ($dt === null || $dt === '' || $dt === '0000-00-00 00:00:00') { return ['has' => false, 'text' => '—', 'days' => null, 'level' => 'none']; } $ts = strtotime($dt); if ($ts === false) { return ['has' => false, 'text' => '—', 'days' => null, 'level' => 'none']; } $now = time(); $days = ($ts - $now) / 86400; $daysLeft = (int) ceil($days); if ($ts < $now) { return ['has' => true, 'text' => '已到期(' . date('Y-m-d H:i', $ts) . ')', 'days' => $daysLeft, 'level' => 'expired']; } $level = $daysLeft <= 3 ? 'danger' : ($daysLeft <= 7 ? 'warn' : 'ok'); return ['has' => true, 'text' => date('Y-m-d H:i', $ts) . '(剩 ' . $daysLeft . ' 天)', 'days' => $daysLeft, 'level' => $level]; } /** * 到期时间徽标(HTML),用于前台订单列表/详情与后台。 */ function expiryBadge($dt) { $s = expiryState($dt); if (!$s['has']) { return '无到期时间'; } $icon = [ 'ok' => 'fa-calendar-check', 'warn' => 'fa-clock', 'danger' => 'fa-triangle-exclamation', 'expired' => 'fa-circle-xmark', ][$s['level']] ?? 'fa-calendar'; return ' ' . h($s['text']) . ''; } /** * 支付方式中文映射。 */ function payTypeLabel($t) { $map = [ 'cash' => '现金(历史)', 'points' => '积分支付', 'direct' => '直接下单', ]; return $map[$t] ?? '直接下单'; } /** * 读取用户当前积分余额。 */ function getUserPoints($uid) { try { $stmt = db()->prepare('SELECT points FROM ' . tn('users') . ' WHERE id = ?'); $stmt->execute([(int) $uid]); $v = $stmt->fetchColumn(); return $v === false ? 0 : (int) $v; } catch (Throwable $e) { return 0; } } /** * 增减用户积分并写入流水(points_log)。 * $amount:正数=增加,负数=扣减(扣减后余额最低为 0)。 * 返回 ['ok'=>bool, 'balance'=>int, 'error'=>string]。 */ function addPoints($uid, $type, $amount, $remark = '') { $uid = (int) $uid; $amount = (int) $amount; try { $pdo = db(); $pdo->beginTransaction(); $stmt = $pdo->prepare('SELECT points FROM ' . tn('users') . ' WHERE id = ? FOR UPDATE'); $stmt->execute([$uid]); $cur = (int) $stmt->fetchColumn(); $balance = max(0, $cur + $amount); $pdo->prepare('UPDATE ' . tn('users') . ' SET points = ? WHERE id = ?') ->execute([$balance, $uid]); $pdo->prepare( 'INSERT INTO ' . tn('points_log') . ' (user_id, type, amount, balance, remark, created_at) VALUES (?,?,?,?,?,NOW())' )->execute([$uid, $type, $amount, $balance, $remark]); $pdo->commit(); return ['ok' => true, 'balance' => $balance, 'error' => '']; } catch (Throwable $e) { if (isset($pdo) && $pdo->inTransaction()) { $pdo->rollBack(); } return ['ok' => false, 'balance' => 0, 'error' => $e->getMessage()]; } } /** * 生成唯一邀请码(大写字母+数字,8 位)。 */ function genInviteCode() { $chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; for ($i = 0; $i < 20; $i++) { $code = ''; for ($j = 0; $j < 8; $j++) { $code .= $chars[random_int(0, strlen($chars) - 1)]; } $stmt = db()->prepare('SELECT 1 FROM ' . tn('users') . ' WHERE invite_code = ?'); $stmt->execute([$code]); if (!$stmt->fetch()) { return $code; } } return strtoupper(substr(md5(uniqid('', true)), 0, 8)); } /** * 根据邀请码查邀请人 ID(不存在返回 0)。 */ function getInviterIdByCode($code) { $code = trim((string) $code); if ($code === '') { return 0; } $stmt = db()->prepare('SELECT id FROM ' . tn('users') . ' WHERE invite_code = ?'); $stmt->execute([$code]); $id = $stmt->fetchColumn(); return $id === false ? 0 : (int) $id; } /** * 今天是否已签到(防重复)。 */ function hasSignedToday($uid) { try { $stmt = db()->prepare('SELECT 1 FROM ' . tn('sign_logs') . ' WHERE user_id = ? AND sign_date = CURDATE()'); $stmt->execute([(int) $uid]); return (bool) $stmt->fetchColumn(); } catch (Throwable $e) { return false; } } /** * 获取站点名称(优先从数据库读取)。 */ function getSiteName() { global $_site_name; if ($_site_name !== null) { return $_site_name; } try { $db = db(); $stmt = $db->query("SELECT v FROM " . tn('settings') . " WHERE k = 'site_name'"); $val = $stmt->fetchColumn(); $_site_name = ($val !== false && $val !== null && $val !== '') ? $val : '自由云商城'; } catch (Exception $e) { $_site_name = '自由云商城'; } return $_site_name; }