springboot+mybatis拦截器实现水平分表操作

Springboot整合ShardingSphere实现分库分表,垂直拆分、水平拆分、公共表的处理 ShardingSphere实现分库分表、水平拆分、垂直拆分、公共部分 阅读详情

1.前言

业务飞速发展导致了数据规模的急速膨胀,单机数据库已经无法适应互联网业务的发展。由于MySQL采用 B+树索引,数据量超过阈值时,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降;高并发访问请求也使得集中式数据库成为系统的最大瓶颈。我们团队结合公司业务背景商议最终选定一份表的形式进行解决这一瓶颈问题,由我作为主要开发主导完成,故写篇博客记录沉淀。

2.MyBatis 允许使用插件来拦截的方法

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  2. ParameterHandler (getParameterObject, setParameters)
  3. ResultSetHandler (handleResultSets, handleOutputParameters)
  4. StatementHandler (prepare, parameterize, batch, update, query)

总体概括为:

  1. 拦截执行器的方法
  2. 拦截参数的处理
  3. 拦截结果集的处理
  4. 拦截Sql语法构建的处理

这4各方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler。

3、Interceptor接口

package org.apache.ibatis.plugin;  
    
import java.util.Properties;  
    
public interface Interceptor {  

//intercept方法就是要进行拦截的时候要执行的方法。
  Object intercept(Invocation invocation) throws Throwable;  

//plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。
  Object plugin(Object target);  
    
//setProperties方法是用于在Mybatis配置文件中指定一些属性的。
  void setProperties(Properties properties);  
    
}  

 4分表实现

4.1、大体思路

分表的表结构已经预设完毕,所以现在我们只需要在进行增删改查的时候直接一次锁定目标表,然后替换目标sql。

4.2、逐步实现

4.2.1 Mybatis如何找到我们新增的拦截服务

对于拦截器Mybatis为我们提供了一个Interceptor接口,前面有提到,通过实现该接口就可以定义我们自己的拦截器。自定义的拦截器需要交给Mybatis管理,这样才能使得Mybatis的执行与拦截器的执行结合在一起,即,利用springboot把自定义拦截器注入。

package com.shinemo.insurance.common.config;

import org.apache.ibatis.plugin.Interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TableShardConfig {

    /**
     * 注册插件
     */
    @Bean
    public Interceptor tableShardInterceptor() {
        return new TableShardInterceptor();
    }

}

4.2.2 应该拦截什么样的对象

因为拦截器是全局拦截的,我们只需要拦截我们需要拦截的mapper,故需要用注解进行标识

package com.shinemo.insurance.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = { ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShard {

    // 表前缀名
    String tableNamePrefix();

    // 值
    String value() default "";

    // 是否是字段名,如果是需要解析请求参数改字段名的值(默认否)
    boolean fieldFlag() default false;

}

我们只需要把这个注解标识在我们要拦截的mapper上

@Mapper
@TableShard(tableNamePrefix = "t_insurance_video_people_", value = "deviceId", fieldFlag = true)
public interface InsuranceVideoPeopleMapper {

//VideoPeople对象中包含deviceId字段
  int insert(VideoPeople videoPeople);
}

4.2.3 实现自定义拦截器

package com.shinemo.insurance.common.config;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;

import com.shinemo.insurance.common.annotation.TableShard;
import com.shinemo.insurance.common.util.HashUtil;

import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.SystemMetaObject;

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,
                                                                                     Integer.class }) })
public class TableShardInterceptor implements Interceptor {

    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        // MetaObject是mybatis里面提供的一个工具类,类似反射的效果
        MetaObject metaObject = getMetaObject(invocation);
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        MappedStatement mappedStatement = (MappedStatement) metaObject
            .getValue("delegate.mappedStatement");

        // 获取Mapper执行方法
        Method method = invocation.getMethod();

        // 获取分表注解
        TableShard tableShard = getTableShard(method, mappedStatement);

        // 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑
        if (tableShard == null) {
            return invocation.proceed();
        }

        //获取值,此值就是拿的注解上value值,注解上value设定的值,并在传入对象中获取,根据业务可以选择适当的值即可,我选取此值的目的是同一台设备的值存入一张表中,有hash冲突的值也存在一张表中
        String value = tableShard.value();
        //value是否字段名,如果是,需要解析请求参数字段名的值
        boolean fieldFlag = tableShard.fieldFlag();

