diff --git a/app/czg/app/controller/OrderController.php b/app/czg/app/controller/OrderController.php new file mode 100644 index 0000000..8f0bc3c --- /dev/null +++ b/app/czg/app/controller/OrderController.php @@ -0,0 +1,244 @@ +getByCode(938); + if ($val && $val['value'] == '1') { + $openId = $this->auth->getUserInfo()['wx_open_id']; + if (!$openId) { + throw new SysException("请先绑定微信", 407); + } + } + } + + public function insertCourseOrders() + { + + $params = request()->get(); + + $userInfo = $this->auth->getUser(); + debounce('insertCourseOrders'.$userInfo['user_id']); + Log::info('生成商品订单信息接口入参为: {}', $params); + + self::checkWxBind(); + + if (!RedisUtils::checkCanCreateOrder($userInfo['user_id'])) { + throw new SysException(ErrEnums::MANY_REQUEST); + } + + $val = (new CommonInfo())->getByCodeToInt(934); + RedisUtils::setUserCanCreateOrder($userInfo['user_id'], $val); + + TbUser::checkEnable($userInfo); + + + // 查询用户是否购买整部短剧 + $courseUserCount = DatabaseRoute::getDb('course_user', $userInfo['user_id'])->where([ + 'course_id' => $params['courseId'], + 'user_id' => $userInfo['user_id'], + 'classify' => 1 + ])->count(); + if ($courseUserCount > 0 ) { + $this->success('成功', [ + 'status' => 1 + ]); + } + + if (!empty($params['courseDetailsId'])) { + // 查询短剧 + $courseDetail = DatabaseRoute::getDb('course_details', [ + 'course_id' => $params['courseId'], + ])->where([ + 'course_id' => $params['courseId'], + 'course_details_id' => $params['courseDetailsId'], + ])->find(); + if (!$courseDetail) { + throw new SysException("未知短剧"); + } + + if ($courseDetail['is_price'] == 2) { + $this->success('成功', [ + 'status' => 1 + ]); + } + + + // 查询是否已购买单集 + $courseUserCount2 = DatabaseRoute::getDb('course_user', $userInfo['user_id'])->where([ + 'course_id' => $params['courseId'], + 'user_id' => $userInfo['user_id'], + 'course_details_id' => $params['courseDetailsId'], + 'classify' => 2 + ])->count(); + + if ($courseUserCount2 > 0 ) { + $this->success('成功', [ + 'status' => 1 + ]); + } + } + + + + //查询短剧信息 + $course = Db::name('course')->where([ + 'course_id' => $params['courseId'] + ])->find(); + if (!$course) { + throw new SysException("短剧不存在"); + } + + // 订单编号 + $ordersNo = uuid(); + + // 获取金币与金额比例 + $commonInfo = (new CommonInfo())->getByCode(914); + $ratio = $commonInfo ? floatval($commonInfo['value']) : 1; + + if (!empty($params['courseDetailsId'])) { + $courseDetail = DatabaseRoute::getDb('course_details', [ + 'course_id' => $params['courseId'] + ])->where('course_details_id', $params['courseDetailsId']) + ->find(); + + if (!$courseDetail) { + throw new SysException('短剧详情不存在'); + } + + $payMoney = floatval($courseDetail['price']); + $payDiamond = bcmul($payMoney, $ratio); + } else { + if (!isset($course['price']) || floatval($course['price']) <= 0) { + throw new SysException("该剧暂不支持整剧购买方式!"); + } + + $payMoney = floatval($course['price']); + $payDiamond = bcmul($payMoney, $ratio); + } + + // 订单数据组装 + $orders = [ + 'orders_id' => Random::generateRandomPrefixedId(18), + 'orders_no' => $ordersNo, + 'user_id' => $userInfo['user_id'], + 'course_id' => $params['courseId'], + 'course_details_id' => $params['courseDetailsId'] ?? null, + 'pay_money' => $payMoney, + 'pay_diamond' => $payDiamond, + 'status' => 0, + 'create_time' => date('Y-m-d H:i:s'), + 'orders_type' => 1, + ]; + + Orders::fillSysUserId($orders, $userInfo); + // 插入订单 + $count = DatabaseRoute::getDb('orders', $userInfo['user_id'], true)->insert($orders); + + $result = [ + 'flag' => 2, + 'orders' => convertToCamelCase($orders), + ]; + + if ($count) { + $this->successWithData($result); + } else { + $this->error(); + } + + } + + + public function payOrders() + { + $orderId = $this->request->post('orderId'); + $userInfo = $this->auth->getUser(); +// $userId = $this->getUserId(); + $order = DatabaseRoute::getDb('orders', $userInfo['user_id'])->where([ + 'orders_id' => $orderId + ])->find(); + if (!$order || $order['status'] != 0) { + $this->error('订单不存在或已支付'); + } + + if ($order['orders_type'] == 1) { + if (!empty($order['course_details_id'])) { + $count = DatabaseRoute::getDb('course_user', $userInfo['user_id'])->where([ + 'classify' => 2, + 'course_id' => $order['course_id'], + 'course_details_id' => $order['course_details_id'] + ])->count(); + }else if (!empty($order['course_details_ids'])) { + $courseDetailsIds = json_decode($order['course_details_ids'], true); + $count = DatabaseRoute::getDb('course_user', $userInfo['user_id'])->where([ + 'course_id' => $order['course_id'], + ['course_details_id', 'in', $courseDetailsIds] + ])->count(); + }else { + $count = DatabaseRoute::getDb('course_user', $userInfo['user_id'])->where([ + 'classify' => 1, + 'course_id' => $order['course_id'] + ])->count(); + } + if ($count) { + $this->errorMsg('您已购买,请不要重复点击'); + } + } + + $userMoney = UserMoney::selectUserMoney($userInfo['user_id'])['data']; + if (bccomp($userMoney['money'], $order['pay_diamond']) < 0) { + $this->errorMsg('账户不足,请充值'); + } + + UserMoney::updateMoney($userInfo['user_id'], $order['pay_diamond'], false); + + DatabaseRoute::getDb('user_money_details', $userInfo['user_id'], true)->insert([ + 'money' => $order['pay_diamond'], + 'user_id' => $userInfo['user_id'], + 'content' => '解锁成功', + 'title' => '金币解锁视频', + 'type' => 2, + 'classify' => 3, + 'create_time' => date('Y-m-d H:i:s'), + 'money_type' => 2 + ]); + + $order['pay_way'] = 6; + $order['diamond'] = 1; + $order['status'] = 1; + $order['pay_time'] = getNormalDate(); + Orders::fillSysUserId($order, $userInfo); + + DatabaseRoute::getDb('orders', $userInfo['user_id'], true, true)->where([ + 'orders_id' => $order['orders_id'] + ])->update($order); + + Orders::insertOrders($order); + + $this->successWithData(1); + + + } + + +} \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index 5c9c58d..c04270e 100644 --- a/app/functions.php +++ b/app/functions.php @@ -1,4 +1,1378 @@ removeEvilAttributes(['style']); + + // 检查到 xss 代码之后使用 cleanXss 替换它 + $antiXss->setReplacement('cleanXss'); + + return $antiXss->xss_clean($string); + } +} + +if (!function_exists('htmlspecialchars_decode_improve')) { + /** + * html解码增强 + * 被 filter函数 内的 htmlspecialchars 编码的字符串,需要用此函数才能完全解码 + * @param string $string + * @param int $flags + * @return string + */ + function htmlspecialchars_decode_improve(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401): string + { + return htmlspecialchars_decode($string, $flags); + } +} + +if (!function_exists('get_sys_config')) { + + /** + * 获取站点的系统配置,不传递参数则获取所有配置项 + * @param string $name 变量名 + * @param string $group 变量分组,传递此参数来获取某个分组的所有配置项 + * @param bool $concise 是否开启简洁模式,简洁模式下,获取多项配置时只返回配置的键值对 + * @return mixed + * @throws Throwable + */ + function get_sys_config(string $name = '', string $group = '', bool $concise = true): mixed + { + if ($name) { + // 直接使用->value('value')不能使用到模型的类型格式化 + $config = configModel::cache($name, null, configModel::$cacheTag)->where('name', $name)->find(); + if ($config) $config = $config['value']; + } else { + if ($group) { + $temp = configModel::cache('group' . $group, null, configModel::$cacheTag)->where('group', $group)->select()->toArray(); + } else { + $temp = configModel::cache('sys_config_all', null, configModel::$cacheTag)->order('weigh desc')->select()->toArray(); + } + if ($concise) { + $config = []; + foreach ($temp as $item) { + $config[$item['name']] = $item['value']; + } + } else { + $config = $temp; + } + } + return $config; + } +} + +if (!function_exists('get_route_remark')) { + + /** + * 获取当前路由后台菜单规则的备注信息 + * @return string + */ + function get_route_remark(): string + { + $controllerName = request()->controller(true); + $actionName = request()->action(true); + $path = str_replace('.', '/', $controllerName); + + $remark = Db::name('admin_rule') + ->where('name', $path) + ->whereOr('name', $path . '/' . $actionName) + ->value('remark'); + return __((string)$remark); + } +} + +if (!function_exists('full_url')) { + + /** + * 获取资源完整url地址;若安装了云存储或 config/buildadmin.php 配置了CdnUrl,则自动使用对应的CdnUrl + * @param string $relativeUrl 资源相对地址 不传入则获取域名 + * @param string|bool $domain 是否携带域名 或者直接传入域名 + * @param string $default 默认值 + * @return string + */ + function full_url(string $relativeUrl = '', string|bool $domain = true, string $default = ''): string + { + // 存储/上传资料配置 + Event::trigger('uploadConfigInit', App::getInstance()); + + $cdnUrl = Config::get('buildadmin.cdn_url'); + if (!$cdnUrl) { + $cdnUrl = request()->upload['cdn'] ?? '//' . request()->host(); + } + + if ($domain === true) { + $domain = $cdnUrl; + } elseif ($domain === false) { + $domain = ''; + } + + $relativeUrl = $relativeUrl ?: $default; + if (!$relativeUrl) return $domain; + + $regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i"; + if (preg_match('/^http(s)?:\/\//', $relativeUrl) || preg_match($regex, $relativeUrl) || $domain === false) { + return $relativeUrl; + } + + $url = $domain . $relativeUrl; + $cdnUrlParams = Config::get('buildadmin.cdn_url_params'); + if ($domain === $cdnUrl && $cdnUrlParams) { + $separator = str_contains($url, '?') ? '&' : '?'; + $url .= $separator . $cdnUrlParams; + } + + return $url; + } +} + +if (!function_exists('encrypt_password')) { + + /** + * 加密密码 + * @deprecated 使用 hash_password 代替 + */ + function encrypt_password($password, $salt = '', $encrypt = 'md5') + { + return $encrypt($encrypt($password) . $salt); + } +} + +if (!function_exists('hash_password')) { + + /** + * 创建密码散列(hash) + */ + function hash_password(string $password): string + { + return password_hash($password, PASSWORD_DEFAULT); + } +} + +if (!function_exists('verify_password')) { + + /** + * 验证密码是否和散列值匹配 + * @param string $password 密码 + * @param string $hash 散列值 + * @param array $extend 扩展数据 + */ + function verify_password(string $password, string $hash, array $extend = []): bool + { + // 第一个表达式直接检查是否为 password_hash 函数创建的 hash 的典型格式,即:$algo$cost$salt.hash + if (str_starts_with($hash, '$') || password_get_info($hash)['algoName'] != 'unknown') { + return password_verify($password, $hash); + } else { + // 兼容旧版 md5 加密的密码 + return encrypt_password($password, $extend['salt'] ?? '') === $hash; + } + } +} + +if (!function_exists('str_attr_to_array')) { + + /** + * 将字符串属性列表转为数组 + * @param string $attr 属性,一行一个,无需引号,比如:class=input-class + * @return array + */ + function str_attr_to_array(string $attr): array + { + if (!$attr) return []; + $attr = explode("\n", trim(str_replace("\r\n", "\n", $attr))); + $attrTemp = []; + foreach ($attr as $item) { + $item = explode('=', $item); + if (isset($item[0]) && isset($item[1])) { + $attrVal = $item[1]; + if ($item[1] === 'false' || $item[1] === 'true') { + $attrVal = !($item[1] === 'false'); + } elseif (is_numeric($item[1])) { + $attrVal = (float)$item[1]; + } + if (strpos($item[0], '.')) { + $attrKey = explode('.', $item[0]); + if (isset($attrKey[0]) && isset($attrKey[1])) { + $attrTemp[$attrKey[0]][$attrKey[1]] = $attrVal; + continue; + } + } + $attrTemp[$item[0]] = $attrVal; + } + } + return $attrTemp; + } +} + +if (!function_exists('action_in_arr')) { + + /** + * 检测一个方法是否在传递的数组内 + * @param array $arr + * @return bool + */ + function action_in_arr(array $arr = []): bool + { + $arr = is_array($arr) ? $arr : explode(',', $arr); + if (!$arr) { + return false; + } + $arr = array_map('strtolower', $arr); + if (in_array(strtolower(request()->action()), $arr) || in_array('*', $arr)) { + return true; + } + return false; + } +} + +if (!function_exists('build_suffix_svg')) { + + /** + * 构建文件后缀的svg图片 + * @param string $suffix 文件后缀 + * @param ?string $background 背景颜色,如:rgb(255,255,255) + * @return string + */ + function build_suffix_svg(string $suffix = 'file', string $background = null): string + { + $suffix = mb_substr(strtoupper($suffix), 0, 4); + $total = unpack('L', hash('adler32', $suffix, true))[1]; + $hue = $total % 360; + [$r, $g, $b] = hsv2rgb($hue / 360, 0.3, 0.9); + + $background = $background ?: "rgb($r,$g,$b)"; + + return ' + + + + + + ' . $suffix . ' + '; + } +} + +if (!function_exists('get_area')) { + + /** + * 获取省份地区数据 + * @throws Throwable + */ + function get_area(): array + { + $province = request()->get('province', ''); + $city = request()->get('city', ''); + $where = ['pid' => 0, 'level' => 1]; + if ($province !== '') { + $where['pid'] = $province; + $where['level'] = 2; + if ($city !== '') { + $where['pid'] = $city; + $where['level'] = 3; + } + } + return Db::name('area') + ->where($where) + ->field('id as value,name as label') + ->select() + ->toArray(); + } +} + +if (!function_exists('hsv2rgb')) { + function hsv2rgb($h, $s, $v): array + { + $r = $g = $b = 0; + + $i = floor($h * 6); + $f = $h * 6 - $i; + $p = $v * (1 - $s); + $q = $v * (1 - $f * $s); + $t = $v * (1 - (1 - $f) * $s); + + switch ($i % 6) { + case 0: + $r = $v; + $g = $t; + $b = $p; + break; + case 1: + $r = $q; + $g = $v; + $b = $p; + break; + case 2: + $r = $p; + $g = $v; + $b = $t; + break; + case 3: + $r = $p; + $g = $q; + $b = $v; + break; + case 4: + $r = $t; + $g = $p; + $b = $v; + break; + case 5: + $r = $v; + $g = $p; + $b = $q; + break; + } + + return [ + floor($r * 255), + floor($g * 255), + floor($b * 255) + ]; + } +} + +if (!function_exists('ip_check')) { + + /** + * IP检查 + * @throws Throwable + */ + function ip_check($ip = null): void + { + $ip = is_null($ip) ? request()->ip() : $ip; + $noAccess = get_sys_config('no_access_ip'); + $noAccess = !$noAccess ? [] : array_filter(explode("\n", str_replace("\r\n", "\n", $noAccess))); + if ($noAccess && IpUtils::checkIp($ip, $noAccess)) { + $response = Response::create(['msg' => 'No permission request'], 'json', 403); + throw new HttpResponseException($response); + } + } +} + +if (!function_exists('set_timezone')) { + + /** + * 设置时区 + * @throws Throwable + */ + function set_timezone($timezone = null): void + { +// $defaultTimezone = Config::get('app.default_timezone'); +// $timezone = is_null($timezone) ? get_sys_config('time_zone') : $timezone; +// if ($timezone && $defaultTimezone != $timezone) { +// Config::set([ +// 'app.default_timezone' => $timezone +// ]); +// date_default_timezone_set($timezone); +// } + date_default_timezone_set('Asia/Shanghai'); + } +} + +if (!function_exists('get_upload_config')) { + + /** + * 获取上传配置 + * @return array + */ + function get_upload_config(): array + { + // 存储/上传资料配置 + Event::trigger('uploadConfigInit', App::getInstance()); + + $uploadConfig = Config::get('upload'); + $uploadConfig['max_size'] = Filesystem::fileUnitToByte($uploadConfig['max_size']); + + $upload = request()->upload; + if (!$upload) { + $uploadConfig['mode'] = 'local'; + return $uploadConfig; + } + unset($upload['cdn']); + return array_merge($upload, $uploadConfig); + } +} + +if (!function_exists('get_auth_token')) { + + /** + * 获取鉴权 token + * @param array $names + * @return string + */ + function get_auth_token(array $names = ['ba', 'token']): string + { + $separators = [ + 'header' => ['', '-'], // batoken、ba-token【ba_token 不在 header 的接受列表内因为兼容性不高,改用 http_ba_token】 + 'param' => ['', '-', '_'], // batoken、ba-token、ba_token + 'server' => ['_'], // http_ba_token + ]; + + $tokens = []; + $request = request(); + foreach ($separators as $fun => $sps) { + foreach ($sps as $sp) { + $tokens[] = $request->$fun(($fun == 'server' ? 'http_' : '') . implode($sp, $names)); + } + } + $tokens = array_filter($tokens); + return array_values($tokens)[0] ?? ''; + } +} + +if (!function_exists('keys_to_camel_case')) { + + /** + * 将数组 key 的命名方式转换为小写驼峰 + * @param array $array 被转换的数组 + * @param array $keys 要转换的 key,默认所有 + * @return array + */ + function keys_to_camel_case(array $array, array $keys = []): array + { + $result = []; + foreach ($array as $key => $value) { + // 将键名转换为驼峰命名 + $camelCaseKey = $keys && in_array($key, $keys) ? parse_name($key, 1, false) : $key; + + if (is_array($value)) { + // 如果值是数组,递归转换 + $result[$camelCaseKey] = keys_to_camel_case($value); + } else { + $result[$camelCaseKey] = $value; + } + } + return $result; + } +} + +if (!function_exists('p')) { + + /** + * 将数组 key 的命名方式转换为小写驼峰 + * @param array $array 被转换的数组 + * @param array $keys 要转换的 key,默认所有 + * @return array + */ + function p(...$p) + { + if(count($p) > 1) { + foreach ($p as $k => $v) { + print_r($v); + print_r('---'); + } + }else { + print_r($p[0]); + } + die; + } +} + +if (!function_exists('returnErrorData')) { + + + function returnErrorData($msg) + { + return ['code' => -1, 'message' => $msg, 'data' => []]; + } +} + + +function getYesterdayFiles($dirPath) +{ + // 1. 获取昨天的日期前缀(格式:两位数日期,如 08) + $prefix = date('d', strtotime('-1 day')); + + // 2. 定义要读取的文件夹路径 + // 示例:读取 public 目录下的 files 文件夹 + // 若读取其他目录,可使用 app_path()、root_path() 等助手函数 + // $dirPath = app_path() . 'data/'; // app/data/ 目录 + + // 3. 验证文件夹是否存在 + if (!is_dir($dirPath)) { + return "文件夹不存在:{$dirPath}"; + } + + // 4. 读取文件夹中的所有文件,并筛选以指定前缀开头的文件 + $fileNames = []; + // 打开目录 + $dirHandle = opendir($dirPath); + if ($dirHandle) { + // 循环读取目录中的文件 + while (($fileName = readdir($dirHandle)) !== false) { + // 排除 . 和 .. 目录 + if ($fileName != '.' && $fileName != '..') { + // 检查文件名是否以昨天日期前缀开头 + if (strpos($fileName, $prefix) === 0) { + $fileNames[] = $fileName; + } + } + } + closedir($dirHandle); // 关闭目录句柄 + } + + // 5. 输出结果 + return [ + 'prefix' => $prefix, + 'dir' => $dirPath, + 'files' => $fileNames, + 'count' => count($fileNames) + ]; +} + + + + +if (!function_exists('returnSuccessData')) { + + + function returnSuccessData($data = [], $arr_data = []) + { + if(!empty($arr_data)) { + $return = $arr_data; + $return['code'] = 0; + $return['message'] = 'ok'; + return $return; + }else { + return ['code' => 0, 'message' => 'ok', 'data' => $data]; + } + } +} + +if (!function_exists('maskPhoneNumber')) { + /** + * 隐藏手机号中间部分 + * @param string $phone 手机号 + * @param int $start 开始位置 + * @param int $length 替换长度 + * @return string 处理后的手机号 + */ + function maskPhoneNumber(string $phone, int $start = 3, int $length = 4): string + { + if (strlen($phone) < $start + $length) { + return $phone; // 长度不足,直接返回原号码 + } + return substr_replace($phone, str_repeat('*', $length), $start, $length); + } +} + + +if (!function_exists('sha256Hex')) { + function sha256Hex($data) { + return hash('sha256', $data); + } +} + + +if (!function_exists('toSerialCode')) { + function toSerialCode($id) { + $r = str_split("0123456789ABCDEFGHIJKLMNOPQRSTUV"); + $binLen = count($r); // 进制长度,通常为32 + $s = 8; // 目标字符串长度 + $e = "00000000"; // 补全字符,通常为8个'0' + + $buf = array_fill(0, 32, ''); + $charPos = 32; + $id = (string) $id; + while (intdiv($id, $binLen) > 0) { + $ind = (int) ($id % $binLen); + $buf[--$charPos] = $r[$ind]; + $id = intdiv($id, $binLen); + } + $buf[--$charPos] = $r[(int) ($id % $binLen)]; + + $str = implode(array_slice($buf, $charPos)); + + // 长度不足时补全 + if (strlen($str) < $s) { + $str = substr($e, 0, $s - strlen($str)) . $str; + } + + return strtoupper($str); + } +} + +function toSnakeCase($string) { + // 将驼峰或帕斯卡命名法转为下划线命名 + $string = preg_replace('/([a-z])([A-Z])/', '$1_$2', $string); + $string = preg_replace('/([A-Z])([A-Z][a-z])/', '$1_$2', $string); + return strtolower($string); +} + +function arrayKeysToSnakeCase($array) { + $result = []; + foreach ($array as $key => $value) { + $newKey = is_string($key) ? toSnakeCase($key) : $key; + if (is_array($value)) { + $value = arrayKeysToSnakeCase($value); // 递归处理 + } + $result[$newKey] = $value; + } + return $result; +} + +function getNormalDate(string $time = '') +{ + return $time ? date($time) : date('Y-m-d H:i:s'); +} + + /** - * Here is your custom functions. + * ThinkPHP 防抖函数,基于 cache() 助手函数实现 + * + * @param string $key 唯一操作标识符(例如 user_id_action) + * @param int $waitMs 等待毫秒数(支持毫秒) + * @param callable $callback 要执行的操作 + * @return mixed|null 等待时间内重复调用将返回 null */ +function debounceAndRun(string $key, callable $callback,int $waitMs = 2000) +{ + // cache() 默认单位是秒,我们转成浮点秒数 + $ttl = $waitMs / 1000; + + // 尝试设置缓存,存在时不执行 + if (!cache($key)) { + cache($key, 1, $ttl); // 设置一个短暂缓存用于防抖 + return $callback(); + } + + throw new Exception("操作过于频繁,请稍后再试"); +} + +function debounce(string $key,int $waitMs = 20) +{ + // cache() 默认单位是秒,我们转成浮点秒数 + $ttl = $waitMs; + + // 尝试设置缓存,存在时不执行 + if (!cache($key)) { + cache($key, 1, $ttl); // 设置一个短暂缓存用于防抖 + }else{ + + throw new SysException("操作过于频繁,请稍后再试"); + } +} + +/** + * 类似 Java 的 StrUtil.format("{}, {}") 占位符替换函数 + * + * @param string $template 模板字符串 + * @param mixed ...$args 依次替换的值 + * @return string + */ +function format(string $template, ...$args): string +{ + foreach ($args as $value) { + if (!is_string($value)) { + $value = json_encode($value); + } + $template = preg_replace('/\{}/', (string)$value, $template, 1); + } + return $template; +} + +if (!function_exists('uuid')) { + function uuid():string + { + $data = random_bytes(16); + $data[6] = chr((ord($data[6]) & 0x0f) | 0x40); + $data[8] = chr((ord($data[8]) & 0x3f) | 0x80); + return vsprintf('%s%s%s%s%s%s%s%s', str_split(bin2hex($data), 4)); + } +} + +if (!function_exists('formatTo4Decimal')) { + /** + * 使用sprintf保留4位小数 + * @param float $number 输入的浮点数 + * @return string 格式化后的字符串 + */ + function formatTo4Decimal(float $number): string + { + // 先乘以10000取整,再除以10000,最后格式化为4位小数 + $truncated = (int)($number * 10000) / 10000; + return sprintf('%.4f', $truncated); + } + +} + +function hasEmpty(...$args): bool +{ + foreach ($args as $arg) { + if (empty($arg)) { + return true; + } + } + return false; +} + +if(!function_exists('shuffleMultiArray')) { + + /** + * 打乱多维数组的顶层元素 + * @param array $array 输入的多维数组 + * @return array 打乱顺序后的数组 + */ + function shuffleMultiArray(array $array): array { + $keys = array_keys($array); + shuffle($keys); // 随机打乱键名顺序 + + $shuffled = []; + foreach ($keys as $key) { + $shuffled[$key] = $array[$key]; + } + + return $shuffled; + } +} + +if(!function_exists('convertToCamelCase')) { + + /** + * 下划线转驼峰 + */ + function convertToCamelCase(array | null $data): array | null { + if (!$data) { + return $data; + } + $result = []; + foreach ($data as $k => $item) { + $camelItem = []; + if(is_array($item)) { + foreach ($item as $key => $value) { + $camelKey = lcfirst(preg_replace_callback('/_([a-z])/', function ($match) { + return strtoupper($match[1]); + }, $key)); + $camelItem[$camelKey] = $value; + } + $result[] = $camelItem; + }else { + $camelKey = lcfirst(preg_replace_callback('/_([a-z])/', function ($match) { + return strtoupper($match[1]); + }, $k)); + $result[$camelKey] = $item; + } + } + return $result; + } +} + + +if(!function_exists('apiconvertToCamelCase')) { + + /** + * 下划线转驼峰 + */ + function apiconvertToCamelCase(array $data): array { + $result = []; + foreach ($data as $k => $item) { + $camelItem = []; + if(is_array($item)) { + foreach ($item as $key => $value) { + $camelKey = lcfirst(preg_replace_callback('/_([a-z])/', function ($match) { + return strtoupper($match[1]); + }, $key)); + $camelItem[$camelKey] = $value; + } + $result[] = $camelItem; + }else { + $camelKey = lcfirst(preg_replace_callback('/_([a-z])/', function ($match) { + return strtoupper($match[1]); + }, $k)); + $result[$camelKey] = $item; + } + } + $result = convertUserIdToString($result); + return $result; + } +} + + +if(!function_exists('page')) { + + /** + * 分页计算 + */ + function page($page = 1, $limit = 10) { + + return ($page - 1) * $limit; + } +} + + +/** + * 自动加锁执行回调逻辑,结束后自动释放锁 + * + * @param string $key 锁的键名 + * @param int $expire 锁过期时间(秒) + * @param \Closure $callback 要执行的回调函数 + * @return mixed 回调函数返回值,失败返回 false + */ +function runWithLock(string $key, int $expire, \Closure $callback) +{ + $lockValue = uniqid(); + $val = cache($key); + if ($val) { + return false; + } + + // 加锁(NX:不存在时设置,EX:过期时间) + $acquired = cache($key, $lockValue, $expire); + + if (!$acquired) { + return false; // 获取锁失败 + } + + try { + return $callback(); // 执行传入的方法 + } finally { + release($key, $lockValue); + } +} + +/** + * 解锁(用Lua防止误删) + */ +function release(string $key, string $value) +{ + $lua = << uuid(), + ]; + Log::info("消息队列发送消息,对列名: $class, 携带数据: ".json_encode($data).', 延时时间: '.$seconds); + if ($seconds > 0) { + Queue::later($seconds, $class, $data); + }else{ + Queue::push($class, $data); + } +} + +if(!function_exists('daysBetween')) { + /** + * 计算两个日期之间的天数差(纯原生函数实现) + * @param string $startDate 开始日期(格式:YYYY-MM-DD) + * @param string $endDate 结束日期(格式:YYYY-MM-DD,默认当前日期) + * @return int 天数差(绝对值) + */ + function daysBetween(string $startDate, string $endDate = null): int { + // 解析开始日期 + $startTimestamp = strtotime($startDate); + if ($startTimestamp === false) { + throw new InvalidArgumentException("无效的开始日期格式: $startDate"); + } + + // 处理结束日期(默认为当前日期) + if ($endDate === null) { + $endTimestamp = time(); + } else { + $endTimestamp = strtotime($endDate); + if ($endTimestamp === false) { + throw new InvalidArgumentException("无效的结束日期格式: $endDate"); + } + } + + // 计算天数差(忽略时区影响,仅计算日期差) + $startDay = strtotime(date('Y-m-d', $startTimestamp)); + $endDay = strtotime(date('Y-m-d', $endTimestamp)); + $daysDiff = ($endDay - $startDay) / (24 * 60 * 60); // 86400秒/天 + + return (int)$daysDiff; + } +} + +if(!function_exists('buildFlowDays')) { + /** + * 生成从开始日期起的连续日期列表 + * @param string $beginDay 开始日期(格式:YYYY-MM-DD) + * @param int $activeDays 连续天数 + * @return array 日期字符串列表(格式:YYYY-MM-DD) + */ + function buildFlowDays(string $beginDay, int $activeDays) + { + $flowDays = []; + $timestamp = strtotime($beginDay); // 转换为时间戳 + + for ($i = 0; $i < $activeDays; $i++) { + // 计算偏移后的时间戳(每天86400秒) + $currentTimestamp = $timestamp + ($i * 86400); + // 格式化为YYYY-MM-DD并添加到列表 + $flowDays[] = date('Y-m-d', $currentTimestamp); + } + + return $flowDays; + } + +} + +if(!function_exists('buildFlowDaysTwo')) { + /** + * 生成从开始日期到结束日期的连续日期列表(纯原生函数实现) + * @param string $beginDay 开始日期(格式:YYYY-MM-DD) + * @param string $endDay 结束日期(格式:YYYY-MM-DD) + * @return array 日期字符串列表(格式:YYYY-MM-DD) + */ + function buildFlowDaysTwo(string $beginDay, string $endDay): array + { + $flowDays = []; + $currentTimestamp = strtotime($beginDay); + $endTimestamp = strtotime($endDay); + + while (true) { + // 将当前日期添加到列表(每次循环开始时添加) + $flowDays[] = date('Y-m-d', $currentTimestamp); + + // 判断是否达到结束日期 + if ($currentTimestamp === $endTimestamp) { + break; + } + + // 未达到结束日期,增加一天 + $currentTimestamp += 86400; // 86400秒 = 1天 + } + + return $flowDays; + } +} + +if(!function_exists('todayAfterSecond')) { + /** + * 获取当日剩余秒数 + * @return int 剩余秒数 + */ + function todayAfterSecond() + { + // 获取当前时间戳 + $now = time(); + + // 获取当天结束时间(23:59:59)的时间戳 + $endOfDay = strtotime('tomorrow -1 second'); + + // 计算剩余秒数 + $diffSeconds = $endOfDay - $now; + + return $diffSeconds; + } + +} + +if(!function_exists('generateRedisKey')) { + + function generateRedisKey($key, $id) + { + return 'sys:limit:' . $key . ':' . $id; + } + +} + +if(!function_exists('bankCard')) { + + function bankCard($bankCardNo) + { + if (empty($bankCardNo)) { + return $bankCardNo; + } + + $bankCardNo = trim($bankCardNo); + $length = strlen($bankCardNo); + + if ($length < 9) { + return $bankCardNo; + } + + $midLength = $length - 8; // 中间需要脱敏的长度 + $buf = ''; + + // 保留前4位 + $buf .= substr($bankCardNo, 0, 4); + + // 中间部分用*替换,每4位加一个空格 + for ($i = 0; $i < $midLength; $i++) { + if ($i % 4 === 0) { + $buf .= ' '; + } + $buf .= '*'; + } + + // 保留后4位,前面加一个空格 + $buf .= ' ' . substr($bankCardNo, -4); + + return $buf; + } + +} + +if(!function_exists('email')) { + + function email($email) + { + if (empty($email)) { + return ''; + } + + $index = strpos($email, '@'); + + if ($index <= 1) { + return $email; + } + + // 保留第一个字符,中间部分用*替换,保留@及后面的域名 + $prefix = substr($email, 0, 1); + $suffix = substr($email, $index); + $maskLength = $index - 1; + $maskedPart = str_repeat('*', $maskLength); + + return $prefix . $maskedPart . $suffix; + } + +} + +if(!function_exists('idCardNum')) { + /** + * 对身份证号进行脱敏处理 + * @param string $idCardNum 身份证号码 + * @param int $front 保留前几位 + * @param int $end 保留后几位 + * @return string 脱敏后的身份证号 + */ + function idCardNum($idCardNum, $front, $end) + { + // 身份证不能为空 + if (empty($idCardNum)) { + return ''; + } + + // 需要截取的长度不能大于身份证号长度 + if (($front + $end) > strlen($idCardNum)) { + return ''; + } + + // 需要截取的位数不能小于0 + if ($front < 0 || $end < 0) { + return ''; + } + + // 保留前$front位和后$end位,中间用*替换 + $maskLength = strlen($idCardNum) - $front - $end; + $maskedPart = str_repeat('*', $maskLength); + + return substr($idCardNum, 0, $front) . $maskedPart . substr($idCardNum, -$end); + } +} + + +if(!function_exists('http_post')) { + function http_post($url, $data, $headers = [], $timeout = 30, $contentType = 'application/json') + { + // 初始化cURL + $ch = curl_init(); + + // 处理请求数据 + if ($contentType === 'application/json') { + $data = json_encode($data); + } elseif (is_array($data)) { + $data = http_build_query($data); + } + + // 设置请求头 + $defaultHeaders = [ + "Content-Type: $contentType", + "Content-Length: " . strlen($data) + ]; + $headers = array_merge($defaultHeaders, $headers); + + // 设置cURL选项 + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + + // 执行请求 + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + + // 关闭cURL + curl_close($ch); + + // 处理错误 + if ($response === false) { + return $error; + } + return $response; + } +} + +function shiro_simple_hash_hex_salt(string $algorithm, string $source, ?string $salt = null, int $iterations = 1): string +{ + if ($iterations < 1) { + $iterations = 1; + } + + $salt = $salt ?? ''; // 将 null 转为空字符串 + $hexSalt = bin2hex($salt); + + // 转为字节 + $sourceBytes = is_string($source) ? $source : strval($source); + $saltBytes = $hexSalt !== '' ? hex2bin($hexSalt) : ''; + + if ($saltBytes === false && $hexSalt !== '') { + throw new InvalidArgumentException("Invalid hex salt: $hexSalt"); + } + + // 初始 hash(含 salt) + $digest = hash_init($algorithm); + if ($saltBytes !== '') { + hash_update($digest, $saltBytes); + } + hash_update($digest, $sourceBytes); + $result = hash_final($digest, true); + + // 后续迭代(不加盐) + for ($i = 1; $i < $iterations; $i++) { + $result = hash($algorithm, $result, true); // binary + } + + return bin2hex($result); +} + + + +if(!function_exists('get_master_connect_name')) { + function get_master_connect_name() + { + return config('database.z_library'); + } +} + +if(!function_exists('get_slave_connect_name')) { + function get_slave_connect_name() + { + return config('database.search_library'); + } +} + + +if(!function_exists('get_file_info')) { + function get_file_info($file) + { + return file_get_contents($file); + } +} + +function buildPageInfo(array $info, bool $isRecord=false) +{ + $size = count($info); + return [ + 'totalCount' => $size, + 'pageSize' => $size, + 'totalPage' => 1, + 'currPage' => 1, + $isRecord ? 'records' : 'list' => $info + ]; +} + +/** + * 将日期补全为当天的开始时间(00:00:00) + * @param string $date 输入日期(支持格式:'2023-10-05'、'2023/10/05'、'2023-10-05 12:30:45' 等) + * @return string 补全后的时间字符串(格式:'Y-m-d 00:00:00') + * @throws \Exception 若日期格式无效则抛出异常 + */ +function completeStartTime($date) { + // 将输入日期转换为时间戳(支持多种格式) + $timestamp = strtotime($date); + if ($timestamp === false) { + throw new \Exception("无效的日期格式:{$date},请使用类似 'YYYY-MM-DD' 的格式"); + } + + // 格式化时间戳为 "YYYY-MM-DD 00:00:00" + return date('Y-m-d 00:00:00', $timestamp); +} +/** + * 模糊删除 Redis 中匹配指定模式的所有 key(使用 SCAN) + * + * @param string $pattern 匹配模式,如 "user_*" + * @param int $count 每次扫描数量,默认 100 + * @return int 删除的 key 数量 + */ +function deleteRedisKeysByPattern(string $pattern, int $count = 100): int +{ + $redis = Cache::store('redis')->handler(); + + $iterator = null; + $deleted = 0; + + do { + $keys = $redis->scan($iterator, $pattern, $count); + if (!empty($keys)) { + $redis->del(...$keys); + $deleted += count($keys); + } + } while ($iterator > 0); + + return $deleted; +} + +/** + * 将多维数组中的user_id或userId键的值转换为字符串类型 + * @param array $data 待处理的多维数组 + * @return array 处理后的数组 + */ +function convertUserIdToString(array $data): array { + $result = []; + + foreach ($data as $key => $value) { + // 处理键为user_id或userId的情况 + $id_list = [ + 'user_id', + 'userId', + 'id', + 'roleId', + 'courseDetailsId', + 'courseId', + 'ordersId', + ]; + if (in_array($key, $id_list)) { + $result[$key] = (string)$value; + continue; + } + + // 递归处理子数组 + if (is_array($value)) { + $result[$key] = convertUserIdToString($value); + } else { + // 其他类型的值保持不变 + $result[$key] = $value; + } + } + + return $result; +} diff --git a/composer.json b/composer.json index 10ea97d..5de0593 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ "webman/think-orm": "^2.1", "vlucas/phpdotenv": "^5.6", "webman/think-cache": "^2.1", - "firebase/php-jwt": "^6.11" + "firebase/php-jwt": "^6.11", + "ext-bcmath": "*" }, "suggest": { "ext-event": "For better performance. " diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..cc46f0a --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +