中州养老项目总结(持续更新)

day01

整体介绍

基础功能

中州养老系统为养老院量身定制开发专业的养老管理软件产品;涵盖来访管理、入退管理、在住管理、服务管理、财务管理等功能模块,涉及从来访参观到退住办理的完整流程。

中州养老项目分为两端,一个是管理后台,另外一个是家属端

  • 管理后台:养老院员工使用,入住、退住,给老人服务记录等
  • 家属端:养老院的老人家属使用,查看老人信息,缴费,下订单等

技术架构

  • 前端主要使用的Vue3+TS
  • 后端主要使用的是Springboot作为基础架构,当然后端也集成了很多其他的技术,比如有Mybatis、Swagger、Spring cache、Spring Security、Xxl-job、Activiti7
  • 数据存储主要使用到了MySQL和Redis
  • 使用了nginx来作为反向代理和前端的静态服务器
  • 其他技术:阿里云物联网平台IOT、对象存储OSS、微信登录、AI工具辅助开发等

DTO 、VO

接收对象和返回对象分别使用了DTO和VO

  • DTO:Data Transfer Object 数据传输对象:xxxDto或者xxxDTO,xxx为业务领域相关的名称, 用于接口的入参
  • VO:Value Object展示对象:xxxVO或者xxxVo,xxx一般为网页名称, 用于接口的出参

接口四要素

搞明白需求之后,我们下面就可以来设计接口了,一个接口包含了四个基本要素,分别是:请求路径、请求方式、接口入参、接口出参

请求路径 命名:以模块名称进行区分(英文)

请求方式(需要符合restFul风格)

  • 查询 GET
  • 新增 POST
  • 修改 PUT
  • 删除 DELETE

接口入参

  • 路径参数
    • 问号传参---->后端形参接收
    • path传参---->后端PathVariable注解接收
  • 请求体参数
    • 前端:json对象
    • 后端:对象接收,DTO

接口出参

  • 统一格式 {code:200,msg:"成功",data:{}}
  • 数据封装,一般为VO
    • 敏感数据过滤
    • 整合数据

床位接口增删改查

新增床位为例

BedController层

@PostMapping("/create")
public ResponseResult createBed(@RequestBody BedDto bedDto){
    bedService.addBed(bedDto);
    return ResponseResult.success();
}

BedService层

 @Override
public void addBed(BedDto bedDto) {
    Bed bed = new Bed();
    //使用了BeanUtils中的copyProerties方法 
    //也可以使用Bed bed = BeanUtil.toBean(bedDto, Bed.class);
    BeanUtils.copyProperties(bedDto, bed);  
    bed.setCreateTime(LocalDateTime.now());
    bed.setCreateBy(1L);
    bed.setBedStatus(0);
    try {
        bedMapper.addBed(bed);
    }catch (Exception e){
        throw new BaseException(BasicEnum.BED_INSERT_FAIL);
    }
}

BedMapper层

<insert id="addBed" parameterType="com.zzyl.entity.Bed">
    insert into bed(bed_number, sort, bed_status, room_id, create_by, remark, create_time)
    values (#{bedNumber}, #{sort}, #{bedStatus}, #{roomId}, #{createBy}, #{remark}, #{createTime})
</insert>

Swagger

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务

Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名knife4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!

目前,一般都使用knife4j框架。

项目中集成

导入 knife4j 的maven坐标(注意:由于knife4j是基于swagger的,所以也会自动导入swagger的依赖)

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

配置类

在配置类中加入 knife4j 相关配置,可以使knife4j在全局生效,目的就是项目中的所有接口都生成在线接口文档

@Configuration
@EnableConfigurationProperties(SwaggerConfigProperties.class)
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfig {

    @Autowired
    SwaggerConfigProperties swaggerConfigProperties;

    @Bean(value = "defaultApi2")
    @ConditionalOnClass(SwaggerConfigProperties.class)
    public Docket defaultApi2() {
        // 构建API文档  文档类型为swagger2
        return new Docket(DocumentationType.SWAGGER_2)
            .select()
            // 配置 api扫描路径
            .apis(RequestHandlerSelectors.basePackage(swaggerConfigProperties.getSwaggerPath()))
            // 指定路径的设置  any代表所有路径
            .paths(PathSelectors.any())
            // api的基本信息
            .build().apiInfo(new ApiInfoBuilder()
                // api文档名称
                .title(swaggerConfigProperties.getTitle())
                // api文档描述
                .description(swaggerConfigProperties.getDescription())
                // api文档版本
                .version("1.0") // 版本
                // api作者信息
                .contact(new Contact(
                    swaggerConfigProperties.getContactName(),
                    swaggerConfigProperties.getContactUrl(),
                    swaggerConfigProperties.getContactEmail()))
                .build());
    }

    /**
     * 增加如下配置可解决Spring Boot 6.x 与Swagger 3.0.0 不兼容问题
     **/
    @Bean
    public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
                                                                         ServletEndpointsSupplier servletEndpointsSupplier,
                                                                         ControllerEndpointsSupplier controllerEndpointsSupplier,
                                                                         EndpointMediaTypes endpointMediaTypes,
                                                                         CorsEndpointProperties corsProperties,
                                                                         WebEndpointProperties webEndpointProperties,
                                                                         Environment environment) {
        List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
        Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
        allEndpoints.addAll(webEndpoints);
        allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
        allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
        String basePath = webEndpointProperties.getBasePath();
        EndpointMapping endpointMapping = new EndpointMapping(basePath);
        boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
        return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(),
                new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null);
    }
    private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
        return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
    }
}

