我觉着,CQRS的写部分最核心、最复杂的部分应该是Writer-Actor了。其它的监管(supervising)、分片部署等都比较规范,没太多的变动。几乎Writer-Actor所有的业务逻辑都在Receive函数里,这个地方应该算是最复杂的地方。我的想法是搭建一个标准框架,保证可以运行Writer-Actor。然后因应业务需求变化在这个Receive函数里用一种标准写法加功能。这就要求Receive函数能够做到代码简洁、逻辑清晰。Receive函数是一个执行Command的地方。在执行之前需要对指令的可执行性进行判断,这样就可能在函数内部造成臃肿的重复代码。比如,大部分指令需要用户登陆后才容许执行、又或者很多指令都需要超级用户权限、又或者开始支付后不容许除支付之外的任何操作,等等。。。
首先,我们可以有一个前置操作,里面包括了大部分对指令可执行性判断逻辑,这样我们可以把许多重复代码从Receive函数中移走,如下:
- //helper functions
- object RunPOSCommand {
- def unapply(arg: POSCommand) = if (cmdFilter(persistenceId,arg,vchState,vchItems,sender())) Some(arg) else None
- }
- def cmdFilter(terminalid: String, cmd: POSCommand, state: VchStates, txns: VchItems, router: ActorRef): Boolean = cmd match {
- case LogOn(opr, passwd) => //only allowed in logOffState
- if (!txns.txnitems.isEmpty) { //in the middle of process
- router ! POSResponse(STATUS.FAIL, s"禁止用户登陆!终端 ${terminalid} 有未完成单据。", state, List())
- false
- } else{
- if (validateUser(opr, passwd).isDefined) true
- else {
- router ! POSResponse(STATUS.FAIL, s"终端-$terminalid: 用户 ${opr} 登陆失败!", state, List())
- false
- }
- }
- case LogOff => //only allowed in logOnState
- if (!txns.txnitems.isEmpty) { //in the middle of process
- router ! POSResponse(STATUS.FAIL, s"禁止用户退出!终端 ${terminalid} 有未完成单据。", state, List())
- false
- } else true
- case VoidAll => //allowed in logOnState and paymentState
- if (txns.txnitems.isEmpty) { //no valid sales
- router ! POSResponse(STATUS.FAIL, s"全单取消失败!终端 ${terminalid} 本单无任何有效销售记录。", state, txns.txnitems)
- false
- } else true
- case OfflinePay(acct,num,amt) =>
- if (txns.totalSales.abs == 0) { // no valid sales. void,refund neg values could produce zero
- router ! POSResponse(STATUS.FAIL, s"支付失败!终端 ${terminalid} 应付金额为零。", state, List())
- false
- } else {
- if(validateAcct(acct).isDefined) true
- else {
- router ! POSResponse(STATUS.FAIL, s"支付失败!终端 ${terminalid} 账号{$acct}不存在。", state, List())
- false
- }
- }
- case Subtotal =>
- if (txns.txnitems.isEmpty) { //in the middle of process
- router ! POSResponse(STATUS.FAIL, s"小计操作失败!终端 ${terminalid} 无任何销售记录。", state, List())
- false
- } else true
- case VCBalance(_,_,_) => true
- case MemberOn(_,_) => true
- case MemberOff => true
- case VoucherNum(_) => true
- ...
- }
如上,前期的判断逻辑都移到cmdFilter(...)里了,然后可以用RunPOSCommand(command)方式来实现对command的执行性判断。现在我们可以在Receive函数里通过调用RunPOSCommand来节省一大截重复代码了,如下:
- private def logOffState: Receive = {
- case RunPOSCommand(LogOn(opr, _)) =>
- persistEvent(LogOned(opr,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- //starting seqenceNr for any voucher. no action logged before login
- vchState = sts._1.copy(jseq = lastSequenceNr + 1);
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 用户{$opr}成功登陆。", vchState, List(vchItems.txnitems.head))
- context.become(logOnState)
- }
- case PassivatePOS =>
- log.info(s"**********${persistenceId} got passivate message and stopping ... ***********")
- context.parent ! PoisonPill
- case _ =>
- sender() ! POSResponse(STATUS.FAIL, s"操作失败!终端 ${persistenceId} 用户未登陆。", vchState, List())
- }
另外,我把所有指令分成三个可执行类别,分别是:登陆前、已登陆、支付中。这三种状态的行为分别用logOffState,logOnState,paymentState三个Receive函数代表:
- private def logOnState: Receive = {
- case RunPOSCommand(LogOff) =>
- persistEvent(LogOffed(vchState)) { evt =>
- val user = vchState.opr
- val sts = updateState(evt,vchState,vchItems)
- vchState = sts._1; vchItems = sts._2
- saveSnapshot(vchState) //state of last voucher
- //手工passivate shard ! ShardRegion.Passivate(PassivatePOS)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 用户 $user 成功退出。", vchState, List(vchItems.txnitems.head))
- context.unbecome() //switch to logOffState
- }
- case RunPOSCommand(SuperOn(su,_)) =>
- persistEvent(SuperOned(su,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 转到管理模式{$su}。", vchState, List(vchItems.txnitems.head))
- }
- private def logOnState: Receive = {
- case RunPOSCommand(LogOff) =>
- persistEvent(LogOffed(vchState)) { evt =>
- val user = vchState.opr
- val sts = updateState(evt,vchState,vchItems)
- vchState = sts._1; vchItems = sts._2
- saveSnapshot(vchState) //state of last voucher
- //手工passivate shard ! ShardRegion.Passivate(PassivatePOS)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 用户 $user 成功退出。", vchState, List(vchItems.txnitems.head))
- context.unbecome() //switch to logOffState
- }
- case RunPOSCommand(SuperOn(su,_)) =>
- persistEvent(SuperOned(su,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 转到管理模式{$su}。", vchState, List(vchItems.txnitems.head))
- }
- private def logOnState: Receive = {
- case RunPOSCommand(LogOff) =>
- persistEvent(LogOffed(vchState)) { evt =>
- val user = vchState.opr
- val sts = updateState(evt,vchState,vchItems)
- vchState = sts._1; vchItems = sts._2
- saveSnapshot(vchState) //state of last voucher
- //手工passivate shard ! ShardRegion.Passivate(PassivatePOS)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 用户 $user 成功退出。", vchState, List(vchItems.txnitems.head))
- context.unbecome() //switch to logOffState
- }
- case RunPOSCommand(SuperOn(su,_)) =>
- persistEvent(SuperOned(su,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 转到管理模式{$su}。", vchState, List(vchItems.txnitems.head))
- }
- //first payment in a voucher
- case RunPOSCommand(OfflinePay(acct,num, amount)) =>
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. stay in logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- }
- }
- else context.become(paymentState) //switch into paymentState
- }
- ...
- }
- private def paymentState: Receive = {
- case RunPOSCommand(OfflinePay(acct,num, amount)) =>
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. return to logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- context.unbecome() //logOnState
- }
- }
- // else wait for other payments and stay in logOnState
- }
- ...
- }
注意上面代码里的context.become,saveSnapshot, 它们分别代表状态转换及一单的终结。
在实现写端时不得不考虑读端Reader-Actor的一些实现方式:这是个收银机软件,以一张销售单为单元,单中有多条交易记录(在例子里是多条收银动作记录),后面的操作记录可能会影响前面记录的状态,如:后面的冲销前面的、后面输入一条折扣指令等,不过单与单之间没有任何瓜葛。前面提到过:我们不想在写端来处理任何业务逻辑,所以对每单中项目状态处理就移到了读端。具体做法是这样的:写端完成一单操作后通知Reader-Actor,并把本单的开始sequenceNr和结束sequenceNr传给Reader-Actor, Reader用静态流方式读取事件,维护本单状态并对单内所有项目状态进行更新并恢复到规定的交易记录格式,最终把所有交易项目写到目标数据库表。假设有一个POSRouter负责派送指令给Writer-Actor,我们同样可以把完成写单据的信息传送给这个POSRouter, 然后由它分配调度Reader-Actor。POSRouter+Reader-Actor可以是cluster-loadbalance模式的。写端存入日志的数据包括每一个动作的类型和详细的数据,如SalesLogged(txnitem), SalesLogged是事件类型,txnItem是事件数据:
- 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.plu //销售类型
- ,qty: Int = 1 //交易数量
- ,price: Int = 0 //单价(分)
- ,amount: Int = 0 //码洋(分)
- ,dscamt: Int = 0 //折扣:负值 net实洋 = amount + dscamt
- ,member: String = "" //会员卡号
- ,code: String = "" //编号(商品、账号...)
- ,desc: String = "" //项目名称
- ,dpt: String = ""
- ,department: String = ""
- ,cat: String = ""
- ,category: String = ""
- ,brd: String = ""
- ,brand: String = ""
- )
首先,我们需要这个TxnItem使Read-Actor能把事件恢复成规定格式的交易记录。也可以用这个结构返回给前端用来显示或者打印交易明细。下面是这些Command和Event的定义代码:
- object Commands {
- case object PassivatePOS //passivate message
- case class DebugMode(debug: Boolean)
- sealed trait POSCommand {}
- case class LogOn(opr: String, passwd: String) extends POSCommand
- case object LogOff extends POSCommand
- case class SuperOn(su: String, passwd: String) extends POSCommand
- case object SuperOff extends POSCommand
- case class MemberOn(cardnum: String, passwd: String) extends POSCommand
- case object MemberOff extends POSCommand //remove member status for the voucher
- case object RefundOn extends POSCommand
- case object RefundOff extends POSCommand
- case object VoidOn extends POSCommand
- case object VoidOff extends POSCommand
- case object VoidAll extends POSCommand
- case object Suspend extends POSCommand
- case class VoucherNum(vnum: Int) extends POSCommand
- case class LogSales(salesType: Int, dpt: String, code: String, qty: Int, price: Int) extends POSCommand
- case object Subtotal extends POSCommand
- case class Discount(code: String, percent: Int) extends POSCommand
- case class OfflinePay(acct: String, num: String, amount: Int) extends POSCommand //settlement 结算支付
- //read only command, no event process
- case class VCBalance(acct: String, num: String, passwd: String) extends POSCommand
- case class VCPay(acct: String, num: String, passwd: String, amount: Int) extends POSCommand
- case class AliPay(acct: String, num: String, amount: Int) extends POSCommand
- case class WxPay(acct: String, num: String, amount: Int) extends POSCommand
- // read only command, no update event
- case class Plu(itemCode: String) extends POSCommand //read only
- }
- object Events {
- sealed trait POSEvent {}
- case class LogOned(txnItem: TxnItem) extends POSEvent
- object LogOned {
- def apply(op: String, vs: VchStates): LogOned = LogOned(TxnItem(vs).copy(
- txntype = TXNTYPE.logon,
- salestype = SALESTYPE.crd,
- opr = op,
- code = op
- ))
- }
- case class LogOffed(txnItem: TxnItem) extends POSEvent
- object LogOffed {
- def apply(vs: VchStates): LogOffed = LogOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.logon,
- salestype = SALESTYPE.crd,
- ))
- }
- case class SuperOned(txnItem: TxnItem) extends POSEvent
- object SuperOned {
- def apply(su: String, vs: VchStates): SuperOned = SuperOned(TxnItem(vs).copy(
- txntype = TXNTYPE.supon,
- salestype = SALESTYPE.crd,
- code = su
- ))
- }
- case class SuperOffed(txnItem: TxnItem) extends POSEvent
- object SuperOffed {
- def apply(vs: VchStates): SuperOffed = SuperOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.supon,
- salestype = SALESTYPE.crd
- ))
- }
- case class MemberOned(txnItem: TxnItem) extends POSEvent
- object MemberOned {
- def apply(cardnum: String,vs: VchStates): MemberOned = MemberOned(TxnItem(vs).copy(
- txntype = TXNTYPE.sales,
- salestype = SALESTYPE.crd,
- member = cardnum
- ))
- }
- case class MemberOffed(txnItem: TxnItem) extends POSEvent //remove member status for the voucher
- object MemberOffed {
- def apply(vs: VchStates): MemberOffed = MemberOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.sales,
- salestype = SALESTYPE.crd,
- member = vs.mbr
- ))
- }
- case class RefundOned(txnItem: TxnItem) extends POSEvent
- object RefundOned {
- def apply(vs: VchStates): RefundOned = RefundOned(TxnItem(vs).copy(
- txntype = TXNTYPE.refund
- ))
- }
- case class RefundOffed(txnItem: TxnItem) extends POSEvent
- object RefundOffed {
- def apply(vs: VchStates): RefundOffed = RefundOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.refund
- ))
- }
- case class VoidOned(txnItem: TxnItem) extends POSEvent
- object VoidOned {
- def apply(vs: VchStates): VoidOned = VoidOned(TxnItem(vs).copy(
- txntype = TXNTYPE.void
- ))
- }
- case class VoidOffed(txnItem: TxnItem) extends POSEvent
- object VoidOffed {
- def apply(vs: VchStates): VoidOffed = VoidOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.void
- ))
- }
- case class NewVoucher(vnum: Int) extends POSEvent //新单, reminder for read-side to set new vnum
- case class EndVoucher(vnum: Int) extends POSEvent //单据终结标示
- case class VoidVoucher(txnItem: TxnItem) extends POSEvent
- object VoidVoucher {
- def apply(vs: VchStates): VoidVoucher = VoidVoucher(TxnItem(vs).copy(
- txntype = TXNTYPE.voidall
- ))
- }
- case class SuspVoucher(txnItem: TxnItem) extends POSEvent
- object SuspVoucher {
- def apply(vs: VchStates): SuspVoucher = SuspVoucher(TxnItem(vs).copy(
- txntype = TXNTYPE.suspend
- ))
- }
- case class VoucherNumed(fnum: Int, tnum: Int) extends POSEvent
- case class SalesLogged(txnItem: TxnItem) extends POSEvent
- case class Subtotaled(txnItem: TxnItem) extends POSEvent
- object Subtotaled {
- def apply(vs: VchStates, vi: VchItems): Subtotaled = {
- val (cnt,tqty,tamt,tdsc) = vi.subTotal
- Subtotaled(TxnItem(vs).copy(
- txntype = TXNTYPE.sales,
- salestype = SALESTYPE.sub,
- qty = tqty,
- amount = tamt,
- dscamt = tdsc,
- price = cnt
- ))
- }
- }
- case class Discounted(txnItem: TxnItem) extends POSEvent
- case class Payment(txnItem: TxnItem) extends POSEvent //settlement 结算支付
- object Payment {
- def apply(acct: String, num: String, vs: VchStates): Payment = Payment(TxnItem(vs).copy(
- txntype = TXNTYPE.sales,
- salestype = SALESTYPE.ttl,
- dpt = acct,
- code = num
- ))
- }
- }
可以看到,在构建Event的同时也构建了TxnItem结构。
返回前端数据格式如下:
- object Responses {
- object STATUS {
- val OK: Int = 0
- val FAIL: Int = -1
- }
- case class POSResponse (sts: Int, msg: String, voucher: VchStates, txnItems: List[TxnItem])
- }
为了便于debug,我们构建了一个虚拟的数据库环节:
- package pos.dao
- import java.time.LocalDate
- import java.time.format.DateTimeFormatter
- case class Item(
- brd: String
- ,dpt: String
- ,cat: String
- ,code: String
- ,name: String
- ,price: Int
- )
- object Items {
- val apple = Item("01","02","01","001", "green apple", 820)
- val grape = Item("01","02","01","002", "red grape", 1050)
- val orage = Item("01","02","01","003", "sunkist orage", 350)
- val banana = Item("01","02","01","004", "demon banana", 300)
- val pineapple = Item("01","02","01","005", "hainan pineapple", 1300)
- val peach = Item("01","02","01","006", "xinjiang peach", 2390)
- val tblItems = List(apple, grape, orage, banana, pineapple, peach)
- sealed trait QueryItemsResult {}
- case class QueryItemsOK(items: List[Item]) extends QueryItemsResult
- case class QueryItemsFail(msg: String) extends QueryItemsResult
- }
- object Codes {
- case class User(code: String, name: String, passwd: String)
- case class Department(code: String, name: String)
- case class Category(code: String, name: String)
- case class Brand(code: String, name: String)
- case class Ra(code: String, name: String)
- case class Account(code: String, name: String)
- case class Disc(code: String, best: Boolean, aggr: Boolean, group: Boolean)
- val ras = List(Ra("01","Delivery"),Ra("02","Cooking"))
- val dpts = List(Department("01","Fruit"),Department("02","Grocery"))
- val cats = List(Category("0101","Fresh Fruit"),Category("0201","Dry Grocery"))
- val brds = List(Brand("01","Sunkist"),Brand("02","Demon"))
- val accts = List(Account("001","Cash"),Account("002","Value Card"), Account("003", "Visa")
- ,Account("004","Alipay"),Account("005","WXPay"))
- val users = List(User("1001","Tiger", "123"),User("1002","John", "123"),User("1003","Maria", "123"))
- def getDpt(code: String) = dpts.find(d => d.code == code)
- def getCat(code: String) = cats.find(d => d.code == code)
- def getBrd(code: String) = brds.find(b => b.code == code)
- def getAcct(code: String) = accts.find(a => a.code == code)
- def getRa(code: String) = ras.find(a => a.code == code)
- }
- object DAO {
- import Items._
- import Codes._
- def getItem(code: String): QueryItemsResult = {
- val optItem = tblItems.find(it => it.code == code)
- optItem match {
- case Some(item) => QueryItemsOK(List(item))
- case None => QueryItemsFail("Invalid item code!")
- }
- }
- def validateDpt(code: String) = dpts.find(d => d.code == code)
- def validateCat(code: String) = cats.find(d => d.code == code)
- def validateBrd(code: String) = brds.find(b => b.code == code)
- def validateRa(code: String) = ras.find(ac => ac.code == code)
- def validateAcct(code: String) = accts.find(ac => ac.code == code)
- def validateUser(userid: String, passwd: String) = users.find(u => (u.code == userid && u.passwd == passwd))
- def lastSecOfDateStr(ldate: LocalDate): String = {
- ldate.format(DateTimeFormatter.ofPattern( "yyyy-MM-dd"))+" 23:59:59"
- }
- }
这个库主要是用来验证和提供TxnItem里的字段值。
好了,回到WriterActor:同样为了方便集群分片监控跟踪和debug,增加了debugMode和重写了persist:
- class WriterActor extends PersistentActor with LogSupport {
- val cluster = Cluster(context.system)
- // shopdptId-posId
- // self.path.parent.name is the type name (utf-8 URL-encoded)
- // self.path.name is the entry identifier (utf-8 URL-encoded) but entity has a supervisor
- // override def persistenceId: String = self.path.parent.parent.name + "-" + self.path.parent.name
- override def persistenceId: String = self.path.parent.name
- override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
- super.preRestart(reason, message)
- log.info(s"Restarting terminal $persistenceId on ${cluster.selfAddress} for $message")
- }
- override def postRestart(reason: Throwable): Unit = {
- super.postRestart(reason)
- log.info(s"terminal $persistenceId on ${cluster.selfAddress} restarted for ${reason.getMessage}")
- }
- override def postStop(): Unit = {
- log.info(s"terminal $persistenceId on ${cluster.selfAddress} stopped!")
- }
- override def preStart(): Unit = {
- log.info(s"terminal $persistenceId on ${cluster.selfAddress} starting...")
- }
- //helper functions
- object RunPOSCommand {
- def unapply(arg: POSCommand) = if (cmdFilter(persistenceId,arg,vchState,vchItems,sender())) Some(arg) else None
- }
- def persistEvent[E](evt: E)(f: E => Unit)(implicit dm: DebugMode) = {
- if (dm.debug)
- log.info(s"********** $persistenceId: persisted event: {$evt} **********")
- else {
- try {
- persist(evt)(f)
- log.debug(s"终端-$persistenceId:event: [$evt] state: [$vchState] : [${vchItems.txnitems.reverse}]")
- }
- catch {
- case err: Throwable =>
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 操作失败![${err.getMessage}]。", vchState, vchItems.txnitems.reverse)
- log.error(s"终端-$persistenceId: 操作失败![${err.getMessage}] current state: [$vchState],[${vchItems.txnitems.reverse}]")
- }
- }
- }
- var debugConfig: com.typesafe.config.Config = _
- var debug: Boolean = _
- try {
- debugConfig = ConfigFactory.load("pos.conf").getConfig("pos.server")
- debug = debugConfig.getBoolean("debug")
- }
- catch {
- case _ : Throwable => debug = false
- }
- log.info(s"********** $persistenceId: debug mode = $debug **********")
- implicit val debugMode = DebugMode(debug)
- //actor state
- var vchState = VchStates()
- var vchItems = VchItems()
- override def receiveRecover: Receive = {
- case evt: POSEvent => //incompleted voucher play back events
- val (vs,vi) = updateState(evt,vchState,vchItems)
- vchState = vs; vchItems = vi
- case SnapshotOffer(_,vs: VchStates) => vchState = vs //restore num,seq ...
- }
-
- override def receiveCommand: Receive = logOffState
- ...
- }
Receive函数在WriteActor启动时默认为logOffState
- private def logOffState: Receive = {
- case RunPOSCommand(LogOn(opr, _)) =>
- persistEvent(LogOned(opr,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- //starting seqenceNr for any voucher. no action logged before login
- vchState = sts._1.copy(jseq = lastSequenceNr + 1);
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 用户{$opr}成功登陆。", vchState, List(vchItems.txnitems.head))
- context.become(logOnState)
- }
-
- case PassivatePOS =>
- log.info(s"**********${persistenceId} got passivate message and stopping ... ***********")
- context.parent ! PoisonPill
- case _ =>
- sender() ! POSResponse(STATUS.FAIL, s"操作失败!终端 ${persistenceId} 用户未登陆。", vchState, List())
- }
除LogOn指令外不接受任何其它指令,一旦接到指令立即通过context.become(logOnState)转状态。passivation只会在这个状态下发挥作用。这也可以理解:即先是没有处理中的单据,又有一段时间的空转,可以passivate了。
在logOnState里可以进行支付操作,如果支付金额小于应付金额则代表部分付款操作,需要转入paymentState:
- private def logOnState: Receive = {
- ...
- //first payment in a voucher
- case RunPOSCommand(OfflinePay(acct,num, amount)) =>
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. stay in logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- }
- }
- else context.become(paymentState) //switch into paymentState
- }
- ...
- }
支付操作可能出现两种情况:整单完成,存入EndVoucher标示,saveSnapshot,或者转入paymentState。同样,在paymentState状态,只接受支付指令,直到应付金额耗尽,转回logOnState:
- private def paymentState: Receive = {
- case RunPOSCommand(OfflinePay(acct,num, amount)) =>
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. return to logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems, lastSeqenceNr)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- context.unbecome() //logOnState
- }
- }
- // else wait for other payments and stay in logOnState
- }
- /* strictly disallow any action other than payment till completion
- case RunPOSCommand(VoidAll) =>
- persistEvent(VoidVoucher(vchState)) { _ =>
- persistEvent(EndVoucher(vchState.num)) { evt =>
- updateState(evt,vchState,vchItems)
- context.unbecome() //in paymentState, switch to logOnState
- }
- }
- case RunPOSCommand(Suspend) =>
- persistEvent(SuspVoucher(vchState)) { _ =>
- persistEvent(EndVoucher(vchState.num)) { evt =>
- updateState(evt,vchState,vchItems)
- context.unbecome() //in paymentState, switch to logOnState
- }
- }
- */
- case RunPOSCommand(SuperOn(su,_)) =>
- persistEvent(SuperOned(su,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- }
- case RunPOSCommand(SuperOff) =>
- persistEvent(SuperOffed(vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- }
- case RunPOSCommand(VCBalance(acct,num,passwd)) =>
- if (POSInterfaces.validateVC(acct,num,passwd)) {
- val res = POSInterfaces.getVCBalance(acct,num)
- if (res.sts == POSInterfaces.VCRESULT.OK)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 储值卡余额{${res.amt}}。", vchState, List(TxnItem(vchState).copy(
- amount = (res.amt * 100).toInt,
- dpt = acct,
- code = num
- )))
- else
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额读取错误![${res.msg}]", vchState, List())
- } else {
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]验证信息错误!", vchState, List())
- }
- case RunPOSCommand(VCPay(acct,num, passwd,amount)) =>
- if (POSInterfaces.validateVC(acct,num,passwd)) {
- val res = POSInterfaces.getVCBalance(acct,num)
- if (res.sts == POSInterfaces.VCRESULT.OK) {
- if ((res.amt * 100).toInt < amount)
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额不足!", vchState,List(TxnItem(vchState).copy(
- amount = (res.amt * 100).toInt,
- dpt = acct,
- code = num
- )))
- else {
- val res = POSInterfaces.payByVC(acct,num,amount/100.00)
- if (res.sts == POSInterfaces.VCRESULT.OK) {
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. stay in logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems, lastSeqenceNr)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- context.unbecome() //switch to logOnState
- }
- }
- }
- }
- }
- }
- else
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额读取错误![${res.msg}]", vchState, List())
- } else {
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]验证信息错误!", vchState, List())
- }
- case _ =>
- sender() ! POSResponse(STATUS.FAIL, s"操作失败!终端 ${persistenceId} 结算中不容许其它非支付操作!", vchState, List(vchItems.txnitems.head))
- }
目前支付部分只处理了线下支付和储值卡支付两个流程。
每单结束时会进行单据处理状态快照存储 saveSnapShot(vchState),真正意义在新一单从零的重新开始。 VchState是在updateState函数里维护的:
- case class VchStates(
- opr: String = "", //收款员
- jseq: BigInt = 0, //begin journal sequence for read-side replay
- num: Int = 0, //当前单号
- seq: Int = 1, //当前序号
- 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
- )
- }
也就是说每次saveSnapShot会把当前单号、操作员存入snapshot方便恢复当前单状态。另外jseq是本单事件存入时的写序号lastSeqenceNr,我们用来通知Reader-Actor从那里开始读journal。这个jseq是如下维护的:
- def updateState(evt: POSEvent, state: VchStates, items: VchItems, lastSeqNr: BigInt = 0): (VchStates, VchItems) = evt match {
- case LogOned(txn) => (state.copy(seq = txn.seq + 1,opr = txn.opr,jseq = lastSeqNr), items)
- ...
- case EndVoucher(vnum) => (state.nextVoucher.copy(jseq = lastSeqNr), VchItems())
- ...
- }
如上所述,只在登陆时或者结单时需要标示jseq。注意:登陆指令只能在完成单据的情况下处理,而lastSeqenceNr是一单的结束序号,+1就是新单开始序号了。
下面把这次示范的完整源代码提供在下面。这是一个只通过了编译的版本。完整可运行的cluster-sharding版本放在下篇中提供:
build.sbt
- name := "akka-cluster-pos"
-
- version := "0.2"
-
- scalaVersion := "2.12.8"
-
- libraryDependencies := Seq(
- "com.typesafe.akka" %% "akka-cluster-sharding" % "2.5.21",
- "com.typesafe.akka" %% "akka-persistence" % "2.5.21",
- "com.typesafe.akka" %% "akka-persistence-cassandra" % "0.92",
- "com.typesafe.akka" %% "akka-persistence-cassandra-launcher" % "0.92" % 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 {
- loglevel = INFO
- actor {
- provider = "cluster"
- }
-
- remote {
- log-remote-lifecycle-events = off
- netty.tcp {
- hostname = "127.0.0.1"
- port = 0
- }
- }
-
- cluster {
- seed-nodes = [
- "akka.tcp://posSystem@127.0.0.1:2551"]
- log-info = off
- sharding {
- role = "shard"
- passivate-idle-entity-after = 10 s
- }
- }
-
- persistence {
- journal.plugin = "cassandra-journal"
- snapshot-store.plugin = "cassandra-snapshot-store"
- }
-
- }
-
- cassandra-journal {
- contact-points = ["192.168.1.18"]
- }
-
- cassandra-snapshot-store {
- contact-points = ["192.168.1.18"]
- }
pos.conf
- pos {
- server {
- debug = false
- }
- }
logback.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <configuration>
-
- <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
- <layout class="ch.qos.logback.classic.PatternLayout">
- <Pattern>
- %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
- </Pattern>
- </layout>
- </appender>
-
- <logger name="sdp.cql" level="info"
- additivity="false">
- <appender-ref ref="STDOUT" />
- </logger>
-
- <logger name="demo.sdp.grpc.cql" level="info"
- additivity="false">
- <appender-ref ref="STDOUT" />
- </logger>
-
- <root level="error">
- <appender-ref ref="STDOUT" />
- </root>
logging/Log.scala
- package sdp.logging
-
- import org.slf4j.Logger
-
- /**
- * Logger which just wraps org.slf4j.Logger internally.
- *
- * @param logger logger
- */
- class Log(logger: Logger) {
-
- // use var consciously to enable squeezing later
- var isDebugEnabled: Boolean = logger.isDebugEnabled
- var isInfoEnabled: Boolean = logger.isInfoEnabled
- var isWarnEnabled: Boolean = logger.isWarnEnabled
- var isErrorEnabled: Boolean = logger.isErrorEnabled
-
- def withLevel(level: Symbol)(msg: => String, e: Throwable = null): Unit = {
- level match {
- case 'debug | 'DEBUG => debug(msg)
- case 'info | 'INFO => info(msg)
- case 'warn | 'WARN => warn(msg)
- case 'error | 'ERROR => error(msg)
- case _ => // nothing to do
- }
- }
-
- def debug(msg: => String): Unit = {
- if (isDebugEnabled && logger.isDebugEnabled) {
- logger.debug(msg)
- }
- }
-
- def debug(msg: => String, e: Throwable): Unit = {
- if (isDebugEnabled && logger.isDebugEnabled) {
- logger.debug(msg, e)
- }
- }
-
- def info(msg: => String): Unit = {
- if (isInfoEnabled && logger.isInfoEnabled) {
- logger.info(msg)
- }
- }
-
- def info(msg: => String, e: Throwable): Unit = {
- if (isInfoEnabled && logger.isInfoEnabled) {
- logger.info(msg, e)
- }
- }
-
- def warn(msg: => String): Unit = {
- if (isWarnEnabled && logger.isWarnEnabled) {
- logger.warn(msg)
- }
- }
-
- def warn(msg: => String, e: Throwable): Unit = {
- if (isWarnEnabled && logger.isWarnEnabled) {
- logger.warn(msg, e)
- }
- }
-
- def error(msg: => String): Unit = {
- if (isErrorEnabled && logger.isErrorEnabled) {
- logger.error(msg)
- }
- }
-
- def error(msg: => String, e: Throwable): Unit = {
- if (isErrorEnabled && logger.isErrorEnabled) {
- logger.error(msg, e)
- }
- }
-
- }
logging/LogSupport.scala
- package sdp.logging
-
- import org.slf4j.LoggerFactory
-
- trait LogSupport {
-
- /**
- * Logger
- */
- protected val log = new Log(LoggerFactory.getLogger(this.getClass))
-
- }
Commands.scala
- package pos.commands
- import pos.states.States._
-
- object Commands {
-
- case object PassivatePOS //passivate message
- case class DebugMode(debug: Boolean)
-
- sealed trait POSCommand {}
-
- case class LogOn(opr: String, passwd: String) extends POSCommand
- case object LogOff extends POSCommand
- case class SuperOn(su: String, passwd: String) extends POSCommand
- case object SuperOff extends POSCommand
- case class MemberOn(cardnum: String, passwd: String) extends POSCommand
- case object MemberOff extends POSCommand //remove member status for the voucher
- case object RefundOn extends POSCommand
- case object RefundOff extends POSCommand
- case object VoidOn extends POSCommand
- case object VoidOff extends POSCommand
- case object VoidAll extends POSCommand
- case object Suspend extends POSCommand
-
- case class VoucherNum(vnum: Int) extends POSCommand
-
-
- case class LogSales(salesType: Int, dpt: String, code: String, qty: Int, price: Int) extends POSCommand
- case object Subtotal extends POSCommand
- case class Discount(code: String, percent: Int) extends POSCommand
-
- case class OfflinePay(acct: String, num: String, amount: Int) extends POSCommand //settlement 结算支付
- //read only command, no event process
- case class VCBalance(acct: String, num: String, passwd: String) extends POSCommand
- case class VCPay(acct: String, num: String, passwd: String, amount: Int) extends POSCommand
- case class AliPay(acct: String, num: String, amount: Int) extends POSCommand
- case class WxPay(acct: String, num: String, amount: Int) extends POSCommand
-
-
- // read only command, no update event
- case class Plu(itemCode: String) extends POSCommand //read only
-
-
- }
-
-
- object Events {
-
- sealed trait POSEvent {}
-
- case class LogOned(txnItem: TxnItem) extends POSEvent
- object LogOned {
- def apply(op: String, vs: VchStates): LogOned = LogOned(TxnItem(vs).copy(
- txntype = TXNTYPE.logon,
- salestype = SALESTYPE.crd,
- opr = op,
- code = op
- ))
- }
- case class LogOffed(txnItem: TxnItem) extends POSEvent
- object LogOffed {
- def apply(vs: VchStates): LogOffed = LogOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.logon,
- salestype = SALESTYPE.crd,
- ))
- }
- case class SuperOned(txnItem: TxnItem) extends POSEvent
- object SuperOned {
- def apply(su: String, vs: VchStates): SuperOned = SuperOned(TxnItem(vs).copy(
- txntype = TXNTYPE.supon,
- salestype = SALESTYPE.crd,
- code = su
- ))
- }
- case class SuperOffed(txnItem: TxnItem) extends POSEvent
- object SuperOffed {
- def apply(vs: VchStates): SuperOffed = SuperOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.supon,
- salestype = SALESTYPE.crd
- ))
- }
- case class MemberOned(txnItem: TxnItem) extends POSEvent
- object MemberOned {
- def apply(cardnum: String,vs: VchStates): MemberOned = MemberOned(TxnItem(vs).copy(
- txntype = TXNTYPE.sales,
- salestype = SALESTYPE.crd,
- member = cardnum
- ))
- }
- case class MemberOffed(txnItem: TxnItem) extends POSEvent //remove member status for the voucher
- object MemberOffed {
- def apply(vs: VchStates): MemberOffed = MemberOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.sales,
- salestype = SALESTYPE.crd,
- member = vs.mbr
- ))
- }
- case class RefundOned(txnItem: TxnItem) extends POSEvent
- object RefundOned {
- def apply(vs: VchStates): RefundOned = RefundOned(TxnItem(vs).copy(
- txntype = TXNTYPE.refund
- ))
- }
- case class RefundOffed(txnItem: TxnItem) extends POSEvent
- object RefundOffed {
- def apply(vs: VchStates): RefundOffed = RefundOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.refund
- ))
- }
- case class VoidOned(txnItem: TxnItem) extends POSEvent
- object VoidOned {
- def apply(vs: VchStates): VoidOned = VoidOned(TxnItem(vs).copy(
- txntype = TXNTYPE.void
- ))
- }
- case class VoidOffed(txnItem: TxnItem) extends POSEvent
- object VoidOffed {
- def apply(vs: VchStates): VoidOffed = VoidOffed(TxnItem(vs).copy(
- txntype = TXNTYPE.void
- ))
- }
-
- case class NewVoucher(vnum: Int) extends POSEvent //新单, reminder for read-side to set new vnum
- case class EndVoucher(vnum: Int) extends POSEvent //单据终结标示
- case class VoidVoucher(txnItem: TxnItem) extends POSEvent
- object VoidVoucher {
- def apply(vs: VchStates): VoidVoucher = VoidVoucher(TxnItem(vs).copy(
- txntype = TXNTYPE.voidall
- ))
- }
- case class SuspVoucher(txnItem: TxnItem) extends POSEvent
- object SuspVoucher {
- def apply(vs: VchStates): SuspVoucher = SuspVoucher(TxnItem(vs).copy(
- txntype = TXNTYPE.suspend
- ))
- }
-
- case class VoucherNumed(fnum: Int, tnum: Int) extends POSEvent
-
- case class SalesLogged(txnItem: TxnItem) extends POSEvent
- case class Subtotaled(txnItem: TxnItem) extends POSEvent
- object Subtotaled {
- def apply(vs: VchStates, vi: VchItems): Subtotaled = {
- val (cnt,tqty,tamt,tdsc) = vi.subTotal
-
- Subtotaled(TxnItem(vs).copy(
- txntype = TXNTYPE.sales,
- salestype = SALESTYPE.sub,
- qty = tqty,
- amount = tamt,
- dscamt = tdsc,
- price = cnt
- ))
- }
- }
- case class Discounted(txnItem: TxnItem) extends POSEvent
- case class Payment(txnItem: TxnItem) extends POSEvent //settlement 结算支付
- object Payment {
- def apply(acct: String, num: String, vs: VchStates): Payment = Payment(TxnItem(vs).copy(
- txntype = TXNTYPE.sales,
- salestype = SALESTYPE.ttl,
- dpt = acct,
- code = num
- ))
- }
-
-
- }
-
- object Responses {
-
- object STATUS {
- val OK: Int = 0
- val FAIL: Int = -1
- }
-
- case class POSResponse (sts: Int, msg: String, voucher: VchStates, txnItems: List[TxnItem])
- }
states.scala
- package pos.states
- import java.time.LocalDate
- import java.time.LocalDateTime
- import java.time.format.DateTimeFormatter
- import java.util.Locale
-
- import pos.commands.Events._
- import pos.commands.Commands._
- import akka.actor._
- import pos.dao.DAO._
- import pos.dao.Codes._
- import pos.commands.Responses._
-
- object States {
-
- val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
-
-
- 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 plu: Int = 0
- val dpt: Int = 1
- val cat: Int = 2
- val brd: Int = 3
- val ra: Int = 4
- val sub: Int = 5
- val ttl: Int = 6
- val dsc: Int = 7
- val crd: Int = 8
- }
-
-
- 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.plu //销售类型
- ,qty: Int = 1 //交易数量
- ,price: Int = 0 //单价(分)
- ,amount: Int = 0 //码洋(分)
- ,dscamt: Int = 0 //折扣:负值 net实洋 = amount + dscamt
- ,member: String = "" //会员卡号
- ,code: String = "" //编号(商品、账号...)
- ,desc: String = "" //项目名称
- ,dpt: String = ""
- ,department: String = ""
- ,cat: String = ""
- ,category: String = ""
- ,brd: String = ""
- ,brand: 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 = 1, //当前序号
- 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
- )
- }
-
- case class VchItems(txnitems: List[TxnItem] = Nil) {
- def subTotal: (Int,Int,Int,Int) = txnitems.foldRight((0,0,0,0)) { case (txn,b) =>
- if (txn.salestype < SALESTYPE.sub && 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 totalSales: Int = txnitems.foldRight(0) { case (txn, b) =>
- if ( txn.salestype <= SALESTYPE.ra)
- (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
- /*
- val amt: Int = txn.salestype match {
- case SALESTYPE.ttl => txn.amount
- case _ => 0
- }
- amt + 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: POSEvent, state: VchStates, items: VchItems, lastSeqNr: BigInt = 0): (VchStates, VchItems) = evt match {
- case LogOned(txn) => (state.copy(seq = txn.seq + 1,opr = txn.opr,jseq = lastSeqNr), items)
- case LogOffed(txn) => (state.copy(seq = state.seq + 1,opr = ""), items)
- case RefundOned(txn) => (state.copy(seq = txn.seq + 1,refd = true), items)
- case RefundOffed(txn) => (state.copy(seq = txn.seq + 1,refd = false), items)
- case VoidOned(txn) => (state.copy(seq = txn.seq + 1,void = true), items)
- case VoidOffed(txn) => (state.copy(seq = txn.seq + 1,void = false), items)
- case SuperOned(txn) => (state.copy(seq = txn.seq + 1, su = txn.code), items)
- case SuperOffed(txn) => (state.copy(seq = txn.seq + 1, su = ""), items)
- case MemberOned(txn) => (state.copy(seq = txn.seq + 1, mbr = txn.member), items)
- case MemberOffed(txn) => (state.copy(seq = txn.seq + 1, mbr=""), items)
-
-
- case SalesLogged(txnitem) => (state.copy(
- seq = state.seq + 1)
- , items.addItem(txnitem))
-
- case Subtotaled(txnitem) => (state.copy(
- seq = state.seq + 1)
- , items.addItem(txnitem))
-
- case Payment(txnItem) =>
- val due = if(items.totalSales > 0) items.totalSales - items.totalPaid else items.totalSales + items.totalPaid
- val bal = if(items.totalSales > 0) due - txnItem.amount else due + txnItem.amount
- (state.copy(
- seq = state.seq + 1,
- due = (if( (txnItem.amount.abs + items.totalPaid.abs) >= items.totalSales.abs) false else true)
- )
- ,items.addItem(txnItem.copy(
- salestype = SALESTYPE.ttl,
- price = due,
- amount = txnItem.amount,
- dscamt = bal,
- department = getAcct(txnItem.dpt).getOrElse(Account("","")).name
- )))
-
- 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 VoidVoucher(vnum) => (state.nextVoucher, VchItems())
- case SuspVoucher(vnum) => (state.nextVoucher, VchItems()) //represented by EndVoucher
- */
- case EndVoucher(vnum) => (state.nextVoucher.copy(jseq = lastSeqNr), VchItems())
-
- case _ => (state, items)
- }
-
- def cmdFilter(terminalid: String, cmd: POSCommand, state: VchStates, txns: VchItems, router: ActorRef): Boolean = cmd match {
- case LogOn(opr, passwd) => //only allowed in logOffState
- if (!txns.txnitems.isEmpty) { //in the middle of process
- router ! POSResponse(STATUS.FAIL, s"禁止用户登陆!终端 ${terminalid} 有未完成单据。", state, List())
- false
- } else{
- if (validateUser(opr, passwd).isDefined) true
- else {
- router ! POSResponse(STATUS.FAIL, s"终端-$terminalid: 用户 ${opr} 登陆失败!", state, List())
- false
- }
- }
- case LogOff => //only allowed in logOnState
- if (!txns.txnitems.isEmpty) { //in the middle of process
- router ! POSResponse(STATUS.FAIL, s"禁止用户退出!终端 ${terminalid} 有未完成单据。", state, List())
- false
- } else true
-
- case VoidAll => //allowed in logOnState and paymentState
- if (txns.txnitems.isEmpty) { //no valid sales
- router ! POSResponse(STATUS.FAIL, s"全单取消失败!终端 ${terminalid} 本单无任何有效销售记录。", state, txns.txnitems)
- false
- } else true
-
- case OfflinePay(acct,num,amt) =>
- if (txns.totalSales.abs == 0) { // no valid sales. void,refund neg values could produce zero
- router ! POSResponse(STATUS.FAIL, s"支付失败!终端 ${terminalid} 应付金额为零。", state, List())
- false
- } else {
- if(validateAcct(acct).isDefined) true
- else {
- router ! POSResponse(STATUS.FAIL, s"支付失败!终端 ${terminalid} 账号{$acct}不存在。", state, List())
- false
- }
- }
- case Subtotal =>
- if (txns.txnitems.isEmpty) { //in the middle of process
- router ! POSResponse(STATUS.FAIL, s"小计操作失败!终端 ${terminalid} 无任何销售记录。", state, List())
- false
- } else true
- case VCBalance(_,_,_) => true
- case MemberOn(_,_) => true
- case MemberOff => true
- case VoucherNum(_) => true
-
- case LogSales(salesType,sdpt,scode,sqty,sprice) =>
- if (state.void) {
- txns.txnitems.find(ti => (ti.txntype == TXNTYPE.sales && ti.salestype == salesType &&
- ti.dpt == sdpt && ti.code == scode && ti.qty == sqty && ti.price == sprice)) match {
- case Some(_) => true
- case None =>
- router ! POSResponse(STATUS.FAIL, s"取消交易失败!终端 ${terminalid} 销售记录不存在。", state, List(TxnItem(state).copy(
- salestype = salesType,
- dpt = sdpt,
- code = scode,
- price = sprice
- )))
- false
- }
-
- } else true
-
- case c @ _ =>
- router ! POSResponse(STATUS.FAIL, s"终端 ${terminalid} 不支持操作 {$c}", state, List())
- false
- }
-
-
- }
POSHandler.scala
- package pos.handler
- import akka.actor._
- import akka.persistence._
- import akka.cluster._
- import pos.commands.Events._
- import pos.commands.Commands._
- import pos.states._
- import States._
- import pos.dao.Items._
- import pos.dao.DAO._
- import pos.dao.Codes._
- import com.typesafe.config.ConfigFactory
- import sdp.logging.LogSupport
- import pos.commands.Responses._
- import pos.interface.POSInterfaces
-
-
- class WriterActor extends PersistentActor with LogSupport {
- val cluster = Cluster(context.system)
- // shopdptId-posId
- // self.path.parent.name is the type name (utf-8 URL-encoded)
- // self.path.name is the entry identifier (utf-8 URL-encoded) but entity has a supervisor
- // override def persistenceId: String = self.path.parent.parent.name + "-" + self.path.parent.name
- override def persistenceId: String = self.path.parent.name
-
- override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
- super.preRestart(reason, message)
- log.info(s"Restarting terminal $persistenceId on ${cluster.selfAddress} for $message")
- }
-
- override def postRestart(reason: Throwable): Unit = {
- super.postRestart(reason)
- log.info(s"terminal $persistenceId on ${cluster.selfAddress} restarted for ${reason.getMessage}")
- }
-
- override def postStop(): Unit = {
- log.info(s"terminal $persistenceId on ${cluster.selfAddress} stopped!")
- }
-
- override def preStart(): Unit = {
- log.info(s"terminal $persistenceId on ${cluster.selfAddress} starting...")
- }
-
- //helper functions
- object RunPOSCommand {
- def unapply(arg: POSCommand) = if (cmdFilter(persistenceId,arg,vchState,vchItems,sender())) Some(arg) else None
- }
-
- def persistEvent[E](evt: E)(f: E => Unit)(implicit dm: DebugMode) = {
- if (dm.debug)
- log.info(s"********** $persistenceId: persisted event: {$evt} **********")
- else {
- try {
- persist(evt)(f)
- log.debug(s"终端-$persistenceId:event: [$evt] state: [$vchState] : [${vchItems.txnitems.reverse}]")
- }
- catch {
- case err: Throwable =>
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 操作失败![${err.getMessage}]。", vchState, vchItems.txnitems.reverse)
- log.error(s"终端-$persistenceId: 操作失败![${err.getMessage}] current state: [$vchState],[${vchItems.txnitems.reverse}]")
- }
- }
- }
-
- var debugConfig: com.typesafe.config.Config = _
- var debug: Boolean = _
- try {
-
- debugConfig = ConfigFactory.load("pos.conf").getConfig("pos.server")
- debug = debugConfig.getBoolean("debug")
- }
- catch {
- case _ : Throwable => debug = false
- }
-
- log.info(s"********** $persistenceId: debug mode = $debug **********")
-
- implicit val debugMode = DebugMode(debug)
-
- //actor state
- var vchState = VchStates()
- var vchItems = VchItems()
-
-
- override def receiveRecover: Receive = {
- case evt: POSEvent => //incompleted voucher play back events
- val (vs,vi) = updateState(evt,vchState,vchItems)
- vchState = vs; vchItems = vi
- case SnapshotOffer(_,vs: VchStates) => vchState = vs //restore num,seq ...
- }
-
- override def receiveCommand: Receive = logOffState
-
- private def logOffState: Receive = {
- case RunPOSCommand(LogOn(opr, _)) =>
- persistEvent(LogOned(opr,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems,lastSequenceNr)
- //starting seqenceNr for any voucher. no action logged before login
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 用户{$opr}成功登陆。", vchState, List(vchItems.txnitems.head))
- context.become(logOnState)
- }
-
- case PassivatePOS =>
- log.info(s"**********${persistenceId} got passivate message and stopping ... ***********")
- context.parent ! PoisonPill
-
- case _ =>
- sender() ! POSResponse(STATUS.FAIL, s"操作失败!终端 ${persistenceId} 用户未登陆。", vchState, List())
- }
-
-
- private def paymentState: Receive = {
- case RunPOSCommand(OfflinePay(acct,num, amount)) =>
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
-
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. return to logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems, lastSequenceNr)
- vchState = sts._1.copy(jseq = lastSequenceNr + 1)
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- context.unbecome() //logOnState
- }
- }
- // else wait for other payments and stay in logOnState
- }
- /* strictly disallow any action other than payment till completion
- case RunPOSCommand(VoidAll) =>
- persistEvent(VoidVoucher(vchState)) { _ =>
- persistEvent(EndVoucher(vchState.num)) { evt =>
- updateState(evt,vchState,vchItems)
- context.unbecome() //in paymentState, switch to logOnState
- }
- }
- case RunPOSCommand(Suspend) =>
- persistEvent(SuspVoucher(vchState)) { _ =>
- persistEvent(EndVoucher(vchState.num)) { evt =>
- updateState(evt,vchState,vchItems)
- context.unbecome() //in paymentState, switch to logOnState
- }
- }
- */
- case RunPOSCommand(SuperOn(su,_)) =>
- persistEvent(SuperOned(su,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- }
- case RunPOSCommand(SuperOff) =>
- persistEvent(SuperOffed(vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- }
-
- case RunPOSCommand(VCBalance(acct,num,passwd)) =>
- if (POSInterfaces.validateVC(acct,num,passwd)) {
- val res = POSInterfaces.getVCBalance(acct,num)
- if (res.sts == POSInterfaces.VCRESULT.OK)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 储值卡余额{${res.amt}}。", vchState, List(TxnItem(vchState).copy(
- amount = (res.amt * 100).toInt,
- dpt = acct,
- code = num
- )))
- else
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额读取错误![${res.msg}]", vchState, List())
- } else {
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]验证信息错误!", vchState, List())
- }
-
- case RunPOSCommand(VCPay(acct,num, passwd,amount)) =>
- if (POSInterfaces.validateVC(acct,num,passwd)) {
- val res = POSInterfaces.getVCBalance(acct,num)
- if (res.sts == POSInterfaces.VCRESULT.OK) {
- if ((res.amt * 100).toInt < amount)
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额不足!", vchState,List(TxnItem(vchState).copy(
- amount = (res.amt * 100).toInt,
- dpt = acct,
- code = num
- )))
- else {
- val res = POSInterfaces.payByVC(acct,num,amount/100.00)
- if (res.sts == POSInterfaces.VCRESULT.OK) {
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. stay in logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems,lastSequenceNr)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- context.unbecome() //switch to logOnState
- }
- }
- }
-
- }
- }
-
- }
- else
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额读取错误![${res.msg}]", vchState, List())
- } else {
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]验证信息错误!", vchState, List())
- }
- case _ =>
- sender() ! POSResponse(STATUS.FAIL, s"操作失败!终端 ${persistenceId} 结算中不容许其它非支付操作!", vchState, List(vchItems.txnitems.head))
-
- }
-
- private def logOnState: Receive = {
- case RunPOSCommand(LogOff) =>
- persistEvent(LogOffed(vchState)) { evt =>
- val user = vchState.opr
- val sts = updateState(evt,vchState,vchItems)
- vchState = sts._1; vchItems = sts._2
- saveSnapshot(vchState) //state of last voucher
- //手工passivate shard ! ShardRegion.Passivate(PassivatePOS)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 用户 $user 成功退出。", vchState, List(vchItems.txnitems.head))
- context.unbecome() //switch to logOffState
- }
-
- case RunPOSCommand(SuperOn(su,_)) =>
- persistEvent(SuperOned(su,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 转到管理模式{$su}。", vchState, List(vchItems.txnitems.head))
- }
- case RunPOSCommand(SuperOff) =>
- persistEvent(SuperOffed(vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 退出管理模式。", vchState, List(vchItems.txnitems.head))
- }
- case RunPOSCommand(RefundOn) =>
- persistEvent(RefundOned(vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 进入退款模式。", vchState, List(vchItems.txnitems.head))
- }
- case RunPOSCommand(RefundOff) =>
- persistEvent(RefundOffed(vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 退出退款模式。", vchState, List(vchItems.txnitems.head))
- }
- case RunPOSCommand(VoidOn) =>
- persistEvent(VoidOned(vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 进入取消模式。", vchState, List(vchItems.txnitems.head))
- }
- case RunPOSCommand(VoidOff) =>
- persistEvent(VoidOffed(vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 退出取消模式。", vchState, List(vchItems.txnitems.head))
- }
- case RunPOSCommand(MemberOn(cardnum, _)) =>
- persistEvent(MemberOned(cardnum,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 进入会员模式,卡号{$cardnum}。", vchState, List(vchItems.txnitems.head))
- }
- case RunPOSCommand(MemberOff) =>
- val cardnum = vchState.mbr
- persistEvent(MemberOffed(vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 退出会员模式,卡号{$cardnum}。", vchState, List(vchItems.txnitems.head))
- }
-
- case RunPOSCommand(VoucherNum(tnum)) =>
- val fnum = vchState.num
- persistEvent(VoucherNumed(fnum,tnum)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功设定单号{$fnum -> $tnum}。", vchState, List())
- }
-
- case RunPOSCommand(Subtotal) =>
- persistEvent(Subtotaled(vchState,vchItems)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 小计:${vchItems.txnitems.head.price} 条项目。", vchState, List(vchItems.txnitems.head))
- }
- //first payment in a voucher
- case RunPOSCommand(OfflinePay(acct,num, amount)) =>
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. stay in logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems,lastSequenceNr)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- }
- }
- else context.become(paymentState) //switch into paymentState
- }
- case RunPOSCommand(VCBalance(acct,num,passwd)) =>
- if (POSInterfaces.validateVC(acct,num,passwd)) {
- val res = POSInterfaces.getVCBalance(acct,num)
- if (res.sts == POSInterfaces.VCRESULT.OK)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 储值卡余额{${res.amt}}。", vchState, List(TxnItem(vchState).copy(
- amount = (res.amt * 100).toInt,
- dpt = acct,
- code = num
- )))
- else
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额读取错误![${res.msg}]", vchState, List())
- } else {
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]验证信息错误!", vchState, List())
- }
-
- case RunPOSCommand(VCPay(acct,num, passwd,amount)) =>
- if (POSInterfaces.validateVC(acct,num,passwd)) {
- val res = POSInterfaces.getVCBalance(acct,num)
- if (res.sts == POSInterfaces.VCRESULT.OK) {
- if ((res.amt * 100).toInt < amount)
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额不足!", vchState,List(TxnItem(vchState).copy(
- amount = (res.amt * 100).toInt,
- dpt = acct,
- code = num
- )))
- else {
- val res = POSInterfaces.payByVC(acct,num,amount/100.00)
- if (res.sts == POSInterfaces.VCRESULT.OK) {
- persistEvent(Payment(acct,num,vchState)) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchItems.totalSales > 0)
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}支付。", vchState, List(vchItems.txnitems.head))
- else
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功完成{${vchItems.txnitems.head.category} ${amount/100.0}退款。", vchState, List(vchItems.txnitems.head))
- if (!vchState.due) { //completed voucher. mark end of voucher and move next. stay in logOnState
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems,lastSequenceNr)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- }
- }
- else context.become(paymentState) //switch into paymentState
- }
-
- }
- }
-
- }
- else
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]余额读取错误![${res.msg}]", vchState, List())
- } else {
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 储值卡[$num]验证信息错误!", vchState, List())
- }
-
- case RunPOSCommand(VoidAll) =>
- val vnum = vchState.num
- persistEvent(VoidVoucher(vchState)) { _ =>
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems,lastSequenceNr)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 完成全单取消{${vnum}}。", vchState, List(vchItems.txnitems.head))
- }
- }
- case RunPOSCommand(Suspend) =>
- val vnum = vchState.num
- persistEvent(SuspVoucher(vchState)) { _ =>
- persistEvent(EndVoucher(vchState.num)) { evt =>
- val sts = updateState(evt, vchState, vchItems,lastSequenceNr)
- vchState = sts._1
- vchItems = sts._2
- saveSnapshot(vchState) //recovery to next voucher
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 完成挂单{${vnum}}。", vchState, List(vchItems.txnitems.head))
- }
- }
-
-
-
- case RunPOSCommand(LogSales(salesType,sdpt,scode,sqty,sprice)) => {
- var pqty = 0
- if (vchState.void) {
- vchItems.txnitems.find(ti => (ti.txntype == TXNTYPE.sales && ti.salestype == salesType &&
- ti.dpt == sdpt && ti.code == scode && ti.qty == sqty && ti.price == sprice)) match {
- case Some(ti) => pqty = -ti.qty
- case None => pqty = sqty
- }
- }
- salesType match {
- case SALESTYPE.plu =>
- getItem(scode) match {
- case QueryItemsOK(items) =>
- val pr = if (sprice > 0) sprice else items.head.price
- val evt = SalesLogged(TxnItem(vchState).copy(
- txntype = if (vchState.void) TXNTYPE.void else TXNTYPE.sales,
- salestype = salesType,
- price = pr,
- qty = pqty,
- amount = pr * pqty,
- code = scode,
- desc = items.head.name,
- dpt = items.head.dpt,
- cat = items.head.cat,
- brd = items.head.brd,
- department = (getDpt(items.head.dpt).getOrElse(Department("", ""))).name,
- category = (getCat(items.head.brd).getOrElse(Category("", ""))).name,
- brand = (getBrd(items.head.brd).getOrElse(Brand("", ""))).name
- ))
- persistEvent(evt) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchState.void) {
- vchItems = vchItems.copy(
- txnitems = vchItems.txnitems.map { ti =>
- if (ti.txntype == TXNTYPE.sales && ti.salestype == salesType &&
- ti.dpt == sdpt && ti.code == scode && ti.qty == sqty && ti.price == sprice)
- ti.copy(txntype = TXNTYPE.voided)
- else
- ti
- })
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功冲销商品销售。", vchState, List(vchItems.txnitems.head))
- } else {
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 商品销售操作成功。", vchState, List(vchItems.txnitems.head))
- }
-
- }
- case QueryItemsFail(msg) =>
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 商品编号错误![$msg]", vchState, List())
- }
- case SALESTYPE.dpt =>
- validateDpt(sdpt) match {
- case Some(dpt) =>
- val evt = SalesLogged(TxnItem(vchState).copy(
- txntype = if (vchState.void) TXNTYPE.void else TXNTYPE.sales,
- salestype = salesType,
- price = sprice,
- qty = pqty,
- amount = sprice * pqty,
- dpt = sdpt,
- department = dpt.name
- ))
- persistEvent(evt) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchState.void) {
- vchItems = vchItems.copy(
- txnitems = vchItems.txnitems.map { ti =>
- if (ti.txntype == TXNTYPE.sales && ti.salestype == salesType &&
- ti.dpt == sdpt && ti.code == scode && ti.qty == sqty && ti.price == sprice)
- ti.copy(txntype = TXNTYPE.voided)
- else
- ti
- })
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功冲销部门销售。", vchState, List(vchItems.txnitems.head))
- } else {
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 部门销售操作成功。", vchState, List(vchItems.txnitems.head))
- }
- }
- case None =>
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 部门编号[$sdpt]错误!", vchState, List())
- }
- case SALESTYPE.brd =>
- validateBrd(sdpt) match {
- case Some(brd) =>
- val evt = SalesLogged(TxnItem(vchState).copy(
- txntype = if (vchState.void) TXNTYPE.void else TXNTYPE.sales,
- salestype = salesType,
- price = sprice,
- qty = pqty,
- amount = sprice * pqty,
- brd = sdpt,
- brand = brd.name
- ))
- persistEvent(evt) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchState.void) {
- vchItems = vchItems.copy(
- txnitems = vchItems.txnitems.map { ti =>
- if (ti.txntype == TXNTYPE.sales && ti.salestype == salesType &&
- ti.dpt == sdpt && ti.code == scode && ti.qty == sqty && ti.price == sprice)
- ti.copy(txntype = TXNTYPE.voided)
- else
- ti
- })
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功冲销品牌销售。", vchState, List(vchItems.txnitems.head))
- } else {
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 品牌销售操作成功。", vchState, List(vchItems.txnitems.head))
- }
- }
- case None =>
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 品牌编号[$sdpt]错误!", vchState, List())
- }
- case SALESTYPE.cat =>
- validateCat(sdpt) match {
- case Some(cat) =>
- val evt = SalesLogged(TxnItem(vchState).copy(
- txntype = if (vchState.void) TXNTYPE.void else TXNTYPE.sales,
- salestype = salesType,
- price = sprice,
- qty = pqty,
- amount = sprice * pqty,
- cat = sdpt,
- category = cat.name
- ))
- persistEvent(evt) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchState.void) {
- vchItems = vchItems.copy(
- txnitems = vchItems.txnitems.map { ti =>
- if (ti.txntype == TXNTYPE.sales && ti.salestype == salesType &&
- ti.dpt == sdpt && ti.code == scode && ti.qty == sqty && ti.price == sprice)
- ti.copy(txntype = TXNTYPE.voided)
- else
- ti
- })
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功冲销分类销售。", vchState, List(vchItems.txnitems.head))
- } else {
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 分类销售操作成功。", vchState, List(vchItems.txnitems.head))
- }
- }
- case None =>
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 分类编号[$sdpt]错误!", vchState, List())
- }
- case SALESTYPE.ra =>
- validateCat(sdpt) match {
- case Some(ra) =>
- val evt = SalesLogged(TxnItem(vchState).copy(
- txntype = if (vchState.void) TXNTYPE.void else TXNTYPE.sales,
- salestype = salesType,
- price = sprice,
- qty = pqty,
- amount = sprice * pqty,
- dpt = sdpt,
- code = scode,
- department = ra.name
- ))
- persistEvent(evt) { evt =>
- val sts = updateState(evt, vchState, vchItems)
- vchState = sts._1
- vchItems = sts._2
- if (vchState.void) {
- vchItems = vchItems.copy(
- txnitems = vchItems.txnitems.map { ti =>
- if (ti.txntype == TXNTYPE.sales && ti.salestype == salesType &&
- ti.dpt == sdpt && ti.code == scode && ti.qty == sqty && ti.price == sprice)
- ti.copy(txntype = TXNTYPE.voided)
- else
- ti
- })
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 成功冲销代收。", vchState, List(vchItems.txnitems.head))
- } else {
- sender() ! POSResponse(STATUS.OK, s"终端-$persistenceId: 代收操作成功。", vchState, List(vchItems.txnitems.head))
- }
- }
- case None =>
- sender() ! POSResponse(STATUS.FAIL, s"终端-$persistenceId: 代收编号[$sdpt]错误!", vchState, List())
- }
-
- }
- }
-
- }
-
-
-
- }
POSDataAccess.scala
- package pos.dao
-
- import java.time.LocalDate
- import java.time.format.DateTimeFormatter
-
-
- case class Item(
- brd: String
- ,dpt: String
- ,cat: String
- ,code: String
- ,name: String
- ,price: Int
-
- )
- object Items {
- val apple = Item("01","02","01","001", "green apple", 820)
- val grape = Item("01","02","01","002", "red grape", 1050)
- val orage = Item("01","02","01","003", "sunkist orage", 350)
- val banana = Item("01","02","01","004", "demon banana", 300)
- val pineapple = Item("01","02","01","005", "hainan pineapple", 1300)
- val peach = Item("01","02","01","006", "xinjiang peach", 2390)
-
- val tblItems = List(apple, grape, orage, banana, pineapple, peach)
-
- sealed trait QueryItemsResult {}
-
- case class QueryItemsOK(items: List[Item]) extends QueryItemsResult
-
- case class QueryItemsFail(msg: String) extends QueryItemsResult
-
- }
-
-
- object Codes {
- case class User(code: String, name: String, passwd: String)
- case class Department(code: String, name: String)
- case class Category(code: String, name: String)
- case class Brand(code: String, name: String)
- case class Ra(code: String, name: String)
- case class Account(code: String, name: String)
- case class Disc(code: String, best: Boolean, aggr: Boolean, group: Boolean)
-
- val ras = List(Ra("01","Delivery"),Ra("02","Cooking"))
- val dpts = List(Department("01","Fruit"),Department("02","Grocery"))
- val cats = List(Category("0101","Fresh Fruit"),Category("0201","Dry Grocery"))
- val brds = List(Brand("01","Sunkist"),Brand("02","Demon"))
- val accts = List(Account("001","Cash"),Account("002","Value Card"), Account("003", "Visa")
- ,Account("004","Alipay"),Account("005","WXPay"))
-
- val users = List(User("1001","Tiger", "123"),User("1002","John", "123"),User("1003","Maria", "123"))
-
- def getDpt(code: String) = dpts.find(d => d.code == code)
- def getCat(code: String) = cats.find(d => d.code == code)
- def getBrd(code: String) = brds.find(b => b.code == code)
- def getAcct(code: String) = accts.find(a => a.code == code)
- def getRa(code: String) = ras.find(a => a.code == code)
- }
-
- object DAO {
- import Items._
- import Codes._
-
- def getItem(code: String): QueryItemsResult = {
- val optItem = tblItems.find(it => it.code == code)
- optItem match {
- case Some(item) => QueryItemsOK(List(item))
- case None => QueryItemsFail("Invalid item code!")
- }
- }
-
- def validateDpt(code: String) = dpts.find(d => d.code == code)
- def validateCat(code: String) = cats.find(d => d.code == code)
- def validateBrd(code: String) = brds.find(b => b.code == code)
- def validateRa(code: String) = ras.find(ac => ac.code == code)
- def validateAcct(code: String) = accts.find(ac => ac.code == code)
-
- def validateUser(userid: String, passwd: String) = users.find(u => (u.code == userid && u.passwd == passwd))
-
- def lastSecOfDateStr(ldate: LocalDate): String = {
- ldate.format(DateTimeFormatter.ofPattern( "yyyy-MM-dd"))+" 23:59:59"
- }
-
-
- }
ClusterMonitor.scala
- package pos.cluster
-
- import akka.actor._
- import akka.cluster.ClusterEvent._
- import akka.cluster._
- import sdp.logging.LogSupport
-
- class ClusterMonitor extends Actor with LogSupport {
- val cluster = Cluster(context.system)
- override def preStart(): Unit = {
- cluster.subscribe(self,initialStateMode = InitialStateAsEvents
- ,classOf[MemberEvent],classOf[UnreachableMember]) //订阅集群状态转换信息
- super.preStart()
- }
-
- override def postStop(): Unit = {
- cluster.unsubscribe(self) //取消订阅
- super.postStop()
- }
-
- override def receive: Receive = {
- case MemberJoined(member) =>
- log.info(s"Member is Joining: {${member.address}}")
- case MemberUp(member) =>
- log.info(s"Member is Up: {${member.address}}")
- case MemberLeft(member) =>
- log.info(s"Member is Leaving: {${member.address}}")
- case MemberExited(member) =>
- log.info(s"Member is Exiting: {${member.address}}")
- case MemberRemoved(member, previousStatus) =>
- log.info(
- s"Member is Removed: {${member.address}} after {${previousStatus}")
- case UnreachableMember(member) =>
- log.info(s"Member detected as unreachable: {${member.address}}")
- cluster.down(member.address) //手工驱除,不用auto-down
- case _: MemberEvent => // ignore
- }
-
- }