Skip to content

约定

约定是团队的默契,为我们的工作提供了一致性和可预测性。统一的规则和准则确保了代码的清晰、高效和无冲突。因此,约定在团队合作和项目开发中至关重要。

在框架的制作过程中,我们为了满足一些常见需求,对一些部分进行了处理,形成了一些潜在的约定。尽管这些约定并非强制性的,但它们有助于提高开发效率和代码质量

后端

1. 逻辑删除

在创建数据表时,我们推荐使用以下命名和物理类型来实现逻辑删除:

sql
`del_flag` enum('T','F') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'F' COMMENT '删除与否',
`delete_id` int DEFAULT NULL COMMENT '(逻辑)删除人',
`delete_time` datetime DEFAULT NULL COMMENT '(逻辑)删除时间',

这样的设计可以清晰地表示记录的删除状态,并且在数据库层面提供了直观的标识。

除了数据库,我们也需要对PO类进行处理:

java
@Table("sys_user")
@Schema(description = "系统用户表")
public class SysUser implements Serializable {
	
	...

    @Column(isLogicDelete = true)     // 通过@Column来指定逻辑删除属性
    private String delFlag;

    ...

}

我们的Mybatis-Flex也指定了逻辑删除标识为TF字符。

yaml
mybatis-flex:
  configuration:
    map-underscore-to-camel-case: true
    jdbc-type-for-null: null
    auto-mapping-behavior: full
    auto-mapping-unknown-column-behavior: none
    cache-enabled: false
  global-config:
    deleted-value-of-logic-delete: "T" # 逻辑删除字段的已删除值
    normal-value-of-logic-delete: "F"  # 逻辑删除字段的未删除值

同时,我们也提供了逻辑删除数据填充的支持:

java
public class EntityLogicDeleteListener extends DefaultLogicDeleteProcessor {

    private static final String FIELD_DELETE_TIME = "delete_time";
    private static final String FIELD_DELETE_ID = "delete_id";

    @Override
    public String buildLogicDeletedSet(String logicColumn, TableInfo tableInfo, IDialect iDialect) {
        StringBuilder sqlBuilder = new StringBuilder();
        sqlBuilder.append(iDialect.wrap(logicColumn))
                .append(EQUALS)
                .append(prepareValue(getLogicDeletedValue()));

        List<String> columns = Arrays.asList(tableInfo.getAllColumns());

        if (columns.contains(FIELD_DELETE_TIME)) {
            sqlBuilder.append(", ")
                    .append(iDialect.wrap(FIELD_DELETE_TIME))
                    .append(EQUALS)
                    .append("now()");
        }

        boolean isLogin = StpUtil.isLogin();
        if (isLogin && columns.contains(FIELD_DELETE_ID)) {
            sqlBuilder.append(", ")
                    .append(iDialect.wrap(FIELD_DELETE_ID))
                    .append(EQUALS)
                    .append(LoginUtils.getLoginUser().getUserInfo().getId());
        }

        return sqlBuilder.toString();
    }

    private static Object prepareValue(Object value) {
        return (!(value instanceof Number) && !(value instanceof Boolean)) ? "'" + value + "'" : value;
    }
}

2. 数据填充

当我们操作某些表时,有时候我们有可能需要根据某些情况进行一些更新操作。 例如 Insert操作需要更新create_id 和 create_time、Update操作要更新update_id和 update_time。

针对于常见的这四个属性,框架提供了默认的数据填充支持。

在创建数据表时,我们推荐使用以下命名和物理类型来实现数据填充

以下四个属性根据业务情况自行组合,系统会自动根据命名匹配并填充

sql
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `create_id` bigint DEFAULT NULL COMMENT '创建人',
  `update_id` bigint DEFAULT NULL COMMENT '更新人',

针对sys_client表的数据填充需求,我们建议按照以下示例对PO类进行命名和类型处理

注意!!

必须遵照以下类型,否则动态填充会异常!!!

java
@Table(value = "sys_client", onInsert = EntityChangeListener.class, onUpdate = EntityChangeListener.class)
public class SysClient implements Serializable {
	...
    
    @Schema(description ="创建者")
    private Long createId;

    @Schema(description ="创建时间")
    private LocalDateTime createTime;

    @Schema(description ="更新者")
    private Long updateId;

    @Schema(description ="更新时间")
    private LocalDateTime updateTime;     
    ...    
}

这样的处理方式可以清晰地描述这些属性的作用,并为数据填充提供了明确的指导。

那么我们系统中是如何实现“动态填充”的呢?

java
public class EntityChangeListener implements InsertListener, UpdateListener, SetListener {

    @Override
    public void onInsert(Object o) {
        setPropertyIfPresent(o, "createId", StpUtil.getLoginIdAsLong());
        setPropertyIfPresent(o, "createTime", LocalDateTime.now());
    }

    @Override
    public void onUpdate(Object o) {
        setPropertyIfPresent(o, "updateId", StpUtil.getLoginIdAsLong());
        setPropertyIfPresent(o, "updateTime", LocalDateTime.now());
    }

    @Override
    public Object onSet(Object entity, String property, Object value) {
        return value;
    }

    private void setPropertyIfPresent(Object object, String propertyName, Object propertyValue) {
        try {
            // 获取对象的 Class 对象
            Class<?> clazz = object.getClass();
            // 获取对象的字段
            Field field = clazz.getDeclaredField(propertyName);
            // 设置字段为可访问,以便访问私有字段
            field.setAccessible(true);
            // 设置字段的值
            field.set(object, propertyValue);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            // 如果字段不存在,则忽略异常
            log.warn(" Fill EntityChangeField failed; Property {} not found. ", propertyName);
        }
    }

}

在我们的系统中,我们通过一个名为 EntityChangeListener 的类来实现动态填充的功能。这个类实现了 InsertListenerUpdateListenerSetListener 接口,分别用于在插入、更新和设置操作时进行监听和处理。

onInsert 方法中,我们会动态设置对象的 createIdcreateTime 属性,以记录创建者和创建时间。而在 onUpdate 方法中,我们会动态设置对象的 updateIdupdateTime 属性,以记录最后更新者和更新时间。

为了实现动态设置属性的功能,我们使用了反射机制来访问对象的属性并设置属性的值。具体来说,我们通过获取对象的 Class 对象和字段对象,并设置字段为可访问状态,然后利用反射机制设置字段的值。

需要注意的是,我们在 setPropertyIfPresent 方法中对属性的设置进行了容错处理,如果属性不存在,则会忽略异常并记录警告日志,以确保程序的稳定性和可靠性。

通过这种方式,我们实现了动态填充的功能,为我们的系统提供了便利和灵活性。


3. SQL的编写

我们的框架集成了Mybatis-Flex,这不仅支持传统的原生XML编写SQL,还引入了更为高效灵活的查询构建方式。

您可以通过以下两种方式来构建SQL查询:

  1. Mybatis-Flex:一种更现代、易于理解和维护的方法,适用于绝大多数查询场景。
  2. 原生XML:在特定情况下,如果需要,您也可以使用传统的XML方式。

虽然我们在这里不展开介绍Mybatis-Flex的具体用法,但强烈推荐您优先采用Mybatis-Flex。它能够满足绝大部分的查询需求,并且通常更加简洁和直观。

如需深入了解Mybatis-Flex的使用方法,您可以访问官方网站获取详细信息。

在遇到特殊情况,原生XML方式可能是必要的选择。但请记住,大多数时候,Mybatis-Flex将是您的首选。


4. 数据权限

详见:数据权限