项目中心-支付模块
This commit is contained in:
@@ -2,19 +2,25 @@ import type { PageResult } from '@/types/api'
|
||||
import type {
|
||||
Bill,
|
||||
BillQuery,
|
||||
BillVo,
|
||||
FinanceStats,
|
||||
Invoice,
|
||||
InvoiceQuery,
|
||||
InvoiceTitle,
|
||||
InvoiceTitlePayload,
|
||||
PayMethod,
|
||||
PayOrderResponse,
|
||||
Payment,
|
||||
PaymentResult,
|
||||
} from '@/types/business/finance'
|
||||
import { toBills, toPlanBills, type PlanLike } from '@/utils/billVo'
|
||||
import { toPayMethod } from '@/utils/payMeta'
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// TODO(api): 待接口文档到位后,将以下 mock 替换为 request<T> 调用
|
||||
// 说明:账单 / 支付相关接口已接后端真数据;仅「发票」与「统计」仍是 mock。
|
||||
// 另有已下线的遗留演示扫码页(pay.vue)用的一组 mock,见文件中部注释。
|
||||
|
||||
/** 账单 mock(可变:支付后状态在前端内存中翻转演示) */
|
||||
/** 遗留演示页用的假账单(真账单走 `GET /web/bill/list`,不读这里) */
|
||||
const MOCK_BILLS: Bill[] = [
|
||||
{
|
||||
id: 'bill-001',
|
||||
@@ -163,6 +169,17 @@ export function getFinanceStats(period?: string): Promise<FinanceStats> {
|
||||
return Promise.resolve(structuredClone(DEFAULT_STATS))
|
||||
}
|
||||
|
||||
/** 真账单 id 一定是纯数字(`vm_bill.id` 是 bigint);非数字只可能来自已下线的演示mock */
|
||||
function isRealBillId(id: string): boolean {
|
||||
return /^\d+$/.test(id)
|
||||
}
|
||||
|
||||
/** 拉取「当前登录用户」的全部账单(`GET /web/bill/list`,不带 ids) */
|
||||
async function fetchOwnBills(): Promise<Bill[]> {
|
||||
const rows = await request<BillVo[]>({ url: '/web/bill/list', method: 'get' })
|
||||
return toBills(rows)
|
||||
}
|
||||
|
||||
function matchBillKeyword(item: Bill, keyword: string): boolean {
|
||||
if (!keyword) return true
|
||||
return (
|
||||
@@ -173,26 +190,39 @@ function matchBillKeyword(item: Bill, keyword: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
/** 账单列表(筛选 + 分页) */
|
||||
export function getBillList(query: BillQuery): Promise<PageResult<Bill>> {
|
||||
const filtered = MOCK_BILLS.filter((item) => {
|
||||
/**
|
||||
* 账单列表(筛选 + 分页)。
|
||||
*
|
||||
* <p>⚠️ `GET /web/bill/list` 只按登录用户返回**全量**账单,后端没有 pageNum / keyword /
|
||||
* status 查询参数;单个客户的账单量级是「几条 ~ 几十条」,故筛选与分页放在前端做。
|
||||
* 若日后账单量变大,应给该端点补后端分页参数,这里再切成一次请求。</p>
|
||||
*/
|
||||
export async function getBillList(query: BillQuery): Promise<PageResult<Bill>> {
|
||||
const all = await fetchOwnBills()
|
||||
const filtered = all.filter((item) => {
|
||||
if (query.status !== 'all' && item.status !== query.status) return false
|
||||
return matchBillKeyword(item, query.keyword)
|
||||
})
|
||||
const start = (query.page - 1) * query.pageSize
|
||||
return Promise.resolve({
|
||||
list: filtered.slice(start, start + query.pageSize).map((item) => ({ ...item })),
|
||||
total: filtered.length,
|
||||
})
|
||||
return { list: filtered.slice(start, start + query.pageSize), total: filtered.length }
|
||||
}
|
||||
|
||||
/** 账单详情(不存在返回 null) */
|
||||
export function getBillDetail(id: string): Promise<Bill | null> {
|
||||
const found = MOCK_BILLS.find((item) => item.id === id) ?? null
|
||||
return Promise.resolve(found ? { ...found } : null)
|
||||
export async function getBillDetail(id: string): Promise<Bill | null> {
|
||||
const rows = await request<BillVo[]>({ url: '/web/bill/list', method: 'get', params: { ids: id } })
|
||||
return toBills(rows)[0] ?? null
|
||||
}
|
||||
|
||||
/** 创建支付单(生成二维码支付信息) */
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 遗留演示扫码页(`/finance/bills/:id/pay`,页面 pay.vue)
|
||||
// ---------------------------------------------------------------------
|
||||
// 该页已无任何入口(收银台 checkout.vue 取代了它),但按既定决定保留。
|
||||
// 它用的是下面这几个 mock 函数,与真账单互不干扰:真账单 id 一定是数字,
|
||||
// 假账单 id 形如 `bill-001`,两条路径靠 `isRealBillId` 分流。
|
||||
// 若要下线该页,这一整段(含 MOCK_PAYMENTS / MOCK_RESULTS)可一并删除。
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** 创建支付单(遗留页演示用:生成二维码支付信息) */
|
||||
export function createPayment(billId: string, method: PayMethod): Promise<Payment> {
|
||||
const bill = MOCK_BILLS.find((item) => item.id === billId)
|
||||
if (!bill) return Promise.reject(new Error('账单不存在'))
|
||||
@@ -206,13 +236,38 @@ export function createPayment(billId: string, method: PayMethod): Promise<Paymen
|
||||
return Promise.resolve({ ...payment })
|
||||
}
|
||||
|
||||
/** 切换支付方式(刷新二维码) */
|
||||
/** 切换支付方式(遗留页演示用:刷新二维码) */
|
||||
export function switchPaymentMethod(billId: string, method: PayMethod): Promise<Payment> {
|
||||
return createPayment(billId, method)
|
||||
}
|
||||
|
||||
/** 查询支付结果(无支付记录且未支付返回 null;演示数据 payTime/tradeNo 固定) */
|
||||
export function getPayResult(billId: string): Promise<PaymentResult | null> {
|
||||
/**
|
||||
* 查询支付结果。
|
||||
*
|
||||
* <p><b>真账单</b>:直接读 `vm_bill` —— 账单被回调置为 PAID 时,`pay_time` /
|
||||
* `transaction_no` / `pay_channel` 已由后端一并回写,这里拼出结果页所需结构;
|
||||
* 未支付则返回 null。</p>
|
||||
*
|
||||
* <p><b>假账单</b>(id 形如 `bill-001`):走下面的遗留演示逻辑。真账单 id 一定是数字,
|
||||
* 故只有已下线的 pay.vue 会命中这条分支。</p>
|
||||
*/
|
||||
export async function getPayResult(billId: string): Promise<PaymentResult | null> {
|
||||
if (isRealBillId(billId)) {
|
||||
const bill = await getBillDetail(billId)
|
||||
if (!bill || bill.status !== '已支付') return null
|
||||
return {
|
||||
method: toPayMethod(bill.payChannel),
|
||||
amount: bill.amount,
|
||||
payTime: bill.payTime ?? '—',
|
||||
tradeNo: bill.tradeNo ?? '—',
|
||||
bills: [bill],
|
||||
}
|
||||
}
|
||||
return getLegacyPayResult(billId)
|
||||
}
|
||||
|
||||
/** 遗留演示:假账单的支付结果(详见上方说明,随 pay.vue 一起可删) */
|
||||
function getLegacyPayResult(billId: string): Promise<PaymentResult | null> {
|
||||
const existing = MOCK_RESULTS.get(billId)
|
||||
if (existing) return Promise.resolve(structuredClone(existing))
|
||||
|
||||
@@ -314,3 +369,98 @@ export function setDefaultTitle(id: string): Promise<InvoiceTitle[]> {
|
||||
export function exportReport(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 收银台(确认支付)——以下为**真实后端接口**,不是 mock
|
||||
// 后端类:vermis-modules/…/controller/pc/pay/WebScanPayController,类级 @SaIgnore(匿名可调)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 取收银台「订单明细」——**复用项目详情「合同财务」那个现成接口**,不另开端点。
|
||||
*
|
||||
* <p>`GET /web/requirementPool/get/finance/{requirementId}`(`ProjectRequirementUserController`)
|
||||
* 的 `{id}` 是**需求 id**;后端把「需求 + 项目」拼成一条联合结构(`loadProject` 用
|
||||
* `vm_rd_project.requirement_id` 反查项目),再把该项目的付款计划随 `billPlans` 一并下发。</p>
|
||||
*
|
||||
* <p>本栈支付口径是 `vm_bill_plan.id`(`vm_finance_payment.bill_id` 实测存的就是计划 id),
|
||||
* 故订单明细直接取这份 `billPlans`,只保留**本次要付的期**:
|
||||
* URL 带 `ids`(期次 id)时按期次过滤,不带则取状态为「待支付」的期。</p>
|
||||
*
|
||||
* @param requirementId 需求 id(项目详情路由 `:id`)
|
||||
* @param ids 期次 id(`vm_bill_plan.id`)列表,可选,用于精确指定付哪一期
|
||||
*/
|
||||
export async function getCheckoutBills(requirementId: string, ids: string[] = []): Promise<Bill[]> {
|
||||
if (!/^\d+$/.test(requirementId)) return []
|
||||
const vo = await request<{ billPlans?: PlanLike[] }>({
|
||||
url: `/web/requirementPool/get/finance/${requirementId}`,
|
||||
method: 'get',
|
||||
})
|
||||
const all = toPlanBills(vo?.billPlans)
|
||||
return ids.length > 0
|
||||
? all.filter((bill) => ids.includes(bill.id))
|
||||
: all.filter((bill) => bill.status === '待支付')
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付结果(收银台「我已完成支付」→ 结果页)。
|
||||
*
|
||||
* <p>口径同 {@link getCheckoutBills}:按**需求 id + 期次 id** 从 `billPlans` 里取出该期,
|
||||
* 支付单状态为 `SUCCESS` 才算已支付——计划本身的 status 可能还没被回调推到 DONE。</p>
|
||||
*/
|
||||
export async function getPlanPayResult(requirementId: string, planId: string): Promise<PaymentResult | null> {
|
||||
const bills = await getCheckoutBills(requirementId, [planId])
|
||||
const bill = bills[0]
|
||||
if (!bill || bill.status !== '已支付') return null
|
||||
return {
|
||||
method: toPayMethod(bill.payChannel),
|
||||
amount: bill.amount,
|
||||
payTime: bill.payTime ?? '—',
|
||||
tradeNo: bill.tradeNo ?? '—',
|
||||
bills: [bill],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信 Native 下单(PC 网站扫码):`POST /pay/web/wechat/native`,`@SaIgnore`。
|
||||
*
|
||||
* @param orderNo 商户订单号(前端生成,见 `utils/payMeta.ts#createMerchantOrderNo`)
|
||||
* @param amount 支付金额,单位 **元**(后端内部 `×100` 转「分」提交微信)
|
||||
* @param billIds 本笔支付覆盖的账单 id(合并支付传多张)。
|
||||
* 后端落 `vm_finance_payment`(status=CREATED)时把它序列化进
|
||||
* `request_payload`,支付成功回调据此为**每张账单**落一行
|
||||
* `vm_finance_payment_bill`,缺了它回调分摊落库会跳过。
|
||||
* @returns `codeUrl`(形如 `weixin://wxpay/bizpayurl?pr=xxx`),前端渲染成二维码供扫
|
||||
*/
|
||||
export function createWechatNativeOrder(
|
||||
orderNo: string,
|
||||
amount: number,
|
||||
billIds: string[] = [],
|
||||
): Promise<PayOrderResponse> {
|
||||
return request<PayOrderResponse>({
|
||||
url: '/pay/web/wechat/native',
|
||||
method: 'post',
|
||||
data: { orderNo, amount, billIds },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付宝电脑网站支付下单:`POST /pay/web/alipay/page`,`@SaIgnore`。
|
||||
*
|
||||
* <p>入参对应后端 `IFinancePaymentBo`。其 `outTradeNo` / `status` 上的 `@NotBlank`
|
||||
* 只声明在 AddGroup / EditGroup 分组上,而控制器用的是默认分组 ⇒ 只传
|
||||
* `outTradeNo` + `amount` 即可(`payChannel` / `status` 由后端自己补),不会校验失败。</p>
|
||||
*
|
||||
* @returns `formBody` —— 后端走 `pageExecute(request, "GET")`,实测内容是
|
||||
* **支付宝收银台跳转地址**(`https://…/gateway.do?…`)而不是 HTML 表单;前端两种形态都兜住。
|
||||
*/
|
||||
export function createAlipayPageOrder(
|
||||
outTradeNo: string,
|
||||
amount: number,
|
||||
billIds: string[] = [],
|
||||
): Promise<PayOrderResponse> {
|
||||
return request<PayOrderResponse>({
|
||||
url: '/pay/web/alipay/page',
|
||||
method: 'post',
|
||||
data: { outTradeNo, amount, billIds },
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user