前端分流页功能

This commit is contained in:
root
2026-06-05 04:22:29 +08:00
parent 8afe25a960
commit 34d76cce74
120 changed files with 10782 additions and 284 deletions
@@ -0,0 +1,12 @@
-- 分流链接:像素配置 JSON 字段 + 权限节点
SET NAMES utf8mb4;
ALTER TABLE `fa_split_link`
ADD COLUMN `pixel_config` mediumtext COMMENT '像素配置JSON' AFTER `auto_reply`;
INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`)
SELECT 'file', m.id, 'split.link/pixel', '像素配置', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal'
FROM `fa_auth_rule` m
WHERE m.name = 'split.link' AND m.ismenu = 1
AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'split.link/pixel' LIMIT 1)
LIMIT 1;
@@ -0,0 +1,4 @@
-- 移除分流模块下重复的「管理我的域名」菜单(统一使用顶级「域名管理」)
SET NAMES utf8mb4;
DELETE FROM `fa_auth_rule` WHERE `name` LIKE 'split.mydomain%';
@@ -1,247 +0,0 @@
<?php
declare(strict_types=1);
namespace app\admin\controller;
use app\admin\model\Domain as DomainModel;
use app\common\controller\Backend;
use app\common\service\CloudflareService;
use app\common\service\DomainDetectRateLimitService;
use think\Db;
use think\Exception;
use think\exception\PDOException;
use think\exception\ValidateException;
/**
* 域名管理
*
* @icon fa fa-globe
* @remark 域名接入Cloudflare,新增后请在详情查看NS并到注册商修改
*/
class Domain extends Backend
{
/** @var DomainModel */
protected $model = null;
protected $searchFields = 'domain';
protected $dataLimit = 'personal';
protected $modelValidate = true;
protected $modelSceneValidate = true;
public function _initialize()
{
parent::_initialize();
$this->model = new DomainModel();
$this->view->assign('zoneStatusList', $this->model->getZoneStatusList());
$this->view->assign('nsStatusList', $this->model->getNsStatusList());
$this->view->assign('dnsStatusList', $this->model->getDnsStatusList());
$this->assignconfig('zoneStatusList', $this->model->getZoneStatusList());
$this->assignconfig('nsStatusList', $this->model->getNsStatusList());
$this->assignconfig('dnsStatusList', $this->model->getDnsStatusList());
}
/**
* 禁止编辑
*/
public function edit($ids = null)
{
$this->error(__('Domain cannot be edited after creation'));
}
/**
* 禁止批量操作
*/
public function multi($ids = '')
{
$this->error(__('Invalid parameters'));
}
/**
* 添加域名并创建 Cloudflare Zone
*/
public function add()
{
if (false === $this->request->isPost()) {
return $this->view->fetch();
}
$params = $this->request->post('row/a', []);
if ($params === []) {
$this->error(__('Parameter %s can not be empty', ''));
}
$params = $this->preExcludeFields($params);
$domain = DomainModel::normalizeDomain((string)($params['domain'] ?? ''));
$params['domain'] = $domain;
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
$params[$this->dataLimitField] = $this->auth->id;
}
$result = false;
Db::startTrans();
try {
if ($this->modelValidate) {
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
$validate = $this->modelSceneValidate ? $name . '.add' : $name;
$this->model->validateFailException()->validate($validate, $params);
}
if (DomainModel::where('domain', $domain)->find()) {
throw new ValidateException(__('Domain already exists'));
}
$cloudflare = new CloudflareService();
$zone = $cloudflare->createZone($domain);
$params['full_url'] = 'https://' . $domain;
$params['zone_id'] = $zone['zone_id'];
$params['nameservers'] = $zone['name_servers'];
$params['zone_status'] = DomainModel::mapCloudflareZoneStatus($zone['status']);
$params['ns_status'] = 'pending';
$params['dns_status'] = 'pending';
$params['check_result'] = '';
$result = $this->model->allowField(true)->save($params);
Db::commit();
} catch (ValidateException $e) {
Db::rollback();
$this->error($e->getMessage());
} catch (PDOException|Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($result === false) {
$this->error(__('No rows were inserted'));
}
$this->success(__('Domain submitted, please check NS in detail page and update at registrar'));
}
/**
* 域名详情
*/
public function detail($ids = null)
{
$row = $this->model->get($ids);
if (!$row) {
$this->error(__('No Results were found'));
}
if (!$this->checkDataLimit($row)) {
$this->error(__('You have no permission'));
}
$data = $row->toArray();
$data['nameservers_list'] = $row->getNameserversArray();
$this->view->assign('row', $data);
return $this->view->fetch();
}
/**
* 检测域名状态
*/
public function detect($ids = null)
{
if (!$this->request->isPost()) {
$this->error(__('Invalid parameters'));
}
$row = $this->model->get($ids);
if (!$row) {
$this->error(__('No Results were found'));
}
if (!$this->checkDataLimit($row)) {
$this->error(__('You have no permission'));
}
try {
(new DomainDetectRateLimitService())->assertCanDetect((int)$this->auth->id, (int)$row['id']);
} catch (Exception $e) {
$this->error($e->getMessage());
}
try {
$cloudflare = new CloudflareService();
$detect = $cloudflare->detectDomain($row->toArray());
$row->save([
'zone_status' => $detect['zone_status'],
'ns_status' => $detect['ns_status'],
'dns_status' => $detect['dns_status'],
'check_time' => time(),
'check_result' => $detect['check_result'],
]);
} catch (Exception $e) {
$this->error($e->getMessage());
}
$this->success(__('Detection completed'), null, $row->toArray());
}
/**
* 删除(需输入完整域名二次确认)
*/
public function del($ids = '')
{
if (!$this->request->isPost()) {
$this->error(__('Invalid parameters'));
}
$ids = $ids ?: $this->request->post('ids', '');
$confirmDomain = trim((string)$this->request->post('confirm_domain', ''));
if ($ids === '' || $confirmDomain === '') {
$this->error(__('Please enter the full domain to confirm deletion'));
}
$pk = $this->model->getPk();
$list = $this->model->where($pk, 'in', $ids)->select();
if (!$list || count($list) === 0) {
$this->error(__('No Results were found'));
}
$count = 0;
Db::startTrans();
try {
foreach ($list as $item) {
if (!$this->checkDataLimit($item)) {
throw new Exception(__('You have no permission'));
}
if (DomainModel::normalizeDomain($confirmDomain) !== DomainModel::normalizeDomain((string)$item['domain'])) {
throw new Exception(__('Domain confirmation does not match'));
}
if (!empty($item['zone_id'])) {
$cloudflare = new CloudflareService();
$cloudflare->deleteZone((string)$item['zone_id']);
}
$count += $item->delete();
}
Db::commit();
} catch (PDOException|Exception $e) {
Db::rollback();
$this->error($e->getMessage());
}
if ($count > 0) {
$this->success();
}
$this->error(__('No rows were deleted'));
}
/**
* @param DomainModel $row
*/
protected function checkDataLimit($row): bool
{
if (!$this->dataLimit) {
return true;
}
$adminIds = $this->getDataLimitAdminIds();
if (!is_array($adminIds)) {
return true;
}
return in_array((int)$row[$this->dataLimitField], $adminIds, true);
}
}
@@ -9,6 +9,7 @@ use app\common\controller\Backend;
use app\common\library\CountryIso;
use app\common\service\SplitAutoReplyService;
use app\common\service\SplitLinkCodeService;
use app\common\service\SplitPixelConfigService;
use app\common\service\SplitPlatformDomainService;
use think\Db;
use think\Exception;
@@ -224,8 +225,8 @@ class Link extends Backend
$this->success('', null, [
'platform_domains' => $platformDomains,
'my_domains' => $myDomains,
'domain_index_url' => (string) url('domain/index'),
'domain_add_url' => (string) url('domain/add'),
'domain_index_url' => (string) url('domain', '', false, false),
'domain_add_url' => (string) url('domain/add', '', false, false),
'config_index_url' => (string) url('general/config/index'),
]);
}
@@ -284,6 +285,58 @@ class Link extends Backend
]);
}
/**
* 像素配置:读取 / 保存
*
* @param string|null $ids 链接 ID
* @return Json
* @throws DbException
*/
public function pixel($ids = null): Json
{
$row = $this->model->get($ids);
if (!$row) {
$this->error(__('No Results were found'));
}
$adminIds = $this->getDataLimitAdminIds();
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
$this->error(__('You have no permission'));
}
if ($this->request->isPost()) {
$payload = $this->request->post('pixel_config/a', []);
if ($payload === []) {
$raw = (string) $this->request->post('pixel_config', '');
$decoded = json_decode($raw, true);
$payload = is_array($decoded) ? $decoded : [];
}
$existing = SplitPixelConfigService::parseStorage((string) $row->getAttr('pixel_config'));
try {
$merged = SplitPixelConfigService::mergeForSave($payload, $existing);
$row->save([
'pixel_config' => SplitPixelConfigService::encodeStorage($merged),
]);
} catch (\InvalidArgumentException $e) {
$this->error($e->getMessage());
} catch (PDOException|Exception $e) {
$this->error($e->getMessage());
}
$this->success(__('Pixel config saved'));
}
$config = SplitPixelConfigService::parseStorage((string) $row->getAttr('pixel_config'));
$masked = SplitPixelConfigService::maskForAdmin($config);
$this->success('', null, [
'id' => (int) $row['id'],
'link_code' => (string) $row['link_code'],
'facebook' => $masked[SplitPixelConfigService::PLATFORM_FACEBOOK],
'tiktok' => $masked[SplitPixelConfigService::PLATFORM_TIKTOK],
'event_options' => SplitPixelConfigService::EVENT_OPTIONS,
]);
}
/**
* 编辑(分流链接码不可修改)
*
@@ -18,6 +18,7 @@ return [
'Status hidden' => '停用',
'Copy split link' => '复制分流链接',
'Manage my domains' => '管理我的域名',
'Domain management' => '域名管理',
'Select main domain' => '选择主域名',
'Platform assigned domain' => '平台分配域名',
'My domains' => '我的域名',
@@ -41,4 +42,24 @@ return [
'Reply statements column' => '回复语',
'NS' => 'NS',
'DNS' => 'DNS',
'Pixel config' => '像素配置',
'Pixel config tip' => '浏览器 Pixel 会在分流中转页触发;服务端回传需要填写对应平台 Access TokenToken 留空会保留原配置。',
'Facebook Pixel' => 'Facebook Pixel',
'TikTok Pixel' => 'TikTok Pixel',
'Add FB Pixel' => '+ 添加FB Pixel',
'Add TK Pixel' => '+ 添加TK Pixel',
'Pixel enabled' => '启用',
'Server postback' => '服务端回传',
'Pixel ID' => 'Pixel ID',
'Access Token' => 'Access Token',
'Test code' => '测试码',
'Pixel event' => '事件',
'Pixel sort' => '排序',
'Pixel config saved' => '像素配置已保存',
'Pixel ID required' => '请填写 Pixel ID',
'Remove row' => '删除',
'Optional' => '选填',
'Pixel list empty' => '暂无配置,请点击上方按钮添加',
'Pixel row index' => '序号',
'Operate' => '操作',
];