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['title']."


".$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 + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + + namespace app\worker; +/** + * 推送主逻辑 + * 主要是处理 onMessage onClose + */ +use GatewayWorker\Lib\Gateway; +use app\worker\Application; +use think\facade\Config; +use Lcobucci\JWT\Builder; +use Lcobucci\JWT\Parser; +use thans\jwt\provider\JWT\Lcobucci; +use utils\Aes; + +class Events +{ + // 使用TP框架 + public static function onWorkerStart() + { + $app = new Application; + $app->initialize(); + } + + // 当有客户端连接时,将client_id返回,让mvc框架判断当前uid并执行绑定 + public static function onConnect($client_id) + { + Gateway::sendToClient($client_id, json_encode(array( + 'type' => 'init', + 'client_id' => $client_id + ))); + } + /** + * 有消息时 + * @param int $client_id + * @param mixed $message + */ + public static function onMessage($client_id, $message) + { + // 客户端传递的是json数据 + $message_data = json_decode($message, true); + if(!$message_data) + { + return ; + } + + // 根据类型执行不同的业务 + switch($message_data['type']) + { + // 客户端回应服务端的心跳 + case 'pong': + break; + case 'ping': + self::sendStatus($client_id); + break; + case 'bindUid': + self::auth($client_id,$message_data); + break; + } + return; + } + + protected static function sendStatus($client_id){ + $uid=$_SESSION['user_id'] ?? 0; + $multiport=false; + if($uid){ + $arr=Gateway::getClientIdByUid($uid); + if(count($arr)>1){ + $multiport=true; + } + } + Gateway::sendToClient($client_id, json_encode(array( + 'type' => 'pong', + 'multiport' => $multiport, + ))); + } + + //验证用户的真实性并绑定 + protected static function auth($client_id, $msg){ + $token=$msg['token'] ?? ''; + $config = Config::get('jwt'); + $keys = $config['secret'] ?: [ + 'public' => $config['public_key'], + 'private' => $config['private_key'], + 'password' => $config['password'], + ]; + $provider = new Lcobucci(new Builder(), new Parser(), $config['algo'], $keys); + try { + $token=str_replace('bearer ','',$token); + $jwtData = $provider->decode((string)$token); + } catch (\Exception $exception) { + self::closeClient($client_id); + } + + $userInfo = $jwtData['info']->getValue(); + //解密token中的用户信息 + $userInfo = Aes::decrypt($userInfo, config('app.aes_token_key')); + //解析json + $userInfo = (array)json_decode($userInfo, true); + if(!$userInfo){ + self::closeClient($client_id); + } + $_SESSION['user_id']=$userInfo['user_id']; + self::sendStatus($client_id); + } + + //断开连接 + protected static function closeClient($client_id){ + $_SESSION['user_id']=null; + Gateway::closeClient($client_id); + } + + /** + * 当断开连接时 + * @param int $client_id + */ + public static function onClose($client_id) + { + $user_id=$_SESSION['user_id']; + if($user_id){ + Gateway::sendToAll(json_encode(array( + 'type' => 'isOnline', + 'time' => time(), + 'data' => ['id'=>$user_id,'is_online'=>0] + ))); + } + + } + +} diff --git a/app/worker/command/GatewayWorker.php b/app/worker/command/GatewayWorker.php new file mode 100644 index 0000000..bffa0ff --- /dev/null +++ b/app/worker/command/GatewayWorker.php @@ -0,0 +1,201 @@ + +// +---------------------------------------------------------------------- + +namespace app\worker\command; + +use GatewayWorker\BusinessWorker; +use GatewayWorker\Gateway; +use GatewayWorker\Register; +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\input\Option; +use think\console\Output; +use think\facade\Config; +use Workerman\Worker; + +/** + * Worker 命令行类 + */ +class GatewayWorker extends Command +{ + public function configure() + { + $this->setName('worker:gateway') + ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload|status|connections", 'start') + ->addOption('host', 'H', Option::VALUE_OPTIONAL, 'the host of workerman server.', null) + ->addOption('port', 'p', Option::VALUE_OPTIONAL, 'the port of workerman server.', null) + ->addOption('daemon', 'd', Option::VALUE_NONE, 'Run the workerman server in daemon mode.') + ->setDescription('GatewayWorker Server for ThinkPHP'); + } + + public function execute(Input $input, Output $output) + { + $action = $input->getArgument('action'); + + if (DIRECTORY_SEPARATOR !== '\\') { + if (!in_array($action, ['start', 'stop', 'reload', 'restart', 'status', 'connections'])) { + $output->writeln("Invalid argument action:{$action}, Expected start|stop|restart|reload|status|connections ."); + exit(1); + } + + global $argv; + array_shift($argv); + array_shift($argv); + array_unshift($argv, 'think', $action); + } else { + $output->writeln("GatewayWorker Not Support On Windows."); + exit(1); + } + + if ('start' == $action) { + $output->writeln('Starting GatewayWorker server...'); + } + + $option = Config::get('gateway'); + + if ($input->hasOption('host')) { + $host = $input->getOption('host'); + } else { + $host = !empty($option['host']) ? $option['host'] : '0.0.0.0'; + } + + if ($input->hasOption('port')) { + $port = $input->getOption('port'); + } else { + $port = !empty($option['port']) ? $option['port'] : '2347'; + } + + $this->start($host, (int) $port, $option); + } + + /** + * 启动 + * @access public + * @param string $host 监听地址 + * @param integer $port 监听端口 + * @param array $option 参数 + * @return void + */ + public function start(string $host, int $port, array $option = []) + { + $registerAddress = !empty($option['registerAddress']) ? $option['registerAddress'] : '127.0.0.1:1236'; + + if (!empty($option['register_deploy'])) { + // 分布式部署的时候其它服务器可以关闭register服务 + // 注意需要设置不同的lanIp + $this->register($registerAddress); + } + + // 启动businessWorker + if (!empty($option['businessWorker_deploy'])) { + $this->businessWorker($registerAddress, $option['businessWorker'] ?? []); + } + + // 启动gateway + if (!empty($option['gateway_deploy'])) { + $this->gateway($registerAddress, $host, $port, $option); + } + + Worker::runAll(); + } + + /** + * 启动register + * @access public + * @param string $registerAddress + * @return void + */ + public function register(string $registerAddress) + { + // 初始化register + new Register('text://' . $registerAddress); + } + + /** + * 启动businessWorker + * @access public + * @param string $registerAddress registerAddress + * @param array $option 参数 + * @return void + */ + public function businessWorker(string $registerAddress, array $option = []) + { + // 初始化 bussinessWorker 进程 + $worker = new BusinessWorker(); + + $this->option($worker, $option); + + $worker->registerAddress = $registerAddress; + } + + /** + * 启动gateway + * @access public + * @param string $registerAddress registerAddress + * @param string $host 服务地址 + * @param integer $port 监听端口 + * @param array $option 参数 + * @return void + */ + public function gateway(string $registerAddress, string $host, int $port, array $option = []) + { + // 初始化 gateway 进程 + if (!empty($option['socket'])) { + $socket = $option['socket']; + unset($option['socket']); + } else { + $protocol = !empty($option['protocol']) ? $option['protocol'] : 'websocket'; + $socket = $protocol . '://' . $host . ':' . $port; + unset($option['host'], $option['port'], $option['protocol']); + } + + $gateway = new Gateway($socket, $option['context'] ?? []); + + // 以下设置参数都可以在配置文件中重新定义覆盖 + $gateway->name = 'Gateway'; + $gateway->count = 4; + $gateway->lanIp = '127.0.0.1'; + $gateway->startPort = 2000; + $gateway->pingInterval = 30; + $gateway->pingNotResponseLimit = 0; + $gateway->pingData = '{"type":"ping"}'; + $gateway->registerAddress = $registerAddress; + + // 全局静态属性设置 + foreach ($option as $name => $val) { + if (in_array($name, ['stdoutFile', 'daemonize', 'pidFile', 'logFile'])) { + Worker::${$name} = $val; + unset($option[$name]); + } + } + + $this->option($gateway, $option); + } + + /** + * 设置参数 + * @access protected + * @param Worker $worker Worker对象 + * @param array $option 参数 + * @return void + */ + protected function option(Worker $worker, array $option = []) + { + // 设置参数 + if (!empty($option)) { + foreach ($option as $key => $val) { + $worker->$key = $val; + } + } + } + +} \ No newline at end of file diff --git a/app/worker/start_businessworker.php b/app/worker/start_businessworker.php new file mode 100644 index 0000000..ca678ca --- /dev/null +++ b/app/worker/start_businessworker.php @@ -0,0 +1,32 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +use \Workerman\Worker; +use \GatewayWorker\BusinessWorker; + +require_once __DIR__ . '/../../vendor/autoload.php'; +// bussinessWorker 进程 +$worker = new BusinessWorker(); +// worker名称 +$worker->name = 'PushBusinessWorker'; +// bussinessWorker进程数量 +$worker->count = 1; +// 服务注册地址 +$worker->registerAddress = '127.0.0.1:1236'; +$worker->eventHandler = 'app\worker\Events'; +// 如果不是在根目录启动,则运行runAll方法 +if(!defined('GLOBAL_START')) +{ + Worker::runAll(); +} + diff --git a/app/worker/start_gateway.php b/app/worker/start_gateway.php new file mode 100644 index 0000000..7ee25b8 --- /dev/null +++ b/app/worker/start_gateway.php @@ -0,0 +1,42 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +use \Workerman\Worker; +use \GatewayWorker\Gateway; +use \Workerman\Autoloader; +require_once __DIR__ . '/../../vendor/autoload.php'; + +// gateway 进程 +$gateway = new Gateway("Websocket://0.0.0.0:8282"); +// 设置名称,方便status时查看 +$gateway->name = 'pushMessage'; +// 设置进程数,gateway进程数建议与cpu核数相同 +$gateway->count = 1; +// 分布式部署时请设置成内网ip(非127.0.0.1) +$gateway->lanIp = '127.0.0.1'; +// 内部通讯起始端口。假如$gateway->count=4,起始端口为2300 +// 则一般会使用2300 2301 2302 2303 4个端口作为内部通讯端口 +$gateway->startPort = 2300; +// 心跳间隔 +$gateway->pingInterval = 20; +// 心跳数据 +$gateway->pingData = '{"type":"ping"}'; +// 服务注册地址 +$gateway->registerAddress = '127.0.0.1:1236'; + +// 如果不是在根目录启动,则运行runAll方法 +if(!defined('GLOBAL_START')) +{ + Worker::runAll(); +} + diff --git a/app/worker/start_register.php b/app/worker/start_register.php new file mode 100644 index 0000000..9efeb8e --- /dev/null +++ b/app/worker/start_register.php @@ -0,0 +1,25 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +use \Workerman\Worker; +use \GatewayWorker\Register; +require_once __DIR__ . '/../../vendor/autoload.php'; +// register 服务必须是text协议 +$register = new Register('text://0.0.0.0:1236'); + +// 如果不是在根目录启动,则运行runAll方法 +if(!defined('GLOBAL_START')) +{ + Worker::runAll(); +} +