如何在掘金评论区抽奖

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

缘由

起源来自一次很无聊的掘金产品功能确认,临时起意说要评论区抽奖,像极了我们的产品经理。 详见 新系列文章内容有奖征集

然后,我想一周后随便写一个随机数,就能得出,谁得奖了,毕竟奖品只有一个,参与人数也没超过10个,猜拳选,都花不了几个时间。

掘金酱抽奖,好像是用的一个在线抽奖软件。

不然我自己也搞一个玩玩咯。

用户侧使用

如果你并不关心如何实现,你只想用这个工具,你可以在你的命令行中键入 npx jjcj

按提示输入或者选择即可选出中奖者。

info  - 欢迎试用掘金评论区抽奖系统,现在开始抽奖...
✔ 请输入关联文章? … 7127209370013663245
event - 获取掘金文章评论数据:0~50
event - 预计进度:10/10
info  - 本次抽奖的关联文章是《7127209370013663245》 共有 10 条评论
✔ 是否排除重复评论数,即每个朋友仅有一次参与机会,多次评论无效? … 否 / 是
event - 过滤重复的评论数,删除同一个朋友发布的多条评论...
info  - 抽奖评论数剩余 9 条
✔ 请输入本次抽奖设置档次,如仅需抽出 1 人,请输入 1
如有设置1等奖,2等奖等,请用英文字符逗号隔开
如 1,3,5 表示抽取 1档1人,2档3人,3档5人 … 1
ready - 正在抽奖中,请稍后...
info  - 获得 1 档的人是:
红尘炼心 - https://juejin.cn/user/254742429175352
复制代码

Untitled.gif

image.png

初始思路

获取抽奖时刻的时间戳,对 1000 取商,得到当前秒数的时间戳,然后对参与人数取余,就得出了获奖选手。

扫描二维码关注公众号,回复: 14458365 查看本文章
const key = parseInt(new Date().getTime() / 1000,10) % data.length;

const c = data[key];
复制代码

好处就是不用动脑子,只要公布了抽奖时间,每个人都能得到一样的结果。比如上次文章发布时间是 2022年08月02日 17:32:39

image.png

一周后开奖,也是就是开奖时间是 2022年08月08日 17:32:39,套上公式,可以得到。

const key = parseInt(new Date('2022-08-09T09:32:39.000Z').getTime() / 1000, 10) % 8;

// 7
复制代码

数一下评论区,去重之后,数一下就能得到本次获奖的朋友。

缺陷

说了上面的方法,最大的好处就是不用动脑子,但是就是不用动脑子,所以稍微动下脑子,就能看出一些问题。比如对参与人数求余,余数其实是从 0 开始计数的。另外这个唯一值就只能选出一个,如果说想选出三个,那就没法用了。

抽奖

抽奖函数

抽奖函数其实很简单,核心就是 m 选 n,比如10个人选2个,100个人选3个。

const luckDraw = (length: number, num: number) => {
  const lucks: number[] = [];
  while (num) {
    const luck = Math.floor(Math.random() * length);
    if (!lucks.includes(luck)) {
      lucks.push(luck);
      num--;
    }
  }
  return lucks;
};
luckDraw(10,2); // [2,4]
luckDraw(100,3);// [66,43,71]
复制代码

小技巧,不少朋友在写类似的方法的时候,会先获取全部的数据,然后再从数据里面去抽取。但是,这里仅仅使用数据的角标抽奖,等选出中奖角标,再去匹配数据,取得中奖者的数据。通过小数据的操作,会让内存的使用降到最低。

多档奖项

多档奖项设置也是比较常见的抽奖需求,比如需要抽出 1 等奖 1 名,二等奖 3 名,三等奖 5 名等。

这里使用比较简单的用户交互,让用户输入想要抽取的奖项数,直接用英文字符的逗号连接即可。

如上述需求,可输入 1,3,5 完成。

修改抽奖函数,将第二参数设置成字符串。使用逗号连接最常见的问题就是中英文符号错误,且每个被连接的应该为一个数字,因此我们直接使用 try catch 包裹,当解析错误的时候,告知用户。主要会出现错误的是 parseInt

const luckDraw = (length: number, num: string, repeat: boolean = false) => {
  try {
    const numList = `${num}`.split(",");
    const luckList: number[][] = [];
    numList.forEach((item) => {
      let n = parseInt(item, 10);
      const lucks: number[] = [];
      while (n) {
        const luck = Math.floor(Math.random() * length);
        if (!lucks.includes(luck) && checkRepeat(repeat, luck)) {
          lucks.push(luck);
          n--;
        }
      }
      luckList.push(lucks);
    });
    return luckList;
  } catch (error) {
    logger.error("奖项设置错误,请重试!");
    process.exit();
  }
};
复制代码

这么处理将获得了一个新的 luckList,是一个二维的字符数组。

luckDraw(10,'2,1', false); // [[2,4],[6]]
复制代码

