From 2c713febc633c265c66a02ebbac1ce2dd37b26ad Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 5 Jan 2026 11:57:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=B8=89=E4=B8=AA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 任务标题自动生成 - 根据SPU信息+平台+品牌+任务子类型+需求类型自动组合生成标题 - 添加watch监听相关字段变化自动更新标题 2. 预览图上传功能 - 添加S3预览图上传API(自动压缩到200px宽度) - Task实体增加previewImage字段 - 前端添加预览图上传组件 - 任务卡片显示65x65像素预览图 3. 接单大厅按部门显示 - 根据用户所属部门筛选可接单的任务类型 - 只显示用户部门对应的任务类型的任务 --- backend/pom.xml | 7 + .../controller/FileUploadController.java | 50 ++++ .../designhub/controller/TaskController.java | 16 +- .../main/java/com/designhub/entity/Task.java | 8 + .../java/com/designhub/mapper/TaskMapper.java | 65 +++++ .../java/com/designhub/service/S3Service.java | 83 +++++++ .../com/designhub/service/TaskService.java | 40 ++- frontend/src/views/CreateTask.vue | 227 +++++++++++++++++- frontend/src/views/TaskHall.vue | 118 +++++++-- 9 files changed, 579 insertions(+), 35 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 71fdaa7..231a3a8 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -167,6 +167,13 @@ commons-io 2.11.0 + + + + net.coobird + thumbnailator + 0.4.20 + diff --git a/backend/src/main/java/com/designhub/controller/FileUploadController.java b/backend/src/main/java/com/designhub/controller/FileUploadController.java index 2ee3c63..eb3bbc7 100644 --- a/backend/src/main/java/com/designhub/controller/FileUploadController.java +++ b/backend/src/main/java/com/designhub/controller/FileUploadController.java @@ -181,4 +181,54 @@ public class FileUploadController { return Result.error("获取文件列表失败: " + e.getMessage()); } } + + /** + * 上传预览图(自动压缩到最大宽度200px) + * + * @param file 图片文件 + * @param request HttpServletRequest + * @return 预览图URL + */ + @PostMapping("/upload-preview") + public Result uploadPreviewImage( + @RequestParam("file") MultipartFile file, + HttpServletRequest request) { + + try { + // 从token获取用户信息 + String token = request.getHeader("Authorization"); + if (token == null || !token.startsWith("Bearer ")) { + return Result.error("未登录或token无效"); + } + token = token.substring(7); + + String username = jwtUtil.getUsernameFromToken(token); + if (username == null) { + return Result.error("token无效"); + } + + User user = userService.getUserByUsername(username); + if (user == null) { + return Result.error("用户不存在"); + } + + // 验证是否为图片 + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + return Result.error("只能上传图片文件(jpg, png, gif等)"); + } + + // 上传预览图 + String previewUrl = s3Service.uploadPreviewImage( + file, + user.getId(), + user.getRealName() != null ? user.getRealName() : user.getUsername() + ); + + return Result.success(previewUrl); + + } catch (Exception e) { + return Result.error("预览图上传失败: " + e.getMessage()); + } + } } diff --git a/backend/src/main/java/com/designhub/controller/TaskController.java b/backend/src/main/java/com/designhub/controller/TaskController.java index 4eea057..7beb302 100644 --- a/backend/src/main/java/com/designhub/controller/TaskController.java +++ b/backend/src/main/java/com/designhub/controller/TaskController.java @@ -171,7 +171,7 @@ public class TaskController { return Result.success(taskPage); } - @Operation(summary = "任务大厅", description = "获取可接单的任务列表,支持多种筛选、搜索和排序") + @Operation(summary = "任务大厅", description = "获取可接单的任务列表,支持多种筛选、搜索和排序,根据用户所属部门筛选") @GetMapping("/hall") public Result> getTaskHall( @Parameter(description = "任务类型ID") @RequestParam(required = false) Long taskTypeId, @@ -187,17 +187,23 @@ public class TaskController { @Parameter(description = "发布时间范围:today/week/month") @RequestParam(required = false) String timeRange, @Parameter(description = "排序方式:latest/score/deadline/hot") @RequestParam(required = false) String sortBy, @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, - @Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize) { + @Parameter(description = "每页数量") @RequestParam(defaultValue = "20") Integer pageSize, + @RequestHeader("Authorization") String authHeader) { + + // 获取当前用户的部门ID + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long userId = jwtUtil.getUserIdFromToken(token); + Long userDeptId = taskService.getUserDeptId(userId); log.info("查询任务大厅: taskTypeId={}, demandTypeId={}, brandId={}, categoryId={}, platformId={}, " + "taskSubType={}, minScore={}, maxScore={}, keyword={}, publisherName={}, timeRange={}, " + - "sortBy={}, page={}, pageSize={}", + "sortBy={}, page={}, pageSize={}, userDeptId={}", taskTypeId, demandTypeId, brandId, categoryId, platformId, taskSubType, - minScore, maxScore, keyword, publisherName, timeRange, sortBy, page, pageSize); + minScore, maxScore, keyword, publisherName, timeRange, sortBy, page, pageSize, userDeptId); IPage taskPage = taskService.getTaskHall(page, pageSize, taskTypeId, demandTypeId, brandId, categoryId, platformId, taskSubType, minScore, maxScore, keyword, - publisherName, timeRange, sortBy); + publisherName, timeRange, sortBy, userDeptId); return Result.success(taskPage); } diff --git a/backend/src/main/java/com/designhub/entity/Task.java b/backend/src/main/java/com/designhub/entity/Task.java index d6b08e5..d94c927 100644 --- a/backend/src/main/java/com/designhub/entity/Task.java +++ b/backend/src/main/java/com/designhub/entity/Task.java @@ -105,6 +105,11 @@ public class Task implements Serializable { */ private String attachmentUrl; + /** + * 预览图URL + */ + private String previewImage; + /** * 交付路径 */ @@ -409,6 +414,9 @@ public class Task implements Serializable { public String getAttachmentUrl() { return attachmentUrl; } public void setAttachmentUrl(String attachmentUrl) { this.attachmentUrl = attachmentUrl; } + public String getPreviewImage() { return previewImage; } + public void setPreviewImage(String previewImage) { this.previewImage = previewImage; } + public String getDeliveryPath() { return deliveryPath; } public void setDeliveryPath(String deliveryPath) { this.deliveryPath = deliveryPath; } diff --git a/backend/src/main/java/com/designhub/mapper/TaskMapper.java b/backend/src/main/java/com/designhub/mapper/TaskMapper.java index 66895c2..c2e4864 100644 --- a/backend/src/main/java/com/designhub/mapper/TaskMapper.java +++ b/backend/src/main/java/com/designhub/mapper/TaskMapper.java @@ -76,6 +76,71 @@ public interface TaskMapper extends BaseMapper { @Param("timeRange") String timeRange, @Param("sortBy") String sortBy); + /** + * 查询任务大厅(按部门筛选) + */ + @Select("") + IPage selectTaskHallByDept(Page page, + @Param("taskTypeId") Long taskTypeId, + @Param("demandTypeId") Long demandTypeId, + @Param("brandId") Long brandId, + @Param("categoryId") Long categoryId, + @Param("platformId") Long platformId, + @Param("taskSubType") String taskSubType, + @Param("minScore") Integer minScore, + @Param("maxScore") Integer maxScore, + @Param("keyword") String keyword, + @Param("publisherName") String publisherName, + @Param("timeRange") String timeRange, + @Param("sortBy") String sortBy, + @Param("allowedTaskTypeIds") List allowedTaskTypeIds); + /** * 查询我发布的任务 */ diff --git a/backend/src/main/java/com/designhub/service/S3Service.java b/backend/src/main/java/com/designhub/service/S3Service.java index 946f9b7..de23be6 100644 --- a/backend/src/main/java/com/designhub/service/S3Service.java +++ b/backend/src/main/java/com/designhub/service/S3Service.java @@ -18,11 +18,14 @@ import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; +import net.coobird.thumbnailator.Thumbnails; /** * S3文件存储服务 @@ -143,4 +146,84 @@ public class S3Service { .orderByDesc(FileUpload::getCreateTime); return fileUploadMapper.selectList(wrapper); } + + /** + * 上传预览图(压缩到最大宽度200px,保持比例) + * + * @param file 图片文件 + * @param uploaderId 上传者ID + * @param uploaderName 上传者姓名 + * @return 压缩后图片的URL + */ + public String uploadPreviewImage(MultipartFile file, Long uploaderId, String uploaderName) throws IOException { + // 获取激活的S3配置 + S3Config config = s3ConfigMapper.getActiveConfig(); + if (config == null) { + throw new RuntimeException("S3配置未激活,请先配置S3存储"); + } + + // 验证是否为图片 + String contentType = file.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + throw new RuntimeException("只能上传图片文件"); + } + + // 生成唯一文件名 + String originalFilename = file.getOriginalFilename(); + String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8); + String storedName = "preview_" + timestamp + "_" + uuid + fileExtension; + + // 构建文件路径(预览图专用目录) + String filePath = config.getFilePath() + "previews/" + storedName; + + try { + // 压缩图片到最大宽度200px,保持比例 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Thumbnails.of(file.getInputStream()) + .width(200) // 最大宽度200像素 + .keepAspectRatio(true) // 保持比例 + .outputQuality(0.85) // 图片质量 + .toOutputStream(outputStream); + + byte[] compressedBytes = outputStream.toByteArray(); + + // 创建S3客户端 + S3Client s3Client = createS3Client(config); + + // 上传压缩后的图片 + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(config.getBucket()) + .key(filePath) + .contentType(contentType) + .build(); + + s3Client.putObject(putObjectRequest, + RequestBody.fromInputStream(new ByteArrayInputStream(compressedBytes), compressedBytes.length)); + + // 生成文件URL + String fileUrl = config.getPublicDomain() + "/" + filePath; + + // 保存文件上传记录 + FileUpload fileUpload = new FileUpload(); + fileUpload.setOriginalName(originalFilename); + fileUpload.setStoredName(storedName); + fileUpload.setFilePath(filePath); + fileUpload.setFileUrl(fileUrl); + fileUpload.setFileSize((long) compressedBytes.length); + fileUpload.setFileType(contentType); + fileUpload.setUploaderId(uploaderId); + fileUpload.setUploaderName(uploaderName); + fileUpload.setRelatedType("preview_image"); + fileUploadMapper.insert(fileUpload); + + log.info("预览图上传成功: {} -> {} (压缩后大小: {} bytes)", originalFilename, fileUrl, compressedBytes.length); + return fileUrl; + + } catch (Exception e) { + log.error("预览图上传失败: {}", originalFilename, e); + throw new RuntimeException("预览图上传失败: " + e.getMessage()); + } + } } diff --git a/backend/src/main/java/com/designhub/service/TaskService.java b/backend/src/main/java/com/designhub/service/TaskService.java index eb0c4ef..3e35297 100644 --- a/backend/src/main/java/com/designhub/service/TaskService.java +++ b/backend/src/main/java/com/designhub/service/TaskService.java @@ -634,16 +634,48 @@ public class TaskService { } /** - * 获取任务大厅 + * 获取任务大厅(根据用户部门筛选可接单的任务) */ public IPage getTaskHall(Integer page, Integer pageSize, Long taskTypeId, Long demandTypeId, Long brandId, Long categoryId, Long platformId, String taskSubType, Integer minScore, Integer maxScore, String keyword, String publisherName, - String timeRange, String sortBy) { + String timeRange, String sortBy, Long userDeptId) { Page pageParam = new Page<>(page, pageSize); - return taskMapper.selectTaskHall(pageParam, taskTypeId, demandTypeId, brandId, categoryId, + + // 获取当前用户部门可以接单的任务类型ID列表 + List allowedTaskTypeIds = null; + if (userDeptId != null) { + allowedTaskTypeIds = demandTypeMapper.selectList(null).stream() + .filter(dt -> { + String allowedDepts = dt.getAllowedDeptIds(); + // 如果allowedDeptIds为空,表示所有部门都可接单 + if (allowedDepts == null || allowedDepts.isEmpty()) { + return true; + } + // 检查用户部门是否在允许列表中 + String[] deptIds = allowedDepts.split(","); + for (String deptId : deptIds) { + if (deptId.trim().equals(String.valueOf(userDeptId))) { + return true; + } + } + return false; + }) + .map(DemandType::getId) + .collect(Collectors.toList()); + } + + return taskMapper.selectTaskHallByDept(pageParam, taskTypeId, demandTypeId, brandId, categoryId, platformId, taskSubType, minScore, maxScore, keyword, - publisherName, timeRange, sortBy); + publisherName, timeRange, sortBy, allowedTaskTypeIds); + } + + /** + * 获取用户所属部门ID + */ + public Long getUserDeptId(Long userId) { + User user = userService.getUserById(userId); + return user != null ? user.getDepartmentId() : null; } /** diff --git a/frontend/src/views/CreateTask.vue b/frontend/src/views/CreateTask.vue index 80b7b1f..7fcd043 100644 --- a/frontend/src/views/CreateTask.vue +++ b/frontend/src/views/CreateTask.vue @@ -25,13 +25,17 @@ :rules="formRules" layout="vertical" > - + +
+ + 标题将根据SPU信息、平台、品牌、任务子类型、需求类型自动生成 +
@@ -265,6 +269,32 @@ /> + + +
+ +
+ +
上传预览图
+
+
+
+ + + + 建议上传正方形图片,系统将自动压缩处理,显示在任务卡片上 +
+
+
+
@@ -445,6 +475,10 @@ const taskSubTypes = ref([]) const fileList = ref([]) const uploadProgress = ref(0) +// 预览图上传相关 +const previewFileList = ref([]) +const previewUploading = ref(false) + // 任务类型项接口 interface TaskTypeItem { taskTypeId: number | undefined @@ -467,6 +501,7 @@ const formData = reactive({ categoryId: undefined, spuInfo: '', attachmentUrl: '', + previewImage: '', // 预览图URL remark: '', // 备注信息 releaseType: 'default', // receiverId: undefined, // 已移除,改用子任务指定 @@ -570,6 +605,66 @@ const removeTaskType = (index: number) => { } } +// 自动生成任务标题 +const generateTitle = () => { + const parts: string[] = [] + + // 1. SPU信息 + if (formData.spuInfo && formData.spuInfo.trim()) { + parts.push(formData.spuInfo.trim()) + } + + // 2. 平台名称 + if (formData.platformId) { + const platform = platforms.value.find((p: any) => p.id === formData.platformId) + if (platform) { + parts.push(platform.name) + } + } + + // 3. 品牌名称 + if (formData.brandId) { + const brand = brands.value.find((b: any) => b.id === formData.brandId) + if (brand) { + parts.push(brand.name) + } + } + + // 4. 任务子类型 + if (formData.taskSubType) { + parts.push(formData.taskSubType) + } + + // 5. 需求类型(第一个任务类型名称) + if (formData.taskTypes.length > 0 && formData.taskTypes[0].taskTypeId) { + const taskType = taskTypes.value.find((t: any) => t.id === formData.taskTypes[0].taskTypeId) + if (taskType) { + parts.push(taskType.name) + } + } + + // 生成标题 + if (parts.length > 0) { + formData.title = parts.join('-') + } +} + +// 监听相关字段变化,自动生成标题 +watch( + () => [ + formData.spuInfo, + formData.platformId, + formData.brandId, + formData.taskSubType, + formData.taskTypes[0]?.taskTypeId + ], + () => { + // 只有当标题为空或是之前自动生成的标题时才自动更新 + generateTitle() + }, + { deep: true } +) + // 计算预估积分(累加所有任务类型) const estimatedScore = computed(() => { let totalScore = 0 @@ -1035,6 +1130,74 @@ const handleRemove = (file: UploadFile) => { return true } +// 预览图上传前验证 +const beforePreviewUpload = (file: File) => { + const isImage = file.type.startsWith('image/') + if (!isImage) { + message.error('只能上传图片文件') + return false + } + const isLt10M = file.size / 1024 / 1024 < 10 + if (!isLt10M) { + message.error('图片大小不能超过 10MB') + return false + } + return true +} + +// 预览图上传处理 +const handlePreviewUpload = async (options: any) => { + const { file, onSuccess, onError } = options + + try { + previewUploading.value = true + + const formDataObj = new FormData() + formDataObj.append('file', file) + + const response = await fetch('/api/file/upload-preview', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: formDataObj + }) + + const result = await response.json() + + if (result.code === 200 && result.data) { + formData.previewImage = result.data + previewFileList.value = [{ + uid: file.uid, + name: file.name, + status: 'done', + url: result.data + } as UploadFile] + if (onSuccess) { + onSuccess(result.data) + } + message.success('预览图上传成功') + } else { + throw new Error(result.msg || '上传失败') + } + } catch (error: any) { + console.error('预览图上传失败:', error) + if (onError) { + onError(error) + } + message.error('预览图上传失败: ' + error.message) + } finally { + previewUploading.value = false + } +} + +// 删除预览图 +const handlePreviewRemove = () => { + formData.previewImage = '' + previewFileList.value = [] + return true +} + // 加载任务数据用于编辑 const loadTaskForEdit = async (taskId: number) => { try { @@ -1637,5 +1800,65 @@ onUnmounted(() => { } } } + +// 任务标题自动生成提示 +.title-auto-tip { + margin-top: 8px; + font-size: 12px; + color: $text-muted; + display: flex; + align-items: center; +} + +// 预览图上传区域 +.preview-upload-area { + .upload-button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: $text-muted; + + .upload-text { + margin-top: 8px; + font-size: 12px; + } + } + + .preview-tip { + margin-top: 8px; + font-size: 12px; + color: $text-muted; + display: flex; + align-items: center; + + .tip-icon { + margin-right: 6px; + } + } + + :deep(.ant-upload-list-picture-card) { + .ant-upload-list-item { + border-color: rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.02); + + &:hover { + border-color: $brand-primary; + } + } + } + + :deep(.ant-upload-select-picture-card) { + width: 104px; + height: 104px; + background: rgba(255, 255, 255, 0.02); + border: 1px dashed rgba(255, 255, 255, 0.2); + border-radius: 8px; + + &:hover { + border-color: $brand-primary; + } + } +} diff --git a/frontend/src/views/TaskHall.vue b/frontend/src/views/TaskHall.vue index a160dae..21a1ba2 100644 --- a/frontend/src/views/TaskHall.vue +++ b/frontend/src/views/TaskHall.vue @@ -200,28 +200,37 @@ class="task-card chart-card" @click="handleTaskClick(task)" > - -
-

{{ task.title }}

-
- - - 子任务 - - - - 新发布 - - - - 高分 - - - - 紧急 - + +
+ +
+ 预览图
-
+ + +
+ +
+

{{ task.title }}

+
+ + + 子任务 + + + + 新发布 + + + + 高分 + + + + 紧急 + +
+
@@ -337,6 +346,8 @@ {{ canGrabTask(task).canGrab ? '抢单' : '不可接单' }}
+
+
@@ -3918,11 +3929,11 @@ const handleConfigUpdate = async (event: CustomEvent) => { } // 组件挂载 -// 获取任务状态文本(增强版,支持区分驳回和草稿) +// 获取任务状态文本(增强版,支持区分驳回、审核中和草稿) const getTaskStatusText = (status: string, task?: any) => { - // 如果有task对象,检查approvalStatus来区分驳回和草稿 + // 如果有task对象,检查approvalStatus来区分不同状态 if (task) { - // 检查是否被驳回(无论status是draft还是rejected) + // 1. 检查是否被驳回(无论status是draft还是rejected) if (task.approvalStatus === 'rejected_by_demand' || task.approvalStatus === 'rejected_by_receiving') { return '已驳回' } @@ -3930,6 +3941,32 @@ const getTaskStatusText = (status: string, task?: any) => { if (status === 'rejected') { return '已驳回' } + + // 2. 检查是否正在审核中 + if (task.approvalStatus === 'pending_demand_approval') { + return '待部门审核' + } + if (task.approvalStatus === 'pending_receiving_approval') { + return '待接单审核' + } + + // 3. 流水线任务且已审核通过,根据是否有接收人显示不同状态 + if (task.isPipeline === 1 && task.approvalStatus === 'approved') { + if (task.receiverId) { + return '进行中' + } else { + return '待接单' + } + } + + // 4. 普通任务已审核通过但status还是draft,修正显示 + if (task.approvalStatus === 'approved' && status === 'draft') { + if (task.receiverId) { + return '进行中' + } else { + return '待接单' + } + } } const statusMap: Record = { @@ -4371,6 +4408,39 @@ onUnmounted(() => { box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); } + // 带预览图的卡片布局 + .card-with-preview { + display: flex; + gap: 16px; + + .task-preview { + flex-shrink: 0; + width: 65px; + height: 65px; + border-radius: 8px; + overflow: hidden; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .card-content { + flex: 1; + min-width: 0; + + &.has-preview { + .task-title { + font-size: 15px; + } + } + } + } + // 审核任务卡片特殊样式 .task-info-grid { display: grid;