莫名
This commit is contained in:
@@ -76,6 +76,35 @@ class AdminController extends Controller
|
||||
return 'assets/uploads/' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理多文件上传(name="gallery[]"),返回相对站点根路径数组。
|
||||
* 逐张复用 uploadFile 的校验逻辑(真实图像/体积上限/类型白名单/SVG 消毒),
|
||||
* 任一文件失败不影响其余文件。
|
||||
*/
|
||||
protected function uploadFiles(string $key): array
|
||||
{
|
||||
if (empty($_FILES[$key]['tmp_name']) || !is_array($_FILES[$key]['tmp_name'])) return [];
|
||||
$names = $_FILES[$key]['name'] ?? [];
|
||||
$errors = $_FILES[$key]['error'] ?? [];
|
||||
$sizes = $_FILES[$key]['size'] ?? [];
|
||||
$out = [];
|
||||
foreach ($_FILES[$key]['tmp_name'] as $i => $tmp) {
|
||||
if (empty($tmp)) continue;
|
||||
// 桥接到单文件校验逻辑
|
||||
$_FILES['_multi_tmp'] = [
|
||||
'name' => $names[$i] ?? 'x.bin',
|
||||
'type' => '',
|
||||
'tmp_name' => $tmp,
|
||||
'error' => $errors[$i] ?? UPLOAD_ERR_OK,
|
||||
'size' => $sizes[$i] ?? 0,
|
||||
];
|
||||
$rel = $this->uploadFile('_multi_tmp');
|
||||
unset($_FILES['_multi_tmp']);
|
||||
if ($rel) $out[] = $rel;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** 消毒 SVG:移除脚本、事件处理器与危险协议,阻断存储型 XSS */
|
||||
protected function sanitizeSvg(string $svg): string
|
||||
{
|
||||
|
||||
@@ -13,17 +13,17 @@ class AuthController extends Controller
|
||||
{
|
||||
if (is_admin()) { $this->redirect(login_landing()); }
|
||||
$error = '';
|
||||
$blocked = false;
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 质量红线:登录必须校验 CSRF + 验证码 + IP/会话双重失败限速,杜绝机器人暴力破解
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
// 质量红线:登录必须校验 CSRF + 验证码 + IP 级双窗口限速(失败 10 分钟/5 次、成功 30 分钟/5 次),杜绝机器人暴力破解
|
||||
if (!csrf_check()) {
|
||||
$error = '表单已过期,请刷新页面后重试';
|
||||
} elseif (ip_login_blocked($ip)) {
|
||||
$error = '尝试次数过多,请 15 分钟后再试';
|
||||
$error = '尝试次数过多,请 30 分钟后再试';
|
||||
$blocked = true;
|
||||
} elseif (!captcha_check($this->post('captcha'))) {
|
||||
$error = '验证码错误,请重新计算';
|
||||
} elseif ($this->isBlocked()) {
|
||||
$error = '尝试次数过多,请 15 分钟后再试';
|
||||
} else {
|
||||
$u = trim($this->post('username'));
|
||||
$p = $this->post('password');
|
||||
@@ -38,8 +38,7 @@ class AuthController extends Controller
|
||||
$user = ['id' => 0, 'username' => $u, 'name' => '管理员', 'role' => 'super_admin'];
|
||||
}
|
||||
if ($ok) {
|
||||
$this->clearAttempts();
|
||||
ip_login_clear($ip);
|
||||
ip_login_register_success($ip); // 记录成功登录(纳入 30 分钟 5 次上限),并重置失败计数
|
||||
session_regenerate_id(true); // 防会话固定
|
||||
$_SESSION['admin_logged'] = true;
|
||||
$_SESSION['admin_id'] = $user['id'] ?? 0;
|
||||
@@ -52,11 +51,15 @@ class AuthController extends Controller
|
||||
$_SESSION['psi_perms'] = $dec($user['psi_perms'] ?? null);
|
||||
$this->redirect(login_landing());
|
||||
}
|
||||
$this->registerAttempt();
|
||||
ip_login_register($ip);
|
||||
ip_login_register_fail($ip); // 记录一次失败(纳入 10 分钟 5 次上限)
|
||||
$error = '用户名或密码错误';
|
||||
}
|
||||
}
|
||||
// 被限速(IP 失败/成功过多)时返回 429 + Retry-After,明确告知客户端稍后再试
|
||||
if ($blocked && !headers_sent()) {
|
||||
http_response_code(429);
|
||||
header('Retry-After: ' . ip_login_remaining($ip));
|
||||
}
|
||||
$captcha = captcha_make(); // 每次渲染都发放新的算术验证码
|
||||
return $this->view('admin/login', ['error' => $error, 'captcha' => $captcha]);
|
||||
}
|
||||
@@ -68,25 +71,6 @@ class AuthController extends Controller
|
||||
&& \Core\App::config('app.driver', 'file') !== 'mysql';
|
||||
}
|
||||
|
||||
/** 暴力破解限速:单会话 15 分钟内失败 5 次即锁定 */
|
||||
private function isBlocked(): bool
|
||||
{
|
||||
$t = $_SESSION['login_attempts'] ?? null;
|
||||
if (!$t || ($t['time'] + 900) < time()) return false;
|
||||
return $t['count'] >= 5;
|
||||
}
|
||||
private function registerAttempt(): void
|
||||
{
|
||||
$t = $_SESSION['login_attempts'] ?? ['count' => 0, 'time' => time()];
|
||||
if (($t['time'] + 900) < time()) { $t = ['count' => 0, 'time' => time()]; }
|
||||
$t['count']++;
|
||||
$_SESSION['login_attempts'] = $t;
|
||||
}
|
||||
private function clearAttempts(): void
|
||||
{
|
||||
unset($_SESSION['login_attempts']);
|
||||
}
|
||||
|
||||
/** 修改当前登录账号的密码 */
|
||||
public function password()
|
||||
{
|
||||
|
||||
@@ -19,13 +19,18 @@ class CaseController extends AdminController
|
||||
|
||||
public function create()
|
||||
{
|
||||
return $this->view('admin/cases_form', ['c' => null]);
|
||||
return $this->view('admin/cases_form', [
|
||||
'c' => null,
|
||||
'mode' => 'fixed',
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
if (!csrf_check()) { $this->redirect('admin/cases'); }
|
||||
$mode = $this->post('mode') === 'builder' ? 'builder' : 'fixed';
|
||||
$cover = $this->uploadFile('cover') ?? $this->post('cover_url', '');
|
||||
$content = ($mode === 'fixed') ? $this->post('content', '') : '';
|
||||
$model = new CustomerCase();
|
||||
$id = $model->insert([
|
||||
'title' => $this->post('title'),
|
||||
@@ -34,48 +39,85 @@ class CaseController extends AdminController
|
||||
'industry' => $this->post('industry', ''),
|
||||
'cover' => $cover,
|
||||
'summary' => $this->post('summary'),
|
||||
'content' => $this->post('content'),
|
||||
'content' => $content,
|
||||
'published_at' => $this->post('published_at', date('Y-m-d')),
|
||||
'sort_order' => (int) $this->post('sort_order', 0),
|
||||
'status' => $this->post('status', 1) ? 1 : 0,
|
||||
'views' => 0,
|
||||
'layout' => $this->post('layout', ''),
|
||||
'mode' => $mode,
|
||||
]);
|
||||
// URL 标识留空时按记录序号顺序生成(短、稳定),避免中文标题导致过长
|
||||
$slug = $this->post('slug') ? slugify($this->post('slug')) : (string)$id;
|
||||
$model->update($id, ['slug' => $slug]);
|
||||
// 新建时若选择「可视化编辑」,保存后直接进入可视化编辑器排版
|
||||
if ($mode === 'builder') {
|
||||
$this->redirect('admin/cases/edit/' . $id);
|
||||
}
|
||||
$this->redirect('admin/cases');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$case = (new CustomerCase())->find($id);
|
||||
if (!$case) { $this->redirect('admin/cases'); }
|
||||
return $this->view('admin/cases_form', ['c' => $case]);
|
||||
$model = new CustomerCase();
|
||||
$c = $model->find($id);
|
||||
if (!$c) { $this->redirect('admin/cases'); }
|
||||
$mode = empty($c['mode']) ? 'fixed' : $c['mode'];
|
||||
if ($mode === 'builder') {
|
||||
$layout = [];
|
||||
if (!empty($c['layout'])) {
|
||||
$dec = json_decode($c['layout'], true);
|
||||
if (is_array($dec)) $layout = $dec;
|
||||
}
|
||||
return $this->view('admin/cases_builder', ['c' => $c, 'layout' => $layout, 'mode' => $mode]);
|
||||
}
|
||||
return $this->view('admin/cases_form', ['c' => $c, 'mode' => $mode]);
|
||||
}
|
||||
|
||||
/** 切换编辑模式(固定版面 <-> 可视化编辑),仅更新 mode 字段 */
|
||||
public function switchMode($id)
|
||||
{
|
||||
$c = (new CustomerCase())->find($id);
|
||||
if (!$c) { $this->redirect('admin/cases'); }
|
||||
$target = ($this->get('mode') === 'builder') ? 'builder' : 'fixed';
|
||||
(new CustomerCase())->update($id, ['mode' => $target]);
|
||||
$this->redirect('admin/cases/edit/' . $id);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
if (!csrf_check()) { $this->redirect('admin/cases'); }
|
||||
$model = new CustomerCase();
|
||||
$case = $model->find($id);
|
||||
if (!$case) { $this->redirect('admin/cases'); }
|
||||
$c = $model->find($id);
|
||||
if (!$c) { $this->redirect('admin/cases'); }
|
||||
$mode = $this->post('mode') === 'builder' ? 'builder' : 'fixed';
|
||||
|
||||
$cover = $this->uploadFile('cover');
|
||||
if (!$cover && $this->post('cover_url')) $cover = $this->post('cover_url');
|
||||
if (!$cover) $cover = $case['cover'] ?? '';
|
||||
$model->update($id, [
|
||||
if (!$cover) $cover = $c['cover'] ?? '';
|
||||
|
||||
$data = [
|
||||
'title' => $this->post('title'),
|
||||
'slug' => $this->post('slug') ? slugify($this->post('slug')) : (string)$id,
|
||||
'customer' => $this->post('customer', ''),
|
||||
'industry' => $this->post('industry', ''),
|
||||
'cover' => $cover,
|
||||
'summary' => $this->post('summary'),
|
||||
'content' => $this->post('content'),
|
||||
'published_at' => $this->post('published_at', date('Y-m-d')),
|
||||
'sort_order' => (int) $this->post('sort_order', 0),
|
||||
'status' => $this->post('status', 1) ? 1 : 0,
|
||||
'layout' => $this->post('layout', ''),
|
||||
]);
|
||||
'mode' => $mode,
|
||||
];
|
||||
|
||||
if ($mode === 'fixed') {
|
||||
$data['content'] = $this->post('content', '');
|
||||
} else {
|
||||
$layout = $this->post('layout', '');
|
||||
if ($layout !== '' && !is_array(json_decode($layout, true))) { $layout = ''; }
|
||||
$data['layout'] = $layout;
|
||||
}
|
||||
|
||||
$model->update($id, $data);
|
||||
$this->redirect('admin/cases');
|
||||
}
|
||||
|
||||
|
||||
@@ -15,47 +15,91 @@ class CategoryController extends AdminController
|
||||
|
||||
public function create()
|
||||
{
|
||||
return $this->view('admin/category_form', ['c' => null]);
|
||||
return $this->view('admin/category_form', [
|
||||
'c' => null,
|
||||
'mode' => 'fixed',
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
if (!csrf_check()) { $this->redirect('admin/categories'); }
|
||||
$cat = new Category();
|
||||
$mode = $this->post('mode') === 'builder' ? 'builder' : 'fixed';
|
||||
$description = ($mode === 'fixed') ? $this->post('description', '') : '';
|
||||
$id = $cat->insert([
|
||||
'name' => $this->post('name'),
|
||||
'slug' => '',
|
||||
'icon' => $this->post('icon', '❄'),
|
||||
'description' => $this->post('description'),
|
||||
'description' => $description,
|
||||
'sort_order' => (int)$this->post('sort_order', 0),
|
||||
'status' => $this->post('status', 1) ? 1 : 0,
|
||||
'layout' => $this->post('layout', ''),
|
||||
'mode' => $mode,
|
||||
]);
|
||||
// URL 标识留空时按记录序号顺序生成(短、稳定)
|
||||
$slug = $this->post('slug') ? slugify($this->post('slug')) : (string)$id;
|
||||
$cat->update($id, ['slug' => $slug]);
|
||||
// 新建时若选择「可视化编辑」,保存后直接进入可视化编辑器排版
|
||||
if ($mode === 'builder') {
|
||||
$this->redirect('admin/categories/edit/' . $id);
|
||||
}
|
||||
$this->redirect('admin/categories');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$cat = new Category();
|
||||
$c = $cat->find($id);
|
||||
if (!$c) { $this->redirect('admin/categories'); }
|
||||
$mode = empty($c['mode']) ? 'fixed' : $c['mode'];
|
||||
if ($mode === 'builder') {
|
||||
$layout = [];
|
||||
if (!empty($c['layout'])) {
|
||||
$dec = json_decode($c['layout'], true);
|
||||
if (is_array($dec)) $layout = $dec;
|
||||
}
|
||||
return $this->view('admin/category_builder', ['c' => $c, 'layout' => $layout, 'mode' => $mode]);
|
||||
}
|
||||
return $this->view('admin/category_form', ['c' => $c, 'mode' => $mode]);
|
||||
}
|
||||
|
||||
/** 切换编辑模式(固定版面 <-> 可视化编辑),仅更新 mode 字段 */
|
||||
public function switchMode($id)
|
||||
{
|
||||
$c = (new Category())->find($id);
|
||||
if (!$c) { $this->redirect('admin/categories'); }
|
||||
return $this->view('admin/category_form', ['c' => $c]);
|
||||
$target = ($this->get('mode') === 'builder') ? 'builder' : 'fixed';
|
||||
(new Category())->update($id, ['mode' => $target]);
|
||||
$this->redirect('admin/categories/edit/' . $id);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
if (!csrf_check()) { $this->redirect('admin/categories'); }
|
||||
(new Category())->update($id, [
|
||||
$cat = new Category();
|
||||
$c = $cat->find($id);
|
||||
if (!$c) { $this->redirect('admin/categories'); }
|
||||
$mode = $this->post('mode') === 'builder' ? 'builder' : 'fixed';
|
||||
|
||||
$data = [
|
||||
'name' => $this->post('name'),
|
||||
'slug' => $this->post('slug') ? slugify($this->post('slug')) : (string)$id,
|
||||
'icon' => $this->post('icon', '❄'),
|
||||
'description' => $this->post('description'),
|
||||
'sort_order' => (int)$this->post('sort_order', 0),
|
||||
'status' => $this->post('status', 1) ? 1 : 0,
|
||||
'layout' => $this->post('layout', ''),
|
||||
]);
|
||||
'mode' => $mode,
|
||||
];
|
||||
|
||||
if ($mode === 'fixed') {
|
||||
$data['description'] = $this->post('description', '');
|
||||
} else {
|
||||
$layout = $this->post('layout', '');
|
||||
if ($layout !== '' && !is_array(json_decode($layout, true))) { $layout = ''; }
|
||||
$data['layout'] = $layout;
|
||||
}
|
||||
|
||||
$cat->update($id, $data);
|
||||
$this->redirect('admin/categories');
|
||||
}
|
||||
|
||||
|
||||
@@ -15,37 +15,67 @@ class NewsController extends AdminController
|
||||
|
||||
public function create()
|
||||
{
|
||||
return $this->view('admin/news_form', ['n' => null]);
|
||||
return $this->view('admin/news_form', [
|
||||
'n' => null,
|
||||
'mode' => 'fixed',
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
if (!csrf_check()) { $this->redirect('admin/news'); }
|
||||
$cover = $this->uploadFile('cover') ?? $this->post('cover_url', '');
|
||||
$news = new News();
|
||||
$mode = $this->post('mode') === 'builder' ? 'builder' : 'fixed';
|
||||
$cover = $this->uploadFile('cover') ?? $this->post('cover_url', '');
|
||||
$content = ($mode === 'fixed') ? $this->post('content', '') : '';
|
||||
$id = $news->insert([
|
||||
'title' => $this->post('title'),
|
||||
'slug' => '',
|
||||
'cover' => $cover,
|
||||
'summary' => $this->post('summary'),
|
||||
'content' => $this->post('content'),
|
||||
'content' => $content,
|
||||
'author' => $this->post('author', '酷冰甲'),
|
||||
'published_at' => $this->post('published_at', date('Y-m-d')),
|
||||
'status' => $this->post('status', 1) ? 1 : 0,
|
||||
'views' => 0,
|
||||
'layout' => $this->post('layout', ''),
|
||||
'mode' => $mode,
|
||||
]);
|
||||
// URL 标识留空时按记录序号顺序生成(短、稳定)
|
||||
$slug = $this->post('slug') ? slugify($this->post('slug')) : (string)$id;
|
||||
$news->update($id, ['slug' => $slug]);
|
||||
// 新建时若选择「可视化编辑」,保存后直接进入可视化编辑器排版
|
||||
if ($mode === 'builder') {
|
||||
$this->redirect('admin/news/edit/' . $id);
|
||||
}
|
||||
$this->redirect('admin/news');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$news = new News();
|
||||
$n = $news->find($id);
|
||||
if (!$n) { $this->redirect('admin/news'); }
|
||||
$mode = empty($n['mode']) ? 'fixed' : $n['mode'];
|
||||
if ($mode === 'builder') {
|
||||
$layout = [];
|
||||
if (!empty($n['layout'])) {
|
||||
$dec = json_decode($n['layout'], true);
|
||||
if (is_array($dec)) $layout = $dec;
|
||||
}
|
||||
return $this->view('admin/news_builder', ['n' => $n, 'layout' => $layout, 'mode' => $mode]);
|
||||
}
|
||||
return $this->view('admin/news_form', ['n' => $n, 'mode' => $mode]);
|
||||
}
|
||||
|
||||
/** 切换编辑模式(固定版面 <-> 可视化编辑),仅更新 mode 字段 */
|
||||
public function switchMode($id)
|
||||
{
|
||||
$n = (new News())->find($id);
|
||||
if (!$n) { $this->redirect('admin/news'); }
|
||||
return $this->view('admin/news_form', ['n' => $n]);
|
||||
$target = ($this->get('mode') === 'builder') ? 'builder' : 'fixed';
|
||||
(new News())->update($id, ['mode' => $target]);
|
||||
$this->redirect('admin/news/edit/' . $id);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
@@ -54,20 +84,32 @@ class NewsController extends AdminController
|
||||
$news = new News();
|
||||
$n = $news->find($id);
|
||||
if (!$n) { $this->redirect('admin/news'); }
|
||||
$mode = $this->post('mode') === 'builder' ? 'builder' : 'fixed';
|
||||
|
||||
$cover = $this->uploadFile('cover');
|
||||
if (!$cover && $this->post('cover_url')) $cover = $this->post('cover_url');
|
||||
if (!$cover) $cover = $n['cover'] ?? '';
|
||||
$news->update($id, [
|
||||
|
||||
$data = [
|
||||
'title' => $this->post('title'),
|
||||
'slug' => $this->post('slug') ? slugify($this->post('slug')) : (string)$id,
|
||||
'cover' => $cover,
|
||||
'summary' => $this->post('summary'),
|
||||
'content' => $this->post('content'),
|
||||
'author' => $this->post('author', '酷冰甲'),
|
||||
'published_at' => $this->post('published_at', date('Y-m-d')),
|
||||
'status' => $this->post('status', 1) ? 1 : 0,
|
||||
'layout' => $this->post('layout', ''),
|
||||
]);
|
||||
'mode' => $mode,
|
||||
];
|
||||
|
||||
if ($mode === 'fixed') {
|
||||
$data['content'] = $this->post('content', '');
|
||||
} else {
|
||||
$layout = $this->post('layout', '');
|
||||
if ($layout !== '' && !is_array(json_decode($layout, true))) { $layout = ''; }
|
||||
$data['layout'] = $layout;
|
||||
}
|
||||
|
||||
$news->update($id, $data);
|
||||
$this->redirect('admin/news');
|
||||
}
|
||||
|
||||
|
||||
@@ -13,32 +13,56 @@ class PageController extends AdminController
|
||||
]);
|
||||
}
|
||||
|
||||
/** 编辑:按页面 mode 渲染对应编辑器(固定版面 / 可视化编辑) */
|
||||
public function edit($id)
|
||||
{
|
||||
$p = (new Page())->find($id);
|
||||
if (!$p) { $this->redirect('admin/pages'); }
|
||||
$layout = [];
|
||||
if (!empty($p['layout'])) {
|
||||
$dec = json_decode($p['layout'], true);
|
||||
if (is_array($dec)) $layout = $dec;
|
||||
$mode = empty($p['mode']) ? 'fixed' : $p['mode'];
|
||||
if ($mode === 'builder') {
|
||||
$layout = [];
|
||||
if (!empty($p['layout'])) {
|
||||
$dec = json_decode($p['layout'], true);
|
||||
if (is_array($dec)) $layout = $dec;
|
||||
}
|
||||
return $this->view('admin/page_builder', ['p' => $p, 'layout' => $layout, 'mode' => $mode]);
|
||||
}
|
||||
return $this->view('admin/page_builder', ['p' => $p, 'layout' => $layout]);
|
||||
return $this->view('admin/page_form', ['p' => $p, 'mode' => $mode]);
|
||||
}
|
||||
|
||||
/** 切换编辑模式(固定版面 <-> 可视化编辑),仅更新 mode 字段 */
|
||||
public function switchMode($id)
|
||||
{
|
||||
$p = (new Page())->find($id);
|
||||
if (!$p) { $this->redirect('admin/pages'); }
|
||||
$target = ($this->get('mode') === 'builder') ? 'builder' : 'fixed';
|
||||
(new Page())->update($id, ['mode' => $target, 'updated_at' => date('Y-m-d')]);
|
||||
$this->redirect('admin/pages/edit/' . $id);
|
||||
}
|
||||
|
||||
/** 保存:兼容两种编辑器,按实际提交的字段写入(content / layout / mode) */
|
||||
public function update($id)
|
||||
{
|
||||
if (!csrf_check()) { $this->redirect('admin/pages'); }
|
||||
$layout = $this->post('layout', '');
|
||||
// 校验:非空的 layout 必须是合法 JSON 数组
|
||||
if ($layout !== '') {
|
||||
$dec = json_decode($layout, true);
|
||||
if (!is_array($dec)) $layout = '';
|
||||
}
|
||||
(new Page())->update($id, [
|
||||
$data = [
|
||||
'title' => $this->post('title'),
|
||||
'layout' => $layout,
|
||||
'updated_at' => date('Y-m-d'),
|
||||
]);
|
||||
];
|
||||
if ($this->post('content') !== null) {
|
||||
$data['content'] = $this->post('content');
|
||||
}
|
||||
if ($this->post('layout') !== null) {
|
||||
$layout = $this->post('layout', '');
|
||||
if ($layout !== '' && !is_array(json_decode($layout, true))) {
|
||||
$layout = '';
|
||||
}
|
||||
$data['layout'] = $layout;
|
||||
}
|
||||
$mode = $this->post('mode');
|
||||
if ($mode === 'builder' || $mode === 'fixed') {
|
||||
$data['mode'] = $mode;
|
||||
}
|
||||
(new Page())->update($id, $data);
|
||||
$this->redirect('admin/pages');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class ProductController extends AdminController
|
||||
return $this->view('admin/product_form', [
|
||||
'p' => null,
|
||||
'cats' => (new Category())->all(),
|
||||
'mode' => 'fixed',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -41,25 +42,33 @@ class ProductController extends AdminController
|
||||
if (!csrf_check()) { $this->redirect('admin/products'); }
|
||||
$product = new Product();
|
||||
$cover = $this->uploadFile('cover') ?? $this->post('cover_url', '');
|
||||
$mode = $this->post('mode') === 'builder' ? 'builder' : 'fixed';
|
||||
$gallery = ($mode === 'fixed') ? $this->uploadFiles('gallery') : [];
|
||||
$description = ($mode === 'fixed') ? $this->post('description', '') : '';
|
||||
$id = $product->insert([
|
||||
'category_id' => (int)$this->post('category_id'),
|
||||
'name' => $this->post('name'),
|
||||
'slug' => '',
|
||||
'cover' => $cover,
|
||||
'summary' => $this->post('summary'),
|
||||
'description' => $this->post('description'),
|
||||
'description' => $description,
|
||||
'price' => (float)$this->post('price', 0),
|
||||
'specs' => $this->specsToJson($this->post('specs', '')),
|
||||
'gallery' => '[]',
|
||||
'gallery' => json_encode($gallery, JSON_UNESCAPED_UNICODE),
|
||||
'tags' => $this->post('tags', ''),
|
||||
'sort_order' => (int)$this->post('sort_order', 0),
|
||||
'status' => $this->post('status', 1) ? 1 : 0,
|
||||
'created_at' => date('Y-m-d'),
|
||||
'layout' => $this->post('layout', ''),
|
||||
'mode' => $mode,
|
||||
]);
|
||||
// URL 标识留空时按记录序号顺序生成(短、稳定)
|
||||
$slug = $this->post('slug') ? slugify($this->post('slug')) : (string)$id;
|
||||
$product->update($id, ['slug' => $slug]);
|
||||
// 新建时若选择「可视化编辑」,保存后直接进入可视化编辑器排版,无需再手动点编辑
|
||||
if ($mode === 'builder') {
|
||||
$this->redirect('admin/products/edit/' . $id);
|
||||
}
|
||||
$this->redirect('admin/products');
|
||||
}
|
||||
|
||||
@@ -68,39 +77,88 @@ class ProductController extends AdminController
|
||||
$product = new Product();
|
||||
$p = $product->find($id);
|
||||
if (!$p) { $this->redirect('admin/products'); }
|
||||
$mode = empty($p['mode']) ? 'fixed' : $p['mode'];
|
||||
$specs = $product->specsArray($p);
|
||||
$specText = '';
|
||||
foreach ($specs as $s) $specText .= ($s['k'] ?? '') . '|' . ($s['v'] ?? '') . "\n";
|
||||
$cats = (new Category())->all();
|
||||
if ($mode === 'builder') {
|
||||
$layout = [];
|
||||
if (!empty($p['layout'])) {
|
||||
$dec = json_decode($p['layout'], true);
|
||||
if (is_array($dec)) $layout = $dec;
|
||||
}
|
||||
return $this->view('admin/product_builder', [
|
||||
'p' => $p,
|
||||
'cats' => $cats,
|
||||
'specText' => $specText,
|
||||
'layout' => $layout,
|
||||
'mode' => $mode,
|
||||
]);
|
||||
}
|
||||
return $this->view('admin/product_form', [
|
||||
'p' => $p,
|
||||
'cats' => (new Category())->all(),
|
||||
'specText'=> $specText,
|
||||
'p' => $p,
|
||||
'cats' => $cats,
|
||||
'specText' => $specText,
|
||||
'mode' => $mode,
|
||||
]);
|
||||
}
|
||||
|
||||
/** 切换编辑模式(固定版面 <-> 可视化编辑),仅更新 mode 字段,其余数据保留 */
|
||||
public function switchMode($id)
|
||||
{
|
||||
$p = (new Product())->find($id);
|
||||
if (!$p) { $this->redirect('admin/products'); }
|
||||
$target = ($this->get('mode') === 'builder') ? 'builder' : 'fixed';
|
||||
(new Product())->update($id, ['mode' => $target]);
|
||||
$this->redirect('admin/products/edit/' . $id);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
if (!csrf_check()) { $this->redirect('admin/products'); }
|
||||
$product = new Product();
|
||||
$p = $product->find($id);
|
||||
if (!$p) { $this->redirect('admin/products'); }
|
||||
$mode = $this->post('mode') === 'builder' ? 'builder' : 'fixed';
|
||||
|
||||
$cover = $this->uploadFile('cover');
|
||||
if (!$cover && $this->post('cover_url')) $cover = $this->post('cover_url');
|
||||
if (!$cover) $cover = $p['cover'] ?? '';
|
||||
$product->update($id, [
|
||||
|
||||
$data = [
|
||||
'category_id' => (int)$this->post('category_id'),
|
||||
'name' => $this->post('name'),
|
||||
'slug' => $this->post('slug') ? slugify($this->post('slug')) : (string)$id,
|
||||
'cover' => $cover,
|
||||
'summary' => $this->post('summary'),
|
||||
'description' => $this->post('description'),
|
||||
'price' => (float)$this->post('price', 0),
|
||||
'specs' => $this->specsToJson($this->post('specs', '')),
|
||||
'tags' => $this->post('tags', ''),
|
||||
'sort_order' => (int)$this->post('sort_order', 0),
|
||||
'status' => $this->post('status', 1) ? 1 : 0,
|
||||
'layout' => $this->post('layout', ''),
|
||||
]);
|
||||
'mode' => $mode,
|
||||
];
|
||||
|
||||
if ($mode === 'fixed') {
|
||||
// 固定版面:保存图集与详细描述,保留原有 layout 不被覆盖
|
||||
$newGal = $this->uploadFiles('gallery');
|
||||
if (!empty($newGal)) {
|
||||
$data['gallery'] = json_encode($newGal, JSON_UNESCAPED_UNICODE);
|
||||
} elseif ($this->post('clear_gallery')) {
|
||||
$data['gallery'] = '[]';
|
||||
} else {
|
||||
$data['gallery'] = $p['gallery'] ?? '[]';
|
||||
}
|
||||
$data['description'] = $this->post('description', '');
|
||||
} else {
|
||||
// 可视化编辑:更新 layout,保留图集/描述原值
|
||||
$layout = $this->post('layout', '');
|
||||
if ($layout !== '' && !is_array(json_decode($layout, true))) { $layout = ''; }
|
||||
$data['layout'] = $layout;
|
||||
}
|
||||
|
||||
$product->update($id, $data);
|
||||
$this->redirect('admin/products');
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class SettingController extends AdminController
|
||||
];
|
||||
private $siteFields = [
|
||||
'site_name', 'site_slogan', 'contact_phone', 'contact_email',
|
||||
'contact_address', 'icp', 'seo_title', 'seo_keywords', 'seo_description',
|
||||
'contact_address', 'site_logo', 'icp', 'gongan', 'seo_title', 'seo_keywords', 'seo_description',
|
||||
];
|
||||
private $payFields = [
|
||||
'pay_enabled', 'pay_mode',
|
||||
@@ -30,6 +30,18 @@ class SettingController extends AdminController
|
||||
if (csrf_check()) {
|
||||
$pairs = [];
|
||||
foreach ($this->siteFields as $k) $pairs[$k] = $this->post($k, '');
|
||||
// 网站 Logo:优先用上传文件,其次用填写的图片路径/网址;两者皆空则保留现值
|
||||
$logoUp = $this->uploadFile('logo');
|
||||
if ($logoUp !== null) {
|
||||
$pairs['site_logo'] = $logoUp;
|
||||
} else {
|
||||
$url = trim((string) $this->post('logo_url', ''));
|
||||
if ($url !== '') {
|
||||
$pairs['site_logo'] = $url;
|
||||
} else {
|
||||
unset($pairs['site_logo']); // 不覆盖,保留数据库现有值(含默认商标)
|
||||
}
|
||||
}
|
||||
$setting->saveMany($pairs, 'site');
|
||||
}
|
||||
$this->redirect('admin/settings');
|
||||
|
||||
@@ -22,6 +22,25 @@ class HomeController extends Controller
|
||||
'keywords' => '降温服,降温背心,水冷降温服,相变降温服,制冷背心,工业降温服,消防降温服,高温作业防护,降温服定制,酷冰甲',
|
||||
'og_type' => 'website',
|
||||
]);
|
||||
// ── 首页 FAQ(可见文本 + FAQPage JSON-LD,GEO 高杠杆信号)────
|
||||
$faqs = [
|
||||
['q' => '降温服是什么?它是怎么实现降温的?', 'a' => '降温服是一类为高温作业人群设计的主动或被动降温装备,主要通过三种原理散热:水冷循环(微型水泵驱动冷水在服装内管路循环带走体热)、相变蓄冷(冰袋或凝胶相变材料在融化过程中持续吸热)、涡扇风冷(小型风扇强制对流散热)。酷冰甲提供这三大系列,覆盖不同场景与续航需求。'],
|
||||
['q' => '穿降温服体感能降多少度?多久能起效?', 'a' => '在常规高温环境下,合格降温服可让核心体表感温度下降约 8–12℃。水冷与风冷方案接通或开机后数分钟内即可感受到明显凉意;相变冰袋方案放入预冷冰袋后即刻生效,单组冰袋可持续 2–4 小时。'],
|
||||
['q' => '降温服可以重复使用吗?一套能用多久?', 'a' => '可以。酷冰甲降温服主体为可水洗服装,水冷、风冷模块与相变冰袋均可反复使用。服装本体在正常保养下可用 2–3 个高温季,冰袋与电池模块按使用频率约 1–2 年更换即可。'],
|
||||
['q' => '支持企业定制和 LOGO 刺绣吗?起订量多少?', 'a' => '支持。我们提供企业 LOGO 绣字、颜色与面料定制、一人一码量体服务。柔性化生产,10 套起订,确认图纸后 7 天打样、约 28 天批量交付,适合班组、车间等小批量统一配发。'],
|
||||
['q' => '降温服适合哪些行业和场景?', 'a' => '广泛用于消防、钢铁、电力、化工、环卫、建筑、物流及户外军训等高温或暴晒场景,也适用于骑行、垂钓、观赛等个人户外降温。可按行业工况推荐对应系列与续航配置。'],
|
||||
['q' => '降温服怎么清洗和保养?', 'a' => '服装本体可轻柔机洗或手洗,避免浸泡电子模块;水冷、风冷主机与电池需拆下后擦干存放,冰袋用后擦干冷藏。长期不用请置于阴凉干燥处,电池保持半电存放。'],
|
||||
];
|
||||
$faqJsonLd = '<script type="application/ld+json">' . json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'FAQPage',
|
||||
'mainEntity' => array_map(fn($f) => [
|
||||
'@type' => 'Question',
|
||||
'name' => $f['q'],
|
||||
'acceptedAnswer' => ['@type' => 'Answer', 'text' => $f['a']],
|
||||
], $faqs),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '</script>';
|
||||
|
||||
$data = [
|
||||
'pageSeo' => [
|
||||
'title' => $seo['title'],
|
||||
@@ -31,6 +50,7 @@ class HomeController extends Controller
|
||||
'og_image' => $seo['og_image'],
|
||||
'canonical' => $seo['canonical'],
|
||||
'noindex' => $seo['noindex'],
|
||||
'jsonld' => $faqJsonLd,
|
||||
],
|
||||
'banners' => array_filter($banner->all(), fn($b) => ($b['status'] ?? 1) == 1),
|
||||
'categories'=> $category->all(),
|
||||
@@ -56,6 +76,7 @@ class HomeController extends Controller
|
||||
['t' => '批量生产', 'd' => '确认图纸后快速打版、批量生产。'],
|
||||
['t' => '成衣交付', 'd' => '精心包装交付上门,启动售后服务。'],
|
||||
],
|
||||
'faqs' => $faqs,
|
||||
];
|
||||
return $this->view('home/index', $data);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ class ProductController extends Controller
|
||||
|
||||
$pName = e($p['name'] ?? '产品详情');
|
||||
$pSummary = mb_substr(strip_tags($p['summary'] ?? $p['body'] ?? ''), 0, 160);
|
||||
$pImage = $p['image'] ?? '';
|
||||
$pImage = $p['cover'] ?? '';
|
||||
$pPrice = $p['price'] ?? '';
|
||||
$pSku = $p['sku'] ?? ($p['model'] ?? '');
|
||||
|
||||
@@ -92,6 +92,23 @@ class ProductController extends Controller
|
||||
'availability' => 'https://schema.org/InStock',
|
||||
]] : []), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '</script>';
|
||||
|
||||
// ── 产品页 FAQ(可见文本 + FAQPage JSON-LD,GEO 信号)────
|
||||
$faqs = [
|
||||
['q' => '这款降温服采用什么降温原理?', 'a' => '根据系列不同,分别采用水冷循环、相变蓄冷或涡扇风冷原理散热:水冷通过微型水泵驱动冷水循环带走体热,相变依靠冰袋/凝胶融化吸热,风冷由风扇强制对流降温。详情可在商品规格表中查看对应方案。'],
|
||||
['q' => '一次可使用多长时间?', 'a' => '相变冰袋方案单组可持续 2–4 小时,可随用随换;水冷与风冷方案续航取决于电池容量,具体以商品规格为准,支持备用电池延长作业时间。'],
|
||||
['q' => '是否支持企业定制与 LOGO 刺绣?', 'a' => '支持。提供企业 LOGO 绣字、颜色与面料定制、一人一码量体服务,10 套起订,确认图纸后 7 天打样、约 28 天批量交付。'],
|
||||
['q' => '如何选择合适的尺码?', 'a' => '提供标准尺码表并支持上门量体,下单后可按身高体重推荐尺码;特殊体型或工种可单独打版,确保合身与活动便利。'],
|
||||
];
|
||||
$faqSchema = '<script type="application/ld+json">' . json_encode([
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'FAQPage',
|
||||
'mainEntity' => array_map(fn($f) => [
|
||||
'@type' => 'Question',
|
||||
'name' => $f['q'],
|
||||
'acceptedAnswer' => ['@type' => 'Answer', 'text' => $f['a']],
|
||||
], $faqs),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . '</script>';
|
||||
|
||||
return $this->view('product/show', [
|
||||
'pageSeo' => [
|
||||
'title' => $pName,
|
||||
@@ -103,12 +120,13 @@ class ProductController extends Controller
|
||||
['name' => '产品中心', 'url' => site_url('products')],
|
||||
['name' => $p['name'] ?? '产品', 'url' => absolute_url()],
|
||||
],
|
||||
'jsonld' => $productSchema,
|
||||
'jsonld' => $productSchema . $faqSchema,
|
||||
],
|
||||
'p' => $p,
|
||||
'cat' => $cat,
|
||||
'related' => array_slice($related, 0, 3),
|
||||
'specs' => $product->specsArray($p),
|
||||
'faqs' => $faqs,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user