Author SHA1 Message Date
guoqibing a4df7e06f6 绩效相关 2026-10-08 14:34:39 +08:00
guoqibing a98e297d90 计算修改 2026-07-22 15:53:16 +08:00
guoqibing 58e4e55a61 资源管理添加环境以及到期时间 2026-05-25 14:14:24 +08:00
guoqibing 739e864699 需求报表 2026-04-24 14:58:24 +08:00
guoqibing ea575012a8 看板,导出迭代,等 2026-01-27 16:30:21 +08:00
guoqibing 0e7e291f50 看板bug 2026-01-22 17:19:31 +08:00
guoqibing 151d4cbf5e 产品绩效,发布优化 2026-01-22 16:41:22 +08:00
guoqibing 5b8d440ff3 绩效看板等修改 2026-01-12 10:10:51 +08:00
guoqibing 7c379ea877 验收产品,测试人开发 2026-01-05 16:48:19 +08:00
guoqibing 5eb21276a9 合并代码,bug修复 2025-12-31 09:32:52 +08:00
guoqibing 3d815dc9e8 合并代码,bug修复 2025-12-30 17:21:21 +08:00
guoqibing 21f522cd1a Merge remote-tracking branch 'origin/dev-2025' into dev-2025
# Conflicts:
#	src/main/java/com/sa/zentao/service/impl/ZtTaskServiceImpl.java
#	src/main/resources/mapper/ZtStoryMapper.xml
2025-12-30 17:17:40 +08:00
guoqibing 679aa5f0c1 产品角色等 2025-12-30 09:43:05 +08:00
guoqibing 99c17f1ef1 需求增加筛选 2025-12-30 09:38:09 +08:00
guoqibing 563699a527 需求增加筛选 2025-12-30 09:38:04 +08:00
guoqibing e948b53e89 产品角色等 2025-12-23 10:17:39 +08:00
guoqibing 9a36facf01 需求增加筛选 2025-12-10 14:12:25 +08:00
guoqibing 3cd02cd773 需求增加筛选 2025-12-02 16:55:34 +08:00
132 changed files with 8169 additions and 732 deletions
+22 -14
View File
@@ -18,6 +18,17 @@
<boot.version>3.3.0</boot.version> <boot.version>3.3.0</boot.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.3</version>
</dependency>
<dependency> <dependency>
<groupId>com.itextpdf</groupId> <groupId>com.itextpdf</groupId>
@@ -88,13 +99,6 @@
<version>1.5.2</version> <version>1.5.2</version>
</dependency> </dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
<dependency> <dependency>
<groupId>com.tencentcloudapi</groupId> <groupId>com.tencentcloudapi</groupId>
@@ -278,15 +282,15 @@
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>1.18.8</version> <version>1.18.34</version>
</dependency> </dependency>
<dependency> <!-- <dependency>-->
<groupId>org.apache.commons</groupId> <!-- <groupId>org.apache.commons</groupId>-->
<artifactId>commons-pool2</artifactId> <!-- <artifactId>commons-pool2</artifactId>-->
<version>2.8.1</version> <!-- <version>2.8.1</version>-->
</dependency> <!-- </dependency>-->
</dependencies> </dependencies>
<build> <build>
@@ -296,7 +300,11 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<!-- 不硬编码 skipTests:需要跳过测试时用命令行 mvn -DskipTests ... -->
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
@@ -22,7 +22,7 @@ public class CodeGenerator {
//自己的名字 //自己的名字
static String Author = "gqb"; static String Author = "gqb";
public static String tableName = "car_driving_license"; public static String tableName = "zt_story_month_workload";
/** /**
* <p> * <p>
@@ -15,26 +15,24 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
@Value("spring.application.name") @Value("${spring.application.name}")
private String application; private String application;
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public Result bizExceptionHandler(Exception e, HttpServletRequest request) { public Result bizExceptionHandler(Exception e, HttpServletRequest request) {
log.error("发生业务异常!原因是: {}", e.getMessage()); log.error("系统发生异常!应用: {}, 请求路径: {}, 异常类型: {}, 异常原因: {}",
log.error(application +"发生了异常 ,异常信息 ,{}",e); application, request.getRequestURI(), e.getClass().getSimpleName(), e.getMessage(), e);
return Result.fail(Code.FAIL); return Result.fail(Code.FAIL);
} }
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
public Result bizExceptionHandler(BusinessException e, HttpServletRequest request) { public Result bizExceptionHandler(BusinessException e, HttpServletRequest request) {
log.error("发生业务异常!原因是: {}", e.getMessage()); log.error("业务异常!应用: {}, 请求路径: {}, 异常原因: {}",
log.error(application +"发生了异常 ,异常信息 ,{}",e); application, request.getRequestURI(), e.getMessage());
if(e.code==null){ if(e.code==null){
return Result.fail(e.value); return Result.fail(e.value);
}else{ }else{
return Result.fail(e.code); return Result.fail(e.code);
} }
} }
} }
@@ -17,9 +17,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
for (int i =0;i<200;i++){
log.info("win");
}
String os = System.getProperty("os.name"); String os = System.getProperty("os.name");
if (os.toLowerCase().startsWith("win")) { //如果是Windows系统 if (os.toLowerCase().startsWith("win")) { //如果是Windows系统
log.info("win"); log.info("win");
@@ -0,0 +1,37 @@
package com.sa.zentao.constants;
/**
* 导出相关常量
*/
public class ExportConstants {
/**
* 导出最大记录数
*/
public static final int MAX_EXPORT_SIZE = 1000000;
/**
* Excel sheet 名称
*/
public static final String SHEET_NAME = "sheet1";
/**
* URL 编码空格
*/
public static final String URL_SPACE_ENCODE = "%20";
/**
* 字符串分隔符
*/
public static final String STRING_SEPARATOR = ":";
/**
* ID 分隔符
*/
public static final String ID_SEPARATOR = ",";
/**
* 逗号替换为空字符串
*/
public static final String COMMA_REPLACE = "";
}
@@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -106,6 +107,7 @@ public class CommonsController {
f.setTitle(title); f.setTitle(title);
f.setSize(bytes.length); f.setSize(bytes.length);
f.setPathname(baseUrl+"/zentao/img/"+fName); f.setPathname(baseUrl+"/zentao/img/"+fName);
f.setUrl("/zentao/img/"+fName); // url 存相对路径:经前端源/代理可达,避免绝对 baseUrl 在本地环境不可达(与 uploadBind 一致)
f.setObjectid(file.getObjectId()); f.setObjectid(file.getObjectId());
fileService.save(f); fileService.save(f);
@@ -115,6 +117,77 @@ public class CommonsController {
} }
} }
/**
* 上传+绑定一步完成(AI 无表单场景):写磁盘+插 zt_file(含 objectType/objectID/url),
* 并按 objectType 刷新主表访问链接字段(zt_story 7 url 列+code_review_status / zt_meeting.url)
* @param file 入参:file 文件、objectType 类型、objectId 主表id、title 可选、reviewResult 可选(aiCodeReview 用)
* @return zt_file 记录(含 id 与 url)
*/
@PostMapping("/uploadBind")
@Transactional
public Result<ZtFile> uploadBind(UploadDTO file){
//上传必须登录(AI 通道带 ai token;UI 带用户 token),文件/动态创建人取 token 身份(CHG-061)
if (RiskUserThreadLocal.get() == null || StringUtils.isEmpty(RiskUserThreadLocal.get().getName())) {
return Result.fail("请登录(上传需携带有效token)");
}
if (file.getFile().isEmpty()) {
return Result.fail(Code.FAIL);
}
if (StringUtils.isEmpty(file.getObjectType()) || file.getObjectId() == null) {
return Result.fail("objectType/objectId不能为空");
}
// CHG-077:story 一律归一为 storyPrd(uploadBind 仅 AI 通道使用;UI 手动附件走两段式上传不进此接口)
file.normalizeObjectTypeForBind();
try {
// 获取文件名
String fName = file.getFile().getOriginalFilename();
String title=StringUtils.isEmpty(file.getTitle())?fName:file.getTitle();
fName=fName.substring(fName.lastIndexOf("."),fName.length());
String extension=fName.toString();
fName= DateUtils.formatDate(new Date(),"yyyyMMddHHmmss")+UUID.randomUUID().toString().replaceAll("-","")+fName;
// 获取文件的字节
byte[] bytes = file.getFile().getBytes();
String p ="";
String os = System.getProperty("os.name");
if (os.toLowerCase().startsWith("win")) { //如果是Windows系统
log.info("win");
os=windowsFilePath +"\\";
}else{ //linux和mac系统
log.info("linux");
os=linuxFilePath+"/";
}
String url = "/zentao/img/" + fName; // 相对路径:经前端源/代理可达,规避跨域(baseUrl 绝对地址浏览器未必可达)
// 先校验 objectType/objectId 并刷新主表字段,非法则抛异常整体回滚不落盘
fileService.refreshOwnerUrl(file.getObjectType(), file.getObjectId(), url, file.getReviewResult());
// 使用文件名在服务器端创建文件
java.nio.file.Path path = java.nio.file.Paths.get(os + fName);
// 写入文件到服务器的指定目录
java.nio.file.Files.write(path, bytes);
ZtFile f=new ZtFile();
f.setAddedby(RiskUserThreadLocal.get()==null?"ai":RiskUserThreadLocal.get().getName());
f.setAddeddate(new Date());
f.setDeleted("0");
f.setExtension(extension);
f.setTitle(title);
f.setSize(bytes.length);
f.setPathname(url);
f.setUrl(url);
f.setObjecttype(file.getObjectType());
f.setObjectid(file.getObjectId());
fileService.save(f);
return Result.success(f);
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
throw new BusinessException("文件上传失败");
}
}
@PostMapping("/copyFile") @PostMapping("/copyFile")
public Result copyFile( UploadDTO file){ public Result copyFile( UploadDTO file){
String files = file.getFiles(); String files = file.getFiles();
@@ -190,9 +263,11 @@ public class CommonsController {
String substring = pathname.substring(pathname.lastIndexOf("/")+1, pathname.length()); String substring = pathname.substring(pathname.lastIndexOf("/")+1, pathname.length());
try { try {
File f = new File(linuxFilePath+"/"+substring); // 与上传保持一致的 OS 路径选择(原硬编码 linuxFilePath 导致 Windows 本地下载全部失败)
// File f = new File("D:\\1.bmp"); String osName = System.getProperty("os.name");
InputStream fis = new BufferedInputStream(new FileInputStream(linuxFilePath+"/"+substring)); String basePath = osName.toLowerCase().startsWith("win") ? windowsFilePath : linuxFilePath;
File f = new File(basePath + File.separator + substring);
InputStream fis = new BufferedInputStream(new FileInputStream(f));
// InputStream fis = new BufferedInputStream(new FileInputStream("D:\\1.bmp")); // InputStream fis = new BufferedInputStream(new FileInputStream("D:\\1.bmp"));
byte[] buffer = new byte[fis.available()]; byte[] buffer = new byte[fis.available()];
@@ -4,18 +4,22 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.dao.*; import com.sa.zentao.dao.*;
import com.sa.zentao.entity.ZtProject; import com.sa.zentao.entity.ZtProject;
import com.sa.zentao.qo.ZtCountQo; import com.sa.zentao.qo.ZtCountQo;
import com.sa.zentao.qo.ZtDocCheckQo;
import com.sa.zentao.qo.ZtProjectQo; import com.sa.zentao.qo.ZtProjectQo;
import com.sa.zentao.service.IZtDocCheckService;
import com.sa.zentao.service.IZtProjectService; import com.sa.zentao.service.IZtProjectService;
import com.sa.zentao.service.impl.IZtCountService; import com.sa.zentao.service.impl.IZtCountService;
import com.sa.zentao.utils.DateUtils; import com.sa.zentao.utils.DateUtils;
import jakarta.servlet.ServletOutputStream; import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -35,6 +39,9 @@ public class ZtCountController {
@Autowired @Autowired
private IZtProjectService projectService; private IZtProjectService projectService;
@Autowired
private IZtDocCheckService docCheckService;
/** /**
* 开发看到的 * 开发看到的
@@ -205,4 +212,48 @@ public class ZtCountController {
return Result.success(countService.monthScopeByProgram(qo)); return Result.success(countService.monthScopeByProgram(qo));
} }
//工作量指标完成率(FR-012 三期):Σ(当月 workload 增量) ÷ (团队可用工作天数×5),测试人员不计入产出方
@RequestMapping(value = "/workloadRate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
public Result workloadRate(@RequestParam(required = false) String month, @RequestParam Integer projectId){
return Result.success(countService.workloadRate(month, projectId));
}
//手动触发文档齐备快照(FR-014 三期),月末 28 日 02:00 另有定时预生成
@RequestMapping(value = "/generateDocCheck", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result generateDocCheck(@RequestBody ZtDocCheckQo qo){
String month = StringUtils.isEmpty(qo.getMonth()) ? DateUtils.formatDate(new Date(), "yyyy-MM") : qo.getMonth();
return Result.success(docCheckService.generateMonthSnapshot(month));
}
//文档齐备申诉(FR-014):状态流 pending → appealing
@RequestMapping(value = "/docCheckAppeal", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result docCheckAppeal(@RequestBody ZtDocCheckQo qo){
docCheckService.appeal(qo.getStoryId(), qo.getMonth(), qo.getDocType(), qo.getReason());
return Result.success();
}
//文档齐备申诉复核(FR-014):appealing → resolved,复核通过回滚扣分
@RequestMapping(value = "/docCheckAppealResolve", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result docCheckAppealResolve(@RequestBody ZtDocCheckQo qo){
docCheckService.resolveAppeal(qo.getStoryId(), qo.getMonth(), qo.getDocType(),
qo.getPass() != null && qo.getPass(), qo.getRemark());
return Result.success();
}
//文档齐备矩阵页数据(FR-014 三期):按需求聚合五类判定/扣分/状态
@RequestMapping(value = "/docCheckMatrix", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
public Result docCheckMatrix(String month){
if (StringUtils.isEmpty(month)) {
month = DateUtils.formatDate(new Date(), "yyyy-MM");
}
return Result.success(docCheckService.listMatrix(month));
}
//月度确认(FR-014 三期):项目经理确认后该月快照锁定
@RequestMapping(value = "/docCheckConfirm", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result docCheckConfirm(@RequestBody ZtDocCheckQo qo){
docCheckService.confirmMonth(qo.getMonth());
return Result.success();
}
} }
@@ -0,0 +1,176 @@
package com.sa.zentao.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.conf.RiskUserThreadLocal;
import com.sa.zentao.dao.BusinessException;
import com.sa.zentao.dao.Result;
import com.sa.zentao.entity.ZtPerfConfig;
import com.sa.zentao.qo.ZtPerfQo;
import com.sa.zentao.service.IZtPerfConfigService;
import com.sa.zentao.service.IZtPerfReportService;
import com.sa.zentao.service.perf.PerfMonthScope;
import org.apache.commons.lang3.StringUtils;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* <p>
* 绩效报表(三期批2:报表引擎+接口) 前端控制器
* </p>
*
* @author ai
* @since 2026-07-28
*/
@RestController
@RequestMapping("/zt-perf")
public class ZtPerfController {
@Autowired
private IZtPerfReportService perfReportService;
@Autowired
private IZtPerfConfigService perfConfigService;
/**
* 按岗位 tab 报表数据(指标/权重/得分/数据来源/明细)
*/
@RequestMapping(value = "/report", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
public Result<PerfMonthScope> report(@RequestParam String month, @RequestParam String role, @RequestParam(required = false) String account) {
return Result.success(perfReportService.report(month, role, account));
}
/**
* 批量生成/重算月度得分写 zt_month_score.scopeJson
*/
@RequestMapping(value = "/generateMonthScore", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result generateMonthScore(@RequestBody ZtPerfQo qo) {
perfReportService.generateMonthScore(qo.getMonth(), qo.getRole(), qo.getAccount());
return Result.success();
}
/**
* 人工评分合并写 scopeJson
*/
@RequestMapping(value = "/manualScore", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result manualScore(@RequestBody ZtPerfQo qo) {
perfReportService.manualScore(qo);
return Result.success();
}
/**
* 申诉登记(写 scopeJson.appeals)
*/
@RequestMapping(value = "/appeal", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result appeal(@RequestBody ZtPerfQo qo) {
perfReportService.appeal(qo);
return Result.success();
}
/**
* 申诉复核:upheld 维持 / overturned 撤销回滚
*/
@RequestMapping(value = "/appealReview", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result appealReview(@RequestBody ZtPerfQo qo) {
perfReportService.appealReview(qo);
return Result.success();
}
/**
* 绩效规则配置列表(可按岗位过滤)
*/
@RequestMapping(value = "/config/list", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
public Result<List<ZtPerfConfig>> configList(@RequestParam(required = false) String role) {
QueryWrapper<ZtPerfConfig> wrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(role)) {
wrapper.eq("role", role);
}
wrapper.orderByAsc("role", "id");
return Result.success(perfConfigService.list(wrapper));
}
/**
* 新增配置(仅 admin)
*/
@RequestMapping(value = "/config/add", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result configAdd(@RequestBody ZtPerfQo qo) {
checkAdmin();
ZtPerfConfig config = buildConfig(qo, new ZtPerfConfig());
Date now = new Date();
String operator = RiskUserThreadLocal.get().getName();
config.setCreateTime(now);
config.setCreateUser(operator);
config.setUpdateTime(now);
config.setUpdateUser(operator);
perfConfigService.save(config);
return Result.success();
}
/**
* 修改配置(仅 admin)
*/
@RequestMapping(value = "/config/updateById", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result configUpdate(@RequestBody ZtPerfQo qo) {
checkAdmin();
if (StringUtils.isBlank(qo.getId())) {
throw new BusinessException("id 不能为空");
}
ZtPerfConfig config = perfConfigService.getById(Integer.valueOf(qo.getId()));
if (config == null) {
throw new BusinessException("配置不存在:" + qo.getId());
}
buildConfig(qo, config);
config.setUpdateTime(new Date());
config.setUpdateUser(RiskUserThreadLocal.get().getName());
perfConfigService.updateById(config);
return Result.success();
}
/**
* 删除配置(仅 admin)
*/
@RequestMapping(value = "/config/delete", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result configDelete(@RequestBody ZtPerfQo qo) {
checkAdmin();
if (StringUtils.isBlank(qo.getId())) {
throw new BusinessException("id 不能为空");
}
perfConfigService.removeById(Integer.valueOf(qo.getId()));
return Result.success();
}
private ZtPerfConfig buildConfig(ZtPerfQo qo, ZtPerfConfig config) {
if (StringUtils.isNotBlank(qo.getRole())) {
config.setRole(qo.getRole());
}
if (StringUtils.isNotBlank(qo.getItemKey())) {
config.setItemKey(qo.getItemKey());
}
if (StringUtils.isNotBlank(qo.getItemName())) {
config.setItemName(qo.getItemName());
}
if (qo.getWeight() != null) {
config.setWeight(BigDecimal.valueOf(qo.getWeight()));
}
if (StringUtils.isNotBlank(qo.getRuleJson())) {
config.setRuleJson(qo.getRuleJson());
}
if (qo.getEnabled() != null) {
config.setEnabled(qo.getEnabled());
}
return config;
}
private void checkAdmin() {
if (!"admin".equals(RiskUserThreadLocal.get().getName())) {
throw new BusinessException("仅管理员可维护绩效配置");
}
}
}
@@ -160,4 +160,9 @@ public class ZtProductController {
public Result projectTeamById(@RequestBody ZtProjectQo qo){ public Result projectTeamById(@RequestBody ZtProjectQo qo){
return Result.success(this.ztProductService.productTeamByPid(qo)); return Result.success(this.ztProductService.productTeamByPid(qo));
} }
@RequestMapping(value = "/getExecutionByProductId", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result getExecutionByProductId(@RequestBody ZtProjectQo qo){
return Result.success(ztProductService.getExecutionByProductId(qo));
}
} }
@@ -8,14 +8,18 @@ import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.metadata.WriteSheet; import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.sa.zentao.constants.ExportConstants;
import com.sa.zentao.dao.*; import com.sa.zentao.dao.*;
import com.sa.zentao.entity.ZtProject; import com.sa.zentao.entity.ZtProject;
import com.sa.zentao.entity.ZtProjectproduct; import com.sa.zentao.entity.ZtProjectproduct;
import com.sa.zentao.entity.ZtStoryFeedback; import com.sa.zentao.entity.ZtStoryFeedback;
import com.sa.zentao.entity.ZtStoryUser;
import com.sa.zentao.enums.StoryStageEnums; import com.sa.zentao.enums.StoryStageEnums;
import com.sa.zentao.enums.StoryStatusEnums; import com.sa.zentao.enums.StoryStatusEnums;
import com.sa.zentao.enums.UserStoryEnums; import com.sa.zentao.enums.UserStoryEnums;
import com.sa.zentao.mapper.ZtProjectMapper;
import com.sa.zentao.qo.SearchQo; import com.sa.zentao.qo.SearchQo;
import com.sa.zentao.qo.StoryQo; import com.sa.zentao.qo.StoryQo;
import com.sa.zentao.qo.ZtProjectQo; import com.sa.zentao.qo.ZtProjectQo;
@@ -26,6 +30,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -60,6 +65,8 @@ public class ZtStoryController {
@Value("${file.backUrl}") @Value("${file.backUrl}")
private String url; private String url;
@Autowired
private ZtProjectMapper ztProjectMapper;
@RequestMapping(value = "/pageList", method = RequestMethod.POST, produces = "application/json; charset=UTF-8") @RequestMapping(value = "/pageList", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result<ZtStoryDTO> storyPageList(@RequestBody ZtProjectQo qo){ public Result<ZtStoryDTO> storyPageList(@RequestBody ZtProjectQo qo){
@@ -68,7 +75,7 @@ public class ZtStoryController {
@RequestMapping(value = "/storyExport", method = RequestMethod.POST) @RequestMapping(value = "/storyExport", method = RequestMethod.POST)
public void storyExport(@RequestBody ZtProjectQo qo, jakarta.servlet.ServletResponse response){ public void storyExport(@RequestBody ZtProjectQo qo, jakarta.servlet.ServletResponse response){
qo.setPageSize(1000000); qo.setPageSize(ExportConstants.MAX_EXPORT_SIZE);
PageInfo<ZtStoryDTO> p = ztStoryService.pageList(qo); PageInfo<ZtStoryDTO> p = ztStoryService.pageList(qo);
List<ZtStoryDTO> list = p.getList(); List<ZtStoryDTO> list = p.getList();
List<Integer> feedbackIds = list.stream().filter(o -> o.getFeedback() != null && o.getFeedback() != 0).map(o -> o.getFeedback()).collect(Collectors.toList()); List<Integer> feedbackIds = list.stream().filter(o -> o.getFeedback() != null && o.getFeedback() != 0).map(o -> o.getFeedback()).collect(Collectors.toList());
@@ -79,12 +86,12 @@ public class ZtStoryController {
ZtStoryFeedback ztStoryFeedback = feedbackMap.get(ztStoryUserDTO.getFeedback()); ZtStoryFeedback ztStoryFeedback = feedbackMap.get(ztStoryUserDTO.getFeedback());
if (ztStoryFeedback != null) { if (ztStoryFeedback != null) {
String reName=ztStoryFeedback.getId()+":"+ztStoryFeedback.getSpec(); String reName=ztStoryFeedback.getId()+ExportConstants.STRING_SEPARATOR+ztStoryFeedback.getSpec();
String reUrl = url+"/#/product-feedback-info/"+ztStoryFeedback.getId(); String reUrl = url+"/#/product-feedback-info/"+ztStoryFeedback.getId();
WriteCellData<String> hyperlink = new WriteCellData<>(reName); WriteCellData<String> hyperlink = new WriteCellData<>(reName);
HyperlinkData hyperlinkData = new HyperlinkData(); HyperlinkData hyperlinkData = new HyperlinkData();
try { try {
hyperlinkData.setAddress(reUrl.replace(" ", "%20")); hyperlinkData.setAddress(reUrl.replace(" ", ExportConstants.URL_SPACE_ENCODE));
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL); hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
hyperlink.setHyperlinkData(hyperlinkData); hyperlink.setHyperlinkData(hyperlinkData);
ztStoryUserDTO.setFeedbackSpecUrl(hyperlink ); ztStoryUserDTO.setFeedbackSpecUrl(hyperlink );
@@ -93,12 +100,12 @@ public class ZtStoryController {
} }
} }
if(ztStoryUserDTO.getParent()!=null&&ztStoryUserDTO.getParent()!=0){ if(ztStoryUserDTO.getParent()!=null&&ztStoryUserDTO.getParent()!=0){
String reName = ztStoryUserDTO.getParent()+":"+ztStoryUserDTO.getParentName(); String reName = ztStoryUserDTO.getParent()+ExportConstants.STRING_SEPARATOR+ztStoryUserDTO.getParentName();
String reUrl = url+"/#/product-story-info/"+ztStoryUserDTO.getParent(); String reUrl = url+"/#/product-story-info/"+ztStoryUserDTO.getParent();
WriteCellData<String> hyperlink = new WriteCellData<>(reName); WriteCellData<String> hyperlink = new WriteCellData<>(reName);
HyperlinkData hyperlinkData = new HyperlinkData(); HyperlinkData hyperlinkData = new HyperlinkData();
try { try {
hyperlinkData.setAddress(reUrl.replace(" ", "%20")); hyperlinkData.setAddress(reUrl.replace(" ", ExportConstants.URL_SPACE_ENCODE));
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL); hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
hyperlink.setHyperlinkData(hyperlinkData); hyperlink.setHyperlinkData(hyperlinkData);
ztStoryUserDTO.setParentNameUrl(hyperlink); ztStoryUserDTO.setParentNameUrl(hyperlink);
@@ -107,12 +114,12 @@ public class ZtStoryController {
} }
} }
if(ztStoryUserDTO.getUserStory()!=null&&ztStoryUserDTO.getUserStory()!=0){ if(ztStoryUserDTO.getUserStory()!=null&&ztStoryUserDTO.getUserStory()!=0){
String reName=ztStoryUserDTO.getUserStory()+":"+ztStoryUserDTO.getUserStoryName(); String reName=ztStoryUserDTO.getUserStory()+ExportConstants.STRING_SEPARATOR+ztStoryUserDTO.getUserStoryName();
String reUrl = url+"/#/product-user-story-info/"+ztStoryUserDTO.getUserStory(); String reUrl = url+"/#/product-user-story-info/"+ztStoryUserDTO.getUserStory();
WriteCellData<String> hyperlink = new WriteCellData<>(reName); WriteCellData<String> hyperlink = new WriteCellData<>(reName);
HyperlinkData hyperlinkData = new HyperlinkData(); HyperlinkData hyperlinkData = new HyperlinkData();
try { try {
hyperlinkData.setAddress(reUrl.replace(" ", "%20")); hyperlinkData.setAddress(reUrl.replace(" ", ExportConstants.URL_SPACE_ENCODE));
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL); hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
hyperlink.setHyperlinkData(hyperlinkData); hyperlink.setHyperlinkData(hyperlinkData);
ztStoryUserDTO.setUserStoryNameUrl(hyperlink); ztStoryUserDTO.setUserStoryNameUrl(hyperlink);
@@ -120,12 +127,12 @@ public class ZtStoryController {
ztStoryUserDTO.setUserStoryNameUrl(null); ztStoryUserDTO.setUserStoryNameUrl(null);
} }
} }
String reName=ztStoryUserDTO.getId()+":"+ztStoryUserDTO.getTitle(); String reName=ztStoryUserDTO.getId()+ExportConstants.STRING_SEPARATOR+ztStoryUserDTO.getTitle();
String reUrl = url+"/#/product-story-info/"+ztStoryUserDTO.getId(); String reUrl = url+"/#/product-story-info/"+ztStoryUserDTO.getId();
WriteCellData<String> hyperlink = new WriteCellData<>(reName); WriteCellData<String> hyperlink = new WriteCellData<>(reName);
HyperlinkData hyperlinkData = new HyperlinkData(); HyperlinkData hyperlinkData = new HyperlinkData();
try { try {
hyperlinkData.setAddress(reUrl.replace(" ", "%20")); hyperlinkData.setAddress(reUrl.replace(" ", ExportConstants.URL_SPACE_ENCODE));
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL); hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
hyperlink.setHyperlinkData(hyperlinkData); hyperlink.setHyperlinkData(hyperlinkData);
ztStoryUserDTO.setStoryUrl(hyperlink); ztStoryUserDTO.setStoryUrl(hyperlink);
@@ -134,7 +141,7 @@ public class ZtStoryController {
} }
if(!StringUtils.isEmpty(ztStoryUserDTO.getReviewedbyName())){ if(!StringUtils.isEmpty(ztStoryUserDTO.getReviewedbyName())){
ztStoryUserDTO.setReviewedbyName(ztStoryUserDTO.getReviewedbyName().replaceAll(",","")); ztStoryUserDTO.setReviewedbyName(ztStoryUserDTO.getReviewedbyName().replaceAll(ExportConstants.ID_SEPARATOR, ExportConstants.COMMA_REPLACE));
} }
ztStoryUserDTO.setStatus(StoryStatusEnums.transfer(ztStoryUserDTO.getStatus())==null?null:StoryStatusEnums.transfer(ztStoryUserDTO.getStatus()).getDesc()); ztStoryUserDTO.setStatus(StoryStatusEnums.transfer(ztStoryUserDTO.getStatus())==null?null:StoryStatusEnums.transfer(ztStoryUserDTO.getStatus()).getDesc());
ztStoryUserDTO.setStage(StoryStageEnums.transfer(ztStoryUserDTO.getStage())==null?null:StoryStageEnums.transfer(ztStoryUserDTO.getStage()).getDesc()); ztStoryUserDTO.setStage(StoryStageEnums.transfer(ztStoryUserDTO.getStage())==null?null:StoryStageEnums.transfer(ztStoryUserDTO.getStage()).getDesc());
@@ -142,10 +149,10 @@ public class ZtStoryController {
ztStoryUserDTO.setYsFlagName(ztStoryUserDTO.getYsFlag()==1?"通过":"不通过"); ztStoryUserDTO.setYsFlagName(ztStoryUserDTO.getYsFlag()==1?"通过":"不通过");
} }
if(!StringUtils.isEmpty(ztStoryUserDTO.getUserStoryName())){ if(!StringUtils.isEmpty(ztStoryUserDTO.getUserStoryName())){
ztStoryUserDTO.setUserStoryName(ztStoryUserDTO.getUserStoryId()+":"+ztStoryUserDTO.getUserStoryName()); ztStoryUserDTO.setUserStoryName(ztStoryUserDTO.getUserStoryId()+ExportConstants.STRING_SEPARATOR+ztStoryUserDTO.getUserStoryName());
} }
if(ztStoryUserDTO.getParent()!=null&&ztStoryUserDTO.getParent()!=0){ if(ztStoryUserDTO.getParent()!=null&&ztStoryUserDTO.getParent()!=0){
ztStoryUserDTO.setParentName(ztStoryUserDTO.getParent()+":"+ztStoryUserDTO.getParentName()); ztStoryUserDTO.setParentName(ztStoryUserDTO.getParent()+ExportConstants.STRING_SEPARATOR+ztStoryUserDTO.getParentName());
} }
} }
@@ -156,7 +163,7 @@ public class ZtStoryController {
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.needHead(Boolean.TRUE).build(); .needHead(Boolean.TRUE).build();
WriteSheet writeSheet = EasyExcel.writerSheet(0, "sheet1") WriteSheet writeSheet = EasyExcel.writerSheet(0, ExportConstants.SHEET_NAME)
.head(ZtStoryDTO.class) .head(ZtStoryDTO.class)
.needHead(Boolean.TRUE).build(); .needHead(Boolean.TRUE).build();
@@ -175,6 +182,47 @@ public class ZtStoryController {
} }
@RequestMapping(value = "/projectStoryExport", method = RequestMethod.POST)
public void projectStoryExport(@RequestBody ZtProjectQo qo, jakarta.servlet.ServletResponse response){
qo.setPageSize(ExportConstants.MAX_EXPORT_SIZE);
PageInfo<ZtStoryDTO> p = ztStoryService.projectStoryPageList(qo);
List<ZtStoryDTO> list = p.getList();
List<ProjectStoryExportDTO> exportList = new java.util.ArrayList<>();
for (ZtStoryDTO dto : list) {
ProjectStoryExportDTO exportDTO = new ProjectStoryExportDTO();
exportDTO.setId(dto.getId());
exportDTO.setTitle(dto.getTitle());
exportDTO.setPri(dto.getPri());
exportDTO.setStatus(StoryStatusEnums.transfer(dto.getStatus()) == null ? null : StoryStatusEnums.transfer(dto.getStatus()).getDesc());
exportDTO.setPlanEndDate(dto.getPlanEndDate());
exportDTO.setStage(StoryStageEnums.transfer(dto.getStage()) == null ? null : StoryStageEnums.transfer(dto.getStage()).getDesc());
exportDTO.setAssignedToName(dto.getAssignedToName());
exportDTO.setYsUserName(dto.getYsUserName());
exportDTO.setTaskCount(dto.getTaskCount());
exportDTO.setOpenedbyName(dto.getOpenedbyName());
exportDTO.setOpeneddate(dto.getOpeneddate());
exportList.add(exportDTO);
}
ExcelWriter excelWriter = null;
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
try {
excelWriter = EasyExcel.write(response.getOutputStream())
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.needHead(Boolean.TRUE).build();
WriteSheet writeSheet = EasyExcel.writerSheet(0, ExportConstants.SHEET_NAME)
.head(ProjectStoryExportDTO.class)
.needHead(Boolean.TRUE).build();
excelWriter.write(exportList, writeSheet);
excelWriter.finish();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@RequestMapping(value = "/myStoryPageList", method = RequestMethod.POST, produces = "application/json; charset=UTF-8") @RequestMapping(value = "/myStoryPageList", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result<ZtStoryDTO> myStoryPageList(@RequestBody ZtProjectQo qo){ public Result<ZtStoryDTO> myStoryPageList(@RequestBody ZtProjectQo qo){
@@ -182,6 +230,27 @@ public class ZtStoryController {
} }
/**
* 测试提交验收
* @param qo
* @return
*/
@RequestMapping(value = "/testSubmitVerified", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result<ZtStoryDTO> testSubmitVerified(@RequestBody ZtStoryDTO qo){
ztStoryService.testSubmitVerified(qo);
return Result.success();
}
/**
* 产品验收通过
* @param qo
* @return
*/
@RequestMapping(value = "/storyProductUserYs", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result<ZtStoryDTO> storyProductUserYs(@RequestBody ZtStoryDTO qo){
ztStoryService.storyProductUserYs(qo);
return Result.success();
}
@RequestMapping(value = "/storyListByProject", method = RequestMethod.POST, produces = "application/json; charset=UTF-8") @RequestMapping(value = "/storyListByProject", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result<ZtStoryDTO> storyListByProject(@RequestBody ZtProjectQo qo){ public Result<ZtStoryDTO> storyListByProject(@RequestBody ZtProjectQo qo){
@@ -314,9 +383,7 @@ public class ZtStoryController {
return Result.success(searchDTOPageInfo); return Result.success(searchDTOPageInfo);
} }
//迭代列表根据项目id 或者产品id
//迭代列表根据项目id
@RequestMapping(value = "/execListByProject", method = RequestMethod.POST, produces = "application/json; charset=UTF-8") @RequestMapping(value = "/execListByProject", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result execListByProject(@RequestBody ZtStoryDTO dto){ public Result execListByProject(@RequestBody ZtStoryDTO dto){
if(dto.getProduct()!=null){ if(dto.getProduct()!=null){
@@ -336,6 +403,26 @@ public class ZtStoryController {
List<ZtProject> list = ztStoryService.execListByProject(dto); List<ZtProject> list = ztStoryService.execListByProject(dto);
return Result.success(list); return Result.success(list);
} }
/**
* 指派产品人
* @param dto
* @return
*/
@RequestMapping(value = "/assignedToProductUser", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result assignedToProductUser(@RequestBody @Validated ZtStoryDTO dto){
this.ztStoryService.assignedToProductUser(dto);
return Result.success();
}
/**
* 指派测试人
* @param dto
* @return
*/
@RequestMapping(value = "/assignedToTestUser", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result assignedToTestUser(@RequestBody @Validated ZtStoryDTO dto){
this.ztStoryService.assignedToTestUser(dto);
return Result.success();
}
@@ -0,0 +1,78 @@
package com.sa.zentao.controller;
import com.sa.zentao.dao.Result;
import com.sa.zentao.entity.ZtStoryExpand;
import com.sa.zentao.service.IZtStoryExpandService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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;
/**
* <p>
* 前端控制器
* </p>
*
* @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<List<ZtStoryExpand>> queryByMonth(String month, Integer projectId) {
List<ZtStoryExpand> list = ztStoryExpandService.queryByMonth(month, projectId);
return Result.success(list);
}
/**
* 按需求 ID 单查(AI 指标区块用)
* @param storyId 需求 ID
* @return 需求扩展信息(无记录返回 data=null)
*/
@RequestMapping(value = "/queryByStoryId", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
public Result<ZtStoryExpand> queryByStoryId(Integer storyId) {
if (storyId == null) {
return Result.fail("storyId不能为空");
}
return Result.success(ztStoryExpandService.getOne(
new QueryWrapper<ZtStoryExpand>().lambda().eq(ZtStoryExpand::getStoryId, storyId)));
}
/**
* 保存或更新(有数据了更新不新增)
* @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();
}
}
@@ -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;
/**
* <p>
* 前端控制器
* </p>
*
* @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<List<String>> listMonths() {
return Result.success(ztStoryMonthWorkloadService.listMonths());
}
/**
* 按 storyId + 月份查询单条记录
*/
@RequestMapping(value = "/getByMonth", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
public Result<ZtStoryMonthWorkload> 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<BigDecimal> maxWorkloadBefore(Integer storyId, String month) {
return Result.success(ztStoryMonthWorkloadService.getMaxWorkloadBefore(storyId, month));
}
}
@@ -7,10 +7,14 @@ import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.sa.zentao.conf.RiskUserThreadLocal;
import com.sa.zentao.dao.*; import com.sa.zentao.dao.*;
import com.sa.zentao.entity.ZtStory; import com.sa.zentao.entity.ZtStory;
import com.sa.zentao.entity.ZtStoryUser; import com.sa.zentao.entity.ZtStoryUser;
import com.sa.zentao.enums.ActionStatus;
import com.sa.zentao.enums.ActionType;
import com.sa.zentao.enums.UserStoryEnums; import com.sa.zentao.enums.UserStoryEnums;
import com.sa.zentao.qo.StoryQo; import com.sa.zentao.qo.StoryQo;
import com.sa.zentao.qo.ZtProjectQo; import com.sa.zentao.qo.ZtProjectQo;
@@ -19,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.io.IOException; import java.io.IOException;
@@ -56,6 +61,17 @@ public class ZtStoryUserController {
return Result.success(); return Result.success();
} }
/**
* 指派产品人
* @param dto
* @return
*/
@RequestMapping(value = "/assignedToProductUser", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result assignedToProductUser(@RequestBody @Validated ZtStoryUserDTO dto){
this.storyUserService.assignedToProductUser(dto);
return Result.success();
}
@RequestMapping(value = "/addStory", method = RequestMethod.POST, produces = "application/json; charset=UTF-8") @RequestMapping(value = "/addStory", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result addStory(@RequestBody ZtStoryUserDTO dto){ public Result addStory(@RequestBody ZtStoryUserDTO dto){
@@ -124,7 +140,7 @@ public class ZtStoryUserController {
eq.in(ZtStoryUser::getStatus, UserStoryEnums.JH.getCode(),UserStoryEnums.CFM.getCode(),UserStoryEnums.KFZ.getCode()); eq.in(ZtStoryUser::getStatus, UserStoryEnums.JH.getCode(),UserStoryEnums.CFM.getCode(),UserStoryEnums.KFZ.getCode());
} }
eq.select(ZtStoryUser::getId,ZtStoryUser::getTitle,ZtStoryUser::getProduct, eq.select(ZtStoryUser::getId,ZtStoryUser::getTitle,ZtStoryUser::getProduct,
ZtStoryUser::getOpenedby,ZtStoryUser::getAssignedto,ZtStoryUser::getYsUser ZtStoryUser::getOpenedby,ZtStoryUser::getAssignedto,ZtStoryUser::getYsUser,ZtStoryUser::getProductUser
); );
List<ZtStoryUser> list = storyUserService.list(eq List<ZtStoryUser> list = storyUserService.list(eq
); );
@@ -100,6 +100,16 @@ public class ZtTaskController {
return Result.success(); return Result.success();
} }
/**
* AI 批量创建任务:storyId 不存在或 type 非 devel/test 整批拒绝;重复跳过记 skipped
* @param dto 报文 {"storyId":N,"tasks":[{"name","type","assignedTo","aiEvaluationTime","planStartDate","deadline"}]}
* @return {"code":0,"data":{"created":N,"taskIds":[...],"skipped":[...]}}
*/
@RequestMapping(value = "/aiBatchAdd", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result aiBatchAdd(@RequestBody ZtTaskAiBatchDTO dto){
return Result.success(ztTaskService.aiBatchAdd(dto));
}
@RequestMapping(value = "/approval", method = RequestMethod.POST, produces = "application/json; charset=UTF-8") @RequestMapping(value = "/approval", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
public Result approval(@RequestBody ZtTaskDTO dto){ public Result approval(@RequestBody ZtTaskDTO dto){
ztTaskService.approval(dto); ztTaskService.approval(dto);
@@ -631,4 +641,16 @@ public class ZtTaskController {
return Result.success(); 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();
}
} }
@@ -135,11 +135,13 @@ public class ZtUserController {
} }
eq.in(ZtUser::getAccount,execution.stream().map(o->o.getAccount()).collect(Collectors.toList())); eq.in(ZtUser::getAccount,execution.stream().map(o->o.getAccount()).collect(Collectors.toList()));
}else{ }else{
this.userMapper.selectOne(new QueryWrapper<ZtUser>().lambda().eq(ZtUser::getAccount,RiskUserThreadLocal.get().getName())); this.userMapper.selectOne(new QueryWrapper<ZtUser>().lambda()
.eq(ZtUser::getAccount,RiskUserThreadLocal.get().getName()));
} }
eq.eq(dto.getUserType()!=null,ZtUser::getUserType,dto.getUserType());
return Result.success(userService.list(eq //CHG-073:userList 剔除 password 列——MD5 哈希不得下发前端(按属性名匹配,实体列名带反引号)
)); eq.select(ZtUser.class, fieldInfo -> !"password".equals(fieldInfo.getProperty()));
return Result.success(userService.list(eq));
} }
@RequestMapping(value = "/storyReviewList", method = RequestMethod.POST, produces = "application/json; charset=UTF-8") @RequestMapping(value = "/storyReviewList", method = RequestMethod.POST, produces = "application/json; charset=UTF-8")
@@ -3,15 +3,33 @@ package com.sa.zentao.dao;
public class BusinessException extends RuntimeException { public class BusinessException extends RuntimeException {
public BusinessException(Code code){ public BusinessException(Code code){
super(code.getValue()); super(code.getValue());
this.code=code; this.code=code;
} }
public BusinessException(String value){ public BusinessException(String value){
super(value); super(value);
this.value=value; this.value=value;
} }
public BusinessException(Code code, String message){
super(message);
this.code=code;
this.value=message;
}
public BusinessException(String value, Throwable cause){
super(value, cause);
this.value=value;
}
public BusinessException(Code code, String message, Throwable cause){
super(message, cause);
this.code=code;
this.value=message;
}
public String value; public String value;
public Code code; public Code code;
} }
+5 -1
View File
@@ -4,7 +4,11 @@ public enum Code {
SUCCESS(0, "成功"), SUCCESS(0, "成功"),
FAIL(-1, "失败"), FAIL(-1, "失败"),
CFTJ(-2, "重复添加"), CFTJ(-2, "重复添加"),
PLEASELOGIN(401, "请登录"); PLEASELOGIN(401, "请登录"),
PARAM_ERROR(400, "参数错误"),
NOT_FOUND(404, "资源不存在"),
FORBIDDEN(403, "无权限访问"),
INTERNAL_ERROR(500, "系统内部错误");
private Integer code; private Integer code;
private String value; private String value;
@@ -160,4 +160,53 @@ public class PerformanceDTO implements Serializable {
private BigDecimal professionalSkillEnhancementScore=BigDecimal.ZERO; private BigDecimal professionalSkillEnhancementScore=BigDecimal.ZERO;
//问题管理得分 //问题管理得分
private BigDecimal developFeedbackStory=BigDecimal.valueOf(10); private BigDecimal developFeedbackStory=BigDecimal.valueOf(10);
/**
* 产品经理
*/
// 产品缺陷率
private BigDecimal productBugRate=BigDecimal.ZERO;
//项目准时率 需求发布准时率 研发需求/用户需求
private BigDecimal productProjectOnTimeRateScore=BigDecimal.ZERO;
//主动性责任感
private BigDecimal productResponsibilityScore=BigDecimal.valueOf(5);
//需求会议
private BigDecimal productMeetScore=BigDecimal.valueOf(10);
//问题响应
private BigDecimal productProblemResponse=BigDecimal.valueOf(15);
//需求准确性
private BigDecimal accurateDemand=BigDecimal.valueOf(20);;
//产品规划
private BigDecimal productPlanning=BigDecimal.valueOf(10);;
//本阶段计划发布数量
private BigDecimal productPlanStoryNum;
//本阶段准时发布研发数量
private BigDecimal productPlanStoryOnTimeNum;
/** ===== CHG-037 新 Excel 角色改造新增字段(score×weight 加权得分) ===== */
//需求PRD工作量指标完成率得分(项目经理20/产品经理40/产品助理50)
private BigDecimal workloadPrdScore;
//团队工作量指标完成率得分(项目经理30/产品经理20)
private BigDecimal workloadTeamScore;
//运维大项任务及时完成得分(20)
private BigDecimal opsMajorTaskScore;
//系统运维监控得分(15)
private BigDecimal opsMonitorScore;
//职场巡检得分(10)
private BigDecimal opsInspectScore;
//数据库备份得分(10)
private BigDecimal opsBackupScore;
//其他运维工作得分(15,人工)
private BigDecimal otherOpsScore;
//开发方向(CHG-038,KFZ 弹窗分流用):frontend=前端 / backend=后端
private String devDirection;
//CHG-041 指标过程值明细(itemKey → "分子/分母/率"),弹窗「绩效数据」列展示
private java.util.Map<String, String> perfRawDetail;
/** ===== CHG-044 达标工时全链路(弹窗「绩效数据」列展示分子分母) ===== */
//团队总工作天数(人·天,=工作天数×KFZ 人数 − 团队请假折算天数)
private BigDecimal teamWorkDays;
//团队请假折算天数(团队请假小时 ÷8)
private BigDecimal teamLeaveDays;
//团队达标总工时(=团队总工作天数 ×5)
private BigDecimal teamTargetTime;
} }
@@ -0,0 +1,52 @@
package com.sa.zentao.dao;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 项目需求导出DTO
* 表头: ID 研发需求名称 P 状态 期望完成时间 阶段 指派给 验收人 任务量 创建者 创建日期
*
* @author gqb
*/
@Data
public class ProjectStoryExportDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "ID", index = 0)
private Integer id;
@ExcelProperty(value = "研发需求名称", index = 1)
private String title;
@ExcelProperty(value = "P", index = 2)
private Integer pri;
@ExcelProperty(value = "状态", index = 3)
private String status;
@ExcelProperty(value = "期望完成时间", index = 4)
private Date planEndDate;
@ExcelProperty(value = "阶段", index = 5)
private String stage;
@ExcelProperty(value = "指派给", index = 6)
private String assignedToName;
@ExcelProperty(value = "验收人", index = 7)
private String ysUserName;
@ExcelProperty(value = "任务量", index = 8)
private Integer taskCount;
@ExcelProperty(value = "创建者", index = 9)
private String openedbyName;
@ExcelProperty(value = "创建日期", index = 10)
private Date openeddate;
}
@@ -26,6 +26,20 @@ public class UploadDTO implements Serializable {
private Integer objectId; private Integer objectId;
//代码审查结果 pass/reject(objectType=aiCodeReview 时可选传入)
private String reviewResult;
private List<Integer> testIds; private List<Integer> testIds;
/**
* uploadBind 入口归一化(CHG-077):story 一律按 storyPrd 处理。
* uploadBind 仅 AI 通道使用(UI 手动附件走两段式上传不进此接口),
* 归一后文件进详情页「需求文档」区块并刷新 prd_url,不再落通用附件区。
*/
public void normalizeObjectTypeForBind() {
if ("story".equals(this.objectType)) {
this.objectType = "storyPrd";
}
}
} }
@@ -103,4 +103,14 @@ public class ZtComputerSourceDTO implements Serializable {
private String code; private String code;
private String ip; private String ip;
/**
* 使用环境:生产环境/备用环境/开发环境/测试环境/UAT环境/办公环境
*/
private String useEnv;
/**
* 到期时间
*/
private Date expireDate;
} }
@@ -3,6 +3,7 @@ package com.sa.zentao.dao;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.sa.zentao.enums.UserType;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -205,6 +206,8 @@ public class ZtProjectDTO<T> implements Serializable {
private String deleted; private String deleted;
private UserType userType;
private List<T> children; private List<T> children;
private List<Integer> storyIds; private List<Integer> storyIds;
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.sa.zentao.entity.ZtStoryCase; import com.sa.zentao.entity.ZtStoryCase;
import com.sa.zentao.entity.ZtStoryreview; import com.sa.zentao.entity.ZtStoryreview;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -31,6 +32,7 @@ public class ZtStoryDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@NotNull(message = "请录入ID")
@ExcelProperty(value = "ID",index =0) @ExcelProperty(value = "ID",index =0)
private Integer id; private Integer id;
@ExcelProperty(value = "研发需求名称",index =1) @ExcelProperty(value = "研发需求名称",index =1)
@@ -48,6 +50,8 @@ public class ZtStoryDTO implements Serializable {
private Date planEndDate; private Date planEndDate;
@ExcelProperty(value = "阶段",index =6) @ExcelProperty(value = "阶段",index =6)
private String stage; private String stage;
@ExcelIgnore
private String stageName;
@ExcelProperty(value = "指派给",index =7) @ExcelProperty(value = "指派给",index =7)
private String assignedToName; private String assignedToName;
@@ -152,8 +156,36 @@ public class ZtStoryDTO implements Serializable {
private Integer frombug; private Integer frombug;
@ExcelIgnore @ExcelIgnore
private Integer feedback; private Integer feedback;
/**
* 产品人
*/
@ExcelIgnore
private String productUser;
/**
* 产品人
*/
@ExcelIgnore
private String productUserName;
@ExcelIgnore
private String productUserColor;
/**
* 产品人
*/
@ExcelIgnore
private String testUser;
@ExcelIgnore
private String testUserColor;
/**
* 产品人
*/
@ExcelIgnore
private String testUserName;
/**
* 1是否内部验收 0不需要
*/
@ExcelIgnore
private Integer innerYsFlag;
@ExcelIgnore @ExcelIgnore
private String keywords; private String keywords;
@@ -217,7 +249,6 @@ public class ZtStoryDTO implements Serializable {
private Date planYsDate; private Date planYsDate;
@ExcelIgnore @ExcelIgnore
private Date activateddate; private Date activateddate;
@ExcelIgnore @ExcelIgnore
@@ -347,4 +378,22 @@ public class ZtStoryDTO implements Serializable {
*/ */
@ExcelIgnore @ExcelIgnore
private String deliverContent; private String deliverContent;
// ===== AI 文档链接与审查状态(对应 zt_story 新列,详见 dev_plan v4.0 §3.1)=====
/** PRD 文档访问链接 */
private String prdUrl;
/** 代码审查报告访问链接 */
private String codeReviewUrl;
/** 工作日志访问链接 */
private String workLogUrl;
/** 测试用例访问链接 */
private String testCaseUrl;
/** 测试报告模版下载访问链接 */
private String testReportDownloadUrl;
/** 测试报告提交访问链接 */
private String testReportSubmitUrl;
/** 其他测试文档访问链接 */
private String testOtherUrl;
/** 审查状态 pass/reject */
private String codeReviewStatus;
} }
@@ -2,18 +2,25 @@ package com.sa.zentao.dao;
import com.alibaba.excel.annotation.ExcelIgnore; import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.data.HyperlinkData;
import com.alibaba.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.data.WriteCellData;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.sa.zentao.entity.ZtProject;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* <p> * <p>
@@ -25,12 +32,14 @@ import java.util.List;
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Slf4j
public class ZtStoryUserDTO implements Serializable { public class ZtStoryUserDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// @TableId(value = "id", type = IdType.AUTO) // @TableId(value = "id", type = IdType.AUTO)
// @ExcelProperty // @ExcelProperty
@NotNull(message = "ID不能为空")
@ExcelProperty(value = "ID",index =0) @ExcelProperty(value = "ID",index =0)
private Integer id; private Integer id;
@@ -46,41 +55,48 @@ public class ZtStoryUserDTO implements Serializable {
*/ */
@ExcelProperty(value = "关联研发需求",index =4) @ExcelProperty(value = "关联研发需求",index =4)
private String storyList; private String storyList;
@ExcelProperty(value = "状态",index =5) @ExcelIgnore
private Set<ZtProject> execList;
/**
* 迭代版本(CHG-078):execList 中迭代名称去重排序逗号拼接,仅供导出/列表展示
*/
@ExcelProperty(value = "迭代版本", index = 5)
private String execNames;
@ExcelProperty(value = "状态",index =6)
private String status; private String status;
@ExcelProperty(value = "来源",index =6) @ExcelProperty(value = "来源",index =7)
private String source; private String source;
@ExcelProperty(value = "期望完成时间",index =7) @ExcelProperty(value = "期望完成时间",index =8)
private Date planEndDate; private Date planEndDate;
/** /**
* 验收人 * 验收人
*/ */
@ExcelIgnore @ExcelIgnore
private String ysUser; private String ysUser;
@ExcelProperty(value = "验收人",index =8) @ExcelProperty(value = "验收人",index =9)
private String ysUserName; private String ysUserName;
@ExcelProperty(value = "评审人",index =9) @ExcelProperty(value = "评审人",index =10)
private String reviewedbyName; private String reviewedbyName;
@ExcelProperty(value = "创建日期",index =10) @ExcelProperty(value = "创建日期",index =11)
private Date openeddate; private Date openeddate;
@ExcelProperty(value = "更新日期",index =11) @ExcelProperty(value = "更新日期",index =12)
private Date lastediteddate; private Date lastediteddate;
@ExcelIgnore @ExcelIgnore
private String closedby; private String closedby;
@ExcelProperty(value = "由谁关闭",index =12) @ExcelProperty(value = "由谁关闭",index =13)
private String closedbyName; private String closedbyName;
@ExcelIgnore @ExcelIgnore
private Date closeddate; private Date closeddate;
@ExcelProperty(value = "关闭原因",index =13) @ExcelProperty(value = "关闭原因",index =14)
private String closedreason; private String closedreason;
@ExcelProperty(value = "需求链接",index =14) @ExcelProperty(value = "需求链接",index =15)
private WriteCellData<String> url; private WriteCellData<String> url;
@ExcelIgnore @ExcelIgnore
private String vision; private String vision;
@@ -130,6 +146,10 @@ public class ZtStoryUserDTO implements Serializable {
@ExcelIgnore @ExcelIgnore
private String color; private String color;
@ExcelIgnore @ExcelIgnore
private String productUserName;
@ExcelIgnore
private String productUserColor;
@ExcelIgnore
private String stage; private String stage;
@ExcelIgnore @ExcelIgnore
@@ -147,7 +167,7 @@ public class ZtStoryUserDTO implements Serializable {
private String openedby; private String openedby;
@NotBlank(message = "指派??")
@ExcelIgnore @ExcelIgnore
private String assignedto; private String assignedto;
@ExcelIgnore @ExcelIgnore
@@ -273,4 +293,30 @@ public class ZtStoryUserDTO implements Serializable {
//1.需要 2.不需要 //1.需要 2.不需要
@ExcelIgnore @ExcelIgnore
private Integer needImprove; private Integer needImprove;
/**
* 需求背景
*/
@ExcelIgnore
private String storyBackground;
/**
* 产品用户
*/
@ExcelIgnore
private String productUser;
public void setLinkUrl(String baseUrl) {
String reName = this.getTitle();
String reUrl = baseUrl + "/#/product-user-story-info/" + this.getId();
WriteCellData<String> hyperlink = new WriteCellData<>(reName);
HyperlinkData hyperlinkData = new HyperlinkData();
try {
hyperlinkData.setAddress(reUrl.replace(" ", "%20"));
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
hyperlink.setHyperlinkData(hyperlinkData);
this.setUrl(hyperlink);
} catch (Exception e) {
log.error(e.getMessage(), e);
this.setUrl(null);
}
}
} }
@@ -0,0 +1,39 @@
package com.sa.zentao.dao;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.List;
/**
* AI 批量创建任务报文(POST /zt-task/aiBatchAdd)
* {"storyId":N,"tasks":[{"name","type","assignedTo","aiEvaluationTime","planStartDate","deadline"}]}
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class ZtTaskAiBatchDTO implements Serializable {
private Integer storyId;
private List<AiTaskItem> tasks;
@Data
@EqualsAndHashCode(callSuper = false)
public static class AiTaskItem implements Serializable {
private String name;
//devel 开发 / test 测试 / design 设计(CHG-081 放行)/ affair 事务(CHG-082 放行)
private String type;
private String assignedTo;
//任务描述,可选,写入 zt_task.desc
private String desc;
//AI评估工时,写入 zt_task.estimate
private Float aiEvaluationTime;
//预计开始,格式 yyyy-MM-dd,写入 zt_task.estStarted
private String planStartDate;
//预计完成,格式 yyyy-MM-dd,写入 zt_task.deadline
private String deadline;
}
}
@@ -212,4 +212,8 @@ public class ZtTaskDTO implements Serializable {
* 交付物 * 交付物
*/ */
private String deliverContent; private String deliverContent;
/**
* 1提交 0不提交
*/
private Integer innerYsFlag;
} }
@@ -128,6 +128,9 @@ public class ZtUserDTO implements Serializable {
private UserType userType; private UserType userType;
//开发方向(CHG-038,仅 KFZ):frontend=前端 / backend=后端
private String devDirection;
private String userTypeValue; private String userTypeValue;
private String roleTypeValue; private String roleTypeValue;
@@ -104,4 +104,14 @@ public class ZtComputerSource implements Serializable {
private String code; private String code;
private String ip; private String ip;
/**
* 使用环境:生产环境/备用环境/开发环境/测试环境/UAT环境/办公环境
*/
private String useEnv;
/**
* 到期时间
*/
private Date expireDate;
} }
@@ -0,0 +1,114 @@
package com.sa.zentao.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 大型需求文档齐备核查快照表(FR-014 三期)
* 一行 = 某大型需求某月某类文档的核查结果;
* 状态流 pending(待确认)→ appealing(申诉中)→ resolved(复核完结,appealResult=pass 时回滚扣分)
* </p>
*
* @author ai
* @since 2026-07-28
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class ZtDocCheck implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 状态-待确认
*/
public static final String STATUS_PENDING = "pending";
/**
* 状态-申诉中
*/
public static final String STATUS_APPEALING = "appealing";
/**
* 状态-复核完结
*/
public static final String STATUS_RESOLVED = "resolved";
/**
* 复核结果-通过(回滚扣分)
*/
public static final String APPEAL_PASS = "pass";
/**
* 复核结果-驳回
*/
public static final String APPEAL_REJECT = "reject";
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 研发需求ID(zt_story.id,大型需求=workload_index>20)
*/
private Integer storyId;
/**
* 考核月份 yyyy-MM
*/
private String month;
/**
* 文档类型 testCase/testReport/aiCodeReview/aiWorkLog/docUpdate
*/
private String docType;
/**
* 是否齐备 1有 0缺失
*/
private Integer present;
/**
* 判定明细(命中记录说明)
*/
private String detail;
/**
* 状态 pending/appealing/resolved
*/
private String status;
/**
* 申诉理由
*/
private String appealReason;
/**
* 复核结果 pass/reject(pass 回滚扣分)
*/
private String appealResult;
/**
* 复核备注
*/
private String resolveRemark;
/**
* 核查时间
*/
private Date checkTime;
private Date createTime;
private Date updateTime;
/**
* 是否计入缺失扣分(缺失 且 非复核通过)
*/
public boolean countsAsMissing() {
return (this.present == null || this.present == 0)
&& !(STATUS_RESOLVED.equals(this.status) && APPEAL_PASS.equals(this.appealResult));
}
}
@@ -29,6 +29,9 @@ public class ZtFile implements Serializable {
private String pathname; private String pathname;
//附件访问链接
private String url;
//文件名 //文件名
private String title; private String title;
//后缀 //后缀
@@ -73,4 +73,9 @@ public class ZtMeeting implements Serializable {
private String result; private String result;
private String meetingAfter; private String meetingAfter;
/**
* 会议纪要MD访问链接(最新一份)
*/
private String url;
} }
@@ -0,0 +1,69 @@
package com.sa.zentao.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* <p>
* 绩效指标规则配置表(三期,prd §7.5 九岗位算分模型)
* 一行 = 某岗位某指标项;rule_json 存规则参数(type: linear/threshold/manual)
* </p>
*
* @author ai
* @since 2026-07-28
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class ZtPerfConfig implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 岗位编码(projectManager/productManager/backendDev/tester/uiEngineer/opsEngineer 等)
*/
private String role;
/**
* 指标编码(与 PerfIndicatorCalculator.key() 对应)
*/
private String itemKey;
/**
* 指标名称
*/
private String itemName;
/**
* 权重(0~1,加分项为0)
*/
private BigDecimal weight;
/**
* 规则参数JSON(type=linear/threshold/manual)
*/
private String ruleJson;
/**
* 是否启用(加分项可配开关)
*/
private Integer enabled;
private String createUser;
private Date createTime;
private String updateUser;
private Date updateTime;
}
@@ -190,7 +190,12 @@ public class ZtStory implements Serializable {
private Date developedDate; private Date developedDate;
//结束日期 测试结束 //结束日期 测试结束
private Date endDate; private Date endDate;
//内部验收时间
@TableField(exist = false)
private Date innerYsTime;
//测试提交验收时间
@TableField(exist = false)
private Date testSubmitYsTime;
private Integer project; private Integer project;
//1通过 2不通过 //1通过 2不通过
private Integer ysFlag; private Integer ysFlag;
@@ -223,5 +228,61 @@ public class ZtStory implements Serializable {
// private Date develDate; // private Date develDate;
// //测试完成时间 // //测试完成时间
// private Date testedDate; // private Date testedDate;
/**
* 内部验收标识 1需要内部验收 0不需要
*/
private Integer innerYsFlag;
/**
* 产品人员
*/
private String productUser;
/**
* 测试人员
*/
private String testUser;
/**
* PRD文档访问链接
*/
@TableField("prd_url")
private String prdUrl;
/**
* 代码审查报告访问链接
*/
@TableField("code_review_url")
private String codeReviewUrl;
/**
* 工作日志访问链接
*/
@TableField("work_log_url")
private String workLogUrl;
/**
* AI项目文档更新记录访问链接(CHG-046)
*/
@TableField("ai_doc_update_url")
private String aiDocUpdateUrl;
/**
* 测试用例访问链接
*/
@TableField("test_case_url")
private String testCaseUrl;
/**
* 测试报告模版下载访问链接
*/
@TableField("test_report_download_url")
private String testReportDownloadUrl;
/**
* 测试报告提交访问链接
*/
@TableField("test_report_submit_url")
private String testReportSubmitUrl;
/**
* 其他测试文档访问链接
*/
@TableField("test_other_url")
private String testOtherUrl;
/**
* 审查状态 pass/reject
*/
@TableField("code_review_status")
private String codeReviewStatus;
} }
@@ -0,0 +1,129 @@
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;
/**
* <p>
*
* </p>
*
* @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;
/**
* AI参与率(只存不算,口径待定)
*/
private String aiParticipationRate;
/**
* 评估工时
*/
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;
/**
* 产品人员
*/
private String productPerson;
/**
* 开发人员
*/
private String developPerson;
/**
* 测试人员
*/
private String testPerson;
/**
* AI框架验收指标(Given/When/Then,MD 文本;框架经 saveOrUpdate 上传,
* 与老验收标准 zt_storyspec.verify 互不干扰——CHG-022)
*/
private String acceptanceCriteria;
}
@@ -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;
/**
* <p>
*
* </p>
*
* @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;
}
@@ -78,6 +78,7 @@ public class ZtStoryUser implements Serializable {
// DQR("storyunconfirmed","需求待确认",8), // DQR("storyunconfirmed","需求待确认",8),
// CFM("confirmed","需求已确认",9), // CFM("confirmed","需求已确认",9),
// KFZ("developing","研发中",10), // KFZ("developing","研发中",10),
// KFZ("released","已发布",10),
// YWC("finished","已完成",11), // YWC("finished","已完成",11),
// CLOSED("closed","已关闭",12), // CLOSED("closed","已关闭",12),
private String status; private String status;
@@ -224,4 +225,9 @@ public class ZtStoryUser implements Serializable {
private String deliverRemark; private String deliverRemark;
private String oldStatus; private String oldStatus;
/**
* 产品用户
*/
private String productUser;
} }
@@ -31,5 +31,6 @@ public class ZtStoryUserspec implements Serializable {
private String files; private String files;
private String storyBackground;
} }
@@ -126,6 +126,9 @@ public class ZtUser implements Serializable {
private UserType userType; private UserType userType;
//开发方向(CHG-038,仅 user_type=KFZ 有效):frontend=前端 / backend=后端,NULL 按后端口径
private String devDirection;
private String productIds; private String productIds;
@TableField(exist = false) @TableField(exist = false)
@@ -62,6 +62,12 @@ public enum ActionStatus {
XGMM(101, "xgmm","修改密码"), XGMM(101, "xgmm","修改密码"),
CZMM(102, "czmm","重置密码"), CZMM(102, "czmm","重置密码"),
CZINFO(103, "czmm","重置"), CZINFO(103, "czmm","重置"),
//测试提交验收
TESTSUBMITYS(200, "testSubmitYs","测试提交验收"),
PRODUCTYS(201, "productYs","产品验收"),
ASSIGNTOPRODUCTUSER(202, "assignProductUser","指派产品人"),
ASSIGNTOTESTUSER(203, "assignTestUser","指派测试人"),
; ;
@EnumValue @EnumValue
@@ -13,6 +13,14 @@ public enum FileTypes {
feedbackStory("feedbackStory", "问题反馈"), feedbackStory("feedbackStory", "问题反馈"),
ywTask("ywTask", "运维任务"), ywTask("ywTask", "运维任务"),
meeting("meeting", "会议"), meeting("meeting", "会议"),
aiCodeReview("aiCodeReview", "代码审查报告"),
aiWorkLog("aiWorkLog", "工作日志"),
aiDocUpdate("aiDocUpdate", "AI项目文档更新记录"),
storyPrd("storyPrd", "需求文档"),
testCase("testCase", "测试用例"),
testReport("testReport", "测试报告模版"),
testReportSubmit("testReportSubmit", "测试报告提交"),
testOther("testOther", "其他测试文档"),
; ;
@EnumValue @EnumValue
@@ -0,0 +1,49 @@
package com.sa.zentao.enums;
public enum KanbanColumnType {
// 需求看板列类型
STORY_BACKLOG("backlog", "待办"),
STORY_DEVELOPING("developing", "研发中"),
STORY_DEVELOPED("developed", "研发完毕"),
STORY_TESTING("testing", "测试中"),
STORY_TESTED("tested", "测试完毕"),
STORY_RELEASED("released", "已发布"),
STORY_VERIFIED("verified", "已验收"),
STORY_CLOSED("closed", "已关闭"),
// Bug看板列类型
BUG_UNCONFIRMED("unconfirmed", "未确认"),
BUG_CONFIRMED("confirmed", "已确认"),
BUG_RESOLVED("resolved", "已解决"),
BUG_CLOSED("closed", "已关闭"),
// 任务看板列类型
TASK_WAIT("wait", "等待"),
TASK_DEVELOPING("developing", "进行中"),
TASK_DEVELOPED("developed", "已完成"),
TASK_CANCEL("cancel", "已取消"),
TASK_CLOSED("closed", "已关闭"),
// 通用列类型
WAIT("wait", "等待"),
DOING("doing", "进行中"),
DONE("done", "已完成"),
CLOSED("closed", "已关闭");
private String value;
private String desc;
private KanbanColumnType(String value, String desc) {
this.value = value;
this.desc = desc;
}
public String getValue() {
return this.value;
}
public String getDesc() {
return this.desc;
}
}
@@ -2,6 +2,9 @@ package com.sa.zentao.enums;
import com.baomidou.mybatisplus.annotation.EnumValue; import com.baomidou.mybatisplus.annotation.EnumValue;
import java.util.ArrayList;
import java.util.List;
public enum StoryStageEnums { public enum StoryStageEnums {
//closed 关闭 wait 初始化 projected 已立项 developing 研发中 developed 研发完毕 testing 测试中 //closed 关闭 wait 初始化 projected 已立项 developing 研发中 developed 研发完毕 testing 测试中
// tested测试完毕 released已发布 verified已验收 closed 已关闭 // tested测试完毕 released已发布 verified已验收 closed 已关闭
@@ -13,6 +16,8 @@ public enum StoryStageEnums {
developed(10, "developed","研发完毕"), developed(10, "developed","研发完毕"),
testing(5, "testing","测试中"), testing(5, "testing","测试中"),
tested(6, "tested","测试完毕"), tested(6, "tested","测试完毕"),
productWaitVerified(9, "productWaitVerified","产品验收中"),
productVerified(10, "productVerified","产品已验收"),
released(7, "released","已发布"), released(7, "released","已发布"),
verified(8, "verified","已验收"), verified(8, "verified","已验收"),
; ;
@@ -28,6 +33,17 @@ public enum StoryStageEnums {
this.desc=desc; this.desc=desc;
} }
public static List<String> beforeStatus(int code){
List<String> list=new ArrayList<String>();
StoryStageEnums[] values = values();
for (StoryStageEnums value : values) {
if (value.code<code&&value.code!=1){
list.add(value.value);
}
}
return list;
}
public int getCode() { public int getCode() {
return this.code; return this.code;
} }
@@ -17,6 +17,7 @@ public enum UserStoryEnums {
DQR("storyunconfirmed","需求待确认",8), DQR("storyunconfirmed","需求待确认",8),
CFM("confirmed","需求已确认",9), CFM("confirmed","需求已确认",9),
KFZ("developing","研发中",10), KFZ("developing","研发中",10),
RELEASED("released","已发布",15),
YWC("finished","已完成",11), YWC("finished","已完成",11),
CLOSED("closed","已关闭",12), CLOSED("closed","已关闭",12),
; ;
@@ -0,0 +1,16 @@
package com.sa.zentao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sa.zentao.entity.ZtDocCheck;
/**
* <p>
* Mapper 接口
* </p>
*
* @author ai
* @since 2026-07-28
*/
public interface ZtDocCheckMapper extends BaseMapper<ZtDocCheck> {
}
@@ -0,0 +1,16 @@
package com.sa.zentao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sa.zentao.entity.ZtPerfConfig;
/**
* <p>
* Mapper 接口
* </p>
*
* @author ai
* @since 2026-07-28
*/
public interface ZtPerfConfigMapper extends BaseMapper<ZtPerfConfig> {
}
@@ -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;
/**
* <p>
* Mapper 接口
* </p>
*
* @author gqb
* @since 2026-03-30
*/
public interface ZtStoryExpandMapper extends BaseMapper<ZtStoryExpand> {
/**
* 按月份和项目查询,关联 zt_story 获取需求名称
* @param month 月份,格式:yyyy-MM
* @param projectId 产品项目 id(zt_project.id),为 null 时不过滤
* @return 需求扩展信息列表(含 storyTitle)
*/
List<ZtStoryExpand> queryByMonthWithTitle(@Param("month") String month, @Param("projectId") Integer projectId);
}
@@ -10,6 +10,7 @@ import org.apache.ibatis.annotations.Param;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
@@ -59,4 +60,7 @@ public interface ZtStoryMapper extends BaseMapper<ZtStory> {
* @return * @return
*/ */
List<SearchDTO> searchAll(@Param("keyword") String keyword,@Param("pIds")List<Integer> pIds); List<SearchDTO> searchAll(@Param("keyword") String keyword,@Param("pIds")List<Integer> pIds);
Map<String, Object> storyReleaseOnTimeByProducts(@Param("pIds") List<Integer> pids,
@Param("startTime") Date firstDayOfMonth,@Param("endTime") Date lastDayOfMonth);
} }
@@ -0,0 +1,16 @@
package com.sa.zentao.mapper;
import com.sa.zentao.entity.ZtStoryMonthWorkload;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author gqb
* @since 2026-03-30
*/
public interface ZtStoryMonthWorkloadMapper extends BaseMapper<ZtStoryMonthWorkload> {
}
@@ -28,6 +28,12 @@ public interface ZtTaskMapper extends BaseMapper<ZtTask> {
List<ItApproval> itApprovalByUserName(@Param("name") String name, @Param("startDate") Date firstDayOfMonth, @Param("endDate") Date lastDayOfMonth); List<ItApproval> itApprovalByUserName(@Param("name") String name, @Param("startDate") Date firstDayOfMonth, @Param("endDate") Date lastDayOfMonth);
/**
* 按姓名批量查询请假审批(条件与 itApprovalByUserName 完全一致,仅 name 改为 IN 批量;
* 供 storyBarChart 月度循环批量化使用,老方法保留不动)
*/
List<ItApproval> itApprovalsByNames(@Param("names") List<String> names, @Param("startDate") Date firstDayOfMonth, @Param("endDate") Date lastDayOfMonth);
List<ZtTask> taskListByIds(@Param("ids") List<String> ids); List<ZtTask> taskListByIds(@Param("ids") List<String> ids);
List<ZtTask> taskListByStoryIds(@Param("ids") List<Integer> sIds); List<ZtTask> taskListByStoryIds(@Param("ids") List<Integer> sIds);
@@ -18,15 +18,27 @@ public class StoryQo extends BaseQo {
private String assignedTo; private String assignedTo;
private String userName; private String userName;
private String module; private String module;
/**
* 多个模块,逗号分割
*/
private String modules;
private String searchVal; private String searchVal;
private Integer productId; private Integer productId;
private String openedby; private String openedby;
private String status; private String status;
/**
* 用户产品
*/
private String productUser;
private List<Integer> productIds; private List<Integer> productIds;
private List<String> statusList; private List<String> statusList;
private String ids; private String ids;
/**
* 迭代
*/
private Integer execution;
private List<String> storyIds; private List<String> storyIds;
} }
@@ -104,4 +104,14 @@ public class ZtComputerSourceQo extends BaseQo {
private String ip; private String ip;
/**
* 使用环境:生产环境/备用环境/开发环境/测试环境/UAT环境/办公环境
*/
private String useEnv;
/**
* 到期时间
*/
private Date expireDate;
} }
@@ -0,0 +1,52 @@
package com.sa.zentao.qo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 文档齐备核查入参(FR-014 三期):generateDocCheck 用 month;
* docCheckAppeal 用 storyId/month/docType/reason;docCheckAppealResolve 另加 pass/remark
* </p>
*
* @author ai
* @since 2026-07-28
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class ZtDocCheckQo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 月份 yyyy-MM
*/
private String month;
/**
* 需求ID
*/
private Integer storyId;
/**
* 文档类型 testCase/testReport/aiCodeReview/aiWorkLog/docUpdate
*/
private String docType;
/**
* 申诉理由
*/
private String reason;
/**
* 复核是否通过
*/
private Boolean pass;
/**
* 复核备注
*/
private String remark;
}
@@ -0,0 +1,66 @@
package com.sa.zentao.qo;
import lombok.Data;
import java.io.Serializable;
/**
* 绩效报表/评分/申诉/配置 请求参数(三期批2 /zt-perf)
*
* @author ai
* @since 2026-07-28
*/
@Data
public class ZtPerfQo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 配置维护时为 zt_perf_config.id;申诉复核时为申诉 id(account@month#序号)
*/
private String id;
/**
* 月份 yyyy-MM
*/
private String month;
/**
* 岗位
*/
private String role;
private String account;
private String itemKey;
/**
* 人工评分分值 0~100
*/
private Double score;
private String remark;
/**
* 申诉原因
*/
private String reason;
/**
* 复核结果:upheld 维持 / overturned 撤销
*/
private String result;
private String reviewer;
/**
* 配置项字段(/zt-perf/config 维护用)
*/
private String itemName;
private Double weight;
private String ruleJson;
private Integer enabled;
}
@@ -37,7 +37,6 @@ public class ZtProjectQo extends BaseQo {
private String severity; private String severity;
private String productName; private String productName;
private Integer productId; private Integer productId;
private List<Integer> productIds; private List<Integer> productIds;
@@ -64,6 +63,10 @@ public class ZtProjectQo extends BaseQo {
private String status; private String status;
private List<String> statusList;
private List<String> stageList;
private String title; private String title;
private Integer module; private Integer module;
@@ -117,4 +120,8 @@ public class ZtProjectQo extends BaseQo {
private String account; private String account;
//1 延期 2不延期 //1 延期 2不延期
private Integer delayFlag=0 ; private Integer delayFlag=0 ;
/**
* 验收人
*/
private String ysUser;
} }
@@ -0,0 +1,60 @@
package com.sa.zentao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.sa.zentao.entity.ZtDocCheck;
import java.util.List;
import java.util.Map;
/**
* <p>
* 大型需求文档齐备核查 服务类(FR-014 三期)
* </p>
*
* @author ai
* @since 2026-07-28
*/
public interface IZtDocCheckService extends IService<ZtDocCheck> {
/**
* 生成某月文档齐备快照:大型需求(workload_index>20 且当月有完成度变化)× 五类文档,
* 结果写 zt_doc_check,并按 缺失数×2 合并写 zt_month_score.scopeJson 的 docCheck 节点
* @param month 考核月份 yyyy-MM
* @return 摘要 {month, storyCount, checkRows, deductByAccount}
*/
Map<String, Object> generateMonthSnapshot(String month);
/**
* 申诉:缺失项状态 pending → appealing
* @param storyId 需求ID
* @param month 月份 yyyy-MM
* @param docType 文档类型
* @param reason 申诉理由
*/
void appeal(Integer storyId, String month, String docType, String reason);
/**
* 复核:appealing → resolved;pass 时回滚该账号当月 docCheck 扣分
* @param storyId 需求ID
* @param month 月份 yyyy-MM
* @param docType 文档类型
* @param pass 复核是否通过
* @param remark 复核备注
*/
void resolveAppeal(Integer storyId, String month, String docType, boolean pass, String remark);
/**
* 某月的全部核查记录(供绩效 calculator 取数)
*/
List<ZtDocCheck> listMonthRows(String month);
/**
* 文档齐备矩阵页数据(按需求聚合:docs 五类判定/扣分/状态/申诉状态)
*/
List<Map<String, Object>> listMatrix(String month);
/**
* 月度确认:该月快照全部标记 CONFIRMED(项目经理确认后锁定)
*/
void confirmMonth(String month);
}
@@ -16,4 +16,16 @@ public interface IZtFileService extends IService<ZtFile> {
void updateFile(String fileIds, Integer objectId, FileTypes type); void updateFile(String fileIds, Integer objectId, FileTypes type);
/**
* uploadBind 绑定后按 objectType 刷新主表访问链接字段
* story→zt_story.prd_url;aiCodeReview→code_review_url+code_review_status;
* aiWorkLog→work_log_url;testCase→test_case_url;testReport→test_report_download_url;
* testReportSubmit→test_report_submit_url;testOther→test_other_url;meeting→zt_meeting.url
* @param objectType 附件类型(FileTypes.code)
* @param objectId 主表记录 id(zt_story.id / zt_meeting.id)
* @param url 附件访问链接
* @param reviewResult 审查结果 pass/reject,仅 objectType=aiCodeReview 时可选
*/
void refreshOwnerUrl(String objectType, Integer objectId, String url, String reviewResult);
} }
@@ -0,0 +1,30 @@
package com.sa.zentao.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.sa.zentao.entity.ZtPerfConfig;
/**
* <p>
* 绩效指标规则配置 服务类(三期)
* </p>
*
* @author ai
* @since 2026-07-28
*/
public interface IZtPerfConfigService extends IService<ZtPerfConfig> {
/**
* 取岗位某指标的启用配置;role 为空或无匹配时回退到该指标任意一条启用配置
* @param role 岗位编码(可为空)
* @param itemKey 指标编码
* @return 配置,无则 null
*/
ZtPerfConfig getEnabledConfig(String role, String itemKey);
/**
* 按账号解析岗位编码(依据 zt_user.userType;前后端无法区分时默认 backendDev)
* @param account 账号
* @return 岗位编码,无法识别返回 null
*/
String resolveRole(String account);
}
@@ -0,0 +1,40 @@
package com.sa.zentao.service;
import com.sa.zentao.qo.ZtPerfQo;
import com.sa.zentao.service.perf.PerfMonthScope;
/**
* <p>
* 绩效报表(三期批2:报表引擎+接口) 服务类
* </p>
*
* @author ai
* @since 2026-07-28
*/
public interface IZtPerfReportService {
/**
* 按岗位 tab 报表:已落库读 zt_month_score.scopeJson,未落库实时计算(不写库)
*/
PerfMonthScope report(String month, String role, String account);
/**
* 批量生成/重算月度得分写 zt_month_score.scopeJson;重算保留已有人工评分与申诉记录
*/
void generateMonthScore(String month, String role, String account);
/**
* 人工评分合并写 scopeJson(PRD §7.5 ❌ 项录入入口)
*/
void manualScore(ZtPerfQo qo);
/**
* 申诉登记:写 scopeJson.appeals(id 规则 account@month#序号)
*/
void appeal(ZtPerfQo qo);
/**
* 申诉复核:upheld 维持原判;overturned 撤销扣分回滚该项为满分并重算总分
*/
void appealReview(ZtPerfQo qo);
}
@@ -57,4 +57,6 @@ public interface IZtProductService extends IService<ZtProduct> {
List<ZtProduct> productListByProgramName(String programName); List<ZtProduct> productListByProgramName(String programName);
PageInfo<ZtProjectDTO> getExecutionByProductId(ZtProjectQo qo);
} }
@@ -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;
/**
* <p>
* 服务类
* </p>
*
* @author gqb
* @since 2026-03-30
*/
public interface IZtStoryExpandService extends IService<ZtStoryExpand> {
/**
* 保存或更新(有数据了更新不新增)
* @param entity 需求扩展信息
*/
void saveOrUpdateExpand(ZtStoryExpand entity);
/**
* 按月份和项目查询(不分页,含需求名称)
* @param month 月份,格式:yyyy-MM
* @param projectId 产品项目 id,为 null 时不过滤
* @return 需求扩展信息列表(含 storyTitle)
*/
List<ZtStoryExpand> queryByMonth(String month, Integer projectId);
/**
* 打分接口
* @param entity 需求扩展信息
*/
void saveScore(ZtStoryExpand entity);
}
@@ -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;
/**
* <p>
* 服务类
* </p>
*
* @author gqb
* @since 2026-03-30
*/
public interface IZtStoryMonthWorkloadService extends IService<ZtStoryMonthWorkload> {
/**
* 保存完成度
* @param entity 月度工作量信息
*/
void saveCompletionDegree(ZtStoryMonthWorkload entity);
/**
* 获取所有不重复的月份列表
* @return 月份列表,格式 yyyy-MM
*/
List<String> listMonths();
/**
* 按 storyId + 月份查询单条记录
*/
ZtStoryMonthWorkload getByStoryIdAndMonth(Integer storyId, String month);
/**
* 查询某需求在指定月份之前(不含)的历史最高完成度
*/
BigDecimal getMaxWorkloadBefore(Integer storyId, String month);
}
@@ -57,7 +57,7 @@ public interface IZtStoryService extends IService<ZtStory> {
void waitStory(Integer story); void waitStory(Integer story);
void startStory(Integer story); void startStory(Integer story);
//开发完成 //开发完成
void finishStory(Integer story,String finishBy); void develFinishStory(Integer story,String finishBy);
//测试完毕 //测试完毕
void testedStory(Integer story,String finishBy); void testedStory(Integer story,String finishBy);
//测试中 //测试中
@@ -77,7 +77,11 @@ public interface IZtStoryService extends IService<ZtStory> {
void storyYs(ZtStoryDTO dto); void storyYs(ZtStoryDTO dto);
ZtStoryDTO getStoryById(Integer id,Integer execution); ZtStoryDTO getStoryById(Integer id,Integer execution);
/**
* 迭代 跟进项目
* @param dto
* @return
*/
List<ZtProject> execListByProject(ZtStoryDTO dto); List<ZtProject> execListByProject(ZtStoryDTO dto);
void changeAssignedTo(ZtStoryDTO dto); void changeAssignedTo(ZtStoryDTO dto);
@@ -105,4 +109,15 @@ public interface IZtStoryService extends IService<ZtStory> {
List<ZtStory> getStoryListByDatePidsProject(List<Integer> pids, Integer id, Date firstDayOfMonth, Date lastDayOfMonth); List<ZtStory> getStoryListByDatePidsProject(List<Integer> pids, Integer id, Date firstDayOfMonth, Date lastDayOfMonth);
PageInfo<SearchDTO> searchByName(SearchQo qo); PageInfo<SearchDTO> searchByName(SearchQo qo);
void testSubmitVerified(ZtStoryDTO qo);
void storyProductUserYs(ZtStoryDTO qo);
Map<String, Object> storyReleaseOnTimeByProducts(@Param("pids") List<Integer> pids, @Param("startTime")Date firstDayOfMonth,
@Param("endTime")Date lastDayOfMonth);
void assignedToProductUser(ZtStoryDTO dto);
void assignedToTestUser(ZtStoryDTO dto);
} }
@@ -54,4 +54,7 @@ public interface IZtStoryUserService extends IService<ZtStoryUser> {
void confirmStory(ZtStoryUserDTO dto); void confirmStory(ZtStoryUserDTO dto);
void needMeetOrDesign(ZtStoryUserDTO dto); void needMeetOrDesign(ZtStoryUserDTO dto);
void assignedToProductUser(ZtStoryUserDTO dto);
} }
@@ -2,6 +2,7 @@ package com.sa.zentao.service;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.sa.zentao.dao.ItApproval; import com.sa.zentao.dao.ItApproval;
import com.sa.zentao.dao.ZtTaskAiBatchDTO;
import com.sa.zentao.dao.ZtTaskDTO; import com.sa.zentao.dao.ZtTaskDTO;
import com.sa.zentao.entity.ZtTask; import com.sa.zentao.entity.ZtTask;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
@@ -9,6 +10,7 @@ import com.sa.zentao.qo.ZtProjectQo;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
@@ -38,6 +40,14 @@ public interface IZtTaskService extends IService<ZtTask> {
void batchAddTask(ZtTaskDTO dto); void batchAddTask(ZtTaskDTO dto);
/**
* AI 批量创建任务(事务):storyId 不存在或 type 非 devel/test 整批拒绝;
* 同 storyId+name+type 已存在跳过记 skipped;创建 zt_task(status=wait、创建人=ai、estimate=aiEvaluationTime)
* @param dto 报文 {"storyId":N,"tasks":[...]}
* @return {"created":N,"taskIds":[...],"skipped":[...]}
*/
Map<String, Object> aiBatchAdd(ZtTaskAiBatchDTO dto);
void approval(ZtTaskDTO dto); void approval(ZtTaskDTO dto);
PageInfo<ZtTaskDTO> myTaskPageList(ZtProjectQo qo); PageInfo<ZtTaskDTO> myTaskPageList(ZtProjectQo qo);
@@ -56,6 +66,12 @@ public interface IZtTaskService extends IService<ZtTask> {
List<ItApproval> itApprovalByUserName(String s, Date firstDayOfMonth, Date lastDayOfMonth); List<ItApproval> itApprovalByUserName(String s, Date firstDayOfMonth, Date lastDayOfMonth);
/**
* 按姓名批量查询请假审批(条件与 itApprovalByUserName 一致,name 改 IN;
* 供 storyBarChart 月度循环批量化,老方法保留不动)
*/
List<ItApproval> itApprovalsByNames(List<String> names, Date firstDayOfMonth, Date lastDayOfMonth);
/** /**
* *
* @param firstDayOfMonth 开始 * @param firstDayOfMonth 开始
@@ -46,7 +46,9 @@ public class FestivalConfigServiceImpl extends ServiceImpl<FestivalConfigMapper,
List<FestivalConfig> festivalConfigs = this.baseMapper.selectList(new QueryWrapper<FestivalConfig>().lambda().eq(FestivalConfig::getName, qo.getName()). List<FestivalConfig> festivalConfigs = this.baseMapper.selectList(new QueryWrapper<FestivalConfig>().lambda()
.eq(FestivalConfig::getYear,qo.getYear())
.eq(FestivalConfig::getName, qo.getName()).
eq(FestivalConfig::getDeleteFlag, DeleteFlagEnum.USED)); eq(FestivalConfig::getDeleteFlag, DeleteFlagEnum.USED));
if(!CollectionUtils.isEmpty(festivalConfigs)){ if(!CollectionUtils.isEmpty(festivalConfigs)){
throw new BusinessException("已存在"); throw new BusinessException("已存在");
File diff suppressed because it is too large Load Diff
@@ -526,7 +526,8 @@ public class ZtBugServiceImpl extends ServiceImpl<ZtBugMapper, ZtBug> implements
ztBugs = this.baseMapper.selectList(new QueryWrapper<ZtBug>().lambda().eq(ZtBug::getTask, task)); ztBugs = this.baseMapper.selectList(new QueryWrapper<ZtBug>().lambda().eq(ZtBug::getTask, task));
} }
if(story!=null&&story!=0){ if(story!=null&&story!=0){
ztBugs = this.baseMapper.selectList(new QueryWrapper<ZtBug>().lambda().eq(ZtBug::getStory, story)); //CHG-067:入参是 toStory 的需求ID,关联查询应打 toStory 列(story 列全库未用)
ztBugs = this.baseMapper.selectList(new QueryWrapper<ZtBug>().lambda().eq(ZtBug::getTostory, story));
} }
List<ZtBugDTO> ztBugDTOS =new ArrayList<>(); List<ZtBugDTO> ztBugDTOS =new ArrayList<>();
if(!CollectionUtils.isEmpty(ztBugs)){ if(!CollectionUtils.isEmpty(ztBugs)){
@@ -0,0 +1,464 @@
package com.sa.zentao.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sa.zentao.dao.BusinessException;
import com.sa.zentao.entity.ZtCase;
import com.sa.zentao.entity.ZtDocCheck;
import com.sa.zentao.entity.ZtFile;
import com.sa.zentao.entity.ZtMonthScore;
import com.sa.zentao.entity.ZtStory;
import com.sa.zentao.entity.ZtStoryExpand;
import com.sa.zentao.entity.ZtStoryMonthWorkload;
import com.sa.zentao.mapper.ZtDocCheckMapper;
import com.sa.zentao.service.IZtCaseService;
import com.sa.zentao.service.IZtDocCheckService;
import com.sa.zentao.service.IZtFileService;
import com.sa.zentao.service.IZtMonthScoreService;
import com.sa.zentao.service.IZtStoryExpandService;
import com.sa.zentao.service.IZtStoryMonthWorkloadService;
import com.sa.zentao.service.IZtStoryService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* <p>
* 大型需求文档齐备核查 服务实现类(FR-014 三期)
* 大型需求 = zt_story_expand.workload_index > 20 且当月有完成度变化(zt_story_month_workload.time=month)
* 五类文档判定源:
* 《需求测试用例》=zt_case(story 关联存在);《需求测试报告》=zt_file(testReportSubmit);
* 《AI代码审查报告》=zt_file(aiCodeReview);《AI工作日志》=zt_file(aiWorkLog);
* 《AI项目文档更新记录》=zt_file(aiWorkLog) 且 extra/title 含 doc_update(宽松匹配)
* 扣分 = 缺失数×2,合并写 zt_month_score.scopeJson 的 docCheck 节点(account+dateStr 维度)
* </p>
*
* @author ai
* @since 2026-07-28
*/
@Service
@Slf4j
public class ZtDocCheckServiceImpl extends ServiceImpl<ZtDocCheckMapper, ZtDocCheck> implements IZtDocCheckService {
/** 《需求测试用例》 */
public static final String DOC_TEST_CASE = "testCase";
/** 《需求测试报告》(判定源 zt_file objectType=testReportSubmit) */
public static final String DOC_TEST_REPORT = "testReport";
/** 《AI代码审查报告》 */
public static final String DOC_AI_CODE_REVIEW = "aiCodeReview";
/** 《AI工作日志》 */
public static final String DOC_AI_WORK_LOG = "aiWorkLog";
/** 《AI项目文档更新记录》(zt_file(aiWorkLog) 中 doc_update 类内容) */
public static final String DOC_DOC_UPDATE = "docUpdate";
public static final List<String> DOC_TYPES = Arrays.asList(
DOC_TEST_CASE, DOC_TEST_REPORT, DOC_AI_CODE_REVIEW, DOC_AI_WORK_LOG, DOC_DOC_UPDATE);
/** 大型需求阈值:AI 评估工作量指数 > 20 */
public static final BigDecimal LARGE_WORKLOAD_INDEX = new BigDecimal("20");
/** 每缺失一份扣分 */
public static final int DEDUCT_PER_MISSING = 2;
/** 系统写入人 */
private static final String SYSTEM_USER = "system";
@Autowired
private IZtStoryExpandService storyExpandService;
@Autowired
private IZtStoryMonthWorkloadService storyMonthWorkloadService;
@Autowired
private IZtStoryService storyService;
@Autowired
private IZtCaseService caseService;
@Autowired
private IZtFileService fileService;
@Autowired
private IZtMonthScoreService monthScoreService;
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> generateMonthSnapshot(String month) {
if (StringUtils.isEmpty(month)) {
throw new BusinessException("月份不能为空");
}
List<Integer> storyIds = listLargeStoryIds(month);
Date now = new Date();
int checkRows = 0;
for (Integer storyId : storyIds) {
for (String docType : DOC_TYPES) {
ZtDocCheck row = checkOne(storyId, month, docType, now);
upsertRow(row);
checkRows++;
}
}
// 按责任人(zt_story.assignedTo)归集缺失扣分
Map<String, BigDecimal> deductByAccount = new HashMap<>();
if (!CollectionUtils.isEmpty(storyIds)) {
List<ZtDocCheck> rows = this.list(new QueryWrapper<ZtDocCheck>().lambda()
.eq(ZtDocCheck::getMonth, month).in(ZtDocCheck::getStoryId, storyIds));
List<ZtStory> stories = storyService.listByIds(storyIds);
Map<Integer, ZtStory> storyMap = stories.stream().collect(Collectors.toMap(ZtStory::getId, o -> o, (a, b) -> a));
Set<String> accounts = new HashSet<>();
for (ZtStory story : stories) {
if (StringUtils.isNotEmpty(story.getAssignedTo())) {
accounts.add(story.getAssignedTo());
}
}
for (String account : accounts) {
BigDecimal deduct = writeAccountDocCheckNode(account, month, rows, storyMap);
deductByAccount.put(account, deduct);
}
}
Map<String, Object> result = new HashMap<>();
result.put("month", month);
result.put("storyCount", storyIds.size());
result.put("checkRows", checkRows);
result.put("deductByAccount", deductByAccount);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void appeal(Integer storyId, String month, String docType, String reason) {
if (storyId == null || StringUtils.isEmpty(month) || StringUtils.isEmpty(docType)) {
throw new BusinessException("storyId/month/docType 不能为空");
}
if (StringUtils.isEmpty(reason)) {
throw new BusinessException("申诉理由不能为空");
}
ZtDocCheck row = getRow(storyId, month, docType);
if (row == null) {
throw new BusinessException("核查记录不存在,请先生成当月快照");
}
if (row.getPresent() != null && row.getPresent() == 1) {
throw new BusinessException("该文档已齐备,无需申诉");
}
if (!ZtDocCheck.STATUS_PENDING.equals(row.getStatus())) {
throw new BusinessException("当前状态不可申诉(仅待确认记录可申诉)");
}
row.setStatus(ZtDocCheck.STATUS_APPEALING);
row.setAppealReason(reason);
row.setUpdateTime(new Date());
this.updateById(row);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void resolveAppeal(Integer storyId, String month, String docType, boolean pass, String remark) {
if (storyId == null || StringUtils.isEmpty(month) || StringUtils.isEmpty(docType)) {
throw new BusinessException("storyId/month/docType 不能为空");
}
ZtDocCheck row = getRow(storyId, month, docType);
if (row == null) {
throw new BusinessException("核查记录不存在");
}
if (!ZtDocCheck.STATUS_APPEALING.equals(row.getStatus())) {
throw new BusinessException("仅申诉中记录可复核");
}
row.setStatus(ZtDocCheck.STATUS_RESOLVED);
row.setAppealResult(pass ? ZtDocCheck.APPEAL_PASS : ZtDocCheck.APPEAL_REJECT);
row.setResolveRemark(remark);
row.setUpdateTime(new Date());
this.updateById(row);
// 复核通过 → 回滚该账号当月 docCheck 扣分(重算责任人维度缺失)
ZtStory story = storyService.getById(storyId);
if (story != null && StringUtils.isNotEmpty(story.getAssignedTo())) {
recomputeAccountDeduction(story.getAssignedTo(), month);
}
}
@Override
public List<ZtDocCheck> listMonthRows(String month) {
if (StringUtils.isEmpty(month)) {
return new ArrayList<>();
}
return this.list(new QueryWrapper<ZtDocCheck>().lambda().eq(ZtDocCheck::getMonth, month));
}
@Override
public List<Map<String, Object>> listMatrix(String month) {
List<ZtDocCheck> rows = listMonthRows(month);
Map<Integer, List<ZtDocCheck>> byStory = rows.stream().collect(Collectors.groupingBy(ZtDocCheck::getStoryId));
List<Map<String, Object>> result = new ArrayList<>();
if (byStory.isEmpty()) {
return result;
}
List<ZtStory> stories = storyService.listByIds(byStory.keySet());
Map<Integer, String> titleMap = stories.stream()
.collect(Collectors.toMap(ZtStory::getId, ZtStory::getTitle, (a, b) -> a));
Map<Integer, String> wiMap = new HashMap<>();
for (ZtStoryExpand e : storyExpandService.list(
new QueryWrapper<ZtStoryExpand>().lambda().in(ZtStoryExpand::getStoryId, byStory.keySet()))) {
wiMap.put(e.getStoryId(), e.getWorkloadIndex());
}
for (Map.Entry<Integer, List<ZtDocCheck>> en : byStory.entrySet()) {
Map<String, Object> row = new HashMap<>();
Map<String, Integer> docs = new HashMap<>();
int missing = 0;
String status = null;
String appealStatus = null;
for (ZtDocCheck c : en.getValue()) {
int present = (c.getPresent() != null && c.getPresent() == 1) ? 1 : 0;
docs.put(c.getDocType(), present);
if (present == 0 && c.countsAsMissing()) {
missing++;
}
if (StringUtils.isNotEmpty(c.getStatus())) {
status = c.getStatus();
}
if (StringUtils.isNotEmpty(c.getAppealResult())) {
appealStatus = c.getAppealResult();
} else if (StringUtils.isNotEmpty(c.getAppealReason())) {
appealStatus = "appealing";
}
}
row.put("storyId", en.getKey());
row.put("title", titleMap.get(en.getKey()));
row.put("workloadIndex", wiMap.get(en.getKey()));
row.put("docs", docs);
row.put("deduct", missing * DEDUCT_PER_MISSING);
row.put("status", status);
row.put("appealStatus", appealStatus);
result.add(row);
}
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void confirmMonth(String month) {
if (StringUtils.isEmpty(month)) {
throw new BusinessException("月份不能为空");
}
List<ZtDocCheck> rows = listMonthRows(month);
for (ZtDocCheck c : rows) {
c.setStatus("CONFIRMED");
}
if (!rows.isEmpty()) {
this.updateBatchById(rows);
}
}
/**
* 大型需求 = workload_index>20 且当月有完成度变化
*/
public List<Integer> listLargeStoryIds(String month) {
List<ZtStoryMonthWorkload> monthRecords = storyMonthWorkloadService.list(
new QueryWrapper<ZtStoryMonthWorkload>().lambda().eq(ZtStoryMonthWorkload::getTime, month));
if (CollectionUtils.isEmpty(monthRecords)) {
return new ArrayList<>();
}
Set<Integer> monthStoryIds = monthRecords.stream().map(ZtStoryMonthWorkload::getStoryId).collect(Collectors.toSet());
List<ZtStoryExpand> expands = storyExpandService.list(
new QueryWrapper<ZtStoryExpand>().lambda().in(ZtStoryExpand::getStoryId, monthStoryIds));
List<Integer> storyIds = new ArrayList<>();
for (ZtStoryExpand expand : expands) {
BigDecimal workloadIndex = parseWorkloadIndex(expand.getWorkloadIndex());
if (workloadIndex != null && workloadIndex.compareTo(LARGE_WORKLOAD_INDEX) > 0) {
storyIds.add(expand.getStoryId());
}
}
return storyIds;
}
private BigDecimal parseWorkloadIndex(String workloadIndex) {
if (StringUtils.isEmpty(workloadIndex) || StringUtils.isEmpty(workloadIndex.trim())) {
return null;
}
try {
return new BigDecimal(workloadIndex.trim());
} catch (NumberFormatException e) {
return null;
}
}
/**
* 单需求单文档类型五路判定
*/
private ZtDocCheck checkOne(Integer storyId, String month, String docType, Date now) {
boolean present;
String detail;
switch (docType) {
case DOC_TEST_CASE: {
long count = caseService.count(new QueryWrapper<ZtCase>().lambda().eq(ZtCase::getStory, storyId));
present = count > 0;
detail = present ? "zt_case 关联用例 " + count + " 条" : "zt_case 无关联用例";
break;
}
case DOC_TEST_REPORT: {
long count = countFile(storyId, "testReportSubmit", false);
present = count > 0;
detail = present ? "zt_file(testReportSubmit) " + count + " 份" : "zt_file(testReportSubmit) 无附件";
break;
}
case DOC_AI_CODE_REVIEW: {
long count = countFile(storyId, "aiCodeReview", false);
present = count > 0;
detail = present ? "zt_file(aiCodeReview) " + count + " 份" : "zt_file(aiCodeReview) 无附件";
break;
}
case DOC_AI_WORK_LOG: {
long count = countFile(storyId, "aiWorkLog", false);
present = count > 0;
detail = present ? "zt_file(aiWorkLog) " + count + " 份" : "zt_file(aiWorkLog) 无附件";
break;
}
case DOC_DOC_UPDATE: {
//CHG-046 《AI项目文档更新记录》改独立通道 zt_file(aiDocUpdate)(原 aiWorkLog 的 doc_update 类从未产出,已废弃)
long count = countFile(storyId, "aiDocUpdate", false);
present = count > 0;
detail = present ? "zt_file(aiDocUpdate) " + count + " 份" : "zt_file(aiDocUpdate) 无附件";
break;
}
default:
throw new BusinessException("未知文档类型:" + docType);
}
ZtDocCheck row = new ZtDocCheck();
row.setStoryId(storyId);
row.setMonth(month);
row.setDocType(docType);
row.setPresent(present ? 1 : 0);
row.setDetail(detail);
row.setStatus(ZtDocCheck.STATUS_PENDING);
row.setCheckTime(now);
return row;
}
/**
* zt_file 附件计数(未删除;docUpdate 类要求 extra/title 含 doc_update,宽松匹配)
*/
private long countFile(Integer storyId, String objectType, boolean docUpdateOnly) {
LambdaQueryWrapper<ZtFile> wrapper = new QueryWrapper<ZtFile>().lambda()
.eq(ZtFile::getObjecttype, objectType)
.eq(ZtFile::getObjectid, storyId)
.and(w -> w.isNull(ZtFile::getDeleted).or().eq(ZtFile::getDeleted, "0"));
if (docUpdateOnly) {
wrapper.and(w -> w.like(ZtFile::getExtra, "doc_update").or().like(ZtFile::getTitle, "doc_update"));
}
return fileService.count(wrapper);
}
/**
* 按 uk_story_month_doc 覆盖写入;申诉中/已复核记录保留状态,仅刷新判定结果
*/
private void upsertRow(ZtDocCheck row) {
ZtDocCheck exist = getRow(row.getStoryId(), row.getMonth(), row.getDocType());
if (exist == null) {
row.setCreateTime(new Date());
row.setUpdateTime(new Date());
this.save(row);
return;
}
exist.setPresent(row.getPresent());
exist.setDetail(row.getDetail());
exist.setCheckTime(row.getCheckTime());
exist.setUpdateTime(new Date());
this.updateById(exist);
}
private ZtDocCheck getRow(Integer storyId, String month, String docType) {
return this.getOne(new QueryWrapper<ZtDocCheck>().lambda()
.eq(ZtDocCheck::getStoryId, storyId)
.eq(ZtDocCheck::getMonth, month)
.eq(ZtDocCheck::getDocType, docType)
.last("LIMIT 1"));
}
/**
* 重算某账号当月的 docCheck 扣分节点(申诉复核通过后回滚用)
*/
private void recomputeAccountDeduction(String account, String month) {
List<ZtDocCheck> rows = listMonthRows(month);
if (CollectionUtils.isEmpty(rows)) {
return;
}
Set<Integer> storyIds = rows.stream().map(ZtDocCheck::getStoryId).collect(Collectors.toSet());
List<ZtStory> stories = storyService.listByIds(storyIds);
Map<Integer, ZtStory> storyMap = stories.stream().collect(Collectors.toMap(ZtStory::getId, o -> o, (a, b) -> a));
writeAccountDocCheckNode(account, month, rows, storyMap);
}
/**
* 合并写 zt_month_score.scopeJson 的 docCheck 节点(account+dateStr 维度,保留已有明细)
* @return 本次缺失扣分
*/
private BigDecimal writeAccountDocCheckNode(String account, String month, List<ZtDocCheck> rows, Map<Integer, ZtStory> storyMap) {
List<ZtDocCheck> mine = rows.stream()
.filter(o -> {
ZtStory story = storyMap.get(o.getStoryId());
return story != null && account.equals(story.getAssignedTo());
})
.collect(Collectors.toList());
List<ZtDocCheck> missing = mine.stream().filter(ZtDocCheck::countsAsMissing).collect(Collectors.toList());
BigDecimal deduct = BigDecimal.valueOf((long) missing.size() * DEDUCT_PER_MISSING);
JSONObject docCheck = new JSONObject();
docCheck.put("month", month);
docCheck.put("missingCount", missing.size());
docCheck.put("deductScore", deduct);
docCheck.put("deductPerMissing", DEDUCT_PER_MISSING);
JSONArray items = new JSONArray();
for (ZtDocCheck row : missing) {
JSONObject item = new JSONObject();
item.put("storyId", row.getStoryId());
item.put("docType", row.getDocType());
item.put("detail", row.getDetail());
items.add(item);
}
docCheck.put("items", items);
ZtMonthScore monthScore = monthScoreService.getOne(new QueryWrapper<ZtMonthScore>().lambda()
.eq(ZtMonthScore::getAccount, account)
.eq(ZtMonthScore::getDateStr, month)
.last("LIMIT 1"));
Date now = new Date();
if (monthScore == null) {
monthScore = new ZtMonthScore();
monthScore.setAccount(account);
monthScore.setDateStr(month);
monthScore.setCreateDate(now);
monthScore.setCreateUser(SYSTEM_USER);
JSONObject scopeJson = new JSONObject();
scopeJson.put("docCheck", docCheck);
monthScore.setScopeJson(scopeJson.toJSONString());
monthScore.setUpdateDate(now);
monthScore.setUpdateUser(SYSTEM_USER);
monthScoreService.save(monthScore);
} else {
JSONObject scopeJson;
try {
scopeJson = StringUtils.isEmpty(monthScore.getScopeJson())
? new JSONObject() : JSON.parseObject(monthScore.getScopeJson());
} catch (Exception e) {
log.warn("zt_month_score scopeJson 非 JSON,重写 docCheck 节点 account={} month={}", account, month);
scopeJson = new JSONObject();
}
scopeJson.put("docCheck", docCheck);
monthScore.setScopeJson(scopeJson.toJSONString());
monthScore.setUpdateDate(now);
monthScore.setUpdateUser(SYSTEM_USER);
monthScoreService.updateById(monthScore);
}
return deduct;
}
}
@@ -1,13 +1,21 @@
package com.sa.zentao.service.impl; package com.sa.zentao.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.sa.zentao.conf.RiskUserThreadLocal;
import com.sa.zentao.dao.BusinessException;
import com.sa.zentao.entity.ZtFile; import com.sa.zentao.entity.ZtFile;
import com.sa.zentao.entity.ZtMeeting;
import com.sa.zentao.entity.ZtStory;
import com.sa.zentao.enums.FileTypes; import com.sa.zentao.enums.FileTypes;
import com.sa.zentao.mapper.ZtFileMapper; import com.sa.zentao.mapper.ZtFileMapper;
import com.sa.zentao.mapper.ZtMeetingMapper;
import com.sa.zentao.mapper.ZtStoryMapper;
import com.sa.zentao.service.IZtFileService; import com.sa.zentao.service.IZtFileService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -26,6 +34,18 @@ import java.util.List;
@Service @Service
public class ZtFileServiceImpl extends ServiceImpl<ZtFileMapper, ZtFile> implements IZtFileService { public class ZtFileServiceImpl extends ServiceImpl<ZtFileMapper, ZtFile> implements IZtFileService {
@Autowired
private ZtStoryMapper storyMapper;
@Autowired
private ZtMeetingMapper meetingMapper;
@Autowired
private com.sa.zentao.service.IZtActionService actionService;
@org.springframework.beans.factory.annotation.Value("${file.baseUrl}")
private String baseUrl;
@Override @Override
@Transactional @Transactional
public void updateFile(String fileIds, Integer objectId, FileTypes type) { public void updateFile(String fileIds, Integer objectId, FileTypes type) {
@@ -50,4 +70,88 @@ public class ZtFileServiceImpl extends ServiceImpl<ZtFileMapper, ZtFile> impleme
this.saveOrUpdateBatch(ztFiles); this.saveOrUpdateBatch(ztFiles);
} }
@Override
@Transactional
public void refreshOwnerUrl(String objectType, Integer objectId, String url, String reviewResult) {
if (StringUtils.isEmpty(objectType) || objectId == null) {
throw new BusinessException("objectType/objectId不能为空");
}
//动态留痕 actor 取 token 身份(无登录态兜底 ai,兼容既有单测直调)(CHG-061)
String actionActor = RiskUserThreadLocal.get() == null || StringUtils.isEmpty(RiskUserThreadLocal.get().getName())
? "ai" : RiskUserThreadLocal.get().getName();
FileTypes type = FileTypes.transferType(objectType);
if (type == null) {
throw new BusinessException("不支持的objectType:" + objectType);
}
switch (type) {
case story:
case storyPrd:
case aiCodeReview:
case aiWorkLog:
case aiDocUpdate:
case testCase:
case testReport:
case testReportSubmit:
case testOther:
ZtStory ztStory = this.storyMapper.selectById(objectId);
if (ztStory == null) {
throw new BusinessException("需求不存在:" + objectId);
}
LambdaUpdateWrapper<ZtStory> wrapper = new UpdateWrapper<ZtStory>().lambda()
.eq(ZtStory::getId, objectId);
switch (type) {
case story:
case storyPrd:
wrapper.set(ZtStory::getPrdUrl, url);
break;
case aiCodeReview:
wrapper.set(ZtStory::getCodeReviewUrl, url);
if (StringUtils.isNotEmpty(reviewResult)) {
wrapper.set(ZtStory::getCodeReviewStatus, reviewResult);
}
break;
case aiWorkLog:
wrapper.set(ZtStory::getWorkLogUrl, url);
break;
case aiDocUpdate:
wrapper.set(ZtStory::getAiDocUpdateUrl, url);
break;
case testCase:
wrapper.set(ZtStory::getTestCaseUrl, url);
break;
case testReport:
wrapper.set(ZtStory::getTestReportDownloadUrl, url);
break;
case testReportSubmit:
wrapper.set(ZtStory::getTestReportSubmitUrl, url);
break;
case testOther:
wrapper.set(ZtStory::getTestOtherUrl, url);
break;
default:
break;
}
this.storyMapper.update(null, wrapper);
//上传动态日志(zt_action):与禅道原生操作一致留痕
actionService.addAction(com.sa.zentao.enums.ActionType.XQ, com.sa.zentao.enums.ActionStatus.BJ,
objectId, ztStory.getProduct() == null ? null : ztStory.getProduct() + "", null, null,
actionActor, "上传" + type.getValue() + ":" + baseUrl + url, null);
break;
case meeting:
ZtMeeting ztMeeting = this.meetingMapper.selectById(objectId);
if (ztMeeting == null) {
throw new BusinessException("会议不存在:" + objectId);
}
this.meetingMapper.update(null, new UpdateWrapper<ZtMeeting>().lambda()
.set(ZtMeeting::getUrl, url)
.eq(ZtMeeting::getId, objectId));
actionService.addAction(com.sa.zentao.enums.ActionType.MEET, com.sa.zentao.enums.ActionStatus.BJ,
objectId, ztMeeting.getProductId() == null ? null : ztMeeting.getProductId() + "", null, null,
actionActor, "上传会议纪要:" + baseUrl + url, null);
break;
default:
throw new BusinessException("uploadBind不支持的objectType:" + objectType);
}
}
} }
@@ -4,10 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.conf.RiskUserThreadLocal; import com.sa.zentao.conf.RiskUserThreadLocal;
import com.sa.zentao.dao.*; import com.sa.zentao.dao.*;
import com.sa.zentao.entity.*; import com.sa.zentao.entity.*;
import com.sa.zentao.enums.ActionStatus; import com.sa.zentao.enums.*;
import com.sa.zentao.enums.ActionType; import com.sa.zentao.utils.KanbanStageMapping;
import com.sa.zentao.enums.KanbanCellType;
import com.sa.zentao.enums.ProjectTypeEnums;
import com.sa.zentao.mapper.ZtKanbancolumnMapper; import com.sa.zentao.mapper.ZtKanbancolumnMapper;
import com.sa.zentao.mapper.ZtKanbanlaneMapper; import com.sa.zentao.mapper.ZtKanbanlaneMapper;
import com.sa.zentao.qo.KanbanQo; import com.sa.zentao.qo.KanbanQo;
@@ -17,6 +15,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sa.zentao.utils.BeanCopyUtil; import com.sa.zentao.utils.BeanCopyUtil;
import com.sa.zentao.utils.DateUtils; import com.sa.zentao.utils.DateUtils;
import com.sa.zentao.utils.KanBanConstant; import com.sa.zentao.utils.KanBanConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +36,7 @@ import java.util.stream.Stream;
* @since 2024-07-09 * @since 2024-07-09
*/ */
@Service @Service
@Slf4j
public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtKanbanlane> implements IZtKanbanlaneService { public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtKanbanlane> implements IZtKanbanlaneService {
@Autowired @Autowired
@@ -116,6 +116,16 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
st.setAssignedToName(ztUser.getNickname()); st.setAssignedToName(ztUser.getNickname());
st.setColor(ztUser.getColor()); st.setColor(ztUser.getColor());
} }
ztUser = userMap.get(st.getProductUser());
if(ztUser!=null){
st.setProductUserName(ztUser.getNickname());
st.setProductUserColor(ztUser.getColor());
}
ztUser = userMap.get(st.getTestUser());
if(ztUser!=null){
st.setTestUserName(ztUser.getNickname());
st.setTestUserColor(ztUser.getColor());
}
if(product!=null){ if(product!=null){
st.setProductName(product.getName()); st.setProductName(product.getName());
} }
@@ -219,7 +229,7 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
ZtKanbancell fromkanbancell = this.kanbancellService.getOne(new QueryWrapper<ZtKanbancell>().lambda() ZtKanbancell fromkanbancell = this.kanbancellService.getOne(new QueryWrapper<ZtKanbancell>().lambda()
.eq(ZtKanbancell::getColumn, fromId)); .eq(ZtKanbancell::getColumn, fromId));
List<String> carIds = new ArrayList<>(Arrays.asList(fromkanbancell.getCards().split(","))); Set<String> carIds = new HashSet<>(Arrays.asList(fromkanbancell.getCards().split(",")));
carIds.remove(qo.getId().toString()); carIds.remove(qo.getId().toString());
@@ -235,7 +245,7 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
if(StringUtils.isEmpty(tokanbancell.getCards())){ if(StringUtils.isEmpty(tokanbancell.getCards())){
tokanbancell.setCards(qo.getId().toString()); tokanbancell.setCards(qo.getId().toString());
}else{ }else{
carIds = new ArrayList<>(Arrays.asList(tokanbancell.getCards().split(","))); carIds = new HashSet<>(Arrays.asList(tokanbancell.getCards().split(",")));
carIds.add(qo.getId().toString()); carIds.add(qo.getId().toString());
tokanbancell.setCards(StringUtils.join(carIds,",")); tokanbancell.setCards(StringUtils.join(carIds,","));
} }
@@ -243,14 +253,14 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
this.kanbancellService.updateById(tokanbancell); this.kanbancellService.updateById(tokanbancell);
ActionType type= null; ActionType type= null;
if("story".equalsIgnoreCase(tabType)){ if(KanbanCellType.STORY.getValue().equalsIgnoreCase(tabType)){
type=ActionType.XQ; type=ActionType.XQ;
//需求 //需求
// storyChange(qo); // storyChange(qo);
//testing 测试中 //testing 测试中
} }
if("bug".equalsIgnoreCase(tabType)){ if(KanbanCellType.BUG.getValue().equalsIgnoreCase(tabType)){
type=ActionType.BUG; type=ActionType.BUG;
//bug //bug
// bugChange(qo); // bugChange(qo);
@@ -261,7 +271,7 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
//closed 关闭 //closed 关闭
} }
if("task".equalsIgnoreCase(tabType)){ if(KanbanCellType.TASK.getValue().equalsIgnoreCase(tabType)){
type=ActionType.RW; type=ActionType.RW;
//task //task
// taskChange(qo); // taskChange(qo);
@@ -273,7 +283,7 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
//完成有新任务回退到进行中 //完成有新任务回退到进行中
// //
// if("canceled".equalsIgnoreCase(statusType)){ // if(KanbanColumnType.TASK_CANCEL.getValue().equalsIgnoreCase(statusType)){
// //取消 // //取消
// } // }
@@ -303,12 +313,15 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
List<String> carIds = new ArrayList<>(Arrays.asList(cell.getCards().split(","))); List<String> carIds = new ArrayList<>(Arrays.asList(cell.getCards().split(",")));
if(carIds.contains(bussId.toString())){ if(carIds.contains(bussId.toString())){
thisZtKanbancell=cell; thisZtKanbancell=cell;
continue; break;
} }
} }
List<String> carIds = new ArrayList<>(Arrays.asList(thisZtKanbancell.getCards().split(","))); Set<String> carIds = new HashSet<>(Arrays.asList(thisZtKanbancell.getCards().split(",")));
while (carIds.contains(bussId.toString())) {
carIds.remove(bussId.toString()); carIds.remove(bussId.toString());
}
if(CollectionUtils.isEmpty(carIds)){ if(CollectionUtils.isEmpty(carIds)){
thisZtKanbancell.setCards(""); thisZtKanbancell.setCards("");
@@ -317,8 +330,13 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
thisZtKanbancell.setCards(StringUtils.join(carIds,",")); thisZtKanbancell.setCards(StringUtils.join(carIds,","));
} }
this.kanbancellService.updateById(thisZtKanbancell);
ZtKanbancell ztKanbanlane = this.baseMapper.getZtKanbanlane(type, toStatus, execId); ZtKanbancell ztKanbanlane = this.baseMapper.getZtKanbanlane(type, toStatus, execId);
if(ztKanbanlane==null){
log.info("看板未获取到execId={} bussId={} type={} toStatus={}", execId, bussId, type, toStatus);
return;
}
this.kanbancellService.updateById(thisZtKanbancell);
String cards = ztKanbanlane.getCards(); String cards = ztKanbanlane.getCards();
List l=StringUtils.isEmpty(cards)?new ArrayList<>():new ArrayList<>(Arrays.asList(cards.split(","))); List l=StringUtils.isEmpty(cards)?new ArrayList<>():new ArrayList<>(Arrays.asList(cards.split(",")));
l.add(bussId.toString()); l.add(bussId.toString());
@@ -329,7 +347,7 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
@Override @Override
public void addStory(Integer id, List<ZtStory> ztStories) { public void addStory(Integer id, List<ZtStory> ztStories) {
ZtKanbanlane lane = this.baseMapper.selectOne(new QueryWrapper<ZtKanbanlane>().lambda() ZtKanbanlane lane = this.baseMapper.selectOne(new QueryWrapper<ZtKanbanlane>().lambda()
.eq(ZtKanbanlane::getExecution, id).eq(ZtKanbanlane::getType, "story")); .eq(ZtKanbanlane::getExecution, id).eq(ZtKanbanlane::getType, KanbanCellType.STORY.getValue()));
List<ZtKanbancolumnDTO> ztKanbancolumnDTOS = this.kanbancolumnMapper.listByLaneIds(Arrays.asList(lane.getId())); List<ZtKanbancolumnDTO> ztKanbancolumnDTOS = this.kanbancolumnMapper.listByLaneIds(Arrays.asList(lane.getId()));
@@ -361,6 +379,13 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
}else if ("verified".equals(st.getStage())){ }else if ("verified".equals(st.getStage())){
ztKanbancolumnDTO = ztKanbancolumnDTOS.stream().filter(o -> o.getType().equals("verified")).collect(Collectors.toList()).get(0); ztKanbancolumnDTO = ztKanbancolumnDTOS.stream().filter(o -> 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<ZtKanbancell>().lambda() ZtKanbancell kanbancell = this.kanbancellService.getOne(new QueryWrapper<ZtKanbancell>().lambda()
.eq(ZtKanbancell::getKanban, id).eq(ZtKanbancell::getColumn, ztKanbancolumnDTO.getId())); .eq(ZtKanbancell::getKanban, id).eq(ZtKanbancell::getColumn, ztKanbancolumnDTO.getId()));
@@ -382,7 +407,7 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
public void addTask(Integer id, List<ZtTask> asList) { public void addTask(Integer id, List<ZtTask> asList) {
ZtKanbanlane lane = this.baseMapper.selectOne(new QueryWrapper<ZtKanbanlane>().lambda() ZtKanbanlane lane = this.baseMapper.selectOne(new QueryWrapper<ZtKanbanlane>().lambda()
.eq(ZtKanbanlane::getExecution, id).eq(ZtKanbanlane::getType, "task")); .eq(ZtKanbanlane::getExecution, id).eq(ZtKanbanlane::getType, KanbanCellType.TASK.getValue()));
List<ZtKanbancolumnDTO> kanbancolumnList = this.kanbancolumnMapper.listByLaneIds(Arrays.asList(lane.getId())); List<ZtKanbancolumnDTO> kanbancolumnList = this.kanbancolumnMapper.listByLaneIds(Arrays.asList(lane.getId()));
@@ -408,11 +433,13 @@ public class ZtKanbanlaneServiceImpl extends ServiceImpl<ZtKanbanlaneMapper, ZtK
@Override @Override
public void addBug(Integer id, List<ZtBug> asList) { public void addBug(Integer id, List<ZtBug> asList) {
ZtKanbanlane lane = this.baseMapper.selectOne(new QueryWrapper<ZtKanbanlane>().lambda() ZtKanbanlane lane = this.baseMapper.selectOne(new QueryWrapper<ZtKanbanlane>().lambda()
.eq(ZtKanbanlane::getExecution, id).eq(ZtKanbanlane::getType, "bug")); .eq(ZtKanbanlane::getExecution, id).eq(ZtKanbanlane::getType, KanbanCellType.BUG.getValue()));
List<ZtKanbancolumnDTO> ztKanbancolumnDTOS = this.kanbancolumnMapper.listByLaneIds(Arrays.asList(lane.getId())); List<ZtKanbancolumnDTO> ztKanbancolumnDTOS = this.kanbancolumnMapper.listByLaneIds(Arrays.asList(lane.getId()));
List<ZtKanbancolumnDTO> backlog = ztKanbancolumnDTOS.stream().filter(o -> o.getType().equals("unconfirmed")).collect(Collectors.toList()); List<ZtKanbancolumnDTO> backlog = ztKanbancolumnDTOS.stream()
.filter(o -> o.getType().equals(KanbanColumnType.BUG_UNCONFIRMED.getValue()))
.collect(Collectors.toList());
ZtKanbancolumnDTO ztKanbancolumnDTO = backlog.get(0); ZtKanbancolumnDTO ztKanbancolumnDTO = backlog.get(0);
ZtKanbancell kanbancell = this.kanbancellService.getOne(new QueryWrapper<ZtKanbancell>().lambda() ZtKanbancell kanbancell = this.kanbancellService.getOne(new QueryWrapper<ZtKanbancell>().lambda()
@@ -20,6 +20,7 @@ import com.sa.zentao.dao.BusinessException;
import com.sa.zentao.dao.ZtMeetingDTO; import com.sa.zentao.dao.ZtMeetingDTO;
import com.sa.zentao.dao.ZtStoryDTO; import com.sa.zentao.dao.ZtStoryDTO;
import com.sa.zentao.entity.ZtMeeting; import com.sa.zentao.entity.ZtMeeting;
import com.sa.zentao.entity.ZtFile;
import com.sa.zentao.entity.ZtProduct; import com.sa.zentao.entity.ZtProduct;
import com.sa.zentao.entity.ZtStoryUser; import com.sa.zentao.entity.ZtStoryUser;
import com.sa.zentao.entity.ZtUser; import com.sa.zentao.entity.ZtUser;
@@ -88,17 +89,17 @@ public class ZtMeetingServiceImpl extends ServiceImpl<ZtMeetingMapper, ZtMeeting
Map<Integer,ZtStoryUser> sMap=getUserStoryMap(list); Map<Integer,ZtStoryUser> sMap=getUserStoryMap(list);
for (ZtMeetingDTO d:list) { for (ZtMeetingDTO d:list) {
StringBuilder b=new StringBuilder(); // StringBuilder b=new StringBuilder();
if(!StringUtils.isEmpty(d.getUsers())){ // if(!StringUtils.isEmpty(d.getUsers())){
String[] split = d.getUsers().split(","); // String[] split = d.getUsers().split(",");
for (String s:split) { // for (String s:split) {
ZtUser ztUser = userMap.get(s); // ZtUser ztUser = userMap.get(s);
if(ztUser!=null){ // if(ztUser!=null){
b.append(ztUser.getNickname()).append(","); // b.append(ztUser.getNickname()).append(",");
} // }
} // }
d.setUsersName(b.toString()); // d.setUsersName(b.toString());
} // }
StringBuilder storyStr=new StringBuilder(); StringBuilder storyStr=new StringBuilder();
if(!StringUtils.isEmpty( d.getStoryIds())){ if(!StringUtils.isEmpty( d.getStoryIds())){
String[] split = d.getStoryIds().split(","); String[] split = d.getStoryIds().split(",");
@@ -157,6 +158,7 @@ public class ZtMeetingServiceImpl extends ServiceImpl<ZtMeetingMapper, ZtMeeting
this.baseMapper.insert(ztMeeting); this.baseMapper.insert(ztMeeting);
fileService.updateFile(dto.getFiles(),ztMeeting.getId(), FileTypes.meeting); fileService.updateFile(dto.getFiles(),ztMeeting.getId(), FileTypes.meeting);
refreshMeetingUrl(ztMeeting);
//添加action //添加action
@@ -165,6 +167,25 @@ public class ZtMeetingServiceImpl extends ServiceImpl<ZtMeetingMapper, ZtMeeting
} }
/**
* CHG-020:刷新会议纪要最新一份 MD 访问链接到 zt_meeting.url
* (老式两段式上传经 updateFile 绑定时同步刷新,与 uploadBind 的 refreshOwnerUrl 双通道一致)
*/
private void refreshMeetingUrl(ZtMeeting ztMeeting) {
ZtFile latest = fileService.getOne(new QueryWrapper<ZtFile>().lambda()
.eq(ZtFile::getObjecttype, FileTypes.meeting.getCode())
.eq(ZtFile::getObjectid, ztMeeting.getId())
.eq(ZtFile::getDeleted, "0")
.orderByDesc(ZtFile::getId).last("limit 1"), false);
if (latest != null) {
String url = StringUtils.isNotEmpty(latest.getUrl()) ? latest.getUrl() : latest.getPathname();
if (StringUtils.isNotEmpty(url)) {
ztMeeting.setUrl(url);
this.baseMapper.updateById(ztMeeting);
}
}
}
@Override @Override
@Transactional @Transactional
public void modify(ZtMeetingDTO dto) { public void modify(ZtMeetingDTO dto) {
@@ -182,6 +203,7 @@ public class ZtMeetingServiceImpl extends ServiceImpl<ZtMeetingMapper, ZtMeeting
this.baseMapper.updateById(ztMeeting); this.baseMapper.updateById(ztMeeting);
fileService.updateFile(dto.getFiles(),ztMeeting.getId(), FileTypes.meeting); fileService.updateFile(dto.getFiles(),ztMeeting.getId(), FileTypes.meeting);
refreshMeetingUrl(ztMeeting);
//添加action //添加action
actionService.addAction(ActionType.MEET, ActionStatus.BJ, ztMeeting.getId(), ztMeeting.getProductId() + "", null, null, actionService.addAction(ActionType.MEET, ActionStatus.BJ, ztMeeting.getId(), ztMeeting.getProductId() + "", null, null,
@@ -0,0 +1,88 @@
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.ZtPerfConfig;
import com.sa.zentao.entity.ZtUser;
import com.sa.zentao.enums.UserType;
import com.sa.zentao.mapper.ZtPerfConfigMapper;
import com.sa.zentao.service.IZtPerfConfigService;
import com.sa.zentao.service.IZtUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* <p>
* 绩效指标规则配置 服务实现类(三期)
* </p>
*
* @author ai
* @since 2026-07-28
*/
@Service
public class ZtPerfConfigServiceImpl extends ServiceImpl<ZtPerfConfigMapper, ZtPerfConfig> implements IZtPerfConfigService {
@Autowired
private IZtUserService userService;
@Override
public ZtPerfConfig getEnabledConfig(String role, String itemKey) {
if (StringUtils.isEmpty(itemKey)) {
return null;
}
if (StringUtils.isNotEmpty(role)) {
ZtPerfConfig config = this.getOne(new QueryWrapper<ZtPerfConfig>().lambda()
.eq(ZtPerfConfig::getRole, role)
.eq(ZtPerfConfig::getItemKey, itemKey)
.eq(ZtPerfConfig::getEnabled, 1)
.last("LIMIT 1"));
if (config != null) {
return config;
}
}
// 回退:该指标任意一条启用配置(岗位未识别/未配置时兜底取规则参数)
List<ZtPerfConfig> list = this.list(new QueryWrapper<ZtPerfConfig>().lambda()
.eq(ZtPerfConfig::getItemKey, itemKey)
.eq(ZtPerfConfig::getEnabled, 1)
.last("LIMIT 1"));
return CollectionUtils.isEmpty(list) ? null : list.get(0);
}
@Override
public String resolveRole(String account) {
if (StringUtils.isEmpty(account)) {
return null;
}
ZtUser user = userService.getOne(new QueryWrapper<ZtUser>().lambda()
.eq(ZtUser::getAccount, account).last("LIMIT 1"));
if (user == null || user.getUserType() == null) {
return null;
}
UserType userType = user.getUserType();
switch (userType) {
case XMJL:
case XMGLY:
return "projectManager";
case CPJL:
return "productManager";
case CP:
case XMZL:
return "productAssistant";
case KFZ:
// 前后端经 userType 无法区分,默认后端;后续可按 zt_user.role 细分
return "backendDev";
case CS:
return "tester";
case UI:
return "uiEngineer";
case YW:
return "opsEngineer";
default:
return null;
}
}
}
@@ -0,0 +1,366 @@
package com.sa.zentao.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.conf.RiskUserThreadLocal;
import com.sa.zentao.dao.BusinessException;
import com.sa.zentao.entity.ZtMonthScore;
import com.sa.zentao.entity.ZtPerfConfig;
import com.sa.zentao.entity.ZtUser;
import com.sa.zentao.qo.ZtPerfQo;
import com.sa.zentao.service.IZtPerfConfigService;
import com.sa.zentao.service.IZtPerfReportService;
import com.sa.zentao.service.IZtMonthScoreService;
import com.sa.zentao.service.IZtUserService;
import com.sa.zentao.service.perf.PerfIndicatorCalculator;
import com.sa.zentao.service.perf.PerfMonthScope;
import com.sa.zentao.service.perf.PerfScoreEngine;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* <p>
* 绩效报表(三期批2:报表引擎+接口) 服务实现类
* </p>
*
* @author ai
* @since 2026-07-28
*/
@Service
public class ZtPerfReportServiceImpl implements IZtPerfReportService {
private static final String APPEAL_PENDING = "pending";
private static final String APPEAL_UPHELD = "upheld";
private static final String APPEAL_OVERTURNED = "overturned";
@Autowired
private IZtMonthScoreService monthScoreService;
@Autowired
private IZtPerfConfigService perfConfigService;
@Autowired
private IZtUserService userService;
@Autowired
private PerfScoreEngine perfScoreEngine;
/**
* 批1指标计算器注册表(对接点;无计算器时 auto 项 score 为 null)
*/
@Autowired(required = false)
private List<PerfIndicatorCalculator> calculators = new ArrayList<>();
@Override
public PerfMonthScope report(String month, String role, String account) {
if (StringUtils.isBlank(account) && RiskUserThreadLocal.get() != null) {
// 未指定账号(前端“全部人员”选项 v1):默认当前登录人
account = RiskUserThreadLocal.get().getName();
}
checkMonthAccount(month, account);
if (StringUtils.isBlank(role)) {
// 岗位缺省按账号解析(批1 IZtPerfConfigService.resolveRole)
role = perfConfigService.resolveRole(account);
if (StringUtils.isBlank(role)) {
throw new BusinessException("岗位不能为空且无法按账号解析");
}
}
ZtMonthScore score = findMonthScore(account, month);
if (score != null && StringUtils.isNotBlank(score.getScopeJson())) {
return JSON.parseObject(score.getScopeJson(), PerfMonthScope.class);
}
// 未落库:实时计算(不写库)
return computeScope(account, month, role);
}
@Override
@Transactional
public void generateMonthScore(String month, String role, String account) {
if (StringUtils.isBlank(month)) {
throw new BusinessException("月份不能为空");
}
Set<String> roles = new LinkedHashSet<>();
if (StringUtils.isNotBlank(role)) {
roles.add(role);
} else if (StringUtils.isNotBlank(account)) {
// 指定账号未指定岗位:按账号解析(批1 IZtPerfConfigService.resolveRole)
String resolved = perfConfigService.resolveRole(account);
if (StringUtils.isBlank(resolved)) {
throw new BusinessException("无法按账号解析岗位:" + account);
}
roles.add(resolved);
} else {
List<ZtPerfConfig> all = perfConfigService.list(new QueryWrapper<ZtPerfConfig>().eq("enabled", 1));
for (ZtPerfConfig c : all) {
if (StringUtils.isNotBlank(c.getRole())) {
roles.add(c.getRole());
}
}
}
if (roles.isEmpty()) {
throw new BusinessException("未找到启用中的绩效规则配置");
}
List<String> accounts = new ArrayList<>();
if (StringUtils.isNotBlank(account)) {
accounts.add(account);
} else {
accounts = userService.list().stream().map(ZtUser::getAccount)
.filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList());
}
Date now = new Date();
String operator = RiskUserThreadLocal.get().getName();
for (String r : roles) {
for (String acc : accounts) {
PerfMonthScope scope = computeScope(acc, month, r);
ZtMonthScore exist = findMonthScore(acc, month);
if (exist == null) {
ZtMonthScore entity = new ZtMonthScore();
entity.setAccount(acc);
entity.setDateStr(month);
entity.setScopeJson(JSON.toJSONString(scope));
entity.setCreateDate(now);
entity.setCreateUser(operator);
entity.setUpdateDate(now);
entity.setUpdateUser(operator);
monthScoreService.save(entity);
} else {
// 重算:保留已有人工评分与申诉记录
if (StringUtils.isNotBlank(exist.getScopeJson())) {
mergeManualAndAppeals(scope, JSON.parseObject(exist.getScopeJson(), PerfMonthScope.class));
}
exist.setScopeJson(JSON.toJSONString(scope));
exist.setUpdateDate(now);
exist.setUpdateUser(operator);
monthScoreService.updateById(exist);
}
}
}
}
@Override
@Transactional
public void manualScore(ZtPerfQo qo) {
checkMonthAccount(qo.getMonth(), qo.getAccount());
if (StringUtils.isBlank(qo.getItemKey()) || qo.getScore() == null) {
throw new BusinessException("指标与分值不能为空");
}
if (qo.getScore() < 0 || qo.getScore() > 100) {
throw new BusinessException("分值需在 0~100 之间");
}
ZtMonthScore entity = requireMonthScore(qo.getAccount(), qo.getMonth());
PerfMonthScope scope = JSON.parseObject(entity.getScopeJson(), PerfMonthScope.class);
PerfMonthScope.Item item = findItem(scope, qo.getItemKey());
if (item == null) {
throw new BusinessException("指标不存在:" + qo.getItemKey());
}
item.setScore(qo.getScore());
item.setSource(PerfScoreEngine.SOURCE_MANUAL);
item.setRemark(qo.getRemark());
perfScoreEngine.recomputeTotal(scope);
saveScopeJson(entity, scope);
}
@Override
@Transactional
public void appeal(ZtPerfQo qo) {
checkMonthAccount(qo.getMonth(), qo.getAccount());
if (StringUtils.isBlank(qo.getItemKey()) || StringUtils.isBlank(qo.getReason())) {
throw new BusinessException("申诉指标与原因不能为空");
}
ZtMonthScore entity = requireMonthScore(qo.getAccount(), qo.getMonth());
PerfMonthScope scope = JSON.parseObject(entity.getScopeJson(), PerfMonthScope.class);
if (findItem(scope, qo.getItemKey()) == null) {
throw new BusinessException("指标不存在:" + qo.getItemKey());
}
if (scope.getAppeals() == null) {
scope.setAppeals(new ArrayList<>());
}
PerfMonthScope.Appeal appeal = new PerfMonthScope.Appeal();
appeal.setId(qo.getAccount() + "@" + qo.getMonth() + "#" + (scope.getAppeals().size() + 1));
appeal.setItemKey(qo.getItemKey());
appeal.setReason(qo.getReason());
appeal.setStatus(APPEAL_PENDING);
appeal.setCreateDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
scope.getAppeals().add(appeal);
saveScopeJson(entity, scope);
}
@Override
@Transactional
public void appealReview(ZtPerfQo qo) {
if (StringUtils.isBlank(qo.getId()) || StringUtils.isBlank(qo.getResult())) {
throw new BusinessException("申诉 id 与复核结果不能为空");
}
if (!APPEAL_UPHELD.equals(qo.getResult()) && !APPEAL_OVERTURNED.equals(qo.getResult())) {
throw new BusinessException("复核结果仅支持 upheld/overturned");
}
// 申诉 id 规则:account@month#序号
String[] parts = qo.getId().split("[@#]");
if (parts.length != 3) {
throw new BusinessException("申诉 id 格式非法:" + qo.getId());
}
ZtMonthScore entity = requireMonthScore(parts[0], parts[1]);
PerfMonthScope scope = JSON.parseObject(entity.getScopeJson(), PerfMonthScope.class);
PerfMonthScope.Appeal target = null;
if (scope.getAppeals() != null) {
for (PerfMonthScope.Appeal a : scope.getAppeals()) {
if (qo.getId().equals(a.getId())) {
target = a;
break;
}
}
}
if (target == null) {
throw new BusinessException("申诉记录不存在:" + qo.getId());
}
if (!APPEAL_PENDING.equals(target.getStatus())) {
throw new BusinessException("申诉已复核,请勿重复操作");
}
target.setStatus(qo.getResult());
target.setReviewer(qo.getReviewer());
target.setReviewDate(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
if (APPEAL_OVERTURNED.equals(qo.getResult())) {
// 撤销:回滚该项扣分为满分并重算总分
PerfMonthScope.Item item = findItem(scope, target.getItemKey());
if (item != null) {
item.setScore(100D);
item.setDeduct(0D);
item.setRemark(StringUtils.isBlank(item.getRemark()) ? "申诉撤销回滚" : item.getRemark() + ";申诉撤销回滚");
}
perfScoreEngine.recomputeTotal(scope);
}
saveScopeJson(entity, scope);
}
/**
* 实时计算:取岗位配置 + 批1指标计算器(PerfIndicatorCalculator 注册表),不调引擎外服务。
* 计算器命中时其返回值已是 0~100 得分(见其接口约定),通过 CTX_PRESCORED_ITEMS 告知引擎直接采信。
*/
private PerfMonthScope computeScope(String account, String month, String role) {
List<ZtPerfConfig> configs = perfConfigService.list(
new QueryWrapper<ZtPerfConfig>().eq("role", role).eq("enabled", 1));
if (CollectionUtils.isEmpty(configs)) {
throw new BusinessException("岗位无启用中的绩效规则配置:" + role);
}
Map<String, Double> metrics = new HashMap<>();
Map<String, String> rawDetails = new HashMap<>();
Set<String> preScored = new HashSet<>();
Map<String, PerfIndicatorCalculator> registry = new HashMap<>();
if (calculators != null) {
for (PerfIndicatorCalculator p : calculators) {
registry.put(p.key(), p);
}
}
for (ZtPerfConfig config : configs) {
PerfIndicatorCalculator calculator = registry.get(config.getItemKey());
if (calculator == null) {
continue;
}
try {
BigDecimal value = calculator.compute(account, month);
metrics.put(config.getItemKey(), value == null ? null : value.doubleValue());
preScored.add(config.getItemKey());
String detail = calculator.consumeRawDetail();
if (detail != null) {
rawDetails.put(config.getItemKey(), detail);
}
} catch (Exception e) {
// 单指标取数失败不阻塞整表:raw 置 null,该项 score 为 null
metrics.put(config.getItemKey(), null);
calculator.consumeRawDetail();
}
}
Map<String, Object> context = new HashMap<>();
context.put(PerfScoreEngine.CTX_PRESCORED_ITEMS, preScored);
PerfMonthScope scope = perfScoreEngine.compute(account, month, role, configs, metrics, context);
//CHG-041 过程值明细挂到对应指标项(弹窗「绩效数据」列展示)
if (scope.getItems() != null) {
for (PerfMonthScope.Item item : scope.getItems()) {
String detail = rawDetails.get(item.getItemKey());
if (detail != null) {
item.setRawDetail(detail);
}
}
}
return scope;
}
/**
* 重算时把旧 scope 的人工评分(source=manual 且 score 非空)与申诉记录合并进新 scope
*/
private void mergeManualAndAppeals(PerfMonthScope newScope, PerfMonthScope oldScope) {
if (oldScope.getItems() != null) {
Map<String, PerfMonthScope.Item> oldItems = oldScope.getItems().stream()
.filter(i -> i.getItemKey() != null)
.collect(Collectors.toMap(PerfMonthScope.Item::getItemKey, i -> i, (a, b) -> a));
for (PerfMonthScope.Item item : newScope.getItems()) {
PerfMonthScope.Item old = oldItems.get(item.getItemKey());
if (old != null && PerfScoreEngine.SOURCE_MANUAL.equals(old.getSource()) && old.getScore() != null) {
item.setScore(old.getScore());
item.setSource(old.getSource());
item.setRemark(old.getRemark());
}
}
}
newScope.setAppeals(oldScope.getAppeals() == null ? new ArrayList<>() : oldScope.getAppeals());
perfScoreEngine.recomputeTotal(newScope);
}
private PerfMonthScope.Item findItem(PerfMonthScope scope, String itemKey) {
if (scope.getItems() == null) {
return null;
}
for (PerfMonthScope.Item item : scope.getItems()) {
if (itemKey.equals(item.getItemKey())) {
return item;
}
}
return null;
}
private ZtMonthScore findMonthScore(String account, String month) {
List<ZtMonthScore> list = monthScoreService.list(new QueryWrapper<ZtMonthScore>()
.eq("account", account).eq("date_str", month));
return CollectionUtils.isEmpty(list) ? null : list.get(0);
}
private ZtMonthScore requireMonthScore(String account, String month) {
ZtMonthScore entity = findMonthScore(account, month);
if (entity == null || StringUtils.isBlank(entity.getScopeJson())) {
throw new BusinessException("月度得分未生成,请先调用 generateMonthScore");
}
return entity;
}
private void saveScopeJson(ZtMonthScore entity, PerfMonthScope scope) {
entity.setScopeJson(JSON.toJSONString(scope));
entity.setUpdateDate(new Date());
//无登录态(单测/未来调度线程)兜底 system,防 NPE(CHG-069)
entity.setUpdateUser(RiskUserThreadLocal.get() == null ? "system" : RiskUserThreadLocal.get().getName());
monthScoreService.updateById(entity);
}
private void checkMonthAccount(String month, String account) {
if (StringUtils.isBlank(month) || StringUtils.isBlank(account)) {
throw new BusinessException("月份与账号不能为空");
}
}
}
@@ -1,6 +1,7 @@
package com.sa.zentao.service.impl; package com.sa.zentao.service.impl;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.pagehelper.Page; import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
@@ -144,12 +145,17 @@ public class ZtProductServiceImpl extends ServiceImpl<ZtProductMapper, ZtProduct
userList.addAll(result.stream().filter(o->!StringUtils.isEmpty(o.getPo())).map(o->o.getPo()).collect(Collectors.toList())); userList.addAll(result.stream().filter(o->!StringUtils.isEmpty(o.getPo())).map(o->o.getPo()).collect(Collectors.toList()));
Map<String, ZtUser> userMap = this.userService.userMapByIds(null); Map<String, ZtUser> userMap = this.userService.userMapByIds(null);
List<ZtStory> allStoryList = this.storyService.list(new QueryWrapper<ZtStory>().lambda().in(ZtStory::getProduct, result.stream().map(o -> o.getId()).collect(Collectors.toList()))); //CHG-070:产品列表统计只要计数,禁止 SELECT *——zt_bug.steps(MEDIUMTEXT,页均 44MB)等大字段拖垮接口(实测 7.3s→0.05s)
List<ZtStory> allStoryList = this.storyService.list(new QueryWrapper<ZtStory>().lambda()
.select(ZtStory::getId, ZtStory::getProduct, ZtStory::getStatus, ZtStory::getStage)
.in(ZtStory::getProduct, result.stream().map(o -> o.getId()).collect(Collectors.toList())));
List<ZtBug> allBugList = bugService.list(new QueryWrapper<ZtBug>().lambda() List<ZtBug> allBugList = bugService.list(new QueryWrapper<ZtBug>().lambda()
.select(ZtBug::getId, ZtBug::getProduct, ZtBug::getStatus)
.ne(ZtBug::getStatus,"closed") .ne(ZtBug::getStatus,"closed")
.in(ZtBug::getProduct, result.stream().map(o -> o.getId()).collect(Collectors.toList()))); .in(ZtBug::getProduct, result.stream().map(o -> o.getId()).collect(Collectors.toList())));
List<ZtStoryUser> allUserStoryList = this.storyUserService.list(new QueryWrapper<ZtStoryUser>().lambda()
List<ZtStoryUser> allUserStoryList = this.storyUserService.list(new QueryWrapper<ZtStoryUser>().lambda().in(ZtStoryUser::getProduct, result.stream().map(o -> o.getId()).collect(Collectors.toList()))); .select(ZtStoryUser::getId, ZtStoryUser::getProduct, ZtStoryUser::getStatus)
.in(ZtStoryUser::getProduct, result.stream().map(o -> o.getId()).collect(Collectors.toList())));
for (ZtProductDTO p:result ) { for (ZtProductDTO p:result ) {
List<ZtStory> storyList = allStoryList.stream().filter(o->o.getProduct().equals(p.getId())).collect(Collectors.toList()); List<ZtStory> storyList = allStoryList.stream().filter(o->o.getProduct().equals(p.getId())).collect(Collectors.toList());
@@ -687,4 +693,63 @@ public class ZtProductServiceImpl extends ServiceImpl<ZtProductMapper, ZtProduct
return ztProjectDTOS.stream().collect(Collectors.groupingBy(ZtProductDTO::getProgram)); return ztProjectDTOS.stream().collect(Collectors.groupingBy(ZtProductDTO::getProgram));
} }
@Override
public PageInfo<ZtProjectDTO> getExecutionByProductId(ZtProjectQo qo) {
Integer productId = qo.getProductId();
if (productId == null || productId == 0) {
throw new BusinessException("产品ID不能为空");
}
// 查询产品关联的项目
List<ZtProjectproduct> projectproductlist = this.projectproductService.list(
new QueryWrapper<ZtProjectproduct>().lambda()
.eq(ZtProjectproduct::getProduct, productId)
);
if (CollectionUtils.isEmpty(projectproductlist)) {
return new PageInfo<>();
}
// 获取项目ID列表
List<Integer> projectIds = projectproductlist.stream()
.map(ZtProjectproduct::getProject)
.collect(Collectors.toList());
// 查询项目关联的迭代
List<ZtExecutionproject> executionprojectList = this.executionprojectService.list(
new QueryWrapper<ZtExecutionproject>().lambda()
.in(ZtExecutionproject::getProject, projectIds)
);
if (CollectionUtils.isEmpty(executionprojectList)) {
return new PageInfo<>();
}
// 获取迭代ID列表
List<Integer> executionIds = executionprojectList.stream()
.map(ZtExecutionproject::getExecution)
.collect(Collectors.toList());
// 分页查询迭代,按ID倒序排列
com.github.pagehelper.Page<ZtProjectDTO> page = PageHelper.startPage(qo.getCurrentPage(), qo.getPageSize());
LambdaQueryWrapper<ZtProject> queryWrapper = new QueryWrapper<ZtProject>().lambda()
.in(ZtProject::getId, executionIds);
// 添加名称搜索
if (!StringUtils.isEmpty(qo.getName())) {
queryWrapper.like(ZtProject::getName, qo.getName());
}
queryWrapper.orderByDesc(ZtProject::getId);
List<ZtProject> executionList = this.projectService.list(queryWrapper);
// 转换为DTO
List<ZtProjectDTO> dtoList = BeanCopyUtil.copyListProperties(executionList, ZtProjectDTO::new);
PageInfo<ZtProjectDTO> ztProjectDTOPageInfo = new PageInfo<>(dtoList);
ztProjectDTOPageInfo.setTotal(page.getTotal());
return ztProjectDTOPageInfo;
}
} }
@@ -260,7 +260,6 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
cell.setColumn(ztKanbancolumn.getId()); cell.setColumn(ztKanbancolumn.getId());
cell.setType(ztKanbanlane.getType()); cell.setType(ztKanbanlane.getType());
kanbancellService.save(cell); kanbancellService.save(cell);
} }
} }
} }
@@ -314,8 +313,11 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
public void projectClose(ZtProjectDTO dto) { public void projectClose(ZtProjectDTO dto) {
ZtProject ztProject = this.baseMapper.selectById(dto.getId()); ZtProject ztProject = this.baseMapper.selectById(dto.getId());
if (ztProject == null || "closed".equalsIgnoreCase(ztProject.getStatus())) { if (ztProject == null) {
throw new BusinessException("未查询到"); throw new BusinessException("未查询到项目,项目ID: " + dto.getId());
}
if ("closed".equalsIgnoreCase(ztProject.getStatus())) {
throw new BusinessException("项目已关闭,无法重复关闭,项目ID: " + dto.getId());
} }
ztProject.setRealEnd(dto.getClosedDate()); ztProject.setRealEnd(dto.getClosedDate());
ztProject.setLastEditedDate(new Date()); ztProject.setLastEditedDate(new Date());
@@ -351,6 +353,7 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
@Override @Override
@Transactional
public void projectStart(ZtProjectDTO dto) { public void projectStart(ZtProjectDTO dto) {
ZtProject ztProject = this.baseMapper.selectById(dto.getId()); ZtProject ztProject = this.baseMapper.selectById(dto.getId());
if (ztProject == null || !"wait".equalsIgnoreCase(ztProject.getStatus())) { if (ztProject == null || !"wait".equalsIgnoreCase(ztProject.getStatus())) {
@@ -386,8 +389,11 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
@Override @Override
public void projectDeleted(ZtProjectDTO dto) { public void projectDeleted(ZtProjectDTO dto) {
ZtProject ztProject = this.baseMapper.selectById(dto.getId()); ZtProject ztProject = this.baseMapper.selectById(dto.getId());
if (ztProject == null || "1".equalsIgnoreCase(ztProject.getDeleted())) { if (ztProject == null) {
throw new BusinessException("未查询到"); throw new BusinessException("未查询到项目,项目ID: " + dto.getId());
}
if ("1".equalsIgnoreCase(ztProject.getDeleted())) {
throw new BusinessException("项目已删除,无法重复删除,项目ID: " + dto.getId());
} }
ztProject.setDeleted("1"); ztProject.setDeleted("1");
ztProject.setCanceledBy(RiskUserThreadLocal.get().getName()); ztProject.setCanceledBy(RiskUserThreadLocal.get().getName());
@@ -422,8 +428,11 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
@Override @Override
public void projectActivated(ZtProjectDTO dto) { public void projectActivated(ZtProjectDTO dto) {
ZtProject ztProject = this.baseMapper.selectById(dto.getId()); ZtProject ztProject = this.baseMapper.selectById(dto.getId());
if (ztProject == null || !"closed".equalsIgnoreCase(ztProject.getStatus())) { if (ztProject == null) {
throw new BusinessException("未查询到"); throw new BusinessException("未查询到项目,项目ID: " + dto.getId());
}
if (!"closed".equalsIgnoreCase(ztProject.getStatus())) {
throw new BusinessException("项目未关闭,无法激活,当前状态: " + ztProject.getStatus() + ",项目ID: " + dto.getId());
} }
LambdaUpdateWrapper<ZtProject> updateWrapper = new UpdateWrapper<ZtProject>().lambda().set(ZtProject::getStatus, "doing") LambdaUpdateWrapper<ZtProject> updateWrapper = new UpdateWrapper<ZtProject>().lambda().set(ZtProject::getStatus, "doing")
.set(ZtProject::getRealEnd,null) .set(ZtProject::getRealEnd,null)
@@ -458,8 +467,11 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
@Override @Override
public void projectSuspended(ZtProjectDTO dto) { public void projectSuspended(ZtProjectDTO dto) {
ZtProject ztProject = this.baseMapper.selectById(dto.getId()); ZtProject ztProject = this.baseMapper.selectById(dto.getId());
if (ztProject == null || !"wait".equalsIgnoreCase(ztProject.getStatus())) { if (ztProject == null) {
throw new BusinessException("未查询到"); throw new BusinessException("未查询到项目,项目ID: " + dto.getId());
}
if (!"wait".equalsIgnoreCase(ztProject.getStatus())) {
throw new BusinessException("项目状态不是等待状态,无法挂起,当前状态: " + ztProject.getStatus() + ",项目ID: " + dto.getId());
} }
ztProject.setStatus("suspended"); ztProject.setStatus("suspended");
ztProject.setLastEditedBy(RiskUserThreadLocal.get().getName()); ztProject.setLastEditedBy(RiskUserThreadLocal.get().getName());
@@ -1106,7 +1118,7 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
if(project!=null&&project!=0){ if(project!=null&&project!=0){
List<ZtExecutionproject> list = this.executionprojectService.list(new QueryWrapper<ZtExecutionproject>().lambda().eq(ZtExecutionproject::getProject, project)); List<ZtExecutionproject> list = this.executionprojectService.list(new QueryWrapper<ZtExecutionproject>().lambda().eq(ZtExecutionproject::getProject, project));
if(!CollectionUtils.isEmpty(list)){ if(!CollectionUtils.isEmpty(list)){
return this.listByIds(list.stream().map(o->o.getExecution()).collect(Collectors.toList())); return this.list(new QueryWrapper<ZtProject>().lambda().in(ZtProject::getId,list.stream().map(o->o.getExecution()).collect(Collectors.toList())).orderByDesc(ZtProject::getId));
} }
} }
@@ -1118,7 +1130,7 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
in(ZtExecutionproject::getProject, list.stream().map(o->o.getProject()).collect(Collectors.toList()))); in(ZtExecutionproject::getProject, list.stream().map(o->o.getProject()).collect(Collectors.toList())));
if(!CollectionUtils.isEmpty(eList)){ if(!CollectionUtils.isEmpty(eList)){
return this.list(new QueryWrapper<ZtProject>().lambda().in(ZtProject::getId,eList.stream().map(o->o.getExecution()).collect(Collectors.toList())) return this.list(new QueryWrapper<ZtProject>().lambda().in(ZtProject::getId,eList.stream().map(o->o.getExecution()).collect(Collectors.toList()))
.ne(ZtProject::getStatus,"closed").ne(ZtProject::getDeleted,"1") .ne(ZtProject::getStatus,"closed").ne(ZtProject::getDeleted,"1").orderByDesc(ZtProject::getId)
); );
} }
} }
@@ -1325,8 +1337,10 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
return new HashMap<>(); return new HashMap<>();
} }
//仅取预计开始在本月的任务:下方按天循环只匹配 estStarted 落在当月的任务,历史任务对结果无贡献(性能优化,口径不变)
List<ZtTask> list = this.taskService.list(new QueryWrapper<ZtTask>().lambda().select(SFunctionColums.taskColumes()) List<ZtTask> list = this.taskService.list(new QueryWrapper<ZtTask>().lambda().select(SFunctionColums.taskColumes())
.notIn(ZtTask::getStatus,"cancel","closed","reviewing","draft") .notIn(ZtTask::getStatus,"cancel","closed","reviewing","draft")
.ge(ZtTask::getEstStarted, firstDayOfMonth).le(ZtTask::getEstStarted, lastDayOfMonth)
.in(ZtTask::getExecution, execList.stream().map(o -> o.getExecution()).collect(Collectors.toList()))); .in(ZtTask::getExecution, execList.stream().map(o -> o.getExecution()).collect(Collectors.toList())));
if (CollectionUtils.isEmpty(list)) { if (CollectionUtils.isEmpty(list)) {
return new HashMap<>(); return new HashMap<>();
@@ -1357,8 +1371,16 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
List<String> accountIds = teams.stream().map(o -> o.getAccount()).distinct().collect(Collectors.toList()); List<String> accountIds = teams.stream().map(o -> o.getAccount()).distinct().collect(Collectors.toList());
List<ZtUser> userList = this.userService.list(); //按需取用户:团队成员 ∪ 任务相关人(原 userService.list() 全表扫描;不在用户表的账号行为不变,仍为 null 跳过)
Map<String, ZtUser> uMap = userList.stream().collect(Collectors.toMap(ZtUser::getAccount, O -> O)); Set<String> userAccounts = new HashSet<>(accountIds);
for (ZtTask t : list) {
if (!StringUtils.isEmpty(t.getAssignedTo())) userAccounts.add(t.getAssignedTo());
if (!StringUtils.isEmpty(t.getOpenedby())) userAccounts.add(t.getOpenedby());
if (!StringUtils.isEmpty(t.getFinishedby())) userAccounts.add(t.getFinishedby());
if (!StringUtils.isEmpty(t.getCanceledby())) userAccounts.add(t.getCanceledby());
if (!StringUtils.isEmpty(t.getClosedby())) userAccounts.add(t.getClosedby());
}
Map<String, ZtUser> uMap = this.userService.userMapByIds(new ArrayList<>(userAccounts));
for (ZtTask t : list) { for (ZtTask t : list) {
if (t.getDeadline() != null) { if (t.getDeadline() != null) {
@@ -1386,17 +1408,16 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
} }
} }
//预分组:yyyy-MM-dd#账号 → 任务列表(原 天数×人数×任务数 三层扫描降为 O(任务数);组内顺序与原 stream filter 一致)
Map<String, List<ZtTask>> taskByDayAccount = list.stream()
.filter(o -> o.getEstStarted() != null && !StringUtils.isEmpty(o.getAssignedTo()))
.collect(Collectors.groupingBy(o -> DateUtils.formatDate(o.getEstStarted()) + "#" + o.getAssignedTo()));
for (int i = 0; i < DateUtils.getDays(date); i++) { for (int i = 0; i < DateUtils.getDays(date); i++) {
Date d = DateUtils.dateAddDay(firstDayOfMonth, i); Date d = DateUtils.dateAddDay(firstDayOfMonth, i);
List<Map<String, Object>> l = new ArrayList<>(); List<Map<String, Object>> l = new ArrayList<>();
for (String s : accountIds) { for (String s : accountIds) {
ZtUser ztUser = uMap.get(s); ZtUser ztUser = uMap.get(s);
// List<ZtEffort> fList = efforts.stream().filter(o -> o.getAccount().equals(s)) List<ZtTask> taskList = taskByDayAccount.getOrDefault(DateUtils.formatDate(d) + "#" + s, Collections.emptyList());
// .filter(o->DateUtils.formatDate(o.getDate()).equals(DateUtils.formatDate(d)))
// .collect(Collectors.toList());
List<ZtTask> taskList = list.stream().filter(o -> o.getEstStarted() != null && DateUtils.formatDate(o.getEstStarted()).equals(DateUtils.formatDate(d)))
.filter(o -> o.getAssignedTo().equals(ztUser.getAccount()))
.collect(Collectors.toList());
for (ZtTask t:taskList ) { for (ZtTask t:taskList ) {
t.setAssignedToName(ztUser.getNickname()); t.setAssignedToName(ztUser.getNickname());
@@ -1941,9 +1962,9 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
List<ZtTask> allTaskList =this.taskService.taskListByEIdsAndDate(firstDayOfMonth,lastDayOfMonth,products.stream().map(o->o.getId()).collect(Collectors.toList())); List<ZtTask> allTaskList =this.taskService.taskListByEIdsAndDate(firstDayOfMonth,lastDayOfMonth,products.stream().map(o->o.getId()).collect(Collectors.toList()));
if (CollectionUtils.isEmpty(allTaskList)) { // if (CollectionUtils.isEmpty(allTaskList)) {
return new PageInfo<>(); // return new PageInfo<>();
} // }
List<ZtTeam> teams = this.teamService.list(new QueryWrapper<ZtTeam>().lambda().eq(ZtTeam::getType, "execution") List<ZtTeam> teams = this.teamService.list(new QueryWrapper<ZtTeam>().lambda().eq(ZtTeam::getType, "execution")
@@ -1965,6 +1986,9 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
Map<String,ZtMonthScore> scoreMap=getAccountScoreMap(date,accountIds); Map<String,ZtMonthScore> scoreMap=getAccountScoreMap(date,accountIds);
//CHG-074:月度达标工时(团队口径人均,与绩效弹窗 myWorkScore 一致)——在窗执行 KFZ 成员 (工作天数×人数−团队请假÷8)×5÷人数
BigDecimal teamPerCapita = countService.teamExamineTime(accountIds, firstDayOfMonth, lastDayOfMonth, date);
for (String account :accountIds) { for (String account :accountIds) {
ZtUser ztUser = uMap.get(account); ZtUser ztUser = uMap.get(account);
@@ -1992,11 +2016,11 @@ public class ZtProjectServiceImpl extends ServiceImpl<ZtProjectMapper, ZtProject
dto=countService.setUserWorkTime(dto,date,approvalList,firstDayOfMonth); dto=countService.setUserWorkTime(dto,date,approvalList,firstDayOfMonth);
d.setQjTime(dto.getApprovalDays()); d.setQjTime(dto.getApprovalDays());
//可用工时 //可用工时(月度达标工时=团队口径人均,CHG-074;无 KFZ 成员时退回个人 0.75 口径兜底)
d.setHaveTime(dto.getExamineTime()); d.setHaveTime(teamPerCapita.signum() > 0 ? teamPerCapita : dto.getExamineTime());
//工作饱和度 //工作饱和度(CHG-074:分子=实绩工时 consumed、分母=团队口径达标工时,与绩效弹窗一致)
d.setSaturation(BigDecimalUtils.isZero(d.getHaveTime())?BigDecimal.valueOf(1):BigDecimalUtils.isZero(d.getWorkTime())?BigDecimal.ZERO:d.getStoryTotalTime().divide(d.getHaveTime(),2,BigDecimal.ROUND_UP)); d.setSaturation(BigDecimalUtils.isZero(d.getHaveTime())?BigDecimal.valueOf(1):BigDecimalUtils.isZero(d.getWorkTime())?BigDecimal.ZERO:d.getWorkTime().divide(d.getHaveTime(),2,BigDecimal.ROUND_UP));
// 任务总量 .filter(o->o.getStatus().equals("done")) // 任务总量 .filter(o->o.getStatus().equals("done"))
d.setTaskCount(BigDecimal.valueOf(taskList.stream().collect(Collectors.toList()).size())); d.setTaskCount(BigDecimal.valueOf(taskList.stream().collect(Collectors.toList()).size()));
@@ -112,7 +112,7 @@ public class ZtReleaseServiceImpl extends ServiceImpl<ZtReleaseMapper, ZtRelease
List<ZtStory> sList = this.storyService.list(new QueryWrapper<ZtStory>() List<ZtStory> sList = this.storyService.list(new QueryWrapper<ZtStory>()
.lambda().eq(ZtStory::getStatus, "active") .lambda().eq(ZtStory::getStatus, "active")
.in(ZtStory::getId, list.stream().map(o -> o.getStory()).distinct().collect(Collectors.toList())) .in(ZtStory::getId, list.stream().map(o -> o.getStory()).distinct().collect(Collectors.toList()))
.eq(ZtStory::getStage, StoryStageEnums.tested.getValue())); .eq(ZtStory::getStage, StoryStageEnums.productVerified.getValue()));
if (!CollectionUtils.isEmpty(sList)) { if (!CollectionUtils.isEmpty(sList)) {
//没有被关联的需求 测试完成的 //没有被关联的需求 测试完成的
List<ZtReleaseDetails> releaseStoryList = this.releaseDetailsService.list(new QueryWrapper<ZtReleaseDetails>().lambda() List<ZtReleaseDetails> releaseStoryList = this.releaseDetailsService.list(new QueryWrapper<ZtReleaseDetails>().lambda()
@@ -368,9 +368,9 @@ public class ZtReleaseServiceImpl extends ServiceImpl<ZtReleaseMapper, ZtRelease
List<ZtStory> ztStories = CollectionUtils.isEmpty(storyList)?new ArrayList<>():storyService.listByIds(storyList); List<ZtStory> ztStories = CollectionUtils.isEmpty(storyList)?new ArrayList<>():storyService.listByIds(storyList);
List<ZtBug> bugs =CollectionUtils.isEmpty(bugList)?new ArrayList<>(): bugService.listByIds(bugList); List<ZtBug> bugs =CollectionUtils.isEmpty(bugList)?new ArrayList<>(): bugService.listByIds(bugList);
long notTested = ztStories.stream().filter(o -> !o.getStage().equals("tested")).count(); List<ZtStory> notProductYsList = ztStories.stream().filter(o -> !o.getStage().equals(StoryStageEnums.productVerified.getValue())).toList();
if(notTested>0){ if(!CollectionUtils.isEmpty(notProductYsList)){
throw new BusinessException("当前需求存在未测试完成的请检查"); throw new BusinessException("当前需求存在未产品验收的需求请检查 id="+notProductYsList.get(0).getId()+" name="+notProductYsList.get(0).getTitle());
} }
//'active','resolved','closed'? //'active','resolved','closed'?
long count = bugs.stream().filter(o -> !Arrays.asList("resolved","closed").contains(o.getStatus())).count(); long count = bugs.stream().filter(o -> !Arrays.asList("resolved","closed").contains(o.getStatus())).count();
@@ -378,7 +378,7 @@ public class ZtReleaseServiceImpl extends ServiceImpl<ZtReleaseMapper, ZtRelease
throw new BusinessException("当前bug存在未完成请检查"); throw new BusinessException("当前bug存在未完成请检查");
} }
for (ZtStory st : ztStories) { for (ZtStory st : ztStories) {
if (!"tested".equals(st.getStage())) { if (! StoryStageEnums.productVerified.getValue().equals(st.getStage())) {
throw new BusinessException(st.getTitle() + " 当前需求无法发布,请检查"); throw new BusinessException(st.getTitle() + " 当前需求无法发布,请检查");
} }
this.storyService.releaseStory(st.getId()); this.storyService.releaseStory(st.getId());
@@ -391,8 +391,6 @@ public class ZtReleaseServiceImpl extends ServiceImpl<ZtReleaseMapper, ZtRelease
} }
if(!CollectionUtils.isEmpty(ztStories)){ if(!CollectionUtils.isEmpty(ztStories)){
sendReleaseMail(ztStories); sendReleaseMail(ztStories);
} }
} else { } else {
actionService.addAction(ActionType.FB, ActionStatus.FBSB, ztRelease.getId(), ztRelease.getProduct() + "", ztRelease.getProject(), null, actionService.addAction(ActionType.FB, ActionStatus.FBSB, ztRelease.getId(), ztRelease.getProduct() + "", ztRelease.getProject(), null,
@@ -0,0 +1,156 @@
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.conf.LoginRiskUser;
import com.sa.zentao.conf.RiskUserThreadLocal;
import com.sa.zentao.dao.BusinessException;
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;
/**
* <p>
* 服务实现类
* </p>
*
* @author gqb
* @since 2026-03-30
*/
@Service
public class ZtStoryExpandServiceImpl extends ServiceImpl<ZtStoryExpandMapper, ZtStoryExpand> implements IZtStoryExpandService {
@Autowired
private IZtStoryMonthWorkloadService ztStoryMonthWorkloadService;
@Autowired
private IZtStoryExpandService ztStoryExpandService;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdateExpand(ZtStoryExpand entity) {
//仅 AI 框架通道可用:必须携带 ai 账户 token(CHG-061,二期必决项落地)
LoginRiskUser aiLoginUser = RiskUserThreadLocal.get();
if (aiLoginUser == null || !"ai".equals(aiLoginUser.getName())) {
throw new BusinessException("该接口仅AI框架通道可用(需ai账户token)");
}
if (entity == null || entity.getStoryId() == null) {
return;
}
QueryWrapper<ZtStoryExpand> 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<ZtStoryExpand> 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<ZtStoryExpand> 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);
}
}
@@ -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;
/**
* <p>
* 服务实现类
* </p>
*
* @author gqb
* @since 2026-03-30
*/
@Service
public class ZtStoryMonthWorkloadServiceImpl extends ServiceImpl<ZtStoryMonthWorkloadMapper, ZtStoryMonthWorkload> 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<ZtStoryMonthWorkload> 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<String> listMonths() {
QueryWrapper<ZtStoryMonthWorkload> 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<ZtStoryMonthWorkload> wrapper = new QueryWrapper<>();
wrapper.eq("story_id", storyId).eq("time", month);
return this.getOne(wrapper);
}
@Override
public BigDecimal getMaxWorkloadBefore(Integer storyId, String month) {
QueryWrapper<ZtStoryMonthWorkload> 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);
}
}
@@ -1,5 +1,6 @@
package com.sa.zentao.service.impl; package com.sa.zentao.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -156,6 +157,14 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
if (ztUser != null) { if (ztUser != null) {
d.setUserStoryCreateUser(ztUser.getNickname()); d.setUserStoryCreateUser(ztUser.getNickname());
} }
ztUser = userMap.get(d.getProductUser());
if (ztUser != null) {
d.setProductUserName(ztUser.getNickname());
}
ztUser = userMap.get(d.getTestUser());
if (ztUser != null) {
d.setTestUserName(ztUser.getNickname());
}
List<ZtProject> ztProjectList = executionMapByStory.get(d.getId()); List<ZtProject> ztProjectList = executionMapByStory.get(d.getId());
if (!CollectionUtils.isEmpty(ztProjectList)) { if (!CollectionUtils.isEmpty(ztProjectList)) {
// d.setExecution(ztProject.getId()); // d.setExecution(ztProject.getId());
@@ -493,6 +502,8 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
Integer id = dto.getId(); Integer id = dto.getId();
ZtStory ztStory = this.baseMapper.selectById(id); ZtStory ztStory = this.baseMapper.selectById(id);
String oldAssignedTo = ztStory.getAssignedTo(); String oldAssignedTo = ztStory.getAssignedTo();
String title = dto.getTitle();
String oldTitle = ztStory.getTitle();
if ("closed".equals(ztStory.getStatus())) { if ("closed".equals(ztStory.getStatus())) {
throw new BusinessException("当前已关闭"); throw new BusinessException("当前已关闭");
} }
@@ -645,7 +656,10 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
actionService.addAction(ActionType.XQ, ActionStatus.BJ, dto.getId(), dto.getProduct() + "", dto.getProject(), null, actionService.addAction(ActionType.XQ, ActionStatus.BJ, dto.getId(), dto.getProduct() + "", dto.getProject(), null,
RiskUserThreadLocal.get().getName(), dto.getRemark(), ""); RiskUserThreadLocal.get().getName(), dto.getRemark(), "");
} }
if(!title.equals(oldTitle)){
actionService.addAction(ActionType.XQ, ActionStatus.BJ, dto.getId(), dto.getProduct() + "", dto.getProject(), null,
RiskUserThreadLocal.get().getName(), oldTitle +" 修改为 -> "+title, "");
}
storySendZpMessage(ztStory.getId(), oldAssignedTo, ztStory.getAssignedTo()); storySendZpMessage(ztStory.getId(), oldAssignedTo, ztStory.getAssignedTo());
} }
@@ -808,6 +822,8 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
for (ZtStoryDTO d : list) { for (ZtStoryDTO d : list) {
d.setRevieweUser(d.getReviewedby().replaceAll(",", "")); d.setRevieweUser(d.getReviewedby().replaceAll(",", ""));
d.setViews(rMap.get(d.getId())); d.setViews(rMap.get(d.getId()));
StoryStageEnums stageEnums = StoryStageEnums.transfer(d.getStage());
d.setStageName(stageEnums==null?null:stageEnums.getDesc());
List<ZtTask> ztTasks = taskCountMap.get(d.getId()); List<ZtTask> ztTasks = taskCountMap.get(d.getId());
ZtStoryCaseDTO ztStoryCaseDTO = caseMap.get(d.getId()); ZtStoryCaseDTO ztStoryCaseDTO = caseMap.get(d.getId());
if (ztStoryCaseDTO != null) { if (ztStoryCaseDTO != null) {
@@ -849,6 +865,14 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
if (ztUser != null) { if (ztUser != null) {
d.setYsUserName(ztUser.getNickname()); d.setYsUserName(ztUser.getNickname());
} }
ztUser = userMap.get(d.getProductUser());
if (ztUser != null) {
d.setProductUserName(ztUser.getNickname());
}
ztUser = userMap.get(d.getTestUser());
if (ztUser != null) {
d.setTestUserName(ztUser.getNickname());
}
} }
} }
@@ -956,6 +980,105 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
return searchDTOPageInfo; return searchDTOPageInfo;
} }
@Override
@Transactional
public void testSubmitVerified(ZtStoryDTO qo) {
ZtStory ztStory = this.baseMapper.selectById(qo.getId());
if(ztStory==null){
throw new BusinessException("未查询到需求,需求ID: " + qo.getId());
}
if(!StoryStatusEnums.active.getValue().equals(ztStory.getStatus())){
throw new BusinessException("需求状态不是激活状态,无法提交验收,当前状态: " + ztStory.getStatus() + ",需求ID: " + qo.getId());
}
if(!StoryStageEnums.tested.getValue().equals(ztStory.getStage())){
throw new BusinessException("需求阶段不是测试完毕,无法提交验收,当前阶段: " + ztStory.getStage() + ",需求ID: " + qo.getId());
}
if(!ObjectUtil.equal(ztStory.getInnerYsFlag(),1)){
throw new BusinessException("无需内部验收,不需要提交,需求ID: " + qo.getId());
}
//产品验收中
ztStory.setStage(StoryStageEnums.productWaitVerified.getValue());
ztStory.setTestSubmitYsTime(new Date());
ztStory.setLasteditedby(RiskUserThreadLocal.get().getName());
ztStory.setLastediteddate(new Date());
this.baseMapper.updateById(ztStory);
List<ZtProjectstory> ztProjectstories = this.projectstoryService.projectListByStory(Arrays.asList(ztStory.getId()), ProjectTypeEnums.execution);
//更改看板
if(!CollectionUtils.isEmpty(ztProjectstories)){
for(ZtProjectstory ztProjectstory:ztProjectstories){
kanbanlaneService.changeStatus(ztProjectstory.getProject(),ztStory.getId(),"story",StoryStageEnums.productWaitVerified.getValue());
}
}
actionService.addAction(ActionType.XQ, ActionStatus.TESTSUBMITYS, ztStory.getId(), ztStory.getProduct() + "", null, null,
RiskUserThreadLocal.get().getName(), "", "");
}
@Override
public void storyProductUserYs(ZtStoryDTO qo) {
ZtStory ztStory = this.baseMapper.selectById(qo.getId());
if(ztStory==null){
throw new BusinessException("未查询到需求,需求ID: " + qo.getId());
}
if(!StoryStatusEnums.active.getValue().equals(ztStory.getStatus())){
throw new BusinessException("需求状态不是激活状态,无法验收,当前状态: " + ztStory.getStatus() + ",需求ID: " + qo.getId());
}
if(!StoryStageEnums.productWaitVerified.getValue().equals(ztStory.getStage())){
throw new BusinessException("需求阶段不是待验收状态,无法验收,当前阶段: " + ztStory.getStage() + ",需求ID: " + qo.getId());
}
//产品验收中
ztStory.setStage(StoryStageEnums.productVerified.getValue());
ztStory.setLasteditedby(RiskUserThreadLocal.get().getName());
ztStory.setLastediteddate(new Date());
ztStory.setInnerYsTime(new Date());
this.baseMapper.updateById(ztStory);
List<ZtProjectstory> ztProjectstories = this.projectstoryService.projectListByStory(Arrays.asList(ztStory.getId()), ProjectTypeEnums.execution);
//更改看板
if(!CollectionUtils.isEmpty(ztProjectstories)){
for(ZtProjectstory ztProjectstory:ztProjectstories){
kanbanlaneService.changeStatus(ztProjectstory.getProject(),ztStory.getId(),"story",StoryStageEnums.productVerified.getValue());
}
}
actionService.addAction(ActionType.XQ, ActionStatus.PRODUCTYS, ztStory.getId(), ztStory.getProduct() + "", null, null,
RiskUserThreadLocal.get().getName(), "", "");
}
@Override
public Map<String, Object> storyReleaseOnTimeByProducts(List<Integer> pids, Date firstDayOfMonth, Date lastDayOfMonth) {
return this.baseMapper.storyReleaseOnTimeByProducts(pids,firstDayOfMonth,lastDayOfMonth);
}
@Override
public void assignedToProductUser(ZtStoryDTO dto) {
ZtStory ztStory = this.baseMapper.selectById(dto.getId());
if(ztStory==null){
throw new BusinessException("未查询到数据");
}
ztStory.setProductUser(dto.getAssignedTo());
ztStory.setLastediteddate(new Date());
ztStory.setLasteditedby(RiskUserThreadLocal.get().getName());
this.baseMapper.updateById(ztStory);
actionService.addAction(ActionType.XQ, ActionStatus.ASSIGNTOPRODUCTUSER, ztStory.getId(), ztStory.getProduct() + "", null, null,
RiskUserThreadLocal.get().getName(), "", dto.getAssignedTo());
}
@Override
public void assignedToTestUser(ZtStoryDTO dto) {
ZtStory ztStory = this.baseMapper.selectById(dto.getId());
if(ztStory==null){
throw new BusinessException("未查询到数据");
}
ztStory.setTestUser(dto.getAssignedTo());
ztStory.setLastediteddate(new Date());
ztStory.setLasteditedby(RiskUserThreadLocal.get().getName());
this.baseMapper.updateById(ztStory);
actionService.addAction(ActionType.XQ, ActionStatus.ASSIGNTOTESTUSER, ztStory.getId(), ztStory.getProduct() + "", null, null,
RiskUserThreadLocal.get().getName(), "", dto.getAssignedTo());
}
private List<SearchDTO> searchBug(SearchQo qo) { private List<SearchDTO> searchBug(SearchQo qo) {
return this.baseMapper.searchBug(qo.getKeyword()); return this.baseMapper.searchBug(qo.getKeyword());
} }
@@ -1153,9 +1276,14 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
} }
} }
/**
* 开发完成
* @param id
* @param finishBy
*/
@Override @Override
@Transactional @Transactional
public void finishStory(Integer id, String finishBy) { public void develFinishStory(Integer id, String finishBy) {
if (id == null || id == 0) { if (id == null || id == 0) {
return; return;
@@ -1207,7 +1335,7 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
throw new BusinessException("未查询到数据-需求"); throw new BusinessException("未查询到数据-需求");
} }
if (!Arrays.asList("wait", "projected", "developing", "developed", "testing").contains(ztStory.getStage())) { if (!Arrays.asList("wait", "projected", "developing", "developed", "testing",StoryStageEnums.productWaitVerified.getValue(),StoryStageEnums.productVerified.getValue()).contains(ztStory.getStage())) {
return; return;
} }
@@ -1242,6 +1370,13 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
actionService.addAction(ActionType.XQ, ActionStatus.CSWC, ztStory.getId(), ztStory.getProduct() + "", ztStory.getProject(), null, actionService.addAction(ActionType.XQ, ActionStatus.CSWC, ztStory.getId(), ztStory.getProduct() + "", ztStory.getProject(), null,
RiskUserThreadLocal.get().getName(), null, ""); RiskUserThreadLocal.get().getName(), null, "");
} }
//不需要内部验收 产品已验收
if(!ObjectUtil.equal(ztStory.getInnerYsFlag(),1)){
newStatus = StoryStageEnums.productVerified.getValue();
ztStory.setStage(newStatus);
ztStory.setInnerYsTime(new Date());
}
if (ztStory.getStartDate() == null) { if (ztStory.getStartDate() == null) {
ztStory.setStartDate(new Date()); ztStory.setStartDate(new Date());
} }
@@ -1253,7 +1388,7 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
for (Integer execId : execIds) { for (Integer execId : execIds) {
if (execId != null) { if (execId != null) {
this.kanbanlaneService.changeStatus(execId, ztStory.getId(), "story", "tested"); this.kanbanlaneService.changeStatus(execId, ztStory.getId(), "story", newStatus);
if (!oldStatus.equalsIgnoreCase(newStatus) && ztStory.getProduct() != 0) { if (!oldStatus.equalsIgnoreCase(newStatus) && ztStory.getProduct() != 0) {
ProductStoryStatus oldPStatus = null; ProductStoryStatus oldPStatus = null;
@@ -1270,7 +1405,6 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
} }
//发布 //发布
@Override @Override
@Transactional @Transactional
@@ -1280,22 +1414,25 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
} }
ZtStory ztStory = this.baseMapper.selectById(story); ZtStory ztStory = this.baseMapper.selectById(story);
if (ztStory == null) {
throw new BusinessException("未查询到数据-需求");
}
if (ztStory.getFeedback() != null && ztStory.getFeedback() != 0) { if (ztStory.getFeedback() != null && ztStory.getFeedback() != 0) {
this.storyFeedbackService.feedbackFinished(ztStory.getFeedback(), null); this.storyFeedbackService.feedbackFinished(ztStory.getFeedback(), null);
} }
if (ztStory == null) {
throw new BusinessException("未查询到数据-需求");
}
if (!Arrays.asList("wait", "projected", "developing", "developed", "testing", "tested").contains(ztStory.getStage())) {
if (!Arrays.asList("wait", "projected", "developing", "developed", "testing", "tested",
StoryStageEnums.productWaitVerified.getValue()
,StoryStageEnums.productVerified.getValue()).contains(ztStory.getStage())) {
return; return;
} }
if ("closed".equals(ztStory.getStatus())) { if ("closed".equals(ztStory.getStatus())) {
throw new BusinessException("当前已关闭"); throw new BusinessException("当前已关闭");
} }
if (!"tested".equals(ztStory.getStage())) { if (!StoryStageEnums.productVerified.getValue().equals(ztStory.getStage())) {
throw new BusinessException("当前无法更改发布状态"); throw new BusinessException("当前无法更改发布状态");
} }
//这个需求被多少执行关联 //这个需求被多少执行关联
@@ -1321,6 +1458,10 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
ztStory.setEndDate(new Date()); ztStory.setEndDate(new Date());
} }
this.baseMapper.updateById(ztStory); this.baseMapper.updateById(ztStory);
//用户需求
if(ztStory.getUserStory()!=null&&ztStory.getUserStory().intValue()!=0){
this.storyUserService.changeStatus(ztStory.getUserStory(),UserStoryEnums.RELEASED);
}
String ysUser = ztStory.getYsUser(); String ysUser = ztStory.getYsUser();
@@ -1354,16 +1495,10 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
@Override @Override
public void taskFinishChangeStatus(Integer story, String finishBy, TaskType type, Boolean cancelFlag) { public void taskFinishChangeStatus(Integer story, String finishBy, TaskType type, Boolean cancelFlag) {
ZtStory ztStory = this.getById(story); ZtStory ztStory = this.getById(story);
if(ztStory == null) { if(ztStory == null|| story == 0) {
return; return;
} }
if (ztStory != null && ztStory.getStatus().equals("closed")) { if (ztStory.getStatus().equals("closed")) {
return;
}
if (type != TaskType.test && type != TaskType.devel) {
// return;
}
if (story == null || story == 0) {
return; return;
} }
if (cancelFlag && type == TaskType.devel) { if (cancelFlag && type == TaskType.devel) {
@@ -1393,7 +1528,7 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
if (!CollectionUtils.isEmpty(testedList)) { if (!CollectionUtils.isEmpty(testedList)) {
testedStory(story, finishBy); testedStory(story, finishBy);
} else { } else {
this.finishStory(story, finishBy); this.develFinishStory(story, finishBy);
} }
} }
@@ -1434,7 +1569,7 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
.eq(ZtTask::getType, TaskType.devel.getCode()) .eq(ZtTask::getType, TaskType.devel.getCode())
.in(ZtTask::getStatus, Arrays.asList("done"))); .in(ZtTask::getStatus, Arrays.asList("done")));
if (!CollectionUtils.isEmpty(develedList)) { if (!CollectionUtils.isEmpty(develedList)) {
this.finishStory(story, finishBy); this.develFinishStory(story, finishBy);
return; return;
} }
return; return;
@@ -1482,7 +1617,7 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
if (done < 1) { if (done < 1) {
} else { } else {
this.finishStory(story, finishBy); this.develFinishStory(story, finishBy);
} }
} }
} else { } else {
@@ -1637,14 +1772,10 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
if (execId != null) { if (execId != null) {
this.kanbanlaneService.changeStatus(execId, i, "story", newStatus); this.kanbanlaneService.changeStatus(execId, i, "story", newStatus);
if (!oldStatus.equalsIgnoreCase(newStatus) && ztStory.getProduct() != 0) { if (!oldStatus.equalsIgnoreCase(newStatus) && ztStory.getProduct() != 0) {
ProductStoryStatus oldPStatus = null; ProductStoryStatus oldPStatus = null;
ProductStoryStatus newPStatus = null; ProductStoryStatus newPStatus = null;
oldPStatus = getProductStatus(oldStatus); oldPStatus = getProductStatus(oldStatus);
newPStatus = getProductStatus(newStatus); newPStatus = getProductStatus(newStatus);
this.productService.productChangeStatus(ztStory.getProduct(), oldPStatus, newPStatus); this.productService.productChangeStatus(ztStory.getProduct(), oldPStatus, newPStatus);
} }
} }
@@ -1848,7 +1979,8 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
} }
List<ZtBug> bugList = this.bugService.list(new QueryWrapper<ZtBug>().lambda().eq(ZtBug::getStory, i).ne(ZtBug::getStatus, "closed")); //CHG-067:需求关联 Bug 走 toStory 列(story 列全库未用)——关闭需求时联动关闭其未关闭 Bug
List<ZtBug> bugList = this.bugService.list(new QueryWrapper<ZtBug>().lambda().eq(ZtBug::getTostory, i).ne(ZtBug::getStatus, "closed"));
for (ZtBug bug : bugList) { for (ZtBug bug : bugList) {
ZtBugQo qo = new ZtBugQo(); ZtBugQo qo = new ZtBugQo();
qo.setId(bug.getId()); qo.setId(bug.getId());
@@ -2146,6 +2278,14 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
if (ztUser != null) { if (ztUser != null) {
d.setYsUserName(ztUser.getNickname()); d.setYsUserName(ztUser.getNickname());
} }
ztUser = userMap.get(d.getProductUser());
if (ztUser != null) {
d.setProductUserName(ztUser.getNickname());
}
ztUser = userMap.get(d.getTestUser());
if (ztUser != null) {
d.setTestUserName(ztUser.getNickname());
}
ZtStoryspec storyspec = this.storyspecService.getOne(new QueryWrapper<ZtStoryspec>().lambda().eq(ZtStoryspec::getStory, d.getId())); ZtStoryspec storyspec = this.storyspecService.getOne(new QueryWrapper<ZtStoryspec>().lambda().eq(ZtStoryspec::getStory, d.getId()));
if (storyspec != null) { if (storyspec != null) {
@@ -2445,6 +2585,7 @@ public class ZtStoryServiceImpl extends ServiceImpl<ZtStoryMapper, ZtStory> impl
ProductStoryStatus oldPStatus = null; ProductStoryStatus oldPStatus = null;
if (status.equalsIgnoreCase("developing") || status.equalsIgnoreCase("testing") if (status.equalsIgnoreCase("developing") || status.equalsIgnoreCase("testing")
|| status.equalsIgnoreCase("tested") || status.equalsIgnoreCase("developed") || status.equalsIgnoreCase("tested") || status.equalsIgnoreCase("developed")
|| status.equalsIgnoreCase(StoryStageEnums.productWaitVerified.getValue())|| status.equalsIgnoreCase(StoryStageEnums.productVerified.getValue())
) { ) {
oldPStatus = ProductStoryStatus.JH; oldPStatus = ProductStoryStatus.JH;
@@ -13,19 +13,13 @@ import com.sa.zentao.conf.LoginRiskUser;
import com.sa.zentao.conf.RiskUserThreadLocal; import com.sa.zentao.conf.RiskUserThreadLocal;
import com.sa.zentao.dao.*; import com.sa.zentao.dao.*;
import com.sa.zentao.entity.*; import com.sa.zentao.entity.*;
import com.sa.zentao.enums.ActionStatus; import com.sa.zentao.enums.*;
import com.sa.zentao.enums.ActionType;
import com.sa.zentao.enums.FileTypes;
import com.sa.zentao.enums.UserStoryEnums;
import com.sa.zentao.mapper.ZtStoryUserMapper; import com.sa.zentao.mapper.ZtStoryUserMapper;
import com.sa.zentao.qo.StoryQo; import com.sa.zentao.qo.StoryQo;
import com.sa.zentao.qo.ZtProjectQo; import com.sa.zentao.qo.ZtProjectQo;
import com.sa.zentao.service.*; import com.sa.zentao.service.*;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sa.zentao.utils.BeanCopyUtil; import com.sa.zentao.utils.*;
import com.sa.zentao.utils.BeanCopyUtilCallBack;
import com.sa.zentao.utils.SendEmail;
import com.sa.zentao.utils.VxMessageUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
@@ -84,6 +78,9 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
@Autowired @Autowired
private VxService vxService; private VxService vxService;
@Autowired
private IZtProjectstoryService projectstoryService;
@Value("${file.backUrl}") @Value("${file.backUrl}")
private String url; private String url;
@@ -128,6 +125,7 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
spec.setSpec(dto.getSpec()); spec.setSpec(dto.getSpec());
spec.setVerify(dto.getVerify()); spec.setVerify(dto.getVerify());
spec.setFiles(dto.getFileUrl()); spec.setFiles(dto.getFileUrl());
spec.setStoryBackground(dto.getStoryBackground());
return spec; return spec;
} }
@@ -153,7 +151,8 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
public void editStory(ZtStoryUserDTO dto) { public void editStory(ZtStoryUserDTO dto) {
Integer id = dto.getId(); Integer id = dto.getId();
ZtStoryUser ztStory = this.baseMapper.selectById(id); ZtStoryUser ztStory = this.baseMapper.selectById(id);
String title = dto.getTitle();
String oldTitle = ztStory.getTitle();
String storyStatus = ztStory.getStatus(); String storyStatus = ztStory.getStatus();
@@ -169,7 +168,10 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
actionService.addAction(ActionType.USERXQ, ActionStatus.BJ, dto.getId(), dto.getProduct() + "", dto.getProject(), null, actionService.addAction(ActionType.USERXQ, ActionStatus.BJ, dto.getId(), dto.getProduct() + "", dto.getProject(), null,
RiskUserThreadLocal.get().getName(), dto.getRemark(), ""); RiskUserThreadLocal.get().getName(), dto.getRemark(), "");
if(!title.equals(oldTitle)) {
actionService.addAction(ActionType.USERXQ, ActionStatus.BJ, dto.getId(), dto.getProduct() + "", dto.getProject(), null,
RiskUserThreadLocal.get().getName(), oldTitle +"修改为 -> "+title, "");
}
BeanUtils.copyProperties(dto, ztStory, "status"); BeanUtils.copyProperties(dto, ztStory, "status");
ztStory.setLasteditedby(RiskUserThreadLocal.get().getName()); ztStory.setLasteditedby(RiskUserThreadLocal.get().getName());
@@ -223,7 +225,7 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
ztStory.setVerify(null); ztStory.setVerify(null);
this.baseMapper.updateById(ztStory); this.baseMapper.updateById(ztStory);
ZtStoryUserspec spec = this.storyUserspecService.getOne(new QueryWrapper<ZtStoryUserspec>().lambda().eq(ZtStoryUserspec::getStory, id)); ZtStoryUserspec spec = this.storyUserspecService.getOne(new QueryWrapper<ZtStoryUserspec>().lambda().eq(ZtStoryUserspec::getStory, id).last(" limit 1"));
if (spec == null) { if (spec == null) {
spec = new ZtStoryUserspec(); spec = new ZtStoryUserspec();
spec.setStory(id); spec.setStory(id);
@@ -232,6 +234,7 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
spec.setSpec(dto.getSpec()); spec.setSpec(dto.getSpec());
spec.setVerify(dto.getVerify()); spec.setVerify(dto.getVerify());
spec.setFiles(dto.getFileUrl()); spec.setFiles(dto.getFileUrl());
spec.setStoryBackground(dto.getStoryBackground());
storyUserspecService.save(spec); storyUserspecService.save(spec);
} else { } else {
spec.setVersion(1); spec.setVersion(1);
@@ -239,9 +242,11 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
spec.setSpec(dto.getSpec()); spec.setSpec(dto.getSpec());
spec.setVerify(dto.getVerify()); spec.setVerify(dto.getVerify());
spec.setFiles(dto.getFileUrl()); spec.setFiles(dto.getFileUrl());
spec.setStoryBackground(dto.getStoryBackground());
storyUserspecService.update(new UpdateWrapper<ZtStoryUserspec>().lambda().eq(ZtStoryUserspec::getStory, id) storyUserspecService.update(new UpdateWrapper<ZtStoryUserspec>().lambda().eq(ZtStoryUserspec::getStory, id)
.set(ZtStoryUserspec::getSpec, dto.getSpec()) .set(ZtStoryUserspec::getSpec, dto.getSpec())
.set(ZtStoryUserspec::getVerify, dto.getVerify()) .set(ZtStoryUserspec::getVerify, dto.getVerify())
.set(ZtStoryUserspec::getStoryBackground, dto.getStoryBackground())
.set(ZtStoryUserspec::getFiles, dto.getFileUrl()) .set(ZtStoryUserspec::getFiles, dto.getFileUrl())
); );
} }
@@ -249,16 +254,15 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
fileService.updateFile(dto.getFiles(), ztStory.getId(), FileTypes.userStory); fileService.updateFile(dto.getFiles(), ztStory.getId(), FileTypes.userStory);
} }
@Override @Override
public Map<String, List<ZtStoryUserDTO>> getKanban(Integer productId) { public Map<String, List<ZtStoryUserDTO>> getKanban(Integer productId) {
List<ZtStoryUser> ztStoryUsers = this.baseMapper.selectList(new QueryWrapper<ZtStoryUser>() List<ZtStoryUser> ztStoryUsers = this.baseMapper.selectList(new QueryWrapper<ZtStoryUser>()
.lambda().select(ZtStoryUser::getId,ZtStoryUser::getPri,ZtStoryUser::getTitle,ZtStoryUser::getStage,ZtStoryUser::getStatus,ZtStoryUser::getOpenedby,ZtStoryUser::getOpeneddate,ZtStoryUser::getLastediteddate) .lambda().select(ZtStoryUser::getId, ZtStoryUser::getPri, ZtStoryUser::getTitle, ZtStoryUser::getStage, ZtStoryUser::getStatus, ZtStoryUser::getOpenedby, ZtStoryUser::getOpeneddate, ZtStoryUser::getLastediteddate, ZtStoryUser::getProductUser)
.eq(ZtStoryUser::getProduct, productId).orderByDesc(ZtStoryUser::getId)); .eq(ZtStoryUser::getProduct, productId).orderByDesc(ZtStoryUser::getId));
Map<String, List<ZtStoryUserDTO>> map = new HashMap<>(); Map<String, List<ZtStoryUserDTO>> map = new HashMap<>();
Map<String, ZtUser> stringZtUserMap = this.userService.userMapByIds(ztStoryUsers.stream().map(o -> o.getOpenedby()).collect(Collectors.toList())); Map<String, ZtUser> stringZtUserMap = this.userService.userMapByIds(null);
List<ZtStoryUserDTO> ztStoryUserDTOS = BeanCopyUtil.copyListProperties(ztStoryUsers, ZtStoryUserDTO::new, new BeanCopyUtilCallBack<ZtStoryUser, ZtStoryUserDTO>() { List<ZtStoryUserDTO> ztStoryUserDTOS = BeanCopyUtil.copyListProperties(ztStoryUsers, ZtStoryUserDTO::new, new BeanCopyUtilCallBack<ZtStoryUser, ZtStoryUserDTO>() {
@Override @Override
@@ -268,6 +272,11 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
s.setOpenedbyName(ztUser.getNickname()); s.setOpenedbyName(ztUser.getNickname());
s.setColor(ztUser.getColor()); s.setColor(ztUser.getColor());
} }
ztUser = stringZtUserMap.get(t.getProductUser());
if (ztUser != null) {
s.setProductUserName(ztUser.getNickname());
s.setProductUserColor(ztUser.getColor());
}
} }
}); });
@@ -305,6 +314,11 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
qo.setProductIds(pIds); qo.setProductIds(pIds);
} }
if (!CollectionUtils.isEmpty(qo.getStatusList()) && qo.getStatusList().contains("unconfirmed")) {
qo.getStatusList().addAll(Arrays.asList("unconfirmed",
"waitcommunicate", "waitdesign", "designdoing", "designdone", "storyunconfirmed"));
}
Page<ZtStoryUserDTO> page = PageHelper.startPage(qo.getCurrentPage(), qo.getPageSize()); Page<ZtStoryUserDTO> page = PageHelper.startPage(qo.getCurrentPage(), qo.getPageSize());
List<ZtStoryUserDTO> list = this.baseMapper.pageList(qo); List<ZtStoryUserDTO> list = this.baseMapper.pageList(qo);
if (!CollectionUtils.isEmpty(list)) { if (!CollectionUtils.isEmpty(list)) {
@@ -313,7 +327,9 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
Map<String, ZtUser> userMap = this.userService.userMapByIds(null); Map<String, ZtUser> userMap = this.userService.userMapByIds(null);
Map<Integer, List<ZtStory>> storyUserMap = getStoryUserMap(list); Map<Integer, List<ZtStory>> storyMap = getStoryMap(list);
//story - exec
Map<Integer,List<ZtProject>> execMap= getExecMap(storyMap);
Map<Integer, List<ZtStoryreviewDTO>> rMap = getReviewMap(list); Map<Integer, List<ZtStoryreviewDTO>> rMap = getReviewMap(list);
for (ZtStoryUserDTO d : list) { for (ZtStoryUserDTO d : list) {
@@ -357,32 +373,101 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
if (ztUser != null) { if (ztUser != null) {
d.setYsUserName(ztUser.getNickname()); d.setYsUserName(ztUser.getNickname());
} }
List<ZtStory> ztStories = storyUserMap.get(d.getId()); ztUser = userMap.get(d.getProductUser());
if (ztUser != null) {
d.setProductUserName(ztUser.getNickname());
}
List<ZtStory> ztStories = storyMap.get(d.getId());
if (!CollectionUtils.isEmpty(ztStories)) { if (!CollectionUtils.isEmpty(ztStories)) {
for (ZtStory story:ztStories){
List<ZtProject> execList = execMap.get(story.getId());
if(!CollectionUtils.isEmpty(execList)){
if(CollectionUtils.isEmpty(d.getExecList())){
d.setExecList(new HashSet<>());
}
d.getExecList().addAll(execList);
}
}
d.setStoryList(ztStories.stream().map(o -> o.getId() + ":" + o.getTitle()).collect(Collectors.joining(","))); d.setStoryList(ztStories.stream().map(o -> o.getId() + ":" + o.getTitle()).collect(Collectors.joining(",")));
d.setSList(CollectionUtils.isEmpty(ztStories) ? null : BeanCopyUtil.copyListProperties(ztStories, ZtStoryDTO::new)); d.setSList(CollectionUtils.isEmpty(ztStories) ? null : BeanCopyUtil.copyListProperties(ztStories, ZtStoryDTO::new));
} }
d.setExecNames(buildExecNames(d.getExecList()));
d.setLinkUrl(url);
String reName = d.getTitle();
String reUrl = url+"/#/product-user-story-info/"+d.getId();
WriteCellData<String> hyperlink = new WriteCellData<>(reName);
HyperlinkData hyperlinkData = new HyperlinkData();
try {
hyperlinkData.setAddress(reUrl.replace(" ", "%20"));
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
hyperlink.setHyperlinkData(hyperlinkData);
d.setUrl(hyperlink );
} catch (Exception e) {
log.error(e.getMessage(), e);
d.setUrl(null );
}
} }
} }
return new PageInfo<ZtStoryUserDTO>(list); return new PageInfo<ZtStoryUserDTO>(list);
} }
private Map<Integer, List<ZtProject>> getExecMap(Map<Integer, List<ZtStory>> storyMap) {
if(storyMap==null||storyMap.isEmpty()){
return new HashMap<>();
}
// 收集到指定类型的列表(比如ArrayList),还可扩展去重、排序等
List<ZtStory> storyList = storyMap.values()
.stream()
.filter(list -> list != null)
.flatMap(List::stream)
// 自定义收集器:指定ArrayList,还可加.distinct()去重
.collect(Collectors.toCollection(ArrayList::new));
if(CollectionUtils.isEmpty(storyList)){
return new HashMap<>();
}
List<ZtProjectstory> list = this.projectstoryService.list(new QueryWrapper<ZtProjectstory>().lambda()
.eq(ZtProjectstory::getType,ProjectTypeEnums.execution.getValue())
.in(ZtProjectstory::getStory, storyList.stream().map(o -> o.getId()).collect(Collectors.toUnmodifiableList())));
if(CollectionUtils.isEmpty(list)){
return new HashMap<>();
}
List<ZtProject> ztExecList = this.projectService.listByIds(list.stream().map(o -> o.getProject()).collect(Collectors.toUnmodifiableList()));
if(CollectionUtils.isEmpty(list)){
return new HashMap<>();
}
// 第一步:先构建 projectId -> ZtProject 的映射(方便快速查找)
Map<Integer, ZtProject> projectIdToProjectMap = ztExecList.stream()
.collect(Collectors.toMap(
ZtProject::getId, // key:项目ID
project -> project, // value:项目对象
(existing, replacement) -> existing // 处理重复projectId的情况,保留原有值
));
// 第二步:按story分组,收集对应的ZtProject列表
Map<Integer, List<ZtProject>> storyToProjectsMap = list.stream()
.collect(Collectors.groupingBy(
ZtProjectstory::getStory, // 分组key:story字段
// 分组value:将每个ZtProjectstory对应的project转换成ZtProject,并收集为列表
Collectors.mapping(
projectStory -> projectIdToProjectMap.get(projectStory.getProject()),
// 过滤掉null(防止projectId不存在的情况),并收集为List
Collectors.filtering(
project -> project != null,
Collectors.toList()
)
)
));
// 最终返回这个Map(你可以根据需要调整返回逻辑)
return storyToProjectsMap;
}
/**
* CHG-078:迭代版本列取值——execList 中迭代名称去重、排序后逗号拼接;空/无有效名称返回 null。
*/
static String buildExecNames(Set<ZtProject> execList) {
if (CollectionUtils.isEmpty(execList)) {
return null;
}
String names = execList.stream()
.filter(Objects::nonNull)
.map(ZtProject::getName)
.filter(StringUtils::isNotEmpty)
.distinct()
.sorted()
.collect(Collectors.joining(","));
return StringUtils.isEmpty(names) ? null : names;
}
@Override @Override
@Transactional @Transactional
public void changeStatus(ZtStoryUserDTO dto) { public void changeStatus(ZtStoryUserDTO dto) {
@@ -475,7 +560,6 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
} }
lambda.set(ZtStoryreview::getReviewdate, new Date()); lambda.set(ZtStoryreview::getReviewdate, new Date());
lambda.eq(ZtStoryreview::getStory, ztStory.getId()); lambda.eq(ZtStoryreview::getStory, ztStory.getId());
lambda.eq(ZtStoryreview::getReviewer, RiskUserThreadLocal.get().getName()); lambda.eq(ZtStoryreview::getReviewer, RiskUserThreadLocal.get().getName());
@@ -499,6 +583,9 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
ztStory.setRevieweddate( ztStory.setRevieweddate(
new Date() new Date()
); );
ztStory.setActivateddate(
new Date()
);
} }
} }
if (dto.getRevieweResult() == 0) { if (dto.getRevieweResult() == 0) {
@@ -516,10 +603,6 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
//添加action //添加action
actionService.addAction(ActionType.USERXQ, actionStatus, ztStory.getId(), ztStory.getProduct() + "", ztStory.getProject(), null, actionService.addAction(ActionType.USERXQ, actionStatus, ztStory.getId(), ztStory.getProduct() + "", ztStory.getProject(), null,
RiskUserThreadLocal.get().getName(), dto.getDesc(), ztStory.getAssignedto()); RiskUserThreadLocal.get().getName(), dto.getDesc(), ztStory.getAssignedto());
return null; return null;
} }
@@ -547,7 +630,11 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
if (ztUser != null) { if (ztUser != null) {
dto.setYsUserName(ztUser.getNickname()); dto.setYsUserName(ztUser.getNickname());
} }
Map<Integer, List<ZtStory>> storyUserMap = getStoryUserMap(Arrays.asList(dto)); ztUser = userMap.get(dto.getProductUser());
if (ztUser != null) {
dto.setProductUserName(ztUser.getNickname());
}
Map<Integer, List<ZtStory>> storyUserMap = getStoryMap(Arrays.asList(dto));
List<ZtStory> ztStories = storyUserMap.get(d.getId()); List<ZtStory> ztStories = storyUserMap.get(d.getId());
if (!CollectionUtils.isEmpty(ztStories)) { if (!CollectionUtils.isEmpty(ztStories)) {
dto.setStoryList(ztStories.stream().map(o -> o.getTitle()).collect(Collectors.joining(","))); dto.setStoryList(ztStories.stream().map(o -> o.getTitle()).collect(Collectors.joining(",")));
@@ -600,8 +687,9 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
List<ZtStory> list = this.storyService.list(new QueryWrapper<ZtStory>().lambda().eq(ZtStory::getUserStory, dto.getId())); List<ZtStory> list = this.storyService.list(new QueryWrapper<ZtStory>().lambda().eq(ZtStory::getUserStory, dto.getId()));
if (!CollectionUtils.isEmpty(list)) { if (!CollectionUtils.isEmpty(list)) {
List<ZtStoryDTO> storyDTOList = BeanCopyUtil.copyListProperties(list, ZtStoryDTO::new); List<ZtStoryDTO> storyDTOList = BeanCopyUtil.copyListProperties(list, ZtStoryDTO::new);
if (!CollectionUtils.isEmpty(storyDTOList)) { Map<Integer,List<ZtProject>> execMap= getExecMap(list.stream().collect(Collectors.groupingBy(ZtStory::getId)));
if (!CollectionUtils.isEmpty(storyDTOList)) {
for (ZtStoryDTO storyDTO : storyDTOList) { for (ZtStoryDTO storyDTO : storyDTOList) {
ztUser = userMap.get(storyDTO.getAssignedTo()); ztUser = userMap.get(storyDTO.getAssignedTo());
if (ztUser != null) { if (ztUser != null) {
@@ -611,10 +699,20 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
if (ztUser != null) { if (ztUser != null) {
storyDTO.setOpenedbyName(ztUser.getNickname()); storyDTO.setOpenedbyName(ztUser.getNickname());
} }
List<ZtProject> execList = execMap.get(storyDTO.getId());
if(!CollectionUtils.isEmpty(execList)){
if(CollectionUtils.isEmpty(dto.getExecList())){
dto.setExecList(new HashSet<>());
}
dto.getExecList().addAll(execList);
}
} }
dto.setSList(storyDTOList); dto.setSList(storyDTOList);
} }
//story - exec
dto.setExecNames(buildExecNames(dto.getExecList()));
} }
List<ZtStoryUserTask> tasks = this.storyUserTaskService.list(new QueryWrapper<ZtStoryUserTask>().lambda().eq(ZtStoryUserTask::getUserStoryId, dto.getId())); List<ZtStoryUserTask> tasks = this.storyUserTaskService.list(new QueryWrapper<ZtStoryUserTask>().lambda().eq(ZtStoryUserTask::getUserStoryId, dto.getId()));
if (!CollectionUtils.isEmpty(tasks)) { if (!CollectionUtils.isEmpty(tasks)) {
@@ -640,6 +738,7 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
ZtStoryUserspec ztStoryUserspec = specList.get(0); ZtStoryUserspec ztStoryUserspec = specList.get(0);
dto.setSpec(ztStoryUserspec.getSpec()); dto.setSpec(ztStoryUserspec.getSpec());
dto.setVerify(ztStoryUserspec.getVerify()); dto.setVerify(ztStoryUserspec.getVerify());
dto.setStoryBackground(ztStoryUserspec.getStoryBackground());
} }
return dto; return dto;
@@ -692,7 +791,6 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
} }
@Override @Override
public void changeStatus(Integer id, UserStoryEnums status) { public void changeStatus(Integer id, UserStoryEnums status) {
ZtStoryUser storyUser = this.baseMapper.selectById(id); ZtStoryUser storyUser = this.baseMapper.selectById(id);
@@ -726,6 +824,15 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
//交付物确认 //交付物确认
this.sendConfirmMessage(storyUser.getId(), ztProduct.getPo()); this.sendConfirmMessage(storyUser.getId(), ztProduct.getPo());
} }
if (UserStoryEnums.RELEASED == status) {
//如果研发需求已经全部发布,那么就发布
// wait 初始化 projected 已立项 developing 研发中 developed 研发完毕 testing 测试中 tested
List<ZtStory> storyList = this.storyService.list(new QueryWrapper<ZtStory>().lambda()
.ne(ZtStory::getStatus, "closed").in(ZtStory::getStage, StoryStageEnums.beforeStatus(7)).eq(ZtStory::getUserStory, id));
if (!CollectionUtils.isEmpty(storyList)) {
return;
}
}
storyUser.setStatus(status.getCode()); storyUser.setStatus(status.getCode());
storyUser.setLasteditedby(RiskUserThreadLocal.get().getName()); storyUser.setLasteditedby(RiskUserThreadLocal.get().getName());
storyUser.setLastediteddate(new Date()); storyUser.setLastediteddate(new Date());
@@ -856,7 +963,19 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
} }
} }
private Map<Integer, List<ZtStory>> getStoryUserMap(List<ZtStoryUserDTO> list) { @Override
public void assignedToProductUser(ZtStoryUserDTO dto) {
ZtStoryUser ztStoryUser = this.baseMapper.selectById(dto.getId());
if (ztStoryUser == null) {
throw new BusinessException("未查询到数据");
}
ztStoryUser.setProductUser(dto.getAssignedto());
this.baseMapper.updateById(ztStoryUser);
actionService.addAction(ActionType.USERXQ, ActionStatus.ASSIGNTOPRODUCTUSER, ztStoryUser.getId(), ztStoryUser.getProduct() + "", null, null,
RiskUserThreadLocal.get().getName(), "", "");
}
private Map<Integer, List<ZtStory>> getStoryMap(List<ZtStoryUserDTO> list) {
List<Integer> ids = list.stream().map(o -> o.getId()).collect(Collectors.toList()); List<Integer> ids = list.stream().map(o -> o.getId()).collect(Collectors.toList());
List<ZtStory> list1 = storyService.list(new QueryWrapper<ZtStory>().lambda().in(ZtStory::getUserStory, ids)); List<ZtStory> list1 = storyService.list(new QueryWrapper<ZtStory>().lambda().in(ZtStory::getUserStory, ids));
@@ -894,14 +1013,17 @@ public class ZtStoryUserServiceImpl extends ServiceImpl<ZtStoryUserMapper, ZtSto
} }
} }
//发送消息给微信 产品已经设计 //发送消息给微信 产品已经设计
private void sendDesigndoneMessage(Integer id, String account) { private void sendDesigndoneMessage(Integer id, String account) {
this.vxService.sendMessageToVx(account, VxMessageUtils.storyUserMessage.designdoneMessage(id), new Date()); this.vxService.sendMessageToVx(account, VxMessageUtils.storyUserMessage.designdoneMessage(id), new Date());
} }
//发送消息给微信 提交交付物 //发送消息给微信 提交交付物
private void sendSubmitMessage(Integer id, String account) { private void sendSubmitMessage(Integer id, String account) {
this.vxService.sendMessageToVx(account, VxMessageUtils.storyUserMessage.submitMessage(id), new Date()); this.vxService.sendMessageToVx(account, VxMessageUtils.storyUserMessage.submitMessage(id), new Date());
} }
//确认交付物 //确认交付物
private void sendConfirmMessage(Integer id, String account) { private void sendConfirmMessage(Integer id, String account) {
this.vxService.sendMessageToVx(account, VxMessageUtils.storyUserMessage.sendConfirmMessage(id), new Date()); this.vxService.sendMessageToVx(account, VxMessageUtils.storyUserMessage.sendConfirmMessage(id), new Date());
@@ -1,5 +1,6 @@
package com.sa.zentao.service.impl; package com.sa.zentao.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
@@ -26,6 +27,8 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -231,6 +234,7 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
ZtStory ztStory = this.storyService.getById(story); ZtStory ztStory = this.storyService.getById(story);
if (ztStory != null) { if (ztStory != null) {
dto.setStoryName(ztStory.getTitle()); dto.setStoryName(ztStory.getTitle());
dto.setInnerYsFlag(ztStory.getInnerYsFlag());
} }
} }
@@ -326,6 +330,14 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
return this.baseMapper.itApprovalByUserName(s, firstDayOfMonth, lastDayOfMonth); return this.baseMapper.itApprovalByUserName(s, firstDayOfMonth, lastDayOfMonth);
} }
@Override
public List<ItApproval> itApprovalsByNames(List<String> names, Date firstDayOfMonth, Date lastDayOfMonth) {
if (CollectionUtils.isEmpty(names)) {
return new ArrayList<>();
}
return this.baseMapper.itApprovalsByNames(names, firstDayOfMonth, lastDayOfMonth);
}
@Override @Override
public List<ZtTask> taskListByEIdsAndDate(Date firstDayOfMonth, Date lastDayOfMonth, List<Integer> pids) { public List<ZtTask> taskListByEIdsAndDate(Date firstDayOfMonth, Date lastDayOfMonth, List<Integer> pids) {
if (CollectionUtils.isEmpty(pids)) { if (CollectionUtils.isEmpty(pids)) {
@@ -577,7 +589,8 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
if (story != null && story != 0) { if (story != null && story != 0) {
ZtStory ztStory = this.storyService.getById(story); ZtStory ztStory = this.storyService.getById(story);
// wait 初始化 projected 已立项 developing 研发中 developed 研发完毕 testing 测试中 tested // wait 初始化 projected 已立项 developing 研发中 developed 研发完毕 testing 测试中 tested
if (!Arrays.asList("wait", "projected", "developing", "developed", "testing", "tested").contains(ztStory.getStage())) { if (!Arrays.asList("wait", "projected", "developing", "developed", "testing", "tested", StoryStageEnums.productWaitVerified.getValue()
, StoryStageEnums.productVerified.getValue()).contains(ztStory.getStage())) {
throw new BusinessException("当前状态无法添加任务"); throw new BusinessException("当前状态无法添加任务");
} }
if (ztStory.getStatus().equals("closed")) { if (ztStory.getStatus().equals("closed")) {
@@ -647,6 +660,9 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
if (ztTask.getDeadline() == null) { if (ztTask.getDeadline() == null) {
throw new BusinessException("当前环境异常请联系管理员"); throw new BusinessException("当前环境异常请联系管理员");
} }
if("test".equals(ztTask.getType())&&"reviewing".equals(ztTask.getStatus())){
ztTask.setStatus("wait");
}
this.baseMapper.insert(ztTask); this.baseMapper.insert(ztTask);
if (ztTask.getDeadline() != null && ztTask.getStory() != null && ztTask.getStory() != 0) { if (ztTask.getDeadline() != null && ztTask.getStory() != null && ztTask.getStory() != 0) {
ZtStory ztStory = this.storyService.getById(ztTask.getStory()); ZtStory ztStory = this.storyService.getById(ztTask.getStory());
@@ -786,6 +802,44 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
} }
} }
if (ztTask.getDeadline() != null && ztTask.getStory() != null && ztTask.getStory() != 0) {
ZtStory ztStory = this.storyService.getById(ztTask.getStory());
ztStory.setDevelPlanEndTime(null);
if (ztTask.getType().equals("devel") && (ztStory.getDevelPlanEndTime() == null || ztTask.getDeadline().after(ztStory.getDevelPlanEndTime()))) {
ztStory.setDevelPlanEndTime(ztTask.getDeadline());
} else {
//查找最大时间那个
List<ZtTask> ztTasks = this.baseMapper.selectList(new QueryWrapper<ZtTask>().lambda()
.select(ZtTask::getDeadline)
.isNotNull(ZtTask::getDeadline)
.eq(ZtTask::getType, "devel")
.ne(ZtTask::getId, ztTask.getId())
.in(ZtTask::getStatus, "pause", "done", "wait", "reviewing", "doing", "draft")
.eq(ZtTask::getStory, ztTask.getStory()).orderByDesc(ZtTask::getDeadline));
this.storyService.update(new UpdateWrapper<ZtStory>().lambda()
.eq(ZtStory::getId, ztTask.getStory())
.set(ZtStory::getDevelPlanEndTime, CollectionUtils.isEmpty(ztTasks) ? null : DateUtils.getDayLast(ztTasks.get(0).getDeadline())));
}
if (ztTask.getType().equals("test") && (ztStory.getTestPlanEndTime() == null || ztTask.getDeadline().after(ztStory.getTestPlanEndTime()))) {
ztStory.setTestPlanEndTime(ztTask.getDeadline());
} else {
if (ztTask.getType().equals("test") && ztStory.getTestPlanEndTime() != null) {
//查找最大时间那个
ztStory.setDevelPlanEndTime(ztTask.getDeadline());
List<ZtTask> ztTasks = this.baseMapper.selectList(new QueryWrapper<ZtTask>().lambda()
.select(ZtTask::getDeadline)
.isNotNull(ZtTask::getDeadline)
.eq(ZtTask::getType, "test")
.ne(ZtTask::getId, ztTask.getId())
.in(ZtTask::getStatus, "pause", "done", "wait", "reviewing", "doing", "draft")
.eq(ZtTask::getStory, ztTask.getStory()).orderByDesc(ZtTask::getDeadline));
this.storyService.update(new UpdateWrapper<ZtStory>().lambda()
.eq(ZtStory::getId, ztTask.getStory())
.set(ZtStory::getTestPlanEndTime, CollectionUtils.isEmpty(ztTasks) ? null : DateUtils.getDayLast(ztTasks.get(0).getDeadline())));
}
}
this.storyService.updateById(ztStory);
}
fileService.updateFile(dto.getFiles(), ztTask.getId(), FileTypes.task); fileService.updateFile(dto.getFiles(), ztTask.getId(), FileTypes.task);
@@ -893,10 +947,10 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
String finishBy = StringUtils.isEmpty(ztTask.getFinishedby()) ? RiskUserThreadLocal.get().getName() : ztTask.getFinishedby(); String finishBy = StringUtils.isEmpty(ztTask.getFinishedby()) ? RiskUserThreadLocal.get().getName() : ztTask.getFinishedby();
//如果是测试任务 提交交付物 //如果是测试任务 提交交付物
if ("test".equals(ztTask.getType()) && !StringUtils.isEmpty(ztTask.getDeliverContent())) { if ("test".equals(ztTask.getType()) && !StringUtils.isEmpty(ztTask.getDeliverContent())) {
actionService.addAction(ActionType.RW, ActionStatus.TJJFW, dto.getId(), projectproduct == null ? null : projectproduct.getProduct().toString(), projectproduct == null ? null : projectproduct.getProject(), ztTask.getExecution(), actionService.addAction(ActionType.RW, ActionStatus.TJJFW, ztTask.getId(), projectproduct == null ? null : projectproduct.getProduct().toString(), projectproduct == null ? null : projectproduct.getProject(), ztTask.getExecution(),
StringUtils.isEmpty(finishBy) ? RiskUserThreadLocal.get().getName() : finishBy, dto.getDeliverContent(), null); StringUtils.isEmpty(finishBy) ? RiskUserThreadLocal.get().getName() : finishBy, dto.getDeliverContent(), null);
if (ztTask.getStory() != null && ztTask.getStory() != 0) { if (ztTask.getStory() != null && ztTask.getStory() != 0) {
actionService.addAction(ActionType.XQ, ActionStatus.TJJFW, dto.getId(), projectproduct == null ? null : projectproduct.getProduct().toString(), projectproduct == null ? null : projectproduct.getProject(), ztTask.getExecution(), actionService.addAction(ActionType.XQ, ActionStatus.TJJFW, ztTask.getStory(), projectproduct == null ? null : projectproduct.getProduct().toString(), projectproduct == null ? null : projectproduct.getProject(), ztTask.getExecution(),
StringUtils.isEmpty(finishBy) ? RiskUserThreadLocal.get().getName() : finishBy, dto.getDeliverContent(), null); StringUtils.isEmpty(finishBy) ? RiskUserThreadLocal.get().getName() : finishBy, dto.getDeliverContent(), null);
} }
if (ztTask.getStory() != null && ztTask.getStory() != 0) { if (ztTask.getStory() != null && ztTask.getStory() != 0) {
@@ -979,6 +1033,7 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
ztTask.setDeliverContent(dto.getDeliverContent()); ztTask.setDeliverContent(dto.getDeliverContent());
ztTask.setLasteditedby(RiskUserThreadLocal.get().getName()); ztTask.setLasteditedby(RiskUserThreadLocal.get().getName());
ztTask.setLastediteddate(new Date()); ztTask.setLastediteddate(new Date());
ztTask.setFeedbackRemark(dto.getFeedbackRemark());
this.baseMapper.updateById(ztTask); this.baseMapper.updateById(ztTask);
ZtEffortDTO e = new ZtEffortDTO(); ZtEffortDTO e = new ZtEffortDTO();
@@ -996,13 +1051,14 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
actionService.addAction(ActionType.RW, ActionStatus.TJJFW, dto.getId(), projectproduct == null ? null : projectproduct.getProduct().toString(), projectproduct == null ? null : projectproduct.getProject(), ztTask.getExecution(), actionService.addAction(ActionType.RW, ActionStatus.TJJFW, dto.getId(), projectproduct == null ? null : projectproduct.getProduct().toString(), projectproduct == null ? null : projectproduct.getProject(), ztTask.getExecution(),
StringUtils.isEmpty(finishBy) ? RiskUserThreadLocal.get().getName() : finishBy, dto.getDeliverContent(), null); StringUtils.isEmpty(finishBy) ? RiskUserThreadLocal.get().getName() : finishBy, dto.getDeliverContent(), null);
if (ztTask.getStory() != null && ztTask.getStory() != 0) { if (ztTask.getStory() != null && ztTask.getStory() != 0) {
//更新需求交付物
ZtStory ztStory = this.storyService.getById(ztTask.getStory()); ZtStory ztStory = this.storyService.getById(ztTask.getStory());
if (ztStory != null) { if (ztStory != null) {
ztStory.setDeliverContent(ztTask.getDeliverContent()); ztStory.setDeliverContent(dto.getDeliverContent());
ztStory.setLasteditedby(RiskUserThreadLocal.get().getName());
ztStory.setLastediteddate(new Date());
this.storyService.updateById(ztStory); this.storyService.updateById(ztStory);
} }
actionService.addAction(ActionType.XQ, ActionStatus.TJJFW, dto.getId(), projectproduct == null ? null : projectproduct.getProduct().toString(), projectproduct == null ? null : projectproduct.getProject(), ztTask.getExecution(), actionService.addAction(ActionType.XQ, ActionStatus.TJJFW, ztTask.getStory(), projectproduct == null ? null : projectproduct.getProduct().toString(), projectproduct == null ? null : projectproduct.getProject(), ztTask.getExecution(),
StringUtils.isEmpty(finishBy) ? RiskUserThreadLocal.get().getName() : finishBy, dto.getDeliverContent(), null); StringUtils.isEmpty(finishBy) ? RiskUserThreadLocal.get().getName() : finishBy, dto.getDeliverContent(), null);
} }
} }
@@ -1041,7 +1097,11 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
if (ztTask.getFeedback() != null && ztTask.getFeedback() != 0) { if (ztTask.getFeedback() != null && ztTask.getFeedback() != 0) {
this.storyFeedbackService.feedbackFinished(ztTask.getFeedback(), ztTask.getFeedbackRemark()); this.storyFeedbackService.feedbackFinished(ztTask.getFeedback(), ztTask.getFeedbackRemark());
} }
// if (ztTask.getStory() != null && ztTask.getStory() != 0 && ObjectUtil.equal(dto.getInnerYsFlag(), 1)) {
// ZtStoryDTO qo = new ZtStoryDTO();
// qo.setId(ztTask.getStory());
// this.storyService.testSubmitVerified(qo);
// }
} }
@Override @Override
@@ -1091,7 +1151,6 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
} }
ZtProjectproduct projectproduct = projectproductService.getOne(new QueryWrapper<ZtProjectproduct>().lambda().eq(ZtProjectproduct::getProject, ztTask.getProject())); ZtProjectproduct projectproduct = projectproductService.getOne(new QueryWrapper<ZtProjectproduct>().lambda().eq(ZtProjectproduct::getProject, ztTask.getProject()));
//添加action //添加action
@@ -1303,6 +1362,167 @@ public class ZtTaskServiceImpl extends ServiceImpl<ZtTaskMapper, ZtTask> impleme
} }
@Override
@Transactional
public Map<String, Object> aiBatchAdd(ZtTaskAiBatchDTO dto) {
//仅 AI 框架通道可用:必须携带 ai 账户 token,创建人/留痕 actor 取自 token 身份(CHG-061)
LoginRiskUser aiLoginUser = RiskUserThreadLocal.get();
if (aiLoginUser == null || !"ai".equals(aiLoginUser.getName())) {
throw new BusinessException("aiBatchAdd仅AI框架通道可用(需ai账户token)");
}
String aiActor = aiLoginUser.getName();
if (dto.getStoryId() == null) {
throw new BusinessException("storyId不能为空");
}
ZtStory ztStory = this.storyService.getById(dto.getStoryId());
if (ztStory == null) {
throw new BusinessException("需求不存在:" + dto.getStoryId());
}
//已发布(含待验收/验收不通过:storyYs验收不通过status仍保持released)、已完成(验收通过)、已关闭的需求不允许AI拆任务
UserStoryEnums storyStatus = UserStoryEnums.transfer(ztStory.getStatus());
if (storyStatus == UserStoryEnums.RELEASED || storyStatus == UserStoryEnums.YWC || storyStatus == UserStoryEnums.CLOSED) {
throw new BusinessException("需求" + storyStatus.getValue() + ",不允许创建任务:" + dto.getStoryId());
}
List<ZtTaskAiBatchDTO.AiTaskItem> tasks = dto.getTasks();
if (CollectionUtils.isEmpty(tasks)) {
throw new BusinessException("tasks不能为空");
}
//type 非 devel/test/design/affair 整批拒绝(design CHG-081 放行、affair CHG-082 放行:均参与AI拆批;不回写需求计划时间、不进绩效专项公式)
for (ZtTaskAiBatchDTO.AiTaskItem item : tasks) {
if (StringUtils.isEmpty(item.getName())) {
throw new BusinessException("任务名称不能为空");
}
if (!TaskType.devel.getCode().equals(item.getType()) && !TaskType.test.getCode().equals(item.getType())
&& !TaskType.design.getCode().equals(item.getType()) && !TaskType.affair.getCode().equals(item.getType())) {
throw new BusinessException("任务类型仅支持devel/test/design/affair:" + item.getType());
}
}
//参数补全:推导 execution/project(老链路 batchAddTask 的看板挂载、project 回填依赖,缺失会NPE)
//需求关联多个迭代时取最后关联的(迭代id自增,project 倒序第一条即最新迭代)
ZtProjectstory execStory = this.projectstoryService.getOne(new QueryWrapper<ZtProjectstory>().lambda()
.eq(ZtProjectstory::getStory, dto.getStoryId())
.eq(ZtProjectstory::getType, ProjectTypeEnums.execution.getValue())
.orderByDesc(ZtProjectstory::getProject)
.last("limit 1"));
if (execStory == null) {
throw new BusinessException("需求未关联迭代,无法拆分任务");
}
Integer execution = execStory.getProject();
ZtProjectstory projectStoryCheck = this.projectstoryService.getOne(new QueryWrapper<ZtProjectstory>().lambda()
.eq(ZtProjectstory::getStory, dto.getStoryId())
.eq(ZtProjectstory::getType, ProjectTypeEnums.project.getValue())
.last("limit 1"));
if (projectStoryCheck == null) {
throw new BusinessException("需求未关联项目,无法拆分任务");
}
//同 storyId+name+type 已存在 → 跳过记 skipped
List<ZtTask> existList = this.list(new QueryWrapper<ZtTask>().lambda().eq(ZtTask::getStory, dto.getStoryId()));
Set<String> existKeys = existList.stream()
.filter(o -> o.getDeleted() == null || "0".equals(o.getDeleted()))
.map(o -> o.getName() + "#" + o.getType())
.collect(Collectors.toSet());
//调用前已有任务 id 快照(含已删除),用于调用后按 id 差集精确识别本次新建(openeddate 精度到秒会截断,时间比较不可靠)
Set<Integer> existIds = existList.stream().map(ZtTask::getId).collect(Collectors.toSet());
List<String> skipped = new ArrayList<>();
List<String> createdDescs = new ArrayList<>();
List<ZtTaskDTO> taskDtos = new ArrayList<>();
Set<String> createdKeys = new HashSet<>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date today = new Date();
for (ZtTaskAiBatchDTO.AiTaskItem item : tasks) {
String key = item.getName() + "#" + item.getType();
if (existKeys.contains(key) || createdKeys.contains(key)) {
skipped.add(item.getName());
continue;
}
//对齐老接口规则:工时必须录入
if (item.getAiEvaluationTime() == null) {
throw new BusinessException("请检查工时:" + item.getName());
}
//CHG-076:assignedTo 支持中文姓名——不是既有账号时按昵称查库映射账号(AI 框架传中文名场景)
String assignedTo = item.getAssignedTo();
if (StringUtils.isNotEmpty(assignedTo) && userService.getByAccount(assignedTo) == null) {
ZtUser byNickname = userService.getOne(new QueryWrapper<ZtUser>().lambda()
.eq(ZtUser::getNickname, assignedTo).ne(ZtUser::getDeleted, "1").last("limit 1"));
if (byNickname != null) {
assignedTo = byNickname.getAccount();
}
}
//补全老接口必传字段:estimate/left/estStarted/deadline/execution(日期缺省补当天,deadline缺省取estStarted)
ZtTaskDTO taskDto = new ZtTaskDTO();
taskDto.setStory(dto.getStoryId());
taskDto.setExecution(execution);
taskDto.setName(item.getName());
taskDto.setType(item.getType());
taskDto.setAssignedTo(assignedTo);
taskDto.setDesc(item.getDesc());
taskDto.setEstimate(item.getAiEvaluationTime());
taskDto.setLeft(item.getAiEvaluationTime());
try {
taskDto.setEstStarted(StringUtils.isNotEmpty(item.getPlanStartDate()) ? sdf.parse(item.getPlanStartDate()) : today);
taskDto.setDeadline(StringUtils.isNotEmpty(item.getDeadline()) ? sdf.parse(item.getDeadline()) : taskDto.getEstStarted());
} catch (ParseException e) {
throw new BusinessException("日期格式错误,应为yyyy-MM-dd:" + item.getName());
}
taskDtos.add(taskDto);
createdKeys.add(key);
//需求级汇总文案收集(一次批拆合并为一条需求动态,CHG-062)
String assigneeSeg = "未指派";
if (StringUtils.isNotEmpty(assignedTo)) {
ZtUser assigneeUser = userService.getByAccount(assignedTo);
String nick = assigneeUser != null && StringUtils.isNotEmpty(assigneeUser.getNickname())
? assigneeUser.getNickname() : item.getAssignedTo();
assigneeSeg = "指派:" + nick;
}
//CHG-081:类型中文名按 TaskType 枚举取值(原写死 test→测试/其他→开发,design 放行后会被误记为「开发」)
createdDescs.add(item.getName() + "(" + TaskType.transferType(item.getType()).getValue()
+ ",工时" + item.getAiEvaluationTime() + "h"
+ "," + assigneeSeg + ")");
}
//走老批量创建链路:project/product回填、需求计划时间回写、消息推送、看板挂载、任务级留痕(RW+XJ)
List<Integer> taskIds = new ArrayList<>();
if (!taskDtos.isEmpty()) {
ZtTaskDTO batchDto = new ZtTaskDTO();
batchDto.setList(taskDtos);
this.batchAddTask(batchDto);
//batchAddTask无返回值:按 id 差集 + name#type 精确匹配取本次新建任务 id
List<ZtTask> createdList = this.list(new QueryWrapper<ZtTask>().lambda()
.eq(ZtTask::getStory, dto.getStoryId()));
taskIds = createdList.stream()
.filter(o -> !existIds.contains(o.getId()))
.filter(o -> createdKeys.contains(o.getName() + "#" + o.getType()))
.map(ZtTask::getId)
.sorted()
.collect(Collectors.toList());
}
//需求级留痕:一次批拆合并写一条(CHG-062)
if (!createdDescs.isEmpty()) {
String[] nums = {"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩"};
StringBuilder sb = new StringBuilder("AI拆分任务 ").append(createdDescs.size()).append(" 个:");
for (int i = 0; i < createdDescs.size(); i++) {
sb.append(i < 10 ? nums[i] : ("(" + (i + 1) + ")")).append(createdDescs.get(i));
if (i < createdDescs.size() - 1) {
sb.append(";");
}
}
if (!skipped.isEmpty()) {
sb.append(";重复跳过 ").append(skipped.size()).append(" 个");
}
actionService.addAction(com.sa.zentao.enums.ActionType.XQ, com.sa.zentao.enums.ActionStatus.BJ,
dto.getStoryId(), ztStory.getProduct() == null ? null : ztStory.getProduct() + "", null, null,
aiActor, sb.toString(), null);
}
Map<String, Object> result = new HashMap<>();
result.put("created", taskIds.size());
result.put("taskIds", taskIds);
result.put("skipped", skipped);
return result;
}
@Override @Override
@Transactional @Transactional
public void approval(ZtTaskDTO dto) { public void approval(ZtTaskDTO dto) {
@@ -354,6 +354,10 @@ public class ZtUserServiceImpl extends ServiceImpl<ZtUserMapper, ZtUser> impleme
} }
ZtUser ztUser = new ZtUser(); ZtUser ztUser = new ZtUser();
BeanUtils.copyProperties(user,ztUser); BeanUtils.copyProperties(user,ztUser);
//开发方向(CHG-038):仅 KFZ 保留,其余属性置空
if (ztUser.getUserType() != com.sa.zentao.enums.UserType.KFZ) {
ztUser.setDevDirection(null);
}
List<String> productList = user.getProductList(); List<String> productList = user.getProductList();
if(!CollectionUtils.isEmpty(productList)){ if(!CollectionUtils.isEmpty(productList)){
ztUser.setProductIds(StringUtils.join(productList,",")); ztUser.setProductIds(StringUtils.join(productList,","));
@@ -401,6 +405,8 @@ public class ZtUserServiceImpl extends ServiceImpl<ZtUserMapper, ZtUser> impleme
ztUser.setPassword(user.getPassword()); ztUser.setPassword(user.getPassword());
ztUser.setUserType(user.getUserType()); ztUser.setUserType(user.getUserType());
//开发方向(CHG-038):非 KFZ 置空,KFZ 按表单值
ztUser.setDevDirection(user.getUserType() == com.sa.zentao.enums.UserType.KFZ ? user.getDevDirection() : null);
ztUser.setEmail(user.getEmail()); ztUser.setEmail(user.getEmail());
ztUser.setPhone(user.getPhone()); ztUser.setPhone(user.getPhone());
if(!CollectionUtils.isEmpty(user.getProductList())){ if(!CollectionUtils.isEmpty(user.getProductList())){
@@ -0,0 +1,74 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import com.sa.zentao.entity.ZtDocCheck;
import com.sa.zentao.entity.ZtStory;
import com.sa.zentao.service.IZtDocCheckService;
import com.sa.zentao.service.IZtStoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 文档齐备扣分基类(FR-014 消费 zt_doc_check 快照):
* score = 100 − 缺失数×每份扣分,扣完截止;复核通过(appealResult=pass)的缺失不计。
*
* @author ai
* @since 2026-07-28
*/
public abstract class AbstractDocReadyCalculator extends AbstractPerfIndicatorCalculator {
@Autowired
protected IZtDocCheckService docCheckService;
@Autowired
protected IZtStoryService storyService;
/**
* 纳入统计的文档类型
*/
protected abstract Set<String> docTypes();
/**
* 每缺失一份扣分(缺省值,rule_json.deductPerStep 可覆盖)
*/
protected abstract int defaultDeductPerMissing();
/**
* 该核查记录是否归属此账号
*/
protected abstract boolean ownRow(ZtDocCheck row, Map<Integer, ZtStory> storyMap, String account);
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
List<ZtDocCheck> rows = docCheckService.listMonthRows(month);
if (CollectionUtils.isEmpty(rows)) {
rawDetail("当月无核查记录");
return FULL_SCORE;
}
Set<String> types = docTypes();
List<ZtDocCheck> missing = rows.stream()
.filter(o -> types.contains(o.getDocType()))
.filter(ZtDocCheck::countsAsMissing)
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(missing)) {
rawDetail("缺失 0 份");
return FULL_SCORE;
}
Set<Integer> storyIds = missing.stream().map(ZtDocCheck::getStoryId).collect(Collectors.toSet());
Map<Integer, ZtStory> storyMap = storyService.listByIds(storyIds).stream()
.collect(Collectors.toMap(ZtStory::getId, o -> o, (a, b) -> a));
long mine = missing.stream().filter(o -> ownRow(o, storyMap, account)).count();
if (rule == null) {
// 无配置:返回原始值(缺失份数)
return BigDecimal.valueOf(mine);
}
int perDeduct = rule.getIntValue("deductPerStep") == 0 ? defaultDeductPerMissing() : rule.getIntValue("deductPerStep");
rawDetail("缺失 " + mine + " 份 × 扣 " + perDeduct + " 分");
return clampScore(FULL_SCORE.subtract(BigDecimal.valueOf(mine * perDeduct)));
}
}
@@ -0,0 +1,41 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import java.math.BigDecimal;
/**
* 运维周频次指标基类(监控/巡检/备份):
* 应做次数 = 当月周数 × 每周次数(timesPerWeek) × 项目数(projects,备份用);
* 缺失 = max(0, 应做 − 实做);score = 100 − 缺失×deductPerMissing,扣完截止。
*
* @author ai
* @since 2026-07-28
*/
public abstract class AbstractOpsCountCalculator extends AbstractPerfIndicatorCalculator {
/**
* 实做次数(zt_yw* 记录数,当月 + 该账号)
*/
protected abstract long actualCount(String account, String month);
protected abstract int defaultTimesPerWeek();
protected abstract int defaultDeductPerMissing();
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
long actual = actualCount(account, month);
if (rule == null) {
// 无配置:返回原始值(实做次数)
return BigDecimal.valueOf(actual);
}
int timesPerWeek = rule.getIntValue("timesPerWeek") == 0 ? defaultTimesPerWeek() : rule.getIntValue("timesPerWeek");
int projects = rule.getIntValue("projects") == 0 ? 1 : rule.getIntValue("projects");
int deductPerMissing = rule.getIntValue("deductPerMissing") == 0 ? defaultDeductPerMissing() : rule.getIntValue("deductPerMissing");
long expected = (long) weeksOfMonth(month) * timesPerWeek * projects;
long missing = Math.max(0, expected - actual);
rawDetail("实做 " + actual + " / 应做 " + expected + "(缺 " + missing + " 次)");
return clampScore(FULL_SCORE.subtract(scaleDeduct(rule, BigDecimal.valueOf(missing * deductPerMissing))));
}
}
@@ -0,0 +1,298 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.sa.zentao.entity.ZtPerfConfig;
import com.sa.zentao.service.IZtPerfConfigService;
import com.sa.zentao.utils.DateUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
* 绩效指标计算器基类:配置加载 + 规则引擎(linear/threshold/manual 三类)。
* rule_json 说明见 sql/20260728_init_zt_perf_config.sql 说明头。
*
* @author ai
* @since 2026-07-28
*/
public abstract class AbstractPerfIndicatorCalculator implements PerfIndicatorCalculator {
protected static final BigDecimal FULL_SCORE = BigDecimal.valueOf(100);
/**
* CHG-041 指标过程值(分子/分母/率,如 "Σ指数314.03 / 达标105h = 299.0%"),
* 供弹窗「绩效数据」列展示;ThreadLocal 保证单例计算器并发安全,compute 消费后清除。
*/
private static final ThreadLocal<String> RAW_DETAIL = new ThreadLocal<>();
/**
* CHG-053 当前选中的产品集(program id,月报下拉):myWorkScore 入口设置,
* 各项目口径指标按该产品集过滤;null = 按本人项目(旧行为)。
*/
private static final ThreadLocal<Integer> PROGRAM = new ThreadLocal<>();
public static void setCurrentProgram(Integer program) {
if (program == null) {
PROGRAM.remove();
} else {
PROGRAM.set(program);
}
}
public static void clearCurrentProgram() {
PROGRAM.remove();
}
protected Integer currentProgram() {
return PROGRAM.get();
}
@Autowired
protected IZtPerfConfigService perfConfigService;
/**
* 子类在 doCompute 内记录过程值
*/
protected void rawDetail(String detail) {
RAW_DETAIL.set(detail);
}
@Override
public String consumeRawDetail() {
String detail = RAW_DETAIL.get();
RAW_DETAIL.remove();
return detail;
}
@Override
public BigDecimal compute(String account, String month) {
RAW_DETAIL.remove();
String role = perfConfigService.resolveRole(account);
ZtPerfConfig config = perfConfigService.getEnabledConfig(role, key());
JSONObject rule = parseRule(config);
//CHG-051 扣分尺度统一为加权尺度(用户拍板"10分满分 10-2"):
//把配置权重放进 rule,扣分按 1/权重 放大到 100 分制再扣(最终 ×权重 = xlsx 字面加权扣分)
if (rule != null && config != null && config.getWeight() != null
&& config.getWeight().compareTo(BigDecimal.ZERO) > 0) {
rule.put("__w", config.getWeight());
}
return doCompute(account, month, rule);
}
/**
* CHG-051 加权尺度扣分换算:xlsx 的"扣 X 分"是权重分值尺度(如 10 分项缺 1 份扣 2),
* 100 分制下需按 1/权重 放大;无权重配置时按原 100 分制扣分(兼容旧行为)。
*/
protected BigDecimal scaleDeduct(JSONObject rule, BigDecimal deduct) {
if (rule == null || deduct == null) {
return deduct;
}
Object w = rule.get("__w");
if (w instanceof BigDecimal && ((BigDecimal) w).compareTo(BigDecimal.ZERO) > 0) {
return deduct.divide((BigDecimal) w, 2, RoundingMode.HALF_UP);
}
return deduct;
}
/**
* 子类实现:先算原始值,再按 rule 套分;rule 为空时返回原始值
*/
protected abstract BigDecimal doCompute(String account, String month, JSONObject rule);
protected JSONObject parseRule(ZtPerfConfig config) {
if (config == null || StringUtils.isEmpty(config.getRuleJson())) {
return null;
}
try {
return JSON.parseObject(config.getRuleJson());
} catch (Exception e) {
return null;
}
}
// ==================== 规则引擎 ====================
/**
* linear:每减/增 X% 扣 Y 分,扣完截止;支持 segments 分段线性。
* 单段参数:direction=below/above、target、step、deductPerStep;
* 分段参数:segments=[{"ge":95,"from":100,"deductPer":1},...](ge 为段下界含,from 为段上界不含,扣分按 from-max(actual,ge) 计)
*
* @param actual 实际值(百分比数值,如 97 表示 97%)
* @return 得分 0~100
*/
protected BigDecimal applyLinear(JSONObject rule, BigDecimal actual) {
if (rule == null || actual == null) {
return actual == null ? BigDecimal.ZERO : actual;
}
JSONArray segments = rule.getJSONArray("segments");
BigDecimal deduct;
if (segments != null && !segments.isEmpty()) {
deduct = BigDecimal.ZERO;
for (int i = 0; i < segments.size(); i++) {
JSONObject seg = segments.getJSONObject(i);
BigDecimal ge = seg.getBigDecimal("ge");
BigDecimal from = seg.getBigDecimal("from");
BigDecimal deductPer = seg.getBigDecimal("deductPer");
if (ge == null || from == null || deductPer == null) {
continue;
}
if (actual.compareTo(from) < 0) {
BigDecimal covered = from.subtract(actual.max(ge));
if (covered.compareTo(BigDecimal.ZERO) > 0) {
deduct = deduct.add(covered.multiply(deductPer));
}
}
}
} else {
BigDecimal target = rule.getBigDecimal("target");
BigDecimal step = rule.getBigDecimal("step");
BigDecimal deductPerStep = rule.getBigDecimal("deductPerStep");
if (target == null || step == null || deductPerStep == null || step.compareTo(BigDecimal.ZERO) == 0) {
return actual;
}
String direction = rule.getString("direction");
BigDecimal diff;
if ("above".equals(direction)) {
diff = actual.subtract(target);
} else {
diff = target.subtract(actual);
}
if (diff.compareTo(BigDecimal.ZERO) <= 0) {
return FULL_SCORE;
}
BigDecimal steps = diff.divide(step, 0, RoundingMode.UP);
deduct = steps.multiply(deductPerStep);
}
return clampScore(FULL_SCORE.subtract(scaleDeduct(rule, deduct)));
}
/**
* threshold:阈值分档,bands=[{"ge":100,"score":100},{"ge":90,"score":40},{"ge":0,"score":0}],取首个 actual>=ge 档
*/
protected BigDecimal applyThreshold(JSONObject rule, BigDecimal actual) {
if (rule == null || actual == null) {
return actual == null ? BigDecimal.ZERO : actual;
}
JSONArray bands = rule.getJSONArray("bands");
if (bands == null || bands.isEmpty()) {
return actual;
}
for (int i = 0; i < bands.size(); i++) {
JSONObject band = bands.getJSONObject(i);
BigDecimal ge = band.getBigDecimal("ge");
BigDecimal score = band.getBigDecimal("score");
if (ge != null && score != null && actual.compareTo(ge) >= 0) {
return clampScore(score);
}
}
return BigDecimal.ZERO;
}
/**
* 按规则类型套分:linear/threshold;manual 或其他 → 返回原始值
*/
protected BigDecimal applyRule(JSONObject rule, BigDecimal actual) {
if (rule == null) {
return actual;
}
String type = rule.getString("type");
if ("linear".equals(type)) {
return applyLinear(rule, actual);
}
if ("threshold".equals(type)) {
return applyThreshold(rule, actual);
}
return actual;
}
protected BigDecimal clampScore(BigDecimal score) {
if (score == null) {
return BigDecimal.ZERO;
}
if (score.compareTo(BigDecimal.ZERO) < 0) {
return BigDecimal.ZERO;
}
if (score.compareTo(FULL_SCORE) > 0) {
return FULL_SCORE;
}
return score;
}
// ==================== 取数辅助 ====================
/**
* yyyy-MM → 当月第一天
*/
protected Date monthToDate(String month) {
if (StringUtils.isEmpty(month)) {
throw new IllegalArgumentException("月份不能为空");
}
String[] parts = month.split("-");
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]) - 1, 1);
return calendar.getTime();
}
protected Date firstDayOfMonth(String month) {
return DateUtils.getFirstDayOfMonth(monthToDate(month));
}
protected Date lastDayOfMonth(String month) {
return DateUtils.getLastDayOfMonth(monthToDate(month));
}
/**
* 当月周数(按自然天数/7 向上取整,运维周频次指标基准)
*/
protected int weeksOfMonth(String month) {
Date d = monthToDate(month);
Calendar calendar = Calendar.getInstance();
calendar.setTime(d);
int days = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
return (int) Math.ceil(days / 7.0);
}
/**
* Float 集合求和(null 跳过)
*/
protected BigDecimal sumFloat(List<Float> values) {
BigDecimal sum = BigDecimal.ZERO;
if (values == null) {
return sum;
}
for (Float v : values) {
if (v != null) {
sum = sum.add(BigDecimal.valueOf(v));
}
}
return sum;
}
/**
* 百分比:分子/分母×100,两位小数;分母为 0 返回 ZERO
*/
protected BigDecimal percent(BigDecimal numerator, BigDecimal denominator) {
if (denominator == null || denominator.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return numerator.multiply(FULL_SCORE).divide(denominator, 2, RoundingMode.HALF_UP);
}
/**
* 千分比:分子/分母×1000,两位小数;分母为 0 返回 ZERO
*/
protected BigDecimal permille(BigDecimal numerator, BigDecimal denominator) {
if (denominator == null || denominator.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
return numerator.multiply(BigDecimal.valueOf(1000)).divide(denominator, 2, RoundingMode.HALF_UP);
}
}
@@ -0,0 +1,55 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sa.zentao.entity.ZtTask;
import com.sa.zentao.service.IZtTaskService;
import com.sa.zentao.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
/**
* 任务及时完成率基类:Σ(按时完成任务的分配工时) ÷ Σ(当月完成任务分配工时)。
* 按时 = finishedDate ≤ deadline(当日末);typeFilter 区分全部任务(开发/UI)与测试任务(type=test)。
*
* @author ai
* @since 2026-07-28
*/
public abstract class AbstractTaskOnTimeRateCalculator extends AbstractPerfIndicatorCalculator {
@Autowired
protected IZtTaskService taskService;
/**
* 任务类型过滤,null = 全部类型
*/
protected abstract String typeFilter();
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
LambdaQueryWrapper<ZtTask> wrapper = new QueryWrapper<ZtTask>().lambda()
.eq(ZtTask::getAssignedTo, account)
.eq(ZtTask::getStatus, "done")
.ge(ZtTask::getFinishedDate, firstDayOfMonth(month))
.le(ZtTask::getFinishedDate, lastDayOfMonth(month));
if (typeFilter() != null) {
wrapper.eq(ZtTask::getType, typeFilter());
}
List<ZtTask> tasks = taskService.list(wrapper);
if (CollectionUtils.isEmpty(tasks)) {
return BigDecimal.ZERO;
}
BigDecimal totalEstimate = sumFloat(tasks.stream().map(ZtTask::getEstimate).collect(Collectors.toList()));
BigDecimal onTimeEstimate = sumFloat(tasks.stream()
.filter(o -> o.getDeadline() != null && o.getFinishedDate() != null
&& o.getFinishedDate().getTime() <= DateUtils.getDayLast(o.getDeadline()).getTime())
.map(ZtTask::getEstimate).collect(Collectors.toList()));
BigDecimal rate = percent(onTimeEstimate, totalEstimate);
return applyRule(rule, rate);
}
}
@@ -0,0 +1,120 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtBug;
import com.sa.zentao.entity.ZtStory;
import com.sa.zentao.entity.ZtTask;
import com.sa.zentao.service.IZtBugService;
import com.sa.zentao.service.IZtTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 线上 Bug 加权扣分基类(线上Bug率/产品缺陷率,同口径)。
* 已锁口径:线上 Bug = zt_bug.bug_type=prod;severity 1 重大、2/3/4 普通(以老弹窗 getBugFindScore 为准);率按 ‰ 计。
* rule mode=perBug:score = 100 − (普通×normalDeduct + 重大×majorDeduct),扣完截止。
* rule 含 exemptPerMille(PM/产品系 4 条规则):先按 PRD §7.3 算
* Σ(当月上线需求 Bug 数) ÷ Σ(上线需求开发任务工时) ‰,≤ exemptPerMille 满分豁免,> 才按 perBug 扣分。
* CHG-050 ①分母由 zt_story.estimate 改 devel 任务 estimate 合计(用户拍板"需求工时是任务sum",estimate 全线未填);
* ②四角色(项目经理/王宇航/产品经理/产品助理)改项目口径(∩当月窗口内执行关联产品的上线需求)。
*
* @author ai
* @since 2026-07-28
*/
public abstract class AbstractWeightedBugCalculator extends AbstractWorkloadRateCalculator {
@Autowired
protected IZtBugService bugService;
@Autowired
protected IZtTaskService taskService;
@Override
protected Set<Integer> scopeStoryIds(String account, String month) {
// 不走完成率管道,钩子占位
return null;
}
@Override
protected int producerCount(String account, String month) {
return 1;
}
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
if (rule != null && rule.getBigDecimal("exemptPerMille") != null) {
return computeWithExempt(account, month, rule);
}
List<ZtBug> bugs = bugService.list(new QueryWrapper<ZtBug>().lambda()
.eq(ZtBug::getBugType, "prod")
.eq(ZtBug::getAssignedTo, account)
.ge(ZtBug::getOpeneddate, firstDayOfMonth(month))
.le(ZtBug::getOpeneddate, lastDayOfMonth(month)));
rawDetail("Bug " + (countMajor(bugs) + countNormal(bugs)) + " 个(重大 " + countMajor(bugs) + "/普通 " + countNormal(bugs) + "),按个扣分");
return perBugScore(bugs, rule);
}
/**
* ≤5‰ 豁免口径(PRD §7.3):分子=当月上线需求(releaseddate 当月,四角色时∩项目关联产品)的 prod Bug 数,
* 分母=当月上线需求的 devel 任务 estimate 合计(CHG-050);率 ≤ exemptPerMille 得满分,否则按个扣分。
*/
private BigDecimal computeWithExempt(String account, String month, JSONObject rule) {
List<ZtStory> stories = storyService.list(new QueryWrapper<ZtStory>().lambda()
.ge(ZtStory::getReleaseddate, firstDayOfMonth(month))
.le(ZtStory::getReleaseddate, lastDayOfMonth(month)));
boolean projectScoped = isProjectScopedRole(account);
if (projectScoped) {
Set<Integer> projectStoryIds = projectScopeStoryIds(account, month);
stories = stories.stream().filter(o -> projectStoryIds.contains(o.getId())).collect(Collectors.toList());
}
if (CollectionUtils.isEmpty(stories)) {
rawDetail("当月无上线需求" + (projectScoped ? "(项目范围)" : ""));
return FULL_SCORE;
}
List<Integer> storyIds = stories.stream().map(ZtStory::getId).collect(Collectors.toList());
BigDecimal totalEstimate = sumFloat(taskService.list(new QueryWrapper<ZtTask>().lambda()
.in(ZtTask::getStory, storyIds)
.eq(ZtTask::getType, "devel")
.eq(ZtTask::getDeleted, "0")
.notIn(ZtTask::getStatus, "cancel", "closed"))
.stream().map(ZtTask::getEstimate).collect(Collectors.toList()));
//CHG-067:zt_bug.story 为死字段(全库 prod/dev Bug 均 story=0),需求关联实际走 toStory(用户拍板"启用的是toStory")
List<ZtBug> bugs = bugService.list(new QueryWrapper<ZtBug>().lambda()
.eq(ZtBug::getBugType, "prod")
.in(ZtBug::getTostory, storyIds));
BigDecimal rate = permille(BigDecimal.valueOf(countMajor(bugs) + countNormal(bugs)), totalEstimate);
rawDetail("Bug " + (countMajor(bugs) + countNormal(bugs)) + " 个(重大 " + countMajor(bugs) + "/普通 " + countNormal(bugs) + ")/ Σ工时 " + totalEstimate.stripTrailingZeros().toPlainString() + "h = " + rate.stripTrailingZeros().toPlainString() + "‰(豁免线 " + rule.getBigDecimal("exemptPerMille") + "‰)");
if (rate.compareTo(rule.getBigDecimal("exemptPerMille")) <= 0) {
return FULL_SCORE;
}
return perBugScore(bugs, rule);
}
private BigDecimal perBugScore(List<ZtBug> bugs, JSONObject rule) {
long major = countMajor(bugs);
long normal = countNormal(bugs);
if (rule == null) {
// 无配置:返回原始值(Bug 总数)
return BigDecimal.valueOf(major + normal);
}
int normalDeduct = rule.getIntValue("normalDeduct") == 0 ? 3 : rule.getIntValue("normalDeduct");
int majorDeduct = rule.getIntValue("majorDeduct") == 0 ? 10 : rule.getIntValue("majorDeduct");
BigDecimal deduct = BigDecimal.valueOf(normal * normalDeduct + major * majorDeduct);
return clampScore(FULL_SCORE.subtract(scaleDeduct(rule, deduct)));
}
private long countMajor(List<ZtBug> bugs) {
return bugs.stream().filter(o -> o.getSeverity() != null && o.getSeverity() == 1).count();
}
private long countNormal(List<ZtBug> bugs) {
return bugs.stream().filter(o -> o.getSeverity() != null
&& (o.getSeverity() == 2 || o.getSeverity() == 3 || o.getSeverity() == 4)).count();
}
}
@@ -0,0 +1,249 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtStoryMonthWorkload;
import com.sa.zentao.entity.ZtUser;
import com.sa.zentao.enums.UserType;
import com.sa.zentao.service.IZtStoryMonthWorkloadService;
import com.sa.zentao.service.IZtUserService;
import com.sa.zentao.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 工作量指标完成率基类(FR-012)。
* 已锁口径:完成率 = Σ(zt_story_month_workload 当月 workload 增量) ÷ (可用工作天数×5×产出人数),
* workload 增量即 zt_story_month_workload.workload_index 列(写入时已按 指数×完成度增量/100 计算),
* 测试人员不计入产出方。
*
* @author ai
* @since 2026-07-28
*/
public abstract class AbstractWorkloadRateCalculator extends AbstractPerfIndicatorCalculator {
@Autowired
protected IZtStoryMonthWorkloadService storyMonthWorkloadService;
@Autowired
protected IZtUserService userService;
@Autowired
protected com.sa.zentao.service.IZtTeamService teamService;
@Autowired
protected com.sa.zentao.service.IZtProjectService projectService;
@Autowired
protected com.sa.zentao.service.IZtExecutionprojectService executionprojectService;
@Autowired
protected com.sa.zentao.service.IZtProjectproductService projectproductService;
@Autowired
protected com.sa.zentao.service.IZtProductService productService;
@Autowired
protected com.sa.zentao.service.IZtStoryService storyService;
/**
* 需求范围过滤;返回 null 表示不限制(团队口径)
*/
protected abstract Set<Integer> scopeStoryIds(String account, String month);
/**
* 产出方人数(分母 = 工作天数×5×人数;个人口径返回 1)
*/
protected abstract int producerCount(String account, String month);
/**
* 是否项目经理(含王宇航变体 projectManagerWyh)
*/
protected boolean isProjectManager(String account) {
String role = perfConfigService.resolveRole(account);
return role != null && role.startsWith("projectManager");
}
/**
* CHG-048 项目口径四角色:项目经理(含王宇航变体)、产品经理、产品助理
*/
protected boolean isProjectScopedRole(String account) {
String role = perfConfigService.resolveRole(account);
return "projectManager".equals(role) || "projectManagerWyh".equals(role)
|| "productManager".equals(role) || "productAssistant".equals(role);
}
/**
* CHG-043 项目经理项目口径:他当月窗口内(begin 或 end 落当月)参与的执行列表
*/
protected List<com.sa.zentao.entity.ZtProject> projectExecs(String account, String month) {
List<com.sa.zentao.entity.ZtTeam> teams = teamService.list(new QueryWrapper<com.sa.zentao.entity.ZtTeam>().lambda()
.eq(com.sa.zentao.entity.ZtTeam::getType, "execution")
.eq(com.sa.zentao.entity.ZtTeam::getAccount, account));
if (teams.isEmpty()) {
return Collections.emptyList();
}
Date first = firstDayOfMonth(month);
Date last = lastDayOfMonth(month);
return projectService.list(new QueryWrapper<com.sa.zentao.entity.ZtProject>().lambda()
.in(com.sa.zentao.entity.ZtProject::getId, teams.stream().map(com.sa.zentao.entity.ZtTeam::getRoot).collect(Collectors.toList())))
.stream().filter(o -> (o.getBegin() != null && !o.getBegin().before(first) && !o.getBegin().after(last))
|| (o.getEnd() != null && !o.getEnd().before(first) && !o.getEnd().after(last)))
.collect(Collectors.toList());
}
/**
* CHG-043 项目口径需求范围:他当月执行 → 关联产品的需求 id 集(无执行/无产品 → 空集,分子为 0)。
* CHG-053 优先按当前选中产品集(program)圈产品;未选中退回本人执行路径。
*/
protected Set<Integer> projectScopeStoryIds(String account, String month) {
Set<Integer> productIds = currentProgramProductIds();
if (productIds == null) {
List<com.sa.zentao.entity.ZtProject> execs = projectExecs(account, month);
if (execs.isEmpty()) {
return Collections.emptySet();
}
List<com.sa.zentao.entity.ZtExecutionproject> eps = executionprojectService.list(new QueryWrapper<com.sa.zentao.entity.ZtExecutionproject>().lambda()
.in(com.sa.zentao.entity.ZtExecutionproject::getExecution, execs.stream().map(com.sa.zentao.entity.ZtProject::getId).collect(Collectors.toList())));
if (eps.isEmpty()) {
return Collections.emptySet();
}
List<com.sa.zentao.entity.ZtProjectproduct> pps = projectproductService.list(new QueryWrapper<com.sa.zentao.entity.ZtProjectproduct>().lambda()
.in(com.sa.zentao.entity.ZtProjectproduct::getProject, eps.stream().map(com.sa.zentao.entity.ZtExecutionproject::getProject).collect(Collectors.toList())));
productIds = pps.stream().map(com.sa.zentao.entity.ZtProjectproduct::getProduct).collect(Collectors.toSet());
}
if (productIds.isEmpty()) {
return Collections.emptySet();
}
return storyService.list(new QueryWrapper<com.sa.zentao.entity.ZtStory>().lambda()
.in(com.sa.zentao.entity.ZtStory::getProduct, productIds)).stream()
.map(com.sa.zentao.entity.ZtStory::getId).collect(Collectors.toSet());
}
/**
* CHG-043 项目口径产出人数:他当月窗口内执行的 KFZ 成员数(测试不计;他本人为 XMGLY 非 KFZ,天然不含)。
* CHG-053 优先按选中产品集下在窗执行的成员数;未选中退回本人执行路径。
*/
protected int projectProducerCount(String account, String month) {
List<com.sa.zentao.entity.ZtProject> execs;
Set<Integer> productIds = currentProgramProductIds();
if (productIds != null) {
if (productIds.isEmpty()) {
return 0;
}
execs = execsOfProducts(productIds, month);
} else {
execs = projectExecs(account, month);
}
if (execs.isEmpty()) {
return 0;
}
List<com.sa.zentao.entity.ZtTeam> members = teamService.list(new QueryWrapper<com.sa.zentao.entity.ZtTeam>().lambda()
.eq(com.sa.zentao.entity.ZtTeam::getType, "execution")
.in(com.sa.zentao.entity.ZtTeam::getRoot, execs.stream().map(com.sa.zentao.entity.ZtProject::getId).collect(Collectors.toList())));
List<String> accounts = members.stream().map(com.sa.zentao.entity.ZtTeam::getAccount).distinct().collect(Collectors.toList());
if (accounts.isEmpty()) {
return 0;
}
return (int) userService.count(new QueryWrapper<ZtUser>().lambda()
.eq(ZtUser::getUserType, UserType.KFZ)
.in(ZtUser::getAccount, accounts));
}
/**
* CHG-053 当前选中产品集(program id)→ 产品 id 集;未选中(null)返回 null
*/
protected Set<Integer> currentProgramProductIds() {
Integer program = currentProgram();
if (program == null) {
return null;
}
return productService.list(new QueryWrapper<com.sa.zentao.entity.ZtProduct>().lambda()
.eq(com.sa.zentao.entity.ZtProduct::getProgram, program)).stream()
.map(com.sa.zentao.entity.ZtProduct::getId).collect(Collectors.toSet());
}
/**
* CHG-053 产品 id 集 → 当月窗口内(begin 或 end 落当月)的执行列表
*/
protected List<com.sa.zentao.entity.ZtProject> execsOfProducts(Set<Integer> productIds, String month) {
List<com.sa.zentao.entity.ZtProjectproduct> pps = projectproductService.list(new QueryWrapper<com.sa.zentao.entity.ZtProjectproduct>().lambda()
.in(com.sa.zentao.entity.ZtProjectproduct::getProduct, productIds));
if (pps.isEmpty()) {
return Collections.emptyList();
}
List<com.sa.zentao.entity.ZtExecutionproject> eps = executionprojectService.list(new QueryWrapper<com.sa.zentao.entity.ZtExecutionproject>().lambda()
.in(com.sa.zentao.entity.ZtExecutionproject::getProject, pps.stream().map(com.sa.zentao.entity.ZtProjectproduct::getProject).collect(Collectors.toList())));
if (eps.isEmpty()) {
return Collections.emptyList();
}
Date first = firstDayOfMonth(month);
Date last = lastDayOfMonth(month);
return projectService.list(new QueryWrapper<com.sa.zentao.entity.ZtProject>().lambda()
.in(com.sa.zentao.entity.ZtProject::getId, eps.stream().map(com.sa.zentao.entity.ZtExecutionproject::getExecution).collect(Collectors.toList())))
.stream().filter(o -> (o.getBegin() != null && !o.getBegin().before(first) && !o.getBegin().after(last))
|| (o.getEnd() != null && !o.getEnd().before(first) && !o.getEnd().after(last)))
.collect(Collectors.toList());
}
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
List<ZtStoryMonthWorkload> records = storyMonthWorkloadService.list(
new QueryWrapper<ZtStoryMonthWorkload>().lambda().eq(ZtStoryMonthWorkload::getTime, month));
Set<Integer> scope = scopeStoryIds(account, month);
BigDecimal total = BigDecimal.ZERO;
if (!CollectionUtils.isEmpty(records)) {
for (ZtStoryMonthWorkload record : records) {
if (scope != null && !scope.contains(record.getStoryId())) {
continue;
}
if (record.getWorkloadIndex() != null) {
total = total.add(record.getWorkloadIndex());
}
}
}
int workDays = DateUtils.getWorkDaysInCurrentMonth(monthToDate(month));
int producers = producerCount(account, month);
// 写死团队月达标总工时(如飞侠车服=525):选中产品集内有配置产品时,替换 工作天数×5×人数 公式算出的基数,其余逻辑不变
BigDecimal denominator;
BigDecimal fixedTeamTime = fixedTeamMonthTimeByProductIds(currentProgramProductIds());
if (fixedTeamTime != null) {
denominator = fixedTeamTime;
} else {
denominator = BigDecimal.valueOf((long) workDays * 5L * producers);
}
BigDecimal rate = percent(total, denominator);
rawDetail("Σ指数 " + total.stripTrailingZeros().toPlainString() + " / 达标 " + denominator.stripTrailingZeros().toPlainString() + "h = " + rate.stripTrailingZeros().toPlainString() + "%");
return applyRule(rule, rate);
}
/**
* 选中产品集内写死的团队月达标总工时合计(如 飞侠车服=525);未选中产品集或无配置返回 null
*/
private BigDecimal fixedTeamMonthTimeByProductIds(Set<Integer> productIds) {
if (CollectionUtils.isEmpty(productIds) || FixedTeamMonthTime.MAP.isEmpty()) {
return null;
}
List<com.sa.zentao.entity.ZtProduct> products = productService.listByIds(productIds);
BigDecimal total = BigDecimal.ZERO;
for (com.sa.zentao.entity.ZtProduct p : products) {
BigDecimal fixed = FixedTeamMonthTime.MAP.get(p.getName());
if (fixed != null) {
total = total.add(fixed);
}
}
return total.compareTo(BigDecimal.ZERO) > 0 ? total : null;
}
/**
* 开发产出人数(userType=KFZ,测试 CS 不计入产出方)
*/
protected int devProducerCount() {
long count = userService.count(new QueryWrapper<ZtUser>().lambda().eq(ZtUser::getUserType, UserType.KFZ));
return (int) count;
}
protected Set<Integer> toStoryIds(List<com.sa.zentao.entity.ZtStoryExpand> expands) {
return expands.stream().map(com.sa.zentao.entity.ZtStoryExpand::getStoryId).collect(Collectors.toSet());
}
}
@@ -0,0 +1,58 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtBug;
import com.sa.zentao.entity.ZtTask;
import com.sa.zentao.service.IZtBugService;
import com.sa.zentao.service.IZtTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
/**
* Bug密度(后端/前端 0.3):Σ(当月完成任务Bug数) ÷ Σ(完成任务分配工时),≤15% 满分、每增1%扣3分。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class BugDensityCalculator extends AbstractPerfIndicatorCalculator {
@Autowired
private IZtTaskService taskService;
@Autowired
private IZtBugService bugService;
@Override
public String key() {
return "bugDensity";
}
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
List<ZtTask> tasks = taskService.list(new QueryWrapper<ZtTask>().lambda()
.eq(ZtTask::getAssignedTo, account)
.eq(ZtTask::getStatus, "done")
.ge(ZtTask::getFinishedDate, firstDayOfMonth(month))
.le(ZtTask::getFinishedDate, lastDayOfMonth(month)));
if (CollectionUtils.isEmpty(tasks)) {
return BigDecimal.ZERO;
}
List<Integer> taskIds = tasks.stream().map(ZtTask::getId).collect(Collectors.toList());
long bugCount = bugService.count(new QueryWrapper<ZtBug>().lambda().in(ZtBug::getTask, taskIds));
BigDecimal estimateSum = sumFloat(tasks.stream().map(ZtTask::getEstimate).collect(Collectors.toList()));
BigDecimal density;
if (estimateSum.compareTo(BigDecimal.ZERO) == 0) {
// 除零边界:有 Bug 视为 100%,无 Bug 为 0
density = bugCount > 0 ? FULL_SCORE : BigDecimal.ZERO;
} else {
density = percent(BigDecimal.valueOf(bugCount), estimateSum);
}
return applyRule(rule, density);
}
}
@@ -0,0 +1,116 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtFile;
import com.sa.zentao.entity.ZtStory;
import com.sa.zentao.service.IZtFileService;
import com.sa.zentao.service.IZtStoryService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
/**
* 代码质量(后端/前端 0.1):解析 zt_file(objectType=aiCodeReview) MD 头部「严重:N/错误:N」计数扣分。
* rule mode=round(后端,默认):初审(round≤1) 严重1处扣3分、错误超6处扣3分;复审(round≥2) 严重1处扣5分、错误1处扣1分;
* rule mode=flat(前端):每发现1问题扣3分(severeDeduct/errorDeduct 可配)。
* 轮次取 zt_file.extra {"round":N},缺省按初审。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class CodeQualityCalculator extends AbstractPerfIndicatorCalculator {
@Autowired
private IZtStoryService storyService;
@Autowired
private IZtFileService fileService;
@Autowired
private MdContentResolver mdContentResolver;
@Override
public String key() {
return "codeQuality";
}
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
List<ZtStory> stories = storyService.list(new QueryWrapper<ZtStory>().lambda()
.eq(ZtStory::getAssignedTo, account));
if (CollectionUtils.isEmpty(stories)) {
// 无负责需求:无审查对象,不扣分
return FULL_SCORE;
}
List<ZtFile> files = fileService.list(new QueryWrapper<ZtFile>().lambda()
.eq(ZtFile::getObjecttype, "aiCodeReview")
.in(ZtFile::getObjectid, stories.stream().map(ZtStory::getId).collect(Collectors.toList()))
.and(w -> w.isNull(ZtFile::getDeleted).or().eq(ZtFile::getDeleted, "0"))
.ge(ZtFile::getAddeddate, firstDayOfMonth(month))
.le(ZtFile::getAddeddate, lastDayOfMonth(month)));
if (CollectionUtils.isEmpty(files)) {
return FULL_SCORE;
}
String mode = rule == null ? "round" : rule.getString("mode");
BigDecimal deduct = BigDecimal.ZERO;
int totalProblems = 0;
for (ZtFile file : files) {
String content = mdContentResolver.readContent(file);
int[] counts = MdContentResolver.parseReviewCounts(content);
totalProblems += counts[0] + counts[1];
if ("flat".equals(mode)) {
int severeDeduct = rule.getIntValue("severeDeduct") == 0 ? 3 : rule.getIntValue("severeDeduct");
int errorDeduct = rule.getIntValue("errorDeduct") == 0 ? 3 : rule.getIntValue("errorDeduct");
deduct = deduct.add(BigDecimal.valueOf((long) counts[0] * severeDeduct + (long) counts[1] * errorDeduct));
} else {
deduct = deduct.add(roundDeduct(rule, parseRound(file), counts));
}
}
if (rule == null) {
// 无配置:返回原始值(问题总数)
return BigDecimal.valueOf(totalProblems);
}
return clampScore(FULL_SCORE.subtract(deduct));
}
/**
* 后端轮次扣分:初审严重1处扣3、错误超6处扣3;复审严重1处扣5、错误1处扣1
*/
private BigDecimal roundDeduct(JSONObject rule, int round, int[] counts) {
if (round >= 2) {
int severeDeduct = rule.getIntValue("reRoundSevereDeduct") == 0 ? 5 : rule.getIntValue("reRoundSevereDeduct");
int errorDeduct = rule.getIntValue("reRoundErrorDeduct") == 0 ? 1 : rule.getIntValue("reRoundErrorDeduct");
return BigDecimal.valueOf((long) counts[0] * severeDeduct + (long) counts[1] * errorDeduct);
}
int severeDeduct = rule.getIntValue("firstRoundSevereDeduct") == 0 ? 3 : rule.getIntValue("firstRoundSevereDeduct");
int threshold = rule.getIntValue("firstRoundErrorThreshold") == 0 ? 6 : rule.getIntValue("firstRoundErrorThreshold");
int errorDeduct = rule.getIntValue("firstRoundErrorDeduct") == 0 ? 3 : rule.getIntValue("firstRoundErrorDeduct");
long deduct = (long) counts[0] * severeDeduct;
if (counts[1] > threshold) {
deduct += errorDeduct;
}
return BigDecimal.valueOf(deduct);
}
/**
* 审查轮次:zt_file.extra {"round":N},缺省 1(初审)
*/
private int parseRound(ZtFile file) {
if (StringUtils.isEmpty(file.getExtra())) {
return 1;
}
try {
JSONObject extra = JSON.parseObject(file.getExtra());
Integer round = extra.getInteger("round");
return round == null || round < 1 ? 1 : round;
} catch (Exception e) {
return 1;
}
}
}
@@ -0,0 +1,65 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtBug;
import com.sa.zentao.entity.ZtTask;
import com.sa.zentao.service.IZtBugService;
import com.sa.zentao.service.IZtTaskService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
/**
* 缺陷检出率(测试工程师 0.3):(普通Bug×1+重大Bug×5) ÷ 测试需求开发工时,>20% 满分、每减1%扣2分。
* 已锁口径:severity 1 重大、2/3/4 普通(以老弹窗 getBugFindScore 为准)。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class DefectFindRateCalculator extends AbstractPerfIndicatorCalculator {
@Autowired
private IZtBugService bugService;
@Autowired
private IZtTaskService taskService;
@Override
public String key() {
return "defectFindRate";
}
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
List<ZtBug> bugs = bugService.list(new QueryWrapper<ZtBug>().lambda()
.eq(ZtBug::getOpenedby, account)
.ge(ZtBug::getOpeneddate, firstDayOfMonth(month))
.le(ZtBug::getOpeneddate, lastDayOfMonth(month)));
// 与 IZtCountService.getBugFindScore 同口径:closed/cancel 且无解决人 不计
bugs = bugs.stream()
.filter(o -> ("closed".equals(o.getStatus()) || "cancel".equals(o.getStatus()))
? StringUtils.isNotEmpty(o.getResolvedby()) : true)
.collect(Collectors.toList());
long major = bugs.stream().filter(o -> o.getSeverity() != null && o.getSeverity() == 1).count();
long normal = bugs.stream().filter(o -> o.getSeverity() != null
&& (o.getSeverity() == 2 || o.getSeverity() == 3 || o.getSeverity() == 4)).count();
long weighted = major * 5 + normal;
List<Integer> storyIds = bugs.stream().map(ZtBug::getTostory).filter(o -> o != null).distinct().collect(Collectors.toList());
BigDecimal devHours = BigDecimal.ZERO;
if (!CollectionUtils.isEmpty(storyIds)) {
List<ZtTask> devTasks = taskService.list(new QueryWrapper<ZtTask>().lambda()
.eq(ZtTask::getType, "devel")
.notIn(ZtTask::getStatus, "cancel", "closed")
.in(ZtTask::getStory, storyIds));
devHours = sumFloat(devTasks.stream().map(ZtTask::getEstimate).collect(Collectors.toList()));
}
BigDecimal rate = percent(BigDecimal.valueOf(weighted), devHours);
return applyRule(rule, rate);
}
}
@@ -0,0 +1,123 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtStory;
import com.sa.zentao.entity.ZtStoryExpand;
import com.sa.zentao.entity.ZtStoryMonthWorkload;
import com.sa.zentao.service.IZtStoryExpandService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 文档齐备(大型需求五类,项目经理 0.1):每缺失一份扣2分,扣完截止。
* CHG-047 改实时判定(用户拍板):直接读 zt_story 五个 url 字段非空即在,
* 不再依赖 zt_doc_check 快照/月末 job;归属改项目口径(项目经理=他当月窗口内执行关联产品的大型需求)。
* 大型需求 = 当月有工作量指数记录且 AI 评估指数 >20(与原快照范围一致)。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class DocReadyScoreCalculator extends AbstractWorkloadRateCalculator {
@Autowired
private IZtStoryExpandService storyExpandService;
@Override
public String key() {
return "docReadyScore";
}
@Override
protected Set<Integer> scopeStoryIds(String account, String month) {
// 本计算器不走完成率管道(doCompute 整体重写),钩子占位
return null;
}
@Override
protected int producerCount(String account, String month) {
return 1;
}
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
// 大型需求:当月有指数记录且指数>20
List<ZtStoryMonthWorkload> records = storyMonthWorkloadService.list(
new QueryWrapper<ZtStoryMonthWorkload>().lambda().eq(ZtStoryMonthWorkload::getTime, month));
if (CollectionUtils.isEmpty(records)) {
rawDetail("当月无大型需求");
return FULL_SCORE;
}
Set<Integer> monthStoryIds = records.stream().map(ZtStoryMonthWorkload::getStoryId).collect(Collectors.toSet());
Set<Integer> largeIds = storyExpandService.list(new QueryWrapper<ZtStoryExpand>().lambda()
.in(ZtStoryExpand::getStoryId, monthStoryIds)).stream()
.filter(o -> parseIndex(o.getWorkloadIndex()).compareTo(BigDecimal.valueOf(20)) > 0)
.map(ZtStoryExpand::getStoryId).collect(Collectors.toSet());
if (largeIds.isEmpty()) {
rawDetail("当月无大型需求");
return FULL_SCORE;
}
// 项目经理归属:∩ 他当月窗口内执行关联产品的需求(CHG-043 项目口径)
Set<Integer> scopeIds = largeIds;
if (isProjectManager(account)) {
Set<Integer> projectStoryIds = projectScopeStoryIds(account, month);
scopeIds = largeIds.stream().filter(projectStoryIds::contains).collect(Collectors.toSet());
if (scopeIds.isEmpty()) {
rawDetail("当月无大型需求(项目范围)");
return FULL_SCORE;
}
}
Map<Integer, ZtStory> storyMap = storyService.listByIds(scopeIds).stream()
.collect(Collectors.toMap(ZtStory::getId, o -> o, (a, b) -> a));
// 五类 url 字段非空即在:用例/报告提交/更新记录/审查/日志
int missing = 0;
StringBuilder missDesc = new StringBuilder();
for (ZtStory s : storyMap.values()) {
List<String> lack = new ArrayList<>();
if (StringUtils.isBlank(s.getTestCaseUrl())) {
lack.add("用例");
}
if (StringUtils.isBlank(s.getTestReportSubmitUrl())) {
lack.add("报告");
}
if (StringUtils.isBlank(s.getAiDocUpdateUrl())) {
lack.add("更新记录");
}
if (StringUtils.isBlank(s.getCodeReviewUrl())) {
lack.add("审查");
}
if (StringUtils.isBlank(s.getWorkLogUrl())) {
lack.add("日志");
}
missing += lack.size();
if (!lack.isEmpty()) {
missDesc.append(s.getId()).append("缺[").append(String.join(",", lack)).append("] ");
}
}
int perDeduct = rule == null || rule.getIntValue("deductPerStep") == 0 ? 2 : rule.getIntValue("deductPerStep");
rawDetail("大型需求 " + scopeIds.size() + " 个,缺失 " + missing + " 份"
+ (missing > 0 ? "(" + missDesc.toString().trim() + ")" : ""));
return clampScore(FULL_SCORE.subtract(scaleDeduct(rule, BigDecimal.valueOf((long) missing * perDeduct))));
}
private BigDecimal parseIndex(String workloadIndex) {
if (workloadIndex == null || workloadIndex.trim().isEmpty()) {
return BigDecimal.ZERO;
}
try {
return new BigDecimal(workloadIndex.trim());
} catch (NumberFormatException e) {
return BigDecimal.ZERO;
}
}
}
@@ -0,0 +1,26 @@
package com.sa.zentao.service.perf;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 团队月度达标总工时写死配置:产品名 → 团队月达标总工时。
* 命中产品的团队,达标基数用写死值替换公式算出的值(工作天数×人数×5),各公式其余逻辑不变。
* 以后别的产品要写死,往 MAP 加一行。
*/
public final class FixedTeamMonthTime {
public static final Map<String, BigDecimal> MAP;
static {
Map<String, BigDecimal> m = new HashMap<>();
// 飞侠车服(外包团队合同包月工时)
m.put("飞侠车服", BigDecimal.valueOf(525));
MAP = Collections.unmodifiableMap(m);
}
private FixedTeamMonthTime() {
}
}
@@ -0,0 +1,125 @@
package com.sa.zentao.service.perf;
import com.sa.zentao.entity.ZtFile;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* MD 附件内容读取器(三期:代码审查报告头部「严重:N/错误:N」解析)。
* 取数顺序:url/pathname 为相对路径(/zentao/img/xxx.md)→ 读本地文件目录;
* 绝对 http 地址 → URL 拉取(与 CommonsController.downloadByIO 同通道)。
*
* @author ai
* @since 2026-07-28
*/
@Component
@Slf4j
public class MdContentResolver {
/** 严重计数(支持全/半角冒号) */
private static final Pattern SEVERE_PATTERN = Pattern.compile("严重\\s*[::]\\s*(\\d+)");
/** 错误计数 */
private static final Pattern ERROR_PATTERN = Pattern.compile("错误\\s*[::]\\s*(\\d+)");
/** 头部扫描行数上限 */
private static final int HEADER_LINES = 30;
@Value("${file.path.windows:}")
private String windowsFilePath;
@Value("${file.path.linux:}")
private String linuxFilePath;
/**
* 读取附件文本内容,失败返回 null(不抛异常,取数降级由调用方决定)
*/
public String readContent(ZtFile ztFile) {
if (ztFile == null) {
return null;
}
String path = StringUtils.isNotEmpty(ztFile.getUrl()) ? ztFile.getUrl() : ztFile.getPathname();
if (StringUtils.isEmpty(path)) {
return null;
}
try {
if (path.startsWith("http://") || path.startsWith("https://")) {
return readByUrl(path);
}
return readLocal(path);
} catch (Exception e) {
log.warn("读取附件内容失败 id={} path={} err={}", ztFile.getId(), path, e.getMessage());
return null;
}
}
private String readLocal(String path) throws Exception {
String fileName = path.substring(path.lastIndexOf("/") + 1);
String dir;
String os = System.getProperty("os.name");
if (os != null && os.toLowerCase().startsWith("win")) {
dir = windowsFilePath;
} else {
dir = linuxFilePath;
}
if (StringUtils.isEmpty(dir)) {
return null;
}
File file = new File(dir, fileName);
if (!file.exists()) {
return null;
}
byte[] bytes = Files.readAllBytes(file.toPath());
return new String(bytes, StandardCharsets.UTF_8);
}
private String readByUrl(String url) throws Exception {
try (InputStream is = new URL(url).openStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buff = new byte[8192];
int count;
while ((count = is.read(buff)) != -1) {
bos.write(buff, 0, count);
}
return new String(bos.toByteArray(), StandardCharsets.UTF_8);
}
}
/**
* 解析审查报告 MD 头部「严重:N/错误:N」计数(仅扫描前 {@value #HEADER_LINES} 行)。
*
* @param content MD 内容(可空)
* @return int[]{严重数, 错误数}
*/
public static int[] parseReviewCounts(String content) {
int severe = 0;
int error = 0;
if (StringUtils.isEmpty(content)) {
return new int[]{0, 0};
}
String[] lines = content.split("\\R");
StringBuilder header = new StringBuilder();
int limit = Math.min(lines.length, HEADER_LINES);
for (int i = 0; i < limit; i++) {
header.append(lines[i]).append('\n');
}
Matcher severeMatcher = SEVERE_PATTERN.matcher(header.toString());
if (severeMatcher.find()) {
severe = Integer.parseInt(severeMatcher.group(1));
}
Matcher errorMatcher = ERROR_PATTERN.matcher(header.toString());
if (errorMatcher.find()) {
error = Integer.parseInt(errorMatcher.group(1));
}
return new int[]{severe, error};
}
}
@@ -0,0 +1,18 @@
package com.sa.zentao.service.perf;
import org.springframework.stereotype.Component;
/**
* 线上Bug(项目经理 0.1:普通扣3/重大扣10;测试工程师 0.2:普通扣5/重大该项0分——规则参数在 zt_perf_config)。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class OnlineBugRateCalculator extends AbstractWeightedBugCalculator {
@Override
public String key() {
return "onlineBugRate";
}
}
@@ -0,0 +1,44 @@
package com.sa.zentao.service.perf;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtYwBackups;
import com.sa.zentao.service.IZtYwBackupsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 数据库备份(运维 0.1):每项目每周全量,缺一个扣3分;实做 = zt_yw_backups 当月记录数;
* 项目数经 rule_json.projects 配置(缺省 1)。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class OpsBackupCalculator extends AbstractOpsCountCalculator {
@Autowired
private IZtYwBackupsService backupsService;
@Override
public String key() {
return "opsBackup";
}
@Override
protected long actualCount(String account, String month) {
return backupsService.count(new QueryWrapper<ZtYwBackups>().lambda()
.eq(ZtYwBackups::getCreateUser, account)
.ge(ZtYwBackups::getBackupsDate, firstDayOfMonth(month))
.le(ZtYwBackups::getBackupsDate, lastDayOfMonth(month)));
}
@Override
protected int defaultTimesPerWeek() {
return 1;
}
@Override
protected int defaultDeductPerMissing() {
return 3;
}
}
@@ -0,0 +1,43 @@
package com.sa.zentao.service.perf;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtYwPatrol;
import com.sa.zentao.service.IZtYwPatrolService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 职场巡检(运维 0.1):每周1次,缺一次扣5分;实做 = zt_yw_patrol 当月记录数。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class OpsInspectCalculator extends AbstractOpsCountCalculator {
@Autowired
private IZtYwPatrolService patrolService;
@Override
public String key() {
return "opsInspect";
}
@Override
protected long actualCount(String account, String month) {
return patrolService.count(new QueryWrapper<ZtYwPatrol>().lambda()
.eq(ZtYwPatrol::getPatrolUser, account)
.ge(ZtYwPatrol::getPatrolDate, firstDayOfMonth(month))
.le(ZtYwPatrol::getPatrolDate, lastDayOfMonth(month)));
}
@Override
protected int defaultTimesPerWeek() {
return 1;
}
@Override
protected int defaultDeductPerMissing() {
return 5;
}
}
@@ -0,0 +1,63 @@
package com.sa.zentao.service.perf;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtUser;
import com.sa.zentao.entity.ZtYwTask;
import com.sa.zentao.service.IZtUserService;
import com.sa.zentao.service.IZtYwTaskService;
import com.sa.zentao.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;
/**
* 运维大项任务及时完成(运维 0.2):当月到期运维任务 及时完成率×20(折成百分制得分=及时率×100)。
* 及时 = finishDate ≤ taskEndDate(当日末)。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class OpsMajorTaskCalculator extends AbstractPerfIndicatorCalculator {
@Autowired
private IZtYwTaskService ywTaskService;
@Autowired
private IZtUserService userService;
@Override
public String key() {
return "opsMajorTask";
}
@Override
protected BigDecimal doCompute(String account, String month, JSONObject rule) {
//CHG-039 同类修复:zt_yw_task.belong_to_user 实际存中文姓名(“岑海峰”),按 account 匹配恒空 → 按昵称匹配,account 兜底
String nameKey = account;
ZtUser u = userService.getByAccount(account);
if (u != null && u.getNickname() != null && !u.getNickname().isEmpty()) {
nameKey = u.getNickname();
}
final String personKey = nameKey;
List<ZtYwTask> tasks = ywTaskService.list(new QueryWrapper<ZtYwTask>().lambda()
.and(w -> w.eq(ZtYwTask::getBelongToUser, personKey).or().eq(ZtYwTask::getBelongToUser, account))
.ge(ZtYwTask::getTaskEndDate, firstDayOfMonth(month))
.le(ZtYwTask::getTaskEndDate, lastDayOfMonth(month)));
if (CollectionUtils.isEmpty(tasks)) {
rawDetail("当月无到期运维任务");
return BigDecimal.ZERO;
}
long onTime = tasks.stream()
.filter(o -> o.getFinishDate() != null
&& o.getFinishDate().getTime() <= DateUtils.getDayLast(o.getTaskEndDate()).getTime())
.count();
BigDecimal rate = percent(BigDecimal.valueOf(onTime), BigDecimal.valueOf(tasks.size()));
rawDetail("及时 " + onTime + " / 到期 " + tasks.size() + " = " + rate.stripTrailingZeros().toPlainString() + "%");
return applyRule(rule, rate);
}
}
@@ -0,0 +1,43 @@
package com.sa.zentao.service.perf;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sa.zentao.entity.ZtYwFwqsearch;
import com.sa.zentao.service.IZtYwFwqsearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 系统运维监控(运维 0.15):每周2次,缺一次扣3分;实做 = zt_yw_fwqsearch 当月记录数。
*
* @author ai
* @since 2026-07-28
*/
@Component
public class OpsMonitorCalculator extends AbstractOpsCountCalculator {
@Autowired
private IZtYwFwqsearchService fwqsearchService;
@Override
public String key() {
return "opsMonitor";
}
@Override
protected long actualCount(String account, String month) {
return fwqsearchService.count(new QueryWrapper<ZtYwFwqsearch>().lambda()
.eq(ZtYwFwqsearch::getCreateUser, account)
.ge(ZtYwFwqsearch::getCreateDate, firstDayOfMonth(month))
.le(ZtYwFwqsearch::getCreateDate, lastDayOfMonth(month)));
}
@Override
protected int defaultTimesPerWeek() {
return 2;
}
@Override
protected int defaultDeductPerMissing() {
return 3;
}
}
@@ -0,0 +1,37 @@
package com.sa.zentao.service.perf;
import java.math.BigDecimal;
/**
* 绩效指标计算器(三期,策略模式)。
* 每个实现对应 zt_perf_config.item_key 一个指标;规则参数从 zt_perf_config.rule_json 读取。
*
* @author ai
* @since 2026-07-28
*/
public interface PerfIndicatorCalculator {
/**
* 指标编码,与 zt_perf_config.item_key 对应
*/
String key();
/**
* 计算某账号某月的指标值。
* 命中启用配置时返回该项得分(0~100,项内扣分扣完截止);
* 无配置/规则类型为 manual 以外的兜底场景返回原始值(如比率%、工时、次数)。
*
* @param account 账号(zt_user.account)
* @param month 月份 yyyy-MM
* @return 得分或原始值,永不返回 null
*/
BigDecimal compute(String account, String month);
/**
* CHG-041 消费并清除上一次 compute 的过程值明细(分子/分母/率),无则返回 null。
* 默认实现返回 null(非 AbstractPerfIndicatorCalculator 体系不受影响)。
*/
default String consumeRawDetail() {
return null;
}
}

Some files were not shown because too many files have changed in this diff Show More