Selaa lähdekoodia

add leader board robots

joe 3 vuotta sitten
vanhempi
commit
da023376a6

+ 17 - 5
app/api/controller/board/UserBoardController.php

@@ -5,6 +5,8 @@ namespace app\api\controller\board;
 use crmeb\services\UtilService;
 use app\models\board\UserBoard;
 use \think\facade\Config;
+use tw\command\console;
+use tw\lib\robot\Robots;
 use tw\redis\BoardRds;
 
 /**
@@ -48,14 +50,14 @@ class UserBoardController
     public function boards()
     {
         [$type,] = UtilService::getMore([
-            ['type', 1],
+            ['type', BoardRds::DAILY],
         ], null, true);
         $board = json_decode((new BoardRds)->get($type), true);
         return app('json')->successful('ok', $board);
     }
 
     /**
-     * 读库
+     * 讀取数据库中每日榜单的今日榜单
      */
     protected function daily_win_money()
     {
@@ -66,11 +68,11 @@ class UserBoardController
             $row['border'] = 0;
             $row['vip'] = 0;
         }
-        return array(
+        return [
             'banner' => Config::get('app.leader_board_banner'),
             'name' => "日榜",
             'board' => $res,
-        );
+        ];
     }
 
     /**
@@ -78,7 +80,17 @@ class UserBoardController
      */
     public function cache_board()
     {
-        $res = (new BoardRds)->set(BoardRds::DAILY, json_encode($this->daily_win_money()));
+        $real_board = $this->daily_win_money();
+        $robot_board = Robots::first_n_by_value(30);
+
+        $board = array_merge($robot_board, $real_board['board']);
+        usort($board, function ($l, $r) {
+            return $r['value'] <=> $l['value'];
+        });
+        
+        $real_board['board'] = $board;
+
+        $res = (new BoardRds)->set(BoardRds::DAILY, json_encode($real_board));
         if (!$res) {
             errlog("cache_board() returned $res");
         }

+ 33 - 0
app/common.php

@@ -98,6 +98,39 @@ function infolog($log)
     return Log::info($log);
 }
 
+/**
+ * 简单封装 mt_rand($min, $max)
+ * 
+ * WHY
+ * 因为 mt_rand 参数使用全閉区间,这种行为对我来说不自然,
+ * 
+ * tw_rand 使用左閉右開的区间
+ */
+function tw_rand(int $min = 0, int $max = 10)
+{
+    return mt_rand($min, $max - 1);
+}
+
+function tw_addf($l, $r, int $scale = 0)
+{
+    return floatval(bcadd($l, $r, $scale));
+}
+
+function tw_subf($l, $r, int $scale = 0)
+{
+    return floatval(bcsub($l, $r, $scale));
+}
+
+function tw_mulf($l, $r, int $scale = 0)
+{
+    return floatval(bcmul($l, $r, $scale));
+}
+
+function tw_divf($l, $r, int $scale = 0)
+{
+    return floatval(bcdiv($l, $r, $scale));
+}
+
 /**
  * 获取所有 幸运2021 活动及其子活动 ID 列表
  */

+ 1 - 0
tests/UserTaskTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace tests;
+
 require __DIR__ . '/../vendor/autoload.php';
 
 use Monolog\Logger;

+ 34 - 6
tw/command/Maintain.php

@@ -6,6 +6,7 @@ use app\admin\model\store\StoreProduct;
 use app\admin\model\store\StoreProductAttr;
 use app\admin\model\store\StoreProductAttrResult;
 use app\admin\model\store\StoreProductAttrValue;
+use app\api\controller\board\UserBoardController;
 use app\models\store\StoreProductRule;
 use think\console\Command;
 use think\console\Input;
@@ -13,6 +14,7 @@ use think\console\input\Argument;
 use think\console\Output;
 use \think\facade\Config;
 use MeiliSearch\Client;
