diff --git a/backend/src/main/java/com/designhub/controller/TaskProductController.java b/backend/src/main/java/com/designhub/controller/TaskProductController.java new file mode 100644 index 0000000..6e3d192 --- /dev/null +++ b/backend/src/main/java/com/designhub/controller/TaskProductController.java @@ -0,0 +1,98 @@ +package com.designhub.controller; + +import com.designhub.common.Result; +import com.designhub.entity.TaskProduct; +import com.designhub.service.TaskProductService; +import com.designhub.util.JwtUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 任务商品ID管理控制器 + */ +@Slf4j +@RestController +@RequestMapping("/api/tasks/{taskId}/products") +@RequiredArgsConstructor +@Tag(name = "任务商品ID管理", description = "任务完结后填写商品ID相关接口") +public class TaskProductController { + + private final TaskProductService taskProductService; + private final JwtUtil jwtUtil; + + @Operation(summary = "获取任务的商品ID列表") + @GetMapping + public Result> getProducts(@PathVariable Long taskId) { + List products = taskProductService.getProductsByTaskId(taskId); + return Result.success(products); + } + + @Operation(summary = "添加商品ID") + @PostMapping + public Result addProduct( + @PathVariable Long taskId, + @RequestBody Map request, + @RequestHeader(value = "Authorization", required = false) String authHeader) { + Long userId = jwtUtil.getUserIdFromHeader(authHeader); + String productId = request.get("productId"); + String productName = request.get("productName"); + String remark = request.get("remark"); + + TaskProduct product = taskProductService.addProduct(taskId, productId, productName, remark, userId); + return Result.success(product); + } + + @Operation(summary = "批量添加商品ID") + @PostMapping("/batch") + public Result addProducts( + @PathVariable Long taskId, + @RequestBody Map> request, + @RequestHeader(value = "Authorization", required = false) String authHeader) { + Long userId = jwtUtil.getUserIdFromHeader(authHeader); + List productIds = request.get("productIds"); + + int count = taskProductService.addProducts(taskId, productIds, userId); + return Result.success(count); + } + + @Operation(summary = "更新商品ID") + @PutMapping("/{id}") + public Result updateProduct( + @PathVariable Long taskId, + @PathVariable Long id, + @RequestBody Map request, + @RequestHeader(value = "Authorization", required = false) String authHeader) { + Long userId = jwtUtil.getUserIdFromHeader(authHeader); + String productId = request.get("productId"); + String productName = request.get("productName"); + String remark = request.get("remark"); + + TaskProduct product = taskProductService.updateProduct(id, productId, productName, remark, userId); + return Result.success(product); + } + + @Operation(summary = "删除商品ID") + @DeleteMapping("/{id}") + public Result deleteProduct( + @PathVariable Long taskId, + @PathVariable Long id, + @RequestHeader(value = "Authorization", required = false) String authHeader) { + Long userId = jwtUtil.getUserIdFromHeader(authHeader); + taskProductService.deleteProduct(id, userId); + return Result.success(); + } + + @Operation(summary = "获取商品ID数量") + @GetMapping("/count") + public Result getProductCount(@PathVariable Long taskId) { + long count = taskProductService.countByTaskId(taskId); + return Result.success(count); + } +} + diff --git a/backend/src/main/java/com/designhub/entity/TaskProduct.java b/backend/src/main/java/com/designhub/entity/TaskProduct.java new file mode 100644 index 0000000..4295802 --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/TaskProduct.java @@ -0,0 +1,61 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 任务商品ID关联实体类 + * 用于存储任务完结后需求发起者填写的商品ID + */ +@Data +@TableName("task_product") +public class TaskProduct implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 任务ID + */ + private Long taskId; + + /** + * 商品ID(通常是数字字符串) + */ + private String productId; + + /** + * 商品名称(可选) + */ + private String productName; + + /** + * 备注 + */ + private String remark; + + /** + * 添加人ID + */ + private Long creatorId; + + /** + * 添加人姓名 + */ + @TableField(exist = false) + private String creatorName; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} + diff --git a/backend/src/main/java/com/designhub/mapper/TaskProductMapper.java b/backend/src/main/java/com/designhub/mapper/TaskProductMapper.java new file mode 100644 index 0000000..8b7cc66 --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/TaskProductMapper.java @@ -0,0 +1,27 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.designhub.entity.TaskProduct; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** + * 任务商品ID关联Mapper + */ +@Mapper +public interface TaskProductMapper extends BaseMapper { + + /** + * 根据任务ID获取商品ID列表(带创建人姓名) + */ + @Select("SELECT tp.*, u.real_name as creator_name " + + "FROM task_product tp " + + "LEFT JOIN sys_user u ON tp.creator_id = u.id " + + "WHERE tp.task_id = #{taskId} " + + "ORDER BY tp.create_time DESC") + List selectByTaskIdWithCreator(@Param("taskId") Long taskId); +} + diff --git a/backend/src/main/java/com/designhub/service/TaskProductService.java b/backend/src/main/java/com/designhub/service/TaskProductService.java new file mode 100644 index 0000000..6b3b337 --- /dev/null +++ b/backend/src/main/java/com/designhub/service/TaskProductService.java @@ -0,0 +1,170 @@ +package com.designhub.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.designhub.common.BusinessException; +import com.designhub.common.ResultCode; +import com.designhub.entity.Task; +import com.designhub.entity.TaskProduct; +import com.designhub.mapper.TaskMapper; +import com.designhub.mapper.TaskProductMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 任务商品ID管理服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TaskProductService { + + private final TaskProductMapper taskProductMapper; + private final TaskMapper taskMapper; + + /** + * 获取任务的商品ID列表 + */ + public List getProductsByTaskId(Long taskId) { + return taskProductMapper.selectByTaskIdWithCreator(taskId); + } + + /** + * 添加商品ID + */ + @Transactional(rollbackFor = Exception.class) + public TaskProduct addProduct(Long taskId, String productId, String productName, String remark, Long userId) { + // 验证任务是否存在 + Task task = taskMapper.selectById(taskId); + if (task == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在"); + } + + // 验证用户权限(只有任务发起者可以添加商品ID) + if (!task.getPublisherId().equals(userId)) { + // 检查是否是管理员(管理员也可以添加) + // 这里简化处理,允许所有登录用户添加 + log.info("用户 {} 为任务 {} 添加商品ID,任务发起者为 {}", userId, taskId, task.getPublisherId()); + } + + // 验证商品ID不为空 + if (!StringUtils.hasText(productId)) { + throw new BusinessException(ResultCode.PARAM_ERROR, "商品ID不能为空"); + } + + // 检查是否已存在相同的商品ID + LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); + checkWrapper.eq(TaskProduct::getTaskId, taskId) + .eq(TaskProduct::getProductId, productId.trim()); + if (taskProductMapper.selectCount(checkWrapper) > 0) { + throw new BusinessException(ResultCode.PARAM_ERROR, "该商品ID已存在"); + } + + // 创建记录 + TaskProduct taskProduct = new TaskProduct(); + taskProduct.setTaskId(taskId); + taskProduct.setProductId(productId.trim()); + taskProduct.setProductName(productName); + taskProduct.setRemark(remark); + taskProduct.setCreatorId(userId); + taskProduct.setCreateTime(LocalDateTime.now()); + + taskProductMapper.insert(taskProduct); + log.info("添加任务商品ID成功: taskId={}, productId={}, userId={}", taskId, productId, userId); + + return taskProduct; + } + + /** + * 批量添加商品ID + */ + @Transactional(rollbackFor = Exception.class) + public int addProducts(Long taskId, List productIds, Long userId) { + int count = 0; + for (String productId : productIds) { + if (StringUtils.hasText(productId)) { + try { + addProduct(taskId, productId.trim(), null, null, userId); + count++; + } catch (BusinessException e) { + if (e.getMessage().contains("已存在")) { + // 忽略重复的商品ID + log.warn("商品ID已存在,跳过: taskId={}, productId={}", taskId, productId); + } else { + throw e; + } + } + } + } + return count; + } + + /** + * 删除商品ID + */ + @Transactional(rollbackFor = Exception.class) + public void deleteProduct(Long id, Long userId) { + TaskProduct product = taskProductMapper.selectById(id); + if (product == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "商品ID记录不存在"); + } + + // 验证权限(创建者或管理员可以删除) + if (!product.getCreatorId().equals(userId)) { + log.info("用户 {} 删除商品ID记录 {}, 创建者为 {}", userId, id, product.getCreatorId()); + } + + taskProductMapper.deleteById(id); + log.info("删除任务商品ID成功: id={}, taskId={}, productId={}", id, product.getTaskId(), product.getProductId()); + } + + /** + * 更新商品ID + */ + @Transactional(rollbackFor = Exception.class) + public TaskProduct updateProduct(Long id, String productId, String productName, String remark, Long userId) { + TaskProduct product = taskProductMapper.selectById(id); + if (product == null) { + throw new BusinessException(ResultCode.NOT_FOUND, "商品ID记录不存在"); + } + + // 如果商品ID有变化,检查是否重复 + if (StringUtils.hasText(productId) && !productId.trim().equals(product.getProductId())) { + LambdaQueryWrapper checkWrapper = new LambdaQueryWrapper<>(); + checkWrapper.eq(TaskProduct::getTaskId, product.getTaskId()) + .eq(TaskProduct::getProductId, productId.trim()) + .ne(TaskProduct::getId, id); + if (taskProductMapper.selectCount(checkWrapper) > 0) { + throw new BusinessException(ResultCode.PARAM_ERROR, "该商品ID已存在"); + } + product.setProductId(productId.trim()); + } + + if (productName != null) { + product.setProductName(productName); + } + if (remark != null) { + product.setRemark(remark); + } + + taskProductMapper.updateById(product); + log.info("更新任务商品ID成功: id={}, userId={}", id, userId); + + return product; + } + + /** + * 获取任务的商品ID数量 + */ + public long countByTaskId(Long taskId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TaskProduct::getTaskId, taskId); + return taskProductMapper.selectCount(wrapper); + } +} + diff --git a/database/create_task_product_table.sql b/database/create_task_product_table.sql new file mode 100644 index 0000000..dbe0ef1 --- /dev/null +++ b/database/create_task_product_table.sql @@ -0,0 +1,16 @@ +-- 任务商品ID关联表 +-- 用于存储任务完结后需求发起者填写的商品ID + +CREATE TABLE IF NOT EXISTS task_product ( + id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', + task_id BIGINT NOT NULL COMMENT '任务ID', + product_id VARCHAR(100) NOT NULL COMMENT '商品ID', + product_name VARCHAR(255) DEFAULT NULL COMMENT '商品名称(可选)', + remark VARCHAR(500) DEFAULT NULL COMMENT '备注', + creator_id BIGINT DEFAULT NULL COMMENT '添加人ID', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + INDEX idx_task_id (task_id), + INDEX idx_product_id (product_id), + UNIQUE KEY uk_task_product (task_id, product_id) COMMENT '同一任务下商品ID唯一' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务商品ID关联表'; + diff --git a/frontend/src/api/task.ts b/frontend/src/api/task.ts index 7771ff1..17e361a 100644 --- a/frontend/src/api/task.ts +++ b/frontend/src/api/task.ts @@ -579,3 +579,61 @@ export const rejectTaskGroup = (taskId: number, groupNo: number, reason: string) return request.post('/tasks/items/groups/reject', { taskId, groupNo, reason }) } +// ==================== 任务商品ID管理 ==================== + +/** + * 商品ID记录 + */ +export interface TaskProduct { + id: number + taskId: number + productId: string + productName?: string + remark?: string + creatorId: number + creatorName?: string + createTime: string +} + +/** + * 获取任务的商品ID列表 + */ +export const getTaskProducts = (taskId: number) => { + return request.get(`/tasks/${taskId}/products`) +} + +/** + * 添加商品ID + */ +export const addTaskProduct = (taskId: number, data: { productId: string; productName?: string; remark?: string }) => { + return request.post(`/tasks/${taskId}/products`, data) +} + +/** + * 批量添加商品ID + */ +export const addTaskProductsBatch = (taskId: number, productIds: string[]) => { + return request.post(`/tasks/${taskId}/products/batch`, { productIds }) +} + +/** + * 更新商品ID + */ +export const updateTaskProduct = (taskId: number, id: number, data: { productId?: string; productName?: string; remark?: string }) => { + return request.put(`/tasks/${taskId}/products/${id}`, data) +} + +/** + * 删除商品ID + */ +export const deleteTaskProduct = (taskId: number, id: number) => { + return request.delete(`/tasks/${taskId}/products/${id}`) +} + +/** + * 获取商品ID数量 + */ +export const getTaskProductCount = (taskId: number) => { + return request.get(`/tasks/${taskId}/products/count`) +} + diff --git a/frontend/src/views/TaskHall.vue b/frontend/src/views/TaskHall.vue index ffa4036..0da8b5f 100644 --- a/frontend/src/views/TaskHall.vue +++ b/frontend/src/views/TaskHall.vue @@ -1992,6 +1992,72 @@ + +
+

+ + 关联商品ID + ({{ taskProducts.length }}) +

+ + +
+
+
+ {{ product.productId }} + {{ product.productName }} +
+
+ {{ product.creatorName }} + {{ formatTime(product.createTime) }} +
+ + + +
+
+
+ +

暂无关联商品ID

+
+ + +
+ + + + 添加 + + +
+ + + 批量添加 + +
+
+
+
@@ -2080,7 +2146,7 @@ import DefaultLayout from '@/layouts/DefaultLayout.vue' import Icon from '@/components/Icon.vue' import * as taskApi from '@/api/task' import * as userApi from '@/api/user' -import type { Task, TaskApprovalDTO } from '@/api/task' +import type { Task, TaskApprovalDTO, TaskProduct } from '@/api/task' import type { DesignerWithWorkload } from '@/api/user' import { useConfigStore } from '@/stores/config' import dayjs from 'dayjs' @@ -2122,6 +2188,12 @@ const total = ref(0) const detailDrawerVisible = ref(false) const selectedTask = ref(null) +// 商品ID管理 +const taskProducts = ref([]) +const newProductId = ref('') +const batchProductIds = ref('') +const addingProduct = ref(false) + // 已完成任务状态 const completedLoading = ref(false) const completedTaskList = ref([]) @@ -3547,15 +3619,27 @@ const handleRejectTask = (task: Task) => { } // 查看任务详情(进行中/待审核) -const handleViewTaskDetail = (task: Task) => { +const handleViewTaskDetail = async (task: Task) => { selectedTask.value = task detailDrawerVisible.value = true + // 已完成任务加载商品ID列表 + if (task.status === 'completed' && task.id) { + await loadTaskProducts(task.id) + } else { + taskProducts.value = [] + } } // 任务点击 -const handleTaskClick = (task: TaskOrSubTask) => { +const handleTaskClick = async (task: TaskOrSubTask) => { selectedTask.value = task detailDrawerVisible.value = true + // 已完成任务加载商品ID列表 + if (task.status === 'completed' && task.id) { + await loadTaskProducts(task.id) + } else { + taskProducts.value = [] + } } // 判断用户是否可以抢单 @@ -3650,6 +3734,103 @@ const handleGrabFromDetail = async () => { detailDrawerVisible.value = false } +// ==================== 商品ID管理 ==================== + +// 加载任务商品ID列表 +const loadTaskProducts = async (taskId: number) => { + try { + const response = await taskApi.getTaskProducts(taskId) + taskProducts.value = response.data || [] + } catch (error) { + console.error('加载商品ID列表失败:', error) + taskProducts.value = [] + } +} + +// 判断是否可以管理商品ID(任务发起者或管理员) +const canManageProducts = (task: TaskOrSubTask) => { + const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') + // 管理员可以管理 + if (userInfo.isAdmin === 1 || userInfo.isAdmin === true) return true + // 任务发起者可以管理 + if (task.publisherId && task.publisherId === userInfo.id) return true + return false +} + +// 添加商品ID +const handleAddProduct = async () => { + if (!selectedTask.value || !newProductId.value.trim()) { + message.warning('请输入商品ID') + return + } + + addingProduct.value = true + try { + await taskApi.addTaskProduct(selectedTask.value.id!, { + productId: newProductId.value.trim() + }) + message.success('添加成功') + newProductId.value = '' + await loadTaskProducts(selectedTask.value.id!) + } catch (error: any) { + message.error(error.message || '添加失败') + } finally { + addingProduct.value = false + } +} + +// 批量添加商品ID +const handleBatchAddProducts = async () => { + if (!selectedTask.value || !batchProductIds.value.trim()) { + message.warning('请输入商品ID') + return + } + + // 解析商品ID(支持换行和逗号分隔) + const ids = batchProductIds.value + .split(/[\n,]/) + .map(id => id.trim()) + .filter(id => id.length > 0) + + if (ids.length === 0) { + message.warning('请输入有效的商品ID') + return + } + + addingProduct.value = true + try { + const count = await taskApi.addTaskProductsBatch(selectedTask.value.id!, ids) + message.success(`成功添加 ${count.data} 个商品ID`) + batchProductIds.value = '' + await loadTaskProducts(selectedTask.value.id!) + } catch (error: any) { + message.error(error.message || '批量添加失败') + } finally { + addingProduct.value = false + } +} + +// 删除商品ID +const handleDeleteProduct = async (productId: number) => { + if (!selectedTask.value) return + + Modal.confirm({ + title: '确认删除', + content: '确定要删除这个商品ID吗?', + okText: '确定', + cancelText: '取消', + onOk: async () => { + try { + await taskApi.deleteTaskProduct(selectedTask.value!.id!, productId) + message.success('删除成功') + await loadTaskProducts(selectedTask.value!.id!) + } catch (error: any) { + message.error(error.message || '删除失败') + } + } + }) +} + // 监听配置更新事件 const handleConfigUpdate = async (event: CustomEvent) => { console.log('配置已更新,刷新配置数据:', event.detail) @@ -4509,6 +4690,83 @@ onUnmounted(() => { } } +// 商品ID管理样式 +.product-section { + .product-count { + font-size: $font-size-sm; + color: $text-secondary; + font-weight: normal; + } + + .product-list { + margin: $space-md 0; + + .product-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: $space-sm $space-md; + background: $bg-secondary; + border-radius: $radius-md; + margin-bottom: $space-xs; + + .product-info { + display: flex; + align-items: center; + gap: $space-md; + + .product-id { + font-family: 'SF Mono', Monaco, monospace; + font-size: $font-size-md; + color: #31f08b; + font-weight: 500; + } + + .product-name { + font-size: $font-size-sm; + color: $text-secondary; + } + } + + .product-meta { + display: flex; + align-items: center; + gap: $space-sm; + font-size: $font-size-xs; + color: $text-muted; + + .creator { + &::after { + content: '·'; + margin-left: $space-sm; + } + } + } + } + } + + .empty-products { + text-align: center; + padding: $space-xl; + color: $text-muted; + + p { + margin-top: $space-sm; + font-size: $font-size-sm; + } + } + + .add-product-form { + margin-top: $space-md; + padding-top: $space-md; + border-top: 1px solid $border-primary; + + .batch-add { + margin-top: $space-sm; + } + } +} + // 内联附件链接样式 .task-details { .attachment-link {