feat: 添加任务评分功能 - 需求方可对已完成任务评分
This commit is contained in:
parent
4bd856ea01
commit
7433656171
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消任务
|
||||
*/
|
||||
|
||||
@ -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 }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务大厅
|
||||
*/
|
||||
|
||||
@ -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(() => {
|
||||
// 设置默认选中当前月份
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否可以审核任务
|
||||
// 对于审批流程中的任务:只有当前审核人可以审核
|
||||
// 对于其他情况:只有部门负责人可以审核
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user