Browse Source
Break the approved account traffic/SIM payment alignment into executable tasks for subagent-driven implementation.main
1 changed files with 483 additions and 0 deletions
@ -0,0 +1,483 @@ |
|||||
|
# Backend New API Frontend Align Implementation Plan |
||||
|
|
||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
||||
|
|
||||
|
**Goal:** 只改前端,把账户中心流量购买与 SIM 充值对齐后台新增的套餐/订单/支付接口,不改后台、不改 Vite 代理。 |
||||
|
|
||||
|
**Architecture:** 在 `urls.js` 集中补齐后台新路径;在 `AccountView.vue` 把“按 GB 下单/人工确认”改成“选套餐 → 创建业务订单 → POST payments”;支付结果只做 toast/`providerPayload` 最小提示,不引入微信支付 SDK。 |
||||
|
|
||||
|
**Tech Stack:** Vue 3 + TDesign Vue Next + axios (`src/utils/http.js`) + Pinia;远程网关 `/api` 代理保持不变。 |
||||
|
|
||||
|
**Spec:** `docs/superpowers/specs/2026-08-31-backend-new-api-frontend-align-design.md` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## File Map |
||||
|
|
||||
|
| File | Responsibility | |
||||
|
|---|---| |
||||
|
| `src/config/urls.js` | 账户/支付/SIM/auth refresh 路径常量 | |
||||
|
| `src/views/AccountView/AccountView.vue` | 套餐选择、下单、支付、取消未支付订单、SIM 充值弹窗 | |
||||
|
| (不改)`vite.config.js` | 继续远程代理 | |
||||
|
| (不改)`laic-backend/**` | 后台只读对照 | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Task 1: 对齐 `urls.js` |
||||
|
|
||||
|
**Files:** |
||||
|
- Modify: `src/config/urls.js` |
||||
|
|
||||
|
- [ ] **Step 1: 更新账户相关常量** |
||||
|
|
||||
|
把 Account 段改成: |
||||
|
|
||||
|
```js |
||||
|
// Auth |
||||
|
export const AUTH_CAPTCHA = '/v1/auth/captcha' |
||||
|
export const AUTH_LOGIN = '/v1/auth/login' |
||||
|
export const AUTH_REGISTER = '/v1/auth/register' |
||||
|
export const AUTH_SMS_CODE = '/v1/auth/sms-code' |
||||
|
export const AUTH_RESET_PASSWORD = '/v1/auth/reset-password' |
||||
|
export const AUTH_REFRESH = '/v1/auth/refresh' |
||||
|
export const AUTH_LOGOUT = '/v1/auth/logout' |
||||
|
|
||||
|
// Account |
||||
|
export const ACCOUNT_PROFILE = '/v1/account/profile' |
||||
|
export const TRAFFIC_BALANCE = '/v1/account/traffic/balance' |
||||
|
export const TRAFFIC_PACKAGES = '/v1/account/traffic-packages' |
||||
|
export const TRAFFIC_ORDERS = '/v1/account/traffic/orders' |
||||
|
export const TRAFFIC_ORDER = (id) => `/v1/account/traffic/orders/${id}` |
||||
|
export const TRAFFIC_USAGE = '/v1/account/traffic/usage' |
||||
|
export const ACCOUNT_RESOURCE_SUMMARY = '/v1/account/resource-summary' |
||||
|
export const ACCOUNT_DOWNLOADS = '/v1/account/downloads' |
||||
|
export const ACCOUNT_CHANGE_PASSWORD = '/v1/account/security/change-password' |
||||
|
export const ACCOUNT_SESSIONS = '/v1/account/security/sessions' |
||||
|
export const INVOICE_PROFILES = '/v1/account/invoice-profiles' |
||||
|
export const INVOICE_PROFILE = (id) => `/v1/account/invoice-profiles/${id}` |
||||
|
export const INVOICE_ORDERS = '/v1/account/invoice-orders' |
||||
|
export const INVOICE_REQUESTS = '/v1/account/invoice-requests' |
||||
|
export const PAYMENTS = '/v1/account/payments' |
||||
|
export const PAYMENT = (id) => `/v1/account/payments/${id}` |
||||
|
export const SIM_CARDS = '/v1/account/sim-cards' |
||||
|
export const SIM_CARD = (id) => `/v1/account/sim-cards/${id}` |
||||
|
export const SIM_PACKAGES = '/v1/account/sim-packages' |
||||
|
export const SIM_CARD_RECHARGE_ORDERS = (id) => `/v1/account/sim-cards/${id}/recharge-orders` |
||||
|
export const SIM_RECHARGE_ORDER = (id) => `/v1/account/sim-recharge-orders/${id}` |
||||
|
export const SIM_RECHARGE_LOGS = '/v1/account/sim-recharge-logs' |
||||
|
``` |
||||
|
|
||||
|
删除:`SIM_CARD_RECHARGE`。 |
||||
|
|
||||
|
- [ ] **Step 2: 全仓确认无旧常量引用** |
||||
|
|
||||
|
Run: `rg -n "SIM_CARD_RECHARGE\\b|sim-cards/.*/recharge'" src` |
||||
|
|
||||
|
Expected: 无匹配(或仅注释/文档)。 |
||||
|
|
||||
|
- [ ] **Step 3: Commit** |
||||
|
|
||||
|
```bash |
||||
|
git add src/config/urls.js |
||||
|
git commit -m "feat(api): align account billing URL constants with backend" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Task 2: 账户中心 — 流量套餐购买 + 支付 + 取消 |
||||
|
|
||||
|
**Files:** |
||||
|
- Modify: `src/views/AccountView/AccountView.vue` |
||||
|
|
||||
|
- [ ] **Step 1: 增加套餐/支付相关状态** |
||||
|
|
||||
|
在现有 `trafficAmount` 附近替换/新增: |
||||
|
|
||||
|
```js |
||||
|
const trafficPackages = ref([]) |
||||
|
const selectedTrafficPackageId = ref(0) |
||||
|
const trafficOrdering = ref(false) |
||||
|
const payingOrderId = ref(0) |
||||
|
``` |
||||
|
|
||||
|
删除对 `trafficAmount` 作为下单主输入的依赖(可删 ref,或保留但不用于创建订单)。 |
||||
|
|
||||
|
- [ ] **Step 2: 增加金额/支付辅助函数** |
||||
|
|
||||
|
在 helpers 区加入: |
||||
|
|
||||
|
```js |
||||
|
function moneyFromFen(fen) { |
||||
|
return fen == null ? '--' : `¥${(Number(fen) / 100).toFixed(2)}` |
||||
|
} |
||||
|
|
||||
|
function payStatusName(status) { |
||||
|
return ({ |
||||
|
unpaid: '待支付', |
||||
|
paid: '已支付', |
||||
|
cancelled: '已取消', |
||||
|
closed: '已关闭' |
||||
|
})[status] || status || '--' |
||||
|
} |
||||
|
|
||||
|
function describePayment(payment) { |
||||
|
if (!payment) return '支付已创建' |
||||
|
const amount = moneyFromFen(payment.totalFeeFen) |
||||
|
if (payment.providerPayload) return `支付已创建(${amount}),请按返回凭证完成支付` |
||||
|
return `支付已创建(${amount}),状态:${payment.status || '--'}` |
||||
|
} |
||||
|
|
||||
|
async function createPayment(businessType, businessOrderId) { |
||||
|
return request.post(urls.PAYMENTS, { businessType, businessOrderId }) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
同步把旧的 `payStatusName` 文案从“待人工确认/已入账”改为上表。 |
||||
|
|
||||
|
- [ ] **Step 3: 加载流量套餐** |
||||
|
|
||||
|
```js |
||||
|
async function loadTrafficPackages() { |
||||
|
try { |
||||
|
const list = await request.get(urls.TRAFFIC_PACKAGES) |
||||
|
trafficPackages.value = Array.isArray(list) ? list : [] |
||||
|
if (!selectedTrafficPackageId.value && trafficPackages.value.length) { |
||||
|
selectedTrafficPackageId.value = trafficPackages.value[0].id |
||||
|
} |
||||
|
} catch { |
||||
|
trafficPackages.value = [] |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
在 `load()` 的 `Promise.allSettled` 中加入 `loadTrafficPackages()`。 |
||||
|
|
||||
|
- [ ] **Step 4: 改流量 Tab 模板为套餐选择** |
||||
|
|
||||
|
把购买区从 `t-input-number` 改为套餐选择,例如: |
||||
|
|
||||
|
```vue |
||||
|
<div class="traffic-purchase-layout" style="margin-top:16px"> |
||||
|
<div class="traffic-amount-block"> |
||||
|
<label> |
||||
|
<span>选择流量套餐</span> |
||||
|
<t-radio-group |
||||
|
v-model="selectedTrafficPackageId" |
||||
|
:disabled="!trafficPackages.length || trafficOrdering" |
||||
|
> |
||||
|
<t-radio |
||||
|
v-for="pkg in trafficPackages" |
||||
|
:key="pkg.id" |
||||
|
:value="pkg.id" |
||||
|
> |
||||
|
{{ pkg.name }} · {{ (Number(pkg.amountBytes) / 1024 / 1024 / 1024).toFixed(0) }}GB · {{ money(pkg.price) }} |
||||
|
<small v-if="pkg.validityDays"> / {{ pkg.validityDays }}天</small> |
||||
|
</t-radio> |
||||
|
</t-radio-group> |
||||
|
<small v-if="!trafficPackages.length">暂无可用套餐</small> |
||||
|
</label> |
||||
|
</div> |
||||
|
<div class="traffic-order-summary"> |
||||
|
<span>订单概要</span> |
||||
|
<strong>云媒体流量订单</strong> |
||||
|
<small>创建订单后发起微信支付;金额以服务端订单为准。</small> |
||||
|
<t-button |
||||
|
theme="primary" |
||||
|
:loading="trafficOrdering" |
||||
|
:disabled="!selectedTrafficPackageId || !trafficPackages.length" |
||||
|
@click="createTrafficOrderAndPay" |
||||
|
>购买并支付</t-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
订单表增加操作列(未支付可取消): |
||||
|
|
||||
|
```js |
||||
|
const orderColumns = [ |
||||
|
{ colKey: 'id', title: '订单号' }, |
||||
|
{ colKey: 'content', title: '内容' }, |
||||
|
{ colKey: 'unitPrice', title: '单价' }, |
||||
|
{ colKey: 'totalPrice', title: '金额' }, |
||||
|
{ colKey: 'payStatus', title: '状态' }, |
||||
|
{ colKey: 'createdAt', title: '时间' }, |
||||
|
{ colKey: 'actions', title: '操作' } |
||||
|
] |
||||
|
``` |
||||
|
|
||||
|
模板: |
||||
|
|
||||
|
```vue |
||||
|
<template #actions="{ row }"> |
||||
|
<t-button |
||||
|
v-if="row.payStatus === 'unpaid'" |
||||
|
variant="text" |
||||
|
theme="primary" |
||||
|
:loading="payingOrderId === row.id" |
||||
|
@click="payTrafficOrder(row)" |
||||
|
>去支付</t-button> |
||||
|
<t-button |
||||
|
v-if="row.payStatus === 'unpaid'" |
||||
|
variant="text" |
||||
|
theme="danger" |
||||
|
@click="cancelTrafficOrder(row.id)" |
||||
|
>取消</t-button> |
||||
|
<span v-else>--</span> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 5: 实现下单/支付/取消** |
||||
|
|
||||
|
```js |
||||
|
async function createTrafficOrderAndPay() { |
||||
|
if (!selectedTrafficPackageId.value) { |
||||
|
ui.toast('请选择流量套餐') |
||||
|
return |
||||
|
} |
||||
|
trafficOrdering.value = true |
||||
|
try { |
||||
|
const order = await request.post(urls.TRAFFIC_ORDERS, { |
||||
|
packageId: Number(selectedTrafficPackageId.value) |
||||
|
}) |
||||
|
const payment = await createPayment('traffic_order', order.id) |
||||
|
ui.toast(describePayment(payment)) |
||||
|
orderPageNum.value = 1 |
||||
|
await Promise.all([loadOrders(), request.get(urls.TRAFFIC_BALANCE).then((data) => { |
||||
|
balance.value = data?.balanceGb == null ? '--' : Number(data.balanceGb).toFixed(1) |
||||
|
})]) |
||||
|
} catch (error) { |
||||
|
ui.toast(error.message || '购买失败') |
||||
|
} finally { |
||||
|
trafficOrdering.value = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function payTrafficOrder(row) { |
||||
|
payingOrderId.value = row.id |
||||
|
try { |
||||
|
const payment = await createPayment('traffic_order', row.id) |
||||
|
ui.toast(describePayment(payment)) |
||||
|
await loadOrders() |
||||
|
} catch (error) { |
||||
|
ui.toast(error.message || '发起支付失败') |
||||
|
} finally { |
||||
|
payingOrderId.value = 0 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function cancelTrafficOrder(id) { |
||||
|
const ok = await ui.confirm('取消该流量订单?', '仅未支付订单可取消。', ['订单将关闭', '可重新选择套餐下单']) |
||||
|
if (!ok) return |
||||
|
try { |
||||
|
await request.delete(urls.TRAFFIC_ORDER(id)) |
||||
|
ui.toast('订单已取消') |
||||
|
await loadOrders() |
||||
|
} catch (error) { |
||||
|
ui.toast(error.message || '取消失败') |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
删除旧 `createOrder()`。 |
||||
|
|
||||
|
- [ ] **Step 6: 浏览器冒烟(远程)** |
||||
|
|
||||
|
1. 打开 `/account` → 云媒体流量 |
||||
|
2. 能看到套餐列表(或空态提示) |
||||
|
3. 有套餐时点“购买并支付”:成功 toast 或明确业务错误(如支付未开通) |
||||
|
4. 未支付订单出现“去支付/取消” |
||||
|
|
||||
|
- [ ] **Step 7: Commit** |
||||
|
|
||||
|
```bash |
||||
|
git add src/views/AccountView/AccountView.vue |
||||
|
git commit -m "feat(account): buy traffic packages via order and payment APIs" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Task 3: 账户中心 — SIM 套餐充值 + 支付 |
||||
|
|
||||
|
**Files:** |
||||
|
- Modify: `src/views/AccountView/AccountView.vue` |
||||
|
|
||||
|
- [ ] **Step 1: 增加 SIM 充值状态** |
||||
|
|
||||
|
```js |
||||
|
const simPackages = ref([]) |
||||
|
const simRechargeOpen = ref(false) |
||||
|
const simRechargeCard = ref(null) |
||||
|
const selectedSimPackageId = ref(0) |
||||
|
const simRecharging = ref(false) |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 2: 加载 SIM 套餐** |
||||
|
|
||||
|
```js |
||||
|
async function loadSimPackages() { |
||||
|
try { |
||||
|
const list = await request.get(urls.SIM_PACKAGES) |
||||
|
simPackages.value = Array.isArray(list) ? list : [] |
||||
|
} catch { |
||||
|
simPackages.value = [] |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
在 `load()` / `loadSim()` 流程中调用(建议 `load()` 的 `allSettled` 增加一项)。 |
||||
|
|
||||
|
- [ ] **Step 3: 表格操作列改为“充值”** |
||||
|
|
||||
|
模板把: |
||||
|
|
||||
|
```vue |
||||
|
<template #actions><span>运营商通道未接入</span></template> |
||||
|
``` |
||||
|
|
||||
|
改为: |
||||
|
|
||||
|
```vue |
||||
|
<template #actions="{ row }"> |
||||
|
<t-button variant="text" theme="primary" @click="openSimRecharge(row)">充值</t-button> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 4: 增加充值弹窗** |
||||
|
|
||||
|
在现有 dialogs 后追加: |
||||
|
|
||||
|
```vue |
||||
|
<t-dialog |
||||
|
v-model:visible="simRechargeOpen" |
||||
|
header="通信卡充值" |
||||
|
width="480px" |
||||
|
destroy-on-close |
||||
|
dialog-class-name="recharge-modal" |
||||
|
placement="center" |
||||
|
> |
||||
|
<p>ICCID:{{ simRechargeCard?.iccid || '--' }}</p> |
||||
|
<label> |
||||
|
<span>选择套餐</span> |
||||
|
<t-radio-group v-model="selectedSimPackageId" :disabled="!simPackages.length || simRecharging"> |
||||
|
<t-radio v-for="pkg in simPackages" :key="pkg.id" :value="pkg.id"> |
||||
|
{{ pkg.name }} · {{ pkg.amountGb }}GB · {{ moneyFromFen(pkg.saleFeeFen) }} |
||||
|
<small v-if="pkg.validityMonths"> / {{ pkg.validityMonths }}个月</small> |
||||
|
</t-radio> |
||||
|
</t-radio-group> |
||||
|
<small v-if="!simPackages.length">暂无可用通信卡套餐</small> |
||||
|
</label> |
||||
|
<template #footer> |
||||
|
<t-button variant="outline" @click="simRechargeOpen = false">取消</t-button> |
||||
|
<t-button |
||||
|
theme="primary" |
||||
|
:loading="simRecharging" |
||||
|
:disabled="!selectedSimPackageId || !simPackages.length" |
||||
|
@click="submitSimRecharge" |
||||
|
>创建并支付</t-button> |
||||
|
</template> |
||||
|
</t-dialog> |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 5: 实现打开/提交** |
||||
|
|
||||
|
```js |
||||
|
function openSimRecharge(card) { |
||||
|
simRechargeCard.value = card |
||||
|
selectedSimPackageId.value = simPackages.value[0]?.id || 0 |
||||
|
simRechargeOpen.value = true |
||||
|
} |
||||
|
|
||||
|
async function submitSimRecharge() { |
||||
|
if (!simRechargeCard.value?.id) return |
||||
|
if (!selectedSimPackageId.value) { |
||||
|
ui.toast('请选择套餐') |
||||
|
return |
||||
|
} |
||||
|
simRecharging.value = true |
||||
|
try { |
||||
|
const order = await request.post( |
||||
|
urls.SIM_CARD_RECHARGE_ORDERS(simRechargeCard.value.id), |
||||
|
{ packageId: Number(selectedSimPackageId.value) } |
||||
|
) |
||||
|
const payment = await createPayment('sim_recharge_order', order.id) |
||||
|
ui.toast(describePayment(payment)) |
||||
|
simRechargeOpen.value = false |
||||
|
await loadSim() |
||||
|
} catch (error) { |
||||
|
ui.toast(error.message || '充值下单失败') |
||||
|
} finally { |
||||
|
simRecharging.value = false |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- [ ] **Step 6: 浏览器冒烟** |
||||
|
|
||||
|
1. `/account` → 通信卡 |
||||
|
2. 有卡时点“充值”,弹窗列出套餐 |
||||
|
3. 创建并支付:成功 toast 或明确业务错误 |
||||
|
4. 充值记录表仍可加载 |
||||
|
|
||||
|
- [ ] **Step 7: Commit** |
||||
|
|
||||
|
```bash |
||||
|
git add src/views/AccountView/AccountView.vue |
||||
|
git commit -m "feat(account): recharge SIM cards via package order and payment APIs" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### Task 4: 回归与收尾核对 |
||||
|
|
||||
|
**Files:** |
||||
|
- Verify: `src/config/urls.js` |
||||
|
- Verify: `src/views/AccountView/AccountView.vue` |
||||
|
|
||||
|
- [ ] **Step 1: 静态核对** |
||||
|
|
||||
|
Run: |
||||
|
|
||||
|
```bash |
||||
|
rg -n "SIM_CARD_RECHARGE\\b|等待管理员确认|运营商通道未接入|amountGb: Number\\(trafficAmount" src |
||||
|
rg -n "TRAFFIC_PACKAGES|PAYMENTS|SIM_PACKAGES|SIM_CARD_RECHARGE_ORDERS|AUTH_REFRESH" src/config/urls.js src/views/AccountView/AccountView.vue |
||||
|
``` |
||||
|
|
||||
|
Expected: |
||||
|
- 旧文案/旧路径无匹配 |
||||
|
- 新常量均被引用(`AUTH_REFRESH` 可仅出现在 `urls.js`) |
||||
|
|
||||
|
- [ ] **Step 2: 页面回归** |
||||
|
|
||||
|
远程环境下打开: |
||||
|
|
||||
|
1. 账户概览 / 资料编辑 |
||||
|
2. 流量余额、订单、消费、下载 |
||||
|
3. 通信卡列表与充值记录 |
||||
|
4. 账号安全会话、发票页 |
||||
|
|
||||
|
确认无白屏、无控制台模块错误。 |
||||
|
|
||||
|
- [ ] **Step 3: 若有未提交改动则提交** |
||||
|
|
||||
|
```bash |
||||
|
git status --short |
||||
|
# 如有残留相关改动: |
||||
|
git add src/config/urls.js src/views/AccountView/AccountView.vue |
||||
|
git commit -m "chore(account): finish backend billing API frontend alignment" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Spec Coverage Check |
||||
|
|
||||
|
| Spec 项 | Task | |
||||
|
|---|---| |
||||
|
| urls 补齐 traffic-packages/payments/sim-packages/recharge-orders/refresh;删除错误 recharge | Task 1 | |
||||
|
| 流量:套餐下单 → 支付;取消未支付 | Task 2 | |
||||
|
| SIM:套餐充值订单 → 支付 | Task 3 | |
||||
|
| 不改代理/不改后台/操作日志保持 commands | 全任务遵守 | |
||||
|
| 远程冒烟验证 | Task 2/3/4 | |
||||
|
|
||||
|
## Placeholder Scan |
||||
|
|
||||
|
无 TBD/TODO;关键步骤含具体代码与命令。 |
||||
Loading…
Reference in new issue