feat: 添加任务商品ID管理功能

This commit is contained in:
Your Name 2026-01-04 11:02:45 +08:00
parent 67a2ce7564
commit 709cea104b
7 changed files with 691 additions and 3 deletions

View File

@ -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<List<TaskProduct>> getProducts(@PathVariable Long taskId) {
List<TaskProduct> products = taskProductService.getProductsByTaskId(taskId);
return Result.success(products);
}
@Operation(summary = "添加商品ID")
@PostMapping
public Result<TaskProduct> addProduct(
@PathVariable Long taskId,
@RequestBody Map<String, String> 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<Integer> addProducts(
@PathVariable Long taskId,
@RequestBody Map<String, List<String>> request,
@RequestHeader(value = "Authorization", required = false) String authHeader) {
Long userId = jwtUtil.getUserIdFromHeader(authHeader);
List<String> productIds = request.get("productIds");
int count = taskProductService.addProducts(taskId, productIds, userId);
return Result.success(count);
}
@Operation(summary = "更新商品ID")
@PutMapping("/{id}")
public Result<TaskProduct> updateProduct(
@PathVariable Long taskId,
@PathVariable Long id,
@RequestBody Map<String, String> 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<Void> 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<Long> getProductCount(@PathVariable Long taskId) {
long count = taskProductService.countByTaskId(taskId);
return Result.success(count);
}
}

View File

@ -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;
}

View File

@ -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<TaskProduct> {
/**
* 根据任务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<TaskProduct> selectByTaskIdWithCreator(@Param("taskId") Long taskId);
}

View File

@ -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<TaskProduct> 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<TaskProduct> 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<String> 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<TaskProduct> 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<TaskProduct> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TaskProduct::getTaskId, taskId);
return taskProductMapper.selectCount(wrapper);
}
}

View File

@ -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关联表';

View File

@ -579,3 +579,61 @@ export const rejectTaskGroup = (taskId: number, groupNo: number, reason: string)
return request.post<any>('/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<TaskProduct[]>(`/tasks/${taskId}/products`)
}
/**
* 添加商品ID
*/
export const addTaskProduct = (taskId: number, data: { productId: string; productName?: string; remark?: string }) => {
return request.post<TaskProduct>(`/tasks/${taskId}/products`, data)
}
/**
* 批量添加商品ID
*/
export const addTaskProductsBatch = (taskId: number, productIds: string[]) => {
return request.post<number>(`/tasks/${taskId}/products/batch`, { productIds })
}
/**
* 更新商品ID
*/
export const updateTaskProduct = (taskId: number, id: number, data: { productId?: string; productName?: string; remark?: string }) => {
return request.put<TaskProduct>(`/tasks/${taskId}/products/${id}`, data)
}
/**
* 删除商品ID
*/
export const deleteTaskProduct = (taskId: number, id: number) => {
return request.delete<void>(`/tasks/${taskId}/products/${id}`)
}
/**
* 获取商品ID数量
*/
export const getTaskProductCount = (taskId: number) => {
return request.get<number>(`/tasks/${taskId}/products/count`)
}

View File

@ -1992,6 +1992,72 @@
</div>
</div>
<!-- 商品ID管理(已完成任务显示) -->
<div v-if="selectedTask.status === 'completed'" class="detail-section product-section">
<h4>
<Icon name="tag" size="sm" style="margin-right: 6px;" />
关联商品ID
<span class="product-count">({{ taskProducts.length }})</span>
</h4>
<!-- 商品ID列表 -->
<div v-if="taskProducts.length > 0" class="product-list">
<div v-for="product in taskProducts" :key="product.id" class="product-item">
<div class="product-info">
<span class="product-id">{{ product.productId }}</span>
<span v-if="product.productName" class="product-name">{{ product.productName }}</span>
</div>
<div class="product-meta">
<span class="creator">{{ product.creatorName }}</span>
<span class="time">{{ formatTime(product.createTime) }}</span>
</div>
<a-button
v-if="canManageProducts(selectedTask)"
type="text"
danger
size="small"
@click="handleDeleteProduct(product.id)">
<Icon name="trash" size="sm" />
</a-button>
</div>
</div>
<div v-else class="empty-products">
<Icon name="inbox" size="lg" style="opacity: 0.3;" />
<p>暂无关联商品ID</p>
</div>
<!-- 添加商品ID -->
<div v-if="canManageProducts(selectedTask)" class="add-product-form">
<a-input-group compact>
<a-input
v-model:value="newProductId"
placeholder="输入商品ID(数字)"
style="width: calc(100% - 80px);"
@pressEnter="handleAddProduct"
/>
<a-button type="primary" @click="handleAddProduct" :loading="addingProduct">
添加
</a-button>
</a-input-group>
<div class="batch-add">
<a-textarea
v-model:value="batchProductIds"
placeholder="批量添加:一行一个商品ID,或用逗号分隔"
:rows="2"
style="margin-top: 8px;"
/>
<a-button
type="default"
size="small"
style="margin-top: 8px;"
@click="handleBatchAddProducts"
:loading="addingProduct">
批量添加
</a-button>
</div>
</div>
</div>
<!-- 根据任务状态显示不同的操作按钮 -->
<div class="detail-actions">
<!-- 可接任务:显示抢单 -->
@ -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<TaskOrSubTask | null>(null)
// 商品ID管理
const taskProducts = ref<TaskProduct[]>([])
const newProductId = ref('')
const batchProductIds = ref('')
const addingProduct = ref(false)
// 已完成任务状态
const completedLoading = ref(false)
const completedTaskList = ref<Task[]>([])
@ -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 {