package com.ruoyi.projectManagement.service;
|
|
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.projectManagement.dto.ProjectWorkflowDtos;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.LocalDate;
|
import java.util.ArrayList;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
|
@Service
|
@RequiredArgsConstructor
|
public class ProjectWorkflowService {
|
private static final TypeReference<List<Map<String, Object>>> FILE_LIST_TYPE = new TypeReference<>() { };
|
private static final List<String> PHASE_CODES = List.of("INITIATION", "DEVELOPMENT", "APPLICATION", "RESULT", "ACCEPTANCE");
|
private static final List<String> PHASE_NAMES = List.of("立项", "开发", "申报", "申报结果", "验收");
|
|
private final NamedParameterJdbcTemplate jdbc;
|
private final ObjectMapper objectMapper;
|
|
public Map<String, Object> getWorkbench(Long projectId) {
|
requireProject(projectId);
|
List<Map<String, Object>> modules = query("""
|
select m.id, m.project_id as projectId, m.module_name as moduleName,
|
m.planned_start_date as plannedStartDate, m.summary,
|
m.project_weight as projectWeight, m.current_progress as currentProgress,
|
(select count(*) from project_management_progress_report r where r.module_id=m.id and r.is_deleted=0) as reportCount,
|
m.create_time as createTime, m.update_time as updateTime
|
from project_management_module m
|
where m.project_id=:projectId and m.is_deleted=0 order by m.id
|
""", Map.of("projectId", projectId));
|
for (Map<String, Object> module : modules) {
|
Long moduleId = asLong(module.get("id"));
|
module.put("members", query("""
|
select id, module_id as moduleId, user_id as userId, user_name as userName,
|
module_share as moduleShare
|
from project_management_module_member
|
where module_id=:moduleId and is_deleted=0 order by id
|
""", Map.of("moduleId", moduleId)));
|
}
|
|
List<Map<String, Object>> followUps = query("""
|
select id, project_id as projectId, follow_date as followDate, detail,
|
attachments, create_user_id as createUserId, create_user_name as createUserName,
|
create_time as createTime
|
from project_management_follow_up
|
where project_id=:projectId and is_deleted=0 order by follow_date desc,id desc
|
""", Map.of("projectId", projectId));
|
parseAttachments(followUps);
|
|
List<Map<String, Object>> reports = query("""
|
select r.id, r.project_id as projectId, r.module_id as moduleId,
|
m.module_name as moduleName, r.reporter_ids as reporterIds,
|
r.reporter_names as reporterNames, r.progress, r.report_date as reportDate,
|
r.reporter_shares as reporterShares, r.remark, r.attachments, r.create_time as createTime
|
from project_management_progress_report r
|
left join project_management_module m on m.id=r.module_id
|
where r.project_id=:projectId and r.is_deleted=0 order by r.report_date desc,r.id desc
|
""", Map.of("projectId", projectId));
|
parseAttachments(reports);
|
for (Map<String, Object> row : reports) {
|
row.put("reporterIdList", parseLongList(row.get("reporterIds")));
|
row.put("reporterNameList", parseStringList(row.get("reporterNames")));
|
row.put("reporterShareList", parseFileList(row.get("reporterShares")));
|
}
|
|
List<Map<String, Object>> materials = query("""
|
select id, project_id as projectId, material_name as materialName,
|
material_type as materialType, remark, attachments,
|
create_user_name as createUserName, create_time as createTime
|
from project_management_material
|
where project_id=:projectId and is_deleted=0 order by id desc
|
""", Map.of("projectId", projectId));
|
parseAttachments(materials);
|
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("modules", modules);
|
result.put("followUps", followUps);
|
result.put("reports", reports);
|
result.put("materials", materials);
|
result.put("sourceBusiness", getSourceBusiness(projectId));
|
result.put("memberStatistics", memberStatistics(projectId));
|
result.put("followWindow", getFollowWindow(projectId));
|
return result;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void startProject(Long projectId) {
|
requireProject(projectId);
|
Integer basicReady = jdbc.queryForObject("""
|
select count(*) from project_management_info
|
where id=:projectId and is_delete=0 and project_classification is not null
|
and project_classification<>'' and plan_start_time is not null and plan_end_time is not null
|
and project_leader_id is not null and project_manager_ids is not null
|
and json_length(project_manager_ids)>0 and follow_cycle is not null and follow_cycle<>''
|
and project_executor_ids is not null and json_length(project_executor_ids)>0
|
""", Map.of("projectId", projectId), Integer.class);
|
if (basicReady == null || basicReady == 0) throw new ServiceException("请先完整填写项目基本情况");
|
List<Map<String, Object>> modules = query("""
|
select id,module_name as moduleName,planned_start_date as plannedStartDate,
|
summary,project_weight as projectWeight
|
from project_management_module where project_id=:projectId and is_deleted=0
|
""", Map.of("projectId", projectId));
|
if (modules.isEmpty()) throw new ServiceException("项目开始前请至少添加一个项目模块");
|
BigDecimal total = BigDecimal.ZERO;
|
for (Map<String, Object> module : modules) {
|
if (isBlank(Objects.toString(module.get("moduleName"), "")) || module.get("plannedStartDate") == null
|
|| isBlank(Objects.toString(module.get("summary"), ""))) {
|
throw new ServiceException("请完整填写每个模块的名称、预计开始时间和模块概要");
|
}
|
total = total.add(nvl((BigDecimal) module.get("projectWeight")));
|
Long moduleId = asLong(module.get("id"));
|
Integer memberCount = jdbc.queryForObject("""
|
select count(*) from project_management_module_member
|
where module_id=:moduleId and is_deleted=0
|
""", Map.of("moduleId", moduleId), Integer.class);
|
if (memberCount == null || memberCount == 0) throw new ServiceException("每个模块至少选择一名执行人");
|
}
|
if (total.setScale(2, RoundingMode.HALF_UP).compareTo(BigDecimal.valueOf(100).setScale(2)) != 0) {
|
throw new ServiceException("项目开始前,所有模块的项目工作量占比合计必须为100%");
|
}
|
jdbc.update("update project_management_info set current_phase='开发',update_time=now() where id=:projectId", Map.of("projectId", projectId));
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void transitionStage(ProjectWorkflowDtos.StageTransitionRequest request) {
|
requireProject(request.getProjectId());
|
String current = jdbc.queryForObject("select current_phase from project_management_info where id=:id and is_delete=0",
|
Map.of("id", request.getProjectId()), String.class);
|
if ("立项".equals(current)) current = "开发";
|
Map<String, String> nextStages = Map.of(
|
"开发", "申报",
|
"申报", "申报结果",
|
"申报结果", "验收",
|
"验收", "已完成"
|
);
|
String expected = nextStages.get(current);
|
if (expected == null || !expected.equals(request.getTargetStage())) {
|
throw new ServiceException("项目阶段只能按开发、申报、申报结果、验收、已完成的顺序推进");
|
}
|
if ("开发".equals(current)) {
|
Integer unfinished = jdbc.queryForObject("""
|
select count(*) from project_management_module
|
where project_id=:projectId and is_deleted=0 and current_progress<100
|
""", Map.of("projectId", request.getProjectId()), Integer.class);
|
if (unfinished != null && unfinished > 0) {
|
throw new ServiceException("所有项目模块进度达到100%后才能进入申报阶段");
|
}
|
}
|
jdbc.update("update project_management_info set current_phase=:stage,update_time=now() where id=:projectId",
|
new MapSqlParameterSource().addValue("stage", expected).addValue("projectId", request.getProjectId()));
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void updateProjectExecutors(ProjectWorkflowDtos.ProjectExecutorsRequest request) {
|
requireProject(request.getProjectId());
|
validateMembers(request.getExecutors());
|
List<Map<String, Object>> rows = query("""
|
select project_executor_ids as executorIds from project_management_info
|
where id=:projectId and is_delete=0
|
""", Map.of("projectId", request.getProjectId()));
|
List<Long> oldIds = rows.isEmpty() ? List.of() : parseLongList(rows.getFirst().get("executorIds"));
|
List<Long> newIds = request.getExecutors().stream().map(ProjectWorkflowDtos.MemberRequest::getUserId).toList();
|
for (Long removedId : oldIds.stream().filter(id -> !newIds.contains(id)).toList()) {
|
if (hasPositiveProjectShare(request.getProjectId(), removedId)) {
|
throw new ServiceException("该项目执行人已有项目汇报工作占比,不能删除");
|
}
|
}
|
jdbc.update("""
|
update project_management_info set project_executor_ids=:ids,project_executor_names=:names,update_time=now()
|
where id=:projectId and is_delete=0
|
""", new MapSqlParameterSource().addValue("projectId", request.getProjectId())
|
.addValue("ids", json(newIds))
|
.addValue("names", json(request.getExecutors().stream().map(ProjectWorkflowDtos.MemberRequest::getUserName).toList())));
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Long savePhase(ProjectWorkflowDtos.PhaseRequest request) {
|
requireProject(request.getProjectId());
|
ensurePhases(request.getProjectId());
|
int index = PHASE_CODES.indexOf(Objects.toString(request.getPhaseCode(), ""));
|
if (index < 0) throw new ServiceException("项目阶段不正确");
|
String status = Objects.toString(request.getStatus(), "IN_PROGRESS").toUpperCase();
|
if (!List.of("NOT_STARTED", "IN_PROGRESS", "COMPLETED").contains(status)) {
|
throw new ServiceException("阶段状态不正确");
|
}
|
if (index > 0 && !"NOT_STARTED".equals(status)) {
|
Integer previousDone = jdbc.queryForObject("""
|
select count(*) from project_management_phase
|
where project_id=:projectId and phase_code=:phaseCode and status='COMPLETED' and is_deleted=0
|
""", new MapSqlParameterSource().addValue("projectId", request.getProjectId())
|
.addValue("phaseCode", PHASE_CODES.get(index - 1)), Integer.class);
|
if (previousDone == null || previousDone == 0) {
|
throw new ServiceException("请先完成上一阶段“" + PHASE_NAMES.get(index - 1) + "”");
|
}
|
}
|
MapSqlParameterSource p = new MapSqlParameterSource()
|
.addValue("projectId", request.getProjectId()).addValue("phaseCode", request.getPhaseCode())
|
.addValue("status", status).addValue("formData", jsonObject(request.getFormData()))
|
.addValue("remark", request.getRemark()).addValue("attachments", json(request.getAttachments()))
|
.addValue("userName", currentUserName());
|
jdbc.update("""
|
update project_management_phase set status=:status,form_data=:formData,remark=:remark,
|
attachments=:attachments,completed_time=case when :status='COMPLETED' then now() else null end,
|
update_user_name=:userName,update_time=now()
|
where project_id=:projectId and phase_code=:phaseCode and is_deleted=0
|
""", p);
|
String currentPhase = "COMPLETED".equals(status) && index == PHASE_CODES.size() - 1
|
? "已完成" : PHASE_NAMES.get(index);
|
jdbc.update("update project_management_info set current_phase=:phase,update_time=now() where id=:projectId",
|
new MapSqlParameterSource().addValue("phase", currentPhase).addValue("projectId", request.getProjectId()));
|
return jdbc.queryForObject("select id from project_management_phase where project_id=:projectId and phase_code=:phaseCode and is_deleted=0",
|
p, Long.class);
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Long saveModule(ProjectWorkflowDtos.ModuleSaveRequest request) {
|
requireProject(request.getProjectId());
|
if (isBlank(request.getModuleName())) {
|
throw new ServiceException("请输入模块名称");
|
}
|
if (request.getPlannedStartDate() == null) throw new ServiceException("请选择模块预计开始时间");
|
if (isBlank(request.getSummary())) throw new ServiceException("请输入模块概要");
|
BigDecimal weight = percent(request.getProjectWeight(), "模块在项目中的工作量占比");
|
BigDecimal otherWeight = jdbc.queryForObject("""
|
select coalesce(sum(project_weight),0) from project_management_module
|
where project_id=:projectId and is_deleted=0 and (:id is null or id<>:id)
|
""", new MapSqlParameterSource().addValue("projectId", request.getProjectId()).addValue("id", request.getId()), BigDecimal.class);
|
BigDecimal available = BigDecimal.valueOf(100).subtract(nvl(otherWeight));
|
if (weight.compareTo(available) > 0) {
|
throw new ServiceException("其他模块已占" + nvl(otherWeight).stripTrailingZeros().toPlainString()
|
+ "%,当前模块最多可设置" + available.stripTrailingZeros().toPlainString() + "%");
|
}
|
validateMembers(request.getMembers());
|
List<Long> projectExecutorIds = projectExecutorIds(request.getProjectId());
|
if (request.getMembers().stream().anyMatch(member -> !projectExecutorIds.contains(member.getUserId()))) {
|
throw new ServiceException("模块执行人必须先加入项目执行人");
|
}
|
|
if (request.getId() != null) {
|
Integer reportCount = jdbc.queryForObject("select count(*) from project_management_progress_report where module_id=:id and is_deleted=0",
|
Map.of("id", request.getId()), Integer.class);
|
if (reportCount != null && reportCount > 0) {
|
Map<String, Object> old = query("""
|
select module_name as moduleName,planned_start_date as plannedStartDate,
|
summary,project_weight as projectWeight from project_management_module
|
where id=:id and project_id=:projectId and is_deleted=0
|
""", new MapSqlParameterSource().addValue("id", request.getId()).addValue("projectId", request.getProjectId())).stream()
|
.findFirst().orElseThrow(() -> new ServiceException("项目模块不存在"));
|
boolean changed = !Objects.equals(Objects.toString(old.get("moduleName"), ""), request.getModuleName().trim())
|
|| !Objects.equals(Objects.toString(old.get("plannedStartDate"), ""), Objects.toString(request.getPlannedStartDate(), ""))
|
|| !Objects.equals(Objects.toString(old.get("summary"), ""), Objects.toString(request.getSummary(), ""))
|
|| nvl((BigDecimal) old.get("projectWeight")).compareTo(weight) != 0;
|
if (changed) throw new ServiceException("该模块已有进度汇报,模块基础信息和占比不能修改,仅可调整执行人");
|
}
|
List<Long> oldMemberIds = query("""
|
select user_id as userId from project_management_module_member
|
where module_id=:moduleId and is_deleted=0
|
""", Map.of("moduleId", request.getId())).stream().map(row -> asLong(row.get("userId"))).toList();
|
List<Long> newMemberIds = request.getMembers().stream().map(ProjectWorkflowDtos.MemberRequest::getUserId).toList();
|
for (Long removedId : oldMemberIds.stream().filter(id -> !newMemberIds.contains(id)).toList()) {
|
if (hasPositiveModuleShare(request.getId(), removedId)) {
|
throw new ServiceException("该模块执行人已有本模块工作占比,不能删除");
|
}
|
}
|
}
|
|
Long moduleId = request.getId();
|
MapSqlParameterSource params = new MapSqlParameterSource()
|
.addValue("projectId", request.getProjectId())
|
.addValue("moduleName", request.getModuleName().trim())
|
.addValue("plannedStartDate", request.getPlannedStartDate())
|
.addValue("summary", request.getSummary())
|
.addValue("projectWeight", weight);
|
if (moduleId == null) {
|
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
|
jdbc.update("""
|
insert into project_management_module
|
(project_id,module_name,planned_start_date,summary,project_weight,current_progress,is_deleted,create_time,update_time)
|
values (:projectId,:moduleName,:plannedStartDate,:summary,:projectWeight,0,0,now(),now())
|
""", params, keyHolder, new String[]{"id"});
|
moduleId = Objects.requireNonNull(keyHolder.getKey()).longValue();
|
} else {
|
params.addValue("id", moduleId);
|
int rows = jdbc.update("""
|
update project_management_module set module_name=:moduleName,
|
planned_start_date=:plannedStartDate,summary=:summary,project_weight=:projectWeight,update_time=now()
|
where id=:id and project_id=:projectId and is_deleted=0
|
""", params);
|
if (rows == 0) throw new ServiceException("项目模块不存在");
|
jdbc.update("update project_management_module_member set is_deleted=1 where module_id=:id", Map.of("id", moduleId));
|
}
|
for (ProjectWorkflowDtos.MemberRequest member : request.getMembers()) {
|
jdbc.update("""
|
insert into project_management_module_member
|
(module_id,user_id,user_name,module_share,is_deleted,create_time,update_time)
|
values (:moduleId,:userId,:userName,:moduleShare,0,now(),now())
|
""", new MapSqlParameterSource()
|
.addValue("moduleId", moduleId)
|
.addValue("userId", member.getUserId())
|
.addValue("userName", member.getUserName())
|
.addValue("moduleShare", BigDecimal.ZERO));
|
}
|
return moduleId;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteModule(Long id) {
|
int count = jdbc.queryForObject("select count(*) from project_management_progress_report where module_id=:id and is_deleted=0", Map.of("id", id), Integer.class);
|
if (count > 0) throw new ServiceException("该模块已有进度汇报,不能删除");
|
jdbc.update("update project_management_module set is_deleted=1,update_time=now() where id=:id", Map.of("id", id));
|
jdbc.update("update project_management_module_member set is_deleted=1,update_time=now() where module_id=:id", Map.of("id", id));
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Long saveFollowUp(ProjectWorkflowDtos.FollowUpRequest request) {
|
requireProject(request.getProjectId());
|
Long leaderId = jdbc.queryForObject("select project_leader_id from project_management_info where id=:id",
|
Map.of("id", request.getProjectId()), Long.class);
|
if (leaderId == null || !leaderId.equals(SecurityUtils.getUserId())) {
|
throw new ServiceException("只有项目负责人可以填写项目跟进");
|
}
|
if (request.getFollowDate() == null || isBlank(request.getDetail())) {
|
throw new ServiceException("请填写跟进日期和跟进详情");
|
}
|
validateFollowWindow(request.getProjectId(), request.getFollowDate(), request.getId());
|
MapSqlParameterSource p = new MapSqlParameterSource()
|
.addValue("id", request.getId()).addValue("projectId", request.getProjectId())
|
.addValue("followDate", request.getFollowDate()).addValue("detail", request.getDetail())
|
.addValue("attachments", json(request.getAttachments()))
|
.addValue("userId", SecurityUtils.getUserId()).addValue("userName", currentUserName());
|
if (request.getId() == null) {
|
GeneratedKeyHolder keys = new GeneratedKeyHolder();
|
jdbc.update("""
|
insert into project_management_follow_up
|
(project_id,follow_date,detail,attachments,create_user_id,create_user_name,is_deleted,create_time,update_time)
|
values (:projectId,:followDate,:detail,:attachments,:userId,:userName,0,now(),now())
|
""", p, keys, new String[]{"id"});
|
return Objects.requireNonNull(keys.getKey()).longValue();
|
}
|
jdbc.update("""
|
update project_management_follow_up set follow_date=:followDate,detail=:detail,
|
attachments=:attachments,update_time=now() where id=:id and project_id=:projectId and is_deleted=0
|
""", p);
|
return request.getId();
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Long saveReport(ProjectWorkflowDtos.ProgressReportRequest request) {
|
requireProject(request.getProjectId());
|
if (request.getModuleId() == null || request.getReportDate() == null) {
|
throw new ServiceException("请选择项目模块和汇报时间");
|
}
|
BigDecimal progress = percent(request.getProgress(), "模块进度");
|
BigDecimal current = jdbc.queryForObject("""
|
select current_progress from project_management_module
|
where id=:moduleId and project_id=:projectId and is_deleted=0
|
""", new MapSqlParameterSource().addValue("moduleId", request.getModuleId()).addValue("projectId", request.getProjectId()), BigDecimal.class);
|
if (current == null) throw new ServiceException("项目模块不存在");
|
if (progress.compareTo(current) < 0) throw new ServiceException("本次进度不能低于模块当前进度");
|
List<ProjectWorkflowDtos.MemberRequest> reporterShares = normalizeReporterShares(request);
|
validateReporterShares(reporterShares);
|
List<Long> moduleMemberIds = query("""
|
select user_id as userId from project_management_module_member
|
where module_id=:moduleId and is_deleted=0
|
""", Map.of("moduleId", request.getModuleId())).stream().map(row -> asLong(row.get("userId"))).toList();
|
if (!moduleMemberIds.contains(SecurityUtils.getUserId())) throw new ServiceException("只有该模块执行人可以提交进度汇报");
|
if (reporterShares.stream().anyMatch(member -> !moduleMemberIds.contains(member.getUserId()))) {
|
throw new ServiceException("汇报人员只能选择该模块执行人");
|
}
|
request.setReporterIds(reporterShares.stream().map(ProjectWorkflowDtos.MemberRequest::getUserId).toList());
|
request.setReporterNames(reporterShares.stream().map(ProjectWorkflowDtos.MemberRequest::getUserName).toList());
|
MapSqlParameterSource p = new MapSqlParameterSource()
|
.addValue("id", request.getId()).addValue("projectId", request.getProjectId())
|
.addValue("moduleId", request.getModuleId())
|
.addValue("reporterIds", json(request.getReporterIds()))
|
.addValue("reporterNames", json(request.getReporterNames()))
|
.addValue("reporterShares", json(reporterShares))
|
.addValue("progress", progress).addValue("reportDate", request.getReportDate())
|
.addValue("remark", request.getRemark()).addValue("attachments", json(request.getAttachments()));
|
Long reportId = request.getId();
|
if (reportId == null) {
|
GeneratedKeyHolder keys = new GeneratedKeyHolder();
|
jdbc.update("""
|
insert into project_management_progress_report
|
(project_id,module_id,reporter_ids,reporter_names,reporter_shares,progress,report_date,remark,attachments,is_deleted,create_time,update_time)
|
values (:projectId,:moduleId,:reporterIds,:reporterNames,:reporterShares,:progress,:reportDate,:remark,:attachments,0,now(),now())
|
""", p, keys, new String[]{"id"});
|
reportId = Objects.requireNonNull(keys.getKey()).longValue();
|
} else {
|
jdbc.update("""
|
update project_management_progress_report set module_id=:moduleId,reporter_ids=:reporterIds,
|
reporter_names=:reporterNames,reporter_shares=:reporterShares,progress=:progress,report_date=:reportDate,remark=:remark,
|
attachments=:attachments,update_time=now()
|
where id=:id and project_id=:projectId and is_deleted=0
|
""", p);
|
}
|
jdbc.update("update project_management_module set current_progress=:progress,update_time=now() where id=:moduleId",
|
new MapSqlParameterSource().addValue("progress", progress).addValue("moduleId", request.getModuleId()));
|
return reportId;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Long saveMaterial(ProjectWorkflowDtos.MaterialRequest request) {
|
requireProject(request.getProjectId());
|
if (isBlank(request.getMaterialName())) throw new ServiceException("请输入资料名称");
|
MapSqlParameterSource p = new MapSqlParameterSource()
|
.addValue("id", request.getId()).addValue("projectId", request.getProjectId())
|
.addValue("materialName", request.getMaterialName()).addValue("materialType", request.getMaterialType())
|
.addValue("remark", request.getRemark()).addValue("attachments", json(request.getAttachments()))
|
.addValue("userId", SecurityUtils.getUserId()).addValue("userName", currentUserName());
|
if (request.getId() == null) {
|
GeneratedKeyHolder keys = new GeneratedKeyHolder();
|
jdbc.update("""
|
insert into project_management_material
|
(project_id,material_name,material_type,remark,attachments,create_user_id,create_user_name,is_deleted,create_time,update_time)
|
values (:projectId,:materialName,:materialType,:remark,:attachments,:userId,:userName,0,now(),now())
|
""", p, keys, new String[]{"id"});
|
return Objects.requireNonNull(keys.getKey()).longValue();
|
}
|
jdbc.update("""
|
update project_management_material set material_name=:materialName,material_type=:materialType,
|
remark=:remark,attachments=:attachments,update_time=now()
|
where id=:id and project_id=:projectId and is_deleted=0
|
""", p);
|
return request.getId();
|
}
|
|
public void softDelete(String table, Long id) {
|
if (!List.of("project_management_follow_up", "project_management_progress_report", "project_management_material", "government_policy_file").contains(table)) {
|
throw new ServiceException("不支持的删除类型");
|
}
|
jdbc.getJdbcTemplate().update("update " + table + " set is_deleted=1,update_time=now() where id=?", id);
|
}
|
|
public List<Map<String, Object>> listCategories() {
|
return query("""
|
select id,parent_id as parentId,category_name as categoryName,sort,remark
|
from project_management_category where is_deleted=0 order by sort,id
|
""", Map.of());
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Long saveCategory(ProjectWorkflowDtos.CategoryRequest request) {
|
if (isBlank(request.getCategoryName())) throw new ServiceException("请输入分类名称");
|
Long parentId = request.getParentId() == null ? 0L : request.getParentId();
|
if (request.getId() != null && request.getId().equals(parentId)) throw new ServiceException("上级分类不能选择自己");
|
MapSqlParameterSource p = new MapSqlParameterSource().addValue("id", request.getId())
|
.addValue("parentId", parentId).addValue("categoryName", request.getCategoryName().trim())
|
.addValue("sort", request.getSort() == null ? 0 : request.getSort()).addValue("remark", request.getRemark());
|
if (request.getId() == null) {
|
GeneratedKeyHolder keys = new GeneratedKeyHolder();
|
jdbc.update("""
|
insert into project_management_category(parent_id,category_name,sort,remark,is_deleted,create_time,update_time)
|
values (:parentId,:categoryName,:sort,:remark,0,now(),now())
|
""", p, keys, new String[]{"id"});
|
return Objects.requireNonNull(keys.getKey()).longValue();
|
}
|
jdbc.update("""
|
update project_management_category set parent_id=:parentId,category_name=:categoryName,
|
sort=:sort,remark=:remark,update_time=now() where id=:id and is_deleted=0
|
""", p);
|
return request.getId();
|
}
|
|
public void deleteCategory(Long id) {
|
int children = jdbc.queryForObject("select count(*) from project_management_category where parent_id=:id and is_deleted=0", Map.of("id", id), Integer.class);
|
if (children > 0) throw new ServiceException("请先删除下级分类");
|
jdbc.update("update project_management_category set is_deleted=1,update_time=now() where id=:id", Map.of("id", id));
|
}
|
|
public List<Map<String, Object>> listPolicies(String keyword) {
|
MapSqlParameterSource p = new MapSqlParameterSource().addValue("keyword", keyword == null ? "" : keyword.trim());
|
List<Map<String, Object>> rows = query("""
|
select id,policy_name as policyName,publish_date as publishDate,policy_url as policyUrl,
|
attachments,maintainer_name as maintainerName,remark,create_time as createTime
|
from government_policy_file where is_deleted=0
|
and (:keyword='' or policy_name like concat('%',:keyword,'%'))
|
order by publish_date desc,id desc
|
""", p);
|
parseAttachments(rows);
|
return rows;
|
}
|
|
@Transactional(rollbackFor = Exception.class)
|
public Long savePolicy(ProjectWorkflowDtos.PolicyRequest request) {
|
if (isBlank(request.getPolicyName())) throw new ServiceException("请输入政策名称");
|
String maintainer = isBlank(request.getMaintainerName()) ? currentUserName() : request.getMaintainerName();
|
MapSqlParameterSource p = new MapSqlParameterSource().addValue("id", request.getId())
|
.addValue("policyName", request.getPolicyName()).addValue("publishDate", request.getPublishDate())
|
.addValue("policyUrl", request.getPolicyUrl()).addValue("attachments", json(request.getAttachments()))
|
.addValue("maintainer", maintainer).addValue("remark", request.getRemark());
|
if (request.getId() == null) {
|
GeneratedKeyHolder keys = new GeneratedKeyHolder();
|
jdbc.update("""
|
insert into government_policy_file
|
(policy_name,publish_date,policy_url,attachments,maintainer_name,remark,is_deleted,create_time,update_time)
|
values (:policyName,:publishDate,:policyUrl,:attachments,:maintainer,:remark,0,now(),now())
|
""", p, keys, new String[]{"id"});
|
return Objects.requireNonNull(keys.getKey()).longValue();
|
}
|
jdbc.update("""
|
update government_policy_file set policy_name=:policyName,publish_date=:publishDate,
|
policy_url=:policyUrl,attachments=:attachments,maintainer_name=:maintainer,
|
remark=:remark,update_time=now() where id=:id and is_deleted=0
|
""", p);
|
return request.getId();
|
}
|
|
private List<Map<String, Object>> memberStatistics(Long projectId) {
|
List<Map<String, Object>> reports = query("""
|
select r.module_id as moduleId,r.progress,r.reporter_ids as reporterIds,
|
r.reporter_names as reporterNames,r.reporter_shares as reporterShares,
|
m.project_weight as projectWeight
|
from project_management_progress_report r
|
join project_management_module m on m.id=r.module_id and m.is_deleted=0
|
where r.project_id=:projectId and r.is_deleted=0
|
order by r.module_id,r.report_date,r.id
|
""", Map.of("projectId", projectId));
|
Map<Long, BigDecimal> previousProgress = new LinkedHashMap<>();
|
Map<Long, Map<String, Object>> totals = new LinkedHashMap<>();
|
for (Map<String, Object> report : reports) {
|
Long moduleId = asLong(report.get("moduleId"));
|
BigDecimal progress = nvl((BigDecimal) report.get("progress"));
|
BigDecimal previous = previousProgress.getOrDefault(moduleId, BigDecimal.ZERO);
|
BigDecimal delta = progress.subtract(previous).max(BigDecimal.ZERO);
|
previousProgress.put(moduleId, progress.max(previous));
|
List<Map<String, Object>> shares = parseFileList(report.get("reporterShares"));
|
if (shares.isEmpty()) shares = equalReporterShares(report.get("reporterIds"), report.get("reporterNames"));
|
BigDecimal moduleWeight = nvl((BigDecimal) report.get("projectWeight"));
|
for (Map<String, Object> share : shares) {
|
Long userId = asLong(share.get("userId"));
|
if (userId == null) continue;
|
String userName = Objects.toString(share.get("userName"), "");
|
BigDecimal memberShare = new BigDecimal(Objects.toString(share.get("moduleShare"), "0"));
|
BigDecimal contribution = moduleWeight.multiply(delta).multiply(memberShare)
|
.divide(BigDecimal.valueOf(10000), 2, RoundingMode.HALF_UP);
|
Map<String, Object> total = totals.computeIfAbsent(userId, id -> {
|
Map<String, Object> item = new LinkedHashMap<>();
|
item.put("userId", id); item.put("userName", userName); item.put("value", BigDecimal.ZERO);
|
return item;
|
});
|
total.put("value", ((BigDecimal) total.get("value")).add(contribution));
|
}
|
}
|
return totals.values().stream()
|
.sorted((a, b) -> ((BigDecimal) b.get("value")).compareTo((BigDecimal) a.get("value")))
|
.toList();
|
}
|
|
private Map<String, Object> getFollowWindow(Long projectId) {
|
List<Map<String, Object>> projectRows = query("""
|
select follow_cycle as followCycle,plan_start_time as planStartTime
|
from project_management_info where id=:projectId and is_delete=0
|
""", Map.of("projectId", projectId));
|
if (projectRows.isEmpty()) return Map.of();
|
String cycle = Objects.toString(projectRows.getFirst().get("followCycle"), "");
|
LocalDate last = jdbc.query("""
|
select follow_date from project_management_follow_up
|
where project_id=:projectId and is_deleted=0 order by follow_date desc,id desc limit 1
|
""", Map.of("projectId", projectId), rs -> rs.next() ? rs.getDate(1).toLocalDate() : null);
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("cycle", cycle);
|
result.put("lastFollowDate", last);
|
LocalDate planStart = projectRows.getFirst().get("planStartTime") instanceof java.sql.Date date ? date.toLocalDate() : null;
|
LocalDate base = last == null ? planStart : last;
|
if (base != null) {
|
LocalDate due = nextDue(base, cycle);
|
result.put("dueDate", due);
|
result.put("windowStart", due.minusDays(3));
|
result.put("windowEnd", due.plusDays(3));
|
}
|
return result;
|
}
|
|
private void ensurePhases(Long projectId) {
|
for (int i = 0; i < PHASE_CODES.size(); i++) {
|
jdbc.update("""
|
insert into project_management_phase
|
(project_id,phase_code,phase_name,phase_order,status,is_deleted,create_time,update_time)
|
select :projectId,:phaseCode,:phaseName,:phaseOrder,'NOT_STARTED',0,now(),now()
|
where not exists(select 1 from project_management_phase
|
where project_id=:projectId and phase_code=:phaseCode and is_deleted=0)
|
""", new MapSqlParameterSource().addValue("projectId", projectId)
|
.addValue("phaseCode", PHASE_CODES.get(i)).addValue("phaseName", PHASE_NAMES.get(i))
|
.addValue("phaseOrder", i + 1));
|
}
|
}
|
|
private Map<String, Object> getSourceBusiness(Long projectId) {
|
List<Map<String, Object>> rows = query("""
|
select sl.id,sl.project_name as projectName,sl.customer_id as customerId,
|
sl.customer_name as customerName,sl.salesman,sl.entry_date as entryDate,
|
sl.remarks,sl.attachment_materials as attachmentMaterials,
|
sl.business_approval_status as approvalStatus,
|
sl.business_approval_remark as approvalRemark
|
from project_management_info p
|
join sales_ledger sl on sl.id=p.source_business_id
|
where p.id=:projectId and p.source_business_id is not null
|
""", Map.of("projectId", projectId));
|
if (rows.isEmpty()) return Map.of();
|
Map<String, Object> row = rows.getFirst();
|
row.put("attachmentList", parseFileList(row.get("attachmentMaterials")));
|
return row;
|
}
|
|
private void validateFollowWindow(Long projectId, LocalDate followDate, Long excludeId) {
|
MapSqlParameterSource p = new MapSqlParameterSource().addValue("projectId", projectId).addValue("excludeId", excludeId);
|
LocalDate last = jdbc.query("""
|
select follow_date from project_management_follow_up
|
where project_id=:projectId and is_deleted=0 and (:excludeId is null or id<>:excludeId)
|
order by follow_date desc,id desc limit 1
|
""", p, rs -> rs.next() ? rs.getDate(1).toLocalDate() : null);
|
String cycle = jdbc.queryForObject("select follow_cycle from project_management_info where id=:projectId", Map.of("projectId", projectId), String.class);
|
LocalDate base = last;
|
if (base == null) {
|
base = jdbc.query("select plan_start_time from project_management_info where id=:projectId", Map.of("projectId", projectId),
|
rs -> rs.next() && rs.getDate(1) != null ? rs.getDate(1).toLocalDate() : null);
|
}
|
if (base == null) throw new ServiceException("请先填写项目预计开始时间");
|
LocalDate due = nextDue(base, cycle);
|
if (followDate.isBefore(due.minusDays(3)) || followDate.isAfter(due.plusDays(3))) {
|
throw new ServiceException("本次跟进应在" + due.minusDays(3) + "至" + due.plusDays(3) + "之间填写");
|
}
|
}
|
|
private LocalDate nextDue(LocalDate base, String cycle) {
|
return switch (Objects.toString(cycle, "MONTH").toUpperCase()) {
|
case "QUARTER", "季度", "每季度" -> base.plusMonths(3);
|
case "YEAR", "年度", "每年" -> base.plusYears(1);
|
default -> base.plusMonths(1);
|
};
|
}
|
|
private void validateMembers(List<ProjectWorkflowDtos.MemberRequest> members) {
|
if (members == null || members.isEmpty()) throw new ServiceException("请至少选择一名模块执行人");
|
List<Long> ids = new ArrayList<>();
|
for (ProjectWorkflowDtos.MemberRequest member : members) {
|
if (member.getUserId() == null || isBlank(member.getUserName())) throw new ServiceException("模块执行人不能为空");
|
if (ids.contains(member.getUserId())) throw new ServiceException("模块执行人不能重复");
|
ids.add(member.getUserId());
|
}
|
}
|
|
private List<Long> projectExecutorIds(Long projectId) {
|
List<Map<String, Object>> rows = query("""
|
select project_executor_ids as executorIds from project_management_info
|
where id=:projectId and is_delete=0
|
""", Map.of("projectId", projectId));
|
return rows.isEmpty() ? List.of() : parseLongList(rows.getFirst().get("executorIds"));
|
}
|
|
private boolean hasPositiveProjectShare(Long projectId, Long userId) {
|
return hasPositiveShare(query("""
|
select reporter_ids as reporterIds,reporter_names as reporterNames,reporter_shares as reporterShares
|
from project_management_progress_report where project_id=:projectId and is_deleted=0
|
""", Map.of("projectId", projectId)), userId);
|
}
|
|
private boolean hasPositiveModuleShare(Long moduleId, Long userId) {
|
return hasPositiveShare(query("""
|
select reporter_ids as reporterIds,reporter_names as reporterNames,reporter_shares as reporterShares
|
from project_management_progress_report where module_id=:moduleId and is_deleted=0
|
""", Map.of("moduleId", moduleId)), userId);
|
}
|
|
private boolean hasPositiveShare(List<Map<String, Object>> reports, Long userId) {
|
for (Map<String, Object> report : reports) {
|
List<Map<String, Object>> shares = parseFileList(report.get("reporterShares"));
|
if (shares.isEmpty()) shares = equalReporterShares(report.get("reporterIds"), report.get("reporterNames"));
|
for (Map<String, Object> share : shares) {
|
if (Objects.equals(asLong(share.get("userId")), userId)
|
&& new BigDecimal(Objects.toString(share.get("moduleShare"), "0")).compareTo(BigDecimal.ZERO) > 0) {
|
return true;
|
}
|
}
|
}
|
return false;
|
}
|
|
private void validateReporterShares(List<ProjectWorkflowDtos.MemberRequest> members) {
|
if (members == null || members.isEmpty()) throw new ServiceException("请至少选择一名汇报人");
|
BigDecimal sum = BigDecimal.ZERO;
|
List<Long> ids = new ArrayList<>();
|
for (ProjectWorkflowDtos.MemberRequest member : members) {
|
if (member.getUserId() == null || isBlank(member.getUserName())) throw new ServiceException("汇报人不能为空");
|
if (ids.contains(member.getUserId())) throw new ServiceException("汇报人不能重复");
|
ids.add(member.getUserId());
|
sum = sum.add(percent(member.getModuleShare(), "本次汇报人员比重"));
|
}
|
if (sum.setScale(2, RoundingMode.HALF_UP).compareTo(BigDecimal.valueOf(100).setScale(2)) != 0) {
|
throw new ServiceException("本次汇报人员比重合计必须为100%");
|
}
|
}
|
|
private List<ProjectWorkflowDtos.MemberRequest> normalizeReporterShares(ProjectWorkflowDtos.ProgressReportRequest request) {
|
if (request.getReporterShares() != null && !request.getReporterShares().isEmpty()) return request.getReporterShares();
|
List<Long> ids = request.getReporterIds() == null ? List.of() : request.getReporterIds();
|
List<String> names = request.getReporterNames() == null ? List.of() : request.getReporterNames();
|
if (ids.isEmpty()) return new ArrayList<>();
|
BigDecimal share = BigDecimal.valueOf(100).divide(BigDecimal.valueOf(ids.size()), 2, RoundingMode.DOWN);
|
List<ProjectWorkflowDtos.MemberRequest> result = new ArrayList<>();
|
for (int i = 0; i < ids.size(); i++) {
|
ProjectWorkflowDtos.MemberRequest member = new ProjectWorkflowDtos.MemberRequest();
|
member.setUserId(ids.get(i)); member.setUserName(i < names.size() ? names.get(i) : "");
|
member.setModuleShare(i == ids.size() - 1 ? BigDecimal.valueOf(100).subtract(share.multiply(BigDecimal.valueOf(i))) : share);
|
result.add(member);
|
}
|
return result;
|
}
|
|
private List<Map<String, Object>> equalReporterShares(Object rawIds, Object rawNames) {
|
List<Long> ids = parseLongList(rawIds);
|
List<String> names = parseStringList(rawNames);
|
if (ids.isEmpty()) return new ArrayList<>();
|
BigDecimal share = BigDecimal.valueOf(100).divide(BigDecimal.valueOf(ids.size()), 2, RoundingMode.DOWN);
|
List<Map<String, Object>> result = new ArrayList<>();
|
for (int i = 0; i < ids.size(); i++) {
|
Map<String, Object> item = new LinkedHashMap<>();
|
item.put("userId", ids.get(i)); item.put("userName", i < names.size() ? names.get(i) : "");
|
item.put("moduleShare", i == ids.size() - 1 ? BigDecimal.valueOf(100).subtract(share.multiply(BigDecimal.valueOf(i))) : share);
|
result.add(item);
|
}
|
return result;
|
}
|
|
private BigDecimal percent(BigDecimal value, String label) {
|
BigDecimal val = nvl(value);
|
if (val.compareTo(BigDecimal.ZERO) < 0 || val.compareTo(BigDecimal.valueOf(100)) > 0) {
|
throw new ServiceException(label + "必须在0到100之间");
|
}
|
return val.setScale(2, RoundingMode.HALF_UP);
|
}
|
|
private void requireProject(Long projectId) {
|
if (projectId == null) throw new ServiceException("项目不能为空");
|
Integer count = jdbc.queryForObject("select count(*) from project_management_info where id=:id and is_delete=0", Map.of("id", projectId), Integer.class);
|
if (count == null || count == 0) throw new ServiceException("项目不存在");
|
}
|
|
private List<Map<String, Object>> query(String sql, Map<String, ?> params) {
|
return jdbc.queryForList(sql, params);
|
}
|
|
private List<Map<String, Object>> query(String sql, MapSqlParameterSource params) {
|
return jdbc.queryForList(sql, params);
|
}
|
|
private void parseAttachments(List<Map<String, Object>> rows) {
|
for (Map<String, Object> row : rows) row.put("attachmentList", parseFileList(row.get("attachments")));
|
}
|
|
private List<Map<String, Object>> parseFileList(Object raw) {
|
if (raw == null || raw.toString().isBlank()) return new ArrayList<>();
|
try {
|
return objectMapper.readValue(raw.toString(), FILE_LIST_TYPE);
|
} catch (Exception ignored) {
|
return new ArrayList<>();
|
}
|
}
|
|
private List<Long> parseLongList(Object raw) {
|
if (raw == null || raw.toString().isBlank()) return new ArrayList<>();
|
try {
|
return objectMapper.readValue(raw.toString(), new TypeReference<>() { });
|
} catch (Exception ignored) {
|
return new ArrayList<>();
|
}
|
}
|
|
private List<String> parseStringList(Object raw) {
|
if (raw == null || raw.toString().isBlank()) return new ArrayList<>();
|
try {
|
return objectMapper.readValue(raw.toString(), new TypeReference<>() { });
|
} catch (Exception ignored) {
|
return new ArrayList<>();
|
}
|
}
|
|
private Map<String, Object> parseObjectMap(Object raw) {
|
if (raw == null || raw.toString().isBlank()) return new LinkedHashMap<>();
|
try {
|
return objectMapper.readValue(raw.toString(), new TypeReference<>() { });
|
} catch (Exception ignored) {
|
return new LinkedHashMap<>();
|
}
|
}
|
|
private String json(Object value) {
|
try {
|
return objectMapper.writeValueAsString(value == null ? List.of() : value);
|
} catch (Exception e) {
|
throw new ServiceException("附件或人员数据格式不正确");
|
}
|
}
|
|
private String jsonObject(Object value) {
|
try {
|
return objectMapper.writeValueAsString(value == null ? Map.of() : value);
|
} catch (Exception e) {
|
throw new ServiceException("阶段信息格式不正确");
|
}
|
}
|
|
private String currentUserName() {
|
return SecurityUtils.getLoginUser() == null ? "" : SecurityUtils.getLoginUser().getNickName();
|
}
|
|
private boolean isBlank(String value) {
|
return value == null || value.trim().isEmpty();
|
}
|
|
private BigDecimal nvl(BigDecimal value) {
|
return value == null ? BigDecimal.ZERO : value;
|
}
|
|
private Long asLong(Object value) {
|
return value == null ? null : Long.valueOf(value.toString());
|
}
|
}
|