FastMovieAI AI短剧创作平台 二开基线

- 源码: xhadmincn/FastMovieAI (Apache 2.0)
- 二开: Docker部署, Swoole改Select, route.php修复, 补update/VERSION
- vendor/示例资源已gitignore
This commit is contained in:
李建琦
2026-08-26 18:37:36 +08:00
commit eae151fd57
888 changed files with 105571 additions and 0 deletions
@@ -0,0 +1,86 @@
<?php
namespace app\expose\utils;
use ArrayAccess;
use JsonSerializable;
/**
* 数据序列化模型
*
* 继承DataModel必须实现属性$data
* @package app\utils
* @property mixed $data
* @method mixed __get(string $name)
* @method void __set(string $name, mixed $value)
* @method bool __isset(string $name)
* @method void __unset(string $name)
* @method string __toString()
* @method mixed offsetGet(mixed $offset)
* @method void offsetSet(mixed $offset, mixed $value)
* @method bool offsetExists(mixed $offset)
* @method void offsetUnset(mixed $offset)
* @method array toArray()
* @method string toJson()
* @method array jsonSerialize()
*/
class DataModel implements ArrayAccess, JsonSerializable
{
protected $data = [];
public function __construct($data=[])
{
$this->data=$data;
}
public function __get($name)
{
return $this->data[$name] ?? null;
}
public function __set($name, $value)
{
$this->data[$name] = $value;
}
public function __isset($name)
{
return isset($this->data[$name]);
}
public function __unset($name)
{
unset($this->data[$name]);
}
public function __toString()
{
return json_encode($this->data, JSON_UNESCAPED_UNICODE);
}
public function offsetGet(mixed $offset):mixed
{
return $this->data[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
if (is_null($offset)) {
$this->data[] = $value;
} else {
$this->data[$offset] = $value;
}
}
public function offsetExists(mixed $offset): bool
{
return $this->__isset($offset);
}
public function offsetUnset(mixed $offset): void
{
$this->__unset($offset);
}
public function toArray()
{
return $this->data;
}
public function toJson()
{
return json_encode($this->data, JSON_UNESCAPED_UNICODE);
}
public function jsonSerialize(): array
{
return $this->data;
}
}
@@ -0,0 +1,27 @@
<?php
namespace app\expose\utils;
class Email
{
public $toemail;
protected $template;
protected $data;
public function setTemplate($template)
{
$this->template = new $template;
return $this;
}
public function setData($data)
{
$this->data = $data;
return $this;
}
public function send()
{
$content = $this->template->builder($this->toemail, $this->data);
p($content);
return true;
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace app\expose\utils;
use app\expose\enum\ResponseCode;
use support\Response;
/**
* JSON响应工具
* @deprecated 请使用app\expose\trait\Json替代
*/
trait Json
{
/**
* 返回成功JSON
*
* @param string $msg 消息
* @param mixed $data 数据
* @return Response
*/
protected static function success($msg = 'success', $data = [])
{
return self::json(['code' => ResponseCode::SUCCESS, 'msg' => $msg, 'data' => $data]);
}
/**
* 返回失败JSON
*
* @param string $msg 消息
* @param mixed $data 数据
* @return Response
*/
protected static function fail(string $msg = 'fail', $data = [])
{
return self::json(['code' => ResponseCode::FAIL, 'msg' => $msg, 'data' => $data]);
}
/**
* 返回数据JSON
*
* @param mixed $data 数据
* @return Response
*/
protected static function resData($data)
{
return self::json(['code' => ResponseCode::SUCCESS, 'msg' => 'success', 'data' => $data]);
}
protected static function event($event, $msg = 'success')
{
return self::code(ResponseCode::SUCCESS_EVENT_PUSH, $msg, [
'event' => $event
]);
}
/**
* 返回自定义JSON
*
* @param int $code 状态码
* @param string $msg 消息
* @param mixed $data 数据
* @return Response
*/
protected static function code($code, $msg = null, $data = [])
{
return self::json(['code' => $code, 'msg' => $msg, 'data' => $data]);
}
/**
* 返回异常消息
*
* @param \Throwable $th 捕获的异常
* @return Response
*/
protected static function exception($th)
{
$data = [];
if (config('app.debug')) {
$data = [
'file' => $th->getFile(),
'line' => $th->getLine(),
'trace' => $th->getTrace(),
];
}
return self::json(['code' => $th->getCode() ? $th->getCode() : ResponseCode::FAIL, 'msg' => $th->getMessage(), 'data' => $data]);
}
/**
* 响应失败
*
* @param \Throwable $th 捕获的异常
* @return Response
*/
protected static function server($th, $http_code = 500)
{
return self::json(['code' => $th->getCode() ? $th->getCode() : ResponseCode::FAIL, 'msg' => $th->getMessage(), 'data' => ['file' => $th->getFile(), 'line' => $th->getLine()]], JSON_UNESCAPED_UNICODE, $http_code);
}
/**
* 返回JSON
*
* @param mixed $data JSON数据
* @param int|null $options JSON编码
* @param int $http_code 服务器响应代码
* @return Response
*/
protected static function json($data, $options = JSON_UNESCAPED_UNICODE, $http_code = 200)
{
$request=request();
if($request&&$request->lang){
$data['msg']=trans($data['msg'],[], null, $request->lang);
}
return new Response($http_code, ['Content-Type' => 'application/json'], json_encode($data, $options));
}
}
@@ -0,0 +1,30 @@
<?php
namespace app\expose\utils;
class Password
{
/**
* 获取密码散列值
*
* @param string $password
* @param [type] $algo
* @return string
*/
public static function encrypt(string $password, string $algo = PASSWORD_DEFAULT): string
{
return password_hash($password, $algo);
}
/**
* 验证密码
*
* @param string $password
* @param string $hash
* @return boolean
*/
public static function verify(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
namespace app\expose\utils;
use \Exception;
/**
* RAS加解密
*/
class Rsa
{
/**
* 解密
*
* @param string 加密后的字符串
* @param string 私钥,可以是文件路径
* @return object
*/
public static function decrypt($token, $rsa_privatekey)
{
if (file_exists($rsa_privatekey)) {
$rsa_privatekey = @file_get_contents($rsa_privatekey);
}
if (empty($rsa_privatekey)) {
throw new Exception('私钥为空');
}
$split = str_split($token, 172); // 1024 bit 固定172
$crypto = '';
foreach ($split as $chunk) {
$isOkay = openssl_private_decrypt(base64_decode($chunk), $decryptData, $rsa_privatekey); // base64在这里使用,因为172字节是一组,是encode来的
if (!$isOkay) {
throw new Exception("解密失败");
}
$crypto .= $decryptData;
}
if (!$crypto) {
throw new Exception("解密失败");
}
return json_decode(base64_decode($crypto));
}
/**
* 加密
*
* @param mixed 可被json序列化的数据
* @param string 公钥,可以是文件路径
* @return string
*/
public static function encrypt(mixed $data, string $rsa_publickey)
{
if (file_exists($rsa_publickey)) {
$rsa_publickey = @file_get_contents($rsa_publickey);
}
if (empty($rsa_publickey)) {
throw new Exception('公钥为空');
}
$data = base64_encode(json_encode($data));
$split = str_split($data, 117); // 1024 bit && OPENSSL_PKCS1_PADDING 不大于117即可
$crypto = '';
foreach ($split as $chunk) {
$isOkay = openssl_public_encrypt($chunk, $encryptData, $rsa_publickey);
if (!$isOkay) {
throw new Exception("加密失败");
}
$crypto .= base64_encode($encryptData);
}
return $crypto;
}
/**
* 创建一对公钥私钥
*
* @return array
*/
public static function createKey()
{
$res = openssl_pkey_new([
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA
]);
openssl_pkey_export($res, $privKey);
$pubKey = openssl_pkey_get_details($res);
return [
'privatekey' => $privKey,
'publickey' => $pubKey["key"]
];
}
# 以下是数字加密解密
const PASSWORD = 'MAW512P89WIAUW9G7OHQWVGAPX51WPSN';
/**
* 数字加密函数
*
* @param int $number
* @param string $password
* @return string
*/
public static function encryptNumber(int $number, string $password = '')
{
// 生成随机的初始向量
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
$password = $password ?: self::PASSWORD;
// 加密数字
$encrypted = openssl_encrypt($number, 'aes-256-cbc', $password, 0, $iv);
// 将初始向量和加密后的数据进行编码并拼接起来
$encoded = base64_encode($iv . $encrypted);
return $encoded;
}
/**
* 数字解密函数
*
* @param string $encrypted
* @param string $password
* @return int
*/
public static function decryptNumber(string $encrypted, string $password = '')
{
// 解码加密后的数据
$decoded = base64_decode($encrypted);
// 提取初始向量和加密后的数据
$ivLength = openssl_cipher_iv_length('aes-256-cbc');
$iv = substr($decoded, 0, $ivLength);
$encryptedData = substr($decoded, $ivLength);
$password = $password ?: self::PASSWORD;
// 解密数据
$decrypted = openssl_decrypt($encryptedData, 'aes-256-cbc', $password, 0, $iv);
return $decrypted;
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace app\expose\utils;
use app\expose\enum\SmsChannels;
use Exception;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Dysmsapi;
use Darabonba\OpenApi\Models\Config;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Models\SendSmsRequest;
use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
class Sms
{
public $mobile;
protected $data;
protected $template;
public function setTemplate($template)
{
# 判断$template是否已经new过
if (is_object($template)) {
$this->template = $template;
return $this;
}
$this->template = new $template;
return $this;
}
public function setData($data)
{
$this->data = $data;
return $this;
}
public function send()
{
$this->template->builder($this->mobile, $this->data);
switch ($this->template->channel) {
case SmsChannels::ALIYUN['value']:
$this->sendAliyun();
break;
case SmsChannels::TENCENT['value']:
$this->sendTencent();
break;
case SmsChannels::SMSBAO['value']:
$this->sendSmsbao();
break;
default:
throw new Exception("未知的短信服务商");
}
}
public function sendAliyun()
{
if (empty($this->template->config)) {
throw new Exception("阿里云短信服务商配置不完整");
}
$config = new Config([
"accessKeyId" => $this->template->config['access_key_id'],
"accessKeySecret" => $this->template->config['access_secret'],
]);
$config->endpoint = "dysmsapi.aliyuncs.com";
$client = new Dysmsapi($config);
$request = new SendSmsRequest([
"phoneNumbers" => $this->mobile,
"templateCode" => $this->template->template_code,
"templateParam" => json_encode($this->data, JSON_UNESCAPED_UNICODE),
"signName" => $this->template->config['sign_name']
]);
$runtime = new RuntimeOptions();
$runtime->maxIdleConns = 3;
$runtime->connectTimeout = 10000;
$runtime->readTimeout = 10000;
// 复制代码运行请自行打印 API 的返回值
$res = $client->sendSms($request, $runtime);
if ($res->body->code == 'OK') {
return true;
}
if ($res->body->message) {
throw new Exception($res->body->message);
}
throw new Exception("发送失败");
}
public function sendTencent()
{
throw new Exception("error");
}
public function sendSmsbao()
{
throw new Exception("error");
}
}
+262
View File
@@ -0,0 +1,262 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
namespace app\expose\utils;
class Str
{
protected static $snakeCache = [];
protected static $camelCache = [];
protected static $studlyCache = [];
/**
* 检查字符串中是否包含某些字符串
* @param string $haystack
* @param string|array $needles
* @return bool
*/
public static function contains(string $haystack, $needles): bool
{
foreach ((array) $needles as $needle) {
if ('' != $needle && mb_strpos($haystack, $needle) !== false) {
return true;
}
}
return false;
}
/**
* 检查字符串是否以某些字符串结尾
*
* @param string $haystack
* @param string|array $needles
* @return bool
*/
public static function endsWith(string $haystack, $needles): bool
{
foreach ((array) $needles as $needle) {
if ((string) $needle === static::substr($haystack, -static::length($needle))) {
return true;
}
}
return false;
}
/**
* 检查字符串是否以某些字符串开头
*
* @param string $haystack
* @param string|array $needles
* @return bool
*/
public static function startsWith(string $haystack, $needles): bool
{
foreach ((array) $needles as $needle) {
if ('' != $needle && mb_strpos($haystack, $needle) === 0) {
return true;
}
}
return false;
}
/**
* 获取指定长度的随机字母数字组合的字符串
*
* @param int $length
* @param int $type
* @param string $addChars
* @return string
*/
public static function random(int $length = 6, int $type = null, string $addChars = ''): string
{
$str = '';
switch ($type) {
case 0:
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' . $addChars;
break;
case 1:
$chars = str_repeat('0123456789', 3);
break;
case 2:
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' . $addChars;
break;
case 3:
$chars = 'abcdefghijklmnopqrstuvwxyz' . $addChars;
break;
case 4:
$chars = "们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书" . $addChars;
break;
default:
$chars = 'ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789' . $addChars;
break;
}
if ($length > 10) {
$chars = $type == 1 ? str_repeat($chars, $length) : str_repeat($chars, 5);
}
if ($type != 4) {
$chars = str_shuffle($chars);
$str = substr($chars, 0, $length);
} else {
for ($i = 0; $i < $length; $i++) {
$str .= mb_substr($chars, floor(mt_rand(0, mb_strlen($chars, 'utf-8') - 1)), 1);
}
}
return $str;
}
/**
* 字符串转小写
*
* @param string $value
* @return string
*/
public static function lower(string $value): string
{
return mb_strtolower($value, 'UTF-8');
}
/**
* 字符串转大写
*
* @param string $value
* @return string
*/
public static function upper(string $value): string
{
return mb_strtoupper($value, 'UTF-8');
}
/**
* 获取字符串的长度
*
* @param string $value
* @return int
*/
public static function length(string $value): int
{
return mb_strlen($value);
}
/**
* 截取字符串
*
* @param string $string
* @param int $start
* @param int|null $length
* @return string
*/
public static function substr(string $string, int $start, int $length = null): string
{
return mb_substr($string, $start, $length, 'UTF-8');
}
/**
* 驼峰转下划线
*
* @param string $value
* @param string $delimiter
* @return string
*/
public static function snake(string $value, string $delimiter = '_'): string
{
$key = $value;
if (isset(static::$snakeCache[$key][$delimiter])) {
return static::$snakeCache[$key][$delimiter];
}
if (!ctype_lower($value)) {
$value = preg_replace('/\s+/u', '', ucwords($value));
$value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value));
}
return static::$snakeCache[$key][$delimiter] = $value;
}
/**
* 下划线转驼峰(首字母小写)
*
* @param string $value
* @return string
*/
public static function camel(string $value): string
{
if (isset(static::$camelCache[$value])) {
return static::$camelCache[$value];
}
return static::$camelCache[$value] = lcfirst(static::studly($value));
}
/**
* 下划线转驼峰(首字母大写)
*
* @param string $value
* @return string
*/
public static function studly(string $value): string
{
$key = $value;
if (isset(static::$studlyCache[$key])) {
return static::$studlyCache[$key];
}
$value = ucwords(str_replace(['-', '_'], ' ', $value));
return static::$studlyCache[$key] = str_replace(' ', '', $value);
}
/**
* 转为首字母大写的标题格式
*
* @param string $value
* @return string
*/
public static function title(string $value): string
{
return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
}
/**
* 手机号,邮箱,脱敏
* @param string|null $value
* @return string|null
*/
public static function mask(?string $value): ?string
{
if (empty($value)) {
return null;
}
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
$arr = explode('@', $value);
$name = $arr[0];
$domain = $arr[1];
// 如果长度小于等于4,就直接显示
if (strlen($name) <= 4) {
$masked = $name;
} else {
$masked = substr($name, 0, 2) . '****' . substr($name, -2);
}
return $masked . '@' . $domain;
} else {
return substr($value, 0, 3) . '****' . substr($value, -4);
}
}
}
@@ -0,0 +1,36 @@
<?php
namespace app\expose\utils;
use app\expose\utils\DataModel;
class TreeModel extends DataModel
{
private function _organizeRecords($regions,$idKey)
{
$organizedRegions = [];
foreach ($regions as $region) {
$organizedRegions[$region[$idKey]] = $region;
$organizedRegions[$region[$idKey]]['children'] = [];
}
return $organizedRegions;
}
private function _buildTree($organized,$pidKey)
{
$tree = [];
foreach ($organized as $id => $record) {
if ($record[$pidKey]) {
$organized[$record[$pidKey]]['children'][] = &$organized[$id];
} else {
$tree[] = &$organized[$id];
}
}
return $tree;
}
public function toTree($idKey='id',$pidKey='pid')
{
$organized=$this->_organizeRecords($this->data,$idKey);
$data=$this->_buildTree($organized,$pidKey);
return $data;
}
}
@@ -0,0 +1,251 @@
<?php
namespace app\expose\utils;
use Exception;
/**
* PHP Class for handling Google Authenticator 2-factor authentication.
*
* @author Michael Kliewe
* @copyright 2012 Michael Kliewe
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*
* @link http://www.phpgangsta.de/
*/
class TwofaAuthenticator
{
protected $_codeLength = 6;
/**
* Create new secret.
* 16 characters, randomly chosen from the allowed base32 characters.
*
* @param int $secretLength
*
* @return string
*/
public function createSecret($secretLength = 16)
{
$validChars = $this->_getBase32LookupTable();
// Valid secret lengths are 80 to 640 bits
if ($secretLength < 16 || $secretLength > 128) {
throw new Exception('Bad secret length');
}
$secret = '';
$rnd = false;
if (function_exists('random_bytes')) {
$rnd = random_bytes($secretLength);
} elseif (function_exists('mcrypt_create_iv')) {
$rnd = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong);
if (!$cryptoStrong) {
$rnd = false;
}
}
if ($rnd !== false) {
for ($i = 0; $i < $secretLength; ++$i) {
$secret .= $validChars[ord($rnd[$i]) & 31];
}
} else {
throw new Exception('No source of secure random');
}
return $secret;
}
/**
* Calculate the code, with given secret and point in time.
*
* @param string $secret
* @param int|null $timeSlice
*
* @return string
*/
public function getCode($secret, $timeSlice = null)
{
if ($timeSlice === null) {
$timeSlice = floor(time() / 30);
}
$secretkey = $this->_base32Decode($secret);
// Pack time into binary string
$time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
// Hash it with users secret key
$hm = hash_hmac('SHA1', $time, $secretkey, true);
// Use last nipple of result as index/offset
$offset = ord(substr($hm, -1)) & 0x0F;
// grab 4 bytes of the result
$hashpart = substr($hm, $offset, 4);
// Unpak binary value
$value = unpack('N', $hashpart);
$value = $value[1];
// Only 32 bits
$value = $value & 0x7FFFFFFF;
$modulo = pow(10, $this->_codeLength);
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
}
/**
* Get QR-Code URL for image, from google charts.
*
* @param string $name
* @param string $secret
* @param string $title
* @param array $params
*
* @return string
*/
public function getQRCode($name, $secret, $title = null)
{
$urlencoded = 'otpauth://totp/'.$name.'?secret='.$secret;
if (isset($title)) {
$urlencoded .= '&issuer='.urlencode($title);
}
return $urlencoded;
}
/**
* Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now.
*
* @param string $secret
* @param string $code
* @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
* @param int|null $currentTimeSlice time slice if we want use other that time()
*
* @return bool
*/
public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
{
if ($currentTimeSlice === null) {
$currentTimeSlice = floor(time() / 30);
}
if (strlen($code) != 6) {
return false;
}
for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
if ($this->timingSafeEquals($calculatedCode, $code)) {
return true;
}
}
return false;
}
/**
* Set the code length, should be >=6.
*
* @param int $length
*
* @return PHPGangsta_GoogleAuthenticator
*/
public function setCodeLength($length)
{
$this->_codeLength = $length;
return $this;
}
/**
* Helper class to decode base32.
*
* @param $secret
*
* @return bool|string
*/
protected function _base32Decode($secret)
{
if (empty($secret)) {
return '';
}
$base32chars = $this->_getBase32LookupTable();
$base32charsFlipped = array_flip($base32chars);
$paddingCharCount = substr_count($secret, $base32chars[32]);
$allowedValues = array(6, 4, 3, 1, 0);
if (!in_array($paddingCharCount, $allowedValues)) {
return false;
}
for ($i = 0; $i < 4; ++$i) {
if ($paddingCharCount == $allowedValues[$i] &&
substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) {
return false;
}
}
$secret = str_replace('=', '', $secret);
$secret = str_split($secret);
$binaryString = '';
for ($i = 0; $i < count($secret); $i = $i + 8) {
$x = '';
if (!in_array($secret[$i], $base32chars)) {
return false;
}
for ($j = 0; $j < 8; ++$j) {
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
}
$eightBits = str_split($x, 8);
for ($z = 0; $z < count($eightBits); ++$z) {
$binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
}
}
return $binaryString;
}
/**
* Get array with all 32 characters for decoding from/encoding to base32.
*
* @return array
*/
protected function _getBase32LookupTable()
{
return array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=', // padding char
);
}
/**
* A timing safe equals comparison
* more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html.
*
* @param string $safeString The internal (safe) value to be checked
* @param string $userString The user submitted (unsafe) value
*
* @return bool True if the two strings are identical
*/
private function timingSafeEquals($safeString, $userString)
{
if (function_exists('hash_equals')) {
return hash_equals($safeString, $userString);
}
$safeLen = strlen($safeString);
$userLen = strlen($userString);
if ($userLen != $safeLen) {
return false;
}
$result = 0;
for ($i = 0; $i < $userLen; ++$i) {
$result |= (ord($safeString[$i]) ^ ord($userString[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}
}
@@ -0,0 +1,57 @@
<?php
namespace app\expose\utils\wechat;
use app\expose\utils\wechat\modules\Event;
use app\expose\utils\wechat\modules\Reply;
use support\Log;
class OfficialAccount
{
use Reply;
use Event;
public function handle($request, $config)
{
$data = $this->getData($request, $config);
if ($data['MsgType'] === 'event') {
return $this->handleEvent($data);
} else {
return $this->replyText($data, 'hello world');
}
}
public function checkSignature($request, $config)
{
$signature = $request->get("signature");
$timestamp = $request->get("timestamp");
$nonce = $request->get("nonce");
$token = $config['token'];
$tmpArr = [$token, $timestamp, $nonce];
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
if ($tmpStr == $signature) {
return true;
}
throw new \Exception('signature error');
}
private function getData($request, $config)
{
$xml = $request->rawBody();
$json = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
$data = json_decode(json_encode($json), true);
if ($request->get('encrypt_type') === 'aes') {
$Encrypt = base64_decode($data['Encrypt']);
$aes_key = $config['aes_key'];
$aes_key = base64_decode($aes_key . '=');
$iv = substr($aes_key, 0, 16);
$xml = openssl_decrypt($Encrypt, 'aes-256-cbc', $aes_key, OPENSSL_RAW_DATA, $iv);
$filterHeader = substr($xml, 20);
$xml = preg_replace('/' . $config['app_id'] . '/', '', $filterHeader);
$json = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
$data = json_decode(json_encode($json), true);
return $data;
} else {
return $data;
}
}
}
@@ -0,0 +1,73 @@
<?php
namespace app\expose\utils\wechat\modules;
use app\expose\enum\EventName;
use support\Redis;
use Webman\Event\Event as EventEvent;
trait Event
{
private function handleEvent($data)
{
switch ($data['Event']) {
// case 'subscribe':
// EventEvent::emit(EventName::WECHAT_OFFICIAL_ACCOUNT_SUBSCRLBE['value'],$data);
// return $this->replyText($data,'欢迎关注');
// case 'unsubscribe':
// EventEvent::emit(EventName::WECHAT_OFFICIAL_ACCOUNT_UNSUBSCRLBE['value'],$data);
// return $this->replyText($data,'路远,再会。');
case 'subscribe':
case 'unsubscribe':
case 'SCAN':
$scene = str_replace('qrscene_', '', $data['EventKey']);
$Scene = Redis::get($scene);
if ($Scene) {
$Scene = json_decode($Scene, true);
if (count($Scene) < 2) {
return $this->replyText($data, '二维码参数错误');
}
$class = $Scene[0];
$method = $Scene[1];
if (class_exists($class)) {
$class = new $class();
$class->FromUserName = $data['FromUserName'];
$class->ToUserName = $data['ToUserName'];
$class->MsgType = $data['MsgType'];
$class->Event = $data['Event'];
$class->EventKey = $scene;
$class->data = $data;
if (method_exists($class, $method)) {
$params = null;
if (isset($Scene[2])) {
$params = $Scene[2];
}
return $this->replyText($data, $class->$method($params));
}
}
return $this->replyText($data, '扫码成功,但未找到对应处理类或方法');
}
return $this->replyText($data, '二维码已过期');
case 'LOCATION':
return $this->replyText($data, '上报地理位置');
case 'CLICK':
return $this->replyText($data, '点击菜单');
case 'VIEW':
return $this->replyText($data, '点击菜单跳转链接');
case 'scancode_push':
return $this->replyText($data, '扫码推事件');
case 'scancode_waitmsg':
return $this->replyText($data, '扫码推事件且弹出“消息接收中”提示框');
case 'pic_sysphoto':
return $this->replyText($data, '弹出系统拍照发图');
case 'pic_photo_or_album':
return $this->replyText($data, '弹出拍照或者相册发图');
case 'pic_weixin':
return $this->replyText($data, '弹出微信相册发图器');
case 'location_select':
return $this->replyText($data, '弹出地理位置选择器');
case 'view_miniprogram':
return $this->replyText($data, '点击菜单跳转小程序');
}
}
}
@@ -0,0 +1,94 @@
<?php
namespace app\expose\utils\wechat\modules;
trait Reply
{
private function replyText($data,$content)
{
$result=[
'ToUserName' => "<![CDATA[{$data['FromUserName']}]]>",
'FromUserName' => "<![CDATA[{$data['ToUserName']}]]>",
'CreateTime' => time(),
'MsgType' => '<![CDATA[text]]>',
'Content' => "<![CDATA[{$content}]]>"
];
return xml(arrayToXml($result));
}
private function replyImage($data,$media_id)
{
$result=[
'ToUserName' => "<![CDATA[{$data['FromUserName']}]]>",
'FromUserName' => "<![CDATA[{$data['ToUserName']}]]>",
'CreateTime' => time(),
'MsgType' => '<![CDATA[image]]>',
'Image' => [
'MediaId' => "<![CDATA[{$media_id}]]>"
]
];
return xml(arrayToXml($result));
}
private function replyVoice($data,$media_id)
{
$result=[
'ToUserName' => "<![CDATA[{$data['FromUserName']}]]>",
'FromUserName' => "<![CDATA[{$data['ToUserName']}]]>",
'CreateTime' => time(),
'MsgType' => '<![CDATA[voice]]>',
'Voice' => [
'MediaId' => "<![CDATA[{$media_id}]]>"
]
];
return xml(arrayToXml($result));
}
private function replyVideo($data,$media_id,$title='',$description='')
{
$result=[
'ToUserName' => "<![CDATA[{$data['FromUserName']}]]>",
'FromUserName' => "<![CDATA[{$data['ToUserName']}]]>",
'CreateTime' => time(),
'MsgType' => '<![CDATA[video]]>',
'Video' => [
'MediaId' => "<![CDATA[{$media_id}]]>",
'Title' => "<![CDATA[{$title}]]>",
'Description' => "<![CDATA[{$description}]]>"
]
];
return xml(arrayToXml($result));
}
private function replyMusic($data,$thumb_media_id,$title='',$description='',$music_url='',$hq_music_url='')
{
$result=[
'ToUserName' => "<![CDATA[{$data['FromUserName']}]]>",
'FromUserName' => "<![CDATA[{$data['ToUserName']}]]>",
'CreateTime' => time(),
'MsgType' => '<![CDATA[music]]>',
'Music' => [
'Title' => "<![CDATA[{$title}]]>",
'Description' => "<![CDATA[{$description}]]>",
'MusicUrl' => "<![CDATA[{$music_url}]]>",
'HQMusicUrl' => "<![CDATA[{$hq_music_url}]]>",
'ThumbMediaId' => "<![CDATA[{$thumb_media_id}]]>"
]
];
return xml(arrayToXml($result));
}
private function replyNews($data,$articles)
{
$result=[
'ToUserName' => "<![CDATA[{$data['FromUserName']}]]>",
'FromUserName' => "<![CDATA[{$data['ToUserName']}]]>",
'CreateTime' => time(),
'MsgType' => '<![CDATA[news]]>',
'ArticleCount' => count($articles),
'Articles' => []
];
foreach ($articles as $article) {
$result['Articles'][]=[
'Title' => "<![CDATA[{$article['title']}]]>",
'Description' => "<![CDATA[{$article['description']}]]>",
'PicUrl' => "<![CDATA[{$article['picurl']}]]>",
'Url' => "<![CDATA[{$article['url']}]]>"
];
}
return xml(arrayToXml($result));
}
}