Loker 发表于 2021-3-24 21:43

【原创源码】【PHP Python HTML】教你从0搭建微信推送斗鱼直播提醒

本帖最后由 Loker 于 2021-3-30 21:34 编辑

#         教你从0搭建微信推送斗鱼直播提醒

> > 自己学习了一些Python和PHP的基础内容,做了个小功能分享一下。有说的不对或者需要改进的地方欢迎指正。
> > 本文持续更新,欢迎插眼。
>
>**[【Gitee同步更新中】](https://gitee.com/LGW_space/dy-live-lintener)**
> ## 功能介绍:
>
> - 用户端
>   - 根据房间号添加房间
>   - 设置是否提醒开播、是否提醒更换房间标题(某些主播会把下次开播时间写在房间标题上)
>   - 删除提醒推送的房间
>   - 用户反馈
> - 云函数
>   - 周期性检测斗鱼房间状态并和数据库保存的房间状态进行比对
>   - 给指定用户推送指定消息(使用(https://wxpusher.zjiecode.com/docs/#/))
> - 服务器端
>   - 提供各种API为用户端和云函数提供数据支持
>
> ## 项目截图:





## 一、项目准备

### 1. 用到的技术内容

- 云函数
- python(3.6/3.7)
- php(7.2 | ThinkPHP6)
- mysql(5.7)
- web基础

### 2. 需要的工具
- 腾讯云
- Pycharm(可选)
- VSCode
- 服务器(CentOS7)
- 数据库管理工具(Navicat)

##二、数据库设计
> 先设计数据库之后会在下一节运行以下代码。

创建数据库 dy_live_listener 并 设置编码为UTF8
```sql
CREATE DATABASE IF NOT EXISTS dy_live_listener DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
```

### 1. 用户表
> 注意:因为用户表中需要存微信名,有些人的微信名是图标或一些表情,所以要使用utf8mb4的编码方式,其他就用UTF8就可以。
```sql
CREATE TABLE `users`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '微信UID',
`username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '微信名',
`user_head_img` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像链接',
`max_room` int(2) NULL DEFAULT 3 COMMENT '最大房间数',
`current_room` int(2) NULL DEFAULT 0 COMMENT '当前房间数',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
```

### 2. 房间表
> 注意:这个表里面也可以用一个自增的ID作为主键。`room_status`字段在斗鱼的API里面是字符类型不是整形,这里也可以调整一下。
```sql
DROP TABLE IF EXISTS `rooms`;
CREATE TABLE `rooms`(
`room_id` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '房间号',
`room_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '房间标题',
`room_status` int(2) NULL DEFAULT NULL COMMENT '房间状态 1:直播中,2:未开播',
`start_time` datetime(0) NULL DEFAULT NULL COMMENT '上次直播开始时间',
`owner_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '主播名',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`room_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
```

### 3. 用户房间关联表
> 一个用户可以关注多个房间,所以用户和房间是一对多的关系。
```sql
CREATE TABLE `user_room`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '微信用户UID',
`room_id` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '房间号',
`live_msg` int(2) NOT NULL DEFAULT 1 COMMENT '是否发送直播提醒 1:是(默认),0:否',
`change_name_msg` int(2) NOT NULL DEFAULT 1 COMMENT '是否发送更改房间标题的提醒 1:是(默认),0:否',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
```

### 4. 用户反馈表
> 用于收集用户反馈,可以不加更新时间字段
```sql
CREATE TABLE `user_feedback`(
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`user_feedback` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
```

##三、创建服务端
> 本人PHP完全是初学状态,ThinkPHP所有的内容全都是在(https://www.kancloud.cn/manual/thinkphp6_0/1037479)里面扒拉的,一些代码可能会很丑陋,希望大神轻点喷(手动狗头)

### 1. 安装软件和环境

> 登录服务器安装php、apache、mysql等(没有服务器先在本地练习运行也可以)

- 本地运行的话推荐使用(https://www.xp.cn/),自带项目所需要的环境。


- 服务器推荐直接使用[宝塔linux面板](https://www.bt.cn/),一键安装所需要的环境和一些软件。
- 控制台登录服务器直接运行下方代码
- 打开面板`http://你的IP地址:8888/` 然后选择安装需要的环境。
```
yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh
```

### 2. 创建数据库
> 可以在宝塔面板中直接创建,也可以通过命令行创建。这里演示从命令行创建数据库。数据库的密码在宝塔面板->数据库->root密码查看;PHPStudy的直接点击右侧数据库里面就有账号和密码。

**2.1** 运行``mysql -uroot -p你的密码``进入数据库。
**2.2** 创建数据库`dy_live_listener`

```sql
CREATE DATABASE IF NOT EXISTS dy_live_listener DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
```

**2.3** 进入创建的数据库`use dy_live_listener;`
**2.4** 依次运行第二节中的代码创建4个表。

### 3. 创建ThinkPHP项目
**3.1** 先安装composer(网上很多教程我就不再赘述,[推个链接](https://www.kancloud.cn/manual/thinkphp6_0/1037481))

**3.2** 使用composer创建项目【[详细安装教程](https://www.kancloud.cn/manual/thinkphp6_0/1037481)】注意不要安装成5.x的版本,推荐安装开发版

- 在服务器中,cd到``/www/wwwroot/``目录下,运行``composer create-project topthink/think tp``创建项目
- 在 Windows 中,直接在PHPStudy的安装目录``X:\phpstudy_pro\WWW``下运行``composer create-project topthink/think=6.0.x-dev tpdemo``创建项目。

**3.3** 修改数据库配置文件``tp\config\database.php``
改为以下内容。密码改成你自己的。

```
// 数据库名
'database'          => env('database.database', 'dy_live_listener'),
// 用户名
'username'          => env('database.username', 'root'),
// 密码
'password'          => env('database.password', '你的密码'),
```
**3.4 **查看是否安装成功
- 命令行进入当前目录,运行`php think run`
- 在浏览器中输入地址:``http://localhost:8000/``看看是否进入欢迎页

**3.5** 新建网站

- 宝塔面板或者PHPStudy中,添加站点,根目录定位到新建项目的`public`文件夹,运行目录为项目根目录
- 配置伪静态,apache为例,nginx的配置方式自行百度。
```
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php
</IfModule>
```

### 4. 创建模型Model
> 因为我们需要用到一对多查询,所以我们可以创建数据库表对应的模型

**4.1** 在``tp\app\``目录下新建`model`文件夹
**4.2** 新建`Users.php`、`UserRoom.php`和`Rooms.php`,代码如下

**Users.php**

> 一个用户可以添加多个房间,所以用户表和用户房间关联表一对多的关系。 [点击查看详细](https://www.kancloud.cn/manual/thinkphp6_0/1037601)
> hasMany('关联模型','外键','主键');

```
<?php
namespace app\model;

use think\Model;

class Users extends Model
{
    public function rooms()
    {
      return $this->hasMany(UserRoom::class,'uid','uid');
    }
}
```

**UserRoom.php**
> 用户房间关联表里的房间号和房间表的房间号是一对一的关系,所以用一对一关联
> hasOne('关联模型类名', '外键', '主键');

```
<?php
namespace app\model;
use think\Model;
class UserRoom extends Model
{
    public function detial()
    {
      return $this->hasOne(Rooms::class, 'room_id', 'room_id');
    }
}
```


**Rooms.php**
> 房间表和用户房间关联表关系也是一对多

```
<?php
namespace app\model;

use think\Model;

class Rooms extends Model
{
    public function users()
    {
      return $this->hasMany(UserRoom::class,'room_id','room_id');
    }
}
```

> 这里我们为什么要使用模型呢?
> 比如我们要查找某个用户关注的房间的详细信息,我们就可以用以下代码完成查询工作
```
$user_info = Users::where('uid', $uid)->find(); // 通过UID找到用户
$user_room = $user_info->rooms;// 查询该UID在用户房间关联表里的数据条数
$temp_list = []; // 临时列表存放所有房间详细信息
foreach ($userroom as $um) {
      //$um->detial获取房间详细数据,添加$um的两个字段
      $detial = $um->detial;
      $detial['live_msg'] = $um['live_msg'];
      $detial['change_name_msg'] = $um['change_name_msg'];
      array_push($temp_list, $detial);   // 此时$temp_list里就是房间的详细信息包括关联表里我们需要的两个字段
}
```

### 5. 创建控制器Controller

> 后期根据业务需要完善Controller

在``tp\app\controller``下新建`Live.php`。内容如下:

> 需要引入模型和日志模块

```php
<?php

namespace app\controller;
use app\model\Users;
use app\model\Rooms;
use app\model\UserRoom;
use think\facade\Db;
use think\facade\Request;
use think\facade\Log;


class Live
{

}
```
### 6. 创建前端页面
> 后期根据业务内容完善该页面。调用的`/live/getuserinfo`接口也会在下一节讲解。

- 创建路径tp\public\admin\start(为啥用这个路径呢?因为我以前用的后台管理模板,懒得换了,哈哈)
- 在该路径下新建index.html和layui文件夹(layui文件夹用于存放layui源文件)
**index.html** 上传完整的代码会报错所以分三部分展示
```
【head部分】
<meta charset="utf-8">
<title>房间关注管理</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="./layui/css/layui.css" media="all" />
<!-- 注意:如果你直接复制所有代码到本地,上述css路径需要改成你本地的 -->
<style>
    /* 解决三个按钮不能在一行的问题 */
    .layui-table-tool-temp {
      padding: 0;
    }
</style>
```
```
【body部分】
<div class="layui-collapse" lay-filter="scoller">
    <div class="layui-colla-item">
      <h2 class="layui-colla-title">关注管理</h2>
      <div class="layui-colla-content layui-show" style="padding: 0;">
      <!-- 综合查询 -->

      <table class="layui-hide" id="test" lay-filter="test"></table>

      <script type="text/html" id="toolbarDemo">

      <div class="layui-btn-container">
      <div class="layui-inline">
            <div class="layui-btn"> <em id="nickname"></em>
                <img id="headimg" src="" class="layui-circle" style="width: 25px;height: 25px;" >
            </div>
      </div>
      <button class="layui-btn layui-btn-normal" lay-event="add" id="addroom">添加</button>
      <button class="layui-btn layui-btn-warm" lay-event="feedback">反馈</button>
      </div>
    </script>
    <script type="text/html" id="barDemo">
      <a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
    </script>
    <script type="text/html" id="switchLive">
      <input type="checkbox" name="live_msg" value="{{d.room_id}}" lay-skin="switch" lay-text="是|否" lay-filter="switchFunc" {{ d.live_msg == 1 ? 'checked' : '' }}>
    </script>
    <script type="text/html" id="switchChangeName">
      <input type="checkbox" name="change_name_msg" value="{{d.room_id}}" lay-skin="switch" lay-text="是|否" lay-filter="switchFunc" {{ d.change_name_msg == 1 ? 'checked' : '' }}>
    </script>

      </div>
    </div>
    <div class="layui-colla-item">
      <h2 class="layui-colla-title">程序介绍</h2>
    </div>
```
```
【JS部分】
<script charset="UTF-8" src="./layui/layui.js"></script>
<!-- 注意:如果你直接复制所有代码到本地,上述js路径需要改成你本地的 -->

<script>
    // 获取url参数
    function getQueryVariable(variable) {
      var query = window.location.search.substring(1);
      var vars = query.split("&");
      for (var i = 0; i < vars.length; i++) {
      var pair = vars.split("=");
      if (pair == variable) { return pair; }
      }
      return (false);
    }

    // 获取参数UID
    var uid = getQueryVariable('uid');

    layui.use(['form', 'table', 'jquery', 'element'], function () {
      var table = layui.table;
      var element = layui.element;
      var form = layui.form;
      $ = layui.jquery;

      render_table();

      function render_table() {
      $.ajax({
          url: '/live/getuserinfo',
          type: 'POST',
          data: {
            'uid': uid
          },
          success: function (data) {
            let user_info = data.data['user_info'];
            sessionStorage.setItem('current_room', user_info['current_room']);
            sessionStorage.setItem('max_room', user_info['max_room']);
            let guanzhu = data.data['user_guanzhu'];
            console.log(data.data);
            table.render({
            elem: '#test'
            , data: guanzhu
            , toolbar: '#toolbarDemo' //开启头部工具栏,并为其绑定左侧模板
            , defaultToolbar: []
            , title: '用户数据表'
            , cols: [[
                { field: 'room_id', title: '房间号', width: 95, fixed: 'left' }
                , { field: 'owner_name', title: '主播', width: 120 }
                , {
                  field: 'room_status', title: '房间状态', width: 90, templet: function (d) {
                  if (d.room_status == '1') {
                      return '直播中'
                  } else if (d.room_status == '2') {
                      return '未开播'
                  } else {
                      return '房间异常'
                  }
                  }
                }
                , { field: 'live_msg', title: '提醒开播', width: 90, templet: '#switchLive', unresize: true }
                , { field: 'change_name_msg', title: '提醒标题', width: 90, templet: '#switchChangeName', unresize: true }
                , { field: 'room_name', title: '房间标题', width: 180 }
                , { field: 'start_time', title: '开始时间', width: 180 }
                , { fixed: 'right', title: '操作', toolbar: '#barDemo', width: 70 }
            ]]
            });
            $("#nickname").html(user_info['username']);
            $("#headimg").attr("src", user_info['user_head_img']);
            $("#addroom").html("添加 " + user_info['current_room'] + "/" + user_info['max_room'])
          }
      });
      }
    });
</script>
```

服务器或本地可以访问 ``http://你的IP地址/admin/start`` 查看该页面

## 四、根据业务逻辑完善前端功能

### 1. 注册

> 注册功能我们需要用到WxPusher的**关注回调**功能,当用户关注主题后,会把该用户的基本信息发送到指定链接。

**1.1** 注册 WxPusher
打开(https://wxpusher.zjiecode.com/admin/login)扫码登录,如没有注册账号,扫码自动注册。
**1.2** 创建新的应用

| 字段         | 内容                                    | 备注                                                         |
| ------------ | --------------------------------------- | ------------------------------------------------------------ |
| 应用名字   | 微信推送斗鱼直播提                   醒 |                                                            |
| 联系方式   | 【你的联系方式】                        |                                                            |
| 推送内容说明 | 【项目简介】                            |                                                            |
| 事件回调地址 | http://IP地址/live/reg                  | 在用户关注**应用**的时候,WxPusher会推送消息到这个URL上,方便开发者获取关注用户的UID。**这里我们写注册功能的API**。 |
| 设置地址   | http://IP地址/admin/start               | 如果有需要用户在微信端跳转到自己的业务,可以设置这个URL,用户可以在微信端跳转到这个url上,并且携带上用户的uid参数。**这里我们写index.html的地址**。 |
| 用户关注提示 | 【关注成功提示】                        | 用扫关注你的应用后,发送的一个提示信息,一般用于提示你推送的内容,关注成功等。 |


**1.3** 妥善保存你的APPToken

在WxPusher后台->应用管理->appToken 拿到你的授权码,调用WxPusher的API时候,需要用到这个appToken。

**1.4** 保存关注应用的链接

在WxPusher后台->应用管理->关注应用 得到你的关注链接。点击[这里](https://wxpusher.zjiecode.com/docs/#/?id=%e7%94%a8%e6%88%b7%e5%85%b3%e6%b3%a8%e5%9b%9e%e8%b0%83)查看用户关注回调的详细介绍。

> 你也可以通过发送这个链接给用户,用户通过这个链接,可以看到应用的相关介绍并直接关注应用。
>
> 用户关注应用后,WxPuhsher会回调UID给开发者,开发者可通过UID发送消息给用户。

**1.5** 编写注册API

在`Live.php`新建reg方法

> 这里的``Db::table('users')``可以直接用模型`User`代替:``User::where($userinfo)->find();``

```php
// 注册 通过WxPusher的关注回调来注册
    public function reg()
    {
      $data = Request::instance()->param('data');// 获取回调数据
      $userinfo = [
            'uid' => $data['uid'],
            'username' => $data['userName'],
            'user_head_img' => $data['userHeadImg']
      ];
      $hasreg = Db::table('users')->where($userinfo)->find();// 是否注册过
      if(!isset($hasreg)){// 没注册过的话就保存用户数据
            $res = Db::name('users')->save($userinfo);
      }
      return json([
            'code'=>1,
            'msg'   =>'success',
            'data'=>$data
      ]);
    }

```


**这样用户先关注【新消息服务】服务号,点击你的关注链接就可以关注你的主题,并且个人信息也会保存到你的数据库中。**

### 2. 用户和关注房间展示

> 已经成功关注应用的用户可以在新消息服务->我的->我的订阅->微信推送斗鱼直播提醒->设置 打开我们配置的前端index页面

layui表格渲染的时候,会调用`/live/getuserinfo`,返回数据渲染到表格中。
我们需要获取的内容包括
- 用户基本信息
- 用户关注的房间基本信息
- 用户对于该房间开播或者更换标题是否接受通知
在`Live.php`新建`getuserinfo`方法
> 注意:可以直接返回`$user_info`,因为我们已经配置了模型,`Users`模型里包含用户基本信息和他在`UserRoom`的内容,而`UserRoom`里又包含`Rooms`的内容。然后在前端解析`$user_info`。

```php
    // 获取用户信息和关注列表
    public function getuserinfo()
    {
      $uid = Request::instance()->param('uid');
      $user_info = Users::where('uid', $uid)->find(); // 找到用户
      $userroom = $user_info->rooms; // 获取UserRoom的数据
      $temp_list = [];
      foreach ($userroom as $um) {
            //$um->detial获取房间详细数据,拼接$um的两个字段
            $detial = $um->detial;
            $detial['live_msg'] = $um['live_msg'];
            $detial['change_name_msg'] = $um['change_name_msg'];
            array_push($temp_list, $detial);
      }
      $data = [
            'user_info' => $user_info,
            'user_guanzhu' => $temp_list
      ];
      return json([
            'code'=>1,
            'msg'   =>'success',
            'data'=>$data
      ]);
    }
```

回调结果:
> 以下数据为我以前本地测试的数据,如下所示,user_info中就已经包含我们需要的所有数据,所以我们可以直接让后端返回`User`再到前端进行解析需要的数据到表格中。、
> 但是在没有添加房间之前我们还无法获取这些房间数据所以可以添加一些假数据在数据库里。

```json
{
    "user_info":{
      "id":13,
      "uid":"UID_xxxxxxxxxxxx",
      "username":"shmily",
      "user_head_img":"http://thirdwx.qlogo.cn/mmopen/xxx/132",
      "max_room":5,
      "current_room":3,
      "create_time":"2021-02-21 16:04:52",
      "update_time":"2021-03-10 20:24:19",
      "rooms":[
            {
                "id":17,
                "uid":"UID_8nnpkmRZVfTtXcM6ZN59EndFvAfF",
                "room_id":"71415",
                "live_msg":1,
                "change_name_msg":1,
                "create_time":"2021-03-23 19:03:22",
                "detial":{
                  "room_id":"71415",
                  "room_name":"3月13日上午10点直播",
                  "room_status":1,
                  "start_time":"2021-03-13 09:53:27",
                  "owner_name":"寅子",
                  "create_time":"2021-03-13 09:55:42",
                  "live_msg":1,
                  "change_name_msg":1
                }
            },
            {
                "id":18,
                "uid":"UID_8nnpkmRZVfTtXcM6ZN59EndFvAfF",
                "room_id":"99999",
                "live_msg":1,
                "change_name_msg":1,
                "create_time":"2021-03-23 19:13:33",
                "detial":{
                  "room_id":"99999",
                  "room_name":"毕业的好日子,康姆了康姆了!",
                  "room_status":2,
                  "start_time":"2021-03-12 20:22:11",
                  "owner_name":"旭旭宝宝",
                  "create_time":"2021-03-13 02:57:01",
                  "live_msg":1,
                  "change_name_msg":1
                }
            },
            {
                "id":25,
                "uid":"UID_8nnpkmRZVfTtXcM6ZN59EndFvAfF",
                "room_id":"4615502",
                "live_msg":1,
                "change_name_msg":1,
                "create_time":"2021-03-23 19:03:23",
                "detial":{
                  "room_id":"4615502",
                  "room_name":"~啾咪~哈哈哈哈~",
                  "room_status":2,
                  "start_time":"2021-03-12 18:13:25",
                  "owner_name":"一条小团团OvO",
                  "create_time":"2021-03-13 00:40:01",
                  "live_msg":1,
                  "change_name_msg":1
                }
            }
      ]
    },
    "user_guanzhu":[
      {
            "room_id":"71415",
            "room_name":"3月13日上午10点直播",
            "room_status":1,
            "start_time":"2021-03-13 09:53:27",
            "owner_name":"寅子",
            "create_time":"2021-03-13 09:55:42",
            "live_msg":1,
            "change_name_msg":1
      },
      {
            "room_id":"99999",
            "room_name":"毕业的好日子,康姆了康姆了!",
            "room_status":2,
            "start_time":"2021-03-12 20:22:11",
            "owner_name":"旭旭宝宝",
            "create_time":"2021-03-13 02:57:01",
            "live_msg":1,
            "change_name_msg":1
      },
      {
            "room_id":"4615502",
            "room_name":"~啾咪~哈哈哈哈~",
            "room_status":2,
            "start_time":"2021-03-12 18:13:25",
            "owner_name":"一条小团团OvO",
            "create_time":"2021-03-13 00:40:01",
            "live_msg":1,
            "change_name_msg":1
      }
    ]
}
```

> 获取到用户数据后我们首先把用户的可添加房间数和最大可添加房间数存到了session中
> ```
> sessionStorage.setItem('current_room', user_info['current_room']);
sessionStorage.setItem('max_room', user_info['max_room']);
> ```
> 然后给表格赋初始数据(代码在上面js部分)
> 最后把用户头像和用户名展示在页面上
> ```
> $("#nickname").html(user_info['username']);
$("#headimg").attr("src", user_info['user_head_img']);
$("#addroom").html("添加 " + user_info['current_room'] + "/" + user_info['max_room']);
> ```

### 3. 添加房间

> 添加房间之前我们需要通过斗鱼的API判断房间是否存在获取房间的基本信息,然后把基本信息存入数据库中,判断是否超过添加上限,不超过的话用户房间关联表添加一行,用户的已添加房间数加一。
> 我们在前端调用斗鱼api接口的话首先需要解决跨域问题,不然调用接口就会报错。
> 我们可以通过[配置代{过}{滤}理的方式解决跨域问题](https://blog.csdn.net/example440982/article/details/81319106) 修改vhost文件``ProxyPass /room http://斗鱼第三方API自行百度/room``,这样我们就可以通过 ``/room/房间号``来获取房间信息。
>
> 因为斗鱼第三方API好像是没公布,所以需要的小伙伴可以自行百度,有些结果里面有这个api。
> 可以在这里先判断用户是否还可以添加房间,因为上一步返回的User信息里面已经有当前用户最大房间数和已添加的房间数了。
> ``<button class="layui-btn layui-btn-normal" lay-event="add" id="addroom">添加</button>`` 因为头部工具栏的按钮中`lay-event`属性为`add`,所以我们可以通过以下方法设置点击事件。(顺便给删除按钮的点击事件占个坑)

```
      //头工具栏事件
      table.on('toolbar(test)', function (obj) {
      if (obj.event == 'add') {
          let max_room = sessionStorage.getItem('max_room');
          let current_room = sessionStorage.getItem('current_room');
          // 在前端先判断是否可以继续添加房间(为了防止用户修改数据,后端也会进行一次判断)
          if (current_room >= max_room) {
            layer.msg('可添加房间已达上限', { time: 1500 });
            return
          }
          layer.prompt({ title: '添加房间' }, function (val, index) {
            // 调用斗鱼接口获取房间信息
            $.ajax({
            url: '/room/' + val,
            success: function (data) {
                if (data.error == 0) {
                  let live_name = data.data.owner_name;
                  // 插入数据
                  $.ajax({
                  url: '/live/addroom',
                  method: 'POST',
                  data: {
                      'uid': uid,
                      'roomid': data.data.room_id,
                      'roominfo': data.data
                  },
                  success: function (data) {
                      layer.close(index);
                      render_table();
                      layer.msg(data.msg, { time: 1500 });
                  },
                  error: function (e) {
                      console.log(e);
                  }
                  });
                } else {
                  layer.msg('未找到该房间', { time: 1000 });
                }
            },
            error: function (e) {
                console.log(e);
                layer.msg('未找到该房间', { time: 1000 });
            }
            });
          });
      }
      
      if (obj.event == 'feedback') {}
      });
```

后端添加房间的方法:在`Live.php`中新建`addroom`方法:(逻辑有点混乱 - 。-)
```
    // 添加房间
    public function addroom()
    {
      $uid = Request::instance()->param('uid');
      $roomid = Request::instance()->param('roomid');
      $roominfo = Request::instance()->param('roominfo');
      // 判断用户
      $user_info = Users::where('uid', $uid)->find();
      $maxroom = $user_info->max_room; // 该用户的最大可添加房间数
      $currentroom = $user_info->current_room;// 当前已添加的房间数
      if ($currentroom >= $maxroom) {
            return json([
                'code'=>0,
                'msg'   =>'您的房间添加上限为'.$maxroom,
                'data' =>'您的房间添加上限为'.$maxroom
            ]);
      } else {
            // 是否在数据库中存在房间信息
            $hasroom = Rooms::where('room_id',$roomid)->find();
            if(!isset($hasroom)){
                // 以前没添加过该房间,就添加
                $res = Rooms::strict(false)->insert($roominfo);
            }
            $data = [
                'uid'      =>      $uid,
                'room_id'=>      $roomid
            ];
            // 是否以前添加过该房间添加user_room
            $has_user_room = UserRoom::where($data)->find();
            if (isset($has_user_room)){
                return json([
                  'code'=>0,
                  'msg'   =>'已经加过该房间',
                  'data'=>'已经加过该房间'
                ]);
            } else {
                $res = UserRoom::insert($data);
                // 用户已添加的房间数加一
                $user_info->inc('current_room')->update();
                return json([
                  'code'=>1,
                  'msg'   =>'添加成功,下次开播就可以收到提醒啦',
                  'data'=>$currentroom
                ]);
            }
      }
    }
```
### 4. 删除房间

> 删除房间比添加房间更简单。
> 如果我们在返回房间数据的时候,顺便把user_room表的id也返回的话,我们可以直接删除该id记录就可以。
> ```
> foreach ($userroom as $um) {
> //$um->detial获取房间数据,拼接$um的两个字段
> $detial = $um->detial;
> // 把用户房间关联表对应的主键也加到房间详情里。
> $detial['user_room_id'] = $um['id'];
> $detial['live_msg'] = $um['live_msg'];
> $detial['change_name_msg'] = $um['change_name_msg'];
> array_push($temp_list, $detial);
> }
> ```


删除按钮的点击事件:
```
      //监听行工具事件【删除】
      table.on('tool(test)', function (obj) {
      var data = obj.data;   // data 保存点击的行数据
      if (obj.event === 'del') {
          layer.confirm('真的删除该行数据么', function (index) {
            $.ajax({
            url: '/live/deletetroom',
            data: {
                'uid': uid,
                'roomid': data.room_id   //这里可以直接返回 'id': data.id 即关联表的主键
            },
            success: function (data) {
                layer.closeAll();
                render_table();
                layer.msg('删除成功', { time: 1000 })
            },
            error: function (e) {
                console.log(e);
            }
            });
          });
      }
      });
```

控制器添加`deletetroom`方法
```
    // 删除房间
    public function deletetroom()
    {
      $uid = Request::instance()->param('uid');
      $roomid = Request::instance()->param('roomid');

      $res = UserRoom::where(['uid'=>$uid, 'room_id'=>$roomid])->delete(); // 删除房间
      if($res){
            $user_info = Users::where('uid', $uid)->find();
            $user_info->dec('current_room')->update(); // 已添加房间数减一
      }
      // 判断是否还有人关注这个房间 是否需要同时删除房间信息
      $hasroom = UserRoom::where('room_id',$roomid)->find();
      if(!isset($hasroom)){
            Rooms::where('room_id', $roomid)->delete();
      }

      return json([
            'code'=>1,
            'msg'   =>'success',
            'data'=>$res
      ]);
    }
```
### 5. 用户反馈

> 这个和添加房间的内容差不多,但是更精简就只是向数据库存数据

前端点击事件(还记得我们前面给这个方法留的位置吗)
```
if (obj.event == 'feedback') {
          //默认prompt
          layer.prompt({ formType: 2, title: '意见反馈' }, function (val, index) {
            $.ajax({
            url: '/live/userfeedback',
            data: {
                'uid': uid,
                'user_feedback': val
            },
            success: function (data) {
                layer.close(index);
                layer.msg('感谢反馈', { time: 1000 });
            },
            error: function (e) {
                console.log(e);
            }
            });
          });
      }
```

控制器新建`userfeedback`方法
```
    // 用户反馈
    public function userfeedback()
    {
      $data = Request::param(false);
      $res = Db::name('user_feedback')->save($data);
      return json([
            'code'=>1,
            'msg'   =>'success',
            'data'=>$res
      ]);
    }
```
### 6. 是否接受消息
> 云函数获取数据库中需要推送消息的用户时候,会同时判断用户设置的接受消息的状态是否为“是”。
> 在用户前端,用户通过点击switch开关,发送ajax请求来改变字段的值。

前端:

```html
【表格渲染】
, { field: 'live_msg', title: '提醒开播', width: 90, templet: '#switchLive', unresize: true }
, { field: 'change_name_msg', title: '提醒标题', width: 90, templet: '#switchChangeName', unresize: true }
【body里面添加】
<script type="text/html" id="switchLive">
<input type="checkbox" name="live_msg" value="{{d.room_id}}" lay-skin="switch" lay-text="是|否" lay-filter="switchFunc" {{ d.live_msg == 1 ? 'checked' : '' }}>
</script>
<script type="text/html" id="switchChangeName">
<input type="checkbox" name="change_name_msg" value="{{d.room_id}}" lay-skin="switch" lay-text="是|否" lay-filter="switchFunc" {{ d.change_name_msg == 1 ? 'checked' : '' }}>
</script>
```

> ``obj.elem.checked ? 1 : 0``:判断当前switch按钮是开还是关,如果是开就返回 1
> `this.name`:用于判断点了哪个switch按钮,他返回的是`input`标签的`name`属性值。

```js
form.on('switch(switchFunc)', function (obj) {
    $.ajax({
      url: '/live/switchmsg',
      data: {
            'uid': uid,
            'roomid': this.value,
            'action': this.name,
            'value': obj.elem.checked ? 1 : 0
      },
      success: function (data) {
            layer.msg('切换成功', { time: 1000 })
      },
      error: function (e) {
            console.log(e);
      }
    });
});
```

后端部分:新建`switchmsg`方法
```php
   // 转换
    public function switchmsg()
    {
      $uid = Request::instance()->param('uid');
      $roomid = Request::instance()->param('roomid');
      $action = Request::instance()->param('action');
      $value = Request::instance()->param('value');

      $res = UserRoom::where([
            'uid'      =>      $uid,
            'room_id'=>      $roomid
      ])->update([$action => $value]);

      return json([
            'code'=>1,
            'msg'   =>'success',
            'data'=>$res
      ]);
    }
```

## 五、云函数的开发
> 云函数我们使用腾讯云函数作为示例,阿里云也同理。(阿里云似乎是不允许触发器的触发周期低于1分钟)

### 1. 注册腾讯云
[官方的注册方法](https://cloud.tencent.com/register?s_url=https%3A%2F%2Fcloud.tencent.com%2F)

### 2. 创建云函数
- 登录腾讯云,在云产品里找到云函数,在【函数服务】里新建云函数
- 选择【自定义创建】
- 修改【函数名称】、【地域】、【运行环境(Python3.6)】、【提交方法(在线编辑)】、【执行方法(`index.main_handler`)】暂时不用修改默认代码。
- 点击完成

### 3. 完善功能
首先引入需要的包
- `requests` 用于发送请求
- ` json` 格式化请求的参数或获取的相应内容
- `logging` 有时候需要打印日志查看(腾讯云的默认日志级别是`info`,我们需要把它的级别调整为`error`来屏蔽掉我们不需要的输出)

```
# -*- coding: utf8 -*-
import requests
import json
import logging
import time
# 调整日志级别
logger = logging.getLogger()
logger.setLevel(logging.ERROR)
```

#### 3.1 根据房间号获取直播状态
> 首先我们就需要获取数据库中全部的房间并且通过斗鱼的api来获取此时的直播间状态。
>
> 当未找到该房间的时候会返回404,因为我们在前端已经做了一次判断,这里其实就不需要再加上404判断了。
> 之后我们需要判断它返回的是不是我们需要获取的内容,最后返回这个房间的信息。

获取到房间后我们就需要通过斗鱼的第三方API获取到此时当前房间信息:

````python
# 根据房间号获取直播状态
def getRoomInfoById(room_id):
    url = 'http://斗鱼第三方Api/room/' + room_id
    try:
      r = requests.get(url, timeout=5)
    except requests.exceptions.RequestException as e:
      print(str(e))
      return 'timeout'
    else:
      if r.status_code == 404:
            return '404'
      json_str = json.loads(r.text)
      if isinstance(json_str, dict):
            if 'error' in json_str.keys() and json_str['error'] == 0:
                room_info = json_str['data']
                return room_info
````

#### 3.2 获取数据库中的全部房间

获取房间:返回当前数据库中所有的房间信息
```python
def getAllRooms():
    time.sleep(0.1)
    url = 'http://你的服务器IP/live/getallrooms'
    try:
      r = requests.get(url, timeout=1)
      if r.status_code == 200:
            json_str = json.loads(r.text)
            rooms = json_str['data']
            return rooms
    except requests.exceptions.ConnectionError as e:
      logger.error(e)
```

后端部分:

```PHP
    // 获取全部房间 不需要创建时间和修改时间字段
    public function getallrooms()
    {
      $rooms = Rooms::select()->hidden(['create_time', 'update_time']);
      return json([
            'code'=>1,
            'msg'   =>'success',
            'data' =>$rooms
      ]);
    }
```

#### 3.3 使用WxPusher给指定用户发送消息

我们在第四章第一节中简单介绍了`WxPusher`的注册和关注方法,这里说一下如何给用户发送消息

用户在关注应用后,我们可以在数据库中获取到用户的`UID`。通过UID我们就可以给用户发送消息。

以下是官方api介绍:

> POST接口 POST接口是功能完整的接口,推荐使用。
>
> Content-Type:application/json
>
> 地址:http://wxpusher.zjiecode.com/api/send/message
>
> 请求数据放在body里面,具体参数如下:

```
{
"appToken":"AT_xxx",
"content":"Wxpusher祝你中秋节快乐!",
"summary":"消息摘要",//消息摘要,显示在微信聊天页面或者模版消息卡片上,限制长度100,可以不传,不传默认截取content前面的内容。
"contentType":1,//内容类型 1表示文字2表示html(只发送body标签内部的数据即可,不包括body标签) 3表示markdown
"topicIds":[ //发送目标的topicId,是一个数组!!!,也就是群发,使用uids单发的时候, 可以不传。
      123
],
"uids":[//发送目标的UID,是一个数组。注意uids和topicIds可以同时填写,也可以只填写一个。
      "UID_xxxx"
],
"url":"http://wxpusher.zjiecode.com" //原文链接,可选参数
}
```

根据这个接口,我们就可以编写函数:

> `content`:我们要发送的内容。
>
> `uids`:要发送的用户列表

```python
def send_WxPusher_msg(content, uids):
    headers = {
      'Content-Type': 'application/json'
    }
    url = 'http://wxpusher.zjiecode.com/api/send/message'
    parameter = {
      "appToken": "你的appToken",
      "content": content,
      "contentType": 1,
      "uids": uids,
    }
    r = requests.post(url=url, headers=headers,
                      data=json.dumps(parameter, ensure_ascii=False).encode('utf-8'))
    json_str = json.loads(r.text)
    # 获取异常用户,包括关闭通知、取消服务号订阅的
    datas = json_str['data']
    for data in datas:
      if data['code'] == 1001:
            logger.error(data['status'])
```

#### 3.4 根据房间号获取接受某项提醒的用户

如果该直播间开播了,或者直播间更换了房间标题(如果和以前保存的房间数据不一样),我们就需要发送消息通知需要通知用户:

> `room_id`:需要提醒的房间号
>
> `action`:提醒的行为(开播或者更换标题)
>
> 返回符合条件的用户列表

```
def getUsersByRoomId(room_id, action):
    url = 'http://你的服务器IP/live/getusersbyroomid'
    data = {
      'room_id': room_id,
      'action': action
    }
    try:
      r = requests.post(url, data=data, timeout=1)
      if r.status_code == 200:
            json_str = json.loads(r.text)
            users = json_str['data']
            return users
    except requests.exceptions.ConnectionError as e:
      logger.error(e)
```

后端:

> `action`:保存的就是数据库中是否提醒开播、是否提醒更换标题两个字段。所以我们只返回这两个字段为**1**,也就是设置提醒的用户UID。

```php
    // 根据房间号获取关注用户
    public function getusersbyroomid()
    {
      $room_id = Request::instance()->param('room_id');
      $action = Request::instance()->param('action');
      $users = UserRoom::where(['room_id'=>$room_id, $action => 1])->column('uid');
      return json([
            'code'=>1,
            'msg'   =>'success',
            'data' =>$users
      ]);
    }
```

#### 3.5 改变房间状态

> 如果从斗鱼获取的当前房间的信息和数据库中的信息不一致,我们就需要修改数据库中的房间信息。

```
def changeStatus(roominfo):
    time.sleep(0.01)
    url = 'http://你的服务器IP/live/changestatus'
    headers = {'Content-Type': 'application/json'}
    data = {
      'room_id': roominfo['room_id'],
      'room_name': roominfo['room_name'],
      'room_status': roominfo['room_status'],
      'start_time': roominfo['start_time'],
      'owner_name': roominfo['owner_name'],
    }
    try:
      r = requests.post(url, headers=headers, data=json.dumps(data, ensure_ascii=False).encode('utf-8'), timeout=1)
      if r.status_code != 200:
            logger.error(r.text)
    except requests.exceptions.ConnectionError as e:
      logger.error(e)
```

后端方法:

```php
    // 更改房间状态
    public function changestatus()
    {
      // $roominfo = Request::post();
      $room_id = Request::instance()->param('room_id');
      $room_name = Request::instance()->param('room_name');
      $room_status = Request::instance()->param('room_status');
      $start_time = Request::instance()->param('start_time');
      $owner_name = Request::instance()->param('owner_name');
      $room = Rooms::where('room_id', $room_id)->find();
      $room->room_name = $room_name;
      $room->room_status = $room_status;
      $room->start_time = $start_time;
      $room->owner_name = $owner_name;
      $room->save();
      return json([
            'code'=>1,
            'msg'   =>'success',
            'data' =>$room_id
      ]);
    }
```

#### 3.6 整合以上功能开发主方法

**终于最后一步啦**

> 整合方法,我们此时就可以写小程序的主方法`main_handler`

```
def main_handler(event, context):

    rooms = getAllRooms()

    for room in rooms:
      curr_info = getRoomInfoById(room['room_id'])
      if curr_info == 'timeout':
            logger.error('Error Timeout:' + str(room['room_id']))
            continue
                # 1. 是否开播的判断
      if curr_info['room_status'] == '1':# 正在直播
            if room['room_status'] == 2:# 之前状态为 未开播
                content = '您关注的主播:' + curr_info['owner_name'] + '开始直播啦' + \
                        '\n房间标题:' + curr_info['room_name'] + \
                        '\n正在直播:' + curr_info['cate_name']
                uids = getUsersByRoomId(room['room_id'], 'live_msg')# 获取UIDs
                if uids: # 排除空列表
                  send_WxPusher_msg(content, uids)
                changeStatus(roominfo=curr_info)
      else:# 没有直播
            if room['room_status'] == 1: # 之前状态为 正在直播
                changeStatus(curr_info)
      # 2. 对房间标题判断
      if curr_info['room_name'] != room['room_name']:
            content1 = '您关注的主播:' + curr_info['owner_name'] + '改变了房间标题' + \
                        '\n当前房间标题:' + curr_info['room_name'] + \
                        '\n上次房间标题:' + room['room_name']
            uids = getUsersByRoomId(room['room_id'], 'change_name_msg')# 获取UIDs
            if uids: # 排除空列表
                send_WxPusher_msg(content1, uids)
            changeStatus(roominfo=curr_info)

```



**到这就基本全部完成啦~**


说明:该帖子暂时完结。不再开新帖发布后续内容。
更新历史:
2021.03.30 更新全部内容,后续可能会改写部分内容。2021.03.25 更新第四节第1、2、3章,更新第三节3.5新建站点内容
2021.03.24 更新一、二、三节基础部分内容



dxaw2458 发表于 2021-3-24 22:12

感谢大佬的分享学习了

Caicc 发表于 2021-3-24 22:45

是个好东西,学习学习。

wangad123 发表于 2021-3-24 22:49

mark,学习学习。

vbnewer 发表于 2021-3-24 23:09

这个工程有点大,mark一下,慢慢学习。

ChaShu 发表于 2021-3-25 00:31

感谢发布原创作品,吾爱破解论坛因你更精彩!

mlk1123 发表于 2021-3-25 08:52

楼主真强

loker嘎嘎 发表于 2021-3-25 11:14

感谢楼主大大的分享{:17_1060:}
值得收藏学习,期待后续的更新!

叶凯 发表于 2021-3-25 16:03

这个wxpusher我也是一直在用,个人感觉比Server酱更好用,平时推送疫情和天气数据到微信

lookatnext 发表于 2021-3-25 16:55

非常感谢,非常感谢
页: [1] 2
查看完整版本: 【原创源码】【PHP Python HTML】教你从0搭建微信推送斗鱼直播提醒