diff --git a/src/main/java/com/sa/zentao/controller/ZtStoryExpandController.java b/src/main/java/com/sa/zentao/controller/ZtStoryExpandController.java
new file mode 100644
index 0000000..dc788dc
--- /dev/null
+++ b/src/main/java/com/sa/zentao/controller/ZtStoryExpandController.java
@@ -0,0 +1,63 @@
+package com.sa.zentao.controller;
+
+import com.sa.zentao.dao.Result;
+import com.sa.zentao.entity.ZtStoryExpand;
+import com.sa.zentao.service.IZtStoryExpandService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ *
+ * 前端控制器
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+@RestController
+@RequestMapping("/zt-story-expand")
+public class ZtStoryExpandController {
+
+ @Autowired
+ private IZtStoryExpandService ztStoryExpandService;
+
+ /**
+ * 打分接口
+ * @param entity 需求扩展信息
+ * @return 操作结果
+ */
+ @RequestMapping(value = "/saveScore", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
+ public Result saveScore(@RequestBody ZtStoryExpand entity) {
+ ztStoryExpandService.saveScore(entity);
+ return Result.success();
+ }
+
+ /**
+ * 按月份和项目查询(不分页)
+ * @param month 月份,格式:yyyy-MM
+ * @param projectId 产品项目 id(zt_project.id),可为空
+ * @return 需求扩展信息列表
+ */
+ @RequestMapping(value = "/queryByMonth", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
+ public Result> queryByMonth(String month, Integer projectId) {
+ List list = ztStoryExpandService.queryByMonth(month, projectId);
+ return Result.success(list);
+ }
+
+ /**
+ * 保存或更新(有数据了更新不新增)
+ * @param entity 需求扩展信息
+ * @return 操作结果
+ */
+ @RequestMapping(value = "/saveOrUpdate", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
+ public Result saveOrUpdate(@RequestBody ZtStoryExpand entity) {
+ ztStoryExpandService.saveOrUpdateExpand(entity);
+ return Result.success();
+ }
+
+}
diff --git a/src/main/java/com/sa/zentao/controller/ZtStoryMonthWorkloadController.java b/src/main/java/com/sa/zentao/controller/ZtStoryMonthWorkloadController.java
new file mode 100644
index 0000000..830c041
--- /dev/null
+++ b/src/main/java/com/sa/zentao/controller/ZtStoryMonthWorkloadController.java
@@ -0,0 +1,63 @@
+package com.sa.zentao.controller;
+
+
+import com.sa.zentao.dao.Result;
+import com.sa.zentao.entity.ZtStoryMonthWorkload;
+import com.sa.zentao.service.IZtStoryMonthWorkloadService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ *
+ * 前端控制器
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+@RestController
+@RequestMapping("/zt-story-month-workload")
+public class ZtStoryMonthWorkloadController {
+
+ @Autowired
+ private IZtStoryMonthWorkloadService ztStoryMonthWorkloadService;
+
+ /**
+ * 保存完成度
+ */
+ @RequestMapping(value = "/saveCompletionDegree", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
+ public Result saveCompletionDegree(@RequestBody ZtStoryMonthWorkload entity) {
+ ztStoryMonthWorkloadService.saveCompletionDegree(entity);
+ return Result.success();
+ }
+
+ /**
+ * 获取所有不重复的月份列表
+ */
+ @RequestMapping(value = "/listMonths", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
+ public Result> listMonths() {
+ return Result.success(ztStoryMonthWorkloadService.listMonths());
+ }
+
+ /**
+ * 按 storyId + 月份查询单条记录
+ */
+ @RequestMapping(value = "/getByMonth", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
+ public Result getByMonth(Integer storyId, String month) {
+ return Result.success(ztStoryMonthWorkloadService.getByStoryIdAndMonth(storyId, month));
+ }
+
+ /**
+ * 查询某需求在指定月份之前的历史最高完成度
+ */
+ @RequestMapping(value = "/maxWorkloadBefore", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
+ public Result maxWorkloadBefore(Integer storyId, String month) {
+ return Result.success(ztStoryMonthWorkloadService.getMaxWorkloadBefore(storyId, month));
+ }
+
+}
diff --git a/src/main/java/com/sa/zentao/controller/ZtTaskController.java b/src/main/java/com/sa/zentao/controller/ZtTaskController.java
index 259d091..aa23144 100644
--- a/src/main/java/com/sa/zentao/controller/ZtTaskController.java
+++ b/src/main/java/com/sa/zentao/controller/ZtTaskController.java
@@ -631,4 +631,16 @@ public class ZtTaskController {
return Result.success();
}
+
+ //更新任务deadline
+ @RequestMapping(value = "/updateDeadline", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
+ public Result updateDeadline(@RequestParam("id") Integer id,
+ @RequestParam("date") String date) throws ParseException {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ ZtTask ztTask = new ZtTask();
+ ztTask.setId(id);
+ ztTask.setDeadline(sdf.parse(date));
+ ztTaskService.updateById(ztTask);
+ return Result.success();
+ }
}
diff --git a/src/main/java/com/sa/zentao/entity/ZtStoryExpand.java b/src/main/java/com/sa/zentao/entity/ZtStoryExpand.java
new file mode 100644
index 0000000..e581440
--- /dev/null
+++ b/src/main/java/com/sa/zentao/entity/ZtStoryExpand.java
@@ -0,0 +1,104 @@
+package com.sa.zentao.entity;
+
+import java.math.BigDecimal;
+import java.io.Serializable;
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ *
+ *
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class ZtStoryExpand implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ @TableId(value = "id", type = IdType.AUTO)
+ private Integer id;
+
+ private Integer storyId;
+
+ /**
+ * 单元数量
+ */
+ private Integer numberUnits;
+
+ /**
+ * 单元业务复杂度
+ */
+ private String unitBusinessComplexity;
+
+ /**
+ * 技术复杂度系数
+ */
+ private String technicalComplexityCoefficient;
+
+ /**
+ * AI效率系数
+ */
+ private String aiEfficiencyCoefficient;
+
+ /**
+ * 评估工时
+ */
+ private BigDecimal evaluationTime;
+
+ /**
+ * 工作量指数
+ */
+ private String workloadIndex;
+
+ /**
+ * 需求状态 未开始 进行中(需求开始) 需求验收->已完成 / inProgress 进行中 finished 完成
+ */
+ private String requirementStatus;
+
+ /**
+ * 需求完成度
+ */
+ private String requirementCompletionDegree;
+
+ private Date createTime;
+
+ private Date updateTime;
+
+ private String createUser;
+
+ private String updateUser;
+
+ /**
+ * 需求名称(关联 zt_story.title,非数据库字段)
+ */
+ @TableField(exist = false)
+ private String storyTitle;
+
+ /**
+ * 创建人昵称(关联 zt_user.nickname,非数据库字段)
+ */
+ @TableField(exist = false)
+ private String createUserNickname;
+
+ /**
+ * 提交月份,格式 yyyy-MM(非数据库字段,用于进度提交)
+ */
+ @TableField(exist = false)
+ private String month;
+
+ /**
+ * 本月完成工时(关联 zt_story_month_workload.evaluation_time,非数据库字段)
+ */
+ @TableField(exist = false)
+ private BigDecimal monthEvaluationTime;
+
+
+}
diff --git a/src/main/java/com/sa/zentao/entity/ZtStoryMonthWorkload.java b/src/main/java/com/sa/zentao/entity/ZtStoryMonthWorkload.java
new file mode 100644
index 0000000..c2d25e3
--- /dev/null
+++ b/src/main/java/com/sa/zentao/entity/ZtStoryMonthWorkload.java
@@ -0,0 +1,56 @@
+package com.sa.zentao.entity;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.io.Serializable;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ *
+ *
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class ZtStoryMonthWorkload implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ @TableId(value = "id", type = IdType.AUTO)
+ private Integer id;
+
+ /**
+ * 月度
+ */
+ private String time;
+
+ /**
+ * 完成度 %
+ */
+ private BigDecimal workload;
+
+ private String createUser;
+
+ private Date createTime;
+
+ private Date updateTime;
+
+ private String updateUser;
+
+ private Integer storyId;
+
+ /**
+ * 评估工时(评估工时 × 完成度比例)
+ */
+ private BigDecimal evaluationTime;
+ /**
+ * 评估工时(评估工时 × 完成度比例)
+ */
+ private BigDecimal workloadIndex;
+}
diff --git a/src/main/java/com/sa/zentao/mapper/ZtStoryExpandMapper.java b/src/main/java/com/sa/zentao/mapper/ZtStoryExpandMapper.java
new file mode 100644
index 0000000..b0db850
--- /dev/null
+++ b/src/main/java/com/sa/zentao/mapper/ZtStoryExpandMapper.java
@@ -0,0 +1,26 @@
+package com.sa.zentao.mapper;
+
+import com.sa.zentao.entity.ZtStoryExpand;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+/**
+ *
+ * Mapper 接口
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+public interface ZtStoryExpandMapper extends BaseMapper {
+
+ /**
+ * 按月份和项目查询,关联 zt_story 获取需求名称
+ * @param month 月份,格式:yyyy-MM
+ * @param projectId 产品项目 id(zt_project.id),为 null 时不过滤
+ * @return 需求扩展信息列表(含 storyTitle)
+ */
+ List queryByMonthWithTitle(@Param("month") String month, @Param("projectId") Integer projectId);
+
+}
diff --git a/src/main/java/com/sa/zentao/mapper/ZtStoryMonthWorkloadMapper.java b/src/main/java/com/sa/zentao/mapper/ZtStoryMonthWorkloadMapper.java
new file mode 100644
index 0000000..e92a9b4
--- /dev/null
+++ b/src/main/java/com/sa/zentao/mapper/ZtStoryMonthWorkloadMapper.java
@@ -0,0 +1,16 @@
+package com.sa.zentao.mapper;
+
+import com.sa.zentao.entity.ZtStoryMonthWorkload;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ *
+ * Mapper 接口
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+public interface ZtStoryMonthWorkloadMapper extends BaseMapper {
+
+}
diff --git a/src/main/java/com/sa/zentao/qo/StoryQo.java b/src/main/java/com/sa/zentao/qo/StoryQo.java
index 817c37d..4ed6f48 100644
--- a/src/main/java/com/sa/zentao/qo/StoryQo.java
+++ b/src/main/java/com/sa/zentao/qo/StoryQo.java
@@ -18,6 +18,10 @@ public class StoryQo extends BaseQo {
private String assignedTo;
private String userName;
private String module;
+ /**
+ * 多个模块,逗号分割
+ */
+ private String modules;
private String searchVal;
private Integer productId;
private String openedby;
diff --git a/src/main/java/com/sa/zentao/qo/ZtProjectQo.java b/src/main/java/com/sa/zentao/qo/ZtProjectQo.java
index 89dc740..1ca53ac 100644
--- a/src/main/java/com/sa/zentao/qo/ZtProjectQo.java
+++ b/src/main/java/com/sa/zentao/qo/ZtProjectQo.java
@@ -120,4 +120,8 @@ public class ZtProjectQo extends BaseQo {
private String account;
//1 延期 2不延期
private Integer delayFlag=0 ;
+ /**
+ * 验收人
+ */
+ private String ysUser;
}
diff --git a/src/main/java/com/sa/zentao/service/IZtStoryExpandService.java b/src/main/java/com/sa/zentao/service/IZtStoryExpandService.java
new file mode 100644
index 0000000..59a6f49
--- /dev/null
+++ b/src/main/java/com/sa/zentao/service/IZtStoryExpandService.java
@@ -0,0 +1,38 @@
+package com.sa.zentao.service;
+
+import com.sa.zentao.entity.ZtStoryExpand;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import java.util.List;
+
+/**
+ *
+ * 服务类
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+public interface IZtStoryExpandService extends IService {
+
+ /**
+ * 保存或更新(有数据了更新不新增)
+ * @param entity 需求扩展信息
+ */
+ void saveOrUpdateExpand(ZtStoryExpand entity);
+
+ /**
+ * 按月份和项目查询(不分页,含需求名称)
+ * @param month 月份,格式:yyyy-MM
+ * @param projectId 产品项目 id,为 null 时不过滤
+ * @return 需求扩展信息列表(含 storyTitle)
+ */
+ List queryByMonth(String month, Integer projectId);
+
+ /**
+ * 打分接口
+ * @param entity 需求扩展信息
+ */
+ void saveScore(ZtStoryExpand entity);
+
+}
diff --git a/src/main/java/com/sa/zentao/service/IZtStoryMonthWorkloadService.java b/src/main/java/com/sa/zentao/service/IZtStoryMonthWorkloadService.java
new file mode 100644
index 0000000..04bfb3a
--- /dev/null
+++ b/src/main/java/com/sa/zentao/service/IZtStoryMonthWorkloadService.java
@@ -0,0 +1,40 @@
+package com.sa.zentao.service;
+
+import com.sa.zentao.entity.ZtStoryMonthWorkload;
+import com.baomidou.mybatisplus.extension.service.IService;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ *
+ * 服务类
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+public interface IZtStoryMonthWorkloadService extends IService {
+
+ /**
+ * 保存完成度
+ * @param entity 月度工作量信息
+ */
+ void saveCompletionDegree(ZtStoryMonthWorkload entity);
+
+ /**
+ * 获取所有不重复的月份列表
+ * @return 月份列表,格式 yyyy-MM
+ */
+ List listMonths();
+
+ /**
+ * 按 storyId + 月份查询单条记录
+ */
+ ZtStoryMonthWorkload getByStoryIdAndMonth(Integer storyId, String month);
+
+ /**
+ * 查询某需求在指定月份之前(不含)的历史最高完成度
+ */
+ BigDecimal getMaxWorkloadBefore(Integer storyId, String month);
+
+}
diff --git a/src/main/java/com/sa/zentao/service/impl/IZtCountService.java b/src/main/java/com/sa/zentao/service/impl/IZtCountService.java
index f7fe31f..00fe526 100644
--- a/src/main/java/com/sa/zentao/service/impl/IZtCountService.java
+++ b/src/main/java/com/sa/zentao/service/impl/IZtCountService.java
@@ -1139,7 +1139,7 @@ public class IZtCountService {
return BigDecimal.ZERO;
}
List list = this.meetingService.list(new QueryWrapper().lambda().in(ZtMeeting::getProductId, pIds)
- .like(ZtMeeting::getUsers, u.getAccount())
+ .and(w -> w.like(ZtMeeting::getUsers, u.getAccount()).or().like(ZtMeeting::getUsers, u.getNickname()))
.ge(ZtMeeting::getMeetingDate, start).le(ZtMeeting::getMeetingDate, end));
if (CollectionUtils.isEmpty(list)) {
return BigDecimal.ZERO;
@@ -1257,7 +1257,7 @@ public class IZtCountService {
return dto;
}
List list = this.meetingService.list(new QueryWrapper().lambda().in(ZtMeeting::getProductId, pIds)
- .like(ZtMeeting::getUsers, u.getAccount())
+ .and(w -> w.like(ZtMeeting::getUsers, u.getAccount()).or().like(ZtMeeting::getUsers, u.getNickname()))
.ge(ZtMeeting::getMeetingDate, start).le(ZtMeeting::getMeetingDate, end));
if (CollectionUtils.isEmpty(list)) {
return dto;
diff --git a/src/main/java/com/sa/zentao/service/impl/ZtKanbanlaneServiceImpl.java b/src/main/java/com/sa/zentao/service/impl/ZtKanbanlaneServiceImpl.java
index b08e45a..6fe6514 100644
--- a/src/main/java/com/sa/zentao/service/impl/ZtKanbanlaneServiceImpl.java
+++ b/src/main/java/com/sa/zentao/service/impl/ZtKanbanlaneServiceImpl.java
@@ -4,11 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.conf.RiskUserThreadLocal;
import com.sa.zentao.dao.*;
import com.sa.zentao.entity.*;
-import com.sa.zentao.enums.ActionStatus;
-import com.sa.zentao.enums.ActionType;
-import com.sa.zentao.enums.KanbanCellType;
-import com.sa.zentao.enums.KanbanColumnType;
-import com.sa.zentao.enums.ProjectTypeEnums;
+import com.sa.zentao.enums.*;
import com.sa.zentao.utils.KanbanStageMapping;
import com.sa.zentao.mapper.ZtKanbancolumnMapper;
import com.sa.zentao.mapper.ZtKanbanlaneMapper;
@@ -383,6 +379,13 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl o.getType().equals("verified")).collect(Collectors.toList()).get(0);
+ }else if (StoryStageEnums.productVerified.getValue().equals(st.getStage())){
+ ztKanbancolumnDTO = ztKanbancolumnDTOS.stream().filter(o -> o.getType().equals(StoryStageEnums.productVerified.getValue())).collect(Collectors.toList()).get(0);
+
+ }else if ( StoryStageEnums.productWaitVerified.getValue().equals(st.getStage())){
+ ztKanbancolumnDTO = ztKanbancolumnDTOS.stream().filter(o -> o.getType().equals(StoryStageEnums.productWaitVerified.getValue())).collect(Collectors.toList()).get(0);
+ }else{
+ ztKanbancolumnDTO = ztKanbancolumnDTOS.stream().filter(o -> o.getType().equals(st.getStage())).collect(Collectors.toList()).get(0);
}
ZtKanbancell kanbancell = this.kanbancellService.getOne(new QueryWrapper().lambda()
.eq(ZtKanbancell::getKanban, id).eq(ZtKanbancell::getColumn, ztKanbancolumnDTO.getId()));
diff --git a/src/main/java/com/sa/zentao/service/impl/ZtStoryExpandServiceImpl.java b/src/main/java/com/sa/zentao/service/impl/ZtStoryExpandServiceImpl.java
new file mode 100644
index 0000000..40a39cd
--- /dev/null
+++ b/src/main/java/com/sa/zentao/service/impl/ZtStoryExpandServiceImpl.java
@@ -0,0 +1,148 @@
+package com.sa.zentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sa.zentao.entity.ZtStoryExpand;
+import com.sa.zentao.entity.ZtStoryMonthWorkload;
+import com.sa.zentao.mapper.ZtStoryExpandMapper;
+import com.sa.zentao.service.IZtStoryExpandService;
+import com.sa.zentao.service.IZtStoryMonthWorkloadService;
+import com.sa.zentao.utils.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ *
+ * 服务实现类
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+@Service
+public class ZtStoryExpandServiceImpl extends ServiceImpl implements IZtStoryExpandService {
+
+ @Autowired
+ private IZtStoryMonthWorkloadService ztStoryMonthWorkloadService;
+ @Autowired
+ private IZtStoryExpandService ztStoryExpandService;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void saveOrUpdateExpand(ZtStoryExpand entity) {
+ if (entity == null || entity.getStoryId() == null) {
+ return;
+ }
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.eq("story_id", entity.getStoryId());
+ ZtStoryExpand exist = this.getOne(wrapper);
+
+ // 已完成的记录不允许再更新
+ if (exist != null && "finished".equals(exist.getRequirementStatus())) {
+ throw new RuntimeException("该需求已完成,不可再修改");
+ }
+
+ if (exist != null) {
+ entity.setId(exist.getId());
+ entity.setUpdateTime(new Date());
+ this.updateById(entity);
+ } else {
+ // 新增默认 inProgress
+ if (entity.getRequirementStatus() == null) {
+ entity.setRequirementStatus("inProgress");
+ }
+ entity.setCreateTime(new Date());
+ entity.setUpdateTime(new Date());
+ this.save(entity);
+ }
+
+ // finished 时:强制完成度100,计算剩余工作量写入 workload
+ if ("finished".equals(entity.getRequirementStatus())) {
+ entity.setMonth(DateUtils.formatDate(new Date(), "yyyy-MM"));
+ entity.setRequirementCompletionDegree("100");
+ writeWorkload(entity, exist);
+ }
+ // inProgress 时:不计算 workload
+ }
+
+ @Override
+ public List queryByMonth(String month, Integer projectId) {
+ return baseMapper.queryByMonthWithTitle(month, projectId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void saveScore(ZtStoryExpand entity) {
+ if (entity.getStoryId() == null) {
+ return;
+ }
+ // 根据完成度设置需求状态
+ if (entity.getRequirementCompletionDegree() != null) {
+ String degree = entity.getRequirementCompletionDegree().trim();
+ entity.setRequirementStatus("100".equals(degree) ? "finished" : "inProgress");
+ }
+ // 保存/更新 zt_story_expand
+ QueryWrapper expandWrapper = new QueryWrapper<>();
+ expandWrapper.eq("story_id", entity.getStoryId());
+ ZtStoryExpand exist = this.getOne(expandWrapper);
+ if (exist != null) {
+ entity.setId(exist.getId());
+ entity.setUpdateTime(new Date());
+ this.updateById(entity);
+ } else {
+ entity.setCreateTime(new Date());
+ entity.setUpdateTime(new Date());
+ this.save(entity);
+ }
+ // 同步写 zt_story_month_workload
+ if (entity.getRequirementCompletionDegree() != null && entity.getMonth() != null) {
+ writeWorkload(entity, exist);
+ }
+ }
+
+ /**
+ * 计算增量工作量并写入 zt_story_month_workload
+ * 增量 = workloadIndex × (本月完成度 - 历史最高) / 100
+ */
+ private void writeWorkload(ZtStoryExpand entity, ZtStoryExpand exist) {
+ BigDecimal degree = new BigDecimal(entity.getRequirementCompletionDegree().trim());
+ // 取工作量指数(优先用 DB 已有值)
+ String workloadIndexStr = (exist != null && exist.getWorkloadIndex() != null)
+ ? exist.getWorkloadIndex()
+ : entity.getWorkloadIndex();
+ BigDecimal baseWorkloadIndex = null;
+ if (workloadIndexStr != null && !workloadIndexStr.trim().isEmpty()) {
+ try {
+ baseWorkloadIndex = new BigDecimal(workloadIndexStr.trim());
+ } catch (NumberFormatException ignored) {}
+ }
+ // 历史最高完成度
+ BigDecimal maxBefore = ztStoryMonthWorkloadService.getMaxWorkloadBefore(entity.getStoryId(), entity.getMonth());
+ BigDecimal prevDegree = maxBefore != null ? maxBefore : BigDecimal.ZERO;
+ ZtStoryMonthWorkload workload = new ZtStoryMonthWorkload();
+ workload.setStoryId(entity.getStoryId());
+ workload.setWorkload(degree);
+ workload.setTime(entity.getMonth());
+ if (baseWorkloadIndex != null) {
+ BigDecimal increment = degree.subtract(prevDegree);
+ //暂时保留,冗余字段
+ workload.setEvaluationTime(
+ increment.compareTo(BigDecimal.ZERO) > 0
+ ? baseWorkloadIndex.multiply(increment).divide(new BigDecimal("100"), 2, java.math.RoundingMode.HALF_UP)
+ : BigDecimal.ZERO
+ );
+ workload.setWorkloadIndex(increment.compareTo(BigDecimal.ZERO) > 0
+ ? baseWorkloadIndex.multiply(increment).divide(new BigDecimal("100"), 2, java.math.RoundingMode.HALF_UP)
+ : BigDecimal.ZERO);
+ }
+ ztStoryMonthWorkloadService.saveCompletionDegree(workload);
+ entity.setRequirementCompletionDegree(degree.toString());
+ this.ztStoryExpandService.updateById(entity);
+ }
+
+}
diff --git a/src/main/java/com/sa/zentao/service/impl/ZtStoryMonthWorkloadServiceImpl.java b/src/main/java/com/sa/zentao/service/impl/ZtStoryMonthWorkloadServiceImpl.java
new file mode 100644
index 0000000..c99bb17
--- /dev/null
+++ b/src/main/java/com/sa/zentao/service/impl/ZtStoryMonthWorkloadServiceImpl.java
@@ -0,0 +1,97 @@
+package com.sa.zentao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sa.zentao.entity.ZtStoryMonthWorkload;
+import com.sa.zentao.mapper.ZtStoryMonthWorkloadMapper;
+import com.sa.zentao.service.IZtStoryMonthWorkloadService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ *
+ * 服务实现类
+ *
+ *
+ * @author gqb
+ * @since 2026-03-30
+ */
+@Service
+public class ZtStoryMonthWorkloadServiceImpl extends ServiceImpl implements IZtStoryMonthWorkloadService {
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void saveCompletionDegree(ZtStoryMonthWorkload entity) {
+ if (entity == null || entity.getStoryId() == null || entity.getTime() == null) {
+ return;
+ }
+ // 规则1:完成度不超过100
+ if (entity.getWorkload() != null && entity.getWorkload().compareTo(new BigDecimal("100")) > 0) {
+ throw new RuntimeException("完成度不能超过100%");
+ }
+ // 规则2:本月不能低于历史最高完成度
+ BigDecimal maxBefore = getMaxWorkloadBefore(entity.getStoryId(), entity.getTime());
+ if (maxBefore != null && entity.getWorkload() != null
+ && entity.getWorkload().compareTo(maxBefore) < 0) {
+ throw new RuntimeException("本月完成度(" + entity.getWorkload() + "%)不能低于历史最高(" + maxBefore + "%)");
+ }
+ // 规则3:按 storyId + time 唯一,同月覆盖
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.eq("story_id", entity.getStoryId()).eq("time", entity.getTime());
+ ZtStoryMonthWorkload exist = this.getOne(wrapper);
+ if (exist != null) {
+ entity.setId(exist.getId());
+ entity.setUpdateTime(new Date());
+ this.updateById(entity);
+ } else {
+ entity.setCreateTime(new Date());
+ entity.setUpdateTime(new Date());
+ this.save(entity);
+ }
+ }
+
+ @Override
+ public List listMonths() {
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.select("DISTINCT time").isNotNull("time").ne("time", "").orderByDesc("time");
+ return this.list(wrapper).stream()
+ .map(ZtStoryMonthWorkload::getTime)
+ .collect(java.util.stream.Collectors.toList());
+ }
+
+ @Override
+ public ZtStoryMonthWorkload getByStoryIdAndMonth(Integer storyId, String month) {
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.eq("story_id", storyId).eq("time", month);
+ return this.getOne(wrapper);
+ }
+
+ @Override
+ public BigDecimal getMaxWorkloadBefore(Integer storyId, String month) {
+ QueryWrapper wrapper = new QueryWrapper<>();
+ wrapper.eq("story_id", storyId)
+ .lt("time", month)
+ .orderByDesc("workload")
+ .last("LIMIT 1");
+ ZtStoryMonthWorkload record = this.getOne(wrapper);
+ return record != null ? record.getWorkload() : null;
+ }
+
+ private String getLastMonth(String yearMonth) {
+ String[] parts = yearMonth.split("-");
+ int year = Integer.parseInt(parts[0]);
+ int month = Integer.parseInt(parts[1]);
+ if (month == 1) {
+ year--;
+ month = 12;
+ } else {
+ month--;
+ }
+ return String.format("%d-%02d", year, month);
+ }
+
+}
diff --git a/src/main/java/com/sa/zentao/service/impl/ZtTaskServiceImpl.java b/src/main/java/com/sa/zentao/service/impl/ZtTaskServiceImpl.java
index 2b4f5a7..47a3427 100644
--- a/src/main/java/com/sa/zentao/service/impl/ZtTaskServiceImpl.java
+++ b/src/main/java/com/sa/zentao/service/impl/ZtTaskServiceImpl.java
@@ -650,6 +650,9 @@ public class ZtTaskServiceImpl extends ServiceImpl impleme
if (ztTask.getDeadline() == null) {
throw new BusinessException("当前环境异常请联系管理员");
}
+ if("test".equals(ztTask.getType())&&"reviewing".equals(ztTask.getStatus())){
+ ztTask.setStatus("wait");
+ }
this.baseMapper.insert(ztTask);
if (ztTask.getDeadline() != null && ztTask.getStory() != null && ztTask.getStory() != 0) {
ZtStory ztStory = this.storyService.getById(ztTask.getStory());
diff --git a/src/main/resources/mapper/ZtStoryExpandMapper.xml b/src/main/resources/mapper/ZtStoryExpandMapper.xml
new file mode 100644
index 0000000..bf8a544
--- /dev/null
+++ b/src/main/resources/mapper/ZtStoryExpandMapper.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/mapper/ZtStoryMapper.xml b/src/main/resources/mapper/ZtStoryMapper.xml
index 00b7abc..6eca629 100644
--- a/src/main/resources/mapper/ZtStoryMapper.xml
+++ b/src/main/resources/mapper/ZtStoryMapper.xml
@@ -407,7 +407,11 @@
left join zt_project pj on pstory.project = pj.id
left join zt_product pt on s.product = pt.id
left join zt_projectstory ps on s.id = ps.story
+ left join zt_user zu on s.ys_user = zu.account
WHERE 1=1
+
+ and zu.nickname like concat('%', #{qo.ysUser}, '%')
+
and s.pri = #{qo.pri}
@@ -417,7 +421,9 @@
and s.title like concat('%', #{qo.title}, '%')
-
+
+ and s.nickname like concat('%', #{qo.ysUser}, '%')
+
and s.ys_user = #{qo.userName}
@@ -932,7 +938,7 @@
from zt_story s
left join zt_product pt on s.product = pt.id
left join zt_storyreview v on s.id = v.story and s.version = v.version
-
+ left join zt_user zu on s.ys_user = zu.account
left join zt_projectstory ps on s.id = ps.story
@@ -940,7 +946,9 @@
WHERE 1=1
-
+
+ and zu.nickname like concat('%', #{qo.ysUser}, '%')
+
and s.test_user like concat('%', #{qo.testUser}, '%')
diff --git a/src/main/resources/mapper/ZtStoryMonthWorkloadMapper.xml b/src/main/resources/mapper/ZtStoryMonthWorkloadMapper.xml
new file mode 100644
index 0000000..eefe69b
--- /dev/null
+++ b/src/main/resources/mapper/ZtStoryMonthWorkloadMapper.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/mapper/ZtStoryUserMapper.xml b/src/main/resources/mapper/ZtStoryUserMapper.xml
index aa3acfa..995b650 100644
--- a/src/main/resources/mapper/ZtStoryUserMapper.xml
+++ b/src/main/resources/mapper/ZtStoryUserMapper.xml
@@ -141,7 +141,9 @@
s.old_status,
s.ys_user,
s.product_user,
- pt.name productName from zt_story_user s LEFT JOIN zt_product pt on s.product = pt.id WHERE 1=1
+ pt.name productName
+ from zt_story_user s
+ LEFT JOIN zt_product pt on s.product = pt.id WHERE 1=1
and s.product in
@@ -228,6 +230,9 @@
and s.module = #{qo.module}
+
+ and FIND_IN_SET(s.module, #{qo.modules})
+
and s.product = #{qo.productId}