python 全栈开发,Day131(向app推送消息,玩具端消息推送)
先下载github代码,下面的操作,都是基于这个版本来的!
https://github.com/987334176/Intelligent_toy/archive/v1.4.zip
注意:由于涉及到版权问题,此附件没有图片和音乐。请参考链接,手动采集一下!
请参考链接:
https://www.cnblogs.com/xiao987334176/p/9647993.html#autoid-3-4-0
一、向app推送消息
redis安装
Redis项目并没有正式支持Windows。然而,微软OpenTeaGeo集团开发并维护了以WIN64为目标的Windows端口。链接如下:
https://github.com/MicrosoftArchive/redis/releases
目前最新版本是3.2
完整下载链接如下:
https://github.com/MicrosoftArchive/redis/releases/download/win-3.2.100/Redis-x64-3.2.100.msi
开始安装
注意:要勾选添加到系统环境变量
默认端口是6379
设置最大内存,这里不设置。表示不限制内存大小!
安装完成后,打开cmd窗口,运行命令
d: cd D:\Program Files\Redis redis-server.exe redis.windows.conf redis-cli
效果如下:
使用Python操作redis,需要安装模块redis
pip install redis
接下来的操作,会将消息数量,存在redis中!
显示消息数量
如果有消息来了,需要在底部选项卡中的消息按钮,显示角标。比如这样:
还有 好友列表,也要显示角标。谁发送了几条消息,比如这样:
消息按钮显示未读消息
进入flask项目,修改setting.py,增加redis配置
import pymongo import os import redis # 数据库配置 client = pymongo.MongoClient(host="127.0.0.1", port=27017) MONGO_DB = client["bananabase"] REDIS_DB = redis.Redis(host="127.0.0.1",port=6379) RET = { # 0: false 2: True "code": 0, "msg": "", # 提示信息 "data": {} } XMLY_URL = "http://m.ximalaya.com/tracks/" # 喜马拉雅链接 CREATE_QR_URL = "http://qr.liantu.com/api.php?text=" # 生成二维码API # 文件目录 AUDIO_FILE = os.path.join(os.path.dirname(__file__), "audio") # 音频 AUDIO_IMG_FILE = os.path.join(os.path.dirname(__file__), "audio_img") # 音频图片 DEVICE_CODE_PATH = os.path.join(os.path.dirname(__file__), "device_code") # 二维码 CHAT_FILE = os.path.join(os.path.dirname(__file__), "chat") # 聊天 # 百度AI配置 APP_ID = "117912345" API_KEY = "3v3igzCkVFUDwFByNEE12345" SECRET_KEY = "jRnwLE7kzC1aRi2FD10OQY3y9O12345" SPEECH = { "spd": 4, 'vol': 5, "pit": 8, "per": 4 }
进入utils文件夹,创建文件 chat_redis.py
from setting import REDIS_DB import json def save_msg(小甜甜,xiao): """ key: xiao { 小甜甜:5, 小豆芽:4 } :return: """ res = json.loads(REDIS_DB.get(xiao)) res["小甜甜"] = 1 if res: pass REDIS_DB.set(xiao,json.dumps(res))
看下面的示例图:
修改 chat_redis.py
from setting import REDIS_DB import json def save_msg(sender, to_user): # 保存消息 # 1.查询一下xiao的Redis是否有数据 user_msg_redis = REDIS_DB.get(to_user) if user_msg_redis: # 2.将xiao的数据反序列化成字典 { sender : n } user_msg_dict = json.loads(user_msg_redis) # 3.判断有没有 sender 的用户发来的消息数量 if user_msg_dict.get(sender): # 数量加1 user_msg_dict[sender] += 1 else: # 第一次,初始值为1 user_msg_dict[sender] = 1 # 4.如果xiao是刚建立好的用户,他是没有消息的,字典是空 else: user_msg_dict = {sender: 1} # 5.序列化用户消息字典user_msg_dict user_msg_redis = json.dumps(user_msg_dict) # 6.存回Redis REDIS_DB.set(to_user, user_msg_redis) def get_msg_list(user): # 获取消息 user_msg_redis = REDIS_DB.get(user) if user_msg_redis: user_msg_dict = json.loads(user_msg_redis) # 统计数量 user_msg_dict["count"] = sum(user_msg_dict.values()) else: user_msg_dict = {"count":0} return user_msg_dict def get_user_msg_one(sender, to_user): # 获取一条消息 user_msg_redis = REDIS_DB.get(to_user) if user_msg_redis: user_msg_dict = json.loads(user_msg_redis) if user_msg_dict.get(sender): return user_msg_dict.get(sender)
修改 im_serv.py
from flask import Flask, request from geventwebsocket.websocket import WebSocket from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer import json, os from uuid import uuid4 from setting import AUDIO_FILE,CHAT_FILE from serv import content from utils import baidu_ai from utils import chat_redis import setting from bson import ObjectId import time app = Flask(__name__) user_socket_dict = {} # 空字典,用来存放用户名和发送消息 @app.route("/toy/<tid>") def toy(tid): # 玩具连接 # 获取请求的WebSocket对象 user_socket = request.environ.get("wsgi.websocket") # type:WebSocket if user_socket: # 设置键值对 user_socket_dict[tid] = user_socket print(user_socket_dict) # {'123456': <geventwebsocket.websocket.WebSocket object at 0x00000176ABD92E18>} file_name = "" to_user = "" # 循环,接收消息 while True: msg = user_socket.receive() if type(msg) == bytearray: file_name = f"{uuid4()}.wav" file_path = os.path.join(CHAT_FILE, file_name) with open(file_path, "wb") as f: f.write(msg) else: msg_dict = json.loads(msg) to_user = msg_dict.get("to_user") msg_type = msg_dict.get("msg_type") if to_user and file_name: other_user_socket = user_socket_dict.get(to_user) if msg_type == "ai": q = baidu_ai.audio2text(file_path) print(q) ret = baidu_ai.my_nlp(q, tid) other_user_socket.send(json.dumps(ret)) else: send_str = { "code": 0, "from_user": tid, "msg_type": "chat", "data": file_name } if other_user_socket: # 当websocket连接存在时 chat_redis.save_msg(tid, to_user) # 保存消息到redis # 发送数据 other_user_socket.send(json.dumps(send_str)) else: # 离线消息 chat_redis.save_msg(tid, to_user) # 保存聊天记录到MongoDB _add_chat(tid, to_user, send_str.get("data")) to_user = "" file_name = "" @app.route("/app/<uid>") def user_app(uid): # 手机app连接 user_socket = request.environ.get("wsgi.websocket") # type:WebSocket if user_socket: user_socket_dict[uid] = user_socket # { uid : websocket} print(user_socket_dict) file_name = "" to_user = "" while True: # 手机听歌 把歌曲发送给 玩具 1.将文件直接发送给玩具 2.将当前听的歌曲名称或ID发送到玩具 msg = user_socket.receive() if type(msg) == bytearray: # 判断类型为bytearray file_name = f"{uuid4()}.amr" # 文件后缀为amr,安卓和ios通用 file_path = os.path.join(CHAT_FILE, file_name) # 存放在chat目录 print(msg) with open(file_path, "wb") as f: f.write(msg) # 写入文件 # 将amr转换为mp3,因为html中的audio不支持amr os.system(f"ffmpeg -i {file_path} {file_path}.mp3") else: msg_dict = json.loads(msg) to_user = msg_dict.get("to_user") # 获取目标用户 if msg_dict.get("msg_type") == "music": other_user_socket = user_socket_dict.get(to_user) send_str = { "code": 0, "from_user": uid, "msg_type": "music", "data": msg_dict.get("data") } other_user_socket.send(json.dumps(send_str)) # res = content._content_one(content_id) if file_name and to_user: # 如果文件名和发送用户同上存在时 # 查询玩具信息 res = setting.MONGO_DB.toys.find_one({"_id": ObjectId(to_user)}) # 获取friend_remark fri = [i.get("friend_remark") for i in res.get("friend_list") if i.get("friend_id") == uid][0] msg_file_name = baidu_ai.text2audio(f"你有来自{fri}的消息") # 获取websocket对象 other_user_socket = user_socket_dict.get(to_user) # 构造数据 send_str = { "code": 0, "from_user": uid, "msg_type": "chat", # 聊天类型 # 后缀必须是mp3的 "data": msg_file_name } # 发送数据给前端页面 other_user_socket.send(json.dumps(send_str)) # 添加聊天记录到数据库 _add_chat(uid, to_user, f"{file_name}.mp3") # 最后一定要清空这2个变量,否则造成混乱 file_name = "" to_user = "" def _add_chat(sender, to_user, msg): # 添加聊天记录到数据库 chat_window = setting.MONGO_DB.chat.find_one({"user_list": {"$all": [sender, to_user]}}) if not chat_window.get("chat_list"): chat_window["chat_list"] = [{ "sender": sender, "msg": msg, "updated_at": time.time(), }] res = setting.MONGO_DB.chat.update_one({"_id": ObjectId(chat_window.get("_id"))}, {"$set": chat_window}) else: chat = { "sender": sender, "msg": msg, "updated_at": time.time(), } res = setting.MONGO_DB.chat.update_one({"_id": ObjectId(chat_window.get("_id"))}, {"$push": {"chat_list": chat}}) return res if __name__ == '__main__': # 创建一个WebSocket服务器 http_serv = WSGIServer(("0.0.0.0", 9528), app, handler_class=WebSocketHandler) # 开始监听HTTP请求 http_serv.serve_forever() ''' { "code": 0, "from_user": uid, # APP用户id "data": music_name # 歌曲名 } '''
重启 im_serv.py
打开网页,让 小甜甜 开机
点击 开始废话,使用麦克风说: 给小鱼 发消息。再点击发送语音!
此时,录制消息按钮后面,会出现 对方id
注意:小鱼 表示玩具对主人(xiao)的称呼
再点击 录制消息,说: hello。最后点击发送语音消息!
进入redis,查看消息,将网页中的 录制消息按钮后面的id复制过来
查看用户id的数据,可以发现数量为1
127.0.0.1:6379> get 5b9bb768e1253281608e96eb "{\"5ba0f1f2e12532418089bf88\": 1}" 127.0.0.1:6379>
那么redis中的数据有了,就可以在APP中渲染了
进入HBuilder项目MyApp,修改 index.html,增加角标
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <title></title> <script src="js/mui.js"></script> <link href="css/mui.min.css" rel="stylesheet" /> </head> <body> <!--底部选项卡--> <nav class="mui-bar mui-bar-tab"> <a class="mui-tab-item mui-active" id="index"> <span class="mui-icon mui-icon-home"></span> <span class="mui-tab-label">首页</span> </a> <a class="mui-tab-item" id="message"> <span class="mui-icon mui-icon-chat"> <span class="mui-badge mui-badge-red" id="msg_count">0</span> </span> <span class="mui-tab-label">消息</span> </a> <a class="mui-tab-item"> <span class="mui-icon mui-icon-email"></span> <span class="mui-tab-label">邮件</span> </a> <a class="mui-tab-item" id="login"> <span class="mui-icon mui-icon-gear"></span> <span class="mui-tab-label">设置</span> </a> </nav> </body> <script type="text/javascript" charset="utf-8"> var ws = null; // websocket对象 mui.init({ subpages: [{ url: "main.html", id: "main.html", styles: window.styles }] }); mui.plusReady(function() { // console.log(JSON.stringify(plus.webview.currentWebview())) if(plus.storage.getItem("user")) { // 判断是否登录 console.log('已结登录了!'); //连接websocket连接 ws = new WebSocket("ws://" + window.ws_serv + "/app/" + plus.storage.getItem("user")) // 发送post请求 console.log(window.serv + "/get_msg_list"); mui.post( // 访问消息列表 window.serv + "/get_msg_list", { user_id: plus.storage.getItem("user") }, function(data) { console.log(JSON.stringify(data)); msg_data = data.data; // 修改消息选项卡的角标数字 document.getElementById("msg_count").innerText = msg_data.count; } ); // 客户端接收服务端数据时触发 ws.onmessage = function() {}; } // 自动重连 ws.onclose = function() { window.location.reload(); } }); // 消息 document.getElementById("message").addEventListener("tap", function() { mui.openWindow({ url: "message.html", id: "message.html", styles: window.styles, extras: { // 传输用户id,给message.html user_id: plus.storage.getItem("user") } }) }); document.getElementById("index").addEventListener("tap", function() { mui.openWindow({ url: "main.html", id: "main.html", styles: window.styles }) }) document.getElementById("login").addEventListener("tap", function() { // 自动登录,判断storage中的user存在,就跳转到user_info,否则跳转login if(plus.storage.getItem("user")) { mui.openWindow({ url: "user_info.html", id: "user_info.html", styles: window.styles, extras: { user_id: plus.storage.getItem("user") } }) } else { mui.openWindow({ url: "login.html", id: "login.html", styles: window.styles }) } }) document.addEventListener("login", function(data) { // fire事件接收消息,使用data.detail // index是为做显示区分 mui.toast("index" + data.detail.msg) }); document.addEventListener("send_music", function(data) { //监听send_music事件 var music_name = data.detail.music_name; //获取player.html使用fire发送的music_name值 var toy_id = data.detail.toy_id; //获取发送的玩具id send_str = { //构造数据 data: music_name, to_user: toy_id, // 目标用户,这里统一格式 msg_type: "music", // 类型为音乐 } // 发送数据给后端,注意要json序列化 ws.send(JSON.stringify(send_str)); }); document.addEventListener("send_msg", function(data) { //发送消息 var filename = data.detail.filename var to_user = data.detail.to_user send_str = { to_user: to_user } ws.send(JSON.stringify(send_str)) plus.io.resolveLocalFileSystemURL(filename, function(entry) { // 可通过entry对象操作test.html文件 entry.file(function(file) { // FileReader文件系统中的读取文件对象,用于获取文件的内容 var fileReader = new plus.io.FileReader(); // alert("getFile:" + JSON.stringify(file)); // readAsDataURL: 以URL编码格式读取文件数据内容 fileReader.readAsDataURL(file, 'utf-8'); // onloadend: 文件读取操作完成时的回调函数 fileReader.onloadend = function(evt) { console.log(evt.target.result); var b = dataURLtoBlob(evt.target.result); ws.send(b); // 发送blob数据 } // alert(file.size + '--' + file.name) }); }); }) function dataURLtoBlob(dataurl) { // 数据转换为Blob // 逻辑很复杂,这里不解释了。直接用就可以了! var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--) { u8arr[n] = bstr.charCodeAt(n); } var a = new Blob([u8arr], { type: mime }); return a } </script> </html>
进入 flask项目,修改 serv-->chat.py
from flask import Blueprint, request, jsonify from setting import MONGO_DB from setting import RET from bson import ObjectId from utils import chat_redis cht = Blueprint("cht", __name__) @cht.route("/chat_list", methods=["POST"]) def chat_list(): # 聊天记录列表 user_id = request.form.get("user_id") friend_id = request.form.get("friend_id") print(friend_id) chat_window = MONGO_DB.chat.find_one({"user_list": {"$all": [user_id, friend_id]}}) fri = MONGO_DB.toys.find_one({"_id": ObjectId(friend_id)}) baby_name = fri.get("baby_name") cl = chat_window.get("chat_list") RET["code"] = 0 RET["msg"] = baby_name RET["data"] = cl return jsonify(RET) @cht.route("/get_msg", methods=["POST"]) def get_msg(): # 获取聊天语言文件 user_id = request.form.get("user_id") sender = request.form.get("sender") chat_window = MONGO_DB.chat.find_one({"user_list": {"$all": [user_id, sender]}}) new_msg = chat_window.get("chat_list")[-1] RET["code"] = 0 RET["msg"] = "" RET["data"] = new_msg.get("msg") return jsonify(RET) @cht.route("/get_msg_list", methods=["POST"]) def get_msg_list(): user_id = request.form.get("user_id") user_msg_dict = chat_redis.get_msg_list(user_id) RET["code"] = 0 RET["msg"] = "" RET["data"] = user_msg_dict return jsonify(RET)
重启 manager.py
打开模拟器,关闭里面的HBuilder进程,重新开启,效果如下:
效果就完成了!
聊天窗口显示未读消息
再来做 聊天窗口,显示角标。上面演示时,小甜甜 给 小鱼(主人) 发送了一条消息。
那么下图中的紫色框后面,应该显示数字1
那么如何实现呢?答案很简单,使用mui.openWindows打开message.html页面时,给它传一个参数msg_data。
参数大概是这个样子
"data":{"5ba0f1f2e12532418089bf88":1,"count":1}
5ba0f1f2e12532418089bf88 是 小甜甜 的_id。在toys表中可以查询!
修改 index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <title></title> <script src="js/mui.js"></script> <link href="css/mui.min.css" rel="stylesheet" /> </head> <body> <!--底部选项卡--> <nav class="mui-bar mui-bar-tab"> <a class="mui-tab-item mui-active" id="index"> <span class="mui-icon mui-icon-home"></span> <span class="mui-tab-label">首页</span> </a> <a class="mui-tab-item" id="message"> <span class="mui-icon mui-icon-chat"> <span class="mui-badge mui-badge-red" id="msg_count">0</span> </span> <span class="mui-tab-label">消息</span> </a> <a class="mui-tab-item"> <span class="mui-icon mui-icon-email"></span> <span class="mui-tab-label">邮件</span> </a> <a class="mui-tab-item" id="login"> <span class="mui-icon mui-icon-gear"></span> <span class="mui-tab-label">设置</span> </a> </nav> </body> <script type="text/javascript" charset="utf-8"> var ws = null; // websocket对象 var msg_data = null; // 消息数据 mui.init({ subpages: [{ url: "main.html", id: "main.html", styles: window.styles }] }); mui.plusReady(function() { // console.log(JSON.stringify(plus.webview.currentWebview())) if(plus.storage.getItem("user")) { // 判断是否登录 console.log('已结登录了!'); //连接websocket连接 ws = new WebSocket("ws://" + window.ws_serv + "/app/" + plus.storage.getItem("user")) // 发送post请求 console.log(window.serv + "/get_msg_list"); mui.post( // 访问消息列表 window.serv + "/get_msg_list", { user_id: plus.storage.getItem("user") }, function(data) { console.log(JSON.stringify(data)); // {"code":0,"data":{"5ba0f1f2e12532418089bf88":1,"count":1},"msg":""} msg_data = data.data; // 修改消息选项卡的角标数字 document.getElementById("msg_count").innerText = msg_data.count; } ); // 客户端接收服务端数据时触发 ws.onmessage = function() {}; } // 自动重连 ws.onclose = function() { window.location.reload(); } }); // 消息 document.getElementById("message").addEventListener("tap", function() { mui.openWindow({ url: "message.html", id: "message.html", styles: window.styles, extras: { // 传输用户id,给message.html user_id: plus.storage.getItem("user"), msg_data: msg_data, // "data":{"5ba0f1f2e12532418089bf88":1,"count":1} } }) }); document.getElementById("index").addEventListener("tap", function() { mui.openWindow({ url: "main.html", id: "main.html", styles: window.styles }) }) document.getElementById("login").addEventListener("tap", function() { // 自动登录,判断storage中的user存在,就跳转到user_info,否则跳转login if(plus.storage.getItem("user")) { mui.openWindow({ url: "user_info.html", id: "user_info.html", styles: window.styles, extras: { user_id: plus.storage.getItem("user") } }) } else { mui.openWindow({ url: "login.html", id: "login.html", styles: window.styles }) } }) document.addEventListener("login", function(data) { // fire事件接收消息,使用data.detail // index是为做显示区分 mui.toast("index" + data.detail.msg) }); document.addEventListener("send_music", function(data) { //监听send_music事件 var music_name = data.detail.music_name; //获取player.html使用fire发送的music_name值 var toy_id = data.detail.toy_id; //获取发送的玩具id send_str = { //构造数据 data: music_name, to_user: toy_id, // 目标用户,这里统一格式 msg_type: "music", // 类型为音乐 } // 发送数据给后端,注意要json序列化 ws.send(JSON.stringify(send_str)); }); document.addEventListener("send_msg", function(data) { //发送消息 var filename = data.detail.filename var to_user = data.detail.to_user send_str = { to_user: to_user } ws.send(JSON.stringify(send_str)) plus.io.resolveLocalFileSystemURL(filename, function(entry) { // 可通过entry对象操作test.html文件 entry.file(function(file) { // FileReader文件系统中的读取文件对象,用于获取文件的内容 var fileReader = new plus.io.FileReader(); // alert("getFile:" + JSON.stringify(file)); // readAsDataURL: 以URL编码格式读取文件数据内容 fileReader.readAsDataURL(file, 'utf-8'); // onloadend: 文件读取操作完成时的回调函数 fileReader.onloadend = function(evt) { console.log(evt.target.result); var b = dataURLtoBlob(evt.target.result); ws.send(b); // 发送blob数据 } // alert(file.size + '--' + file.name) }); }); }) function dataURLtoBlob(dataurl) { // 数据转换为Blob // 逻辑很复杂,这里不解释了。直接用就可以了! var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--) { u8arr[n] = bstr.charCodeAt(n); } var a = new Blob([u8arr], { type: mime }); return a } </script> </html>
修改 message.html,渲染页面。create_content多加一个参数
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <link rel="stylesheet" type="text/css" href="css/mui.css" /> </head> <body> <header class="mui-bar mui-bar-nav"> <a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a> <h1 class="mui-title">我的好友</h1> </header> <div class="mui-content"> <ul class="mui-table-view" id="friend_list"> </ul> </div> </body> <script src="js/mui.js" type="text/javascript" charset="utf-8"></script> <script type="text/javascript"> mui.init() var Sdata = null; mui.back = function(){}; // 加载HTML5Puls mui.plusReady(function() { Sdata = plus.webview.currentWebview(); // post请求 mui.post( // 好友列表 window.serv + "/friend_list", {user_id:Sdata.user_id}, function(data){ console.log(JSON.stringify(data)); // 循环好友列表 for (var i = 0; i < data.data.length; i++) { // 执行自定义方法,渲染页面 create_content(data.data[i],Sdata.msg_data); } } ) }); function create_content(content,msg_data){ // <li class="mui-table-view-cell mui-media"> // <a href="javascript:;"> // <img class="mui-media-object mui-pull-left" src="../images/shuijiao.jpg"> // <div class="mui-media-body"> // 幸福 // <p class='mui-ellipsis'>能和心爱的人一起睡觉,是件幸福的事情;可是,打呼噜怎么办?</p> // </div> // </a> // </li> // 角标 var spantag = document.createElement("span"); spantag.className = "mui-badge mui-badge-red"; // content是一个字典,要获取friend_id。不能使用get,只能使用. // 如果获取不到,值为undefine spantag.innerText = msg_data[content.friend_id] var litag = document.createElement("li"); litag.className = "mui-table-view-cell mui-media"; var atag = document.createElement("a"); atag.id = content.friend_id; // 点击事件 atag.onclick = function(){ console.log(this.id); open_chat(this.id); //执行自定义方法open_chat } var imgtag = document.createElement("img"); imgtag.className = "mui-media-object mui-pull-left"; imgtag.src = "avatar/" + content.friend_avatar; var divtag = document.createElement("div"); divtag.className = "mui-media-body"; divtag.innerText = content.friend_remark; var ptag = document.createElement("p"); ptag.className = "mui-ellipsis"; ptag.innerText = content.friend_name; litag.appendChild(atag); atag.appendChild(imgtag); atag.appendChild(divtag); atag.appendChild(spantag); divtag.appendChild(ptag); document.getElementById("friend_list").appendChild(litag); } function open_chat(friend_id){ // 打开chat.html mui.openWindow({ url:"chat.html", id:"chat.html", extras:{ // 传参给chat.html friend_id:friend_id } }) } </script> </html>
使用模拟器重新访问,效果如下:
发现了 undefined,这个不应该出现角标。这个是一个已知的bug,有兴趣的人,可以修改一下。
提示:使用css中的display:none
角标重置
点击 小甜甜,再返回页面时,这里的角标应该重置为0。并且不显示才对。
前端操作
修改 message.html,触发点击事件时,角标重置为0
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <link rel="stylesheet" type="text/css" href="css/mui.css" /> </head> <body> <header class="mui-bar mui-bar-nav"> <a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a> <h1 class="mui-title">我的好友</h1> </header> <div class="mui-content"> <ul class="mui-table-view" id="friend_list"> </ul> </div> </body> <script src="js/mui.js" type="text/javascript" charset="utf-8"></script> <script type="text/javascript"> mui.init() var Sdata = null; mui.back = function(){}; // 加载HTML5Puls mui.plusReady(function() { Sdata = plus.webview.currentWebview(); // post请求 mui.post( // 好友列表 window.serv + "/friend_list", {user_id:Sdata.user_id}, function(data){ console.log(JSON.stringify(data)); // 循环好友列表 for (var i = 0; i < data.data.length; i++) { // 执行自定义方法,渲染页面 create_content(data.data[i],Sdata.msg_data); } } ) }); function create_content(content,msg_data){ // <li class="mui-table-view-cell mui-media"> // <a href="javascript:;"> // <img class="mui-media-object mui-pull-left" src="../images/shuijiao.jpg"> // <div class="mui-media-body"> // 幸福 // <p class='mui-ellipsis'>能和心爱的人一起睡觉,是件幸福的事情;可是,打呼噜怎么办?</p> // </div> // </a> // </li> // 角标 var spantag = document.createElement("span"); spantag.className = "mui-badge mui-badge-red"; // content是一个字典,要获取friend_id。不能使用get,只能使用. // 如果获取不到,值为undefine spantag.innerText = msg_data[content.friend_id] var litag = document.createElement("li"); litag.className = "mui-table-view-cell mui-media"; var atag = document.createElement("a"); atag.id = content.friend_id; // 点击事件 atag.onclick = function(){ console.log(this.id); spantag.innerText = 0; // 重置为0 //执行自定义方法open_chat open_chat(this.id); } var imgtag = document.createElement("img"); imgtag.className = "mui-media-object mui-pull-left"; imgtag.src = "avatar/" + content.friend_avatar; var divtag = document.createElement("div"); divtag.className = "mui-media-body"; divtag.innerText = content.friend_remark; var ptag = document.createElement("p"); ptag.className = "mui-ellipsis"; ptag.innerText = content.friend_name; litag.appendChild(atag); atag.appendChild(imgtag); atag.appendChild(divtag); atag.appendChild(spantag); divtag.appendChild(ptag); document.getElementById("friend_list").appendChild(litag); } function open_chat(friend_id){ // 打开chat.html mui.openWindow({ url:"chat.html", id:"chat.html", extras:{ // 传参给chat.html friend_id:friend_id } }) } </script> </html>
使用模拟器访问,效果如下:
可以发现,聊天列表返回时,已经重置为0了。但是底部现象卡还没有变动!莫急,下面来处理它。
怎么实现呢?由于index.html页面是母模,它只负责显示底部选项卡。
在chat.html页面给index.html,执行一个fire(开火)事件就可以了!但是:一个页面,只能fire一次。
由于chat.html已经存在了一个fire事件。所以只能在message.html做fire
修改 message.html,增加fire
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <link rel="stylesheet" type="text/css" href="css/mui.css" /> </head> <body> <header class="mui-bar mui-bar-nav"> <a class="mui-action-back mui-icon mui-icon-left-nav mui-pull-left"></a> <h1 class="mui-title">我的好友</h1> </header> <div class="mui-content"> <ul class="mui-table-view" id="friend_list"> </ul> </div> </body> <script src="js/mui.js" type="text/javascript" charset="utf-8"></script> <script type="text/javascript"> mui.init() var Sdata = null; mui.back = function(){}; // 加载HTML5Puls mui.plusReady(function() { Sdata = plus.webview.currentWebview(); // post请求 mui.post( // 好友列表 window.serv + "/friend_list", {user_id:Sdata.user_id}, function(data){ console.log(JSON.stringify(data)); // 循环好友列表 for (var i = 0; i < data.data.length; i++) { // 执行自定义方法,渲染页面 create_content(data.data[i],Sdata.msg_data); } } ) }); function create_content(content,msg_data){ // <li class="mui-table-view-cell mui-media"> // <a href="javascript:;"> // <img class="mui-media-object mui-pull-left" src="../images/shuijiao.jpg"> // <div class="mui-media-body"> // 幸福 // <p class='mui-ellipsis'>能和心爱的人一起睡觉,是件幸福的事情;可是,打呼噜怎么办?</p> // </div> // </a> // </li> // 角标 var spantag = document.createElement("span"); spantag.className = "mui-badge mui-badge-red"; // content是一个字典,要获取friend_id。不能使用get,只能使用. // 如果获取不到,值为undefine spantag.innerText = msg_data[content.friend_id] var litag = document.createElement("li"); litag.className = "mui-table-view-cell mui-media"; var atag = document.createElement("a"); atag.id = content.friend_id; // 点击事件 atag.onclick = function(){ // console.log(this.id); //执行自定义方法open_chat open_chat(this.id,spantag.innerText); spantag.innerText = 0; // 重置为0 } var imgtag = document.createElement("img"); imgtag.className = "mui-media-object mui-pull-left"; imgtag.src = "avatar/" + content.friend_avatar; var divtag = document.createElement("div"); divtag.className = "mui-media-body"; divtag.innerText = content.friend_remark; var ptag = document.createElement("p"); ptag.className = "mui-ellipsis"; ptag.innerText = content.friend_name; litag.appendChild(atag); atag.appendChild(imgtag); atag.appendChild(divtag); atag.appendChild(spantag); divtag.appendChild(ptag); document.getElementById("friend_list").appendChild(litag); } function open_chat(friend_id,cut_count){ // 打开chat.html // 获取index.html var index = plus.webview.getWebviewById("HBuilder") // 执行fire mui.fire(index,"cut_msg_count",{cut:cut_count}) mui.openWindow({ url:"chat.html", id:"chat.html", extras:{ // 传参给chat.html friend_id:friend_id } }) } </script> </html>
修改 index.html,监听 cut_msg_count事件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <title></title> <script src="js/mui.js"></script> <link href="css/mui.min.css" rel="stylesheet" /> </head> <body> <!--底部选项卡--> <nav class="mui-bar mui-bar-tab"> <a class="mui-tab-item mui-active" id="index"> <span class="mui-icon mui-icon-home"></span> <span class="mui-tab-label">首页</span> </a> <a class="mui-tab-item" id="message"> <span class="mui-icon mui-icon-chat"> <span class="mui-badge mui-badge-red" id="msg_count">0</span> </span> <span class="mui-tab-label">消息</span> </a> <a class="mui-tab-item"> <span class="mui-icon mui-icon-email"></span> <span class="mui-tab-label">邮件</span> </a> <a class="mui-tab-item" id="login"> <span class="mui-icon mui-icon-gear"></span> <span class="mui-tab-label">设置</span> </a> </nav> </body> <script type="text/javascript" charset="utf-8"> var ws = null; // websocket对象 var msg_data = null; // 消息数据 mui.init({ subpages: [{ url: "main.html", id: "main.html", styles: window.styles }] }); mui.plusReady(function() { // console.log(JSON.stringify(plus.webview.currentWebview())) if(plus.storage.getItem("user")) { // 判断是否登录 console.log('已结登录了!'); //连接websocket连接 ws = new WebSocket("ws://" + window.ws_serv + "/app/" + plus.storage.getItem("user")) // 发送post请求 console.log(window.serv + "/get_msg_list"); mui.post( // 访问消息列表 window.serv + "/get_msg_list", { user_id: plus.storage.getItem("user") }, function(data) { console.log(JSON.stringify(data)); // {"code":0,"data":{"5ba0f1f2e12532418089bf88":1,"count":1},"msg":""} msg_data = data.data; // 修改消息选项卡的角标数字 document.getElementById("msg_count").innerText = msg_data.count; } ); // 客户端接收服务端数据时触发 ws.onmessage = function() {}; } // 自动重连 ws.onclose = function() { window.location.reload(); } }); // 消息 document.getElementById("message").addEventListener("tap", function() { mui.openWindow({ url: "message.html", id: "message.html", styles: window.styles, extras: { // 传输用户id,给message.html user_id: plus.storage.getItem("user"), msg_data: msg_data, // "data":{"5ba0f1f2e12532418089bf88":1,"count":1} } }) }); document.getElementById("index").addEventListener("tap", function() { mui.openWindow({ url: "main.html", id: "main.html", styles: window.styles }) }) document.getElementById("login").addEventListener("tap", function() { // 自动登录,判断storage中的user存在,就跳转到user_info,否则跳转login if(plus.storage.getItem("user")) { mui.openWindow({ url: "user_info.html", id: "user_info.html", styles: window.styles, extras: { user_id: plus.storage.getItem("user") } }) } else { mui.openWindow({ url: "login.html", id: "login.html", styles: window.styles }) } }) document.addEventListener("login", function(data) { // fire事件接收消息,使用data.detail // index是为做显示区分 mui.toast("index" + data.detail.msg) }); document.addEventListener("send_music", function(data) { //监听send_music事件 var music_name = data.detail.music_name; //获取player.html使用fire发送的music_name值 var toy_id = data.detail.toy_id; //获取发送的玩具id send_str = { //构造数据 data: music_name, to_user: toy_id, // 目标用户,这里统一格式 msg_type: "music", // 类型为音乐 } // 发送数据给后端,注意要json序列化 ws.send(JSON.stringify(send_str)); }); document.addEventListener("send_msg", function(data) { //发送消息 var filename = data.detail.filename var to_user = data.detail.to_user send_str = { to_user: to_user } ws.send(JSON.stringify(send_str)) plus.io.resolveLocalFileSystemURL(filename, function(entry) { // 可通过entry对象操作test.html文件 entry.file(function(file) { // FileReader文件系统中的读取文件对象,用于获取文件的内容 var fileReader = new plus.io.FileReader(); // alert("getFile:" + JSON.stringify(file)); // readAsDataURL: 以URL编码格式读取文件数据内容 fileReader.readAsDataURL(file, 'utf-8'); // onloadend: 文件读取操作完成时的回调函数 fileReader.onloadend = function(evt) { console.log(evt.target.result); var b = dataURLtoBlob(evt.target.result); ws.send(b); // 发送blob数据 } // alert(file.size + '--' + file.name) }); }); }); // 监听cut_msg_count事件,由message.html向index.html执行fire document.addEventListener("cut_msg_count", function(data) { var msg_count = document.getElementById("msg_count"); var cut = parseInt(data.detail.cut); // parseInt表示强制转换 var count = parseInt(msg_count.innerText); // 默认获取innerText是字符串,需要强制转换 msg_count.innerText = count - cut; // 总数 减去 点击聊天会话的数量,比如小甜甜的 }); function dataURLtoBlob(dataurl) { // 数据转换为Blob // 逻辑很复杂,这里不解释了。直接用就可以了! var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--) { u8arr[n] = bstr.charCodeAt(n); } var a = new Blob([u8arr], { type: mime }); return a } </script> </html>
效果如下:
可以发现,底部选项卡,也变成0了
后端操作
那是因为后端redis的数据没有更改。
进入flask项目,修改 utils-->chat_redis.py
from setting import REDIS_DB import json def save_msg(sender, to_user): # 保存消息 # 1.查询一下xiao的Redis是否有数据 user_msg_redis = REDIS_DB.get(to_user) if user_msg_redis: # 2.将xiao的数据反序列化成字典 { sender : n } user_msg_dict = json.loads(user_msg_redis) # 3.判断有没有 sender 的用户发来的消息数量 if user_msg_dict.get(sender): # 数量加1 user_msg_dict[sender] += 1 else: # 第一次,初始值为1 user_msg_dict[sender] = 1 # 4.如果xiao是刚建立好的用户,他是没有消息的,字典是空 else: user_msg_dict = {sender: 1} # 5.序列化用户消息字典user_msg_dict user_msg_redis = json.dumps(user_msg_dict) # 6.存回Redis REDIS_DB.set(to_user, user_msg_redis) def get_msg_list(user): # 获取消息 user_msg_redis = REDIS_DB.get(user) if user_msg_redis: user_msg_dict = json.loads(user_msg_redis) # 统计数量 user_msg_dict["count"] = sum(user_msg_dict.values()) else: user_msg_dict = {"count":0} return user_msg_dict def get_user_msg_one(sender, to_user): # 获取用户一个好友消息 user_msg_redis = REDIS_DB.get(to_user) if user_msg_redis: user_msg_dict = json.loads(user_msg_redis) if user_msg_dict.get(sender): # return user_msg_dict.get(sender) user_msg_dict[sender] = 0 else: user_msg_dict = {sender:0} user_msg_redis = json.dumps(user_msg_dict) REDIS_DB.set(to_user,user_msg_redis) # 修改redis
修改 serv-->chat.py,增加 chat_redis.get_user_msg_one
from flask import Blueprint, request, jsonify from setting import MONGO_DB from setting import RET from bson import ObjectId from utils import chat_redis cht = Blueprint("cht", __name__) @cht.route("/chat_list", methods=["POST"]) def chat_list(): # 聊天记录列表 user_id = request.form.get("user_id") friend_id = request.form.get("friend_id") print(friend_id) chat_window = MONGO_DB.chat.find_one({"user_list": {"$all": [user_id, friend_id]}}) fri = MONGO_DB.toys.find_one({"_id": ObjectId(friend_id)}) baby_name = fri.get("baby_name") cl = chat_window.get("chat_list") RET["code"] = 0 RET["msg"] = baby_name RET["data"] = cl # 获取用户单个好友记录,修改redis的值 chat_redis.get_user_msg_one(friend_id,user_id) return jsonify(RET) @cht.route("/get_msg", methods=["POST"]) def get_msg(): # 获取聊天语言文件 user_id = request.form.get("user_id") sender = request.form.get("sender") chat_window = MONGO_DB.chat.find_one({"user_list": {"$all": [user_id, sender]}}) new_msg = chat_window.get("chat_list")[-1] RET["code"] = 0 RET["msg"] = "" RET["data"] = new_msg.get("msg") return jsonify(RET) @cht.route("/get_msg_list", methods=["POST"]) def get_msg_list(): user_id = request.form.get("user_id") user_msg_dict = chat_redis.get_msg_list(user_id) RET["code"] = 0 RET["msg"] = "" RET["data"] = user_msg_dict return jsonify(RET)
打开夜神模拟器,重启 里面的HBuilder APP。再次点击,查看redis
127.0.0.1:6379> get 5b9bb768e1253281608e96eb "{\"5ba0f1f2e12532418089bf88\": 0}"
发现已经更新为0了
消息增加
但是发消息,可能不止一条。如果有消息,角标的数字应该自动加。
进入 HBuilder项目MyApp,修改index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <title></title> <script src="js/mui.js"></script> <link href="css/mui.min.css" rel="stylesheet" /> </head> <body> <!--底部选项卡--> <nav class="mui-bar mui-bar-tab"> <a class="mui-tab-item mui-active" id="index"> <span class="mui-icon mui-icon-home"></span> <span class="mui-tab-label">首页</span> </a> <a class="mui-tab-item" id="message"> <span class="mui-icon mui-icon-chat"> <span class="mui-badge mui-badge-red" id="msg_count">0</span> </span> <span class="mui-tab-label">消息</span> </a> <a class="mui-tab-item"> <span class="mui-icon mui-icon-email"></span> <span class="mui-tab-label">邮件</span> </a> <a class="mui-tab-item" id="login"> <span class="mui-icon mui-icon-gear"></span> <span class="mui-tab-label">设置</span> </a> </nav> </body> <script type="text/javascript" charset="utf-8"> var ws = null; // websocket对象 var msg_data = null; // 消息数据 mui.init({ subpages: [{ url: "main.html", id: "main.html", styles: window.styles }] }); mui.plusReady(function() { // console.log(JSON.stringify(plus.webview.currentWebview())) if(plus.storage.getItem("user")) { // 判断是否登录 console.log('已结登录了!'); //连接websocket连接 ws = new WebSocket("ws://" + window.ws_serv + "/app/" + plus.storage.getItem("user")) // 发送post请求 console.log(window.serv + "/get_msg_list"); mui.post( // 访问消息列表 window.serv + "/get_msg_list", { user_id: plus.storage.getItem("user") }, function(data) { console.log(JSON.stringify(data)); // {"code":0,"data":{"5ba0f1f2e12532418089bf88":1,"count":1},"msg":""} msg_data = data.data; // 修改消息选项卡的角标数字 document.getElementById("msg_count").innerText = msg_data.count; } ); // 客户端接收服务端数据时触发 ws.onmessage = function(data) { console.log(data.data); var msg = JSON.parse(data.data); var chat = plus.webview.getWebviewById("chat.html"); mui.fire(chat, "new_msg", { // 向chat.html传值 data: msg }); var msg_count = document.getElementById("msg_count"); // 当前页面加1 msg_count.innerText = parseInt(msg_count.innerText) + 1; // 加1,用于message.html显示 msg_data[msg.from_user]++; }; } // 自动重连 ws.onclose = function() { window.location.reload(); } }); // 消息 document.getElementById("message").addEventListener("tap", function() { mui.openWindow({ url: "message.html", id: "message.html", styles: window.styles, extras: { // 传输用户id,给message.html user_id: plus.storage.getItem("user"), msg_data: msg_data, // "data":{"5ba0f1f2e12532418089bf88":1,"count":1} } }) }); document.getElementById("index").addEventListener("tap", function() { mui.openWindow({ url: "main.html", id: "main.html", styles: window.styles }) }) document.getElementById("login").addEventListener("tap", function() { // 自动登录,判断storage中的user存在,就跳转到user_info,否则跳转login if(plus.storage.getItem("user")) { mui.openWindow({ url: "user_info.html", id: "user_info.html", styles: window.styles, extras: { user_id: plus.storage.getItem("user") } }) } else { mui.openWindow({ url: "login.html", id: "login.html", styles: window.styles }) } }) document.addEventListener("login", function(data) { // fire事件接收消息,使用data.detail // index是为做显示区分 mui.toast("index" + data.detail.msg) }); document.addEventListener("send_music", function(data) { //监听send_music事件 var music_name = data.detail.music_name; //获取player.html使用fire发送的music_name值 var toy_id = data.detail.toy_id; //获取发送的玩具id send_str = { //构造数据 data: music_name, to_user: toy_id, // 目标用户,这里统一格式 msg_type: "music", // 类型为音乐 } // 发送数据给后端,注意要json序列化 ws.send(JSON.stringify(send_str)); }); document.addEventListener("send_msg", function(data) { //发送消息 var filename = data.detail.filename var to_user = data.detail.to_user send_str = { to_user: to_user } ws.send(JSON.stringify(send_str)) plus.io.resolveLocalFileSystemURL(filename, function(entry) { // 可通过entry对象操作test.html文件 entry.file(function(file) { // FileReader文件系统中的读取文件对象,用于获取文件的内容 var fileReader = new plus.io.FileReader(); // alert("getFile:" + JSON.stringify(file)); // readAsDataURL: 以URL编码格式读取文件数据内容 fileReader.readAsDataURL(file, 'utf-8'); // onloadend: 文件读取操作完成时的回调函数 fileReader.onloadend = function(evt) { console.log(evt.target.result); var b = dataURLtoBlob(evt.target.result); ws.send(b); // 发送blob数据 } // alert(file.size + '--' + file.name) }); }); }); // 监听cut_msg_count事件,由message.html向index.html执行fire document.addEventListener("cut_msg_count", function(data) { var msg_count = document.getElementById("msg_count"); var cut = parseInt(data.detail.cut); // parseInt表示强制转换 var count = parseInt(msg_count.innerText); // 默认获取innerText是字符串,需要强制转换 msg_count.innerText = count - cut; // 总数 减去 点击聊天会话的数量,比如小甜甜的 }); function dataURLtoBlob(dataurl) { // 数据转换为Blob // 逻辑很复杂,这里不解释了。直接用就可以了! var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while(n--) { u8arr[n] = bstr.charCodeAt(n); } var a = new Blob([u8arr], { type: mime }); return a } </script> </html>
打开网页,发送消息
发送2条消息
这里会实时变化
这里还是0,是因为这个页面的plusReady只会加载一次。这个是一个小bug
如果关闭进程,再次开启,就会有了!
进入 flask后端,修改im_serv.py
from flask import Flask, request from geventwebsocket.websocket import WebSocket from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer import json, os from uuid import uuid4 from setting import AUDIO_FILE,CHAT_FILE from serv import content from utils import baidu_ai from utils import chat_redis import setting from bson import ObjectId import time app = Flask(__name__) user_socket_dict = {} # 空字典,用来存放用户名和发送消息 @app.route("/toy/<tid>") def toy(tid): # 玩具连接 # 获取请求的WebSocket对象 user_socket = request.environ.get("wsgi.websocket") # type:WebSocket if user_socket: # 设置键值对 user_socket_dict[tid] = user_socket print(user_socket_dict) # {'123456': <geventwebsocket.websocket.WebSocket object at 0x00000176ABD92E18>} file_name = "" to_user = "" # 循环,接收消息 while True: msg = user_socket.receive() if type(msg) == bytearray: file_name = f"{uuid4()}.wav" file_path = os.path.join(CHAT_FILE, file_name) with open(file_path, "wb") as f: f.write(msg) else: msg_dict = json.loads(msg) to_user = msg_dict.get("to_user") msg_type = msg_dict.get("msg_type") if to_user and file_name: other_user_socket = user_socket_dict.get(to_user) if msg_type == "ai": q = baidu_ai.audio2text(file_path) print(q) ret = baidu_ai.my_nlp(q, tid) other_user_socket.send(json.dumps(ret)) else: send_str = { "code": 0, "from_user": tid, "msg_type": "chat", "data": file_name } if other_user_socket: # 当websocket连接存在时 chat_redis.save_msg(tid, to_user) # 保存消息到redis # 发送数据 other_user_socket.send(json.dumps(send_str)) else: # 离线消息 chat_redis.save_msg(tid, to_user) # 保存聊天记录到MongoDB _add_chat(tid, to_user, send_str.get("data")) to_user = "" file_name = "" @app.route("/app/<uid>") def user_app(uid): # 手机app连接 user_socket = request.environ.get("wsgi.websocket") # type:WebSocket if user_socket: user_socket_dict[uid] = user_socket # { uid : websocket} print(user_socket_dict) file_name = "" to_user = "" while True: # 手机听歌 把歌曲发送给 玩具 1.将文件直接发送给玩具 2.将当前听的歌曲名称或ID发送到玩具 msg = user_socket.receive() if type(msg) == bytearray: # 判断类型为bytearray file_name = f"{uuid4()}.amr" # 文件后缀为amr,安卓和ios通用 file_path = os.path.join(CHAT_FILE, file_name) # 存放在chat目录 print(msg) with open(file_path, "wb") as f: f.write(msg) # 写入文件 # 将amr转换为mp3,因为html中的audio不支持amr os.system(f"ffmpeg -i {file_path} {file_path}.mp3") else: msg_dict = json.loads(msg) to_user = msg_dict.get("to_user") # 获取目标用户 if msg_dict.get("msg_type") == "music": other_user_socket = user_socket_dict.get(to_user) send_str = { "code": 0, "from_user": uid, "msg_type": "music", "data": msg_dict.get("data") } other_user_socket.send(json.dumps(send_str)) # res = content._content_one(content_id) if file_name and to_user: # 如果文件名和发送用户同上存在时 # 查询玩具信息 res = setting.MONGO_DB.toys.find_one({"_id": ObjectId(to_user)}) # 获取friend_remark fri = [i.get("friend_remark") for i in res.get("friend_list") if i.get("friend_id") == uid][0] msg_file_name = baidu_ai.text2audio(f"你有来自{fri}的消息") # 获取websocket对象 other_user_socket = user_socket_dict.get(to_user) # 构造数据 send_str = { "code": 0, "from_user": uid, "msg_type": "chat", # 聊天类型 # 后缀必须是mp3的 "data": msg_file_name } if other_user_socket: chat_redis.save_msg(uid, to_user) # 发送数据给前端页面 other_user_socket.send(json.dumps(send_str)) else: # 保存redis chat_redis.save_msg(uid, to_user) # 添加聊天记录到数据库 _add_chat(uid, to_user, f"{file_name}.mp3") # 最后一定要清空这2个变量,否则造成混乱 file_name = "" to_user = "" def _add_chat(sender, to_user, msg): # 添加聊天记录到数据库 chat_window = setting.MONGO_DB.chat.find_one({"user_list": {"$all": [sender, to_user]}}) if not chat_window.get("chat_list"): chat_window["chat_list"] = [{ "sender": sender, "msg": msg, "updated_at": time.time(), }] res = setting.MONGO_DB.chat.update_one({"_id": ObjectId(chat_window.get("_id"))}, {"$set": chat_window}) else: chat = { "sender": sender, "msg": msg, "updated_at": time.time(), } res = setting.MONGO_DB.chat.update_one({"_id": ObjectId(chat_window.get("_id"))}, {"$push": {"chat_list": chat}}) return res if __name__ == '__main__': # 创建一个WebSocket服务器 http_serv = WSGIServer(("0.0.0.0", 9528), app, handler_class=WebSocketHandler) # 开始监听HTTP请求 http_serv.serve_forever() ''' { "code": 0, "from_user": uid, # APP用户id "data": music_name # 歌曲名 } '''
给小甜甜发送消息,可能不止一条。后端收取消息,要有多条
修改 serv-->chat.py,改为[-count:] 。如果是2条,就是[-2:]。表示最后2条!
from flask import Blueprint, request, jsonify from setting import MONGO_DB from setting import RET from bson import ObjectId from utils import chat_redis cht = Blueprint("cht", __name__) @cht.route("/chat_list", methods=["POST"]) def chat_list(): # 聊天记录列表 user_id = request.form.get("user_id") friend_id = request.form.get("friend_id") print(friend_id) chat_window = MONGO_DB.chat.find_one({"user_list": {"$all": [user_id, friend_id]}}) fri = MONGO_DB.toys.find_one({"_id": ObjectId(friend_id)}) baby_name = fri.get("baby_name") cl = chat_window.get("chat_list") RET["code"] = 0 RET["msg"] = baby_name RET["data"] = cl # 获取用户单个好友记录,修改redis的值 chat_redis.get_user_msg_one(friend_id,user_id) return jsonify(RET) @cht.route("/get_msg", methods=["POST"]) def get_msg(): # 获取聊天语言文件 user_id = request.form.get("user_id") sender = request.form.get("sender") count = 1 # 初始值 if not sender: msg_dict = chat_redis.get_msg_list(user_id) print(msg_dict,"msg_dict") sender = list(msg_dict.keys())[0] count = msg_dict[sender] else: # 获取用户某个好友的值 count = chat_redis.get_user_msg_one(sender,user_id) # $all 表示多个条件都成立时。这里表示user_list字段中user_id和sender必须都存在才行! chat_window = MONGO_DB.chat.find_one({"user_list": {"$all": [user_id, sender]}}) # [-count:] 表示获取最后的几条消息。比如: -1: 表示最后一条 new_msg = chat_window.get("chat_list")[-count:] RET["code"] = 0 RET["msg"] = "" RET["data"] = new_msg # chat_redis.get_user_msg_one(sender,user_id) return jsonify(RET) @cht.route("/get_msg_list", methods=["POST"]) def get_msg_list(): user_id = request.form.get("user_id") user_msg_dict = chat_redis.get_msg_list(user_id) RET["code"] = 0 RET["msg"] = "" RET["data"] = user_msg_dict return jsonify(RET)
修改 utils-->chat_redis.py
from setting import REDIS_DB import json def save_msg(sender, to_user): # 保存消息 # 1.查询一下xiao的Redis是否有数据 user_msg_redis = REDIS_DB.get(to_user) if user_msg_redis: # 2.将xiao的数据反序列化成字典 { sender : n } user_msg_dict = json.loads(user_msg_redis) # 3.判断有没有 sender 的用户发来的消息数量 if user_msg_dict.get(sender): # 数量加1 user_msg_dict[sender] += 1 else: # 第一次,初始值为1 user_msg_dict[sender] = 1 # 4.如果xiao是刚建立好的用户,他是没有消息的,字典是空 else: user_msg_dict = {sender: 1} # 5.序列化用户消息字典user_msg_dict user_msg_redis = json.dumps(user_msg_dict) # 6.存回Redis REDIS_DB.set(to_user, user_msg_redis) def get_msg_list(user): # 获取消息 user_msg_redis = REDIS_DB.get(user) if user_msg_redis: user_msg_dict = json.loads(user_msg_redis) # 统计数量 user_msg_dict["count"] = sum(user_msg_dict.values()) else: user_msg_dict = {"count":0} return user_msg_dict def get_user_msg_one(sender, to_user): # 获取用户一个好友消息 user_msg_redis = REDIS_DB.get(to_user) if user_msg_redis: user_msg_dict = json.loads(user_msg_redis) if user_msg_dict.get(sender): return user_msg_dict.get(sender) # user_msg_dict[sender] = 0 else: user_msg_dict = {sender:0} user_msg_redis = json.dumps(user_msg_dict) REDIS_DB.set(to_user,user_msg_redis) # 修改redis
修改index.html,使用player.onended。它会接收多条
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <audio src="" autoplay="autoplay" controls id="player"></audio> <br> <input type="text" id="device_id"/> <button onclick="start_toy()">玩具开机键</button> <br> <button onclick="start_reco()">开始废话</button> <br> <button onclick="stop_reco()">发送语音</button> <br> <button onclick="start_reco()">录制消息</button> <span id="to_user"></span> <br> <button onclick="send_reco()">发送语音消息</button> <br> <button onclick="recv_msg()">收取消息</button> </body> <script src="/static/recorder.js"></script> <script src="/static/jquery.min.js"></script> <script type="application/javascript"> var serv = "http://127.0.0.1:9527"; var ws_serv = "ws://127.0.0.1:9528"; // 获取音频文件 var get_music = serv + "/get_audio/"; var get_chat = serv + "/get_chat/"; var ws = null; // WebSocket 对象 var reco = null; // 创建AudioContext对象 var audio_context = new AudioContext(); var toy_id = null; //要获取音频和视频 navigator.getUserMedia = (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia); // 拿到媒体对象,允许音频对象 navigator.getUserMedia({audio: true}, create_stream, function (err) { console.log(err) }); //创建媒体流容器 function create_stream(user_media) { var stream_input = audio_context.createMediaStreamSource(user_media); // 给Recoder 创建一个空间,麦克风说的话,都可以录入。是一个流 reco = new Recorder(stream_input); } function start_reco() { //开始录音 reco.record(); //往里面写流 } function stop_reco() { //停止录音 reco.stop(); //停止写入流 get_audio(); //调用自定义方法 reco.clear(); //清空容器 } {#function get_audio() { // 获取音频#} {# reco.exportWAV(function (wav_file) {#} {# ws.send(wav_file); //使用websocket连接发送数据给后端#} {# })#} {# }#} function send_reco() { reco.stop(); send_audio(); reco.clear(); } function send_audio() { var to_user = document.getElementById("to_user").innerText; var send_str = { "to_user": to_user }; ws.send(JSON.stringify(send_str)); reco.exportWAV(function (wav_file) { ws.send(wav_file); }) } function get_audio() { var send_str = { "to_user": toy_id, "msg_type": "ai" }; ws.send(JSON.stringify(send_str)); reco.exportWAV(function (wav_file) { ws.send(wav_file); }) } function start_toy() { // 玩具开机 // 获取输入的设备id var device_id = document.getElementById("device_id").value; // 发送post请求 $.post( // 这里的地址必须是127.0.0.1,否则会有跨域问题 "http://127.0.0.1:9527/device_toy_id", // 发送设备id {device_id: device_id}, function (data) { console.log(data); toy_id = data.data.toy_id; // 玩具id // 修改audio标签的src属性 document.getElementById("player").src = get_music + data.data.audio; if (toy_id) { // 判断玩具id存在时 ws = new WebSocket(ws_serv + "/toy/" + toy_id); ws.onmessage = function (data) { // console.log(get_music + data.data); var content = JSON.parse(data.data); //反序列化数据 // 判断消息类型 if (content.msg_type == "chat") { document.getElementById("player").src = get_chat + content.data; document.getElementById("to_user").innerText = content.from_user; console.log(content.from_user + "给你发送了一条消息"); } if (content.msg_type == "music") { document.getElementById("player").src = get_music + content.data; console.log(content.from_user + "给你点播了歌儿"); } }; ws.onclose = function () { window.location.reload(); } } }, "json" // 规定预期的服务器响应的数据类型为json ); } function recv_msg() { var to_user = document.getElementById("to_user").innerText; var player = document.getElementById("player"); to_user = document.getElementById("to_user").innerText; $.post( serv + "/get_msg", {user_id: toy_id, sender: to_user}, function (data) { // shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值 var msg = data.data.shift(); document.getElementById("to_user").innerText = msg.sender; player.src = get_chat + msg.msg; //修改audio标签src属性 // onended 事件在视频/音频(audio/video)播放结束时触发 player.onended = function () { // 如果长度大于0,也就是有1条或者多条时 if(data.data.length > 0){ //修改audio标签src属性,有多条时,会轮询触发 player.src = get_chat + data.data.shift().msg; }else{ return null; } } }, "json" ) } </script> </html>
重启 manager.py和im_serv.py
重新访问网页,让玩具开机。连续录制2个语音
再点击收取消息,网页会先播放一条,再紧着播放第二条!
二、玩具端消息推送
APP发送语音后,页面语音提示,你有来自 xx 的消息
修改 serv-->chat.py,修改get_msg视图函数
from flask import Blueprint, request, jsonify from setting import MONGO_DB from setting import RET from bson import ObjectId from utils import chat_redis cht = Blueprint("cht", __name__) @cht.route("/chat_list", methods=["POST"]) def chat_list(): # 聊天记录列表 user_id = request.form.get("user_id") friend_id = request.form.get("friend_id") print(friend_id) chat_window = MONGO_DB.chat.find_one({"user_list": {"$all": [user_id, friend_id]}}) fri = MONGO_DB.toys.find_one({"_id": ObjectId(friend_id)}) baby_name = fri.get("baby_name") cl = chat_window.get("chat_list") RET["code"] = 0 RET["msg"] = baby_name RET["data"] = cl # 获取用户单个好友记录,修改redis的值 chat_redis.get_user_msg_one(friend_id,user_id) return jsonify(RET) @cht.route("/get_msg", methods=["POST"]) def get_msg(): # 获取聊天语言文件 user_id = request.form.get("user_id") sender = request.form.get("sender") count = 1 # 初始值 if not sender: msg_dict = chat_redis.get_msg_list(user_id) # print(msg_dict,"msg_dict") # 未读数量 sender = [i for i in msg_dict.keys() if msg_dict[i] != 0 and i != "count"] if sender: sender = sender[0] count = msg_dict[sender] else: pass # 没有任何消息了,可以调用合成语言,提示一下 # filename= baidu_ai.text2audio("") # new_msg = [{sender:"",msg:filename}] else: # 获取用户某个好友的值 count = chat_redis.get_user_msg_one(sender,user_id) # $all 表示多个条件都成立时。这里表示user_list字段中user_id和sender必须都存在才行! chat_window = MONGO_DB.chat.find_one({"user_list": {"$all": [user_id, sender]}}) # [-count:] 表示获取最后的几条消息。比如: -1: 表示最后一条 new_msg = chat_window.get("chat_list")[-count:] # 这里可以提示,您收到来自xx的几条消息 # filename= baidu_ai.text2audio("") # new_msg.insert(0,{ # "sender":sender, # "msg":filename # }) RET["code"] = 0 RET["msg"] = "" RET["data"] = new_msg chat_redis.get_user_msg_one(sender,user_id) return jsonify(RET) @cht.route("/get_msg_list", methods=["POST"]) def get_msg_list(): user_id = request.form.get("user_id") user_msg_dict = chat_redis.get_msg_list(user_id) RET["code"] = 0 RET["msg"] = "" RET["data"] = user_msg_dict return jsonify(RET)
重启 manager.py和im_serv.py
使用App发送2条消息
玩具页面会有语音提示,你有来自 小鱼的消息
点击收取消息。会自动播放2条语音!
今日总结:
1.向app推送消息 Redis 中存储消息: to_user : { sender : 1 } 消息按钮 未读消息 在message点击打开 chat_window的时候 发起一个fire(cut)事件给index页面 index页面 根据 cut 值进行删减 角标的数字 chat_window 未读消息 message页面将未读消息数字置空 = 0 chat_window 页面 发起的 chat_list 请求,加入逻辑 将redis中的未读消息置空 = 0 2.玩具端消息推送 消息列表 - 区分用户 array.shift() 删除array中的第一个元素并返回 批量收取一个用户的消息 AudioContext.onended = function(){ AudioContext.src = "music.mp3" } 批量收取消息的逻辑: 1.从sender未读消息中拿出未读数量 未读数量不是 0 2.从chat_list中拿出最后的几条未读消息
完整代码,参考github:
https://github.com/987334176/Intelligent_toy/archive/v1.5.zip