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:
2026-08-04 23:53:31 +08:00
parent 71f508e02b
commit 4372018c09
34 changed files with 1032 additions and 86 deletions
+1 -3
View File
@@ -251,7 +251,6 @@ describe('validateStoreWithdraw', () => {
requestAmount: 200,
todayApplied: 0,
dailyLimit: 5000,
hasPendingRequest: false,
hasBankAccount: true,
};
@@ -261,8 +260,7 @@ describe('validateStoreWithdraw', () => {
).toBe(false);
});
it('rejects pending request / missing bank / over available', () => {
expect(validateStoreWithdraw({ ...base, hasPendingRequest: true }).ok).toBe(false);
it('rejects missing bank / over available', () => {
expect(validateStoreWithdraw({ ...base, hasBankAccount: false }).ok).toBe(false);
expect(validateStoreWithdraw({ ...base, requestAmount: 1001 }).ok).toBe(false);
});
+2 -5
View File
@@ -105,20 +105,16 @@ export type ValidateStoreWithdrawInput = {
requestAmount: number;
todayApplied: number;
dailyLimit: number;
hasPendingRequest: boolean;
hasBankAccount: boolean;
};
/** 门店未出账提现护栏(FIN-002 单日上限 + 幂等/账户) */
/** 门店未出账提现护栏(FIN-002 单日上限 + 账户) */
export function validateStoreWithdraw(
input: ValidateStoreWithdrawInput,
): { ok: boolean; message?: string } {
if (!input.hasBankAccount) {
return { ok: false, message: '请先完善入驻收款账户后再提现' };
}
if (input.hasPendingRequest) {
return { ok: false, message: '已有待审核提现申请,请等待处理完成' };
}
if (!(input.requestAmount > 0)) {
return { ok: false, message: '提现金额必须大于 0' };
}
@@ -354,3 +350,4 @@ export function orderTabToStatuses(tab: string): string[] | undefined {
export * from './city-partner';
export * from './dev-plan';
export * from './support-ticket';
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { validateSupportTicketStatusTransition } from './support-ticket';
describe('validateSupportTicketStatusTransition', () => {
it('allows DEVELOPING -> TESTING', () => {
expect(
validateSupportTicketStatusTransition('DEVELOPING', 'TESTING', {
linkedTaskCount: 1,
isSuperAdmin: true,
}).ok,
).toBe(true);
});
it('blocks PENDING_REVIEW -> DEVELOPING without tasks', () => {
expect(
validateSupportTicketStatusTransition('PENDING_REVIEW', 'DEVELOPING', {
linkedTaskCount: 0,
isSuperAdmin: true,
}).ok,
).toBe(false);
});
it('blocks rollback PASSED -> DEVELOPING', () => {
expect(
validateSupportTicketStatusTransition('PASSED', 'DEVELOPING', {
linkedTaskCount: 1,
isSuperAdmin: true,
}).ok,
).toBe(false);
});
it('requires super admin and reason for REJECTED', () => {
expect(
validateSupportTicketStatusTransition('DEVELOPING', 'REJECTED', {
linkedTaskCount: 1,
isSuperAdmin: false,
rejectReason: 'x',
}).ok,
).toBe(false);
expect(
validateSupportTicketStatusTransition('DEVELOPING', 'REJECTED', {
linkedTaskCount: 1,
isSuperAdmin: true,
rejectReason: '',
}).ok,
).toBe(false);
});
});
+45
View File
@@ -0,0 +1,45 @@
export type SupportTicketStatus =
| 'PENDING_REVIEW'
| 'REJECTED'
| 'DEVELOPING'
| 'TESTING'
| 'PASSED';
export type SupportTicketStatusTransitionContext = {
linkedTaskCount: number;
isSuperAdmin: boolean;
rejectReason?: string;
};
const FORWARD_TRANSITIONS: Record<SupportTicketStatus, SupportTicketStatus[]> = {
PENDING_REVIEW: ['DEVELOPING', 'REJECTED'],
DEVELOPING: ['TESTING', 'REJECTED'],
TESTING: ['PASSED', 'REJECTED'],
REJECTED: [],
PASSED: [],
};
/** 技术支持工单批量/人工改状态校验(禁止回退) */
export function validateSupportTicketStatusTransition(
from: SupportTicketStatus,
to: SupportTicketStatus,
ctx: SupportTicketStatusTransitionContext,
): { ok: boolean; message?: string } {
if (from === to) return { ok: true };
const allowed = FORWARD_TRANSITIONS[from] ?? [];
if (!allowed.includes(to)) {
return { ok: false, message: `不允许从「${from}」变更为「${to}` };
}
if (to === 'REJECTED') {
if (!ctx.isSuperAdmin) return { ok: false, message: '仅最高管理员可驳回工单' };
if (!ctx.rejectReason?.trim()) return { ok: false, message: '请填写驳回理由' };
}
if (from === 'PENDING_REVIEW' && to === 'DEVELOPING' && ctx.linkedTaskCount < 1) {
return { ok: false, message: '待评审工单转入开发须至少 1 条关联开发任务' };
}
return { ok: true };
}
+12
View File
@@ -202,6 +202,18 @@ export interface DevPlanTaskDispatchInput {
export interface DevPlanTaskBatchUpdateInput {
taskIds: string[];
status?: DevPlanTaskStatusDto;
versionId?: string | null;
}
export interface DevPlanLinkedTaskSummary {
id: string;
+3 -1
View File
@@ -63,7 +63,6 @@ export interface StoreWithdrawSummaryDto {
remainingDailyLimit: number;
isPrimary: boolean;
hasBankAccount: boolean;
hasPendingRequest: boolean;
bankAccount?: StoreWithdrawBankAccountDto | null;
}
@@ -184,6 +183,9 @@ export const STORE_SETTLEMENT_DEFAULT_RATE = 0.6;
/** 酒厂账单结算比例(酒单实付 × 比例),暂定 30% */
export const WINERY_SETTLEMENT_RATE = 0.3;
/** 酒厂账单出账滞后自然日(T+3:支付日 + 3 天后纳入账单) */
export const WINERY_SETTLEMENT_LAG_DAYS = 3;
export type { LogisticsSettlementMethod } from './enums';
export { LOGISTICS_SETTLEMENT_METHOD_LABELS } from './enums';
import type { LogisticsSettlementMethod } from './enums';
@@ -5,6 +5,7 @@ export interface StorePackageItemDto {
dishes: string;
usableTime?: string | null;
otherNotes?: string | null;
imageUrl?: string | null;
sortOrder: number;
}
@@ -47,6 +47,7 @@ export interface SupportTicketDto {
reviewerName?: string | null;
reviewedAt?: string | null;
remark?: string | null;
attachmentUrls?: string[] | null;
createdAt: string;
updatedAt: string;
completedAt?: string | null;
@@ -57,6 +58,15 @@ export interface CreateSupportTicketRequest {
title: string;
content?: string;
remark?: string;
attachmentUrls?: string[];
}
export interface UpdateSupportTicketRequest {
ticketType?: SupportTicketTypeDto;
title?: string;
content?: string;
remark?: string;
attachmentUrls?: string[];
}
export interface RejectSupportTicketRequest {
@@ -73,6 +83,8 @@ export interface ReviewSupportTicketRequest {
rejectReason?: string;
note?: string;
tasks?: CreateDevPlanTaskFromTicketInput[];
dispatchToWecom?: boolean;
dispatchSupplement?: string;
}
export interface SupportTicketLinkedTaskDto {
@@ -108,3 +120,10 @@ export interface BatchReviewConfirmItem {
export interface BatchReviewConfirmRequest {
items: BatchReviewConfirmItem[];
}
export interface BatchUpdateSupportTicketStatusRequest {
ticketIds: string[];
status: SupportTicketStatusDto;
rejectReason?: string;
note?: string;
}