在上述代码中引用了一个配置,用来定制项目中的一些特殊信息,比如扫描的包、项目相关信息等

@Setter
@Getter
@NoArgsConstructor
@ToString
@ConfigurationProperties(prefix = "zzyl.framework.swagger")
public class SwaggerConfigProperties implements Serializable {

    /**
     * 扫描的路径,哪些接口需要使用在线文档
     */
    public String swaggerPath;

    /**
     * 项目名称
     */
    public String title;

    /**
     * 具体描述
     */
    public String description;

    /**
     * 组织名称
     */
    public String contactName;

    /**
     * 联系网址
     */
    public String contactUrl;

    /**
     * 联系邮箱
     */
    public String contactEmail;
}

所以上述代码具体的配置,是在application.yml文件中来定义

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
zzyl:
  framework:
    swagger:
      swagger-path: com.zzyl.controller
      title: 项目名称
      description: 具体描述
      contact-name: 组织名称
      contact-url: 联系网址
      contact-email: 联系邮箱

静态资源映射

如果想要swagger生效,还需要设置静态资源映射,否则接口文档页面无法访问,

找到配置类为:WebMvcConfig,添加如下代码

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    //支持webjars
    registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/");
    //支持swagger
    registry.addResourceHandler("swagger-ui.html")
            .addResourceLocations("classpath:/META-INF/resources/");
    //支持小刀
    registry.addResourceHandler("doc.html")
            .addResourceLocations("classpath:/META-INF/resources/");
}

常用注解

注解说明
@Api用在类上,描述Controller的作用
@ApiOperation用在方法上,说明方法的用途、作用
@ApiParam用在方法的参数上,描述单个形参的含义
@ApiImplicitParam用在方法上,描述单个形参的含义,与上面相比使用范围更广
@ApiModel用在类上,用对象来接收参数或者返回参数,描述类的含义
@ApiModelProperty用在类的属性上,用对象来接收参数或者返回参数,描述字段的含义

改造代码

BedController示例 , 改造完可以直接访问在线接口文档

@RestController
@RequestMapping("/bed")
@Api(tags = "床位管理相关接口")
public class BedController extends BaseController {

    @Autowired
    private BedService bedService;

    @GetMapping("/read/room/{roomId}")
    @ApiOperation(value = "根据房间id查询床位", notes = "传入房间id")
    public ResponseResult<List<BedVo>> readBedByRoomId(
            @ApiParam(value = "房间ID", required = true) @PathVariable("roomId") Long roomId) {
        List<BedVo> beds = bedService.getBedsByRoomId(roomId);
        return success(beds);
    }

    @PostMapping("/create")
    @ApiOperation(value = "创建床位", notes = "传入床位对象,包括床位号和所属房间号")
    public ResponseResult createBed(@RequestBody BedDto bedDto) {
        bedService.addBed(bedDto);
        return success();
    }
}

全局异常处理

全局异常处理逻辑

一般项目开发有两种异常:

  • 预期异常(程序员手动抛出)
  • 运行时异常

BaseException

基础异常,如果业务中需要手动抛出异常,则需要抛出该异常

@Getter
@Setter
public class BaseException extends RuntimeException {

    private BasicEnum basicEnum;

    public BaseException(BasicEnum basicEnum) {
        this.basicEnum = basicEnum;
    }
}

其中BaseException中的参数为一个枚举,可以在BasicEnum自定义业务中涉及到的异常

@Getter
@AllArgsConstructor
public enum BasicEnum implements IBasicEnum {

