diff --git a/apps/admin-web/src/lib/hq-log.ts b/apps/admin-web/src/lib/hq-log.ts index 7c4cda7..d9e7bf7 100644 --- a/apps/admin-web/src/lib/hq-log.ts +++ b/apps/admin-web/src/lib/hq-log.ts @@ -38,6 +38,10 @@ export const HQ_OPERATION_ACTION_OPTIONS = [ { value: 'DELIVERY_UPDATE', label: '编辑配送单' }, { value: 'TICKET_APPROVE', label: '工单通过' }, { value: 'TICKET_REJECT', label: '工单驳回' }, + { value: 'TICKET_CREATE', label: '创建工单' }, + { value: 'INVOICE_CREATE', label: '创建发票申请' }, + { value: 'INVOICE_ISSUE', label: '开具发票' }, + { value: 'INVOICE_REJECT', label: '驳回发票' }, { value: 'STORE_PAYOUT_CONFIRM', label: '门店打款确认' }, { value: 'STORE_PAYOUT_BATCH_CONFIRM', label: '批量门店打款' }, { value: 'STORE_BILL_CONFIRM', label: '门店对账单确认打款' }, diff --git a/apps/admin-web/src/pages/InvoicesPage.tsx b/apps/admin-web/src/pages/InvoicesPage.tsx index 75fe0fe..5fd284d 100644 --- a/apps/admin-web/src/pages/InvoicesPage.tsx +++ b/apps/admin-web/src/pages/InvoicesPage.tsx @@ -5,7 +5,9 @@ import { Drawer, Form, Input, + Modal, Select, + Space, Table, Tag, Typography, @@ -47,6 +49,19 @@ type Row = { remark?: string; }; +type CreateFormValues = { + orderNo: string; + titleType: InvoiceTitleType; + invoiceKind: InvoiceKind; + titleName: string; + taxNo?: string; + addressPhone?: string; + bankAccount?: string; + email: string; + phone: string; + remark?: string; +}; + export default function InvoicesPage() { const [filters, setFilters] = useState>({}); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( @@ -61,6 +76,11 @@ export default function InvoicesPage() { const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [uploading, setUploading] = useState(false); + const [createOpen, setCreateOpen] = useState(false); + const [creating, setCreating] = useState(false); + const [createForm] = Form.useForm(); + const invoiceKind = Form.useWatch('invoiceKind', createForm); + const titleType = Form.useWatch('titleType', createForm); async function openDetail(id: string) { setDetail(await request(`/admin/invoices/${id}`)); @@ -98,6 +118,36 @@ export default function InvoicesPage() { setDrawerOpen(false); } + async function submitCreate() { + const values = await createForm.validateFields(); + setCreating(true); + try { + await request('/admin/invoices', { + method: 'POST', + body: JSON.stringify({ + orderNo: values.orderNo.trim(), + titleType: values.titleType, + invoiceKind: values.invoiceKind, + titleName: values.titleName.trim(), + taxNo: values.taxNo?.trim() || undefined, + addressPhone: values.addressPhone?.trim() || undefined, + bankAccount: values.bankAccount?.trim() || undefined, + email: values.email.trim(), + phone: values.phone.trim(), + remark: values.remark?.trim() || undefined, + }), + }); + message.success('发票申请已创建'); + setCreateOpen(false); + createForm.resetFields(); + reload(); + } catch (e) { + message.error(e instanceof Error ? e.message : '创建失败'); + } finally { + setCreating(false); + } + } + const columns: ColumnsType = [ { title: '申请单号', dataIndex: 'invoiceNo', width: 180 }, { title: '订单号', dataIndex: 'orderNo', width: 160 }, @@ -138,7 +188,30 @@ export default function InvoicesPage() { return (
- 发票管理 +
+ + 发票管理 + + +
setDrawerOpen(false)} extra={ detail?.status === 'PENDING' ? ( - <> + - - + ) : null } > @@ -242,6 +315,110 @@ export default function InvoicesPage() { )} + + setCreateOpen(false)} + onOk={() => void submitCreate()} + confirmLoading={creating} + destroyOnClose + okText="提交" + width={520} + > + + + + + + ({ + value: t, + label: INVOICE_TITLE_TYPE_LABELS[t], + disabled: invoiceKind === 'SPECIAL' && t === 'PERSONAL', + }))} + /> + + + + + {(titleType === 'ENTERPRISE' || invoiceKind === 'SPECIAL') && ( + + + + )} + {invoiceKind === 'SPECIAL' && ( + <> + + + + + + + + )} + + + + + + + + + + +
); } diff --git a/apps/admin-web/src/pages/TicketsPage.tsx b/apps/admin-web/src/pages/TicketsPage.tsx index e7ab642..ac2ee88 100644 --- a/apps/admin-web/src/pages/TicketsPage.tsx +++ b/apps/admin-web/src/pages/TicketsPage.tsx @@ -1,5 +1,18 @@ import { useState } from 'react'; -import { Button, Descriptions, Drawer, Form, Image, Input, Select, Table, Typography, message } from 'antd'; +import { + Button, + Descriptions, + Drawer, + Form, + Image, + Input, + Modal, + Select, + Space, + Table, + Typography, + message, +} from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { TICKET_TYPE_LABELS, type TicketTypeDto } from '@dukang/shared-types'; import { request } from '../lib/api'; @@ -32,6 +45,13 @@ export default function TicketsPage() { ); const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); + const [createOpen, setCreateOpen] = useState(false); + const [creating, setCreating] = useState(false); + const [createForm] = Form.useForm<{ + ticketType: TicketTypeDto; + orderNo: string; + remark?: string; + }>(); async function approve(id: string) { await request(`/admin/tickets/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }); @@ -50,6 +70,29 @@ export default function TicketsPage() { setDrawerOpen(false); } + async function submitCreate() { + const values = await createForm.validateFields(); + setCreating(true); + try { + await request('/admin/tickets', { + method: 'POST', + body: JSON.stringify({ + ticketType: values.ticketType, + orderNo: values.orderNo.trim(), + remark: values.remark?.trim() || undefined, + }), + }); + message.success('工单已创建'); + setCreateOpen(false); + createForm.resetFields(); + reload(); + } catch (e) { + message.error(e instanceof Error ? e.message : '创建失败'); + } finally { + setCreating(false); + } + } + const columns: ColumnsType = [ { title: '工单号', dataIndex: 'ticketNo', width: 180 }, { @@ -84,7 +127,21 @@ export default function TicketsPage() { return (
- 工单中心 +
+ + 工单中心 + + +
setDrawerOpen(false)} extra={ detail && (detail.status === 'PENDING' || detail.status === 'OPEN') ? ( - <> - - + ) : null } > @@ -173,6 +230,44 @@ export default function TicketsPage() { )} + + setCreateOpen(false)} + onOk={() => void submitCreate()} + confirmLoading={creating} + destroyOnClose + okText="提交" + > + + + + + + + + +
); } diff --git a/apps/admin-web/src/pages/WineryBillsPage.tsx b/apps/admin-web/src/pages/WineryBillsPage.tsx index a046c87..a28c4e4 100644 --- a/apps/admin-web/src/pages/WineryBillsPage.tsx +++ b/apps/admin-web/src/pages/WineryBillsPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Button, Card, @@ -6,6 +6,7 @@ import { Descriptions, Drawer, Form, + Input, Modal, Select, Space, @@ -17,10 +18,13 @@ import { } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { Dayjs } from 'dayjs'; -import { WINERY_SETTLEMENT_RATE } from '@dukang/shared-types'; +import { + WINERY_SETTLEMENT_RATE, + type SystemConfigFormResponse, +} from '@dukang/shared-types'; import { fmtTime } from '../lib/constants'; import { downloadExcelCsv } from '../lib/exportExcel'; -import { request } from '../lib/api'; +import { request, type HqProfile } from '../lib/api'; import { useAdminList } from '../lib/useAdminList'; type Row = { @@ -59,8 +63,16 @@ const DELIVERY_LABELS: Record = { CROSS_CITY: '跨城', }; +const WINERY_BANK_KEYS = [ + 'WINERY_BANK_ACCOUNT_NAME', + 'WINERY_BANK_NAME', + 'WINERY_BANK_BRANCH', + 'WINERY_BANK_ACCOUNT_NO', +] as const; + export default function WineryBillsPage() { const [form] = Form.useForm(); + const [bankForm] = Form.useForm>(); const [filters, setFilters] = useState>({}); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/winery-bills', @@ -79,6 +91,18 @@ export default function WineryBillsPage() { const [selectedKeys, setSelectedKeys] = useState([]); const [detail, setDetail] = useState<(Row & { items?: BillItem[] }) | null>(null); const [drawerOpen, setDrawerOpen] = useState(false); + const [profile, setProfile] = useState(null); + const [bankOpen, setBankOpen] = useState(false); + const [bankLoading, setBankLoading] = useState(false); + const [bankSaving, setBankSaving] = useState(false); + + useEffect(() => { + request('/admin/auth/me').then(setProfile).catch(() => {}); + }, []); + + const canEditWineryBank = + profile?.adminRole === 'SUPER_ADMIN' || + (profile?.permissionKeys ?? []).includes('system_settings_winery_bank'); function confirmPay(ids: string[], amountHint?: number) { Modal.confirm({ @@ -126,6 +150,41 @@ export default function WineryBillsPage() { } } + async function openBankModal() { + setBankOpen(true); + setBankLoading(true); + try { + const cfg = await request('/admin/system-config'); + const values: Record = {}; + for (const key of WINERY_BANK_KEYS) { + values[key] = cfg.values[key] ?? ''; + } + bankForm.setFieldsValue(values); + } catch (e) { + message.error(e instanceof Error ? e.message : '加载失败'); + setBankOpen(false); + } finally { + setBankLoading(false); + } + } + + async function saveBank() { + const values = await bankForm.validateFields(); + setBankSaving(true); + try { + await request('/admin/system-config', { + method: 'PUT', + body: JSON.stringify({ values }), + }); + message.success('酒厂银行账户已保存'); + setBankOpen(false); + } catch (e) { + message.error(e instanceof Error ? e.message : '保存失败'); + } finally { + setBankSaving(false); + } + } + const summary = data?.summary; const ratePct = Math.round(WINERY_SETTLEMENT_RATE * 100); const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id)); @@ -185,14 +244,29 @@ export default function WineryBillsPage() { return (
- - - 酒厂对账单 - - - 每日 8:00 汇总昨日已付订单(实付 × {ratePct}%);未打款红色、已打款绿色,可展开订单明细 - - +
+ + + 酒厂对账单 + + + 每日 8:00 汇总昨日已付订单(实付 × {ratePct}%);未打款红色、已打款绿色,可展开订单明细 + + + {canEditWineryBank ? ( + + ) : null} +
{summary && ( @@ -337,6 +411,43 @@ export default function WineryBillsPage() { )} + + setBankOpen(false)} + onOk={() => void saveBank()} + confirmLoading={bankSaving} + destroyOnClose + okText="保存" + > +
+ + + + + + + + + + + + +
+
); } diff --git a/packages/shared-types/src/hq-permissions.ts b/packages/shared-types/src/hq-permissions.ts index 60312f8..774cd6f 100644 --- a/packages/shared-types/src/hq-permissions.ts +++ b/packages/shared-types/src/hq-permissions.ts @@ -24,6 +24,7 @@ export const HQ_PERMISSION_CATALOG = [ { key: 'system_settings_oss', label: '对象存储 OSS', group: '系统设置' }, { key: 'system_settings_app', label: '应用链接', group: '系统设置' }, { key: 'system_settings_deploy', label: '发布部署', group: '系统设置' }, + { key: 'system_settings_winery_bank', label: '酒厂银行账户', group: '系统设置' }, ] as const; export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key']; @@ -37,6 +38,7 @@ export const SYSTEM_CONFIG_GROUP_PERMISSION: Record = { oss: 'system_settings_oss', app: 'system_settings_app', deploy: 'system_settings_deploy', + winery_bank: 'system_settings_winery_bank', }; export const SYSTEM_SETTINGS_PERMISSION_KEYS = Object.values( @@ -97,6 +99,7 @@ export const HQ_ROLE_DEFAULT_PERMISSIONS: Record = { 'benefit', 'invoices', 'logs', + 'system_settings_winery_bank', ], CUSTOMER_SERVICE: ['dashboard', 'users', 'orders', 'tickets', 'invoices', 'logs'], }; diff --git a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts index 1a8603e..225320b 100644 --- a/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts +++ b/server/dukang-api/src/common/hq-operation/hq-operation.constants.ts @@ -45,6 +45,10 @@ export const HqOperationAction = { DELIVERY_UPDATE: 'DELIVERY_UPDATE', TICKET_APPROVE: 'TICKET_APPROVE', TICKET_REJECT: 'TICKET_REJECT', + TICKET_CREATE: 'TICKET_CREATE', + INVOICE_CREATE: 'INVOICE_CREATE', + INVOICE_ISSUE: 'INVOICE_ISSUE', + INVOICE_REJECT: 'INVOICE_REJECT', STORE_PAYOUT_CONFIRM: 'STORE_PAYOUT_CONFIRM', STORE_PAYOUT_BATCH_CONFIRM: 'STORE_PAYOUT_BATCH_CONFIRM', STORE_BILL_CONFIRM: 'STORE_BILL_CONFIRM', @@ -119,6 +123,10 @@ export const HQ_OPERATION_ACTION_LABELS: Record = { [HqOperationAction.DELIVERY_UPDATE]: '编辑配送单', [HqOperationAction.TICKET_APPROVE]: '工单通过', [HqOperationAction.TICKET_REJECT]: '工单驳回', + [HqOperationAction.TICKET_CREATE]: '创建工单', + [HqOperationAction.INVOICE_CREATE]: '创建发票申请', + [HqOperationAction.INVOICE_ISSUE]: '开具发票', + [HqOperationAction.INVOICE_REJECT]: '驳回发票', [HqOperationAction.STORE_PAYOUT_CONFIRM]: '门店打款确认', [HqOperationAction.STORE_PAYOUT_BATCH_CONFIRM]: '批量门店打款', [HqOperationAction.STORE_BILL_CONFIRM]: '门店对账单确认打款', diff --git a/server/dukang-api/src/common/system-config/system-config.registry.ts b/server/dukang-api/src/common/system-config/system-config.registry.ts index 22c12b5..6338c37 100644 --- a/server/dukang-api/src/common/system-config/system-config.registry.ts +++ b/server/dukang-api/src/common/system-config/system-config.registry.ts @@ -9,6 +9,7 @@ export const SYSTEM_CONFIG_GROUPS: SystemConfigGroupMeta[] = [ { key: 'oss', label: '对象存储 OSS' }, { key: 'app', label: '应用链接' }, { key: 'deploy', label: '发布部署' }, + { key: 'winery_bank', label: '酒厂银行账户' }, ]; const G = { @@ -19,6 +20,7 @@ const G = { oss: 'oss', app: 'app', deploy: 'deploy', + winery_bank: 'winery_bank', } as const; /** HQ 可维护字段(不含 NODE_ENV / DATABASE_URL / JWT 等基础设施项) */ @@ -101,6 +103,37 @@ export const SYSTEM_CONFIG_FIELDS: SystemConfigFieldMeta[] = [ { key: 'DEPLOY_WEBHOOK_URL', label: '发布 Webhook URL', group: G.deploy, type: 'string', requiresRestart: false }, { key: 'DEPLOY_WEBHOOK_SECRET', label: '发布 Webhook Secret', group: G.deploy, type: 'password', secret: true, requiresRestart: false }, + + { + key: 'WINERY_BANK_ACCOUNT_NAME', + label: '户名', + group: G.winery_bank, + type: 'string', + requiresRestart: false, + description: '酒厂收款账户户名,打款时对照', + }, + { + key: 'WINERY_BANK_NAME', + label: '开户银行', + group: G.winery_bank, + type: 'string', + requiresRestart: false, + }, + { + key: 'WINERY_BANK_BRANCH', + label: '开户支行', + group: G.winery_bank, + type: 'string', + requiresRestart: false, + placeholder: '可选', + }, + { + key: 'WINERY_BANK_ACCOUNT_NO', + label: '银行账号', + group: G.winery_bank, + type: 'string', + requiresRestart: false, + }, ]; /** 已从 HQ 配置移除、仅保留在 .env 的键(启动时从 DB 清理) */ diff --git a/server/dukang-api/src/modules/common/dto/common-mutate.dto.ts b/server/dukang-api/src/modules/common/dto/common-mutate.dto.ts index 02793ae..4e0f1d4 100644 --- a/server/dukang-api/src/modules/common/dto/common-mutate.dto.ts +++ b/server/dukang-api/src/modules/common/dto/common-mutate.dto.ts @@ -170,6 +170,23 @@ export class CreateTicketDto { extraJson?: Record; } +export class AdminCreateTicketDto { + @IsString() + @IsIn(['REFUND', 'RESHIPMENT', 'ALERT', 'DAMAGE_RETURN', 'RETURN_REFUND']) + ticketType: string; + + @IsString() + @IsNotEmpty() + orderNo: string; + + @IsOptional() + @IsString() + remark?: string; + + @IsOptional() + evidenceUrls?: string[]; +} + export class UpdateTicketStatusDto { @IsString() @IsNotEmpty() diff --git a/server/dukang-api/src/modules/ops/admin-invoices.controller.ts b/server/dukang-api/src/modules/ops/admin-invoices.controller.ts index 83f4177..613d6ad 100644 --- a/server/dukang-api/src/modules/ops/admin-invoices.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-invoices.controller.ts @@ -2,8 +2,14 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/co import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; import { AuthUser } from '../../common/guards/jwt-auth.guard'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; +import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { TradeService } from '../trade/trade.service'; -import { IssueInvoiceDto, RejectInvoiceDto } from '../trade/dto/after-sale.dto'; +import { + AdminCreateInvoiceDto, + IssueInvoiceDto, + RejectInvoiceDto, +} from '../trade/dto/after-sale.dto'; @Controller('admin/invoices') @UseGuards(HqAuthGuard) @@ -23,12 +29,29 @@ export class AdminInvoicesController { }); } + @Post() + @HqOperation({ + action: HqOperationAction.INVOICE_CREATE, + refType: 'INVOICE', + batch: true, + includeBody: true, + }) + create(@Body() body: AdminCreateInvoiceDto) { + return this.tradeService.adminCreateInvoice(body); + } + @Get(':id') detail(@Param('id') id: string) { return this.tradeService.adminGetInvoice(BigInt(id)); } @Post(':id/issue') + @HqOperation({ + action: HqOperationAction.INVOICE_ISSUE, + refType: 'INVOICE', + refIdParam: 'id', + includeBody: true, + }) issue( @CurrentUser() user: AuthUser, @Param('id') id: string, @@ -38,6 +61,12 @@ export class AdminInvoicesController { } @Post(':id/reject') + @HqOperation({ + action: HqOperationAction.INVOICE_REJECT, + refType: 'INVOICE', + refIdParam: 'id', + includeBody: true, + }) reject( @CurrentUser() user: AuthUser, @Param('id') id: string, diff --git a/server/dukang-api/src/modules/ops/admin-tickets.controller.ts b/server/dukang-api/src/modules/ops/admin-tickets.controller.ts index 88b542a..99ced79 100644 --- a/server/dukang-api/src/modules/ops/admin-tickets.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-tickets.controller.ts @@ -8,6 +8,7 @@ import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { AdminTicketsService } from './admin-tickets.service'; import { TicketListQueryDto } from '../common/dto/common-query.dto'; +import { AdminCreateTicketDto } from '../common/dto/common-mutate.dto'; @Controller('admin/tickets') @UseGuards(HqAuthGuard) @@ -19,6 +20,17 @@ export class AdminTicketsController { return this.service.list(query); } + @Post() + @HqOperation({ + action: HqOperationAction.TICKET_CREATE, + refType: 'TICKET', + batch: true, + includeBody: true, + }) + create(@Body() body: AdminCreateTicketDto) { + return this.service.createByHq(body); + } + @Get(':id') detail(@Param('id') id: string) { return this.service.detail(BigInt(id)); diff --git a/server/dukang-api/src/modules/ops/admin-tickets.service.ts b/server/dukang-api/src/modules/ops/admin-tickets.service.ts index 5da3fde..c6da3cd 100644 --- a/server/dukang-api/src/modules/ops/admin-tickets.service.ts +++ b/server/dukang-api/src/modules/ops/admin-tickets.service.ts @@ -40,6 +40,43 @@ export class AdminTicketsService { return this.ticketService.detail(id); } + async createByHq(body: { + ticketType: string; + orderNo: string; + remark?: string; + evidenceUrls?: string[]; + }) { + const orderNo = body.orderNo?.trim(); + if (!orderNo) throw new BadRequestException('请填写订单号'); + const order = await this.prisma.order.findFirst({ where: { orderNo } }); + if (!order) throw new NotFoundException('订单不存在'); + if (order.status === 'PENDING_PAY' || order.status === 'CANCELLED') { + throw new BadRequestException('当前订单不可创建工单'); + } + if (['REFUNDING', 'REFUNDED'].includes(order.status) && body.ticketType !== 'ALERT') { + throw new BadRequestException('订单已在退款流程中'); + } + + const pending = await this.prisma.commonTicket.findFirst({ + where: { + ticketType: body.ticketType as never, + refType: 'ORDER', + refId: order.id, + status: { in: ['PENDING', 'OPEN'] }, + }, + }); + if (pending) throw new BadRequestException('该类型工单已在处理中'); + + const evidenceUrls = (body.evidenceUrls ?? []).filter((u) => typeof u === 'string' && u.trim()); + return this.ticketService.create({ + ticketType: body.ticketType, + refType: 'ORDER', + refId: order.id.toString(), + remark: body.remark ?? '', + extraJson: evidenceUrls.length ? { evidenceUrls } : undefined, + }); + } + private parseExtra(raw: unknown): TicketCollabExtra { if (!raw || typeof raw !== 'object') return {}; return raw as TicketCollabExtra; diff --git a/server/dukang-api/src/modules/trade/dto/after-sale.dto.ts b/server/dukang-api/src/modules/trade/dto/after-sale.dto.ts index 2aeea0f..f41b44b 100644 --- a/server/dukang-api/src/modules/trade/dto/after-sale.dto.ts +++ b/server/dukang-api/src/modules/trade/dto/after-sale.dto.ts @@ -64,6 +64,13 @@ export class CreateInvoiceDto { remark?: string; } +export class AdminCreateInvoiceDto extends CreateInvoiceDto { + @IsString() + @IsNotEmpty() + @MaxLength(32) + orderNo: string; +} + export class IssueInvoiceDto { @IsString() @IsNotEmpty() diff --git a/server/dukang-api/src/modules/trade/trade.service.ts b/server/dukang-api/src/modules/trade/trade.service.ts index addf0b7..182cc96 100644 --- a/server/dukang-api/src/modules/trade/trade.service.ts +++ b/server/dukang-api/src/modules/trade/trade.service.ts @@ -754,6 +754,27 @@ export class TradeService { return days; } + async adminCreateInvoice( + body: { + orderNo: string; + titleType: string; + invoiceKind: string; + titleName: string; + taxNo?: string; + addressPhone?: string; + bankAccount?: string; + email: string; + phone: string; + remark?: string; + }, + ) { + const orderNo = body.orderNo?.trim(); + if (!orderNo) throw new BadRequestException('请填写订单号'); + const order = await this.prisma.order.findFirst({ where: { orderNo } }); + if (!order) throw new NotFoundException('订单不存在'); + return this.createInvoice(order.userId, order.id, body); + } + async adminListInvoices(query: { status?: string; page?: number; pageSize?: number }) { const page = query.page ?? 1; const pageSize = query.pageSize ?? 20;