工单和发票

酒厂银行账户
This commit is contained in:
2026-07-23 14:51:20 +08:00
parent 75765bf9d4
commit 82c447b193
13 changed files with 575 additions and 21 deletions
@@ -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<string, string> = {
[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]: '门店对账单确认打款',
@@ -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 清理) */
@@ -170,6 +170,23 @@ export class CreateTicketDto {
extraJson?: Record<string, unknown>;
}
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()
@@ -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,
@@ -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));
@@ -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;
@@ -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()
@@ -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;