buhuazhen
7 天以前 101f7a8f8c6ab2c830ca28992f2e14bb4f29bb17
feat(projectManagement): 优化项目管理计划及节点功能

- 修正Plan实体中字段命名,describe改为description,创建与更新用户类型由String改为Integer
- 添加PlanNode实体,支持计划节点的增删改查及排序管理
- 完善PlanService及实现类,新增保存和删除计划节点接口
- PlanController新增保存、删除及分页获取项目计划接口
- PlanMapper及XML调整字段映射,新增查询条件过滤已删除数据
- PlanVo支持计划节点列表展示,完善时间格式化注解
- SavePlanVo新增计划节点列表支持,SavePlanNodeVo定义计划节点数据结构
- CustomerFollowUpFileService调整注解为Nullable
- 项目计划保存时,支持同时保存与更新关联计划节点数据
- 删除项目计划时,同时软删除相关计划节点
- 查询项目计划列表时,加载对应计划节点列表数据并聚合展示
已添加3个文件
已修改8个文件
301 ■■■■ 文件已修改
src/main/java/com/ruoyi/basic/service/CustomerFollowUpFileService.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/controller/PlanController.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/pojo/Plan.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/pojo/PlanNode.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/service/PlanService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/service/impl/PlanServiceImpl.java 92 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/vo/PlanNodeVo.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/vo/PlanVo.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/vo/SavePlanNodeVo.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/vo/SavePlanVo.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/projectManagement/PlanMapper.xml 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/CustomerFollowUpFileService.java
@@ -4,6 +4,7 @@
import com.ruoyi.basic.pojo.CustomerFollowUpFile;
import com.ruoyi.common.vo.SimpleFileVo;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import java.util.List;
@@ -29,7 +30,7 @@
     * @param <T>
     */
    <T> void fillAttachment(
           @Null List<T> list,
           @Nullable List<T> list,
           @NotNull Function<T, String> getAttachmentIds,
           @NotNull BiConsumer<T, List<SimpleFileVo>> setAttachmentList
    );
src/main/java/com/ruoyi/projectManagement/controller/PlanController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.projectManagement.controller;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.projectManagement.service.PlanService;
import com.ruoyi.projectManagement.vo.SavePlanNodeVo;
import com.ruoyi.projectManagement.vo.SavePlanVo;
import com.ruoyi.projectManagement.vo.SearchPlanVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
 * @author buhuazhen
 * @date 2026/3/7
 * @email 3038525872@qq.com
 */
