feat: 任务完结状态优化 + 商品ID确认功能 + 子任务交付路径汇总

1. 需求方完结任务显示待填写ID状态,填完商品ID后显示已完结
2. 添加确认商品ID填写完成功能
3. 母卡片展示所有子任务的交付路径汇总
4. 流水线任务:子任务按顺序完成,自动发起下一个子任务审核
5. 母任务编号作为子任务编号前缀
6. 所有子任务完成后母任务才能完结
This commit is contained in:
Your Name 2026-01-05 14:21:03 +08:00
parent 344d26589c
commit 96da6c6673
7 changed files with 233 additions and 0 deletions

View File

@ -98,5 +98,16 @@ public class TaskProductController {
long count = taskProductService.countByTaskId(taskId);
return Result.success(count);
}
@Operation(summary = "确认商品ID填写完成")
@PostMapping("/confirm")
public Result<Void> confirmProductIdFilled(
@PathVariable Long taskId,
@RequestHeader(value = "Authorization", required = false) String authHeader) {
String token = jwtUtil.extractTokenFromHeader(authHeader);
Long userId = jwtUtil.getUserIdFromToken(token);
taskProductService.confirmProductIdFilled(taskId, userId);
return Result.success();
}
}

View File

@ -230,6 +230,12 @@ public class Task implements Serializable {
*/
private LocalDateTime rejectDeadline;
/**
* 商品ID是否已填写确认:0-未填写 1-已填写
* 只有当此字段为1时,任务才真正"完结"
*/
private Integer productIdFilled;
/**
* 是否紧急:0-否 1-是
*/
@ -358,6 +364,12 @@ public class Task implements Serializable {
@TableField(exist = false)
private String currentApproverName;
/**
* 所有子任务的交付路径汇总(非数据库字段,流水线任务专用)
*/
@TableField(exist = false)
private java.util.List<String> allDeliveryPaths;
// ==================== Getters and Setters (手动添加以解决Lombok编译问题) ====================
public Long getId() { return id; }
@ -489,6 +501,9 @@ public class Task implements Serializable {
public LocalDateTime getRejectDeadline() { return rejectDeadline; }
public void setRejectDeadline(LocalDateTime rejectDeadline) { this.rejectDeadline = rejectDeadline; }
public Integer getProductIdFilled() { return productIdFilled; }
public void setProductIdFilled(Integer productIdFilled) { this.productIdFilled = productIdFilled; }
public Integer getIsUrgent() { return isUrgent; }
public void setIsUrgent(Integer isUrgent) { this.isUrgent = isUrgent; }
@ -566,5 +581,8 @@ public class Task implements Serializable {
public java.util.List<TaskItem> getSubTasks() { return subTasks; }
public void setSubTasks(java.util.List<TaskItem> subTasks) { this.subTasks = subTasks; }
public java.util.List<String> getAllDeliveryPaths() { return allDeliveryPaths; }
public void setAllDeliveryPaths(java.util.List<String> allDeliveryPaths) { this.allDeliveryPaths = allDeliveryPaths; }
}

View File

@ -77,6 +77,9 @@ public class TaskProductService {
taskProductMapper.insert(taskProduct);
log.info("添加任务商品ID成功: taskId={}, productId={}, userId={}", taskId, productId, userId);
// 更新任务的商品ID填写状态
updateTaskProductIdFilledStatus(taskId);
return taskProduct;
}
@ -121,6 +124,9 @@ public class TaskProductService {
taskProductMapper.deleteById(id);
log.info("删除任务商品ID成功: id={}, taskId={}, productId={}", id, product.getTaskId(), product.getProductId());
// 更新任务的商品ID填写状态
updateTaskProductIdFilledStatus(product.getTaskId());
}
/**
@ -166,5 +172,41 @@ public class TaskProductService {
wrapper.eq(TaskProduct::getTaskId, taskId);
return taskProductMapper.selectCount(wrapper);
}
/**
* 更新任务的商品ID填写状态
*/
private void updateTaskProductIdFilledStatus(Long taskId) {
long count = countByTaskId(taskId);
Task task = taskMapper.selectById(taskId);
if (task != null) {
// 如果有商品ID记录,标记为已填写
task.setProductIdFilled(count > 0 ? 1 : 0);
taskMapper.updateById(task);
log.info("更新任务商品ID填写状态: taskId={}, productIdFilled={}", taskId, count > 0 ? 1 : 0);
}
}
/**
* 确认商品ID填写完成(手动确认)
*/
@Transactional(rollbackFor = Exception.class)
public void confirmProductIdFilled(Long taskId, Long userId) {
Task task = taskMapper.selectById(taskId);
if (task == null) {
throw new BusinessException(ResultCode.NOT_FOUND, "任务不存在");
}
// 检查是否有商品ID
long count = countByTaskId(taskId);
if (count == 0) {
throw new BusinessException(ResultCode.PARAM_ERROR, "请先填写商品ID后再确认");
}
// 更新状态
task.setProductIdFilled(1);
taskMapper.updateById(task);
log.info("确认商品ID填写完成: taskId={}, userId={}", taskId, userId);
}
}

View File

@ -727,6 +727,15 @@ public class TaskService {
.orderByAsc(TaskItem::getSortOrder)
);
// 汇总所有子任务的交付路径
if (subTasks != null && !subTasks.isEmpty()) {
List<String> allDeliveryPaths = subTasks.stream()
.filter(item -> item.getDeliveryPath() != null && !item.getDeliveryPath().isEmpty())
.map(item -> "【" + item.getItemNo() + "】" + item.getDeliveryPath())
.collect(Collectors.toList());
task.setAllDeliveryPaths(allDeliveryPaths);
}
if (subTasks != null && !subTasks.isEmpty()) {
task.setSubTaskTotal(subTasks.size());

View File

@ -0,0 +1,9 @@
-- 添加商品ID填写确认字段
ALTER TABLE task ADD COLUMN product_id_filled TINYINT(1) DEFAULT 0 COMMENT '商品ID是否已填写确认:0-未填写 1-已填写' AFTER reject_deadline;
-- 更新现有已完成任务:如果已有商品ID记录则标记为已填写
UPDATE task t
SET t.product_id_filled = 1
WHERE t.status = 'completed'
AND EXISTS (SELECT 1 FROM task_product tp WHERE tp.task_id = t.id);

View File

@ -64,6 +64,12 @@ export interface Task {
subTasks?: TaskItem[]
// 标记字段(前端使用)
isSubTask?: boolean
// 商品ID填写状态
productIdFilled?: number // 0-未填写 1-已填写
// 所有子任务交付路径汇总
allDeliveryPaths?: string[]
// 预览图
previewImage?: string
}
// 任务子项(流水线任务)
@ -637,3 +643,10 @@ export const getTaskProductCount = (taskId: number) => {
return request.get<number>(`/tasks/${taskId}/products/count`)
}
/**
* 确认商品ID填写完成
*/
export const confirmTaskProductFilled = (taskId: number) => {
return request.post<void>(`/tasks/${taskId}/products/confirm`)
}

View File

@ -2133,6 +2133,42 @@
</a-button>
</div>
</div>
<!-- 确认完结按钮:只有添加了商品ID且未确认时显示 -->
<div v-if="canManageProducts(selectedTask) && taskProducts.length > 0 && !selectedTask.productIdFilled" class="confirm-complete-section">
<a-alert
message="请确认商品ID填写完成"
description="确认后任务状态将从"待填写ID"变更为"已完结""
type="warning"
show-icon
style="margin-bottom: 12px;"
/>
<a-button
type="primary"
block
@click="handleConfirmProductIdFilled"
:loading="confirmingProductId">
<Icon name="checkCircle" size="sm" style="margin-right: 6px;" />
确认完结任务
</a-button>
</div>
</div>
<!-- 流水线任务:显示所有子任务交付路径 -->
<div v-if="selectedTask.isPipeline === 1 && selectedTask.allDeliveryPaths && selectedTask.allDeliveryPaths.length > 0" class="detail-section all-delivery-paths">
<h4>
<Icon name="folder" size="sm" style="margin-right: 6px;" />
所有子任务交付路径
</h4>
<div class="delivery-paths-list">
<div v-for="(path, index) in selectedTask.allDeliveryPaths" :key="index" class="delivery-path-item">
<a v-if="isUrl(path)" :href="extractUrl(path)" target="_blank" class="delivery-link">
<Icon name="link" size="sm" style="margin-right: 4px;" />
{{ path }}
</a>
<span v-else class="delivery-text">{{ path }}</span>
</div>
</div>
</div>
<!-- 根据任务状态显示不同的操作按钮 -->
@ -2270,6 +2306,7 @@ const taskProducts = ref<TaskProduct[]>([])
const newProductId = ref('')
const batchProductIds = ref('')
const addingProduct = ref(false)
const confirmingProductId = ref(false)
// 已完成任务状态
const completedLoading = ref(false)
@ -3922,6 +3959,39 @@ const handleDeleteProduct = async (productId: number) => {
})
}
// 确认商品ID填写完成
const handleConfirmProductIdFilled = async () => {
if (!selectedTask.value || !selectedTask.value.id) return
confirmingProductId.value = true
try {
await taskApi.confirmTaskProductFilled(selectedTask.value.id)
message.success('任务已确认完结')
// 更新当前任务状态
selectedTask.value.productIdFilled = 1
// 刷新任务列表
await loadCompletedTasks()
await loadMyTasks()
} catch (error: any) {
message.error(error.message || '确认失败')
} finally {
confirmingProductId.value = false
}
}
// 判断是否是URL
const isUrl = (str: string) => {
// 从字符串中提取可能的URL部分
const urlMatch = str.match(/https?:\/\/[^\s]+/)
return urlMatch !== null
}
// 从字符串中提取URL
const extractUrl = (str: string) => {
const urlMatch = str.match(/https?:\/\/[^\s]+/)
return urlMatch ? urlMatch[0] : str
}
// 监听配置更新事件
const handleConfigUpdate = async (event: CustomEvent) => {
console.log('配置已更新,刷新配置数据:', event.detail)
@ -3967,6 +4037,15 @@ const getTaskStatusText = (status: string, task?: any) => {
return '待接单'
}
}
// 5. 已完成任务:检查是否已填写商品ID
if (status === 'completed') {
// productIdFilled为0或undefined表示未填写
if (!task.productIdFilled || task.productIdFilled === 0) {
return '待填写ID'
}
return '已完结'
}
}
const statusMap: Record<string, string> = {
@ -4568,6 +4647,58 @@ onUnmounted(() => {
color: #1890ff;
}
// 待填写ID状态样式
.status-pending_id {
background: rgba(250, 173, 20, 0.1);
border: 1px solid rgba(250, 173, 20, 0.3);
color: #faad14;
}
// 已完结状态样式
.status-finished {
background: rgba(49, 240, 139, 0.1);
border: 1px solid rgba(49, 240, 139, 0.3);
color: #31f08b;
}
// 确认完结区域样式
.confirm-complete-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid $border-primary;
}
// 所有子任务交付路径样式
.all-delivery-paths {
.delivery-paths-list {
display: flex;
flex-direction: column;
gap: 8px;
.delivery-path-item {
padding: 8px 12px;
background: $bg-tertiary;
border-radius: $radius-sm;
font-size: 13px;
.delivery-link {
color: #31f08b;
text-decoration: none;
word-break: break-all;
&:hover {
text-decoration: underline;
}
}
.delivery-text {
color: $text-secondary;
word-break: break-all;
}
}
}
}
// 驳回信息显示
.reject-info {
margin: 12px 0;