You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

14 KiB

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 段改成:

// 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
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 附近替换/新增:

const trafficPackages = ref([])
const selectedTrafficPackageId = ref(0)
const trafficOrdering = ref(false)
const payingOrderId = ref(0)

删除对 trafficAmount 作为下单主输入的依赖(可删 ref,或保留但不用于创建订单)。

  • Step 2: 增加金额/支付辅助函数

在 helpers 区加入:

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: 加载流量套餐
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 改为套餐选择,例如:

<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>

订单表增加操作列(未支付可取消):

const orderColumns = [
  { colKey: 'id', title: '订单号' },
  { colKey: 'content', title: '内容' },
  { colKey: 'unitPrice', title: '单价' },
  { colKey: 'totalPrice', title: '金额' },
  { colKey: 'payStatus', title: '状态' },
  { colKey: 'createdAt', title: '时间' },
  { colKey: 'actions', title: '操作' }
]

模板:

<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: 实现下单/支付/取消
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
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 充值状态

const simPackages = ref([])
const simRechargeOpen = ref(false)
const simRechargeCard = ref(null)
const selectedSimPackageId = ref(0)
const simRecharging = ref(false)
  • Step 2: 加载 SIM 套餐
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: 表格操作列改为“充值”

模板把:

<template #actions><span>运营商通道未接入</span></template>

改为:

<template #actions="{ row }">
  <t-button variant="text" theme="primary" @click="openSimRecharge(row)">充值</t-button>
</template>
  • Step 4: 增加充值弹窗

在现有 dialogs 后追加:

<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: 实现打开/提交
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
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:

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: 若有未提交改动则提交
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;关键步骤含具体代码与命令。