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

416 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
use app\common\service\SplitPixelConfigService;
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');
// 使用站内相对路径,避免生产环境 domain() 与反向代理/HTTPS 不一致导致 JS 跨域丢 Cookie
$scriptUrl = (string) url('split.link/script', ['v' => $version], '', false);
$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,
'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'),
]);
}
/**
* 读取平台分配域名列表(优先 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()),
]);
}
/**
* 像素配置:读取 / 保存
*
* @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,
]);
}
/**
* 编辑(分流链接码不可修改)
*
* @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);
}
}