fix: some
This commit is contained in:
@@ -31,7 +31,7 @@ export const notificationApi = {
|
||||
markAllRead() {
|
||||
return request({
|
||||
url: '/api/c/v1/notifications/read-all',
|
||||
method: 'POST'
|
||||
method: 'PUT'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
<text class="expiry-value">{{ formatShanghaiExpiry(reminder.expires_at) }}</text>
|
||||
</view>
|
||||
<view class="reminder-description">为避免套餐到期影响使用,请及时续费。</view>
|
||||
<button class="reminder-confirm" @tap="$emit('close')">我知道了</button>
|
||||
<button class="reminder-confirm" @tap="$emit('renew')">立即续费</button>
|
||||
<button class="reminder-later" @tap="$emit('close')">我知道了</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -24,7 +25,7 @@
|
||||
<script setup>
|
||||
import { formatShanghaiExpiry } from '@/utils/expiry-reminder.js';
|
||||
defineProps({ reminder: { type: Object, required: true } });
|
||||
defineEmits(['close']);
|
||||
defineEmits(['close', 'renew']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -69,7 +70,7 @@
|
||||
color: var(--text-tertiary);
|
||||
background: transparent;
|
||||
}
|
||||
.reminder-close::after, .reminder-confirm::after { border: none; }
|
||||
.reminder-close::after, .reminder-confirm::after, .reminder-later::after { border: none; }
|
||||
.clock-halo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -127,6 +128,7 @@
|
||||
background: var(--reminder-color);
|
||||
}
|
||||
.expiry-red { --reminder-color: #d94040; --reminder-tint: rgba(217, 64, 64, 0.08); }
|
||||
.reminder-later { margin: 12rpx 0 0; padding: 14rpx; font-size: 24rpx; line-height: 1.5; color: var(--text-tertiary); background: transparent; }
|
||||
.expiry-purple { --reminder-color: #8850c4; --reminder-tint: rgba(136, 80, 196, 0.08); }
|
||||
.expiry-pink { --reminder-color: #ce4878; --reminder-tint: rgba(206, 72, 120, 0.08); }
|
||||
</style>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { assetApi, isAssetRealNameCompleted, orderApi } from '@/api/index.js';
|
||||
import {
|
||||
handlePaymentError,
|
||||
@@ -63,8 +63,9 @@
|
||||
paymentRefreshTarget: { type: String, default: PAYMENT_REFRESH_TARGETS.ORDER_LIST }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['completed']);
|
||||
const emit = defineEmits(['completed', 'visibility-change']);
|
||||
const show = ref(false);
|
||||
watch(show, (value) => emit('visibility-change', value), { flush: 'sync' });
|
||||
const submitting = ref(false);
|
||||
const packageIds = ref([]);
|
||||
const packageNames = ref([]);
|
||||
|
||||
@@ -8,21 +8,22 @@
|
||||
- 四种绑定模式使用原 bind-phone 入参;资产首次绑号可输入与客户号不同的号码,1192 提示管理员并禁用当前提交,1184 保留后端错误。按用户最新确认,强制绑定成功保留有效 token 和资产标识,直接进入首页,不再要求重登。
|
||||
- 客户换号只更新后端确认的客户号码,不改资产关联;归属事实失效,下一次真实登录重新确认。换号的原号来源不再放进 URL。
|
||||
- asset/info 的共享返回入口接收一次性临期提醒,首页按最新要求使用居中、可关闭弹窗,已移除旧横幅组件。0 天正常显示,使用服务器等级和上海时区;与风险换货/运营候选、普通通知依次展示。绑定页预填收到的提醒留在当前会话,进入首页展示。
|
||||
- 旧首页通知弹窗排除临期项,通知中心保留。全部已读改 POST,单条已读保持 PUT;成功后重新读取列表和计数,失败不乐观清空,延迟响应不能覆盖新的资产上下文。
|
||||
- 旧首页通知弹窗排除临期项,通知中心保留。全部已读按用户 2026-10-14 最新确认恢复为 PUT,单条已读也保持 PUT;成功后重新读取列表和计数,失败不乐观清空,延迟响应不能覆盖新的资产上下文。
|
||||
- 临期弹窗不再展示等级文字标签,仅使用等级配色;新增“立即续费”,先关闭提醒再打开首页原有续费窗口,复用当前套餐 ID/名称/价格和全部原有支付校验,准备和展示期间暂停其他通知。
|
||||
- 通知卡片移除左侧未读强调边框和小圆点,标题右侧显示“未读/已读”小按钮。点击卡片或按钮成功标记后更新,失败仍显示未读。
|
||||
|
||||
## 验证证据
|
||||
|
||||
| 证据 | 结果 | 说明 |
|
||||
|---|---|---|
|
||||
| `npm run test:h5-integration` | 17/17 通过 | 实际 store/API 模块及页面 setup,使用 Vue 响应性;原生 API、生命周期和后端响应为模拟 |
|
||||
| `npm run test:h5-integration` | 19/19 通过 | 实际 store/API 模块及页面 setup,使用 Vue 响应性;原生 API、生命周期和后端响应为模拟 |
|
||||
| `npm run build:h5` | 通过 | uni-app H5 开发配置构建 |
|
||||
| `npm run build:h5:production` | 通过 | uni-app H5 生产配置构建 |
|
||||
| `openspec validate update-october-batch1-h5-integration --strict` | 通过 | 提案、需求和场景结构校验 |
|
||||
| `git diff --check` | 通过 | 默认 Git 换行配置下未发现补丁空白错误;Windows 换行提示不影响构建 |
|
||||
| 本地浏览器,390×844 / 320×640 | 通过 | 居中临期弹窗、0 天/上海时间、关闭、通知标题右侧未读按钮、点击卡片变已读;此前绑定表单及 1192 检查保留,无页面运行错误 |
|
||||
|
||||
测试文件:[tests/october-h5.test.mjs](../../../tests/october-h5.test.mjs)。覆盖绑定模式/成功后的分类更新、登录提示开关、状态持久化/隔离、换号不迁移资产、1192/1184、强制绑定直接进入首页且 token/客户信息/提醒保留、预填失败、共享接收/并发、0~15 天/上海边界、旧弹窗过滤、POST/PUT 请求方法、通知失败与切换、模拟 A/B 全部已读以及 401 状态清理。
|
||||
测试文件:[tests/october-h5.test.mjs](../../../tests/october-h5.test.mjs)。覆盖绑定模式/成功后的分类更新、登录提示开关、状态持久化/隔离、换号不迁移资产、1192/1184、强制绑定直接进入首页且 token/客户信息/提醒保留、预填失败、共享接收/并发、0~15 天/上海边界、旧弹窗过滤、PUT 已读请求、通知失败与切换、模拟 A/B 全部已读、401 状态清理以及临期弹窗续费复用/队列暂停。
|
||||
|
||||
本次修订删除了绑定成功后的登录态清理和“绑定手机号成功,请重新登录”提示,替代旧手动重登提案;真正的退出或 401 登录过期处理不变。
|
||||
|
||||
@@ -51,7 +52,7 @@ agent-browser --session october-h5-ui close
|
||||
|
||||
1. 微信公众号真实登录应返回三个布尔字段;尤其验证客户无号/资产有号时只给客户绑定,以及客户已有号时资产可绑定不同号码。
|
||||
2. 确认 asset/info.bound_phone 仍表示当前客户手机号,与登录布尔事实区分;换号后资产号码保持原值。若实际响应与对接文档冲突,先确认契约,不猜测接口。
|
||||
3. 同客户 A/B 两个临期资产分别扫码进入,确认服务器每天只返回一次、临期列表/未读数仅计当前资产,以及 A 上 POST 全部已读不改变 B 的临期通知。
|
||||
3. 同客户 A/B 两个临期资产分别扫码进入,确认服务器每天只返回一次、临期列表/未读数仅计当前资产,以及 A 上 PUT 全部已读不改变 B 的临期通知。
|
||||
4. 完整回归真实支付、实名认证、套餐订购以及运营/风险换货候选弹窗。本次没有修改这些接口或候选组件,构建成功不替代真实业务验收。
|
||||
|
||||
完成结果已按实际验证范围回写 [tasks.md](tasks.md),真实联调项保留未勾选;不归档历史提案。
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
| `pages/change-phone/change-phone.vue` 把 `newPhone` 写入资产缓存 | 与新文档“只换客户手机号”冲突 | 仅更新客户手机号,保留资产关联 |
|
||||
| 首页、TrafficCard、绑定页等分别请求 `asset/info` | 一次性提醒可能先被非展示调用方取走 | 在 API 返回入口集中接收,再由资产信息区展示 |
|
||||
| 首页 `loadUnreadNotifications` 展示所有未读项 | 仍以旧临期通知触发弹窗 | 从旧弹窗来源排除临期项,通知中心不删除 |
|
||||
| `api/modules/notification.js` 的 `markAllRead` 使用 PUT | 与新文档方法不一致 | 明确切换 POST,不影响 `markRead` 的 PUT |
|
||||
| `api/modules/notification.js` 的 `markAllRead` 方法约定曾冲突 | 用户最新确认应为 PUT | 保持 PUT,不采用此前 POST 约定 |
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
@@ -54,12 +54,14 @@
|
||||
|
||||
### 4. 一次性临期提醒使用弹窗及共享接收
|
||||
|
||||
首页就是当前 H5 的资产信息展示页,本次不新增资产详情路由。按用户最新要求,移除横幅,使用 `ExpiryReminderPopup` 居中弹窗:等级色、小型时钟图形、突出显示剩余天数和预计到期时间,提供右上关闭及“我知道了”操作。
|
||||
首页就是当前 H5 的资产信息展示页,本次不新增资产详情路由。按用户最新要求,移除横幅,使用 `ExpiryReminderPopup` 居中弹窗:等级配色、小型时钟图形、突出显示剩余天数和预计到期时间;不展示 level_name 标签。提供“立即续费”、右上关闭及“我知道了”操作。
|
||||
|
||||
首页按现有风险换货/运营候选 → 临期提醒 → 普通通知的顺序展示,一次只显示一个。asset/info 和候选请求未完成时不消费待展示提醒;通知已显示时才收到临期提醒,则等通知关闭后显示。候选按钮立即导航时保持提醒待展示;候选组件卸载后的异步响应不得回调新页面。临期关闭不再占首页布局,也不因此创建新的业务请求。
|
||||
|
||||
原样保留 `expiry_reminder.remaining_days`、`expires_at`、`level`、`level_name`;使用服务端等级映射 red / purple / pink 的样式,保留文本提示,不能只靠颜色传达紧急程度。`remaining_days=0` 必须正常展示。日期展示遵循 Asia/Shanghai,不能用客户端当前时区重算剩余自然日或等级。
|
||||
|
||||
“立即续费”先关闭临期弹窗且不推进其他通知,再调用与首页按钮相同的 startHomepageRenewal,使用当前套餐 ID、名称和续费价格。续费窗口准备中/可见时暂停其他提醒,关闭后继续排队。无可续费套餐或无支付方式时沿用原提示,不新增下单接口,也不在点击提醒按钮时自动创建订单。
|
||||
|
||||
`assetApi.getInfo` 成功返回处集中接收非空提醒并交给共享状态,同次资产进入期间的并发响应不允许用“未返回字段”的响应覆盖已经接收的提醒。共享状态隔离当前客户上下文与 identifier,避免切换资产/退出后显示旧资产或他人的提醒。提醒到达时若资产信息区尚不可见,应先暂存,待对应资产可见时再展示,不能静默丢弃。
|
||||
|
||||
后端负责“客户 + 资产 + 上海自然日”的每日一次;前端只防止同一次接收被多个组件重复展示,不增加每日定时推送、补发请求或长期缓存复播。关闭/已展示的提醒在当天后续无字段响应时不得重新出现。审核所有 `asset/info` 调用方,特别是绑定前预填及首页 TrafficCard 的请求顺序;绑定成功使用同一 token,提醒无需跨登录传递。真实退出、登录过期或切换上下文仍清理旧提醒。
|
||||
@@ -72,7 +74,7 @@
|
||||
|
||||
列表、未读数保持原入参,不增加文档未定义的 identifier/asset_id;后端基于当前认证资产过滤临期通知,并非所有通知都只属于当前资产。切换资产后刷新列表和计数,不将 A 的未读结果拼接到 B。
|
||||
|
||||
全部已读使用 POST,保留通知中心主动触发;失败不提前清除未读状态。成功后以刷新结果对齐本地列表/未读数,不能把其他资产的临期通知置已读。单条已读继续 PUT。风险换货、运营及临期弹窗均不得自动调用全部已读。
|
||||
全部已读按用户最新确认使用 PUT,保留通知中心主动触发;失败不提前清除未读状态。成功后以刷新结果对齐本地列表/未读数,不能把其他资产的临期通知置已读。单条已读继续 PUT。风险换货、运营及临期弹窗均不得自动调用全部已读。
|
||||
|
||||
通知卡片保留常规边框,移除左侧未读强调边框和小圆点。标题右侧显示绿色“未读”或灰色“已读”小按钮;点击整张卡片或状态按钮调用现有单条 PUT,成功后显示“已读”并刷新计数,失败保留“未读”。按钮阻止事件冒泡,避免一次点击触发两次标记。
|
||||
|
||||
@@ -87,7 +89,7 @@
|
||||
|
||||
1. 提案批准后,先完成状态拆分及旧缓存失效,再调整绑定/换号行为。
|
||||
2. 加入临期提醒共享接收和居中弹窗及排队显示,排除首页旧临期通知弹窗来源,调整通知阅读状态按钮。
|
||||
3. 切换全部已读为 POST,回归当前资产的列表与未读数。
|
||||
3. 保持全部已读为 PUT,回归当前资产的列表与未读数。
|
||||
4. 完成任务中的场景验证及 H5 构建,记录实际结果;不以静态构建代替真实接口联调。
|
||||
5. 需要回退时只回退本次前端改动;服务端新契约不变,不能恢复“客户换号即资产迁移”的旧假设作为兼容策略。
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
- 对 `bind-phone` 的 `1192` 提示“该资产已绑定手机号,如需更换请联系管理员”,不提供重试当前绑定请求的操作;保留 `1184` 的既有错误处理。
|
||||
- 绑定成功保留有效 token 和资产标识;登录引导的绑定页直接进入首页,不再清除登录态、提示或要求重新登录。普通绑定成功仍返回上一页。
|
||||
- **BREAKING(替代旧前端假设)**:`change-phone` 只修改客户手机号,不更新任何资产手机号关联;停止将新客户手机号写入资产绑定缓存。
|
||||
- 接收 `asset/info.expiry_reminder`,在首页展示居中、可关闭的临期弹窗,展示后端提供的剩余天数、到期时间和等级;无字段时不自行计算或补发提醒。
|
||||
- 接收 `asset/info.expiry_reminder`,在首页展示居中、可关闭的临期弹窗,显示剩余天数、到期时间及等级配色,不显示等级名称标签;“立即续费”复用首页已有续费流程。无字段时不自行计算或补发提醒。
|
||||
- 统一接收各 `asset/info` 调用返回的一次性提醒,防止首页子组件或其他调用方先获得字段后将其丢弃;不为提醒新增轮询或额外请求。
|
||||
- 首页旧未读通知弹窗不再以临期通知作为弹窗依据;临期通知仍保留在通知中心,其他通知和既有运营/风险换货弹窗不变。
|
||||
- 临期弹窗与风险换货/运营、普通通知弹窗依次展示,不叠加;通知卡片去掉左侧未读边框,标题右侧使用“未读/已读”小按钮,点击卡片或按钮后按单条已读响应更新。
|
||||
- **BREAKING(请求方法切换)**:全部已读改为 `POST /api/c/v1/notifications/read-all`,不再发送旧 `PUT` 或自动回退;通知列表和未读数沿用现有入参,由后端过滤当前资产的临期通知。
|
||||
- 全部已读保持 `PUT /api/c/v1/notifications/read-all`,以用户 2026-10-14 最新确认覆盖此前 POST 约定;通知列表和未读数沿用现有入参,由后端过滤当前资产的临期通知。
|
||||
|
||||
## Scope and confirmed decisions
|
||||
|
||||
用户于 2026-10-13 确认:不处理代理注册;手机号行为以当前对接说明为准;全部已读以新文档的 `POST` 为准。
|
||||
用户确认:不处理代理注册;手机号行为以当前对接说明为准。全部已读以 2026-10-14 最新确认的 `PUT` 为准,不采用此前文档中的 POST。
|
||||
|
||||
后续用户补充确认:绑定手机号不会使 token 失效,绑定成功不得再跳转登录页。本次修订覆盖旧强制绑定后手动重登流程。
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
| `GET /api/c/v1/asset/info` | 接收一次性 `expiry_reminder`,区分手机号用途 | 原资产、套餐、实名、支付字段 |
|
||||
| `GET /api/c/v1/notifications` | 回归当前资产临期通知的可见性 | 分页、排序、通知中心展示 |
|
||||
| `GET /api/c/v1/notifications/unread-count` | 使用后端当前上下文计数 | 原请求参数、未读入口 |
|
||||
| `POST /api/c/v1/notifications/read-all` | 从旧 PUT 切换为 POST | 用户主动点击“全部已读”的入口 |
|
||||
| `PUT /api/c/v1/notifications/read-all` | 保持 PUT,撤销此前 POST 改动 | 用户主动点击“全部已读”的入口 |
|
||||
|
||||
明确排除:
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
- [update-phone-bind-association-flow](../update-phone-bind-association-flow/proposal.md):其“不同于客户主号则拒绝”“换号迁移全部资产关联”“用 bound_phone 判断资产关联”的旧约定,被本提案的 F1 约定替代;不覆盖其历史文件及完成标记。
|
||||
- [update-post-bind-wechat-login-recheck](../update-post-bind-wechat-login-recheck/proposal.md):根据用户最新确认,替代其“强制绑定后清除 token 并要求手动重登”的约定;改为保留登录态直接进入首页,不改写历史提案。
|
||||
- [update-july-h5-c-iteration](../update-july-h5-c-iteration/proposal.md):仅替代临期通知作为首页弹窗来源的约定,保留通知中心、其他通知和套餐到期信息展示。
|
||||
- [add-personal-notifications](../add-personal-notifications/proposal.md):仅切换全部已读方法及遵循当前资产临期通知范围,不把单条已读的 PUT 一并改为 POST。
|
||||
- [add-personal-notifications](../add-personal-notifications/proposal.md):保持单条/全部已读的 PUT,遵循当前资产临期通知范围及最新卡片样式。
|
||||
- [add-h5-popup-candidate-flow](../add-h5-popup-candidate-flow/proposal.md):保留候选弹窗与单条已读链路;候选结束后再显示临期弹窗,不调用全部已读来关闭任何弹窗。
|
||||
|
||||
技术选择、字段依据及风险见 [design.md](design.md),执行及验收清单见 [tasks.md](tasks.md)。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
### Requirement: Expiry reminders SHALL use the optional asset-info response
|
||||
|
||||
H5 SHALL 仅以 `GET /api/c/v1/asset/info` 返回的可选 `expiry_reminder` 作为本次临期主动提醒来源,在首页展示居中、可关闭的临期弹窗,而不是内联横幅。剩余天数、到期时间、等级及等级名称 SHALL 使用 `remaining_days`、`expires_at`、`level`、`level_name`,SHALL NOT 用旧未读临期通知或客户端计算补发提醒。
|
||||
H5 SHALL 仅以 `GET /api/c/v1/asset/info` 返回的可选 `expiry_reminder` 作为本次临期主动提醒来源,在首页展示居中、可关闭的临期弹窗,而不是内联横幅。剩余天数、到期时间及配色 SHALL 使用 `remaining_days`、`expires_at`、`level`;SHALL NOT 展示 level_name 等级标签,也 SHALL NOT 用旧未读临期通知或客户端计算补发提醒。
|
||||
|
||||
#### Scenario: Asset info includes a reminder
|
||||
|
||||
@@ -75,3 +75,18 @@ H5 SHALL 在风险换货/运营候选结束后展示待处理临期提醒,再
|
||||
|
||||
- **WHEN** 普通通知弹窗正在显示,随后收到 expiry_reminder
|
||||
- **THEN** H5 SHALL 暂存临期提醒,等通知关闭后再展示
|
||||
|
||||
### Requirement: Expiry popup renewal SHALL reuse the homepage renewal flow
|
||||
|
||||
H5 SHALL 在临期弹窗提供“立即续费”,点击后先关闭提醒,再调用首页已有续费入口,使用当前套餐 ID、名称及价格。续费窗口准备/展示期间 SHALL 暂停其他通知弹窗,窗口关闭后再恢复;SHALL NOT 在提醒中另建下单或支付实现。
|
||||
|
||||
#### Scenario: User taps renew in an expiry popup
|
||||
|
||||
- **WHEN** 当前资产有可续费套餐,用户点击临期弹窗“立即续费”
|
||||
- **THEN** 临期弹窗 SHALL 关闭,首页原有续费窗口 SHALL 打开
|
||||
- **AND** 其他排队通知 SHALL 不与续费窗口重叠
|
||||
|
||||
#### Scenario: Current asset has no renewable package
|
||||
|
||||
- **WHEN** 用户点击“立即续费”,但当前套餐 ID 或名称无效
|
||||
- **THEN** H5 SHALL 使用首页原有“当前资产暂无可续费套餐”提示,不创建订单
|
||||
|
||||
@@ -15,25 +15,25 @@ H5 SHALL 保持 `GET /api/c/v1/notifications` 与 `GET /api/c/v1/notifications/u
|
||||
- **WHEN** 当前认证资产由 A 切换为 B
|
||||
- **THEN** H5 SHALL 刷新列表和未读数,防止 A 的延迟响应覆盖 B
|
||||
|
||||
### Requirement: Mark-all-read SHALL use POST and preserve other assets' expiry notifications
|
||||
### Requirement: Mark-all-read SHALL use PUT and preserve other assets' expiry notifications
|
||||
|
||||
H5 SHALL 在用户主动点击通知中心“全部已读”时调用 `POST /api/c/v1/notifications/read-all`,SHALL NOT 使用或自动回退旧 PUT。其他资产的临期通知 SHALL 由后端保留未读;客户端成功后 SHALL 按服务端刷新结果更新当前视图,失败 SHALL 保留未读状态。单条已读 SHALL 继续使用 `PUT /api/c/v1/notifications/{id}/read`。
|
||||
H5 SHALL 在用户主动点击通知中心“全部已读”时调用 `PUT /api/c/v1/notifications/read-all`,SHALL NOT 发送 POST 或用不同方法重试。其他资产的临期通知 SHALL 由后端保留未读;客户端成功后 SHALL 按服务端刷新结果更新当前视图,失败 SHALL 保留未读状态。单条已读 SHALL 继续使用 `PUT /api/c/v1/notifications/{id}/read`。
|
||||
|
||||
#### Scenario: User marks all notifications read on asset A
|
||||
|
||||
- **WHEN** 客户在 A 上主动执行全部已读
|
||||
- **THEN** H5 SHALL 发送 POST 并对齐当前视图
|
||||
- **THEN** H5 SHALL 发送 PUT 并对齐当前视图
|
||||
- **AND** 资产 B 的临期通知 SHALL 保持未读
|
||||
|
||||
#### Scenario: Mark-all-read fails
|
||||
|
||||
- **WHEN** POST 全部已读请求失败
|
||||
- **THEN** H5 SHALL 不提前清空未读计数或把本地项全部标成已读,也不改用 PUT 重试
|
||||
- **WHEN** PUT 全部已读请求失败
|
||||
- **THEN** H5 SHALL 不提前清空未读计数或把本地项全部标成已读,也不改用 POST 重试
|
||||
|
||||
#### Scenario: User reads one notification
|
||||
|
||||
- **WHEN** 用户执行单条已读操作
|
||||
- **THEN** H5 SHALL 继续发送对应通知的 PUT 请求,不受 read-all 方法切换影响
|
||||
- **THEN** H5 SHALL 继续发送对应通知的 PUT 请求
|
||||
|
||||
#### Scenario: Reminder or popup is displayed
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
## 4. 当前资产通知与全部已读
|
||||
|
||||
- [x] 4.1 将 markAllRead 切换为 POST /api/c/v1/notifications/read-all,不回退 PUT;单条已读继续 PUT。
|
||||
- [x] 4.1 按用户最新确认,将 markAllRead 恢复为 PUT /api/c/v1/notifications/read-all,不发送 POST;单条已读继续 PUT。
|
||||
- [x] 4.2 保持列表/未读数入参不变,切换资产后刷新相关结果,禁止旧资产结果覆盖新资产。
|
||||
- [x] 4.3 全部已读成功后按服务端结果对齐当前列表/计数,失败保留未读状态;不在任何提醒/弹窗展示链路调用 read-all。
|
||||
|
||||
@@ -36,12 +36,13 @@
|
||||
- [x] 5.3 通过自动/模拟测试验证换号只更新客户、资产关联保持、归属失效及旧缓存/账号/资产隔离。
|
||||
- [x] 5.4 通过自动/模拟测试验证字段缺省、0/3/4/7/8/15 天及等级、上海日期边界、重复进入和 A/B 上下文;浏览器检查 0 天弹窗及关闭行为。
|
||||
- [x] 5.5 通过共享接收和首页脚本测试验证并发/其他调用方先返回提醒不丢失,以及旧首页弹窗只排除临期通知。
|
||||
- [ ] 5.6 用同一客户的 A/B 两资产验证列表、未读数与 A 上 POST 全部已读;B 的临期通知仍保持未读,普通通知范围不被前端扩大过滤。
|
||||
- [ ] 5.6 用同一客户的 A/B 两资产验证列表、未读数与 A 上 PUT 全部已读;B 的临期通知仍保持未读,普通通知范围不被前端扩大过滤。
|
||||
- [ ] 5.7 验证单条已读仍 PUT、read-all 失败不提前清空、运营/风险换货弹窗和支付/实名/套餐展示不回归。
|
||||
- [x] 5.8 执行 npm run build:h5、npm run build:h5:production,均通过;实施记录明确区分模拟验证与待真实联调场景。
|
||||
- [x] 5.9 执行 openspec validate update-october-batch1-h5-integration --strict,校验通过;仅按实际完成结果更新本任务清单。
|
||||
- [x] 5.10 新增并执行 npm run test:h5-integration,17 组自动/模拟测试全部通过,覆盖强制绑定、token 保留、弹窗排队、通知未读按钮及失败状态。
|
||||
- [x] 5.10 新增并执行 npm run test:h5-integration,19 组自动/模拟测试通过,覆盖强制绑定、token 保留、弹窗排队、续费复用、PUT 全部已读及通知状态。
|
||||
- [x] 5.11 使用本地模拟预览检查移动端居中临期弹窗、绑定表单预填/不同号输入及 1192 弹窗/禁用操作;不发送真实业务请求。
|
||||
- [x] 5.12 去掉通知卡片左侧未读边框,改为标题右侧未读/已读小按钮;验证点击卡片成功后更新、失败不提前显示已读。
|
||||
- [x] 5.13 移除临期弹窗等级文字标签,新增“立即续费”并复用首页入口;续费窗口打开时不推进其他通知。
|
||||
|
||||
5.6 的客户端 A/B 隔离逻辑、5.7 的单条 PUT 和失败不提前清空均已模拟测试通过;真实后端范围及完整支付/实名/候选弹窗回归未完成,不勾选。
|
||||
|
||||
@@ -98,14 +98,14 @@
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
<ExpiryReminderPopup v-if="visibleExpiryReminder" :reminder="visibleExpiryReminder"
|
||||
@close="closeExpiryReminder" />
|
||||
@close="closeExpiryReminder" @renew="renewFromExpiryReminder" />
|
||||
|
||||
<NotificationPopup :show="notificationPopupShow" :items="notificationItems"
|
||||
:current="notificationPopupCurrent" @change="onNotificationChange" @close="closeNotificationPopup"
|
||||
@renew="startHomepageRenewal" />
|
||||
|
||||
<RenewalPaymentPopup ref="renewalPopupRef" :identifier="userStore.state.identifier"
|
||||
paymentRefreshTarget="home" @completed="loadAssetInfo" />
|
||||
paymentRefreshTarget="home" @completed="loadAssetInfo" @visibility-change="onRenewalVisibilityChange" />
|
||||
|
||||
<PopupCandidate v-if='popupCandidateShow' page='home' :identifier='userStore.state.identifier'
|
||||
@close='onPopupCandidateClose' />
|
||||
@@ -162,6 +162,8 @@
|
||||
const homeActive = ref(false);
|
||||
const shownExpiryReminder = ref(null);
|
||||
const awaitingHomeReminders = ref(false);
|
||||
const renewalOpening = ref(false);
|
||||
const renewalVisible = ref(false);
|
||||
let notificationFallbackPending = false;
|
||||
let notificationQueued = false;
|
||||
let homeEntryId = 0;
|
||||
@@ -172,18 +174,18 @@
|
||||
});
|
||||
const showPendingExpiryReminder = () => {
|
||||
const entry = assetStore.state.expiryReminder;
|
||||
if (!homeActive.value || awaitingHomeReminders.value || popupCandidateShow.value || notificationPopupShow.value ||
|
||||
if (!homeActive.value || awaitingHomeReminders.value || renewalOpening.value || renewalVisible.value || popupCandidateShow.value || notificationPopupShow.value ||
|
||||
visibleExpiryReminder.value || !entry || entry.shown || !isCurrentSession(entry) || entry.day !== getShanghaiDay()) return;
|
||||
shownExpiryReminder.value = { ...entry };
|
||||
assetStore.markExpiryReminderShown();
|
||||
};
|
||||
const closeExpiryReminder = () => {
|
||||
const closeExpiryReminder = (resumeQueue = true) => {
|
||||
shownExpiryReminder.value = null;
|
||||
assetStore.dismissExpiryReminder();
|
||||
advanceHomePopupQueue();
|
||||
if (resumeQueue) advanceHomePopupQueue();
|
||||
};
|
||||
const advanceHomePopupQueue = () => {
|
||||
if (!homeActive.value || awaitingHomeReminders.value || popupCandidateShow.value ||
|
||||
if (!homeActive.value || awaitingHomeReminders.value || renewalOpening.value || renewalVisible.value || popupCandidateShow.value ||
|
||||
visibleExpiryReminder.value || notificationPopupShow.value) return;
|
||||
showPendingExpiryReminder();
|
||||
if (visibleExpiryReminder.value) return;
|
||||
@@ -415,7 +417,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
const startHomepageRenewal = () => {
|
||||
const onRenewalVisibilityChange = (visible) => {
|
||||
renewalVisible.value = visible;
|
||||
if (!visible) advanceHomePopupQueue();
|
||||
};
|
||||
const startHomepageRenewal = async () => {
|
||||
if (renewalOpening.value || renewalVisible.value) return;
|
||||
const packageId = Number(deviceInfo.currentPackageId || 0);
|
||||
const packageName = String(deviceInfo.packageName || '').trim();
|
||||
if (!packageId || !packageName || packageName === '-') {
|
||||
@@ -423,7 +430,19 @@
|
||||
return;
|
||||
}
|
||||
|
||||
renewalPopupRef.value?.open([packageId], [packageName], deviceInfo.renewalPrice);
|
||||
renewalOpening.value = true;
|
||||
try {
|
||||
await renewalPopupRef.value?.open([packageId], [packageName], deviceInfo.renewalPrice);
|
||||
} finally {
|
||||
renewalOpening.value = false;
|
||||
advanceHomePopupQueue();
|
||||
}
|
||||
};
|
||||
const renewFromExpiryReminder = async () => {
|
||||
if (!visibleExpiryReminder.value || renewalOpening.value || renewalVisible.value) return;
|
||||
closeExpiryReminder(false);
|
||||
await startHomepageRenewal();
|
||||
advanceHomePopupQueue();
|
||||
};
|
||||
|
||||
const modifyWifi = () => {
|
||||
|
||||
@@ -225,11 +225,11 @@ test('shared asset-info receiver keeps a child-first reminder and rejects stale
|
||||
assert.equal(asset.state.expiryReminder.identifier, 'B');
|
||||
});
|
||||
|
||||
test('notification APIs use POST read-all and retain PUT single-read without new parameters', async () => {
|
||||
test('notification APIs use PUT for read-all and single-read without new parameters', async () => {
|
||||
const r = runtime();
|
||||
const { notificationApi } = await r.load('api/modules/notification.js');
|
||||
const all = notificationApi.markAllRead();
|
||||
assert.equal(r.requests[0].method, 'POST');
|
||||
assert.equal(r.requests[0].method, 'PUT');
|
||||
assert.match(r.requests[0].url, /\/notifications\/read-all$/);
|
||||
r.reply(r.requests[0]); await all;
|
||||
const one = notificationApi.markRead(12);
|
||||
@@ -398,6 +398,38 @@ test('expiry popup waits for an open notification and closing it does not mark e
|
||||
assert.equal(page.visibleExpiryReminder.value, null);
|
||||
});
|
||||
|
||||
test('expiry renewal reuses homepage renewal and keeps queued notifications behind payment', async () => {
|
||||
const r = runtime();
|
||||
r.api.assetApi = { getInfo: async () => ({ identifier: 'A', current_package_id: 12, current_package: '月套餐', renewal_price: 990 }) };
|
||||
r.api.notificationApi = {
|
||||
getList: async () => ({ items: [{ id: 2, category: 'approval', is_read: false }] }),
|
||||
getUnreadCount: async () => ({ count: 1 }), markRead: async () => {}
|
||||
};
|
||||
const asset = (await r.load('store/asset.js')).useAssetStore();
|
||||
asset.receiveExpiryReminder({ token: 'session-A', identifier: 'A' }, { remaining_days: 0, level: 'red' });
|
||||
const page = await r.load('pages/index/index.vue', ['visibleExpiryReminder', 'notificationPopupShow', 'onPopupCandidateClose', 'loadUnreadNotifications', 'renewalPopupRef', 'onRenewalVisibilityChange', 'renewFromExpiryReminder', 'startHomepageRenewal']);
|
||||
await r.shown[0](); page.onPopupCandidateClose('empty');
|
||||
await page.loadUnreadNotifications();
|
||||
assert.equal(page.visibleExpiryReminder.value.remaining_days, 0);
|
||||
const opens = [];
|
||||
page.renewalPopupRef.value = { open: async (...args) => {
|
||||
assert.equal(page.visibleExpiryReminder.value, null);
|
||||
assert.equal(page.notificationPopupShow.value, false);
|
||||
opens.push(plain(args));
|
||||
page.onRenewalVisibilityChange(true);
|
||||
} };
|
||||
await page.renewFromExpiryReminder();
|
||||
assert.deepEqual(opens, [[[12], ['月套餐'], 990]]);
|
||||
assert.equal(page.notificationPopupShow.value, false);
|
||||
page.onRenewalVisibilityChange(false);
|
||||
assert.equal(page.notificationPopupShow.value, true);
|
||||
page.notificationPopupShow.value = false;
|
||||
await page.startHomepageRenewal();
|
||||
assert.deepEqual(opens[1], opens[0]);
|
||||
const source = readFileSync(resolve(root, 'components/ExpiryReminderPopup.vue'), 'utf8');
|
||||
assert.match(source, /@tap="\$emit\('renew'\)">立即续费/);
|
||||
});
|
||||
|
||||
test('notification center refreshes server scope after read-all; failures keep unread items', async () => {
|
||||
for (const fail of [false, true]) {
|
||||
const r = runtime();
|
||||
|
||||
Reference in New Issue
Block a user