更新
This commit is contained in:
@@ -173,6 +173,53 @@ class MediaChannelService
|
||||
$query->whereRaw('(' . implode(' OR ', $segments) . ')', $bindings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter a fact table by its external_userid without joining the denormalized
|
||||
* contact rows. The contact table may contain several rows for one customer;
|
||||
* a normal JOIN therefore both scans follow_users TEXT repeatedly and
|
||||
* multiplies facts. Enterprise tag channels use the normalized relation
|
||||
* table, while legacy name-only channels keep a deduplicated JSON fallback.
|
||||
*
|
||||
* @param array<string, mixed>|null $channel
|
||||
*/
|
||||
public static function applyExternalUserChannelFilter(Query $query, string $field, ?array $channel): void
|
||||
{
|
||||
if ($channel === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tagId = trim((string) ($channel['source_tag_id'] ?? ''));
|
||||
if ($tagId !== '') {
|
||||
$tagTable = self::tableWithPrefix('qywx_external_contact_tag');
|
||||
$query->whereRaw(
|
||||
"{$field} IN (SELECT channel_tag.external_userid FROM {$tagTable} channel_tag WHERE channel_tag.tag_id = ?)",
|
||||
[$tagId]
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$patterns = self::buildLikePatterns($channel);
|
||||
if ($patterns === []) {
|
||||
$query->whereRaw('1 = 0');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$segments = [];
|
||||
$bindings = [];
|
||||
foreach ($patterns as $pattern) {
|
||||
$segments[] = 'channel_contact.follow_users LIKE ?';
|
||||
$bindings[] = $pattern;
|
||||
}
|
||||
$contactTable = self::tableWithPrefix('qywx_external_contact');
|
||||
$query->whereRaw(
|
||||
"{$field} IN (SELECT channel_contact.external_userid FROM {$contactTable} channel_contact"
|
||||
. ' WHERE channel_contact.delete_time IS NULL AND (' . implode(' OR ', $segments) . '))',
|
||||
$bindings
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{scanned_contacts: int, discovered_tags: int, inserted_or_updated: int}
|
||||
*/
|
||||
@@ -275,6 +322,13 @@ class MediaChannelService
|
||||
return array_values(array_unique($patterns));
|
||||
}
|
||||
|
||||
private static function tableWithPrefix(string $table): string
|
||||
{
|
||||
$prefix = (string) (Db::getConfig('connections.mysql.prefix') ?: 'zyt_');
|
||||
|
||||
return $prefix . $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $followUsers
|
||||
* @return array<int, array{source_tag_id: string, source_tag_name: string, source_group_name: string}>
|
||||
|
||||
@@ -9,6 +9,30 @@ use think\facade\Db;
|
||||
/** 公开获客助手链接分流:按权重随机,并在事务内维护当日限额与点击计数。 */
|
||||
class QywxPromotionRedirectService
|
||||
{
|
||||
/** @return array{status:int,widget_config_json:?string}|null */
|
||||
public static function publicPoolConfig(string $publicKey): ?array
|
||||
{
|
||||
if (preg_match('/^[a-f0-9]{32}$/', $publicKey) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = Db::name('qywx_promotion_pool')
|
||||
->where('public_key', $publicKey)
|
||||
->whereNull('delete_time')
|
||||
->field('status,widget_config_json')
|
||||
->find();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => (int) ($row['status'] ?? 0),
|
||||
'widget_config_json' => isset($row['widget_config_json'])
|
||||
? (string) $row['widget_config_json']
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{url:string,link_id:int}|null */
|
||||
public static function pick(string $publicKey, array $context = []): ?array
|
||||
{
|
||||
@@ -76,8 +100,7 @@ class QywxPromotionRedirectService
|
||||
|
||||
public static function poolExists(string $publicKey): bool
|
||||
{
|
||||
return preg_match('/^[a-f0-9]{32}$/', $publicKey) === 1
|
||||
&& Db::name('qywx_promotion_pool')->where('public_key', $publicKey)->whereNull('delete_time')->count() > 0;
|
||||
return self::publicPoolConfig($publicKey) !== null;
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $links */
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\qywx;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* 获客助手公开浮窗配置与脚本。
|
||||
*
|
||||
* 管理端输入严格校验;数据库中的未知版本或损坏配置一律回退为关闭状态。
|
||||
*/
|
||||
class QywxPromotionWidgetService
|
||||
{
|
||||
private const VERSION = 1;
|
||||
|
||||
private const TEMPLATES = ['bubble', 'pill', 'card', 'message', 'edge', 'bar'];
|
||||
|
||||
private const POSITIONS = ['bottom-right', 'bottom-left'];
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function defaults(): array
|
||||
{
|
||||
return [
|
||||
'v' => self::VERSION,
|
||||
'enabled' => false,
|
||||
'template' => 'bubble',
|
||||
'position' => 'bottom-right',
|
||||
'title' => '专属顾问在线',
|
||||
'subtitle' => '点击添加企业微信,获取一对一服务',
|
||||
'button_text' => '立即咨询',
|
||||
'primary_color' => '#139A8C',
|
||||
'bottom_offset' => 28,
|
||||
'show_mobile' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function fromInput(mixed $input): array
|
||||
{
|
||||
if (!is_array($input)) {
|
||||
throw new InvalidArgumentException('浮窗配置格式无效');
|
||||
}
|
||||
|
||||
$defaults = self::defaults();
|
||||
$version = self::integerValue(self::inputValue($input, 'v', self::VERSION), '配置版本');
|
||||
if ($version !== self::VERSION) {
|
||||
throw new InvalidArgumentException('不支持的浮窗配置版本');
|
||||
}
|
||||
|
||||
$template = self::textValue(self::inputValue($input, 'template', $defaults['template']), '模板', 1, 20);
|
||||
if (!in_array($template, self::TEMPLATES, true)) {
|
||||
throw new InvalidArgumentException('浮窗模板无效');
|
||||
}
|
||||
|
||||
$position = self::textValue(self::inputValue($input, 'position', $defaults['position']), '位置', 1, 20);
|
||||
if (!in_array($position, self::POSITIONS, true)) {
|
||||
throw new InvalidArgumentException('浮窗位置无效');
|
||||
}
|
||||
|
||||
$color = strtoupper(trim(self::stringValue(
|
||||
self::inputValue($input, 'primary_color', $defaults['primary_color']),
|
||||
'主题色'
|
||||
)));
|
||||
if (preg_match('/^#[0-9A-F]{6}$/D', $color) !== 1) {
|
||||
throw new InvalidArgumentException('主题色必须是 #RRGGBB 格式');
|
||||
}
|
||||
|
||||
$bottomOffset = self::integerValue(
|
||||
self::inputValue($input, 'bottom_offset', $defaults['bottom_offset']),
|
||||
'底部距离'
|
||||
);
|
||||
if ($bottomOffset < 16 || $bottomOffset > 160) {
|
||||
throw new InvalidArgumentException('底部距离必须在 16-160 之间');
|
||||
}
|
||||
|
||||
return [
|
||||
'v' => self::VERSION,
|
||||
'enabled' => self::booleanValue(self::inputValue($input, 'enabled', $defaults['enabled']), '启用状态'),
|
||||
'template' => $template,
|
||||
'position' => $position,
|
||||
'title' => self::textValue(self::inputValue($input, 'title', $defaults['title']), '标题', 1, 24),
|
||||
'subtitle' => self::textValue(self::inputValue($input, 'subtitle', $defaults['subtitle']), '副标题', 0, 48),
|
||||
'button_text' => self::textValue(
|
||||
self::inputValue($input, 'button_text', $defaults['button_text']),
|
||||
'按钮文案',
|
||||
1,
|
||||
12
|
||||
),
|
||||
'primary_color' => $color,
|
||||
'bottom_offset' => $bottomOffset,
|
||||
'show_mobile' => self::booleanValue(
|
||||
self::inputValue($input, 'show_mobile', $defaults['show_mobile']),
|
||||
'移动端展示状态'
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public static function decode(mixed $stored): array
|
||||
{
|
||||
if (!is_string($stored) || trim($stored) === '') {
|
||||
return self::defaults();
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = json_decode($stored, true, 16, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($decoded)) {
|
||||
return self::defaults();
|
||||
}
|
||||
foreach (array_keys(self::defaults()) as $key) {
|
||||
if (!array_key_exists($key, $decoded)) {
|
||||
return self::defaults();
|
||||
}
|
||||
}
|
||||
|
||||
return self::fromInput($decoded);
|
||||
} catch (\Throwable) {
|
||||
return self::defaults();
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $config */
|
||||
public static function encode(array $config): string
|
||||
{
|
||||
return self::jsonForScript(self::fromInput($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成可直接跨站安装的完整脚本。真实获客链接始终只由跳转端点选择。
|
||||
*
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public static function renderScript(string $key, string $goUrl, array $config, bool $poolEnabled = true): string
|
||||
{
|
||||
$config = self::fromInput($config);
|
||||
if (!$poolEnabled) {
|
||||
$config['enabled'] = false;
|
||||
}
|
||||
|
||||
$jsonKey = self::jsonForScript($key);
|
||||
$jsonGo = self::jsonForScript($goUrl);
|
||||
$jsonConfig = self::jsonForScript($config);
|
||||
|
||||
return <<<JS
|
||||
(function(w,d){
|
||||
'use strict';
|
||||
var key={$jsonKey},goPath={$jsonGo},config={$jsonConfig},scriptNode=d.currentScript||null;
|
||||
var go=resolveGoUrl(goPath);
|
||||
var registry=w.WecomPromotion=w.WecomPromotion||{};
|
||||
var previous=registry[key];
|
||||
if(previous&&previous.__widgetVersion===1&&typeof previous.destroy==='function'){
|
||||
previous.destroy();
|
||||
}
|
||||
var root=null,mediaQuery=null,readyHandler=null,destroyed=false,manuallyHidden=false,api=null;
|
||||
var rootId='wecom-promotion-widget-'+key;
|
||||
var selector='[data-wecom-promotion="'+key+'"],.wecom-promotion-link[data-pool="'+key+'"]';
|
||||
|
||||
function findScriptNode(){
|
||||
if(scriptNode&&scriptNode.src){return scriptNode;}
|
||||
var scripts=d.getElementsByTagName('script');
|
||||
var marker='/api/qywx-promotion/js/'+key;
|
||||
for(var index=scripts.length-1;index>=0;index--){
|
||||
if((scripts[index].src||'').indexOf(marker)!==-1){scriptNode=scripts[index];return scriptNode;}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveGoUrl(value){
|
||||
if(/^https?:\/\//i.test(value)){return value;}
|
||||
var node=findScriptNode();
|
||||
if(node&&node.src&&typeof w.URL==='function'){
|
||||
try{return new w.URL(value,node.src).href;}catch(error){}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sourceUrl(){
|
||||
var location=w.location||{};
|
||||
var origin=location.origin||((location.protocol&&location.host)?location.protocol+'//'+location.host:'');
|
||||
return origin+(location.pathname||'/');
|
||||
}
|
||||
|
||||
function openPromotion(){
|
||||
w.location.assign(go+'?from='+encodeURIComponent(sourceUrl()));
|
||||
}
|
||||
|
||||
function handleDocumentClick(event){
|
||||
var path=typeof event.composedPath==='function'?event.composedPath():[];
|
||||
var node=null;
|
||||
for(var index=0;index<path.length;index++){
|
||||
var candidate=path[index];
|
||||
if(candidate&&candidate.nodeType===1&&candidate.matches&&candidate.matches(selector)){node=candidate;break;}
|
||||
}
|
||||
var target=event.target;
|
||||
if(!node){node=target&&target.closest?target.closest(selector):null;}
|
||||
if(!node){return;}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openPromotion();
|
||||
}
|
||||
|
||||
function isMobileHidden(){
|
||||
return config.show_mobile===false&&mediaQuery&&mediaQuery.matches;
|
||||
}
|
||||
|
||||
function applyVisibility(){
|
||||
if(root){root.hidden=manuallyHidden||isMobileHidden();}
|
||||
}
|
||||
|
||||
function handleViewportChange(){
|
||||
applyVisibility();
|
||||
}
|
||||
|
||||
function appendText(parent,tag,className,value){
|
||||
var node=d.createElement(tag);
|
||||
node.className=className;
|
||||
node.textContent=value;
|
||||
parent.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function mount(){
|
||||
if(destroyed||root||!config.enabled||!d.body){return;}
|
||||
var stale=d.getElementById(rootId);
|
||||
if(stale&&stale.parentNode){stale.parentNode.removeChild(stale);}
|
||||
|
||||
root=d.createElement('div');
|
||||
root.id=rootId;
|
||||
root.className='wcp-host wcp-host-'+config.position+' wcp-host-'+config.template;
|
||||
root.setAttribute('data-wecom-promotion-widget',key);
|
||||
|
||||
var surface=root.attachShadow?root.attachShadow({mode:'open'}):root;
|
||||
var style=d.createElement('style');
|
||||
var nonceNode=findScriptNode();
|
||||
var nonce=nonceNode?(nonceNode.nonce||nonceNode.getAttribute('nonce')||''):'';
|
||||
if(nonce){style.setAttribute('nonce',nonce);}
|
||||
var hostRules='position:fixed;z-index:2147483000;right:20px;bottom:calc('+config.bottom_offset+'px + env(safe-area-inset-bottom, 0px));max-width:calc(100vw - 32px);pointer-events:none;--wcp-primary:'+config.primary_color+';font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;color:#fff;line-height:1.4;-webkit-font-smoothing:antialiased';
|
||||
style.textContent=':host{'+hostRules+'}.wcp-host{'+hostRules+'}' +
|
||||
':host(.wcp-host-bottom-left){right:auto;left:20px}.wcp-host-bottom-left{right:auto;left:20px}' +
|
||||
':host(.wcp-host-edge.wcp-host-bottom-right){right:0}.wcp-host-edge.wcp-host-bottom-right{right:0}' +
|
||||
':host(.wcp-host-edge.wcp-host-bottom-left){right:auto;left:0}.wcp-host-edge.wcp-host-bottom-left{right:auto;left:0}' +
|
||||
':host([hidden]){display:none!important}.wcp-host[hidden]{display:none!important}.wcp-root,.wcp-root *{box-sizing:border-box}.wcp-root{pointer-events:none}' +
|
||||
'.wcp-button{pointer-events:auto;position:relative;display:flex;align-items:center;gap:12px;margin:0;border:0;cursor:pointer;color:#fff;background:var(--wcp-primary);font:inherit;text-align:left;box-shadow:0 14px 38px rgba(18,48,46,.24);transition:transform .2s ease,box-shadow .2s ease;appearance:none;-webkit-appearance:none}' +
|
||||
'.wcp-button:hover{transform:translateY(-2px);box-shadow:0 18px 44px rgba(18,48,46,.3)}.wcp-button:active{transform:translateY(0)}.wcp-button:focus-visible{outline:3px solid rgba(255,255,255,.96);outline-offset:3px}' +
|
||||
'.wcp-icon{display:flex;flex:0 0 auto;align-items:center;justify-content:center;width:38px;height:38px;border-radius:50%;background:rgba(255,255,255,.18);font-size:17px;font-weight:800}' +
|
||||
'.wcp-copy{display:flex;min-width:0;flex-direction:column}.wcp-title{font-size:15px;font-weight:750;line-height:1.25}.wcp-subtitle{margin-top:2px;max-width:240px;font-size:12px;line-height:1.4;opacity:.86}' +
|
||||
'.wcp-cta{flex:0 0 auto;padding:7px 11px;border-radius:999px;background:#fff;color:var(--wcp-primary);font-size:12px;font-weight:750;white-space:nowrap}' +
|
||||
'.wcp-bubble .wcp-button{width:66px;height:66px;justify-content:center;padding:0;border-radius:50%}.wcp-bubble .wcp-icon{width:46px;height:46px;font-size:19px}.wcp-bubble .wcp-copy,.wcp-bubble .wcp-cta{position:absolute;right:76px;visibility:hidden;opacity:0;transform:translateX(8px);transition:opacity .18s ease,transform .18s ease;pointer-events:none}' +
|
||||
'.wcp-bottom-left.wcp-bubble .wcp-copy,.wcp-bottom-left.wcp-bubble .wcp-cta{right:auto;left:76px}.wcp-bubble .wcp-copy{bottom:27px;width:220px;padding:11px 13px;border-radius:12px;background:#173f3b;box-shadow:0 12px 30px rgba(0,0,0,.2)}.wcp-bubble .wcp-cta{bottom:-1px;padding:5px 10px}' +
|
||||
'.wcp-bubble .wcp-button:hover .wcp-copy,.wcp-bubble .wcp-button:hover .wcp-cta,.wcp-bubble .wcp-button:focus-visible .wcp-copy,.wcp-bubble .wcp-button:focus-visible .wcp-cta{visibility:visible;opacity:1;transform:translateX(0)}' +
|
||||
'.wcp-pill .wcp-button{min-height:58px;padding:9px 12px;border-radius:999px}.wcp-pill .wcp-subtitle{display:none}' +
|
||||
'.wcp-card .wcp-button{width:min(340px,calc(100vw - 40px));padding:15px;border-radius:18px}.wcp-card .wcp-icon{width:46px;height:46px}.wcp-card .wcp-copy{flex:1}.wcp-card .wcp-cta{border-radius:10px}' +
|
||||
'.wcp-message .wcp-button{width:min(330px,calc(100vw - 40px));padding:13px 14px;border-radius:18px 18px 4px 18px}.wcp-bottom-left.wcp-message .wcp-button{border-radius:18px 18px 18px 4px}.wcp-message .wcp-copy{flex:1}.wcp-message .wcp-cta{padding:6px 9px}' +
|
||||
'.wcp-edge .wcp-button{min-height:62px;max-width:270px;padding:10px 15px;border-radius:16px 0 0 16px}.wcp-bottom-left.wcp-edge .wcp-button{border-radius:0 16px 16px 0}.wcp-edge .wcp-subtitle{display:none}.wcp-edge .wcp-cta{padding:6px 9px}' +
|
||||
'.wcp-bar .wcp-button{width:min(420px,calc(100vw - 40px));padding:11px 14px;border-radius:12px}.wcp-bar .wcp-copy{flex:1}.wcp-bar .wcp-icon{width:34px;height:34px}.wcp-bar .wcp-subtitle{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
|
||||
'@media(max-width:767px){.wcp-host{max-width:calc(100vw - 24px)}.wcp-card .wcp-button,.wcp-message .wcp-button,.wcp-bar .wcp-button{width:calc(100vw - 40px)}.wcp-subtitle{max-width:180px}.wcp-card .wcp-cta,.wcp-message .wcp-cta{display:none}}' +
|
||||
'@media(prefers-reduced-motion:reduce){.wcp-button,.wcp-bubble .wcp-copy,.wcp-bubble .wcp-cta{transition:none!important}}';
|
||||
surface.appendChild(style);
|
||||
|
||||
var container=d.createElement('div');
|
||||
container.className='wcp-root wcp-'+config.template+' wcp-'+config.position;
|
||||
var button=d.createElement('button');
|
||||
button.type='button';
|
||||
button.className='wcp-button';
|
||||
button.setAttribute('aria-label',config.title+':'+config.button_text);
|
||||
appendText(button,'span','wcp-icon','企');
|
||||
var copy=d.createElement('span');
|
||||
copy.className='wcp-copy';
|
||||
appendText(copy,'strong','wcp-title',config.title);
|
||||
if(config.subtitle!==''){appendText(copy,'span','wcp-subtitle',config.subtitle);}
|
||||
button.appendChild(copy);
|
||||
appendText(button,'span','wcp-cta',config.button_text);
|
||||
button.addEventListener('click',function(event){
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
openPromotion();
|
||||
});
|
||||
container.appendChild(button);
|
||||
surface.appendChild(container);
|
||||
d.body.appendChild(root);
|
||||
|
||||
if(config.show_mobile===false&&typeof w.matchMedia==='function'){
|
||||
mediaQuery=w.matchMedia('(max-width: 767px)');
|
||||
if(mediaQuery.addEventListener){mediaQuery.addEventListener('change',handleViewportChange);}
|
||||
else if(mediaQuery.addListener){mediaQuery.addListener(handleViewportChange);}
|
||||
}
|
||||
applyVisibility();
|
||||
}
|
||||
|
||||
function show(){
|
||||
if(destroyed||!config.enabled){return;}
|
||||
manuallyHidden=false;
|
||||
if(root){applyVisibility();return;}
|
||||
if(d.body){mount();}
|
||||
else if(!readyHandler){
|
||||
readyHandler=function(){readyHandler=null;mount();};
|
||||
d.addEventListener('DOMContentLoaded',readyHandler,{once:true});
|
||||
}
|
||||
}
|
||||
|
||||
function hide(){
|
||||
manuallyHidden=true;
|
||||
applyVisibility();
|
||||
}
|
||||
|
||||
function destroy(){
|
||||
if(destroyed){return;}
|
||||
destroyed=true;
|
||||
d.removeEventListener('click',handleDocumentClick,true);
|
||||
if(readyHandler){d.removeEventListener('DOMContentLoaded',readyHandler);readyHandler=null;}
|
||||
if(mediaQuery){
|
||||
if(mediaQuery.removeEventListener){mediaQuery.removeEventListener('change',handleViewportChange);}
|
||||
else if(mediaQuery.removeListener){mediaQuery.removeListener(handleViewportChange);}
|
||||
mediaQuery=null;
|
||||
}
|
||||
if(root&&root.parentNode){root.parentNode.removeChild(root);}
|
||||
root=null;
|
||||
if(registry[key]===api){delete registry[key];}
|
||||
}
|
||||
|
||||
d.addEventListener('click',handleDocumentClick,true);
|
||||
api={open:openPromotion,show:show,hide:hide,destroy:destroy,config:config,__widgetVersion:1};
|
||||
registry[key]=api;
|
||||
if(config.enabled){show();}
|
||||
})(window,document);
|
||||
JS;
|
||||
}
|
||||
|
||||
private static function booleanValue(mixed $value, string $label): bool
|
||||
{
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
if ($value === 1 || $value === '1') {
|
||||
return true;
|
||||
}
|
||||
if ($value === 0 || $value === '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException($label . '必须是布尔值');
|
||||
}
|
||||
|
||||
private static function inputValue(array $input, string $key, mixed $default): mixed
|
||||
{
|
||||
return array_key_exists($key, $input) ? $input[$key] : $default;
|
||||
}
|
||||
|
||||
private static function integerValue(mixed $value, string $label): int
|
||||
{
|
||||
if (is_int($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_string($value) && preg_match('/^-?\d+$/D', $value) === 1) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException($label . '必须是整数');
|
||||
}
|
||||
|
||||
private static function stringValue(mixed $value, string $label): string
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
throw new InvalidArgumentException($label . '必须是字符串');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function textValue(mixed $value, string $label, int $min, int $max): string
|
||||
{
|
||||
$value = self::stringValue($value, $label);
|
||||
$value = preg_replace('/\s+/u', ' ', trim($value)) ?? '';
|
||||
$length = mb_strlen($value);
|
||||
if ($length < $min || $length > $max) {
|
||||
throw new InvalidArgumentException(sprintf('%s长度必须在 %d-%d 个字符之间', $label, $min, $max));
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function jsonForScript(mixed $value): string
|
||||
{
|
||||
return json_encode(
|
||||
$value,
|
||||
JSON_UNESCAPED_UNICODE
|
||||
| JSON_UNESCAPED_SLASHES
|
||||
| JSON_HEX_TAG
|
||||
| JSON_HEX_AMP
|
||||
| JSON_HEX_APOS
|
||||
| JSON_HEX_QUOT
|
||||
| JSON_THROW_ON_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user