feat: 添加任务评分功能 - 需求方可对已完成任务评分

This commit is contained in:
Your Name 2026-01-08 14:35:55 +08:00
parent 4bd856ea01
commit 7433656171
7 changed files with 321 additions and 7 deletions

View File

@ -47,7 +47,7 @@ public class DesignerController {
@Operation(summary = "获取设计师进行中的任务列表")
@GetMapping("/{userId}/in-progress-tasks")
public Result<List<Task>> getDesignerInProgressTasks(@PathVariable Long userId) {
public Result<List<java.util.Map<String, Object>>> getDesignerInProgressTasks(@PathVariable Long userId) {
return Result.success(designerService.getDesignerInProgressTasks(userId));
}
}

View File

@ -152,6 +152,21 @@ public class TaskController {
return Result.success("取消成功");
}
@Operation(summary = "评分任务", description = "需求方对已完成任务进行评分")
@PostMapping("/{id}/rate")
public Result<String> rateTask(
@PathVariable Long id,
@RequestParam Integer rating,
@RequestParam(required = false) String comment,
@RequestHeader("Authorization") String authHeader) {
log.info("评分任务: {}, 评分: {}, 评语: {}", id, rating, comment);
String token = jwtUtil.extractTokenFromHeader(authHeader);
Long userId = jwtUtil.getUserIdFromToken(token);
taskService.rateTask(id, userId, rating, comment);
return Result.success("评分成功");
}
@Operation(summary = "获取已完成任务列表", description = "根据用户角色返回不同范围的已完成任务")
@GetMapping("/completed")
public Result<IPage<Task>> getCompletedTasks(

View File

@ -439,15 +439,61 @@ public class DesignerService {
}
/**
* 获取设计师进行中的任务列表
* 获取设计师进行中的任务列表(包括主任务和子任务)
* 返回统一格式的 Map 列表
*/
public List<Task> getDesignerInProgressTasks(Long userId) {
return taskMapper.selectList(
public List<java.util.Map<String, Object>> getDesignerInProgressTasks(Long userId) {
List<java.util.Map<String, Object>> result = new ArrayList<>();
// 1. 查询主任务
List<Task> mainTasks = taskMapper.selectList(
new LambdaQueryWrapper<Task>()
.eq(Task::getReceiverId, userId)
.eq(Task::getStatus, "in_progress")
.eq(Task::getDeleted, 0)
.orderByDesc(Task::getCreateTime)
);
for (Task task : mainTasks) {
java.util.Map<String, Object> item = new java.util.LinkedHashMap<>();
item.put("id", task.getId());
item.put("taskNo", task.getTaskNo());
item.put("title", task.getTitle());
item.put("status", task.getStatus());
item.put("progress", task.getProgress());
item.put("score", task.getScore());
item.put("deadline", task.getDeadline());
item.put("createTime", task.getCreateTime());
item.put("isSubTask", false);
item.put("type", "主任务");
result.add(item);
}
// 2. 查询子任务
List<TaskItem> subTasks = taskItemMapper.selectList(
new LambdaQueryWrapper<TaskItem>()
.eq(TaskItem::getReceiverId, userId)
.eq(TaskItem::getStatus, "in_progress")
.orderByDesc(TaskItem::getCreateTime)
);
for (TaskItem item : subTasks) {
java.util.Map<String, Object> taskItem = new java.util.LinkedHashMap<>();
taskItem.put("id", item.getId());
taskItem.put("taskNo", item.getItemNo());
taskItem.put("title", "子任务 #" + item.getGroupNo());
taskItem.put("status", item.getStatus());
taskItem.put("progress", item.getProgress());
taskItem.put("score", item.getTotalScore());
taskItem.put("deadline", item.getDeadline());
taskItem.put("createTime", item.getCreateTime());
taskItem.put("isSubTask", true);
taskItem.put("type", "子任务");
taskItem.put("mainTaskId", item.getTaskId());
result.add(taskItem);
}
return result;
}
}

View File

@ -1290,6 +1290,64 @@ public class TaskService {
}
}
/**
* 对已完成的任务进行评分(需求方)
*/
@Transactional(rollbackFor = Exception.class)
public void rateTask(Long taskId, Long userId, Integer rating, String comment) {
log.info("用户 {} 对任务 {} 评分: {}", userId, taskId, rating);
Task task = getTaskById(taskId);
// 验证权限(必须是任务发布人)
if (!userId.equals(task.getPublisherId())) {
throw new BusinessException(ResultCode.FORBIDDEN, "只有任务发布人可以评分");
}
// 验证任务状态(必须是已完成)
if (!"completed".equals(task.getStatus())) {
throw new BusinessException(ResultCode.TASK_STATUS_ERROR, "只能对已完成的任务评分");
}
// 验证评分是否已存在
if (task.getRating() != null && task.getRating() > 0) {
throw new BusinessException(ResultCode.PARAM_ERROR, "该任务已经评分过了");
}
// 验证评分值
if (rating == null || rating < 1 || rating > 5) {
throw new BusinessException(ResultCode.PARAM_ERROR, "评分必须在1-5之间");
}
// 更新评分
task.setRating(rating);
if (comment != null && !comment.trim().isEmpty()) {
task.setReviewComment(comment);
}
taskMapper.updateById(task);
// 发送通知给设计师
if (task.getReceiverId() != null) {
try {
User publisher = userService.getUserById(userId);
String publisherName = publisher != null ?
(publisher.getRealName() != null ? publisher.getRealName() : publisher.getUsername()) : "需求方";
String ratingDesc = rating == 5 ? "五星好评" : rating + "星评价";
notificationService.sendTaskNotification(
task.getReceiverId(),
taskId,
"任务获得评分",
String.format("您完成的任务【%s】已被 %s 评为 %s", task.getTitle(), publisherName, ratingDesc)
);
} catch (Exception e) {
log.error("发送任务评分通知失败", e);
}
}
log.info("任务 {} 评分成功: {} 星", taskId, rating);
}
/**
* 取消任务
*/

View File

@ -240,6 +240,15 @@ export const cancelTask = (id: number, reason: string) => {
})
}
/**
* 评分任务(需求方对已完成任务进行评分)
*/
export const rateTask = (id: number, rating: number, comment?: string) => {
return request.post<any>(`/tasks/${id}/rate`, null, {
params: { rating, comment }
})
}
/**
* 获取任务大厅
*/

