feat: 在一级需求部门统计页面增加用户维度的数据分析功能
This commit is contained in:
parent
4a74768a32
commit
348e7ed71e
@ -153,5 +153,15 @@ public class AnalyticsController {
|
|||||||
log.info("获取品牌任务分布: startTime={}, endTime={}", startTime, endTime);
|
log.info("获取品牌任务分布: startTime={}, endTime={}", startTime, endTime);
|
||||||
return Result.success(analyticsService.getPrimaryTaskBrandDistribution(startTime, endTime));
|
return Result.success(analyticsService.getPrimaryTaskBrandDistribution(startTime, endTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取一级需求部门用户排名(用户维度统计)")
|
||||||
|
@GetMapping("/primary-dept/user-ranking")
|
||||||
|
public Result<List<PrimaryDeptUserRankingVO>> getPrimaryDeptUserRanking(
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
|
||||||
|
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime
|
||||||
|
) {
|
||||||
|
log.info("获取一级需求部门用户排名: startTime={}, endTime={}", startTime, endTime);
|
||||||
|
return Result.success(analyticsService.getPrimaryDeptUserRanking(startTime, endTime));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2695,5 +2695,135 @@ public class AnalyticsService {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一级需求部门用户排名(用户维度统计)
|
||||||
|
*/
|
||||||
|
public List<PrimaryDeptUserRankingVO> getPrimaryDeptUserRanking(LocalDateTime startTime, LocalDateTime endTime) {
|
||||||
|
log.info("获取一级需求部门用户排名: startTime={}, endTime={}", startTime, endTime);
|
||||||
|
|
||||||
|
List<PrimaryDeptUserRankingVO> result = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
// 1. 查询时间范围内的一级需求任务
|
||||||
|
LambdaQueryWrapper<Task> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
queryWrapper.eq(Task::getIsPrimaryTask, 1)
|
||||||
|
.ge(Task::getCreateTime, startTime)
|
||||||
|
.le(Task::getCreateTime, endTime)
|
||||||
|
.eq(Task::getDeleted, 0)
|
||||||
|
.isNotNull(Task::getPrimaryPublisherId);
|
||||||
|
|
||||||
|
List<Task> allTasks = taskMapper.selectList(queryWrapper);
|
||||||
|
|
||||||
|
// 2. 按用户分组
|
||||||
|
java.util.Map<Long, List<Task>> tasksByUser = allTasks.stream()
|
||||||
|
.filter(t -> t.getPrimaryPublisherId() != null)
|
||||||
|
.collect(java.util.stream.Collectors.groupingBy(Task::getPrimaryPublisherId));
|
||||||
|
|
||||||
|
// 3. 为每个用户创建统计数据
|
||||||
|
for (java.util.Map.Entry<Long, List<Task>> entry : tasksByUser.entrySet()) {
|
||||||
|
Long userId = entry.getKey();
|
||||||
|
List<Task> userTasks = entry.getValue();
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
User user = userMapper.selectById(userId);
|
||||||
|
if (user == null || user.getDeleted() == 1 || user.getStatus() == 0) {
|
||||||
|
continue; // 跳过已删除或禁用的用户
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimaryDeptUserRankingVO vo = new PrimaryDeptUserRankingVO();
|
||||||
|
vo.setUserId(userId);
|
||||||
|
vo.setUserName(user.getRealName() != null && !user.getRealName().isEmpty()
|
||||||
|
? user.getRealName() : user.getUsername());
|
||||||
|
|
||||||
|
// 获取用户部门信息
|
||||||
|
if (user.getDeptId() != null) {
|
||||||
|
Department userDept = departmentMapper.selectById(user.getDeptId());
|
||||||
|
if (userDept != null) {
|
||||||
|
vo.setDeptId(userDept.getId());
|
||||||
|
vo.setDeptName(userDept.getDeptName());
|
||||||
|
|
||||||
|
// 获取一级部门和二级部门名称(向上查找部门层级)
|
||||||
|
List<Department> hierarchy = new java.util.ArrayList<>();
|
||||||
|
Department current = userDept;
|
||||||
|
while (current != null && current.getParentId() != null && current.getParentId() > 0) {
|
||||||
|
Department parent = departmentMapper.selectById(current.getParentId());
|
||||||
|
if (parent != null) {
|
||||||
|
hierarchy.add(0, parent); // 插入到列表开头
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hierarchy 现在从根部门到当前部门的父部门
|
||||||
|
// 一级部门是 hierarchy[0],二级部门是 hierarchy[1](如果存在)
|
||||||
|
if (hierarchy.size() >= 1) {
|
||||||
|
vo.setFirstLevelDeptName(hierarchy.get(0).getDeptName());
|
||||||
|
}
|
||||||
|
if (hierarchy.size() >= 2) {
|
||||||
|
vo.setSecondLevelDeptName(hierarchy.get(1).getDeptName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vo.setTaskCount(userTasks.size());
|
||||||
|
|
||||||
|
// 统计审核通过数
|
||||||
|
long approvedCount = userTasks.stream()
|
||||||
|
.filter(t -> !"rejected_by_demand".equals(t.getApprovalStatus()) && !"draft".equals(t.getStatus()))
|
||||||
|
.count();
|
||||||
|
vo.setApprovedCount((int) approvedCount);
|
||||||
|
|
||||||
|
// 统计审核驳回数
|
||||||
|
long rejectedCount = userTasks.stream()
|
||||||
|
.filter(t -> "rejected_by_demand".equals(t.getApprovalStatus()))
|
||||||
|
.count();
|
||||||
|
vo.setRejectedCount((int) rejectedCount);
|
||||||
|
|
||||||
|
// 计算审核通过率
|
||||||
|
if (userTasks.size() > 0) {
|
||||||
|
vo.setApprovalRate(BigDecimal.valueOf(approvedCount * 100.0 / userTasks.size())
|
||||||
|
.setScale(2, RoundingMode.HALF_UP));
|
||||||
|
} else {
|
||||||
|
vo.setApprovalRate(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算消耗积分
|
||||||
|
BigDecimal consumedScore = userTasks.stream()
|
||||||
|
.filter(t -> t.getScore() != null)
|
||||||
|
.map(Task::getScore)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
vo.setConsumedScore(consumedScore);
|
||||||
|
|
||||||
|
// 统计已完成任务数
|
||||||
|
long completedCount = userTasks.stream()
|
||||||
|
.filter(t -> "completed".equals(t.getStatus()))
|
||||||
|
.count();
|
||||||
|
vo.setCompletedCount((int) completedCount);
|
||||||
|
|
||||||
|
// 统计进行中任务数
|
||||||
|
long inProgressCount = userTasks.stream()
|
||||||
|
.filter(t -> "in_progress".equals(t.getStatus()))
|
||||||
|
.count();
|
||||||
|
vo.setInProgressCount((int) inProgressCount);
|
||||||
|
|
||||||
|
// 统计待审核任务数
|
||||||
|
long pendingCount = userTasks.stream()
|
||||||
|
.filter(t -> "pending_primary_approval".equals(t.getApprovalStatus()) ||
|
||||||
|
"pending_detail_completion".equals(t.getApprovalStatus()))
|
||||||
|
.count();
|
||||||
|
vo.setPendingCount((int) pendingCount);
|
||||||
|
|
||||||
|
result.add(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 按任务数量降序排序
|
||||||
|
result.sort((a, b) -> Integer.compare(b.getTaskCount(), a.getTaskCount()));
|
||||||
|
|
||||||
|
// 5. 设置排名
|
||||||
|
for (int i = 0; i < result.size(); i++) {
|
||||||
|
result.get(i).setRank(i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,131 @@
|
|||||||
|
package com.designhub.vo;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一级需求部门用户排名VO(用户维度统计)
|
||||||
|
*/
|
||||||
|
public class PrimaryDeptUserRankingVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排名
|
||||||
|
*/
|
||||||
|
private Integer rank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户姓名
|
||||||
|
*/
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户部门ID
|
||||||
|
*/
|
||||||
|
private Long deptId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户部门名称
|
||||||
|
*/
|
||||||
|
private String deptName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一级部门名称
|
||||||
|
*/
|
||||||
|
private String firstLevelDeptName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二级部门名称
|
||||||
|
*/
|
||||||
|
private String secondLevelDeptName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起任务数
|
||||||
|
*/
|
||||||
|
private Integer taskCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核通过数
|
||||||
|
*/
|
||||||
|
private Integer approvedCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核驳回数
|
||||||
|
*/
|
||||||
|
private Integer rejectedCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核通过率(百分比)
|
||||||
|
*/
|
||||||
|
private BigDecimal approvalRate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消耗积分
|
||||||
|
*/
|
||||||
|
private BigDecimal consumedScore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已完成任务数
|
||||||
|
*/
|
||||||
|
private Integer completedCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进行中任务数
|
||||||
|
*/
|
||||||
|
private Integer inProgressCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待审核任务数
|
||||||
|
*/
|
||||||
|
private Integer pendingCount;
|
||||||
|
|
||||||
|
// ==================== Getters and Setters ====================
|
||||||
|
|
||||||
|
public Integer getRank() { return rank; }
|
||||||
|
public void setRank(Integer rank) { this.rank = rank; }
|
||||||
|
|
||||||
|
public Long getUserId() { return userId; }
|
||||||
|
public void setUserId(Long userId) { this.userId = userId; }
|
||||||
|
|
||||||
|
public String getUserName() { return userName; }
|
||||||
|
public void setUserName(String userName) { this.userName = userName; }
|
||||||
|
|
||||||
|
public Long getDeptId() { return deptId; }
|
||||||
|
public void setDeptId(Long deptId) { this.deptId = deptId; }
|
||||||
|
|
||||||
|
public String getDeptName() { return deptName; }
|
||||||
|
public void setDeptName(String deptName) { this.deptName = deptName; }
|
||||||
|
|
||||||
|
public String getFirstLevelDeptName() { return firstLevelDeptName; }
|
||||||
|
public void setFirstLevelDeptName(String firstLevelDeptName) { this.firstLevelDeptName = firstLevelDeptName; }
|
||||||
|
|
||||||
|
public String getSecondLevelDeptName() { return secondLevelDeptName; }
|
||||||
|
public void setSecondLevelDeptName(String secondLevelDeptName) { this.secondLevelDeptName = secondLevelDeptName; }
|
||||||
|
|
||||||
|
public Integer getTaskCount() { return taskCount; }
|
||||||
|
public void setTaskCount(Integer taskCount) { this.taskCount = taskCount; }
|
||||||
|
|
||||||
|
public Integer getApprovedCount() { return approvedCount; }
|
||||||
|
public void setApprovedCount(Integer approvedCount) { this.approvedCount = approvedCount; }
|
||||||
|
|
||||||
|
public Integer getRejectedCount() { return rejectedCount; }
|
||||||
|
public void setRejectedCount(Integer rejectedCount) { this.rejectedCount = rejectedCount; }
|
||||||
|
|
||||||
|
public BigDecimal getApprovalRate() { return approvalRate; }
|
||||||
|
public void setApprovalRate(BigDecimal approvalRate) { this.approvalRate = approvalRate; }
|
||||||
|
|
||||||
|
public BigDecimal getConsumedScore() { return consumedScore; }
|
||||||
|
public void setConsumedScore(BigDecimal consumedScore) { this.consumedScore = consumedScore; }
|
||||||
|
|
||||||
|
public Integer getCompletedCount() { return completedCount; }
|
||||||
|
public void setCompletedCount(Integer completedCount) { this.completedCount = completedCount; }
|
||||||
|
|
||||||
|
public Integer getInProgressCount() { return inProgressCount; }
|
||||||
|
public void setInProgressCount(Integer inProgressCount) { this.inProgressCount = inProgressCount; }
|
||||||
|
|
||||||
|
public Integer getPendingCount() { return pendingCount; }
|
||||||
|
public void setPendingCount(Integer pendingCount) { this.pendingCount = pendingCount; }
|
||||||
|
}
|
||||||
@ -555,6 +555,25 @@ export interface BrandTaskDistribution {
|
|||||||
completedCount: number;
|
completedCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 一级需求部门用户排名(用户维度统计)
|
||||||
|
export interface PrimaryDeptUserRanking {
|
||||||
|
rank: number;
|
||||||
|
userId: number;
|
||||||
|
userName: string;
|
||||||
|
deptId?: number;
|
||||||
|
deptName?: string;
|
||||||
|
firstLevelDeptName?: string;
|
||||||
|
secondLevelDeptName?: string;
|
||||||
|
taskCount: number;
|
||||||
|
approvedCount: number;
|
||||||
|
rejectedCount: number;
|
||||||
|
approvalRate: number;
|
||||||
|
consumedScore: number;
|
||||||
|
completedCount: number;
|
||||||
|
inProgressCount: number;
|
||||||
|
pendingCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取一级需求部门统计数据
|
// 获取一级需求部门统计数据
|
||||||
export const getPrimaryDeptStatistics = async (
|
export const getPrimaryDeptStatistics = async (
|
||||||
startTime: string,
|
startTime: string,
|
||||||
@ -612,3 +631,17 @@ export const getPrimaryTaskBrandDistribution = async (
|
|||||||
}
|
}
|
||||||
return response as unknown as BrandTaskDistribution[];
|
return response as unknown as BrandTaskDistribution[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取一级需求部门用户排名(用户维度统计)
|
||||||
|
export const getPrimaryDeptUserRanking = async (
|
||||||
|
startTime: string,
|
||||||
|
endTime: string
|
||||||
|
): Promise<PrimaryDeptUserRanking[]> => {
|
||||||
|
const params = { startTime, endTime };
|
||||||
|
|
||||||
|
const response = await request.get('/analytics/primary-dept/user-ranking', { params });
|
||||||
|
if (response && typeof response === 'object' && 'data' in response) {
|
||||||
|
return response.data as PrimaryDeptUserRanking[];
|
||||||
|
}
|
||||||
|
return response as unknown as PrimaryDeptUserRanking[];
|
||||||
|
};
|
||||||
|
|||||||
@ -670,6 +670,37 @@
|
|||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户维度统计 -->
|
||||||
|
<div class="table-card full-width" style="margin-top: 16px;">
|
||||||
|
<div class="table-header">
|
||||||
|
<h3 class="chart-title">用户维度统计(一级需求)</h3>
|
||||||
|
</div>
|
||||||
|
<a-table
|
||||||
|
:columns="primaryDeptUserColumns"
|
||||||
|
:data-source="primaryDeptUserRanking"
|
||||||
|
:pagination="{ pageSize: 10 }"
|
||||||
|
row-key="userId"
|
||||||
|
size="middle"
|
||||||
|
class="dark-table"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'rank'">
|
||||||
|
<span :class="['rank-badge', record.rank <= 3 ? 'top-' + record.rank : '']">
|
||||||
|
{{ record.rank }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'approvalRate'">
|
||||||
|
<span :class="record.approvalRate >= 90 ? 'text-success' : record.approvalRate >= 70 ? 'text-warning' : 'text-danger'">
|
||||||
|
{{ formatScore(record.approvalRate) }}%
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'consumedScore'">
|
||||||
|
<span class="score-value">{{ formatScore(record.consumedScore) }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
@ -769,6 +800,7 @@ import {
|
|||||||
getPrimaryDeptStatistics,
|
getPrimaryDeptStatistics,
|
||||||
getPrimaryDeptRanking,
|
getPrimaryDeptRanking,
|
||||||
getPrimaryTaskBrandDistribution,
|
getPrimaryTaskBrandDistribution,
|
||||||
|
getPrimaryDeptUserRanking,
|
||||||
type DemandStatistics,
|
type DemandStatistics,
|
||||||
type DesignerPerformance,
|
type DesignerPerformance,
|
||||||
type ProcessEfficiency,
|
type ProcessEfficiency,
|
||||||
@ -779,7 +811,8 @@ import {
|
|||||||
type DimensionAnalysis,
|
type DimensionAnalysis,
|
||||||
type PrimaryDeptStatistics,
|
type PrimaryDeptStatistics,
|
||||||
type PrimaryDeptRanking,
|
type PrimaryDeptRanking,
|
||||||
type BrandTaskDistribution
|
type BrandTaskDistribution,
|
||||||
|
type PrimaryDeptUserRanking
|
||||||
} from '@/api/analytics'
|
} from '@/api/analytics'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
@ -901,6 +934,7 @@ const primaryDeptStats = reactive<PrimaryDeptStatistics>({
|
|||||||
})
|
})
|
||||||
const primaryDeptRanking = ref<PrimaryDeptRanking[]>([])
|
const primaryDeptRanking = ref<PrimaryDeptRanking[]>([])
|
||||||
const brandDistribution = ref<BrandTaskDistribution[]>([])
|
const brandDistribution = ref<BrandTaskDistribution[]>([])
|
||||||
|
const primaryDeptUserRanking = ref<PrimaryDeptUserRanking[]>([])
|
||||||
|
|
||||||
// 部门筛选
|
// 部门筛选
|
||||||
const selectedDeptId = ref<number | undefined>(undefined)
|
const selectedDeptId = ref<number | undefined>(undefined)
|
||||||
@ -1152,6 +1186,23 @@ const brandDistributionColumns = [
|
|||||||
{ title: '已完成', dataIndex: 'completedCount', key: 'completedCount', align: 'center', width: 80 }
|
{ title: '已完成', dataIndex: 'completedCount', key: 'completedCount', align: 'center', width: 80 }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 一级需求部门用户排名列(用户维度统计)
|
||||||
|
const primaryDeptUserColumns = [
|
||||||
|
{ title: '排名', key: 'rank', dataIndex: 'rank', align: 'center', width: 60 },
|
||||||
|
{ title: '用户姓名', dataIndex: 'userName', key: 'userName', ellipsis: true, width: 120 },
|
||||||
|
{ title: '部门名称', dataIndex: 'deptName', key: 'deptName', ellipsis: true },
|
||||||
|
{ title: '一级部门', dataIndex: 'firstLevelDeptName', key: 'firstLevelDeptName', ellipsis: true, width: 120 },
|
||||||
|
{ title: '二级部门', dataIndex: 'secondLevelDeptName', key: 'secondLevelDeptName', ellipsis: true, width: 120 },
|
||||||
|
{ title: '发起任务数', dataIndex: 'taskCount', key: 'taskCount', align: 'center', width: 100 },
|
||||||
|
{ title: '审核通过数', dataIndex: 'approvedCount', key: 'approvedCount', align: 'center', width: 100 },
|
||||||
|
{ title: '审核驳回数', dataIndex: 'rejectedCount', key: 'rejectedCount', align: 'center', width: 100 },
|
||||||
|
{ title: '审核通过率', key: 'approvalRate', align: 'center', width: 100 },
|
||||||
|
{ title: '消耗积分', key: 'consumedScore', align: 'center', width: 100 },
|
||||||
|
{ title: '已完成', dataIndex: 'completedCount', key: 'completedCount', align: 'center', width: 80 },
|
||||||
|
{ title: '进行中', dataIndex: 'inProgressCount', key: 'inProgressCount', align: 'center', width: 80 },
|
||||||
|
{ title: '待审核', dataIndex: 'pendingCount', key: 'pendingCount', align: 'center', width: 80 }
|
||||||
|
]
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const handleMonthChange = (value: string | null) => {
|
const handleMonthChange = (value: string | null) => {
|
||||||
selectedMonth.value = value
|
selectedMonth.value = value
|
||||||
@ -1229,14 +1280,16 @@ const loadAllData = async () => {
|
|||||||
|
|
||||||
// 加载一级需求部门统计数据
|
// 加载一级需求部门统计数据
|
||||||
try {
|
try {
|
||||||
const [primaryStats, primaryRanking, brandDist] = await Promise.all([
|
const [primaryStats, primaryRanking, brandDist, userRanking] = await Promise.all([
|
||||||
getPrimaryDeptStatistics(startTime, endTime),
|
getPrimaryDeptStatistics(startTime, endTime),
|
||||||
getPrimaryDeptRanking(startTime, endTime),
|
getPrimaryDeptRanking(startTime, endTime),
|
||||||
getPrimaryTaskBrandDistribution(startTime, endTime)
|
getPrimaryTaskBrandDistribution(startTime, endTime),
|
||||||
|
getPrimaryDeptUserRanking(startTime, endTime)
|
||||||
])
|
])
|
||||||
Object.assign(primaryDeptStats, primaryStats)
|
Object.assign(primaryDeptStats, primaryStats)
|
||||||
primaryDeptRanking.value = primaryRanking
|
primaryDeptRanking.value = primaryRanking
|
||||||
brandDistribution.value = brandDist
|
brandDistribution.value = brandDist
|
||||||
|
primaryDeptUserRanking.value = userRanking
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('加载一级需求部门统计失败:', e)
|
console.warn('加载一级需求部门统计失败:', e)
|
||||||
}
|
}
|
||||||
|
|||||||
356
更新通知-2026-01-22.md
Normal file
356
更新通知-2026-01-22.md
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
# 系统更新通知
|
||||||
|
|
||||||
|
**更新日期:** 2026年1月22日
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 更新概览
|
||||||
|
|
||||||
|
本次更新主要包含以下内容:
|
||||||
|
- ✨ 扩展任务搜索功能,支持搜索任务卡片的所有信息
|
||||||
|
- 🔒 一级需求部门发起任务必须上传预览图
|
||||||
|
- 🎨 任务卡片增加一级需求人显示,优化长名字显示
|
||||||
|
- 🐛 修复流水线任务异常扣分问题
|
||||||
|
- 🐛 修复需求部门看不到一级需求任务的问题
|
||||||
|
- 🐛 修复需求部门负责人无法驳回任务的问题
|
||||||
|
- 🐛 修复筛选功能搜索报错问题
|
||||||
|
- 🐛 修复 SQL 查询错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 新增功能
|
||||||
|
|
||||||
|
### 1. 扩展任务搜索功能
|
||||||
|
|
||||||
|
**功能描述:** 任务搜索功能现在支持搜索任务卡片的所有信息,不再仅限于任务标题和编号。
|
||||||
|
|
||||||
|
**搜索范围包括:**
|
||||||
|
- ✅ 任务标题 (`title`)
|
||||||
|
- ✅ 任务编号 (`task_no`)
|
||||||
|
- ✅ SPU信息 (`spu_info`)
|
||||||
|
- ✅ 备注信息 (`remark`)
|
||||||
|
- ✅ 产品条码 (`product_barcodes`)
|
||||||
|
- ✅ 发布人姓名 (`publisher_name`)
|
||||||
|
- ✅ 发布人部门名称 (`publisher_dept_name`)
|
||||||
|
- ✅ 品牌名称 (`brand_name`)
|
||||||
|
- ✅ 类目名称 (`category_name`)
|
||||||
|
- ✅ 平台名称 (`platform_name`)
|
||||||
|
- ✅ 任务类型名称 (`task_type_name`)
|
||||||
|
- ✅ 一级需求人姓名 (`primary_publisher_name`)
|
||||||
|
- ✅ 一级需求部门名称 (`primary_publisher_dept_name`)
|
||||||
|
- ✅ 需求部门发起人姓名 (`detail_completer_name`)
|
||||||
|
- ✅ 接收人姓名 (`receiver_name`)
|
||||||
|
|
||||||
|
**适用场景:**
|
||||||
|
- 任务大厅搜索
|
||||||
|
- 进行中任务搜索
|
||||||
|
- 已完成任务搜索
|
||||||
|
- 子任务搜索
|
||||||
|
|
||||||
|
**使用示例:**
|
||||||
|
- 搜索品牌名称(如"卓禾")可以找到该品牌的所有任务
|
||||||
|
- 搜索发布人姓名可以找到该用户发布的所有任务
|
||||||
|
- 搜索一级需求人姓名可以找到该用户发起的一级需求任务
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 一级需求部门必须上传预览图
|
||||||
|
|
||||||
|
**功能描述:** 一级需求部门用户发起任务时,系统强制要求上传预览图。
|
||||||
|
|
||||||
|
**实现方式:**
|
||||||
|
- 前端验证:表单验证规则中添加预览图必填验证
|
||||||
|
- 后端验证:创建任务时检查预览图是否上传
|
||||||
|
- 用户提示:预览图上传区域显示明确的必填提示
|
||||||
|
|
||||||
|
**影响范围:**
|
||||||
|
- 一级需求部门用户创建普通任务
|
||||||
|
- 一级需求部门用户创建流水线任务
|
||||||
|
|
||||||
|
**用户体验:**
|
||||||
|
- 如果未上传预览图,提交时会显示错误提示:"一级需求部门发起任务必须上传预览图"
|
||||||
|
- 预览图上传区域会显示红色警告图标和提示文字
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 任务卡片增加一级需求人显示
|
||||||
|
|
||||||
|
**功能描述:** 对于一级需求部门发起的任务,任务卡片现在会显示一级需求人和需求部门发起人的信息,让任务来源更加清晰。
|
||||||
|
|
||||||
|
**显示内容:**
|
||||||
|
- ✅ **一级需求人**:显示一级需求部门发起人的姓名和部门名称(橙色标识)
|
||||||
|
- ✅ **需求部门发起人**:显示补充任务明细的需求部门成员姓名(绿色标识)
|
||||||
|
- ✅ **发布人**:显示任务的发布人姓名(默认绿色标识)
|
||||||
|
|
||||||
|
**显示逻辑:**
|
||||||
|
- 如果是一级需求任务(`isPrimaryTask=1`):
|
||||||
|
- 显示"一级需求人"(如果存在)
|
||||||
|
- 显示"需求部门发起人"(如果存在,即已补充明细)
|
||||||
|
- 否则显示"发布人"
|
||||||
|
- 如果是普通任务:
|
||||||
|
- 显示"发布人"
|
||||||
|
|
||||||
|
**优化内容:**
|
||||||
|
- 一级需求人名字超过 8 个字符时显示省略号
|
||||||
|
- 需求部门发起人名字超过 8 个字符时显示省略号
|
||||||
|
- 发布人名字超过 8 个字符时显示省略号
|
||||||
|
- 鼠标悬停时通过 Tooltip 显示完整信息(包含部门名称)
|
||||||
|
|
||||||
|
**显示效果:**
|
||||||
|
- 一级需求任务示例:
|
||||||
|
- `一级需求人: 张三... (运营中心...)`(橙色)
|
||||||
|
- `需求部门发起人: 李四...`(绿色)
|
||||||
|
- 悬停提示:`一级需求人: 张三丰 (运营中心A-事业二部)`
|
||||||
|
|
||||||
|
**适用位置:**
|
||||||
|
- 任务大厅任务卡片
|
||||||
|
- 进行中任务卡片
|
||||||
|
- 已完成任务卡片
|
||||||
|
- 我发起的任务卡片
|
||||||
|
- 待审核任务卡片
|
||||||
|
- 草稿箱任务卡片
|
||||||
|
- 子任务卡片
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 问题修复
|
||||||
|
|
||||||
|
### 1. 修复流水线任务异常扣分问题
|
||||||
|
|
||||||
|
**问题描述:**
|
||||||
|
- 流水线任务的子任务按时完成后,主任务仍然被系统检查进度更新情况
|
||||||
|
- 导致已完成子任务的用户被错误扣分
|
||||||
|
- 影响范围:所有流水线任务的接收人
|
||||||
|
|
||||||
|
**问题原因:**
|
||||||
|
- 进度检查服务 (`ProgressCheckService`) 在检查任务进度时,没有区分流水线任务的主任务和子任务
|
||||||
|
- 流水线任务的主任务 (`is_pipeline=1`) 在子任务完成后,主任务状态仍为 `in_progress`,但主任务本身不需要更新进度
|
||||||
|
- 系统错误地对主任务进行了进度检查,导致已完成子任务的用户被扣分
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 修改 `ProgressCheckService.checkDailyProgressUpdate()` 方法
|
||||||
|
- 在进度检查时排除流水线任务的主任务(`is_pipeline=1`)
|
||||||
|
- 只对流水线任务的子任务进行进度检查
|
||||||
|
- 清理了历史错误扣分记录(共 21 条)
|
||||||
|
|
||||||
|
**修复效果:**
|
||||||
|
- ✅ 流水线任务的主任务不再被检查进度
|
||||||
|
- ✅ 已完成子任务的用户不会再被错误扣分
|
||||||
|
- ✅ 历史错误扣分记录已清理
|
||||||
|
|
||||||
|
**影响用户:**
|
||||||
|
- 所有流水线任务的接收人
|
||||||
|
- 特别是已完成子任务但主任务仍在进行中的用户
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 修复需求部门看不到一级需求任务的问题
|
||||||
|
|
||||||
|
**问题描述:**
|
||||||
|
- 需求部门负责人审核通过一级需求部门的任务后,在"进行中任务"中看不到该任务
|
||||||
|
- 需求部门成员补充完任务明细并发布后,在"进行中任务"中也看不到该任务
|
||||||
|
- 影响范围:所有需求部门负责人和成员
|
||||||
|
|
||||||
|
**问题原因:**
|
||||||
|
- 一级需求部门的任务在需求部门负责人审核通过后,状态变为 `pending_detail_completion`(待明细完善)
|
||||||
|
- 此时任务的 `publisher_dept_id` 是一级需求部门的 ID,不是需求部门的 ID
|
||||||
|
- 需求部门负责人的"进行中任务"查询只按 `publisherDeptIds` 过滤,无法找到这些任务
|
||||||
|
- 需求部门成员的"进行中任务"查询只查询 `role='publisher'` 的任务,无法找到作为 `detail_completer` 的任务
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- **后端修改:**
|
||||||
|
- 在 `TaskController.getTaskList` 中添加 `includeDetailCompleterDeptIds` 参数
|
||||||
|
- 在 `TaskService.getTaskList` 中添加按明细完善人部门查询的逻辑
|
||||||
|
- 在 `TaskController.getMyTasks` 中添加 `role='detail_completer'` 支持
|
||||||
|
- 新增 `TaskService.getMyDetailCompleterTasks` 方法,查询用户作为明细完善人的任务
|
||||||
|
- **前端修改:**
|
||||||
|
- 需求部门负责人查看"进行中任务"时,传递 `includeDetailCompleterDeptIds` 参数
|
||||||
|
- 需求部门成员查看"进行中任务"时,同时查询 `publisher` 和 `detail_completer` 角色的任务并合并结果
|
||||||
|
|
||||||
|
**修复效果:**
|
||||||
|
- ✅ 需求部门负责人可以在"进行中任务"中看到已审核通过的一级需求任务
|
||||||
|
- ✅ 需求部门成员可以在"进行中任务"中看到自己作为明细完善人的任务
|
||||||
|
- ✅ 任务状态流转更加清晰,用户能正确跟踪任务进度
|
||||||
|
|
||||||
|
**影响用户:**
|
||||||
|
- 所有需求部门负责人
|
||||||
|
- 所有需求部门成员
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 修复需求部门负责人无法驳回任务的问题
|
||||||
|
|
||||||
|
**问题描述:**
|
||||||
|
- 需求部门负责人在驳回一级需求部门的任务时,系统报错:"需求部门负责人不存在"
|
||||||
|
- 导致需求部门负责人无法正常驳回任务
|
||||||
|
- 影响范围:所有需求部门负责人
|
||||||
|
|
||||||
|
**问题原因:**
|
||||||
|
- 一级需求部门的任务被驳回时,系统尝试将任务返回给需求部门的负责人
|
||||||
|
- 但某些一级需求部门(`dept_type='primary_demand'`)可能没有设置 `leader_id`
|
||||||
|
- 当 `TaskApprovalService.rejectByReceivingLeader` 方法检查 `demandDept.getLeaderId()` 时,如果为 `null`,会抛出异常
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 修改 `TaskApprovalService.rejectByReceivingLeader` 方法
|
||||||
|
- 当需求部门的 `leader_id` 为 `null` 时,不再抛出异常
|
||||||
|
- 改为将任务的 `currentApproverId` 设置为任务的 `publisherId`,将任务返回给原始发起人
|
||||||
|
- 这样即使需求部门没有负责人,任务也能正常被驳回并返回给发起人
|
||||||
|
|
||||||
|
**修复效果:**
|
||||||
|
- ✅ 需求部门负责人可以正常驳回一级需求部门的任务
|
||||||
|
- ✅ 即使需求部门没有负责人,任务也能正常被驳回
|
||||||
|
- ✅ 任务会正确返回给原始发起人
|
||||||
|
|
||||||
|
**影响用户:**
|
||||||
|
- 所有需求部门负责人
|
||||||
|
- 特别是需要驳回一级需求部门任务的负责人
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 修复筛选功能搜索报错
|
||||||
|
|
||||||
|
**问题描述:**
|
||||||
|
- 错误信息:`TypeError: Assignment to constant variable`
|
||||||
|
- 触发场景:在任务大厅、进行中任务、已完成任务等页面使用筛选功能时
|
||||||
|
|
||||||
|
**问题原因:**
|
||||||
|
- 使用 `v-model` 绑定 `reactive` 对象时,Vue 尝试重新赋值整个对象
|
||||||
|
- `reactive` 对象不能直接重新赋值,导致报错
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 将所有 `TaskFilters` 组件的 `v-model` 改为 `:model-value` 和 `@update:model-value`
|
||||||
|
- 在事件处理函数中使用 `Object.assign` 更新对象属性,而不是重新赋值整个对象
|
||||||
|
|
||||||
|
**修复的筛选器:**
|
||||||
|
- ✅ 可接任务筛选 (`filters`)
|
||||||
|
- ✅ 待审核任务筛选 (`reviewFilters`)
|
||||||
|
- ✅ 进行中任务筛选 (`inProgressFilters`)
|
||||||
|
- ✅ 已完成任务筛选 (`completedFilters`)
|
||||||
|
- ✅ 我发起的任务筛选 (`publishedFilters`)
|
||||||
|
- ✅ 草稿箱筛选 (`draftsFilters`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 修复 SQL 查询错误
|
||||||
|
|
||||||
|
**问题描述:**
|
||||||
|
- 错误信息:`Unknown column 't.description' in 'where clause'`
|
||||||
|
- 触发场景:使用搜索功能时
|
||||||
|
|
||||||
|
**问题原因:**
|
||||||
|
- 在扩展搜索功能时,SQL 查询中引用了 `task` 表中不存在的 `description` 字段
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 从所有 SQL 查询中移除了对 `t.description` 字段的引用
|
||||||
|
- 修复了以下文件中的查询:
|
||||||
|
- `TaskMapper.java` - `selectTaskHall` 和 `selectTaskHallByDept` 方法
|
||||||
|
- `TaskItemMapper.java` - 所有子任务查询方法
|
||||||
|
- `TaskService.java` - `getCompletedTasks` 方法
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 技术细节
|
||||||
|
|
||||||
|
### 前端修改
|
||||||
|
|
||||||
|
**文件:** `frontend/src/views/TaskHall.vue`
|
||||||
|
- 导入 `Tooltip` 组件
|
||||||
|
- 添加 `truncateText` 辅助函数
|
||||||
|
- 修改所有显示需求人名字的地方,添加 Tooltip 和截断显示
|
||||||
|
- 修复 `v-model` 绑定问题,改用 `:model-value` 和 `@update:model-value`
|
||||||
|
- **修复需求部门看不到一级需求任务:**
|
||||||
|
- 需求部门负责人查看"进行中任务"时,添加 `includeDetailCompleterDeptIds` 参数
|
||||||
|
- 需求部门成员查看"进行中任务"时,同时查询 `publisher` 和 `detail_completer` 角色的任务并合并结果
|
||||||
|
- 更新 `loadDashboardStats` 统计逻辑,包含 `detail_completer` 角色的任务
|
||||||
|
|
||||||
|
**文件:** `frontend/src/views/CreateTask.vue`
|
||||||
|
- 添加预览图必填验证规则
|
||||||
|
- 在提交时添加额外的预览图验证
|
||||||
|
- 更新预览图上传区域的提示信息
|
||||||
|
|
||||||
|
**文件:** `frontend/src/api/task.ts`
|
||||||
|
- 在 `MyTasksParams` 接口中添加 `detail_completer` 作为有效的 `role` 值
|
||||||
|
- 在 `getTasks` 函数中添加 `includeDetailCompleterDeptIds` 参数
|
||||||
|
|
||||||
|
### 后端修改
|
||||||
|
|
||||||
|
**文件:** `backend/src/main/java/com/designhub/mapper/TaskMapper.java`
|
||||||
|
- 扩展 `selectTaskHall` 和 `selectTaskHallByDept` 的搜索条件
|
||||||
|
- 添加明细完善人、接收人、任务类型的关联查询
|
||||||
|
- 移除不存在的 `description` 字段引用
|
||||||
|
|
||||||
|
**文件:** `backend/src/main/java/com/designhub/mapper/TaskItemMapper.java`
|
||||||
|
- 扩展所有子任务查询方法的搜索条件
|
||||||
|
- 移除不存在的 `description` 字段引用
|
||||||
|
|
||||||
|
**文件:** `backend/src/main/java/com/designhub/service/TaskService.java`
|
||||||
|
- 扩展 `getCompletedTasks` 方法的搜索条件
|
||||||
|
- 在 `createTask` 和 `createPipelineTask` 方法中添加预览图必填验证
|
||||||
|
- 移除不存在的 `description` 字段引用
|
||||||
|
- **修复需求部门看不到一级需求任务:**
|
||||||
|
- 在 `getTaskList` 方法中添加 `includeDetailCompleterDeptIds` 参数支持
|
||||||
|
- 添加按明细完善人部门查询的逻辑(支持一级需求任务)
|
||||||
|
- 在 `getMyTasks` 方法中添加 `role='detail_completer'` 支持
|
||||||
|
- 新增 `getMyDetailCompleterTasks` 方法,查询用户作为明细完善人的任务
|
||||||
|
- 在 `fillTaskRelatedInfo` 方法中添加 `detailCompleterName` 的填充逻辑
|
||||||
|
|
||||||
|
**文件:** `backend/src/main/java/com/designhub/controller/TaskController.java`
|
||||||
|
- **修复需求部门看不到一级需求任务:**
|
||||||
|
- 在 `getTaskList` 接口中添加 `includeDetailCompleterDeptIds` 参数(逗号分隔的部门ID列表)
|
||||||
|
- 在 `getMyTasks` 接口中添加 `role='detail_completer'` 支持
|
||||||
|
|
||||||
|
**文件:** `backend/src/main/java/com/designhub/service/ProgressCheckService.java`
|
||||||
|
- **修复流水线任务异常扣分:**
|
||||||
|
- 修改 `checkDailyProgressUpdate` 方法,排除流水线任务的主任务(`is_pipeline=1`)
|
||||||
|
- 只对流水线任务的子任务进行进度检查
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 影响范围
|
||||||
|
|
||||||
|
### 用户影响
|
||||||
|
- ✅ **一级需求部门用户**:发起任务时必须上传预览图
|
||||||
|
- ✅ **所有用户**:搜索功能更强大,可以搜索更多字段
|
||||||
|
- ✅ **所有用户**:任务卡片显示更美观,长名字不再影响布局
|
||||||
|
- ✅ **流水线任务接收人**:不再被错误扣分,已完成子任务后不会被检查主任务进度
|
||||||
|
- ✅ **需求部门负责人**:可以在"进行中任务"中看到已审核通过的一级需求任务
|
||||||
|
- ✅ **需求部门成员**:可以在"进行中任务"中看到自己作为明细完善人的任务
|
||||||
|
|
||||||
|
### 功能影响
|
||||||
|
- ✅ **搜索功能**:支持搜索任务卡片的所有信息
|
||||||
|
- ✅ **筛选功能**:修复了搜索时的报错问题
|
||||||
|
- ✅ **任务创建**:一级需求部门用户必须上传预览图
|
||||||
|
- ✅ **进度检查**:流水线任务的主任务不再被检查进度,避免错误扣分
|
||||||
|
- ✅ **任务可见性**:需求部门可以正确查看一级需求任务的进度
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **一级需求部门用户**:从本次更新开始,发起任务时必须上传预览图,否则无法提交任务。
|
||||||
|
|
||||||
|
2. **搜索功能**:搜索功能已扩展,现在可以搜索更多字段,但不再支持搜索 `description` 字段(该字段在数据库中不存在)。
|
||||||
|
|
||||||
|
3. **名字显示**:任务卡片中的需求人名字如果超过 8 个字符会自动截断,鼠标悬停可查看完整信息。
|
||||||
|
|
||||||
|
4. **流水线任务进度检查**:流水线任务的主任务不再进行进度检查,只有子任务会被检查进度。已完成子任务的用户不会再被错误扣分。
|
||||||
|
|
||||||
|
5. **一级需求任务可见性**:
|
||||||
|
- 需求部门负责人审核通过一级需求任务后,可以在"进行中任务"中看到该任务
|
||||||
|
- 需求部门成员补充完任务明细并发布后,可以在"进行中任务"中看到该任务
|
||||||
|
- 任务状态为 `pending_detail_completion`(待明细完善)时,会显示在需求部门的"进行中任务"中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 问题反馈
|
||||||
|
|
||||||
|
如果在使用过程中遇到任何问题,请及时反馈:
|
||||||
|
- 系统异常
|
||||||
|
- 功能异常
|
||||||
|
- 用户体验问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**更新完成时间:** 2026年1月22日 15:40
|
||||||
|
|
||||||
|
**部署状态:** ✅ 已部署到生产环境
|
||||||
Loading…
x
Reference in New Issue
Block a user