feat(order): 对齐退款流程并支持状态 6 申请退款中

This commit is contained in:
2026-09-16 03:53:45 +08:00
parent 98c45f2c1e
commit 567d05fa9d
8 changed files with 267 additions and 9 deletions
@@ -0,0 +1,67 @@
---
name: 修正 typings 中 GetApp 类型参数的约束缺失(TS2344)
overview: 修正 vendored 类型声明 typings/types/wx/lib.wx.app.d.ts 第 265 行:为 getApp 的泛型参数补上 extends IAnyObject 约束,消除全量 tsc 校验时唯一的既有 TS2344 报错,与 miniprogram-api-typings 上游新版本写法保持一致。
todos:
- id: fix-getapp-constraint
content: 修正 lib.wx.app.d.ts 第 265 行 GetApp 泛型约束为 T extends IAnyObject = IAnyObject
status: completed
- id: verify-typings-fix
content: 用 [skill:lsp-code-analysis] 核查 GetApp 引用面,运行 tsc --noEmit 确认全项目 0 报错
status: completed
dependencies:
- fix-getapp-constraint
---
## 需求概述
修正前端仓库 `typings` 内置类型声明中的既有 TypeScript 报错,使全量类型校验(含 typings)通过,为后续接入 CI 类型检查扫清障碍。用户已确认采用"单行修正"方案。
## 报错与根因
- 报错(全项目唯一一条,位于 typings、非业务代码):`typings/types/wx/lib.wx.app.d.ts(265,57): error TS2344: Type 'T' does not satisfy the constraint 'IAnyObject'`。
- 根因:`Instance<T extends IAnyObject>` 要求类型参数受约束,而 `GetApp` 泛型只写了默认值 `<T = IAnyObject>`;TypeScript 中默认值不等于约束,故 `Instance<T>` 触发 TS2344。上游新版 typings 已修正为 `<T extends IAnyObject = IAnyObject>`。
- 该文件自仓库首次提交引入后从未改动(`git diff -- typings` 为空),报错仅在全量 `tsc --noEmit` 时暴露,不影响微信开发者工具的编译、预览与上传。
## 核心内容
- 仅修正该声明文件中的一行泛型签名:补齐约束并保留默认值,即 `T extends IAnyObject = IAnyObject`。
- 不升级整套 typings、不改动任何业务代码;修正后对外类型行为保持不变。
- 验收标准:`tsc --noEmit` 全项目(含 typings)0 报错,IDE 诊断无新增告警。
## 技术方案
### 修改点(单文件单行)
- 文件:`typings/types/wx/lib.wx.app.d.ts` 第 265 行
- 修改前:`<T = IAnyObject>(opts?: GetAppOption): Instance<T>`
- 修改后:`<T extends IAnyObject = IAnyObject>(opts?: GetAppOption): Instance<T>`
### 关键决策与兼容性
- 选择"补约束"而非"升级整套 typings":一行修正即与上游新版本写法一致,改动面最小、零运行时影响;整套升级会引入大量无关声明变更,收益低、风险高。
- 影响面核查(已确认):业务代码中不存在 `getApp` 调用,仅有 `miniprogram/app.ts` 的 `App<IAppOption>(...)` 注册(属于 `App.Constructor`,与 `GetApp` 接口无关);`getApp()` 若被调用仍走默认值 `IAnyObject`,类型不变。该修正只影响声明本身。
### 验证方式
- 运行 `npx --yes -p typescript@5.4.5 tsc --noEmit -p tsconfig.json`,预期 0 报错。
- 用 [skill:lsp-code-analysis] 核查 `GetApp`/`Instance` 相关符号引用面,确认修正无其他消费点被波及。
- `git diff` 复核为单行 diff。
### 目录结构(仅列出改动文件)
```
zt-closet-wxmini-frontend/
└── typings/
└── types/wx/lib.wx.app.d.ts # [MODIFY] 第 265 行 GetApp 泛型补充约束 T extends IAnyObject = IAnyObject,修复 TS2344 报错;仅改此一行,不格式化文件、不动其他声明
```
### 实现说明
- 严格控制改动范围:单行修改,不改缩进风格与文件其余内容,保证 `git diff` 只有一行变更。
- 不做额外的 typings 升级或业务代码调整;若未来升级 miniprogram-api-typings,此行与上游新写法一致,可被覆盖而无冲突。
### Skill
- **lsp-code-analysis**
- Purpose: 用语义化引用分析核查 `GetApp`、`Instance` 等符号的引用面,确认修正仅影响该声明、无其他消费点被波及(业务代码无 `getApp` 调用)。
- Expected outcome: 输出相关符号的引用清单与类型解析结果,确认修正后无新增错误、无遗漏消费方。
@@ -0,0 +1,96 @@
---
name: 前端对齐后端退款流程重构(订单状态6)
overview: 前端唯一未同步的后端变更是最新提交 b94dd6d(退款全流程重构 + 新增订单状态 6「申请退款中」)。需在订单页补齐状态 6 的筛选、文案、样式与提示,并按设计文档补充退款状态轮询;其余近期后端改动前端均已同步。
todos:
- id: sync-order-status-def
content: 补充 config.ts OrderStatus 枚举与 models.ts 注释的状态 6「申请退款中」
status: completed
- id: order-page-status-6
content: order.ts 增加状态 6 文案、胶囊、筛选 tab 与 iOS 退款判定;order.wxml 增加审核中提示
status: completed
dependencies:
- sync-order-status-def
- id: refund-status-polling
content: order.ts 实现退款状态轮询:6→5/6→2 提示并刷新,页面隐藏即停止
status: completed
dependencies:
- order-page-status-6
- id: verify-order-sync
content: 用 [skill:lsp-code-analysis] 核查订单状态引用覆盖,运行 npx tsc --noEmit 并手工验证退款全流程
status: completed
dependencies:
- refund-status-polling
---
## 需求概述
用户修改了衣橱小程序的后端代码(退款流程重构),需要检查配套小程序前端是否需要同步修改,并给出具体修改方案。
补充业务约束(用户强调):**处于「6 申请退款中」的订单,用户不可再次发起退款,只能等待审核/退款结果**。
## 排查结论
- 经逐提交核对,前端此前已同步后端近期绝大多数改动:通知「全部已读」接口、通过社区公开穿搭只读查看衣物、衣物处理失败原因与处理状态字段等,均已在现有前端代码中落地。
- 唯一尚未同步的是后端最新的退款流程重构:订单新增「6 申请退款中」状态(申请退款后进入待审核、退款失败回到「已支付」可重新申请、退款成功变为「已退款」)。前端订单页目前缺少该状态的展示与退款结果跟踪能力,需要补齐。
## 核心功能
- 订单列表状态徽标新增「申请退款中」(进行中语义),并在「已支付」与「已取消」之间新增对应筛选标签
- 申请退款提交后,订单显示「申请退款中」及审核中提示;前端周期性跟踪退款结果:成功显示「已退款」并提示、失败回到「已支付」并提示可重新申请
- **「申请退款中」订单为纯等待态:不展示任何退款/取消等操作入口,用户不可再次发起退款,只能等待结果**(后端同样拒绝:`RefundApply` 仅接受订单状态 `2 已支付`,状态 6 再次申请返回 71001「当前状态不允许退款」)
- iOS 虚拟支付订单不展示「申请退款」入口(按后端规则需走 App Store 退款)
- 其余交互(退款原因填写与长度限制、提交成功提示、退款相关错误码文案)已满足新契约,无需调整
## 排查范围与结论
对比前端最新提交(98c45f2)与后端仓库 /root/zt-closet-backend 最新提交(b94dd6d):
- 已同步:`89b59c7` 通知全部已读(前端 notification.ts 已调用 `POST /notification/read-all`,并处理返回的 unread_count)、`c2519c4` 社区公开穿搭只读查看衣物(clothes-detail/outfit-detail 已携带 `outfit_uuid`)、`e9b9754` 衣物处理失败原因(upload/clothes-detail 已使用 `image_process_status`/`fail_reason`)。
- 未同步:`b94dd6d` 退款全流程重构 —— 订单状态枚举扩展为 `oneof=1 2 3 4 5 6`,`OrderStatusRefunding = 6`,退款申请原子流转 2→6 并落待审核流水,退款成功→5、失败→回2。前端需对齐。
## 技术栈
- 微信小程序原生(TypeScript + WXML + WXSS),沿用现有 `utils/request` 封装、`utils/logger` 日志、`STATUS_*` 常量映射模式与全局 pill 胶囊样式体系,无新增依赖。
- 类型校验:`npx tsc --noEmit`(仓库无 npm scripts)。
## 实现方案
1. **状态契约对齐**:`utils/config.ts` 的 `OrderStatus` 枚举新增 `REFUNDING = 6 // 申请退款中`;`types/models.ts` 中 `order_status` 注释补充 `6=申请退款中`。
2. **订单页展示**(对齐 Stitch.md 设计约定):
- `STATUS_TEXT` 增加 `6: '申请退款中'`;`STATUS_CLS` 增加 `6: 'pill--ink'`(遵循 app.wxss「进行中/强调 = pill--ink」约定);`STATUS_FILTERS` 在「已支付」之后插入 `{ name: '申请退款中', value: 6 }`(索引与数组同步,筛选逻辑不受影响)。
- wxml 操作区新增 `wx:elif="{{item.order_status === 6}}"` 分支:**仅渲染灰字等待提示(如「退款审核中,审核结果将通过站内通知告知」),不提供任何可点击操作项**——不显示「申请退款」,也不显示「取消订单」,用户只能等待轮询/通知给出结果。优先复用 `.od-countdown` 同类灰字样式,必要时新增等价小类。
3. **iOS 虚拟支付退款入口收敛**:`RefundApply` 对 iOS 虚拟订单返回 71004(错误码文案前端已具备兜底)。订单响应无平台字段,页面用 `wx.getSystemInfoSync().platform` 判定(与 mall.ts 的 normalizePlatform 同一数据源,需 try/catch 兜底),新增 `_canRefund` = 状态 `2` 且非(`pay_channel === 2` 且 iOS),wxml 按钮改用该标记(状态 6 天然不在 `_canRefund` 范围,不会再出现退款入口)。
4. **退款状态轮询**(设计文档明确约定「前端轮询 GET /order」):列表存在状态 6 订单时,每约 10 秒静默请求 `GET /order?status=6&page=1&page_size=50`(`showError: false`,独立于 fetchList 的 reqSeq 机制,避免干扰列表与闪烁);被跟踪订单从响应中消失即表示已离开退款中 → 触发一次 `refresh()`,并按刷新结果提示:`5`→「退款成功」、`2`→「退款未通过,可重新申请」(找不到时用通用文案);仍为 6 继续等待。无状态 6 订单不发请求;`onHide/onUnload` 清理定时器(与既有倒计时 timer 分开管理,避免互相覆盖)。
5. **不改动项(经核实)**:退款提交交互与文案已满足新契约;状态 6 期间前端无退款入口、后端也拒绝重复申请,天然不会触发 71001;`mall.ts` 支付轮询终态判断(3/4/5)在支付场景不会遇到状态 6;错误码 71000-71004 文案已齐备。
## 实现说明(执行要点)
- 性能与请求量:轮询为单用户轻量查询(page_size 50 封顶),仅在存在状态 6 订单时触发;失败静默降级(`logger.warn`)等待下一轮重试,不阻塞 UI。
- 日志:复用 `logger`(模块名 `order`),仅状态流转与请求失败时记录,成功轮询走 debug,避免日志刷屏。
- 影响面控制:改动仅限订单页与类型/枚举注释,其余页面与接口无波及;若后端未上线新版本(不返回状态 6),前端行为与现状完全一致,可安全先行发布。
## 目录结构(仅列出改动文件)
```
zt-closet-wxmini-frontend/
└── miniprogram/
├── utils/
│ └── config.ts # [MODIFY] OrderStatus 枚举新增 REFUNDING = 6;错误码区已含 71000-71004,无需改动
├── types/
│ └── models.ts # [MODIFY] OrderItem.order_status 注释补充「6 申请退款中」
└── pages/
└── order/
├── order.ts # [MODIFY] STATUS_TEXT/STATUS_CLS/STATUS_FILTERS 增加状态 6;新增 _canRefund(含 iOS 判定);实现退款状态轮询与生命周期清理
├── order.wxml # [MODIFY] 状态 6 操作区仅渲染等待提示(无任何操作按钮);「申请退款」按钮改用 _canRefund
└── order.wxss # [MODIFY/可选] 仅在需要新提示样式时补充,优先复用既有灰字样式
```
## Agent Extensions
### Skill
- **lsp-code-analysis**
- Purpose: 利用语义化引用分析核查 `order_status`、`OrderStatus`、`STATUS_TEXT/STATUS_CLS` 等符号的全部引用,确认状态 6 同步无遗漏(区别于文本搜索的补充验证)
- Expected outcome: 输出完整的引用清单,确认所有订单状态消费点均已覆盖,无遗漏文件
+85 -6
View File
@@ -12,21 +12,35 @@ const STATUS_FILTERS = [
{ name: '全部', value: 0 },
{ name: '待支付', value: 1 },
{ name: '已支付', value: 2 },
{ name: '申请退款中', value: 6 },
{ name: '已取消', value: 3 },
{ name: '已过期', value: 4 },
{ name: '已退款', value: 5 },
]
const STATUS_TEXT: Record<number, string> = { 1: '待支付', 2: '已支付', 3: '已取消', 4: '已过期', 5: '已退款' }
// 零彩色状态语义:待支付 = 实心黑;已支付/已退款 = 描边;已取消/已过期 = 降透明
const STATUS_TEXT: Record<number, string> = { 1: '待支付', 2: '已支付', 3: '已取消', 4: '已过期', 5: '已退款', 6: '申请退款中' }
// 零彩色状态语义:待支付/申请退款中 = 实心黑(进行中);已支付/已退款 = 描边;已取消/已过期 = 降透明
const STATUS_CLS: Record<number, string> = {
1: 'pill--ink',
2: 'pill--line',
3: 'pill--dim',
4: 'pill--dim',
5: 'pill--line pill--dim',
6: 'pill--ink',
}
// 退款结果轮询间隔:仅在列表存在「申请退款中」订单时才发请求
const REFUND_POLL_MS = 10 * 1000
// 设备是否 iOS:iOS 虚拟支付订单退款需走 App Store(后端返回 71004),前端不展示退款入口
const IS_IOS = (() => {
try {
return wx.getSystemInfoSync().platform === 'ios'
} catch (e) {
return false
}
})()
Page({
data: {
filters: STATUS_FILTERS,
@@ -62,19 +76,22 @@ Page({
onShow() {
this.refresh()
this.startCountdown()
this.startRefundWatch()
},
onHide() {
this.stopCountdown()
this.stopRefundWatch()
},
onUnload() {
this.stopCountdown()
this.stopRefundWatch()
},
refresh() {
refresh(): Promise<void> {
this.setData({ page: 1, hasMore: true, loading: this.data.list.length === 0 })
this.fetchList(1)
return this.fetchList(1)
},
onReachBottom() {
@@ -112,7 +129,67 @@ Page({
if (changed) this.setData({ list: next })
},
fetchList(page: number) {
// ===== 退款结果跟踪:申请退款中的订单只等待结果(不可再次发起退款),定期核对是否已出结果 =====
startRefundWatch() {
this.stopRefundWatch()
;(this as any).refundTimer = setInterval(() => this.pollRefunding(), REFUND_POLL_MS)
},
stopRefundWatch() {
const timer = (this as any).refundTimer
if (timer) {
clearInterval(timer)
;(this as any).refundTimer = null
}
},
// 仅当列表存在「申请退款中」订单时才发请求:命中说明仍未出结果,缺失即视为状态已流转
pollRefunding() {
const tracked = this.data.list.filter((it) => it.order_status === 6).map((it) => it.order_no)
if (!tracked.length) return
request<PagedResult<OrderItem>>({
url: '/order',
data: { status: 6, page: 1, page_size: 50 },
showError: false,
})
.then((res) => {
const still = new Set(normalizePaged<OrderItem>(res).list.map((it) => it.order_no))
const changed = tracked.filter((no) => !still.has(no))
if (changed.length) this.settleRefundResult(changed)
})
.catch((e: any) => logger.warn(LOG, '退款结果轮询失败(静默降级), code=', e && e.code))
},
// 核对流转后的最新状态并刷新列表:5=退款成功(终态)、2=退款未通过可重新申请;仍为 6 则继续等待
settleRefundResult(orderNos: string[]) {
request<PagedResult<OrderItem>>({ url: '/order', data: { page: 1, page_size: 50 }, showError: false })
.then((res) => {
const list = normalizePaged<OrderItem>(res).list
orderNos.forEach((no) => {
const latest = list.find((it) => it.order_no === no)
if (!latest) {
logger.warn(LOG, '退款结果核对:未定位到订单最新状态,仅刷新列表, orderNo=', no)
return
}
if (latest.order_status === 6) return
if (latest.order_status === 5) {
logger.info(LOG, '退款成功, orderNo=', no)
wx.showToast({ title: '退款成功', icon: 'success' })
} else if (latest.order_status === 2) {
logger.info(LOG, '退款未通过,可重新申请, orderNo=', no)
wx.showToast({ title: '退款未通过,可重新申请', icon: 'none' })
} else {
logger.info(LOG, '退款状态已更新, orderNo=', no, 'status=', latest.order_status)
wx.showToast({ title: '退款状态已更新', icon: 'none' })
}
})
this.refresh()
})
.catch((e: any) => logger.warn(LOG, '退款结果核对失败(静默降级), code=', e && e.code))
},
// 返回列表请求的 Promise:退款结果核对后需要等列表刷新完成
fetchList(page: number): Promise<void> {
const params: Record<string, any> = { page, page_size: this.data.pageSize }
const filter = this.data.activeFilter
if (filter > 0) params.status = filter
@@ -122,7 +199,7 @@ Page({
;(this as any).reqSeq = seq
if (page > 1) this.setData({ loadingMore: true })
request<PagedResult<OrderItem>>({ url: '/order', data: params })
return request<PagedResult<OrderItem>>({ url: '/order', data: params })
.then((res) => {
if ((this as any).reqSeq !== seq) return
const { list: raw, total } = normalizePaged<OrderItem>(res)
@@ -138,6 +215,8 @@ Page({
_countdown: it.order_status === 1 && it.expire_time > now ? formatCountdown(it.expire_time - now) : '',
_benefit: it.member_days > 0 ? `${it.member_days} 天会员` : '',
_canCancel: it.order_status === 1 && it.pay_channel !== 2,
// 退款入口:仅「已支付」可申请;iOS 虚拟支付订单需走 App Store(后端 71004),不展示入口
_canRefund: it.order_status === 2 && !(it.pay_channel === 2 && IS_IOS),
_payChannelText: it.pay_channel === 2 ? '虚拟支付' : '',
}))
const merged = mergePaged(this.data.list, list, page === 1)
+9 -1
View File
@@ -57,10 +57,18 @@
<view class="btn btn--ink btn--sm" data-no="{{item.order_no}}" bindtap="onPay">去支付</view>
</block>
<block wx:elif="{{item.order_status === 2}}">
<view class="btn btn--ghost btn--sm" data-no="{{item.order_no}}" bindtap="onRefund">申请退款</view>
<view
wx:if="{{item._canRefund}}"
class="btn btn--ghost btn--sm"
data-no="{{item.order_no}}"
bindtap="onRefund"
>申请退款</view>
</block>
<!-- 状态 6「申请退款中」为纯等待态:不提供任何操作入口(不可再次发起退款,后端也拒绝重复申请) -->
</view>
</view>
<view wx:if="{{item.order_status === 6}}" class="od-note">退款审核中,审核结果将通过站内通知告知</view>
</view>
</view>
+7
View File
@@ -90,6 +90,13 @@
font-family: var(--serif);
}
/* 状态 6「申请退款中」纯等待提示(无任何操作入口) */
.od-note {
margin-top: 18rpx;
font-size: 23rpx;
color: var(--ink-3);
}
.od-refund-input {
margin: 0 var(--pad);
width: auto;
+1 -1
View File
@@ -92,7 +92,7 @@ export interface OrderItem {
unit_price: number // 分
pay_amount: number // 分
pay_channel: number // 1 微信支付 2 虚拟支付
order_status: number // 1 待支付 2 已支付 3 已取消 4 已过期 5 已退款
order_status: number // 1 待支付 2 已支付 3 已取消 4 已过期 5 已退款 6 申请退款中
member_days: number
expire_time: number // 秒
pay_time: number
+1
View File
@@ -38,6 +38,7 @@ export enum OrderStatus {
CANCELLED = 3, // 已取消
EXPIRED = 4, // 已过期
REFUNDED = 5, // 已退款
REFUNDING = 6, // 申请退款中(纯等待态,不可再次申请退款)
}
// 额度类型
+1 -1
View File
@@ -262,7 +262,7 @@ declare namespace WechatMiniprogram.App {
}
interface GetApp {
<T = IAnyObject>(opts?: GetAppOption): Instance<T>
<T extends IAnyObject = IAnyObject>(opts?: GetAppOption): Instance<T>
}
}