处理异常数据

比较常见的异常是奖项设置错误,这里需要分允许重复中奖和不允许重复中奖两种情况。比如总的评论数只有 3 条,却要抽 10 个人。其实可以认定为 3 个人都中奖了,没必要抽,但是还是需要将中奖名单打印出来。

修改抽奖函数,注意奖项总数大于参与人数且不允许重复中奖的时候,会中断程序,因为这是一个预知的错误。而同一档的奖项大于参与人数,表示所有人都中奖了,后续依旧需要打印中奖者名单,所以不会中断程序。

const luckDraw = (length: number, num: string, repeat: boolean = false) => {
  try {
    const numList = `${num}`.split(",");
    let sum = 0;
    const luckList: number[][] = [];
    numList.forEach((item) => {
      let n = parseInt(item, 10);
      sum = sum + n;
      const lucks: number[] = [];
      if (!repeat && sum > length) {
        logger.error(
          "奖项设置错误: 当前设置奖项总数大于参与人数,请允许重复中奖!"
        );
        process.exit();
      }
      if (n > length) {
        logger.warn(
          "奖项设置错误: 当前设置奖项总数大于参与人数,所有人都中奖了!"
        );
        n = length - 1;
        while (n) {
          lucks.push(n);
          n--;
        }
        // 从 0 计数
        lucks.push(n);
      }
      while (n) {
        const luck = Math.floor(Math.random() * length);
        if (!lucks.includes(luck) && checkRepeat(repeat, luck)) {
          lucks.push(luck);
          n--;
        }
      }
      luckList.push(lucks);
    });
    return luckList;
  } catch (error) {
    logger.error("奖项设置错误,请重试!");
    process.exit();
  }
};
复制代码

数据

数据通过掘金评论接口获取,这里需要注意的是请求间隔和每次允许请求的数量。评论接口的 limit 最大有效值为 50。这意味着我们要分页请求数据。

请求方法就不写了,很简单的 fetch 请求,处理好 cursorhas_more 就可以了。

这里分享一下等待函数。

const sleep = (t: number) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(t);
    }, t);
  });
};
// 先睡 1 秒
await sleep(1000);
复制代码

拿一个评论数比较多的文章做测试:

event - 获取掘金文章评论数据:0~50
event - 预计进度:50/1505
// ...
event - 获取掘金文章评论数据:1500~1550
event - 预计进度:1505/1505
info  - 本次抽奖的关联文章是《7123120819437322247》 共有 1505 条评论
复制代码

制定抽奖流程

有了数据和核心的抽奖函数,接下来的事情就变得很简单了。只要建立一个 cli 工具,将数据和抽奖函数放进去就可以完成了。

工具

1、 logger 日志使用 umijs/utils 中的 logger,好处就是方便,而且会生成对应的日志文件。

2、 chalk 控制台加点颜色,看起来更好看

3、 prompts 是一个控制台的交互系统,用于完善抽奖流程的交互行为,比如接受用户输入,让用户选择 是/否 等。

抽奖流程

简单的制定一下抽奖流程

1、欢迎试用掘金评论区抽奖系统,现在开始抽奖...

logger.info("欢迎试用掘金评论区抽奖系统,现在开始抽奖...");
复制代码

2、请输入关联文章?让用户输入关联的文章 id

const { itemId } = await prompts({
    type: "text",
    name: "itemId",
    message: "请输入关联文章?",
  });
复制代码

3、自动获取文章评论数据

  let data: any = [];
  try {
    if (!itemId) {
      throw new Error("请输入关联文章。");
    }
    const agent = new https.Agent({
      rejectUnauthorized: false,
    });
    let cursor = "0";
    let has_more = true;
    while (has_more) {
      const body = {
        item_id: itemId,
        // item_id: "7127209370013663245",
        item_type: 2,
        cursor,
        // 掘金接口一次最多获取 50 条评论
        limit: 50,
        sort: 0,
        client_type: 2608,
      };

      logger.event(
        `获取掘金文章评论数据:${cursor}~${parseInt(cursor, 10) + 50}`
      );

      const response = await fetch(
        "https://api.juejin.cn/interact_api/v1/comment/list",
        {
          method: "post",
          body: JSON.stringify(body),
          agent,
          headers: { "Content-Type": "application/json" },
        }
      );
      const res = await response.json();

      if (res.err_no !== 0) {
        has_more = false;
        logger.error(res.err_msg);
      } else {
        data = data.concat(res.data);
        cursor = res.cursor;
        has_more = res.has_more;
      }

      logger.event(`预计进度:${cursor}/${res.count}`);

      // 睡一会儿
      await sleep(1000);
    }
  } catch (error) {
    // eslint-disable-next-line no-console
    logger.error("获取 juejin 文章数据出错:", error);
    process.exit();
  }
  if (data.length === 0) {
    logger.error("获取 juejin 文章数据出错或评论数为 0");
    process.exit();
  }
  logger.info(`本次抽奖的关联文章是《${itemId}》 共有 ${data.length} 条评论`);

