diff --git a/.gitignore b/.gitignore index dfa40a8..de445f2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ **/*.rs.bk *.iml *.lock +/.idea diff --git a/Cargo.toml b/Cargo.toml index 65e4aee..41a7297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ license = "MIT" [dependencies] nature_common = {path = "../Nature-Common", version="0.0.2"} -nature_demo_common = {path = "../Nature-Demo-Common", version="0.1.0"} +nature_demo_common = {path = "../Nature-Demo-Common", version="0.0.2"} serde_json = "1.0" serde = "1.0" diff --git a/README.md b/README.md index 4a50b99..bbbe295 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,35 @@ -# A concrete example -At here we would build an Online-Shop based on Nature. The project will involves order, pay, warehouse and delivery domain. Don't worry about the complexity, we start at simple first, then step by step to achieve the final target. +# Nature 应用示例 +[English](README_EN.md)|中文 -Nature have provide all implement for this demo. you will find all of them in the following projects. +如果你是第一次了解 Nature , 建议你从头到尾阅读这些 Demo。 每个章节都包含一些不同的 **Nature 要点**,以帮助你更好的了解 Nature 以及如何用 Nature 独有的方式来解决问题;同时阐述Nature 是如何简化技术性代码,使开发人员的聚焦于业务本身。下面为 Demo 相关的项目列表。 -- [test entry](https://github.com/llxxbb/Nature-Demo) -- [common defines](https://github.com/llxxbb/Nature-Demo-Common) -- [converter](https://github.com/llxxbb/Nature-Demo-Converter) +- [示例的入口](https://github.com/llxxbb/Nature-Demo) +- [服务于示例的一些通用封装](https://github.com/llxxbb/Nature-Demo-Common) +- [示例项目的转换器实现](https://github.com/llxxbb/Nature-Demo-Converter) +- [基于Restful的转换器实现](https://github.com/llxxbb/Nature-Demo-Converter-Restful) -For the benefit of the simplicity, here use local-converter to instead of http based converter. +如何启动 Nature 项目请参考:[项目准备](doc/unfinished/prepare.md) -## How to read it +## 网上商城 DEMO -If you are the first time to know Nature, It's best to read it from top to bottom. +这个Demo涉及的场景比较多,如订单,支付,库房,配送以及多维度的销售统计等。 -In the whole demo description. there are some sections titled with **"Nature key points"** that would mind your attention how to do the thing in Nature way. +这并不是一个完整的用于实际生产的例子,我们只关注业务核心逻辑。 -## Let‘s begin +| 章节 | 内容摘要 | Nature 要点 | +| -------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------ | +| [生成订单](doc/ZH/emall/emall-1-order-generate.md) | 用户向 Nature 提交一个订单 | `Meta`, master `meta`, target-state, `Converter` ,提交`Instance`到Nature。 | +| [支付订单](doc/ZH/emall/emall-2-pay-the-bill.md) | 用户可以对一个金额比较大的订单进行多次支付 | 选择上游,上下文(sys.target), 并发冲突控制 | +| [出库](doc/ZH/emall/emall-3-stock-out.md) | 库房的系统比较老旧,处理订单比较慢 | 提交`state-instance` ,回调,与已有系统的对接。 | +| [配送](doc/ZH/emall/emall-4-delivery.md) | 和第三方协作 | 参数化输入 | +| [签收](doc/ZH/emall/emall-5-signed.md) | 用户接收了订单中的货物 | 延迟转换 | -| chapter | digest | key points | -| --------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------ | -| [prepare](doc/prepare.md) | prepare for the demo | how to run Nature | -| [generate order](doc/order-generate.md) | user commit an order into to Nature | `Meta`, master `meta`, define target-state, `Converter` and how to commit business object to Nature | -| [pay for the bill](doc/pay-the-bill.md) | user can pay many times for the big bill. | upstream select, state conflict control | -| [stock-out](doc/stock-out.md) | the warehouse system is slow to process the order's goods | input state instance, callback | -| [delivery](doc/delivery.md) | collaborate with the third-party | parameterization input | -| [signed](doc/signed.md) | user received the goods | delay converter | - - -The following unfinished yet. - -| chapter | digest | key points | -| ------------- | ------------------------------------------------------- | ------------------------------- | -| e-book | extend category with need not delivery but can download | context | -| logistic bill | one order split into many logistic bill | user appointed id, grey deploy | - -[Q&A](doc/q&a.md) +## 统计DEMO +可以把Nature 看做一个简单的流式统计框架。 +| chapter | digest | key points | +| --------------------------------------- | ------------------------------------------------------------ | ----------------------------------------- | +| [sale statistics](doc/ZH/emall/emall-6-statistics.md) | from goods view, make statistics freely, extensible, no coding. | context, embedded counter, serial process | +| user consumption data | make data which can be got by user id, such as order list | parallel process | diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..825361f --- /dev/null +++ b/README_EN.md @@ -0,0 +1,46 @@ +# A concrete example + +English|[中文](README.md) + +At here we would build an Online-Shop based on Nature. The project will involves order, pay, warehouse and delivery domain. Even more we make some statistics through multi-dimensions. + +Don't worry about the complexity, we start at simple first, then step by step to achieve the final target. Even thou I think the code lines are great reduced compare to the traditional development, conservative estimate they are less than half. + +## How to read it + +If you are the first time to know Nature, It's best to view this demo from top to bottom. + +Each chapter include little key-points of Nature, this let you come to know Nature. + +In the whole demo description. there are some sections titled with **"Nature key points"** that would mind your attention how to do the thing in Nature way. + +## Demo projects + +Nature have provide all implement for this demo. you will find all of them in the following projects. + +- [test entry](https://github.com/llxxbb/Nature-Demo) +- [common defines](https://github.com/llxxbb/Nature-Demo-Common) +- [converter](https://github.com/llxxbb/Nature-Demo-Converter) +- [restful-converter](https://github.com/llxxbb/Nature-Demo-Converter-Restful) + +## Let‘s begin + +| chapter | digest | key points | +| --------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------ | +| [prepare](doc/EN/prepare.md) | prepare for the demo | how to run Nature | +| [generate order](doc/EN/emall/emall-1-order-generate.md) | user commit an order into to Nature | `Meta`, master `meta`, define target-state, `Converter` and how to commit business object to Nature | +| [pay for the bill](doc/EN/emall/emall-2-pay-the-bill.md) | user can pay many times for the big bill. | upstream select, state conflict control | +| [stock-out](doc/EN/emall/emall-3-stock-out.md) | the warehouse system is slow to process the order's goods | input state instance, callback | +| [delivery](doc/EN/emall/emall-4-delivery.md) | collaborate with the third-party | parameterization input | +| [signed](doc/EN/emall/emall-5-signed.md) | user received the goods | delay converter | + + +The following unfinished yet. + +| chapter | digest | key points | +| ------------------------------------ | ------------------------------------------------------------ | ----------------------------------------- | +| [sale statistics](doc/EN/emall/emall-6-statistics.md) | from goods view, make statistics freely, extensible, no coding. | context, embedded counter, serial process | +| user consumption data | make data which can be got by user id, such as order list | parallel process | + + + diff --git a/doc/order-generate.md b/doc/EN/emall/emall-1-order-generate.md similarity index 91% rename from doc/order-generate.md rename to doc/EN/emall/emall-1-order-generate.md index bfc53b7..f7544a5 100644 --- a/doc/order-generate.md +++ b/doc/EN/emall/emall-1-order-generate.md @@ -8,18 +8,18 @@ We suppose the user have goods selected, and use it to generate an order. First we will define two `meta`s. please insert the follow data to nature.sqlite. -- /B/sale/order: includes normal order properties. +- B:sale/order: includes normal order properties. -- /B/sale/orderState: the status for new, paid, outbound, dispatching, signed etcetera. +- B:sale/orderState: the status for new, paid, outbound, dispatching, signed etcetera. ```sqlite INSERT INTO meta (full_key, description, version, states, fields, config) -VALUES('/B/sale/order', 'order', 1, '', '', '{}'); +VALUES('B:sale/order', 'order', 1, '', '', '{}'); INSERT INTO meta (full_key, description, version, states, fields, config) -VALUES('/B/sale/orderState', 'order state', 1, 'new|paid|package|outbound|dispatching|signed|canceling|canceled', '', '{"master":"/B/sale/order:1"}'); +VALUES('B:sale/orderState', 'order state', 1, 'new|paid|package|outbound|dispatching|signed|canceling|canceled', '', '{"master":"B:sale/order:1"}'); ``` ### Nature key points @@ -28,7 +28,7 @@ In tradition design, order and order state will be fill into one table, in this mutex state are separated by "|". -`master` means if you did not appoint a `executor` for `orderState`, Nature will give a default conversion with empty body, and it's id will be same as `/B/sale/order`. You will see a `converter` that need a implement in the next chapter. +`master` means if you did not appoint a `executor` for `orderState`, Nature will give a default conversion with empty body, and it's id will be same as `B:sale/order`. You will see a `converter` that need a implement in the next chapter. ## Define `converter` @@ -37,7 +37,7 @@ When we input an `Order` from outside, we set a `new` state for this order by co ```sqlite INSERT INTO relation (from_meta, to_meta, settings) -VALUES('/B/sale/order:1', '/B/sale/orderState:1', '{"target_states":{"add":["new"]}}'); +VALUES('B:sale/order:1', '/B/sale/orderState:1', '{"target_states":{"add":["new"]}}'); ``` Let's see some explanation: diff --git a/doc/pay-the-bill.md b/doc/EN/emall/emall-2-pay-the-bill.md similarity index 100% rename from doc/pay-the-bill.md rename to doc/EN/emall/emall-2-pay-the-bill.md diff --git a/doc/stock-out.md b/doc/EN/emall/emall-3-stock-out.md similarity index 92% rename from doc/stock-out.md rename to doc/EN/emall/emall-3-stock-out.md index 49f1ad4..1f5704e 100644 --- a/doc/stock-out.md +++ b/doc/EN/emall/emall-3-stock-out.md @@ -14,7 +14,7 @@ Another thing is, a warehouse process `stock-out-application` instead of `order` -- orderState:paid --> orderState:package INSERT INTO relation (from_meta, to_meta, settings) -VALUES('/B/sale/orderState:1', '/B/sale/orderState:1', '{"selector":{"source_state_include":["paid"]},"executor":[{"protocol":"http","url":"http://localhost:8082/send_to_warehouse"}],"target_states":{"add":["package"]}}'); +VALUES('B:sale/orderState:1', 'B:sale/orderState:1', '{"selector":{"source_state_include":["paid"]},"executor":[{"protocol":"http","url":"http://localhost:8082/send_to_warehouse"}],"target_states":{"add":["package"]}}'); ``` ### Nature key points diff --git a/doc/delivery.md b/doc/EN/emall/emall-4-delivery.md similarity index 86% rename from doc/delivery.md rename to doc/EN/emall/emall-4-delivery.md index c42126c..414d8ce 100644 --- a/doc/delivery.md +++ b/doc/EN/emall/emall-4-delivery.md @@ -9,7 +9,7 @@ The problem is that we want to query express info by waybill id, and we do not w ```sqlite INSERT INTO meta (full_key, description, version, states, fields, config) -VALUES('/B/third/waybill', 'waybill', 1, '', '', '{}'); +VALUES('B:third/waybill', 'waybill', 1, '', '', '{}'); ``` ## Define converter @@ -18,12 +18,12 @@ VALUES('/B/third/waybill', 'waybill', 1, '', '', '{}'); -- orderState:outbound --> waybill INSERT INTO relation (from_meta, to_meta, settings) -VALUES('/B/sale/orderState:1', '/B/third/waybill:1', '{"selector":{"source_state_include":["outbound"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:go_express"}]}'); +VALUES('B:sale/orderState:1', 'B:third/waybill:1', '{"selector":{"source_state_include":["outbound"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:go_express"}]}'); -- waybill --> orderState:dispatching INSERT INTO relation (from_meta, to_meta, settings) -VALUES('/B/third/waybill:1', '/B/sale/orderState:1', '{"target_states":{"add":["dispatching"]}}'); +VALUES('B:third/waybill:1', 'B:sale/orderState:1', '{"target_states":{"add":["dispatching"]}}'); ``` ## Converter Implement diff --git a/doc/signed.md b/doc/EN/emall/emall-5-signed.md similarity index 69% rename from doc/signed.md rename to doc/EN/emall/emall-5-signed.md index 2e6c3c9..e75d4ff 100644 --- a/doc/signed.md +++ b/doc/EN/emall/emall-5-signed.md @@ -9,7 +9,7 @@ For our benefit, we make fortnight to 1 seconds, so that you can see the result ```sqlite INSERT INTO meta (full_key, description, version, states, fields, config) -VALUES('/B/sale/orderSign', 'order finished', 1, '', '', '{}'); +VALUES('B:sale/orderSign', 'order finished', 1, '', '', '{}'); ``` ## Define converter @@ -18,12 +18,12 @@ VALUES('/B/sale/orderSign', 'order finished', 1, '', '', '{}'); -- orderState:dispatching --> orderSign INSERT INTO relation (from_meta, to_meta, settings) -VALUES('/B/sale/orderState:1', '/B/sale/orderSign:1', '{"delay":1,"selector":{"source_state_include":["dispatching"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:auto_sign"}]}'); +VALUES('B:sale/orderState:1', 'B:sale/orderSign:1', '{"delay":1,"selector":{"source_state_include":["dispatching"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:auto_sign"}]}'); -- orderSign --> orderState:signed INSERT INTO relation (from_meta, to_meta, settings) -VALUES('/B/sale/orderSign:1', '/B/sale/orderState:1', '{"target_states":{"add":["signed"]}}'); +VALUES('B:sale/orderSign:1', 'B:sale/orderState:1', '{"target_states":{"add":["signed"]}}'); ``` ### Nature key points diff --git a/doc/EN/emall/emall-6-statistics.md b/doc/EN/emall/emall-6-statistics.md new file mode 100644 index 0000000..052e1c9 --- /dev/null +++ b/doc/EN/emall/emall-6-statistics.md @@ -0,0 +1,65 @@ +# Statistics + +After paid we want to make statistics for the products, and analysis them by multi-dimensions, but we are lazy to writing the code. Luckily Nature can do that for you. + +## Define `meta` + +```sqlite +INSERT INTO meta +(full_key, description, version, states, fields, config) +VALUES('M:statistics/orderTask', 'total sold every hour', 1, '', '', '{"multi_meta":{"keys":["minute","hour"]}, "conflict_avoid": true}'); +``` + +### how to make statistics + +If we we increase the counter for every order use `state-instance`, there would be many conflicts for high parallel process, and another question is that we would generated great volume of `state-instace`, so it's a terrible thing. + +There is a way to do it is that we count it every minute for minute data and every hour for hour data. to do that we should generate one none state task-instance for every minute and one for every hour. + +### Nature key points + +**"M"** `metaType` : express `multi-meta ` which will be processed parallelly, each key is defined in the `multi_meta.keys` property. For this demo, after converted Nature will save two instances. + +``` +B:statistics/orderTask/minute with para: current minute +B:statistics/orderTask/hour with para: current hour +``` + +**"conflict_avoid"** setting tell Nature that the same instances will generated many times and Nature should cache it and check it befor save. If `false`(default) is set would lead to a large number of duplicated insertions. so the performance would be very bad. + +## Define converter + +```sqlite +-- orderState:paid --> task +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderState:1', 'M:statistics/orderTask:1', '{"selector":{"source_state_include":["paid"]},"executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:statistics_task"}]}'); +``` + + + +## unready + +why delay 70 seconds? + +## + +### Questions + +There is a question, how to identify each inputted data for `consume/input`? used Nature generated instance id? no, it's hard to query it out, so we use parameterize instance technology in this converter. + +update the stateful-counter is a big bottleneck problem for busy system, so we use Nature's `delay` technology and stateless `meta` to hold every past minute data. You can form you hour data, day data and any wide range data through this mechanism, but in this demo we stopped at minute data, It's enough for you to understand how to use Nature for statistics effectively. + +### Nature key points + +Another question is how to give multi-dimensions info to the following converter?, sealed it to the `Instance.content` property? This is not a good idea, because `content`'s structure must be resolved by code! that is not we wanted. `context` will face on this problem. here we just used them in converter settings, no coding! (of course you can use `context` in your code explicitly). + + + +```sqlite +-- orderSign --> orderState:signed +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:statistics/consume/input:1', 'B:statistics/consume/product/total/minute:1', '{"target_states":{"add":["signed"]}}'); +``` + diff --git a/doc/ZH/emall/emall-1-order-generate.md b/doc/ZH/emall/emall-1-order-generate.md new file mode 100644 index 0000000..0933da4 --- /dev/null +++ b/doc/ZH/emall/emall-1-order-generate.md @@ -0,0 +1,162 @@ +# 生成订单 + +我们假设用户已经选择了商品,并以此生成订单。 + +## 定义 `Meta` + +首先我们需要定义两个 `Meta`,请执行下面的sql脚本 + +```sqlite +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'sale/order', 'order', 1, '', '', '{}'); + +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'sale/orderState', 'order state', 1, 'new|paid|package|outbound|dispatching|signed|canceling|canceled', '', '{"master":"B:sale/order:1"}'); +``` + +- B: `MetaType::Business` +- sale/order: 为订单 `Meta`。里面包含了商品,用户等信息. +- sale/orderState: 为订单状态`Meta`。里面定义了订单所用到的各种状态信息。 +- master:说明 orderState 依附于 order。这会 让Nature 对 orderState的处理方式产生影响,如 ID 属性,下面会有说明。 + +### Nature 关键点 + +在传统的设计方式里,“订单”和“订单状态”一般情况下会放到一张数据表中,新的状态会覆盖掉旧的状态,所以要跟踪这些状态变化需要额外的机制来保障,这是一件比较困难的事。Nature 建议将订单和订单状态分开存放,原因是订单数据是不变的而订单状态是需要变化的。 + +**Nature 中的常规数据一旦生成将不允许改变或者删除,而状态数据的每次变更都会生成一个新副本。**所以如果将订单和订单状态合在一起, Nature 将产生过多的冗余数据。用好 Nature 的这种机制既满足了状态跟踪需求,又减少了存储空间,而这个复杂性对Nature 的使用人员来讲是无感知的。 + +**“|”**:表示 `orderState` 的状态是**互斥**的,既当生成一个 `paid` 状态的`orderState` 实例时,这个实例的状态不允许包含诸如 `new`等的其它状态。Nature 对互斥支持的很好,如果你输入一个新的状态,她自动会替换掉与之互斥的其它状态。 + +`master` 说明 `orderState` 依附于 `order`,这是个非常重要的属性,如果应用的好,你只需定义 `converter` 而可无需实现 `converter` 就可以实现`Meta`间示例的转换。 + +## 定义 `converter` + +当你从外部输入一个`order Instance`到 Nature 后,我们需要设置这个 `order` 的状态为 `new`。要实现这个功能我们需要定义一个 `converter`, 请执行下面的 sql。 + +```sqlite +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/order:1', 'B:sale/orderState:1', '{"target_states":{"add":["new"]}}'); +``` + +`relation`数据表用于存储 `converter` 的定义,相关说明如下: + +| 字段或属性 | 说明 | +| --------------- | ------------------------------------------------------------ | +| from_meta | `converter`的输入,格式为 [`MetaType`]:[key]:[version] | +| to_meta | `converter`的输出,格式同 from_meta | +| settings | 是一个 `JSON` 形式的配置对象,用于说明如何这个关系。 | +| `target_states` | 当 `converter` 转换完成后,该属性会要求 Nature 在返回的实例上添加或移除状态。 | + +## 定义`Order`和相关的业务对象 + +在 `Nature-Demo-Common` 项目中我们需要定义一些业务对象,它们会被 `Nature-Demo`项目用到。 + +```rust +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct Commodity { + pub id: u32, + pub name: String, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct SelectedCommodity { + pub item: Commodity, + pub num: u32, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct Order { + pub user_id: u32, + pub price: u32, + pub items: Vec, + pub address: String, +} +``` + +### Nature 要点 + +**我们不需要为`Order`定义ID属性**, `Order` 实例在运行时会依附于一个 `Instance`,而Nature会自动为`Instance`创建一个ID。 + +在这里我们没有定义 `OrderState`对象, 这是因为除了 `Meta`中定义的状态列表外我们不在需要什么其他属性。 + +## 提交 `Instance` 到 Nature + +在 Nature-Demo 项目中,我们构建了一个 `Order` 的实例,它包含了一部电话和两块电池。 + +```rust +fn create_order() -> Order { + Order { + user_id: 123, + price: 1000, + items: vec![ + SelectedCommodity { + item: Commodity { id: 1, name: "phone".to_string() }, + num: 1, + }, + SelectedCommodity { + item: Commodity { id: 2, name: "battery".to_string() }, + num: 2, + } + ], + address: "a.b.c".to_string(), + } +} +``` + +并且把这个实例的JSON形式绑定到 `Instance。content` 上,这个`Instance`的 `MetaType` 为 "/B/order:1"。 + +```rust + // 创建一个订单对象 + let order = create_order(); + // ---- 闯将一个 instance, 其 meta 为: "B:order:1" + let mut instance = Instance::new("/sale/order").unwrap(); + instance.content = serde_json::to_string(&order).unwrap(); +``` + +然后我们把这个`Instance` 提交给 Nature + +```rust + let response = CLIENT.post(URL_INPUT).json(&instance).send(); + let id_s: String = response.unwrap().text().unwrap(); + let id: Result = serde_json::from_str(&id_s).unwrap(); + let id = id.unwrap(); +``` + +`URL_INPUT` 参数的形式是: "http://{server}:{port}/input"。Nature 将保存这个 `Instance`,如果成功Nature 将返回这个`Instance`的ID,否则返回错误信息。 + +#### Nature 要点 + +用于创建 `instance` 的 `meta` 必须已经在meta 数据表中定义过。 + +如果你没有为 `Instance` 指定一个ID,Nature 会为你生成一个 128 位的 hash 值作为它的ID + +同一个`Instance`你可以提交多次,它们会返回相同的ID,Nature 是幂等的。 + +## Nature 幕后为你做了什么 + +Nature 通过 `Order` 的 `Relation`会好到 `OrderState` ,因为 `Relation` 中的`Converter`没有定义 `Executor`, Nature 会自动进行转换,将 order `Instance` 转换为 orderState `Instance` 。 + +因为 orderState 的 master 是 order ,所以Nature 将orderState `Instance` 的 ID 设置为 order `Instance` 的ID。 + +又因为`Converter` 的 target_states 属性指定了“new” 状态。所以 orderState的状态里有一个“new”。 + +### Nature 要点 + +在这个示例中 order 和 orderState 的 `Instance` 具有相同的 ID, 这样做的好处就是,我可以用一个ID就可以将所有相关联的业务数据一次性提取出来。而传统数据库的设计方式往往是需要外键转换的,这会影响性能。 + +## 与传统开发方式的区别 + +传统方式下设计对代码的约束是比较弱的,但通过上面的例子你可以看到,虽然我们的代码里面有 order 的定义,但是我们无法对`Meta`中的 order 进行重新定义,甚至orderState的值我们都不能自由设置。这说明Nature 的`设计时`会对`运行时`进行强制约束。 + +这种约束就像接口对实现的约束效果是一样的。只不过接口只能由代码来体现,而Nature的约束则可以有业务方来直接表达。这就减少的很多中间环节,时间和人员成本也就跟着降下来了。另一方面,因为减少的中间环节,信息就不会失真,目标表达更准确,代码也就少走了很多弯路, + +不知道你有没有发现,所有的 `Instance` 都是由Nature 进行存储的,也就是说业务系统可以完全不用考虑数据库的事情,我不知道这会为业务系统减少多少负担。 + +Demo中有反复提交的演示,以说明Nature 是幂等的。不仅如此Nature 还会为你默默的处理好像重试、最终一致性等问题,大幅度减少传统业务系统的技术复杂度,使开发人员更专注于业务的实现。 + +Nature 对业务系统简化的不仅仅是技术复杂性,对业务逻辑的简化也是比较显著。本示例中业务系统只是提交一个 order 的`Instance`到 Nature, Nature 就自动生成了orderState 并维护了它的状态。状态处理在业务系统中是非常难以维护的业务逻辑,尤其是业务一致性保障及状态跟踪。而Nature 几乎不用写代码就可以实现复杂的状态处理。 + +业务系统越简单就越不容易出错,也就越健壮、稳定。 \ No newline at end of file diff --git a/doc/ZH/emall/emall-2-pay-the-bill.md b/doc/ZH/emall/emall-2-pay-the-bill.md new file mode 100644 index 0000000..72a6f85 --- /dev/null +++ b/doc/ZH/emall/emall-2-pay-the-bill.md @@ -0,0 +1,222 @@ +# 支付订单 + +现在我们要买单。我们让情景故意复杂一点,我们假设用户的每张银行卡里的钱都不足以全额支付这笔订单,但是三张加起来是可以的。 + + ## 定义`meta` + +```sqlite +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'finance/payment', 'order payment', 1, '', '', '{}'); + +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'finance/orderAccount', 'order account', 1, 'unpaid|partial|paid', '', '{"master":"B:sale/order:1"}'); +``` + +`payment` :用于记录用户每一笔的支付情况 + +`orderAccount`:用于记录订单地支付状态,它也是个`state-meta`,因为它有状态定义。 + +## 定义 `converter` + +```sqlite +-- order --> orderAccount +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/order:1', 'B:finance/orderAccount:1', '{"executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:order_receivable"}],"target_states":{"add":["unpaid"]}}'); + +-- payment --> orderAccount +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:finance/payment:1', 'B:finance/orderAccount:1', '{"executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:pay_count"}]}'); + +-- orderAccount --> orderState +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:finance/orderAccount:1', 'B:sale/orderState:1', '{"selector":{"source_state_include":["paid"]},"target_states":{"add":["paid"]}}'); +``` + +我们需要几个Nature 外的`Converter` 来完成我们的任务: + +**order --> orderAccount** :用于为每个订单创建一个订单账并记录订单地应收信息。 + +**payment --> orderAccount** :用于记录订单的每一笔支付,并根据支付情况设置支付状态。 + +**orderAccount --> orderState** :因为没有指定`executor`,所以是一个自动的转换器, + +### `settings`中的属性说明 + +| 属性 | 描述 | +| -------------------- | ------------------------------------------------------------ | +| executor | 用于告诉 Nature 使用用户自定义的转换器 | +| protocol | 告诉 Nature 如何与 `executor`通讯。`LocalRust` 告诉 Nature `executor` 是本地的一个 lib 包。 | +| url | 告诉Nature 哪里可以找到这个 `executor`。 | +| source_state_include | 是一个过滤器,在本示例里,只有上游的状态包含“paid” `Converter` 才可以进行转换 | + +## 定义业务对象 + +我们需要在`Nature-Demo-Common`项目中定义一些业务对象,它们会被接下来的 `Nature-Demo` 和 `Nature-Demo-Converter`项目使用。 + +```rust +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct Payment { + pub order: u128, + pub from_account: String, + pub paid: u32, + pub pay_time: i64, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] +pub struct OrderAccount { + pub receivable: u32, + /// 不能超过应收的值, 过多的钱需要放到 diff 中去。 + /// 这样每一笔的超出都可以被跟踪 + pub total_paid: u32, + pub last_paid: u32, + /// 变账的原因 + pub reason: OrderAccountReason, + /// 正: 超付, 负 : 欠款 + pub diff: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub enum OrderAccountReason { + NewOrder, + Pay, + CancelOrder, +} + +impl Default for OrderAccountReason { + fn default() -> Self { + OrderAccountReason::Pay + } +} +``` + +## 实现执行器 "**order --> orderAccount**" + +这个实现我们把它放在 `Nature-Demo-Converter` 项目里如下: + +```rust +#[no_mangle] +pub extern fn order_receivable(para: &ConverterParameter) -> ConverterReturned { + let order: Order = serde_json::from_str(¶.from.content).unwrap(); + let oa = OrderAccount { + receivable: order.price, + total_paid: 0, + last_paid: 0, + reason: OrderAccountReason::NewOrder, + diff: 0 - order.price as i32, + }; + let mut instance = Instance::default(); + instance.content = serde_json::to_string(&oa).unwrap(); + ConverterReturned::Instances(vec![instance]) +} +``` + +这个实现没有什么秘密,但是你需要知道如何实现一个本地执行器。 + +### Nature 要点 + +在执行器里可以通过下面的语句来获得业务对象。 + +```rust +let biz_obj = serde_json::from_str(¶.from.content).unwrap(); +``` + +执行器需要将返回的业务对象赋值给 `Instance.content` 属性。 + +对于像 `orderAccount` 这样的`state-meta`你只能返回一个 `Instance`。 + +## 实现执行器 **payment --> orderAccount**" + +```rust +#[no_mangle] +pub extern fn pay_count(para: &ConverterParameter) -> ConverterReturned { + let payment: Payment = serde_json::from_str(¶.from.content).unwrap(); + if para.last_state.is_none(){ + return ConverterReturned::EnvError; + } + let old = para.last_state.as_ref().unwrap(); + let mut oa: OrderAccount = serde_json::from_str(&old.content).unwrap(); + let mut state = String::new(); + if payment.paid > 0 { + state = "partial".to_string(); + } + oa.total_paid += payment.paid; + oa.diff = oa.total_paid as i32 - oa.receivable as i32; + if oa.diff > 0 { + oa.total_paid = oa.receivable; + } + if oa.diff == 0 { + state = "paid".to_string(); + } + oa.last_paid = payment.paid; + oa.reason = OrderAccountReason::Pay; + let mut instance = Instance::default(); + instance.content = serde_json::to_string(&oa).unwrap(); + instance.states.insert(state); + ConverterReturned::Instances(vec![instance]) +} +``` + +### Nature 要点 + +如果 `orderAccount` 还没有被初始化,表示应收还没有写入,我们需要等待其写入。这时需要返回`ConverterReturned::EnvError`,这样Nature 在将来的某一个时刻可以重试这次的执行过程。 + +你可以通过`¶.from.content`来得到`Payment`。 + +如果目标是一个`state-meta`,Nature 会将其当前最新的一个`Instance`传递给执行器。如下面的代码可以得到最新的`orderAccount`, + +```rust + let old = para.last_state.as_ref().unwrap(); + let mut oa: OrderAccount = serde_json::from_str(&old.content).unwrap(); +``` + +但是Nature 如何知道要加载的`orderAccount`的id呢?答案在下一下节。 + +因为 `orderAccount` 是一个 `state-meta`,所以当你返回一个新的`orderAccount` `Instance`时,Nature 将自动增加它的`state_version` 值。你**不必担心冲突问题**,Nature 会检测到这种情况并重新调用执行器,以修正结果。如示例中演示的那样。 + +## 提交支付数据到 Nature + +You will see the whole codes in project `Nature-Demo`, key codes list here only: + +```rust +pub fn user_pay(order_id: u128) { + let _first = pay(order_id, 100, "a", Local::now().timestamp_millis()); + let time = Local::now().timestamp_millis(); + let _second = pay(order_id, 200, "b", time); + let _third = pay(order_id, 700, "c", Local::now().timestamp_millis()); + let _second_repeat = pay(order_id, 200, "b", time); +} + +fn pay(id: u128, num: u32, account: &str, time: i64) -> u128 { + let payment = Payment { + order: id, + from_account: account.to_string(), + paid: num, + pay_time: time, + }; + let mut context: HashMap = HashMap::new(); + context.insert("sys.target".to_string(), id.to_string()); + match send_instance_with_context("finance/payment", &payment, &context) { + Ok(id) => id, + _ => 0 + } +} +``` + +### Nature 要点 + +还记得上一小节的问题吗?秘密就在于 **"sys.target"** 上下文上。调用者是知道要为那个订单付款的,而`orderAccount`和`order`共享同一个ID,所以上一小节中的`orderAccount`的ID值就来源于**"sys.target"** 的值。可见相同ID如果应用的恰当,业务逻辑上会有简化。在这个示例中,我们就不需要自己写代码查`orderAccount`数据了,Nature 可以自动为我们查出来。 + +## Nature 幕后为你做了什么 + +在这个示例里我们还是没有为`orderState`写一行代码,但是我们可以看到数据库里有两条数据,也就是说Nature 自动为我们生成了一条新的状态数据。版本1 的状态是“new”, 版本2的状态是“paid”,**这就是Nature的原则,永远不会修改和删除数据**。 + +## 与传统开发方式的区别 + +我们大约写了100行的代码完成了这个复杂的业务逻辑。包含并发,状态冲突控制,重试策略等,这在传统开发模式下是不太可能做到的。 + +你可以看到,我们在增加 `orderAccount` 是,没有变更上一节中已有的逻辑。在传统开发方式下一般是自上而下控制,很可能的情形是,在生成`order`的时候同时生成`orderState`和`orderAccount`,并用事务来保证一致性。这是一种非常复杂和低效的方式,而Nature 利用`自由选择`上游的方式实现了插拔式的工件,使得既有系统非常容易扩展和维护。 \ No newline at end of file diff --git a/doc/ZH/emall/emall-3-stock-out.md b/doc/ZH/emall/emall-3-stock-out.md new file mode 100644 index 0000000..e4c72cf --- /dev/null +++ b/doc/ZH/emall/emall-3-stock-out.md @@ -0,0 +1,93 @@ +# 出库 + +当订单支付完成后,我们就需要履行合同了。第一步就是出库。我们假设库房管理系统已经存在且比较老旧,运行缓慢,不能直接和Nature进行通信。为了使它能够和Nature能够进行通讯,库房的开发工程师封装了一个中间层并将它部署到库房里。因为库房是一个独立的系统,所以我们就不需要库房相关的`Meta`定义了。 + +我们在这里假设库房管理系统与Nature的通讯往往要超时,所以我们在本示例里采用一种新的机制来面对这个问题:回调。 + +## 一些限制说明 + +在真实的情况中,一个订单可能包含不同的商品,而这些商品也可能分布在不同的库房中。一般情况下每个库房的商品都需要单独跟踪。本示例为了简单起见,假定所有的商品都在同一个库房里。 + +## 定义`Converter` + +只要将订单下传到库房管理系统,我们就认为订单正在打包了。 + +```sqlite +-- orderState:paid --> orderState:package +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderState:1', 'B:sale/orderState:1', '{"selector":{"source_state_include":["paid"]},"executor":[{"protocol":"http","url":"http://localhost:8082/send_to_warehouse"}],"target_states":{"add":["package"]}}'); +``` + +### Nature 要点 + +`Protocol::http`: Nature 可以通过Http协议与外部的`executor`进行通讯。 + +## 处理流程示意图 + +```mermaid +graph LR + order:paid-->send[出库申请] + send-->wh[出库处理] + wh-->order:outbound +``` + +## 实现`executor` + +下面这个`executor`的实现主要是将订单信息下传到库房: + +```rust +fn send_to_warehouse(para: Json) -> HttpResponse { + thread::spawn(move || send_to_warehouse_thread(para.0)); + // 让Nature等待60s,如果60s内没有响应,Nature将会重试。 + HttpResponse::Ok().json(ConverterReturned::Delay(60)) +} + +fn send_to_warehouse_thread(para: ConverterParameter) { + // TODO 将订单下传给库房管理系统。 + // 等待 50ms, 以模拟上面的下传操作时间。 + thread::sleep(Duration::new(0, 50000)); + // 返回库房的处理结果 + let rtn = DelayedInstances { + task_id: para.task_id, + result: ConverterReturned::Instances(vec![para.from]), + }; + let rtn = CLIENT.post(&*NATURE_CALLBACK_ADDRESS).json(&rtn).send(); + let text: String = rtn.unwrap().text().unwrap(); + if text.contains("Err") { + error!("{}", text); + } else { + debug!("warehouse business processed!") + } +} +``` + +上面的代码并没有写出业务逻辑来,真正的业务逻辑需要在新起的线程里异步处理。这里只是给出了如何和Nature进行异步通信的方法。 + +### Nature 要点 + +`callback`:`executor`可以异步执行一个需要长时间运行的任务,在这种情况下,`executor`需要立即返回`ConverterReturned::Delay(seconds)` 给Nature,此返回值的意思是,挂起当前的处理并等待通知,如在等待指定的时间内还没有反馈,则进行重试, + +当`executor`处理完后需要将正式的结果通过`DelayedInstances` 来告知Nature 而不是`ConverterReturned`。并且`DelayedInstances.task_id` 的值一定是`para.task_id`的值,Nature 可以通过这个 task_id 来唤起挂起的任务。 + +## 将出库信息反馈给Nature + +接收了出库申请后,库房管理人员或机器人要依据订单地内容进行拣货和打包和出库。此时中间层应该通知Nature 改变订单地状态,以驱动后面的流程。示例代码如下: + +```rust + let mut instance = Instance::new("/sale/orderState").unwrap(); + instance.id = one_order.id; + instance.state_version = one_order.state_version + 1; + instance.states.insert("outbound".to_string()); + let rtn = send_instance(&instance); +``` + +### Nature 要点 + +一定要设置`instance.id `为要出库的订单ID,否则Nature 会分配一个新的ID,这将导致订单在系统中无法出库。 + +`state_version` 必须要在原有的基础上加一,否则会引起冲突,无法处理. + +## 与传统开发方式的区别 + +Nature 能够强有力的对逻辑实现进行肢解,大幅度降低彼此之间的耦合,这样就为基于分布式的异构系统间的业务往来提供了良好的协作平台,即便是老旧的系统仍然可以发挥应有的价值。 \ No newline at end of file diff --git a/doc/ZH/emall/emall-4-delivery.md b/doc/ZH/emall/emall-4-delivery.md new file mode 100644 index 0000000..37b7bf3 --- /dev/null +++ b/doc/ZH/emall/emall-4-delivery.md @@ -0,0 +1,52 @@ +# 配送 + +现在我们需要一些快递公司来帮助我们将包裹送给消费者,Nature 将记录这些派件单信息并在以后的某个时间进行查询,如每个月的结算。 + +我们想按照快递公司名称和派件单ID来与对方进行结算,假设我们不想在Nature 外单独建立一个数据库来存储这些信息,让我们看一下Nature 是怎么面对这个问题的。 + +## 定义`meta` + +```sqlite +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'third/waybill', 'waybill', 1, '', '', '{}'); +``` + +## 定义`Converter` + +```sqlite +-- orderState:outbound --> waybill +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderState:1', 'B:third/waybill:1', '{"selector":{"source_state_include":["outbound"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:go_express"}]}'); + +-- waybill --> orderState:dispatching +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:third/waybill:1', 'B:sale/orderState:1', '{"target_states":{"add":["dispatching"]}}'); +``` + +## 实现`executor` + +```rust +#[no_mangle] +#[allow(unused_attributes)] +#[allow(improper_ctypes)] +pub extern fn go_express(para: &ConverterParameter) -> ConverterReturned { + // "any one" 会被Nature修正为正确的目标`Meta,这里只是说明 `executor`无法重定向目标`Meta`,否则容易引发流程上的混乱和不可控。 + let mut ins = Instance::new("any one").unwrap(); + ins.id = para.from.id; + // 服务于下一个转换器,用于找出 orderState 对应的 `Instance` + ins.context.insert("sys.target".to_owned(), para.from.id.to_string()); + // ... 将包裹信息发送给快递公司,并等待其返回派件单ID, + // 模拟一个派件单ID,快递公司模拟为:ems + ins.para = "/ems/".to_owned() + &generate_id(¶.master.clone().unwrap().data).unwrap().to_string(); + ConverterReturned::Instances(vec![ins]) +} +``` + +### Nature 要点 + +Nature 使用`Instance.para`保存**"company id + waybill id"**。这样你就可以用 `para`来获取`Instance`了。 + +再一次我们使用了`sys.target context`,这可能让人有一些奇怪,因为 `waybill`根本不需要它。但是下一个`Converter` **waybill --> orderState:dispatching** 的 `orderState` 的`Instance`ID如何确定呢?因为这是个`auto converter`,`waybill`本身是没有这个信息的,所以这个信息只能放到`sys.target`里。 \ No newline at end of file diff --git a/doc/ZH/emall/emall-5-signed.md b/doc/ZH/emall/emall-5-signed.md new file mode 100644 index 0000000..de07709 --- /dev/null +++ b/doc/ZH/emall/emall-5-signed.md @@ -0,0 +1,35 @@ +## 签收 + +这是订单处理流程的最后一步:签收。但是物流公司并不主动将签收信号反馈给我们,我们需要用户登录到我们的系统上来,然后点击签收按钮,但是他们中的很多人根本不这么做。那么我们怎么完成这些订单呢?一个可行的方法是,我们等待两个星期,如果这期间没有投诉,我们就自动签收它。 + +为了我们的时间着想,示例中我们将两星期压缩到1s,这样你就能够很快的看到结果。 + + + +## 定义`meta` + +```sqlite +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'sale/orderSign', 'order finished', 1, '', '', '{}'); +``` + +## 定义converter + +```sqlite +-- orderState:dispatching --> orderSign +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderState:1', 'B:sale/orderSign:1', '{"delay":1,"selector":{"source_state_include":["dispatching"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:auto_sign"}]}'); + +-- orderSign --> orderState:signed +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderSign:1', 'B:sale/orderState:1', '{"target_states":{"add":["signed"]}}'); +``` + +### Nature 要点 + +`delay`:这个属性告诉Nature 不要让执行器立即执行任务,而是要等待指定的时间后再执行。 + +被推迟的任务只能通过`Nature-Retry`项目才能重新执行,所以本示例你需要将它启动起来。 \ No newline at end of file diff --git a/doc/ZH/statistics/emall-6-statistics.md b/doc/ZH/statistics/emall-6-statistics.md new file mode 100644 index 0000000..bf5af5a --- /dev/null +++ b/doc/ZH/statistics/emall-6-statistics.md @@ -0,0 +1,65 @@ +# Statistics + +After paid we want to make statistics for the products, and analysis them by multi-dimensions, but we are lazy to writing the code. Luckily Nature can do that for you. + +## Define `meta` + +```sqlite +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('M', 'statistics/orderTask', 'total sold every hour', 1, '', '', '{"multi_meta":{"keys":["minute","hour"]}, "conflict_avoid": true}'); +``` + +### how to make statistics + +If we we increase the counter for every order use `state-instance`, there would be many conflicts for high parallel process, and another question is that we would generated great volume of `state-instace`, so it's a terrible thing. + +There is a way to do it is that we count it every minute for minute data and every hour for hour data. to do that we should generate one none state task-instance for every minute and one for every hour. + +### Nature key points + +**"M"** `metaType` : express `multi-meta ` which will be processed parallelly, each key is defined in the `multi_meta.keys` property. For this demo, after converted Nature will save two instances. + +``` +B:statistics/orderTask/minute with para: current minute +B:statistics/orderTask/hour with para: current hour +``` + +**"conflict_avoid"** setting tell Nature that the same instances will generated many times and Nature should cache it and check it befor save. If `false`(default) is set would lead to a large number of duplicated insertions. so the performance would be very bad. + +## Define converter + +```sqlite +-- orderState:paid --> task +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderState:1', 'M:statistics/orderTask:1', '{"selector":{"source_state_include":["paid"]},"executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:statistics_task"}]}'); +``` + + + +## unready + +why delay 70 seconds? + +## + +### Questions + +There is a question, how to identify each inputted data for `consume/input`? used Nature generated instance id? no, it's hard to query it out, so we use parameterize instance technology in this converter. + +update the stateful-counter is a big bottleneck problem for busy system, so we use Nature's `delay` technology and stateless `meta` to hold every past minute data. You can form you hour data, day data and any wide range data through this mechanism, but in this demo we stopped at minute data, It's enough for you to understand how to use Nature for statistics effectively. + +### Nature key points + +Another question is how to give multi-dimensions info to the following converter?, sealed it to the `Instance.content` property? This is not a good idea, because `content`'s structure must be resolved by code! that is not we wanted. `context` will face on this problem. here we just used them in converter settings, no coding! (of course you can use `context` in your code explicitly). + + + +```sqlite +-- orderSign --> orderState:signed +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:statistics/consume/input:1', 'B:statistics/consume/product/total/minute:1', '{"target_states":{"add":["signed"]}}'); +``` + diff --git a/doc/demo-emall.sql b/doc/demo-emall.sql new file mode 100644 index 0000000..85e3335 --- /dev/null +++ b/doc/demo-emall.sql @@ -0,0 +1,74 @@ +-- generate order --------------------------------------------- +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'sale/order', 'order', 1, '', '', '{}'); + +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'sale/orderState', 'order state', 1, 'new|paid|package|outbound|dispatching|signed|canceling|canceled', '', '{"master":"B:sale/order:1"}'); + +-- order --> orderState +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/order:1', 'B:sale/orderState:1', '{"target_states":{"add":["new"]}}'); + +-- pay for the bill --------------------------------------------- +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'finance/payment', 'order payment', 1, '', '', '{}'); + +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'finance/orderAccount', 'order account', 1, 'unpaid|partial|paid', '', '{"master":"B:sale/order:1"}'); + +-- order --> orderAccount +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/order:1', 'B:finance/orderAccount:1', '{"executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:order_receivable"}],"target_states":{"add":["unpaid"]}}'); + +-- payment --> orderAccount +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:finance/payment:1', 'B:finance/orderAccount:1', '{"executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:pay_count"}]}'); + +-- orderAccount --> orderState +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:finance/orderAccount:1', 'B:sale/orderState:1', '{"selector":{"source_state_include":["paid"]},"target_states":{"add":["paid"]}}'); + +-- stock out --------------------------------------------- +-- orderState:paid --> orderState:package +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderState:1', 'B:sale/orderState:1', '{"selector":{"source_state_include":["paid"]},"executor":[{"protocol":"http","url":"http://localhost:8082/send_to_warehouse"}],"target_states":{"add":["package"]}}'); + +-- delivery --------------------------------------------- +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'third/waybill', 'waybill', 1, '', '', '{}'); + +-- orderState:outbound --> waybill +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderState:1', 'B:third/waybill:1', '{"selector":{"source_state_include":["outbound"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:go_express"}]}'); + +-- waybill --> orderState:dispatching +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:third/waybill:1', 'B:sale/orderState:1', '{"target_states":{"add":["dispatching"]}}'); + +-- signed --------------------------------------------- +INSERT INTO meta +(meta_type, meta_key, description, version, states, fields, config) +VALUES('B', 'sale/orderSign', 'order finished', 1, '', '', '{}'); + +-- orderState:dispatching --> orderSign +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderState:1', 'B:sale/orderSign:1', '{"delay":1,"selector":{"source_state_include":["dispatching"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:auto_sign"}]}'); + +-- orderSign --> orderState:signed +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('B:sale/orderSign:1', 'B:sale/orderState:1', '{"target_states":{"add":["signed"]}}'); + diff --git a/doc/demo.sql b/doc/demo.sql deleted file mode 100644 index 6dd702c..0000000 --- a/doc/demo.sql +++ /dev/null @@ -1,73 +0,0 @@ --- generate order --------------------------------------------- -INSERT INTO meta -(full_key, description, version, states, fields, config) -VALUES('/B/sale/order', 'order', 1, '', '', '{}'); - -INSERT INTO meta -(full_key, description, version, states, fields, config) -VALUES('/B/sale/orderState', 'order state', 1, 'new|paid|package|outbound|dispatching|signed|canceling|canceled', '', '{"master":"/B/sale/order:1"}'); - --- order --> orderState -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/sale/order:1', '/B/sale/orderState:1', '{"target_states":{"add":["new"]}}'); - --- pay for the bill --------------------------------------------- -INSERT INTO meta -(full_key, description, version, states, fields, config) -VALUES('/B/finance/payment', 'order payment', 1, '', '', '{}'); - -INSERT INTO meta -(full_key, description, version, states, fields, config) -VALUES('/B/finance/orderAccount', 'order account', 1, 'unpaid|partial|paid', '', '{"master":"/B/sale/order:1"}'); - --- order --> orderAccount -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/sale/order:1', '/B/finance/orderAccount:1', '{"executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:order_receivable"}],"target_states":{"add":["unpaid"]}}'); - --- payment --> orderAccount -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/finance/payment:1', '/B/finance/orderAccount:1', '{"executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:pay_count"}]}'); - --- orderAccount --> orderState -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/finance/orderAccount:1', '/B/sale/orderState:1', '{"selector":{"source_state_include":["paid"]},"target_states":{"add":["paid"]}}'); - --- stock out --------------------------------------------- --- orderState:paid --> orderState:package -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/sale/orderState:1', '/B/sale/orderState:1', '{"selector":{"source_state_include":["paid"]},"executor":[{"protocol":"http","url":"http://localhost:8082/send_to_warehouse"}],"target_states":{"add":["package"]}}'); - --- stock out --------------------------------------------- -INSERT INTO meta -(full_key, description, version, states, fields, config) -VALUES('/B/third/waybill', 'waybill', 1, '', '', '{}'); - --- orderState:outbound --> waybill -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/sale/orderState:1', '/B/third/waybill:1', '{"selector":{"source_state_include":["outbound"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:go_express"}]}'); - --- waybill --> orderState:dispatching -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/third/waybill:1', '/B/sale/orderState:1', '{"target_states":{"add":["dispatching"]}}'); - --- signed --------------------------------------------- -INSERT INTO meta -(full_key, description, version, states, fields, config) -VALUES('/B/sale/orderSign', 'order finished', 1, '', '', '{}'); - --- orderState:dispatching --> orderSign -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/sale/orderState:1', '/B/sale/orderSign:1', '{"delay":1,"selector":{"source_state_include":["dispatching"]}, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:auto_sign"}]}'); - --- orderSign --> orderState:signed -INSERT INTO relation -(from_meta, to_meta, settings) -VALUES('/B/sale/orderSign:1', '/B/sale/orderState:1', '{"target_states":{"add":["signed"]}}'); \ No newline at end of file diff --git a/doc/plan.md b/doc/plan.md index 98aaafc..ec6d7b1 100644 --- a/doc/plan.md +++ b/doc/plan.md @@ -1,8 +1,39 @@ # plan for demo -## find use case +## statistics + +```sql +INSERT INTO meta +(full_key, description, version, states, fields, config) +VALUES('B:statistics/productConsume/total/minute', 'total sold every minute', 1, '', '', '{}'); + +INSERT INTO meta +(full_key, description, version, states, fields, config) +VALUES('B:statistics/productConsume/sex/minute', 'total sold every minute', 1, '', '', '{}'); + +INSERT INTO meta +(full_key, description, version, states, fields, config) +VALUES('B:statistics/productConsume/ageRange/minute', 'total sold every minute', 1, '', '', '{}'); + +INSERT INTO meta +(full_key, description, version, states, fields, config) +VALUES('B:statistics/productConsume/total/hour', 'total sold every minute', 1, '', '', '{}'); + +INSERT INTO meta +(full_key, description, version, states, fields, config) +VALUES('B:statistics/productConsume/sex/hour', 'total sold every minute', 1, '', '', '{}'); -assign: delay execute. +INSERT INTO meta +(full_key, description, version, states, fields, config) +VALUES('/B/statistics/productConsume/ageRange/hour', 'total sold every minute', 1, '', '', '{}'); + +-- orderState:paid --> consumeInput +INSERT INTO relation +(from_meta, to_meta, settings) +VALUES('/B/statistics/product/consumeInput:1', 'M:statistics/productConsume/task/minute:1', '{"delay":70, "executor":[{"protocol":"localRust","url":"nature_demo_converter.dll:consume_input"}]}'); +``` + +## find use case use_upstream_id @@ -10,6 +41,18 @@ self convert to self, task_error : can't finish the task +context + +### 对“/” 分隔符的作用 + +用于可视化业务领域的分级管理 + +## 查分 + +业务情景,通过一个学生ID 利用`converter` 查询各科的成绩,然后求这个ID的总分。 + +Nature 技术点: after_finished 转换器设置。 + ## `MetaType::Null` This `converter` does not generate any thing to Nature, the reason is `to_meta` is "**/N:1**", it's `MetaType::Null`. But why this is usable? in this demo, we want to notify the warehouse, and Nature can achieve it **reliably**. @@ -26,16 +69,6 @@ use_upstream_id **`use_upstream_id`** property will be convenient for state data and it can only used to **state data**, because converter can return many **normal data**, the same id would make them conflict. - - -## Context - -generate order - -## Null instance - -You can not return empty array unless the target `meta type` is Null - ## Define `converter` | field | value description | @@ -50,7 +83,7 @@ You can not return empty array unless the target `meta type` is Null ## unfinished -###### ![process flow](processing_flow.png) +###### ![process flow](unfinished/processing_flow.png) ## plan goals @@ -79,7 +112,7 @@ __Notice:__ I specified status field for the `OrderStatus` goal, it is the only The second step is design path from one goal to another, let's see: -![how](how.png) +![how](unfinished/how.png) I drew the picture intent to make you understand easily. in actually the data makes up this picture comes from another table: `one_step_flow`. Let's see: diff --git a/doc/prepare.md b/doc/prepare.md deleted file mode 100644 index 18259fd..0000000 --- a/doc/prepare.md +++ /dev/null @@ -1,53 +0,0 @@ -# Prepare - - -## Download code & compile - -Before we start, we need nature can be run on you local machine. Nature is written in rust, so it need a compile environment. download the following code to a directory and compile Nature. - -- https://github.com/llxxbb/Nature -- https://github.com/llxxbb/Nature-Common -- https://github.com/llxxbb/Nature-DB -- https://github.com/llxxbb/Nature-Retry -- https://github.com/llxxbb/Nature-Integrate-test-converter -- https://github.com/llxxbb/Nature-Demo -- https://github.com/llxxbb/Nature-Demo-Converter -- https://github.com/llxxbb/Nature-Demo-Common -- https://github.com/llxxbb/Nature-Demo-Converter-Restful - -When the compiling finished, there will be tow execute files generated in Nature's target directory, you can run it directly according to your needs. for window that would be: - -- nature.exe : The Nature main program. -- retry.exe : reload failed task for Nature. - -## .env - -This is your config file for `nature` and `retry` to run. You can get it from the root of it's the project. and copy it to the directory where the compiled files lived in. Maybe you need modify the following items: - -```toml -DATABASE_URL=nature.sqlite - -NATURE_SERVER_ADDRESS=http://localhost:8080/redo_task - -SERVER_PORT=8080 -``` -## Create database for nature.sqlite - -The script is under the path : Nature-DB/migrations/2019-04-27_init/up.sql. - -If you have diesel_cli installed, you can run the following cmd in you shell window. - -```shell -diesel migration run -``` - -When nature.sqlite created. copy it to the path where nature.exe lived in. - -### mysql - -By default, we use sqlite to store data for Nature , if you want to use mysql, please edit the Nature's cargo.toml file, use mysql to replace sqlite in the following line and recompile Nature, then change the `DATABASE_URL` property to mysql in `.env` file. - -```toml -nature_db = {path = "../Nature-DB", features = ["sqlite"], version = "0.0.2"} -``` - diff --git a/doc/q&a.md b/doc/q&a.md deleted file mode 100644 index 32c7d01..0000000 --- a/doc/q&a.md +++ /dev/null @@ -1,6 +0,0 @@ -# Question And Answer - -## while doesn't implement shopping cart? - -Nature support shopping cart process, but it is complex for the start of the demo. Nature can use status meta to express it, you will find it here(unfinished). - diff --git a/doc/how.png b/doc/unfinished/how.png similarity index 100% rename from doc/how.png rename to doc/unfinished/how.png diff --git a/doc/how.puml b/doc/unfinished/how.puml similarity index 100% rename from doc/how.puml rename to doc/unfinished/how.puml diff --git a/doc/plan_goals.png b/doc/unfinished/plan_goals.png similarity index 100% rename from doc/plan_goals.png rename to doc/unfinished/plan_goals.png diff --git a/doc/plan_goals.puml b/doc/unfinished/plan_goals.puml similarity index 100% rename from doc/plan_goals.puml rename to doc/unfinished/plan_goals.puml diff --git a/doc/unfinished/prepare.md b/doc/unfinished/prepare.md new file mode 100644 index 0000000..afec8fe --- /dev/null +++ b/doc/unfinished/prepare.md @@ -0,0 +1,65 @@ +# 项目准备 + +## 环境说明 + +Nature 是用 rust 语言编写的, 你需要自行准备编译环境。这里以 windows 环境进行说明。 + +Nature 缺省使用 sqlite 数据库,如果想使用 mysql 数据库, 请编辑 Nature/cargo.toml文件并将 nature_db的依赖修改成下面的样子,并修改Nature/.env中数据库连接信息。 + +```toml +nature_db = {path = "../Nature-DB", features = ["mysql"], version = "0.0.2"} +``` + +## 下载代码 + +下载下面项目的代码到同一个目录下 + +- https://github.com/llxxbb/Nature +- https://github.com/llxxbb/Nature-Common +- https://github.com/llxxbb/Nature-DB +- https://github.com/llxxbb/Nature-Retry +- https://github.com/llxxbb/Nature-Integrate-test-converter +- https://github.com/llxxbb/Nature-Demo +- https://github.com/llxxbb/Nature-Demo-Converter +- https://github.com/llxxbb/Nature-Demo-Common +- https://github.com/llxxbb/Nature-Demo-Converter-Restful + +## 编译项目 + +然后进入 Nature 子目录并运行下面的命令。 + +```shell +cargo build +``` + +当编译完成后,在 Nature/target目录下有三个可执行文件: + +- nature.exe : Nature 的主程序. +- retry.exe : 为 Nature 重新加载因环境问题失败的任务,使其能够重新运行。 +- restful_converter.exe:服务于示例项目的基于restful的转换器实现 + + +## 修改配置文件 + +Nature/.env 文件是项目的配置文件,将其拷贝到Nature/target目录下,并修改相应的值,下面为缺省的值。 + +```toml +DATABASE_URL=nature.sqlite + +NATURE_SERVER_ADDRESS=http://localhost:8080/redo_task + +SERVER_PORT=8080 +``` +## 创建数据库 + +数据库的创建脚本位于Nature-DB/migrations/2019-04-27_init/up.sql。如果你安装了diesel_cli,你可以在终端上运行下面的命令: + +```shell +diesel migration run +``` + +当 nature.sqlite 创建完成后,将其复制到Nature/target目录下 + +## 启动 + +进入Nature/target目录,运行编译生成的三个可执行文件。 \ No newline at end of file diff --git a/doc/processing-flow.puml b/doc/unfinished/processing-flow.puml similarity index 100% rename from doc/processing-flow.puml rename to doc/unfinished/processing-flow.puml diff --git a/doc/processing_flow.png b/doc/unfinished/processing_flow.png similarity index 100% rename from doc/processing_flow.png rename to doc/unfinished/processing_flow.png diff --git a/src/common.rs b/src/common.rs index 5db45ca..f795dae 100644 --- a/src/common.rs +++ b/src/common.rs @@ -61,7 +61,7 @@ fn get_state_instance_by_id(id: u128, meta_full: &str, sta_ver: i32) -> Option Instance { loop { - if let Some(ins) = get_state_instance_by_id(order_id, "/B/sale/orderState:1", state_ver) { + if let Some(ins) = get_state_instance_by_id(order_id, "B:sale/orderState:1", state_ver) { return ins; } else { sleep(Duration::from_nanos(200000)) diff --git a/src/demo.rs b/src/demo.rs index 78d1dea..bbbac8c 100644 --- a/src/demo.rs +++ b/src/demo.rs @@ -10,15 +10,17 @@ fn demo_all_test() { user_pay(id); dbg!("package and outbound"); outbound(id); - dbg!("delay for auto signed"); + dbg!("delivery"); let _ = wait_for_order_state(id, 5); + dbg!("delay for auto signed"); + let _ = wait_for_order_state(id, 6); } #[test] fn temp_test() { let response = CLIENT.post(URL_GET_BY_ID).json(&ParaForQueryByID { id: 271448073389351988786345053349058430028, - meta: "/B/sale/orderState:1".to_string(), + meta: "B:sale/orderState:1".to_string(), state_version_from: 0, limit: 1, }).send(); diff --git a/src/finance.rs b/src/finance.rs index 22e9f84..c9efb64 100644 --- a/src/finance.rs +++ b/src/finance.rs @@ -20,7 +20,7 @@ pub fn user_pay(order_id: u128) { fn wait_until_order_account_is_ready(order_id: u128) { loop { - if let Some(_) = get_instance_by_id(order_id, "/B/finance/orderAccount:1") { + if let Some(_) = get_instance_by_id(order_id, "B:finance/orderAccount:1") { break; } else { sleep(Duration::from_nanos(200000)) @@ -37,7 +37,7 @@ fn pay(id: u128, num: u32, account: &str, time: i64) -> u128 { }; let mut context: HashMap = HashMap::new(); context.insert("sys.target".to_string(), id.to_string()); - match send_business_object_with_context("/finance/payment", &payment, &context) { + match send_business_object_with_context("finance/payment", &payment, &context) { Ok(id) => id, _ => 0 } diff --git a/src/sale.rs b/src/sale.rs index e27b673..79aedba 100644 --- a/src/sale.rs +++ b/src/sale.rs @@ -15,7 +15,7 @@ pub fn send_order_to_nature() -> u128 { assert_eq!(msg.contains("DaoDuplicated"), true); // check created instance for order - let rtn = get_instance_by_id(id, "/B/sale/order:1").unwrap(); + let rtn = get_instance_by_id(id, "B:sale/order:1").unwrap(); assert_eq!(rtn.id, id); // check created instance for order state @@ -24,11 +24,11 @@ pub fn send_order_to_nature() -> u128 { fn wait_until_order_state_is_ready(order_id: u128) -> u128 { loop { - if let Some(ins) = get_instance_by_id(order_id, "/B/sale/orderState:1") { + if let Some(ins) = get_instance_by_id(order_id, "B:sale/orderState:1") { assert_eq!(ins.id, order_id); assert_eq!(ins.states.contains("new"), true); let from = ins.from.as_ref().unwrap(); - assert_eq!(from.meta, "/B/sale/order:1"); + assert_eq!(from.meta, "B:sale/order:1"); return ins.id; } else { sleep(Duration::from_nanos(200000)) diff --git a/src/warehouse.rs b/src/warehouse.rs index 9d2efab..df51bb0 100644 --- a/src/warehouse.rs +++ b/src/warehouse.rs @@ -5,7 +5,7 @@ use crate::{send_instance, wait_for_order_state}; pub fn outbound(order_id: u128) { // for package let last = wait_for_order_state(order_id, 3); - let mut instance = Instance::new("/sale/orderState").unwrap(); + let mut instance = Instance::new("sale/orderState").unwrap(); instance.id = last.id; instance.state_version = last.state_version + 1; instance.states.clear();