更新
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\common\service\qywx\MediaChannelService;
|
||||
use think\facade\Db;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
$tagQuery = Db::name('qywx_external_contact_event')->alias('e');
|
||||
MediaChannelService::applyExternalUserChannelFilter(
|
||||
$tagQuery,
|
||||
'e.external_userid',
|
||||
[
|
||||
'source_tag_id' => 'tag-regression-id',
|
||||
'source_tag_name' => '回归渠道',
|
||||
]
|
||||
);
|
||||
$tagSql = (string)$tagQuery->fetchSql()->select();
|
||||
if (!str_contains($tagSql, 'qywx_external_contact_tag')) {
|
||||
throw new RuntimeException('tag 渠道未使用结构化客户标签关系表');
|
||||
}
|
||||
if (!str_contains($tagSql, ' IN (SELECT channel_tag.external_userid')) {
|
||||
throw new RuntimeException('tag 渠道未通过去重子查询过滤 external_userid');
|
||||
}
|
||||
if (str_contains($tagSql, 'follow_users') || str_contains($tagSql, 'LIKE')) {
|
||||
throw new RuntimeException('tag 渠道仍在扫描 follow_users JSON');
|
||||
}
|
||||
|
||||
$legacyQuery = Db::name('order')->alias('o');
|
||||
MediaChannelService::applyExternalUserChannelFilter(
|
||||
$legacyQuery,
|
||||
'o.payer_external_userid',
|
||||
[
|
||||
'source_tag_id' => '',
|
||||
'source_tag_name' => '仅名称老渠道',
|
||||
]
|
||||
);
|
||||
$legacySql = (string)$legacyQuery->fetchSql()->select();
|
||||
if (!str_contains($legacySql, ' IN (SELECT channel_contact.external_userid')) {
|
||||
throw new RuntimeException('老渠道回退未使用去重 external_userid 子查询');
|
||||
}
|
||||
if (!str_contains($legacySql, 'channel_contact.delete_time IS NULL')) {
|
||||
throw new RuntimeException('老渠道回退包含了已删除客户记录');
|
||||
}
|
||||
|
||||
echo "MEDIA_CHANNEL_EXTERNAL_USER_FILTER_OK\n";
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\common\service\qywx\QywxPromotionWidgetService;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
function widgetAssert(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
function expectInvalidWidget(array $overrides, string $message): void
|
||||
{
|
||||
try {
|
||||
QywxPromotionWidgetService::fromInput($overrides + QywxPromotionWidgetService::defaults());
|
||||
} catch (InvalidArgumentException) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$defaults = QywxPromotionWidgetService::defaults();
|
||||
widgetAssert($defaults === [
|
||||
'v' => 1,
|
||||
'enabled' => false,
|
||||
'template' => 'bubble',
|
||||
'position' => 'bottom-right',
|
||||
'title' => '专属顾问在线',
|
||||
'subtitle' => '点击添加企业微信,获取一对一服务',
|
||||
'button_text' => '立即咨询',
|
||||
'primary_color' => '#139A8C',
|
||||
'bottom_offset' => 28,
|
||||
'show_mobile' => true,
|
||||
], '默认浮窗配置与公开契约不一致');
|
||||
|
||||
$normalised = QywxPromotionWidgetService::fromInput([
|
||||
'enabled' => '1',
|
||||
'template' => 'card',
|
||||
'position' => 'bottom-left',
|
||||
'title' => " 在线\n顾问 ",
|
||||
'subtitle' => '',
|
||||
'button_text' => '去咨询',
|
||||
'primary_color' => '#a1b2c3',
|
||||
'bottom_offset' => '64',
|
||||
'show_mobile' => '0',
|
||||
]);
|
||||
widgetAssert($normalised['v'] === 1, '缺省输入未补齐配置版本');
|
||||
widgetAssert($normalised['enabled'] === true, '启用状态规范化失败');
|
||||
widgetAssert($normalised['title'] === '在线 顾问', '文案空白规范化失败');
|
||||
widgetAssert($normalised['primary_color'] === '#A1B2C3', '主题色未规范为大写');
|
||||
widgetAssert($normalised['bottom_offset'] === 64, '底部距离规范化失败');
|
||||
widgetAssert($normalised['show_mobile'] === false, '移动端开关规范化失败');
|
||||
widgetAssert(
|
||||
QywxPromotionWidgetService::decode(QywxPromotionWidgetService::encode($normalised)) === $normalised,
|
||||
'浮窗配置编解码不能稳定往返'
|
||||
);
|
||||
|
||||
expectInvalidWidget(['v' => 2], '未知版本没有被拒绝');
|
||||
expectInvalidWidget(['template' => 'html'], '未知模板没有被拒绝');
|
||||
expectInvalidWidget(['position' => 'top-right'], '未知位置没有被拒绝');
|
||||
expectInvalidWidget(['title' => ''], '空标题没有被拒绝');
|
||||
expectInvalidWidget(['title' => str_repeat('中', 25)], '超长标题没有被拒绝');
|
||||
expectInvalidWidget(['subtitle' => str_repeat('中', 49)], '超长副标题没有被拒绝');
|
||||
expectInvalidWidget(['button_text' => str_repeat('中', 13)], '超长按钮文案没有被拒绝');
|
||||
expectInvalidWidget(['primary_color' => 'red;background:url(x)'], 'CSS 注入色值没有被拒绝');
|
||||
expectInvalidWidget(['bottom_offset' => 15], '过小底部距离没有被拒绝');
|
||||
expectInvalidWidget(['bottom_offset' => 161], '过大底部距离没有被拒绝');
|
||||
expectInvalidWidget(['show_mobile' => 'yes'], '非法布尔值没有被拒绝');
|
||||
expectInvalidWidget(['template' => null], '显式 null 模板没有被拒绝');
|
||||
|
||||
$decodedInvalid = QywxPromotionWidgetService::decode('{broken');
|
||||
widgetAssert($decodedInvalid === $defaults && $decodedInvalid['enabled'] === false, '损坏 JSON 未 fail-closed');
|
||||
$decodedIncomplete = QywxPromotionWidgetService::decode('{"enabled":true}');
|
||||
widgetAssert($decodedIncomplete === $defaults && $decodedIncomplete['enabled'] === false, '字段缺失配置未 fail-closed');
|
||||
$decodedUnknown = QywxPromotionWidgetService::decode('{"v":2,"enabled":true}');
|
||||
widgetAssert($decodedUnknown === $defaults && $decodedUnknown['enabled'] === false, '未知版本未 fail-closed');
|
||||
$decodedIllegal = QywxPromotionWidgetService::decode('{"v":1,"enabled":true,"template":"raw-html"}');
|
||||
widgetAssert($decodedIllegal === $defaults && $decodedIllegal['enabled'] === false, '非法持久化配置未 fail-closed');
|
||||
|
||||
$xssConfig = QywxPromotionWidgetService::fromInput([
|
||||
'v' => 1,
|
||||
'enabled' => true,
|
||||
'template' => 'message',
|
||||
'position' => 'bottom-right',
|
||||
'title' => '<img onerror=x>',
|
||||
'subtitle' => '</script>',
|
||||
'button_text' => '咨询',
|
||||
'primary_color' => '#139A8C',
|
||||
'bottom_offset' => 28,
|
||||
'show_mobile' => true,
|
||||
]);
|
||||
$encoded = QywxPromotionWidgetService::encode($xssConfig);
|
||||
widgetAssert(!str_contains($encoded, '<img') && str_contains($encoded, '\\u003Cimg'), '持久化 JSON 未使用 HEX 转义');
|
||||
|
||||
$key = str_repeat('a', 32);
|
||||
$script = QywxPromotionWidgetService::renderScript(
|
||||
$key,
|
||||
'/api/qywx-promotion/go/' . $key,
|
||||
$xssConfig,
|
||||
true
|
||||
);
|
||||
widgetAssert(!str_contains($script, '<img onerror=x>'), 'XSS 文案以原始标签进入公开脚本');
|
||||
widgetAssert(!str_contains($script, 'innerHTML'), '公开脚本不得使用 innerHTML');
|
||||
widgetAssert(str_contains($script, 'node.textContent=value'), '公开脚本文案未通过 textContent 写入');
|
||||
widgetAssert(str_contains($script, 'data-wecom-promotion'), '旧 data-wecom-promotion 触发方式丢失');
|
||||
widgetAssert(str_contains($script, '.wecom-promotion-link[data-pool'), '旧 data-pool 触发方式丢失');
|
||||
widgetAssert(str_contains($script, 'w.WecomPromotion=w.WecomPromotion||{}'), '全局 WecomPromotion 注册表丢失');
|
||||
widgetAssert(str_contains($script, 'open:openPromotion'), '全局 open 方法丢失');
|
||||
widgetAssert(str_contains($script, 'show:show') && str_contains($script, 'hide:hide') && str_contains($script, 'destroy:destroy'), '浮窗生命周期方法不完整');
|
||||
widgetAssert(str_contains($script, 'location.origin') && str_contains($script, 'location.pathname'), '来源地址未限制为 origin + pathname');
|
||||
widgetAssert(!str_contains($script, 'location.href'), '公开脚本仍发送完整 location.href');
|
||||
widgetAssert(str_contains($script, 'attachShadow'), '公开脚本未隔离浮窗样式');
|
||||
widgetAssert(str_contains($script, 'd.currentScript') && str_contains($script, 'new w.URL(value,node.src)'), '跳转地址未从安装脚本来源解析');
|
||||
widgetAssert(str_contains($script, "style.setAttribute('nonce',nonce)"), '公开脚本未向动态样式传递 CSP nonce');
|
||||
widgetAssert(str_contains($script, 'event.composedPath'), '公开脚本未兼容 Shadow DOM 内的手动触发元素');
|
||||
widgetAssert(!str_contains($script, 'root.style.'), '公开脚本仍依赖会被严格 CSP 拦截的元素内联样式');
|
||||
widgetAssert(str_contains($script, 'safe-area-inset-bottom'), '公开脚本未适配移动端安全区');
|
||||
foreach (['bubble', 'pill', 'card', 'message', 'edge', 'bar'] as $template) {
|
||||
widgetAssert(str_contains($script, '.wcp-' . $template), '公开脚本缺少模板:' . $template);
|
||||
}
|
||||
|
||||
$disabledScript = QywxPromotionWidgetService::renderScript($key, 'https://example.test/go', $xssConfig, false);
|
||||
widgetAssert(str_contains($disabledScript, '"enabled":false'), '停用方案仍会自动挂载浮窗');
|
||||
widgetAssert(str_contains($disabledScript, 'open:openPromotion'), '停用方案脚本没有保留手动 open 兼容接口');
|
||||
|
||||
echo "QYWX_PROMOTION_WIDGET_SERVICE_OK\n";
|
||||
Reference in New Issue
Block a user