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;
}