fix: restore enterprise application lists

This commit is contained in:
hectorzhao
2026-07-29 08:11:51 +08:00
parent 751c03d421
commit 500f43f673
6 changed files with 31 additions and 2 deletions
@@ -1,4 +1,5 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { SmsConfigService } from './sms-config.service'; import { SmsConfigService } from './sms-config.service';
function createPrismaMock() { function createPrismaMock() {
@@ -240,7 +241,12 @@ describe('SmsConfigService', () => {
expect(prisma.smsApplication.findMany).toHaveBeenCalledWith(expect.objectContaining({ expect(prisma.smsApplication.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({ status: 'active', tenant: { name: { contains: '租户' } }, name: { contains: '应用' } }), where: expect.objectContaining({ status: 'active', tenant: { name: { contains: '租户' } }, name: { contains: '应用' } }),
include: { tenant: true, ipAllowlist: true, httpConfig: true }, include: { tenant: true, ipAllowlist: true, httpConfig: true },
omit: { secretHash: true },
})); }));
const applicationModel = Prisma.dmmf.datamodel.models.find((model) => model.name === 'SmsApplication');
const applicationFields = new Set(applicationModel?.fields.map((field) => field.name));
const omittedFields = Object.keys(prisma.smsApplication.findMany.mock.calls[0][0].omit);
expect(omittedFields.every((field) => applicationFields.has(field))).toBe(true);
expect(prisma.smsApplication.findMany.mock.calls[0][0]).not.toHaveProperty('take'); expect(prisma.smsApplication.findMany.mock.calls[0][0]).not.toHaveProperty('take');
expect(prisma.smsMessageRecord.groupBy).toHaveBeenCalledWith(expect.objectContaining({ expect(prisma.smsMessageRecord.groupBy).toHaveBeenCalledWith(expect.objectContaining({
by: ['applicationId', 'status'], by: ['applicationId', 'status'],
-1
View File
@@ -213,7 +213,6 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
}, },
omit: { omit: {
secretHash: true, secretHash: true,
passwordCipher: true,
}, },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
...(query.page && query.pageSize ? { ...(query.page && query.pageSize ? {
@@ -1809,3 +1809,9 @@
- 企业应用和签名等下拉框使用轻量选项接口,不得为了选择器加载连接、资料、报备任务等完整关联对象。 - 企业应用和签名等下拉框使用轻量选项接口,不得为了选择器加载连接、资料、报备任务等完整关联对象。
- 分页排序字段应有数据库索引;Nginx 对 JSON、JavaScript、CSS、CSV、文本和 SVG 响应启用 gzip,降低传输与解析等待。 - 分页排序字段应有数据库索引;Nginx 对 JSON、JavaScript、CSS、CSV、文本和 SVG 响应启用 gzip,降低传输与解析等待。
- 通道组本轮按用户明确要求排除,保留现状,后续如数据规模扩大再单独改造。 - 通道组本轮按用户明确要求排除,保留现状,后续如数据规模扩大再单独改造。
## 企业应用列表字段安全约束(2026-07-29)
- 企业应用列表及分页接口只能在 Prisma 查询中引用 `SmsApplication` 模型真实存在的字段;接口入参或响应别名(例如 `passwordCipher`)不得作为数据库 `select``omit` 或筛选字段。
- 企业应用列表必须排除真实敏感字段 `secretHash`,不得通过列表接口返回应用接入密码;密码只允许在既有受控的专用参数接口中按权限读取。
- 企业应用黑名单等页面加载筛选项时应使用轻量企业应用选项接口,避免依赖包含连接状态和统计信息的完整企业应用列表。
+8
View File
@@ -4009,3 +4009,11 @@ npm run verify:phase8
1. 应用分页索引 migration。 1. 应用分页索引 migration。
2. 对短信、任务、报备、应用、签名、模板、通道、上行和充值分页 SQL 执行计划进行检查。 2. 对短信、任务、报备、应用、签名、模板、通道、上行和充值分页 SQL 执行计划进行检查。
3. 携带 `Accept-Encoding: gzip` 请求大于 1KB 的 JSON,验证响应 `Content-Encoding: gzip` 3. 携带 `Accept-Encoding: gzip` 请求大于 1KB 的 JSON,验证响应 `Content-Encoding: gzip`
## 2026-07-29 企业应用列表 Prisma 字段回归用例
| 用例编号 | 场景 | 预期结果 |
| --- | --- | --- |
| TC-APP-LIST-001 | 运营端打开企业应用列表并请求分页接口 | Prisma 查询仅排除真实存在的 `secretHash`;接口 HTTP 200,不因 DTO 字段 `passwordCipher` 触发 Prisma 校验错误 |
| TC-APP-LIST-002 | 运营端打开企业应用黑名单并加载企业应用筛选项 | 黑名单及筛选项均从真实后端返回;企业应用加载不触发 HTTP 500,列表响应不包含 `secretHash` |
| TC-APP-LIST-003 | 校验企业应用列表 Prisma `omit` 字段 | 所有 `omit` 键都存在于当前生成客户端的 `SmsApplication` DMMF 模型,模型字段变更或错误别名会使回归测试失败 |
+10
View File
@@ -2688,3 +2688,13 @@ git diff --check
- 发布后真实`OperationsService`在与问题复现相同的`2026-07-27``2026-07-28`范围查询第一页25条:总数443,当前页46条提交、90条回执、25条下游投递,序列化后约110139字节,真实PostgreSQL查询及对象构造耗时约171.4ms;原全量响应约4.47MB,体积下降约97.6%。Nginx对Vite的`text/javascript`资源真实返回`Content-Encoding: gzip`,不是仅写配置未生效。 - 发布后真实`OperationsService`在与问题复现相同的`2026-07-27``2026-07-28`范围查询第一页25条:总数443,当前页46条提交、90条回执、25条下游投递,序列化后约110139字节,真实PostgreSQL查询及对象构造耗时约171.4ms;原全量响应约4.47MB,体积下降约97.6%。Nginx对Vite的`text/javascript`资源真实返回`Content-Encoding: gzip`,不是仅写配置未生效。
- API、Gateway、Nginx、PostgreSQL、MinIO和Redis均active`12026/17890/8090/3000/6379/5432/9000`监听;内外健康接口、首页、运营端和客户端均HTTP 200,公网CMPP 17890可连接。Redis Stream消费者1、`pending=0``lag=0`API/Gateway近10分钟error级journal为0。 - API、Gateway、Nginx、PostgreSQL、MinIO和Redis均active`12026/17890/8090/3000/6379/5432/9000`监听;内外健康接口、首页、运营端和客户端均HTTP 200,公网CMPP 17890可连接。Redis Stream消费者1、`pending=0``lag=0`API/Gateway近10分钟error级journal为0。
- 4条active供应商通道均为`connected 1/1`;最近120秒活跃下游客户连接为0。本轮未发送、重投或补发真实短信,未修改通道账号、密码、启停状态、企业余额或客户连接。 - 4条active供应商通道均为`connected 1/1`;最近120秒活跃下游客户连接为0。本轮未发送、重投或补发真实短信,未修改通道账号、密码、启停状态、企业余额或客户连接。
## 2026-07-29 企业应用列表 Prisma 字段回归修复(发布前)
- 预生产企业应用页面和企业应用黑名单页面 HTTP 500 已定位为列表查询 `omit` 错误引用 DTO/响应别名 `passwordCipher`;真实 `SmsApplication` 模型只保存敏感字段 `secretHash`Prisma 7.9.0 在执行 SQL 前抛出 `PrismaClientValidationError`
- 企业应用列表查询已移除不存在的 `passwordCipher`,继续明确排除真实敏感字段 `secretHash`。企业应用黑名单改为调用轻量企业应用选项接口,不再为了筛选项加载连接状态、统计及完整关联;本次修复不修改企业、应用、黑名单、通道、余额或短信数据。
- 新增回归断言:企业应用查询的 `omit` 必须等于 `{ secretHash: true }`,且每个排除字段都必须存在于当前生成 Prisma Client 的 `SmsApplication` DMMF 模型,避免 Mock 测试再次接受不存在的数据库字段。
- 代码检查确认短信入库和通道路由不会调用手机号段页面分页接口:运营商直接读取启用的 `PhoneCarrierRule`,省份通过唯一索引 `PhoneSegment.prefix` 逐级精确查询;本次页面列表错误不影响号码段识别链路。
- Node.js v24.14.0 下短信配置定向 1 suite / 62 tests、API 全量 26 suites / 367 tests 全部通过;Prisma format、validate、generate、API TypeScript 正式构建、前端 TypeScript/Vite 生产构建、Gateway `go test ./...``go vet ./...`、依赖安全门禁和 `git diff --check` 均通过。Jest 仅保留既有强制结束异步句柄提示,Vite 仅保留既有大 chunk 体积事实。
- 发布前使用预生产真实 Prisma Client 和 PostgreSQL 只读执行 `SmsApplication.findMany(... omit: { secretHash: true })` 成功返回 10 条企业应用,结果中 `secretHash` 泄漏计数为 0;未写入或修改预生产数据。
- 既有 `api/tsconfig.build.tsbuildinfo``tsconfig.tsbuildinfo``outputs/` 和空文件 `=` 继续作为构建或临时产物保留,不纳入提交。
@@ -33,7 +33,7 @@ export function AdminEnterpriseBlacklistPage() {
Promise.all([ Promise.all([
adminApi.listEnterpriseBlacklist(filters), adminApi.listEnterpriseBlacklist(filters),
adminApi.listTenants(), adminApi.listTenants(),
adminApi.listEnterpriseApplications(), adminApi.listEnterpriseApplicationOptions(),
]) ])
.then(([blacklist, tenantItems, applicationItems]) => { .then(([blacklist, tenantItems, applicationItems]) => {
setItems(blacklist as EnterpriseBlacklistItem[]); setItems(blacklist as EnterpriseBlacklistItem[]);