fix: 第1批 M9 套餐临期提醒按资产、扫码进入时触发
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m17s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m17s
- 每日扫描不再给个人客户写临期通知,店铺通知不变 - H5 资产信息在当前资产剩余 0~15 天且当天首次进入时返回 expiry_reminder,并为该客户、该资产写一条临期通知(Redis 去重,失败不占键) - 个人通知列表、未读数、全部已读中临期提醒只处理当前资产(按 ref_key) - OpenSpec Change scope-expiry-reminder-to-current-asset:6/9,临期 fixture 端到端验证与每日扫描验证待测试部署后完成
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
@@ -0,0 +1,31 @@
|
||||
## Context
|
||||
|
||||
承接《第1批-8-9月迭代缺陷修复技术方案》第 4 节。`packageexpiry` 的 `Publish` 在每日扫描中给店铺接收人(`appendShopNotification`)和所有绑定客户(`loadRecipients`)批量写通知;个人通知列表(`PersonalNotificationListRequest`,`internal/model/dto/notification_dto.go:94`)无资产过滤。H5 令牌带当前资产(`middleware.GetCurrentAsset`),资产信息接口为 `GET /api/c/v1/asset/info`。Outbox 写入按 `event_id` 冲突忽略(`internal/infrastructure/messaging/outbox/repository.go:103`)。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** 临期提醒只在客户进入该资产时出现,每天一次;通知中心只显示当前资产的临期提醒。
|
||||
|
||||
**Non-Goals:** 不改到期推算、提醒窗口与等级、店铺通知;不新增接口入参;无迁移。
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **进入时判定,复用最终到期推算。** 在资产信息接口内按当前资产调用 `packageexpiry` 的推算,与每日扫描同口径。
|
||||
2. **去重顺序:先查键 → 写通知 → `SET NX` → 抢到才返回字段。** 方案原顺序是“先写通知再 `SET NX`”,当天每次进入都会写一次 Outbox(虽冲突忽略,仍是无效写)。评审确认先用 Redis `EXISTS`/`GET` 检查,键存在直接返回,只有未提醒时才写通知。写通知成功后才占键,写失败不占键、不返回字段,避免“当天已去重但通知没发出”。两个并发请求都通过检查时,通知由事件号幂等,只有抢到键的一方返回字段。键定义放 `pkg/constants/redis.go`,TTL 到当天上海时间结束再加缓冲。
|
||||
3. **Redis 不可用按未提醒处理。** 字段可能当天多次返回,通知由事件号兜底不重复;接受该降级,不因 Redis 故障让资产信息接口失败。
|
||||
4. **只用 `ref_key` 过滤,列表、未读数、全部已读同一口径。** `ref_id` 不带资产类型,卡 5 与设备 5 冲突。过滤只作用于 `type = package.expiring AND ref_type = asset`。“全部已读”(`PUT /notifications/read-all`)也加同样的条件,否则会把客户在其他资产才能看到的临期提醒一并标为已读。
|
||||
5. **停用个人客户预发布。** 去掉 `Publish` 里给绑定客户批量写通知的循环,店铺通知不变。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [GET 接口产生写副作用] → 只在当天首次命中时写一次通知,其余请求只读 Redis。
|
||||
- [设备补虚拟号后历史提醒匹配不上] → 影响可接受,已在 spec 中写明。
|
||||
- [资产信息接口延迟增加] → 推算与 Redis 检查各一次;推算结果为非临期时不访问 Redis。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
无迁移。测试环境部署后,每日扫描不再产生个人客户临期通知;已有个人通知按过滤规则展示。
|
||||
|
||||
## Open Questions
|
||||
|
||||
- 资产信息响应中提醒字段的具体命名在实现时确定,并写入接口文档。
|
||||
@@ -0,0 +1,26 @@
|
||||
## Why
|
||||
|
||||
套餐临期提醒发给所有绑定过该资产的客户(`reminder_publisher.go` 的 `loadRecipients`),个人通知列表又没有资产过滤,客户在任一资产页都会看到名下全部资产的临期提醒(运营清单 26)。业务已确认改为按资产提醒:哪个资产被扫码进入就提醒哪个,同一客户、同一资产、同一天只提醒一次。依据为《第1批-8-9月迭代缺陷修复技术方案》第 4 节(M9)。
|
||||
|
||||
## What Changes
|
||||
|
||||
- H5 资产信息接口 `GET /api/c/v1/asset/info` 在当前资产处于临期窗口(剩余 0~15 天)且当天未提醒过时,返回临期提醒字段(剩余天数、到期时间、等级),并为“当前客户、当前资产”写一条 `package.expiring` 个人通知;同一客户、同一资产、同一北京时间日期只提醒一次。
|
||||
- **BREAKING** 每日扫描不再给个人客户写临期通知;店铺通知不变。
|
||||
- **BREAKING** 个人通知列表、未读数与“全部已读”中,临期提醒只处理当前资产的;其他类型通知不受影响,接口入参不变。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
无。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `notification-delivery`:每日临期扫描只通知店铺接收人;新增 H5 进入资产时的临期提醒;个人通知中心临期提醒按当前资产过滤。
|
||||
|
||||
## Impact
|
||||
|
||||
- 代码:`internal/infrastructure/packageexpiry/reminder_publisher.go`(去掉个人客户批量写入)、H5 资产信息 Handler 与服务(复用 `packageexpiry` 最终到期推算、写 Outbox 个人通知)、个人通知列表与未读数查询、`pkg/constants/redis.go`(去重键)、资产信息响应 DTO。
|
||||
- 接口:`GET /api/c/v1/asset/info` 新增字段;个人通知列表与未读数结果范围变化;重新生成接口文档。
|
||||
- 依赖:Redis(去重键);Outbox 按 `event_id` 冲突忽略写入(`internal/infrastructure/messaging/outbox/repository.go`)。
|
||||
- 无数据库迁移。
|
||||
@@ -0,0 +1,65 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 套餐临期每日站内提醒
|
||||
系统 SHALL 在每日扫描时,向最终到期时间可精确推算且剩余 0 至 15 个上海自然日的资产所属店铺后台接收人创建套餐临期站内通知。系统 MUST 对同一资产、同一最终到期日期、同一剩余天数和同一接收人保持幂等。每日扫描 MUST NOT 向个人客户创建套餐临期通知;个人客户的临期提醒只按“H5 进入资产时的套餐临期提醒”产生。
|
||||
|
||||
#### Scenario: 临期窗口内连续两日提醒
|
||||
- **GIVEN** 一项资产的最终到期时间可精确推算,昨天剩余 3 个上海自然日,今天剩余 2 个上海自然日,且两日均有有效店铺接收人
|
||||
- **WHEN** 每日临期扫描分别执行
|
||||
- **THEN** 系统分别为店铺接收人创建昨天和今天的套餐临期通知
|
||||
|
||||
#### Scenario: 不在临期窗口的资产
|
||||
- **GIVEN** 一项资产剩余超过 15 个上海自然日、已经到期或最终到期时间不可精确推算
|
||||
- **WHEN** 每日临期扫描执行
|
||||
- **THEN** 系统不为该资产创建套餐临期通知
|
||||
|
||||
#### Scenario: 每日扫描不通知个人客户
|
||||
- **GIVEN** 一项临期资产绑定了个人客户
|
||||
- **WHEN** 每日临期扫描执行
|
||||
- **THEN** 系统只为店铺接收人创建通知,不为个人客户创建通知
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: H5 进入资产时的套餐临期提醒
|
||||
|
||||
个人客户访问 `GET /api/c/v1/asset/info` 时,系统 SHALL 以令牌中的当前资产,按与每日扫描相同的最终到期推算判断是否临期(剩余 0~15 个上海自然日,≤3 红、≤7 紫、≤15 粉红)。命中且该客户当天(上海自然日)尚未就该资产提醒过时,系统 MUST 在响应中返回临期提醒字段(剩余天数、到期时间、等级),并为当前客户、当前资产写一条 `package.expiring` 个人通知,通知幂等事件号 MUST 包含资产、到期日、剩余天数与客户。
|
||||
|
||||
同一客户、同一资产、同一上海自然日 MUST 至多返回一次提醒字段、至多产生一条通知;去重键为“客户 + 资产标识 + 日期”。系统 MUST 先检查去重键,已存在则不返回字段、不写通知;不存在时先写通知,写入成功后再占用去重键,抢占成功才返回字段。通知写入失败时 MUST NOT 占用去重键、MUST NOT 返回字段,下次进入重试。去重存储不可用时系统按“未提醒过”处理,提醒字段可能当天多次返回,通知由事件号幂等保证不重复。
|
||||
|
||||
#### Scenario: 当天首次进入临期资产
|
||||
- **WHEN** 客户首次在当天进入一项剩余 2 天的资产
|
||||
- **THEN** 响应返回剩余天数 2、到期时间与红色等级,并新增一条该资产的临期通知
|
||||
|
||||
#### Scenario: 同一天再次进入
|
||||
- **WHEN** 同一客户当天再次进入该资产
|
||||
- **THEN** 响应不返回临期提醒字段,不新增通知
|
||||
|
||||
#### Scenario: 第二天再次进入
|
||||
- **WHEN** 同一客户第二天进入该资产且仍在临期窗口
|
||||
- **THEN** 再次返回提醒字段并新增一条通知
|
||||
|
||||
#### Scenario: 不同客户进入同一资产
|
||||
- **WHEN** 客户 B 当天进入客户 A 已被提醒过的资产
|
||||
- **THEN** 客户 B 独立获得一次提醒
|
||||
|
||||
#### Scenario: 非临期资产
|
||||
- **WHEN** 客户进入剩余超过 15 天或最终到期不可精确推算的资产
|
||||
- **THEN** 响应不返回临期提醒字段,不写通知
|
||||
|
||||
### Requirement: 个人通知中心临期提醒按当前资产过滤
|
||||
|
||||
个人客户通知列表、未读数与“全部已读”(`PUT /api/c/v1/notifications/read-all`)SHALL 对 `type = package.expiring` 且 `ref_type = asset` 的通知,只处理 `ref_key` 等于当前资产标识的通知(卡为 ICCID;设备优先虚拟号,为空则 IMEI);MUST NOT 使用 `ref_id` 过滤。“全部已读”MUST NOT 把其他资产的临期提醒标为已读。其他类型通知 MUST 不受影响,接口入参不变。设备后补虚拟号前产生的历史提醒 `ref_key` 为 IMEI,补号后不再匹配,属可接受限制。
|
||||
|
||||
#### Scenario: 客户名下两个临期资产
|
||||
- **GIVEN** 客户名下资产 A、B 都有临期通知
|
||||
- **WHEN** 客户在资产 A 中查询通知列表与未读数
|
||||
- **THEN** 临期通知只含 A 的,未读数只计 A 的临期通知与其他类型通知
|
||||
|
||||
#### Scenario: 全部已读只处理当前资产的临期提醒
|
||||
- **GIVEN** 客户名下资产 A、B 都有未读临期通知
|
||||
- **WHEN** 客户在资产 A 中点“全部已读”
|
||||
- **THEN** A 的临期通知与其他类型通知变为已读,B 的临期通知仍为未读,客户进入 B 时仍能看到
|
||||
|
||||
#### Scenario: 非临期通知不受过滤
|
||||
- **WHEN** 客户在资产 A 中查询通知列表
|
||||
- **THEN** 非 `package.expiring` 类型的通知照常返回
|
||||
@@ -0,0 +1,20 @@
|
||||
## 1. 停用个人客户预发布
|
||||
|
||||
- [x] 1.1 `internal/infrastructure/packageexpiry/reminder_publisher.go` 的 `Publish` 去掉给绑定客户批量写个人通知的循环,保留 `appendShopNotification`。
|
||||
|
||||
## 2. 进入资产时提醒
|
||||
|
||||
- [x] 2.1 `pkg/constants/redis.go` 新增去重键(客户 ID + 资产标识 + 上海日期)及 TTL 计算(当天结束加缓冲)。
|
||||
- [x] 2.2 资产信息服务按当前资产复用 `packageexpiry` 最终到期推算;命中窗口时依次:检查键 → 经 Outbox 写 `package.expiring` 个人通知(事件号 `pex:资产:到期日:剩余天数:客户`)→ `SET NX` → 抢到才返回字段;写通知失败不占键不返回;Redis 不可用按未提醒处理。
|
||||
- [x] 2.3 资产信息响应 DTO 新增临期提醒字段(剩余天数、到期时间、等级),更新接口说明。
|
||||
|
||||
## 3. 通知中心过滤
|
||||
|
||||
- [x] 3.1 个人通知列表、未读数与“全部已读”对 `type = package.expiring AND ref_type = asset` 只保留 `ref_key` 等于当前资产标识(卡 ICCID;设备虚拟号,空则 IMEI)的记录,其他类型不变,入参不变。
|
||||
|
||||
## 4. 文档与测试环境验证
|
||||
|
||||
- [x] 4.1 `gofmt`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`。
|
||||
- [ ] 4.2 按 ENG-TEST-001(测试库 `junhong_cmp_test`、Redis DB 6)构造同一客户绑定两个临期资产:进入 A 只返回 A 的字段并新增 A 的通知,通知中心与未读数只含 A;在 A 中“全部已读”后 B 的临期通知仍未读;同一天再次进入 A 不返回字段、不新增通知;清理键模拟第二天后再次提醒;客户 B 进入 A 独立提醒一次;非临期通知不受过滤。
|
||||
- [ ] 4.3 手动触发每日扫描:不再给个人客户写临期通知,店铺通知照常。
|
||||
- [ ] 4.4 运行 `openspec validate --all` 与 `./scripts/context-health.sh`。
|
||||
Reference in New Issue
Block a user