fix(产品管理、项目管理、登录密码校验、工作报告): 修复用户们提出的一系列问题。

This commit is contained in:
dk
2026-06-23 17:51:24 +08:00
parent 5cabc7c541
commit 64e4100c11
13 changed files with 140 additions and 20 deletions

View File

@@ -23,6 +23,11 @@ public final class ProductObjectConstants {
*/
public static final String STATUS_PAUSED = "paused";
/**
* 产品废弃状态。
*/
public static final String STATUS_ABANDONED = "abandoned";
/**
* 产品自动编码前缀。
*/

View File

@@ -6,6 +6,7 @@ import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductCreateWithTeamReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOptionRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductRespVO;
@@ -22,6 +23,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 产品管理")
@@ -82,6 +85,12 @@ public class ProductController {
return success(productService.getProductOverviewSummary());
}
@GetMapping("/options")
@Operation(summary = "获取可绑定产品下拉选项")
public CommonResult<List<ProductOptionRespVO>> getProductOptions() {
return success(productService.getProductOptions());
}
@PostMapping("/change-status")
@Operation(summary = "变更产品状态")
public CommonResult<Boolean> changeProductStatus(@Valid @RequestBody ProductStatusActionReqVO reqVO) {

View File

@@ -0,0 +1,21 @@
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 产品下拉选项 Response VO")
@Data
public class ProductOptionRespVO {
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "产品编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CNPD2026001")
private String code;
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "灿能管理后台")
private String name;
@Schema(description = "产品方向字典值", example = "direction_value")
private String directionCode;
}

View File

@@ -48,5 +48,4 @@ public interface ProductMapper extends BaseMapperX<ProductDO> {
.eq(ProductDO::getId, id)
.eq(ProductDO::getStatusCode, statusCode));
}
}

View File

@@ -38,7 +38,9 @@ public final class AttachmentValidator {
// 压缩
"zip", "rar", "7z",
// 媒体
"mp4", "mp3"
"mp4", "mp3",
// 其他
"sql", "xml", "json"
);
/** 禁止的扩展名黑名单(即使在白名单也禁,兜底防可执行 / 脚本类)。 */

View File

