diff --git a/api/src/operations/operations.service.spec.ts b/api/src/operations/operations.service.spec.ts index dc958ca..cb74d64 100644 --- a/api/src/operations/operations.service.spec.ts +++ b/api/src/operations/operations.service.spec.ts @@ -43,6 +43,7 @@ function createPrismaMock() { rechargeOrder: { findMany: jest.fn().mockResolvedValue([{ id: 'order-1', tenantId: 'tenant-1', amountCents: 1000 }]), }, + reportMaterialImportItem: { count: jest.fn().mockResolvedValue(0) }, smsTemplate: { count: jest.fn().mockResolvedValue(1), }, @@ -542,6 +543,7 @@ describe('OperationsService', () => { enterpriseCertifications: 1, smsAudits: 2, signatures: 1, + signatureImports: 0, drainageInfos: 0, templates: 1, total: 5, @@ -729,6 +731,7 @@ describe('OperationsService', () => { templates: 1, signatures: 1, drainageInfos: 0, + signatureImports: 0, total: 5, }); expect(prisma.smsTemplate.count).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1', auditStatus: 'pending' } }); @@ -740,6 +743,16 @@ describe('OperationsService', () => { expect(prisma.$queryRaw).not.toHaveBeenCalled(); }); + it('counts pending signature import rows within the requested tenant', async () => { + const prisma = createPrismaMock(); + prisma.reportMaterialImportItem.count.mockResolvedValue(19); + const service = new OperationsService(prisma as never); + await expect(service.pendingAudits('tenant-1')).resolves.toMatchObject({ signatureImports: 19, total: 24 }); + expect(prisma.reportMaterialImportItem.count).toHaveBeenCalledWith({ + where: { reportType: 'signature', status: 'pending_review', batch: { tenantId: 'tenant-1' } }, + }); + }); + it('rejects invalid send quality dates', async () => { const service = new OperationsService(createPrismaMock() as never); await expect(service.sendQuality('2026-02-31')).rejects.toThrow('统计日期无效'); diff --git a/api/src/operations/queries/dashboard.queries.ts b/api/src/operations/queries/dashboard.queries.ts index 3409928..2f9fa59 100644 --- a/api/src/operations/queries/dashboard.queries.ts +++ b/api/src/operations/queries/dashboard.queries.ts @@ -439,13 +439,17 @@ pendingAudits(tenantId?: string) { this.prisma.smsDrainageInfo.count({ where: { tenantId, auditStatus: 'pending' } }), this.prisma.enterpriseCertification.count({ where: { tenantId, status: 'pending' } }), this.prisma.smsSendTask.count({ where: { tenantId, status: 'pending_review' } }), - ]).then(([templates, signatures, drainageInfos, enterpriseCertifications, smsAudits]) => ({ + this.prisma.reportMaterialImportItem.count({ + where: { reportType: 'signature', status: 'pending_review', batch: { tenantId } }, + }), + ]).then(([templates, signatures, drainageInfos, enterpriseCertifications, smsAudits, signatureImports]) => ({ templates, signatures, drainageInfos, enterpriseCertifications, smsAudits, - total: templates + signatures + drainageInfos + enterpriseCertifications + smsAudits, + signatureImports, + total: templates + signatures + drainageInfos + enterpriseCertifications + smsAudits + signatureImports, })); } } diff --git a/docs/first-version-development-requirements.md b/docs/first-version-development-requirements.md index 7c551f7..e9f13c1 100644 --- a/docs/first-version-development-requirements.md +++ b/docs/first-version-development-requirements.md @@ -2186,3 +2186,8 @@ - 引流信息的业务主字段统一为“引流 URL 或号码”,支持带 `http/https` 协议或不带协议的域名/URL、手机号码和固定电话号码。客户端新增/修改弹窗不再显示或要求“名称”,原“访问地址”统一改名为“引流 URL 或号码”。 - API 和服务端必须真实接受上述三种内容并拒绝无效任意文本。数据库既有 `SmsDrainageInfo.siteName` 仅作为兼容列保留,新建或修改时由后端同步写入规范化后的 URL/号码;客户端不得继续提交或依赖独立名称。 - 运营端单条审核列表、审核详情、导入审核、报备任务和报备记录统一只展示 URL/号码,不再把兼容列作为独立名称。批量导入映射和官方模板去掉“站点名称”必填列,只要求所属短信签名及“引流 URL 或号码”。 + +### 2026-09-05 签名导入待审通知与通道成本展示 + +- 运营端右上角新增“签名导入待审”,按签名导入资料中 pending_review 的明细条数计入待审核总数,独立于正式签名待审;点击进入签名审核的导入页签。统计遵守批次tenantId范围,invalid/approved/rejected明细不计入;导入提交及审核成功后立即刷新,不预先改计数。 +- 短信通道组新建/编辑的省网、全国列表展示成本价格(元/条),使用通道现有unitPrice及10000单位/元换算,保留4位小数;零成本显示0.0000,缺失通道/价格显示“—”。本次不改变通道选择、路由优先级和保存行为。 diff --git a/docs/system-functional-test-cases.md b/docs/system-functional-test-cases.md index 3c4a929..e2e255b 100644 --- a/docs/system-functional-test-cases.md +++ b/docs/system-functional-test-cases.md @@ -5093,3 +5093,13 @@ npm run verify:phase8 | TC-REPORT-MATERIAL-DETAIL-001 | 查看包含当前图片字段、未删除历史字段及旧图片引用的报备资料 | 当前字段展示名称、代码、导出名及图片预览;未删除历史字段继续展示且同时显示字段名称和代码;图片可内联查看并保留下载入口,缺失内容显示明确占位 | | TC-REPORT-CHANNEL-IDENTITY-001 | 打开短信通道管理的报备详情 | 页面同时明确展示“通道名称”和“通道编号”,列表、筛选、状态修改和窄屏布局不受影响 | | TC-DASHBOARD-METRIC-ORDER-001 | 打开运营看板并按从左到右、从上到下读取指标 | 顺序为发送总量、消息分片数、总体成功率、到达率、活跃签名、消费金额、返还金额、计收金额、利润、利润率;所有数值继续来自真实API口径 | + +## 2026-09-05 签名导入通知与通道组成本回归 + +| 编号 | 操作 | 预期 | +| --- | --- | --- | +| TC-IMPORT-NOTICE-001 | 提交签名批量导入,打开右上角待审核任务并点击签名导入待审 | 导入成功后即时重新请求真实统计;pending_review明细逐条计数,跳转签名审核tab=import;失败不发送成功刷新事件 | +| TC-IMPORT-NOTICE-002 | 部分通过/驳回导入资料,并按租户查询统计 | 完成明细不计数,剩余待审保留,成功审核即时刷新;租户隔离,空租户结果0,不把无效行算作任务 | +| TC-CHANNEL-COST-001 | 新建/编辑通道组,查看省网和全国列表,分别使用非零、零及缺失价格 | 成本价格按现有单位换算为元/条并保留4位;零显示0.0000,缺失显示—;三尺寸表格内部滚动,无整页溢出;不修改价格和路由 | + +上述通知口径增加签名导入这一类审核任务,替代TC-ADMIN-NOTICE-0715-09中“仅五类”的数量限定;下游投递告警仍不计入待审核任务。 diff --git a/docs/testing-progress.md b/docs/testing-progress.md index 5faacb0..ec974e1 100644 --- a/docs/testing-progress.md +++ b/docs/testing-progress.md @@ -4550,3 +4550,17 @@ git diff --check - 发布后12项服务ActiveState/MainPID/NRestarts与发布前完全一致;API、独立Callback、Gateway及MinIO健康正常。PostgreSQL只读查询确认96项已完成migration、未完成0项;三条短信Stream组状态逐字一致,pending/lag均为0。发布窗口warning及以上journal为0行,相对恢复归档日志末尾的新增应用错误匹配为0;所有既有dist文件摘要校验通过。 - 工作站从真实HTTP入口回读index.html、主CSS和主JS:CSS/JS与本地构建字节摘要一致;index.html与服务器候选字节一致,相对本机构建仅有CR字符和末尾空白差异,去除CR并裁剪末尾空白后全文相同。发布后Playwright/Edge再次检查两端登录页三尺寸的首次进入、刷新和跨入口导航,共6个状态均非空、无横向溢出、无控制台/页面异常和失败HTTP响应。仅做未登录入口回归,未创建账号或绕过验证码,登录后业务交互仍未重跑;未以此冒充完整业务验收。 - 发布证据包含恢复点中的`release-manifest-verified.json`、候选及实际门禁日志、完整资源摘要、服务/Stream前后快照及journal,以及本机临时目录中的`browser-smoke.json`、6张截图和`http-after.json`。本段验收文档单独本地提交并同步测试机;不推送远端,不发送、补发、重投或重新入队短信,不修改余额、通道或客户配置。 + +## 2026-09-05 签名导入待审通知与通道组成本(本地提交) + +- 当前main基于15a1f9d,已有文档改动和未跟踪方案保留。本次仅授权修改并本地提交,不推送、不部署。需求见first-version-development-requirements.md末节,用例TC-IMPORT-NOTICE-001/002、TC-CHANNEL-COST-001。 +- 根因:导入资料先进入ReportMaterialImportItem,原pendingAudits仅统计正式签名等5类表,漏计导入待审;提交导入和审核完成未触发顶部刷新。真实测试库signature pending_review=19、approved=20、invalid=27、rejected=19,正式签名pending=0,解释原通知缺失。 +- 修复:新增signatureImports可兼容旧响应的计数字段,按reportType=signature/status=pending_review并经batch.tenantId限定计数,计入total;右上角新增“签名导入待审”直达/admin/signatures?tab=import。导入及审核成功时触发既有刷新事件;不改正式审核流程、不重复创建任务。通道组省网/全国列表展示unitPrice/10000、四位小数成本,缺失显示—,零显示0.0000;无CSS、数据库结构、队列、路由或价格写入改动。 +- 自动验证:前端13套65项通过,补充导入成功刷新事件断言后该2项定向测试通过;API全量55套执行,新增测试后619项通过、1项因既有dashboard响应断言未包含新字段而失败,更新该断言后operations定向32项全部通过(累计620项通过)。前端TypeScript/Vite和API构建通过;Stylelint、CSS治理15项通过。Chart既有包体告警仍在。 +- 质量门禁:format:check和lint已执行,未宣称全绿。HEAD与本轮逐文件ESLint对照确认dashboard.queries.ts原有39条unused等错误、通道组表单原有1条set-state-in-effect错误,另有2条既有依赖警告,本轮相同。Prettier基线证明API查询、API测试、通道组表单3文件原已不通过;AdminLayout新增格式已修正。为保持最小范围未夹带整文件格式化或历史Hooks重构。git diff --check通过。 +- 真实验证:从候选构建提取实际pendingAudits函数,在测试机用真实Prisma/PostgreSQL只读执行;全平台和所选QA租户signatureImports=19/total=21,不存在租户全0。没有替换服务器应用文件或启动新业务服务。 +- 浏览器:本地生产预览http://127.0.0.1:4173代理测试环境真实API,正常算术验证码登录临时运营账号。1600×1000、1366×768、390×844进入及刷新通道组编辑页,两个成本列存在、真实零成本显示0.0000、无整页溢出;右上角新入口在三尺寸均进入导入审核页签;pageerror和console error均0。Browser技能不可用,按前端测试技能用Playwright/Edge。证据在本机临时目录cmpp-notice-cost-qa/browser.json、截图和lint-baseline.json。 +- 验收边界:测试机API仍是15a1f9d,故候选页面上的新计数按旧响应兼容显示0;新统计用真实数据库函数单独验证,未把分段证据冒充新版本端到端部署验收。未执行新的文件上传/审核写入、通道组保存、非零/缺失价格真实夹具及失败注入;不发送短信,不改余额或通道配置。临时用户精确删除后0残留,logout后无剩余Redis会话,本机凭据/storageState及服务器凭据已清理。 +- 后续交互建议(未实施):现有历史优先级10/20与编辑下拉1—5不兼容;同优先级/同省份会过滤原行。建议先修正历史值回显和冲突处理,再加入全国路由上下移动/拖拽、明确顺序预览、可搜索通道选择及未保存提示;与本次成本展示分开实施。 +- 提交前补充:签名审核页页签由URL直接派生,修正已停留在该页时通知只改query而未切换页签的问题;新增同路由query切换回归测试通过。该文件另有HEAD已存在的1条preserve-manual-memoization错误和1条依赖警告,未扩展整改。 +- 最终前端全量14套66项通过,最终TypeScript/Vite生产构建通过;本轮共13个目标文件精确暂存,已有文档段落保持未暂存。 diff --git a/src/api/types/identity-config.ts b/src/api/types/identity-config.ts index c213ea0..1205f7a 100644 --- a/src/api/types/identity-config.ts +++ b/src/api/types/identity-config.ts @@ -128,6 +128,7 @@ export type UserPayload = { }; export type PendingAuditCounts = { + signatureImports?: number; enterpriseCertifications: number; smsAudits: number; templates: number; diff --git a/src/apps/admin/AdminChannelGroupFormPage.tsx b/src/apps/admin/AdminChannelGroupFormPage.tsx index c29a1ab..1121130 100644 --- a/src/apps/admin/AdminChannelGroupFormPage.tsx +++ b/src/apps/admin/AdminChannelGroupFormPage.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { CheckCircle2, Info, Pencil, Plus, RadioTower, Trash2 } from 'lucide-react'; import { useNavigate, useParams } from 'react-router-dom'; import { adminApi, type AdminChannel, type ChannelGroup } from '@/api/adminApi'; +import { formatRateAmount, MONEY_UNITS_PER_YUAN } from '@/utils/currency'; import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui'; type Carrier = 'mobile' | 'unicom' | 'telecom'; @@ -227,6 +228,15 @@ export function AdminChannelGroupFormPage() { width: '280px', render: (route) => channelById.get(route.channelId)?.name ?? '未命名通道', }, + { + key: 'cost', + title: '成本价格(元/条)', + width: '180px', + render: (route) => { + const price = channelById.get(route.channelId)?.unitPrice; + return price == null ? '—' : formatRateAmount(price / MONEY_UNITS_PER_YUAN); + }, + }, { key: 'status', title: '通道状态', @@ -253,6 +263,15 @@ export function AdminChannelGroupFormPage() { width: '280px', render: (route) => channelById.get(route.channelId)?.name ?? '未命名通道', }, + { + key: 'cost', + title: '成本价格(元/条)', + width: '180px', + render: (route) => { + const price = channelById.get(route.channelId)?.unitPrice; + return price == null ? '—' : formatRateAmount(price / MONEY_UNITS_PER_YUAN); + }, + }, { key: 'status', title: '通道状态', diff --git a/src/apps/admin/AdminSignatureAuditPage.test.tsx b/src/apps/admin/AdminSignatureAuditPage.test.tsx new file mode 100644 index 0000000..d9e89f9 --- /dev/null +++ b/src/apps/admin/AdminSignatureAuditPage.test.tsx @@ -0,0 +1,21 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter, Link } from 'react-router-dom'; +import { expect, it, vi } from 'vitest'; +import { AdminSignatureAuditPage } from './AdminSignatureAuditPage'; + +vi.mock('@/api/adminApi', () => ({ adminApi: { listEnterpriseSignatures: vi.fn().mockResolvedValue([]) } })); +vi.mock('./ReportImportAuditPanel', () => ({ ReportImportAuditPanel: () =>
{error}
: null}