+use tw\lib\robot\Robots;
 
 /**
  * 维护命令,使用 nginx 帐号运行
@@ -29,7 +31,7 @@ class Maintain extends Command
     protected function configure()
     {
         $this->setName('maintain')
-            ->addArgument('category', Argument::REQUIRED, 'act|prod|trash|reindex|test_reindex|none')
+            ->addArgument('category', Argument::REQUIRED, 'act|prod|trash|reindex|robot_order|order_reset|none')
             ->setDescription('maintain some application data.');
     }
 
@@ -288,7 +290,6 @@ class Maintain extends Command
      */
     protected function trash()
     {
-        echo getRate(5);
     }
 
     /**
@@ -309,6 +310,31 @@ class Maintain extends Command
         warnlog('backup database:' . json_encode($output));
     }
 
+    /**
+     * 创建机器人
+     */
+    protected function robot_init()
+    {
+        return Robots::to_redis();
+    }
+
+    /**
+     * 机器人定时下单
+     */
+    protected function robot_order()
+    {
+        Robots::order();
+        (new UserBoardController)->cache_board();
+    }
+
+    /**
+     * 每日重置机器人日榜数据
+     */
+    protected function robot_reset()
+    {
+        Robots::to_redis();
+    }
+
     /**
      * 重新索引商品
      * 
@@ -390,11 +416,13 @@ class Maintain extends Command
     {
     }
 
-    protected function test_reindex()
-    {
-    }
-
     protected function none()
     {
+        // Robots::build();
+        // print_r(Robots::$robots);
+        // Robots::to_redis();
+        // Robots::order();
+        $robots = Robots::first_n_by_value(10);
+        console::log($robots);
     }
 }

+ 1 - 1
tw/command/console.php

@@ -3,7 +3,7 @@
 namespace tw\command;
 
 /**
- * 向浏览器那样
+ * 
  * 
  * print to console.
  */

+ 104 - 0
tw/lib/robot/BoardBot.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace tw\lib\robot;
+
+use app\admin\model\store\StoreProduct;
+use think\facade\Log;
+
+
+/**
+ * ## 需求
+ * 前端排行榜功能,爲避免出現無人上榜或榜單比較蕭條的情況,需要定制一批機器人上班
+ * 
+ * ## 排行榜機制
+ * 定時執行:對表執行訂單統計排序查詢,結果就作爲榜單數據,存入 redis。前端 API 直接從 redis 取數據。
+ * 
+ * ## 機器人機制
+ * 機器人個人信息不入庫。按自己的定時器 定時隨機指定部分機器人“下單“並大概率盈利(所謂下單並不入庫)。
+ * 根據下單情況,所有機器人維護一個單獨的”內部排行榜“。
+ * 
+ * 隨機性:如果榜單需要 20 名,機器人則最少需要 20 x 3 = 60 名,才不會在榜單上全是老面孔,同時又有老面孔。
+ * 訂單金額根據實際商品金額生成。
+ * 
+ * ## 最終排行榜
+ * 定時執行訂單統計時,同時取出機器人排行榜,混合後作爲最終結果 存入 redis。前端 API 不受影響。
+ */
+class BoardBot
+{
+    protected $win_rate = 80; // 盈利概率
+
+    /**
+     * 機器人下單,需要定時執行
+     * 
+     * 結果存入機器人自己的排行榜
+     */
+    public function make_orders()
+    {
+        // make choice which activities randomly
+        $c = config('activity.clearance_cate_id');
+        $l = config('activity.lucky_cate_id');
+        $a = config('activity.lucky_a_cate_id');
+        $b = config('activity.lucky_b_cate_id');
+
+        $acts = [
+            $c, $l, $a, $b,
+        ];
+
+        $rates = [
+            $c => config('activity.clearrance_rate'),
+            $l => config('activity.luck_rate'),
+            $a => config('activity.luck_a_rate'),
+            $b => config('activity.luck_b_rate'),
+        ];
+        // get activities products' prices.
+        $act_idx = tw_rand(0, count($acts));
+        echo "act_idx = $act_idx" . PHP_EOL;
+        $act = $acts[$act_idx];
+        $rate = $rates[$act];
+
+        // all prices
+        $prices = StoreProduct::where('is_del', 0)
+            ->where('is_show', 1)
+            ->where('cate_id', $act)
+            ->field('price,cost')
+            ->select()
+            ->toArray();
+
+        // make choice whome participates this order- time.
+        $robot_ids = array_keys($this->robots);
+        $selected = [];
+        $total = count($robot_ids);
+        if ($total < 10) {
+            $msg = "robots num is not enough. num=$total";
+            Log::error($msg);
+            echo $msg . PHP_EOL;
+            return;
+        }
+
+        $selnum = tw_rand($total / 3, $total - 5);
+
+        for ($i = 0; $i < $selnum; $i++) {
+            $idx = tw_rand(0, count($robot_ids));
+            $selected[] = $robot_ids[$idx];
+
+            // each win by rate.
+            $price = tw_rand(0, count($prices));
+            $win = tw_rand(0, 100) < 80;
+            if ($win) {
+                $selected[$robot_ids[$idx]] = $price * $rate;
+            }
+
+            array_splice($robot_ids, $idx, 1);
+        }
+    }
+
+    /**
+     * 獲取機器人的榜單
+     * 
+     * @first: select first-N as the on-board
+     * 
+     */
+    public function get_board($first = 16)
+    {
+    }
+}

