diff --git a/app/.htaccess b/app/.htaccess
new file mode 100644
index 0000000..3418e55
--- /dev/null
+++ b/app/.htaccess
@@ -0,0 +1 @@
+deny from all
\ No newline at end of file
diff --git a/app/AppService.php b/app/AppService.php
new file mode 100644
index 0000000..96556e8
--- /dev/null
+++ b/app/AppService.php
@@ -0,0 +1,22 @@
+app = $app;
+ $this->request = $this->app->request;
+ // 控制器初始化
+ $this->initialize();
+ }
+
+ // 初始化
+ protected function initialize()
+ {
+ $this->userInfo=$this->request->userInfo;
+ $this->uid=$this->userInfo['user_id'] ?? 0;
+ $config=Config::getSystemInfo();
+ if($config){
+ $this->globalConfig = $config;
+ $this->chatSetting = $config['chatInfo'] ?? [];
+ }
+ // 验证版本,如果不一致,就需要退出重新登陆
+ $version =config('app.app_version');
+ $oldVersion=Cache::get('app_version');
+ if($version!=$oldVersion){
+ Cache::set('app_version',$version);
+ JWTAuth::refresh();
+ Cache::delete('systemInfo');
+ }
+ }
+
+ /**
+ * 验证数据
+ * @access protected
+ * @param array $data 数据
+ * @param string|array $validate 验证器名或者验证规则数组
+ * @param array $message 提示信息
+ * @param bool $batch 是否批量验证
+ * @return array|string|true
+ * @throws ValidateException
+ */
+ protected function validate(array $data, $validate, array $message = [], bool $batch = false)
+ {
+ if (is_array($validate)) {
+ $v = new Validate();
+ $v->rule($validate);
+ } else {
+ if (strpos($validate, '.')) {
+ // 支持场景
+ [$validate, $scene] = explode('.', $validate);
+ }
+ $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
+ $v = new $class();
+ if (!empty($scene)) {
+ $v->scene($scene);
+ }
+ }
+
+ $v->message($message);
+
+ // 是否批量验证
+ if ($batch || $this->batchValidate) {
+ $v->batch(true);
+ }
+
+ return $v->failException(true)->check($data);
+ }
+
+
+ /**
+ * 自动获取前端传递的分页数量
+ * @param \think\Model|\think\model\relation\HasMany $model
+ * @return \think\Paginator
+ */
+ protected function paginate($model)
+ {
+ $limit = $this->request->param('limit', 20);
+ return $model->paginate($limit);
+ }
+
+}
diff --git a/app/BaseModel.php b/app/BaseModel.php
new file mode 100644
index 0000000..542556d
--- /dev/null
+++ b/app/BaseModel.php
@@ -0,0 +1,103 @@
+userInfo ?? null;
+ self::$uid=request()->userInfo['user_id'] ?? null;
+ }
+
+ /**
+ * 获取树状信息
+ * @param array $config
+ */
+ public static function getCheckNode($arr, $pid, $field = "parent_id", $table = '')
+ {
+ if (!$table) {
+ $res = self::find($pid);
+ } else {
+ $res = Db::name($table)->find($pid);
+ }
+ if ($res) {
+ if ($res[$field] > 0) {
+ array_unshift($arr, $res[$field]);
+ return self::getCheckNode($arr, $res[$field], $field, $table);
+ }
+ }
+ return $arr;
+ }
+
+ // 获取错误信息
+ public function getError()
+ {
+ return $this->error;
+ }
+
+ /**
+ * 获取模型的json字段数组
+ * @return array
+ */
+ public function getJsonFieldName(): array
+ {
+ return $this->json;
+ }
+
+ // 匹配列表信息
+ public static function filterIdr($data, $many, $field)
+ {
+ if ($many) {
+ $idr = \utils\Arr::arrayToString($data, $field, false);
+ } else {
+ $idr = [];
+ if (is_array($field)) {
+ foreach ($field as $v) {
+ $idr[] = $data[$v];
+ }
+ } else {
+ $idr = [$data[$field]];
+ }
+ }
+ $key = array_search(0, $idr);
+ if ($key) {
+ array_splice($idr, $key, 1);
+ }
+ $idr = array_unique($idr);
+
+ return $idr ? : [];
+ }
+
+ // 获取某一项数据的统计
+ public static function getTotal($map,$where=[],$field,$group){
+ return self::field($field)
+ ->where($map)
+ ->where($where)
+ ->group($group)
+ ->select()->toArray();
+ }
+
+}
\ No newline at end of file
diff --git a/app/ExceptionHandle.php b/app/ExceptionHandle.php
new file mode 100644
index 0000000..453d126
--- /dev/null
+++ b/app/ExceptionHandle.php
@@ -0,0 +1,58 @@
+ $code, 'msg' => $msg?:lang('system.forbidden'), 'data' => []]));
+}
+
+
+/**
+ * ajax数据返回,规范格式
+ * @param array $data 返回的数据,默认空数组
+ * @param string $msg 信息
+ * @param int $code 错误码,0-未出现错误|其他出现错误
+ * @param array $extend 扩展数据
+ */
+function ret($code, $msg = "",$data = [],$count=0, $page=0)
+{
+ $ret = ["code" =>$code, "msg" => $msg,'count'=>$count, "data" => $data,'page'=>$page];
+ return json($ret);
+}
+
+
+/* @param string $string 原文或者密文
+* @param string $operation 操作(ENCODE | DECODE), 默认为 DECODE
+* @param string $key 密钥
+* @param int $expiry 密文有效期, 加密时候有效, 单位 秒,0 为永久有效
+* @return string 处理后的 原文或者 经过 base64_encode 处理后的密文
+*
+* @example
+*
+* $a = authcode('abc', 'ENCODE', 'key');
+* $b = authcode($a, 'DECODE', 'key'); // $b(abc)
+*
+* $a = authcode('abc', 'ENCODE', 'key', 3600);
+* $b = authcode('abc', 'DECODE', 'key'); // 在一个小时内,$b(abc),否则 $b 为空
+*/
+function authcode($string, $operation = 'DECODE', $key = '', $expiry = 3600) {
+
+ $ckey_length = 4;
+ // 随机密钥长度 取值 0-32;
+ // 加入随机密钥,可以令密文无任何规律,即便是原文和密钥完全相同,加密结果也会每次不同,增大破解难度。
+ // 取值越大,密文变动规律越大,密文变化 = 16 的 $ckey_length 次方
+ // 当此值为 0 时,则不产生随机密钥
+
+ $key = md5($key ? $key : 'default_key'); //这里可以填写默认key值
+ $keya = md5(substr($key, 0, 16));
+ $keyb = md5(substr($key, 16, 16));
+ $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
+
+ $cryptkey = $keya.md5($keya.$keyc);
+ $key_length = strlen($cryptkey);
+
+ $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
+ $string_length = strlen($string);
+
+ $result = '';
+ $box = range(0, 255);
+
+ $rndkey = array();
+ for($i = 0; $i <= 255; $i++) {
+ $rndkey[$i] = ord($cryptkey[$i % $key_length]);
+ }
+
+ for($j = $i = 0; $i < 256; $i++) {
+ $j = ($j + $box[$i] + $rndkey[$i]) % 256;
+ $tmp = $box[$i];
+ $box[$i] = $box[$j];
+ $box[$j] = $tmp;
+ }
+
+ for($a = $j = $i = 0; $i < $string_length; $i++) {
+ $a = ($a + 1) % 256;
+ $j = ($j + $box[$a]) % 256;
+ $tmp = $box[$a];
+ $box[$a] = $box[$j];
+ $box[$j] = $tmp;
+ $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
+ }
+
+ if($operation == 'DECODE') {
+ if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
+ return substr($result, 26);
+ } else {
+ return '';
+ }
+ } else {
+ return $keyc.str_replace('=', '', base64_encode($result));
+ }
+}
+
+function ssoTokenEncode($str,$key='lvzhesso',$expire=0){
+ $ids=encryptIds($str);
+ return authcode($ids,"ENCODE",$key,$expire);
+}
+
+function ssoTokenDecode($str,$key='lvzhesso'){
+ $ids=authcode($str,"DECODE",$key);
+ try{
+ return decryptIds($ids);
+ }catch(\Exception $e){
+ return '';
+ }
+}
+
+
+//id加密
+function encryptIds($str)
+{
+ $hash = config('hashids');
+ return \Hashids\Hashids::instance($hash['length'], $hash['salt'])->encode($str);
+}
+
+//id解密
+function decryptIds($str)
+{
+ $hash = config('hashids');
+ return \Hashids\Hashids::instance($hash['length'], $hash['salt'])->decode($str);
+}
+
+ /**
+ * 短信发送示例
+ *
+ * @mobile 短信发送对象手机号码
+ * @action 短信发送场景,会自动传入短信模板
+ * @parme 短信内容数组
+ */
+ function sendSms($mobile, $action, $parme)
+ {
+ $config = config('sms');
+ //$this->SmsDefaultDriver是从数据库中读取的短信默认驱动
+ $driver = $config['driver'] ?: 'aliyun';
+ $conf=$config[$driver];
+ $sms = new SkSms($driver, $conf);//传入短信驱动和配置信息
+ //判断短信发送驱动,非阿里云和七牛云,需将内容数组主键序号化
+ if ($driver == 'aliyun') {
+ $result = $sms->$action($mobile, $parme);
+ } elseif ($driver == 'qiniu') {
+ $result = $sms->$action([$mobile], $parme);
+ } elseif ($driver == 'upyun') {
+ $result = $sms->$action($mobile, implode('|', restoreArray($parme)));
+ } else {
+ $result = $sms->$action($mobile, restoreArray($parme));
+ }
+ if ($result['code'] == 200) {
+ $data['code'] = 200;
+ $data['msg'] = lang('system.sendOK');
+ } else {
+ $data['code'] = $result['code'];
+ $data['msg'] = $result['msg'];
+ }
+ return $data;
+ }
+
+ /**
+ * 数组主键序号化
+ *
+ * @arr 需要转换的数组
+ */
+ function restoreArray($arr)
+ {
+ if (!is_array($arr)){
+ return $arr;
+ }
+ $c = 0;
+ $new = [];
+ foreach ($arr as $key => $value) {
+ $new[$c] = $value;
+ $c++;
+ }
+ return $new;
+ }
+
+//密码生成规则
+function password_hash_tp($password,$salt)
+{
+ return md5($salt.$password.$salt);
+}
+
+// 获取主域名
+function getMainHost(){
+ $host=config('app.app_host','');
+ if($host){
+ return $host;
+ }
+ $port=request()->port();
+ $domain=request()->domain();
+ // halt($domain);
+ // 判断url是否有端口
+ if(!hasPort($domain)){
+ if($port!=80 && $port !=443){
+ return request()->domain().":".$port;
+ }
+ }
+ return $domain;
+}
+
+function hasPort($domainOrIp) {
+ // 查找冒号的位置
+ $colonPos = strrpos($domainOrIp, ':');
+ if ($colonPos!== false) {
+ // 获取冒号后面的字符串
+ $portPart = substr($domainOrIp, $colonPos + 1);
+ // 判断冒号后面的字符串是否为纯数字
+ return ctype_digit($portPart);
+ }
+ return false;
+}
+
+// 获取url中的主机名
+function getHost($url){
+ if(!preg_match('/http[s]:\/\/[\w.]+[\w\/]*[\w.]*\??[\w=&\+\%]*/is',$url)){
+ return '';
+ }
+ $search = '~^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?~i';
+ $url = trim($url);
+ preg_match_all($search, $url ,$rr);
+ return $rr[4][0];
+}
+
+//根据姓名画头像
+function circleAvatar($str,$s,$uid=0,$is_save=0,$save_path=''){
+ //定义输出为图像类型
+ header("content-type:image/png");
+ $str =$str?:"A";
+ $uid =$uid?:rand(0,10);
+ $text=\utils\Str::getLastName($str,2);
+ $width = $height = $s?:80;
+ if($width<40 or $width>120){
+ $width = $height =80;
+ }
+ $colors=['#F56C6C','#E6A23C','#fbbd08','#67C23A','#39b54a','#1cbbb4','#409EFF','#6739b6','#e239ff','#e03997'];
+ $color=hex2rgb($colors[(int)$uid%10]);
+ $size=$width/4;
+ $textLeft=($height/2)-$size-$width/10;
+ if($width<=80){
+ $text=\utils\Str::getLastName($str,1);
+ $size=$width/2;
+ $textLeft=$size/3;
+ }
+//新建图象
+ $pic=imagecreate($width,$height);
+//定义黑白颜色
+ $background=imagecolorallocate($pic,$color['r'],$color['g'],$color['b']);
+ $textColor=imagecolorallocate($pic,255,255,255);
+ imagefill($pic,0,0,$background);//填充背景色
+//定义字体
+ $font=root_path()."/public/static/fonts/PingFangHeavy.ttf";
+ //写 TTF 文字到图中
+ imagettftext($pic,$size,0,$textLeft,($height/2)+$size/2,$textColor,$font,$text);
+ if($is_save){
+ $path=$save_path."/".$uid.".png";
+ $dir = pathinfo($path,PATHINFO_DIRNAME);
+ if(!is_dir($dir)){
+ $file_create_res = mkdir($dir,0777,true);
+ if(!$file_create_res){
+ imagedestroy($pic);
+ return false;//没有创建成功
+ }
+ }
+ imagepng($pic,$path);
+ imagedestroy($pic);
+ return $path;
+ }else{
+ //输出图象
+ imagepng($pic);
+ //结束图形,释放内存空间
+ imagedestroy($pic);
+ return $pic;
+ }
+}
+
+//头像拼接
+function avatarUrl($path, $str = "雨",$uid=0,$s=80)
+{
+ // $str = Str::strFilter($str);
+ preg_match_all('/[\x{4e00}-\x{9fff}]+/u', $str, $matches);
+ $str=implode('', $matches[0]);
+ if($str==''){
+ $str="无";
+ }
+ if ($path) {
+ // 判断头像路径中是否有http
+ if (strpos($path, 'http') !== false) {
+ $url = $path;
+ } else {
+ $url = getDiskUrl() .'/'. ltrim($path,'/') ;
+ }
+ }else {
+ if($str){
+ $url=getMainHost()."/avatar/".$str.'/'.$s.'/'.$uid;
+ }else{
+ $url='';
+ }
+ }
+ return $url;
+}
+
+// 获取文件的地址
+function getFileUrl($path){
+ if (strpos($path, 'http') !== false) {
+ return $path;
+ }
+ return getDiskUrl() .'/'. ltrim($path,'/') ;
+}
+
+/**
+ * 十六进制 转 RGB
+ */
+function hex2rgb($hexColor)
+{
+ $color = str_replace('#', '', $hexColor);
+ if (strlen($color) > 3) {
+ $rgb = array(
+ 'r' => hexdec(substr($color, 0, 2)),
+ 'g' => hexdec(substr($color, 2, 2)),
+ 'b' => hexdec(substr($color, 4, 2))
+ );
+ } else {
+ $color = $hexColor;
+ $r = substr($color, 0, 1) . substr($color, 0, 1);
+ $g = substr($color, 1, 1) . substr($color, 1, 1);
+ $b = substr($color, 2, 1) . substr($color, 2, 1);
+ $rgb = array(
+ 'r' => hexdec($r),
+ 'g' => hexdec($g),
+ 'b' => hexdec($b)
+ );
+ }
+ return $rgb;
+}
+
+/**
+ * 将数组按字母A-Z排序
+ * @return [type] [description]
+ */
+function chartSort($array, $field,$isGroup=true,$chart='chart')
+{
+ $newArray = [];
+ foreach ($array as $k => &$v) {
+ $v[$chart] = getFirstChart($v[$field]);
+ $newArray[] = $v;
+ }
+ $data = [];
+ if($isGroup){
+ foreach ($newArray as $k => $v) {
+ if (array_key_exists($v[$chart], $data)) {
+ $data[$v[$chart]][] = $v;
+ } else {
+ $data[$v[$chart]] = [];
+ $data[$v[$chart]][] = $v;
+ }
+ }
+ ksort($data);
+ }else{
+ return $newArray;
+ }
+ return $data;
+}
+
+/**
+ * 返回取汉字的第一个字的首字母
+ * @param [type] $str [string]
+ * @return [type] [strind]
+ */
+function getFirstChart($str)
+{
+ $str = str_replace(' ', '', $str);
+ // 过滤特殊符号
+ $str = preg_replace('/[^\x{4e00}-\x{9fa5}A-Za-z0-9]/u', '', $str);
+ if (empty($str) || is_numeric($str)) {
+ return '#';
+ }
+ $char = ord($str[0]);
+ if ($char >= ord('A') && $char <= ord('z')) {
+ return strtoupper($str[0]);
+ }
+ $s1 = iconv('UTF-8', 'gb2312//IGNORE', $str);
+ $s2 = iconv('gb2312', 'UTF-8//IGNORE', $s1);
+ $s = $s2 == $str ? $s1 : $str;
+ $asc = ord($s[0]) * 256 + ord($s[1]) - 65536;
+ if ($asc >= -20319 && $asc <= -20284) return 'A';
+ if ($asc >= -20283 && $asc <= -19776) return 'B';
+ if ($asc >= -19775 && $asc <= -19219) return 'C';
+ if ($asc >= -19218 && $asc <= -18711) return 'D';
+ if ($asc >= -18710 && $asc <= -18527) return 'E';
+ if ($asc >= -18526 && $asc <= -18240) return 'F';
+ if ($asc >= -18239 && $asc <= -17923) return 'G';
+ if ($asc >= -17922 && $asc <= -17418) return 'H';
+ if ($asc >= -17417 && $asc <= -16475) return 'J';
+ if ($asc >= -16474 && $asc <= -16213) return 'K';
+ if ($asc >= -16212 && $asc <= -15641) return 'L';
+ if ($asc >= -15640 && $asc <= -15166) return 'M';
+ if ($asc >= -15165 && $asc <= -14923) return 'N';
+ if ($asc >= -14922 && $asc <= -14915) return 'O';
+ if ($asc >= -14914 && $asc <= -14631) return 'P';
+ if ($asc >= -14630 && $asc <= -14150) return 'Q';
+ if ($asc >= -14149 && $asc <= -14091) return 'R';
+ if ($asc >= -14090 && $asc <= -13319) return 'S';
+ if ($asc >= -13318 && $asc <= -12839) return 'T';
+ if ($asc >= -12838 && $asc <= -12557) return 'W';
+ if ($asc >= -12556 && $asc <= -11848) return 'X';
+ if ($asc >= -11847 && $asc <= -11056) return 'Y';
+ if ($asc >= -11055 && $asc <= -10247) return 'Z';
+ return "#";
+}
+
+// 拼接聊天对象
+function chat_identify($from_user,$to_user){
+ $identify=[$from_user,$to_user];
+ sort($identify);
+ return implode('-',$identify);
+}
+
+//数组中获取ID字符串
+function arrayToString($array,$field,$isStr=true){
+ $idArr = [];
+ foreach ($array as $k => $v) {
+ if(is_array($field)){
+ foreach($field as $val){
+ $idArr[]=$v[$val];
+ }
+ }else{
+ $idArr[] = $v[$field];
+ }
+ }
+ if ($isStr) {
+ $idStr = implode(',', $idArr);
+ return $idStr;
+ } else {
+ return $idArr;
+ }
+
+}
+
+// 根据文件后缀进行分类
+function getFileType($ext,$rst=false){
+ $ext=strtolower($ext);
+ $image=['jpg','jpeg','png','bmp','gif','webp','ico'];
+ $radio=['mp3','wav','wmv','amr'];
+ $video=['mp4','3gp','avi','m2v','mkv','mov'];
+ $doc=['ppt','pptx','doc','docx','xls','xlsx','pdf','txt','md'];
+ $msgType='file';
+ if(in_array($ext,$doc)){
+ $fileType=1;
+ }elseif(in_array($ext,$image)){
+ $fileType=2;
+ $msgType='image';
+ }elseif(in_array($ext,$radio)){
+ $fileType=3;
+ $msgType='voice';
+ }elseif(in_array($ext,$video)){
+ $fileType=4;
+ $msgType='video';
+ }else{
+ $fileType=9;
+ }
+ if($rst){
+ return $msgType;
+ }else{
+ return $fileType;
+ }
+}
+
+/**
+ * 二位数组排序
+ * $array 需要排序的数组
+ * $sort_key 需要排序的字段
+ * $sort_order 正序还是倒序
+ * $sort_type 排序的类型:数字,字母
+ */
+function sortArray($arrays, $sort_key, $sort_order = SORT_ASC, $sort_type = SORT_NUMERIC)
+{
+ if (is_array($arrays)) {
+ foreach ($arrays as $array) {
+ if (is_array($array)) {
+ $key_arrays[] = $array[$sort_key];
+ } else {
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ array_multisort($key_arrays, $sort_order, $sort_type, $arrays);
+ return $arrays;
+}
+
+//gateway向web页面推送消息
+function wsSendMsg($user, $type, $data, $isGroup=0)
+{
+ $message = json_encode([
+ 'type' => $type,
+ 'time' => time(),
+ 'data' => $data
+ ]);
+ try{
+ Gateway::$registerAddress = config('gateway.registerAddress');
+ if (!$user) {
+ Gateway::sendToAll($message);
+ } else {
+ if (!$isGroup) {
+ $send = 'sendToUid';
+ // 如果是单聊和语音通话需要使用unipush推送
+ $event=$data['extends']['event'] ?? '';
+ if(in_array($type,['simple']) || ($event=='calling' && $type=='webrtc')){
+ unipush($user,$data);
+ }
+ } else {
+ $send = "sendToGroup";
+ }
+ Gateway::$send($user, $message);
+ }
+ }catch(\Exception $e){
+ //忽略错误
+ }
+
+}
+
+// 绑定unipush的cid
+function bindCid($uid,$cid){
+ $url=env('unipush.url','');
+ if(!$url){
+ return false;
+ }
+ $data=[
+ 'type'=>'bindCid',
+ 'alias'=>[[
+ 'cid'=>$cid,
+ 'alias'=>$uid
+ ]]
+
+ ];
+ try{
+ $data=json_encode($data);
+ utils\Curl::curl_post($url,$data,true,["Content-Type: application/json"]);
+ }catch(\Exception $e){
+ //忽略错误
+ }
+
+}
+
+// unipush推送
+function unipush($toUser,$data){
+ $url=env('unipush.url','');
+ if(!$url){
+ return false;
+ }
+ $content='';
+ if($data['type']=='text'){
+ $content=Str::subStr($data['content'],0,50);
+ }else{
+ $content=getMsgType($data['type'],$data['extends']['type'] ?? 0);
+ }
+ // 这个推送不需要发给发送人
+ $fromUser=$data['fromUser']['id'] ?? '';
+ if(is_array($toUser)){
+ $toUser=array_diff($toUser,[$fromUser]);
+ }
+ $is_force=env('unipush.is_force',false);
+ $data=[
+ 'type'=>'push',
+ 'toUser'=>$toUser,
+ 'title'=>$data['fromUser']['displayName'],
+ 'content'=>$content,
+ 'force_notification'=>$is_force,
+ 'payload'=>$data
+ ];
+ try{
+ $data=json_encode($data);
+ utils\Curl::curl_post($url,$data,true,["Content-Type: application/json"]);
+ }catch(\Exception $e){
+ //忽略错误
+ }
+}
+
+// 预览文件
+function previewUrl($url){
+ $previewUrl=env('preview.own','');
+ // $preview='';
+ // $suffix=explode('.',$url);
+ // $ext=$suffix[count($suffix)-1];
+ // $media=['jpg','jpeg','png','bmp','gif','pdf','mp3','wav','wmv','amr','mp4','3gp','avi','m2v','mkv','mov','webp'];
+ // $doc=['ppt','pptx','doc','docx','xls','xlsx','pdf'];
+ // if(in_array($ext,$media) && $previewConf['own']){
+ // $preview=$previewConf['own']."view.html?src=".$url;
+ // }elseif(in_array($ext,$doc) && $previewConf['yzdcs']){
+ // $preview=$previewConf['yzdcs'].'?k='.$previewConf['keycode'].'&url='.$url;
+ // }else{
+
+ // }
+ if($previewUrl){
+ $preview=$previewUrl.$url;
+ }else{
+ $preview=getMainHost()."/view.html?src=".$url;
+ }
+ return $preview;
+}
+
+/**
+ * 解析sql语句
+ * @param string $content sql内容
+ * @param int $limit 如果为1,则只返回一条sql语句,默认返回所有
+ * @param array $prefix 替换表前缀
+ * @return array|string 除去注释之后的sql语句数组或一条语句
+ */
+function parse_sql($sql = '', $limit = 0, $prefix = []) {
+ // 被替换的前缀
+ $from = '';
+ // 要替换的前缀
+ $to = '';
+ // 替换表前缀
+ if (!empty($prefix)) {
+ $to = current($prefix);
+ $from = current(array_flip($prefix));
+ }
+ if ($sql != '') {
+ // 纯sql内容
+ $pure_sql = [];
+ // 多行注释标记
+ $comment = false;
+ // 按行分割,兼容多个平台
+ $sql = str_replace(["\r\n", "\r"], "\n", $sql);
+ $sql = explode("\n", trim($sql));
+ // 循环处理每一行
+ foreach ($sql as $key => $line) {
+ // 跳过空行
+ if ($line == '') {
+ continue;
+ }
+ // 跳过以#或者--开头的单行注释
+ if (preg_match("/^(#|--)/", $line)) {
+ continue;
+ }
+ // 跳过以/**/包裹起来的单行注释
+ if (preg_match("/^\/\*(.*?)\*\//", $line)) {
+ continue;
+ }
+ // 多行注释开始
+ if (substr($line, 0, 2) == '/*') {
+ $comment = true;
+ continue;
+ }
+ // 多行注释结束
+ if (substr($line, -2) == '*/') {
+ $comment = false;
+ continue;
+ }
+ // 多行注释没有结束,继续跳过
+ if ($comment) {
+ continue;
+ }
+ // 替换表前缀
+ if ($from != '') {
+ $line = str_replace('`'.$from, '`'.$to, $line);
+ }
+ if ($line == 'BEGIN;' || $line =='COMMIT;') {
+ continue;
+ }
+ // sql语句
+ array_push($pure_sql, $line);
+ }
+ // 只返回一条语句
+ if ($limit == 1) {
+ return implode("",$pure_sql);
+ }
+ // 以数组形式返回sql语句
+ $pure_sql = implode("\n",$pure_sql);
+ $pure_sql = explode(";\n", $pure_sql);
+ return $pure_sql;
+ } else {
+ return $limit == 1 ? '' : [];
+ }
+}
+
+/**
+ * 更新或添加环境变量
+ *
+ * @param string $key 环境变量的键
+ * @param string $value 环境变量的值
+ * @return bool 成功返回 true,失败返回 false
+ */
+function updateEnv($key, $value)
+{
+ $envFile = app()->getRootPath() . '.env';
+ if (!file_exists($envFile) || !is_writable($envFile)){
+ return false;
+ }
+
+ // 读取 .env 文件内容
+ $envContent = file_get_contents($envFile);
+ $keyPattern = preg_quote($key, '/');
+ $pattern = "/^{$keyPattern}=(.*)\$/m";
+
+ if (preg_match($pattern, $envContent)) {
+ // 如果找到了键值对,替换其值
+ $replacement = "{$key}={$value}";
+ $newEnvContent = preg_replace($pattern, $replacement, $envContent);
+ } else {
+ // 如果没有找到键值对,添加新的键值对
+ $newEnvContent = $envContent . PHP_EOL . "{$key}={$value}";
+ }
+ // 保存更新后的 .env 文件内容
+ return file_put_contents($envFile, $newEnvContent) !== false;
+}
+
+// 获取文件的域名
+function getDiskUrl(){
+ $disk=env('filesystem.driver','local');
+ $url=getMainHost();
+ if($disk=='aliyun'){
+ $url=env('filesystem.aliyun_url','');
+ }elseif($disk=='qiniu'){
+ $url=env('filesystem.qiniu_url','');
+ }elseif($disk=='qcloud'){
+ $url=env('filesystem.qcloud_cdn','');
+ }
+ $url=rtrim($url,'/');
+ return $url;
+}
+
+/**
+ * 合成图片
+ * @param array $pic_list [图片列表数组]
+ * @param boolean $is_save [是否保存,true保存,false输出到浏览器]
+ * @param string $save_path [保存路径]
+ * @return boolean|string
+ */
+function getGroupAvatar($pic_list=array(),$is_save=false,$save_path=''){
+ //验证参数
+ if(empty($pic_list) || empty($save_path)){
+ return false;
+ }
+ if($is_save){
+ //如果需要保存,需要传保存地址
+ if(empty($save_path)){
+ return false;
+ }
+ }
+ // 只操作前9个图片
+ $pic_list = array_slice($pic_list, 0, 9);
+ //设置背景图片宽高
+ $bg_w = 150; // 背景图片宽度
+ $bg_h = 150; // 背景图片高度
+ //新建一个真彩色图像作为背景
+ $background = imagecreatetruecolor($bg_w,$bg_h);
+ //为真彩色画布创建白灰色背景,再设置为透明
+ $color = imagecolorallocate($background, 202, 201, 201);
+ imagefill($background, 0, 0, $color);
+ imageColorTransparent($background, $color);
+ //根据图片个数设置图片位置
+ $pic_count = count($pic_list);
+ $lineArr = array();//需要换行的位置
+ $space_x = 3;
+ $space_y = 3;
+ $line_x = 0;
+ switch($pic_count) {
+ case 1: // 正中间
+ $start_x = intval($bg_w/4); // 开始位置X
+ $start_y = intval($bg_h/4); // 开始位置Y
+ $pic_w = intval($bg_w/2); // 宽度
+ $pic_h = intval($bg_h/2); // 高度
+ break;
+ case 2: // 中间位置并排
+ $start_x = 2;
+ $start_y = intval($bg_h/4) + 3;
+ $pic_w = intval($bg_w/2) - 5;
+ $pic_h = intval($bg_h/2) - 5;
+ $space_x = 5;
+ break;
+ case 3:
+ $start_x = 40; // 开始位置X
+ $start_y = 5; // 开始位置Y
+ $pic_w = intval($bg_w/2) - 5; // 宽度
+ $pic_h = intval($bg_h/2) - 5; // 高度
+ $lineArr = array(2);
+ $line_x = 4;
+ break;
+ case 4:
+ $start_x = 4; // 开始位置X
+ $start_y = 5; // 开始位置Y
+ $pic_w = intval($bg_w/2) - 5; // 宽度
+ $pic_h = intval($bg_h/2) - 5; // 高度
+ $lineArr = array(3);
+ $line_x = 4;
+ break;
+ case 5:
+ $start_x = 30; // 开始位置X
+ $start_y = 30; // 开始位置Y
+ $pic_w = intval($bg_w/3) - 5; // 宽度
+ $pic_h = intval($bg_h/3) - 5; // 高度
+ $lineArr = array(3);
+ $line_x = 5;
+ break;
+ case 6:
+ $start_x = 5; // 开始位置X
+ $start_y = 30; // 开始位置Y
+ $pic_w = intval($bg_w/3) - 5; // 宽度
+ $pic_h = intval($bg_h/3) - 5; // 高度
+ $lineArr = array(4);
+ $line_x = 5;
+ break;
+ case 7:
+ $start_x = 53; // 开始位置X
+ $start_y = 5; // 开始位置Y
+ $pic_w = intval($bg_w/3) - 5; // 宽度
+ $pic_h = intval($bg_h/3) - 5; // 高度
+ $lineArr = array(2,5);
+ $line_x = 5;
+ break;
+ case 8:
+ $start_x = 30; // 开始位置X
+ $start_y = 5; // 开始位置Y
+ $pic_w = intval($bg_w/3) - 5; // 宽度
+ $pic_h = intval($bg_h/3) - 5; // 高度
+ $lineArr = array(3,6);
+ $line_x = 5;
+ break;
+ case 9:
+ $start_x = 5; // 开始位置X
+ $start_y = 5; // 开始位置Y
+ $pic_w = intval($bg_w/3) - 5; // 宽度
+ $pic_h = intval($bg_h/3) - 5; // 高度
+ $lineArr = array(4,7);
+ $line_x = 5;
+ break;
+ }
+ foreach( $pic_list as $k=>$pic_path ) {
+ $kk = $k + 1;
+ if ( in_array($kk, $lineArr) ) {
+ $start_x = $line_x;
+ $start_y = $start_y + $pic_h + $space_y;
+ }
+ //获取图片文件扩展类型和mime类型,判断是否是正常图片文件
+ //非正常图片文件,相应位置空着,跳过处理
+ $image_mime_info = @getimagesize($pic_path);
+ if($image_mime_info && !empty($image_mime_info['mime'])){
+ $mime_arr = explode('/',$image_mime_info['mime']);
+ if(is_array($mime_arr) && $mime_arr[0] == 'image' && !empty($mime_arr[1])){
+ switch($mime_arr[1]) {
+ case 'jpg':
+ case 'jpeg':
+ $imagecreatefromjpeg = 'imagecreatefromjpeg';
+ break;
+ case 'png':
+ $imagecreatefromjpeg = 'imagecreatefrompng';
+ break;
+ case 'gif':
+ default:
+ $imagecreatefromjpeg = 'imagecreatefromstring';
+ $pic_path = file_get_contents($pic_path);
+ break;
+ }
+ //创建一个新图像
+ $resource = $imagecreatefromjpeg($pic_path);
+ //将图像中的一块矩形区域拷贝到另一个背景图像中
+ // $start_x,$start_y 放置在背景中的起始位置
+ // 0,0 裁剪的源头像的起点位置
+ // $pic_w,$pic_h copy后的高度和宽度
+ imagecopyresized($background,$resource,$start_x,$start_y,0,0,$pic_w,$pic_h,imagesx($resource),imagesy($resource));
+ }
+ }
+ // 最后两个参数为原始图片宽度和高度,倒数两个参数为copy时的图片宽度和高度
+ $start_x = $start_x + $pic_w + $space_x;
+ }
+ if($is_save){
+ $dir = pathinfo($save_path,PATHINFO_DIRNAME);
+ if(!is_dir($dir)){
+ $file_create_res = mkdir($dir,0777,true);
+ if(!$file_create_res){
+ return false;//没有创建成功
+ }
+ }
+ $res = imagejpeg($background,$save_path);
+ imagedestroy($background);
+ if($res){
+ return true;
+ }else{
+ return false;
+ }
+ }else{
+ //直接输出
+ header("Content-type: image/jpg");
+ imagejpeg($background);
+ imagedestroy($background);
+ }
+}
+
+/**
+ * 获取一个唯一token
+ * @return string
+ */
+function getOnlyToken()
+{
+ return md5(uniqid(md5(microtime(true)), true));
+}
+
+// 设置排序规则
+function orderBy($field, $type, $prefix = '', $default = 'update_time')
+{
+ $type=is_numeric($type)?($type==1?'asc':'desc'):$type;
+ if ($field) {
+ $order = $prefix . $field . ' ' . $type;
+ } else {
+ $order = $prefix . $default . ' desc';
+ }
+ return $order;
+}
+
+// 获取文件后缀图片
+function getExtUrl($path){
+ $ext=explode('.',$path);
+ $ext=end($ext);
+ // 如果是图片文件,就直接返回图片地址
+ $image=['jpg','jpeg','png','bmp','gif','webp'];
+ if(in_array($ext,$image)){
+ return getFileUrl($path);
+ }
+ $extUrl='/static/img/ext/'.strtoupper($ext).'.png';
+ // 判断文件是否存在
+ if(!file_exists(public_path().$extUrl)){
+ $extUrl='/static/img/ext/folder.png';
+ }
+ return getMainHost().$extUrl;
+}
+
+// 字符串内容加解密函数
+function str_encipher($str,$encode=true,$key=''){
+ if($key==''){
+ $key=config('app.aes_chat_key');
+ }
+ if($key=='' || $str==''){
+ return $str;
+ }
+ if($encode){
+ $s=\utils\Aes::encrypt($str,$key);
+ }else{
+ $s=\utils\Aes::decrypt($str,$key) ?:'';
+ }
+ return $s;
+}
+
+// 推送时获取消息的类型
+function getMsgType($type,$callVideo=false){
+ $msgName=lang('messageType.other');
+ switch($type){
+ case 'image':
+ $msgName=lang('messageType.image');
+ break;
+ case 'voice':
+ $msgName=lang('messageType.voice');
+ break;
+ case 'emoji':
+ $msgName=lang('messageType.emoji');
+ break;
+ case 'video':
+ $msgName=lang('messageType.video');
+ break;
+ case 'file':
+ $msgName=lang('messageType.file');
+ break;
+ case 'webrtc':
+ if($callVideo){
+ $msgName=lang('messageType.webrtcAudio');
+ }else{
+ $msgName=lang('messageType.webrtcVideo');
+ }
+ break;
+ }
+ return $msgName;
+}
+
+// 获取app的下载链接
+function getAppDowmUrl($platform='andriod'){
+ $config=config('version.'.$platform);
+ $name=config('version.app_name');
+ if($platform=='windows'){
+ $packageName=$name."_Setup_".$config['version'].".exe";
+ $path="/downloadApp/windows";
+ }elseif($platform=='mac'){
+ $packageName=$name."_Setup_".$config['version'].".dmg";
+ $path="/downloadApp/mac";
+ }else{
+ $packageName=$name."_Setup_".$config['version'].".apk";
+ $path="/downloadApp/andriod";
+ }
+ if(is_file(PACKAGE_PATH . $packageName)){
+ return getMainHost().$path;
+ }else{
+ return '';
+ }
+}
+
+// php匹配文本中的所有url
+function getAllUrl($text){
+ // 使用正则表达式匹配带有或不带有协议头的URL
+ $pattern = '/\b(?:https?:\/\/)?[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/i';
+ // 使用preg_replace()函数将URL转换为标签
+ $replaced_text = preg_replace_callback($pattern, function($matches) {
+ $url = $matches[0];
+ if(utils\Regular::is_url($url)){
+ $newUrl=$url;
+ if (!preg_match("~^(?:f|ht)tps?://~i", $url)) {
+ $newUrl = "http://" . $url;
+ }
+ return '' . $url . '';
+ }else{
+ return $url;
+ }
+
+ }, $text);
+ return $replaced_text;
+}
+
+
+// 将链接转成可点击的标签
+function preg_link($text){
+ // 判断文本中是否有img标签
+ if(preg_match('/]+>/i', $text)){
+ return $text;
+ }
+ // 匹配更广泛的 URL 的正则表达式
+ $pattern ='/\b(?:https?:\/\/|ftp:\/\/)?([a-z0-9-+&@#\/%?=~_|!:,.;]*\.[a-z]{2,}(?:\/[a-z0-9-+&@#\/%?=~_|!:,.;]*)*)\b/i';
+ // 使用preg_replace()函数将URL转换为标签
+ $replaced_text = preg_replace_callback($pattern, function($matches) {
+ $url = $matches[0];
+ $isUrl=preg_match('/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]|[a-z0-9-+&@#\/%?=~_|!:,.;]*\.[a-z]{2,}\b/i',$url) ? true : false;
+ if($isUrl){
+ $newUrl=$url;
+ if (!preg_match("~^(?:f|ht)tps?://~i", $url)) {
+ $newUrl = "https://" . $url;
+ }
+ return '' . $url . '';
+ }else{
+ return $url;
+ }
+
+ }, $text);
+
+ return $replaced_text;
+
+}
+
diff --git a/app/event.php b/app/event.php
new file mode 100644
index 0000000..5aa6e1c
--- /dev/null
+++ b/app/event.php
@@ -0,0 +1,20 @@
+ [
+ ],
+
+ 'listen' => [
+ 'AppInit' => [],
+ 'HttpRun' => [],
+ 'HttpEnd' => [],
+ 'LogLevel' => [],
+ 'LogWrite' => [],
+ 'UserRegister'=>['app\common\listener\UserRegister'],
+ 'GroupChange'=>['app\enterprise\listener\GroupChange'],
+ 'GreenText'=>['app\common\listener\GreenText'],
+ ],
+
+ 'subscribe' => [
+ ],
+];
diff --git a/app/lang/en_us copy.php b/app/lang/en_us copy.php
new file mode 100644
index 0000000..331d464
--- /dev/null
+++ b/app/lang/en_us copy.php
@@ -0,0 +1,62 @@
+ 'Cannot add yourself as a friend',
+ 'you_are_already_friends' => 'You are already friends',
+ 'you_have_already_applied_please_wait' => 'You have already applied, please wait for their approval',
+ 'new_friend' => 'New Friend',
+ 'has_added_you_as_a_friend' => 'has added you as a friend',
+ 'added_successfully' => 'Added successfully',
+ 'request_does_not_exist' => 'Request does not exist',
+ 'operation_successful' => 'Operation successful',
+ 'friend_does_not_exist' => 'Friend does not exist',
+ 'deleted_successfully' => 'Deleted successfully',
+ 'note_cannot_be_empty' => 'Note cannot be empty',
+ 'settings_updated_successfully' => 'Settings updated successfully',
+
+ // Group
+ 'you_do_not_have_permission_only_owners_and_admins_can_modify' => 'You do not have permission, only group owners and administrators can modify!',
+ 'modified_successfully' => 'Modified successfully',
+ 'member_limit_exceeded' => 'Member limit exceeded',
+ 'you_do_not_have_permission' => 'You do not have permission!',
+ 'settings_failed' => 'Settings failed!',
+ 'you_do_not_have_permission_to_create_group' => 'You do not have permission to create a group!',
+ 'please_select_at_least_two_members' => 'Please select at least two members!',
+ 'group_created' => 'Group created',
+ 'your_permission_is_not_sufficient' => 'Your permission is not sufficient!',
+ 'deleted_successfully_group' => 'Deleted successfully',
+ 'please_enter_content' => 'Please enter content!',
+ 'you_are_already_in_this_group' => 'You are already in this group!',
+ 'joined_successfully' => 'Joined successfully',
+ 'group_does_not_exist' => 'Group does not exist',
+ 'user_does_not_exist' => 'User does not exist',
+ 'transferred_successfully' => 'Transferred successfully',
+ 'change_failed' => 'Change failed',
+
+ // IM
+ 'private_chat_is_currently_disabled' => 'Private chat is currently disabled!',
+ 'you_are_not_on_their_friend_list_cannot_send_message' => 'You are not on their friend list, cannot send a message!',
+ 'they_are_not_your_friend_cannot_send_message' => 'They are not your friend, cannot send a message!',
+ 'send_failed' => 'Send failed',
+ 'please_select_users_or_quantity_not_exceeding_10' => 'Please select users or quantity not exceeding 10!',
+ 'message_does_not_exist' => 'Message does not exist',
+ 'forwarding_failed_due_to_rules' => 'Forwarding failed due to rules',
+ 'forwarded_successfully' => 'Forwarded successfully',
+ 'you' => 'You',
+ 'other' => 'Other',
+ 'cannot_withdraw_after_2_minutes' => 'Cannot withdraw after 2 minutes!',
+ 'withdrawn_a_message' => 'Withdrawn a message',
+ 'you_do_not_have_permission_to_withdraw_this_message' => 'You do not have permission to withdraw this message',
+ 'a_message_was_withdrawn_by_an_admin' => 'A message was withdrawn by an admin',
+ 'deleted_successfully_im' => 'Deleted successfully',
+ 'call_cancelled' => 'Call cancelled',
+ 'declined' => 'Declined',
+ 'not_connected' => 'Not connected',
+ 'call_duration' => 'Call duration',
+ 'busy' => 'Busy',
+ 'operation_performed_on_another_device' => 'Operation performed on another device',
+ 'video_call' => 'Video call',
+ 'audio_call' => 'Audio call',
+ 'answer_call_request' => 'Answer call request',
+ 'data_exchange_in_progress' => 'Data exchange in progress'
+];
diff --git a/app/lang/en_us.php b/app/lang/en_us.php
new file mode 100644
index 0000000..5dd163a
--- /dev/null
+++ b/app/lang/en_us.php
@@ -0,0 +1,156 @@
+ [
+ 'success' => 'Operation successful',
+ 'fail' => 'Operation failed',
+ 'error' => 'System error',
+ 'forbidden' => 'Access forbidden',
+ 'exist' => 'The record does not exist',
+ 'sendOK' => 'Sent successfully',
+ 'sendFail' => 'Sending failed',
+ 'delOk' => 'Deletion successful',
+ 'settingOk' => 'Settings updated successfully',
+ 'notNull' => 'Cannot be empty',
+ 'editOk' => 'Edit successful',
+ 'editFail' => 'Edit failed',
+ 'addOk' => 'Addition successful',
+ // The original 'addFail' seems to be a typo, assuming it should be '添加失败' which translates to 'Addition failed'
+ 'addFail' => 'Addition failed',
+ 'joinOk' => 'Joining successful',
+ 'notAuth' => 'You do not have the permission to perform this operation!',
+ 'demoMode' => 'Modifications are not supported in demo mode',
+ 'parameterError' => 'Parameter error',
+ 'longTime' => 'Request timeout',
+ 'apiClose' => 'API is closed',
+ 'appIdError' => 'appId error',
+ 'signError' => 'Signature error',
+ 'tooFast'=>"You visited too fast!"
+ ],
+ 'messageType' => [
+ 'other' => "[Unsupported message type]",
+ 'image' => "[Image]",
+ 'emoji' => "[DIYemoji]",
+ 'voice' => "[Voice]",
+ 'video' => "[Video]",
+ 'file' => "[File]",
+ 'webrtcAudio' => "[Audio call request with you]",
+ 'webrtcVideo' => "[Video call request with you]",
+ ],
+ 'friend' => [
+ 'notAddOwn' => "You cannot add yourself as a friend",
+ 'already' => "You are already friends",
+ 'repeatApply' => "You have already sent a request, please wait for the other person to accept",
+ 'new' => "New friend",
+ 'apply' => "Has added you as a friend",
+ 'notApply' => "The request does not exist",
+ 'not' => "Friend does not exist",
+ 'newChat' => "You have been successfully added as friends, let's start chatting now! ",
+ 'limit' => "Your friends have reached the limit! ",
+ ],
+ 'group' => [
+ 'name' => "Group chat",
+ 'notAuth' => "You do not have permission to perform this action. Only the group owner and administrators can make changes!",
+ 'userLimit' => "The number of members cannot exceed {:userMax} people!",
+ 'inviteLimit'=>'The number of people invited at a single time cannot exceed {:limit}! ',
+ 'invite' => "{:username} has invited you to join the group chat",
+ 'removeUser'=>"You have been removed from Group chats! ",
+ 'notCustom'=>"You are not a member of this group and have no right to send messages!",
+ 'add' => "{:username} has created a group chat",
+ 'join'=>"{:username} join the group chat",
+ 'atLeast' => "Please select at least two people!",
+ 'alreadyJoin' => "You are already in this group!",
+ 'exist' => "The group chat does not exist",
+ 'notice'=>"Announcement",
+ 'all'=>"All",
+ 'noSpeak'=>"You have been banned from speaking, recovery time is {:time}",
+ 'notSpeak'=>"Group chat has been banned!",
+ 'limit'=>"You have been restricted from creating Group chats! ",
+ ],
+ 'user' => [
+ 'exist' => "User does not exist",
+ 'codeErr' => "Verification code is incorrect!",
+ 'newCodeErr' => "New verification code is incorrect!",
+ 'passErr' => "Original password is incorrect!",
+ 'already' => "Account already exists",
+ 'registerOk' => "Registration successful",
+ 'loginOk' => "Login successful",
+ 'tokenFailure' => "TOKEN has expired!",
+ 'forbid' => "Your account has been disabled",
+ 'passError' => "Password is incorrect",
+ 'logoutOk' => "Logout successful!",
+ 'closeRegister' => "The system has disabled registration!",
+ 'inviteCode' => "Invite code has expired!",
+ 'accountVerify' => "Account must be a phone number or email",
+ 'waitMinute' => "Please try again after one minute!",
+ "loginAccount" => "Login account",
+ "registerAccount" => "Register account",
+ "editPass" => "Change password",
+ "editAccount" => "Edit account",
+ 'loginError' => 'Login information is incorrect. Please log in again.',
+ 'mustToken' => 'Please log in to the system first',
+ 'blacklist' => 'Login has expired. Please log in again',
+ 'expired' => 'Login has expired. Please log in again',
+ 'notOwn' =>"Customer service can't be for him",
+ 'loginLimit' =>"Your password has been wrong too many times. Please try again later! ",
+ 'registerLimit'=>"Please register again in {:time} minutes"
+ ],
+ 'im' => [
+ 'forbidChat' => "Private chatting is currently prohibited!",
+ 'notFriend' => "You are not on their friend list, cannot send messages!",
+ 'friendNot' => "They are not your friend, cannot send messages!",
+ 'forwardLimit' => "Please select fewer than {:count} recipients for forwarding!",
+ 'exist' => "Message does not exist",
+ 'forwardRule' => "Forwarding failed for {:count} messages due to rule restrictions!",
+ 'forwardOk' => 'Message forwarded successfully',
+ 'you' => 'You',
+ 'other' => 'Recipient',
+ 'redoLimitTime' => "Cannot recall messages after {:time} minutes!",
+ 'redo' => "A message has been recalled",
+ 'manageRedo' => "A message has been recalled by (an admin)",
+ 'msgContentLimit' => "Message content too long",
+ 'sendTimeLimit'=>'The message sending interval is {:time} seconds!',
+ 'forbidMsg'=>'[The message is suspected of violation and has been blocked]',
+ ],
+ 'webRtc' => [
+ 'cancel' => 'Call has been canceled',
+ 'refuse' => 'Call has been rejected',
+ 'notConnected' => 'Call not connected',
+ 'duration' => 'Call duration: {:time}',
+ 'busy' => 'Busy',
+ 'other' => 'Operation performed on another device',
+ 'video' => 'Video call',
+ 'audio' => 'Audio call',
+ 'answer' => 'Answer call request',
+ 'exchange' => 'Data exchange in progress',
+ 'fail' => 'Call failed',
+ ],
+ 'email' => [
+ 'input' => 'Please enter a valid email address',
+ 'testTitle' => "Test Email",
+ 'testContent' => "This is a test email. If you receive it, it means all your configurations are correct!",
+ ],
+ 'task' => [
+ 'schedule' => 'Scheduled Task',
+ 'queue' => 'Message Queue',
+ 'worker' => 'Message Push',
+ 'clearStd' => 'Clear Logs',
+ 'null' => "Unknown task",
+ 'winRun' => "To start on Windows, please run the 'start_for_win.bat' file in the root directory",
+ 'alreadyRun' => "Process is already running",
+ 'startOk' => "Started successfully",
+ 'startFail' => "Failed to start",
+ 'notRun' => "Process is not running",
+ 'logExist' => "Log does not exist",
+ ],
+ 'file' => [
+ 'preview' => "Preview file",
+ 'browserDown' => "Please use the browser to download",
+ 'exist' => "The file does not exist", // Note: This might be a duplicate of 'preview' and could be replaced with a more specific message
+ 'uploadLimit' => "File size cannot exceed {:size}MB",
+ 'typeNotSupport' => "File format is not supported",
+ 'uploadOk' => "Upload successful",
+ ],
+ 'scan' => [
+ 'failure' => 'QR code has expired'
+ ]
+];
\ No newline at end of file
diff --git a/app/lang/zh_cn.php b/app/lang/zh_cn.php
new file mode 100644
index 0000000..7e83a88
--- /dev/null
+++ b/app/lang/zh_cn.php
@@ -0,0 +1,155 @@
+[
+ 'success'=>'操作成功',
+ 'fail'=>'操作失败',
+ 'error'=>'系统错误',
+ 'forbidden'=>"禁止访问",
+ 'exist'=>"记录不存在",
+ 'sendOK'=>"发送成功",
+ 'sendFail'=>"发送失败",
+ 'delOk'=>"删除成功",
+ 'settingOk'=>"设置成功",
+ 'notNull'=>"不能为空",
+ 'editOk'=>'修改成功',
+ 'editFail'=>'修改失败',
+ 'addOk'=>'添加成功',
+ 'addFail'=>'添加成功',
+ 'joinOk'=>'加入成功',
+ 'notAuth'=>"您没有操作权限!",
+ 'demoMode'=>"演示模式不支持修改",
+ 'parameterError'=>"参数错误",
+ 'longTime'=>'请求超时',
+ 'apiClose'=>"接口已关闭",
+ 'appIdError'=>'appId错误',
+ 'signError'=>'签名错误',
+ 'toofast'=>"您访问的太快了!"
+ ],
+ 'messageType'=>[
+ 'other'=>"[暂不支持的消息类型]",
+ 'image'=>'[图片]',
+ 'voice'=>'[语音]',
+ 'emoji'=>'[自定义表情]',
+ 'video'=>'[视频]',
+ 'file'=>'[文件]',
+ 'webrtcAudio'=>'[正在请求与您语音通话]',
+ 'webrtcVideo'=>'[正在请求与您视频通话]',
+ ],
+ 'friend'=>[
+ 'notAddOwn'=>"不能添加自己为好友",
+ 'already'=>"你们已经是好友了",
+ 'repeatApply'=>"你已经申请过了,请等待对方同意",
+ 'new'=>"新朋友",
+ "apply"=>"添加您为好友",
+ 'notApply'=>"申请不存在",
+ 'not'=>"好友不存在",
+ 'newChat'=>"你们已经成功添加为好友,现在开始聊天吧!",
+ 'limit'=>"您的好友已达上限!",
+ ],
+ 'group'=>[
+ 'name'=>"群聊",
+ 'notAuth'=>'你没有操作权限,只有群主和群管理员才可以修改!',
+ 'userLimit'=>'人数不能超过{:userMax}人!',
+ 'inviteLimit'=>'单次邀请人数不能超过{:limit}人!',
+ 'invite'=>"{:username}邀请你加入群聊",
+ 'removeUser'=>"您已被移出群聊!",
+ 'notCustom'=>"您不是本群成员,无权发送消息!",
+ 'add'=>"{:username}创建了群聊",
+ 'join'=>"{:username}加入了群聊",
+ 'atLeast'=>"请至少选择两人!",
+ 'alreadyJoin'=>'您已经加入该群!',
+ 'exist'=>"群聊不存在",
+ 'notice'=>"群公告",
+ 'all'=>"所有人",
+ 'noSpeak'=>"您已被禁言,恢复时间为:{:time}",
+ 'notSpeak'=>"群聊已禁言!",
+ 'limit'=>"您已被限制创建群聊!",
+ ],
+ 'user'=>[
+ 'exist'=>"用户不存在",
+ 'codeErr'=>'验证码不正确!',
+ 'newCodeErr'=>'新验证码不正确!',
+ 'passErr'=>"原密码不正确!",
+ 'already'=>"账户已存在",
+ 'registerOk'=>"注册成功",
+ 'loginOk'=>"登陆成功",
+ 'tokenFailure'=>"TOKEN已失效!",
+ 'forbid'=>'您的账号已被禁用',
+ 'passError'=>'密码错误',
+ 'logoutOk'=>'退出成功!',
+ 'closeRegister'=>'当前系统已关闭注册功能!',
+ 'inviteCode'=>'邀请码已失效!',
+ 'accountVerify'=>'账户必须为手机号或者邮箱',
+ 'waitMinute'=>"请一分钟后再试!",
+ "loginAccount"=>"登录账户",
+ "registerAccount"=>"注册账户",
+ "editPass"=>"修改密码",
+ "editAccount"=>"修改账户",
+ 'loginError' => '登陆信息有误 请重新登录',
+ 'mustToken' => '请先登陆系统',
+ 'blacklist' => '登陆已失效 请重新登陆',
+ 'expired' => '登陆已过期 请重新登陆',
+ 'notOwn' =>"客服不能为他本人",
+ 'loginLimit' =>"您的密码错误次数过多,请稍后再试!",
+ 'registerLimit'=>"请{:time}分钟后再注册!",
+ ],
+ 'im'=>[
+ 'forbidChat'=>"目前禁止用户私聊!",
+ 'notFriend'=>"您不在TA的好友列表,不能发消息!",
+ 'friendNot'=>"TA还不是您的好友,不能发消息!",
+ 'forwardLimit'=>"请选择转发的用户或者数量不操作{:count}个!",
+ 'exist'=>"消息不存在",
+ 'forwardRule'=>"由于规则限制,转发失败{:count}条!",
+ 'forwardOk'=>'转发成功',
+ 'you'=>'你',
+ 'other'=>'对方',
+ 'redoLimitTime'=>"超过{:time}分钟不能撤回!",
+ 'redo'=>"撤回了一条消息",
+ 'manageRedo'=>'被(管理员)撤回了一条消息',
+ 'msgContentLimit'=>'你发送的消息长度太长了!',
+ 'sendTimeLimit'=>'消息发送时间间隔为 {:time} 秒!',
+ 'forbidMsg'=>'[该消息涉嫌违规,已被屏蔽]',
+ ],
+ 'webRtc'=>[
+ 'cancel'=>'已取消通话',
+ 'refuse'=>'已拒绝',
+ 'notConnected'=>'未接通',
+ 'duration'=>'通话时长:{:time}',
+ 'busy'=>'忙线中',
+ 'other'=>'其他端已操作',
+ 'video'=>'视频通话',
+ 'audio'=>'语音通话',
+ 'answer'=>'接听通话请求',
+ 'exchange'=>'数据交换中',
+ 'fail'=>'通话失败',
+ ],
+ 'email'=>[
+ 'input'=>'请输入正确的邮箱',
+ 'testTitle'=>"测试邮件",
+ 'testContent'=>'这是一封测试邮件,当您收到之后表明您的所有配置都是正确的!',
+ ],
+ 'task'=>[
+ 'schedule' => '计划任务',
+ 'queue' => '消息队列',
+ 'worker' => '消息推送',
+ 'clearStd' => '清理日志',
+ 'null'=>"未知任务",
+ 'winRun'=>"windows启动请运行根目录下的:start_for_win.bat",
+ 'alreadyRun'=>"进程已启动",
+ 'startOk'=>"启动成功",
+ 'startFail'=>"启动失败",
+ 'notRun'=>"进程未启动",
+ 'logExist'=>"日志不存在",
+ ],
+ 'file'=>[
+ 'preview'=>"预览文件",
+ 'browserDown'=>"请使用浏览器下载",
+ 'exist'=>"文件不存在",
+ 'uploadLimit'=>"文件大小不能超过{:size}MB",
+ 'typeNotSupport'=>"文件格式不支持",
+ 'uploadOk'=>"上传成功"
+ ],
+ 'scan'=>[
+ 'failure'=>'二维码已失效'
+ ]
+];
\ No newline at end of file
diff --git a/app/manage/controller/Config.php b/app/manage/controller/Config.php
new file mode 100644
index 0000000..695abf0
--- /dev/null
+++ b/app/manage/controller/Config.php
@@ -0,0 +1,104 @@
+request->param('name');
+ $data = Conf::where(['name'=>$name])->value('value');
+ return success('', $data);
+ }
+
+ /**
+ * 获取配置
+ * @return \think\response\Json
+ */
+ public function getAllConfig()
+ {
+ $name=['sysInfo','chatInfo','smtp','fileUpload','compass'];
+ $list = Conf::where(['name'=>$name])->select();
+ return success('', $list);
+ }
+
+ /**
+ * 修改配置
+ * @return \think\response\Json
+ */
+ public function setConfig()
+ {
+ $name = $this->request->param('name');
+ $value = $this->request->param('value');
+ if(Conf::where(['name'=>$name])->find()){
+ Conf::where(['name'=>$name])->update(['value'=>$value]);
+ }else{
+ Conf::create(['name'=>$name,'value'=>$value]);
+ }
+ if($name=='fileUpload'){
+ updateEnv('driver',$value['disk']);
+ updateEnv('own',$value['preview']);
+ foreach ($value['aliyun'] as $k=>$v){
+ if($v){
+ updateEnv('aliyun_'.$k,$v);
+ }
+ }
+ foreach ($value['qiniu'] as $k=>$v){
+ if($v){
+ updateEnv('qiniu_'.$k,$v);
+ }
+ }
+ foreach ($value['qcloud'] as $k=>$v){
+ if($v){
+ updateEnv('qcloud_'.$k,$v);
+ }
+ }
+ }else{
+ // 更新系统缓存
+ $systemInfo=Conf::getSystemInfo(true);
+ // 向所有人推送新的设置
+ wsSendMsg(0,'updateConfig',$systemInfo);
+ }
+ return success(lang('system.editOk'));
+ }
+
+ /**
+ * 获取邀请链接
+ * @return \think\response\Json
+ */
+ public function getInviteLink(){
+ $uid=$this->userInfo['user_id'];
+ // 邀请码仅两天有效
+ $code=\utils\Str::random(8);
+ Cache::set($code,$uid,172800);
+ $url=getMainHost().'/index.html/#/register?inviteCode='.$code;
+ return success('',$url);
+ }
+
+ // 发送测试邮件
+ public function sendTestEmail(){
+ $email=$this->request->param('email');
+ if(!$email || !(\utils\Regular::is_email($email))){
+ return warning(lang('email.input'));
+ }
+ $conf=Conf::where(['name'=>'smtp'])->value('value');
+ $mail=new \mail\Mail($conf);
+ $mail->sendEmail([$email],lang('email.testTitle'),lang('email.testContent'));
+ return success(lang('system.sendOk'));
+
+ }
+}
\ No newline at end of file
diff --git a/app/manage/controller/Group.php b/app/manage/controller/Group.php
new file mode 100644
index 0000000..4f72cd6
--- /dev/null
+++ b/app/manage/controller/Group.php
@@ -0,0 +1,166 @@
+request->param();
+ //搜索关键词
+ if ($keyword = $this->request->param('keywords')) {
+ $model = $model->whereLike('name|name_py', '%' . $keyword . '%');
+ }
+ // 排序
+ $order='group_id DESC';
+ if ($param['order_field'] ?? '') {
+ $order = orderBy($param['order_field'],$param['order_type'] ?? 1);
+ }
+ $list = $this->paginate($model->where($map)->order($order));
+ if ($list) {
+ $data = $list->toArray()['data'];
+ $userList=UserModel::matchUser($data,true,'owner_id',120);
+ foreach($data as $k=>$v){
+ $data[$k]['avatar']=avatarUrl($v['avatar'],$v['name'],$v['group_id'],120);
+ $data[$k]['owner_id_info']=$userList[$v['owner_id']] ?? [];
+ }
+ }
+ return success('', $data, $list->total(), $list->currentPage());
+ }
+
+ // 更换群主
+ public function changeOwner()
+ {
+ $group_id = $this->request->param('group_id');
+ $user_id = $this->request->param('user_id');
+ $group=GroupModel::where('group_id',$group_id)->find();
+ if(!$group){
+ return warning(lang('group.exist'));
+ }
+ $user=UserModel::where('user_id',$user_id)->find();
+ if(!$user){
+ return warning(lang('user.exist'));
+ }
+ Db::startTrans();
+ try{
+ GroupUser::where('group_id',$group_id)->where('user_id',$user_id)->update(['role'=>1]);
+ GroupUser::where('group_id',$group_id)->where('user_id',$group->owner_id)->update(['role'=>3]);
+ $group->owner_id=$user_id;
+ $group->save();
+ wsSendMsg($group_id,"changeOwner",['group_id'=>'group-'.$group_id,'user_id'=>$user_id],1);
+ Db::commit();
+ return success('');
+ }catch (\Exception $e){
+ Db::rollback();
+ return warning('');
+ }
+ }
+
+ // 解散群聊
+ public function del()
+ {
+ $group_id = $this->request->param('group_id');
+ $group=GroupModel::where('group_id',$group_id)->find();
+ if(!$group){
+ return warning(lang('group.exist'));
+ }
+ Db::startTrans();
+ try{
+ // 删除团队成员
+ GroupUser::where('group_id',$group_id)->delete();
+ // 删除团队
+ GroupModel::destroy($group_id);
+ wsSendMsg($group_id,"removeGroup",['group_id'=>'group-'.$group_id],1);
+ Db::commit();
+ return success('');
+ }catch (\Exception $e){
+ Db::rollback();
+ return warning('');
+ }
+ }
+
+ // 添加群成员
+ public function addGroupUser(){
+ $param = $this->request->param();
+ $uid=$this->userInfo['user_id'];
+ $group_id = $param['group_id'];
+ $group=GroupModel::where('group_id',$group_id)->find();
+ if(!$group){
+ return warning(lang('group.exist'));
+ }
+ $user_ids=$param['user_ids'];
+ $data=[];
+ try{
+ foreach($user_ids as $k=>$v){
+ $data[]=[
+ 'group_id'=>$group_id,
+ 'user_id'=>$v,
+ 'role'=>3,
+ 'invite_id'=>$uid
+ ];
+ }
+ $groupUser=new GroupUser;
+ $groupUser->saveAll($data);
+ $url=GroupModel::setGroupAvatar($group_id);
+ wsSendMsg($group_id,"addGroupUser",['group_id'=>"group-".$group_id,'avatar'=>$url],1);
+ return success(lang('system.addOk'));
+ }catch(\Exception $e){
+ return error($e->getMessage());
+ }
+
+ }
+
+ // 删除群成员
+ public function delGroupUser(){
+ $param = $this->request->param();
+ $group_id = $param['group_id'];
+ $group=GroupModel::where('group_id',$group_id)->find();
+ if(!$group){
+ return warning(lang('group.exist'));
+ }
+ $user_id=$param['user_id'];
+ $groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
+ if($groupUser){
+ $groupUser->delete();
+ wsSendMsg($group_id,"removeUser",['group_id'=>'group-'.$group_id],1);
+ return success('');
+ }else{
+ return warning('');
+ }
+
+ }
+
+ // 设置管理员
+ public function setManager(){
+ $param = $this->request->param();
+ $group_id = $param['group_id'];
+ $group=GroupModel::where('group_id',$group_id)->find();
+ if(!$group){
+ return warning(lang('group.exist'));
+ }
+ $user_id=$param['user_id'];
+ $role=$param['role'];
+ $groupUser=GroupUser::where(['group_id'=>$group_id,'user_id'=>$user_id])->find();
+ if($groupUser){
+ $groupUser->role=$role;
+ $groupUser->save();
+ wsSendMsg($group_id,"setManager",['group_id'=>'group-'.$group_id],1);
+ return success('');
+ }else{
+ return warning('');
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/manage/controller/Index.php b/app/manage/controller/Index.php
new file mode 100644
index 0000000..d66ce82
--- /dev/null
+++ b/app/manage/controller/Index.php
@@ -0,0 +1,107 @@
+userInfo['user_id']!=1){
+ return warning('system.noAuth');
+ }
+ Message::where(['status'=>1])->delete();
+ return success('system.clearOk');
+ }
+
+ // 公告列表
+ public function noticeList(){
+ $model=new Message();
+ // 排序
+ $order='msg_id DESC';
+ $map=['chat_identify'=>"admin_notice"];
+ $list = $this->paginate($model->where($map)->order($order));
+ if ($list) {
+ $data = $list->toArray()['data'];
+ foreach($data as $k=>$v){
+ $data[$k]['title']=$v['extends']['title'];
+ }
+ }
+ return success('', $data, $list->total(), $list->currentPage());
+ }
+
+ // 删除公告
+ public function delNotice(){
+ $param=$this->request->param();
+ $msgId=$param['id'] ?:0;
+ $map=['msg_id'=>$msgId];
+ $message=Message::where($map)->find();
+ if($message){
+ Message::where($map)->delete();
+ }
+ return success('');
+ }
+
+ // 发布公告
+ public function publishNotice(){
+ $userInfo=$this->userInfo;
+ if($userInfo['user_id']!=1){
+ return warning('system.noAuth');
+ }
+ $param=$this->request->param();
+ $msgId=$param['msgId'] ?? 0;
+ $content="
".$param['content']."
"; + $data=[ + 'from_user'=>$userInfo['user_id'], + 'to_user'=>0, + 'content'=>str_encipher($content,true), + 'chat_identify'=>'admin_notice', + 'create_time'=>time(), + 'type'=>'text', + 'is_group'=>2, + 'is_read'=>1, + 'is_top'=>0, + 'is_notice'=>1, + 'at'=>[], + 'pid'=>0, + 'extends'=>['title'=>$param['title'],'notice'=>$param['content']], + ]; + if($msgId){ + Message::where(['msg_id'=>$msgId])->update([ + 'content'=>$data['content'], + 'extends'=>$data['extends'], + ]); + }else{ + $data['id']=\utils\Str::getUuid(); + $message=new Message(); + $message->save($data); + $msgId=$message->msg_id; + } + $msgInfo=$data; + $msgInfo['status']='successd'; + $msgInfo['msg_id']=$msgId; + $msgInfo['user_id']=$userInfo['user_id']; + $msgInfo['sendTime']=time()*1000; + $msgInfo['toContactId']='admin_notice'; + $msgInfo['to_user']='admin_notice'; + $msgInfo['content']=$param['title']; + $msgInfo['fromUser']=[ + 'id'=>$userInfo['user_id'], + 'avatar'=>avatarUrl($userInfo['avatar'],$userInfo['realname'],$userInfo['user_id'],120), + 'displayName'=>$userInfo['realname'] + ]; + wsSendMsg(0,'simple',$msgInfo,1); + return success(''); + } +} \ No newline at end of file diff --git a/app/manage/controller/Message.php b/app/manage/controller/Message.php new file mode 100644 index 0000000..3bb49ba --- /dev/null +++ b/app/manage/controller/Message.php @@ -0,0 +1,209 @@ +request->param(); + $user_id=$param['user_id'] ?? 0; + $toContactId=$param['toContactId'] ?? 0; + $is_group=($param['is_group'] ?? 0) ? $param['is_group']-1 : -1; + $map = [ 'status' => 1]; + if($user_id){ + if(!$toContactId){ + return warning(lang('system.parameterError')); + } + $chat_identify=chat_identify($param['user_id'],$param['toContactId']); + $map['chat_identify'] = $chat_identify; + } + if($is_group>=0){ + $map['is_group']=$is_group; + } + $type = isset($param['type']) ? $param['type'] : ''; + $where = []; + if ($type && $type != "all") { + $map['type'] = $type; + } else { + $where[] = ['type', 'not in', ['event','admin_notice','webrtc']]; + } + $keywords = isset($param['keywords']) ? $param['keywords'] : ''; + if ($keywords && in_array($type, ['text', 'all'])) { + $where[] = ['content', 'like', '%' . $keywords . '%']; + $where[] = ['type', '=', 'text']; + } + $listRows = $param['limit'] ?: 20; + $pageSize = $param['page'] ?: 1; + $last_id = $param['last_id'] ?? 0; + if($last_id){ + $where[]=['msg_id','<',$last_id]; + } + $list = MessageModel::getList($map, $where, 'msg_id desc', $listRows, $pageSize); + $data = $this->recombileMsg($list); + return success('', $data, $list->total(),$list->currentPage()); + } + + protected function recombileMsg($list,$isPagination=true) + { + $data = []; + if ($list) { + $listData = $isPagination ? $list->toArray()['data'] : $list; + $userList = User::matchUser($listData, true, 'from_user', 120); + foreach ($listData as $k => $v) { + $content = str_encipher($v['content'],false); + $preview = ''; + $ext=''; + if (in_array($v['type'], $this->fileType)) { + $content = getFileUrl($content); + $preview = previewUrl($content); + $ext=getExtUrl($content); + } + $fromUser = $userList[$v['from_user']]; + $toContactId=$v['is_group'] ==1 ? 'group-'.$v['to_user'] : $v['to_user']; + $atList=($v['at'] ?? null) ? explode(',',$v['at']): []; + if($v['is_group']==0){ + $toUser=User::where(['user_id'=>$v['to_user']])->field(User::$defaultField)->find() ?? []; + if($toUser){ + $toUser=[ + 'name'=>$toUser['realname'] + ]; + } + + }else{ + $toUser=Group::where(['group_id'=>$v['to_user']])->find(); + if($toUser){ + $toUser=[ + 'name'=>$toUser['name'] + ]; + } + } + $data[] = [ + 'msg_id' => $v['msg_id'], + 'id' => $v['id'], + 'status' => "succeed", + 'type' => $v['type'], + 'sendTime' => $v['create_time'] * 1000, + 'create_time' => is_string($v['create_time']) ? $v['create_time'] : date('Y-m-d H:i:s',$v['create_time']), + 'content' => $content, + 'preview' => $preview, + 'download' => $v['file_id'] ? getMainHost().'/filedown/'.encryptIds($v['file_id']) : '', + 'is_read' => $v['is_read'], + 'is_group' => $v['is_group'], + 'at' => $atList, + 'toContactId' => $toContactId, + 'from_user' => $v['from_user'], + 'file_id' => $v['file_id'], + 'file_cate' => $v['file_cate'], + 'fileName' => $v['file_name'], + 'fileSize' => $v['file_size'], + 'fromUser' => $fromUser, + 'toUser' => $toUser, + 'extUrl'=>$ext, + 'extends'=>is_string($v['extends'])?json_decode($v['extends'],true) : $v['extends'] + ]; + } + } + return $data; + } + + // 获取某个联系人的好友列表 + public function getContacts(){ + $param = $this->request->param(); + $user_id=$param['user_id'] ?? 0; + if(!$user_id){ + return warning(lang('system.parameterError')); + } + $config=$this->globalConfig; + $listRows = $param['limit'] ?: 20; + $pageSize = $param['page'] ?: 1; + $keywords = $param['keywords'] ?: ''; + $where=[['status','=',1]]; + if($keywords){ + $where[] = ['realname', 'like', '%' . $keywords . '%']; + } + $hasConvo=$param['hasConvo'] ?? 0; + if($hasConvo){ + // 查询最近的联系人 + $map1 = [['to_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]]; + $map2 = [['from_user', '=', $user_id], ['is_last', '=', 1], ['is_group', '=', 0]]; + $msgField = 'from_user,to_user,content as lastContent,create_time as lastSendTime,chat_identify,type,del_user'; + $lasMsgList = Db::name('message') + ->field($msgField) + ->whereOr([$map1, $map2]) + ->order('create_time desc') + ->select(); + $ids1=\utils\Arr::arrayToString($lasMsgList,'from_user',false); + $ids2=\utils\Arr::arrayToString($lasMsgList,'to_user',false); + $ids=array_merge($ids1,$ids2); + $userList = array_diff($ids, [$user_id]); + $where[]=['user_id','in',$userList]; + }else{ + // 如果是社区模式,就只查询的好友,如果是企业模式,就查询所有用户 + if($config['sysInfo']['runMode']==1){ + $where[]=['user_id','<>',$user_id]; + }else{ + $friendList = Friend::getFriend(['create_user' => $user_id,'status'=>1]); + $userList = array_keys($friendList); + $where[]=['user_id','in',$userList]; + } + } + + $list = User::where($where)->field(User::$defaultField)->paginate(['list_rows'=>$listRows,'page'=>$pageSize]); + $data=[]; + if($list){ + $data=$list->toArray()['data']; + foreach ($data as $k => $v) { + $data[$k]['avatar'] = avatarUrl($v['avatar'], $v['realname'], $v['user_id'], 120); + $data[$k]['id'] = $v['user_id']; + } + } + return success('',$data,$list->total(),$list->currentPage()); + } + + // 消息处理 + public function dealMsg(){ + $param = $this->request->param(); + $id = $param['id']; + $message = MessageModel::where(['id' => $id])->find(); + if ($message) { + $dealType=$param['dealType'] ?? 0; + $content=$message['content'] ?? ''; + if($dealType==1){ + MessageModel::where(['id' => $id])->delete(); + // 如果是最后一条消息,需要将上一条设置为最后一条 + if($message['is_last']){ + MessageModel::where(['chat_identify'=>$message['chat_identify']])->order('msg_id desc')->limit(1)->update(['is_last'=>1]); + } + $action='delMessage'; + }else{ + $content=str_encipher(lang('im.forbidMsg'),true); + MessageModel::where(['id' => $id])->update(['content'=>$content,'type'=>'text']); + $action='updateMessage'; + } + $toContactId = $message['to_user']; + if ($message['is_group'] == 1) { + $toContactId = explode('-', $message['chat_identify'])[1]; + } + $data=[ + 'id'=>$message['id'], + 'content'=>str_encipher($content,false), + ]; + wsSendMsg($toContactId, $action, $data, $message['is_group']); + return success(''); + } else { + return warning(lang('im.exist')); + } + } +} \ No newline at end of file diff --git a/app/manage/controller/Task.php b/app/manage/controller/Task.php new file mode 100644 index 0000000..3a29c6e --- /dev/null +++ b/app/manage/controller/Task.php @@ -0,0 +1,190 @@ +rootPath = root_path(); + chdir($this->rootPath); + $this->taskNames = [ + 'schedule' => lang('task.schedule'), + 'queue' => lang('task.queue'), + 'worker' => lang('task.worker'), + 'clearStd' => lang('task.clearStd'), + ]; + } + + /** + * 任务列表 + * @return Response + */ + public function getTaskList() + { + $data = $this->taskMsg(); + + if (!count($data)) { + return warning(''); + } + + foreach ($data as &$datum) { + $expName = explode('_', $datum['name']); + + $datum['remark'] = $this->taskNames[$expName[count($expName) - 1]] ?? lang('task.null'); + } + unset($datum); + return success('', $data); + } + + /** + * 启动全部进程 + * @return Response + */ + public function startTask() + { + if(strpos(strtolower(PHP_OS), 'win') === 0) + { + return warning(lang('task.winRun')); + } + + if (count($this->taskMsg())) { + return warning(lang('task.alreadyRun')); + } + + // 启动 + $out = Terminal::instance(2)->exec('php think task start'); + if (!count($this->analysisMsg($out))) { + return warning(lang('task.startFail')); + } + + return success(lang('task.startOk')); + } + + /** + * 强制停止全部进程 + * @return Response + */ + public function stopTask() + { + if (!count($this->taskMsg())) { + return warning(lang('task.notRun')); + } + + // 强制停止 + Terminal::instance(2)->exec('php think task stop force'); + + return success(''); + } + + /** + * 获取单个任务日志 + * @return Response + */ + public function getTaskLog() + { + $name = $this->request->param('name'); + + $path = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'easy_task' . DIRECTORY_SEPARATOR . 'Std' . DIRECTORY_SEPARATOR; + + if (!file_exists($path . 'exec_' . $name . '.std')) { + $expName = explode('_', $name); + $name = $expName[count($expName) - 1]; + if (!file_exists($path . 'exec_' . $name . '.std')) { + return warning(lang('task.logExist')); + } + } + + return success('', file_get_contents($path . 'exec_' . $name . '.std')); + } + + /** + * 清理单个任务日志 + * @return Response + */ + public function clearTaskLog() + { + $name = $this->request->param('name'); + + $path = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'easy_task' . DIRECTORY_SEPARATOR . 'Std' . DIRECTORY_SEPARATOR; + + if (!file_exists($path . 'exec_' . $name . '.std')) { + $expName = explode('_', $name); + $name = $expName[count($expName) - 1]; + if (!file_exists($path . 'exec_' . $name . '.std')) { + return warning(lang('task.logExist')); + } + } + + file_put_contents($path . 'exec_' . $name . '.std', ''); + return success(''); + } + + + /** + * 获取运行状态 + * @return array + */ + private function taskMsg() + { + $out = Terminal::instance(2)->exec('php think task status'); + return $this->analysisMsg($out); + } + + /** + * 解析数据 + * @param string $out 带解析数据 + * @return array + */ + private function analysisMsg(string $out) + { + $re = '/│ *([\w+]+) *│ *([\w+]+)[ ]*│ *([\w+]+|[0-9- :]+) *│ *([\w+]+) *│ *([\w+]+) *│ *([\w+]+) *│/m'; + + preg_match_all($re, $out, $matches, PREG_SET_ORDER, 0); + + if (!count($matches)) { + return []; + } + + $data = []; + $names = $matches[0]; + unset($names[0]); + $names = array_values($names); + unset($matches[0]); + + foreach ($matches as $match) { + $temp = []; + foreach ($match as $key => $item) { + if ($key !== 0) { + $temp[$names[$key - 1]] = $item; + } + } + $data[] = $temp; + } + + return $data; + } +} \ No newline at end of file diff --git a/app/manage/controller/User.php b/app/manage/controller/User.php new file mode 100644 index 0000000..c7d4c1e --- /dev/null +++ b/app/manage/controller/User.php @@ -0,0 +1,204 @@ +request->param(); + //搜索关键词 + if ($keyword = $this->request->param('keywords')) { + $model = $model->whereLike('realname|account|name_py|email', '%' . $keyword . '%'); + } + // 排序 + $order='user_id DESC'; + if ($param['order_field'] ?? '') { + $order = orderBy($param['order_field'],$param['order_type'] ?? 1); + } + $list = $this->paginate($model->where($map)->order($order)); + if ($list) { + $data = $list->toArray()['data']; + foreach($data as $k=>$v){ + $data[$k]['avatar']=avatarUrl($v['avatar'],$v['realname'],$v['user_id'],120); + $data[$k]['location']=$v['last_login_ip'] ? implode(" ", \Ip::find($v['last_login_ip'])) : '--'; + $data[$k]['reg_location']=$v['register_ip'] ? implode(" ", \Ip::find($v['register_ip'])) : '--'; + $data[$k]['last_login_time']=$v['last_login_time'] ? date('Y-m-d H:i:s',$v['last_login_time']) : '--'; + unset($data[$k]['password']); + } + } + return success('', $data, $list->total(), $list->currentPage()); + } + + // 添加用户 + public function add() + { + try{ + $data = $this->request->param(); + $user=new UserModel(); + $verify=$user->checkAccount($data); + if(!$verify){ + return warning($user->getError()); + } + $salt=\utils\Str::random(4); + $data['password'] = password_hash_tp($data['password'],$salt); + $data['salt'] =$salt; + $data['register_ip'] =$this->request->ip(); + $data['name_py'] = pinyin_sentence($data['realname']); + $user->save($data); + $data['user_id']=$user->user_id; + return success(lang('system.addOk'), $data); + }catch (\Exception $e){ + return error(lang('system.addFail')); + } + } + + // 修改用户 + public function edit() + { + try{ + $data = $this->request->param(); + $user=new UserModel(); + $verify=$user->checkAccount($data); + if(!$verify){ + return warning($user->getError()); + } + $user=UserModel::find($data['user_id']); + $user->account =$data['account']; + $user->realname =$data['realname']; + $user->email =$data['email']; + $user->remark=$data['remark']; + $user->sex =$data['sex'] ?? 0; + $user->friend_limit =$data['friend_limit']; + $user->group_limit =$data['group_limit']; + $csUid=$data['cs_uid'] ?? 0; + if($csUid && $csUid==$data['user_id']){ + return warning(lang('user.notOwn')); + } + $user->cs_uid =$data['cs_uid']; + // 只有超管才能设置管理员 + if($this->userInfo['user_id']==1){ + $user->role =$data['role']; + } + $user->status =$data['status']; + $user->name_py= pinyin_sentence($data['realname']); + $user->save(); + return success(lang('system.editOk'), $data); + }catch (\Exception $e){ + return error(lang('system.editFail')); + } + } + + // 删除用户 + public function del() + { + $user_id = $this->request->param('user_id'); + $user=UserModel::find($user_id); + if(!$user || $user->user_id==1){ + return warning(lang('user.exist')); + } + Db::startTrans(); + try{ + // 删除其好友关系 + Friend::where('create_user', $user_id)->whereOr(['friend_user_id'=>$user_id])->delete(); + // 删除其群组关系 + GroupUser::where('user_id', $user_id)->delete(); + UserModel::destroy($user_id); + Db::commit(); + return success(lang('system.delOk')); + }catch (\Exception $e){ + Db::rollback(); + return error($e->getMessage()); + } + } + + // 修改用户状态 + public function setStatus() + { + $user_id = $this->request->param('user_id'); + $user=UserModel::find($user_id); + if(!$user){ + return warning(lang('user.exist')); + } + try{ + $status = $this->request->param('status',0); + // 将禁用状态写入缓存 + if(!$status){ + Cache::set('forbidUser_'.$user_id,true,env('jwt.ttl',86400)); + } + UserModel::where('user_id', $user_id)->update(['status'=>$status]); + return success(lang('system.editOk')); + }catch (\Exception $e){ + return error(lang('system.editFail')); + } + } + + // 获取用户信息 + public function detail() + { + $user_id = $this->request->param('user_id'); + $user=UserModel::find($user_id); + if(!$user){ + return error(lang('user.exist')); + } + $user->avatar=avatarUrl($user->avatar,$user->realname,$user->user_id,120); + $location=''; + if($user->last_login_ip){ + $location=implode(" ", \Ip::find($user->last_login_ip)); + } + $user->location=$location; + $user->password=''; + return success('', $user); + } + + // 设置用户角色 + public function setRole() + { + $user_id = $this->request->param('user_id'); + $user=UserModel::find($user_id); + if(!$user){ + return warning(lang('user.exist')); + } + try{ + $role = $this->request->param('role'); + UserModel::where('user_id', $user_id)->update(['role'=>$role]); + return success(''); + }catch (\Exception $e){ + return error(''); + } + } + + // 修改密码 + public function editPassword() + { + $user_id = $this->request->param('user_id'); + $user=UserModel::find($user_id); + if(!$user){ + return warning(lang('user.exist')); + } + try{ + $password = $this->request->param('password',''); + if($password){ + $salt=$user->salt; + $user->password= password_hash_tp($password,$salt); + Cache::set('forbidUser_'.$user_id,true,env('jwt.ttl',86400)); + } + $user->save(); + return success(''); + }catch (\Exception $e){ + return error(''); + } + } + +} \ No newline at end of file diff --git a/app/manage/middleware.php b/app/manage/middleware.php new file mode 100644 index 0000000..f83b1f3 --- /dev/null +++ b/app/manage/middleware.php @@ -0,0 +1,5 @@ + + */ +namespace app\manage\model; + +use app\BaseModel; +use think\facade\Cache; +class Config extends BaseModel +{ + protected $json = ['value']; + protected $jsonAssoc = true; + + // 获取系统配置信息 + public static function getSystemInfo($update=false){ + $name='systemInfo'; + // $auth=request()->header('Authorization'); + $nameFields=['sysInfo','fileUpload','chatInfo','compass']; + // 如果是登录状态才会返回chatINfo + // if($auth){ + // $name='all'.$name; + // $nameFields[]="chatInfo"; + // } + if(Cache::has($name) && !$update){ + $systemInfo=Cache::get($name); + }else{ + $systemInfo=[]; + $conf=Config::where([['name','in',$nameFields]])->select()->toArray(); + foreach($conf as $v){ + $value=[]; + if($v['name']=='fileUpload'){ + $value['size'] = $v['value']['size']; + $value['preview'] = $v['value']['preview']; + $value['fileExt'] = $v['value']['fileExt']; + }else{ + $value=$v['value']; + } + $systemInfo[$v['name']]=$value; + } + Cache::set($name,$systemInfo,7*86400); + } + return $systemInfo; + } +} \ No newline at end of file diff --git a/app/middleware.php b/app/middleware.php new file mode 100644 index 0000000..0bdd37e --- /dev/null +++ b/app/middleware.php @@ -0,0 +1,19 @@ + Request::class, + 'think\exception\Handle' => ExceptionHandle::class, +]; diff --git a/app/service.php b/app/service.php new file mode 100644 index 0000000..db1ee6a --- /dev/null +++ b/app/service.php @@ -0,0 +1,9 @@ + +// +---------------------------------------------------------------------- + +namespace app\worker; + +use think\App; +use think\exception\Handle; +use think\exception\HttpException; +use Workerman\Connection\TcpConnection; +use Workerman\Protocols\Http\Response; +/** + * Worker应用对象 + */ +class Application extends App +{ + /** + * 处理Worker请求 + * @access public + * @param \Workerman\Connection\TcpConnection $connection + * @param void + */ + public function worker(TcpConnection $connection) + { + try { + $this->beginTime = microtime(true); + $this->beginMem = memory_get_usage(); + $this->db->clearQueryTimes(); + + $pathinfo = ltrim(strpos($_SERVER['REQUEST_URI'], '?') ? strstr($_SERVER['REQUEST_URI'], '?', true) : $_SERVER['REQUEST_URI'], '/'); + + $this->request + ->setPathinfo($pathinfo) + ->withInput($GLOBALS['HTTP_RAW_POST_DATA']); + + while (ob_get_level() > 1) { + ob_end_clean(); + } + + ob_start(); + $response = $this->http->run(); + $content = ob_get_clean(); + + ob_start(); + + $response->send(); + $this->http->end($response); + + $content .= ob_get_clean() ?: ''; + + $this->httpResponseCode($response->getCode()); + $header=[]; + foreach ($response->getHeader() as $name => $val) { + // 发送头部信息 + $header[$name] =!is_null($val) ? $val : ''; + } + if (strtolower($_SERVER['HTTP_CONNECTION']) === "keep-alive") { + $connection->send(new Response(200, $header, $content)); + } else { + $connection->close(new Response(200, $header, $content)); + } + } catch (HttpException | \Exception | \Throwable $e) { + $this->exception($connection, $e); + } + } + + /** + * 是否运行在命令行下 + * @return bool + */ + public function runningInConsole(): bool + { + return false; + } + + protected function httpResponseCode($code = 200) + { + new Response($code); + } + + protected function exception($connection, $e) + { + if ($e instanceof \Exception) { + $handler = $this->make(Handle::class); + $handler->report($e); + + $resp = $handler->render($this->request, $e); + $content = $resp->getContent(); + $code = $resp->getCode(); + + $this->httpResponseCode(new Response($code, [], $content)); + $connection->send($content); + } else { + $connection->send(new Response(500, [], $e->getMessage())); + } + } + +} \ No newline at end of file diff --git a/app/worker/Events.php b/app/worker/Events.php new file mode 100644 index 0000000..be4ee0c --- /dev/null +++ b/app/worker/Events.php @@ -0,0 +1,142 @@ + + * @copyright walkor