完善数字分身积分充值与财务管理 #38
@@ -85,42 +85,6 @@ def update_order_status(order_no: str, body: dict = Body(...)):
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/orders/{order_no}/refund")
|
|
||||||
def request_refund(order_no: str, body: dict = Body(...)):
|
|
||||||
try:
|
|
||||||
data = finance_service.request_refund(
|
|
||||||
order_no,
|
|
||||||
reason=str(body.get("reason") or "").strip(),
|
|
||||||
operator=str(body.get("operator") or "后台管理员").strip(),
|
|
||||||
)
|
|
||||||
return ApiResponse(data=data, message="退款申请已提交")
|
|
||||||
except FinanceServiceError as exc:
|
|
||||||
_raise(exc)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/refunds")
|
|
||||||
def refunds(
|
|
||||||
page: int = Query(1, ge=1),
|
|
||||||
page_size: int = Query(20, ge=1, le=100),
|
|
||||||
status: str = Query(""),
|
|
||||||
):
|
|
||||||
db = _session()
|
|
||||||
try:
|
|
||||||
total, items = finance_service.list_refunds(db, page=page, page_size=page_size, status=status)
|
|
||||||
return ApiResponse(data={"total": total, "page": page, "page_size": page_size, "items": items})
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/refunds/{refund_no}/confirm")
|
|
||||||
def confirm_refund(refund_no: str, body: dict = Body(...)):
|
|
||||||
try:
|
|
||||||
data = finance_service.confirm_refund(refund_no, body)
|
|
||||||
return ApiResponse(data=data, message="退款结果已登记")
|
|
||||||
except FinanceServiceError as exc:
|
|
||||||
_raise(exc)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/invoices")
|
@router.get("/invoices")
|
||||||
def invoices(
|
def invoices(
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
|
|||||||
@@ -419,7 +419,6 @@ def charge_via_huihui_org(payload: dict = Body(...), authorization: str = Header
|
|||||||
scene = str(payload.get("payScene") or "APP").upper()
|
scene = str(payload.get("payScene") or "APP").upper()
|
||||||
if scene not in PAYMENT_SCENES:
|
if scene not in PAYMENT_SCENES:
|
||||||
return fail("支付场景不正确", 400)
|
return fail("支付场景不正确", 400)
|
||||||
pay_type = 6 if scene == "LITE" else (1 if method == "wechat" else 2)
|
|
||||||
if scene == "LITE" and method != "wechat":
|
if scene == "LITE" and method != "wechat":
|
||||||
return fail("微信小程序仅支持微信支付", 400)
|
return fail("微信小程序仅支持微信支付", 400)
|
||||||
cents = _price_cents(plan.price)
|
cents = _price_cents(plan.price)
|
||||||
@@ -427,7 +426,7 @@ def charge_via_huihui_org(payload: dict = Body(...), authorization: str = Header
|
|||||||
result = HuihuiOrgPaymentClient().create_order(
|
result = HuihuiOrgPaymentClient().create_order(
|
||||||
token=user.huihui_token, user_id=user.huihui_user_id,
|
token=user.huihui_token, user_id=user.huihui_user_id,
|
||||||
name=user.nickname, phone=user.phone, avatar=user.avatar_url,
|
name=user.nickname, phone=user.phone, avatar=user.avatar_url,
|
||||||
amount=f"{cents / 100:.2f}", pay_type=pay_type,
|
amount=f"{cents / 100:.2f}",
|
||||||
)
|
)
|
||||||
except HuihuiPaymentError as exc:
|
except HuihuiPaymentError as exc:
|
||||||
return fail(str(exc), 502)
|
return fail(str(exc), 502)
|
||||||
@@ -445,7 +444,10 @@ def charge_via_huihui_org(payload: dict = Body(...), authorization: str = Header
|
|||||||
)
|
)
|
||||||
db.add(order)
|
db.add(order)
|
||||||
db.commit()
|
db.commit()
|
||||||
return ok(_payment_payload(order, get_or_create_account(db, user.id)))
|
result = _payment_payload(order, get_or_create_account(db, user.id))
|
||||||
|
if "fat-open.99hui.com" in os.getenv("HUIHUI_ORG_BASE_URL", ""):
|
||||||
|
result["checkoutToken"] = user.huihui_token
|
||||||
|
return ok(result)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/token/payment/{order_id}")
|
@router.get("/token/payment/{order_id}")
|
||||||
@@ -457,27 +459,7 @@ def payment_status(order_id: str, authorization: str = Header(None), db: Session
|
|||||||
).first()
|
).first()
|
||||||
if not order:
|
if not order:
|
||||||
return fail("支付订单不存在", 404)
|
return fail("支付订单不存在", 404)
|
||||||
if order.provider == "huihui_org" and order.status == "pending":
|
# 会会组织支付以回调为准;查询此接口只返回本地订单,不向会会轮询查单。
|
||||||
try:
|
|
||||||
details = HuihuiOrgPaymentClient().get_order(
|
|
||||||
token=user.huihui_token, user_id=user.huihui_user_id,
|
|
||||||
order_no=order.provider_order_no,
|
|
||||||
)
|
|
||||||
status = int(details.get("payStatus") or 0)
|
|
||||||
if (str(details.get("orderNo") or "") == order.provider_order_no
|
|
||||||
and str(details.get("userId") or "") == user.huihui_user_id
|
|
||||||
and int(details.get("chargeType") or 0) == 9
|
|
||||||
and _callback_amount_cents({"chargeAmount": details.get("chargeAmount")}) == order.price_cents):
|
|
||||||
order.provider_status = str(status)
|
|
||||||
if status in (2, 3, 4):
|
|
||||||
_settle_paid_order(db, order, provider_status=f"ORG_{status}",
|
|
||||||
transaction_no=order.provider_order_no)
|
|
||||||
elif status == 5:
|
|
||||||
order.status = "closed"
|
|
||||||
db.commit()
|
|
||||||
db.refresh(order)
|
|
||||||
except (HuihuiPaymentError, TypeError, ValueError):
|
|
||||||
pass # 查询失败时保持待支付,不将未经核对的结果入账。
|
|
||||||
if order.provider == "wechat_virtual" and order.status == "pending" and user.wechat_mp_openid:
|
if order.provider == "wechat_virtual" and order.status == "pending" and user.wechat_mp_openid:
|
||||||
try:
|
try:
|
||||||
provider_data = query_wechat_virtual_order(
|
provider_data = query_wechat_virtual_order(
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ class HuihuiOrgPaymentClient(HuihuiPaymentClient):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def create_order(self, *, token: str, user_id: str, name: str, phone: str,
|
def create_order(self, *, token: str, user_id: str, name: str, phone: str,
|
||||||
avatar: str, amount: str, pay_type: int) -> dict:
|
avatar: str, amount: str) -> dict:
|
||||||
body = {"chargeAmount": float(amount), "payType": pay_type,
|
body = {"chargeAmount": float(amount),
|
||||||
"commodityName": "数字分身积分充值", "userId": user_id,
|
"commodityName": "数字分身积分充值", "userId": user_id,
|
||||||
"name": name or "", "phoneNumber": phone or "", "avatar": avatar or ""}
|
"name": name or "", "phoneNumber": phone or "", "avatar": avatar or ""}
|
||||||
return self._request("POST", "/order/tokenRecharge", token=token, user_id=user_id, body=body)
|
return self._request("POST", "/order/tokenRecharge", token=token, user_id=user_id, body=body)
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export interface TokenPaymentOrder {
|
|||||||
status: 'pending' | 'paid' | 'failed' | 'closed' | 'refunded'
|
status: 'pending' | 'paid' | 'failed' | 'closed' | 'refunded'
|
||||||
provider: 'huihui' | 'wechat_virtual' | 'huihui_org'
|
provider: 'huihui' | 'wechat_virtual' | 'huihui_org'
|
||||||
providerOrderNo?: string
|
providerOrderNo?: string
|
||||||
|
checkoutToken?: string
|
||||||
providerStatus: string
|
providerStatus: string
|
||||||
payMessage: string
|
payMessage: string
|
||||||
failureReason: string
|
failureReason: string
|
||||||
|
|||||||
@@ -45,8 +45,8 @@
|
|||||||
<section class="checkout-section">
|
<section class="checkout-section">
|
||||||
<button
|
<button
|
||||||
class="checkout-btn"
|
class="checkout-btn"
|
||||||
:class="{ disabled: !selectedPlan || charging || Boolean(pendingOrderId) }"
|
:class="{ disabled: !selectedPlan || charging }"
|
||||||
:disabled="!selectedPlan || charging || Boolean(pendingOrderId)"
|
:disabled="!selectedPlan || charging"
|
||||||
@click="doCharge"
|
@click="doCharge"
|
||||||
>
|
>
|
||||||
{{ checkoutLabel }}
|
{{ checkoutLabel }}
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
chargeTokenOrg,
|
chargeTokenOrg,
|
||||||
@@ -93,10 +93,9 @@ import {
|
|||||||
getRechargePlans,
|
getRechargePlans,
|
||||||
getTokenBalance,
|
getTokenBalance,
|
||||||
getTokenOrders,
|
getTokenOrders,
|
||||||
getTokenPaymentStatus,
|
|
||||||
type TokenPaymentOrder
|
type TokenPaymentOrder
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
import { isInUniWebView, onNativeMessage } from '@/utils/uniapp-bridge'
|
import { isInUniWebView } from '@/utils/uniapp-bridge'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -119,7 +118,6 @@ const selectedPlan = ref<any>(null)
|
|||||||
// 支付方式
|
// 支付方式
|
||||||
const paymentNotice = ref('')
|
const paymentNotice = ref('')
|
||||||
const paymentNoticeTone = ref<'pending' | 'success' | 'error'>('pending')
|
const paymentNoticeTone = ref<'pending' | 'success' | 'error'>('pending')
|
||||||
const pendingOrderId = ref(sessionStorage.getItem('hh_pending_payment_order') || '')
|
|
||||||
const recentOrders = ref<Array<TokenPaymentOrder & { invoice?: any }>>([])
|
const recentOrders = ref<Array<TokenPaymentOrder & { invoice?: any }>>([])
|
||||||
const invoiceOrder = ref<(TokenPaymentOrder & { invoice?: any }) | null>(null)
|
const invoiceOrder = ref<(TokenPaymentOrder & { invoice?: any }) | null>(null)
|
||||||
const invoiceType = ref<'personal' | 'company'>('personal')
|
const invoiceType = ref<'personal' | 'company'>('personal')
|
||||||
@@ -128,9 +126,6 @@ const invoiceTaxNumber = ref('')
|
|||||||
const invoiceEmail = ref('')
|
const invoiceEmail = ref('')
|
||||||
const invoiceError = ref('')
|
const invoiceError = ref('')
|
||||||
const invoiceSubmitting = ref(false)
|
const invoiceSubmitting = ref(false)
|
||||||
let pollTimer: number | undefined
|
|
||||||
let pollDeadline = 0
|
|
||||||
let removeNativeListener: (() => void) | undefined
|
|
||||||
|
|
||||||
// 从后端加载余额与套餐
|
// 从后端加载余额与套餐
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -205,7 +200,6 @@ const formatPointsCopy = (value: string) => value.replace(/\bToken\b/gi, '积分
|
|||||||
const checkoutLabel = computed(() => {
|
const checkoutLabel = computed(() => {
|
||||||
if (!selectedPlan.value) return '请选择充值套餐'
|
if (!selectedPlan.value) return '请选择充值套餐'
|
||||||
if (charging.value) return '正在创建支付订单…'
|
if (charging.value) return '正在创建支付订单…'
|
||||||
if (pendingOrderId.value) return '等待支付结果…'
|
|
||||||
return '去支付'
|
return '去支付'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -216,50 +210,6 @@ const payScene = () => {
|
|||||||
return 'H5' as const
|
return 'H5' as const
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearPendingOrder = () => {
|
|
||||||
pendingOrderId.value = ''
|
|
||||||
sessionStorage.removeItem('hh_pending_payment_order')
|
|
||||||
if (pollTimer) window.clearTimeout(pollTimer)
|
|
||||||
pollTimer = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const pollPayment = async () => {
|
|
||||||
if (!pendingOrderId.value) return
|
|
||||||
try {
|
|
||||||
const order = await getTokenPaymentStatus(pendingOrderId.value)
|
|
||||||
if (order.status === 'paid') {
|
|
||||||
currentBalance.value = order.balance
|
|
||||||
paymentNoticeTone.value = 'success'
|
|
||||||
paymentNotice.value = `支付成功,${order.pointsAmount.toLocaleString()} 积分已到账`
|
|
||||||
clearPendingOrder()
|
|
||||||
void loadData()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (order.status === 'failed' || order.status === 'closed') {
|
|
||||||
paymentNoticeTone.value = 'error'
|
|
||||||
paymentNotice.value = order.status === 'closed' ? '订单已关闭,请重新发起支付' : (order.failureReason || '支付未完成,请重新发起')
|
|
||||||
clearPendingOrder()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('查询支付结果失败', error)
|
|
||||||
}
|
|
||||||
if (Date.now() < pollDeadline) pollTimer = window.setTimeout(pollPayment, 2000)
|
|
||||||
else {
|
|
||||||
paymentNoticeTone.value = 'pending'
|
|
||||||
paymentNotice.value = '支付结果仍在确认中,可稍后刷新余额或重新发起'
|
|
||||||
clearPendingOrder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startPolling = (orderId: string) => {
|
|
||||||
pendingOrderId.value = orderId
|
|
||||||
sessionStorage.setItem('hh_pending_payment_order', orderId)
|
|
||||||
pollDeadline = Date.now() + 2 * 60 * 1000
|
|
||||||
if (pollTimer) window.clearTimeout(pollTimer)
|
|
||||||
void pollPayment()
|
|
||||||
}
|
|
||||||
|
|
||||||
const doCharge = async () => {
|
const doCharge = async () => {
|
||||||
if (!selectedPlan.value || charging.value) return
|
if (!selectedPlan.value || charging.value) return
|
||||||
charging.value = true
|
charging.value = true
|
||||||
@@ -269,18 +219,19 @@ const doCharge = async () => {
|
|||||||
if (order.status === 'failed') throw new Error(order.failureReason || '支付订单创建失败')
|
if (order.status === 'failed') throw new Error(order.failureReason || '支付订单创建失败')
|
||||||
if (!order.providerOrderNo || !/^\d+$/.test(order.providerOrderNo)) throw new Error('会会充值订单号缺失')
|
if (!order.providerOrderNo || !/^\d+$/.test(order.providerOrderNo)) throw new Error('会会充值订单号缺失')
|
||||||
paymentNoticeTone.value = 'pending'
|
paymentNoticeTone.value = 'pending'
|
||||||
paymentNotice.value = '支付订单已创建,正在等待付款结果'
|
paymentNotice.value = '支付订单已创建,支付结果将由会会回调确认'
|
||||||
startPolling(order.id)
|
|
||||||
const checkoutBase = window.location.hostname === 'digital.99hui.com'
|
const checkoutBase = window.location.hostname === 'digital.99hui.com'
|
||||||
? 'https://99hui.com/huihui-h5/'
|
? 'https://99hui.com/huihui-h5/'
|
||||||
: 'https://fat-open.99hui.com/huihui-h5/'
|
: 'https://fat-open.99hui.com/huihui-h5/'
|
||||||
const evn = sessionStorage.getItem('hh_launch_evn') || 'wx'
|
const evn = sessionStorage.getItem('hh_launch_evn') || 'wx'
|
||||||
const evnPart = `&evn=${encodeURIComponent(evn)}`
|
const evnPart = `&evn=${encodeURIComponent(evn)}`
|
||||||
window.location.assign(`${checkoutBase}#/payOrder?orderNo=${encodeURIComponent(order.providerOrderNo)}&isOrg=true${evnPart}`)
|
const tokenPart = order.checkoutToken
|
||||||
|
? `&token=${encodeURIComponent(order.checkoutToken)}`
|
||||||
|
: ''
|
||||||
|
window.location.assign(`${checkoutBase}#/payOrder?orderNo=${encodeURIComponent(order.providerOrderNo)}&isOrg=true${evnPart}${tokenPart}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
paymentNoticeTone.value = 'error'
|
paymentNoticeTone.value = 'error'
|
||||||
paymentNotice.value = error?.message || '充值失败,请重试'
|
paymentNotice.value = error?.message || '充值失败,请重试'
|
||||||
clearPendingOrder()
|
|
||||||
} finally {
|
} finally {
|
||||||
charging.value = false
|
charging.value = false
|
||||||
}
|
}
|
||||||
@@ -293,32 +244,6 @@ const goBack = () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadData()
|
void loadData()
|
||||||
removeNativeListener = onNativeMessage((message) => {
|
|
||||||
if (message?.type !== 'paymentResult') return
|
|
||||||
if (message.orderId && message.orderId !== pendingOrderId.value) return
|
|
||||||
if (['cancelled', 'canceled'].includes(String(message.status).toLowerCase())) {
|
|
||||||
paymentNoticeTone.value = 'error'
|
|
||||||
paymentNotice.value = '支付已取消'
|
|
||||||
clearPendingOrder()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (String(message.status).toLowerCase() === 'failed') {
|
|
||||||
paymentNoticeTone.value = 'error'
|
|
||||||
paymentNotice.value = message.message || '支付未完成,请重新发起'
|
|
||||||
clearPendingOrder()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
void pollPayment()
|
|
||||||
})
|
|
||||||
if (pendingOrderId.value) {
|
|
||||||
paymentNotice.value = '正在确认上一笔支付结果'
|
|
||||||
startPolling(pendingOrderId.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (pollTimer) window.clearTimeout(pollTimer)
|
|
||||||
removeNativeListener?.()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -100,9 +100,6 @@ export const getFinancePlans = () => request.get('/finance/plans')
|
|||||||
export const updateFinancePlan = (id, data) => request.patch(`/finance/plans/${id}`, data)
|
export const updateFinancePlan = (id, data) => request.patch(`/finance/plans/${id}`, data)
|
||||||
export const getFinanceOrders = (params) => request.get('/finance/orders', { params })
|
export const getFinanceOrders = (params) => request.get('/finance/orders', { params })
|
||||||
export const updateFinanceOrderStatus = (orderNo, data) => request.patch(`/finance/orders/${orderNo}/status`, data)
|
export const updateFinanceOrderStatus = (orderNo, data) => request.patch(`/finance/orders/${orderNo}/status`, data)
|
||||||
export const requestFinanceRefund = (orderNo, data) => request.post(`/finance/orders/${orderNo}/refund`, data)
|
|
||||||
export const getFinanceRefunds = (params) => request.get('/finance/refunds', { params })
|
|
||||||
export const confirmFinanceRefund = (refundNo, data) => request.post(`/finance/refunds/${refundNo}/confirm`, data)
|
|
||||||
export const getFinanceInvoices = (params) => request.get('/finance/invoices', { params })
|
export const getFinanceInvoices = (params) => request.get('/finance/invoices', { params })
|
||||||
export const updateFinanceInvoice = (invoiceId, data) => request.patch(`/finance/invoices/${invoiceId}`, data)
|
export const updateFinanceInvoice = (invoiceId, data) => request.patch(`/finance/invoices/${invoiceId}`, data)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
<div class="summary-grid">
|
<div class="summary-grid">
|
||||||
<div class="stat-card"><div class="stat-label">实收金额</div><div class="stat-value">¥{{ money(summary.paid_revenue) }}</div><small>{{ summary.paid_orders }} 笔已支付</small></div>
|
<div class="stat-card"><div class="stat-label">实收金额</div><div class="stat-value">¥{{ money(summary.paid_revenue) }}</div><small>{{ summary.paid_orders }} 笔已支付</small></div>
|
||||||
<div class="stat-card"><div class="stat-label">待支付订单</div><div class="stat-value warning">{{ summary.pending_orders }}</div><small>可关闭或标记失败</small></div>
|
<div class="stat-card"><div class="stat-label">待支付订单</div><div class="stat-value warning">{{ summary.pending_orders }}</div><small>可关闭或标记失败</small></div>
|
||||||
<div class="stat-card"><div class="stat-label">处理中退款</div><div class="stat-value danger">{{ summary.processing_refunds }}</div><small>等待支付渠道确认</small></div>
|
|
||||||
<div class="stat-card"><div class="stat-label">待开发票</div><div class="stat-value success">{{ summary.pending_invoices }}</div><small>等待财务处理</small></div>
|
<div class="stat-card"><div class="stat-label">待开发票</div><div class="stat-value success">{{ summary.pending_invoices }}</div><small>等待财务处理</small></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,33 +48,16 @@
|
|||||||
<el-table-column label="下单时间" min-width="165"><template #default="{ row }">{{ dateTime(row.created_at) }}</template></el-table-column>
|
<el-table-column label="下单时间" min-width="165"><template #default="{ row }">{{ dateTime(row.created_at) }}</template></el-table-column>
|
||||||
<el-table-column label="操作" width="165" fixed="right">
|
<el-table-column label="操作" width="165" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="row.status === 'paid' && ['none','',null].includes(row.refund_status)" link type="danger" @click="refund(row)">退款</el-button>
|
|
||||||
<el-dropdown v-if="row.status === 'pending'" @command="command => closeOrder(row, command)">
|
<el-dropdown v-if="row.status === 'pending'" @command="command => closeOrder(row, command)">
|
||||||
<el-button link type="warning">状态处理<el-icon><ArrowDown /></el-icon></el-button>
|
<el-button link type="warning">状态处理<el-icon><ArrowDown /></el-icon></el-button>
|
||||||
<template #dropdown><el-dropdown-menu><el-dropdown-item command="closed">关闭订单</el-dropdown-item><el-dropdown-item command="failed">标记失败</el-dropdown-item></el-dropdown-menu></template>
|
<template #dropdown><el-dropdown-menu><el-dropdown-item command="closed">关闭订单</el-dropdown-item><el-dropdown-item command="failed">标记失败</el-dropdown-item></el-dropdown-menu></template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<span v-if="row.status !== 'pending' && !(row.status === 'paid' && ['none','',null].includes(row.refund_status))" class="muted">已处理</span>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-pagination v-model:current-page="orderPage" :page-size="20" :total="orderTotal" layout="total, prev, pager, next" @change="loadOrders" />
|
<el-pagination v-model:current-page="orderPage" :page-size="20" :total="orderTotal" layout="total, prev, pager, next" @change="loadOrders" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="退款管理" name="refunds">
|
|
||||||
<el-table :data="refunds" v-loading="loading" class="data-table">
|
|
||||||
<el-table-column label="退款单号" min-width="220" prop="refund_no" />
|
|
||||||
<el-table-column label="原订单" min-width="210" prop="order_no" />
|
|
||||||
<el-table-column label="用户" width="150"><template #default="{ row }">{{ row.user_nickname || row.user_phone || '--' }}</template></el-table-column>
|
|
||||||
<el-table-column label="金额" width="100"><template #default="{ row }">¥{{ money(row.amount) }}</template></el-table-column>
|
|
||||||
<el-table-column label="状态" width="110"><template #default="{ row }"><el-tag :type="tagType(row.status)">{{ statusLabel[row.status] || row.status }}</el-tag></template></el-table-column>
|
|
||||||
<el-table-column label="原因" min-width="160" prop="reason" show-overflow-tooltip />
|
|
||||||
<el-table-column label="创建时间" min-width="165"><template #default="{ row }">{{ dateTime(row.created_at) }}</template></el-table-column>
|
|
||||||
<el-table-column label="对账确认" width="155" fixed="right">
|
|
||||||
<template #default="{ row }"><template v-if="['pending','processing'].includes(row.status)"><el-button link type="success" @click="confirmRefund(row, 'succeeded')">已退款</el-button><el-button link type="danger" @click="confirmRefund(row, 'failed')">失败</el-button></template><span v-else class="muted">{{ row.failure_reason || '已完成' }}</span></template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="发票管理" name="invoices">
|
<el-tab-pane label="发票管理" name="invoices">
|
||||||
<el-table :data="invoices" v-loading="loading" class="data-table">
|
<el-table :data="invoices" v-loading="loading" class="data-table">
|
||||||
<el-table-column label="订单号" min-width="210" prop="order_no" />
|
<el-table-column label="订单号" min-width="210" prop="order_no" />
|
||||||
@@ -113,13 +95,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { confirmFinanceRefund, getFinanceInvoices, getFinanceOrders, getFinancePlans, getFinanceRefunds, getFinanceSummary, requestFinanceRefund, updateFinanceInvoice, updateFinanceOrderStatus, updateFinancePlan } from '@/api'
|
import { getFinanceInvoices, getFinanceOrders, getFinancePlans, getFinanceSummary, updateFinanceInvoice, updateFinanceOrderStatus, updateFinancePlan } from '@/api'
|
||||||
|
|
||||||
const activeTab = ref('orders')
|
const activeTab = ref('orders')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const summary = ref({ paid_revenue: 0, paid_orders: 0, pending_orders: 0, processing_refunds: 0, pending_invoices: 0 })
|
const summary = ref({ paid_revenue: 0, paid_orders: 0, pending_orders: 0, pending_invoices: 0 })
|
||||||
const orders = ref([]), refunds = ref([]), invoices = ref([])
|
const orders = ref([]), invoices = ref([])
|
||||||
const plans = ref([]), planDialog = ref(false), currentPlan = ref(null)
|
const plans = ref([]), planDialog = ref(false), currentPlan = ref(null)
|
||||||
const planForm = ref({ name: '', amount: 1, price: 0, badge: '', desc: '' })
|
const planForm = ref({ name: '', amount: 1, price: 0, badge: '', desc: '' })
|
||||||
const orderPage = ref(1), orderTotal = ref(0)
|
const orderPage = ref(1), orderTotal = ref(0)
|
||||||
@@ -137,18 +119,13 @@ const tagType = value => ({ paid: 'success', succeeded: 'success', issued: 'succ
|
|||||||
async function loadSummary() { const res = await getFinanceSummary(); summary.value = res.data || summary.value }
|
async function loadSummary() { const res = await getFinanceSummary(); summary.value = res.data || summary.value }
|
||||||
async function loadPlans() { const res = await getFinancePlans(); plans.value = res.data || [] }
|
async function loadPlans() { const res = await getFinancePlans(); plans.value = res.data || [] }
|
||||||
async function loadOrders() { const res = await getFinanceOrders({ page: orderPage.value, page_size: 20, ...filters.value }); orders.value = res.data?.items || []; orderTotal.value = res.data?.total || 0 }
|
async function loadOrders() { const res = await getFinanceOrders({ page: orderPage.value, page_size: 20, ...filters.value }); orders.value = res.data?.items || []; orderTotal.value = res.data?.total || 0 }
|
||||||
async function loadRefunds() { const res = await getFinanceRefunds({ page: 1, page_size: 100 }); refunds.value = res.data?.items || [] }
|
|
||||||
async function loadInvoices() { const res = await getFinanceInvoices({ page: 1, page_size: 100 }); invoices.value = res.data?.items || [] }
|
async function loadInvoices() { const res = await getFinanceInvoices({ page: 1, page_size: 100 }); invoices.value = res.data?.items || [] }
|
||||||
async function loadActive() { loading.value = true; try { if (activeTab.value === 'plans') await loadPlans(); if (activeTab.value === 'orders') await loadOrders(); if (activeTab.value === 'refunds') await loadRefunds(); if (activeTab.value === 'invoices') await loadInvoices() } finally { loading.value = false } }
|
async function loadActive() { loading.value = true; try { if (activeTab.value === 'plans') await loadPlans(); if (activeTab.value === 'orders') await loadOrders(); if (activeTab.value === 'invoices') await loadInvoices() } finally { loading.value = false } }
|
||||||
async function loadAll() { loading.value = true; try { await Promise.all([loadSummary(), loadPlans(), loadOrders(), loadRefunds(), loadInvoices()]) } finally { loading.value = false } }
|
async function loadAll() { loading.value = true; try { await Promise.all([loadSummary(), loadPlans(), loadOrders(), loadInvoices()]) } finally { loading.value = false } }
|
||||||
function editPlan(row) { currentPlan.value = row; planForm.value = { name: row.name, amount: Number(row.amount), price: Number(row.price), badge: row.badge || '', desc: row.desc || '' }; planDialog.value = true }
|
function editPlan(row) { currentPlan.value = row; planForm.value = { name: row.name, amount: Number(row.amount), price: Number(row.price), badge: row.badge || '', desc: row.desc || '' }; planDialog.value = true }
|
||||||
async function savePlan() { if (!currentPlan.value) return; saving.value = true; try { await updateFinancePlan(currentPlan.value.id, planForm.value); ElMessage.success('充值套餐已更新'); planDialog.value = false; await loadPlans() } finally { saving.value = false } }
|
async function savePlan() { if (!currentPlan.value) return; saving.value = true; try { await updateFinancePlan(currentPlan.value.id, planForm.value); ElMessage.success('充值套餐已更新'); planDialog.value = false; await loadPlans() } finally { saving.value = false } }
|
||||||
|
|
||||||
async function refund(row) {
|
|
||||||
try { const { value } = await ElMessageBox.prompt('退款成功后会扣回本订单发放的全部积分。', `退款 ¥${money(row.price)}`, { inputPlaceholder: '请输入退款原因', inputValidator: value => value?.trim() ? true : '退款原因不能为空', confirmButtonText: '提交退款', cancelButtonText: '取消' }); await requestFinanceRefund(row.order_no, { reason: value, operator: '后台管理员' }); ElMessage.success('退款已提交渠道处理'); await loadAll() } catch (error) { if (error !== 'cancel' && error !== 'close') console.error(error) }
|
|
||||||
}
|
|
||||||
async function closeOrder(row, status) { try { await ElMessageBox.confirm(`确认将订单标记为“${status === 'closed' ? '已关闭' : '失败'}”?`, '订单状态处理', { type: 'warning' }); await updateFinanceOrderStatus(row.order_no, { status, reason: '后台管理员处理' }); ElMessage.success('订单状态已更新'); await loadAll() } catch {} }
|
async function closeOrder(row, status) { try { await ElMessageBox.confirm(`确认将订单标记为“${status === 'closed' ? '已关闭' : '失败'}”?`, '订单状态处理', { type: 'warning' }); await updateFinanceOrderStatus(row.order_no, { status, reason: '后台管理员处理' }); ElMessage.success('订单状态已更新'); await loadAll() } catch {} }
|
||||||
async function confirmRefund(row, status) { try { const message = status === 'succeeded' ? '仅在支付渠道后台已确认退款到账时操作。' : '确认供应商退款失败?'; await ElMessageBox.confirm(message, '退款对账确认', { type: 'warning' }); await confirmFinanceRefund(row.refund_no, { status, failureReason: status === 'failed' ? '供应商退款失败' : '' }); ElMessage.success('退款结果已登记'); await loadAll() } catch {} }
|
|
||||||
function openInvoice(row) { currentInvoice.value = row; invoiceForm.value = { invoiceNo: '', invoiceUrl: '', remark: '' }; invoiceDialog.value = true }
|
function openInvoice(row) { currentInvoice.value = row; invoiceForm.value = { invoiceNo: '', invoiceUrl: '', remark: '' }; invoiceDialog.value = true }
|
||||||
async function issueInvoice() { if (!invoiceForm.value.invoiceNo.trim()) return ElMessage.warning('请填写发票号码'); saving.value = true; try { await updateFinanceInvoice(currentInvoice.value.id, { status: 'issued', ...invoiceForm.value }); ElMessage.success('发票已登记'); invoiceDialog.value = false; await loadAll() } finally { saving.value = false } }
|
async function issueInvoice() { if (!invoiceForm.value.invoiceNo.trim()) return ElMessage.warning('请填写发票号码'); saving.value = true; try { await updateFinanceInvoice(currentInvoice.value.id, { status: 'issued', ...invoiceForm.value }); ElMessage.success('发票已登记'); invoiceDialog.value = false; await loadAll() } finally { saving.value = false } }
|
||||||
async function rejectInvoice(row) { try { const { value } = await ElMessageBox.prompt('请输入驳回原因', '驳回发票申请', { inputValidator: value => value?.trim() ? true : '驳回原因不能为空' }); await updateFinanceInvoice(row.id, { status: 'rejected', remark: value }); ElMessage.success('发票申请已驳回'); await loadAll() } catch {} }
|
async function rejectInvoice(row) { try { const { value } = await ElMessageBox.prompt('请输入驳回原因', '驳回发票申请', { inputValidator: value => value?.trim() ? true : '驳回原因不能为空' }); await updateFinanceInvoice(row.id, { status: 'rejected', remark: value }); ElMessage.success('发票申请已驳回'); await loadAll() } catch {} }
|
||||||
|
|||||||
Reference in New Issue
Block a user