@RestController
@RequestMapping("/projectManagement/plan")
@Api(value = "PlanController", tags = "项目管理计划表(项目管理类型)")
@RequiredArgsConstructor
public class PlanController {
    private final PlanService planService;
    @PostMapping("/save")
    @ApiOperation("保存")
    public AjaxResult save(@RequestBody @Valid SavePlanVo savePlanVo) {
        planService.savePlan(savePlanVo);
        return AjaxResult.success();
    }
    @PostMapping("/delete/{id}")
    @ApiOperation("删除")
    public AjaxResult delete(@PathVariable Long id) {
        planService.deletePlan(id);
        return AjaxResult.success();
    }
    @PostMapping("/listPage")
    @ApiOperation("分页列表")
    public AjaxResult listPage(@RequestBody SearchPlanVo searchPlanVo) {
        return AjaxResult.success(planService.searchPlan(searchPlanVo));
    }
}
src/main/java/com/ruoyi/projectManagement/pojo/Plan.java
@@ -36,9 +36,9 @@
    /**
     * è®¡åˆ’描述
     */
    @TableField(value = "describe")
    @TableField(value = "description")
    @ApiModelProperty(value="计划描述")
    private String describe;
    private String description;
    /**
     * é™„ä»¶ ,进行分割
@@ -73,12 +73,12 @@
     */
    @TableField(value = "create_user",fill = FieldFill.INSERT)
    @ApiModelProperty(value="创建人")
    private String createUser;
    private Integer createUser;
    /**
     * 
     */
    @TableField(value = "update_user",fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty(value="更新人")
    private String updateUser;
    private Integer updateUser;
}
src/main/java/com/ruoyi/projectManagement/pojo/PlanNode.java
@@ -1,10 +1,9 @@
package com.ruoyi.projectManagement.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
import lombok.Data;
@@ -19,72 +18,86 @@
     * 
     */
    @TableId(type = IdType.AUTO)
    @TableField(value = "id")
    private Long id;
    /**
     * å¯¹åº”id
     */
    @TableField(value = "project_management_plan_id")
    private Long projectManagementPlanId;
    /**
     * æŽ’序
     */
    @TableField(value = "sort")
    private Integer sort;
    /**
     * é˜¶æ®µåç§°
     */
    @TableField(value = "name")
    private String name;
    /**
     * è´Ÿè´£ä»»ID
     */
    @TableField(value = "leader_id")
    private Long leaderId;
    /**
     * è´Ÿè´£ä»»åç§°
     */
    @TableField(value = "leader_name")
    private String leaderName;
    /**
     * é¢„计工期(天)
     */
    @TableField(value = "estimated_duration")
    private Integer estimatedDuration;
    /**
     *
     * å·¥ä»·
     */
    @TableField(value = "hourly_rate")
    private String hourlyRate;
    /**
     * ä½œä¸šå†…容
     */
    @TableField(value = "work_content")
    private String workContent;
    /**
     * 
     */
    @TableField(value = "is_delete")
    private Integer isDelete;
    /**
     * 
     */
    private Date createTime;
    @TableField(value = "create_time",fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * 
     */
    private Date updateTime;
    @TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * 
     */
    private String createUser;
    @TableField(value = "create_user",fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * 
     */
    private String updateUser;
    @TableField(value = "update_user",fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
src/main/java/com/ruoyi/projectManagement/service/PlanService.java
@@ -4,10 +4,13 @@
import com.ruoyi.projectManagement.pojo.Plan;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.projectManagement.vo.PlanVo;
import com.ruoyi.projectManagement.vo.SavePlanNodeVo;
import com.ruoyi.projectManagement.vo.SavePlanVo;
import com.ruoyi.projectManagement.vo.SearchPlanVo;
import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import java.util.List;
/**
@@ -24,11 +27,19 @@
    void savePlan(@NotNull SavePlanVo savePlanVo);
    /**
     * ä¿å­˜è®¡åˆ’节点
     * @param planId
     * @param savePlanNodeVos
     */
    void savePlanNode(@NotNull Long planId,@Nullable List<SavePlanNodeVo> savePlanNodeVos);
    /**
     * åˆ é™¤é¡¹ç›®ç®¡ç†è®¡åˆ’
     * @param id
     */
    void deletePlan(@NotNull Long id);
    void deletePlanNode(@Nullable List<Long> ids);
    IPage<PlanVo> searchPlan(@NotNull SearchPlanVo searchPlanVo);
}
src/main/java/com/ruoyi/projectManagement/service/impl/PlanServiceImpl.java
@@ -1,21 +1,32 @@
package com.ruoyi.projectManagement.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.basic.service.CustomerFollowUpFileService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.projectManagement.mapper.PlanMapper;
import com.ruoyi.projectManagement.mapper.PlanNodeMapper;
import com.ruoyi.projectManagement.pojo.Plan;
import com.ruoyi.projectManagement.pojo.PlanNode;
import com.ruoyi.projectManagement.service.PlanService;
import com.ruoyi.projectManagement.vo.PlanVo;
import com.ruoyi.projectManagement.vo.SavePlanVo;
import com.ruoyi.projectManagement.vo.SearchPlanVo;
import com.ruoyi.projectManagement.vo.*;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
 * @author buhuazhen
@@ -31,6 +42,12 @@
    private final CustomerFollowUpFileService customerFollowUpFileService;
    private final PlanNodeMapper planNodeMapper;
    @Lazy
    @Autowired
    private PlanService planService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void savePlan(SavePlanVo savePlanVo) {
@@ -38,18 +55,55 @@
        // é™„件处理 , æ‹¼æŽ¥
        String attachments = String.join(",", Optional.ofNullable(savePlanVo.getAttachmentIds()).orElse(Collections.emptyList()));
        plan.setAttachment(attachments);
        if (savePlanVo.getId() == null) {
            planMapper.insert(plan);
        } else {
            planMapper.updateById(plan);
        }
        // todo@ èŠ‚ç‚¹ä¿å­˜
        planService.savePlanNode(plan.getId(), savePlanVo.getSavePlanNodeList());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void savePlanNode(Long planId, @Nullable List<SavePlanNodeVo> savePlanNodeVos) {
        Assert.notNull(planId, () -> new ServiceException("计划ID不能为空"));
        if (CollUtil.isEmpty(savePlanNodeVos)) {
            return;
        }
        // åˆ é™¤å¤šä½™èŠ‚ç‚¹
        List<PlanNode> needDeleteNode = planNodeMapper.selectList(new LambdaQueryWrapper<PlanNode>()
                .select(PlanNode::getId)
                .eq(PlanNode::getProjectManagementPlanId, planId)
                .ne(PlanNode::getId, savePlanNodeVos.get(0).getId())
                .notIn(PlanNode::getId, savePlanNodeVos.stream().map(SavePlanNodeVo::getId).collect(Collectors.toList())));
        deletePlanNode(needDeleteNode.stream().map(PlanNode::getId).collect(Collectors.toList()));
        List<PlanNode> planNodes = BeanUtil.copyToList(savePlanNodeVos, PlanNode.class);
        // è®¾ç½®æŽ’序索引
        IntStream.range(0, savePlanNodeVos.size()).forEach(i -> {
            planNodes.get(i).setSort(i);
            planNodes.get(i).setProjectManagementPlanId(planId);
            if (planNodes.get(i).getId() == null) {
                planNodeMapper.insert(planNodes.get(i));
            } else {
                planNodeMapper.updateById(planNodes.get(i));
            }
        });
    }
    private List<PlanNode> getPlanNodeByPlanId(Long planId) {
        return planNodeMapper.selectList(new LambdaQueryWrapper<PlanNode>()
                .eq(PlanNode::getIsDelete, 0)
                .eq(PlanNode::getProjectManagementPlanId, planId));
    }
    private List<PlanNode> getPlanNodeByPlanIds(List<Long> planIds) {
        return planNodeMapper.selectList(new LambdaQueryWrapper<PlanNode>()
                .eq(PlanNode::getIsDelete, 0)
                .in(PlanNode::getProjectManagementPlanId, planIds));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -58,9 +112,18 @@
                new LambdaUpdateWrapper<Plan>()
                        .eq(Plan::getId, id)
                        .set(Plan::getIsDelete, 1));
        // todo@ å¯¹åº”节点全部删除
        planService.deletePlanNode(getPlanNodeByPlanId(id).stream().map(PlanNode::getId).collect(Collectors.toList()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deletePlanNode(List<Long> ids) {
        if (CollUtil.isNotEmpty(ids)) {
            planNodeMapper.update(null,
                    new LambdaUpdateWrapper<PlanNode>()
                            .in(PlanNode::getId, ids)
                            .set(PlanNode::getIsDelete, 1));
        }
    }
    @Override
@@ -69,8 +132,11 @@
        IPage<PlanVo> resultPage = planIPage.convert(plan -> BeanUtil.copyProperties(plan, PlanVo.class));
        // æ–‡ä»¶èŽ·å–
        customerFollowUpFileService.fillAttachment(resultPage.getRecords(), PlanVo::getAttachment, PlanVo::setAttachmentList);
        // todo@ node èŠ‚ç‚¹èŽ·å–
        Map<Long, List<PlanNodeVo>> collect = getPlanNodeByPlanIds(resultPage.getRecords().stream().map(PlanVo::getId).collect(Collectors.toList()))
                .stream()
                .map(it -> BeanUtil.copyProperties(it, PlanNodeVo.class))
                .collect(Collectors.groupingBy(PlanNodeVo::getProjectManagementPlanId, Collectors.toList()));
        resultPage.getRecords().forEach(planVo -> planVo.setPlanNodeList(collect.getOrDefault(planVo.getId(), Collections.emptyList())));
        return resultPage;
    }
src/main/java/com/ruoyi/projectManagement/vo/PlanNodeVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package com.ruoyi.projectManagement.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
 * @author buhuazhen
 * @date 2026/3/7
 * @email 3038525872@qq.com
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlanNodeVo implements Serializable {
    private Long id;
    private Long projectManagementPlanId;
    private Integer sort;
    private String name;
    private Long leaderId;
    private String leaderName;
    private Integer estimatedDuration;
    private String hourlyRate;
    private String workContent;
}
src/main/java/com/ruoyi/projectManagement/vo/PlanVo.java
@@ -1,10 +1,12 @@
package com.ruoyi.projectManagement.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.ruoyi.common.vo.SimpleFileVo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
@@ -25,7 +27,7 @@
    private String name;
    private String describe;
    private String description;
    // é™„ä»¶
    private List<SimpleFileVo> attachmentList;
@@ -33,8 +35,14 @@
    @JsonIgnore
    private String attachment;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    private List<PlanNodeVo> planNodeList;
}
src/main/java/com/ruoyi/projectManagement/vo/SavePlanNodeVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.ruoyi.projectManagement.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
 * @author buhuazhen
 * @date 2026/3/7
 * @email 3038525872@qq.com
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SavePlanNodeVo implements Serializable {
    private Long id;
    private Long projectManagementPlanId;
    private Integer sort;
    @NotBlank
    private String name;
    @NotNull
    private Long leaderId;
    @NotBlank
    private String leaderName;
    private Integer estimatedDuration;
    private String hourlyRate;
    private String workContent;
}
src/main/java/com/ruoyi/projectManagement/vo/SavePlanVo.java
@@ -4,6 +4,7 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
import java.util.List;
@@ -22,10 +23,11 @@
    @NotBlank
    private String name;
    @NotBlank
    private String describe;
    private String description;
    private List<String> attachmentIds;
    @Valid
    private List<SavePlanNodeVo> savePlanNodeList;
}
src/main/resources/mapper/projectManagement/PlanMapper.xml
@@ -7,17 +7,17 @@
    <resultMap id="BaseResultMap" type="com.ruoyi.projectManagement.pojo.Plan">
            <id property="id" column="id" jdbcType="BIGINT"/>
            <result property="name" column="name" jdbcType="VARCHAR"/>
            <result property="describe" column="describe" jdbcType="VARCHAR"/>
            <result property="description" column="description" jdbcType="VARCHAR"/>
            <result property="attachment" column="attachment" jdbcType="VARCHAR"/>
            <result property="is_delete" column="is_delete" jdbcType="INTEGER"/>
            <result property="create_time" column="create_time" jdbcType="TIMESTAMP"/>
            <result property="update_time" column="update_time" jdbcType="TIMESTAMP"/>
            <result property="create_user" column="create_user" jdbcType="VARCHAR"/>
            <result property="update_user" column="update_user" jdbcType="VARCHAR"/>
            <result property="isDelete" column="is_delete" jdbcType="INTEGER"/>
            <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
            <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
            <result property="createUser" column="create_user" jdbcType="VARCHAR"/>
            <result property="updateUser" column="update_user" jdbcType="VARCHAR"/>
    </resultMap>
    <sql id="Base_Column_List">
        id,name,describe,
        id,name,description,
        attachment,is_delete,create_time,
        update_time,create_user,update_user
    </sql>
@@ -25,6 +25,7 @@
    <select id="selectPlanPage" resultMap="BaseResultMap">
        select t1.*
        from project_management_plan as t1
            where t1.is_delete = 0
        order by t1.create_time desc
    </select>
</mapper>