123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- # -*- coding: utf-8 -*-
- # !/usr/bin/env python
- import datetime
- import logging
- from mongoengine import DoesNotExist
- from apilib.monetary import RMB, VirtualCoin, Ratio
- from apilib.utils_sys import memcache_lock
- from apps.web.constant import Const, DEALER_CONSUMPTION_AGG_KIND
- from apps.web.core.device_define.baojia import CMD_CODE, CARD_STATUS, DEFAULT_PARAM, BILLING_TYPE
- from apps.web.device.models import Device, Group
- from apps.web.eventer import EventBuilder
- from apps.web.eventer.base import FaultEvent, WorkEvent
- from apps.web.eventer.errors import NoCommandHandlerAvailable
- from apps.web.user.models import VCardConsumeRecord, ServiceProgress, CardRechargeOrder, CardConsumeRecord, \
- ConsumeRecord, MyUser, UserVirtualCard
- from apps.web.user.transaction_deprecated import refund_money
- logger = logging.getLogger(__name__)
- class builder(EventBuilder):
- def __getEvent__(self, device_event):
- event_data = self.deviceAdapter.analyze_event_data(device_event['data'])
- if event_data is None or 'cmdCode' not in event_data:
- return None
- if event_data['cmdCode'] in [CMD_CODE.DEVICE_SUBMIT_DEVICE_FAULT_0A]:
- return ChargingBAOJIAFaultEvent(self.deviceAdapter, event_data)
- elif event_data["cmdCode"] in [CMD_CODE.CARD_BALANCE_09, CMD_CODE.CARD_CONSUME_13, CMD_CODE.CARD_CONSUME_SURE_27]:
- return ChargingBJIDCardEvent(self.deviceAdapter, event_data)
- elif event_data["cmdCode"] in [CMD_CODE.CARD_REMOTE_CHARGE_12]:
- return ChargingBJICCardEvent(self.deviceAdapter, event_data)
- elif event_data["cmdCode"] in [CMD_CODE.DEVICE_SUBMIT_CHARGE_FINISHED_16]:
- return ChargingBJFinishEvent(self.deviceAdapter, event_data)
- elif event_data["cmdCode"] in [CMD_CODE.PORT_STATUS_21]:
- return ChargingBJReportEvent(self.deviceAdapter, event_data)
- else:
- raise NoCommandHandlerAvailable('[BJDZ] no command handler for cmd %s, curDevInfo=%s' % (event_data["cmdCode"], event_data))
- class ChargingBJFinishEvent(WorkEvent):
- @staticmethod
- def cache_key(devNo, cmd):
- return "event-{}-{}".format(devNo, cmd)
- def do(self, **args):
- devNo = self.device['devNo']
- logger.info('baojia charging event detected, devNo=%s,curInfo=%s' % (devNo, self.event_data))
- cmdCode = self.event_data['cmdCode']
- # 实际测试的时候会有多条结束事件一起报上来
- cacheKey = self.cache_key(devNo, cmdCode)
- with memcache_lock(cacheKey, value = '1', expire = 1) as acquired:
- if not acquired:
- return
- self.do_finished()
- def do_finished(self):
- """
- 网络支付的端口结束
- :return:
- """
- devCache = Device.get_dev_control_cache(self.device.devNo)
- portStr = self.event_data.get("portStr")
- portCache = devCache.get(portStr, dict())
- billingType = portCache.get("billingType", BILLING_TYPE.TIME)
- cardNo = portCache.get("cardNo")
- refundProtection = bool(int(portCache.get("refundProtection", 0)))
- refundProtectionTime = portCache.get("refundProtectionTime")
- vCardId = portCache.get("vCardId")
- coins = portCache.get("coins")
- openId = portCache.get("openId")
- startTime = portCache.get("startTime")
- need = portCache.get("need{}".format(billingType.capitalize()), 0xFFFF)
- left = self.event_data.get("left{}".format(billingType.capitalize()), 0x0000)
- reason = self.event_data.get("reason")
- user = MyUser.objects.filter(openId=openId, groupId=self.device.groupId).first()
- group = Group.get_group(self.device.groupId)
- if not user:
- Device.clear_port_control_cache(self.device.devNo, portStr)
- return
- consumeDict = {
- "chargeIndex": portStr,
- "reason": reason,
- "need{}".format(billingType.capitalize()): need,
- "left{}".format(billingType.capitalize()): left
- }
- # 处理相应的退费 虚拟卡的退费和扫码的退费不一样的 刷卡的退费会在另一条指令处理
- if cardNo:
- # 这个地方仅仅是更新一下
- consumeDict.update({"cardNo": cardNo})
- elif vCardId:
- # 尝试进行虚拟卡退费
- refundCoins = self._get_refund_coins(
- coins,
- left,
- need,
- refundProtection,
- refundProtectionTime,
- startTime
- )
- if refundCoins > VirtualCoin(0):
- vCard = UserVirtualCard.objects.get(id=vCardId)
- consumeRcdId = portCache.get("consumeRcdId", '')
- try:
- vCardConsumeRcd = VCardConsumeRecord.objects.get(id=consumeRcdId)
- except DoesNotExist:
- logger.info('can not find the consume rcd id = %s' % consumeRcdId)
- else:
- if billingType == BILLING_TYPE.TIME:
- vCard.refund_quota(
- consumeRcd=vCardConsumeRcd,
- usedTime=min(need-left, need),
- spendElec=0,
- backCoins=refundCoins
- )
- else:
- vCard.refund_quota(
- consumeRcd=vCardConsumeRcd,
- spendElec=min(need - left, need),
- usedTime=0,
- backCoins=refundCoins
- )
- else:
- refundCoins = self._get_refund_coins(
- coins,
- left,
- need,
- refundProtection,
- refundProtectionTime,
- startTime
- )
- consumeDict.update({DEALER_CONSUMPTION_AGG_KIND.COIN: str(VirtualCoin(coins))})
- if refundCoins > VirtualCoin(0):
- consumeDict.update({
- DEALER_CONSUMPTION_AGG_KIND.COIN: str((VirtualCoin(coins) - refundCoins)),
- DEALER_CONSUMPTION_AGG_KIND.REFUNDED_COINS: str(refundCoins.mongo_amount)
- })
- refund_money(self.device, refundCoins, openId)
- self.notify_user(
- managerialOpenId=user.managerialOpenId or "",
- templateName="refund_coins",
- title=u"金币退款",
- backCount=u"金币: {}".format(refundCoins),
- finishTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- )
- # 结束服务进程 通知用户服务结束
- ServiceProgress.update_progress_and_consume_rcd(
- self.device.ownerId,
- {
- "open_id": openId,
- "device_imei": self.device.devNo,
- "port": int(portStr),
- "isFinished": False
- },
- consumeDict
- )
- self.notify_user(
- managerialOpenId=user.managerialOpenId if user else "",
- templateName="service_complete",
- title=u"\\n\\n结束原因:\\t\\t{reason}\\n\\n设备编号:\\t\\t{logicalCode}-{port}\\n\\n服务地址:\\t\\t{group}".format(
- reason=reason,
- logicalCode=self.device["logicalCode"],
- port=portStr,
- group=group.get("address", ""),
- ),
- service=u"充电服务",
- finishTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- remark=u'谢谢您的支持'
- )
- # 清空端口信息
- Device.clear_port_control_cache(self.device.devNo, portStr)
- def _get_refund_coins(self, coins, left, need, refundProtection, refundProtectionTime, startTime):
- """
- 获取退费的额度 FFFF 的情况已经在退费保护里面涉及到了
- :param coins:
- :param left:
- :param need:
- :param refundProtection:
- :param refundProtectionTime:
- :param startTime:
- :return:
- """
- # 首先检查退费保护
- nowTime = datetime.datetime.now()
- startTime = datetime.datetime.strptime(startTime, "%Y-%m-%d %H:%M:%S")
- _time = (nowTime - startTime).total_seconds() / 60
- if refundProtection and _time < int(refundProtectionTime):
- return VirtualCoin(coins)
- # 然后检查退费开关
- if not self.device.is_auto_refund:
- return VirtualCoin(0)
- try:
- refundCoins = Ratio(float(left) / float(need)) * VirtualCoin(coins)
- except ZeroDivisionError:
- refundCoins = VirtualCoin(0)
- # 防止退大额度
- if refundCoins > VirtualCoin(coins):
- refundCoins = VirtualCoin(0)
- return refundCoins
- class ChargingBAOJIAFaultEvent(FaultEvent):
- """
- 基类的函数现在有问题,但是被太多接口引用, 先覆写吧,后续搞清楚了再修改基类函数
- """
- def do(self, **args):
- pass
- class ChargingBJIDCardEvent(WorkEvent):
- def do(self):
- funCode = self.event_data["cmdCode"]
- if funCode == CMD_CODE.CARD_BALANCE_09:
- self._do_query()
- elif funCode == CMD_CODE.CARD_CONSUME_13:
- self._do_settle()
- else:
- self._do_start_report()
- def _do_query(self):
- """ 查询 """
- cardNo = self.event_data.get("cardNo", "")
- cardNoHex = self.event_data.get("cardNoHex", "")
- card = self.update_card_dealer_and_type(cardNo)
- # 没有查询到有效卡
- if not card:
- balance = 0
- else:
- balance = str(card.balance)
- self.deviceAdapter._response_card_balance(cardNoHex, balance)
- def _do_settle(self):
- """ 结算 """
- cardNoHex = self.event_data.get("cardNoHex")
- cardNo = self.event_data.get("cardNo")
- payType = self.event_data.get("payType")
- money = self.event_data.get("money")
- cardType = self.event_data.get("cardType")
- params = [cardNo]
- if cardType == "01":
- params.append("ID")
- else:
- params.append("IC")
- card = self.update_card_dealer_and_type(*params)
- # 无效卡
- if not card or not card.openId:
- logger.error("invalid card <{}>".format(cardNo))
- self.deviceAdapter._response_card_consume(cardNoHex, 0, cardType, CARD_STATUS.STATUS_NO_CARD)
- return
- # 冻结卡
- if card.frozen:
- logger.error("card <{}> be frozen".format(cardNo))
- self.deviceAdapter._response_card_consume(cardNoHex, 0, cardType, CARD_STATUS.STATUS_FROZEN_CARD)
- return
- # 处理扣费事件
- if payType == "01":
- status = self._do_settle_consume(card, money)
- # 处理退费事件 需要注意余额回收 和 设备的退费开关
- else:
- status = self._do_settle_refund(card, money)
- return self.deviceAdapter._response_card_consume(cardNoHex, card.balance, cardType, status)
- pass
- def _do_start_report(self):
- """ 启动上报 """
- portStr = self.event_data.get("portStr")
- cardNo = self.event_data.get("cardNo")
- money = self.event_data.get("money")
- _time = self.event_data.get("time")
- elec = self.event_data.get("elec")
- card = self.update_card_dealer_and_type(cardNo)
- record = CardConsumeRecord.objects.filter(cardId=str(card.id)).order_by("-dateTimeAdded").first()
- if not record:
- return
- consumeRecord = ConsumeRecord.objects.get(orderNo=record.linkedConsumeRcdOrderNo)
- attachParas = consumeRecord.attachParas
- attachParas.update({"chargeIndex": portStr})
- consumeRecord.attachParas = attachParas
- consumeRecord.save()
- serviceDict = {
- 'orderNo': consumeRecord.orderNo,
- 'money': money,
- 'coin': money,
- 'cardOrderNo': record.orderNo
- }
- if _time:
- serviceDict.update({"needTime": str(_time)})
- elif elec:
- serviceDict.update({"needElec": str(elec)})
- ServiceProgress.register_card_service(
- self.device,
- int(portStr),
- card,
- serviceDict
- )
- otherConf = self.device.get("otherConf", dict())
- refundProtection = otherConf.get("refundProtection", DEFAULT_PARAM.DEFAULT_REFUND_PROTECTION)
- refundProtectionTime = otherConf.get("refundProtectionTime", DEFAULT_PARAM.DEFAULT_REFUND_PROTECTION_TIME)
- devCache = Device.get_dev_control_cache(self.device.devNo)
- portCache = devCache.get(portStr, {})
- cacheDict = {
- "openId": card.openId,
- "status": Const.DEV_WORK_STATUS_WORKING,
- "coins": float(money),
- "consumeType": "card",
- "isStart": True,
- "startTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
- # TODO 刷卡消费到底是什么模式?
- "billingType": BILLING_TYPE.TIME,
- "refundProtection": refundProtection,
- "refundProtectionTime": refundProtectionTime,
- "cardNo": cardNo,
- "needTime": _time
- }
- portCache.update(cacheDict)
- Device.update_dev_control_cache(self.device.devNo, {portStr: portCache})
- pass
- def _do_settle_consume(self, card, money):
- """ 消费结算 """
- balance = self.event_data.get("balance")
- payMoney = RMB(money)
- if card.cardType == "ID":
- # 检查有没有没有充值的订单
- card_recharge_order = CardRechargeOrder.get_last_to_do_one(str(card.id))
- self.recharge_id_card(card = card, rechargeType = 'append', order = card_recharge_order)
- card.reload()
- if VirtualCoin(card.balance) < VirtualCoin(money):
- logger.error("<{}> card balance not enough, balance is <{}>".format(card.cardNo, str(card.balance)))
- return CARD_STATUS.STATUS_CONSUME_FAIL
- self.record_consume_for_card(card, payMoney)
- self.consume_money_for_card(card, payMoney)
- self.notify_balance_has_consume_for_card(card, payMoney)
- return CARD_STATUS.STATUS_CONSUME_SUCCESS
- # IC卡需要写入刷卡后的金额
- else:
- card.balance = RMB(balance)
- card.save()
- self.record_consume_for_card(card, payMoney)
- return CARD_STATUS.STATUS_CONSUME_SUCCESS
- def _do_settle_refund(self, card, money):
- """ 余额回收结算 """
- # 对于 返回费用的 上限判断
- refundMoney = RMB(money)
- # 没有开启退款的或者退款额度是0的直接失败
- if card.cardType == "ID" and refundMoney <= RMB(0) or not self.device.is_auto_refund:
- return CARD_STATUS.STATUS_REFUND_FAIL
- # 退费金额高于了最后一单的消费金额 就不让退费
- record = CardConsumeRecord.objects.filter(cardId = str(card.id)).order_by("-dateTimeAdded").first()
- if not record or record.money < refundMoney:
- logger.error("card <{}> has no consume record, refund money is <{}>".format(card.cardNo, refundMoney))
- return CARD_STATUS.STATUS_REFUND_FAIL
- logger.info("card <{}> last ont consumeRecord is <{}>, consumeMoney is <{}>".format(card.cardNo, record.orderNo,
- record.money))
- self.refund_money_for_card(refundMoney, str(card.id))
- self.notify_user(
- card.managerialOpenId,
- 'refund_coins',
- **{
- 'title': u'退币完成!您的卡号是%s,卡别名:%s' % (card.cardNo, card.nickName),
- 'backCount': u'金币:%s' % refundMoney,
- 'finishTime': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- })
- return CARD_STATUS.STATUS_REFUND_SUCCESS
- class ChargingBJICCardEvent(WorkEvent):
- def do(self):
- cardNo = self.event_data["cardNo"]
- money = self.event_data["money"]
- res = self.event_data.get("res")
- reason = self.event_data.get("reason")
- reasonCode = self.event_data.get("reasonCode")
- card = self.update_card_dealer_and_type(cardNo, "IC")
- # 在测试的时候发现,偶尔会出现IC卡卡号为 00000000 的情况
- if not card or not card.openId:
- return
- # 充值不成功的 后续是否要处理现金退费
- if res != "01" and reasonCode in ["03", "04"]:
- group = Group.get_group(self.device.groupId)
- self.notify_dealer(
- templateName = "device_fault",
- title = "注意!{}!".format(reason),
- device = u"{groupNumber}组-{logicalCode}".format(groupNumber = self.device["groupNumber"],
- logicalCode = self.device["logicalCode"]),
- location = u"{address}-{groupName}".format(address = group["address"],
- groupName = group["groupName"]),
- fault = u"客户离线卡充值失败,设备原因 <{}> 导致".format(reason),
- notifyTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- )
- return
- card.balance += RMB(money)
- logger.info("card remote recharge success, card <{}> balance is <{}>".format(card.cardNo, card.balance))
- card.save()
- class ChargingBJReportEvent(WorkEvent):
- def do(self):
- portInfo = self.event_data.get("portInfo", dict())
- devCache = Device.get_dev_control_cache(self.device.devNo)
- for portStr, info in portInfo.items():
- portCache = devCache.get(portStr, dict())
- # 端口状态如果是空闲的情况下, 将isStart信息清除掉
- if info.get("status", Const.DEV_WORK_STATUS_IDLE) == Const.DEV_WORK_STATUS_IDLE:
- portCache.pop("isStart", None)
- billingType = portCache.get("billingType", BILLING_TYPE.TIME)
- popType = BILLING_TYPE.ELEC if billingType == BILLING_TYPE.TIME else BILLING_TYPE.TIME
- info.pop("left".format(popType.capitalize()), None)
- portCache.update(info)
- Device.update_dev_control_cache(self.device.devNo, devCache)
|