让凭证数组与套餐作废任务进入可联调状态
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s

Constraint: 后端订单套餐作废接口使用 /api/admin/order-package-invalidate-tasks,上传
  purpose 只能使用 attachment。
  Rejected: 继续提交逗号拼接凭证字符串 | 新提交路径必须使用 string[],历史字符串只做读取归一化。
  Confidence: high
  Scope-risk: broad
  Directive: 新增订单套餐作废权限时需同时配置菜单 URL 与
  order_package_invalidate_task:create/detail 按钮权限。
  Tested: bun run build;development 真实后端联调上传、创建、列表、详情通过;复机接口未触发。
  Not-tested: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览
This commit is contained in:
2026-06-23 09:50:12 +08:00
parent ebf9739f06
commit 0c3065f970
71 changed files with 4264 additions and 240 deletions

View File

@@ -22,7 +22,10 @@
</div>
</ElCard>
<PaymentVoucherDialog :file-key="paymentVoucherFileKey" @close="paymentVoucherFileKey = ''" />
<PaymentVoucherDialog
:file-keys="paymentVoucherFileKeys"
@close="paymentVoucherFileKeys = []"
/>
</div>
</template>
@@ -36,6 +39,7 @@
import { AgentRechargeService } from '@/api/modules'
import type { AgentRecharge, AgentRechargeStatus, AgentRechargePaymentMethod } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
defineOptions({ name: 'AgentRechargeDetail' })
@@ -45,7 +49,7 @@
const loading = ref(false)
const detailData = ref<AgentRecharge | null>(null)
const paymentVoucherFileKey = ref('')
const paymentVoucherFileKeys = ref<string[]>([])
const pageTitle = computed(() => `充值订单详情`)
@@ -132,19 +136,25 @@
label: '支付凭证',
fullWidth: true,
render: (data) =>
data.payment_voucher_key
hasVoucherKeys(data.payment_voucher_key)
? h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => {
paymentVoucherFileKey.value = data.payment_voucher_key
paymentVoucherFileKeys.value = toVoucherKeyList(data.payment_voucher_key)
}
},
() => '查看支付凭证'
)
: h('span', '-')
},
{
label: '运营备注',
prop: 'remark',
formatter: (value) => value || '-',
fullWidth: true
}
]
},

View File

