From 344b2afe3bafac92295c4f54eb03ea12118a0edb Mon Sep 17 00:00:00 2001 From: hectorzhao Date: Mon, 13 Jul 2026 10:16:12 +0800 Subject: [PATCH] fix: clarify signature reporting status --- .../dictionaries/dictionaries.service.spec.ts | 16 +++++++ api/src/dictionaries/dictionaries.service.ts | 6 ++- .../first-version-development-requirements.md | 4 +- docs/system-functional-test-cases.md | 4 ++ docs/testing-progress.md | 7 +++ index.html | 2 +- src/apps/admin/AdminChannelReportPage.tsx | 9 +++- src/apps/admin/AdminDrainageFieldsPage.tsx | 5 +- .../admin/AdminEnterpriseApplicationsPage.tsx | 1 - .../admin/AdminEnterpriseSignaturesPage.tsx | 46 +++++++++++++------ src/components/ui/Select.tsx | 29 ++++++++++-- src/styles/components.css | 30 ++++++++++++ src/styles/global.css | 7 ++- 13 files changed, 140 insertions(+), 26 deletions(-) diff --git a/api/src/dictionaries/dictionaries.service.spec.ts b/api/src/dictionaries/dictionaries.service.spec.ts index 2e268f4..e52b70e 100644 --- a/api/src/dictionaries/dictionaries.service.spec.ts +++ b/api/src/dictionaries/dictionaries.service.spec.ts @@ -128,4 +128,20 @@ describe('DictionariesService', () => { ); expect(prisma.drainageField.create).toHaveBeenCalledTimes(1); }); + + it('only accepts Arabic numerals and English letters in report field codes', async () => { + const prisma = createPrismaMock(); + const service = new DictionariesService(prisma as never); + + await expect(service.createDrainageField({ code: 'License2026', name: '营业执照', fieldType: 'image' })).resolves.toEqual( + expect.objectContaining({ code: 'License2026' }), + ); + expect(() => service.createDrainageField({ code: 'license_code', name: '营业执照', fieldType: 'image' })).toThrow( + 'code must contain only Arabic numerals and English letters', + ); + expect(() => service.createDrainageField({ code: '营业执照', name: '营业执照', fieldType: 'image' })).toThrow( + 'code must contain only Arabic numerals and English letters', + ); + expect(prisma.drainageField.create).toHaveBeenCalledTimes(1); + }); }); diff --git a/api/src/dictionaries/dictionaries.service.ts b/api/src/dictionaries/dictionaries.service.ts index 560b8e5..beb5422 100644 --- a/api/src/dictionaries/dictionaries.service.ts +++ b/api/src/dictionaries/dictionaries.service.ts @@ -255,12 +255,16 @@ export class DictionariesService { } createDrainageField(data: CreateDrainageFieldDto) { + const code = data.code?.trim(); + if (!code || !/^[A-Za-z0-9]+$/.test(code)) { + throw new BadRequestException('code must contain only Arabic numerals and English letters'); + } if (!['string', 'image', 'file'].includes(data.fieldType)) { throw new BadRequestException('fieldType must be string, image or file'); } return this.prisma.drainageField.create({ data: { - code: data.code, + code, name: data.name, fieldType: data.fieldType, required: data.required ?? false, diff --git a/docs/first-version-development-requirements.md b/docs/first-version-development-requirements.md index 9513c08..c0b72fc 100644 --- a/docs/first-version-development-requirements.md +++ b/docs/first-version-development-requirements.md @@ -181,7 +181,7 @@ ### 4.7 通道签名报备 -1. 运营端先在“报备字段库”维护字段编码、名称、类型、是否必填等标准定义;字段类型只允许字符串、图片、文件三种。通道报备详情只能从字段库选择字段,并指定用途为签名报备、引流信息报备或两者共用,不得在通道内另建同名孤立字段。 +1. 运营端先在“报备字段库”维护字段编码、名称、类型、是否必填等标准定义;字段代码只允许阿拉伯数字和英文大小写字母,字段类型只允许字符串、图片、文件三种。通道报备详情只能从字段库选择字段,并指定用途为签名报备、引流信息报备或两者共用,不得在通道内另建同名孤立字段。 2. 通道组配置通道,企业应用通过路由规则选择通道组。企业签名和引流信息编辑时,系统必须沿“企业应用 -> 生效路由规则 -> 通道组 -> 组内通道 -> 通道报备字段”实时解析字段合集。 3. 同一字段被多个通道引用时按字段库记录去重;任一通道将该字段配置为必填,则企业资料中按必填处理,并保留该字段来源的全部通道用于后续分别报备。 4. 企业签名弹窗只展示签名报备/两者共用字段;每条引流信息只展示引流信息报备/两者共用字段。文件字段走真实对象存储上传,其他字段保存真实值,必填校验同时在前端和 NestJS API 执行。 @@ -421,6 +421,8 @@ - 企业应用列表提供 CMPP 连接参数查看与一键复制能力,参数来源于真实应用/通道配置,不允许只在前端拼接假数据。 - 企业应用新增/编辑必须展示发送队列等级,并真实保存普通队列或优先队列配置;列表或详情应能看到该配置,便于运营核对高优先级应用。 - 企业应用新增时选择企业必须使用通用下拉控件和真实企业接口;下拉控件的视觉、尺寸、禁用态、错误态应与系统内其他 Select 保持一致。 +- 所有企业和企业应用下拉控件必须支持按名称搜索;企业应用管理列表不展示 AppID。 +- 企业短信模板列表必须采用自适应布局,在常用桌面及平板视口下无需水平滚动即可看到编辑、删除等操作。 ### 5.12 运营端审核 diff --git a/docs/system-functional-test-cases.md b/docs/system-functional-test-cases.md index 2a4be1c..4b17329 100644 --- a/docs/system-functional-test-cases.md +++ b/docs/system-functional-test-cases.md @@ -3205,6 +3205,10 @@ npm run verify:phase8 | --- | --- | --- | | TC-MOCK-CLEAN-001 | 断开 API 或让 API 返回 500,访问客户端充值套餐、账单流水、批量任务、短信发送、签名、模板页面。 | 页面展示错误态或空态;不得出现前端静态套餐、任务、模板、签名或最近发送记录。 | | TC-MOCK-CLEAN-002 | 访问运营端数据统计、账务账户、发送监控、安全控制、手机号段库、报备字段库、通道组、报备任务、报备记录。 | 所有列表和卡片来自真实 API;新增动作写入数据库;后端缺失的编辑/删除能力不得用本地状态伪造。 | +| TC-UI-ENTERPRISE-SELECT-001 | 逐一打开包含企业或企业应用选择的表单和筛选项,输入部分企业/应用名称。 | 下拉面板提供搜索框并实时缩小真实 API 选项范围;清空后恢复全部选项。 | +| TC-UI-TEMPLATE-RESPONSIVE-001 | 在 1024px、1366px 和宽屏视口打开企业短信模板页。 | 模板卡片自适应换列,页面不出现水平滚动,每张卡片的编辑和删除按钮直接可见。 | +| TC-ADMIN-REPORT-FIELD-CODE-001 | 在报备字段库分别提交 `License2026`、`license_code`、中文和空白代码,并直接调用真实新增 API 复验。 | 只有 `License2026` 写入 PostgreSQL;前端阻止非法值,API 同样返回 400,不依赖前端校验。 | +| TC-ADMIN-CHANNEL-REPORT-SIGNATURE-001 | 打开包含数据库签名 `【安徽航天信息】` 的通道报备详情及签名详情弹窗。 | 两处均只显示单层 `【安徽航天信息】`,不出现重复中括号。 | | TC-MOCK-CLEAN-003 | 运营端创建企业、编辑企业、禁用/启用企业、删除企业,再刷新页面和重新登录客户端。 | Tenant 状态持久化;列表刷新后状态不丢;禁用/删除企业阻断客户端业务访问;动作写系统日志。 | | TC-MOCK-CLEAN-004 | 客户端提交签名材料文件、创建模板并提交审核,运营端查看企业签名和企业模板列表。 | 文件元数据和材料关联写入后端;签名/模板进入真实审核状态;运营端列表可查到同一条记录。 | | TC-MOCK-CLEAN-005 | 运营端短信审核通过、批量通过、驳回风控审核任务。 | 调用 `admin/risk-review/tasks` 真实接口;通过必须弹窗确认;状态刷新后仍持久化;不再显示固定手机号样例。 | diff --git a/docs/testing-progress.md b/docs/testing-progress.md index 6e2802d..34119f5 100644 --- a/docs/testing-progress.md +++ b/docs/testing-progress.md @@ -1645,9 +1645,16 @@ git diff --check - 已执行 `send-chain.service.spec.ts` 40 项通过,覆盖全局 reporting 可发、主通道未通过而备用通道通过时选备用,以及所有候选均未通过时拒绝;API build 和前端 build 通过。 - 已提交并 push `ade06058` 后部署生产;部署前完成 PostgreSQL 与 `eab05958` 运行源码备份,无待执行 migration。生产 `.deployed-commit=ade06058`,`cmpp-api`、`cmpp-gateway`、Nginx、MinIO 均为 active,`12026/17890/8090/3000` 监听,API/Gateway health 与外部 HTTP 200。生产源码已确认包含 approved 通道候选过滤;【安徽航天信息】当前仍为全局 reporting、一个通道 approved、一个通道 pending,未主动发送计费短信,交由用户使用真实业务流量验证只走 approved 通道。 +## 2026-07-13 企业签名审核与通道报备状态展示分离 + +- 生产核查【安徽航天信息】确认状态保存成功:两个 `carrier=all` 通道分别为 approved、reporting,三网真实汇总均为 reporting 且 1/2 通过;Bug 在于前端将 reporting 转换成旧 pending,再显示成“审核中”,并丢失通过数/总数。 +- 企业签名列表和报备详情改为直接使用 API `carrierReportSummary.status/approved/total`:展示未报备、报备中、部分通过(x/y)、全部通过(x/y)、资料待补充、报备失败和不适用;签名 `auditStatus` 另列显示草稿、待审核、已通过、已驳回。详情同时列出每个目标通道的真实状态,不再读取 `drainageInfo.carrierStatus` 冒充当前汇总。 +- 按用户授权,本次将工作区其他会话的字段代码校验、Select 搜索、应用列表及样式等改动一并测试和发布;API 全量 13 suites、137 项通过,API build、前端 build、`git diff --check` 通过。 + ## 2026-07-13 企业页面可用性与报备展示修正 - 企业短信模板卡片改为自适应列宽,卡片和操作区允许换行,避免中等视口下固定三列造成水平滚动。 - 企业应用管理列表隐藏 AppID 列;共用 `Select` 对标签中包含“企业”或“应用”的下拉自动提供名称搜索,选项仍来自各页已接入的真实 API。 - 报备字段代码在前端和 NestJS API 双层限制为 `A-Z/a-z/0-9`;API 会拒绝下划线、中文、空白等非法代码,防止绕过页面写入 PostgreSQL。 - 运营端浏览器标题更新为“聆界短信管理平台”。通道报备详情和签名详情弹窗在展示前剔除数据中已存在的外层中/英文括号,统一只渲染一层中括号。 +- 本地验证:API 全量 13 suites、137 项通过,API build、前端 build 和 `git diff --check` 通过;前端仅有既有 chunk size warning。应用内浏览器确认本地运营端登录路由、非空 DOM、无框架错误覆盖,且标题为“聆界短信管理平台”;因本地未启动真实 API,验证码请求返回 502,无法进入登录后页面完成模板响应式、下拉搜索和签名实数据的视觉点击验收,未注入 mock 或绕过认证。截图阶段应用内浏览器页面挂载超时,未将截图标记为通过。 diff --git a/index.html b/index.html index d54d08d..323bfa3 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - CMPP 短信平台 + 聆界短信管理平台
diff --git a/src/apps/admin/AdminChannelReportPage.tsx b/src/apps/admin/AdminChannelReportPage.tsx index 69baaf1..91e9138 100644 --- a/src/apps/admin/AdminChannelReportPage.tsx +++ b/src/apps/admin/AdminChannelReportPage.tsx @@ -26,6 +26,11 @@ function asRecord(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } +function formatSignatureName(value?: string | null) { + const name = String(value ?? '-').trim().replace(/^[【\[]+|[】\]]+$/g, ''); + return `【${name || '-'}】`; +} + function drainageItems(signature?: ClientSmsSignature) { const payload = asRecord(signature?.drainageInfo); return Array.isArray(payload.links) ? payload.links.filter((item): item is DrainageItem => Boolean(item) && typeof item === 'object') : []; @@ -47,7 +52,7 @@ function DetailModal({ drainage, signature, task, onClose }: { drainage?: Draina return ( 关闭} onClose={onClose} open size="xl" title={drainage ? '查看引流信息详情' : '查看签名详情'}>
- {drainage ? String(drainage.siteName || drainage.url || '引流信息') : `【${signature?.name ?? task.signature?.name ?? '-'}】`} + {drainage ? String(drainage.siteName || drainage.url || '引流信息') : formatSignatureName(signature?.name ?? task.signature?.name)}

企业{signature?.tenant?.name ?? task.tenantId}

企业应用{signature?.application?.name ?? '-'}

{!drainage ? <>

签名依据{String(profile.basis ?? '-')}

公司名称{String(profile.companyName ?? '-')}

统一社会信用代码{String(profile.creditCode ?? '-')}

: null} @@ -170,7 +175,7 @@ export function AdminChannelReportPage() { return
-
【{signature?.name ?? task.signature?.name ?? '-'}】{signature?.tenant?.name ?? task.tenantId}{links.length ? {links.length} : null}
+
{formatSignatureName(signature?.name ?? task.signature?.name)}{signature?.tenant?.name ?? task.tenantId}{links.length ? {links.length} : null}
diff --git a/src/apps/admin/AdminDrainageFieldsPage.tsx b/src/apps/admin/AdminDrainageFieldsPage.tsx index dd5dbd8..6b3afb6 100644 --- a/src/apps/admin/AdminDrainageFieldsPage.tsx +++ b/src/apps/admin/AdminDrainageFieldsPage.tsx @@ -34,6 +34,7 @@ export function AdminDrainageFieldsPage() { const [fieldType, setFieldType] = useState('string'); const [description, setDescription] = useState(''); const [error, setError] = useState(''); + const codeError = code && !/^[A-Za-z0-9]+$/.test(code) ? '字段代码只能包含阿拉伯数字和英文大小写字母' : ''; function loadData() { adminApi.listDrainageFields() @@ -106,7 +107,7 @@ export function AdminDrainageFieldsPage() { footer={( <> - + )} onClose={() => setCreating(false)} @@ -114,7 +115,7 @@ export function AdminDrainageFieldsPage() { title="添加报备字段" >
- setCode(event.target.value)} value={code} /> + setCode(event.target.value)} placeholder="仅允许数字和英文字母" value={code} /> setName(event.target.value)} value={name} /> setSearchKeyword(event.target.value)} onKeyDown={(event) => event.stopPropagation()} placeholder={searchPlaceholder ?? '输入名称搜索'} value={searchKeyword} /> + + ) : null} + {visibleOptions.map((option) => (
) : null}
diff --git a/src/styles/components.css b/src/styles/components.css index 3a360bc..a8f0307 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -664,6 +664,36 @@ color: var(--color-text-inverse); } +.ui-select__search { + align-items: center; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + display: flex; + gap: var(--space-2); + margin: var(--space-1); + min-height: 36px; + padding: 0 var(--space-3); +} + +.ui-select__search:focus-within { + border-color: var(--color-selected); +} + +.ui-select__search input { + background: transparent; + border: 0; + color: var(--color-text); + min-width: 0; + outline: 0; + width: 100%; +} + +.ui-select__empty { + color: var(--color-text-muted); + padding: var(--space-3); + text-align: center; +} + .ui-tag { align-items: center; border-radius: var(--radius-full); diff --git a/src/styles/global.css b/src/styles/global.css index 6a28ea6..9231a6a 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -3648,7 +3648,8 @@ h3 { .template-card-grid { display: grid; gap: var(--space-5); - grid-template-columns: repeat(3, minmax(280px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); + min-width: 0; } .template-card { @@ -3658,6 +3659,7 @@ h3 { display: grid; gap: var(--space-4); min-height: 300px; + min-width: 0; overflow: hidden; padding: var(--space-6); position: relative; @@ -3726,6 +3728,8 @@ h3 { align-items: center; border-top: 1px solid var(--color-border); display: flex; + flex-wrap: wrap; + gap: var(--space-3); justify-content: space-between; margin-top: auto; padding-top: var(--space-4); @@ -3737,6 +3741,7 @@ h3 { .template-card-footer div { display: flex; + flex-wrap: wrap; gap: var(--space-3); }