先下载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 = ""
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