# Orange **Repository Path**: swingfer/Orange ## Basic Information - **Project Name**: Orange - **Description**: 橘子生成器,为CMS系统设计的专属代码生成器,独创关联表跳跃设计,遵循阿里巴巴规范,使您的开发事半功倍!多多fork,Start 鸭!欢迎参与贡献!蟹蟹! - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: https://gitee.com/qiu-qian/Orange - **GVP Project**: No ## Statistics - **Stars**: 10 - **Forks**: 7 - **Created**: 2020-07-18 - **Last Updated**: 2025-08-01 ## Categories & Tags **Categories**: code-generator **Tags**: None ## README # Orange 为CMS系统设计的专属代码生成器,独创关联表跳跃设计,遵循阿里巴巴规范,使您的开发事半功倍! * [Orange](#Orange) * [项目亮点](#%E9%A1%B9%E7%9B%AE%E4%BA%AE%E7%82%B9) * [使用教程](#%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B) * [配置与启动](#%E9%85%8D%E7%BD%AE%E4%B8%8E%E5%90%AF%E5%8A%A8) * [关联表的跳跃查询设计](#%E5%85%B3%E8%81%94%E8%A1%A8%E7%9A%84%E8%B7%B3%E8%B7%83%E6%9F%A5%E8%AF%A2%E8%AE%BE%E8%AE%A1) * [生成代码说明(参考)](#%E7%94%9F%E6%88%90%E4%BB%A3%E7%A0%81%E8%AF%B4%E6%98%8E%E5%8F%82%E8%80%83) * [BasicDO\.java](#basicdojava) * [SysUserDO\.java](#sysuserdojava) * [SysLogLoginDO\.java](#sysloglogindojava) * [BasicDAO\.java](#basicdaojava) * [SysUserDAO\.java](#sysuserdaojava) * [BasicLinkDAO\.java](#basiclinkdaojava) * [SysUserRoleLinkDAO\.java](#sysuserrolelinkdaojava) * [SysLogLoginDAO\.xml](#sysloglogindaoxml) * [BasicDAO\.xml](#basicdaoxml) * [SysUserDAO\.xml](#sysuserdaoxml) * [SysUserRoleLinkDAO\.xml](#sysuserrolelinkdaoxml) * [BasicService\.java](#basicservicejava) * [SysUserService\.java](#sysuserservicejava) * [SysUserServiceImpl\.java](#sysuserserviceimpljava) * [二次开发](#%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91) * [项目架构](#%E9%A1%B9%E7%9B%AE%E6%9E%B6%E6%9E%84) * [模块的配置与扩展](#%E6%A8%A1%E5%9D%97%E7%9A%84%E9%85%8D%E7%BD%AE%E4%B8%8E%E6%89%A9%E5%B1%95) * [结尾](#%E7%BB%93%E5%B0%BE) * [捐赠](#%E6%8D%90%E8%B5%A0) ## 项目亮点 基于Java的代码生成器有很多,本项目有一些我个人的见解 1. 对不同作用的表有着不同的设计,可以自行选择表的基类,优化表结构,并且为关联表设置独特的跳跃访问方法 2. 基于配置文件直接启动,虽然不同那些图形界面操作简单,但更易于扩展与设计,对有有Java基础的用户也是很简单的 3. 对mybatis的设计上也遵循Java的 封装,继承,多态思想 4. 模块化的设计,使二次开发更易于扩展,毕竟每个人的的代码规范都是略有差异 5. ...... ## 使用教程 ### 配置与启动 配置环境 * jdk 1.8 * maven3 这里我们拿一张MySQL的数据表来讲解使用方法:
我们要来设计这张表的 domain,dao,mapper.xml,service层的代码,首先观察表格不难发现,它是由五张数据表和三张关联表组成的。并且有四张表格都有七个公共字段,分别为 id ,is_use,order_num,create_by,create_time,update_by,update_time,remark,而sys_log_login这张没有公共字段, 显然在生成代码的时候需要考虑这些情况。 下载代码,打开 `src\main\resources\orange.properties` ``` properties # 作者 author=swing #生成文件的输出路径 exportUrl=D:\\code\\java\\Orange # 默认生成包路径 basicPackage=com.swing.sky.web.generator.result #阿里巴巴规范下的公共字段(这里使用的是字段对应的Java属性) basicColumns=id,use,orderNum,createBy,createTime,updateBy,updateTime,remark #数据库名 schemaName=sky_new #数据表使用 table_数字 的格式命名(此名要与数据库中的名字完全一致) table_2=sys_user table_3=sys_role table_4=sys_dept table_5=sys_menu table_1=sys_log_login # on开启公共字段提取,off反之,默认为on(这里表示sys_log_login这个表没有公共字段) table_1.basic.enable=off #关联表 link_table_1=sys_user,sys_user_role,sys_role link_table_2=sys_role,sys_role_dept,sys_dept link_table_3=sys_role,sys_role_menu,sys_menu ``` 我将每一个配置所表示的含义已标注 确认配置完成后,只需要运行`src\main\java\com\swing\sky\web\generator\Gen.java`的主方法即可生成,关于生成文件的预览,请参考 [生成代码说明(参考)](#%E7%94%9F%E6%88%90%E4%BB%A3%E7%A0%81%E8%AF%B4%E6%98%8E%E5%8F%82%E8%80%83) ### 关联表的跳跃查询设计 先来看一个问题,假设所有的关联都是一对多的关系,那么依据上表,我想获取某个用户的所有菜单集合,那该怎么做呢? 通过关联表很容易做到这一点,如图:
我们只需要通过 user_id 获取所有的 role_id 然后使用每一个 role_id 去获取其 menu 集合,然后将所有的 menu 累加,然后去重即可 思路很清晰,代码实现也很简单,但如果像这样的业务需求十分庞大,那么代码量就很不可观了,于是我设计了这种跳跃查询的方法,使用一行代码即可完成任意复杂关系链的调用,其核心设计是为关联增加一些强大且通用的功能,一个关联表有如下方:(说明:假设一个关联表的命名为 user_role 那么前者(表user)被称为 one,后者(role表)被称为 two)
而要完成上面的提到的功能,只需要如下代码即可:(这些方法都会默认生成) ```java Long userId = 1L; List menus = roleMenuLinkDAO.listTwoByOneIds(userRoleLinkDAO.listTwoIdsByOneId(userId)); ``` So easy ! ### 生成代码说明(参考) 本项目目前支持生成十六种类型的文件,由于它的可扩展性,后期会更多,也欢迎您的贡献 依据上面和表结构的配置,生成的文件如下(相同的类型的表只做一个说明) #### BasicDO.java ```java package com.swing.sky.web.generator.result.domain; import java.util.Date; import java.util.Objects; /** * xxxDO 数据对象的共有字段 * * @author swing */ public class BasicDO { protected static final long serialVersionUID = 1L; /** * 主键id,自增字段 */ protected Long id; /** * 是否使用(1 使用,0 停用) */ protected Boolean use; /** * 显示顺序 */ protected Integer orderNum; /** * 创建者 */ protected String createBy; /** * 创建时间 */ protected Date createTime; /** * 更新者 */ protected String updateBy; /** * 更新时间 */ protected Date updateTime; /** * 备注 */ protected String remark; public BasicDO(Boolean use, Integer orderNum, String createBy, Date createTime, String updateBy, Date updateTime, String remark) { this.use = use; this.orderNum = orderNum; this.createBy = createBy; this.createTime = createTime; this.updateBy = updateBy; this.updateTime = updateTime; this.remark = remark; } public BasicDO() { } public Boolean getUse() { return use; } @Override public boolean equals(Object o) { //方法体略 } @Override public int hashCode() { return Objects.hash(id, use, orderNum, createBy, createTime, updateBy, updateTime, remark); } @Override public String toString() { //方法体略 } } ``` #### SysUserDO.java ```java package com.swing.sky.web.generator.result.domain; import com.swing.sky.web.generator.result.domain.BasicDO; import java.io.Serializable; import java.util.Date; /** * 用户信息表:对象 sys_user * * @author swing */ public class SysUserDO extends BasicDO implements Serializable{ private static final long serialVersionUID=1L; /** * 部门id */ private Long deptId; /** * 用户账号 */ private String username; /** * 密码 */ private String password; /** * 用户昵称 */ private String nickName; /** * 用户邮箱 */ private String email; /** * 手机号码 */ private String phone; /** * 用户性别(M男 W女 N未知) */ private String gender; /** * 头像地址 */ private String avatar; /** * 是否删除 (1 删除,0 未删除) */ private Boolean deleted; /** * 无参构造函数 */ public SysUserDO() { } /** * 全参构造函数 */ public SysUserDO(Long deptId, String username, String password, String nickName, String email, String phone, String gender, String avatar, Boolean deleted) { this.deptId = deptId; this.username = username; this.password = password; this.nickName = nickName; this.email = email; this.phone = phone; this.gender = gender; this.avatar = avatar; this.deleted = deleted; } public Long getDeptId() {return deptId;} public void setDeptId(Long deptId) {this.deptId = deptId;} //剩余的的get/set方法略 @Override public String toString() { //方法体略 } } ``` #### SysLogLoginDO.java ```java package com.swing.sky.web.generator.result.domain; import java.io.Serializable; import java.util.Date; /** * 系统访问记录:对象 sys_log_login * * @author swing */ public class SysLogLoginDO implements Serializable { private static final long serialVersionUID = 1L; /** * 访问ID */ private Long id; /** * 用户账号 */ private String username; /** * 客户端类型 */ private String clientType; /** * 是否成功(1成功 失败) */ private Boolean success; /** * 提示消息 */ private String message; /** * 登录IP地址 */ private String ip; /** * 登录地点 */ private String location; /** * 操作系统 */ private String os; /** * 浏览器类型 */ private String browser; /** * 访问时间 */ private Date createTime; /** * 无参构造函数 */ public SysLogLoginDO() { } /** * 全参构造函数 */ public SysLogLoginDO(Long id, String username, String clientType, Boolean success, String message, String ip, String location, String os, String browser, Date createTime) { this.id = id; this.username = username; this.clientType = clientType; this.success = success; this.message = message; this.ip = ip; this.location = location; this.os = os; this.browser = browser; this.createTime = createTime; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } //剩余的get/set方法略 @Override public String toString() { //方法体略 } } ``` #### BasicDAO.java ```java /** * 基本 dao 方法 * * @author swing */ public interface BasicDAO { /** * 插入 * * @param t 内容 * @return 影响行数 */ int insert(T t); /** * 删除 * * @param id 主键 * @return 影响行数 */ int deleteById(Long id); /** * 批量删除 * * @param ids 需要删除的信息Id集合 * @return 结果 */ int batchDeleteByIds(Long[] ids); /** * 更新 * * @param t 内容 * @return 影响行数 */ int update(T t); /** * 根据主键获取实体类 * * @param id 主键 * @return 实体类 */ T getById(Long id); /** * 查询符合条件的集合 * * @param beginTime 开始时间 * @param endTime 终止时间 * @param t 条件 * @return 符合条件的集合 */ List listByCondition(@Param("condition") T t, @Param("beginTime") String beginTime, @Param("endTime") String endTime); } ``` #### SysUserDAO.java ```java /** * 用户信息表 * * @author swing */ public interface SysUserDAO extends BasicDAO { } ``` #### BasicLinkDAO.java ```java /** * 关联表的基本方法 * * @author swing */ public interface BasicLinkDAO { /** * 批量插入信息 * * @param items 信息集合 * @return 影响行数 */ int batchInsert(List items); /** * 根据One的id 删除T * * @param id One的id * @return 影响行数 */ int deleteItemByOneId(Long id); /** * 根据One的id 批量删除T * * @param ids id数组 * @return 影响行数 */ int batchDeleteItemByOneIds(Long[] ids); /** * 根据Two的id 删除T * * @param id Two的id * @return 影响行数 */ int deleteItemByTwoId(Long id); /** * 根据Two的id 批量删除T * * @param ids id数组 * @return 影响行数 */ int batchDeleteItemByTwoIds(Long[] ids); /** * 根据One的id统计数据量 * * @param id One的id * @return 数量 */ int countItemByOneId(Long id); /** * 根据Two的id统计数据量 * * @param id Two的id * @return 数量 */ int countItemByTwoId(Long id); /** * 根据Two的id列出One的信息列表 * * @param id Two的Id * @return 信息列表 */ List listOneByTwoId(Long id); /** * 根据Two的ids列出One的信息列表(去重复) * * @param ids Two的Ids * @return 信息列表 */ List listOneByTwoIds(Long[] ids); /** * 根据Two的id列出One的id数组 * * @param id Two的Id * @return 信息列表 */ Long[] listOneIdsByTwoId(Long id); /** * 根据Two的ids列出One的id数组(去重复) * * @param ids Two的Ids * @return 信息列表 */ Long[] listOneIdsByTwoIds(Long[] ids); /** * 根据Two的id列出One的信息列表 * * @param id Two的Id * @return 信息列表 */ List listTwoByOneId(Long id); /** * 根据Two的id列出One的信息列表(去重复) * * @param ids Two的Ids * @return 信息列表 */ List listTwoByOneIds(Long[] ids); /** * 根据Two的id列出One的idid数组 * * @param id Two的Id * @return 信息列表 */ Long[] listTwoIdsByOneId(Long id); /** * 根据Two的id列出One的id数组(去重复) * * @param ids Two的Ids * @return 信息列表 */ Long[] listTwoIdsByOneIds(Long[] ids); } ``` #### SysUserRoleLinkDAO.java ```java /** * @author swing */ public interface SysUserRoleLinkDAO extends BasicLinkDAO { } ``` #### SysLogLoginDAO.xml ```xml sys_log_login sys_log_login.id, sys_log_login.username, sys_log_login.client_type, sys_log_login.is_success, sys_log_login.message, sys_log_login.ip, sys_log_login.location, sys_log_login.os, sys_log_login.browser, sys_log_login.create_time delete from where id = #{id,jdbcType=BIGINT} delete from where id in #{item} insert into id, username, client_type, is_success, message, ip, location, os, browser, create_time, #{id,jdbcType=BIGINT}, #{username,jdbcType=VARCHAR}, #{clientType,jdbcType=CHAR}, #{success,jdbcType=BOOLEAN}, #{message,jdbcType=VARCHAR}, #{ip,jdbcType=VARCHAR}, #{location,jdbcType=VARCHAR}, #{os,jdbcType=VARCHAR}, #{browser,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP}, update id = #{id,jdbcType=BIGINT}, username = #{username,jdbcType=VARCHAR}, client_type = #{clientType,jdbcType=CHAR}, is_success = #{success,jdbcType=BOOLEAN}, message = #{message,jdbcType=VARCHAR}, ip = #{ip,jdbcType=VARCHAR}, location = #{location,jdbcType=VARCHAR}, os = #{os,jdbcType=VARCHAR}, browser = #{browser,jdbcType=VARCHAR}, create_time = #{createTime,jdbcType=TIMESTAMP}, where id = #{id,jdbcType=BIGINT} ``` #### BasicDAO.xml ```xml is_use, order_num, create_by, create_time, update_by, update_time, remark, #{use,jdbcType=BOOLEAN}, #{orderNum,jdbcType=INTEGER}, #{createBy,jdbcType=VARCHAR}, #{createTime,jdbcType=TIMESTAMP}, #{updateBy,jdbcType=VARCHAR}, #{updateTime,jdbcType=TIMESTAMP}, #{remark,jdbcType=VARCHAR}, is_use = #{use,jdbcType=BOOLEAN}, order_num = #{orderNum,jdbcType=INTEGER}, create_by = #{createBy,jdbcType=VARCHAR}, create_time = #{createTime,jdbcType=TIMESTAMP}, update_by = #{updateBy,jdbcType=VARCHAR}, update_time = #{updateTime,jdbcType=TIMESTAMP}, remark = #{remark,jdbcType=VARCHAR}, ``` #### SysUserDAO.xml ```xml sys_user sys_user.id, sys_user.dept_id, sys_user.username, sys_user.password, sys_user.nick_name, sys_user.email, sys_user.phone, sys_user.gender, sys_user.avatar, sys_user.is_deleted, sys_user.is_use, sys_user.order_num, sys_user.create_by, sys_user.create_time, sys_user.update_by, sys_user.update_time, sys_user.remark delete from where id = #{id,jdbcType=BIGINT} delete from where id in #{item} insert into dept_id, username, password, nick_name, email, phone, gender, avatar, is_deleted, #{deptId,jdbcType=BIGINT}, #{username,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, #{nickName,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}, #{phone,jdbcType=VARCHAR}, #{gender,jdbcType=CHAR}, #{avatar,jdbcType=VARCHAR}, #{deleted,jdbcType=BOOLEAN}, update dept_id = #{deptId,jdbcType=BIGINT}, username = #{username,jdbcType=VARCHAR}, password = #{password,jdbcType=VARCHAR}, nick_name = #{nickName,jdbcType=VARCHAR}, email = #{email,jdbcType=VARCHAR}, phone = #{phone,jdbcType=VARCHAR}, gender = #{gender,jdbcType=CHAR}, avatar = #{avatar,jdbcType=VARCHAR}, is_deleted = #{deleted,jdbcType=BOOLEAN}, where id = #{id,jdbcType=BIGINT} ``` #### SysUserRoleLinkDAO.xml ```xml sys_user_role user_id role_id (#{item.userId},#{item.roleId}) .,. .=.id .=.id insert into ( ) values delete from where = #{id} delete from where in #{item} delete from where = #{id} delete from where in #{item} ``` #### BasicService.java ```java /** * 服务层基本接口 * * @author swing */ public interface BasicService { /** * 插入 * * @param t 内容 * @return 影响行数 */ int insert(T t); /** * 删除 * * @param id 主键 * @return 影响行数 */ int deleteById(Long id); /** * 批量删除 * * @param ids 需要删除的信息Id集合 * @return 结果 */ int batchDeleteByIds(Long[] ids); /** * 更新 * * @param t 内容 * @return 影响行数 */ int update(T t); /** * 根据主键获取实体类 * * @param id 主键 * @return 实体类 */ T getById(Long id); /** * 查询符合条件的集合(此方法只有管理员用户可以使用,可以没有限制地获取该资源的所有记录) * 入对资源的访问需要进行权限限制,请使用扩展的方法: * List listByConditionAndUserId(Long userId, T t, String beginTime, String endTime); * * @param beginTime 开始时间 * @param endTime 终止时间 * @param t 条件 * @return 符合条件的集合 */ List listByCondition(T t, String beginTime, String endTime); } ``` #### SysUserService.java ```java /** * 用户信息表 * * @author swing */ public interface SysUserService extends BasicService { } ``` #### SysUserServiceImpl.java ```java /** * 用户信息表 * * @author swing */ public class SysUserServiceImpl implements SysUserService { @Resource private SysUserDAO sysUserDAO; @Override public int insert(SysUserDO sysUserDO) { return sysUserDAO.insert(sysUserDO); } @Override public int deleteById(Long id) { return sysUserDAO.deleteById(id); } @Override public int batchDeleteByIds(Long[] ids) { return sysUserDAO.batchDeleteByIds(ids); } @Override public int update(SysUserDO sysUserDO) { return sysUserDAO.update(sysUserDO); } @Override public SysUserDO getById(Long id) { return sysUserDAO.getById(id); } @Override public List listByCondition(SysUserDO sysUserDO, String beginTime, String endTime) { return sysUserDAO.listByCondition(sysUserDO, beginTime, endTime); } } ``` ### 二次开发 如果这些生成规则无法满足你的私人定制,那么欢迎对其进行二次开发,可联系我,然后我给你提供代码分支 本项目是基于 velocity 上开发的 #### 项目架构
其中 该项目的核心类是 moduleHouse ,其中包括所有的配置文件信息,和数据库信息 #### 模块的配置与扩展 `src\main\resources\gen.properties` ```properties #打包工具(使用逗号隔开,然后在下文使用 (模块名.配置)的形式配置文件信息) packageTool=maven #打包工具定制化 maven.java.path=src\\main\\java maven.test.java.path=src\\test\\java maven.resources.path=src\\main\\resources maven.test.resources.path=src\\test\\resources #生成文件的模块名,在此处填写模块名,使用逗号隔开,然后在下文使用 (模块名.配置)的形式配置文件信息 modules=linkMapper,daoLink,basicDomain,domain,domainBasic,basicDao,dao,daoBasic,basicLinkDao,basicMapper,mapper,mapperBasic,basicService,service,serviceBasic,serviceImpl #basicDomain #文件类型(main:主文件,test:测试,src:源码,resource:资源文件) basicDomain.type=main/src #包名 basicDomain.packageName=domain #后缀 basicDomain.suffix=DO #文件扩展名 basicDomain.extension=.java #模板文件路径 basicDomain.templateUrl=vm/java/domain/BasicDO.java.vm ##默认文件名(没有此属性默认根据表明来转换) basicDomain.defaultFileName=BasicDO #domain domain.type=main/src domain.packageName=domain domain.suffix=DO domain.extension=.java domain.templateUrl=vm/java/domain/DO.java.vm #domainBasic domainBasic.type=main/src domainBasic.packageName=domain domainBasic.suffix=DO domainBasic.extension=.java domainBasic.templateUrl=vm/java/domain/DO_BASIC.java.vm #basicDao basicDao.type=main/src basicDao.suffix=DAO basicDao.packageName=dao basicDao.extension=.java basicDao.templateUrl=vm/java/dao/BasicDAO.java.vm basicDao.defaultFileName=BasicDAO #dao dao.type=main/src dao.suffix=DAO dao.packageName=dao dao.extension=.java dao.templateUrl=vm/java/dao/DAO.java.vm #daoBasic daoBasic.type=main/src daoBasic.suffix=DAO daoBasic.packageName=dao daoBasic.extension=.java daoBasic.templateUrl=vm/java/dao/DAO_BASIC.java.vm #basicLinkDao basicLinkDao.type=main/src basicLinkDao.suffix=LinkDAO basicLinkDao.packageName=dao basicLinkDao.extension=.java basicLinkDao.templateUrl=vm/java/dao/BasicLinkDAO.java.vm basicLinkDao.defaultFileName=BasicLinkDAO #daoLink daoLink.type=main/src daoLink.suffix=LinkDAO daoLink.packageName=dao daoLink.extension=.java daoLink.templateUrl=vm/java/dao/DAO_LINK.java.vm #basicMapper basicMapper.type=main/resources basicMapper.suffix=DAO basicMapper.packageName=mybatis basicMapper.extension=.xml basicMapper.templateUrl=vm/java/mybatis/BasicMapper.xml.vm basicMapper.defaultFileName=BasicDAO #mapper mapper.type=main/resources mapper.suffix=DAO mapper.packageName=mybatis mapper.extension=.xml mapper.templateUrl=vm/java/mybatis/Mapper.xml.vm #mapperBasic mapperBasic.type=main/resources mapperBasic.suffix=DAO mapperBasic.packageName=mybatis mapperBasic.extension=.xml mapperBasic.templateUrl=vm/java/mybatis/Mapper_BASIC.xml.vm #linkMapper linkMapper.type=main/resources linkMapper.suffix=LinkDAO linkMapper.packageName=mybatis linkMapper.extension=.xml linkMapper.templateUrl=vm/java/mybatis/Link_Mapper.xml.vm #basicService basicService.type=main/src basicService.suffix=Service basicService.packageName=service basicService.extension=.java basicService.templateUrl=vm/java/service/BasicService.java.vm basicService.defaultFileName=BasicService #service service.type=main/src service.suffix=Service service.packageName=service service.extension=.java service.templateUrl=vm/java/service/Service.java.vm #serviceBasic serviceBasic.type=main/src serviceBasic.suffix=Service serviceBasic.packageName=service serviceBasic.extension=.java serviceBasic.templateUrl=vm/java/service/Service_BASIC.java.vm #serviceImpl serviceImpl.type=main/src serviceImpl.suffix=ServiceImpl serviceImpl.packageName=service.impl serviceImpl.extension=.java serviceImpl.templateUrl=vm/java/service/ServiceImpl.java.vm ``` 一个module即对应一个类型的生成文件,如果需要生成属于自己的模板文件,请在modules中配置模块名称,然后再下文配置模块的详细信息 包括模板位置,并再 `src\main\java\com\swing\sky\web\generator\constant\ModuleConstants.java` 中新增该模块的常量表示 然后在 VelocityContextBuilder 中配置该模板文件生成时需要的上下文 VelocityContext 即可 扩展 so easy! ### 结尾 可fork本仓库参与贡献 有啥问题可在评论区留言,我努力回复,努力帮忙! 多多fork多多start!!!😁 ### 捐赠 可以请作者喝一瓶哇哈哈: