链接管理模块
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\library;
|
||||
|
||||
/**
|
||||
* ISO 3166-1 alpha-2 国家代码与中文名称
|
||||
*/
|
||||
class CountryIso
|
||||
{
|
||||
/**
|
||||
* 常用投放国家 ISO2 => 中文名
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private const COUNTRIES = [
|
||||
'CN' => '中国',
|
||||
'HK' => '中国香港',
|
||||
'MO' => '中国澳门',
|
||||
'TW' => '中国台湾',
|
||||
'US' => '美国',
|
||||
'CA' => '加拿大',
|
||||
'GB' => '英国',
|
||||
'DE' => '德国',
|
||||
'FR' => '法国',
|
||||
'IT' => '意大利',
|
||||
'ES' => '西班牙',
|
||||
'NL' => '荷兰',
|
||||
'BE' => '比利时',
|
||||
'CH' => '瑞士',
|
||||
'AT' => '奥地利',
|
||||
'SE' => '瑞典',
|
||||
'NO' => '挪威',
|
||||
'DK' => '丹麦',
|
||||
'FI' => '芬兰',
|
||||
'IE' => '爱尔兰',
|
||||
'PT' => '葡萄牙',
|
||||
'PL' => '波兰',
|
||||
'CZ' => '捷克',
|
||||
'HU' => '匈牙利',
|
||||
'RO' => '罗马尼亚',
|
||||
'GR' => '希腊',
|
||||
'RU' => '俄罗斯',
|
||||
'UA' => '乌克兰',
|
||||
'TR' => '土耳其',
|
||||
'IL' => '以色列',
|
||||
'SA' => '沙特阿拉伯',
|
||||
'AE' => '阿联酋',
|
||||
'QA' => '卡塔尔',
|
||||
'KW' => '科威特',
|
||||
'IN' => '印度',
|
||||
'PK' => '巴基斯坦',
|
||||
'BD' => '孟加拉国',
|
||||
'TH' => '泰国',
|
||||
'VN' => '越南',
|
||||
'MY' => '马来西亚',
|
||||
'SG' => '新加坡',
|
||||
'ID' => '印度尼西亚',
|
||||
'PH' => '菲律宾',
|
||||
'JP' => '日本',
|
||||
'KR' => '韩国',
|
||||
'AU' => '澳大利亚',
|
||||
'NZ' => '新西兰',
|
||||
'BR' => '巴西',
|
||||
'MX' => '墨西哥',
|
||||
'AR' => '阿根廷',
|
||||
'CL' => '智利',
|
||||
'CO' => '哥伦比亚',
|
||||
'PE' => '秘鲁',
|
||||
'ZA' => '南非',
|
||||
'EG' => '埃及',
|
||||
'NG' => '尼日利亚',
|
||||
'KE' => '肯尼亚',
|
||||
];
|
||||
|
||||
/**
|
||||
* 下拉选项 ISO2 => 中文名
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getOptions(): array
|
||||
{
|
||||
return self::COUNTRIES;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验是否为合法 ISO2 代码
|
||||
*/
|
||||
public static function isValidCode(string $code): bool
|
||||
{
|
||||
$code = strtoupper(trim($code));
|
||||
return isset(self::COUNTRIES[$code]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化并过滤国家代码列表
|
||||
*
|
||||
* @param array<int, string>|string $codes
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function normalizeCodes($codes): array
|
||||
{
|
||||
if (is_string($codes)) {
|
||||
$codes = explode(',', $codes);
|
||||
}
|
||||
if (!is_array($codes)) {
|
||||
return [];
|
||||
}
|
||||
$result = [];
|
||||
foreach ($codes as $code) {
|
||||
$code = strtoupper(trim((string)$code));
|
||||
if ($code !== '' && self::isValidCode($code) && !in_array($code, $result, true)) {
|
||||
$result[] = $code;
|
||||
}
|
||||
}
|
||||
sort($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 逗号分隔 ISO 代码转中文展示
|
||||
*/
|
||||
public static function codesToText(string $codes): string
|
||||
{
|
||||
if ($codes === '') {
|
||||
return '';
|
||||
}
|
||||
$parts = [];
|
||||
foreach (explode(',', $codes) as $code) {
|
||||
$code = strtoupper(trim($code));
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
$parts[] = self::COUNTRIES[$code] ?? $code;
|
||||
}
|
||||
return implode(',', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组转存储字符串
|
||||
*
|
||||
* @param array<int, string> $codes
|
||||
*/
|
||||
public static function codesToStorage(array $codes): string
|
||||
{
|
||||
return implode(',', self::normalizeCodes($codes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 配置模型
|
||||
*/
|
||||
class Config extends Model
|
||||
{
|
||||
|
||||
// 表名,不含前缀
|
||||
protected $name = 'config';
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = false;
|
||||
// 定义时间戳字段名
|
||||
protected $createTime = false;
|
||||
protected $updateTime = false;
|
||||
// 追加属性
|
||||
protected $append = [
|
||||
'extend_html'
|
||||
];
|
||||
protected $type = [
|
||||
'setting' => 'json',
|
||||
];
|
||||
|
||||
/**
|
||||
* 读取配置类型
|
||||
* @return array
|
||||
*/
|
||||
public static function getTypeList()
|
||||
{
|
||||
$typeList = [
|
||||
'string' => __('String'),
|
||||
'password' => __('Password'),
|
||||
'text' => __('Text'),
|
||||
'editor' => __('Editor'),
|
||||
'number' => __('Number'),
|
||||
'date' => __('Date'),
|
||||
'time' => __('Time'),
|
||||
'datetime' => __('Datetime'),
|
||||
'datetimerange' => __('Datetimerange'),
|
||||
'select' => __('Select'),
|
||||
'selects' => __('Selects'),
|
||||
'image' => __('Image'),
|
||||
'images' => __('Images'),
|
||||
'file' => __('File'),
|
||||
'files' => __('Files'),
|
||||
'switch' => __('Switch'),
|
||||
'checkbox' => __('Checkbox'),
|
||||
'radio' => __('Radio'),
|
||||
'city' => __('City'),
|
||||
'selectpage' => __('Selectpage'),
|
||||
'selectpages' => __('Selectpages'),
|
||||
'array' => __('Array'),
|
||||
'custom' => __('Custom'),
|
||||
];
|
||||
return $typeList;
|
||||
}
|
||||
|
||||
public static function getRegexList()
|
||||
{
|
||||
$regexList = [
|
||||
'required' => '必选',
|
||||
'digits' => '数字',
|
||||
'letters' => '字母',
|
||||
'date' => '日期',
|
||||
'time' => '时间',
|
||||
'email' => '邮箱',
|
||||
'url' => '网址',
|
||||
'qq' => 'QQ号',
|
||||
'IDcard' => '身份证',
|
||||
'tel' => '座机电话',
|
||||
'mobile' => '手机号',
|
||||
'zipcode' => '邮编',
|
||||
'chinese' => '中文',
|
||||
'username' => '用户名',
|
||||
'password' => '密码'
|
||||
];
|
||||
return $regexList;
|
||||
}
|
||||
|
||||
public function getExtendHtmlAttr($value, $data)
|
||||
{
|
||||
$result = preg_replace_callback("/\{([a-zA-Z]+)\}/", function ($matches) use ($data) {
|
||||
if (isset($data[$matches[1]])) {
|
||||
return $data[$matches[1]];
|
||||
}
|
||||
}, $data['extend']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取分类分组列表(优先数据库 configgroup,避免 site.php 未同步时分组缺失)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getGroupList()
|
||||
{
|
||||
$groupList = config('site.configgroup');
|
||||
if (!is_array($groupList)) {
|
||||
$groupList = [];
|
||||
}
|
||||
try {
|
||||
$dbJson = \think\Db::name('config')->where('name', 'configgroup')->value('value');
|
||||
$dbList = json_decode((string) $dbJson, true);
|
||||
if (is_array($dbList) && $dbList !== []) {
|
||||
$groupList = $dbList;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 数据库不可用时回退 site.php
|
||||
}
|
||||
foreach ($groupList as $k => &$v) {
|
||||
$v = __($v);
|
||||
}
|
||||
return $groupList;
|
||||
}
|
||||
|
||||
public static function getArrayData($data)
|
||||
{
|
||||
if (!isset($data['value'])) {
|
||||
$result = [];
|
||||
foreach ($data as $index => $datum) {
|
||||
$result['field'][$index] = $datum['key'];
|
||||
$result['value'][$index] = $datum['value'];
|
||||
}
|
||||
$data = $result;
|
||||
}
|
||||
$fieldarr = $valuearr = [];
|
||||
$field = $data['field'] ?? ($data['key'] ?? []);
|
||||
$value = $data['value'] ?? [];
|
||||
foreach ($field as $m => $n) {
|
||||
if ($n != '') {
|
||||
$fieldarr[] = $field[$m];
|
||||
$valuearr[] = $value[$m];
|
||||
}
|
||||
}
|
||||
return $fieldarr ? array_combine($fieldarr, $valuearr) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串解析成键值数组
|
||||
* @param string $text
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($text, $split = "\r\n")
|
||||
{
|
||||
$content = explode($split, $text);
|
||||
$arr = [];
|
||||
foreach ($content as $k => $v) {
|
||||
if (stripos($v, "|") !== false) {
|
||||
$item = explode('|', $v);
|
||||
$arr[$item[0]] = $item[1];
|
||||
}
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将键值数组转换为字符串
|
||||
* @param array $array
|
||||
* @return string
|
||||
*/
|
||||
public static function encode($array, $split = "\r\n")
|
||||
{
|
||||
$content = '';
|
||||
if ($array && is_array($array)) {
|
||||
$arr = [];
|
||||
foreach ($array as $k => $v) {
|
||||
$arr[] = "{$k}|{$v}";
|
||||
}
|
||||
$content = implode($split, $arr);
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地上传配置信息
|
||||
* @return array
|
||||
*/
|
||||
public static function upload()
|
||||
{
|
||||
$uploadcfg = config('upload');
|
||||
|
||||
$uploadurl = request()->module() ? $uploadcfg['uploadurl'] : ($uploadcfg['uploadurl'] === 'ajax/upload' ? 'index/' . $uploadcfg['uploadurl'] : $uploadcfg['uploadurl']);
|
||||
|
||||
if (!preg_match("/^((?:[a-z]+:)?\/\/)(.*)/i", $uploadurl) && substr($uploadurl, 0, 1) !== '/') {
|
||||
$uploadurl = url($uploadurl, '', false);
|
||||
}
|
||||
$uploadcfg['fullmode'] = isset($uploadcfg['fullmode']) && $uploadcfg['fullmode'];
|
||||
$uploadcfg['thumbstyle'] = $uploadcfg['thumbstyle'] ?? '';
|
||||
|
||||
$upload = [
|
||||
'cdnurl' => $uploadcfg['cdnurl'],
|
||||
'uploadurl' => $uploadurl,
|
||||
'bucket' => 'local',
|
||||
'maxsize' => $uploadcfg['maxsize'],
|
||||
'mimetype' => $uploadcfg['mimetype'],
|
||||
'chunking' => $uploadcfg['chunking'],
|
||||
'chunksize' => $uploadcfg['chunksize'],
|
||||
'savekey' => $uploadcfg['savekey'],
|
||||
'multipart' => [],
|
||||
'multiple' => $uploadcfg['multiple'],
|
||||
'fullmode' => $uploadcfg['fullmode'],
|
||||
'thumbstyle' => $uploadcfg['thumbstyle'],
|
||||
'storage' => 'local'
|
||||
];
|
||||
return $upload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新配置文件
|
||||
*/
|
||||
public static function refreshFile()
|
||||
{
|
||||
//如果没有配置权限无法进行修改
|
||||
if (!\app\admin\library\Auth::instance()->check('general/config/edit')) {
|
||||
return false;
|
||||
}
|
||||
$config = [];
|
||||
$configList = self::all();
|
||||
foreach ($configList as $k => $v) {
|
||||
$value = $v->toArray();
|
||||
if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) {
|
||||
$value['value'] = explode(',', $value['value']);
|
||||
}
|
||||
if ($value['type'] == 'array') {
|
||||
$value['value'] = (array)json_decode($value['value'], true);
|
||||
}
|
||||
$config[$value['name']] = $value['value'];
|
||||
}
|
||||
file_put_contents(
|
||||
CONF_PATH . 'extra' . DS . 'site.php',
|
||||
'<?php' . "\n\nreturn " . var_export($config, true) . ";\n"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
/**
|
||||
* 分流链接自动回复语句(一行一条)
|
||||
*/
|
||||
class SplitAutoReplyService
|
||||
{
|
||||
/** 最多条数 */
|
||||
private const MAX_LINES = 200;
|
||||
|
||||
/** 单条最大字符数 */
|
||||
private const MAX_LINE_LENGTH = 500;
|
||||
|
||||
/** 列表预览最大字符数 */
|
||||
private const LIST_PREVIEW_MAX = 20;
|
||||
|
||||
/**
|
||||
* 列表单元格预览(单行最多 50 字,超出加 ...)
|
||||
*/
|
||||
public static function previewForList(string $raw, int $max = self::LIST_PREVIEW_MAX): string
|
||||
{
|
||||
$display = self::formatDisplay($raw);
|
||||
if ($display === '') {
|
||||
return '';
|
||||
}
|
||||
$flat = preg_replace('/\s+/u', ' ', str_replace(["\r\n", "\r", "\n"], ' ', $display));
|
||||
$flat = trim((string) $flat);
|
||||
if ($flat === '') {
|
||||
return '';
|
||||
}
|
||||
if (mb_strlen($flat, 'UTF-8') <= $max) {
|
||||
return $flat;
|
||||
}
|
||||
return mb_substr($flat, 0, $max, 'UTF-8') . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析为多行文本数组
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function parseLines(string $raw): array
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
$parts = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$lines = [];
|
||||
foreach ($parts as $part) {
|
||||
$line = trim((string) $part);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
if (strlen($line) > self::MAX_LINE_LENGTH) {
|
||||
$line = mb_substr($line, 0, self::MAX_LINE_LENGTH, 'UTF-8');
|
||||
}
|
||||
$lines[] = $line;
|
||||
if (count($lines) >= self::MAX_LINES) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为数据库存储(换行分隔)
|
||||
*
|
||||
* @param string|array<int, string> $value
|
||||
*/
|
||||
public static function formatStorage($value): string
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$lines = $value;
|
||||
} else {
|
||||
$lines = self::parseLines((string) $value);
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 供表单 textarea 回显
|
||||
*/
|
||||
public static function formatDisplay(string $stored): string
|
||||
{
|
||||
$lines = self::parseLines($stored);
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\admin\model\split\Link;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 分流链接码生成服务
|
||||
*/
|
||||
class SplitLinkCodeService
|
||||
{
|
||||
private const CODE_LENGTH = 9;
|
||||
|
||||
private const CHARSET = 'abcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
private const MAX_ATTEMPTS = 50;
|
||||
|
||||
/**
|
||||
* 规范化链接码(小写、去空格)
|
||||
*/
|
||||
public static function normalize(string $code): string
|
||||
{
|
||||
return strtolower(trim($code));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为合法 9 位小写字母链接码
|
||||
*/
|
||||
public static function isValidFormat(string $code): bool
|
||||
{
|
||||
return (bool) preg_match('/^[a-z]{9}$/', $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 链接码是否未被占用
|
||||
*/
|
||||
public static function isAvailable(string $code, int $excludeId = 0): bool
|
||||
{
|
||||
$code = self::normalize($code);
|
||||
$query = Link::where('link_code', $code);
|
||||
if ($excludeId > 0) {
|
||||
$query->where('id', '<>', $excludeId);
|
||||
}
|
||||
return !$query->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一 9 位小写字母链接码
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function generateUnique(): string
|
||||
{
|
||||
for ($i = 0; $i < self::MAX_ATTEMPTS; $i++) {
|
||||
$code = $this->generateRandom();
|
||||
if (!Link::where('link_code', $code)->find()) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
throw new Exception('分流链接生成失败,请稍后重试');
|
||||
}
|
||||
|
||||
private function generateRandom(): string
|
||||
{
|
||||
$chars = self::CHARSET;
|
||||
$max = strlen($chars) - 1;
|
||||
$code = '';
|
||||
for ($i = 0; $i < self::CODE_LENGTH; $i++) {
|
||||
$code .= $chars[random_int(0, $max)];
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\admin\model\Domain as DomainModel;
|
||||
|
||||
/**
|
||||
* 平台分配域名解析(支持多域名,一行一个)
|
||||
*/
|
||||
class SplitPlatformDomainService
|
||||
{
|
||||
/**
|
||||
* 解析配置文本为合法根域名列表(去重、小写)
|
||||
*
|
||||
* @param string $raw 换行或逗号分隔的域名文本
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function parseList(string $raw): array
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parts = preg_split('/[\r\n,]+/', $raw) ?: [];
|
||||
$domains = [];
|
||||
foreach ($parts as $part) {
|
||||
$domain = DomainModel::normalizeDomain(trim((string) $part));
|
||||
if ($domain === '' || isset($domains[$domain])) {
|
||||
continue;
|
||||
}
|
||||
if (!DomainModel::isValidRootDomain($domain)) {
|
||||
continue;
|
||||
}
|
||||
$domains[$domain] = $domain;
|
||||
}
|
||||
|
||||
return array_values($domains);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将域名列表格式化为配置存储文本(一行一个)
|
||||
*
|
||||
* @param array<int, string> $domains
|
||||
*/
|
||||
public static function formatList(array $domains): string
|
||||
{
|
||||
$lines = [];
|
||||
foreach ($domains as $domain) {
|
||||
$domain = DomainModel::normalizeDomain((string) $domain);
|
||||
if ($domain !== '' && DomainModel::isValidRootDomain($domain)) {
|
||||
$lines[$domain] = $domain;
|
||||
}
|
||||
}
|
||||
return implode("\n", array_values($lines));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user