Files
links/patches/application/admin/controller/split/Link.php
T

421 lines
14 KiB
PHP
Raw Normal View History

2026-06-03 12:10:25 +08:00
<?php
declare(strict_types=1);
namespace app\admin\controller\split;
use app\admin\model\Domain as DomainModel;
use app\common\controller\Backend;
use app\common\library\CountryIso;
use app\common\service\SplitAutoReplyService;
use app\common\service\SplitLinkCodeService;
2026-06-05 04:22:29 +08:00
use app\common\service\SplitPixelConfigService;
2026-06-03 12:10:25 +08:00
use app\common\service\SplitPlatformDomainService;
use think\Db;
use think\Exception;
use think\exception\DbException;
use think\exception\PDOException;
use think\exception\ValidateException;
use think\response\Json;
/**
* 链接管理
*
* @icon fa fa-link
* @remark 分流链接管理,自动生成9位字母链接码
*/
class Link extends Backend
{
/** @var \app\admin\model\split\Link */
protected $model = null;
protected $searchFields = 'link_code,description';
protected $dataLimit = 'personal';
protected $modelValidate = true;
protected $modelSceneValidate = true;
/** @var string[] 无需鉴权的操作 */
protected $noNeedRight = ['script', 'copyinfo'];
/** @var string patches 视图目录(未完整部署时使用) */
private const PATCH_VIEW_DIR = 'patches/application/admin/view/split/link/';
public function _initialize()
{
parent::_initialize();
$lang = $this->request->langset();
$lang = preg_match('/^([a-zA-Z\-_]{2,10})$/i', $lang) ? $lang : 'zh-cn';
$langFile = ROOT_PATH . 'patches/application/admin/lang/' . $lang . '/split/link.php';
if (is_file($langFile)) {
\think\Lang::load($langFile);
}
$this->model = new \app\admin\model\split\Link();
$countryList = CountryIso::getOptions();
$this->view->assign('countryList', $countryList);
$this->view->assign('ipProtectList', $this->model->getIpProtectList());
$this->view->assign('randomShuffleList', $this->model->getRandomShuffleList());
$this->view->assign('statusList', $this->model->getStatusList());
$this->assignconfig('countryList', $countryList);
$this->assignconfig('ipProtectList', $this->model->getIpProtectList());
$this->assignconfig('randomShuffleList', $this->model->getRandomShuffleList());
$this->assignconfig('statusList', $this->model->getStatusList());
$this->setupPatchFrontend();
}
/**
* 未部署 JS 到 public 时,或 patches 版本较新时,jsname 指向 script 接口
*/
private function setupPatchFrontend(): void
{
$patchJs = ROOT_PATH . 'patches/public/assets/js/backend/split/link.js';
$publicJs = ROOT_PATH . 'public/assets/js/backend/split/link.js';
$usePatchJs = is_file($patchJs) && (
!is_file($publicJs) || filemtime($patchJs) >= filemtime($publicJs)
);
if (!$usePatchJs) {
return;
}
$cfg = is_array($this->view->config ?? null) ? $this->view->config : [];
$version = (string) \think\Config::get('site.version');
$scriptUrl = (string) url('split.link/script', ['v' => $version], false, true);
if (strpos($scriptUrl, '?') === false) {
$scriptUrl .= '?v=' . $version;
}
if (strpos($scriptUrl, '://') === false) {
$scriptUrl = $this->request->domain() . $scriptUrl;
}
$cfg['jsname'] = $scriptUrl;
$this->view->assign('config', $cfg);
$this->view->config = $cfg;
}
/**
* 渲染模板(优先 patches 视图,回退 application
*/
private function fetchPatch(string $template): string
{
$patchFile = ROOT_PATH . self::PATCH_VIEW_DIR . $template . '.html';
$appFile = APP_PATH . 'admin/view/split/link/' . $template . '.html';
if (is_file($patchFile)) {
$file = $patchFile;
} elseif (is_file($appFile)) {
$file = $appFile;
} else {
$this->error('模板文件不存在');
}
return (string) $this->view->fetch($file);
}
/**
* 列表
*
* @return string|Json
* @throws DbException
* @throws \think\Exception
*/
public function index()
{
$this->request->filter(['strip_tags', 'trim']);
if (false === $this->request->isAjax()) {
return $this->fetchPatch('index');
}
if ($this->request->request('keyField')) {
return $this->selectpage();
}
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
$list = $this->model
->where($where)
->order($sort, $order)
->paginate($limit);
$result = ['total' => $list->total(), 'rows' => $list->items()];
return json($result);
}
/**
* 添加(打开表单时预生成分流链接,可编辑后保存)
*/
public function add()
{
if (false === $this->request->isPost()) {
try {
$defaultLinkCode = (new SplitLinkCodeService())->generateUnique();
} catch (Exception $e) {
$this->error($e->getMessage());
}
$this->view->assign('defaultLinkCode', $defaultLinkCode);
return $this->fetchPatch('add');
}
$params = $this->request->post('row/a', []);
if ($params === []) {
$this->error(__('Parameter %s can not be empty', ''));
}
$params = $this->preExcludeFields($params);
if (isset($params['link_code'])) {
$params['link_code'] = SplitLinkCodeService::normalize((string) $params['link_code']);
}
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);
}
$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();
}
/**
* 复制分流链接弹窗所需数据(平台域名 + 我的域名列表)
*
* @return Json
*/
public function copyinfo(): Json
{
$platformDomains = $this->getPlatformDomains();
$domainQuery = (new DomainModel())->order('id', 'desc');
if ($this->dataLimit) {
$adminIds = $this->getDataLimitAdminIds();
if (is_array($adminIds)) {
$domainQuery->where($this->dataLimitField, 'in', $adminIds);
}
}
$myDomains = [];
foreach ($domainQuery->select() as $domainRow) {
$myDomains[] = [
'id' => (int) $domainRow['id'],
'domain' => (string) $domainRow['domain'],
'ns_status' => (string) $domainRow['ns_status'],
'ns_status_text' => (string) $domainRow['ns_status_text'],
'dns_status' => (string) $domainRow['dns_status'],
'dns_status_text' => (string) $domainRow['dns_status_text'],
];
}
$this->success('', null, [
'platform_domains' => $platformDomains,
'my_domains' => $myDomains,
2026-06-05 04:22:29 +08:00
'domain_index_url' => (string) url('domain', '', false, false),
'domain_add_url' => (string) url('domain/add', '', false, false),
2026-06-03 12:10:25 +08:00
'config_index_url' => (string) url('general/config/index'),
]);
}
/**
* 读取平台分配域名列表(优先 site 配置,回退数据库)
*
* @return array<int, string>
*/
private function getPlatformDomains(): array
{
$raw = trim((string) \think\Config::get('site.split_platform_domain'));
if ($raw === '') {
$raw = trim((string) Db::name('config')->where('name', 'split_platform_domain')->value('value'));
}
return SplitPlatformDomainService::parseList($raw);
}
/**
* 自动回复:读取 / 保存与当前链接关联的回复语句
*
* @param string|null $ids 链接 ID
* @return Json
* @throws DbException
*/
public function autoreply($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()) {
$autoReply = (string) $this->request->post('auto_reply', '');
$lines = SplitAutoReplyService::parseLines($autoReply);
try {
$row->save([
'auto_reply' => SplitAutoReplyService::formatStorage($lines),
]);
} catch (PDOException|Exception $e) {
$this->error($e->getMessage());
}
$this->success(__('Auto reply saved'));
}
$this->success('', null, [
'id' => (int) $row['id'],
'link_code' => (string) $row['link_code'],
'auto_reply' => SplitAutoReplyService::formatDisplay((string) $row->getAttr('auto_reply')),
'line_count' => count($row->getAutoReplyLines()),
]);
}
2026-06-05 04:22:29 +08:00
/**
* 像素配置:读取 / 保存
*
* @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,
]);
}
2026-06-03 12:10:25 +08:00
/**
* 编辑(分流链接码不可修改)
*
* @param string|null $ids
* @return string
* @throws DbException
*/
public function edit($ids = null)
{
$row = $this->model->get($ids);
if (!$row) {
$this->error(__('No Results were found'));
}
$adminIds = $this->getDataLimitAdminIds();
if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) {
$this->error(__('You have no permission'));
}
if (false === $this->request->isPost()) {
$this->view->assign('row', $row);
$this->view->assign('selectedCountries', $row->getSelectedCountries());
return $this->fetchPatch('edit');
}
$params = $this->request->post('row/a', []);
if ($params === []) {
$this->error(__('Parameter %s can not be empty', ''));
}
$params = $this->preExcludeFields($params);
unset($params['link_code']);
$result = false;
Db::startTrans();
try {
if ($this->modelValidate) {
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
$validate = $this->modelSceneValidate ? $name . '.edit' : $name;
$row->validateFailException()->validate($validate, $params);
}
$result = $row->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 updated'));
}
$this->success();
}
/**
* 输出后台列表/表单 JSpatches 未部署到 public 时由 RequireJS 加载)
*/
public function script(): void
{
$jsFile = ROOT_PATH . 'patches/public/assets/js/backend/split/link.js';
if (!is_file($jsFile)) {
$jsFile = ROOT_PATH . 'public/assets/js/backend/split/link.js';
}
if (!is_file($jsFile)) {
$this->error('脚本文件不存在');
}
$content = file_get_contents($jsFile);
if ($content === false) {
$this->error('读取脚本失败');
}
$response = response($content, 200, [
'Content-Type' => 'application/javascript; charset=utf-8',
'Cache-Control' => 'public, max-age=3600',
]);
throw new \think\exception\HttpResponseException($response);
}
}