        if (fieldFlag) {
            //获取请求参数
            Object parameterObject = boundSql.getParameterObject();

            if (parameterObject instanceof MapperMethod.ParamMap) {
                // ParamMap类型逻辑处理
                MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;
                // 根据字段名获取参数值
                Object valueObject = parameterMap.get(value);
                if (valueObject == null) {
                    throw new RuntimeException(String.format("入参字段%s无匹配", value));
                }
                //替换sql
                replaceSql(tableShard, valueObject, metaObject, boundSql);

            } else {
                // 单参数逻辑

                //如果是基础类型抛出异常
                if (isBaseType(parameterObject)) {
                    throw new RuntimeException("单参数非法,请使用@Param注解");
                }

                if (parameterObject instanceof Map) {
                    Map<String, Object> parameterMap = (Map<String, Object>) parameterObject;
                    Object valueObject = parameterMap.get(value);
                    //替换sql
                    replaceSql(tableShard, valueObject, metaObject, boundSql);
                } else {
                    //非基础类型对象
                    Class<?> parameterObjectClass = parameterObject.getClass();
                    Field declaredField = parameterObjectClass.getDeclaredField(value);
                    declaredField.setAccessible(true);
                    Object valueObject = declaredField.get(parameterObject);
                    //替换sql
                    replaceSql(tableShard, valueObject, metaObject, boundSql);
                }
            }

        } else {//无需处理parameterField
            //替换sql
            replaceSql(tableShard, value, metaObject, boundSql);
        }
        //把原有的简单查询语句替换为分表查询语句了,现在是时候将程序的控制权交还给Mybatis下一个拦截器处理
        return invocation.proceed();
    }

    /**
     * @description:
     * @param target
     * @return: Object
     */
    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    /**
     * @description: 基本数据类型验证,true是,false否
     * @param object
     * @return: boolean
     */
    private boolean isBaseType(Object object) {
        if (object.getClass().isPrimitive() || object instanceof String || object instanceof Integer
            || object instanceof Double || object instanceof Float || object instanceof Long
            || object instanceof Boolean || object instanceof Byte || object instanceof Short) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * @description: 替换sql
     * @param tableShard 分表注解
     * @param value      值
     * @param metaObject mybatis反射对象
     * @param boundSql   sql信息对象
     * @return: void
     */
    private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject,
                            BoundSql boundSql) {
        String tableNamePrefix = tableShard.tableNamePrefix();
        //        // 获取策略class
        //        Class<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();
        //        // 从spring ioc容器获取策略类
        //        ITableShardStrategy tableShardStrategy = SpringBeanUtil.getBean(strategyClazz);
        // 生成分表名
        String shardTableName = generateTableName(tableNamePrefix, (String) value);
        // 获取sql
        String sql = boundSql.getSql();
        // 完成表名替换
        metaObject.setValue("delegate.boundSql.sql",
            sql.replaceAll(tableNamePrefix, shardTableName));
    }

    /**
     * 生成表名
     *
     * @param tableNamePrefix 表名前缀
     * @param value           价值
     * @return {@link String}
     */
    private String generateTableName(String tableNamePrefix, String value) {

//我们分了1024张表
        int prime = 1024;
//hash取模运算过后,锁定目标表
        int rotatingHash = HashUtil.rotatingHash(value, prime);
        return tableNamePrefix + rotatingHash;
    }

    /**
     * @description: 获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果
     * @param invocation
     * @return: MetaObject
     */
    private MetaObject getMetaObject(Invocation invocation) {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // MetaObject是mybatis里面提供的一个工具类,类似反射的效果
        MetaObject metaObject = MetaObject.forObject(statementHandler,
            SystemMetaObject.DEFAULT_OBJECT_FACTORY,
            SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);

        return metaObject;
    }

    /**
     * @description: 获取分表注解
     * @param method
     * @param mappedStatement
     * @return: TableShard
     */
    private TableShard getTableShard(Method method,
                                     MappedStatement mappedStatement) throws ClassNotFoundException {
        String id = mappedStatement.getId();
        // 获取Class
        final String className = id.substring(0, id.lastIndexOf("."));
        // 分表注解
        TableShard tableShard = null;
        // 获取Mapper执行方法的TableShard注解
        tableShard = method.getAnnotation(TableShard.class);
        // 如果方法没有设置注解,从Mapper接口上面获取TableShard注解
        if (tableShard == null) {
            // 获取TableShard注解
            tableShard = Class.forName(className).getAnnotation(TableShard.class);
        }
        return tableShard;
    }

}