复制代码

4、是否排除重复评论数,即每个朋友仅有一次参与机会,多次评论无效

const { filter } = await prompts({
    type: "toggle",
    name: "filter",
    message: "是否排除重复评论数,即每个朋友仅有一次参与机会,多次评论无效?",
    initial: true,
    active: "是",
    inactive: "否",
  });
  if (filter) {
    // 过滤重复的评论数
    data = filterArray(data);
    logger.event("过滤重复的评论数,删除同一个朋友发布的多条评论...");
    logger.info(`抽奖评论数剩余 ${data.length} 条`);
  }
复制代码

5、请输入本次抽奖设置档次?让用户输入抽奖的奖项设置,规则前面提到过,如 1,3,5

 const { grade = "" } = await prompts({
    type: "text",
    name: "grade",
    message:
      "请输入本次抽奖设置档次,如仅需抽出 1 人,请输入 1\n如有设置1等奖,2等奖等,请用英文字符逗号隔开\n如 1,3,5 表示抽取 1档1人,2档3人,3档5人",
  });
复制代码

6、如果用户设置了多档奖项,则需要指定是否允许重复中奖多个档位奖项

  let isRepeat = false;
  if (grade.includes(",")) {
    const { repeat } = await prompts({
      type: "toggle",
      name: "repeat",
      message:
        "是否允许重复中奖多个档位奖项,如中过1等奖的人是否允许继续中2等奖?",
      initial: false,
      active: "是",
      inactive: "否",
    });
    isRepeat = repeat;
  }
复制代码

7、调用抽奖函数选出中奖者

  logger.ready("正在抽奖中,请稍后...");
  const luskList = luckDraw(data.length, grade, isRepeat);
复制代码

8、 打印中奖人名单

  luskList.forEach((luck, i) => {
    logger.info(`获得 ${i + 1} 档的人是:`);
    luck.forEach((item: any) => {
      const user = data[item];
      console.log(
        chalk.redBright(user?.user_info?.user_name),
        "-",
        chalk.blue(`https://juejin.cn/user/${user?.user_info.user_id}`)
      );
    });
  });
复制代码

封装 CLI

这个内容在之前的手写框架系列中,有提到过,这里再简单的说明一下。

1、先查一下可用报名,比如掘金抽奖 - 直接用首字母 jjcj

访问一下 https://www.npmjs.com/package/jjcj 如果页面显示 404 则表示大概率这个报名可用。

2、新建一个空文件夹 jjcj

执行 npm init -y,生成 package.json 文件。

3、安装使用到的模块

pnpm i @umijs/utils [email protected] prompts typescript
复制代码

需要注意的是 node-fetch 需要指定使用 2.x 的版本。因为新版本,好像需要 node 升级到 18,不会会有一个 require,没细看原因。当然你也可以用其他的请求方式去请求接口。

4、配置执行命令

如果你期望安装完这个包,之后用 jjcj 调用命令,则做如下配置,这里其实相当于一个全局的 alias,即 jjcj 相当于 node ./bin/jjcj.js

  "bin": {
    "jjcj": "bin/jjcj.js"
  },
复制代码

5、新建 bin/jjcj.js

#!/usr/bin/env node
// setNodeTitle
process.title = '掘金抽奖';
require('../dist/cli')
    .run()
    .catch((e) => {
        console.error(e);
        process.exit(1);
    });
复制代码

process.title = '掘金抽奖'; 会在你执行命令的时候,修改命令行工具的标题。

image.png

6、新建 src/cli.ts

如果你喜欢写 es5 的代码,那你可以直接在 bin/jjcj.js 文件中编写相关逻辑,但是我现在比较喜欢用 ts 写 es6 的代码,所以还需要加一层编译。

7、使用 father@next 编译文件

新版本的 father 非常好用,只需要新建一个 .fathertc.ts 即可。可以说非常智能了。速度也非常快。

import { defineConfig } from 'father';

export default defineConfig({
  cjs: {
    output: 'dist',
  },
});
复制代码

8、发布上线

$pnpm build
// father build

$npm publish

npm notice === Tarball Details === 
npm notice name:          jjcj                                    
npm notice version:       0.0.2                                   
npm notice filename:      jjcj-0.0.2.tgz                          
npm notice package size:  55.0 kB                                 
npm notice unpacked size: 153.8 kB                                
npm notice shasum:        e49ec83af8335ce69b182a331a06957b11a589cd
npm notice integrity:     sha512-IAjh8+dLyUYmD[...]B1Y1abj+2ue7w==
npm notice total files:   9                                       
npm notice 
+ [email protected]
复制代码

源码归档 juejin-luck-draw

感谢阅读,希望本文对你有用。

猜你喜欢

转载自juejin.im/post/7129482663794049032