Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a17df78b8 | ||
|
|
2a181777ad |
@@ -21,6 +21,7 @@ export type DownstreamDeliveryQueueRequest = {
|
||||
receiptDedupeKey?: string;
|
||||
queueHttpWebhook?: boolean;
|
||||
queueCmppDelivery?: boolean;
|
||||
allowBusinessRejectionCmppDelivery?: boolean;
|
||||
propagateHttpQueueError?: boolean;
|
||||
};
|
||||
|
||||
@@ -80,6 +81,7 @@ export async function queueFinalReceiptDeliveries(
|
||||
message: FinalReceiptMessage;
|
||||
payload: Record<string, unknown>;
|
||||
segmentPayloads?: Record<number, Record<string, unknown>>;
|
||||
allowBusinessRejectionCmppDelivery?: boolean;
|
||||
propagateHttpQueueError?: boolean;
|
||||
},
|
||||
) {
|
||||
@@ -125,6 +127,7 @@ export async function queueFinalReceiptDeliveries(
|
||||
: `receipt:${message.id}:segment:${target.segmentIndex}`,
|
||||
queueHttpWebhook: false,
|
||||
queueCmppDelivery: true,
|
||||
allowBusinessRejectionCmppDelivery: data.allowBusinessRejectionCmppDelivery,
|
||||
});
|
||||
}
|
||||
return { queued: true, cmppTargetCount: targets.length };
|
||||
|
||||
@@ -1205,7 +1205,7 @@ describe('SendChainService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects new submissions synchronously when the application interface was disabled after bind', async () => {
|
||||
it('accepts Submit after bind and emits an auditable REJECTD receipt when the interface was disabled', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsApplication.findFirst.mockResolvedValue({
|
||||
id: 'app-1',
|
||||
@@ -1222,6 +1222,7 @@ describe('SendChainService', () => {
|
||||
id: 'app-1',
|
||||
tenantId: 'tenant-1',
|
||||
cmppAccount: '100001',
|
||||
status: 'active',
|
||||
interfaceEnabled: false,
|
||||
downstreamReceiptRetryEnabled: true,
|
||||
downstreamUplinkRetryEnabled: true,
|
||||
@@ -1232,12 +1233,34 @@ describe('SendChainService', () => {
|
||||
account: '100001',
|
||||
phoneNumber: '13800000001',
|
||||
content: 'hello',
|
||||
sequenceId: 701,
|
||||
remoteIp: '127.0.0.1',
|
||||
})).rejects.toThrow('CMPP account is disabled for new submissions');
|
||||
})).resolves.toEqual(expect.objectContaining({
|
||||
accepted: true,
|
||||
messageRecordId: 'record-1',
|
||||
status: 'accepted',
|
||||
}));
|
||||
|
||||
expect(prisma.smsMessageRecord.create).not.toHaveBeenCalled();
|
||||
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
|
||||
expect(prisma.cmppDownstreamDelivery.create).not.toHaveBeenCalled();
|
||||
expect(prisma.smsMessageRecord.create).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.smsMessageRecord.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { id: 'record-1' },
|
||||
data: expect.objectContaining({
|
||||
status: 'failed',
|
||||
receiptStatus: 'undelivered',
|
||||
receiptRawStatus: 'REJECTD',
|
||||
errorCode: 'INTERFACE',
|
||||
}),
|
||||
}));
|
||||
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ rawStatus: 'REJECTD', errorCode: 'INTERFACE' }),
|
||||
}));
|
||||
expect(prisma.cmppDownstreamDelivery.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ status: 'pending', deliveryType: 'receipt' }),
|
||||
}));
|
||||
expect(service['postGatewayControl']).toHaveBeenCalledWith(
|
||||
'/downstream/receipt',
|
||||
expect.objectContaining({ rawStatus: 'REJECTD', errorCode: 'INTERFACE' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('queues an HTTP webhook but not CMPP delivery for an HTTP-only application', async () => {
|
||||
@@ -2376,23 +2399,44 @@ describe('SendChainService', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a disabled application before the merged Inbox statement can insert', async () => {
|
||||
it('persists fast-path Submit before evaluating application or tenant business state', async () => {
|
||||
const previous = process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
|
||||
process.env.CMPP_INBOUND_FAST_PATH_ENABLED = 'true';
|
||||
try {
|
||||
const { service, prisma } = createService();
|
||||
prisma.$queryRaw.mockResolvedValueOnce([{
|
||||
validationError: 'CMPP account is disabled for new submissions',
|
||||
payloadHash: null,
|
||||
response: null,
|
||||
}]);
|
||||
prisma.$queryRaw.mockImplementationOnce((query) => {
|
||||
const payloadHash = query.values.find((value: unknown) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value));
|
||||
return Promise.resolve([{
|
||||
validationError: null,
|
||||
payloadHash,
|
||||
response: {
|
||||
accepted: true,
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
taskId: '',
|
||||
messageId: 'MSG-disabled-after-bind',
|
||||
messageRecordId: '',
|
||||
status: 'accepted_pending',
|
||||
phoneCount: 1,
|
||||
messages: [],
|
||||
},
|
||||
}]);
|
||||
});
|
||||
|
||||
await expect(service.submitInboundMessage({
|
||||
requestId: 'cmpp-inbound:disabled',
|
||||
account: '100001',
|
||||
phoneNumber: '13800000001',
|
||||
content: 'hello',
|
||||
})).rejects.toThrow('CMPP account is disabled for new submissions');
|
||||
})).resolves.toEqual(expect.objectContaining({
|
||||
accepted: true,
|
||||
status: 'accepted_pending',
|
||||
}));
|
||||
const sql = prisma.$queryRaw.mock.calls[0][0].strings.join(' ');
|
||||
expect(sql).not.toContain("application.status <> 'active'");
|
||||
expect(sql).not.toContain('NOT application."interfaceEnabled"');
|
||||
expect(sql).toContain('CMPP source IP is not in application allowlist');
|
||||
expect(sql).toContain('CMPP Src_Id must equal the access number assigned to this application');
|
||||
expect(prisma.cmppInboundSubmissionInbox.findUnique).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
if (previous == null) delete process.env.CMPP_INBOUND_FAST_PATH_ENABLED;
|
||||
@@ -3928,7 +3972,7 @@ describe('SendChainService', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('allows a disabling application to reconnect for receipt draining but rejects new submissions', async () => {
|
||||
it('allows a disabling application to reconnect for receipt draining and audits later Submit as REJECTD', async () => {
|
||||
const { service, prisma } = createService();
|
||||
prisma.smsApplication.findFirst.mockResolvedValue({
|
||||
id: 'app-1',
|
||||
@@ -3943,6 +3987,16 @@ describe('SendChainService', () => {
|
||||
ipAllowlist: [{ ipCidr: '127.0.0.1/32' }],
|
||||
tenant: { id: 'tenant-1', status: 'active', certificationStatus: 'approved' },
|
||||
});
|
||||
prisma.smsApplication.findUnique.mockResolvedValue({
|
||||
id: 'app-1',
|
||||
tenantId: 'tenant-1',
|
||||
cmppAccount: '100001',
|
||||
status: 'deleted',
|
||||
interfaceEnabled: false,
|
||||
downstreamReceiptRetryEnabled: true,
|
||||
downstreamUplinkRetryEnabled: true,
|
||||
httpConfig: { enabled: false },
|
||||
});
|
||||
|
||||
await expect(service.authenticateInboundApplication({
|
||||
account: '100001',
|
||||
@@ -3955,9 +4009,20 @@ describe('SendChainService', () => {
|
||||
account: '100001',
|
||||
phoneNumber: '13800000001',
|
||||
content: 'hello',
|
||||
sequenceId: 702,
|
||||
remoteIp: '127.0.0.1',
|
||||
})).rejects.toThrow('disabled for new submissions');
|
||||
expect(prisma.smsMessageRecord.create).not.toHaveBeenCalled();
|
||||
})).resolves.toEqual(expect.objectContaining({ accepted: true, status: 'accepted' }));
|
||||
expect(prisma.smsMessageRecord.create).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.smsReceiptRecord.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ rawStatus: 'REJECTD', errorCode: 'ACCOUNT' }),
|
||||
}));
|
||||
expect(prisma.cmppDownstreamDelivery.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ status: 'pending', deliveryType: 'receipt' }),
|
||||
}));
|
||||
expect(service['postGatewayControl']).toHaveBeenCalledWith(
|
||||
'/downstream/receipt',
|
||||
expect.objectContaining({ rawStatus: 'REJECTD', errorCode: 'ACCOUNT' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('lets Gateway read historical pending receipts after an application or enterprise is disabled', async () => {
|
||||
|
||||
@@ -210,6 +210,7 @@ export class SendDownstreamDeliveryService {
|
||||
},
|
||||
});
|
||||
const deliveryAllowed = application?.status === 'active' || application?.status === 'disabling';
|
||||
const cmppDeliveryAllowed = deliveryAllowed || data.allowBusinessRejectionCmppDelivery === true;
|
||||
if (deliveryAllowed && data.queueHttpWebhook !== false) {
|
||||
try {
|
||||
await this.openApi?.queueWebhookEvent({
|
||||
@@ -229,7 +230,9 @@ export class SendDownstreamDeliveryService {
|
||||
if (data.queueCmppDelivery === false) {
|
||||
return null;
|
||||
}
|
||||
if (application?.interfaceEnabled !== true) {
|
||||
if (!application?.cmppAccount || (
|
||||
application.interfaceEnabled !== true && data.allowBusinessRejectionCmppDelivery !== true
|
||||
)) {
|
||||
return null;
|
||||
}
|
||||
const payload = { account: application?.cmppAccount, applicationId: data.applicationId, ...data.payload };
|
||||
@@ -249,11 +252,11 @@ export class SendDownstreamDeliveryService {
|
||||
dedupeKey,
|
||||
deliveryType: data.deliveryType,
|
||||
payload,
|
||||
retryEnabled: deliveryAllowed && (data.deliveryType === 'uplink'
|
||||
retryEnabled: cmppDeliveryAllowed && (data.deliveryType === 'uplink'
|
||||
? application?.downstreamUplinkRetryEnabled ?? true
|
||||
: application?.downstreamReceiptRetryEnabled ?? true),
|
||||
status: deliveryAllowed ? 'pending' : 'abandoned',
|
||||
lastError: deliveryAllowed ? null : '企业应用已停用,保留回执但不再向客户应用推送',
|
||||
status: cmppDeliveryAllowed ? 'pending' : 'abandoned',
|
||||
lastError: cmppDeliveryAllowed ? null : '企业应用已停用,保留回执但不再向客户应用推送',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -278,7 +281,7 @@ export class SendDownstreamDeliveryService {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (!deliveryAllowed) {
|
||||
if (!cmppDeliveryAllowed) {
|
||||
return delivery;
|
||||
}
|
||||
const claimId = `api-direct:${process.pid}:${randomUUID()}`;
|
||||
@@ -478,6 +481,7 @@ export class SendDownstreamDeliveryService {
|
||||
(request) => this.facade.queueAndTryDownstreamDelivery(request),
|
||||
{
|
||||
message,
|
||||
allowBusinessRejectionCmppDelivery: errorCode === 'ACCOUNT' || errorCode === 'INTERFACE',
|
||||
payload: {
|
||||
messageId: message.messageId,
|
||||
gatewayMessageId: `PLATFORM:${message.messageId}`,
|
||||
|
||||
@@ -233,9 +233,6 @@ async submitInboundMessage(data: GatewayInboundSubmitDto) {
|
||||
if (!application) {
|
||||
throw new BadRequestException('CMPP account is invalid');
|
||||
}
|
||||
if (application.status !== 'active' || application.tenant.status !== 'active' || !application.interfaceEnabled) {
|
||||
throw new BadRequestException('CMPP account is disabled for new submissions');
|
||||
}
|
||||
if (data.longMessage) {
|
||||
if (data.remoteIp && !isIpAllowed(data.remoteIp, application.ipAllowlist.map((item) => item.ipCidr))) {
|
||||
throw new BadRequestException('CMPP source IP is not in application allowlist');
|
||||
@@ -360,10 +357,10 @@ async submitInboundMessage(data: GatewayInboundSubmitDto) {
|
||||
const remoteIp = data.remoteIp?.replace(/^::ffff:/, '').trim() || null;
|
||||
const submittedSrcId = data.srcId?.trim() ?? '';
|
||||
|
||||
// One indexed statement must both validate the current application state and create the
|
||||
// durable Inbox row. Keeping those operations in one database snapshot prevents a disable
|
||||
// racing between a separate SELECT and INSERT, while the unique request key remains the
|
||||
// authoritative idempotency boundary.
|
||||
// One indexed statement validates protocol-level constraints and creates the durable Inbox
|
||||
// row. Application/tenant/interface state is deliberately evaluated by the workflow worker:
|
||||
// an already-authenticated connection must receive a successful SubmitResp first, followed
|
||||
// by an auditable REJECTD receipt if the business resource was disabled after bind.
|
||||
const rows = await this.prisma.$queryRaw<PersistedInboundWorkflowRow[]>(Prisma.sql`
|
||||
WITH application AS (
|
||||
SELECT app.id,
|
||||
@@ -383,10 +380,6 @@ async submitInboundMessage(data: GatewayInboundSubmitDto) {
|
||||
), validation AS (
|
||||
SELECT application.*,
|
||||
CASE
|
||||
WHEN application.status <> 'active'
|
||||
OR application."tenantStatus" <> 'active'
|
||||
OR NOT application."interfaceEnabled"
|
||||
THEN 'CMPP account is disabled for new submissions'
|
||||
WHEN ${remoteIp}::text IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM "SmsApplicationIpAllowlist" allowlist
|
||||
|
||||
@@ -1229,6 +1229,7 @@
|
||||
- Submit 应用身份使用 bind 已鉴权账号;`MsgSrc` 使用应用级企业代码并独立校验。企业代码与登录账号不同时仍能正确定位应用,企业代码不匹配时返回失败。
|
||||
- 鉴权失败、IP 白名单不符、任一目标手机号等协议参数不合法时返回非零 SubmitResp,且整包不创建短信记录;禁止多号码 Submit 返回成功后只保存或发送首号码。
|
||||
- 已鉴权且参数合法的 Submit 必须先返回成功 SubmitResp 和平台 Msg_Id;内容不匹配审核模板、签名/报备未通过、余额不足、应用在 bind 后停用、无可用通道、上游 Submit 最终失败时,均须真实创建短信记录、`SmsReceiptRecord` 和 `CmppDownstreamDelivery`,并向客户下发 `undelivered/REJECTD` Deliver Receipt,不得仅以 SubmitResp 失败替代回执。
|
||||
- 应用、企业或CMPP接口在bind后停用/删除时,仅本次已受理Submit产生的`ACCOUNT/INTERFACE`平台失败回执可绕过当前业务启用状态投向原CMPP会话;不得因此允许新bind、供应商Submit、普通HTTP消息转CMPP回执或其他停用资源继续发送。
|
||||
- Gateway 对每次 submit 记录 `submit_received` 和 `submit_accepted`/`submit_rejected`;日志可按账号、IP、sequenceId、号码和 messageId 定位,拒绝时包含 NestJS 真实业务原因和 CMPP result,但不包含明文短信正文。
|
||||
- CMPP 包在进入 handler 前因长度、命令字、读包或 Unpack 失败时,Gateway 记录 `read/unpack packet failed`、远端地址、协议模式、错误类型和原始错误,不得静默断开。
|
||||
- 企业应用列表和连接详情展示真实下游 CMPP 会话:bind 后当前连接数加一,显示客户 IP、企业代码、CMPP 版本、连接建立时间与最后心跳;连接持续未响应 `ACTIVE_TEST` 超过阈值后转为心跳超时/断开,不能继续显示为正常连接。
|
||||
|
||||
@@ -3985,3 +3985,21 @@ git diff --check
|
||||
- 有效正价结果:单企业100档999/999、P50/P95/P99=`72/243/466ms`、23个批次平均43.43/最大64、Inbox 10.721秒排空、首次供应商Submit `95.85 TPS`;单企业150冲击1498/1498、`53/83/117ms`、33批平均45.39/最大64、12.453秒排空、首次Submit `122.87 TPS`;双企业合计200冲击1999/1999、`80/148/287ms`、两企业各22批、首次Submit `129.39 TPS`,两企业分别64.69/65.96。全部零拒绝、零节流、零连接错误。
|
||||
- 双企业200档单价全部325:1999条账务记录,其中1988 charged/646100、11 refunded/3575;2145个含主备补发的SubmitId及Outbox全部唯一/published,Outbox最大发布0.111秒。最终1936 delivered、49模拟器no-receipt为submitted、11最终失败已退款、3条首次接受后补发结果码8保留charged。1950条终态回执投递1950次、重复0、最大尝试1;客户端离线产生的973 pending由14账号零发送排空。
|
||||
- 收尾状态:Inbox全83194条completed,Submit Outbox全27107条published,命令/结果/协议日志Stream pending和lag均0,下游pending=0,数据库锁等待/idle transaction=0,9服务active、API/Gateway健康、Redis PONG;14个测试应用active且单价325,28条指定白名单保留,原phase5三条和新增phase6三条临时号段规则均active。未执行长期稳态,当前建议单企业限速90 TPS、单Gateway平台总限速100 TPS;入口150/200冲击不等于完整供应商TPS。
|
||||
|
||||
## 2026-08-25 测试环境主要发送流程与 P0 拦截回归
|
||||
|
||||
- 按用户最新要求停止性能压测,本轮仅在测试机`100.93.204.60`和本地供应商模拟器执行低流量功能回归,未访问或修改预生产/生产,未实施多 Gateway P2。代码级门禁限定`api/`根目录后,SendChain、号码频次和Gateway事件3套141项全部通过;首次从仓库根目录执行时被用户保留的`outputs/`旧快照误匹配,未作为产品失败且未修改该目录。
|
||||
- 正常正价链路通过:`MSG-a33198db-c3ba-4002-9358-97a128aca87f`、`MSG-b2b329ad-b088-4b41-a19a-c94d2e7a525c`均为`delivered`,走`LGST-M-P`并各产生1笔`charged=325`。本轮测试窗口共落库26条,12条delivered、14条failed;12笔charged合计3900,账户从1827951变为1824051,冻结16笔/5200与释放16笔/5200相抵,补发没有重复计费。
|
||||
- 发送前拦截重新验证通过:签名未审核2条为`failed/SIGNATURE`,模板强校验2条为`failed/TEMPLATE`,余额不足2条为`failed/BALANCE`;三类均供应商Submit=0、账单=0,并生成客户失败投递。移动主备签名通道报备改为pending后,`MSG-05db68dd-9185-4c1d-8e4b-5bf114390c41`和`MSG-b4bf86a6-271e-45eb-9c73-8a2930d54894`均为`failed/ROUTE`、供应商Submit=0、账单=0。签名审核、模板模式、余额和6条报备任务均已恢复。
|
||||
- 号码频次通过:专用应用级5分钟1条规则下,固定上海时区同一窗口的第2条`MSG-7f8df0db-7a03-4965-ab93-021c0504abb6`和`MSG-90429628-c566-40c3-824c-73cc3600075a`均为`failed/RISK`,命中阈值1/实际2,供应商Submit=0、账单=0。一次初测恰好跨越19:05固定窗口边界,按设计重新计数,不作为失败;本轮临时规则已删除,两测试号码活动命中已解除,既有全局规则和平台白名单未修改。
|
||||
- 企业应用`inactive/deleted`、企业`inactive/deleted`四次新连接均返回CMPP Bind状态3并断开,未创建短信记录。bind后再停用的严格复验同样拦截发送;此前两次跨机器时间未校准的尝试虽送达并计费,但无法证明配置变更早于Submit,明确作废且不用于判断。严格复验暴露`P0-SEND-AUDIT-001`:应用或企业停用后,参数合法Submit返回非零SubmitResp,且测试号码`13800389700/13800389800`均没有`SmsMessageRecord`、失败回执或下游投递;这与`TC-SEND-037`要求“已鉴权合法Submit先返回成功并留痕,再以REJECTD业务失败”冲突。当前单元测试反而断言同步拒绝且不落库,说明需求、测试和实现口径已漂移,需先确认权威语义再修复,不能把“拦截生效”冒充完整失败审计闭环。
|
||||
- 通道行为通过:仅`LGST-M-P`停用时,`MSG-94d30de8-78cb-48a4-bdbf-e6ef6dd2fb93`、`MSG-22f26498-0289-4b79-aa0c-a1fa54bb0b99`自动改走`LGST-M-B`并delivered;移动主备同时停用时两条均`failed/ROUTE`、供应商Submit=0、账单=0。确定性故障模拟下,`MSG-c28b231f-2489-41fc-9d0f-6223bc8307bd`和`MSG-a65283b6-9349-4838-a12d-fee2e5ee07ff`先在主通道结果码8/rejected,再创建带`retryOfSubmitRecordId`的备用accepted提交并最终delivered,每条只计费325。
|
||||
- 最终恢复回读:企业/应用active、接口开启、模板模式direct_send、应用单价325、余额1824051;`【LG压测】`审核和汇总报备approved,6/6通道报备approved;六通道active、端口17900、连接6/6,普通模拟器已恢复;每应用两条指定白名单及phase5/phase6六条临时运营商号段规则全部保留,临时频控规则0。Inbox仅completed、Submit Outbox仅published,命令/结果/协议日志Stream均pending0/lag0,业务BullMQ wait/active/delayed/failed/prioritized均0,遗留`gateway.submit.queue` wait仍为84119且未增长;数据库未授予锁0、idle transaction0,9项服务active,本轮窗口核心服务error级journal为0。
|
||||
|
||||
## 2026-08-25 P0-SEND-AUDIT-001 本地修复与提交前验证
|
||||
|
||||
- 根因确认有两段:普通路径在应用查询后、快速路径在Inbox合并SQL内,都把应用/企业/接口状态作为SubmitResp同步拒绝条件,导致已鉴权合法Submit在短信记录前退出;即使后续业务状态机生成`ACCOUNT/INTERFACE`失败回执,下游队列仍会因当前`interfaceEnabled=false`直接返回。两处行为共同造成测试号码没有`SmsMessageRecord`、`SmsReceiptRecord`和`CmppDownstreamDelivery`。
|
||||
- 最小修复保留新bind的应用/企业/接口拒绝、账号存在性、IP白名单、`Src_Id`和协议参数校验;已鉴权合法Submit先进入原有持久化流程,普通路径立即、快速路径由Worker按最新状态生成`failed/ACCOUNT`或`failed/INTERFACE`、`undelivered/REJECTD`。仅这两类平台业务失败回执携带窄范围许可,即使资源随后停用/删除或接口关闭,也可创建下游投递并尝试送到原CMPP会话;普通HTTP消息、上行和其他停用资源投递语义未放宽。
|
||||
- 单元回归覆盖接口bind后关闭、应用处于disabling且回执入队时已deleted/interface关闭、快速路径Inbox SQL不再提前检查业务状态,同时确认IP白名单与`Src_Id`校验仍保留;既有“接口关闭时新bind拒绝”和“HTTP-only应用不产生CMPP投递”用例继续通过。
|
||||
- 提交前门禁:API全量45套526项通过;非增量TypeScript正式构建通过且未要求改写用户保留的`tsbuildinfo`;Gateway `go test ./... -count=1`和`go vet ./...`通过;`git diff --check`通过。无数据库迁移、无新增环境变量,部署脚本和环境变量校验无需修改;回滚只需revert本轮代码/文档提交并重新构建。
|
||||
- 本轮按用户授权仅在本地修复和验证,没有部署到测试环境,也没有执行新的短信发送或压力测试;测试环境仍运行旧部署标记`b5005f21d5092b7e2759efdce0ebd02798e4552f+test.phase6.tenant-microbatch.utc.keepalive.connection-race`,恢复资产仍为`/opt/cmpp-platform-backups/phase6-connection-race-20260825T095448Z`。因此P0已由代码和自动化测试验证,但尚未在测试环境重新做真实CMPP闭环验收;预生产/生产未访问,多Gateway P2未实施,未push远端。
|
||||
|
||||
Reference in New Issue
Block a user