Foxnic-SQL (16) ——Foxnic-SQL 的模型关联办法
概述
本节咱们将用一个简略的例子,来阐明对象之间的关联关系,以及 Foxnic-SQL 是如何解决这种关联关系的。首先,咱们引入商城下单的简略业务模型,这个模型外面包含了商品、订单、订单明细以及收件人地址,这个模型足够简略,所以很容易剖析出他们之间的关联关系。
本文中的示例代码均可在 https://gitee.com/LeeFJ/foxni… 我的项目中找到。
业务剖析
从上面这个 ER 图,咱们能够看到四个表,订单 (example_order)、订单明细 (example_order_item)、商品 (example_goods)、收件地址 (example_address),如果按惯例逻辑,咱们从订单开始动手,一个订单蕴含若干商品,并投第到一个地址上。
不难剖析出他们之间的关系:一个订单只会有一个地址,所以他们是一对一的;把这个关系反一下,同一个地址可能会收到多个订单,所以他们是一对多的关系;通常来讲,订单和商品是属于多对多关系,因为一个商品能够属于不同的订单,一个订单也能够蕴含多个商品;同理能够推得,地址和商品也是多对多的关系。然而这样的关系剖析仿佛有点搞脑子。
Foxnic-SQL 将这种进行简化,只有一对一或一对多两种关系。即,如果咱们将上述的四个表转换为实体类,当某一个类要持有对方时,它要为对方设置繁多属性还是 List 属性来判断两者关系。
例如 Order 类要持有 Address 类时,它只须要定义一个属性 private Address address; 就能够了,这是 Order 对于 Address 就是一对一的关系;再如,Order 类要持有 Goods 则须要为其定义一个列表 private List<Goods> goodsList; 才能够,这时 Order 对于 Goods 就是一对多的关系。
Foxnic-SQL 要求尽可能剖析分明这些实体之间的关系,并配置到零碎中,最终造成一个实体关系的大地图,当咱们定义分明 A 持有 B 所须要的属性类型,咱们同时也就是构建了一个全局的对象模型,如图所示:
上面,咱们用下单的这个例子,来零碎阐明 Foxnic-SQL 是如何实现关系搭建与模型的 Join。
生成表构造元数据
要使表之间的关系能够在 Java 代码中可配置,那就必须能够在 Java 代码中能够援用到表。所以,第一步的工作就是将数据表对象化,Foxnic-SQL 的代码生成模块曾经实现了这部分工作,我只有做一些简略的配置即可实现调用生成 Java 模式的表构造,代码如下:
package com.leefj.foxnic.sql.demo.generator;
import com.github.foxnic.dao.spec.DAO;
import com.github.foxnic.generator.builder.constants.DBMetaClassFile;
import com.leefj.foxnic.sql.demo.config.DBInstance;
public class ExampleDBMetaGenerator {
/**
* 运行 main 函数生成代码
* */
public static void main(String[] args) throws Exception {ExampleDBMetaGenerator g = new ExampleDBMetaGenerator();
g.buildDBMeta();}
/**
* 生成 DBMeta 数据
* */
private void buildDBMeta() {
// 初始化 DAO 对象
DAO dao= DBInstance.DEFAULT.dao();
// 初始化全局设置
GeneratorUtil.initGlobalSettings();
// 初始化 DBMetaClassFile 构建器
DBMetaClassFile dbMetaBuilder=new DBMetaClassFile(dao,GeneratorUtil.getProject(), "com.leefj.foxnic.sql.demo.config","ExampleTables");
// 过滤与排除不须要的表
dbMetaBuilder.setTableFilter(table->{table=table.toLowerCase();
// 仅生成以 example_ 结尾的表
if(table.startsWith("example_")) return true;
return false;
});
// 保留为 Java 代码
dbMetaBuilder.save(true);
}
}
运行 main 函数,将生成 com.leefj.foxnic.sql.demo.config.db.ExampleTables 类,这个类蕴含了相干业务表的表构造信息。因为代码较多此处不做展现,可签出残缺我的项目 https://gitee.com/LeeFJ/foxni… 查看。
生成根底实体
所谓生成根底实体就是齐全依照表构造生成实体类,每个表按默认的命名转换器生成实体,对应的表字段则生成相应的类的成员属性。
package com.leefj.foxnic.sql.demo.generator;
import com.github.foxnic.commons.project.maven.MavenProject;
import com.github.foxnic.dao.spec.DAO;
import com.github.foxnic.generator.builder.model.PoClassFile;
import com.github.foxnic.generator.config.ModuleContext;
import com.github.foxnic.sql.meta.DBTable;
import com.leefj.foxnic.sql.demo.config.DBInstance;
import com.leefj.foxnic.sql.demo.config.db.ExampleTables;
/**
* 实体类生成器
* */
public class EntityGenerator {
public static interface Config {void config(PoClassFile poType);
}
private static final String BASE_PACKAGE = "com.leefj.foxnic.sql.demo.app";
/**
* 须要首先运行 ExampleDBMetaGenerator 生成 ExampleTables 类
* */
public static void main(String[] args) {EntityGenerator generator = new EntityGenerator();
// 生成商品实体类
generator.generate(ExampleTables.EXAMPLE_GOODS.$TABLE);
// 生成订单实体类
generator.generate(ExampleTables.EXAMPLE_ORDER.$TABLE);
// 生成订单明细实体类
generator.generate(ExampleTables.EXAMPLE_ORDER_ITEM.$TABLE);
// 生成地址实体类
generator.generate(ExampleTables.EXAMPLE_ADDRESS.$TABLE);
}
/**
* 按表生成
* */
public void generate(DBTable table) {generate(table,null);
}
/**
* 按表生成
* */
public void generate(DBTable table,Config config) {DAO dao = DBInstance.DEFAULT.dao();
MavenProject project = GeneratorUtil.getProject();
String pkg = table.name().split("_")[0];
String prefix = pkg + "_";
ModuleContext context = new ModuleContext(GeneratorUtil.initGlobalSettings(),table,prefix,BASE_PACKAGE + "." + pkg);
context.setDomainProject(project);
context.setServiceProject(project);
context.setDAO(dao);
if(config!=null) {config.config(context.getPoClassFile());
}
context.buildPo();
context.buildVo();
context.buildService();}
}
运行 main 办法将生成实体类代码,以上代码生成 PO、VO 对象以及这些类型对应的元数据类型,代码生成后如图所示:
退出扩大属性
上面咱们要为这些实体类退出扩大属性,正如咱们刚刚在上文中剖析的那样,Order 类要持有 Address 类时,须要定义一个属性 private Address address; Order 类要持有 Goods 则须要为其定义一个列表 private List<Goods> goodsList; 才能够。咱们批改上一步中的 EntityGenerator 类,调整 main 办法如下:
/**
* 须要首先运行 ExampleDBMetaGenerator 生成 ExampleTables 类
* */
public static void main(String[] args) {EntityGenerator generator = new EntityGenerator();
// 生成商品实体类
generator.generate(ExampleTables.EXAMPLE_GOODS.$TABLE, poType -> {
// Goods 对象 通过 orderList 属性持有 Order
poType.addListProperty(Goods.class,"orderList","订单明细商品","订单明细商品");
// Goods 对象 通过 addressList 属性持有 Address
poType.addListProperty(Address.class,"addressList","收件地址","收件地址,包含收件人以及手机号码");
// Goods 对象 通过 itemList 属性持有 OrderItem
poType.addListProperty(OrderItem.class,"itemList","订单明细","订单明细");
});
// 生成订单实体类
generator.generate(ExampleTables.EXAMPLE_ORDER.$TABLE , poType -> {
// Order 对象 通过 goodsList 属性持有 Goods
poType.addListProperty(Goods.class,"goodsList","订单明细商品","订单明细商品");
// Order 对象 通过 address 属性持有 Address
poType.addSimpleProperty(Address.class,"address","收件地址","收件地址,包含收件人以及手机号码");
// Order 对象 通过 itemList 属性持有 OrderItem
poType.addListProperty(OrderItem.class,"itemList","订单明细","订单明细");
});
// 生成订单明细实体类
generator.generate(ExampleTables.EXAMPLE_ORDER_ITEM.$TABLE, poType -> {
// OrderItem 对象 通过 goodsList 属性持有 Goods
poType.addSimpleProperty(Goods.class,"goods","订单明细商品","订单明细商品");
// OrderItem 对象 通过 address 属性持有 Address
poType.addSimpleProperty(Address.class,"address","收件地址","收件地址,包含收件人以及手机号码");
// OrderItem 对象 通过 order 属性持有 Order
poType.addListProperty(Order.class,"order","订单","订单");
});
// 生成地址实体类
generator.generate(ExampleTables.EXAMPLE_ADDRESS.$TABLE, poType -> {
// Address 对象 通过 goodsList 属性 持有 Goods
poType.addListProperty(Goods.class,"goodsList","订单明细商品","订单明细商品");
// Address 对象 通过 orderList 持有 Order
poType.addListProperty(Address.class,"orderList","收件地址","收件地址,包含收件人以及手机号码");
// Address 对象 通过 itemList 持有 OrderItem
poType.addListProperty(OrderItem.class,"itemList","订单明细","订单明细");
});
}
通过下面的代码,咱们定义了任意对象如果要持有对方时须要定义的属性。当然,这些定义的属性中有局部没有理论的业务意义,我没这里仅仅是从技术角度来阐明对象之间关联属性的定义方法。再次运行
留神,代码生成并非只是本文中展现的一种范式,小伙伴们可按须要自行实现。
配置关联关系
后面的步骤,咱们生成了实体类以及实体类的扩大属性,有了扩大属性后咱们要为这些扩大属性配置好关联关系。关联关系在 ExampleRelationManager 类中配置,如下代码所示:
import com.github.foxnic.dao.relation.RelationManager;
import com.leefj.foxnic.sql.demo.app.domain.example.meta.AddressMeta;
import com.leefj.foxnic.sql.demo.app.domain.example.meta.GoodsMeta;
import com.leefj.foxnic.sql.demo.app.domain.example.meta.OrderItemMeta;
import com.leefj.foxnic.sql.demo.app.domain.example.meta.OrderMeta;
import com.leefj.foxnic.sql.demo.config.db.ExampleTables;
public class ExampleRelationManager extends RelationManager {
@Override
protected void config() {this.setupOrder();
}
public void setupOrder() {
// 以下是 Order 扩大属性的关联关系配置
// Order.address 属性通过 EXAMPLE_ORDER.ADDRESS_ID 字段关联到 EXAMPLE_ADDRESS.ID 字段
this.property(OrderMeta.ADDRESS_PROP)
.using(ExampleTables.EXAMPLE_ORDER.ADDRESS_ID).join(ExampleTables.EXAMPLE_ADDRESS.ID)
// 因为订单地址不常变动,能够开启缓存
.cache(true);
// Order.goodsList 属性通过 EXAMPLE_ORDER.ID 字段关联到 EXAMPLE_ORDER_ITEM.ORDER_ID 字段
// 再通过 EXAMPLE_ORDER_ITEM.GOODS_ID 字段关联到 EXAMPLE_GOODS.ID 字段
this.property(OrderMeta.GOODS_LIST_PROP)
.using(ExampleTables.EXAMPLE_ORDER.ID).join(ExampleTables.EXAMPLE_ORDER_ITEM.ORDER_ID)
.using(ExampleTables.EXAMPLE_ORDER_ITEM.GOODS_ID).join(ExampleTables.EXAMPLE_GOODS.ID)
// 订单一旦生成,订单明细不再变动,开启缓存
.cache(true);
// Order.itemList 属性通过 EXAMPLE_ORDER.ID 字段关联到 EXAMPLE_ORDER_ITEM.ORDER_ID 字段
this.property(OrderMeta.ITEM_LIST_PROP)
.using(ExampleTables.EXAMPLE_ORDER.ID).join(ExampleTables.EXAMPLE_ORDER_ITEM.ORDER_ID)
.cache(true);
// 以下是 OrderItem 扩大属性的关联关系配置
// OrderItem.order 属性通过 EXAMPLE_ORDER_ITEM.ORDER_ID 字段关联到 EXAMPLE_ORDER.ID 字段
this.property(OrderItemMeta.ORDER_PROP)
.using(ExampleTables.EXAMPLE_ORDER_ITEM.ORDER_ID).join(ExampleTables.EXAMPLE_ORDER.ID)
.cache(true);
// OrderItem.goods 属性通过 EXAMPLE_ORDER_ITEM.GOODS_ID 字段关联到 EXAMPLE_GOODS.ID 字段
this.property(OrderItemMeta.GOODS_PROP)
.using(ExampleTables.EXAMPLE_ORDER_ITEM.GOODS_ID).join(ExampleTables.EXAMPLE_GOODS.ID)
.cache(true);
// 以下是 Goods 扩大属性的关联关系配置
this.property(GoodsMeta.ORDER_LIST_PROP)
.using(ExampleTables.EXAMPLE_GOODS.ID).join(ExampleTables.EXAMPLE_ORDER_ITEM.GOODS_ID)
.using(ExampleTables.EXAMPLE_ORDER_ITEM.ORDER_ID).join(ExampleTables.EXAMPLE_ORDER.ID)
.cache(true);
this.property(GoodsMeta.ADDRESS_LIST_PROP)
.using(ExampleTables.EXAMPLE_GOODS.ID).join(ExampleTables.EXAMPLE_ORDER_ITEM.GOODS_ID)
.using(ExampleTables.EXAMPLE_ORDER_ITEM.ORDER_ID).join(ExampleTables.EXAMPLE_ORDER.ID)
.using(ExampleTables.EXAMPLE_ORDER.ADDRESS_ID).join(ExampleTables.EXAMPLE_ADDRESS.ID)
.cache(true);
this.property(GoodsMeta.ITEM_LIST_PROP)
.using(ExampleTables.EXAMPLE_GOODS.ID).join(ExampleTables.EXAMPLE_ORDER_ITEM.GOODS_ID)
.cache(true);
// 以下是 Address 扩大属性的关联关系配置
this.property(AddressMeta.ORDER_LIST_PROP)
.using(ExampleTables.EXAMPLE_ADDRESS.ID).join(ExampleTables.EXAMPLE_ORDER.ADDRESS_ID)
.cache(true);
this.property(AddressMeta.ITEM_LIST_PROP)
.using(ExampleTables.EXAMPLE_ADDRESS.ID).join(ExampleTables.EXAMPLE_ORDER.ADDRESS_ID)
.using(ExampleTables.EXAMPLE_ORDER.ID).join(ExampleTables.EXAMPLE_ORDER_ITEM.ORDER_ID)
.cache(true);
this.property(AddressMeta.GOODS_LIST_PROP)
.using(ExampleTables.EXAMPLE_ADDRESS.ID).join(ExampleTables.EXAMPLE_ORDER.ADDRESS_ID)
.using(ExampleTables.EXAMPLE_ORDER.ID).join(ExampleTables.EXAMPLE_ORDER_ITEM.ORDER_ID)
.using(ExampleTables.EXAMPLE_ORDER_ITEM.GOODS_ID).join(ExampleTables.EXAMPLE_GOODS.ID)
.cache(true);
}
}
同样,咱们在关系配置中简直配置了所有的两两关系,当然某些关系并没有业务意义,仅做技术展现。关系配置时还能够有其它更多的管制,例如额定的条件、排序等,请小伙伴们自行摸索。
通过 Service 拉取模型
通过之前的代码生成,不仅生成的 PO、VO 类,同时也生成了 Service 类,通过 DBInstance 能够拿到这个 Service 对象,若是在 Web 环境下,间接应用 Spring 的依赖注入即可拿到 Service 对象。通过 Service 拉取模型的示例代码如下:
/**
* 通过 Service 的 Join 办法实现属性数据的填充操作
* */
@Test
public void demo_1() {
// 取得 Service
IOrderService orderService=DBInstance.DEFAULT.getService(OrderServiceImpl.class);
// 查问所有订单
List<Order> orders= orderService.queryList(new Order());
// 填充订单属性
orderService.join(orders, OrderMeta.ADDRESS,OrderMeta.ITEM_LIST,OrderMeta.GOODS_LIST);
// 输入填属性填充后的订单列表
System.out.println(JSONUtil.format(JSONUtil.toJSONArray(orders)));
}
以上代码首先查问了订单列表,此时取得的订单列表并未填充扩大属性值,在调用 Service 的 join 办法,指定须要 Join 的属性名称即可。
Service 的 join 办法事实上是间接调用了 DAO 的 join 办法,本例中 join 了三个属性,这三个属性的查问与 join 过程并非程序执行的,而是利用了 java 的 fork…join 框架进行了并行操作。join 过程中会有一个输入,如下所示:
JOIN(DATA) FORK(21):128 , el=7 >>>
Order :: List<Goods> goodsList , properties : id , route example_order to example_goods
example_order_item(goods_id) = example_goods(id)
第一行的 JOIN(DATA) 是指以后的 join 操作是数据操作,另外还有一种 JOIN(SEARCH),在数据搜寻时用到,吗这里不做开展。FORK(21):128,其中 21 是指线程 ID,128 是指 fork 时拆分的行数。这里是指如果 orders 的元素个数超过 128,那么就按 128 的规格进行 frok 工作拆解。el=7 是指以后 orders 理论数量是 7 个。
Order :: List<Goods> goodsList 是指以后正在解决 Order 类的 goodsList 属性,properties : id 是指关联的起始属性是 Order.id , route example_order to example_goods 是指从 example_order 表路由到 example_goods 表。example_order_item(goods_id) = example_goods(id) 展现了本次 join 的关联关系。这些关联关系咱们在 ExampleRelationManager 类里做了配置。
最初,咱们能够在 SQL 日志上看到 join 最终执行的 SQL 语句:
┏━━━━━ SQL [SELECT t_0.* , t_1.order_id join_f0 , t_0.id join_fs_example_goods_sf_nioj_id …] ━━━━━
┣ 语句:SELECT t_0. , t_1.order_id join_f0 , t_0.id join_fs_example_goods_sf_nioj_id , t_0.id pk_join_fs_example_goods_sf_nioj_id , t_1.goods_id join_fs_example_order_item_sf_nioj_goods_id , t_1.order_id join_fs_example_order_item_sf_nioj_order_id , t_1.id pk_join_fs_example_order_item_sf_nioj_id FROM (select from example_goods WHERE (deleted= ?) ) t_0
join (select * from example_order_item WHERE ( deleted= ?) ) t_1 on t_1.goods_id = t_0.id
WHERE t_1.order_id IN (? , ? , ? , ? , ? , ? , ?) AND t_1.deleted= ?
┣ 参数:{“PARAM_1″:0,”PARAM_9″:”651345265944428544″,”PARAM_8″:”651017423926853632″,”PARAM_10″:0,”PARAM_7″:”583188757629370368″,”PARAM_6″:”651024562955223040″,”PARAM_5″:”651017546694131712″,”PARAM_4″:”583028022102200320″,”PARAM_3″:”651024388312793088″,”PARAM_2”:0}
┣ 执行:SELECT t_0. , t_1.order_id join_f0 , t_0.id join_fs_example_goods_sf_nioj_id , t_0.id pk_join_fs_example_goods_sf_nioj_id , t_1.goods_id join_fs_example_order_item_sf_nioj_goods_id , t_1.order_id join_fs_example_order_item_sf_nioj_order_id , t_1.id pk_join_fs_example_order_item_sf_nioj_id FROM (select from example_goods WHERE (deleted= 0) ) t_0
join (select * from example_order_item WHERE ( deleted= 0) ) t_1 on t_1.goods_id = t_0.id
WHERE t_1.order_id IN (‘651024388312793088’ , ‘583028022102200320’ , ‘651017546694131712’ , ‘651024562955223040’ , ‘583188757629370368’ , ‘651017423926853632’ , ‘651345265944428544’) AND t_1.deleted= 0
┣ 后果:
┣━ 耗时:84ms , start = 1672796438302
┣━ 返回:RcdSet,size=2
┣ TID:null
┗━━━━━ SQL [SELECT t_0.* , t_1.order_id join_f0 , t_0.id join_fs_example_goods_sf_nioj_id …] ━━━━━
通过 DAO 拉取模型
DAO 自身也有 join 办法,用法和 Service 的 join 办法类似,这里不做开展。本例中,咱们应用 DAO 的 fill…with 办法拉取模型。
/**
* 通过 DAO 对象的 fill...with 办法实现属性数据的填充
* */
@Test
public void demo_2() {
// 取得 Service
IOrderService orderService=DBInstance.DEFAULT.getService(OrderServiceImpl.class);
// 查问所有订单
List<Order> orders= orderService.queryList(new Order());
DAO dao=orderService.dao();
// 通过 dao 的 fill 办法填充 orders 列表外面元素的扩大属性
dao.fill(orders)
// 填充 orders 外面每个元素的 address 属性
.with(OrderMeta.ADDRESS)
// 填充 orders 外面每个元素的 itemList 属性
.with(OrderMeta.ITEM_LIST)
// 填充 orders 外面每个元素的 goodsList 属性
.with(OrderMeta.GOODS_LIST)
// 执行最终的 join 操作
.execute();
// 输入填属性填充后的订单列表
System.out.println(JSONUtil.format(JSONUtil.toJSONArray(orders)));
}
通过还能够进一步拉取级联模型:
/**
* 通过 DAO 对象的 fill...with 办法实现属性数据的级联填充
* */
@Test
public void demo_3() {
// 取得 Service
IOrderService orderService=DBInstance.DEFAULT.getService(OrderServiceImpl.class);
// 查问所有订单
List<Order> orders= orderService.queryList(new Order());
DAO dao=orderService.dao();
// 通过 dao 的 fill 办法填充 orders 列表外面元素的扩大属性
dao.fill(orders)
// 填充 orders 外面每个元素的 address 属性,并填充 address 属性的 orderList 属性
.with(OrderMeta.ADDRESS,AddressMeta.ORDER_LIST)
// 填充 orders 外面每个元素的 address 属性,并填充 address 属性的 goodsList 属性
.with(OrderMeta.ADDRESS,AddressMeta.GOODS_LIST)
// 填充 orders 外面每个元素的 itemList 属性
.with(OrderMeta.ITEM_LIST)
// 填充 orders 外面每个元素的 goodsList 属性
.with(OrderMeta.GOODS_LIST)
// 执行最终的 join 操作
.execute();
// 输入填属性填充后的订单列表
System.out.println(JSONUtil.format(JSONUtil.toJSONArray(orders)));
}
刻画模型
通过下面的两个示例,咱们留神到,咱们在拉取对象时,并未指定字段,这样就会拉出多余的字段,为了准确刻画须要拉取的模型字段,咱们能够为 fill…with 指定 fields , 如下代码所示:
/**
* 通过 DAO 对象的 fill...with 办法实现属性数据的填充,并用 fields 指定查问字段
* */
@Test
public void demo_4() {
// 取得 Service
IOrderService orderService=DBInstance.DEFAULT.getService(OrderServiceImpl.class);
// 查问所有订单
List<Order> orders= orderService.queryList(new Order());
DAO dao=orderService.dao();
// 构建 Order 对象须要查问的字段
FieldsBuilder orderFields=FieldsBuilder.build(dao, ExampleTables.EXAMPLE_ORDER.$TABLE);
orderFields.addAll().removeDBTreatyFields();
// 构建 address 对象须要的字段
FieldsBuilder addressFields=FieldsBuilder.build(dao, ExampleTables.EXAMPLE_ADDRESS.$TABLE);
addressFields.addAll().removeDBTreatyFields();
// 通过 dao 的 fill 办法填充 orders 列表外面元素的扩大属性
dao.fill(orders)
// 填充 orders 外面每个元素的 address 属性
.with(OrderMeta.ADDRESS)
// 填充 orders 外面每个元素的 itemList 属性
.with(OrderMeta.ITEM_LIST)
// 填充 orders 外面每个元素的 goodsList 属性
.with(OrderMeta.GOODS_LIST)
// 指定字段
.fields(orderFields,addressFields)
// 执行最终的 join 操作
.execute();
// 输入填属性填充后的订单列表
System.out.println(JSONUtil.format(JSONUtil.toJSONArray(orders)));
}
模型拆卸的准则
从整个示例的过程,大家留神到,示例中的绝大多是代码是生成的,在代码生成的根底上,咱们配置两个对象之间的援用关系和关联关系。
Foxnic-SQL 的模型构建过程是按需拉取的,不同接口须要不同的数据,开发人员通过 Service 的 join 办法或 DAO 的 fill…with 办法手动指定本次要拉取的属性范畴,使拉取过程更加合乎接口要求。
事实上,援用关系和关联关系在一个零碎中个别都是固定的,惟一不同的是在不同的场合拉取模型的范畴,这个由开发人员管制,更加灵便不便。
小结
本节次要介绍了在 Foxni-SQL 中应用 join 办法填充对象模型。根本步骤包含,生成数据库表元数据、生成 PO、VO、Service 代码、配置关联关系、应用 Service 或 DAO 实现模型拆卸。
相干我的项目
https://gitee.com/LeeFJ/foxnic
https://gitee.com/LeeFJ/foxni…
https://gitee.com/lank/eam
https://gitee.com/LeeFJ/foxni…
官网文档
http://foxnicweb.com/docs/doc…