chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ywxapp\middleware;
|
||||
|
||||
use Closure;
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
use ywxapp\service\AddonHotReload;
|
||||
|
||||
/**
|
||||
* 插件热重载中间件
|
||||
*
|
||||
* 在开发环境中自动检测插件文件变更并执行热重载
|
||||
*/
|
||||
class AddonHotReloadMiddleware
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return Response
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// 仅在开发环境启用
|
||||
if (config('app.app_debug')) {
|
||||
try {
|
||||
// 启用自动热重载检测
|
||||
AddonHotReload::enableAutoReload();
|
||||
} catch (\Exception $e) {
|
||||
// 静默处理错误,避免影响正常请求
|
||||
\think\facade\Log::warning("插件热重载中间件执行失败:" . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ywxapp\middleware;
|
||||
|
||||
use think\facade\Config;
|
||||
use ywxapp\service\AddonPerformanceMonitor;
|
||||
|
||||
/**
|
||||
* 插件性能监控中间件(全局注册于 app/middleware.php)
|
||||
*
|
||||
* 设计要点:
|
||||
* - 从请求路径首段解析插件名(插件路由统一为 /<插件名>/<group>/...),
|
||||
* 仅对「已启用插件」的请求做性能采样,核心应用(admin/index/api 等)零开销;
|
||||
* - 用 AddonPerformanceMonitor::measure() 包裹 $next($request),把每次插件
|
||||
* HTTP 请求的真实耗时/内存/成功失败写入缓存,使 addon:health 的性能项有真数据;
|
||||
* - measure() 的 finally 已防御性吞掉缓存异常,本中间件无需再包 try/catch,
|
||||
* 且绝不二次调用 $next(避免重复执行业务);
|
||||
* - 控制器抛异常时 measure() 记录为失败并原样向外抛出,框架错误处理不受影响。
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AddonPerformanceMiddleware
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
$addon = $this->resolveAddon($request);
|
||||
if ($addon === null) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$action = $this->resolveAction($request);
|
||||
|
||||
return AddonPerformanceMonitor::measure($addon, $action, function () use ($next, $request) {
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从路径首段解析插件名,并确认其为已启用插件(Config::get('addon') 仅含 state=1)
|
||||
*/
|
||||
private function resolveAddon($request): ?string
|
||||
{
|
||||
$path = trim((string) $request->pathinfo(), '/');
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
$segments = explode('/', $path);
|
||||
$maybe = $segments[0];
|
||||
|
||||
$addon = Config::get('addon', []);
|
||||
foreach ($addon as $info) {
|
||||
if (($info['name'] ?? '') === $maybe) {
|
||||
return $maybe;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动作标签:HTTP 方法 + 插件名之后的剩余路径(如 GET backend/addonreview/index)
|
||||
*/
|
||||
private function resolveAction($request): string
|
||||
{
|
||||
$path = trim((string) $request->pathinfo(), '/');
|
||||
$segments = explode('/', $path);
|
||||
array_shift($segments); // 去掉插件名首段
|
||||
$rest = implode('/', $segments);
|
||||
return ($request->method() ?? 'GET') . ' ' . ($rest === '' ? 'index' : $rest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace ywxapp\middleware;
|
||||
|
||||
/**
|
||||
* AllowCrossDomain 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class AllowCrossDomain
|
||||
{
|
||||
|
||||
|
||||
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
$origin = $request->header('Origin') ?: '';
|
||||
|
||||
// 允许的域名列表(根据实际情况修改)
|
||||
$allowedOrigins = [
|
||||
'http://localhost',
|
||||
'http://localhost:3000',
|
||||
'http://localhost:5173',
|
||||
'http://127.0.0.1',
|
||||
'http://127.0.0.1:3000',
|
||||
'https://your-production-domain.com'
|
||||
];
|
||||
|
||||
// 检查来源是否允许
|
||||
if (in_array($origin, $allowedOrigins)) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
|
||||
// 明确允许的请求头(必须包含 content-type)
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-CSRF-TOKEN');
|
||||
|
||||
// 允许的方法
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
||||
|
||||
// 预检请求直接返回
|
||||
if ($request->method() == 'OPTIONS') {
|
||||
header('Access-Control-Max-Age: 86400'); // 24小时缓存
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
header('Content-Length: 0');
|
||||
return response()->code(204);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ywxapp\middleware;
|
||||
|
||||
use Closure;
|
||||
use think\App;
|
||||
use think\exception\HttpException;
|
||||
use think\Request;
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* 多根目录多应用中间件
|
||||
*
|
||||
* 核心语义:
|
||||
* - "blog" → 按 roots顺序查找,找到第一个匹配即用
|
||||
* - "app2:blog" → 强制在 /app2/blog找,找不到直接 404
|
||||
* - 都不支持"先查 app找不到再降级到 app2"再降级这种串行写法
|
||||
* (显式前缀必须严格匹配,避免行为不可预测)
|
||||
*/
|
||||
class MultiApp
|
||||
{
|
||||
protected App $app;
|
||||
|
||||
/** @var array<string,string> 根目录标识 => 绝对路径 */
|
||||
protected array $roots = [];
|
||||
|
||||
/** @var array<string,string> 根目录标识 => 命名空间前缀 */
|
||||
protected array $namespaces = [];
|
||||
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->roots = $app->config->get('app.app_roots', [
|
||||
'app' => $app->getBasePath(),
|
||||
'addon' => $app->getRootPath() . 'addon' . DIRECTORY_SEPARATOR,
|
||||
]);
|
||||
// $this->namespaces = $app->config->get('app.app_namespaces', [
|
||||
// 'app' => 'app',
|
||||
// 'addon' => 'addon',
|
||||
// ]);
|
||||
$this->namespaces = [
|
||||
'app' => 'app',
|
||||
'addon' => 'addon',
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if (!$this->parseMultiApp()) {
|
||||
return $next($request);
|
||||
}
|
||||
return $this->app->middleware
|
||||
->pipeline('app')
|
||||
->send($request)
|
||||
->then(function ($request) use ($next) {
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
protected function getRoutePath(): string
|
||||
{
|
||||
return $this->app->getAppPath() . 'route' . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心解析
|
||||
*/
|
||||
protected function parseMultiApp(): bool
|
||||
{
|
||||
$scriptName = $this->getScriptName();
|
||||
$defaultApp = $this->app->config->get('app.default_app') ?: 'index';
|
||||
$appName = $this->app->http->getName();
|
||||
|
||||
// ==================== 阶段 1:独立入口 / 显式绑定 ====================
|
||||
if ($appName || ($scriptName && !in_array($scriptName, ['index', 'router', 'think']))) {
|
||||
$appName = $appName ?: $scriptName;
|
||||
$this->app->http->setBind();
|
||||
|
||||
// 独立入口可不走 domain_bind,直接解析
|
||||
$resolved = $this->resolve($appName);
|
||||
if (!$resolved) {
|
||||
throw new HttpException(404, 'app not exists:' . $appName);
|
||||
}
|
||||
return $this->applyResolved($resolved, $appName);
|
||||
}
|
||||
|
||||
// ==================== 阶段 2: 自动识别 ====================
|
||||
$this->app->http->setBind(false);
|
||||
$appName = null;
|
||||
|
||||
// -------- 2.1 域名绑定 --------
|
||||
$bind = $this->app->config->get('app.domain_bind', []);
|
||||
if (!empty($bind)) {
|
||||
$subDomain = $this->app->request->subDomain();
|
||||
$domain = $this->app->request->host(true);
|
||||
if (isset($bind[$domain])) {
|
||||
$appName = $bind[$domain];
|
||||
$this->app->http->setBind();
|
||||
} elseif (isset($bind[$subDomain])) {
|
||||
$appName = $bind[$subDomain];
|
||||
$this->app->http->setBind();
|
||||
} elseif (isset($bind['*'])) {
|
||||
$appName = $bind['*'];
|
||||
$this->app->http->setBind();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->app->http->isBind()) {
|
||||
$resolved = $this->resolve($appName);
|
||||
if (!$resolved) {
|
||||
throw new HttpException(404, 'app not exists:' . $appName);
|
||||
}
|
||||
return $this->applyResolved($resolved, $appName);
|
||||
}
|
||||
|
||||
// -------- 2.2 URL 路径识别 --------
|
||||
$path = $this->app->request->pathinfo();
|
||||
$map = $this->app->config->get('app.app_map', []);
|
||||
$deny = $this->app->config->get('app.deny_app_list', []);
|
||||
|
||||
$fullName = current(explode('/', $path));
|
||||
if (strpos($fullName, '.')) {
|
||||
$fullName = strstr($fullName, '.', true);
|
||||
}
|
||||
|
||||
// 分支1: 命中 app_map
|
||||
if (isset($map[$fullName])) {
|
||||
if ($map[$fullName] instanceof Closure) {
|
||||
$mapped = call_user_func_array($map[$fullName], [$this->app]) ?: $fullName;
|
||||
} else {
|
||||
$mapped = $map[$fullName];
|
||||
}
|
||||
$resolved = $this->resolve($mapped);
|
||||
}
|
||||
// 分支 2: 黑名单 / map 显式禁用
|
||||
elseif ($fullName !== '' && (false !== array_search($fullName, $map) || in_array($fullName, $deny))) {
|
||||
throw new HttpException(404, 'app not exists:' . $fullName);
|
||||
}
|
||||
// 分支 3: map 通配 *
|
||||
elseif ($fullName !== '' && isset($map['*'])) {
|
||||
$resolved = $this->resolve($map['*']);
|
||||
}
|
||||
// 分支 4: URL 段本身带前缀(强制指定根)
|
||||
elseif (str_contains($fullName, ':')) {
|
||||
$resolved = $this->resolve($fullName);
|
||||
}
|
||||
// 分支 5: 默认行为 —— 多根顺序查找
|
||||
else {
|
||||
$name = $fullName !== '' ? $fullName : null;
|
||||
// URL 没给应用名 → 用 default_app
|
||||
$name ??= $defaultApp;
|
||||
$resolved = $this->resolve($name); // ← prefix='',走多根查找
|
||||
}
|
||||
|
||||
// ===== 查找失败处理 =====
|
||||
if (!$resolved) {
|
||||
// app_express=true 且不是显式前缀时 → 兜底 default_app
|
||||
$express = $this->app->config->get('app.app_express', false);
|
||||
if ($express && !str_contains($fullName, ':')) {
|
||||
$resolved = $this->resolve($defaultApp);
|
||||
}
|
||||
if (!$resolved) {
|
||||
throw new HttpException(404, 'app not exists:' . ($fullName ?: $defaultApp));
|
||||
}
|
||||
}
|
||||
|
||||
// 重写 pathinfo: 把 URL 第一段剥掉
|
||||
if ($fullName) {
|
||||
$this->app->request->setRoot('/' . $fullName);
|
||||
$this->app->request->setPathinfo(
|
||||
strpos($path, '/') ? ltrim(strstr($path, '/'), '/') : ''
|
||||
);
|
||||
}
|
||||
|
||||
return $this->applyResolved($resolved, $fullName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析应用名为最终 [prefix, name, appPath]
|
||||
* 支持:
|
||||
* "app2:blog" → ['app2', 'blog', '/www/app2/blog/']
|
||||
* "blog" → ['', 'blog', null] ← 调用方拿到 null 时按 roots 顺序兜底
|
||||
*/
|
||||
protected function resolve(string $appName): ?array
|
||||
{
|
||||
[$prefix, $name] = $this->splitAppName($appName);
|
||||
|
||||
if ($prefix !== '') {
|
||||
// 显式前缀: 必须严格匹配,失败立即返回 null(不再降级)
|
||||
if (!isset($this->roots[$prefix])) {
|
||||
return null;
|
||||
}
|
||||
$path = $this->roots[$prefix] . $name . DIRECTORY_SEPARATOR;
|
||||
return is_dir($path) ? [$prefix, $name, $path] : null;
|
||||
}
|
||||
|
||||
// 多根顺序查找
|
||||
foreach ($this->roots as $key => $root) {
|
||||
$path = $root . $name . DIRECTORY_SEPARATOR;
|
||||
if (is_dir($path)) {
|
||||
return [$key, $name, $path];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分应用名
|
||||
* "app2:blog" → ["app2", "blog"]
|
||||
* "blog" → ["", "blog"]
|
||||
*/
|
||||
protected function splitAppName(string $fullName): array
|
||||
{
|
||||
if ($fullName !== '' && str_contains($fullName, ':')) {
|
||||
[$prefix, $name] = explode(':', $fullName, 2);
|
||||
return [$prefix ?: '', $name];
|
||||
}
|
||||
return ['', $fullName];
|
||||
}
|
||||
|
||||
protected function getScriptName(): string
|
||||
{
|
||||
if (isset($_SERVER['SCRIPT_FILENAME'])) {
|
||||
$file = $_SERVER['SCRIPT_FILENAME'];
|
||||
} elseif (isset($_SERVER['argv'][0])) {
|
||||
$file = realpath($_SERVER['argv'][0]);
|
||||
}
|
||||
return isset($file) ? pathinfo($file, PATHINFO_FILENAME) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 resolve() 结果落地到 App上下文
|
||||
*/
|
||||
protected function applyResolved(array $resolved, string $urlName): bool
|
||||
{
|
||||
[$prefix, $name, $appPath] = $resolved;
|
||||
|
||||
$this->app->http->name($name);
|
||||
|
||||
// 应用目录
|
||||
$this->app->setAppPath($appPath);
|
||||
|
||||
// 命名空间
|
||||
$nsPrefix = $this->namespaces[$prefix] ?? $prefix;
|
||||
$this->app->setNamespace($nsPrefix . '\\' . $name);
|
||||
|
||||
// 运行时: 按根 + 应用双层隔离
|
||||
$this->app->setRuntimePath(
|
||||
$this->app->getRuntimePath() . $prefix . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR
|
||||
);
|
||||
|
||||
// 路由目录
|
||||
$this->app->http->setRoutePath($this->getRoutePath());
|
||||
|
||||
// 应用专属配置/中间件/provider/语言包
|
||||
$this->loadApp($name, $appPath);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function loadApp(string $appName, string $appPath): void
|
||||
{
|
||||
if (is_file($appPath . 'common.php')) {
|
||||
include_once $appPath . 'common.php';
|
||||
}
|
||||
|
||||
$files = glob($appPath . 'config' . DIRECTORY_SEPARATOR . '*' . $this->app->getConfigExt());
|
||||
foreach ($files as $file) {
|
||||
$this->app->config->load($file, pathinfo($file, PATHINFO_FILENAME));
|
||||
}
|
||||
|
||||
if (is_file($appPath . 'event.php')) {
|
||||
$this->app->loadEvent(include $appPath . 'event.php');
|
||||
}
|
||||
if (is_file($appPath . 'middleware.php')) {
|
||||
$this->app->middleware->import(include $appPath . 'middleware.php', 'app');
|
||||
}
|
||||
if (is_file($appPath . 'provider.php')) {
|
||||
$this->app->bind(include $appPath . 'provider.php');
|
||||
}
|
||||
|
||||
$this->app->loadLangPack($this->app->lang->defaultLangSet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ywxapp\middleware;
|
||||
|
||||
use think\facade\Db;
|
||||
use ywxapp\model\BaseModel;
|
||||
use ywxapp\library\SpiderDetect;
|
||||
|
||||
/**
|
||||
* 全站搜索蜘蛛统计中间件(全局注册于 app/middleware.php)
|
||||
*
|
||||
* 设计要点:
|
||||
* - 仅命中蜘蛛 UA 才落库,普通用户请求零额外查询;
|
||||
* - 落库在响应生成之后($next 之后),不阻塞蜘蛛抓取响应;
|
||||
* - 明细写 spider_log,按日聚合 upsert 到 spider_stat(报表免全表扫描);
|
||||
* - 任何数据库异常静默吞掉(统计绝不能影响业务),缺表时经 BaseModel 自愈引擎 自愈一次;
|
||||
* - install.sql 为唯一事实源,此处 DDL 仅为老库运行时兜底。
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class SpiderStat
|
||||
{
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
try {
|
||||
$ua = (string) $request->header('user-agent', '');
|
||||
$spider = SpiderDetect::detect($ua);
|
||||
if ($spider !== null) {
|
||||
$this->record($request, $response, $spider, $ua);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 统计失败绝不影响正常响应
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 落库:明细 + 按日聚合
|
||||
*/
|
||||
protected function record($request, $response, string $spider, string $ua): void
|
||||
{
|
||||
$prefix = config('database.connections.mysql.prefix', 'wxapp_');
|
||||
$logTable = $prefix . 'spider_log';
|
||||
$statTable = $prefix . 'spider_stat';
|
||||
|
||||
$data = [
|
||||
'spider' => $spider,
|
||||
'url' => mb_substr((string) $request->url(), 0, 500),
|
||||
'ip' => mb_substr((string) $request->ip(), 0, 45),
|
||||
'app' => mb_substr((string) (app('http')->getName() ?: ''), 0, 20),
|
||||
'user_agent' => mb_substr($ua, 0, 500),
|
||||
'http_code' => method_exists($response, 'getCode') ? (int) $response->getCode() : 200,
|
||||
'create_at' => time(),
|
||||
];
|
||||
|
||||
try {
|
||||
$this->insert($logTable, $statTable, $data, $spider);
|
||||
} catch (\Throwable $e) {
|
||||
// 表可能不存在(老库未升级):自愈一次后重试
|
||||
$this->ensureTables($logTable, $statTable);
|
||||
$this->insert($logTable, $statTable, $data, $spider);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写明细 + 聚合 upsert
|
||||
*/
|
||||
protected function insert(string $logTable, string $statTable, array $data, string $spider): void
|
||||
{
|
||||
Db::table($logTable)->insert($data);
|
||||
// 按日聚合:主键(stat_date, spider),存在则计数+1
|
||||
Db::execute(
|
||||
"INSERT INTO `{$statTable}` (`stat_date`, `spider`, `count`) VALUES (?, ?, 1) "
|
||||
. "ON DUPLICATE KEY UPDATE `count` = `count` + 1",
|
||||
[date('Y-m-d'), $spider]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺表自愈(DDL 与 public/install/install.sql 保持一致)
|
||||
*/
|
||||
protected function ensureTables(string $logTable, string $statTable): void
|
||||
{
|
||||
BaseModel::ensureTable($logTable, "CREATE TABLE IF NOT EXISTS `{$logTable}` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`spider` varchar(20) NOT NULL DEFAULT '' COMMENT '蜘蛛标识',
|
||||
`url` varchar(500) NOT NULL DEFAULT '' COMMENT '抓取URL',
|
||||
`ip` varchar(45) NOT NULL DEFAULT '' COMMENT '来源IP',
|
||||
`app` varchar(20) NOT NULL DEFAULT '' COMMENT '应用名',
|
||||
`user_agent` varchar(500) NOT NULL DEFAULT '' COMMENT 'User-Agent',
|
||||
`http_code` smallint unsigned NOT NULL DEFAULT '200' COMMENT '响应状态码',
|
||||
`create_at` int NOT NULL DEFAULT '0' COMMENT '抓取时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_spider_time` (`spider`,`create_at`),
|
||||
KEY `idx_create_at` (`create_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='搜索蜘蛛抓取日志'");
|
||||
|
||||
BaseModel::ensureTable($statTable, "CREATE TABLE IF NOT EXISTS `{$statTable}` (
|
||||
`stat_date` date NOT NULL COMMENT '统计日期',
|
||||
`spider` varchar(20) NOT NULL DEFAULT '' COMMENT '蜘蛛标识',
|
||||
`count` int unsigned NOT NULL DEFAULT '0' COMMENT '抓取次数',
|
||||
PRIMARY KEY (`stat_date`,`spider`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='搜索蜘蛛按日统计'");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user