码桶

发现社区成员的开源项目

geekssl /

wstats

公开
main
wstats/public/install/sql/install.sql
install.sql22.7 KB
-- =====================================================================
-- WebStats 全量安装脚本  (install.sql)
-- 引擎:MySQL 5.7+ / 8.0    字符集:utf8mb4
--
-- 使用方式(任选其一):
--   A. 安装向导自动执行(推荐):访问 http(s)://你的域名/install/
--   B. 命令行:
--        mysql -u<用户> -p <库名> < sql/install.sql
--      注意:命令行的分区占位标记需自行替换为分区定义(见文末说明),
--            或直接使用 public/install/cli.php 安装(会自动展开)。
--
-- 特性:
--   1. 全部 CREATE TABLE IF NOT EXISTS —— 幂等,可安全重复执行;
--      绝不包含 DROP/TRUNCATE,安装过程不会破坏已有数据。
--   2. 不含 CREATE DATABASE / USE,库名由安装器(或命令行)指定。
--   3. events 明细表按 day 做 RANGE 月分区;分区边界必须是可解析的日期字面量
--      (scripts/cron.php 的 partition/clean 依赖它),故不使用 MAXVALUE。
--
-- 表清单:
--   users        管理员/用户
--   sites        接入站点
--   auth_tokens  登录令牌
--   events       事件明细(大表,按月分区)
--   sessions     访客会话聚合
--   site_daily   站点每日汇总(cron rollup 生成)
--   funnels      转化漏斗定义
--   goals        转化目标(页面目标 / 自定义事件目标)
--   api_tokens   开放 API 个人访问令牌(PAT,只读白名单接口)
--   site_members 站点协作成员(多用户只读/可编辑)
--   notify_channels / alert_rules / alert_logs / report_subscriptions / sys_settings
--                告警与日报(推送渠道 / 规则 / 记录 / 订阅)
-- =====================================================================

