feat(other): 产品基础功能提交
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package com.njcn.rdms.module.project;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 项目交付域服务启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class ProjectServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ProjectServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Project API 实现包,放置对外暴露 RPC 接口的实现类
|
||||
*/
|
||||
package com.njcn.rdms.module.project.api;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 管理端控制器包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.controller.admin;
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
import com.njcn.rdms.module.project.service.product.ProductService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - 产品管理")
|
||||
@RestController
|
||||
@RequestMapping("/project/product")
|
||||
@Validated
|
||||
public class ProductController {
|
||||
|
||||
@Resource
|
||||
private ProductService productService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建产品")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:create')")
|
||||
public CommonResult<Long> createProduct(@Valid @RequestBody ProductSaveReqVO createReqVO) {
|
||||
return success(productService.createProduct(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新产品")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:update')")
|
||||
public CommonResult<Boolean> updateProduct(@Valid @RequestBody ProductSaveReqVO updateReqVO) {
|
||||
productService.updateProduct(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获取产品详情")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:query')")
|
||||
public CommonResult<ProductRespVO> getProduct(@RequestParam("id") Long id) {
|
||||
ProductDO product = productService.getProduct(id);
|
||||
return success(BeanUtils.toBean(product, ProductRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取产品分页")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:query')")
|
||||
public CommonResult<PageResult<ProductRespVO>> getProductPage(@Valid ProductPageReqVO pageReqVO) {
|
||||
PageResult<ProductDO> pageResult = productService.getProductPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, ProductRespVO.class));
|
||||
}
|
||||
|
||||
@PostMapping("/change-status")
|
||||
@Operation(summary = "变更产品状态")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:status')")
|
||||
public CommonResult<Boolean> changeProductStatus(@Valid @RequestBody ProductStatusActionReqVO reqVO) {
|
||||
productService.changeProductStatus(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除产品")
|
||||
@PreAuthorize("@ss.hasPermission('project:product:delete')")
|
||||
public CommonResult<Boolean> deleteProduct(@Valid @RequestBody ProductDeleteReqVO reqVO) {
|
||||
productService.deleteProduct(reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品删除 Request VO")
|
||||
@Data
|
||||
public class ProductDeleteReqVO {
|
||||
|
||||
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "确认输入的产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
|
||||
@NotBlank(message = "确认产品名称不能为空")
|
||||
@Size(max = 128, message = "确认产品名称长度不能超过128个字符")
|
||||
private String productName;
|
||||
|
||||
@Schema(description = "删除原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品录入错误")
|
||||
@NotBlank(message = "删除原因不能为空")
|
||||
@Size(max = 500, message = "删除原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageParam;
|
||||
import com.njcn.rdms.framework.dict.validation.InDict;
|
||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.njcn.rdms.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Schema(description = "管理后台 - 产品分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "关键词,匹配产品编码或产品名称", example = "CNPD2026001")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "产品方向字典值", example = "embedded")
|
||||
@InDict(type = ProjectDictTypeConstants.PRODUCT_DIRECTION)
|
||||
private String directionCode;
|
||||
|
||||
@Schema(description = "产品经理用户编号", example = "1024")
|
||||
private Long managerUserId;
|
||||
|
||||
@Schema(description = "产品状态编码", example = "active")
|
||||
@Size(max = 32, message = "产品状态编码长度不能超过32个字符")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "更新时间", example = "[2026-04-01 00:00:00, 2026-04-30 23:59:59]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 产品 Response VO")
|
||||
@Data
|
||||
public class ProductRespVO {
|
||||
|
||||
@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 = "embedded")
|
||||
private String directionCode;
|
||||
|
||||
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "产品经理用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long managerUserId;
|
||||
|
||||
@Schema(description = "产品描述", example = "面向研发管理的一体化产品")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "产品状态编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "active")
|
||||
private String statusCode;
|
||||
|
||||
@Schema(description = "最近一次状态动作原因", example = "阶段性暂停")
|
||||
private String lastStatusReason;
|
||||
|
||||
@Schema(description = "备注", example = "预留")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import com.njcn.rdms.framework.dict.validation.InDict;
|
||||
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品保存 Request VO")
|
||||
@Data
|
||||
public class ProductSaveReqVO {
|
||||
|
||||
@Schema(description = "产品编号", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "产品编码,为空时由系统自动生成", example = "CNPD2026001")
|
||||
@Size(max = 64, message = "产品编码长度不能超过64个字符")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "产品方向字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "embedded")
|
||||
@NotBlank(message = "产品方向不能为空")
|
||||
@Size(max = 32, message = "产品方向长度不能超过32个字符")
|
||||
@InDict(type = ProjectDictTypeConstants.PRODUCT_DIRECTION)
|
||||
private String directionCode;
|
||||
|
||||
@Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "RDMS产品平台")
|
||||
@NotBlank(message = "产品名称不能为空")
|
||||
@Size(max = 128, message = "产品名称长度不能超过128个字符")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "产品经理用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品经理不能为空")
|
||||
private Long managerUserId;
|
||||
|
||||
@Schema(description = "产品描述", example = "面向研发管理的一体化产品")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "备注", example = "预留")
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.product;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 产品状态动作 Request VO")
|
||||
@Data
|
||||
public class ProductStatusActionReqVO {
|
||||
|
||||
@Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "产品编号不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "动作编码,如 pause、resume、archive、abandon", requiredMode = Schema.RequiredMode.REQUIRED, example = "pause")
|
||||
@NotBlank(message = "动作编码不能为空")
|
||||
@Size(max = 32, message = "动作编码长度不能超过32个字符")
|
||||
private String actionCode;
|
||||
|
||||
@Schema(description = "动作原因;是否必填由状态流转配置决定", example = "当前阶段受环境限制暂停推进")
|
||||
@Size(max = 500, message = "动作原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 应用端控制器包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.controller.app;
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* 提供 RESTful API 给前端:
|
||||
* 1. admin 包:提供给管理后台 rdms-ui-admin 前端项目
|
||||
* 2. app 包:提供给用户 APP rdms-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分
|
||||
*/
|
||||
package com.njcn.rdms.module.project.controller;
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* DTO、VO、DO 等对象转换包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.convert;
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.audit;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* RDMS通用业务审计日志表
|
||||
*/
|
||||
@TableName("rdms_biz_audit_log")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BizAuditLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 业务对象类型
|
||||
*/
|
||||
private String bizType;
|
||||
/**
|
||||
* 业务对象ID
|
||||
*/
|
||||
private Long bizId;
|
||||
/**
|
||||
* 动作类型
|
||||
*/
|
||||
private String actionType;
|
||||
/**
|
||||
* 原状态
|
||||
*/
|
||||
private String fromStatus;
|
||||
/**
|
||||
* 目标状态
|
||||
*/
|
||||
private String toStatus;
|
||||
/**
|
||||
* 关键字段变更摘要
|
||||
*/
|
||||
private String fieldChanges;
|
||||
/**
|
||||
* 动作原因或说明
|
||||
*/
|
||||
private String reason;
|
||||
/**
|
||||
* 操作人用户ID
|
||||
*/
|
||||
private Long operatorUserId;
|
||||
/**
|
||||
* 操作人名称快照
|
||||
*/
|
||||
private String operatorName;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 数据对象包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.dal.dataobject;
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.product;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 产品主表
|
||||
*/
|
||||
@TableName("rdms_product")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 产品编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 产品编码
|
||||
*/
|
||||
private String code;
|
||||
/**
|
||||
* 产品方向字典值
|
||||
*/
|
||||
private String directionCode;
|
||||
/**
|
||||
* 产品状态编码
|
||||
*/
|
||||
private String statusCode;
|
||||
/**
|
||||
* 产品名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 产品经理用户编号
|
||||
*/
|
||||
private Long managerUserId;
|
||||
/**
|
||||
* 产品描述
|
||||
*/
|
||||
private String description;
|
||||
/**
|
||||
* 最近一次状态动作原因
|
||||
*/
|
||||
private String lastStatusReason;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.product;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 产品状态日志表
|
||||
*/
|
||||
@TableName("rdms_product_status_log")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ProductStatusLogDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 产品ID
|
||||
*/
|
||||
private Long productId;
|
||||
/**
|
||||
* 动作类型
|
||||
*/
|
||||
private String actionType;
|
||||
/**
|
||||
* 变更前状态编码
|
||||
*/
|
||||
private String fromStatus;
|
||||
/**
|
||||
* 变更后状态编码
|
||||
*/
|
||||
private String toStatus;
|
||||
/**
|
||||
* 动作原因
|
||||
*/
|
||||
private String reason;
|
||||
/**
|
||||
* 操作人用户ID
|
||||
*/
|
||||
private Long operatorUserId;
|
||||
/**
|
||||
* 操作人名称快照
|
||||
*/
|
||||
private String operatorName;
|
||||
/**
|
||||
* 产品编码快照
|
||||
*/
|
||||
private String productCodeSnapshot;
|
||||
/**
|
||||
* 产品名称快照
|
||||
*/
|
||||
private String productNameSnapshot;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.status;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* RDMS对象状态模型表
|
||||
*/
|
||||
@TableName("rdms_object_status_model")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ObjectStatusModelDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 对象类型
|
||||
*/
|
||||
private String objectType;
|
||||
/**
|
||||
* 状态编码
|
||||
*/
|
||||
private String statusCode;
|
||||
/**
|
||||
* 状态名称
|
||||
*/
|
||||
private String statusName;
|
||||
/**
|
||||
* 排序值
|
||||
*/
|
||||
private Integer sort;
|
||||
/**
|
||||
* 配置状态
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 是否初始状态
|
||||
*/
|
||||
private Boolean initialFlag;
|
||||
/**
|
||||
* 是否终态
|
||||
*/
|
||||
private Boolean terminalFlag;
|
||||
/**
|
||||
* 是否允许编辑对象主数据
|
||||
*/
|
||||
private Boolean allowEdit;
|
||||
/**
|
||||
* 是否允许新建项目
|
||||
*/
|
||||
private Boolean allowCreateProject;
|
||||
/**
|
||||
* 是否允许新增需求
|
||||
*/
|
||||
private Boolean allowCreateRequirement;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.njcn.rdms.module.project.dal.dataobject.status;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* RDMS对象状态流转表
|
||||
*/
|
||||
@TableName("rdms_object_status_transition")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ObjectStatusTransitionDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 对象类型
|
||||
*/
|
||||
private String objectType;
|
||||
/**
|
||||
* 动作编码
|
||||
*/
|
||||
private String actionCode;
|
||||
/**
|
||||
* 动作名称
|
||||
*/
|
||||
private String actionName;
|
||||
/**
|
||||
* 起始状态编码
|
||||
*/
|
||||
private String fromStatusCode;
|
||||
/**
|
||||
* 目标状态编码
|
||||
*/
|
||||
private String toStatusCode;
|
||||
/**
|
||||
* 是否必须填写原因
|
||||
*/
|
||||
private Boolean needReason;
|
||||
/**
|
||||
* 配置状态
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.audit;
|
||||
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface BizAuditLogMapper extends BaseMapperX<BizAuditLogDO> {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* MyBatis Mapper 包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.dal.mysql;
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ProductMapper extends BaseMapperX<ProductDO> {
|
||||
|
||||
default PageResult<ProductDO> selectPage(ProductPageReqVO reqVO) {
|
||||
LambdaQueryWrapperX<ProductDO> queryWrapper = new LambdaQueryWrapperX<>();
|
||||
if (StringUtils.hasText(reqVO.getKeyword())) {
|
||||
queryWrapper.and(wrapper -> wrapper.like(ProductDO::getCode, reqVO.getKeyword())
|
||||
.or()
|
||||
.like(ProductDO::getName, reqVO.getKeyword()));
|
||||
}
|
||||
queryWrapper.eqIfPresent(ProductDO::getDirectionCode, reqVO.getDirectionCode())
|
||||
.eqIfPresent(ProductDO::getManagerUserId, reqVO.getManagerUserId())
|
||||
.eqIfPresent(ProductDO::getStatusCode, reqVO.getStatusCode())
|
||||
.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime())
|
||||
.orderByDesc(BaseDO::getUpdateTime);
|
||||
return selectPage(reqVO, queryWrapper);
|
||||
}
|
||||
|
||||
default ProductDO selectByCode(String code) {
|
||||
return selectOne(ProductDO::getCode, code);
|
||||
}
|
||||
|
||||
default ProductDO selectByName(String name) {
|
||||
return selectOne(ProductDO::getName, name);
|
||||
}
|
||||
|
||||
default List<ProductDO> selectListByCodePrefix(String codePrefix) {
|
||||
return selectList(new LambdaQueryWrapperX<ProductDO>()
|
||||
.likeRight(ProductDO::getCode, codePrefix)
|
||||
.orderByDesc(ProductDO::getCode));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.product;
|
||||
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ProductStatusLogMapper extends BaseMapperX<ProductStatusLogDO> {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.status;
|
||||
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ObjectStatusModelMapper extends BaseMapperX<ObjectStatusModelDO> {
|
||||
|
||||
default ObjectStatusModelDO selectByObjectTypeAndStatusCode(String objectType, String statusCode) {
|
||||
return selectOne(new LambdaQueryWrapperX<ObjectStatusModelDO>()
|
||||
.eq(ObjectStatusModelDO::getObjectType, objectType)
|
||||
.eq(ObjectStatusModelDO::getStatusCode, statusCode));
|
||||
}
|
||||
|
||||
default List<ObjectStatusModelDO> selectListByObjectType(String objectType) {
|
||||
return selectList(new LambdaQueryWrapperX<ObjectStatusModelDO>()
|
||||
.eq(ObjectStatusModelDO::getObjectType, objectType)
|
||||
.orderByAsc(ObjectStatusModelDO::getSort));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.rdms.module.project.dal.mysql.status;
|
||||
|
||||
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ObjectStatusTransitionMapper extends BaseMapperX<ObjectStatusTransitionDO> {
|
||||
|
||||
default ObjectStatusTransitionDO selectByObjectTypeAndFromStatusAndAction(String objectType,
|
||||
String fromStatusCode,
|
||||
String actionCode) {
|
||||
return selectOne(new LambdaQueryWrapperX<ObjectStatusTransitionDO>()
|
||||
.eq(ObjectStatusTransitionDO::getObjectType, objectType)
|
||||
.eq(ObjectStatusTransitionDO::getFromStatusCode, fromStatusCode)
|
||||
.eq(ObjectStatusTransitionDO::getActionCode, actionCode)
|
||||
.eq(ObjectStatusTransitionDO::getStatus, 0));
|
||||
}
|
||||
|
||||
default List<ObjectStatusTransitionDO> selectListByObjectTypeAndFromStatus(String objectType, String fromStatusCode) {
|
||||
return selectList(new LambdaQueryWrapperX<ObjectStatusTransitionDO>()
|
||||
.eq(ObjectStatusTransitionDO::getObjectType, objectType)
|
||||
.eq(ObjectStatusTransitionDO::getFromStatusCode, fromStatusCode)
|
||||
.eq(ObjectStatusTransitionDO::getStatus, 0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 持久层包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.dal;
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.njcn.rdms.module.project.framework.rpc.config;
|
||||
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Project 模块的 RPC 配置
|
||||
*/
|
||||
@Configuration(value = "projectRpcConfiguration", proxyBeanMethods = false)
|
||||
@EnableFeignClients(clients = {AdminUserApi.class})
|
||||
public class RpcConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.rdms.module.project.framework.security.config;
|
||||
|
||||
import com.njcn.rdms.framework.security.config.AuthorizeRequestsCustomizer;
|
||||
import com.njcn.rdms.module.project.enums.ApiConstants;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
|
||||
/**
|
||||
* Project 模块的 Security 配置
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false, value = "projectSecurityConfiguration")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Bean("projectAuthorizeRequestsCustomizer")
|
||||
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
|
||||
return new AuthorizeRequestsCustomizer() {
|
||||
|
||||
@Override
|
||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
// Swagger 接口文档
|
||||
registry.requestMatchers("/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/webjars/**").permitAll()
|
||||
.requestMatchers("/swagger-ui").permitAll()
|
||||
.requestMatchers("/swagger-ui/**").permitAll();
|
||||
// Druid 监控
|
||||
registry.requestMatchers("/druid/**").permitAll();
|
||||
// Spring Boot Actuator 的安全配置
|
||||
registry.requestMatchers("/actuator").permitAll()
|
||||
.requestMatchers("/actuator/**").permitAll();
|
||||
// RPC 服务的安全配置
|
||||
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 服务层包
|
||||
*/
|
||||
package com.njcn.rdms.module.project.service;
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
|
||||
/**
|
||||
* 产品 Service 接口
|
||||
*/
|
||||
public interface ProductService {
|
||||
|
||||
/**
|
||||
* 创建产品
|
||||
*
|
||||
* @param createReqVO 创建请求
|
||||
* @return 产品编号
|
||||
*/
|
||||
Long createProduct(ProductSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新产品
|
||||
*
|
||||
* @param updateReqVO 更新请求
|
||||
*/
|
||||
void updateProduct(ProductSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 获取产品详情
|
||||
*
|
||||
* @param id 产品编号
|
||||
* @return 产品信息
|
||||
*/
|
||||
ProductDO getProduct(Long id);
|
||||
|
||||
/**
|
||||
* 获取产品分页
|
||||
*
|
||||
* @param pageReqVO 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 变更产品状态
|
||||
*
|
||||
* @param reqVO 状态动作请求
|
||||
*/
|
||||
void changeProductStatus(ProductStatusActionReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除产品
|
||||
*
|
||||
* @param reqVO 删除请求
|
||||
*/
|
||||
void deleteProduct(ProductDeleteReqVO reqVO);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.njcn.rdms.framework.common.pojo.PageResult;
|
||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.rdms.framework.common.util.object.BeanUtils;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO;
|
||||
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
|
||||
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.product.ProductStatusLogMapper;
|
||||
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
|
||||
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
|
||||
import com.njcn.rdms.module.system.api.user.AdminUserApi;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
|
||||
/**
|
||||
* 产品 Service 实现类
|
||||
*/
|
||||
@Service
|
||||
public class ProductServiceImpl implements ProductService {
|
||||
|
||||
private static final String PRODUCT_OBJECT_TYPE = "product";
|
||||
private static final String PRODUCT_ACTIVE_STATUS = "active";
|
||||
private static final String PRODUCT_PAUSED_STATUS = "paused";
|
||||
private static final String PRODUCT_ARCHIVED_STATUS = "archived";
|
||||
private static final String PRODUCT_ABANDONED_STATUS = "abandoned";
|
||||
|
||||
private static final String PRODUCT_CREATE_ACTION = "create";
|
||||
private static final String PRODUCT_UPDATE_ACTION = "update";
|
||||
private static final String PRODUCT_DELETE_ACTION = "delete";
|
||||
|
||||
private static final String PRODUCT_CODE_PREFIX = "CNPD";
|
||||
|
||||
@Resource
|
||||
private ProductMapper productMapper;
|
||||
@Resource
|
||||
private ProductStatusLogMapper productStatusLogMapper;
|
||||
@Resource
|
||||
private BizAuditLogMapper bizAuditLogMapper;
|
||||
@Resource
|
||||
private ObjectStatusTransitionMapper objectStatusTransitionMapper;
|
||||
@Resource
|
||||
private AdminUserApi adminUserApi;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createProduct(ProductSaveReqVO createReqVO) {
|
||||
validateCreateReqVO(createReqVO);
|
||||
validateManagerUser(createReqVO.getManagerUserId());
|
||||
|
||||
ProductDO product = new ProductDO();
|
||||
product.setCode(generateProductCode(createReqVO.getCode()));
|
||||
product.setDirectionCode(createReqVO.getDirectionCode());
|
||||
product.setStatusCode(PRODUCT_ACTIVE_STATUS);
|
||||
product.setName(createReqVO.getName().trim());
|
||||
product.setManagerUserId(createReqVO.getManagerUserId());
|
||||
product.setDescription(normalizeNullableText(createReqVO.getDescription()));
|
||||
product.setRemark(normalizeNullableText(createReqVO.getRemark()));
|
||||
productMapper.insert(product);
|
||||
|
||||
writeBizAuditLog(product, PRODUCT_CREATE_ACTION, null, PRODUCT_ACTIVE_STATUS,
|
||||
buildFieldChanges(null, product), null);
|
||||
return product.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateProduct(ProductSaveReqVO updateReqVO) {
|
||||
if (updateReqVO.getId() == null) {
|
||||
throw invalidParamException("产品编号不能为空");
|
||||
}
|
||||
ProductDO product = validateProductExists(updateReqVO.getId());
|
||||
validateManagerUser(updateReqVO.getManagerUserId());
|
||||
validateProductCodeUnchanged(product, updateReqVO.getCode());
|
||||
validateProductEditable(product, updateReqVO);
|
||||
validateProductNameUnique(updateReqVO.getId(), updateReqVO.getName());
|
||||
|
||||
ProductDO before = BeanUtils.toBean(product, ProductDO.class);
|
||||
|
||||
product.setDirectionCode(updateReqVO.getDirectionCode());
|
||||
product.setName(updateReqVO.getName().trim());
|
||||
product.setManagerUserId(updateReqVO.getManagerUserId());
|
||||
product.setDescription(normalizeNullableText(updateReqVO.getDescription()));
|
||||
product.setRemark(normalizeNullableText(updateReqVO.getRemark()));
|
||||
productMapper.updateById(product);
|
||||
|
||||
writeBizAuditLog(product, PRODUCT_UPDATE_ACTION, product.getStatusCode(), product.getStatusCode(),
|
||||
buildFieldChanges(before, product), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProductDO getProduct(Long id) {
|
||||
return validateProductExists(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO) {
|
||||
return productMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void changeProductStatus(ProductStatusActionReqVO reqVO) {
|
||||
ProductDO product = validateProductExists(reqVO.getId());
|
||||
String actionCode = reqVO.getActionCode().trim();
|
||||
ObjectStatusTransitionDO transition = validateProductTransition(product.getStatusCode(), actionCode);
|
||||
String reason = normalizeNullableText(reqVO.getReason());
|
||||
validateTransitionReason(transition, reason);
|
||||
|
||||
String fromStatus = product.getStatusCode();
|
||||
String toStatus = transition.getToStatusCode();
|
||||
product.setStatusCode(toStatus);
|
||||
product.setLastStatusReason(reason);
|
||||
productMapper.updateById(product);
|
||||
|
||||
writeProductStatusLog(product, actionCode, fromStatus, toStatus, reason);
|
||||
writeBizAuditLog(product, actionCode, fromStatus, toStatus, null, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteProduct(ProductDeleteReqVO reqVO) {
|
||||
ProductDO product = validateProductExists(reqVO.getId());
|
||||
if (!Objects.equals(product.getName(), reqVO.getProductName().trim())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_DELETE_NAME_MISMATCH);
|
||||
}
|
||||
|
||||
String reason = reqVO.getReason().trim();
|
||||
String fromStatus = product.getStatusCode();
|
||||
productMapper.deleteById(reqVO.getId());
|
||||
|
||||
writeProductStatusLog(product, PRODUCT_DELETE_ACTION, fromStatus, null, reason);
|
||||
writeBizAuditLog(product, PRODUCT_DELETE_ACTION, fromStatus, null, null, reason);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateCreateReqVO(ProductSaveReqVO createReqVO) {
|
||||
validateProductCodeUnique(null, createReqVO.getCode());
|
||||
validateProductNameUnique(null, createReqVO.getName());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ProductDO validateProductExists(Long id) {
|
||||
if (id == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
|
||||
}
|
||||
ProductDO product = productMapper.selectById(id);
|
||||
if (product == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
|
||||
}
|
||||
return product;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateProductCodeUnique(Long id, String code) {
|
||||
if (!StringUtils.hasText(code)) {
|
||||
return;
|
||||
}
|
||||
String normalizedCode = code.trim();
|
||||
ProductDO product = productMapper.selectByCode(normalizedCode);
|
||||
if (product == null) {
|
||||
return;
|
||||
}
|
||||
if (id == null || !product.getId().equals(id)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_CODE_DUPLICATE, normalizedCode);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateProductNameUnique(Long id, String name) {
|
||||
String normalizedName = name.trim();
|
||||
ProductDO product = productMapper.selectByName(normalizedName);
|
||||
if (product == null) {
|
||||
return;
|
||||
}
|
||||
if (id == null || !product.getId().equals(id)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_NAME_DUPLICATE, normalizedName);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateManagerUser(Long managerUserId) {
|
||||
adminUserApi.validateUser(managerUserId);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateProductCodeUnchanged(ProductDO product, String code) {
|
||||
if (!StringUtils.hasText(code)) {
|
||||
return;
|
||||
}
|
||||
if (!Objects.equals(product.getCode(), code.trim())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_CODE_NOT_MODIFIABLE);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateProductEditable(ProductDO product, ProductSaveReqVO updateReqVO) {
|
||||
if (PRODUCT_ARCHIVED_STATUS.equals(product.getStatusCode())
|
||||
|| PRODUCT_ABANDONED_STATUS.equals(product.getStatusCode())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_NOT_ALLOW_EDIT);
|
||||
}
|
||||
if (!PRODUCT_PAUSED_STATUS.equals(product.getStatusCode())) {
|
||||
return;
|
||||
}
|
||||
if (!Objects.equals(product.getDirectionCode(), updateReqVO.getDirectionCode())
|
||||
|| !Objects.equals(product.getName(), updateReqVO.getName().trim())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ObjectStatusTransitionDO validateProductTransition(String fromStatusCode, String actionCode) {
|
||||
ObjectStatusTransitionDO transition = objectStatusTransitionMapper
|
||||
.selectByObjectTypeAndFromStatusAndAction(PRODUCT_OBJECT_TYPE, fromStatusCode, actionCode);
|
||||
if (transition == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED, actionCode);
|
||||
}
|
||||
return transition;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
|
||||
if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateProductCode(String code) {
|
||||
String normalizedCode = normalizeNullableText(code);
|
||||
if (StringUtils.hasText(normalizedCode)) {
|
||||
validateProductCodeUnique(null, normalizedCode);
|
||||
return normalizedCode;
|
||||
}
|
||||
|
||||
String year = String.valueOf(LocalDate.now().getYear());
|
||||
String codePrefix = PRODUCT_CODE_PREFIX + year;
|
||||
int nextSequence = 1;
|
||||
for (ProductDO product : productMapper.selectListByCodePrefix(codePrefix)) {
|
||||
String existedCode = product.getCode();
|
||||
if (!StringUtils.hasText(existedCode) || !existedCode.matches(codePrefix + "\\d{3}")) {
|
||||
continue;
|
||||
}
|
||||
nextSequence = Integer.parseInt(existedCode.substring(codePrefix.length())) + 1;
|
||||
break;
|
||||
}
|
||||
if (nextSequence > 999) {
|
||||
throw invalidParamException("{} 年产品自动编码序号已用尽", year);
|
||||
}
|
||||
String generatedCode = codePrefix + String.format("%03d", nextSequence);
|
||||
validateProductCodeUnique(null, generatedCode);
|
||||
return generatedCode;
|
||||
}
|
||||
|
||||
private void writeProductStatusLog(ProductDO product, String actionType, String fromStatus,
|
||||
String toStatus, String reason) {
|
||||
ProductStatusLogDO statusLog = new ProductStatusLogDO();
|
||||
statusLog.setProductId(product.getId());
|
||||
statusLog.setActionType(actionType);
|
||||
statusLog.setFromStatus(fromStatus);
|
||||
statusLog.setToStatus(toStatus);
|
||||
statusLog.setReason(defaultText(reason));
|
||||
statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
statusLog.setProductCodeSnapshot(product.getCode());
|
||||
statusLog.setProductNameSnapshot(product.getName());
|
||||
productStatusLogMapper.insert(statusLog);
|
||||
}
|
||||
|
||||
private void writeBizAuditLog(ProductDO product, String actionType, String fromStatus, String toStatus,
|
||||
String fieldChanges, String reason) {
|
||||
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||
auditLog.setBizType(PRODUCT_OBJECT_TYPE);
|
||||
auditLog.setBizId(product.getId());
|
||||
auditLog.setActionType(actionType);
|
||||
auditLog.setFromStatus(fromStatus);
|
||||
auditLog.setToStatus(toStatus);
|
||||
auditLog.setFieldChanges(fieldChanges);
|
||||
auditLog.setReason(reason);
|
||||
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
private String buildFieldChanges(ProductDO before, ProductDO after) {
|
||||
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||
appendFieldChange(fieldChanges, "code", valueOf(before, ProductDO::getCode), valueOf(after, ProductDO::getCode));
|
||||
appendFieldChange(fieldChanges, "directionCode", valueOf(before, ProductDO::getDirectionCode),
|
||||
valueOf(after, ProductDO::getDirectionCode));
|
||||
appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProductDO::getStatusCode),
|
||||
valueOf(after, ProductDO::getStatusCode));
|
||||
appendFieldChange(fieldChanges, "name", valueOf(before, ProductDO::getName), valueOf(after, ProductDO::getName));
|
||||
appendFieldChange(fieldChanges, "managerUserId", valueOf(before, ProductDO::getManagerUserId),
|
||||
valueOf(after, ProductDO::getManagerUserId));
|
||||
appendFieldChange(fieldChanges, "description", valueOf(before, ProductDO::getDescription),
|
||||
valueOf(after, ProductDO::getDescription));
|
||||
appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, ProductDO::getLastStatusReason),
|
||||
valueOf(after, ProductDO::getLastStatusReason));
|
||||
appendFieldChange(fieldChanges, "remark", valueOf(before, ProductDO::getRemark), valueOf(after, ProductDO::getRemark));
|
||||
return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
|
||||
}
|
||||
|
||||
private <T> T valueOf(ProductDO product, Function<ProductDO, T> getter) {
|
||||
return product == null ? null : getter.apply(product);
|
||||
}
|
||||
|
||||
private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
|
||||
if (Objects.equals(before, after)) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> value = new LinkedHashMap<>();
|
||||
value.put("before", before);
|
||||
value.put("after", after);
|
||||
fieldChanges.put(fieldName, value);
|
||||
}
|
||||
|
||||
private String normalizeNullableText(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String defaultText(String value) {
|
||||
return StringUtils.hasText(value) ? value : "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#################### 注册中心 + 配置中心相关配置 ####################
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: 192.168.1.103:18848 # Nacos 服务器地址
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
metadata:
|
||||
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
|
||||
config: # 【注册中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
|
||||
#################### 数据库相关配置 ####################
|
||||
# 数据源配置项
|
||||
autoconfigure:
|
||||
exclude:
|
||||
datasource:
|
||||
druid: # Druid 【监控】相关的全局配置
|
||||
web-stat-filter:
|
||||
enabled: true
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
allow: # 设置白名单,不填则允许所有访问
|
||||
url-pattern: /druid/*
|
||||
login-username: # 控制台管理用户名和密码
|
||||
login-password:
|
||||
filter:
|
||||
stat:
|
||||
enabled: true
|
||||
log-slow-sql: true # 慢 SQL 记录
|
||||
slow-sql-millis: 100
|
||||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
dynamic: # 多数据源配置
|
||||
druid: # Druid 【连接池】相关的全局配置
|
||||
initial-size: 5 # 初始连接数
|
||||
min-idle: 10 # 最小连接池数量
|
||||
max-active: 20 # 最大连接池数量
|
||||
max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟)
|
||||
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟)
|
||||
min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟)
|
||||
max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟)
|
||||
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true # 是否开启 PreparedStatement 缓存
|
||||
max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||
username: root
|
||||
password: njcnpqs
|
||||
|
||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||
data:
|
||||
redis:
|
||||
host: 192.168.1.22 # 地址
|
||||
port: 16379 # 端口
|
||||
database: 1 # 数据库索引
|
||||
# password: njcnpqs # 密码,建议生产环境开启
|
||||
|
||||
|
||||
#################### 监控相关配置 ####################
|
||||
|
||||
# Actuator 监控端点的配置项
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
|
||||
exposure:
|
||||
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 *,可以开放所有端点。
|
||||
|
||||
|
||||
# 日志文件配置
|
||||
logging:
|
||||
file:
|
||||
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
|
||||
|
||||
#################### RDMS 相关配置 ####################
|
||||
|
||||
# RDMS 配置项,设置当前项目所有自定义的配置
|
||||
rdms:
|
||||
demo: true # 开启演示模式
|
||||
@@ -0,0 +1,98 @@
|
||||
#################### 注册中心 + 配置中心相关配置 ####################
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
server-addr: 192.168.1.103:18848 # Nacos 服务器地址
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
metadata:
|
||||
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
|
||||
config: # 【注册中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
|
||||
#################### 数据库相关配置 ####################
|
||||
# 数据源配置项
|
||||
autoconfigure:
|
||||
exclude:
|
||||
datasource:
|
||||
druid: # Druid 【监控】相关的全局配置
|
||||
web-stat-filter:
|
||||
enabled: true
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
allow: # 设置白名单,不填则允许所有访问
|
||||
url-pattern: /druid/*
|
||||
login-username: # 控制台管理用户名和密码
|
||||
login-password:
|
||||
filter:
|
||||
stat:
|
||||
enabled: true
|
||||
log-slow-sql: true # 慢 SQL 记录
|
||||
slow-sql-millis: 100
|
||||
merge-sql: true
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
dynamic: # 多数据源配置
|
||||
druid: # Druid 【连接池】相关的全局配置
|
||||
initial-size: 5 # 初始连接数
|
||||
min-idle: 10 # 最小连接池数量
|
||||
max-active: 20 # 最大连接池数量
|
||||
max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟)
|
||||
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟)
|
||||
min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟)
|
||||
max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟)
|
||||
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true # 是否开启 PreparedStatement 缓存
|
||||
max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量
|
||||
primary: master
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://192.168.1.22:13306/rdms_v3?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||
username: root
|
||||
password: njcnpqs
|
||||
|
||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1 # 地址
|
||||
port: 16379 # 端口
|
||||
database: 1 # 数据库索引
|
||||
# password: njcnpqs # 密码,建议生产环境开启
|
||||
|
||||
|
||||
#################### 监控相关配置 ####################
|
||||
|
||||
# Actuator 监控端点的配置项
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
|
||||
exposure:
|
||||
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 *,可以开放所有端点。
|
||||
|
||||
|
||||
# 日志文件配置
|
||||
logging:
|
||||
level:
|
||||
# 配置本模块 MyBatis Mapper 打印日志
|
||||
com.njcn.rdms.module.project.dal.mysql: debug
|
||||
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR
|
||||
|
||||
# RDMS 配置项,设置当前项目所有自定义的本地扩展配置
|
||||
rdms:
|
||||
env: # 多环境的配置项
|
||||
tag: ${HOSTNAME}
|
||||
captcha:
|
||||
enable: false
|
||||
security:
|
||||
mock-enable: true
|
||||
access-log: # 访问日志的配置项
|
||||
enable: true
|
||||
@@ -0,0 +1,105 @@
|
||||
spring:
|
||||
application:
|
||||
name: rdms-project-server
|
||||
profiles:
|
||||
active: local
|
||||
main:
|
||||
allow-circular-references: true # 允许循环依赖,因为项目当前沿用三层架构组织方式。
|
||||
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如 Feign 等会存在重复定义的服务
|
||||
config:
|
||||
import:
|
||||
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
|
||||
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
|
||||
# Servlet 配置
|
||||
servlet:
|
||||
# 文件上传相关配置项
|
||||
multipart:
|
||||
max-file-size: 16MB # 单个文件大小
|
||||
max-request-size: 32MB # 设置总上传的文件大小
|
||||
# Jackson 配置项
|
||||
jackson:
|
||||
serialization:
|
||||
write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
|
||||
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式,例如 1611460870401
|
||||
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
|
||||
fail-on-empty-beans: false # 允许序列化无属性的 Bean
|
||||
# Cache 配置项
|
||||
cache:
|
||||
type: REDIS
|
||||
redis:
|
||||
time-to-live: 1h # 设置过期时间为 1 小时
|
||||
data:
|
||||
redis:
|
||||
repositories:
|
||||
enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度
|
||||
# 热部署配置
|
||||
devtools:
|
||||
restart:
|
||||
enabled: true
|
||||
|
||||
server:
|
||||
port: 48082
|
||||
|
||||
logging:
|
||||
file:
|
||||
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
|
||||
|
||||
--- #################### 接口文档配置 ####################
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true # 1. 是否开启 Swagger 接口文档的元数据
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面
|
||||
path: /swagger-ui
|
||||
default-flat-param-object: true
|
||||
|
||||
knife4j:
|
||||
enable: true
|
||||
setting:
|
||||
language: zh_cn
|
||||
|
||||
# MyBatis Plus 的配置项
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true # 虽然默认为 true,但是还是显示指定下。
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法
|
||||
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
|
||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||
banner: false # 关闭控制台的 Banner 打印
|
||||
type-aliases-package: ${rdms.info.base-package}.dal.dataobject
|
||||
encryptor:
|
||||
password: cDHvwsYb9eyLNBHp # 加解密秘钥,生产环境务必通过 Nacos 注入,切勿硬编码
|
||||
|
||||
mybatis-plus-join:
|
||||
banner: false # 关闭控制台的 Banner 打印
|
||||
|
||||
# VO 转换(数据翻译)相关
|
||||
easy-trans:
|
||||
is-enable-global: false # 默认禁用全局翻译,避免额外性能开销
|
||||
|
||||
--- #################### RDMS 相关配置 ####################
|
||||
rdms:
|
||||
info:
|
||||
version: 1.0.0
|
||||
base-package: com.njcn.rdms.module.project
|
||||
web:
|
||||
admin-ui:
|
||||
url: https://www.baidu.com # Admin 管理后台 UI 的占位地址,联调时替换成实际前端入口
|
||||
xss:
|
||||
enable: false
|
||||
exclude-urls:
|
||||
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
|
||||
swagger:
|
||||
title: 项目交付域管理后台
|
||||
description: 提供项目集、项目、产品、需求、任务、工单、执行等管理能力
|
||||
author: RDMS
|
||||
version: ${rdms.info.version}
|
||||
url: https://example.com
|
||||
email: dev@example.com
|
||||
license: Apache 2.0
|
||||
license-url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
debug: false
|
||||
Reference in New Issue
Block a user