    SUCCEED(200, "操作成功"),
    SECURITY_ACCESSDENIED_FAIL(401, "权限不足!"),
    LOGIN_FAIL(401, "用户登录失败"),
    LOGIN_LOSE_EFFICACY(401, "登录状态失效,请重新登录"),
    SYSYTEM_FAIL(500, "系统运行异常"),
    
    //编码
    public final int code;
    //信息
    public final String msg;
}

GlobalExceptionHandler

全局异常处理器

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理自定义异常BaseException。
     * 返回自定义异常中的错误代码和错误消息。
     *
     * @param exception 自定义异常
     * @return 响应数据,包含错误代码和错误消息
     */
    @ExceptionHandler(BaseException.class)
    public ResponseResult<Object> handleBaseException(BaseException exception) {
        exception.printStackTrace();
        if (ObjectUtil.isNotEmpty(exception.getBasicEnum())) {
            log.error("自定义异常处理:{}", exception.getBasicEnum().getMsg());
        }

        return ResponseResult.error(exception.getBasicEnum());

    }

    /**
     * 处理其他未知异常。
     * 返回HTTP响应状态码500,包含错误代码和异常堆栈信息。
     *
     * @param exception 未知异常
     * @return 响应数据,包含错误代码和异常堆栈信息
     */
    @ExceptionHandler(Exception.class)
    public ResponseResult<Object> handleUnknownException(Exception exception) {
        exception.printStackTrace();
        if (ObjectUtil.isNotEmpty(exception.getCause())) {
            log.error("其他未知异常:{}", exception.getMessage());
        }
        return ResponseResult.error(500,exception.getMessage());
    }

}

day02

MySQL常见数据类型

MySQL中的数据类型有很多,主要分为三类:数值类型、字符串类型、日期时间类型。

数值类型

类型大小有符号(SIGNED)范围无符号(UNSIGNED)范围描述
TINYINT1byte(-128,127)(0,255)小整数值
SMALLINT2bytes(-32768,32767)(0,65535)大整数值
MEDIUMINT3bytes(-8388608,8388607)(0,16777215)大整数值
INT/INTEGER4bytes(-2147483648,2147483647)(0,4294967295)大整数值
BIGINT8bytes(-263,263-1)(0,2^64-1)极大整数值
FLOAT4bytes(-3.402823466 E+38,3.402823466351 E+38)0 和 (1.175494351 E-38,3.402823466 E+38)单精度浮点数值
DOUBLE8bytes(-1.7976931348623157 E+308,1.7976931348623157 E+308)0 和 (2.2250738585072014 E-308,1.7976931348623157 E+308)双精度浮点数值
DECIMAL依赖于M(精度)和D(标度)的值依赖于M(精度)和D(标度)的值小数值(精确定点数)

字符串类型

类型大小描述
CHAR0-255 bytes定长字符串(需要指定长度)
VARCHAR0-65535 bytes变长字符串(需要指定长度)
TINYBLOB0-255 bytes不超过255个字符的二进制数据
TINYTEXT0-255 bytes短文本字符串
BLOB0-65 535 bytes二进制形式的长文本数据
TEXT0-65 535 bytes长文本数据
MEDIUMBLOB0-16 777 215 bytes二进制形式的中等长度文本数据
MEDIUMTEXT0-16 777 215 bytes中等长度文本数据
LONGBLOB0-4 294 967 295 bytes二进制形式的极大文本数据
LONGTEXT0-4 294 967 295 bytes极大文本数据

char是定长字符串, 指定长度多长就会占用多少个字符, 和字段值的长度无关

varchar是变长字符串, 指定的长度为最大占用长度 , 相对来说, char的性能会更高些

日期时间类型

类型大小范围格式描述
DATE31000-01-01 至 9999-12-31YYYY-MM-DD日期值
TIME3-838:59:59 至 838:59:59HH:MM:SS时间值或持续时间
YEAR11901 至 2155YYYY年份值
DATETIME81000-01-01 00:00:00 至 9999-12-31 23:59:59YYYY-MM-DD HH:MM:SS混合日期和时间值
TIMESTAMP41970-01-01 00:00:01 至 2038-01-19 03:14:07YYYY-MM-DD HH:MM:SS混合日期和时间值,时间戳

Mybatis字段自动填充拦截器

在执行新增操作的时候, 我们并没有单独设置创建人和创建时间和修改时间的字段, 但是在表中存储的数据中, 这三个字段是有值的, 这是因为提供了Mybatis字段自动填充拦截器