-- ---------------------------------------------------------------------
-- 用户表
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `users` (
  `id`          INT UNSIGNED    NOT NULL AUTO_INCREMENT,
  `email`       VARCHAR(190)    NOT NULL,
  `password`    VARCHAR(255)    NOT NULL,                -- password_hash()
  `nickname`    VARCHAR(64)     NOT NULL DEFAULT '',
  `lang`        VARCHAR(10)     NOT NULL DEFAULT 'zh-CN',
  `status`      TINYINT         NOT NULL DEFAULT 1,      -- 1 正常 0 禁用
  `is_admin`    TINYINT         NOT NULL DEFAULT 0,      -- 1 系统管理员(可访问系统设置)
  `created_at`  INT UNSIGNED    NOT NULL,
  `updated_at`  INT UNSIGNED    NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 站点表(一个用户可管理多个站点)
-- site_key   : SDK 初始化使用的公开站点标识
-- verify_token: 文件验证令牌,验证文件 = {domain}/wstat_verify_{token}.txt
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `sites` (
  `id`            INT UNSIGNED  NOT NULL AUTO_INCREMENT,
  `user_id`       INT UNSIGNED  NOT NULL,
  `name`          VARCHAR(100)  NOT NULL,
  `domain`        VARCHAR(190)  NOT NULL,                -- 仅主机名,不含协议/路径
  `site_key`      VARCHAR(16)   NOT NULL,
  `timezone`      VARCHAR(64)   NOT NULL DEFAULT 'Asia/Shanghai',
  `status`        TINYINT       NOT NULL DEFAULT 1,
  `verify_token`  VARCHAR(32)   NOT NULL DEFAULT '',
  `verified_at`   INT UNSIGNED  NOT NULL DEFAULT 0,
  `created_at`    INT UNSIGNED  NOT NULL,
  `updated_at`    INT UNSIGNED  NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_site_key` (`site_key`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 登录令牌表(随机 256bit token,服务端存储原文,无需 JWT 依赖)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `auth_tokens` (
  `token`       CHAR(64)      NOT NULL,
  `user_id`     INT UNSIGNED  NOT NULL,
  `expires_at`  INT UNSIGNED  NOT NULL,
  `created_at`  INT UNSIGNED  NOT NULL,
  PRIMARY KEY (`token`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 事件明细流水(核心大表,只追加、按月分区)
-- 覆盖:pageview / event / perf / click / scroll ...(payload 存细节)
-- day = 站点时区下的本地日期(用于分区与按日聚合)
-- 主键必须包含分区列 day => PRIMARY KEY(id, day)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `events` (
  `id`          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `day`         DATE            NOT NULL,
  `site_id`     INT UNSIGNED    NOT NULL,
  `session_id`  VARCHAR(40)     NOT NULL DEFAULT '',
  `visitor_id`  VARCHAR(40)     NOT NULL DEFAULT '',
  `end_user`    VARCHAR(128)    NOT NULL DEFAULT '',     -- 终端用户标识(SDK identify 上报,登录用户分析)
  `type`        VARCHAR(16)     NOT NULL,                -- pageview|event|perf|click|scroll|hb|outlink|download|search
  `ts`          INT UNSIGNED    NOT NULL,                -- 服务端接收时间(unix)
  `url`         VARCHAR(600)    NOT NULL DEFAULT '',
  `title`       VARCHAR(255)    NOT NULL DEFAULT '',
  `ref`         VARCHAR(600)    NOT NULL DEFAULT '',
  `browser`     VARCHAR(40)     NOT NULL DEFAULT '',
  `os`          VARCHAR(40)     NOT NULL DEFAULT '',
  `device`      VARCHAR(20)     NOT NULL DEFAULT '',     -- desktop|mobile|tablet|bot
  `screen`      VARCHAR(24)     NOT NULL DEFAULT '',
  `lang`        VARCHAR(16)     NOT NULL DEFAULT '',
  `ip`          VARCHAR(45)     NOT NULL DEFAULT '',     -- 原始IP(用于独立IP统计/地理解析)
  `country`     VARCHAR(64)     NOT NULL DEFAULT '',
  `province`    VARCHAR(64)     NOT NULL DEFAULT '',
  `city`        VARCHAR(64)     NOT NULL DEFAULT '',
  `source`      VARCHAR(32)     NOT NULL DEFAULT '',     -- direct|search|social|link|paid|utm
  `medium`      VARCHAR(32)     NOT NULL DEFAULT '',
  `utm_source`  VARCHAR(128)    NOT NULL DEFAULT '',     -- 归因 UTM source/medium 原样落库
  `utm_medium`  VARCHAR(128)    NOT NULL DEFAULT '',
  `click_source` VARCHAR(64)    NOT NULL DEFAULT '',     -- 广告平台名(Google Ads/Meta Ads...)
  `ref_host`    VARCHAR(255)    NOT NULL DEFAULT '',     -- 外链 referrer 主机名(去重聚合用)
  `campaign`    VARCHAR(128)    NOT NULL DEFAULT '',
  `content`     VARCHAR(128)    NOT NULL DEFAULT '',
  `term`        VARCHAR(128)    NOT NULL DEFAULT '',
  `click_id`    VARCHAR(128)    NOT NULL DEFAULT '',     -- gclid/msclkid/fbclid/ttclid...
  `payload`     JSON            NULL,                    -- 附加数据(性能指标、事件参数)
  PRIMARY KEY (`id`, `day`),
  KEY `idx_site_day_type` (`site_id`, `day`, `type`),
  KEY `idx_site_day`      (`site_id`, `day`),
  KEY `idx_session`       (`session_id`, `ts`),
  KEY `idx_visitor`       (`site_id`, `visitor_id`, `ts`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
PARTITION BY RANGE (TO_DAYS(`day`)) (
__PARTITIONS__
);

-- ---------------------------------------------------------------------
-- 会话表(一次访问=一个 session,服务端聚合落库)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `sessions` (
  `id`          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `site_id`     INT UNSIGNED    NOT NULL,
  `session_id`  VARCHAR(40)     NOT NULL,
  `visitor_id`  VARCHAR(40)     NOT NULL DEFAULT '',
  `end_user`    VARCHAR(128)    NOT NULL DEFAULT '',     -- 终端用户标识(会话内首个携带值)
  `start_ts`    INT UNSIGNED    NOT NULL,
  `end_ts`      INT UNSIGNED    NOT NULL,
  `pageviews`   SMALLINT UNSIGNED NOT NULL DEFAULT 1,
  `duration`    INT UNSIGNED    NOT NULL DEFAULT 0,      -- 秒
  `bounce`      TINYINT         NOT NULL DEFAULT 0,      -- 1=跳出(单页会话)
  `is_new`      TINYINT         NOT NULL DEFAULT 0,      -- 1=新访客(90天内未见)
  `entry_url`   VARCHAR(600)    NOT NULL DEFAULT '',
  `exit_url`    VARCHAR(600)    NOT NULL DEFAULT '',
  `source`      VARCHAR(32)     NOT NULL DEFAULT 'direct',
  `medium`      VARCHAR(32)     NOT NULL DEFAULT '',
  `campaign`    VARCHAR(128)    NOT NULL DEFAULT '',
  `content`     VARCHAR(128)    NOT NULL DEFAULT '',
  `term`        VARCHAR(128)    NOT NULL DEFAULT '',
  `click_id`    VARCHAR(128)    NOT NULL DEFAULT '',
  `browser`     VARCHAR(40)     NOT NULL DEFAULT '',
  `os`          VARCHAR(40)     NOT NULL DEFAULT '',
  `device`      VARCHAR(20)     NOT NULL DEFAULT '',
  `screen`      VARCHAR(24)     NOT NULL DEFAULT '',
  `lang`        VARCHAR(16)     NOT NULL DEFAULT '',
  `ip`          VARCHAR(45)     NOT NULL DEFAULT '',     -- 会话首访 IP
  `country`     VARCHAR(64)     NOT NULL DEFAULT '',
  `province`    VARCHAR(64)     NOT NULL DEFAULT '',
  `city`        VARCHAR(64)     NOT NULL DEFAULT '',
  `created_at`  INT UNSIGNED    NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_session` (`site_id`, `session_id`),
  KEY `idx_site_start` (`site_id`, `start_ts`),
  KEY `idx_visitor`    (`visitor_id`, `site_id`),
  KEY `idx_euser`      (`site_id`, `end_user`),
  KEY `idx_new`        (`site_id`, `is_new`, `start_ts`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 站点每日汇总(cron rollup 生成,概览趋势读取此表)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `site_daily` (
  `id`         INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `site_id`    INT UNSIGNED NOT NULL,
  `day`        DATE         NOT NULL,
  `pv`         INT UNSIGNED NOT NULL DEFAULT 0,
  `uv`         INT UNSIGNED NOT NULL DEFAULT 0,
  `ipc`        INT UNSIGNED NOT NULL DEFAULT 0,
  `visits`     INT UNSIGNED NOT NULL DEFAULT 0,
  `bounce`     INT UNSIGNED NOT NULL DEFAULT 0,
  `duration`   BIGINT UNSIGNED NOT NULL DEFAULT 0,       -- 当日会话时长合计(秒)
  `new_users`  INT UNSIGNED NOT NULL DEFAULT 0,
  `updated_at` INT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_site_day` (`site_id`, `day`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 转化漏斗定义
--   steps      : JSON 数组,按顺序的漏斗步骤(URL 匹配串),2~8 步
--   match_type : prefix = 前缀匹配(默认,/product 匹配 /product/123)
--                contains = 包含匹配   exact = 精确匹配
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `funnels` (
  `id`         INT UNSIGNED  NOT NULL AUTO_INCREMENT,
  `site_id`    INT UNSIGNED  NOT NULL,
  `name`       VARCHAR(100)  NOT NULL,
  `steps`      TEXT          NOT NULL,
  `match_type` VARCHAR(10)   NOT NULL DEFAULT 'prefix',
  `is_active`  TINYINT       NOT NULL DEFAULT 1,
  `created_at` INT UNSIGNED  NOT NULL,
  `updated_at` INT UNSIGNED  NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_site` (`site_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 转化目标(Goal)
--   type: page  = 页面目标(到达某 URL,按路径匹配 match_type: prefix|contains|exact)
--         event = 自定义事件目标(SDK WStat.track('事件名') 上报,payload.n 精确匹配)
--   target: page 目标存路径片段;event 目标存事件名
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `goals` (
  `id`         INT UNSIGNED  NOT NULL AUTO_INCREMENT,
  `site_id`    INT UNSIGNED  NOT NULL,
  `name`       VARCHAR(100)  NOT NULL,
  `type`       VARCHAR(10)   NOT NULL DEFAULT 'page',
  `target`     VARCHAR(255)  NOT NULL,
  `match_type` VARCHAR(10)   NOT NULL DEFAULT 'contains',
  `is_active`  TINYINT       NOT NULL DEFAULT 1,
  `created_at` INT UNSIGNED  NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_site` (`site_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 开放 API 个人访问令牌(PAT)
--   与 auth_tokens(登录会话)隔离;仅可用于 /open/v1/* 只读接口白名单。
--   expires_at = 0 表示永不过期;last_used_at 记录最近调用时间。
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `api_tokens` (
  `id`           INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id`      INT UNSIGNED NOT NULL,
  `name`         VARCHAR(60)  NOT NULL DEFAULT '',
  `token`        CHAR(64)     NOT NULL,
  `expires_at`   INT UNSIGNED NOT NULL DEFAULT 0,
  `last_used_at` INT UNSIGNED NOT NULL DEFAULT 0,
  `is_active`    TINYINT      NOT NULL DEFAULT 1,
  `created_at`   INT UNSIGNED NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_token` (`token`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 站点协作成员(多用户只读/可编辑授权)
--   role: viewer = 只读(可看全部报表,不可写)
--         editor = 可编辑(可管理漏斗等站点内分析对象)
--   站点所有者(sites.user_id)不在此表,天然为 owner。
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `site_members` (
  `id`         INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `site_id`    INT UNSIGNED NOT NULL,
  `user_id`    INT UNSIGNED NOT NULL,
  `role`       VARCHAR(10)  NOT NULL DEFAULT 'viewer',
  `invited_by` INT UNSIGNED NOT NULL DEFAULT 0,
  `created_at` INT UNSIGNED NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_site_user` (`site_id`, `user_id`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 推送渠道(告警与日报共用)
--   type: webhook 通用 | feishu 飞书 | dingtalk 钉钉 | wecom 企业微信
--         serverchan Server酱 | bark | email 邮件
--   config: JSON,按 type 取键,例如
--     webhook   {"url":"https://...","secret":""}
--     feishu    {"url":"https://open.feishu.cn/open-apis/bot/v2/hook/xxx"}
--     dingtalk  {"url":"https://oapi.dingtalk.com/robot/send?access_token=xxx","secret":"SEC..."}
--     email     {"to":["[email protected]"]}
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `notify_channels` (
  `id`         INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id`    INT UNSIGNED NOT NULL,
  `name`       VARCHAR(60)  NOT NULL,
  `type`       VARCHAR(16)  NOT NULL,
  `config`     TEXT         NOT NULL,
  `is_active`  TINYINT      NOT NULL DEFAULT 1,
  `last_status` VARCHAR(200) NOT NULL DEFAULT '',    -- 最近一次发送结果(便于自检)
  `last_sent_at` INT UNSIGNED NOT NULL DEFAULT 0,
  `created_at` INT UNSIGNED NOT NULL,
  `updated_at` INT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 告警规则
--   site_id   : 0 = 应用到该用户全部站点
--   metric    : pv | uv | visits | online | bounce_rate | avg_duration
--   window_min: 评估窗口(分钟),如 30 = 最近30分钟
--   compare   : ratio_up   较基线上升超过 threshold%(暴涨)
--               ratio_down 较基线下降超过 threshold%(暴跌)
--               abs_over   绝对值高于 threshold
--               abs_under  绝对值低于 threshold
--               zero       窗口内该指标为 0(如站点挂掉)
--   baseline  : prev_day = 昨日同窗口(默认) | prev_window = 上一等长窗口
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `alert_rules` (
  `id`          INT UNSIGNED  NOT NULL AUTO_INCREMENT,
  `user_id`     INT UNSIGNED  NOT NULL,
  `site_id`     INT UNSIGNED  NOT NULL DEFAULT 0,
  `name`        VARCHAR(100)  NOT NULL,
  `metric`      VARCHAR(20)   NOT NULL,
  `window_min`  INT UNSIGNED  NOT NULL DEFAULT 60,
  `compare`     VARCHAR(16)   NOT NULL DEFAULT 'ratio_down',
  `baseline`    VARCHAR(16)   NOT NULL DEFAULT 'prev_day',
  `threshold`   DECIMAL(12,2) NOT NULL DEFAULT 50.00,
  `min_sample`  INT UNSIGNED  NOT NULL DEFAULT 10,   -- 基线样本下限,避免夜间噪声误报
  `level`       VARCHAR(10)   NOT NULL DEFAULT 'warn',
  `cooldown_min` INT UNSIGNED NOT NULL DEFAULT 60,   -- 同规则重复触发最小间隔
  `channels`    VARCHAR(255)  NOT NULL DEFAULT '',   -- 渠道 id,逗号分隔;空=用户全部启用渠道
  `recover_notify` TINYINT    NOT NULL DEFAULT 0,    -- 恢复通知:触发后恢复正常时补发一条
  `firing`      TINYINT       NOT NULL DEFAULT 0,    -- 当前是否处于触发态(恢复判定用,引擎维护)
  `quiet_start` VARCHAR(8)    NOT NULL DEFAULT '',   -- 静默时段开始 HH:MM(站点本地时区),空=不启用
  `quiet_end`   VARCHAR(8)    NOT NULL DEFAULT '',   -- 静默时段结束 HH:MM(支持跨零点,如 23:00~07:00)
  `is_active`   TINYINT       NOT NULL DEFAULT 1,
  `last_fired_at` INT UNSIGNED NOT NULL DEFAULT 0,
  `created_at`  INT UNSIGNED  NOT NULL,
  `updated_at`  INT UNSIGNED  NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_user` (`user_id`),
  KEY `idx_site` (`site_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 告警触发记录(留痕 + 前端历史)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `alert_logs` (
  `id`          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `rule_id`     INT UNSIGNED NOT NULL,
  `user_id`     INT UNSIGNED NOT NULL DEFAULT 0,
  `site_id`     INT UNSIGNED NOT NULL DEFAULT 0,
  `level`       VARCHAR(10)  NOT NULL DEFAULT 'warn',
  `metric`      VARCHAR(20)  NOT NULL DEFAULT '',
  `value`       DECIMAL(14,2) NOT NULL DEFAULT 0,
  `baseline`    DECIMAL(14,2) NOT NULL DEFAULT 0,
  `message`     VARCHAR(500) NOT NULL DEFAULT '',
  `channel_ids` VARCHAR(255) NOT NULL DEFAULT '',
  `status`      VARCHAR(16)  NOT NULL DEFAULT 'pending',  -- sent | failed | skipped
  `error`       VARCHAR(255) NOT NULL DEFAULT '',
  `created_at`  INT UNSIGNED NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_rule_time` (`rule_id`, `created_at`),
  KEY `idx_site_time` (`site_id`, `created_at`),
  KEY `idx_user_time` (`user_id`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 日报/周报订阅
--   freq: daily 每日 | weekly 每周(days 为 1=周一 … 7=周日,逗号分隔)
--   hour: 发送小时(站点本地时区,0-23)
--   last_sent_day: 幂等标记,避免同一天重复发送
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `report_subscriptions` (
  `id`           INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id`      INT UNSIGNED NOT NULL,
  `site_id`      INT UNSIGNED NOT NULL DEFAULT 0,   -- 0 = 该用户全部站点(合并一封)
  `name`         VARCHAR(100) NOT NULL DEFAULT '',
  `freq`         VARCHAR(10)  NOT NULL DEFAULT 'daily',
  `days`         VARCHAR(20)  NOT NULL DEFAULT '1,2,3,4,5,6,7',
  `hour`         TINYINT UNSIGNED NOT NULL DEFAULT 9,
  `channels`     VARCHAR(255) NOT NULL DEFAULT '',
  `is_active`    TINYINT      NOT NULL DEFAULT 1,
  `last_sent_day` VARCHAR(10) NOT NULL DEFAULT '',
  `last_status`  VARCHAR(200) NOT NULL DEFAULT '',
  `created_at`   INT UNSIGNED NOT NULL,
  `updated_at`   INT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (`id`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 用户操作日志(个人中心「我的日志」与用户管理审计;只追加,量级=人工操作)
--   action: login|logout|profile_update|password_change|password_reset
--         | user_create|user_update|user_disable|user_enable|user_delete|user_reset_password
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `user_op_logs` (
  `id`         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id`    INT UNSIGNED    NOT NULL,
  `action`     VARCHAR(40)     NOT NULL,
  `detail`     VARCHAR(500)    NOT NULL DEFAULT '',
  `ip`         VARCHAR(45)     NOT NULL DEFAULT '',
  `ua`         VARCHAR(255)    NOT NULL DEFAULT '',
  `created_at` INT UNSIGNED    NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_user_time` (`user_id`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------
-- 系统设置(键值表;管理员可改:注册开关 / 采集开关 / 告警开关 / SMTP 邮件)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `sys_settings` (
  `name`       VARCHAR(64)  NOT NULL,
  `value`      TEXT         NULL,
  `updated_at` INT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- =====================================================================
-- 关于上面 events 表的分区占位行(安装器会自动展开为真实分区定义):
--   手工执行时,请把该行替换为下面结构(按实际月份调整):
--     PARTITION p202608 VALUES LESS THAN (TO_DAYS('2026-09-01')),
--     PARTITION p202609 VALUES LESS THAN (TO_DAYS('2026-10-01')),
--     PARTITION p202610 VALUES LESS THAN (TO_DAYS('2026-11-01')),
--     PARTITION p202611 VALUES LESS THAN (TO_DAYS('2026-12-01')),
--     PARTITION p202612 VALUES LESS THAN (TO_DAYS('2027-01-01'))
--   分区名规范:pYYYYMM;最后一个边界必须是日期(供 cron partition/clean 解析),
--   不要使用 MAXVALUE,否则自动预建分区与清理逻辑将失效。
--
-- 安装后的定时任务(crontab / 宝塔计划任务)—— 命令在**项目根目录**下执行:
--   * * * * *   php {path}/scripts/cron.php session    # 每1分钟:回收空闲会话
--   0 * * * *   php {path}/scripts/cron.php rollup      # 每小时:生成 site_daily
--   0 3 * * *   php {path}/scripts/cron.php partition   # 每天:预建未来分区
--   0 4 * * *   php {path}/scripts/cron.php clean       # 每天:清理过期明细
-- worker 常驻:php {path}/scripts/worker.php(建议 supervisor / 宝塔守护进程)
-- =====================================================================