chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
@@ -0,0 +1,152 @@
<div class="layui-card" style="margin:15px;">
<div class="layui-card-header">真人认证审核</div>
<div class="layui-card-body">
<form class="layui-form" lay-filter="searchForm">
<div class="layui-form-item">
<div class="layui-inline">
<input class="layui-input" name="keyword" placeholder="真实姓名/昵称" autocomplete="off">
</div>
<div class="layui-inline">
<select name="status">
<option value="">全部状态</option>
<option value="-1">未提交</option>
<option value="0">待审核</option>
<option value="1">已通过</option>
<option value="2">已驳回</option>
</select>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="searchBtn">搜索</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<script type="text/html" id="barTpl">
<a class="layui-btn layui-btn-xs" lay-event="review">审核</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
</script>
<script type="text/html" id="statusTpl">
{{# if(d.status == 1){ }}
<span class="layui-badge layui-bg-green">已通过</span>
{{# } else if(d.status == 2){ }}
<span class="layui-badge layui-bg-red">已驳回</span>
{{# } else if(d.status == 0){ }}
<span class="layui-badge layui-bg-orange">待审核</span>
{{# } else { }}
<span class="layui-badge">未提交</span>
{{# } }}
</script>
<script type="text/html" id="imgTpl">
{{# if(d.front_img){ }}<img src="{{d.front_img}}" style="max-width:90px;max-height:90px;" onerror="this.style.display='none'">{{# } }}
</script>
<script type="text/html" id="reviewTpl">
<div style="padding:20px;">
<table class="layui-table">
<tr><td>UID</td><td>{{d.uid}}</td></tr>
<tr><td>昵称</td><td>{{d.nickname||'--'}}</td></tr>
<tr><td>真实姓名</td><td>{{d.real_name}}</td></tr>
<tr><td>证件号(脱敏)</td><td>{{d.id_card}}</td></tr>
<tr><td>AI初筛</td><td>分数 {{d.ai_score}} / 结果 {{d.ai_result==1?'通过':'未通过'}}<br><small>{{d.ai_detail||''}}</small></td></tr>
</table>
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
<div>证件正面<br><img src="{{d.front_img}}" style="width:110px;" onerror="this.src=''"></div>
<div>证件反面<br><img src="{{d.back_img}}" style="width:110px;" onerror="this.src=''"></div>
<div>手持证件<br><img src="{{d.hold_img}}" style="width:110px;" onerror="this.src=''"></div>
<div>人脸照<br><img src="{{d.face_img}}" style="width:110px;" onerror="this.src=''"></div>
</div>
<form class="layui-form" id="reviewForm">
<input type="hidden" name="id" value="{{d.id}}">
<div class="layui-form-item">
<label class="layui-form-label">驳回原因</label>
<div class="layui-input-block">
<input type="text" name="reason" placeholder="驳回时填写" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn layui-bg-green" id="passBtn">通过</button>
<button type="button" class="layui-btn layui-btn-danger" id="rejectBtn">驳回</button>
</div>
</div>
</form>
</div>
</script>
<script>
layui.use(['table','form','layer','laytpl','http'], function(){
var table = layui.table, form = layui.form, layer = layui.layer, laytpl = layui.laytpl, $ = layui.$, http = layui.http;
var base = '/wxchat/backend/auth';
var tableIns = table.render({
elem: '#dataTable',
url: base + '/index',
page: true,
limit: 15,
cols: [[
{field:'id', title:'ID', width:70},
{field:'uid', title:'UID', width:90},
{field:'nickname', title:'昵称', width:120},
{field:'real_name', title:'真实姓名', width:110},
{field:'id_card', title:'证件号', width:160},
{field:'ai_score', title:'AI分数', width:90},
{field:'status', title:'状态', width:100, templet:'#statusTpl'},
{field:'create_at', title:'提交时间', width:170, templet:function(d){return d.create_at?layui.util.toDateString(d.create_at*1000,'yyyy-MM-dd HH:mm'):'';}},
{title:'操作', width:130, toolbar:'#barTpl'}
]],
parseData: function(res){ return {code: res.code, msg: res.message, count: res.count||0, data: res.data||[]}; }
});
form.on('submit(searchBtn)', function(data){
tableIns.reload({where: data.field, page:{curr:1}});
return false;
});
function post(action, data){
return http.post(base + '/'+action, data);
}
table.on('tool(dataTable)', function(obj){
var d = obj.data;
if(obj.event==='del'){
layer.confirm('确定删除该认证记录?', function(){
post('del', {ids: d.id}).then(function(res){
if(res.code===0){ layer.msg('已删除',{icon:1}); tableIns.reload(); }
else layer.msg(res.message||'删除失败',{icon:2});
});
});
return;
}
if(obj.event==='review'){
post('detail', {id: d.id}).then(function(res){
if(res.code!==0){ layer.msg(res.message||'获取失败',{icon:2}); return; }
var tpl = laytpl($('#reviewTpl').html()).render(res.data);
layer.open({
type:1, title:'认证审核 #'+d.id, area:['640px','auto'], content: tpl,
success:function(){
$('#passBtn').on('click', function(){
post('review', {id:d.id, pass:1}).done(function(r){
if(r.code===0){ layer.msg('已通过',{icon:1}); layer.closeAll(); tableIns.reload(); }
else layer.msg(r.message||'失败',{icon:2});
});
});
$('#rejectBtn').on('click', function(){
var reason = $('#reviewForm [name=reason]').val();
post('review', {id:d.id, pass:0, reason:reason}).done(function(r){
if(r.code===0){ layer.msg('已驳回',{icon:1}); layer.closeAll(); tableIns.reload(); }
else layer.msg(r.message||'失败',{icon:2});
});
});
}
});
});
}
});
});
</script>
@@ -0,0 +1,171 @@
<div class="layui-card" style="margin:15px;">
<div class="layui-card-header">语音 / 视频房管理</div>
<div class="layui-card-body">
<form class="layui-form" lay-filter="searchForm">
<div class="layui-form-item">
<div class="layui-inline">
<input class="layui-input" name="keyword" placeholder="房间标题" autocomplete="off">
</div>
<div class="layui-inline">
<select name="type">
<option value="0">全部类型</option>
<option value="1">语音</option>
<option value="2">视频</option>
<option value="3">游戏</option>
</select>
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="searchBtn">搜索</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<script type="text/html" id="barTpl">
<a class="layui-btn layui-btn-xs" lay-event="members">成员</a>
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="close">关房</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
</script>
<script type="text/html" id="statusTpl">
{{# if(d.status == 1){ }}
<span class="layui-badge layui-bg-green">直播中</span>
{{# } else { }}
<span class="layui-badge">已关闭</span>
{{# } }}
</script>
<script type="text/html" id="formTpl">
<form class="layui-form" style="padding:20px;" id="roomForm" lay-filter="roomForm">
<input type="hidden" name="id" value="{{d.id||''}}">
<div class="layui-form-item">
<label class="layui-form-label">房间标题</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" value="{{d.title||''}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">公告</label>
<div class="layui-input-block">
<input type="text" name="notice" class="layui-input" value="{{d.notice||''}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">房间状态</label>
<div class="layui-input-inline">
<input type="radio" name="status" value="1" title="直播中" {{ d.status==undefined||d.status==1?'checked':'' }}>
<input type="radio" name="status" value="0" title="关闭" {{ d.status==0?'checked':'' }}>
</div>
<label class="layui-form-label">弹幕</label>
<div class="layui-input-inline">
<input type="radio" name="danmaku_on" value="1" title="开" {{ d.danmaku_on==undefined||d.danmaku_on==1?'checked':'' }}>
<input type="radio" name="danmaku_on" value="0" title="关" {{ d.danmaku_on==0?'checked':'' }}>
</div>
</div>
</form>
</script>
<script type="text/html" id="membersTpl">
<div style="padding:15px;">
<table class="layui-table">
<thead><tr><th>UID</th><th></th><th></th><th></th></tr></thead>
<tbody>
{{# layui.each(d.members, function(i, m){ }}
<tr>
<td>{{m.uid}}</td>
<td>{{m.nickname||'--'}}</td>
<td>{{m.role==1?'主播':'观众'}}</td>
<td>{{m.join_at?layui.util.toDateString(m.join_at*1000,'yyyy-MM-dd HH:mm'):''}}</td>
</tr>
{{# }); }}
{{# if(d.members.length===0){ }}
<tr><td colspan="4" style="text-align:center;">暂无成员</td></tr>
{{# } }}
</tbody>
</table>
</div>
</script>
<script>
layui.use(['table','form','layer','laytpl','util','http'], function(){
var table = layui.table, form = layui.form, layer = layui.layer, laytpl = layui.laytpl, util = layui.util, $ = layui.$, http = layui.http;
var base = '/wxchat/backend/room';
var tableIns = table.render({
elem: '#dataTable',
url: base + '/index',
page: true,
limit: 15,
cols: [[
{field:'id', title:'ID', width:70},
{field:'title', title:'房间标题'},
{field:'type', title:'类型', width:80, templet:function(d){return d.type==1?'语音':(d.type==2?'视频':'游戏');}},
{field:'owner_name', title:'房主', width:120},
{field:'member_count', title:'成员数', width:90},
{field:'mic_count', title:'麦位数', width:80},
{field:'hot', title:'热度', width:80},
{field:'danmaku_on', title:'弹幕', width:70, templet:function(d){return d.danmaku_on==1?'开':'关';}},
{field:'status', title:'状态', width:90, templet:'#statusTpl'},
{field:'create_at', title:'创建时间', width:170, templet:function(d){return d.create_at?util.toDateString(d.create_at*1000,'yyyy-MM-dd HH:mm'):'';}},
{title:'操作', width:230, toolbar:'#barTpl'}
]],
parseData: function(res){ return {code: res.code, msg: res.message, count: res.count||0, data: res.data||[]}; }
});
form.on('submit(searchBtn)', function(data){
tableIns.reload({where: data.field, page:{curr:1}});
return false;
});
function post(action, data){
return http.post(base + '/'+action, data);
}
table.on('tool(dataTable)', function(obj){
var d = obj.data;
if(obj.event==='members'){
post('detail', {id: d.id}).then(function(res){
if(res.code!==0){ layer.msg(res.message||'获取失败',{icon:2}); return; }
var tpl = laytpl($('#membersTpl').html()).render(res.data);
layer.open({type:1, title:'房间成员 #'+d.id, area:['560px','auto'], content: tpl});
});
return;
}
if(obj.event==='edit'){
var tpl = laytpl($('#formTpl').html()).render(d);
layer.open({type:1, title:'编辑房间 #'+d.id, area:['520px','auto'], content: tpl,
btn:['保存','取消'],
success:function(){ form.render(); },
yes:function(index){
post('save', $('#roomForm').serialize()).then(function(r){
if(r.code===0){ layer.close(index); layer.msg('保存成功',{icon:1}); tableIns.reload(); }
else layer.msg(r.message||'失败',{icon:2});
});
}
});
return;
}
if(obj.event==='close'){
layer.confirm('确定关闭该房间?', function(){
post('close', {id: d.id}).then(function(r){
if(r.code===0){ layer.msg('已关闭',{icon:1}); tableIns.reload(); }
else layer.msg(r.message||'失败',{icon:2});
});
});
return;
}
if(obj.event==='del'){
layer.confirm('确定删除该房间(含成员与麦位)?', function(){
post('del', {ids: d.id}).then(function(r){
if(r.code===0){ layer.msg('已删除',{icon:1}); tableIns.reload(); }
else layer.msg(r.message||'失败',{icon:2});
});
});
}
});
});
</script>
@@ -0,0 +1,145 @@
<div class="layui-card" style="margin:15px;">
<div class="layui-card-header">任务赚钱管理</div>
<div class="layui-card-body">
<form class="layui-form" lay-filter="searchForm">
<div class="layui-form-item">
<div class="layui-inline">
<input class="layui-input" name="keyword" placeholder="任务标题" autocomplete="off">
</div>
<div class="layui-inline">
<button class="layui-btn" lay-submit lay-filter="searchBtn">搜索</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
<button type="button" class="layui-btn layui-btn-normal" id="addBtn">新增任务</button>
</div>
</div>
</form>
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
<script type="text/html" id="barTpl">
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
</script>
<script type="text/html" id="statusTpl">
{{# if(d.status == 1){ }}
<span class="layui-badge layui-bg-green">启用</span>
{{# } else { }}
<span class="layui-badge">禁用</span>
{{# } }}
</script>
<script type="text/html" id="formTpl">
<form class="layui-form" style="padding:20px;" id="taskForm" lay-filter="taskForm">
<input type="hidden" name="id" value="{{d.id||''}}">
<div class="layui-form-item">
<label class="layui-form-label">任务标题</label>
<div class="layui-input-block">
<input type="text" name="title" required lay-verify="required" placeholder="如:每日签到" class="layui-input" value="{{d.title||''}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">任务类型</label>
<div class="layui-input-block">
<input type="text" name="type" required lay-verify="required" placeholder="daily/invite/profile" class="layui-input" value="{{d.type||''}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">任务描述</label>
<div class="layui-input-block">
<input type="text" name="desc" class="layui-input" value="{{d.desc||''}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">奖励金币</label>
<div class="layui-input-inline">
<input type="number" name="reward_coins" class="layui-input" value="{{d.reward_coins||0}}">
</div>
<label class="layui-form-label">目标值</label>
<div class="layui-input-inline">
<input type="number" name="target" class="layui-input" value="{{d.target||1}}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">排序</label>
<div class="layui-input-inline">
<input type="number" name="sort" class="layui-input" value="{{d.sort||0}}">
</div>
<label class="layui-form-label">状态</label>
<div class="layui-input-inline">
<input type="radio" name="status" value="1" title="启用" {{ d.status==undefined||d.status==1?'checked':'' }}>
<input type="radio" name="status" value="0" title="禁用" {{ d.status==0?'checked':'' }}>
</div>
</div>
</form>
</script>
<script>
layui.use(['table','form','layer','laytpl','http'], function(){
var table = layui.table, form = layui.form, layer = layui.layer, laytpl = layui.laytpl, $ = layui.$, http = layui.http;
var base = '/wxchat/backend/task';
var tableIns = table.render({
elem: '#dataTable',
url: base + '/index',
page: true,
limit: 15,
cols: [[
{field:'id', title:'ID', width:70},
{field:'title', title:'标题'},
{field:'type', title:'类型', width:100},
{field:'reward_coins', title:'奖励金币', width:100},
{field:'target', title:'目标值', width:80},
{field:'participants', title:'参与人数', width:100},
{field:'claimed', title:'已领取', width:80},
{field:'sort', title:'排序', width:70},
{field:'status', title:'状态', width:90, templet:'#statusTpl'},
{title:'操作', width:140, toolbar:'#barTpl'}
]],
parseData: function(res){ return {code: res.code, msg: res.message, count: res.count||0, data: res.data||[]}; }
});
form.on('submit(searchBtn)', function(data){
tableIns.reload({where: data.field, page:{curr:1}});
return false;
});
function post(action, data){
return http.post(base + '/'+action, data);
}
$('#addBtn').on('click', function(){
openForm({});
});
function openForm(row){
var tpl = laytpl($('#formTpl').html()).render(row);
layer.open({
type:1, title: row.id ? '编辑任务' : '新增任务', area:['520px','auto'], content: tpl,
btn:['保存','取消'],
success:function(){ form.render(); },
yes:function(index){
var data = $('#taskForm').serialize();
post('save', data).then(function(res){
if(res.code===0){ layer.close(index); layer.msg('保存成功',{icon:1}); tableIns.reload(); }
else layer.msg(res.message||'保存失败',{icon:2});
});
}
});
}
table.on('tool(dataTable)', function(obj){
var d = obj.data;
if(obj.event==='edit'){ openForm(d); }
else if(obj.event==='del'){
layer.confirm('确定删除该任务?', function(){
post('del', {ids: d.id}).then(function(res){
if(res.code===0){ layer.msg('已删除',{icon:1}); tableIns.reload(); }
else layer.msg(res.message||'删除失败',{icon:2});
});
});
}
});
});
</script>
+251
View File
@@ -0,0 +1,251 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace ywxapp\library;
use think\exception\HttpException;
use think\facade\Db;
use think\facade\Event;
use ywxapp\library\Result;
use ywxapp\model\Rule as MemberRule;
use ywxapp\model\MemberUser as UsersModel;
/**
* Member 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Member
{
/***
* 是否登录
* @var bool
*/
protected $isLogin = false;
/**
* Summary of config
* @var array
*/
protected $config = [];
/**
* Summary of options
* @var array
*/
protected $options = [];
/**
* 登录标识
* @var bool
*/
protected $token;
/**
* 个人信息
* @return \ywxapp\model\Member
*/
protected $info;
/**
* 权限规则
* @return array
*/
protected $powers;
/**
* 角色
* @var array
*/
protected $roles;
/**
* 用户资料,用于临时存放一些用户数据,比如用户IP地址,需要使用后销毁
* @var array
*/
protected $profile;
/**
* 允许查询的字段
* @var array
*/
protected $allowFields = ['id', 'account', 'nickname', 'roles', 'avatar', 'score'];
public function __construct($options = [])
{
$this->info = new \stdClass();
// if ($config = Config::get('member')) {
// $this->config = array_merge($this->config, $config);
// }
// $this->options = array_merge($this->config, $options);
}
/**
* 初始化用户信息
*/
public function initUser($uid)
{
$this->direct($uid);
return $this;
}
/**
* 直接登录账号.
*
* @param int $id
* @return void
*/
public function direct($uid)
{
$info = UsersModel::findOrEmpty($uid);
if ($info->isEmpty()) {
Result::instance()->error("Uid:$uid is incorrect");
}
Db::startTrans();
try {
if ($info->update_time < \ywxapp\utils\Date::unixtime('day')) {
$info->successions = $info->logintime < \ywxapp\utils\Date::unixtime(
'day',
-1
) ? 1 : $info->successions + 1;
$info->maxsuccessions = max($info->successions, $info->maxsuccessions);
}
// $user->prevtime = $user->logintime;
//记录本次登录的IP和时间
//$user->loginip =request()->ip();
//重置登录失败次数
//$user->loginfailure = 0;
$info->save();
$this->info = $info;
// $this->roles = $info->roles;
// $this->powers = $info->roles;
$this->isLogin = true;
//登录成功的事件
Event::trigger('user_login_successed', $this->info);
Db::commit();
} catch (\think\Exception $e) {
Db::rollback();
throw new HttpException(4001, $e->getMessage());
}
}
/**
* 获取会员组别规则列表.
* @return array
*/
public function getRuleList()
{
if ($this->powers) {
return $this->powers;
}
if (! $$this->roles) {
return [];
}
$power = [];
foreach ($this->roles as $role) {
$powers = array_merge($powers, explode(',', $role->powers));
}
$powers = MemberRule::whereIn('id', implode(',', $power))
->where('status', 0)
->field('id,pid,name,title,ismenu')
->select();
$this->powers = $powers;
return $this->powers;
}
private function getEncryptPassword($password, $salt = '')
{
return md5(md5($password) . $salt);
}
/**
* 用户权限验证
*/
public function verifyAuth($noNeedLogin = ["*"], $noNeedRight = ["*"])
{
$request = app()->request;
$modulename = app()->http->getName();
$controllername = strtolower($request->controller());
$actionname = strtolower($request->action());
$path = str_replace('.', '/', $controllername) . '/' . $actionname;
if (! $this->match($noNeedLogin)) {
if (! $this->isLogin) {
Result::instance()->error('Please login first');
}
// 判断是否需要验证权限
if (! $this->match($noNeedRight) && ! $this->check($path)) {
Result::instance()->error('You have no permission');
}
}
}
/**
* 检测当前控制器和方法是否匹配传递的数组.
* @param array $arr 需要验证权限的数组
* @return bool
*/
private function match($arr = [])
{
if (in_array(strtolower(app()->request->action()), $arr) || in_array('*', $arr)) {
return true;
}
return false;
}
/**
* 检测是否是否有对应权限.
* @param string $path 控制器/方法
* @param string $module 模块 默认为当前模块
* @return bool
*/
private function check($path = null, $module = null)
{
$ruleList = $this->getRuleList();
$rules = [];
foreach ($ruleList as $k => $v) {
$rules[] = $v['name'];
}
$url = strtolower(str_replace('.', '/', $path));
return in_array($url, $rules) ? true : false;
}
public static function instance($options = [])
{
return app()->auth;
}
public function __get($name)
{
if ($name == 'info' && $this->info != null) {
return $this->info;
//return array_intersect_key($this->info->toArray(), array_flip($this->allowFields));
}
return $this->$name;
}
public function __set($name, $value)
{
$this->$name = $value;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+482
View File
@@ -0,0 +1,482 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔐 端到端加密聊天</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", sans-serif
}
body {
background: #f0f2f5;
padding: 20px;
max-width: 900px;
margin: 0 auto
}
.header {
text-align: center;
margin-bottom: 20px;
padding: 15px;
background: #1a73e8;
color: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1)
}
.chat-container {
display: flex;
gap: 20px
}
.panel {
background: white;
border-radius: 12px;
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.1);
padding: 20px;
flex: 1
}
.title {
font-size: 18px;
margin-bottom: 15px;
color: #1a73e8;
display: flex;
align-items: center;
gap: 8px
}
.title i {
font-size: 20px
}
#chatBox {
height: 400px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
background: #fafafa;
line-height: 1.6
}
.msg {
margin-bottom: 12px;
padding: 10px 15px;
border-radius: 18px;
max-width: 80%
}
.sent {
background: #e3f2fd;
margin-left: auto;
border-bottom-right-radius: 5px
}
.received {
background: #f1f8e9;
border-bottom-left-radius: 5px
}
.sys {
background: #fff8e1;
color: #5d4037;
font-size: 13px;
text-align: center;
margin: 5px 0
}
.input-area {
display: flex;
gap: 10px
}
textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 20px;
resize: none;
font-size: 15px;
height: 45px
}
button {
background: #1a73e8;
color: white;
border: none;
border-radius: 20px;
padding: 0 25px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s
}
button:hover {
background: #0d5bbb;
transform: scale(1.03)
}
button:disabled {
background: #b0b0b0;
cursor: not-allowed;
transform: none
}
.status {
padding: 8px;
border-radius: 6px;
margin-top: 10px;
font-size: 14px;
text-align: center
}
.status.connected {
background: #e8f5e9;
color: #2e7d32
}
.status.connecting {
background: #e3f2fd;
color: #1565c0
}
.status.error {
background: #ffebee;
color: #c62828
}
.debug {
background: #2d2d2d;
color: #64dd17;
padding: 10px;
border-radius: 6px;
margin-top: 15px;
font-family: monospace;
font-size: 13px;
max-height: 150px;
overflow: auto;
display: none
}
.badge {
background: #e3f2fd;
color: #1565c0;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
margin-left: 5px
}
@media (max-width:768px) {
.chat-container {
flex-direction: column
}
}
</style>
</head>
<body>
<div class="header">
<h1>🔐 端到端加密聊天 <span class="badge">微信级安全</span></h1>
<div style="margin-top:8px; font-size:14px; opacity:0.9">AES-256-CBC加密 | SHA1签名验证 | 防篡改防重放</div>
</div>
<div class="chat-container">
<div class="panel">
<div class="title">💬 聊天窗口</div>
<div id="chatBox"></div>
<div class="input-area">
<textarea id="messageInput" placeholder="输入消息... (按Ctrl+Enter发送)" disabled></textarea>
<button id="sendBtn" disabled>发送</button>
</div>
<div id="statusBar" class="status connecting">⏳ 正在连接服务器...</div>
<button id="toggleDebug" style="margin-top:10px; padding:5px 15px; background:#f5f5f5; color:#5f6368">显示调试日志</button>
<div id="debugLog" class="debug"></div>
</div>
<div class="panel">
<div class="title">⚙️ 连接配置</div>
<div style="margin-bottom:15px">
<div style="margin-bottom:8px"><strong>用户ID:</strong></div>
<input type="text" id="userId" value="user_<?= bin2hex(random_bytes(4)) ?>" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px">
</div>
<div style="margin-bottom:15px">
<div style="margin-bottom:8px"><strong>服务器地址:</strong></div>
<input type="text" id="serverUrl" value="wss://dev.xixingwl.cn:2346" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px">
</div>
<button id="connectBtn" style="width:100%; background:#34a853">🔌 连接服务器</button>
<div style="margin-top:20px; padding:12px; background:#e8f0fe; border-radius:8px; font-size:13px; line-height:1.5">
<strong>🔒 安全说明:</strong><br>
• 所有消息端到端加密,服务器无法查看内容<br>
• 每条消息独立签名,防篡改防伪造<br>
• 时间戳验证,防止消息重放攻击
</div>
</div>
</div>
<script src="/assets/ywxapp/modules/aescgm.js"></script>
<script>
// =============== 聊天客户端 ===============
class ChatClient {
constructor() {
this.ws = null;
this.crypto = null;
this.userId = '';
this.isConnected = false;
this.initUI();
}
initUI() {
// DOM元素
this.chatBox = document.getElementById('chatBox');
this.messageInput = document.getElementById('messageInput');
this.sendBtn = document.getElementById('sendBtn');
this.statusBar = document.getElementById('statusBar');
this.connectBtn = document.getElementById('connectBtn');
this.toggleDebug = document.getElementById('toggleDebug');
this.debugLog = document.getElementById('debugLog');
// 事件绑定
this.connectBtn.onclick = () => this.connect();
this.sendBtn.onclick = () => this.sendMessage();
this.messageInput.onkeypress = (e) => {
if (e.ctrlKey && e.key === 'Enter') this.sendMessage();
};
this.toggleDebug.onclick = () => {
this.debugLog.style.display = this.debugLog.style.display === 'none' ? 'block' : 'none';
};
// 初始化状态
this.updateStatus('⏳ 未连接,请配置后点击连接', 'connecting');
}
log(msg, type = 'info') {
const prefix = type === 'error' ? '❌' : type === 'success' ? '✅' : '️';
this.debugLog.innerHTML += `<span style="color:${type === 'error' ? '#ff5252' : type === 'success' ? '#4caf50' : '#aaa'}">${prefix} ${msg}</span><br>`;
this.debugLog.scrollTop = this.debugLog.scrollHeight;
console.log(`[ChatClient] ${msg}`);
}
updateStatus(text, className) {
this.statusBar.textContent = text;
this.statusBar.className = `status ${className}`;
}
addMessage(text, type = 'received', user = '系统') {
const div = document.createElement('div');
div.className = type === 'sys' ? 'sys' : `msg ${type}`;
if (type === 'sys') {
div.innerHTML = text;
} else {
div.innerHTML = `<strong>${user}:</strong> ${this.escapeHtml(text)}`;
}
this.chatBox.appendChild(div);
this.chatBox.scrollTop = this.chatBox.scrollHeight;
}
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
async connect() {
try {
this.userId = document.getElementById('userId').value.trim() || 'anonymous';
const serverUrl = document.getElementById('serverUrl').value.trim();
if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) {
throw new Error('服务器地址必须以 ws:// 或 wss:// 开头');
}
this.updateStatus('🔄 正在连接...', 'connecting');
this.connectBtn.disabled = true;
// 创建WebSocket
this.ws = new WebSocket(serverUrl);
this.ws.onopen = () => {
this.log('WebSocket连接已建立');
// 发送认证请求
this.ws.send(JSON.stringify({
action: 'auth',
token: 'your_secure_token_123456', // 与服务端约定
user_id: this.userId
}));
};
this.ws.onmessage = (event) => this.handleMessage(event.data);
this.ws.onerror = (error) => {
this.log(`连接错误: ${error.message}`, 'error');
this.log(error);
this.updateStatus('❌ 连接失败: ' + error.message, 'error');
this.connectBtn.disabled = false;
};
this.ws.onclose = () => {
this.isConnected = false;
this.log('连接已关闭', 'error');
if (this.isConnected) {
this.updateStatus('⚠️ 连接意外断开', 'error');
this.addMessage('连接已断开', 'sys');
}
this.connectBtn.disabled = false;
this.messageInput.disabled = true;
this.sendBtn.disabled = true;
};
} catch (e) {
this.log(`连接异常: ${e.message}`, 'error');
this.updateStatus('❌ ' + e.message, 'error');
this.connectBtn.disabled = false;
}
}
handleMessage(data) {
try {
const msg = JSON.parse(data);
// 认证响应
if (msg.action === 'auth_success') {
// 初始化加密模块 (使用服务端返回的密钥)
this.crypto = new ChatCrypto(
msg.encodingAesKey,
'your_secure_token_123456',
'CHAT_APP_2024'
);
this.isConnected = true;
this.updateStatus(`✅ 已连接 | 会话ID: ${msg.session_id.substring(0, 8)}...`, 'connected');
this.addMessage(`欢迎 ${this.userId}!端到端加密已启用`, 'sys');
this.messageInput.disabled = false;
this.sendBtn.disabled = false;
this.connectBtn.textContent = '🔌 已连接';
this.connectBtn.style.background = '#9e9e9e';
this.connectBtn.disabled = true;
this.log('认证成功,加密模块已初始化', 'success');
return;
}
// 错误处理
if (msg.error) {
this.log(`服务端错误: ${msg.error} (代码: ${msg.code})`, 'error');
if (msg.code === 401) {
this.updateStatus('❌ 认证失败,请检查token', 'error');
this.ws.close();
}
this.addMessage(`[系统] ${msg.error}`, 'sys');
return;
}
// =============== 接收聊天消息 ===============
// 验证签名
if (!this.crypto.verifySignature(
msg.signature,
msg.timestamp,
msg.nonce,
msg.encrypt
)) {
this.addMessage(`[警告] 消息签名验证失败,来源: ${msg.from_user}`, 'sys');
this.log(`收到无效签名消息 (来源: ${msg.from_user})`, 'error');
return;
}
// 防重放检查
if (Math.abs(Date.now() / 1000 - msg.timestamp) > 300) {
this.log(`收到过期消息 (时间差: ${Math.abs(Date.now() / 1000 - msg.timestamp)}秒)`, 'error');
return;
}
// 解密消息
try {
const plainText = this.crypto.decrypt(msg.encrypt);
const chatData = JSON.parse(plainText);
this.addMessage(chatData.content, 'received', chatData.from_user || msg.from_user);
this.log(`收到消息: "${chatData.content.substring(0, 20)}..." (来源: ${msg.from_user})`, 'success');
} catch (e) {
this.addMessage(`[系统] 消息解密失败: ${e.message}`, 'sys');
this.log(`解密异常: ${e.message}`, 'error');
}
} catch (e) {
this.log(`消息处理异常: ${e.message}`, 'error');
console.error(e);
}
}
sendMessage() {
const content = this.messageInput.value.trim();
if (!content || !this.isConnected || !this.crypto) return;
try {
// 构建消息体
const chatMsg = {
content: content,
type: 'text',
timestamp: Math.floor(Date.now() / 1000)
};
console.log('原始消息体:', chatMsg);
console.log('加密前消息体字符串:', JSON.stringify(chatMsg));
console.log('加密前消息体字符串:', window.encryptJson(chatMsg, 'your_secure_token_123456'));
// 加密
const encryptMsg = this.crypto.encrypt(JSON.stringify(chatMsg));
// 生成签名参数
const timestamp = Math.floor(Date.now() / 1000);
const nonce = this.crypto.getRandomStr(10);
const signature = this.crypto.generateSignature(timestamp, nonce, encryptMsg);
// 发送
this.ws.send(JSON.stringify({
encrypt: encryptMsg,
signature: signature,
timestamp: timestamp,
nonce: nonce
}));
// 显示到聊天框
this.addMessage(content, 'sent', this.userId);
this.messageInput.value = '';
this.log(`发送消息: "${content.substring(0, 20)}..."`, 'success');
} catch (e) {
this.log(`发送失败: ${e.message}`, 'error');
alert('发送失败: ' + e.message);
}
}
}
const aes = new AESGCM();
// 加密示例
aes.encrypt('Hello World!', 'mypassword')
.then(encrypted => {
console.log('加密结果:', encrypted);
// 解密示例
return aes.decrypt(encrypted, 'mypassword');
})
.then(decrypted => {
console.log('解密结果:', decrypted);
})
.catch(error => {
console.error('操作失败:', error);
});
// 初始化
document.addEventListener('DOMContentLoaded', () => {
console.log('页面加载完成,正在初始化聊天客户端...');
window.chatClient = new ChatClient();
document.getElementById('messageInput').focus();
});
</script>
</body>
</html>