feat(other): 产品基础功能提交

This commit is contained in:
2026-04-18 14:19:45 +08:00
parent 0c91f5deaa
commit 38c69c748c
75 changed files with 5139 additions and 1047 deletions

View File

@@ -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);
}
}

View File

@@ -0,0 +1,4 @@
/**
* Project API 实现包,放置对外暴露 RPC 接口的实现类
*/
package com.njcn.rdms.module.project.api;

View File

@@ -0,0 +1,4 @@
/**
* 管理端控制器包
*/
package com.njcn.rdms.module.project.controller.admin;

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
/**
* 应用端控制器包
*/
package com.njcn.rdms.module.project.controller.app;

View File

@@ -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;

View File

@@ -0,0 +1,4 @@
/**
* DTO、VO、DO 等对象转换包
*/
package com.njcn.rdms.module.project.convert;

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
/**
* 数据对象包
*/
package com.njcn.rdms.module.project.dal.dataobject;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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> {
}

View File

@@ -0,0 +1,4 @@
/**
* MyBatis Mapper 包
*/
package com.njcn.rdms.module.project.dal.mysql;

View File

@@ -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));
}
}

View File

@@ -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> {
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,4 @@
/**
* 持久层包
*/
package com.njcn.rdms.module.project.dal;

View File

@@ -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 {
}

View File

@@ -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();
}
};
}
}

View File

@@ -0,0 +1,4 @@
/**
* 服务层包
*/
package com.njcn.rdms.module.project.service;

View File

@@ -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);
}

View File

@@ -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 : "";
}
}

View File

@@ -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 # 开启演示模式

View File

@@ -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

View File

@@ -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