| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- <?php
- namespace tw\async\activities;
- use app\admin\model\user\User;
- use app\admin\model\user\UserBill;
- use app\models\store\StoreOrder;
- use app\models\store\StoreOrderCartInfo;
- use app\models\system\SystemAwardHistory;
- use app\models\user\UserNotice;
- use crmeb\basic\BaseModel;
- use tw\redis\ActivityPool;
- /**
- * 活动基类
- *
- * 活动设计为:一个活动有一个商品分类,该分类下的商品为该活动商品
- */
- abstract class ActivityCalc {
- protected $ctx = [];
- public function __construct() {
- }
- /**
- * 根据分类 ID 来获取该分类下已被购买的单件商品,及其订单,用户信息
- * @param $cate_id
- * @return array
- */
- protected static function getOrders($cate_id) {
- $products = StoreOrderCartInfo::alias('ci')
- ->join('store_order o', 'o.id=ci.oid')
- ->join('store_product p', 'ci.product_id=p.id')
- ->join('user u', 'u.uid = o.uid')
- ->where('o.paid', 1)
- ->where('o.status', '>=', 0)
- ->where('o.refund_status', 0)
- ->where('o.is_del', 0)
- ->where('o.is_system_del', 0)
- ->where('p.cate_id', $cate_id)
- ->where('ci.activity', '')
- ->field('o.id, o.order_id, o.pay_price, ci.product_id, ci.cart_info, u.uid, u.now_money, u.pay_count, p.store_name')
- ->select();
- $products = $products ? $products->toArray() : [];
- return $products;
- }
- protected function getPoolKey()
- {
- $KEY = 'activity:pool:';
- return $KEY . $this->getName();
- }
-
- /**
- * 用统一的架构来计算输赢
- */
- public function calc() {
- // 活动分类ID
- $cate_id = $this->getId();
- $products = self::getOrders($cate_id);
- $pNum = count($products);
- $minNum = $this->getMinimalProductNum();
- if ($pNum < $minNum) {
- warnlog("not enough order. product num: $pNum, $minNum needed, stop activity:" . $this->getNameCN());
- SystemAwardHistory::create([
- 'activity' => $this->getName(),
- 'result' => $this->getResult(mt_rand(0, 10) < 5 ? true : false),
- 'ts' => time(),
- 'order_num' => $pNum,
- 'winner_num' => 0,
- 'total_paid' => 0,
- 'diff_paid' => 0,
- 'profit' => 0,
- 'rate' => $this->repRate(),
- ]);
- return;
- }
- // 按商品处理订单
- $counter = 0; // 订单中商品计数器
- $left = []; // 左侧商品
- $right = []; // 右侧商品
- $left_spent = 0; // 左侧总价
- $right_spent = 0; // 右侧总价
- $left_cost = 0; // 左侧总成本
- $right_cost = 0; // 右侧总成本
- // 打乱商品顺序
- shuffle($products);
- shuffle($products);
- // 标记有多件商品的订单, 再次统计到属于某个订单的商品时, 订单会重复
- $orders = [];
- foreach($products as &$p) {
- $counter += 1;
- $ci = json_decode($p['cart_info'], true);
- $num = $ci['cart_num']; // 购买个数
- $productInfo = isset($ci['productInfo']) ? $ci['productInfo'] : [];
- $attrInfo = isset($productInfo['attrInfo']) ? $productInfo['attrInfo'] : [];
- // 价格, 成本
- $price = isset($attrInfo['price']) ? $attrInfo['price'] : 0.0;
- $cost = isset($attrInfo['cost']) ? $attrInfo['cost'] : 0.0;
- // 付款总额, 总成本
- $ppaid = bcmul($num, $price, 2);// product paid
- $pcost = bcmul($num, $cost, 2);
- // orderId
- $oid = $p['id'];
- // 标记付款和成本
- $p['paid'] = $ppaid;
- $p['cost'] = $pcost;
- $p['image'] = isset($attrInfo['image']) ? $attrInfo['image'] : '';
- // 处理一个订单多件商品的情况
- if (!isset($orders[$oid])) {
- $orders[$oid] = 1;
- } else {
- $orders[$oid] += 1;
- }
- // if ($ppaid <= 1.0) {
- // Log::warning("impossible price, order: ". $p['id']);
- // continue;
- // }
- // if ($pcost <= 0.1) {
- // Log::warning("impossible cost, order: ". $p['id']);
- // continue;
- // }
- // 按一个规则, 把商品分为两份, 其中一份中奖
- if ($this->leaningJudge($counter, $p, $attrInfo)) {
- $left[] = $p;
- $left_spent = bcadd($left_spent, $ppaid, 2);
- $left_cost = bcadd($left_cost, $pcost, 2);
- } else {
- $right[] = $p;
- $right_spent = bcadd($right_spent, $ppaid, 2);
- $right_cost = bcadd($right_cost, $pcost, 2);
- }
- } // foreach
-
- // 利润
- $left_profit = bcsub($left_spent, $left_cost, 2);
- $right_profit = bcsub($right_spent, $right_cost, 2);
- // 定输赢
- $diff_money = abs(bcsub($left_spent, $right_spent, 2));
- // if ($diff_money < 0.0) {
- // $diff_money = -$diff_money;
- // }
- // 假定左边胜利
- $profit = $right_profit;
- $winners = $left;
- $losers = $right;
- // 左边利润高则右边胜利
- if ($left_profit > $right_profit) {
- $winners = $right;
- $losers = $left;
- $profit = $left_profit;
- }
- // 如果只有 1 件商品
- if ($pNum == 1) {
- $KEY = 'activity:pool';
- // 此时,订单必为 loser
- // 47% 概率胜
- $itWin = false; // 这单胜了 ?
- $dice = mt_rand(0, 100);
- if ( $dice < 47) {
- $itWin = true;
- }
- $poolBalance = floatval((new ActivityPool)->get('', $this->getName()));// pool 中金额
- if (!$itWin) {
- // 如果为新用户,且池子有钱,让他胜
- if ($poolBalance > $profit && $products[0]['pay_count'] <= 1) {
- $itWin = true;
- }
- }
-
- if ($itWin) {
- $profit = -$profit;
- list($winners, $losers) = [$losers, $winners]; // swap
- }
- (new ActivityPool)->hincr_by_float('', $this->getName(), floatval($profit));
- infolog($this->getName() . ": single order[product]. dice:$dice, remain:$poolBalance, pay_count:" . $products[0]['pay_count']);
- }
- $result = $this->getResult($winners == $left);
- warnlog('products:' . json_encode($products)
- . ' left:' . json_encode($left) . ' left_spent:' . $left_spent
- . ' left_cost:' . $left_cost . ' left_profit:' . $left_profit
- . ' right' . json_encode($right) . ' right_spent:' . $right_spent
- . ' right_cost:' . $right_cost . ' right_profit:' . $right_profit
- . ' diff_money:' . $diff_money . ' profit:' . $profit . ' result:' . $result);
- // 保存开奖结果
- SystemAwardHistory::create([
- 'activity' => $this->getName(),
- 'result' => $result,
- 'ts' => time(),
- 'order_num' => count($left) + count($right),
- 'winner_num' => count($winners),
- 'total_paid' => $left_spent + $right_spent,
- 'diff_paid' => $diff_money,
- 'profit' => $profit,
- 'rate' => $this->repRate(),
- ]);
- // 结果处置 胜方退款,败方发货
- foreach($winners as $p) {
- $single = $orders[$p['id']] == 1;
- $this->execute($p, $single, true);
- }
- foreach($losers as $p) {
- $single = $orders[$p['id']] == 1;
- $this->execute($p, $single, false);
- }
- warnlog("activity" . $this->getNameCN() . " calc finished. result: $result");
- }
- protected function execute($product, $single=true, $win=true) {
- BaseModel::beginTrans();
- $result = 0;
- $reparation = 0.0;
- if ($win) {
- $result = 1;
- $reparation = bcmul(bcsub($product['paid'], $product['cost'], 2), $this->repRate(), 2);
- }
- if ($reparation < 0.0) {
- warnlog("reparation = $reparation, orderId=" . $product['id']);
- $reparation = 0.0;
- }
- // 标记商品为已参与活动
- $r1 = StoreOrderCartInfo::where('oid', $product['id'])
- ->where('product_id', $product['product_id'])
- ->update([
- 'activity' => $this->getName(),
- 'result' => $result,
- 'reparation' => $reparation,
- ]);
- if (!$win) {
- BaseModel::checkTrans($r1);
- return;
- }
- $refund_price = bcadd($product['paid'], $reparation, 2);
- // 订单全部商品都中奖的退单
- $r2 = true;
- if ($single) {
- $r2 = StoreOrder::where('id', $product['id'])->update([
- 'refund_price' => $refund_price,
- 'refund_status' => 2,
- ]);
- }
- // 中奖的 退款并赔款返回到佣金
- $r3 = UserBill::income('活动' . $this->getNameCN(), $product['uid'], 'now_money', 'brokerage',
- $refund_price, $product['id'], bcadd($product['now_money'], $refund_price, 2),
- '订单退款' . $product['paid'] . '+' . $reparation . '元');
- $r4 = User::bcInc($product['uid'], 'brokerage_price', $refund_price, 'uid');
- $ok = $r1 && $r2 && $r3 && $r4;
- BaseModel::checkTrans($ok);
- if ($ok) {
- $pname = $product['store_name'];
- $order_id = $product['order_id'];
- $paid = $product['paid'];
- // 中奖用户发送中奖消息.
- UserNotice::sendNoticeTo($product['uid'], '您的订单已处理 点击查看',
- "您好,由于活动商品库存有限,您的订单 $pname(订单号 $order_id) 未能成功发货。 该商品付款 $paid 元已如数退还,并赔付您 $reparation 元作为补偿,请在”我的佣金”中核实。对您造成的不便我们深感抱歉。感谢您对我们的信任和喜爱,美天旺祝您生活愉快。", $product['image']);
- }
- }
- // 活动所属的 $categoryId
- abstract protected function getId();
- // 活动英文名, 长度 <8
- abstract protected function getName();
- // 活动中文名
- abstract protected function getNameCN();
- // 赔款所占商品利润的百分比
- abstract protected function repRate();
- // 满足开奖的最小订单数
- abstract protected function getMinimalProductNum();
- // 获得开奖结果 int
- abstract protected function getResult($leftwin=true);
- /**
- * 根据此函数返回值来划分 getOrders() 返回的订单为两份, 其中一份中奖, 当本函数返回 true, $product 加入"左边"
- *
- * @param $index: $product 所在序号, 从 1 开始
- * @param $product: getOrders() 返回列表中的单个元素
- * @param $attr: 商品属性
- * @return boolean true 加入左边
- */
- abstract protected function leaningJudge($index, $product, $attr);
- }
|