+ 251 - 0
tw/lib/robot/Robots.php

@@ -0,0 +1,251 @@
+<?php
+
+namespace tw\lib\robot;
+
+use think\facade\Log;
+use tw\command\console;
+use tw\redis\RobotsInfoRds;
+
+/**
+ * 手动指定 robot 用户资料
+ * 
+ * robot 用户昵称头像要求固定,level, vip 可以不定期升级
+ * 
+ */
+class Robots
+{
+
+    // 昵称
+    static public $nicknames = [
+        "萌傻卿",
+        "勇敢的小萝卜",
+        "且听风铃",
+        "我去叫彭于晏",
+        "晚点相遇",
+        "トキメク",
+        "Coisini",
+
+        "给你一口甜",
+        "是小宝阿",
+        "星河几重",
+        "养了一个月亮",
+        "白日梦我",
+        "CD女王",
+        "百事可爱",
+
+        "一觉睡到小时候",
+        "Pumpkin",
+        "咋又饿了呢",
+        "Kilig",
+        "幸识",
+        "433",
+        "搬砖小土妞",
+
+        "初遇",
+        "睡了",
+        "可口可爱",
+        "咕噜叽叽",
+        "whaoe",
+        "超级消烦员",
+        "萝莉啰嗦",
+
+        "骑着蜗牛追导弹",
+        "月亮邮递员",
+        "小公举",
+        "智慧女孩不要秃头",
+        "清蒸肉肉",
+        "联合国认证小可爱",
+        "给糖就不闹",
+
+        "激萌美少女李逵",
+        "文件转输助手",
+        "沙雕少女",
+        "来了老弟",
+        "我妈妈告诉我要欺负你们",
+        "看瓜少年和猹",
+        "超级凶傲呜",
+
+        "拉风的鼻涕",
+        "作业不多吗",
+        "我是你爸爸",
+        "很秀但没必要",
+        "有关部门",
+        "二三",
+        "孙尚香",
+
+        "芝士就是力量",
+        "颈上鲜草莓",
+        "你家小祖宗",
+        "蟑螂恶霸",
+        "胖大海",
+        "蔡文姬腿堡",
+        "美少女壮士",
+
+        "萝卜蹲",
+        "祖国老花朵",
+        "脑子里有个泡",
+        "萌够就回家",
+        "怪力少女",
+        "new bee",
+        "你爸爸我",
+
+        "铁锤妹妹",
+    ];  // 64 N
+
+    // 头像 base url
+    static protected $HEADER_BASE_URL = 'http://twongstatic.shotshock.shop/headers/wechat/';
+    /**
+     * 头像,七牛保存,名称按 1-64 的 png 图片,所以可以生成 url 
+     */
+    static public $headers = [];
+
+    // robot 起始 ID
+    static protected $ROBOT_BASE_ID = 44040001;
+
+    /**
+     *  "uid" => 123,
+     *  "avatar" => "http://2.png",
+     *  "nickname" => "xxx",
+     *  "level" => 1,
+     *  "value" => 23.3,
+     *  "border" => 1,
+     *  "vip" => 2,
+     */
+    static public $robots = []; // N x 3 機器人
+
+    /**
+     * 构建机器人信息。
+     * 
+     * 可重现:多次构建结果应一致
+     * 
+     * TODO:结果存入 redis
+     * 
+     * @return boolean
+     */
+    public static function build(): bool
+    {
+        self::_build_headers();
+
+        if (
+            count(self::$nicknames) != count(self::$headers) ||
+            count(self::$nicknames) != 64
+        ) {
+            Log::error('nickname or headers has wrong numbers');
+            return false;
+        }
+
+        // clear
+        self::$robots = [];
+
+        for ($i = 0; $i < 64; $i++) {
+            $uid = self::$ROBOT_BASE_ID + $i;
+            self::$robots[$uid] = [
+                'uid' => $uid,
+                'avatar' => self::$headers[$i],
+                'nickname' => self::$nicknames[$i],
+                'level' => tw_rand(1, 4),
+                'value' => 0,
+                'border' => 0,
+                'vip' => 0,
+            ];
+        } // for
+
+        return true;
+    }
+
+    /**
+     * 如果没有操作七牛中文件,不要修改本函数。
+     * 
+     * 本函数不是用于生成,而是对已有资源的描述/信息提取。
+     */
+    protected static function _build_headers()
+    {
+        // clear
+        self::$headers = [];
+
+        for ($i = 1; $i <= 64; $i++) {
+            self::$headers[] = self::$HEADER_BASE_URL . $i . '.png';
+        }
+    }
+
+    /**
+     * 把成員 robots 寫入 redis
+     */
+    public static function to_redis()
+    {
+        if (count(self::$robots) <= 0) {
+            self::build();
+        }
+
+        foreach (self::$robots as $robot) {
+            (new RobotsInfoRds)->set(false, $robot['uid'], $robot);
+        }
+    }
+
+    /**
+     * 加载 redis 数据到 self::$robots
+     */
+    public static function from_redis()
+    {
+        $robots = (new RobotsInfoRds)->getAll(false);
+        foreach ($robots as $k => $v) {
+            $robots[$k] = json_decode($v, true);
+        }
+        self::$robots = $robots;
+    }
+
+    /**
+     * 下單,隨機選擇若幹機器人,模擬下單(增加 value 值)
+     * 
+     * 排行時,簡單取出所有機器人數據,內存排序即可
+     */
+    public static function order()
+    {
+        if (!self::$robots) {
+            self::from_redis();
+        }
+        // 下单人数
+        $robot_num = tw_rand(0, count(self::$robots) / 2);
+
+        for ($i = 0; $i < $robot_num; $i++) {
+            // 下单人UID
+            $index = tw_rand(0, count(self::$robots));
+            $uid = self::$ROBOT_BASE_ID + $index;
+            // 获利金额
+            $value = tw_divf(tw_rand(500, 5000), 100.0, 2);
+
+            // get - update - set
+            $srobot = (new RobotsInfoRds)->get(false, $uid);
+            if (!$srobot) {
+                console::log('not found: uid=' . $uid);
+            }
+            $robot = json_decode($srobot, true);
+            $robot['value'] = tw_addf($robot['value'], $value, 2);
+
+            (new RobotsInfoRds)->set(false, $uid, $robot);
+        }
+    }
+
+    /**
+     * 重置所有機器人 value 字段
+     */
+    public static function reset()
+    {
+        return self::build();
+    }
+
+    /**
+     * 根據 value 字段的前 n 個
+     */
+    public static function first_n_by_value(int $n): array
+    {
+        $robots = (new RobotsInfoRds)->getAll(false);
+        foreach ($robots as $uid => $robot) {
+            $robots[$uid] = json_decode($robot, true);
+        }
+        usort($robots, function ($l, $r) {
+            return $r['value'] <=> $l['value'];
+        });
+        return array_slice($robots, 0, $n);
+    }
+}