@@ -103,6 +103,16 @@
@change="createFormRef?.validateField('payment_voucher_key')"
/>
</ElFormItem>
<ElFormItem label="运营备注" prop="remark">
<ElInput
v-model="createForm.remark"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="请输入运营备注(可选)"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
@@ -159,8 +169,8 @@
<!-- 支付凭证预览 -->
<PaymentVoucherDialog
:file-key="paymentVoucherFileKey"
@close="paymentVoucherFileKey = ''"
:file-keys="paymentVoucherFileKeys"
@close="paymentVoucherFileKeys = []"
/>
</ElCard>
</div>
@@ -176,6 +186,7 @@
ElTag,
ElButton,
ElCascader,
ElInput,
ElInputNumber,
ElSelect,
ElOption
@@ -192,6 +203,7 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { formatDateTime } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import { useAuth } from '@/composables/useAuth'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
@@ -209,7 +221,7 @@
const createDialogVisible = ref(false)
const confirmPayDialogVisible = ref(false)
const currentRecharge = ref<AgentRecharge | null>(null)
const paymentVoucherFileKey = ref('')
const paymentVoucherFileKeys = ref<string[]>([])
// 搜索表单初始值
const initialSearchState: AgentRechargeQueryParams = {
@@ -366,12 +378,14 @@
amount: number
payment_method: string
shop_id: number | null
payment_voucher_key?: string
payment_voucher_key: string[]
remark: string
}>({
amount: MIN_RECHARGE_AMOUNT,
payment_method: '',
shop_id: null,
payment_voucher_key: undefined
payment_voucher_key: [],
remark: ''
})
const confirmPayForm = reactive<ConfirmOfflinePaymentRequest>({
@@ -475,6 +489,13 @@
width: 120,
formatter: (row: AgentRecharge) => row.payment_channel || '-'
},
{
prop: 'remark',
label: '运营备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: (row: AgentRecharge) => row.remark || '-'
},
{
prop: 'created_at',
label: '创建时间',
@@ -628,7 +649,8 @@
createForm.amount = MIN_RECHARGE_AMOUNT
createForm.payment_method = ''
createForm.shop_id = null
createForm.payment_voucher_key = undefined
createForm.payment_voucher_key = []
createForm.remark = ''
voucherUploading.value = false
uploadRef.value?.clearFiles(false)
}
@@ -649,11 +671,15 @@
const data: CreateAgentRechargeRequest = {
amount: Math.round(createForm.amount * 100), // 元转分
payment_method: createForm.payment_method as AgentRechargePaymentMethod,
shop_id: createForm.shop_id!
shop_id: createForm.shop_id!,
remark: createForm.remark || undefined
}
if (createForm.payment_method === 'offline' && createForm.payment_voucher_key) {
data.payment_voucher_key = createForm.payment_voucher_key
if (
createForm.payment_method === 'offline' &&
hasVoucherKeys(createForm.payment_voucher_key)
) {
data.payment_voucher_key = toVoucherKeyList(createForm.payment_voucher_key)
}
await AgentRechargeService.createAgentRecharge(data)
@@ -733,7 +759,7 @@
// 线下支付且有凭证可以查看
if (
row.payment_method === 'offline' &&
row.payment_voucher_key &&
hasVoucherKeys(row.payment_voucher_key) &&
hasAuth('agent_recharge:view_payment_voucher')
) {
actions.push({
@@ -761,8 +787,8 @@
// 查看支付凭证
const handleViewPaymentVoucher = (row: AgentRecharge) => {
if (!row.payment_voucher_key) return
paymentVoucherFileKey.value = row.payment_voucher_key
if (!hasVoucherKeys(row.payment_voucher_key)) return
paymentVoucherFileKeys.value = toVoucherKeyList(row.payment_voucher_key)
}
</script>

View File

@@ -22,7 +22,7 @@
</div>
</ElCard>
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
<PaymentVoucherDialog :file-keys="refundVoucherFileKeys" @close="refundVoucherFileKeys = []" />
<!-- 审批通过对话框 -->
<ElDialog
@@ -150,12 +150,26 @@
placeholder="请输入退款原因"
/>
</ElFormItem>
<ElFormItem label="退款凭证" prop="refund_voucher_key">
<VoucherUpload
ref="resubmitUploadRef"
v-model="resubmitForm.refund_voucher_key"
voucher-name="退款凭证"
@uploading-change="resubmitVoucherUploading = $event"
@change="resubmitFormRef?.validateField('refund_voucher_key')"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
确认提交
<ElButton
type="primary"
@click="handleResubmitRefund"
:loading="resubmitLoading || resubmitVoucherUploading"
:disabled="resubmitVoucherUploading"
>
{{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }}
</ElButton>
</div>
</template>
@@ -179,7 +193,9 @@
ResubmitRefundRequest
} from '@/types/api'
import { formatDateTime, yuanToFen } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
defineOptions({ name: 'RefundDetail' })
@@ -188,7 +204,7 @@
const loading = ref(false)
const refund = ref<Refund | null>(null)
const refundVoucherFileKey = ref('')
const refundVoucherFileKeys = ref<string[]>([])
const pageTitle = computed(() => `退款详情`)
@@ -199,10 +215,12 @@
const approveLoading = ref(false)
const rejectLoading = ref(false)
const resubmitLoading = ref(false)
const resubmitVoucherUploading = ref(false)
const approveFormRef = ref()
const rejectFormRef = ref()
const resubmitFormRef = ref()
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const approveRules = ref()
const rejectRules = ref({
@@ -222,10 +240,12 @@
const resubmitForm = reactive<{
requested_refund_amount?: number
actual_received_amount?: number
refund_voucher_key: string[]
refund_reason?: string
}>({
requested_refund_amount: undefined,
actual_received_amount: undefined,
refund_voucher_key: [],
refund_reason: ''
})
@@ -295,14 +315,14 @@
label: '退款凭证',
fullWidth: true,
render: (data) =>
data.refund_voucher_key
hasVoucherKeys(data.refund_voucher_key)
? h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => {
refundVoucherFileKey.value = data.refund_voucher_key
refundVoucherFileKeys.value = toVoucherKeyList(data.refund_voucher_key)
}
},
() => '查看退款凭证'
@@ -411,7 +431,10 @@
resubmitFormRef.value?.resetFields()
resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined
resubmitForm.refund_voucher_key = []
resubmitForm.refund_reason = ''
resubmitVoucherUploading.value = false
resubmitUploadRef.value?.clearFiles(false)
}
const handleApproveRefund = async () => {
@@ -463,6 +486,10 @@
const handleResubmitRefund = async () => {
if (!resubmitFormRef.value || !refund.value) return
if (resubmitVoucherUploading.value) {
ElMessage.warning('退款凭证上传中,请稍候')
return
}
const refundId = refund.value.id
await resubmitFormRef.value.validate(async (valid: boolean) => {
@@ -474,6 +501,9 @@
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
refund_reason: resubmitForm.refund_reason || undefined
}
if (hasVoucherKeys(resubmitForm.refund_voucher_key)) {
data.refund_voucher_key = toVoucherKeyList(resubmitForm.refund_voucher_key)
}
await RefundService.resubmitRefund(refundId, data)
ElMessage.success('重新提交成功')

View File

@@ -49,7 +49,10 @@
<CreateRefundDialog v-model="createDialogVisible" @success="handleCreateSuccess" />
<!-- 退款凭证预览 -->
<PaymentVoucherDialog :file-key="refundVoucherFileKey" @close="refundVoucherFileKey = ''" />
<PaymentVoucherDialog
:file-keys="refundVoucherFileKeys"
@close="refundVoucherFileKeys = []"
/>
<!-- 审批通过对话框 -->
<ElDialog
@@ -217,12 +220,26 @@
placeholder="请输入退款原因"
/>
</ElFormItem>
<ElFormItem label="退款凭证" prop="refund_voucher_key">
<VoucherUpload
ref="resubmitUploadRef"
v-model="resubmitForm.refund_voucher_key"
voucher-name="退款凭证"
@uploading-change="resubmitVoucherUploading = $event"
@change="resubmitFormRef?.validateField('refund_voucher_key')"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
确认提交
<ElButton
type="primary"
@click="handleResubmitRefund"
:loading="resubmitLoading || resubmitVoucherUploading"
:disabled="resubmitVoucherUploading"
>
{{ resubmitVoucherUploading ? '凭证上传中...' : '确认提交' }}
</ElButton>
</div>
</template>
@@ -250,8 +267,10 @@
import type { SearchFormItem } from '@/types'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { fenToYuan, formatDateTime, yuanToFen } from '@/utils/business/format'
import { hasVoucherKeys, toVoucherKeyList } from '@/utils/business'
import { RoutesAlias } from '@/router/routesAlias'
import PaymentVoucherDialog from '@/components/business/PaymentVoucherDialog.vue'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
import { useAuth } from '@/composables/useAuth'
@@ -266,14 +285,16 @@
const rejectLoading = ref(false)
const returnLoading = ref(false)
const resubmitLoading = ref(false)
const resubmitVoucherUploading = ref(false)
const tableRef = ref()
const resubmitUploadRef = ref<InstanceType<typeof VoucherUpload>>()
const createDialogVisible = ref(false)
const approveDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const returnDialogVisible = ref(false)
const resubmitDialogVisible = ref(false)
const currentRefund = ref<Refund | null>(null)
const refundVoucherFileKey = ref('')
const refundVoucherFileKeys = ref<string[]>([])
// 搜索表单初始值
const initialSearchState: RefundQueryParams = {
@@ -410,10 +431,12 @@
const resubmitForm = reactive<{
requested_refund_amount?: number
actual_received_amount?: number
refund_voucher_key: string[]
refund_reason?: string
}>({
requested_refund_amount: undefined,
actual_received_amount: undefined,
refund_voucher_key: [],
refund_reason: ''
})
@@ -812,6 +835,7 @@
// 预填充原有数据
resubmitForm.requested_refund_amount = fenToYuan(row.requested_refund_amount) || undefined
resubmitForm.actual_received_amount = fenToYuan(row.actual_received_amount) || undefined
resubmitForm.refund_voucher_key = []
resubmitForm.refund_reason = row.refund_reason
resubmitDialogVisible.value = true
}
@@ -821,13 +845,20 @@
resubmitFormRef.value?.resetFields()
resubmitForm.requested_refund_amount = undefined
resubmitForm.actual_received_amount = undefined
resubmitForm.refund_voucher_key = []
resubmitForm.refund_reason = ''
resubmitVoucherUploading.value = false
resubmitUploadRef.value?.clearFiles(false)
currentRefund.value = null
}
// 重新提交
const handleResubmitRefund = async () => {
if (!resubmitFormRef.value || !currentRefund.value) return
if (resubmitVoucherUploading.value) {
ElMessage.warning('退款凭证上传中,请稍候')
return
}
await resubmitFormRef.value.validate(async (valid) => {
if (valid) {
@@ -840,6 +871,9 @@
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
refund_reason: resubmitForm.refund_reason || undefined
}
if (hasVoucherKeys(resubmitForm.refund_voucher_key)) {
data.refund_voucher_key = toVoucherKeyList(resubmitForm.refund_voucher_key)
}
await RefundService.resubmitRefund(refundId, data)
ElMessage.success('重新提交成功')
@@ -873,7 +907,7 @@
// 获取操作按钮
const getActions = (row: Refund) => {
const actions: any[] = []
const voucherKey = row.refund_voucher_key
const voucherKeys = toVoucherKeyList(row.refund_voucher_key)
// 待审批状态可以打回重填、审批通过或审批拒绝
if (row.status === 1) {
@@ -912,7 +946,7 @@
})
}
if (voucherKey && hasAuth('refund:view_voucher')) {
if (voucherKeys.length && hasAuth('refund:view_voucher')) {
actions.push({
label: '查看退款凭证',
handler: () => handleViewRefundVoucher(row),
@@ -925,8 +959,8 @@
const handleViewRefundVoucher = (row: Refund) => {
const voucherKey = row.refund_voucher_key
if (!voucherKey) return
refundVoucherFileKey.value = voucherKey
if (!hasVoucherKeys(voucherKey)) return
refundVoucherFileKeys.value = toVoucherKeyList(voucherKey)
}
</script>