fix: accept scheme-less drainage URLs

This commit is contained in:
hectorzhao
2026-08-28 17:58:45 +08:00
parent c68ac7a3db
commit 5328bb09bf
11 changed files with 50 additions and 13 deletions
+3
View File
@@ -48,6 +48,9 @@ describe('strict client write DTOs', () => {
it.each([
'https://example.com/path',
'example.com/path?source=sms',
'www.example.com',
'192.0.2.10:8080/landing',
'13800138000',
'+86 138-0013-8000',
'0755-12345678',
+3 -2
View File
@@ -19,6 +19,7 @@ import {
ValidateNested,
} from 'class-validator';
import { IsBoundedJsonObject } from './bounded-json-object.validator';
import { DRAINAGE_TARGET_ERROR, DRAINAGE_TARGET_PATTERN } from './drainage-target';
export class ClientCertificationSubmissionDto {
@IsString() @MinLength(1) @MaxLength(200) companyName!: string;
@@ -105,8 +106,8 @@ export class ClientDrainageInfoDto {
@IsString()
@MinLength(1)
@MaxLength(2048)
@Matches(/^(?:https?:\/\/\S+|(?:\+?86[\s-]?)?1(?:[\s-]?\d){10}|(?:\+?86[\s-]?)?(?:\(?0\d{2,3}\)?[\s-]?)?\d{7,8}(?:[\s-]?(?:转|ext\.?)?[\s-]?\d{1,6})?)$/i, {
message: '引流信息必须是 http/https URL、手机号码或固定电话号码',
@Matches(DRAINAGE_TARGET_PATTERN, {
message: DRAINAGE_TARGET_ERROR,
})
url!: string;
@IsOptional() @IsString() @MaxLength(1000) remark?: string;
+8
View File
@@ -0,0 +1,8 @@
export const DRAINAGE_TARGET_PATTERN = /^(?:(?:https?:\/\/)?(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{2,59})|(?:\d{1,3}\.){3}\d{1,3})(?::\d{1,5})?(?:[/?#]\S*)?|(?:\+?86[\s-]?)?1(?:[\s-]?\d){10}|(?:\+?86[\s-]?)?(?:\(?0\d{2,3}\)?[\s-]?)?\d{7,8}(?:[\s-]?(?:转|ext\.?)?[\s-]?\d{1,6})?)$/i;
export const DRAINAGE_TARGET_ERROR = '引流信息必须是 URL(可不带协议)、手机号码或固定电话号码';
export function normalizeDrainageTarget(value?: string) {
const target = value?.trim() ?? '';
return target && DRAINAGE_TARGET_PATTERN.test(target) ? target : undefined;
}
@@ -24,7 +24,7 @@ export class ReportOfficialExportService {
sheet.addRow(headers);
sheet.addRow(reportType === 'signature'
? ['示例签名', '验证码通知', '请在本单元格插入图片', '请在本单元格插入图片', '示例行,导入前请删除']
: ['示例签名', 'https://example.com 或 13800138000', '示例行,导入前请删除', '请在本单元格插入图片']);
: ['示例签名', 'example.com/path 或 13800138000', '示例行,导入前请删除', '请在本单元格插入图片']);
styleHeader(sheet.getRow(1));
sheet.columns.forEach((column) => { column.width = 24; });
sheet.getRow(2).height = 48;
+3 -6
View File
@@ -10,14 +10,11 @@ import { APPLICATION_DISABLE_GRACE_MS, DEFAULT_APPLICATION_DISABLE_SCAN_INTERVAL
import { SmsReportValidationService } from './report-validation.service';
import { SmsAuditService } from './audit.service';
import { shanghaiDateRange } from '../common/shanghai-date-range';
const DRAINAGE_TARGET_PATTERN = /^(?:https?:\/\/\S+|(?:\+?86[\s-]?)?1(?:[\s-]?\d){10}|(?:\+?86[\s-]?)?(?:\(?0\d{2,3}\)?[\s-]?)?\d{7,8}(?:[\s-]?(?:转|ext\.?)?[\s-]?\d{1,6})?)$/i;
import { DRAINAGE_TARGET_ERROR, normalizeDrainageTarget as readDrainageTarget } from '../common/drainage-target';
function normalizeDrainageTarget(value?: string) {
const target = value?.trim() ?? '';
if (!target || !DRAINAGE_TARGET_PATTERN.test(target)) {
throw new BadRequestException('引流信息必须是 http/https URL、手机号码或固定电话号码');
}
const target = readDrainageTarget(value);
if (!target) throw new BadRequestException(DRAINAGE_TARGET_ERROR);
return target;
}
@@ -1024,6 +1024,20 @@ describe('SmsConfigService', () => {
expect(prisma.channelSignatureReportTask.create).not.toHaveBeenCalled();
});
it('accepts a scheme-less drainage URL and synchronizes the compatibility name', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.findUnique.mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', auditStatus: 'approved' });
prisma.channelRouteRule.findMany.mockResolvedValue([]);
const service = new SmsConfigService(prisma as never);
await expect(service.createDrainageInfo('sig-1', { url: 'example.com/path', reportValues: {} }, {}, 'tenant-1'))
.resolves.toEqual(expect.objectContaining({ id: 'drainage-1', auditStatus: 'pending' }));
expect(prisma.smsDrainageInfo.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ siteName: 'example.com/path', url: 'example.com/path' }),
}));
});
it('resets an approved signature to pending when key content is changed', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.findUnique.mockResolvedValue({
@@ -2183,6 +2183,6 @@
## 2026-08-28 签名与引流三网状态及引流字段统一
- 客户端“签名与引流信息”列表将移动、联通、电信的真实报备可用性集中展示为同一组三网状态单元;每个单元同时显示运营商名称、状态指示和“报备通过/暂不可用”,签名与关联引流信息使用一致布局。状态继续来自应用当前路由、通道运营商范围和真实报备任务汇总,不新增静态兜底或前端推测。
- 引流信息的业务主字段统一为“引流 URL 或号码”,支持 `http/https` URL、手机号码和固定电话号码。客户端新增/修改弹窗不再显示或要求“名称”,原“访问地址”统一改名为“引流 URL 或号码”。
- 引流信息的业务主字段统一为“引流 URL 或号码”,支持 `http/https` 协议或不带协议的域名/URL、手机号码和固定电话号码。客户端新增/修改弹窗不再显示或要求“名称”,原“访问地址”统一改名为“引流 URL 或号码”。
- API 和服务端必须真实接受上述三种内容并拒绝无效任意文本。数据库既有 `SmsDrainageInfo.siteName` 仅作为兼容列保留,新建或修改时由后端同步写入规范化后的 URL/号码;客户端不得继续提交或依赖独立名称。
- 运营端单条审核列表、审核详情、导入审核、报备任务和报备记录统一只展示 URL/号码,不再把兼容列作为独立名称。批量导入映射和官方模板去掉“站点名称”必填列,只要求所属短信签名及“引流 URL 或号码”。
+1 -1
View File
@@ -4959,7 +4959,7 @@ npm run verify:phase8
| --- | --- | --- |
| TC-DRAINAGE-UI-001 | 客户端查看签名及展开后的引流信息列表 | 移动、联通、电信在同一组三网状态单元中清晰展示;每项含运营商名称和真实“报备通过/暂不可用”状态,签名与引流行布局一致 |
| TC-DRAINAGE-UI-002 | 客户端打开新增或修改引流信息弹窗 | 仅有必填“引流 URL 或号码”,不存在“名称”和“访问地址”;提示明确支持 URL、手机号码和固定电话号码 |
| TC-DRAINAGE-UI-003 | 分别提交 `https://example.com/path``13800138000``0755-12345678` | 三种输入均通过真实客户端 API 和服务端校验,写入 PostgreSQL;兼容 `siteName` 列与 `url` 列同步保存相同目标值,进入真实待审核流程 |
| TC-DRAINAGE-UI-003 | 分别提交 `https://example.com/path``example.com/path``www.example.com``13800138000``0755-12345678` | 带协议URL、不带协议URL、手机和固话均通过真实客户端 API 和服务端校验,写入 PostgreSQL;兼容 `siteName` 列与 `url` 列同步保存相同目标值,进入真实待审核流程 |
| TC-DRAINAGE-UI-004 | 提交普通文字或空值 | 前端禁止空值提交;绕过前端提交普通文字时后端返回受控参数错误,不写数据库、不创建审核记录 |
| TC-DRAINAGE-UI-005 | 运营端查看单条审核列表、详情、报备任务及报备记录 | 统一显示“引流 URL 或号码”及真实目标值,不再显示独立站点名称或旧“引流地址”标签;审核通过/驳回仍调用原真实 API |
| TC-DRAINAGE-UI-006 | 下载引流官方导入模板并配置导入映射 | 模板和映射仅要求“所属短信签名”“引流 URL 或号码”,不再要求“站点名称”;导入项进入真实审核批次 |
+13
View File
@@ -4136,3 +4136,16 @@ git diff --check
- C8只新增`docs/sendchain-service-decomposition-plan-20260828.md`,按认证、长短信、Inbox Repository/Worker、消息准备/持久化、Worker生命周期、路由、限速、Submit命令、Outbox和任务进度刷新分S0~S8实施;本轮没有修改发送链运行代码。
- 回归通过:认证相关3套26项、API全量Jest、前端34项、API/前端TypeScript、Vite生产构建、真实Redis多实例契约、ESLint、Prettier、结构质量和依赖缓解门禁。首次从仓库根目录启动Jest时误扫描`outputs/`历史副本且未加载API转换配置,随后使用`api/jest.config.cjs`明确项目范围通过;未修改受保护历史资产。
- 本轮运行代码没有变化,测试环境仍运行已验证的`226527f1cef122e4dbd163da782485e8c0254dc5`,因此没有重复建立恢复资产或形式化重新部署。未访问预生产,未写入测试环境业务数据,未发送、补发或重投短信。
## 2026-08-28 签名与引流三网状态及引流字段统一发布
- 重新验证本地基线为`main`,实施前`HEAD=origin/main=9ac41e9`;既有`docs/code-quality-reassessment-20260828-v2.md`修改、未跟踪`docs/code-quality-reassessment-20260828-v3.md`和空文件`=`均保留且未纳入提交。本轮功能提交为`c68ac7a3db82d1bdb867260378df18f93cca0fd2`,未推送远端。
- 客户端签名与引流列表将移动、联通、电信改为同一组三网状态卡片,签名和引流行统一显示真实汇总得到的“报备通过/暂不可用”。新增/修改弹窗移除“名称”,将“访问地址”统一为“引流 URL 或号码”;URL 保持安全外链,手机和固定电话按普通文本展示。
- 客户端和运营端不再提交独立`siteName`。后端 DTO 与领域服务真实接受`http/https` URL、手机号码和固定电话号码,拒绝空值及任意文字;既有非空兼容列由服务端同步写入同一规范化目标。单条审核、审核详情、导入审核、报备任务、报备记录、导入映射和官方模板同步移除独立站点名称口径,没有使用 mock、静态数据或 localStorage 代替产品功能。
- 本地门禁通过:前端 Vitest 5个文件36项、TypeScript、Vite生产构建和bundle预算通过;API Jest 51套571项、TypeScript正式构建和Prisma schema校验通过;Gateway`go test ./... -count=1``go vet ./...`、增量Lint/Prettier、结构质量及`git diff --check`通过。专项用例覆盖URL、手机、带格式手机、固定电话、分机号、非法文字拒绝、兼容列同步和官方模板无站点名称。
- 应用内Browser访问测试机在导航阶段连续两次超时并重置,未据此声称登录后线上页面已验收。随后用与正式发布一致的生产构建做本地API拦截视觉夹具:桌面展开态和新增弹窗、390px窄屏均渲染正常,页面级横向溢出为false,URL为`A`、固定电话为`SPAN`,弹窗不存在“名称/访问地址”,控制台事件0。夹具只用于布局回归,不作为线上业务数据证据。
- 部署前测试机真实基线为`226527f1cef122e4dbd163da782485e8c0254dc5`,源码与数据库均95项migration11项相关服务activeAPI/Gateway/Redis健康。新恢复点`/opt/cmpp-platform-backups/drainage-carrier-ui-20260828T092840Z`包含238MB PostgreSQL custom dump、211MB完整运行目录、24KB环境/systemd/Nginx配置、原部署标记、服务/Stream状态和恢复说明;`pg_restore --list`842行、运行tar 52592项、配置tar 309项,`sha256sum -c`全部成功后才开始覆盖。
- 精确Git归档`outputs/cmpp-drainage-carrier-ui-c68ac7a-20260828T092917Z.tar.gz`为2612752字节,本地和服务器SHA-256均为`ef39ebdef18efc5937b2e4001398d4cb4667f739c1fe6b4a2206208ff280054f`且tar可读。标准部署脚本完成两套依赖安装、安全门禁、Prisma、前端/API/Gateway构建、Nginx校验和服务重启;95项migration无待执行项,旧运行目录保留为`/opt/cmpp-platform.previous-drainage-20260828T093754Z`
- 测试环境最终`.deployed-commit=c68ac7a3db82d1bdb867260378df18f93cca0fd2`。11项相关服务均activeAPI、Gateway、Gateway Callback健康,Redis PONG`gateway.submit.commands``gateway.submit.results``gateway.protocol.logs`全部`pending=0/lag=0`,发布窗口7项核心服务error级journal均为0。
- 线上前端资源为`index-BLUa_d1S.js``index-BB9q6lcg.css``ClientSignaturesPage-COD8bZjo.js``AdminDrainageAuditPage-Dg8qCDBw.js`;服务器与工作站HTTP回读SHA-256一致。首页、客户端登录、运营端登录和API健康均HTTP 200。真实测试库当前未删除引流信息为0,因此未创建业务记录模拟页面数据,也未执行审核写操作;未发送、补发或重投短信,未修改余额、通道或客户配置。
- 仅部署测试环境`100.93.204.60`;预生产`8.160.169.106`未访问、未部署、未覆盖、未回退或修改。
@@ -64,8 +64,9 @@ export function DrainageFormModal({ applicationId, item, onClose, onSubmit }: {
<h3></h3>
<Input
label="* 引流url或号码"
hint="支持带或不带协议的 URL、手机号码或固定电话号码"
onChange={(event) => update('url', event.target.value)}
placeholder="请输入引流url或号码"
placeholder="例如:example.com/path 或 13800138000"
required
value={form.url}
/>
+1 -1
View File
@@ -251,7 +251,7 @@ function DrainageModal({ item, signature, onClose, onSaved }: { item?: ClientDra
>
<div className="signature-form">
{item?.rejectReason ? <div className="client-signature-reason"><strong></strong><span>{item.rejectReason}</span></div> : null}
<Input hint="支持 http/https URL、手机号码或固定电话号码" label="引流 URL 或号码" onChange={(event) => setUrl(event.target.value)} placeholder="例如:https://example.com 或 13800138000" required value={url} />
<Input hint="支持带或不带协议的 URL、手机号码或固定电话号码" label="引流 URL 或号码" onChange={(event) => setUrl(event.target.value)} placeholder="例如:example.com/path 或 13800138000" required value={url} />
<Textarea label="说明" onChange={(event) => setRemark(event.target.value)} rows={3} value={remark ?? ''} />
<section className="client-signature-form-section">
<div><h3></h3><p> URL </p></div>