feat: v3.4.12 ticket iteration
Refund rollback, winery T+3, multi withdraw, mini-user store detail, dev plan batch edit and WeCom dispatch, support ticket edit/attachments/batch status, package imageUrl. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -655,6 +655,7 @@ model CommonSupportTicket {
|
||||
reviewerName String? @map("reviewer_name") @db.VarChar(64)
|
||||
reviewedAt DateTime? @map("reviewed_at") @db.DateTime(3)
|
||||
remark String? @db.VarChar(512)
|
||||
attachmentUrls Json? @map("attachment_urls")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
completedAt DateTime? @map("completed_at") @db.DateTime(3)
|
||||
@@ -1278,6 +1279,7 @@ model StorePackage {
|
||||
dishes String @db.Text
|
||||
usableTime String? @map("usable_time") @db.VarChar(256)
|
||||
otherNotes String? @map("other_notes") @db.VarChar(512)
|
||||
imageUrl String? @map("image_url") @db.VarChar(512)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3)
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.DateTime(3)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsIn, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { ArrayMinSize, IsArray, IsBoolean, IsIn, IsNotEmpty, IsOptional, IsString, MaxLength, ValidateIf, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsInt, Min } from 'class-validator';
|
||||
|
||||
@@ -41,6 +41,58 @@ export class CreateSupportTicketDto {
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
remark?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
attachmentUrls?: string[];
|
||||
}
|
||||
|
||||
export class UpdateSupportTicketDto {
|
||||
@IsOptional()
|
||||
@IsIn(['BUG', 'SUGGESTION', 'OTHER'])
|
||||
ticketType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(128)
|
||||
title?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
content?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
remark?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
attachmentUrls?: string[];
|
||||
}
|
||||
|
||||
export class BatchUpdateSupportTicketStatusDto {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsString({ each: true })
|
||||
ticketIds!: string[];
|
||||
|
||||
@IsIn(['PENDING_REVIEW', 'REJECTED', 'DEVELOPING', 'TESTING', 'PASSED'])
|
||||
status!: string;
|
||||
|
||||
@ValidateIf((o: BatchUpdateSupportTicketStatusDto) => o.status === 'REJECTED')
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(512)
|
||||
rejectReason?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(512)
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export class RejectSupportTicketDto {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { SupportTicketStatus, SupportTicketType } from '@prisma/client';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { CreateDevPlanTaskFromTicketInput } from '@dukang/shared-types';
|
||||
import { validateSupportTicketStatusTransition } from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { AlertService } from '../../common/alert/alert.service';
|
||||
@@ -16,8 +17,22 @@ import type {
|
||||
RejectSupportTicketDto,
|
||||
SupportTicketListQueryDto,
|
||||
SupportTicketRemarkDto,
|
||||
UpdateSupportTicketDto,
|
||||
} from './dto/support-ticket.dto';
|
||||
|
||||
function normalizeAttachmentUrls(raw: unknown): string[] | null {
|
||||
if (!Array.isArray(raw)) return null;
|
||||
const urls = raw.map((u) => String(u || '').trim()).filter(Boolean);
|
||||
return urls.length ? urls : null;
|
||||
}
|
||||
|
||||
function mapSupportTicketRow<T extends { attachmentUrls?: unknown }>(ticket: T) {
|
||||
return {
|
||||
...ticket,
|
||||
attachmentUrls: normalizeAttachmentUrls(ticket.attachmentUrls),
|
||||
};
|
||||
}
|
||||
|
||||
function generateSupportTicketNo() {
|
||||
return `ST${Date.now()}${Math.floor(Math.random() * 900 + 100)}`;
|
||||
}
|
||||
@@ -43,6 +58,9 @@ export class SupportTicketService {
|
||||
title: dto.title.trim(),
|
||||
content: dto.content?.trim() || null,
|
||||
remark: dto.remark?.trim() || null,
|
||||
attachmentUrls: dto.attachmentUrls?.length
|
||||
? (dto.attachmentUrls.map((u) => u.trim()).filter(Boolean) as unknown as Prisma.InputJsonValue)
|
||||
: undefined,
|
||||
creatorId: creator.id,
|
||||
creatorName: creator.name,
|
||||
},
|
||||
@@ -73,7 +91,80 @@ export class SupportTicketService {
|
||||
].join('\n'),
|
||||
)
|
||||
.catch(() => {});
|
||||
return serializeBigInt(ticket);
|
||||
return serializeBigInt(mapSupportTicketRow(ticket));
|
||||
}
|
||||
|
||||
async update(id: bigint, dto: UpdateSupportTicketDto) {
|
||||
const ticket = await this.getOrThrow(id);
|
||||
if (ticket.status !== 'PENDING_REVIEW') {
|
||||
throw new BadRequestException('仅待评审工单可编辑');
|
||||
}
|
||||
const data: Prisma.CommonSupportTicketUpdateInput = {};
|
||||
if (dto.ticketType != null) data.ticketType = dto.ticketType as SupportTicketType;
|
||||
if (dto.title != null) data.title = dto.title.trim();
|
||||
if (dto.content !== undefined) data.content = dto.content?.trim() || null;
|
||||
if (dto.remark !== undefined) data.remark = dto.remark?.trim() || null;
|
||||
if (dto.attachmentUrls !== undefined) {
|
||||
const urls = dto.attachmentUrls.map((u) => u.trim()).filter(Boolean);
|
||||
data.attachmentUrls = urls.length ? (urls as unknown as Prisma.InputJsonValue) : Prisma.JsonNull;
|
||||
}
|
||||
const updated = await this.prisma.commonSupportTicket.update({ where: { id }, data });
|
||||
return serializeBigInt(mapSupportTicketRow(updated));
|
||||
}
|
||||
|
||||
async batchUpdateStatus(
|
||||
ticketIds: bigint[],
|
||||
status: SupportTicketStatus,
|
||||
ctx: {
|
||||
isSuperAdmin: boolean;
|
||||
rejectReason?: string;
|
||||
note?: string;
|
||||
reviewer?: { id: bigint; name: string };
|
||||
},
|
||||
) {
|
||||
const results: Array<{ ticketId: string; ok: boolean; message?: string }> = [];
|
||||
for (const id of ticketIds) {
|
||||
try {
|
||||
const ticket = await this.getOrThrow(id);
|
||||
const linkedMap = await this.devPlan.listLinkedTasksByTicketIds([id]);
|
||||
const linkedTaskCount = linkedMap.get(String(id))?.length ?? 0;
|
||||
const guard = validateSupportTicketStatusTransition(
|
||||
ticket.status as 'PENDING_REVIEW' | 'REJECTED' | 'DEVELOPING' | 'TESTING' | 'PASSED',
|
||||
status as 'PENDING_REVIEW' | 'REJECTED' | 'DEVELOPING' | 'TESTING' | 'PASSED',
|
||||
{
|
||||
linkedTaskCount,
|
||||
isSuperAdmin: ctx.isSuperAdmin,
|
||||
rejectReason: ctx.rejectReason,
|
||||
},
|
||||
);
|
||||
if (!guard.ok) {
|
||||
results.push({ ticketId: String(id), ok: false, message: guard.message });
|
||||
continue;
|
||||
}
|
||||
const data: Prisma.CommonSupportTicketUpdateInput = { status };
|
||||
if (ctx.note?.trim()) data.remark = ctx.note.trim();
|
||||
if (status === 'REJECTED') {
|
||||
data.rejectReason = ctx.rejectReason!.trim();
|
||||
data.completedAt = new Date();
|
||||
if (ctx.reviewer) {
|
||||
data.reviewerId = ctx.reviewer.id;
|
||||
data.reviewerName = ctx.reviewer.name;
|
||||
data.reviewedAt = new Date();
|
||||
}
|
||||
}
|
||||
if (status === 'PASSED') data.completedAt = new Date();
|
||||
await this.prisma.commonSupportTicket.update({ where: { id }, data });
|
||||
results.push({ ticketId: String(id), ok: true });
|
||||
} catch (err) {
|
||||
results.push({
|
||||
ticketId: String(id),
|
||||
ok: false,
|
||||
message: err instanceof Error ? err.message : '更新失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
const successCount = results.filter((r) => r.ok).length;
|
||||
return { results, successCount, failCount: results.length - successCount };
|
||||
}
|
||||
|
||||
async list(query: SupportTicketListQueryDto) {
|
||||
@@ -98,7 +189,7 @@ export class SupportTicketService {
|
||||
]);
|
||||
const linkedMap = await this.devPlan.listLinkedTasksByTicketIds(items.map((i) => i.id));
|
||||
const enriched = items.map((ticket) => ({
|
||||
...ticket,
|
||||
...mapSupportTicketRow(ticket),
|
||||
linkedTasks: (linkedMap.get(String(ticket.id)) ?? []).map((t) => ({
|
||||
id: t.id,
|
||||
taskNo: t.taskNo,
|
||||
@@ -114,7 +205,7 @@ export class SupportTicketService {
|
||||
if (!ticket) throw new NotFoundException('技术支持工单不存在');
|
||||
const linkedMap = await this.devPlan.listLinkedTasksByTicketIds([id]);
|
||||
return serializeBigInt({
|
||||
...ticket,
|
||||
...mapSupportTicketRow(ticket),
|
||||
linkedTasks: (linkedMap.get(String(id)) ?? []).map((t) => ({
|
||||
id: t.id,
|
||||
taskNo: t.taskNo,
|
||||
@@ -189,6 +280,8 @@ export class SupportTicketService {
|
||||
rejectReason?: string;
|
||||
note?: string;
|
||||
tasks?: CreateDevPlanTaskFromTicketInput[];
|
||||
dispatchToWecom?: boolean;
|
||||
dispatchSupplement?: string;
|
||||
},
|
||||
) {
|
||||
if (input.decision === 'REJECT') {
|
||||
@@ -202,7 +295,7 @@ export class SupportTicketService {
|
||||
throw new BadRequestException('仅待评审工单可审批');
|
||||
}
|
||||
|
||||
await this.devPlan.createTasksFromTicket(id, input.tasks, reviewer.id);
|
||||
const createdTasks = await this.devPlan.createTasksFromTicket(id, input.tasks, reviewer.id);
|
||||
|
||||
const updated = await this.prisma.commonSupportTicket.update({
|
||||
where: { id },
|
||||
@@ -214,9 +307,31 @@ export class SupportTicketService {
|
||||
remark: input.note?.trim() || ticket.remark,
|
||||
},
|
||||
});
|
||||
|
||||
if (input.dispatchToWecom) {
|
||||
try {
|
||||
await this.devPlan.dispatchTasks(
|
||||
{
|
||||
taskIds: createdTasks.map((t) => t.id),
|
||||
supplement: input.dispatchSupplement,
|
||||
},
|
||||
reviewer.id,
|
||||
);
|
||||
} catch (err) {
|
||||
this.alert.notify({
|
||||
level: 'P2',
|
||||
category: 'ops',
|
||||
title: '审批后企微派发失败',
|
||||
detail: `工单 ${ticket.ticketNo}\n${err instanceof Error ? err.message : String(err)}`,
|
||||
dedupeKey: `support_review_dispatch_fail|${ticket.ticketNo}`,
|
||||
dedupeTtlSec: 120,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const linkedMap = await this.devPlan.listLinkedTasksByTicketIds([id]);
|
||||
return serializeBigInt({
|
||||
...updated,
|
||||
...mapSupportTicketRow(updated),
|
||||
linkedTasks: (linkedMap.get(String(id)) ?? []).map((t) => ({
|
||||
id: t.id,
|
||||
taskNo: t.taskNo,
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
UpdateDevPlanSettingsDto,
|
||||
UpdateDevPlanTaskDto,
|
||||
UpdateDevPlanVersionDto,
|
||||
DevPlanTaskBatchUpdateDto,
|
||||
} from './dto/dev-plan.dto';
|
||||
|
||||
@Controller('admin/dev-plan')
|
||||
@@ -91,6 +92,12 @@ export class AdminDevPlanController {
|
||||
return this.service.dispatchTasks(body, account.id);
|
||||
}
|
||||
|
||||
@Post('tasks/batch-update')
|
||||
@HqOperation({ action: HqOperationAction.DEV_PLAN_TASK_UPDATE, refType: 'DEV_PLAN_TASK', includeBody: true, batch: true })
|
||||
batchUpdateTasks(@Body() body: DevPlanTaskBatchUpdateDto) {
|
||||
return this.service.batchUpdateTasks(body);
|
||||
}
|
||||
|
||||
@Get('versions')
|
||||
listVersions(
|
||||
@Query('status') status?: string,
|
||||
|
||||
@@ -48,6 +48,8 @@ import type {
|
||||
|
||||
DevPlanTaskDispatchDto,
|
||||
|
||||
DevPlanTaskBatchUpdateDto,
|
||||
|
||||
DevPlanTaskListQueryDto,
|
||||
|
||||
UpdateDevPlanSettingsDto,
|
||||
@@ -1009,6 +1011,100 @@ export class DevPlanService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async appendVersionTasks(versionId: bigint, taskIds: string[]) {
|
||||
|
||||
const version = await this.prisma.devPlanVersion.findUnique({ where: { id: versionId } });
|
||||
|
||||
if (!version) throw new NotFoundException('版本不存在');
|
||||
|
||||
|
||||
|
||||
const ids = taskIds.map(BigInt);
|
||||
|
||||
const existing = await this.prisma.devPlanVersionTask.findMany({
|
||||
|
||||
where: { versionId, taskId: { in: ids } },
|
||||
|
||||
select: { taskId: true },
|
||||
|
||||
});
|
||||
|
||||
const linked = new Set(existing.map((row) => row.taskId.toString()));
|
||||
|
||||
const toAdd = ids.filter((id) => !linked.has(id.toString()));
|
||||
|
||||
if (!toAdd.length) return;
|
||||
|
||||
|
||||
|
||||
const maxSort = await this.prisma.devPlanVersionTask.aggregate({
|
||||
|
||||
where: { versionId },
|
||||
|
||||
_max: { sortOrder: true },
|
||||
|
||||
});
|
||||
|
||||
let sort = (maxSort._max.sortOrder ?? -1) + 1;
|
||||
|
||||
|
||||
|
||||
await this.prisma.devPlanVersionTask.createMany({
|
||||
|
||||
data: toAdd.map((taskId, index) => ({ versionId, taskId, sortOrder: sort + index })),
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async batchUpdateTasks(dto: DevPlanTaskBatchUpdateDto) {
|
||||
|
||||
if (!dto.taskIds.length) throw new BadRequestException('请至少选择 1 条任务');
|
||||
|
||||
if (!dto.status && !dto.versionId) {
|
||||
|
||||
throw new BadRequestException('请至少指定状态或关联版本');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const taskIds = dto.taskIds.map(BigInt);
|
||||
|
||||
const tasks = await this.prisma.devPlanTask.findMany({ where: { id: { in: taskIds } } });
|
||||
|
||||
if (tasks.length !== taskIds.length) throw new BadRequestException('部分任务不存在');
|
||||
|
||||
|
||||
|
||||
if (dto.status) {
|
||||
|
||||
for (const task of tasks) {
|
||||
|
||||
await this.updateTask(task.id, { status: dto.status });
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (dto.versionId) {
|
||||
|
||||
await this.appendVersionTasks(BigInt(dto.versionId), dto.taskIds);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return { updated: taskIds.length };
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ArrayMinSize, IsArray, IsBoolean, IsIn, IsNotEmpty, IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
DEV_PLAN_TASK_STATUSES,
|
||||
@@ -161,6 +152,29 @@ export class ReviewSupportTicketDto {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DevPlanTaskFromTicketDto)
|
||||
tasks?: DevPlanTaskFromTicketDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
dispatchToWecom?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dispatchSupplement?: string;
|
||||
}
|
||||
|
||||
export class DevPlanTaskBatchUpdateDto {
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsString({ each: true })
|
||||
taskIds!: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(DEV_PLAN_TASK_STATUSES)
|
||||
status?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
versionId?: string | null;
|
||||
}
|
||||
|
||||
export class BatchReviewPreviewDto {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
@@ -22,6 +23,8 @@ import {
|
||||
RejectSupportTicketDto,
|
||||
SupportTicketListQueryDto,
|
||||
SupportTicketRemarkDto,
|
||||
UpdateSupportTicketDto,
|
||||
BatchUpdateSupportTicketStatusDto,
|
||||
} from '../common/dto/support-ticket.dto';
|
||||
import {
|
||||
BatchReviewConfirmDto,
|
||||
@@ -83,11 +86,39 @@ export class AdminSupportTicketsController {
|
||||
return this.service.batchReviewConfirm(account, body.items);
|
||||
}
|
||||
|
||||
@Post('batch-update-status')
|
||||
@UseGuards(SuperAdminGuard)
|
||||
async batchUpdateStatus(
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Body() body: BatchUpdateSupportTicketStatusDto,
|
||||
) {
|
||||
const account = await this.resolveHqAccount(user);
|
||||
const profile = await this.prisma.hqAccount.findUnique({
|
||||
where: { id: user.actorId },
|
||||
select: { adminRole: true },
|
||||
});
|
||||
return this.service.batchUpdateStatus(
|
||||
body.ticketIds.map(BigInt),
|
||||
body.status as never,
|
||||
{
|
||||
isSuperAdmin: profile?.adminRole === 'SUPER_ADMIN',
|
||||
rejectReason: body.rejectReason,
|
||||
note: body.note,
|
||||
reviewer: account,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(BigInt(id));
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(@Param('id') id: string, @Body() body: UpdateSupportTicketDto) {
|
||||
return this.service.update(BigInt(id), body);
|
||||
}
|
||||
|
||||
@Post(':id/review')
|
||||
@UseGuards(SuperAdminGuard)
|
||||
@HqOperation({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Prisma } from '@prisma/client';
|
||||
import {
|
||||
DEFAULT_STORE_WITHDRAW_DAILY_LIMIT,
|
||||
DEFAULT_XFX_LOGISTICS_PRICING,
|
||||
WINERY_SETTLEMENT_LAG_DAYS,
|
||||
WINERY_SETTLEMENT_RATE,
|
||||
} from '@dukang/shared-types';
|
||||
import {
|
||||
@@ -40,6 +41,15 @@ function dayWindow(anchor = new Date()) {
|
||||
return { start, end, billDate: start };
|
||||
}
|
||||
|
||||
function wineryDayWindow(anchor = new Date(), lagDays = WINERY_SETTLEMENT_LAG_DAYS) {
|
||||
const billDate = startOfDay(anchor);
|
||||
billDate.setDate(billDate.getDate() - lagDays);
|
||||
const start = billDate;
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 1);
|
||||
return { start, end, billDate: start };
|
||||
}
|
||||
|
||||
function round2(n: number) {
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
@@ -205,7 +215,6 @@ export class SettlementService {
|
||||
remainingDailyLimit: Math.max(0, round2(dailyLimit - todayApplied)),
|
||||
isPrimary: account.isPrimary === 1,
|
||||
hasBankAccount,
|
||||
hasPendingRequest: !!pending,
|
||||
bankAccount: {
|
||||
bankAccountName: account.bankAccountName,
|
||||
bankAccountNo: account.bankAccountNo,
|
||||
@@ -264,7 +273,6 @@ export class SettlementService {
|
||||
requestAmount,
|
||||
todayApplied,
|
||||
dailyLimit,
|
||||
hasPendingRequest: !!pending,
|
||||
hasBankAccount,
|
||||
});
|
||||
if (!guard.ok) throw new BadRequestException(guard.message);
|
||||
@@ -276,14 +284,6 @@ export class SettlementService {
|
||||
if (!picked.ok) throw new BadRequestException(picked.message);
|
||||
|
||||
const created = await this.prisma.$transaction(async (tx) => {
|
||||
const stillPending = await tx.storeWithdrawRequest.findFirst({
|
||||
where: { storeId, status: 'PENDING_REVIEW' },
|
||||
select: { id: true },
|
||||
});
|
||||
if (stillPending) {
|
||||
throw new BadRequestException('已有待审核提现申请,请等待处理完成');
|
||||
}
|
||||
|
||||
const payoutIds = picked.selected.map((p) => p.id);
|
||||
const locked = await tx.storePayout.findMany({
|
||||
where: {
|
||||
@@ -1573,7 +1573,7 @@ export class SettlementService {
|
||||
// ─── Winery bills ────────────────────────────────────
|
||||
|
||||
async generateWineryBillForDay(anchor = new Date()) {
|
||||
const { start, end, billDate } = dayWindow(anchor);
|
||||
const { start, end, billDate } = wineryDayWindow(anchor);
|
||||
const rate = WINERY_SETTLEMENT_RATE;
|
||||
|
||||
const existing = await this.prisma.wineryBill.findUnique({ where: { billDate } });
|
||||
|
||||
@@ -44,6 +44,9 @@ export class StorePackageService {
|
||||
const otherNotes = item.otherNotes != null && String(item.otherNotes).trim()
|
||||
? String(item.otherNotes).trim()
|
||||
: null;
|
||||
const imageUrl = item.imageUrl != null && String(item.imageUrl).trim()
|
||||
? String(item.imageUrl).trim()
|
||||
: null;
|
||||
const sortOrder = item.sortOrder != null ? Number(item.sortOrder) : index;
|
||||
return {
|
||||
name,
|
||||
@@ -51,6 +54,7 @@ export class StorePackageService {
|
||||
dishes,
|
||||
usableTime,
|
||||
otherNotes,
|
||||
imageUrl,
|
||||
sortOrder: Number.isFinite(sortOrder) ? sortOrder : index,
|
||||
};
|
||||
}
|
||||
@@ -62,6 +66,7 @@ export class StorePackageService {
|
||||
dishes: string;
|
||||
usableTime: string | null;
|
||||
otherNotes: string | null;
|
||||
imageUrl: string | null;
|
||||
sortOrder: number;
|
||||
}) {
|
||||
return {
|
||||
@@ -71,6 +76,7 @@ export class StorePackageService {
|
||||
dishes: row.dishes,
|
||||
usableTime: row.usableTime,
|
||||
otherNotes: row.otherNotes,
|
||||
imageUrl: row.imageUrl,
|
||||
sortOrder: row.sortOrder,
|
||||
};
|
||||
}
|
||||
@@ -214,6 +220,7 @@ export class StorePackageService {
|
||||
dishes: pkg.dishes,
|
||||
usableTime: pkg.usableTime ?? null,
|
||||
otherNotes: pkg.otherNotes ?? null,
|
||||
imageUrl: pkg.imageUrl ?? null,
|
||||
sortOrder: pkg.sortOrder ?? index,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -225,6 +225,7 @@ export class StoreService {
|
||||
dishes: p.dishes,
|
||||
usableTime: p.usableTime,
|
||||
otherNotes: p.otherNotes,
|
||||
imageUrl: p.imageUrl,
|
||||
sortOrder: p.sortOrder,
|
||||
})),
|
||||
}),
|
||||
|
||||
@@ -593,6 +593,22 @@ export class TradeService {
|
||||
try {
|
||||
refundResult = await this.payProvider.refundOrder(orderId, outRefundNo, remark);
|
||||
} catch (err) {
|
||||
await this.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: { status: fromStatus, payStatus: 'PAID' },
|
||||
});
|
||||
await this.prisma.commonEvent.create({
|
||||
data: buildOrderStatusEvent({
|
||||
orderId,
|
||||
fromStatus: 'REFUNDING',
|
||||
toStatus: fromStatus,
|
||||
operator: actorType,
|
||||
remark: `退款发起失败,已恢复:${err instanceof Error ? err.message : String(err)}`.slice(
|
||||
0,
|
||||
500,
|
||||
),
|
||||
}),
|
||||
});
|
||||
this.alert.notify({
|
||||
level: 'P0',
|
||||
category: 'pay',
|
||||
|
||||
Reference in New Issue
Block a user