feat: 增加模板通道拒收策略并修复运营页面
This commit is contained in:
@@ -12,39 +12,59 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "src/apps/admin/AdminHome.css",
|
||||
"owners": ["src/apps/admin/AdminHome.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/AdminHome.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["admin-dashboard"]
|
||||
"roots": [
|
||||
"admin-dashboard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/AdminAnalyticsPage.css",
|
||||
"owners": ["src/apps/admin/AdminAnalyticsPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/AdminAnalyticsPage.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["admin-analytics-page"]
|
||||
"roots": [
|
||||
"admin-analytics-page"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/layouts/AlertNotificationMenu.css",
|
||||
"owners": ["src/layouts/AlertNotificationMenu.tsx"],
|
||||
"owners": [
|
||||
"src/layouts/AlertNotificationMenu.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["alert-notification-menu"]
|
||||
"roots": [
|
||||
"alert-notification-menu"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/ReportMaterialImportModal.css",
|
||||
"owners": ["src/apps/admin/ReportMaterialImportModal.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/ReportMaterialImportModal.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["report-material-import-modal"]
|
||||
"roots": [
|
||||
"report-material-import-modal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/channels/AdminChannelsPage.css",
|
||||
"owners": ["src/apps/admin/AdminChannelsPage.tsx"],
|
||||
"legacyFingerprint": "fc2d168035c9adac5b330a7526424831ac18725da92a8c2392600e08a0891c69",
|
||||
"owners": [
|
||||
"src/apps/admin/AdminChannelsPage.tsx"
|
||||
],
|
||||
"legacyFingerprint": "2c5e14e0747d1c05a82155f4b49321b01984cb38007a57322c1c3cf119fa22db",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。 2026-09-20 按模板拒收策略专项方案修复通道页窄屏查询遮挡及运营商选项换行,三尺寸验收后更新基线。",
|
||||
"removalCondition": "按页面或公共组件确认消费者、根类和真实浏览器等价证据后,移除历史摘要并登记 roots;有意样式变更须附依据和验证后更新基线。"
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/enterprise-applications/AdminEnterpriseApplicationsPage.css",
|
||||
"owners": ["src/apps/admin/AdminEnterpriseApplicationsPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/AdminEnterpriseApplicationsPage.tsx"
|
||||
],
|
||||
"legacyFingerprint": "ac5abed49be7edb910ef8746c36f72e953f199b6073d13617594f1158fe4c69b",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -52,7 +72,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/security-detection/AdminSecurityDetectionPage.css",
|
||||
"owners": ["src/apps/admin/security-detection/AdminSecurityDetectionPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/security-detection/AdminSecurityDetectionPage.tsx"
|
||||
],
|
||||
"legacyFingerprint": "ba5bc7ea3d09615685fee210c625d85ddf1c368d587922fb4953b24591675e06",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -60,7 +82,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/sms-records/AdminSmsRecordsPage.css",
|
||||
"owners": ["src/apps/admin/AdminSmsRecordsPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/AdminSmsRecordsPage.tsx"
|
||||
],
|
||||
"legacyFingerprint": "a7cdbded6c765662dee0769c9c02159bb01045af08e2fa1842a11f79699fad02",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -68,7 +92,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/sms-task-progress/AdminSmsTaskProgressPage.css",
|
||||
"owners": ["src/apps/admin/AdminSmsTaskProgressPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/AdminSmsTaskProgressPage.tsx"
|
||||
],
|
||||
"legacyFingerprint": "a222961a94f15a388892a272e42bf64e122ea55f067bf618126e67ed107014b2",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -76,7 +102,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/system-monitoring/AdminSystemMonitoringPage.css",
|
||||
"owners": ["src/apps/admin/system-monitoring/AdminSystemMonitoringPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/system-monitoring/AdminSystemMonitoringPage.tsx"
|
||||
],
|
||||
"legacyFingerprint": "59d09cdce53ef804773d46b1e5807e0e273f2c3ba7e2aa21b864d0d718f75db2",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -84,7 +112,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/apps/client/ClientUsersPage.css",
|
||||
"owners": ["src/apps/client/ClientUsersPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/client/ClientUsersPage.tsx"
|
||||
],
|
||||
"legacyFingerprint": "af9cdb6f0229056437dab22fc0533fa9b0df2e3b013b69088a9815c4468518db",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -92,7 +122,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/admin.css",
|
||||
"owners": ["src/main.tsx"],
|
||||
"owners": [
|
||||
"src/main.tsx"
|
||||
],
|
||||
"legacyFingerprint": "b31d1360ca34c0bd5bc81d3687f5db649d5eb775db41faaec58c1ffd358891a1",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -100,7 +132,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/client.css",
|
||||
"owners": ["src/main.tsx"],
|
||||
"owners": [
|
||||
"src/main.tsx"
|
||||
],
|
||||
"legacyFingerprint": "b496831577a2296cca8bd1e3ba5a5c9574068f73f8f4c8176fe023957f803219",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -108,7 +142,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/components.css",
|
||||
"owners": ["src/main.tsx"],
|
||||
"owners": [
|
||||
"src/main.tsx"
|
||||
],
|
||||
"legacyFingerprint": "8adee9fa7adcc5df32137c6428b944d9a79fd7aab0ceb8ae699aca6686294702",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -116,7 +152,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/01-operations-dashboard.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "a9b28b65f14fb518f16620c4ae0fe337fe7807202818154c134b612384f9167d",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -124,7 +162,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/02-client-sending.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "b84920adb1891089df78af28066fe0bc1c0d2452057a294a798554312be57b41",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -132,7 +172,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/03-client-records.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "1e3eb6377c5436bc008f77cf1721575f5ace209cf0296d635002249272dcf794",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -140,7 +182,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/04-signatures.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "0ff9be0753940b7d3312042a93b6a97d1cca92d3e045bfa3f13d064467377874",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -148,7 +192,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/05-templates.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "79d3e6a2c2e1cef9dcecaac985344cdc36ce30a31ccfae6d5c29f5801152bd76",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -156,7 +202,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/06-auth-enterprise.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "f391234e8afe2085bc3d64c4e86d2f89e9f4aa2554d84abf9ea9f6228a35fbc7",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -164,7 +212,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/07-admin-operations.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "e8414200ee4b8354130880f1e3c7b71ea3972d278b1b72414897fb14d554958b",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -172,7 +222,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/08-reporting.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "db762c6fef81aac2b4a76746b5a78a607e9711aaeb4e509e2800b86b17eee6a5",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -180,7 +232,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/09-channels.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "a32eab767f85b49324e17721b70b8dd0c8f0e5a14d6ef2d3502a56c69f9c0e7a",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -188,7 +242,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/10-signature-quality.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "9d79afed6c1550e747dcf6c5ab554a4977b62a93d66421a0a01add9f875e2c30",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -196,7 +252,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/11-deliveries-reporting.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "9792e6aedb4c74dcd6aeb26925391315e7df662c038a32be5ffcf3eee282374d",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -204,7 +262,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/12-admin-configuration.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "60ac29279c18da425f52efe88b2de9a686a20c4e54ec54d6a424d027490c8e81",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -212,7 +272,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/13-client-signatures.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "c90bb9ea3bf4ab278379a806b94187a559a46557de344c934be55968fef5287b",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -220,7 +282,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/14-responsive-requeue.css",
|
||||
"owners": ["src/styles/domains/index.css"],
|
||||
"owners": [
|
||||
"src/styles/domains/index.css"
|
||||
],
|
||||
"legacyFingerprint": "3d3daaca99d4efbdaaea4dd1ec6a0239274ed52f6de658ed9f889a6d565ec567",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -228,7 +292,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/domains/index.css",
|
||||
"owners": ["src/main.tsx"],
|
||||
"owners": [
|
||||
"src/main.tsx"
|
||||
],
|
||||
"legacyFingerprint": "3f287af46d6c7e7f921c43913b52a9a727a8e343a80ad46c45d25baaed073177",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -236,7 +302,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/reset.css",
|
||||
"owners": ["src/main.tsx"],
|
||||
"owners": [
|
||||
"src/main.tsx"
|
||||
],
|
||||
"legacyFingerprint": "4be47c806f9a6981a8b73f3844d707fd5cdaaf0d343323de07c6e39089224df1",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -244,7 +312,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/shell.css",
|
||||
"owners": ["src/main.tsx"],
|
||||
"owners": [
|
||||
"src/main.tsx"
|
||||
],
|
||||
"legacyFingerprint": "93e26e9dcaa88447e054aeff8e20d4902cdaea54f0ec5a5b5659c84d9c4b9bd8",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -252,7 +322,9 @@
|
||||
},
|
||||
{
|
||||
"file": "src/styles/tokens.css",
|
||||
"owners": ["src/main.tsx"],
|
||||
"owners": [
|
||||
"src/main.tsx"
|
||||
],
|
||||
"legacyFingerprint": "754135f86b0828fa004270a4be6e7a087cb794eefa3860e8d45f89a6eb1f7223",
|
||||
"stylelintLegacy": true,
|
||||
"reason": "2026-09-05 现有样式保留已发布级联;不授权向历史文件追加新业务规则。",
|
||||
@@ -260,50 +332,103 @@
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/AdminChannelGroupFormPage.css",
|
||||
"owners": ["src/apps/admin/AdminChannelGroupFormPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/AdminChannelGroupFormPage.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["channel-group-editor"]
|
||||
"roots": [
|
||||
"channel-group-editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/channel-groups/RouteConfigModal.css",
|
||||
"owners": ["src/apps/admin/channel-groups/RouteConfigModal.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/channel-groups/RouteConfigModal.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["channel-route-editor"]
|
||||
"roots": [
|
||||
"channel-route-editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/report-notifications/report-notifications.css",
|
||||
"owners": ["src/apps/report-notifications/ReportNotificationsPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/report-notifications/ReportNotificationsPage.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["report-notifications-page"]
|
||||
"roots": [
|
||||
"report-notifications-page"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/AdminMonitorPage.css",
|
||||
"owners": ["src/apps/admin/AdminMonitorPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/AdminMonitorPage.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["sending-monitor"]
|
||||
"roots": [
|
||||
"sending-monitor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/sending-monitor/MonitorRuleManager.css",
|
||||
"owners": ["src/apps/admin/sending-monitor/MonitorRuleManager.tsx"],
|
||||
"owners": [
|
||||
"src/apps/admin/sending-monitor/MonitorRuleManager.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["monitor-rules"]
|
||||
"roots": [
|
||||
"monitor-rules"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/client/http-docs/HttpDeveloperDocs.css",
|
||||
"owners": ["src/apps/client/http-docs/HttpDeveloperDocs.tsx", "src/apps/client/http-docs/ClientHttpDocsPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/client/http-docs/HttpDeveloperDocs.tsx",
|
||||
"src/apps/client/http-docs/ClientHttpDocsPage.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["client-http-docs"]
|
||||
"roots": [
|
||||
"client-http-docs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/client/ClientTemplatesPage.css",
|
||||
"owners": ["src/apps/client/ClientTemplatesPage.tsx"],
|
||||
"owners": [
|
||||
"src/apps/client/ClientTemplatesPage.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": ["client-templates-page"]
|
||||
"roots": [
|
||||
"client-templates-page"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/shared/http-signature/HttpSignaturePage.css",
|
||||
"owners": ["src/apps/shared/http-signature/HttpSignaturePage.tsx"],
|
||||
"roots": ["http-signature-page"]
|
||||
"owners": [
|
||||
"src/apps/shared/http-signature/HttpSignaturePage.tsx"
|
||||
],
|
||||
"roots": [
|
||||
"http-signature-page"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/QualityStatusBar.css",
|
||||
"owners": [
|
||||
"src/apps/admin/QualityStatusBar.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": [
|
||||
"quality-status-bar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/apps/admin/TemplateOptOutModal.css",
|
||||
"owners": [
|
||||
"src/apps/admin/TemplateOptOutModal.tsx"
|
||||
],
|
||||
"stylelintLegacy": false,
|
||||
"roots": [
|
||||
"template-optout"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRequire } from 'node:module';
|
||||
import { randomBytes, randomUUID } from 'node:crypto';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
const url = new URL(process.env.OPT_OUT_TEST_DATABASE_URL || '');
|
||||
assert(['127.0.0.1', 'localhost'].includes(url.hostname) && url.pathname.startsWith('/cmpp_qa_optout_'));
|
||||
const redis = new URL(process.env.OPT_OUT_TEST_REDIS_URL || 'redis://127.0.0.1:16436');
|
||||
assert(['127.0.0.1', 'localhost'].includes(redis.hostname));
|
||||
Object.assign(process.env, {
|
||||
NODE_ENV: 'test',
|
||||
DATABASE_URL: url.toString(),
|
||||
REDIS_URL: redis.toString(),
|
||||
HTTP_API_MASTER_KEY: randomBytes(32).toString('hex'),
|
||||
MINIO_ENDPOINT: '127.0.0.1:19400',
|
||||
GATEWAY_CONTROL_URL: 'http://127.0.0.1:19401',
|
||||
SIGNATURE_ANALYTICS_ENABLED: 'false',
|
||||
HOME_DASHBOARD_ENABLED: 'false',
|
||||
SEND_SUBMIT_OUTBOX_PUBLISH_ENABLED: 'true',
|
||||
});
|
||||
// No workers/publishers/Gateway are started. Commands stay in this isolated database.
|
||||
const require = createRequire(new URL('../../api/package.json', import.meta.url));
|
||||
Object.assign(process.env, {
|
||||
GATEWAY_STARTUP_RECONNECT_DELAY_MS: '3600000',
|
||||
GATEWAY_CONNECTION_RECONCILER_DISABLED: 'true',
|
||||
GATEWAY_CONNECTING_TIMEOUT_SCANNER_DISABLED: 'true',
|
||||
SMS_RECEIPT_TIMEOUT_SCAN_ENABLED: 'false',
|
||||
SMS_SCHEDULED_DISPATCH_SCAN_ENABLED: 'false',
|
||||
CMPP_INBOUND_LONG_MESSAGE_SCAN_ENABLED: 'false',
|
||||
UPSTREAM_RECEIPT_INBOX_SCAN_ENABLED: 'false',
|
||||
CMPP_DOWNSTREAM_REQUEUE_TASK_SCAN_ENABLED: 'false',
|
||||
CMPP_PROCESS_ROLE: 'api',
|
||||
API_ENABLE_SEND_WORKER: 'false',
|
||||
CMPP_INBOUND_WORKFLOW_WORKER_ENABLED: 'false',
|
||||
});
|
||||
require('reflect-metadata');
|
||||
Object.defineProperty(BigInt.prototype, 'toJSON', {
|
||||
value() {
|
||||
return Number(this);
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
const { NestFactory } = require('@nestjs/core'),
|
||||
{ AppModule } = require('./dist/app.module');
|
||||
const { PrismaService } = require('./dist/prisma/prisma.service'),
|
||||
{ SessionService } = require('./dist/auth/session.service');
|
||||
const { UsersService } = require('./dist/users/users.service'),
|
||||
{ SendChainService } = require('./dist/send-chain/send-chain.service');
|
||||
const { completionContext } = require('./dist/send-chain/completion-context');
|
||||
const app = await NestFactory.create(AppModule, { logger: ['error'] });
|
||||
app.setGlobalPrefix('api');
|
||||
const pass = (name) => console.log('PASS', name);
|
||||
try {
|
||||
await app.listen(Number(process.env.OPT_OUT_TEST_PORT || 0), '127.0.0.1');
|
||||
const base = (await app.getUrl()) + '/api',
|
||||
db = app.get(PrismaService),
|
||||
chain = app.get(SendChainService);
|
||||
const stamp = randomUUID().slice(0, 8),
|
||||
key = () => randomUUID();
|
||||
const tenant = await db.tenant.create({ data: { name: '拒收策略验收企业', code: key() } });
|
||||
const application = await db.smsApplication.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
name: '拒收策略隔离应用',
|
||||
cmppAccount: key(),
|
||||
cmppEnterpriseCode: '000001',
|
||||
secretHash: 'unused',
|
||||
interfaceEnabled: false,
|
||||
templateMismatchMode: 'direct_send',
|
||||
},
|
||||
});
|
||||
const signature = await db.smsSignature.create({
|
||||
data: { tenantId: tenant.id, applicationId: application.id, name: '【拒收验收】', auditStatus: 'approved' },
|
||||
});
|
||||
const content = signature.name + '文'.repeat(65); // 71 characters, two parts.
|
||||
assert.equal(content.length, 71);
|
||||
const template = await db.smsTemplate.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
applicationId: application.id,
|
||||
signatureId: signature.id,
|
||||
name: '拒收策略验收模板' + stamp,
|
||||
content,
|
||||
auditStatus: 'approved',
|
||||
billingUnits: 2,
|
||||
},
|
||||
});
|
||||
const channel = await db.smsChannel.create({
|
||||
data: {
|
||||
name: '隔离通道甲' + stamp,
|
||||
code: key(),
|
||||
carrier: 'mobile',
|
||||
carriers: ['mobile', 'unicom'],
|
||||
status: 'active',
|
||||
gatewayHost: '127.0.0.1',
|
||||
gatewayPort: 1,
|
||||
account: key(),
|
||||
passwordCipher: 'unused',
|
||||
srcId: '1069',
|
||||
sendRegion: '全国',
|
||||
unitPrice: 325,
|
||||
config: { serviceId: 'SMS' },
|
||||
},
|
||||
});
|
||||
const backup = await db.smsChannel.create({
|
||||
data: {
|
||||
name: '隔离通道乙',
|
||||
code: key(),
|
||||
carrier: 'mobile',
|
||||
carriers: ['mobile'],
|
||||
status: 'active',
|
||||
gatewayHost: '127.0.0.1',
|
||||
gatewayPort: 1,
|
||||
account: key(),
|
||||
passwordCipher: 'unused',
|
||||
srcId: '1069',
|
||||
sendRegion: '全国',
|
||||
unitPrice: 325,
|
||||
config: { serviceId: 'SMS' },
|
||||
},
|
||||
});
|
||||
for (const c of [channel, backup]) {
|
||||
await db.cmppConnectionState.create({
|
||||
data: { channelId: c.id, connectionId: key(), status: 'connected', currentConnections: 1 },
|
||||
});
|
||||
await db.channelSignatureReportTask.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
signatureId: signature.id,
|
||||
channelId: c.id,
|
||||
carrier: 'mobile',
|
||||
approvalScope: 'carrier_specific',
|
||||
status: 'approved',
|
||||
},
|
||||
});
|
||||
}
|
||||
const group = await db.smsChannelGroup.create({
|
||||
data: {
|
||||
code: key(),
|
||||
name: '隔离移动组',
|
||||
carrier: 'mobile',
|
||||
items: {
|
||||
create: [
|
||||
{ channelId: channel.id, carrier: 'mobile', priority: 1 },
|
||||
{ channelId: backup.id, carrier: 'mobile', priority: 2 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const unicom = await db.smsChannelGroup.create({
|
||||
data: {
|
||||
code: key(),
|
||||
name: '隔离联通组',
|
||||
carrier: 'unicom',
|
||||
items: { create: { channelId: channel.id, carrier: 'unicom' } },
|
||||
},
|
||||
});
|
||||
await db.channelRouteRule.create({
|
||||
data: { tenantId: tenant.id, applicationId: application.id, groupId: group.id, carrier: 'mobile' },
|
||||
});
|
||||
await db.channelRouteRule.create({
|
||||
data: { tenantId: tenant.id, applicationId: application.id, groupId: unicom.id, carrier: 'unicom' },
|
||||
});
|
||||
const users = app.get(UsersService),
|
||||
sessions = app.get(SessionService);
|
||||
const user = await users.create({
|
||||
username: 'optout' + stamp,
|
||||
email: stamp + '@example.invalid',
|
||||
displayName: '隔离运营验收',
|
||||
password: randomBytes(24).toString('hex'),
|
||||
roleCode: 'platform_admin',
|
||||
});
|
||||
const session = await sessions.create(user.id, 'admin', 0),
|
||||
cookie = sessions.cookieName('admin');
|
||||
const headers = { 'content-type': 'application/json', cookie: cookie + '=' + session.token };
|
||||
const req = (path, body, method = body === undefined ? 'GET' : 'PUT', head = headers) =>
|
||||
fetch(base + path, { method, headers: head, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
||||
const path = '/admin/enterprise-templates/' + template.id + '/opt-out-policy';
|
||||
assert.equal((await req(path, undefined, 'GET', {})).status, 401);
|
||||
const customer = await users.create({
|
||||
username: 'client' + stamp,
|
||||
email: 'c' + stamp + '@example.invalid',
|
||||
displayName: '隔离客户',
|
||||
password: randomBytes(24).toString('hex'),
|
||||
roleCode: 'enterprise_admin',
|
||||
tenantId: tenant.id,
|
||||
});
|
||||
const clientSession = await sessions.create(customer.id, 'client', 0);
|
||||
assert.equal(
|
||||
(await req(path, undefined, 'GET', { cookie: sessions.cookieName('client') + '=' + clientSession.token })).status,
|
||||
401,
|
||||
);
|
||||
assert.equal((await req(path, { rules: [], preserveFragments: false })).status, 400);
|
||||
assert.equal(
|
||||
(await req(path, { rules: [{ channelId: 'foreign', action: 'add' }], preserveFragments: true })).status,
|
||||
400,
|
||||
);
|
||||
const rules = [
|
||||
{ channelId: channel.id, action: 'add' },
|
||||
{ channelId: backup.id, action: 'remove' },
|
||||
];
|
||||
assert.equal((await req(path, { rules, preserveFragments: true })).status, 200);
|
||||
assert.deepEqual((await req(path).then((r) => r.json())).rules, rules);
|
||||
assert.equal(
|
||||
await db.operationLog.count({ where: { resourceId: template.id, action: 'sms_template.opt_out_policy.update' } }),
|
||||
1,
|
||||
);
|
||||
pass('real HTTP policy save/read, authentication, scope, mandatory fragment protection and durable audit');
|
||||
const reduced = await req('/admin/channels/' + channel.id, { carriers: ['mobile'] });
|
||||
assert.equal(reduced.status, 200, await reduced.text());
|
||||
assert.deepEqual((await db.smsChannel.findUnique({ where: { id: channel.id } })).carriers, ['mobile']);
|
||||
assert.equal(await db.smsChannelGroupItem.count({ where: { groupId: unicom.id, channelId: channel.id } }), 1);
|
||||
pass('carrier reduction saves with active group references preserved');
|
||||
const batch = await db.smsBatchTask.create({
|
||||
data: { tenantId: tenant.id, applicationId: application.id, taskNo: key(), content, phoneTotal: 1 },
|
||||
});
|
||||
const message = await db.smsMessageRecord.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
applicationId: application.id,
|
||||
signatureId: signature.id,
|
||||
batchTaskId: batch.id,
|
||||
messageId: key(),
|
||||
phoneNumber: '13800138000',
|
||||
carrier: 'mobile',
|
||||
content,
|
||||
billingUnits: 2,
|
||||
unitPrice: 425,
|
||||
amountCents: 850,
|
||||
},
|
||||
});
|
||||
// No templateId intentionally: direct_send must still match its template.
|
||||
const route = await chain.selectChannelForMessage(message);
|
||||
assert.equal(route.contentPolicy.content, content + '拒收请回复R');
|
||||
await chain.submitMessageToGateway(message, route, 0);
|
||||
const first = await db.smsSubmitRecord.findFirstOrThrow({ where: { messageRecordId: message.id } });
|
||||
const written = await db.smsMessageRecord.findUniqueOrThrow({ where: { id: message.id } });
|
||||
const outbox = await db.gatewaySubmitOutbox.findUniqueOrThrow({ where: { submitId: first.submitId } });
|
||||
assert.equal(written.originalContent, content);
|
||||
assert.equal(written.content, outbox.payload.content);
|
||||
assert.equal(first.sentContent, written.content);
|
||||
assert.equal(written.billingUnits, 2);
|
||||
assert.equal(Number(written.amountCents), 850);
|
||||
assert.equal(Number(first.costAmountCents), 650);
|
||||
const route2 = await chain.selectChannelForMessage(written, { excludeChannelIds: [channel.id] });
|
||||
await chain.submitMessageToGateway(written, route2, 1, first.id);
|
||||
const retried = await db.smsMessageRecord.findUniqueOrThrow({ where: { id: message.id } });
|
||||
assert.equal(retried.content, content);
|
||||
assert.equal(retried.originalContent, content);
|
||||
assert.equal(
|
||||
(await db.gatewaySubmitOutbox.findUniqueOrThrow({ where: { submitId: first.submitId } })).payload.content,
|
||||
content + '拒收请回复R',
|
||||
);
|
||||
assert.equal(await db.gatewaySubmitOutbox.count({ where: { messageRecordId: message.id, status: 'pending' } }), 2);
|
||||
pass('single submit and alternate-channel retry: real transactional content/Outbox snapshots and unchanged charges');
|
||||
const before = await db.smsSubmitRecord.count();
|
||||
await assert.rejects(
|
||||
db.$transaction((tx) =>
|
||||
completionContext.run({ tx, messageRecordId: message.id }, async () => {
|
||||
await chain.submitMessageToGateway(retried, route, 2);
|
||||
throw new Error('rollback verification');
|
||||
}),
|
||||
),
|
||||
/rollback verification/,
|
||||
);
|
||||
assert.equal(await db.smsSubmitRecord.count(), before);
|
||||
assert.equal((await db.smsMessageRecord.findUniqueOrThrow({ where: { id: message.id } })).content, content);
|
||||
pass('failed transaction rolls back content, submit and Outbox together');
|
||||
const gateway = chain.submission.gatewaySubmit;
|
||||
const many = [];
|
||||
for (let i = 0; i < 2; i++)
|
||||
many.push(
|
||||
await db.smsMessageRecord.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
applicationId: application.id,
|
||||
signatureId: signature.id,
|
||||
batchTaskId: batch.id,
|
||||
messageId: key(),
|
||||
phoneNumber: '13800138000',
|
||||
carrier: 'mobile',
|
||||
content,
|
||||
billingUnits: 2,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await gateway.processSendJobBatch(many.map((m) => ({ messageRecordId: m.id })));
|
||||
for (const m of many) {
|
||||
const row = await db.smsMessageRecord.findUniqueOrThrow({ where: { id: m.id } });
|
||||
assert.equal(row.content, content + '拒收请回复R');
|
||||
assert.equal(row.originalContent, content);
|
||||
}
|
||||
pass('microbatch uses the same policy and persists original text');
|
||||
for (const state of [
|
||||
{ status: 'delivered', submitStatus: 'accepted', receiptStatus: 'delivered' },
|
||||
{ status: 'submit_failed', submitStatus: 'rejected' },
|
||||
{ status: 'failed', submitStatus: 'accepted', receiptStatus: 'undelivered' },
|
||||
{ status: 'timeout', submitStatus: 'accepted' },
|
||||
]) {
|
||||
await db.smsMessageRecord.create({
|
||||
data: {
|
||||
tenantId: tenant.id,
|
||||
applicationId: application.id,
|
||||
signatureId: signature.id,
|
||||
messageId: key(),
|
||||
phoneNumber: '13800138000',
|
||||
content,
|
||||
...state,
|
||||
},
|
||||
});
|
||||
}
|
||||
const quality = await req('/admin/operations/signature-quality?keyword=' + encodeURIComponent(tenant.name)).then(
|
||||
(r) => r.json(),
|
||||
);
|
||||
const stat = quality.items.find((i) => i.signatureId === signature.id);
|
||||
assert.equal(stat.total, stat.successCount + stat.submitFailureCount + stat.failureCount + stat.unknownCount);
|
||||
assert.equal(stat.failureCount, 1);
|
||||
assert(stat.unknownCount >= 1);
|
||||
pass('real quality query yields four exclusive categories, including timeout without receipt');
|
||||
if (process.env.OPT_OUT_TEST_BROWSER === 'true') {
|
||||
const {
|
||||
chromium,
|
||||
} = require('C:/Users/hectorzhao/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
const ui = process.env.OPT_OUT_TEST_UI_URL || 'http://127.0.0.1:17438';
|
||||
assert.equal(new URL(ui).hostname, '127.0.0.1');
|
||||
const auth = await req('/admin/auth/session').then((r) => r.json());
|
||||
await page
|
||||
.context()
|
||||
.addCookies([{ name: cookie, value: session.token, url: ui, httpOnly: true, sameSite: 'Lax' }]);
|
||||
await page.addInitScript((data) => localStorage.setItem('cmpp-auth-session:admin', JSON.stringify(data)), auth);
|
||||
for (const [width, height] of [
|
||||
[1600, 1000],
|
||||
[1366, 768],
|
||||
[390, 844],
|
||||
]) {
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(ui + '/#/admin/enterprise-templates');
|
||||
const row = page.locator('.admin-enterprise-template-row').filter({ hasText: template.name }).first();
|
||||
await row.getByRole('button', { name: '拒收指令', exact: true }).click();
|
||||
await page.getByLabel(channel.name + '的拒收指令').waitFor();
|
||||
assert(await page.getByLabel('避免影响消息分片数').isDisabled());
|
||||
await page.screenshot({ path: `.local-data/template-optout-20260920/template-${width}.png`, fullPage: true });
|
||||
await page.getByRole('button', { name: '保存策略', exact: true }).click();
|
||||
await page.getByRole('heading', { name: '模板拒收指令', exact: true }).waitFor({ state: 'hidden' });
|
||||
await page.goto(ui + '/#/admin/analytics');
|
||||
await page.locator('.quality-status-bar__track').first().waitFor();
|
||||
assert.match(await page.locator('.quality-status-bar__track').first().getAttribute('title'), /未收到回执/);
|
||||
await page.screenshot({ path: `.local-data/template-optout-20260920/quality-${width}.png`, fullPage: true });
|
||||
await page.reload();
|
||||
await page.locator('.quality-status-bar__track').first().waitFor();
|
||||
await page.goto(ui + '/#/admin/sms-records');
|
||||
await page
|
||||
.locator('.admin-sms-record-card')
|
||||
.filter({ hasText: '拒收请回复R' })
|
||||
.first()
|
||||
.getByRole('button', { name: '查看发送详情' })
|
||||
.click();
|
||||
await page.getByRole('heading', { name: '原始短信内容', exact: true }).waitFor();
|
||||
await page.getByRole('heading', { name: /第 1 次提交通道内容/ }).waitFor();
|
||||
await page.screenshot({ path: `.local-data/template-optout-20260920/detail-${width}.png`, fullPage: true });
|
||||
await req('/admin/channels/' + channel.id, { carriers: ['mobile', 'unicom'] });
|
||||
await page.goto(ui + '/#/admin/channels');
|
||||
await page.getByLabel('通道名称', { exact: true }).fill(channel.name);
|
||||
await page.getByRole('button', { name: '查询', exact: true }).click();
|
||||
await page
|
||||
.locator('.sms-channel-table__row')
|
||||
.filter({ hasText: channel.name })
|
||||
.getByRole('button', { name: '编辑', exact: true })
|
||||
.click();
|
||||
await page.getByRole('heading', { name: '编辑通道', exact: true }).waitFor();
|
||||
await page.getByRole('checkbox', { name: '联通', exact: true }).uncheck();
|
||||
await page.screenshot({ path: `.local-data/template-optout-20260920/channel-${width}.png`, fullPage: true });
|
||||
await page.getByRole('button', { name: '确认', exact: true }).click();
|
||||
await page.getByRole('heading', { name: '编辑通道', exact: true }).waitFor({ state: 'hidden' });
|
||||
assert.deepEqual((await db.smsChannel.findUniqueOrThrow({ where: { id: channel.id } })).carriers, ['mobile']);
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
pass('real API browser saves, locked checkbox, route/refresh, four-segment bar and three sizes');
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
assert.equal(
|
||||
await db.gatewaySubmitOutbox.count({
|
||||
where: { messageRecordId: { in: [message.id, ...many.map((m) => m.id)] }, status: { not: 'pending' } },
|
||||
}),
|
||||
0,
|
||||
);
|
||||
writeFileSync(
|
||||
'.local-data/template-optout-20260920/fixture.json',
|
||||
JSON.stringify({ tenantId: tenant.id, templateId: template.id, messageId: message.id }),
|
||||
);
|
||||
pass('no command published, no Gateway/SMSC started, no external SMS sent');
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
Reference in New Issue
Block a user