diff --git a/.workbuddy/memory/2026-07-15.md b/.workbuddy/memory/2026-07-15.md index ef40be7..ca65ff0 100644 --- a/.workbuddy/memory/2026-07-15.md +++ b/.workbuddy/memory/2026-07-15.md @@ -166,3 +166,41 @@ - 「系统运行」加 `a-tooltip title="自您首次开通小程序商城起"`(用户要求)。 - 数据流复用:`loadSubscriptionInfo()` 原本就调 `getProductDetail(65)` 填充 `currentProduct`,无需新增接口调用。 - 类型校验:vue-tsc 对 `shop/dashboard/index.vue` 行级错误 0 条;cms 那批历史错误与本次无关。 + +## Dashboard 到期时间续费功能 + 标签矛盾修复 + +- 背景:dashboard「到期时间」行原先在订阅已过期时同时显示绿色「已激活」标签 + 「(已过期)」红字,状态自相矛盾;且即将到期/已过期时无续费入口(用户反馈"没有按钮呢")。 +- 改动 `src/views/shop/dashboard/index.vue`: + - 标签动态化:`a-tag :color="isExpired ? 'red' : 'green'"`,文案 `已过期/已激活`;已过期时显示「(已过期N天)」。 + - 新增续费按钮:`showRenewButton`(`daysToExpire <= 7` 即即将到期或已过期时显示),点击 `onRenew`。 + - 复用订阅 Modal:新增 `subscribeMode`('subscribe'|'renew')+ `renewing` 状态,Modal 的 title/okText/confirm-loading/@ok 按 mode 动态切换;免费应用 alert 文案也按 mode 区分。 + - `confirmRenew` 调 `subscriptionStore.renew(sub.id, selectedPeriod)`(POST /app/subscription/renew/{id}?period=month|year),成功后 `refreshSubscription()`。 + - `onSubscribe` 补设 `subscribeMode='subscribe'`。 +- 续费 API 语义:`renewSubscription` 返回 string(同步完成,疑似余额扣款/免费续期)。若后端续费需走微信扫码支付,需改为返回支付二维码结构并接 pay 流程——待后端确认。 +- 关键字段再确认(用户):开始时间用 `appSubscription.startTime`,到期时间用 `appSubscription.expireTime`,daysToExpire 与 `dayjs()`(浏览器本地时间)对比。 + +## Dashboard 续费改走 subscribe+pay 扫码(后端 renew 接口未实现) + +- 触发:用户截图显示 `POST /api/app/subscription/renew/90` 返回 404,确认后端没有实现 renew 接口。 +- 方案:续费不再调 `store.renew` / `renewSubscription`,改为复用现有 `subscribe + pay` 扫码支付链路(与首次订阅完全一致)。后端 subscribe 对已有 active 订阅会处理续期/创建新记录。 +- 改动 `src/views/shop/dashboard/index.vue` 的 `confirmRenew`: + - 删掉 `await subscriptionStore.renew(sub.id, ...)` 改为 `subscribe({ productId, subscriptionPeriod })`。 + - 免费应用走 `subResult.status === 'active'` 分支,提示「续费成功」并刷新。 + - 付费应用走 `pay(subscriptionId, 'wechat', envVersion)` → 拿到 `miniappQrcode` → 弹支付 Modal + `startPolling`。 + - `payProductName` fallback 文案改为「应用续费」(与订阅的「应用订阅」区分)。 +- `onRenew` 入口、`subscribeMode='renew'`、续费 Modal title/按钮复用不变,UI 上用户感知不到底层调的是 subscribe。 +- 教训/记录:调后端接口前最好先确认接口是否真实存在;前端 `api/app/appSubscription/index.ts` 的 `renewSubscription` 函数可保留(防止后端后续补上接口),但 dashboard 暂不调用。 + +## Dashboard 续费最终定稿:方案1 renew+pay(前端已就绪,等后端实现) + +- 触发:上一步用 subscribe 走续费,后端返回「您已订阅该应用,无需重复购买」——subscribe 对已有 active 订阅是拒绝的,续费不能复用 subscribe。 +- 定稿方案(用户确认):方案1 —— 后端实现 `/renew/{id}?period=xxx` 返回 `SubscribeResult`(含 subscriptionId),前端再调 `pay` 生成小程序码。与 subscribe+pay 对称,职责清晰。 +- 三处改动(前端已全部就绪,等后端补 renew 接口即可联调通): + 1. `src/api/app/appSubscription/index.ts` `renewSubscription`:返回类型 `string` → `SubscribeResult`;取 `res.data.data`(原取 `res.data.message`)。 + 2. `src/store/modules/appSubscription.ts` `renew`:返回类型 `string` → `SubscribeResult`,返回 result 而非 msg。 + 3. `src/views/shop/dashboard/index.vue` `confirmRenew`:改回调 `subscriptionStore.renew(sub.id, period)` 拿 `renewResult` → 免费(status=active)直接成功;付费走 `pay(renewResult.subscriptionId, 'wechat', envVersion)` → 弹码 + `startPolling`。与 confirmSubscribe 完全对称,仅第一步 subscribe→renew、传 sub.id 而非 productId。 +- **后端接口约定(待实现)**:`POST /api/app/subscription/renew/{id}?period=month|year` + - 入参:id=原订阅ID,period=month/year + - 逻辑:校验原订阅归属 → 创建 pending 续费记录(startTime=原expireTime或now,expireTime=新到期)→ 返回 `{ subscriptionId, subscriptionNo, status, message, payPrice?, orderNo? }`(同 SubscribeResult) + - 前端拿到后调 `pay/{subscriptionId}` 生成微信小程序码,与订阅流程一致 +- 续费按钮触发条件不变:`showRenewButton`(daysToExpire<=7,即将到期或已过期)。 diff --git a/src/api/app/appSubscription/index.ts b/src/api/app/appSubscription/index.ts index a420181..6284b29 100644 --- a/src/api/app/appSubscription/index.ts +++ b/src/api/app/appSubscription/index.ts @@ -221,18 +221,19 @@ export async function mpConfirm( } /** - * 续费 + * 续费:基于已有订阅创建续费订单(pending),返回新 subscriptionId * POST /app/subscription/renew/{id}?period=month|year + * 返回结构与 subscribe 一致(含 subscriptionId),前端再调 pay 生成支付码 */ export async function renewSubscription( id: number, period: 'month' | 'year' = 'month' -): Promise { - const res = await request.post>(`${BASE}/renew/${id}`, undefined, { +): Promise { + const res = await request.post>(`${BASE}/renew/${id}`, undefined, { params: { period } }); if (res.data.code === 0) { - return res.data.message || '续费成功'; + return res.data.data as SubscribeResult; } return Promise.reject(new Error(res.data.message)); } diff --git a/src/store/modules/appSubscription.ts b/src/store/modules/appSubscription.ts index 0fc8288..f618892 100644 --- a/src/store/modules/appSubscription.ts +++ b/src/store/modules/appSubscription.ts @@ -286,15 +286,15 @@ export const useAppSubscriptionStore = defineStore('appSubscription', { // ============================================================ /** - * 续费 + * 续费:基于已有订阅创建续费订单,返回新 subscriptionId(再调 pay 发起支付) * @param id 订阅ID * @param period 周期:month/year */ - async renew(id: number, period: 'month' | 'year' = 'month'): Promise { + async renew(id: number, period: 'month' | 'year' = 'month'): Promise { try { - const msg = await renewSubscription(id, period); + const result = await renewSubscription(id, period); this.invalidateCache(); - return msg; + return result; } catch (error) { console.error('续费失败:', error); throw error; diff --git a/src/views/shop/dashboard/index.vue b/src/views/shop/dashboard/index.vue index 1a8adf6..b7bb488 100644 --- a/src/views/shop/dashboard/index.vue +++ b/src/views/shop/dashboard/index.vue @@ -185,13 +185,19 @@