View File

@ -386,6 +386,25 @@ const handleMonthChange = () => {
loadLeaderboard(activeLeaderboard.value)
}
// 加载设计师进行中的任务
const loadDesignerTasks = async (designer: DesignerStats) => {
if (designer.loadingTasks) return
designer.loadingTasks = true
try {
const response = await designerApi.getDesignerInProgressTasks(designer.userId)
if (response.code === 200) {
designer.inProgressTasks = response.data || []
designer.showTasks = true
}
} catch (error) {
console.error('加载设计师任务失败:', error)
message.error('加载任务列表失败')
} finally {
designer.loadingTasks = false
}
}
// 组件挂载
onMounted(() => {
// 设置默认选中当前月份

View File

@ -1375,6 +1375,38 @@
<span style="color: #ffffff;">完成: {{ formatDate(task.completeTime) }}</span>
</div>
</div>
<!-- 评分区域 -->
<div class="task-rating" style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #303030;">
<!-- 已评分:显示评分结果 -->
<div v-if="task.rating" style="display: flex; align-items: center; justify-content: space-between;">
<span style="color: #888; font-size: 12px;">已评分</span>
<div style="display: flex; gap: 2px;">
<Icon
v-for="i in 5"
:key="i"
name="star"
size="sm"
:color="i <= task.rating ? '#fadb14' : '#303030'"
/>
</div>
</div>
<!-- 未评分且是发布人:显示评分按钮 -->
<a-button
v-else-if="isTaskPublisher(task)"
type="primary"
size="small"
block
@click.stop="showRatingModal(task)"
>
<Icon name="star" size="sm" style="margin-right: 4px;" />
立即评分
</a-button>
<!-- 未评分且不是发布人 -->
<div v-else style="text-align: center; color: #666; font-size: 12px;">
等待发布人评分
</div>
</div>
</div>
</div>
@ -2190,15 +2222,31 @@
</a-button>
</div>
<!-- 已完成任务:显示评分(如果还没评分) -->
<!-- 已完成任务:显示评分(如果还没评分,且是发布人) -->
<a-button
v-else-if="selectedTask.status === 'completed' && !selectedTask.rating && canApproveTask(selectedTask)"
v-else-if="selectedTask.status === 'completed' && !selectedTask.rating && isTaskPublisher(selectedTask)"
type="primary"
size="large"
block>
block
@click="showRatingModal(selectedTask)">
<Icon name="star" size="sm" style="margin-right: 6px;" />
评分
</a-button>
<!-- 已评分任务:显示评分结果 -->
<div v-else-if="selectedTask.status === 'completed' && selectedTask.rating" class="rating-result" style="text-align: center; padding: 12px 0;">
<div style="color: #888; margin-bottom: 8px;">已评分</div>
<div style="display: flex; justify-content: center; gap: 4px;">
<Icon
v-for="i in 5"
:key="i"
name="star"
size="lg"
:color="i <= (selectedTask.rating || 0) ? '#fadb14' : '#303030'"
/>
</div>
</div>
<!-- 其他状态:仅显示关闭按钮 -->
<a-button
v-else
@ -2210,6 +2258,77 @@
</div>
</div>
</a-drawer>
<!-- 评分弹窗 -->
<a-modal
v-model:open="ratingModalVisible"
title="任务评分"
:width="420"
:footer="null"
centered
class="rating-modal"
>
<div class="rating-content" style="padding: 20px 0;">
<div class="rating-task-info" style="margin-bottom: 24px; padding: 16px; background: #1a1a1a; border-radius: 8px;">
<div style="color: #888; font-size: 12px; margin-bottom: 4px;">任务名称</div>
<div style="color: #fff; font-weight: 500;">{{ ratingTask?.title }}</div>
</div>
<div class="rating-stars" style="text-align: center; margin-bottom: 24px;">
<div style="color: #888; margin-bottom: 12px;">请为本次任务评分</div>
<div style="display: flex; justify-content: center; gap: 12px;">
<div
v-for="i in 5"
:key="i"
style="cursor: pointer; transition: transform 0.2s;"
@click="ratingValue = i"
@mouseenter="(e: MouseEvent) => (e.target as HTMLElement).style.transform = 'scale(1.2)'"
@mouseleave="(e: MouseEvent) => (e.target as HTMLElement).style.transform = 'scale(1)'"
>
<Icon
name="star"
size="xl"
:color="i <= ratingValue ? '#fadb14' : '#303030'"
/>
</div>
</div>
<div style="color: #fadb14; margin-top: 8px; font-size: 14px;">
{{ ratingValue === 5 ? '非常满意' : ratingValue === 4 ? '比较满意' : ratingValue === 3 ? '一般' : ratingValue === 2 ? '不太满意' : '非常不满意' }}
</div>
</div>
<div class="rating-comment" style="margin-bottom: 24px;">
<div style="color: #888; margin-bottom: 8px;">评价留言(选填)</div>
<a-textarea
v-model:value="ratingComment"
placeholder="请输入对设计师的评价..."
:rows="3"
:maxlength="200"
show-count
style="background: #1a1a1a; border-color: #303030; color: #fff;"
/>
</div>
<div class="rating-actions" style="display: flex; gap: 12px;">
<a-button
style="flex: 1;"
size="large"
@click="ratingModalVisible = false"
>
取消
</a-button>
<a-button
type="primary"
style="flex: 1;"
size="large"
:loading="ratingLoading"
@click="submitRating"
>
提交评分
</a-button>
</div>
</div>
</a-modal>
</div>
</DefaultLayout>
</template>
@ -2277,6 +2396,13 @@ const total = ref(0)
const detailDrawerVisible = ref(false)
const selectedTask = ref<TaskOrSubTask | null>(null)
// 评分弹窗
const ratingModalVisible = ref(false)
const ratingTask = ref<TaskOrSubTask | null>(null)
const ratingValue = ref(5)
const ratingComment = ref('')
const ratingLoading = ref(false)
// 商品ID管理
const taskProducts = ref<TaskProduct[]>([])
const newProductId = ref('')
@ -4029,6 +4155,47 @@ const canCompleteTask = (task: TaskOrSubTask) => {
return task.receiverId === userInfo.id && (task.progress || 0) >= 100
}
// 判断是否是任务发布人
const isTaskPublisher = (task: TaskOrSubTask) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
return task.publisherId === userInfo.id
}
// 显示评分弹窗
const showRatingModal = (task: TaskOrSubTask) => {
ratingTask.value = task
ratingValue.value = 5
ratingComment.value = ''
ratingModalVisible.value = true
}
// 提交评分
const submitRating = async () => {
if (!ratingTask.value) return
ratingLoading.value = true
try {
await taskApi.rateTask(ratingTask.value.id!, ratingValue.value, ratingComment.value || undefined)
message.success('评分成功!')
ratingModalVisible.value = false
// 更新当前任务的评分
if (selectedTask.value && selectedTask.value.id === ratingTask.value.id) {
selectedTask.value.rating = ratingValue.value
}
// 刷新已完成任务列表
if (activeTab.value === 'completed') {
loadCompletedTasks()
}
} catch (error: any) {
console.error('评分失败:', error)
message.error(error.msg || error.message || '评分失败')
} finally {
ratingLoading.value = false
}
}
// 判断是否可以审核任务
// 对于审批流程中的任务:只有当前审核人可以审核
// 对于其他情况:只有部门负责人可以审核