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 */ 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(); } /** * 输出后台列表/表单 JS(patches 未部署到 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); } }