利用Mybatis拦截器对数据库水平分表 首先你要知道在哪些sql上面要处理分表?你可能需要一个注解: package com.dusk.domyself.stock.common.split; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPo 阅读详情

相关推荐

springboot简单的分表插件

业务上做了水平分表,公司基础架构没有提供分表中间件,开源的中间件用起来门槛高,有较大的学习成本。自己基于springboot+mybatis+jsqlparser实现了一个简单的分表插件。 笼统来说就是拦截SQL,分析SQL,替换tableName,返回重写后的SQL给Mybatis 项目里引入maven依赖: <dependency> <g...

XxieYyuHhui的博客 858

Mybaits拦截器实现水平分表

*** 表名*//*** 算法策略*/// 需要分表的table的Mapper接口1.Mapper文件问题老系统比较混乱,存在多表关联查询,有些Mapper.xml对象对应的sql不是唯一表。这里需要注意,因为注解是针对整个Mapper文件的,只要是上面的sql都会拦截。但是需要分表的sql都必须要有分表字段。需要避免不分表的sql走策略算法。最好将需要分表的表拆成单表,逻辑在代码里处理。2.分页插件pagehelper导致自定义插件无效。

2302_77659577的博客 1718

使用Spring Boot和MyBatis实现水平分表拦截器

您可以使用Spring Initializr(https://start.spring.io/)来生成一个基本的Spring Boot项目骨架,或者使用您喜欢的IDE工具创建一个空的Spring Boot项目。假设我们有一个名为"users"的表,我们将其拆分为多个小表,命名规则为"users_0"、“users_1”、"users_2"等。请确保在数据库中创建这些分表。创建一个名为"ShardingInterceptor"的类,它将实现MyBatis的Interceptor接口,并重写其中的方法。

BxuqBlockchain的博客 314

使用Mybatis拦截器实现数据分表

文章目录使用Mybatis拦截器进行数据分表实战分表注解Mybaits的拦截器实现参考 使用Mybatis拦截器进行数据分表实战 在项目中我们是用Mybatis + TKMapper + MYSQL存储了一些消息日志,但是现在随着业务数据暴增, 单表支撑不了这么多数据. 因此决定把表做水平切分, 按照月份来给表进行切分。这样当我们需要housekeep数据的时候,就可以直接drop掉表了,不论是备份还是删除效率都会比较高 那我们就会是用到Mybaits的拦截器 Mybatis插件机制:   Mybatis

C. 4091

5分钟搞定Vivado IP核的VCS仿真:从Export脚本到Verdi波形查看全流程

本文详细介绍了如何利用Vivado的Export Simulation功能,快速搭建Vivado IP核的VCS与Verdi联合仿真环境。通过自动化脚本生成、FSDB波形配置及高效调试技巧,实现从工程导出到波形查看的全流程,大幅提升FPGA验证效率,尤其适合IC验证工程师和竞赛团队。

weixin_29306261的博客 481

手把手教你用VSCode+SDL在Ubuntu上跑LVGL8.2模拟器(附插件清单)

本文详细介绍了在Ubuntu系统上使用VSCode和SDL库搭建LVGL8.2模拟器环境的完整流程,特别适合嵌入式开发者(如全志F1C100s平台)进行UI设计验证。从环境配置、项目编译到VSCode深度集成,提供了实用技巧和常见问题解决方案,帮助开发者实现'先模拟后真机'的高效开发模式。

weixin_33737134的博客 358

玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

利用Mybatis拦截器对数据库水平分表

Java学习之道 4968

超大规模AI集群跨数据中心互联:Scale-Across技术实现与部署实践

分布式系统通过将计算任务分散到多个节点协同执行,实现算力的横向扩展。其核心原理在于网络通信、数据同步和资源调度机制的技术协同。在AI基础设施领域,分布式架构能有效突破单数据中心算力瓶颈,支撑万亿参数大模型训练。特别是面对长距离传输时延和带宽稳定性挑战,需要采用RDMA、确定性网络等关键技术。Scale-Across作为分布式系统的高级形态,通过跨地域多数据中心互联,构建逻辑统一的AI算力池,为超大规模AI训练和全球推理服务提供核心支撑。本文聚焦AI集群跨数据中心互联的技术路径,涵盖全栈优化、网络自治等实践方

chouxuyi9789的博客 400

mysql水平分表 mybatis_玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

利用Mybatis拦截器对数据库水平分表需求描述当数据量比较多时,放在一个表中的时候会影响查询效率;或者数据的时效性只是当月有效的时候;这时我们就会涉及到数据库的分表操作了。当然,你也可以使用比较完善的第三方组件:sharding-jdbc来实现;但是你使用后会发现,貌似对oracle的兼容性不是很好。所以最后我还是决定使用Mybatis拦截器对数据库进行水平分表。为什么要选用Mybatis拦截器...

weixin_39534395的博客 2389

学习利用ce修改游戏生命参数

易语言代码 游戏的简陋界面 用ce查找存储这个1000即生命值的内存地址(得先附加到目标程序上,点击左上角那个电脑按钮) 看到有很多值为1000的,但哪个是生命值呢?真相只有一个 我们点击一下打我这个按钮,看下面的ce界面上地址为004A65C0处的值变了,所以这个就是我们要找的生命值 接下来我们对生命值进行修改,修改为99

zsd747289639的博客 5万+

ABAP 里的 XML 与 JSON 处理,一套从接口落地到 Clean Core 的完整参考

ABAP 里的 XML 和 JSON 处理并不只是几段工具类调用。iXML、sXML、、XCO、、code page conversion、GZIP、对象序列化,这些能力背后对应的是不同的数据规模、访问模式、接口契约和运行时边界。小报文、复杂树结构、需要修改节点,iXML 很顺手。大报文、顺序读写、内存敏感,sXML 更可靠。ABAP 结构和 XML、JSON 之间要做标准转换,是主干。现代 JSON API、ABAP Cloud、命名转换,XCO 值得优先考虑。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 224

mybatis 拦截器_玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

利用Mybatis拦截器对数据库水平分表需求描述当数据量比较多时,放在一个表中的时候会影响查询效率;或者数据的时效性只是当月有效的时候;这时我们就会涉及到数据库的分表操作了。当然,你也可以使用比较完善的第三方组件:sharding-jdbc来实现;但是你使用后会发现,貌似对oracle的兼容性不是很好。所以最后我还是决定使用Mybatis拦截器对数据库进行水平分表。为什么要选用Mybatis拦截器...

weixin_39737001的博客 251

spring boot 拦截器排除拦截某些链接_玩转SpringBoot之整合Mybatis拦截器对数据库水平分表...

利用Mybatis拦截器对数据库水平分表需求描述当数据量比较多时,放在一个表中的时候会影响查询效率;或者数据的时效性只是当月有效的时候;这时我们就会涉及到数据库的分表操作了。当然,你也可以使用比较完善的第三方组件:sharding-jdbc来实现;但是你使用后会发现,貌似对oracle的兼容性不是很好。所以最后我还是决定使用Mybatis拦截器对数据库进行水平分表。为什么要选用Mybatis拦截器...

weixin_31201481的博客 388

TI硬件笔试面试题型解析上

摘要: 本专栏聚焦大厂硬件笔试面试题解析,当前第14期以德州仪器(TI)为例,涵盖模拟电路与数字电路核心考点。模拟电路部分涉及最大功率传递定理、运放设计(虚短/虚断)、共模抑制比(CMRR)及高频PCB信号完整性;数字电路包括CMOS与TTL对比、建立/保持时间、竞争冒险消除、D触发器分频及“线与”逻辑实现。文章强调通过例题掌握解题方法与知识点,助力读者应对硬件工程岗位的笔试与面试挑战,附常见考点总结与实用建议。全文注重理论与实践结合,提供典型题型解析与设计思路。(150字)

启芯硬件的博客 950

用一台笔记本跑起 SAP HANA:从 express edition 的定位,到本地硬件门槛与安装实操

SAP HANA是面向个人开发者、初创团队与教学科研场景的精简发行版,保留了in-memory列存引擎、SQLScriptAFLGraphSpatial等核心能力,可免费用于开发与一定规模的生产用途(上限为 32 GB RAM)。它最大的特征,是能在资源受限的laptop或上快速落地,让你在本地完成从建模、开发到测试的完整闭环。SAP在交付形态上,提供了多种安装包:预配置的VM OVA、面向Linux的二进制安装包,以及历史上出现过的容器化方案;你可以按现有设备与熟悉的运维方式择优选择。官方文档把VM。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 534

水平分表(Mybatis插件实现)

水平分表(Mybatis插件实现) tips:(这里可以根据实际业务进行选择,比如每个月数据不是很平均,就可以按照预算数据量来进行分割 例子,按照id mod 100 把表分成100张,然后进行表名+ (id mod 100)取 ) 这里为了简单起见,假定月份数据量相差不大 将一张大表按照月份划分 fee_202001,fee_202002,fee_202003… 表中只有三个字段:id,费用金额fee_amt,费用日期fee_date 其中gmt_create,gmt_modified是阿里出的java泰

小菜鸟的大博客 1144

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

SpringBoot集成Mybatis多数据源实现分表分库

SpringBoot实现分库分表

zy_javaAndweb的博客 2876

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

Mybatis拦截器实现分表和sql慢日志监控

本文介绍了MyBatis拦截器在SpringBoot项目中的应用,重点讲解了其在分表处理与SQL慢日志监控中的实现。MyBatis提供Executor、ParameterHandler等四个拦截点,通过动态代理技术实现拦截功能。文章详细展示了基于时间字段的水平分表方案,包括自定义注解、分表策略和拦截器实现;同时提供了SQL执行耗时监控的实现方法。这些实践案例展示了MyBatis拦截器在日志记录、性能优化等场景的灵活应用,为开发者提供了实用的技术参考方案。

m0_74399067的博客 494

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

SpringBoot-Hikari-Mybatis集成,整合sharding-jdbc,实现分库分表

一、概述 为何要使用HiKariCP? 字节码精简:优化代码,直到编译后的字节码最少,这样,CPU缓存可以加载更多的程序代码; 优化代理和拦截器:减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一; 自定义数组类型(FastStatementList)代替ArrayList:避免每次get()调用都要进行range check,避免调用rem...

IT瘾君子的博客 4521

玩转SpringBoot之整合Mybatis拦截器对数据库水平分表(转)

https://www.cnblogs.com/mmzs/p/11174551.html#_label0

Strom72的博客 468

mybatis垂直分库_使用Spring AbstractRoutingDataSource实现读写分离,实现垂直分库和水平分表...

1.使用场景分库分表的使用场景:对大数据量的表或者库进行分割,将数据依据逻辑规则分别放置不同的库,减轻单库单表的压力。具体又分为垂直分库,水平分库,垂直分表,水平分表。读写分离的应用场景:应用程序在进行读取数据库数据和写入数据库数据时,分别操作不同的数据库,从数据库及时数据同主数据库保持一致,就可以提高数据库的访问能力。2.实现方式a. 可以使用mycat(对此不熟,仅仅听说过)b. 使用shar...

weixin_36137385的博客 206

SpringBoot+Vue校园外卖系统开发实战

现代Web开发中,前后端分离架构已成为主流技术方案,其核心价值在于提升开发效率和系统可维护性。SpringBoot作为Java生态的明星框架,通过自动配置和起步依赖简化了后端服务搭建;Vue.js则以其响应式特性和组合式API革新了前端开发体验。在校园外卖系统这类实时性要求较高的场景中,WebSocket实现了订单状态的实时推送,而MyBatis的动态SQL能力则高效处理了复杂查询需求。通过SpringCache缓存机制和MySQL索引优化,系统性能得到显著提升。该项目典型展示了全栈技术栈在解决实际业务问题

anjueci1221的博客 302

SpringBoot考勤管理系统开发与部署全攻略

企业考勤管理系统是数字化转型的核心组件,基于SpringBoot框架开发能显著提升开发效率。系统采用三层架构设计,结合MySQL分库分表和Redis缓存实现高性能数据处理。通过Drools规则引擎支持灵活的考勤规则配置,满足多班次排班等复杂场景。技术实现上整合了Spring Security安全框架和MyBatis-Plus持久层方案,确保系统安全性和可维护性。典型应用场景包括考勤记录采集、异常考勤自动判定和统计报表生成,特别适合中大型企业快速构建数字化考勤平台。

weixin_34026276的博客 366
上一篇: 记 SpringBoot 使用@RequestBody 接收不到参数
下一篇: SpringBoot 多环境配置文件切换
jothan_lee
博客等级 码龄9年 30粉丝 58原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值