+ 21 - 0
tw/redis/RobotBoardRds.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace tw\redis;
+
+use tw\redis\traits\SortedSetTrait;
+use tw\redis\traits\KeyTrait;
+
+/**
+ * 纯機器人排行榜
+ * 
+ */
+class RobotBoardRds extends Base
+{
+    use SortedSetTrait;
+    use KeyTrait;
+
+    protected function key($pid = false): string
+    {
+        return 'robot:board';
+    }
+}

+ 21 - 0
tw/redis/RobotsInfoRds.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace tw\redis;
+
+use tw\redis\traits\HashTrait;
+use tw\redis\traits\KeyTrait;
+
+/**
+ * 机器人基本信息
+ * 
+ */
+class RobotsInfoRds extends Base
+{
+    use HashTrait;
+    use KeyTrait;
+
+    protected function key($pid = false): string
+    {
+        return 'robots:info';
+    }
+}

+ 3 - 0
tw/redis/traits/HashTrait.php

@@ -32,6 +32,9 @@ trait HashTrait
      */
     public function set($word, string $attr, $value): bool
     {
+        if (is_array($value)) {
+            $value = json_encode($value);
+        }
         return TwRedis::hSet($this->key($word), $attr, $value);
     }
 

+ 1 - 1
vendor/phpunit/phpunit/.phpunit.result.cache

@@ -1 +1 @@
-{"version":1,"defects":{"tests\\UserTaskTest::test_generate_user_poster":5},"times":{"tests\\UserTaskTest::test_generate_user_poster":0.04}}
+{"version":1,"defects":{"tests\\UserTaskTest::test_generate_user_poster":5,"tests\\RedisTest::test_zset":5},"times":{"tests\\UserTaskTest::test_generate_user_poster":0.015,"tests\\RedisTest::test_zset":0.002}}