Akka-CQRS(7)- CQRS Reader Actor 示范
我们在这篇通过一个具体CQRS-Reader-Actor的例子来示范akka-persistence的query端编程和应用。在前面的博客里我们设计了一个CQRS模式POS机程序的操作动作录入过程,并示范了如何实现CQRS的写端编程。现在我们可以根据这个例子来示范如何通过CQRS的读端reader-actor读取由writer-actor所写入的操作事件并将二进制格式的事件恢复成数据库表的行数据。
首先看看reader是如何从cassandra数据库里按顺序读出写入事件的:cassandra-plugin提供了currentEventsByPersistenceId函数,使用方法如下:
// obtain read journal by plugin id
val readJournal =
PersistenceQuery(system).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier) // issue query to journal
val source: Source[EventEnvelope, NotUsed] =
readJournal.currentEventsByPersistenceId("2022", 0, Long.MaxValue)
这个函数返回一个akka-stream的Source[EventEnvelope,_]类型,一个静态Source。还有另外一个eventsByPersistenceId函数可以返回实时动态的akka-stream Source。我们可以runFold一个静态的Source对流元素进行汇总形成一个集合:
// materialize stream, consuming events
val futureActions: Future[List[Any]] = source.runFold(List[Any]()) { (lstAny, evl) => evl.event :: lstAny }
我们可以通过模式匹配pattern-matching把List元素分辨出来:
implicit val system = ActorSystem("reader")
implicit val ec = system.dispatcher
implicit val mat = ActorMaterializer()
futureActions.onComplete {
case Success(acts) => acts.reverse.foreach {act => act match {
case LogOned(txn) => println(s"LogOn: $txn" )
case SalesLogged(txn) => println(s"LogSales: $txn")
case _ => println("unkown action !!!!!")
}}
case Failure(exception) => println(exception.getMessage)
}
试着运行一下得到下面的输出:
LogOn: TxnItem(20190509,16:12:23,1001,0,1,6,8,1,0,0,0,,1001,,,,,,,)
LogSales: TxnItem(20190509,16:12:34,1001,0,2,0,0,0,1300,0,0,,005,hainan pineapple,02,Grocery,01,,01,Sunkist)
LogSales: TxnItem(20190509,16:12:35,1001,0,3,0,0,0,300,0,0,,004,demon banana,02,Grocery,01,,01,Sunkist)
LogSales: TxnItem(20190509,16:12:36,1001,0,4,0,0,0,1050,0,0,,002,red grape,02,Grocery,01,,01,Sunkist)
unkown action !!!!!
unkown action !!!!!
如此已经可以将写操作中存入的事件恢复成为Action类结构了。
好了,现在我们稍微认真点做个详细的reader示范。回到我们的POS例子:如果我们调用以下写端指令:
posref ! POSMessage(1022, LogOn("1001", "123"))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, LogSales("0", "0", apple.code, 1,820))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, LogSales("0", "0", pineapple.code, 2, 1300))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, Subtotal(0))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, Discount(DISCTYPE.duplicated,false,"001",5))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, LogSales("0", "0", banana.code, 1, 300))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, LogSales("0", "0", grape.code, 3, 1050))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, Subtotal(1))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, Discount(DISCTYPE.duplicated,true,"001",10))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, LogSales("0", "0", orage.code, 10, 350))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, Discount(DISCTYPE.duplicated,false,"001",10))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, Subtotal(0))
scala.io.StdIn.readLine()
posref ! POSMessage(1022, Suspend)
scala.io.StdIn.readLine()
前面的博文里一再强调:CQRS的写端应简单直接地将指令存写入数据库,避免任何复杂的控制程序,尽量在读端读出指令、模拟处理过程中的状态转变处理,还原指令预期应产生的结果,在这个示范里就是正式的交易数据了。实际上我们在写端已经做了些状态转变处理过程:就在updataState函数里:
def updateState(evt: Action, lastSeqNr: BigInt = 0)(implicit nodeAddress: NodeAddress, persistenceId: PID, state: VchStates, items: VchItems, curItem: TxnItem): (VchStates, VchItems) = {
val (vs, vi) = updateStateImpl(evt, lastSeqNr)
log.step(s"${nodeAddress.address}-${persistenceId.id} run updateState($evt, $lastSeqNr) with results state[$vs], txns[$vi].")
(vs, vi)
}
def updateStateImpl(evt: Action, lastSeqNr: BigInt = 0)(implicit state: VchStates, items: VchItems, curItem: TxnItem): (VchStates, VchItems) = evt match {
case LogOned(csr) => (state.copy(seq = state.seq + 1, opr = csr, jseq = lastSeqNr), items)
case LogOffed => (state.copy(seq = state.seq + 1, opr = ""), items)
case RefundOned => (state.copy(seq = state.seq + 1, refd = true), items)
case RefundOffed => (state.copy(seq = state.seq + 1, refd = false), items)
case VoidOned => (state.copy(seq = state.seq + 1, void = true), items)
case VoidOffed => (state.copy(seq = state.seq + 1, void = false), items)
case SuperOned(suser) => (state.copy(seq = state.seq + 1, su = suser), items)
case SuperOffed => (state.copy(seq = state.seq + 1, su = ""), items)
case MemberOned(num) => (state.copy(seq = state.seq + 1, mbr = num), items)
case MemberOffed => (state.copy(seq = state.seq + 1, mbr = ""), items)
case SalesLogged(_,_,_,_,_) => (state.copy(
seq = state.seq + 1)
, items.addItem(curItem))
case Subtotaled(_) => (state.copy(
seq = state.seq + 1)
, items.addItem(curItem))
case Discounted(_,_,_,_) => (state.copy(
seq = state.seq + 1)
, items.addItem(curItem))
case PaymentMade(_,_,_) =>
val due = if (items.totalSales > 0) items.totalSales - items.totalPaid else items.totalSales + items.totalPaid
val bal = if (items.totalSales > 0) due - curItem.amount else due + curItem.amount
(state.copy(
seq = state.seq + 1,
due = (if ((curItem.amount.abs + items.totalPaid.abs) >= items.totalSales.abs) false else true)
)
,items.addItem(curItem.copy(
salestype = SALESTYPE.ttl,
price = due,
amount = curItem.amount,
dscamt = bal
)))
case VoucherNumed(_, tnum) =>
val vi = items.copy(txnitems = items.txnitems.map { it => it.copy(num = tnum) })
(state.copy(seq = state.seq + 1, num = tnum), vi)
case EndVoucher(vnum) => (state.nextVoucher.copy(jseq = lastSeqNr + 1), VchItems())
case _ => (state, items)
}
在写端的这个模拟处理结果是为了实时向客户端反馈它发出指令后产生的结果,如:
case cmd @ LogSales(acct,sdpt,scode,sqty,sprice) if cmdFilter(cmd,sender()) => {
log.step(s"${nodeAddress.address}-${persistenceId} running LogSales($acct,$sdpt,$scode,$sqty,$sprice) with state[$vchState],txns[$vchItems] ...")
var pqty = sqty
if (vchState.void || vchState.refd) pqty = -sqty
log.step(s"${nodeAddress.address}-${persistenceId} run LogSales($acct,$sdpt,$scode,$sqty,$sprice).")
persistEvent(SalesLogged(acct,sdpt,scode,sqty,sprice)) { evt =>
curTxnItem = buildTxnItem(evt).copy(
qty =pqty,
amount = pqty * sprice
)
val sts = updateState(evt)
vchState = sts._1
vchItems = sts._2
if (vchState.void) {
val (lstTxns, found) = vchItems.txnitems.foldLeft((List[TxnItem](), false)) { case (b, ti) =>
if (b._2)
(ti :: b._1, true)
else if (ti.txntype == TXNTYPE.sales && ti.salestype == SALESTYPE.itm && ti.acct == acct &&
ti.dpt == sdpt && ti.code == scode && ti.qty == sqty && ti.price == sprice)
(ti.copy(txntype = TXNTYPE.voided) :: b._1, true)
else (ti :: b._1, false)
}
vchItems = vchItems.copy(txnitems = lstTxns.reverse)
sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功冲销商品销售。", vchState, List(vchItems.topTxnItem))
} else {
sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 商品销售操作成功。", vchState, List(vchItems.topTxnItem))
}
}
}
上面这个POSResponse类实时把当前交易状态返回给客户端。而这个vchItems.topTxnItem正是一条正式的交易记录,也就是我们需要在读端还原的数据。所以,这个updateState在读端还是需要调用的。先看看整体reader示范的程序结构:
object ReaderDemo extends App {
implicit val system = ActorSystem("reader")
implicit val ec = system.dispatcher
implicit val mat = ActorMaterializer()
val cluster = Cluster(system)
implicit val nodeAddress: NodeAddress = NodeAddress(cluster.selfAddress.toString)
States.setShowSteps(true)
Reader.readActions(0, Long.MaxValue,"1022")
scala.io.StdIn.readLine()
mat.shutdown()
system.terminate()
}
object Reader extends LogSupport {
def readActions(startSeq: Long, endSeq: Long, persistenceId: String)(implicit sys: ActorSystem, ec: ExecutionContextExecutor, mat: ActorMaterializer, nodeAddress: NodeAddress) = {
implicit var vchState = VchStates()
implicit var vchItems = VchItems()
implicit var curTxnItem = TxnItem()
implicit val pid = PID(persistenceId)
// obtain read journal by plugin id
val readJournal =
PersistenceQuery(sys).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier)
// issue query to journal
val source: Source[EventEnvelope, NotUsed] =
readJournal.currentEventsByPersistenceId(persistenceId, startSeq, endSeq)
// materialize stream, consuming events
val futureActions: Future[List[Any]] = source.runFold(List[Any]()) { (lstAny, evl) => evl.event :: lstAny }
futureActions.onComplete {
case Success(txns) =>
buildVoucher(txns)
//vchItems.txnitems里就是恢复的正式交易记录。可以在这里把它们写入到业务数据库
println(vchItems.txnitems.reverse)
case Failure(excpt) =>
}
def buildVoucher(actions: List[Any])= {
actions.reverse.foreach { txn =>
txn match {
case EndVoucher(_) =>
vchItems.txnitems.foreach(println)
case ti@_ =>
curTxnItem = buildTxnItem(ti.asInstanceOf[Action])
val sts = updateState(ti.asInstanceOf[Action],0)
vchState = sts._1
vchItems = sts._2
}
}
}
}
}
顺便提一下:这个updateState函数的其中一个主要功能是对集合vchItems.txnitems内的txnitem元素进行更改处理,但我们使用的是函数式编程模式的不可变集合(immutable collections),每次都需要对集合进行遍历,如fold:
def subTotal: (Int, Int, Int, Int) = txnitems.foldRight((0, 0, 0, 0)) { case (txn, b) =>
if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales)
b.copy(_1 = b._1 + 1, _2 = b._2 + txn.qty, _3 = b._3 + txn.amount, _4 = b._4 + txn.dscamt)
else b
}
突然想到如果在一个很大的集合中只需要更新前面几条数据怎么办?难道还是需要traverse所有元素吗?在我们的例子里还真有这么样的需求:
def groupTotal(level:Int): (Int, Int, Int, Int) = {
val gts = txnitems.foldLeftWhile((0, 0, 0, 0, 0)) { case (b,txn) =>
if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales)
((b._1._1 +1,b._1._2 + txn.qty, b._1._3 + txn.amount, b._1._4 + txn.dscamt, b._1._5),false)
else {
if (txn.salestype == SALESTYPE.sub) {
if (((b._1._5) + 1) >= level)
((b._1._1, b._1._2, b._1._3, b._1._4, b._1._5 + 1), true)
else
((b._1._1, b._1._2, b._1._3, b._1._4, b._1._5 + 1), false)
} else b
}
}
(gts._1,gts._2,gts._3,gts._4)
}
这是一个分层小记函数:在遍历指令集时遇到另一条小记指令就终止小记运算,调用了foldLeftWhile函数来实现这样的场景。foldLeftWhile是一个定制的函数:
implicit class FoldLeftWhile[A](trav: Seq[A]) {
def foldLeftWhile[B](z: B)(op: ((B,Boolean), A) => (B, Boolean)): B = {
def go(acc: (B, Boolean), l: Seq[A]): (B, Boolean) = l match {
case h +: t =>
val nacc = op(acc, h)
if (!nacc._2)
go(nacc, t)
else
nacc
case _ => acc
}
go((z, false), trav)._1
}
}
下面附上本次示范中的源代码
build.sbt
name := "akka-cluster-reader" version := "0.1" scalaVersion := "2.12.8" libraryDependencies := Seq(
"com.typesafe.akka" %% "akka-cluster-sharding" % "2.5.19",
"com.typesafe.akka" %% "akka-persistence" % "2.5.19",
"com.typesafe.akka" %% "akka-persistence-query" % "2.5.19",
"com.typesafe.akka" %% "akka-persistence-cassandra" % "0.93",
"com.typesafe.akka" %% "akka-persistence-cassandra-launcher" % "0.93" % Test,
"ch.qos.logback" % "logback-classic" % "1.2.3"
)
resources/application.conf
akka.actor.warn-about-java-serializer-usage = off
akka.log-dead-letters-during-shutdown = off
akka.log-dead-letters = off
akka.remote.use-passive-connections=off akka {
loglevel = INFO
actor {
provider = "cluster"
} remote {
log-remote-lifecycle-events = on
netty.tcp {
hostname = "192.168.11.189"
port = 2551
}
} cluster {
seed-nodes = [
"akka.tcp://reader@192.168.11.189:2551"] log-info = off
sharding {
role = "shard"
passivate-idle-entity-after = 10 m
}
} persistence {
journal.plugin = "cassandra-journal"
snapshot-store.plugin = "cassandra-snapshot-store"
} } cassandra-journal {
contact-points = ["192.168.11.189"]
} cassandra-snapshot-store {
contact-points = ["192.168.11.189"]
}
message/Messages.scala
package datatech.cloud.pos import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
import akka.cluster.sharding._ object Messages { val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.CHINA) sealed trait Command {} case class LogOn(opr: String, passwd: String) extends Command
case object LogOff extends Command
case class SuperOn(su: String, passwd: String) extends Command
case object SuperOff extends Command
case class MemberOn(cardnum: String, passwd: String) extends Command
case object MemberOff extends Command //remove member status for the voucher
case object RefundOn extends Command
case object RefundOff extends Command
case object VoidOn extends Command
case object VoidOff extends Command
case object VoidAll extends Command
case object Suspend extends Command case class VoucherNum(vnum: Int) extends Command case class LogSales(acct: String, dpt: String, code: String, qty: Int, price: Int) extends Command
case class Subtotal(level: Int) extends Command
case class Discount(disctype: Int, grouped: Boolean, code: String, percent: Int) extends Command case class Payment(acct: String, num: String, amount: Int) extends Command //settlement 结算支付 // read only command, no update event
case class Plu(itemCode: String) extends Command //read only
case object GetTxnItems extends Command /* discount type */
object DISCTYPE {
val duplicated: Int = 0
val best: Int = 1
val least: Int = 2
val keep: Int = 3
} /* result message returned to client on the wire */
object TXNTYPE {
val sales: Int = 0
val refund: Int = 1
val void: Int = 2
val voided: Int = 3
val voidall: Int = 4
val subtotal: Int = 5
val logon: Int = 6
val supon: Int = 7 // super user on/off
val suspend: Int = 8 } object SALESTYPE {
val itm: Int = 2
val sub: Int = 10
val ttl: Int = 11
val dsc: Int = 12
val crd: Int = 13
} case class TxnItem(
txndate: String = LocalDate.now.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
,txntime: String = LocalDateTime.now.format(dateTimeFormatter).substring(11)
,opr: String = ""//工号
,num: Int = 0 //销售单号
,seq: Int = 1 //交易序号
,txntype: Int = TXNTYPE.sales//交易类型
,salestype: Int = SALESTYPE.itm //销售类型
,qty: Int = 1 //交易数量
,price: Int = 0 //单价(分)
,amount: Int = 0 //码洋(分)
,disc: Int = 0 //折扣率 (%)
,dscamt: Int = 0 //折扣额:负值 net实洋 = amount + dscamt
,member: String = "" //会员卡号
,code: String = "" //编号(商品、卡号...)
,acct: String = "" //账号
,dpt: String = "" //部类
)
object TxnItem {
def apply(vs: VchStates): TxnItem = TxnItem().copy(
opr = vs.opr,
num = vs.num,
seq = vs.seq,
member = vs.mbr
)
} case class VchStatus( //操作状态锁留给前端维护
qty: Int = 1,
refund: Boolean = false,
void: Boolean = false) case class VchStates(
opr: String = "", //收款员
jseq: BigInt = 0, //begin journal sequence for read-side replay
num: Int = 0, //当前单号
seq: Int = 0, //当前序号
void: Boolean = false, //取消模式
refd: Boolean = false, //退款模式
due: Boolean = true, //当前余额
su: String = "",
mbr: String = ""
) { def nextVoucher : VchStates = VchStates().copy(
opr = this.opr,
jseq = this.jseq + 1,
num = this.num + 1
)
} object STATUS {
val OK: Int = 0
val FAIL: Int = -1
} case class POSResponse (sts: Int, msg: String, voucher: VchStates, txnItems: List[TxnItem]) /* message on the wire (exchanged message) */
val shardName = "POSShard" case class POSMessage(id: Long, cmd: Command) {
def shopId = id.toString.head.toString
def posId = id.toString
} val getPOSId: ShardRegion.ExtractEntityId = {
case posCommand: POSMessage => (posCommand.posId,posCommand.cmd)
}
val getShopId: ShardRegion.ExtractShardId = {
case posCommand: POSMessage => posCommand.shopId
} case object PassivatePOS //passivate message
case object FilteredOut
case class DebugMode(debug: Boolean)
case class NodeAddress(address: String)
case class PID(id: String) }
States.scala
package datatech.cloud.pos
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import Messages._
import sdp.logging._ object Actions { implicit class FoldLeftWhile[A](trav: Seq[A]) {
def foldLeftWhile[B](z: B)(op: ((B,Boolean), A) => (B, Boolean)): B = {
def go(acc: (B, Boolean), l: Seq[A]): (B, Boolean) = l match {
case h +: t =>
val nacc = op(acc, h)
if (!nacc._2)
go(nacc, t)
else
nacc
case _ => acc
}
go((z, false), trav)._1
}
} case class ReadActions(startSeq: Int, endSeq: Int, persistenceId: String) sealed trait Action {}
case class LogOned(opr: String) extends Action
case object LogOffed extends Action
case class SuperOned(su: String) extends Action
case object SuperOffed extends Action
case class MemberOned(cardnum: String) extends Action
case object MemberOffed extends Action //remove member status for the voucher
case object RefundOned extends Action
case object RefundOffed extends Action
case object VoidOned extends Action
case object VoidOffed extends Action case class SalesLogged(acct: String, dpt: String, code: String, qty: Int, price: Int) extends Action
case class Subtotaled(level: Int) extends Action
case class Discounted(disctype: Int, grouped: Boolean, code: String, percent: Int) extends Action case class NewVoucher(vnum: Int) extends Action //新单, reminder for read-side to set new vnum
case class EndVoucher(vnum: Int) extends Action //单据终结标示
case object VoidVoucher extends Action case object SuspVoucher extends Action case class VoucherNumed(fnum: Int, tnum: Int) extends Action case class PaymentMade(acct: String, num: String, amount: Int) extends Action //settlement 结算支付 } object States extends LogSupport {
import Actions._ def setShowSteps(b: Boolean) = log.stepOn = b def buildTxnItem(evt: Action)(implicit vs: VchStates, vi: VchItems): TxnItem = evt match {
case LogOned(op) => TxnItem(vs).copy(
txntype = TXNTYPE.logon,
salestype = SALESTYPE.crd,
opr = op,
code = op
)
case LogOffed => TxnItem(vs).copy(
txntype = TXNTYPE.logon,
salestype = SALESTYPE.crd
)
case SuperOned(su) => TxnItem(vs).copy(
txntype = TXNTYPE.supon,
salestype = SALESTYPE.crd,
code = su
)
case SuperOffed => TxnItem(vs).copy(
txntype = TXNTYPE.supon,
salestype = SALESTYPE.crd
)
case MemberOned(cardnum) => TxnItem(vs).copy(
txntype = TXNTYPE.sales,
salestype = SALESTYPE.crd,
member = cardnum
)
case MemberOffed => TxnItem(vs).copy(
txntype = TXNTYPE.sales,
salestype = SALESTYPE.crd
)
case RefundOned => TxnItem(vs).copy(
txntype = TXNTYPE.refund
)
case RefundOffed => TxnItem(vs).copy(
txntype = TXNTYPE.refund
)
case VoidOned => TxnItem(vs).copy(
txntype = TXNTYPE.void
)
case VoidOffed => TxnItem(vs).copy(
txntype = TXNTYPE.void
)
case VoidVoucher => TxnItem(vs).copy(
txntype = TXNTYPE.voidall,
code = vs.num.toString,
acct = vs.num.toString
)
case SuspVoucher => TxnItem(vs).copy(
txntype = TXNTYPE.suspend,
code = vs.num.toString,
acct = vs.num.toString
)
case Subtotaled(level) =>
TxnItem(vs).copy(
txntype = TXNTYPE.sales,
salestype = SALESTYPE.sub
)
case Discounted(dt,gp,code,pct) => TxnItem(vs).copy(
txntype = TXNTYPE.sales,
salestype = SALESTYPE.dsc,
acct = code,
disc = pct
)
case PaymentMade(act,num,amt) => TxnItem(vs).copy(
txntype = TXNTYPE.sales,
salestype = SALESTYPE.ttl,
acct = act,
code = num,
amount = amt
) case SalesLogged(sacct,sdpt,scode,sqty,sprice) => TxnItem(vs).copy(
txntype = TXNTYPE.sales,
salestype = SALESTYPE.itm,
acct = sacct,
dpt = sdpt,
code = scode,
qty = sqty,
price = sprice,
amount = sprice * sqty,
dscamt = 0
)
case _ => TxnItem(vs)
} case class VchItems(txnitems: List[TxnItem] = Nil) { def noSales: Boolean = (txnitems.find(txn => txn.salestype == SALESTYPE.itm)).isEmpty def subTotal: (Int, Int, Int, Int) = txnitems.foldRight((0, 0, 0, 0)) { case (txn, b) =>
if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales)
b.copy(_1 = b._1 + 1, _2 = b._2 + txn.qty, _3 = b._3 + txn.amount, _4 = b._4 + txn.dscamt)
else b
} def groupTotal(level:Int): (Int, Int, Int, Int) = {
val gts = txnitems.foldLeftWhile((0, 0, 0, 0, 0)) { case (b,txn) =>
if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales)
((b._1._1 +1,b._1._2 + txn.qty, b._1._3 + txn.amount, b._1._4 + txn.dscamt, b._1._5),false)
else {
if (txn.salestype == SALESTYPE.sub) {
if (((b._1._5) + 1) >= level)
((b._1._1, b._1._2, b._1._3, b._1._4, b._1._5 + 1), true)
else
((b._1._1, b._1._2, b._1._3, b._1._4, b._1._5 + 1), false)
} else b
}
}
(gts._1,gts._2,gts._3,gts._4)
} def updateDisc(dt: Int, grouped: Boolean, disc: Int): (List[TxnItem],(Int,Int,Int,Int)) = {
//(salestype,(cnt,qty,amt,dsc),hassub,list)
val accu = txnitems.foldLeft((-1, (0,0,0,0), false, List[TxnItem]())) { case (b, txn) =>
var discAmt = 0
if ((b._1) < 0) {
if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales) {
if (txn.dscamt == 0)
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) - (txn.amount * disc / 100)
), false, txn.copy(
dscamt = - (txn.amount * disc / 100)) :: (b._4)))
else {
dt match {
case DISCTYPE.duplicated =>
if (txn.dscamt != 0) {
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) - (txn.amount + txn.dscamt) * disc / 100
), false, txn.copy(
dscamt = -(txn.amount + txn.dscamt) * disc / 100) :: (b._4)
))
} else {
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) - txn.amount * disc / 100
), false, txn.copy(
dscamt = -txn.amount * disc / 100) :: (b._4)
))
}
case DISCTYPE.keep => ((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) + txn.dscamt), false, txn :: (b._4)))
case DISCTYPE.best =>
discAmt = -(txn.amount * disc / 100)
if (discAmt < txn.dscamt)
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) + discAmt), false, txn.copy(
dscamt = discAmt
) :: (b._4)))
else
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) + txn.dscamt), false, txn :: (b._4)))
}
} } else ((b._1,b._2,b._3,txn :: (b._4)))
} else {
if ((b._3))
(((b._1), (b._2), true, txn :: (b._4)))
else {
if (txn.salestype == SALESTYPE.sub) {
if (grouped)
(((b._1), (b._2), true, txn :: (b._4)))
else
(((b._1), (b._2), false, txn :: (b._4)))
} else {
if (txn.salestype == SALESTYPE.itm && txn.txntype == TXNTYPE.sales) {
dt match {
case DISCTYPE.duplicated =>
if (txn.dscamt != 0) {
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) - (txn.amount + txn.dscamt) * disc / 100), false, txn.copy(
dscamt = -(txn.amount + txn.dscamt) * disc / 100) :: (b._4)
))
} else {
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) - txn.amount * disc / 100), false, txn.copy(
dscamt = -(txn.amount * disc / 100)) :: (b._4)
))
}
case DISCTYPE.keep => ((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) + txn.dscamt), false, txn :: (b._4)))
case DISCTYPE.best =>
discAmt = -(txn.amount * disc / 100)
if (discAmt < txn.dscamt)
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) + discAmt), false, txn.copy(
dscamt = discAmt
) :: (b._4)))
else
((txn.salestype, (
(b._2._1) + 1,
(b._2._2) + txn.qty,
(b._2._3) + txn.amount,
(b._2._4) + txn.dscamt), false, txn :: (b._4)))
}
}
else ((b._1, b._2, b._3, txn :: (b._4)))
}
}
} }
(accu._4.reverse,accu._2)
} def totalSales: Int = txnitems.foldRight(0) { case (txn, b) =>
if (txn.salestype == SALESTYPE.itm)
(txn.amount + txn.dscamt) + b
else b /*
val amt: Int = txn.salestype match {
case (SALESTYPE.plu | SALESTYPE.cat | SALESTYPE.brd | SALESTYPE.ra) => txn.amount + txn.dscamt
case _ => 0
}
amt + b */
} def totalPaid: Int = txnitems.foldRight(0) { case (txn, b) =>
if (txn.txntype == TXNTYPE.sales && txn.salestype == SALESTYPE.ttl)
txn.amount + b
else b
} def addItem(item: TxnItem): VchItems = VchItems((item :: txnitems)) //.reverse) } def LastSecOfDate(ldate: LocalDate): LocalDateTime = {
val dtStr = ldate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + " 23:59:59"
LocalDateTime.parse(dtStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
} def dateStr(dt: LocalDate): String = dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) def updateState(evt: Action, lastSeqNr: BigInt = 0)(implicit nodeAddress: NodeAddress, persistenceId: PID, state: VchStates, items: VchItems, curItem: TxnItem): (VchStates, VchItems) = {
val (vs, vi) = updateStateImpl(evt, lastSeqNr)
log.step(s"${nodeAddress.address}-${persistenceId.id} run updateState($evt, $lastSeqNr) with results state[$vs], txns[$vi].")
(vs, vi)
} def updateStateImpl(evt: Action, lastSeqNr: BigInt = 0)(implicit state: VchStates, items: VchItems, curItem: TxnItem): (VchStates, VchItems) = evt match {
case LogOned(csr) => (state.copy(seq = state.seq + 1, opr = csr, jseq = lastSeqNr), items)
case LogOffed => (state.copy(seq = state.seq + 1, opr = ""), items)
case RefundOned => (state.copy(seq = state.seq + 1, refd = true), items)
case RefundOffed => (state.copy(seq = state.seq + 1, refd = false), items)
case VoidOned => (state.copy(seq = state.seq + 1, void = true), items)
case VoidOffed => (state.copy(seq = state.seq + 1, void = false), items)
case SuperOned(suser) => (state.copy(seq = state.seq + 1, su = suser), items)
case SuperOffed => (state.copy(seq = state.seq + 1, su = ""), items)
case MemberOned(num) => (state.copy(seq = state.seq + 1, mbr = num), items)
case MemberOffed => (state.copy(seq = state.seq + 1, mbr = ""), items) case SalesLogged(_,_,_,_,_) => (state.copy(
seq = state.seq + 1)
, items.addItem(curItem)) case Subtotaled(level) =>
var subs = (0,0,0,0)
if (level == 0)
subs = items.subTotal
else
subs = items.groupTotal(level)
val (cnt, tqty, tamt, tdsc) = subs val subttlItem =
TxnItem(state).copy(
txntype = TXNTYPE.sales,
salestype = SALESTYPE.sub,
qty = tqty,
amount = tamt,
dscamt = tdsc,
price = cnt
)
(state.copy(
seq = state.seq + 1)
, items.addItem(subttlItem)) case Discounted(dt,gp,code,pct) =>
val (lstItems, accum) = items.updateDisc(dt,gp,pct)
val discItem = TxnItem(state).copy(
txntype = TXNTYPE.sales,
salestype = SALESTYPE.dsc,
acct = code,
disc = pct,
price = accum._1,
qty = accum._2,
amount = accum._3,
dscamt = accum._4
)
(state.copy(
seq = state.seq + 1)
, items.copy(txnitems = lstItems).addItem(discItem)) case PaymentMade(_,_,_) =>
val due = if (items.totalSales > 0) items.totalSales - items.totalPaid else items.totalSales + items.totalPaid
val bal = if (items.totalSales > 0) due - curItem.amount else due + curItem.amount
(state.copy(
seq = state.seq + 1,
due = (if ((curItem.amount.abs + items.totalPaid.abs) >= items.totalSales.abs) false else true)
)
,items.addItem(curItem.copy(
salestype = SALESTYPE.ttl,
price = due,
amount = curItem.amount,
dscamt = bal
))) case VoucherNumed(_, tnum) =>
val vi = items.copy(txnitems = items.txnitems.map { it => it.copy(num = tnum) })
(state.copy(seq = state.seq + 1, num = tnum), vi) case EndVoucher(vnum) => (state.nextVoucher.copy(jseq = lastSeqNr + 1), VchItems()) case _ => (state, items)
} }
Reader.scala
package datatech.cloud.pos
import akka.actor._
import akka.stream.scaladsl._ import scala.util._
import akka._
import akka.persistence.query._
import akka.persistence.cassandra.query.scaladsl.CassandraReadJournal import scala.concurrent._
import akka.stream._
import sdp.logging._
import Actions._
import States._
import Messages._
import akka.cluster._ class Reader extends Actor with LogSupport {
import Reader._
val cluster = Cluster(context.system)
implicit val nodeAddress: NodeAddress = NodeAddress(cluster.selfAddress.toString)
override def receive: Receive = {
case ReadActions(si, ei, pid) => readActions(si,ei, pid)(context.system, context.dispatcher, ActorMaterializer(),nodeAddress)
}
} object Reader extends LogSupport { def readActions(startSeq: Long, endSeq: Long, persistenceId: String)(implicit sys: ActorSystem, ec: ExecutionContextExecutor, mat: ActorMaterializer, nodeAddress: NodeAddress) = {
implicit var vchState = VchStates()
implicit var vchItems = VchItems()
implicit var curTxnItem = TxnItem()
implicit val pid = PID(persistenceId)
// obtain read journal by plugin id
val readJournal =
PersistenceQuery(sys).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier) // issue query to journal
val source: Source[EventEnvelope, NotUsed] =
readJournal.currentEventsByPersistenceId(persistenceId, startSeq, endSeq) // materialize stream, consuming events
val futureActions: Future[List[Any]] = source.runFold(List[Any]()) { (lstAny, evl) => evl.event :: lstAny } futureActions.onComplete {
case Success(txns) => buildVoucher(txns)
case Failure(excpt) =>
} def buildVoucher(actions: List[Any])= { actions.reverse.foreach { txn =>
txn match {
case EndVoucher(_) =>
vchItems.txnitems.foreach(println)
case ti@_ =>
curTxnItem = buildTxnItem(ti.asInstanceOf[Action])
val sts = updateState(ti.asInstanceOf[Action],0)
vchState = sts._1
vchItems = sts._2
}
}
}
} }
ReaderDemo.scala
import akka.actor._
import akka.stream._
import akka.stream.scaladsl._
import akka.cluster._ import scala.concurrent._
import scala.util._
import akka._
import akka.persistence.query._
import akka.persistence.cassandra.query.scaladsl.CassandraReadJournal
import datatech.cloud.pos._
import Actions._
import Messages._
import Reader._
import sdp.logging.LogSupport object ReaderDemo extends App { implicit val system = ActorSystem("reader")
implicit val ec = system.dispatcher
implicit val mat = ActorMaterializer() val cluster = Cluster(system)
implicit val nodeAddress: NodeAddress = NodeAddress(cluster.selfAddress.toString) States.setShowSteps(true) Reader.readActions(0, Long.MaxValue,"1022") scala.io.StdIn.readLine() mat.shutdown()
system.terminate()
}
Akka-CQRS(7)- CQRS Reader Actor 示范的更多相关文章
- Akka(2):Actor生命周期管理 - 监控和监视
在开始讨论Akka中对Actor的生命周期管理前,我们先探讨一下所谓的Actor编程模式.对比起我们习惯的行令式(imperative)编程模式,Actor编程模式更接近现实中的应用场景和功能测试模式 ...
- 以Akka为示例,介绍Actor模型
许多开发者在创建和维护多线程应用程序时经历过各种各样的问题,他们希望能在一个更高层次的抽象上进行工作,以避免直接和线程与锁打交道.为了帮助这些开发者,Arun Manivannan编写了一系列的博客帖 ...
- Akka-CQRS(4)- CQRS Writer Actor 示范
我觉着,CQRS的写部分最核心.最复杂的部分应该是Writer-Actor了.其它的监管(supervising).分片部署等都比较规范,没太多的变动.几乎Writer-Actor所有的业务逻辑都在R ...
- Akka-CQRS(8)- CQRS Reader Actor 应用实例
前面我们已经讨论了CQRS-Reader-Actor的基本工作原理,现在是时候在之前那个POS例子里进行实际的应用示范了. 假如我们有个业务系统也是在cassandra上的,那么reader就需要把从 ...
- Akka(1):Actor - 靠消息驱动的运算器
Akka是由各种角色和功能的Actor组成的,工作的主要原理是把一项大的计算任务分割成小环节,再按各环节的要求构建相应功能的Actor,然后把各环节的运算托付给相应的Actor去独立完成.Akka是个 ...
- Akka(3): Actor监管 - 细述BackoffSupervisor
在上一篇讨论中我们谈到了监管:在Akka中就是一种直属父子监管树结构,父级Actor负责处理直属子级Actor产生的异常.当时我们把BackoffSupervisor作为父子监管方式的其中一种.实际上 ...
- CQRS学习——Cqrs补丁,async实验以及实现[其二]
实验——async什么时候提高吞吐 async是一个语法糖,用来简化异步编程,主要是让异步编程在书写上接近于同步编程.总的来收,在await的时候,相当于附加上了一个.ContinueWith(). ...
- Akka系列(八):Akka persistence设计理念之CQRS
前言........ 这一篇文章主要是讲解Akka persistence的核心设计理念,也是CQRS(Command Query Responsibility Segregation)架构设计的典型 ...
- CQRS(Command and Query Responsibility Segregation)与EventSources实例
CQRS The CQRS pattern and event sourcing are not mere simplistic solutions to the problems associate ...
随机推荐
- ORACLE隐藏参数查看及修改
查看隐藏参数 select x.ksppinm name, y.ksppstvl value, y.ksppstdf isdefault, decode(bitand(y.ksppstvf,7),1, ...
- Centos7配置ssh免密登录群发
ssh免密登录是客户端发送自己的公钥到服务器.用公钥进行解密,自己生成的私钥进行加密. 首先在客户端查看sshd服务是否启动 [zhiwei@zhiwei1 ~]$ ps -Af|grep sshd; ...
- 7.redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
作者:中华石杉 面试题 redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗? 面试官心理分析 在前几年, ...
- 设置linux代理完成apt-get
最近ubuntu的服务器被公司关闭了外网使用权限,但是安装软件又需要连接外网,那么就只能通过代理来连接了. 先按照下面的这篇帖子来设置windows端的代理. https://blog.csdn.ne ...
- Cutting Bamboos(2019年牛客多校第九场H题+二分+主席树)
题目链接 传送门 题意 有\(n\)棵竹子,然后有\(q\)次操作,每次操作给你\(l,r,x,y\),表示对\([l,r]\)区间的竹子砍\(y\)次,每次砍伐的长度和相等(自己定砍伐的高度\(le ...
- U-Boot的常用命令详解
U-Boot还提供了更加详细的命令帮助,通过help命令还可以查看每个命令的参数说明.由于开发过程的需要,有必要先把U-Boot命令的用法弄清楚.接下来,根据每一条命令的帮助信息,解释一下这些命令的功 ...
- Idea如何快速生成Junit测试类
测试是保证代码必不可少的环节,自己构建测试方法太慢,并且命名也不规范,idea中提供了,一键构建测试结构的功能... 2.步骤 1.在需要做测试的类的当前窗口,直接按快捷键:按ctrl+shift+t ...
- 如何用<dl>标签做表格而不用table标签
我们都知道很多的内容编辑器(TinyMCE编辑器.fck)都有插入表格功能,快速方便,但是这些表格用到的<table>标签,可以查看html源代码就能发现,table标签对搜索引擎不是很友 ...
- [51nod1227]平均最小公倍数(莫比乌斯反演+杜教筛)
题意 求 $\sum_{i=a}^b \sum_{j=1}^i \frac{lcm(i,j)}{i}$. 分析 只需要求出前缀和, $$\begin{aligned}\sum_{i=1}^n \sum ...
- selenium 中在 iframe 内的元素定位
有些时候 元素明明就在 但是通过什么方式定位都提示 定位不到元素 此时就要考虑元素是不是内嵌在iframe 中 对于内嵌在 ifra中的元素定位 首先定位到 iframe 元素 例如 iframe = ...