首先在 intercept 包下创建了 AutoFillInterceptor 类 , 用来拦截mapper层对数据的updateinsert操作

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class AutoFillInterceptor implements Interceptor {

    private static final String CREATE_BY = "createBy";
    private static final String UPDATE_BY = "updateBy";

    private static final String CREATE_TIME = "createTime";
    private static final String UPDATE_TIME = "updateTime";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        // 获取用于描述SQL语句的映射信息
        MappedStatement ms = (MappedStatement) args[0];
        SqlCommandType sqlCommandType = ms.getSqlCommandType();

        // 获取sql参数实体 ParamMap
        Object parameter = args[1];

        if (parameter != null && sqlCommandType != null) {
            // 获取用户ID
            Long userId = loadUserId();

            if (SqlCommandType.INSERT.equals(sqlCommandType)) {
                // 插入操作
                if (parameter instanceof MapperMethod.ParamMap) {
                    // 批量插入的情况
                    MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameter;
                    ArrayList list = (ArrayList) paramMap.get("list");
                    list.forEach(v -> {
                        // 设置创建人和创建时间字段值
                        setFieldValByName(CREATE_BY, userId, v);
                        setFieldValByName(CREATE_TIME, LocalDateTime.now(), v);
                        setFieldValByName(UPDATE_TIME, LocalDateTime.now(), v);
                    });
                    paramMap.put("list", list);
                } else {
                    // 单条插入的情况
                    // 设置创建人和创建时间字段值
                    setFieldValByName(CREATE_BY, userId, parameter);
                    setFieldValByName(CREATE_TIME, LocalDateTime.now(), parameter);
                    setFieldValByName(UPDATE_TIME, LocalDateTime.now(), parameter);
                }
            } else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
                // 更新操作
                // 设置更新人和更新时间字段值
                setFieldValByName(UPDATE_BY, userId, parameter);
                setFieldValByName(UPDATE_TIME, LocalDateTime.now(), parameter);
            }
        }

        // 继续执行原始方法
        return invocation.proceed();
    }

    /**
     * 通过反射设置实体的字段值
     * @param fieldName 字段名
     * @param fieldVal  字段值
     * @param parameter 实体对象
     */
    private void setFieldValByName(String fieldName, Object fieldVal, Object parameter) {
        MetaObject metaObject = SystemMetaObject.forObject(parameter);
        if (fieldName.equals(CREATE_BY)) {
            Object value = metaObject.getValue(fieldName);
            if (ObjectUtil.isNotEmpty(value)) {
                return;
            }
        }

        if (metaObject.hasSetter(fieldName)) {
            metaObject.setValue(fieldName, fieldVal);
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            // 对目标对象进行包装,返回代理对象
            return Plugin.wrap(target, this);
        }
        // 非 Executor 类型的对象,直接返回原始对象
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        // 读取配置文件中的属性,此处没有使用
    }

    /**
     * 获取当前用户的ID,用于填充创建人和更新人字段的值
     *
     * @return 当前用户ID
     */
    public static Long loadUserId() {
        // 从 ThreadLocal 中获取用户ID
        Long userId = UserThreadLocal.getUserId();
        // 如果 ThreadLocal 中不存在用户ID,则从管理用户ID中获取
        if (ObjectUtil.isNotEmpty(userId)) {
            return userId;
        }
        userId = UserThreadLocal.getMgtUserId();
        // 如果管理用户ID也不存在,则默认返回ID为1的用户
        if (!EmptyUtil.isNullOrEmpty(userId)) {
            return userId;
        }
        return 1L;
    }
}

然后再 config 包下创建了 MybatisConfig 类 , 使 AutoFillInterceptor拦截器生效

/**
 *  webMvc高级配置
 */
@Configuration
public class MybatisConfig {
    /***
     *  自动填充拦截器
     */
    @Bean
    public AutoFillInterceptor autoFillInterceptor(){
        return new AutoFillInterceptor();
    }
}

在数据库的建表语句中 , 也可以加入对创建时间和更新时间的设置 ( 只是举例方法 )

create table nursing_project
(
    id                  bigint auto_increment comment '编号'  primary key,
    create_time         datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
)

当然, 对这些字段的拦截操作使用AOP也可以实现, 这里不再举例

TDesign

鬼扯一下 : ) TDesign 具有统一的价值观, 一致的设计语言和视觉风格, 帮助用户形成连续、统一的体验认知。在此基础上,TDesign 提供了开箱即用的 UI 组件库、设计指南和相关设计资产,以优雅高效的方式将设计和研发从重复劳动中解放出来,同时方便大家在 TDesign 的基础上扩展,更好的的贴近业务需求。

官网地址:https://tdesign.tencent.com/

组件使用可以参考官方文档, 这里不做解释

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值