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 '';
+ }
+}
+
+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 @@
+