@@ -4,6 +4,7 @@ import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductCreateWithTeamReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOptionRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductRespVO;
@@ -12,6 +13,8 @@ import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductS
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
import java.util.List;
/**
* 产品 Service 接口
*/
@@ -89,6 +92,13 @@ public interface ProductService {
*/
ProductOverviewSummaryRespVO getProductOverviewSummary();
/**
* 获取可绑定产品下拉选项。
*
* @return 产品下拉选项
*/
List<ProductOptionRespVO> getProductOptions();
/**
* 变更产品状态
*

View File

@@ -20,6 +20,7 @@ import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductC
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductContextRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductCreateWithTeamReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOptionRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
@@ -68,6 +69,10 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil
@Service
public class ProductServiceImpl implements ProductService {
private static final List<String> PRODUCT_BIND_EXCLUDED_STATUS_CODES = List.of(
ProductObjectConstants.STATUS_PAUSED,
ProductObjectConstants.STATUS_ABANDONED);
@Resource
private ProductMapper productMapper;
@Resource
@@ -455,6 +460,29 @@ public class ProductServiceImpl implements ProductService {
return respVO;
}
@Override
public List<ProductOptionRespVO> getProductOptions() {
Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
ObjectDataScope scope = objectDataScopeService.compute(loginUserId, ProductObjectConstants.OBJECT_TYPE);
if (scope.getState() == ObjectDataScope.State.EMPTY) {
return Collections.emptyList();
}
LambdaQueryWrapperX<ProductDO> wrapper = new LambdaQueryWrapperX<>();
wrapper.notIn(ProductDO::getStatusCode, PRODUCT_BIND_EXCLUDED_STATUS_CODES);
wrapper.orderByDesc(ProductDO::getCreateTime);
applyProductScopeFilter(wrapper, scope);
return productMapper.selectList(wrapper).stream()
.map(product -> {
ProductOptionRespVO respVO = new ProductOptionRespVO();
respVO.setId(product.getId());
respVO.setCode(product.getCode());
respVO.setName(product.getName());
respVO.setDirectionCode(product.getDirectionCode());
return respVO;
})
.toList();
}
/**
* 按 scope 算出 (statusCode, countValue) 行集,喂给 {@link #buildProductStatusCounts}。
* EMPTY 直接空集ALL 走原全表 GROUP BY SQLID_LIST 用 wrapper 取 status_codeJava 端 group + count。
@@ -469,6 +497,24 @@ public class ProductServiceImpl implements ProductService {
// ID_LIST
LambdaQueryWrapperX<ProductDO> wrapper = new LambdaQueryWrapperX<>();
wrapper.select(ProductDO::getStatusCode);
applyProductScopeFilter(wrapper, scope);
return productMapper.selectList(wrapper).stream()
.filter(p -> p.getStatusCode() != null)
.collect(Collectors.groupingBy(ProductDO::getStatusCode, Collectors.counting()))
.entrySet().stream()
.map(e -> {
Map<String, Object> row = new HashMap<>();
row.put("statusCode", e.getKey());
row.put("countValue", e.getValue());
return row;
})
.collect(Collectors.toList());
}
private void applyProductScopeFilter(LambdaQueryWrapperX<ProductDO> wrapper, ObjectDataScope scope) {
if (scope.getState() != ObjectDataScope.State.ID_LIST) {
return;
}
Set<Long> ids = scope.getIds();
Set<String> dcs = scope.getDirectionCodes();
wrapper.and(w -> {
@@ -484,17 +530,6 @@ public class ProductServiceImpl implements ProductService {
w.in(ProductDO::getDirectionCode, dcs);
}
});
return productMapper.selectList(wrapper).stream()
.filter(p -> p.getStatusCode() != null)
.collect(Collectors.groupingBy(ProductDO::getStatusCode, Collectors.counting()))
.entrySet().stream()
.map(e -> {
Map<String, Object> row = new HashMap<>();
row.put("statusCode", e.getKey());
row.put("countValue", e.getValue());
return row;
})
.collect(Collectors.toList());
}
@Override

View File

@@ -185,7 +185,7 @@ public class WorkReportCommonService {
WeeklyReportRespVO respVO = latestDraft;
respVO.setIsBusinessTrip(Boolean.TRUE.equals(reqVO.getIsBusinessTrip()));
respVO.setTravelSegments(BeanUtils.toBean(defaultList(reqVO.getTravelSegments()), WeeklyReportTravelSegmentRespVO.class));
respVO.setReviewItems(mergeReviewItems(reqVO.getReviewItems(), latestDraft.getReviewItems()));
respVO.setReviewItems(mergeWeeklyReviewItems(reqVO.getReviewItems(), latestDraft.getReviewItems()));
respVO.setPlanItems(mergePlanItems(reqVO.getPlanItems(), latestDraft.getPlanItems()));
respVO.setTotalTravelDays(Boolean.TRUE.equals(reqVO.getIsBusinessTrip())
? defaultIfNull(sumTravelDays(reqVO.getTravelSegments()))
@@ -1132,6 +1132,45 @@ public class WorkReportCommonService {
return merged;
}
private List<PersonalReportReviewItemRespVO> mergeWeeklyReviewItems(List<PersonalReportReviewItemReqVO> currentItems,
List<PersonalReportReviewItemRespVO> latestItems) {
List<PersonalReportReviewItemRespVO> merged = BeanUtils.toBean(defaultList(currentItems),
PersonalReportReviewItemRespVO.class);
Map<String, PersonalReportReviewItemRespVO> latestItemMap = defaultList(latestItems).stream()
.filter(Objects::nonNull)
.filter(item -> StringUtils.hasText(item.getItemTitle()))
.collect(Collectors.toMap(item -> normalizeMergeText(item.getItemTitle()),
item -> item, (left, right) -> right, LinkedHashMap::new));
Set<String> existingKeys = new LinkedHashSet<>();
for (PersonalReportReviewItemRespVO item : merged) {
String key = normalizeMergeText(item.getItemTitle());
if (!StringUtils.hasText(key)) {
continue;
}
PersonalReportReviewItemRespVO latestItem = latestItemMap.get(key);
if (latestItem != null) {
item.setWorkHours(latestItem.getWorkHours());
item.setContentText(latestItem.getContentText());
item.setContentJson(latestItem.getContentJson());
}
existingKeys.add(key);
}
for (PersonalReportReviewItemRespVO item : defaultList(latestItems)) {
String key = normalizeMergeText(item.getItemTitle());
if (StringUtils.hasText(key) && existingKeys.contains(key)) {
continue;
}
merged.add(BeanUtils.toBean(item, PersonalReportReviewItemRespVO.class));
if (StringUtils.hasText(key)) {
existingKeys.add(key);
}
}
for (int i = 0; i < merged.size(); i++) {
merged.get(i).setItemNumber(i + 1);
}
return merged;
}
private List<PersonalReportPlanItemRespVO> mergePlanItems(List<PersonalReportPlanItemReqVO> currentItems,
List<PersonalReportPlanItemRespVO> latestItems) {
List<PersonalReportPlanItemRespVO> merged = BeanUtils.toBean(defaultList(currentItems),

View File

@@ -25,7 +25,7 @@ public class AuthLoginReqVO extends CaptchaVerificationReqVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 4, max = 30, message = "密码长度为 4-30")
private String password;
}

View File

@@ -38,6 +38,6 @@ public class AuthRegisterReqVO extends CaptchaVerificationReqVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 4, max = 30, message = "密码长度为 4-30")
private String password;
}

View File

@@ -12,12 +12,12 @@ public class UserProfileUpdatePasswordReqVO {
@Schema(description = "旧密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "旧密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 4, max = 30, message = "密码长度为 4-30")
private String oldPassword;
@Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "654321")
@NotEmpty(message = "新密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 4, max = 30, message = "密码长度为 4-30")
private String newPassword;
}

View File

@@ -89,7 +89,7 @@ public class UserSaveReqVO {
// ========== 仅【创建】时,需要传递的字段 ==========
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 4, max = 30, message = "密码长度为 4-30")
private String password;
@AssertTrue(message = "密码不能为空")

View File

@@ -17,7 +17,7 @@ public class UserUpdatePasswordReqVO {
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotEmpty(message = "密码不能为空")
@Length(min = 4, max = 16, message = "密码长度为 4-16")
@Length(min = 4, max = 30, message = "密码长度为 4-30")
private String password;
}