From 1baa6d87cfdcb27b368f45aa4e79e607159bceb7 Mon Sep 17 00:00:00 2001 From: break Date: Fri, 26 Jun 2026 18:14:26 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A9=B3=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + CONTEXT.md | 6 + scripts/tests/agentRechargeActions.test.mjs | 110 ++++++++++++++++ scripts/tests/agentRechargeDisplay.test.mjs | 49 +++++++ scripts/tests/agentRechargeService.test.mjs | 69 ++++++++++ src/api/modules/agentRecharge.ts | 13 ++ src/types/api/agentRecharge.ts | 5 + .../agent-recharge/agentRechargeActions.ts | 56 ++++++++ .../agent-recharge/agentRechargeDisplay.ts | 3 + src/views/finance/agent-recharge/index.vue | 120 +++++++++++++----- 10 files changed, 404 insertions(+), 30 deletions(-) create mode 100644 scripts/tests/agentRechargeActions.test.mjs create mode 100644 scripts/tests/agentRechargeDisplay.test.mjs create mode 100644 scripts/tests/agentRechargeService.test.mjs create mode 100644 src/views/finance/agent-recharge/agentRechargeActions.ts create mode 100644 src/views/finance/agent-recharge/agentRechargeDisplay.ts diff --git a/.gitignore b/.gitignore index 3740780..d5d5b7d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ npminstall-debug.log # ai .agents + +#build +one-pipe-system-dist.zip diff --git a/CONTEXT.md b/CONTEXT.md index 74df4d6..65d139a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -13,3 +13,9 @@ This context captures the business language used by the admin system for orders, **Operational batch task**: An asynchronous admin task created from an uploaded batch import file and tracked through task status, counts, operator identity, and failure details. _Avoid_: Inline order operation, synchronous import **Operational remark**: A note captured when an admin creates a financial or operational record. Operational remarks are retained for later review and are not edited after creation. _Avoid_: Editable comment, processing note + +**Recharge order**: A C-side asset recharge order created when a user recharges an IoT card or device asset wallet. It is identified by `recharge_order_no`, is associated with `resource_type` and `resource_id`, and may be rejected while pending payment. _Avoid_: Agent recharge order + +**Rejected recharge order**: A recharge order that an admin has rejected before payment because the recharge request should not proceed. Rejection is an irreversible terminal state and does not involve refunds. _Avoid_: Refunded recharge, closed recharge + +**Agent recharge order**: A financial recharge record for an agent/shop wallet in the admin finance module. It uses a different API contract and status model from C-side recharge orders. _Avoid_: Recharge order diff --git a/scripts/tests/agentRechargeActions.test.mjs b/scripts/tests/agentRechargeActions.test.mjs new file mode 100644 index 0000000..d910d68 --- /dev/null +++ b/scripts/tests/agentRechargeActions.test.mjs @@ -0,0 +1,110 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { mkdtemp, rm, stat } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import * as esbuild from 'esbuild' + +async function resolveSrcAlias(path) { + const resolvedPath = join(process.cwd(), 'src', path.slice(2)) + + try { + if ((await stat(resolvedPath)).isDirectory()) { + return join(resolvedPath, 'index.ts') + } + } catch { + // Let esbuild report the original resolution failure. + } + + return resolvedPath +} + +async function importBundledActions() { + const tempDir = await mkdtemp(join(tmpdir(), 'agent-recharge-actions-')) + const bundlePath = join(tempDir, 'agentRechargeActions.mjs') + + await esbuild.build({ + entryPoints: ['src/views/finance/agent-recharge/agentRechargeActions.ts'], + bundle: true, + outfile: bundlePath, + format: 'esm', + platform: 'node', + absWorkingDir: process.cwd(), + plugins: [ + { + name: 'alias-src', + setup(build) { + build.onResolve({ filter: /^@\// }, async (args) => ({ + path: await resolveSrcAlias(args.path) + })) + } + } + ] + }) + + const module = await import(pathToFileURL(bundlePath).href) + return { + module, + cleanup: () => rm(tempDir, { recursive: true, force: true }) + } +} + +test('pending agent recharge exposes reject action for admins with rejection permission', async () => { + const { module, cleanup } = await importBundledActions() + const { buildAgentRechargeActions } = module + const row = { + id: 42, + status: 1, + payment_method: 'wechat', + payment_voucher_key: [] + } + const handledRows = [] + + try { + const actions = buildAgentRechargeActions(row, { + hasAuth: (permission) => permission === 'agent_recharge:reject', + onViewPaymentVoucher: () => {}, + onConfirmPayment: () => {}, + onReject: (recharge) => handledRows.push(recharge) + }) + + assert.deepEqual( + actions.map((action) => action.label), + ['拒绝'] + ) + + actions[0].handler() + + assert.deepEqual(handledRows, [row]) + } finally { + await cleanup() + } +}) + +test('non-pending agent recharge does not expose reject action', async () => { + const { module, cleanup } = await importBundledActions() + const { buildAgentRechargeActions } = module + const row = { + id: 42, + status: 2, + payment_method: 'wechat', + payment_voucher_key: [] + } + + try { + const actions = buildAgentRechargeActions(row, { + hasAuth: (permission) => permission === 'agent_recharge:reject', + onViewPaymentVoucher: () => {}, + onConfirmPayment: () => {}, + onReject: () => {} + }) + + assert.deepEqual( + actions.map((action) => action.label), + [] + ) + } finally { + await cleanup() + } +}) diff --git a/scripts/tests/agentRechargeDisplay.test.mjs b/scripts/tests/agentRechargeDisplay.test.mjs new file mode 100644 index 0000000..8321015 --- /dev/null +++ b/scripts/tests/agentRechargeDisplay.test.mjs @@ -0,0 +1,49 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import * as esbuild from 'esbuild' + +async function importBundledDisplay() { + const tempDir = await mkdtemp(join(tmpdir(), 'agent-recharge-display-')) + const bundlePath = join(tempDir, 'agentRechargeDisplay.mjs') + + await esbuild.build({ + entryPoints: ['src/views/finance/agent-recharge/agentRechargeDisplay.ts'], + bundle: true, + outfile: bundlePath, + format: 'esm', + platform: 'node', + absWorkingDir: process.cwd() + }) + + const module = await import(pathToFileURL(bundlePath).href) + return { + module, + cleanup: () => rm(tempDir, { recursive: true, force: true }) + } +} + +test('agent recharge rejection reason display uses backend rejection_reason when present', async () => { + const { module, cleanup } = await importBundledDisplay() + + try { + assert.equal(module.formatRejectionReason('凭证不清晰'), '凭证不清晰') + } finally { + await cleanup() + } +}) + +test('agent recharge rejection reason display falls back to dash when absent', async () => { + const { module, cleanup } = await importBundledDisplay() + + try { + assert.equal(module.formatRejectionReason(''), '-') + assert.equal(module.formatRejectionReason(null), '-') + assert.equal(module.formatRejectionReason(undefined), '-') + } finally { + await cleanup() + } +}) diff --git a/scripts/tests/agentRechargeService.test.mjs b/scripts/tests/agentRechargeService.test.mjs new file mode 100644 index 0000000..8abe6e0 --- /dev/null +++ b/scripts/tests/agentRechargeService.test.mjs @@ -0,0 +1,69 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import * as esbuild from 'esbuild' + +test('agent recharge rejection posts the rejection reason to the order rejection endpoint', async () => { + const calls = [] + const tempDir = await mkdtemp(join(tmpdir(), 'agent-recharge-service-')) + const bundlePath = join(tempDir, 'agentRechargeService.mjs') + const httpMockPath = join(tempDir, 'httpMock.mjs') + + await writeFile( + httpMockPath, + ` + export const calls = globalThis.__agentRechargeHttpCalls + export default { + post(options) { + calls.push(options) + return Promise.resolve({ code: 0, data: undefined }) + } + } + `, + 'utf8' + ) + + globalThis.__agentRechargeHttpCalls = calls + + try { + await esbuild.build({ + entryPoints: ['src/api/modules/agentRecharge.ts'], + bundle: true, + outfile: bundlePath, + format: 'esm', + platform: 'node', + absWorkingDir: process.cwd(), + plugins: [ + { + name: 'mock-http-boundary', + setup(build) { + build.onResolve({ filter: /^@\/utils\/http$/ }, () => ({ + path: httpMockPath + })) + } + } + ] + }) + + const { AgentRechargeService } = await import(pathToFileURL(bundlePath).href) + + await AgentRechargeService.rejectAgentRecharge(42, { + reject_reason: '支付凭证不符合要求' + }) + + assert.deepEqual(calls, [ + { + url: '/api/admin/agent-recharges/42/reject', + data: { + reject_reason: '支付凭证不符合要求' + } + } + ]) + } finally { + delete globalThis.__agentRechargeHttpCalls + await rm(tempDir, { recursive: true, force: true }) + } +}) diff --git a/src/api/modules/agentRecharge.ts b/src/api/modules/agentRecharge.ts index 91c86cf..84413ca 100644 --- a/src/api/modules/agentRecharge.ts +++ b/src/api/modules/agentRecharge.ts @@ -9,6 +9,7 @@ import type { AgentRechargeListResponse, CreateAgentRechargeRequest, ConfirmOfflinePaymentRequest, + RejectAgentRechargeRequest, BaseResponse } from '@/types/api' @@ -55,4 +56,16 @@ export class AgentRechargeService extends BaseService { data ) } + + /** + * 拒绝代理充值订单 + * @param id 充值订单ID + * @param data 拒绝请求参数 + */ + static rejectAgentRecharge( + id: number, + data: RejectAgentRechargeRequest + ): Promise> { + return this.post>(`/api/admin/agent-recharges/${id}/reject`, data) + } } diff --git a/src/types/api/agentRecharge.ts b/src/types/api/agentRecharge.ts index c2600cd..931cb73 100644 --- a/src/types/api/agentRecharge.ts +++ b/src/types/api/agentRecharge.ts @@ -72,3 +72,8 @@ export interface CreateAgentRechargeRequest { export interface ConfirmOfflinePaymentRequest { operation_password: string } + +// 拒绝代理充值订单请求 +export interface RejectAgentRechargeRequest { + reject_reason: string +} diff --git a/src/views/finance/agent-recharge/agentRechargeActions.ts b/src/views/finance/agent-recharge/agentRechargeActions.ts new file mode 100644 index 0000000..42c581f --- /dev/null +++ b/src/views/finance/agent-recharge/agentRechargeActions.ts @@ -0,0 +1,56 @@ +import { AgentRechargeStatus, type AgentRecharge } from '@/types/api' +import { hasVoucherKeys } from '@/utils/business' + +export interface AgentRechargeAction { + label: string + handler: () => void + type: 'primary' | 'danger' +} + +interface BuildAgentRechargeActionsOptions { + hasAuth: (permission: string) => boolean + onViewPaymentVoucher: (row: AgentRecharge) => void + onConfirmPayment: (row: AgentRecharge) => void + onReject: (row: AgentRecharge) => void +} + +export const buildAgentRechargeActions = ( + row: AgentRecharge, + options: BuildAgentRechargeActionsOptions +): AgentRechargeAction[] => { + const actions: AgentRechargeAction[] = [] + + if ( + row.payment_method === 'offline' && + hasVoucherKeys(row.payment_voucher_key) && + options.hasAuth('agent_recharge:view_payment_voucher') + ) { + actions.push({ + label: '查看支付凭证', + handler: () => options.onViewPaymentVoucher(row), + type: 'primary' + }) + } + + if ( + row.status === AgentRechargeStatus.PENDING && + row.payment_method === 'offline' && + options.hasAuth('agent_recharge:confirm_payment') + ) { + actions.push({ + label: '确认支付', + handler: () => options.onConfirmPayment(row), + type: 'primary' + }) + } + + if (row.status === AgentRechargeStatus.PENDING && options.hasAuth('agent_recharge:reject')) { + actions.push({ + label: '拒绝', + handler: () => options.onReject(row), + type: 'danger' + }) + } + + return actions +} diff --git a/src/views/finance/agent-recharge/agentRechargeDisplay.ts b/src/views/finance/agent-recharge/agentRechargeDisplay.ts new file mode 100644 index 0000000..85b921e --- /dev/null +++ b/src/views/finance/agent-recharge/agentRechargeDisplay.ts @@ -0,0 +1,3 @@ +export const formatRejectionReason = (reason?: string | null): string => { + return reason || '-' +} diff --git a/src/views/finance/agent-recharge/index.vue b/src/views/finance/agent-recharge/index.vue index 7597e35..c39ddee 100644 --- a/src/views/finance/agent-recharge/index.vue +++ b/src/views/finance/agent-recharge/index.vue @@ -167,6 +167,38 @@ + + + + + {{ currentRecharge?.recharge_no }} + + + + + + + + (null) const paymentVoucherFileKeys = ref([]) @@ -337,6 +373,7 @@ const createFormRef = ref() const confirmPayFormRef = ref() + const rejectFormRef = ref() const uploadRef = ref>() const MIN_RECHARGE_AMOUNT = 0.01 @@ -374,6 +411,10 @@ ] }) + const rejectRules = reactive({ + reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }] + }) + const createForm = reactive<{ amount: number payment_method: string @@ -392,6 +433,10 @@ operation_password: '' }) + const rejectForm = reactive({ + reject_reason: '' + }) + const rechargeList = ref([]) // 格式化货币 - 将分转换为元 @@ -735,6 +780,44 @@ }) } + // 显示拒绝对话框 + const handleShowReject = (row: AgentRecharge) => { + currentRecharge.value = row + rejectDialogVisible.value = true + } + + // 拒绝对话框关闭后的清理 + const handleRejectDialogClosed = () => { + rejectFormRef.value?.resetFields() + rejectForm.reject_reason = '' + currentRecharge.value = null + } + + // 拒绝代理充值订单 + const handleRejectRecharge = async () => { + const formRef = rejectFormRef.value + const recharge = currentRecharge.value + if (!formRef || !recharge) return + + await formRef.validate(async (valid) => { + if (valid) { + rejectLoading.value = true + try { + await AgentRechargeService.rejectAgentRecharge(recharge.id, { + reject_reason: rejectForm.reject_reason + }) + ElMessage.success('拒绝成功') + rejectDialogVisible.value = false + await getTableData() + } catch (error) { + console.error(error) + } finally { + rejectLoading.value = false + } + } + }) + } + // 处理名称点击 const handleNameClick = (row: AgentRecharge) => { if (hasAuth('agent_recharge:detail_page')) { @@ -754,35 +837,12 @@ // 获取操作按钮 const getActions = (row: AgentRecharge) => { - const actions: any[] = [] - - // 线下支付且有凭证可以查看 - if ( - row.payment_method === 'offline' && - hasVoucherKeys(row.payment_voucher_key) && - hasAuth('agent_recharge:view_payment_voucher') - ) { - actions.push({ - label: '查看支付凭证', - handler: () => handleViewPaymentVoucher(row), - type: 'primary' - }) - } - - // 待支付且线下转账的订单可以确认支付 - if ( - row.status === 1 && - row.payment_method === 'offline' && - hasAuth('agent_recharge:confirm_payment') - ) { - actions.push({ - label: '确认支付', - handler: () => handleShowConfirmPay(row), - type: 'primary' - }) - } - - return actions + return buildAgentRechargeActions(row, { + hasAuth, + onViewPaymentVoucher: handleViewPaymentVoucher, + onConfirmPayment: handleShowConfirmPay, + onReject: handleShowReject + }) } // 查看支付凭证