fix: support demand identity selection on task creation

This commit is contained in:
Your Name 2026-03-30 10:12:24 +08:00
parent eb6f0c28b1
commit a727cc0622
7 changed files with 261 additions and 62 deletions

View File

@ -38,6 +38,7 @@ public class PipelineTaskDTO {
private Long brandId;
private Long categoryId;
private Long platformId;
private Long publisherDeptId;
private String spuInfo;
private String taskSubType;
private String attachmentUrl;

View File

@ -88,30 +88,10 @@ public class TaskService {
// 检查是否是管理员(管理员可以同时拥有需求部门和接单部门权限)
boolean isAdmin = isAdminUser(user.getId());
if (!isAdmin) {
if (user.getDeptId() == null) {
throw new BusinessException(ResultCode.FORBIDDEN, "用户部门信息不完整");
}
var department = departmentService.getDepartmentById(user.getDeptId());
if (department == null) {
throw new BusinessException(ResultCode.FORBIDDEN, "用户部门不存在");
}
String deptType = department.getDeptType();
if (!"demand".equals(deptType) && !"primary_demand".equals(deptType)) {
throw new BusinessException(ResultCode.FORBIDDEN, "只有需求部门或一级需求部门可以发起任务,您所在的部门是接单部门");
}
// 判断是否为一级需求部门发起的任务
isPrimaryTask = "primary_demand".equals(deptType);
}
// 设置发布人部门ID
if (task.getPublisherDeptId() == null && user.getDeptId() != null) {
task.setPublisherDeptId(user.getDeptId());
}
Department publisherDept = resolvePublishingDepartment(user, task.getPublisherDeptId(), isAdmin);
isPrimaryTask = "primary_demand".equals(publisherDept.getDeptType());
task.setPublisherDeptId(publisherDept.getId());
}
// 如果是一级需求部门发起的任务,设置相关字段
@ -289,26 +269,8 @@ public class TaskService {
// 检查是否是管理员
boolean isAdmin = isAdminUser(user.getId());
boolean isPrimaryTask = false;
if (!isAdmin) {
if (user.getDeptId() == null) {
throw new BusinessException(ResultCode.FORBIDDEN, "用户部门信息不完整");
}
var department = departmentService.getDepartmentById(user.getDeptId());
if (department == null) {
throw new BusinessException(ResultCode.FORBIDDEN, "用户部门不存在");
}
String deptType = department.getDeptType();
if (!"demand".equals(deptType) && !"primary_demand".equals(deptType)) {
throw new BusinessException(ResultCode.FORBIDDEN, "只有需求部门或一级需求部门可以发起任务");
}
// 判断是否为一级需求部门发起的任务
isPrimaryTask = "primary_demand".equals(deptType);
}
Department publisherDept = resolvePublishingDepartment(user, dto.getPublisherDeptId(), isAdmin);
boolean isPrimaryTask = "primary_demand".equals(publisherDept.getDeptType());
// 验证流水线模式
if (!"sequential".equals(dto.getPipelineMode()) && !"parallel".equals(dto.getPipelineMode())) {
@ -358,12 +320,12 @@ public class TaskService {
int totalScoreInt = totalScoreDecimal.intValue(); // 用于积分检查
// 检查部门积分是否充足(管理员跳过积分检查)
if (!isAdmin && user.getDeptId() != null) {
if (!teamScorePoolService.checkBalance(user.getDeptId(), totalScoreInt)) {
if (!isAdmin && publisherDept.getId() != null) {
if (!teamScorePoolService.checkBalance(publisherDept.getId(), totalScoreInt)) {
throw new BusinessException(ResultCode.INSUFFICIENT_SCORE, "部门积分余额不足");
}
if (!teamScorePoolService.checkPeriodLimit(user.getDeptId(), totalScoreInt)) {
if (!teamScorePoolService.checkPeriodLimit(publisherDept.getId(), totalScoreInt)) {
throw new BusinessException(ResultCode.SCORE_LIMIT_EXCEEDED, "超过本周期积分消耗限制");
}
}
@ -378,7 +340,7 @@ public class TaskService {
task.setThirdPartyTaskNo(generateThirdPartyTaskNo());
task.setTitle(dto.getTitle());
task.setPublisherId(publisherId);
task.setPublisherDeptId(user.getDeptId());
task.setPublisherDeptId(publisherDept.getId());
task.setIsPipeline(1);
task.setPipelineMode(dto.getPipelineMode());
task.setScore(totalScoreDecimal);
@ -428,9 +390,9 @@ public class TaskService {
if (isPrimaryTask) {
task.setIsPrimaryTask(1);
task.setPrimaryPublisherId(publisherId);
task.setPrimaryPublisherDeptId(user.getDeptId());
task.setPrimaryPublisherDeptId(publisherDept.getId());
task.setDetailCompleted(0); // 明细未完善
log.info("一级需求部门发起流水线任务: publisherId={}, deptId={}", publisherId, user.getDeptId());
log.info("一级需求部门发起流水线任务: publisherId={}, deptId={}", publisherId, publisherDept.getId());
} else {
task.setIsPrimaryTask(0);
task.setDetailCompleted(1); // 普通任务明细已完善
@ -1318,6 +1280,62 @@ public class TaskService {
return user != null ? user.getDeptId() : null;
}
private Department resolvePublishingDepartment(User user, Long requestedDeptId, boolean isAdmin) {
if (user == null) {
throw new BusinessException(ResultCode.FORBIDDEN, "用户不存在");
}
List<Department> userDepartments = new java.util.ArrayList<>(userService.getUserDepartments(user.getId()));
if (user.getDeptId() != null && userDepartments.stream().noneMatch(dept -> user.getDeptId().equals(dept.getId()))) {
Department primaryDept = departmentService.getDepartmentById(user.getDeptId());
if (primaryDept != null) {
userDepartments.add(primaryDept);
}
}
List<Department> publishingDepartments = userDepartments.stream()
.filter(java.util.Objects::nonNull)
.filter(dept -> dept.getStatus() != null && dept.getStatus() == 1)
.filter(dept -> dept.getDeleted() == null || dept.getDeleted() == 0)
.filter(dept -> "demand".equals(dept.getDeptType()) || "primary_demand".equals(dept.getDeptType()))
.distinct()
.collect(Collectors.toList());
if (requestedDeptId != null) {
Department requestedDept = departmentService.getDepartmentById(requestedDeptId);
if (requestedDept == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "发起部门不存在");
}
if (!"demand".equals(requestedDept.getDeptType()) && !"primary_demand".equals(requestedDept.getDeptType())) {
throw new BusinessException(ResultCode.PARAM_ERROR, "发起部门必须是需求部门或一级需求部门");
}
if (!isAdmin && publishingDepartments.stream().noneMatch(dept -> requestedDeptId.equals(dept.getId()))) {
throw new BusinessException(ResultCode.FORBIDDEN, "您不属于所选发起部门");
}
return requestedDept;
}
if (user.getDeptId() != null) {
Department currentDept = departmentService.getDepartmentById(user.getDeptId());
if (currentDept != null && ("demand".equals(currentDept.getDeptType()) || "primary_demand".equals(currentDept.getDeptType()))) {
return currentDept;
}
if (isAdmin && currentDept != null) {
return currentDept;
}
}
if (publishingDepartments.size() == 1) {
return publishingDepartments.get(0);
}
if (publishingDepartments.isEmpty()) {
throw new BusinessException(ResultCode.FORBIDDEN, "只有需求部门或一级需求部门可以发起任务");
}
throw new BusinessException(ResultCode.PARAM_ERROR, "请先选择发起部门");
}
/**
* 获取我发布的任务(支持多维度筛选)
*/
@ -1823,7 +1841,7 @@ public class TaskService {
.filter(java.util.Objects::nonNull)
.anyMatch(dept -> "demand".equals(dept.getDeptType())
&& dept.getStatus() != null && dept.getStatus() == 1
&& dept.getDeleted() != null && dept.getDeleted() == 0);
&& (dept.getDeleted() == null || dept.getDeleted() == 0));
if (isDemandDeptMemberForPrimaryTask) {
log.info("需求部门成员 {} 编辑一级需求任务 {}", userId, id);
}
@ -1909,12 +1927,13 @@ public class TaskService {
// 检查部门积分是否充足(管理员跳过积分检查,一级需求部门无明细提交场景跳过)
User user = userService.getUserById(userId);
if (!isAdmin && !allowPrimaryTaskWithoutItems && user.getDeptId() != null) {
if (!teamScorePoolService.checkBalance(user.getDeptId(), totalScoreInt)) {
Department publisherDept = resolvePublishingDepartment(user, existTask.getPublisherDeptId(), isAdmin);
if (!isAdmin && !allowPrimaryTaskWithoutItems && publisherDept.getId() != null) {
if (!teamScorePoolService.checkBalance(publisherDept.getId(), totalScoreInt)) {
throw new BusinessException(ResultCode.INSUFFICIENT_SCORE, "部门积分余额不足");
}
if (!teamScorePoolService.checkPeriodLimit(user.getDeptId(), totalScoreInt)) {
if (!teamScorePoolService.checkPeriodLimit(publisherDept.getId(), totalScoreInt)) {
throw new BusinessException(ResultCode.SCORE_LIMIT_EXCEEDED, "超过本周期积分消耗限制");
}
}

View File

@ -1190,6 +1190,7 @@ public class UserService {
return UserDepartmentVO.builder()
.deptId(dept.getId())
.deptName(dept.getDeptName())
.deptType(dept.getDeptType())
.isPrimary(ud.getIsPrimary())
.build();
}
@ -1328,9 +1329,16 @@ public class UserService {
new LambdaQueryWrapper<UserDepartment>()
.eq(UserDepartment::getUserId, userId)
);
return userDepts.stream()
LinkedHashSet<Long> deptIds = userDepts.stream()
.map(UserDepartment::getDeptId)
.collect(Collectors.toList());
.collect(Collectors.toCollection(LinkedHashSet::new));
User user = userMapper.selectById(userId);
if (user != null && user.getDeptId() != null) {
deptIds.add(user.getDeptId());
}
return new ArrayList<>(deptIds);
}
/**

View File

@ -18,6 +18,11 @@ public class UserDepartmentVO {
* 部门名称
*/
private String deptName;
/**
* 部门类型:demand / receiving / primary_demand
*/
private String deptType;
/**
* 是否为主部门:0-否,1-是

View File

@ -43,9 +43,27 @@ export interface UserInfo {
avatar: string
deptId: number
deptName: string
deptType?: string
departments?: UserDepartment[]
roleIds?: string // 角色ID列表(逗号分隔)
roles: string[]
permissions: string[]
isDepartmentLeader?: number | boolean
isAdmin?: number | boolean
managedDepartments?: ManagedDepartment[]
}
export interface UserDepartment {
deptId: number
deptName: string
deptType?: string
isPrimary?: number
}
export interface ManagedDepartment {
deptId: number
deptName: string
deptType: string
}
/**
@ -94,4 +112,3 @@ export const refreshToken = () => {
`/auth/refresh`
)
}

View File

@ -129,11 +129,13 @@ export interface PipelineTaskRequest {
title: string
isPipeline: boolean
pipelineMode: 'sequential' | 'parallel'
publisherDeptId?: number
items: Array<{
taskTypeId: number
quantity: number
sortOrder: number
receiverId?: number // 指定接收人
selectedDeptId?: number
}>
brandId?: number
categoryId?: number

View File

@ -8,6 +8,25 @@
<div class="balance-label">团队积分余额</div>
<div class="balance-value">{{ teamScore }} 分</div>
</div>
<div v-if="availablePublishingDepartments.length > 0" class="publisher-dept-card">
<div class="publisher-dept-label">发起部门</div>
<a-select
v-model:value="selectedPublishingDeptId"
size="large"
class="publisher-dept-select"
:disabled="editMode || formData.isPrimaryTaskEdit || availablePublishingDepartments.length === 1"
:placeholder="availablePublishingDepartments.length > 1 ? '请选择发起部门' : '当前发起部门'"
>
<a-select-option
v-for="dept in availablePublishingDepartments"
:key="dept.deptId"
:value="dept.deptId"
>
{{ dept.deptName }}{{ dept.deptType === 'primary_demand' ? '(一级需求)' : '' }}
</a-select-option>
</a-select>
<div class="publisher-dept-tip">任务积分和审批流按当前发起部门计算</div>
</div>
<div class="balance-tip">
<span class="tip-icon">
<Icon name="info" size="sm" color="#31f08b" />
@ -644,6 +663,7 @@ const loading = ref(false)
// 团队积分余额
const teamScore = ref(0)
const selectedPublishingDeptId = ref<number | undefined>(undefined)
// 判断是否是管理员(管理员跳过积分检查)
const isAdmin = computed(() => {
@ -659,6 +679,54 @@ const isAdmin = computed(() => {
return false
})
interface PublishingDepartmentOption {
deptId: number
deptName: string
deptType: string
isPrimary?: number
}
const availablePublishingDepartments = computed<PublishingDepartmentOption[]>(() => {
const userInfo = authStore.userInfo
if (!userInfo) return []
const result: PublishingDepartmentOption[] = []
const seen = new Set<number>()
const departments = Array.isArray(userInfo.departments) ? userInfo.departments : []
departments.forEach((dept) => {
if (!dept?.deptId || seen.has(dept.deptId)) return
if (dept.deptType !== 'demand' && dept.deptType !== 'primary_demand') return
result.push({
deptId: dept.deptId,
deptName: dept.deptName,
deptType: dept.deptType,
isPrimary: dept.isPrimary
})
seen.add(dept.deptId)
})
if (
userInfo.deptId &&
!seen.has(userInfo.deptId) &&
(userInfo.deptType === 'demand' || userInfo.deptType === 'primary_demand')
) {
result.unshift({
deptId: userInfo.deptId,
deptName: userInfo.deptName,
deptType: userInfo.deptType,
isPrimary: 1
})
seen.add(userInfo.deptId)
}
return result
})
const activePublishingDept = computed(() => {
return availablePublishingDepartments.value.find(dept => dept.deptId === selectedPublishingDeptId.value)
})
// 任务子类型列表
const taskSubTypes = ref<TaskSubType[]>([])
@ -676,9 +744,7 @@ const primaryDeptConfig = ref({
// 判断当前用户是否是一级需求部门用户
const isPrimaryDeptUser = computed(() => {
const userInfo = authStore.userInfo as any
if (!userInfo) return false
return userInfo.deptType === 'primary_demand'
return activePublishingDept.value?.deptType === 'primary_demand'
})
// 判断是否应该显示任务明细区域
@ -759,6 +825,37 @@ const formData = reactive({
timeLimit: undefined as number | undefined // 时效要求
})
const initializePublishingDept = (preferredDeptId?: number) => {
const departments = availablePublishingDepartments.value
if (!departments.length) {
selectedPublishingDeptId.value = undefined
return
}
if (preferredDeptId && departments.some(dept => dept.deptId === preferredDeptId)) {
selectedPublishingDeptId.value = preferredDeptId
return
}
if (selectedPublishingDeptId.value && departments.some(dept => dept.deptId === selectedPublishingDeptId.value)) {
return
}
const currentDeptId = authStore.userInfo?.deptId
const currentDeptType = authStore.userInfo?.deptType
if (
currentDeptId &&
(currentDeptType === 'demand' || currentDeptType === 'primary_demand') &&
departments.some(dept => dept.deptId === currentDeptId)
) {
selectedPublishingDeptId.value = currentDeptId
return
}
const primaryPublishingDept = departments.find(dept => dept.isPrimary === 1)
selectedPublishingDeptId.value = primaryPublishingDept?.deptId || departments[0].deptId
}
// 新条码输入框
const newBarcodeInput = ref('')
@ -1503,6 +1600,7 @@ const handleSaveDraft = async () => {
title: formData.title,
isPipeline: true,
pipelineMode: 'sequential',
publisherDeptId: selectedPublishingDeptId.value,
items: validTaskTypes.map((item, index) => ({
taskTypeId: item.taskTypeId!,
quantity: item.quantity || 1,
@ -1558,6 +1656,11 @@ const handleSubmit = async () => {
// 验证表单
await formRef.value.validate()
if (!isAdmin.value && !selectedPublishingDeptId.value) {
message.error('请选择发起部门')
return
}
// 一级需求部门用户必须上传预览图
if (isPrimaryDeptUser.value && !formData.previewImage) {
message.error('一级需求部门发起任务必须上传预览图')
@ -1646,6 +1749,7 @@ const handleSubmit = async () => {
title: formData.title,
isPipeline: !isPrimaryDeptWithoutDetail, // 一级需求部门不填明细时,不标记为流水线任务
pipelineMode: 'sequential', // 固定为顺序流水线
publisherDeptId: selectedPublishingDeptId.value,
items: validTaskTypes.map((item, index) => ({
taskTypeId: item.taskTypeId!,
quantity: item.quantity,
@ -1675,6 +1779,7 @@ const handleSubmit = async () => {
// 一级需求部门不填写任务明细时,需要设置 isPipeline=true 来触发审核流程
const taskData = {
title: formData.title,
publisherDeptId: selectedPublishingDeptId.value,
brandId: formData.brandId,
platformId: formData.platformId,
categoryId: formData.categoryId,
@ -1714,6 +1819,7 @@ const handleSubmit = async () => {
// 任务明细将由需求部门在审核时填写
const taskData = {
title: formData.title,
publisherDeptId: selectedPublishingDeptId.value,
brandId: formData.brandId,
platformId: formData.platformId,
categoryId: formData.categoryId,
@ -1800,13 +1906,13 @@ const loadTeamScore = async () => {
console.log('[loadTeamScore] 当前用户信息:', authStore.userInfo)
// 确保用户信息是最新的
if (!authStore.userInfo || !authStore.userInfo.deptId) {
if (!authStore.userInfo) {
console.log('[loadTeamScore] 用户信息缺失或无部门ID,重新获取用户信息')
await authStore.fetchUserInfo()
console.log('[loadTeamScore] 重新获取后的用户信息:', authStore.userInfo)
}
const deptId = authStore.userInfo?.deptId
const deptId = selectedPublishingDeptId.value
console.log('[loadTeamScore] 部门ID:', deptId)
if (!deptId) {
@ -2190,6 +2296,7 @@ const loadTaskForEdit = async (taskId: number) => {
if (task) {
console.log('加载任务数据:', task)
initializePublishingDept(task.publisherDeptId || task.primaryPublisherDeptId)
// 填充基本表单数据
formData.title = task.title || ''
@ -2314,6 +2421,7 @@ const loadTaskForEdit = async (taskId: number) => {
}
console.log('任务数据加载成功,formData.taskTypes:', formData.taskTypes)
await loadTeamScore()
}
} catch (error) {
console.error('加载任务数据失败:', error)
@ -2403,6 +2511,7 @@ const loadPrimaryTaskForEdit = async () => {
}
console.log('加载一级需求任务数据:', task)
initializePublishingDept(task.publisherDeptId || task.primaryPublisherDeptId)
// 设置编辑模式
editMode.value = true
@ -2513,6 +2622,7 @@ const loadPrimaryTaskForEdit = async () => {
formData.primaryPublisherDeptId = task.primaryPublisherDeptId
message.info('请完善任务明细后提交')
await loadTeamScore()
} catch (error) {
console.error('加载一级需求任务失败:', error)
@ -2530,6 +2640,7 @@ onMounted(async () => {
try {
await authStore.fetchUserInfo()
console.log('用户信息已刷新:', authStore.userInfo)
initializePublishingDept()
} catch (error) {
console.error('刷新用户信息失败:', error)
}
@ -2561,6 +2672,22 @@ onMounted(async () => {
window.addEventListener('configUpdated', handleConfigUpdate as EventListener)
})
watch(availablePublishingDepartments, (departments) => {
if (!departments.length) {
selectedPublishingDeptId.value = undefined
return
}
if (!selectedPublishingDeptId.value || !departments.some(dept => dept.deptId === selectedPublishingDeptId.value)) {
initializePublishingDept()
}
}, { immediate: true })
watch(selectedPublishingDeptId, async (deptId, oldDeptId) => {
if (deptId === oldDeptId) return
await loadTeamScore()
})
// 组件卸载
onUnmounted(() => {
window.removeEventListener('configUpdated', handleConfigUpdate as EventListener)
@ -2643,6 +2770,26 @@ onUnmounted(() => {
}
}
.publisher-dept-card {
margin-bottom: $space-md;
}
.publisher-dept-label {
margin-bottom: $space-xs;
font-size: $font-size-sm;
color: $text-secondary;
}
.publisher-dept-select {
width: 100%;
}
.publisher-dept-tip {
margin-top: $space-xs;
font-size: $font-size-xs;
color: $text-tertiary;
}
.time-estimate {
display: flex;
flex-wrap: wrap;