649 lines
18 KiB
PHP
649 lines
18 KiB
PHP
<?php
|
||
// 公共函数:转义、认证、模板助手、CSRF、主题等。
|
||
if (!defined('IN_APP')) {
|
||
http_response_code(403);
|
||
exit('Forbidden');
|
||
}
|
||
|
||
/**
|
||
* HTML 转义,防止 XSS。
|
||
*/
|
||
function h($str)
|
||
{
|
||
return htmlspecialchars((string) $str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||
}
|
||
|
||
/**
|
||
* 格式化人民币价格。
|
||
*/
|
||
function money($n)
|
||
{
|
||
return '¥' . number_format((float) $n, 2);
|
||
}
|
||
|
||
/**
|
||
* 跳转。
|
||
*/
|
||
function redirect($url)
|
||
{
|
||
// 防开放重定向 / 协议相对 URL 被当作 host(如 //evil.com 或 http://x)
|
||
if (is_string($url)
|
||
&& (strpos($url, '//') === 0
|
||
|| preg_match('#^[a-z][a-z0-9+.\-]*://#i', $url))) {
|
||
$url = 'index.php';
|
||
}
|
||
header('Location: ' . $url);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* 获取访问者真实 IP(兼容 CDN / 反向代理)。
|
||
* 优先级:X-Forwarded-For(取第一个)→ X-Real-IP → Client-IP → REMOTE_ADDR。
|
||
*/
|
||
function getClientIp()
|
||
{
|
||
$candidates = [];
|
||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||
$candidates = array_merge($candidates, explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
|
||
}
|
||
foreach (['HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $k) {
|
||
if (!empty($_SERVER[$k])) {
|
||
$candidates[] = $_SERVER[$k];
|
||
}
|
||
}
|
||
foreach ($candidates as $ip) {
|
||
$ip = trim($ip);
|
||
// 过滤私有/保留地址,避免 CDN 内部地址误导
|
||
if ($ip !== '' && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||
return $ip;
|
||
}
|
||
}
|
||
return trim((string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'));
|
||
}
|
||
|
||
/**
|
||
* 某 IP 今日已注册账号数(用于「每日每 IP 最多 2 个」限制)。
|
||
*/
|
||
function regCountToday($ip)
|
||
{
|
||
try {
|
||
$stmt = db()->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 = '<div style="font-family:Segoe UI,Helvetica,Arial,sans-serif;max-width:480px;margin:0 auto;padding:24px;'
|
||
. 'border:1px solid #e3e8ef;border-radius:16px;">'
|
||
. '<h2 style="color:#1a73e8;margin:0 0 12px;">验证你的邮箱</h2>'
|
||
. '<p style="color:#1f2430;line-height:1.7;">你好 ' . h($name) . ',感谢注册 ' . h(SITE_NAME) . '!</p>'
|
||
. '<p style="color:#1f2430;line-height:1.7;">请点击下方按钮完成邮箱验证:</p>'
|
||
. '<p style="margin:18px 0;"><a href="' . h($url) . '" style="background:#1a73e8;color:#fff;padding:11px 22px;'
|
||
. 'border-radius:999px;text-decoration:none;display:inline-block;font-weight:600;">验证邮箱</a></p>'
|
||
. '<p style="color:#5f6b7a;font-size:.85rem;line-height:1.6;">如按钮无法点击,请复制以下链接到浏览器打开:<br>' . h($url) . '</p>'
|
||
. '<hr style="border:none;border-top:1px solid #eef1f6;margin:18px 0;">'
|
||
. '<p style="color:#9aa7b8;font-size:.8rem;margin:0;">若非本人操作,请忽略此邮件。</p>'
|
||
. '</div>';
|
||
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 '<input type="hidden" name="csrf" value="' . csrfToken() . '">';
|
||
}
|
||
|
||
/**
|
||
* 校验 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 '<span class="exp-badge exp-none">无到期时间</span>';
|
||
}
|
||
$icon = [
|
||
'ok' => 'fa-calendar-check',
|
||
'warn' => 'fa-clock',
|
||
'danger' => 'fa-triangle-exclamation',
|
||
'expired' => 'fa-circle-xmark',
|
||
][$s['level']] ?? 'fa-calendar';
|
||
return '<span class="exp-badge exp-' . $s['level'] . '"><i class="fas ' . $icon . '"></i> ' . h($s['text']) . '</span>';
|
||
}
|
||
|
||
/**
|
||
* 支付方式中文映射。
|
||
*/
|
||
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;
|
||
} |