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
@@ -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 {