From 275713f8937d727469c617949ffce3b7280d73cd Mon Sep 17 00:00:00 2001 From: ASUS <515617283@qq.com> Date: Wed, 13 Aug 2025 18:31:52 +0800 Subject: [PATCH] add --- app/common/controller/BaseController.php | 203 ++++++ app/common/library/Auth.php | 651 ++++++++++++++++++ app/common/library/Email.php | 71 ++ app/common/library/Menu.php | 156 +++++ app/common/library/SnowFlake.php | 87 +++ app/common/library/Token.php | 232 +++++++ app/common/library/Upload.php | 341 +++++++++ app/common/library/token/Driver.php | 92 +++ .../token/TokenExpirationException.php | 16 + app/common/library/token/driver/Mysql.php | 109 +++ app/common/library/token/driver/Redis.php | 146 ++++ app/common/library/upload/Driver.php | 47 ++ app/common/library/upload/driver/Local.php | 148 ++++ app/common/model/Attachment.php | 115 ++++ app/common/model/BaseModel.php | 150 ++++ app/common/model/Common.php | 125 ++++ app/common/model/Config.php | 83 +++ app/common/model/CourseCollect.php | 14 + app/common/model/SysUser.php | 80 +++ app/common/model/User.php | 51 ++ app/common/model/UserMoneyLog.php | 41 ++ app/common/model/UserScoreLog.php | 11 + app/controller/IndexController.php | 175 ----- app/czg/app/controller/CourseController.php | 22 + app/czg/app/model/AlibabaSms.php | 93 +++ app/czg/app/model/Announcement.php | 21 + app/czg/app/model/Banner.php | 13 + app/czg/app/model/Cash.php | 118 ++++ app/czg/app/model/CommonInfo.php | 32 + app/czg/app/model/Course.php | 443 ++++++++++++ app/czg/app/model/CourseCollect.php | 84 +++ app/czg/app/model/CourseDetails.php | 53 ++ app/czg/app/model/DiscSpinningRecord.php | 128 ++++ app/czg/app/model/HelpClassify.php | 20 + app/czg/app/model/HelpWord.php | 16 + app/czg/app/model/Invite.php | 179 +++++ app/czg/app/model/InviteAchievement.php | 19 + app/czg/app/model/InviteMoney.php | 30 + app/czg/app/model/MessageInfo.php | 60 ++ app/czg/app/model/Msg.php | 117 ++++ app/czg/app/model/Orders.php | 602 ++++++++++++++++ app/czg/app/model/TaskCenter.php | 17 + app/czg/app/model/TaskCenterRecord.php | 420 +++++++++++ app/czg/app/model/TbUser.php | 309 +++++++++ app/czg/app/model/TbUserBlacklist.php | 34 + app/czg/app/model/UniAdCallbackRecord.php | 180 +++++ app/czg/app/model/UserInfo.php | 53 ++ app/czg/app/model/UserMoney.php | 121 ++++ app/czg/app/model/UserMoneyDetails.php | 15 + app/czg/app/model/UserPrizeExchange.php | 85 +++ app/czg/app/model/UserSignRecord.php | 233 +++++++ app/czg/app/model/WithDraw.php | 243 +++++++ app/enums/ErrEnums.php | 16 + app/exception/CzgException.php | 35 + app/exception/HttpResponseException.php | 22 + app/exception/SysException.php | 35 + app/utils/AliUtils.php | 94 +++ app/utils/JwtUtils.php | 86 +++ app/utils/RedisUtils.php | 230 +++++++ app/utils/WuYouPayUtils.php | 192 ++++++ composer.json | 3 +- composer.lock | 71 +- config/buildadmin.php | 87 +++ config/route.php | 1 - extend/alei/Export.php | 231 +++++++ extend/ba/Auth.php | 316 +++++++++ extend/ba/Captcha.php | 443 ++++++++++++ extend/ba/ClickCaptcha.php | 338 +++++++++ extend/ba/Date.php | 195 ++++++ extend/ba/Depends.php | 212 ++++++ extend/ba/Exception.php | 17 + extend/ba/Filesystem.php | 248 +++++++ extend/ba/Random.php | 87 +++ extend/ba/TableManager.php | 186 +++++ extend/ba/Terminal.php | 509 ++++++++++++++ extend/ba/Tree.php | 145 ++++ extend/ba/Version.php | 133 ++++ 77 files changed, 10658 insertions(+), 178 deletions(-) create mode 100644 app/common/controller/BaseController.php create mode 100644 app/common/library/Auth.php create mode 100644 app/common/library/Email.php create mode 100644 app/common/library/Menu.php create mode 100644 app/common/library/SnowFlake.php create mode 100644 app/common/library/Token.php create mode 100644 app/common/library/Upload.php create mode 100644 app/common/library/token/Driver.php create mode 100644 app/common/library/token/TokenExpirationException.php create mode 100644 app/common/library/token/driver/Mysql.php create mode 100644 app/common/library/token/driver/Redis.php create mode 100644 app/common/library/upload/Driver.php create mode 100644 app/common/library/upload/driver/Local.php create mode 100644 app/common/model/Attachment.php create mode 100644 app/common/model/BaseModel.php create mode 100644 app/common/model/Common.php create mode 100644 app/common/model/Config.php create mode 100644 app/common/model/CourseCollect.php create mode 100644 app/common/model/SysUser.php create mode 100644 app/common/model/User.php create mode 100644 app/common/model/UserMoneyLog.php create mode 100644 app/common/model/UserScoreLog.php create mode 100644 app/czg/app/controller/CourseController.php create mode 100644 app/czg/app/model/AlibabaSms.php create mode 100644 app/czg/app/model/Announcement.php create mode 100644 app/czg/app/model/Banner.php create mode 100644 app/czg/app/model/Cash.php create mode 100644 app/czg/app/model/CommonInfo.php create mode 100644 app/czg/app/model/Course.php create mode 100644 app/czg/app/model/CourseCollect.php create mode 100644 app/czg/app/model/CourseDetails.php create mode 100644 app/czg/app/model/DiscSpinningRecord.php create mode 100644 app/czg/app/model/HelpClassify.php create mode 100644 app/czg/app/model/HelpWord.php create mode 100644 app/czg/app/model/Invite.php create mode 100644 app/czg/app/model/InviteAchievement.php create mode 100644 app/czg/app/model/InviteMoney.php create mode 100644 app/czg/app/model/MessageInfo.php create mode 100644 app/czg/app/model/Msg.php create mode 100644 app/czg/app/model/Orders.php create mode 100644 app/czg/app/model/TaskCenter.php create mode 100644 app/czg/app/model/TaskCenterRecord.php create mode 100644 app/czg/app/model/TbUser.php create mode 100644 app/czg/app/model/TbUserBlacklist.php create mode 100644 app/czg/app/model/UniAdCallbackRecord.php create mode 100644 app/czg/app/model/UserInfo.php create mode 100644 app/czg/app/model/UserMoney.php create mode 100644 app/czg/app/model/UserMoneyDetails.php create mode 100644 app/czg/app/model/UserPrizeExchange.php create mode 100644 app/czg/app/model/UserSignRecord.php create mode 100644 app/czg/app/model/WithDraw.php create mode 100644 app/enums/ErrEnums.php create mode 100644 app/exception/CzgException.php create mode 100644 app/exception/HttpResponseException.php create mode 100644 app/exception/SysException.php create mode 100644 app/utils/AliUtils.php create mode 100644 app/utils/JwtUtils.php create mode 100644 app/utils/RedisUtils.php create mode 100644 app/utils/WuYouPayUtils.php create mode 100644 config/buildadmin.php create mode 100644 extend/alei/Export.php create mode 100644 extend/ba/Auth.php create mode 100644 extend/ba/Captcha.php create mode 100644 extend/ba/ClickCaptcha.php create mode 100644 extend/ba/Date.php create mode 100644 extend/ba/Depends.php create mode 100644 extend/ba/Exception.php create mode 100644 extend/ba/Filesystem.php create mode 100644 extend/ba/Random.php create mode 100644 extend/ba/TableManager.php create mode 100644 extend/ba/Terminal.php create mode 100644 extend/ba/Tree.php create mode 100644 extend/ba/Version.php diff --git a/app/common/controller/BaseController.php b/app/common/controller/BaseController.php new file mode 100644 index 0000000..b13404f --- /dev/null +++ b/app/common/controller/BaseController.php @@ -0,0 +1,203 @@ +request = request(); + + $needLogin = !action_in_arr($this->noNeedLogin); + // 初始化会员鉴权实例 + $this->auth = Auth::instance(); + $token = request()->header('token'); + if ($token) { + if(!$this->auth->init($token)) { + $this->error('Token expiration', [], 409); + } + }; + if ($needLogin) { + $this->error('Token expiration', [], 409); + } + if ($needLogin) { + if (!$this->auth->isLogin()) { + $this->error('请登录', [ + 'type' => $this->auth::NEED_LOGIN + ], $this->auth::LOGIN_RESPONSE_CODE); + } + } + + } + + + /** + * 操作成功 + * @param string $msg 提示消息 + * @param mixed $data 返回数据 + * @param int $code 错误码 + * @param string|null $type 输出类型 + * @param array $header 发送的 header 信息 + * @param array $options Response 输出参数 + */ + protected function success(string $msg = '', mixed $data = null, int $code = 0, string $type = null, array $header = [], array $options = []): void + { + $this->result($msg, $data, $code, $type, $header, $options); + } + + + protected function successWithData(mixed $data = null, string $msg = '', int $code = 0, string $type = null, array $header = [], array $options = []): void + { + $this->result($msg, $data, $code, $type, $header, $options); + } + + /** + * 操作失败 + * @param string $msg 提示消息 + * @param mixed $data 返回数据 + * @param int $code 错误码 + * @param string|null $type 输出类型 + * @param array $header 发送的 header 信息 + * @param array $options Response 输出参数 + */ + protected function error(string $msg = '', mixed $data = null, int $code = -1, string $type = null, array $header = [], array $options = []): void + { + $this->result($msg, $data, $code, $type, $header, $options); + } + + protected function errorMsg(string $msg = '', mixed $data = null, int $code = -1, string $type = null, array $header = [], array $options = []): void + { + $this->result($msg, $data, $code, $type, $header, $options, 'data', 'msg'); + } + + + + /** + * 操作成功 + * @param string $msg 提示消息 + * @param mixed $data 返回数据 + * @param int $code 错误码 + * @param string|null $type 输出类型 + * @param array $header 发送的 header 信息 + * @param array $options Response 输出参数 + */ + protected function n_success(array $data, string $msg = 'ok', int $code = 0, string $type = null, array $header = [], array $options = []): void + { + $this->resultApi($data, $msg, $code, $type, $header, $options); + } + + /** + * 操作失败 + * @param string $msg 提示消息 + * @param mixed $data 返回数据 + * @param int $code 错误码 + * @param string|null $type 输出类型 + * @param array $header 发送的 header 信息 + * @param array $options Response 输出参数 + */ + protected function n_error(string $msg, array $data = [], int $code = -1, string $type = null, array $header = [], array $options = []): void + { + $this->resultApi($data, $msg, $code, $type, $header, $options); + } + + /** + * 多种操作结果 + */ + public function resultApi(array $data = [], string $msg = 'ok', int $code = 0, string $type = null, array $header = [], array $options = []) + { + $data['code'] = $code; + $data['message'] = $msg; + $data['time'] = $this->request->server('REQUEST_TIME'); + if(isset($data['data']['records'])) { + $data['data']['records'] = apiconvertToCamelCase($data['data']['records']); + } + if(isset($data['page']['list'])) { + $data['page']['list'] = apiconvertToCamelCase($data['page']['list']); + } + if(isset($data['data']['list'])) { + $data['data']['list'] = apiconvertToCamelCase($data['data']['list']); + } + if(isset($data['data']['userEntity'])) { + $data['data']['userEntity'] = apiconvertToCamelCase($data['data']['userEntity']); + } + $result = $data; + + $type = $type ?: $this->responseType; + + $response = new Response(); + $response->header('Content-Type', $type); + $response->withBody($result); + return $response; + } + + + + /** + * 返回 API 数据 + * @param string $msg 提示消息 + * @param mixed $data 返回数据 + * @param int $code 错误码 + * @param string|null $type 输出类型 + * @param array $header 发送的 header 信息 + * @param array $options Response 输出参数 + */ + public function result(string $msg, mixed $data = null, int $code = 0, string $type = null, array $header = [], array $options = [], string $dataKey = 'data', string $msgKey = 'message') + { + if(isset($data['records'])) { + $data['records'] = apiconvertToCamelCase($data['records']); + } + if(isset($data['page'])) { + $data['page'] = apiconvertToCamelCase($data['page']); + } + if(isset($data['list'])) { + $data['list'] = apiconvertToCamelCase($data['list']); + } + $result = [ + 'code' => $code, + 'message' => $msg, + 'time' => time(), + $dataKey => $data, + ]; + throw new \app\exception\HttpResponseException(json($result)); + } + + public function ApiDataReturn($data) + { + return json($data); + } + + + + +} \ No newline at end of file diff --git a/app/common/library/Auth.php b/app/common/library/Auth.php new file mode 100644 index 0000000..2ab99d8 --- /dev/null +++ b/app/common/library/Auth.php @@ -0,0 +1,651 @@ + 'user_group', // 用户组数据表名 +// 'auth_group_access' => '', // 用户-用户组关系表(关系字段) +// 'auth_rule' => 'user_rule', // 权限规则表 +// ], $config)); +// +// $this->setKeepTime((int)config('buildadmin.user_token_keep_time')); + } + + /** + * 魔术方法-会员信息字段 + * @param $name + * @return mixed 字段信息 + */ + public function __get($name): mixed + { + return $this->model[$name]; + } + + /** + * 初始化 + * @access public + * @param array $options 传递给 /ba/Auth 的参数 + * @return Auth + */ + public static function instance(array $options = []): Auth + { + $request = request(); + if (!isset($request->userAuth)) { + $request->userAuth = new static($options); + } + return $request->userAuth; + } + + /** + * 根据Token初始化会员登录态 + * @param $token + * @return bool + * @throws Throwable + */ + public function init($token): bool + { + $jwtUtil = (new JwtUtils()); + + $tokenData = $jwtUtil->getClaimByToken($token); + if ($tokenData) { + + /** + * 过期检查,过期则抛出 @see TokenExpirationException + */ + + $userId = $tokenData->sub; + if (!isset($tokenData->type) || ($tokenData->type == self::TOKEN_TYPE && $userId > 0)) { + + $where = $sale = ['user_id' => $userId]; + $user_id_db_name = DatabaseRoute::getConnection('tb_user', $sale); + $this->model = Db::connect($user_id_db_name)->name('tb_user')->where($where)->find(); + if (!$this->model) { + $this->setError('Account not exist'); + return false; + } + + if ($this->model['status'] != 1) { + $this->setError('Account disabled'); + return false; + } + + $this->token = $token; + $this->newloginSuccessful(); + return true; + } + } + + $this->setError('Token login failed'); + $this->reset(); + return false; + } + + /** + * 会员注册,可使用关键词参数方式调用:$auth->register('u18888888888', email: 'test@qq.com') + * @param string $username + * @param string $password + * @param string $mobile + * @param string $email + * @param int $group 会员分组 ID 号 + * @param array $extend 扩展数据,如 ['status' => 'disable'] + * @return bool + */ + public function register(string $username, string $password = '', string $mobile = '', string $email = '', int $group = 1, array $extend = []): bool + { + $validate = Validate::rule([ + 'email|' . __('Email') => 'email|unique:user', + 'mobile|' . __('Mobile') => 'mobile|unique:user', + 'username|' . __('Username') => 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user', + 'password|' . __('Password') => 'regex:^(?!.*[&<>"\'\n\r]).{6,32}$', + ]); + $params = [ + 'username' => $username, + 'password' => $password, + 'mobile' => $mobile, + 'email' => $email, + ]; + if (!$validate->check($params)) { + $this->setError($validate->getError()); + return false; + } + + // 按需生成随机密码 + if (!$password) { + $password = Random::build(); + } + + // 用户昵称 + $nickname = preg_replace_callback('/1[3-9]\d{9}/', function ($matches) { + // 对 username 中出现的所有手机号进行脱敏处理 + $mobile = $matches[0]; + return substr($mobile, 0, 3) . '****' . substr($mobile, 7); + }, $username); + + $ip = request()->ip(); + $time = time(); + $data = [ + 'group_id' => $group, + 'nickname' => $nickname, + 'join_ip' => $ip, + 'join_time' => $time, + 'last_login_ip' => $ip, + 'last_login_time' => $time, + 'status' => 'enable', // 状态:enable=启用,disable=禁用,使用 string 存储可以自定义其他状态 + ]; + $data = array_merge($params, $data); + $data = array_merge($data, $extend); + + Db::startTrans(); + try { + $this->model = User::create($data); + $this->token = Random::uuid(); + Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime); + Db::commit(); + + $this->model->resetPassword($this->model->id, $password); + + Event::trigger('userRegisterSuccess', $this->model); + } catch (Throwable $e) { + $this->setError($e->getMessage()); + Db::rollback(); + return false; + } + return true; + } + + /** + * 会员登录 + * @param string $username 用户名 + * @param string $password 密码 + * @param bool $keep 是否保持登录 + * @return bool + * @throws Throwable + */ + public function login(string $username, string $password, bool $keep): bool + { + // 判断账户类型 + $accountType = false; + $validate = Validate::rule([ + 'mobile' => 'mobile', + 'email' => 'email', + 'username' => 'regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$', + ]); + if ($validate->check(['mobile' => $username])) $accountType = 'mobile'; + if ($validate->check(['email' => $username])) $accountType = 'email'; + if ($validate->check(['username' => $username])) $accountType = 'username'; + if (!$accountType) { + $this->setError('Account not exist'); + return false; + } + + $this->model = User::where($accountType, $username)->find(); + if (!$this->model) { + $this->setError('Account not exist'); + return false; + } + if ($this->model->status == 'disable') { + $this->setError('Account disabled'); + return false; + } + + // 登录失败重试检查 + $userLoginRetry = Config::get('buildadmin.user_login_retry'); + if ($userLoginRetry && $this->model->last_login_time) { + // 重置失败次数 + if ($this->model->login_failure > 0 && time() - $this->model->last_login_time >= 86400) { + $this->model->login_failure = 0; + $this->model->save(); + + // 重获模型实例,避免单实例多次更新 + $this->model = User::where($accountType, $username)->find(); + } + + if ($this->model->login_failure >= $userLoginRetry) { + $this->setError('Please try again after 1 day'); + return false; + } + } + + // 密码检查 + if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt])) { + $this->loginFailed(); + $this->setError('Password is incorrect'); + return false; + } + + // 清理 token + if (Config::get('buildadmin.user_sso')) { + Token::clear(self::TOKEN_TYPE, $this->model->id); + Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id); + } + + if ($keep) { + $this->setRefreshToken($this->refreshTokenKeepTime); + } + $this->loginSuccessful(); + return true; + } + + + /** + * 会员登录 + * @param string $username 用户名 + * @param string $password 密码 + * @param bool $keep 是否保持登录 + * @return bool + * @throws Throwable + */ + public function newlogin(string $phone, string $password, bool $keep): bool + { + $this->model = TbUser::GetByusername($phone); + if (!$this->model) { + $this->setError('未注册, 请先注册'); + return false; + } + if ($this->model['status'] == 0) { + $this->setError('账号已被禁用,请联系客服处理!'); + return false; + } + + if(empty($this->model['password'])) { + $this->setError('当前账号未绑定密码,请前往忘记密码中进行重置!'); + return false; + } + // 验证密码 + if(TbUser::CheckPassword($this->model['password'], $password)) { + // 实名认证信息 + $userInfo = new UserInfo(); + $where = ['user_id' => $this->model['user_id']]; + $userInfo->setConnection($userInfo::findbefore($userInfo, $where)); + $userInfo = $userInfo->where($where)->find(); + if($userInfo && $userInfo->cert_no) { + // 继续查黑名单表 + $userBlack = TbUserBlacklist::where(['id_card_no' => $userInfo->cert_no])->find(); + if($userBlack) { + $this->setError('系统正在维护中,请稍后再试!'); + return false; + } + } +// if ($keep) { +// $this->setRefreshToken($this->refreshTokenKeepTime); +// } + $this->newloginSuccessful(); + return true; + }else { + $this->setError('账号或密码不正确!'); + return false; + } + } + + + + + /** + * 直接登录会员账号 + * @param int $userId 用户ID + * @return bool + * @throws Throwable + */ + public function direct(int $userId): bool + { + $this->model = User::find($userId); + if (!$this->model) return false; + if (Config::get('buildadmin.user_sso')) { + Token::clear(self::TOKEN_TYPE, $this->model->id); + Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id); + } + return $this->loginSuccessful(); + } + + /** + * 直接登录会员账号 new + * @param int $userId 用户ID + * @return bool + * @throws Throwable + */ + public function newdirect(int $userId): bool + { + if (Config::get('buildadmin.user_sso')) { + Token::clear(self::TOKEN_TYPE, $this->model->id); + Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id); + } + return $this->newloginSuccessful(); + } + + + /** + * 登录成功 new + * @return bool + */ + public function newloginSuccessful(): bool + { + if (!$this->model) { + return false; + } + // 新增登录时间 + $db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $this->model['user_id']], true)); + $db->name('tb_user')->where(['user_id' => $this->model['user_id']])->update(['on_line_time' => date('Y-m-d H:i:s')]); + $this->loginEd = true; + if (!$this->token) { + $this->token = (new JwtUtils())->generateToken($this->model['user_id'], 'user'); + } + return true; + } + + + /** + * 检查旧密码是否正确 + * @param $password + * @return bool + * @deprecated 请使用 verify_password 公共函数代替 + */ + public function checkPassword($password): bool + { + return verify_password($password, $this->model->password, ['salt' => $this->model->salt]); + } + + /** + * 登录成功 + * @return bool + */ + public function loginSuccessful(): bool + { + if (!$this->model) { + return false; + } + $this->model->startTrans(); + try { + $this->model->login_failure = 0; + $this->model->last_login_time = time(); + $this->model->last_login_ip = request()->ip(); + $this->model->save(); + $this->loginEd = true; + + if (!$this->token) { + $this->token = Random::uuid(); + Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime); + } + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->setError($e->getMessage()); + return false; + } + return true; + } + + /** + * 登录失败 + * @return bool + */ + public function loginFailed(): bool + { + if (!$this->model) return false; + $this->model->startTrans(); + try { + $this->model->login_failure++; + $this->model->last_login_time = time(); + $this->model->last_login_ip = request()->ip(); + $this->model->save(); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->setError($e->getMessage()); + return false; + } + return $this->reset(); + } + + public function get_user_id_db_connect() + { + return $this->user_id_db_connect; + } + + + + /** + * 退出登录 + * @return bool + */ + public function logout(): bool + { + if (!$this->loginEd) { + $this->setError('You are not logged in'); + return false; + } + return $this->reset(); + } + + /** + * 是否登录 + * @return bool + */ + public function isLogin(): bool + { + return $this->loginEd; + } + + /** + * 获取会员模型 + * @return User + */ + public function getUser() + { + return $this->model; + } + + /** + * 获取会员Token + * @return string + */ + public function getToken(): string + { + return $this->token; + } + + /** + * 设置刷新Token + * @param int $keepTime + * @return void + */ + public function setRefreshToken(int $keepTime = 0): void + { + $this->refreshToken = Random::uuid(); + Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime); + } + + /** + * 获取会员刷新Token + * @return string + */ + public function getRefreshToken(): string + { + return $this->refreshToken; + } + + /** + * 获取会员信息 - 只输出允许输出的字段 + * @return array + */ + public function getUserInfo(): array + { + if (!$this->model) return []; + $info = $this->model; + $info = array_intersect_key($info, array_flip($this->getAllowFields())); + $info['token'] = $this->getToken(); + $info['refresh_token'] = $this->getRefreshToken(); + return $info; + } + + /** + * 获取允许输出字段 + * @return array + */ + public function getAllowFields(): array + { + return $this->allowFields; + } + + /** + * 设置允许输出字段 + * @param $fields + * @return void + */ + public function setAllowFields($fields): void + { + $this->allowFields = $fields; + } + + /** + * 设置Token有效期 + * @param int $keepTime + * @return void + */ + public function setKeepTime(int $keepTime = 0): void + { + $this->keepTime = $keepTime; + } + + + + + + /** + * 设置错误消息 + * @param string $error + * @return Auth + */ + public function setError(string $error): Auth + { + $this->error = $error; + return $this; + } + + /** + * 获取错误消息 + * @return string + */ + public function getError(): string + { + return $this->error ? __($this->error) : ''; + } + + /** + * 属性重置(注销、登录失败、重新初始化等将单例数据销毁) + */ + protected function reset(bool $deleteToken = true): bool + { + if ($deleteToken && $this->token) { +// Token::delete($this->token); + } + + $this->token = ''; + $this->loginEd = false; + $this->model = null; + $this->refreshToken = ''; + $this->setError(''); + $this->setKeepTime((int)Config('buildadmin.user_token_keep_time')); + return true; + } +} \ No newline at end of file diff --git a/app/common/library/Email.php b/app/common/library/Email.php new file mode 100644 index 0000000..4852564 --- /dev/null +++ b/app/common/library/Email.php @@ -0,0 +1,71 @@ + 'utf-8', //编码格式 + 'debug' => true, //调式模式 + 'lang' => 'zh_cn', + ]; + + /** + * 构造函数 + * @param array $options + * @throws Throwable + */ + public function __construct(array $options = []) + { + $this->options = array_merge($this->options, $options); + + parent::__construct($this->options['debug']); + $langSet = Lang::getLangSet(); + if ($langSet == 'zh-cn' || !$langSet) $langSet = 'zh_cn'; + $this->options['lang'] = $this->options['lang'] ?: $langSet; + + $this->setLanguage($this->options['lang'], root_path() . 'vendor' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'phpmailer' . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR); + $this->CharSet = $this->options['charset']; + + $sysMailConfig = get_sys_config('', 'mail'); + $this->configured = true; + foreach ($sysMailConfig as $item) { + if (!$item) { + $this->configured = false; + } + } + if ($this->configured) { + $this->Host = $sysMailConfig['smtp_server']; + $this->SMTPAuth = true; + $this->Username = $sysMailConfig['smtp_user']; + $this->Password = $sysMailConfig['smtp_pass']; + $this->SMTPSecure = $sysMailConfig['smtp_verification'] == 'SSL' ? self::ENCRYPTION_SMTPS : self::ENCRYPTION_STARTTLS; + $this->Port = $sysMailConfig['smtp_port']; + + $this->setFrom($sysMailConfig['smtp_sender_mail'], $sysMailConfig['smtp_user']); + } + } + + public function setSubject($subject): void + { + $this->Subject = "=?utf-8?B?" . base64_encode($subject) . "?="; + } +} \ No newline at end of file diff --git a/app/common/library/Menu.php b/app/common/library/Menu.php new file mode 100644 index 0000000..1c22a3b --- /dev/null +++ b/app/common/library/Menu.php @@ -0,0 +1,156 @@ +where((is_numeric($parent) ? 'id' : 'name'), $parent)->find(); + if ($parentRule) { + $pid = $parentRule['id']; + } + foreach ($menu as $item) { + if (!self::requiredAttrCheck($item)) { + continue; + } + + // 属性 + $item['status'] = 1; + if (!isset($item['pid'])) { + $item['pid'] = $pid; + } + + $sameOldMenu = $model->where('name', $item['name'])->find(); + if ($sameOldMenu) { + // 存在相同名称的菜单规则 + if ($mode == 'cover') { + $sameOldMenu->save($item); + } elseif ($mode == 'rename') { + $count = $model->where('name', $item['name'])->count(); + $item['name'] = $item['name'] . '-CONFLICT-' . $count; + $item['path'] = $item['path'] . '-CONFLICT-' . $count; + $item['title'] = $item['title'] . '-CONFLICT-' . $count; + $sameOldMenu = $model->create($item); + } elseif ($mode == 'ignore') { + // 忽略同名菜单时,当前 pid 下没有同名菜单,则创建同名新菜单,以保证所有新增菜单的上下级结构 + $sameOldMenu = $model + ->where('name', $item['name']) + ->where('pid', $item['pid']) + ->find(); + + if (!$sameOldMenu) { + $sameOldMenu = $model->create($item); + } + } + } else { + $sameOldMenu = $model->create($item); + } + if (!empty($item['children'])) { + self::create($item['children'], $sameOldMenu['id'], $mode, $position); + } + } + } + + /** + * 删菜单 + * @param string|int $id 规则name或id + * @param bool $recursion 是否递归删除子级菜单、是否删除自身,是否删除上级空菜单 + * @param string $position 位置:backend=后台,frontend=前台 + * @return bool + * @throws Throwable + */ + public static function delete(string|int $id, bool $recursion = false, string $position = 'backend'): bool + { + if (!$id) { + return true; + } + $model = $position == 'backend' ? new AdminRule() : new UserRule(); + $menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find(); + if (!$menuRule) { + return true; + } + + $children = $model->where('pid', $menuRule['id'])->select()->toArray(); + if ($recursion && $children) { + foreach ($children as $child) { + self::delete($child['id'], true, $position); + } + } + + if (!$children || $recursion) { + $menuRule->delete(); + self::delete($menuRule->pid, false, $position); + } + return true; + } + + /** + * 启用菜单 + * @param string|int $id 规则name或id + * @param string $position 位置:backend=后台,frontend=前台 + * @return bool + * @throws Throwable + */ + public static function enable(string|int $id, string $position = 'backend'): bool + { + $model = $position == 'backend' ? new AdminRule() : new UserRule(); + $menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find(); + if (!$menuRule) { + return false; + } + $menuRule->status = 1; + $menuRule->save(); + return true; + } + + /** + * 禁用菜单 + * @param string|int $id 规则name或id + * @param string $position 位置:backend=后台,frontend=前台 + * @return bool + * @throws Throwable + */ + public static function disable(string|int $id, string $position = 'backend'): bool + { + $model = $position == 'backend' ? new AdminRule() : new UserRule(); + $menuRule = $model->where((is_numeric($id) ? 'id' : 'name'), $id)->find(); + if (!$menuRule) { + return false; + } + $menuRule->status = 0; + $menuRule->save(); + return true; + } + + public static function requiredAttrCheck($menu): bool + { + $attrs = ['type', 'title', 'name']; + foreach ($attrs as $attr) { + if (!array_key_exists($attr, $menu)) { + return false; + } + if (!$menu[$attr]) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/app/common/library/SnowFlake.php b/app/common/library/SnowFlake.php new file mode 100644 index 0000000..0d9307b --- /dev/null +++ b/app/common/library/SnowFlake.php @@ -0,0 +1,87 @@ +handler)) { + return $this->handler; + } + $name = $name ?: $this->getDefaultDriver(); + + if (is_null($name)) { + throw new InvalidArgumentException( + sprintf( + 'Unable to resolve NULL driver for [%s].', + static::class + ) + ); + } + + return $this->createDriver($name); + } + + /** + * 创建驱动句柄 + * @param string $name + * @return object + */ + protected function createDriver(string $name): object + { + $type = $this->resolveType($name); + + $method = 'create' . Str::studly($type) . 'Driver'; + + $params = $this->resolveParams($name); + + if (method_exists($this, $method)) { + return $this->$method(...$params); + } + + $class = $this->resolveClass($type); + + if (isset($this->instance[$type])) { + return $this->instance[$type]; + } + + return new $class(...$params); + } + + /** + * 默认驱动 + * @return string + */ + protected function getDefaultDriver(): string + { + return $this->getConfig('default'); + } + + /** + * 获取驱动配置 + * @param string|null $name 要获取的配置项,不传递获取完整token配置 + * @param null $default + * @return array|string + */ + protected function getConfig(string $name = null, $default = null): array|string + { + if (!is_null($name)) { + return Config::get('buildadmin.token.' . $name, $default); + } + + return Config::get('buildadmin.token'); + } + + /** + * 获取驱动配置参数 + * @param $name + * @return array + */ + protected function resolveParams($name): array + { + $config = $this->getStoreConfig($name); + return [$config]; + } + + /** + * 获取驱动类 + * @param string $type + * @return string + */ + protected function resolveClass(string $type): string + { + if ($this->namespace || str_contains($type, '\\')) { + $class = str_contains($type, '\\') ? $type : $this->namespace . Str::studly($type); + + if (class_exists($class)) { + return $class; + } + } + + throw new InvalidArgumentException("Driver [$type] not supported."); + } + + /** + * 获取驱动配置 + * @param string $store + * @param string|null $name + * @param null $default + * @return array|string + */ + protected function getStoreConfig(string $store, string $name = null, $default = null): array|string + { + if ($config = $this->getConfig("stores.$store")) { + return Arr::get($config, $name, $default); + } + + throw new InvalidArgumentException("Store [$store] not found."); + } + + /** + * 获取驱动类型 + * @param string $name + * @return string + */ + protected function resolveType(string $name): string + { + return $this->getStoreConfig($name, 'type', 'Mysql'); + } + + /** + * 设置token + * @param string $token + * @param string $type + * @param int $user_id + * @param int|null $expire + * @return bool + */ + public function set(string $token, string $type, int $user_id, int $expire = null): bool + { + return $this->getDriver()->set($token, $type, $user_id, $expire); + } + + /** + * 获取token + * @param string $token + * @param bool $expirationException + * @return array + */ + public function get(string $token, bool $expirationException = true): array + { + return $this->getDriver()->get($token, $expirationException); + } + + /** + * 检查token + * @param string $token + * @param string $type + * @param int $user_id + * @param bool $expirationException + * @return bool + */ + public function check(string $token, string $type, int $user_id, bool $expirationException = true): bool + { + return $this->getDriver()->check($token, $type, $user_id, $expirationException); + } + + /** + * 删除token + * @param string $token + * @return bool + */ + public function delete(string $token): bool + { + return $this->getDriver()->delete($token); + } + + /** + * 清理指定用户token + * @param string $type + * @param int $user_id + * @return bool + */ + public function clear(string $type, int $user_id): bool + { + return $this->getDriver()->clear($type, $user_id); + } + + /** + * Token过期检查 + * @throws TokenExpirationException + */ + public function tokenExpirationCheck(array $token): void + { + if (isset($token['expire_time']) && $token['expire_time'] <= time()) { + throw new TokenExpirationException(); + } + } +} \ No newline at end of file diff --git a/app/common/library/Upload.php b/app/common/library/Upload.php new file mode 100644 index 0000000..312ae34 --- /dev/null +++ b/app/common/library/Upload.php @@ -0,0 +1,341 @@ + 'local', // 默认驱动:local=本地 + 'handler' => [], // 驱动句柄 + 'namespace' => '\\app\\common\\library\\upload\\driver\\', // 驱动类的命名空间 + ]; + + /** + * 存储子目录 + */ + protected string $topic = 'default'; + + /** + * 构造方法 + * @param ?UploadedFile $file 上传的文件 + * @param array $config 配置 + * @throws Throwable + */ + public function __construct(?UploadedFile $file = null, array $config = []) + { + $upload = Config::get('upload'); + $this->config = array_merge($upload, $config); + + if ($file) { + $this->setFile($file); + } + } + + /** + * 设置上传文件 + * @param ?UploadedFile $file + * @return Upload + * @throws Throwable + */ + public function setFile(?UploadedFile $file): Upload + { + if (empty($file)) { + throw new Exception(__('No files were uploaded')); + } + + $suffix = strtolower($file->extension()); + $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file'; + $fileInfo['suffix'] = $suffix; + $fileInfo['type'] = $file->getMime(); + $fileInfo['size'] = $file->getSize(); + $fileInfo['name'] = $file->getOriginalName(); + $fileInfo['sha1'] = $file->sha1(); + + $this->file = $file; + $this->fileInfo = $fileInfo; + return $this; + } + + /** + * 设置上传驱动 + */ + public function setDriver(string $driver): Upload + { + $this->driver['name'] = $driver; + return $this; + } + + /** + * 获取上传驱动句柄 + * @param ?string $driver 驱动名称 + * @param bool $noDriveException 找不到驱动是否抛出异常 + * @return bool|Driver + */ + public function getDriver(?string $driver = null, bool $noDriveException = true): bool|Driver + { + if (is_null($driver)) { + $driver = $this->driver['name']; + } + if (!isset($this->driver['handler'][$driver])) { + $class = $this->resolveDriverClass($driver); + if ($class) { + $this->driver['handler'][$driver] = new $class(); + } elseif ($noDriveException) { + throw new InvalidArgumentException(__('Driver %s not supported', [$driver])); + } + } + return $this->driver['handler'][$driver] ?? false; + } + + /** + * 获取驱动类 + */ + protected function resolveDriverClass(string $driver): bool|string + { + if ($this->driver['namespace'] || str_contains($driver, '\\')) { + $class = str_contains($driver, '\\') ? $driver : $this->driver['namespace'] . Str::studly($driver); + if (class_exists($class)) { + return $class; + } + } + return false; + } + + /** + * 设置存储子目录 + */ + public function setTopic(string $topic): Upload + { + $this->topic = $topic; + return $this; + } + + /** + * 检查是否是图片并设置好相关属性 + * @return bool + * @throws Throwable + */ + protected function checkIsImage(): bool + { + if (in_array($this->fileInfo['type'], ['image/gif', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/png', 'image/webp']) || in_array($this->fileInfo['suffix'], ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp'])) { + $imgInfo = getimagesize($this->file->getPathname()); + if (!$imgInfo || !isset($imgInfo[0]) || !isset($imgInfo[1])) { + throw new Exception(__('The uploaded image file is not a valid image')); + } + $this->fileInfo['width'] = $imgInfo[0]; + $this->fileInfo['height'] = $imgInfo[1]; + $this->isImage = true; + return true; + } + return false; + } + + /** + * 上传的文件是否为图片 + * @return bool + */ + public function isImage(): bool + { + return $this->isImage; + } + + /** + * 获取文件后缀 + * @return string + */ + public function getSuffix(): string + { + return $this->fileInfo['suffix'] ?: 'file'; + } + + /** + * 获取文件保存路径和名称 + * @param ?string $saveName + * @param ?string $filename + * @param ?string $sha1 + * @return string + */ + public function getSaveName(?string $saveName = null, ?string $filename = null, ?string $sha1 = null): string + { + if ($filename) { + $suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file'; + } else { + $suffix = $this->fileInfo['suffix']; + } + $filename = $filename ?: $this->fileInfo['name']; + $sha1 = $sha1 ?: $this->fileInfo['sha1']; + $replaceArr = [ + '{topic}' => $this->topic, + '{year}' => date("Y"), + '{mon}' => date("m"), + '{day}' => date("d"), + '{hour}' => date("H"), + '{min}' => date("i"), + '{sec}' => date("s"), + '{random}' => Random::build(), + '{random32}' => Random::build('alnum', 32), + '{fileName}' => $this->getFileNameSubstr($filename, $suffix), + '{suffix}' => $suffix, + '{.suffix}' => $suffix ? '.' . $suffix : '', + '{fileSha1}' => $sha1, + ]; + $saveName = $saveName ?: $this->config['save_name']; + return Filesystem::fsFit(str_replace(array_keys($replaceArr), array_values($replaceArr), $saveName)); + } + + /** + * 验证文件是否符合上传配置要求 + * @throws Throwable + */ + public function validates(): void + { + if (empty($this->file)) { + throw new Exception(__('No files have been uploaded or the file size exceeds the upload limit of the server')); + } + + $size = Filesystem::fileUnitToByte($this->config['max_size']); + $mime = $this->checkConfig($this->config['allowed_mime_types']); + $suffix = $this->checkConfig($this->config['allowed_suffixes']); + + // 文件大小 + $fileValidateRule = ValidateRule::fileSize($size, __('The uploaded file is too large (%sMiB), Maximum file size:%sMiB', [ + round($this->fileInfo['size'] / pow(1024, 2), 2), + round($size / pow(1024, 2), 2) + ])); + + // 文件后缀 + if ($suffix) { + $fileValidateRule->fileExt($suffix, __('The uploaded file format is not allowed')); + } + // 文件 MIME 类型 + if ($mime) { + $fileValidateRule->fileMime($mime, __('The uploaded file format is not allowed')); + } + + // 图片文件利用tp内置规则做一些额外检查 + if ($this->checkIsImage()) { + $fileValidateRule->image("{$this->fileInfo['width']},{$this->fileInfo['height']}", __('The uploaded image file is not a valid image')); + } + + Validate::failException() + ->rule([ + 'file' => $fileValidateRule, + 'topic' => ValidateRule::is('alphaDash', __('Topic format error')), + 'driver' => ValidateRule::is('alphaDash', __('Driver %s not supported', [$this->driver['name']])), + ]) + ->check([ + 'file' => $this->file, + 'topic' => $this->topic, + 'driver' => $this->driver['name'], + ]); + } + + /** + * 上传文件 + * @param ?string $saveName + * @param int $adminId + * @param int $userId + * @return array + * @throws Throwable + */ + public function upload(?string $saveName = null, int $adminId = 0, int $userId = 0): array + { + $this->validates(); + + $driver = $this->getDriver(); + $saveName = $saveName ?: $this->getSaveName(); + $params = [ + 'topic' => $this->topic, + 'admin_id' => $adminId, + 'user_id' => $userId, + 'url' => $driver->url($saveName, false), + 'width' => $this->fileInfo['width'] ?? 0, + 'height' => $this->fileInfo['height'] ?? 0, + 'name' => $this->getFileNameSubstr($this->fileInfo['name'], $this->fileInfo['suffix'], 100) . ".{$this->fileInfo['suffix']}", + 'size' => $this->fileInfo['size'], + 'mimetype' => $this->fileInfo['type'], + 'storage' => $this->driver['name'], + 'sha1' => $this->fileInfo['sha1'] + ]; + + // 附件数据入库 - 不依赖模型新增前事件,确保入库前文件已经移动完成 + $attachment = Attachment::where('sha1', $params['sha1']) + ->where('topic', $params['topic']) + ->where('storage', $params['storage']) + ->find(); + if ($attachment && $driver->exists($attachment->url)) { + $attachment->quote++; + $attachment->last_upload_time = time(); + } else { + $driver->save($this->file, $saveName); + $attachment = new Attachment(); + $attachment->data(array_filter($params)); + } + $attachment->save(); + return $attachment->toArray(); + } + + /** + * 获取文件名称字符串的子串 + */ + public function getFileNameSubstr(string $fileName, string $suffix, int $length = 15): string + { + // 对 $fileName 中不利于传输的字符串进行过滤 + $pattern = "/[\s:@#?&\/=',+]+/u"; + $fileName = str_replace(".$suffix", '', $fileName); + $fileName = preg_replace($pattern, '', $fileName); + return mb_substr(htmlspecialchars(strip_tags($fileName)), 0, $length); + } + + /** + * 检查配置项,将 string 类型的配置转换为 array,并且将所有字母转换为小写 + */ + protected function checkConfig($configItem): array + { + if (is_array($configItem)) { + return array_map('strtolower', $configItem); + } else { + return explode(',', strtolower($configItem)); + } + } +} diff --git a/app/common/library/token/Driver.php b/app/common/library/token/Driver.php new file mode 100644 index 0000000..e694dab --- /dev/null +++ b/app/common/library/token/Driver.php @@ -0,0 +1,92 @@ +handler; + } + + /** + * @param string $token + * @return string + */ + protected function getEncryptedToken(string $token): string + { + $config = Config::get('buildadmin.token'); + return hash_hmac($config['algo'], $token, $config['key']); + } + + /** + * @param int $expireTime + * @return int + */ + protected function getExpiredIn(int $expireTime): int + { + return $expireTime ? max(0, $expireTime - time()) : 365 * 86400; + } +} \ No newline at end of file diff --git a/app/common/library/token/TokenExpirationException.php b/app/common/library/token/TokenExpirationException.php new file mode 100644 index 0000000..ce91e6d --- /dev/null +++ b/app/common/library/token/TokenExpirationException.php @@ -0,0 +1,16 @@ +options = array_merge($this->options, $options); + } + + if ($this->options['name']) { + $this->handler = Db::connect($this->options['name'])->name($this->options['table']); + } else { + $this->handler = Db::name($this->options['table']); + } + } + + /** + * @throws Throwable + */ + public function set(string $token, string $type, int $userId, int $expire = null): bool + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + $expireTime = $expire !== 0 ? time() + $expire : 0; + $token = $this->getEncryptedToken($token); + $this->handler->insert([ + 'token' => $token, + 'type' => $type, + 'user_id' => $userId, + 'create_time' => time(), + 'expire_time' => $expireTime, + ]); + + // 每隔48小时清理一次过期Token + $time = time(); + $lastCacheCleanupTime = Cache::get('last_cache_cleanup_time'); + if (!$lastCacheCleanupTime || $lastCacheCleanupTime < $time - 172800) { + Cache::set('last_cache_cleanup_time', $time); + $this->handler->where('expire_time', '<', time())->where('expire_time', '>', 0)->delete(); + } + return true; + } + + /** + * @throws Throwable + */ + public function get(string $token): array + { + $data = $this->handler->where('token', $this->getEncryptedToken($token))->find(); + if (!$data) { + return []; + } + + $data['token'] = $token; // 返回未加密的token给客户端使用 + $data['expires_in'] = $this->getExpiredIn($data['expire_time'] ?? 0); // 返回剩余有效时间 + return $data; + } + + /** + * @throws Throwable + */ + public function check(string $token, string $type, int $userId): bool + { + $data = $this->get($token); + if (!$data || ($data['expire_time'] && $data['expire_time'] <= time())) return false; + return $data['type'] == $type && $data['user_id'] == $userId; + } + + /** + * @throws Throwable + */ + public function delete(string $token): bool + { + $this->handler->where('token', $this->getEncryptedToken($token))->delete(); + return true; + } + + /** + * @throws Throwable + */ + public function clear(string $type, int $userId): bool + { + $this->handler->where('type', $type)->where('user_id', $userId)->delete(); + return true; + } +} \ No newline at end of file diff --git a/app/common/library/token/driver/Redis.php b/app/common/library/token/driver/Redis.php new file mode 100644 index 0000000..c1a14d3 --- /dev/null +++ b/app/common/library/token/driver/Redis.php @@ -0,0 +1,146 @@ +options = array_merge($this->options, $options); + } + $this->handler = new \Redis(); + if ($this->options['persistent']) { + $this->handler->pconnect($this->options['host'], $this->options['port'], $this->options['timeout'], 'persistent_id_' . $this->options['select']); + } else { + $this->handler->connect($this->options['host'], $this->options['port'], $this->options['timeout']); + } + + if ('' != $this->options['password']) { + $this->handler->auth($this->options['password']); + } + + if (false !== $this->options['select']) { + $this->handler->select($this->options['select']); + } + } + + /** + * @throws Throwable + */ + public function set(string $token, string $type, int $userId, int $expire = null): bool + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + $expireTime = $expire !== 0 ? time() + $expire : 0; + $token = $this->getEncryptedToken($token); + $tokenInfo = [ + 'token' => $token, + 'type' => $type, + 'user_id' => $userId, + 'create_time' => time(), + 'expire_time' => $expireTime, + ]; + $tokenInfo = json_encode($tokenInfo, JSON_UNESCAPED_UNICODE); + if ($expire) { + $expire += $this->expiredHold; + $result = $this->handler->setex($token, $expire, $tokenInfo); + } else { + $result = $this->handler->set($token, $tokenInfo); + } + $this->handler->sAdd($this->getUserKey($type, $userId), $token); + return $result; + } + + /** + * @throws Throwable + */ + public function get(string $token): array + { + $key = $this->getEncryptedToken($token); + $data = $this->handler->get($key); + if (is_null($data) || false === $data) { + return []; + } + $data = json_decode($data, true); + + $data['token'] = $token; // 返回未加密的token给客户端使用 + $data['expires_in'] = $this->getExpiredIn($data['expire_time'] ?? 0); // 过期时间 + return $data; + } + + /** + * @throws Throwable + */ + public function check(string $token, string $type, int $userId): bool + { + $data = $this->get($token); + if (!$data || ($data['expire_time'] && $data['expire_time'] <= time())) return false; + return $data['type'] == $type && $data['user_id'] == $userId; + } + + /** + * @throws Throwable + */ + public function delete(string $token): bool + { + $data = $this->get($token); + if ($data) { + $key = $this->getEncryptedToken($token); + $this->handler->del($key); + $this->handler->sRem($this->getUserKey($data['type'], $data['user_id']), $key); + } + return true; + } + + /** + * @throws Throwable + */ + public function clear(string $type, int $userId): bool + { + $userKey = $this->getUserKey($type, $userId); + $keys = $this->handler->sMembers($userKey); + $this->handler->del($userKey); + $this->handler->del($keys); + return true; + } + + /** + * 获取会员的key + * @param $type + * @param $userId + * @return string + */ + protected function getUserKey($type, $userId): string + { + return $this->options['prefix'] . $type . '-' . $userId; + } +} \ No newline at end of file diff --git a/app/common/library/upload/Driver.php b/app/common/library/upload/Driver.php new file mode 100644 index 0000000..f0a8a3e --- /dev/null +++ b/app/common/library/upload/Driver.php @@ -0,0 +1,47 @@ +options = Config::get('filesystem.disks.public'); + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 保存文件 + * @param UploadedFile $file + * @param string $saveName + * @return bool + */ + public function save(UploadedFile $file, string $saveName): bool + { + $savePathInfo = pathinfo($saveName); + $saveFullPath = $this->getFullPath($saveName); + + // cgi 直接 move + if (request()->isCgi()) { + $file->move($saveFullPath, $savePathInfo['basename']); + return true; + } + + set_error_handler(function ($type, $msg) use (&$error) { + $error = $msg; + }); + + // 建立文件夹 + if (!is_dir($saveFullPath) && !mkdir($saveFullPath, 0755, true)) { + restore_error_handler(); + throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $saveFullPath, strip_tags($error))); + } + + // cli 使用 rename + $saveName = $this->getFullPath($saveName, true); + if (!rename($file->getPathname(), $saveName)) { + restore_error_handler(); + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $file->getPathname(), $saveName, strip_tags($error))); + } + + restore_error_handler(); + @chmod($saveName, 0666 & ~umask()); + return true; + } + + /** + * 删除文件 + * @param string $saveName + * @return bool + */ + public function delete(string $saveName): bool + { + $saveFullName = $this->getFullPath($saveName, true); + if ($this->exists($saveFullName)) { + @unlink($saveFullName); + } + Filesystem::delEmptyDir(dirname($saveFullName)); + return true; + } + + /** + * 获取资源 URL 地址 + * @param string $saveName 资源保存名称 + * @param string|bool $domain 是否携带域名 或者直接传入域名 + * @param string $default 默认值 + * @return string + */ + public function url(string $saveName, string|bool $domain = true, string $default = ''): string + { + $saveName = $this->clearRootPath($saveName); + + if ($domain === true) { + $domain = '//' . request()->host(); + } elseif ($domain === false) { + $domain = ''; + } + + $saveName = $saveName ?: $default; + if (!$saveName) return $domain; + + $regex = "/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i"; + if (preg_match('/^http(s)?:\/\//', $saveName) || preg_match($regex, $saveName) || $domain === false) { + return $saveName; + } + return str_replace('\\', '/', $domain . $saveName); + } + + /** + * 文件是否存在 + * @param string $saveName + * @return bool + */ + public function exists(string $saveName): bool + { + $saveFullName = $this->getFullPath($saveName, true); + return file_exists($saveFullName); + } + + /** + * 获取文件的完整存储路径 + * @param string $saveName + * @param bool $baseName 是否包含文件名 + * @return string + */ + public function getFullPath(string $saveName, bool $baseName = false): string + { + $savePathInfo = pathinfo($saveName); + $root = $this->getRootPath(); + $dirName = $savePathInfo['dirname'] . '/'; + + // 以 root 路径开始时单独返回,避免重复调用此方法时造成 $dirName 的错误拼接 + if (str_starts_with($saveName, $root)) { + return Filesystem::fsFit($baseName || !isset($savePathInfo['extension']) ? $saveName : $dirName); + } + + return Filesystem::fsFit($root . $dirName . ($baseName ? $savePathInfo['basename'] : '')); + } + + public function clearRootPath(string $saveName): string + { + return str_replace($this->getRootPath(), '', Filesystem::fsFit($saveName)); + } + + public function getRootPath(): string + { + return Filesystem::fsFit(str_replace($this->options['url'], '', $this->options['root'])); + } +} \ No newline at end of file diff --git a/app/common/model/Attachment.php b/app/common/model/Attachment.php new file mode 100644 index 0000000..cc8a461 --- /dev/null +++ b/app/common/model/Attachment.php @@ -0,0 +1,115 @@ +getDriver($row['storage'], false); + return $driver ? $driver->url($row['url']) : full_url($row['url']); + } + + /** + * 新增前 + * @throws Throwable + */ + protected static function onBeforeInsert($model): bool + { + $repeat = $model->where([ + ['sha1', '=', $model->sha1], + ['topic', '=', $model->topic], + ['storage', '=', $model->storage], + ])->find(); + if ($repeat) { + $driver = self::$upload->getDriver($repeat->storage, false); + if ($driver && !$driver->exists($repeat->url)) { + $repeat->delete(); + return true; + } else { + $repeat->quote++; + $repeat->last_upload_time = time(); + $repeat->save(); + return false; + } + } + return true; + } + + /** + * 新增后 + */ + protected static function onAfterInsert($model): void + { + Event::trigger('AttachmentInsert', $model); + + if (!$model->last_upload_time) { + $model->quote = 1; + $model->last_upload_time = time(); + $model->save(); + } + } + + /** + * 删除后 + */ + protected static function onAfterDelete($model): void + { + Event::trigger('AttachmentDel', $model); + + $driver = self::$upload->getDriver($model->storage, false); + if ($driver && $driver->exists($model->url)) { + $driver->delete($model->url); + } + } + + public function admin(): BelongsTo + { + return $this->belongsTo(Admin::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} \ No newline at end of file diff --git a/app/common/model/BaseModel.php b/app/common/model/BaseModel.php new file mode 100644 index 0000000..5c5fb98 --- /dev/null +++ b/app/common/model/BaseModel.php @@ -0,0 +1,150 @@ +getTable(), $where, false); + } + + + + /** + * 写入数据前处理数据库连接 + * @param array $data 写入的数据 + * @return void + */ + protected static function onBeforeInsert($data) + { + $connection = DatabaseRoute::getConnection($data->getTable(), $data, true); + print_r($connection); + $data->setConnection($connection); + } + + /** + * 更新数据前处理数据库连接 + * @param array $data 更新的数据 + * @return void + */ + protected static function onBeforeUpdate($data) + { + $connection = DatabaseRoute::getConnection($data->getTable(), $data->getWhere(), true); + $data->setConnection($connection); + } + + + /** + * 从查询选项中提取分库键的值(深度解析版) + */ + private static function extractKeyFromOptions(array $options, string $keyField) + { + $dbKey = null; + + // 1. 处理顶级 where 条件 + if (isset($options['where'])) { + $dbKey = static::parseWhereCondition($options['where'], $keyField); + if ($dbKey !== null) return $dbKey; + } + + // 2. 处理 whereOr 条件 + if (isset($options['whereOr'])) { + $dbKey = static::parseWhereCondition($options['whereOr'], $keyField); + if ($dbKey !== null) return $dbKey; + } + + // 3. 处理软删除条件 + if (isset($options['soft_delete']) && is_array($options['soft_delete'])) { + if ($options['soft_delete'][0] === $keyField) { + $dbKey = $options['soft_delete'][2] ?? null; + Log::info("条件类型3: 软删除条件中匹配到 {$keyField}={$dbKey}"); + return $dbKey; + } + } + + // 4. 处理字段绑定(bind) + if (isset($options['bind'][$keyField])) { + $dbKey = $options['bind'][$keyField]; + Log::info("条件类型4: 绑定参数中匹配到 {$keyField}={$dbKey}"); + return $dbKey; + } + + // 5. 处理查询参数(param) + if (isset($options['param'][$keyField])) { + $dbKey = $options['param'][$keyField]; + Log::info("条件类型5: 查询参数中匹配到 {$keyField}={$dbKey}"); + return $dbKey; + } + + // 6. 处理数据(data)- 适用于更新操作 + if (isset($options['data'][$keyField])) { + $dbKey = $options['data'][$keyField]; + Log::info("条件类型6: 数据中匹配到 {$keyField}={$dbKey}"); + return $dbKey; + } + + // 未找到分库键 + Log::warning("条件解析失败: 未找到 {$keyField} 的值"); + return null; + } + + /** + * 解析 where 条件(处理 AND/OR 逻辑) + */ + private static function parseWhereCondition($where, string $keyField) + { + $dbKey = null; + + // 处理数组形式的 where 条件 + if (is_array($where)) { + foreach ($where as $index => $whereItem) { + // 1. 处理 AND/OR 逻辑分组 + if (is_string($index) && in_array(strtoupper($index), ['AND', 'OR', 'NOT'])) { + Log::info("条件类型1: 发现 {$index} 逻辑分组,开始递归解析"); + + // 递归解析逻辑分组中的条件 + $dbKey = static::parseWhereCondition($whereItem, $keyField); + if ($dbKey !== null) { + Log::info("条件类型1: {$index} 分组中匹配到 {$keyField}={$dbKey}"); + return $dbKey; + } + } // 2. 处理 ['user_id', '=', 123] 形式 + elseif (is_array($whereItem) && count($whereItem) >= 3) { + if ($whereItem[0] === $keyField) { + $dbKey = $whereItem[2]; + Log::info("条件类型2: 直接匹配 {$keyField}={$dbKey}"); + return $dbKey; + } + } // 3. 处理闭包条件: ['user_id', function($query){...}] + elseif (is_array($whereItem) && isset($whereItem[1]) && $whereItem[1] instanceof \Closure) { + Log::info("条件类型3: 发现闭包条件,开始递归解析"); + + // 创建临时查询对象执行闭包 + $subQuery = new Query(); + $whereItem[1]($subQuery); + + // 递归解析闭包中的条件 + $dbKey = static::extractKeyFromOptions($subQuery->getOptions(), $keyField); + if ($dbKey !== null) { + Log::info("条件类型3: 闭包中匹配到 {$keyField}={$dbKey}"); + return $dbKey; + } + } + } + return null; + } + } + +} \ No newline at end of file diff --git a/app/common/model/Common.php b/app/common/model/Common.php new file mode 100644 index 0000000..796c8f6 --- /dev/null +++ b/app/common/model/Common.php @@ -0,0 +1,125 @@ +name($table)->insertGetId($data); + }elseif($operate == 'update') { + return $db->name($table)->where($where)->update($data); + }elseif ($operate == 'select') { + return $db->name($table)->where($where)->select(); + }elseif ($operate == 'find') { + return $db->name($table)->where($where)->find(); + } + } + + public static function db_count($table, $sale, $where) + { + $connect_name = DatabaseRoute::getConnection($table, $sale); + $db = Db::connect($connect_name); + return $db->name($table)->where($where)->count(); + + } + + public static function getAppUseKv() + { + $info = Db::connect(config('database.search_library'))->name('common_info')->where(['is_app_use' => 1])->field('id, value')->select(); + $data = []; + foreach ($info as $k => $v) { + $data[$v['id']] = $v['value']; + } + return returnSuccessData($data); + } + + + + /** + * 检查是否允许访问(基于Redis的频率限制) + * @param string $id 唯一标识(如用户ID、IP等) + * @param string $key 操作类型(如"updateWeekCourseView") + * @param int $count 允许的访问次数 + * @param int $seconds 时间窗口(秒) + * @return bool 是否允许访问 + */ + public static function isAccessAllowed($id, $key, $count, $seconds, $sys_data = false) + { + if($sys_data) { + $redisKey = 'sys:data:' . $key . ':' . $id; + }else { + $redisKey = generateRedisKey($key, $id); + } + // 获取当前访问次数 + $currentCount = Cache::get($redisKey); + + if ($currentCount === null) { + // 首次访问:初始化计数器并设置过期时间 + Cache::set($redisKey, $seconds, 1); + return true; + } + if ((int)$currentCount < $count) { + // 未超过限制:增加计数 + Cache::set($redisKey,$currentCount + 1); + return true; + } + // 已超过限制 + return false; + } + + + + + + /** + * @param int $courseId 课程ID + * @return int 周播放量 + */ + public static function getCourseWeekViewCount($courseId) + { + $key = "course:viewCount:{$courseId}"; + // 从Redis获取周播放量 + $viewCount = Cache::get($key); + if (empty($viewCount)) { + // 计算下周一的时间戳 + $now = time(); + $dayOfWeek = date('N', $now); // 1-7,1表示周一 + $daysToMonday = $dayOfWeek === 1 ? 7 : (1 - $dayOfWeek + 7) % 7; + $nextMonday = $now + $daysToMonday * 86400; + + // 计算剩余秒数并设置缓存 + $seconds = $nextMonday - $now; + Cache::set($key, 1, $seconds); + return 1; + } + // 播放量递增并返回 + $newCount = Cache::set($key, $viewCount + 1, -1); + return $newCount; + } + + + + + + +} \ No newline at end of file diff --git a/app/common/model/Config.php b/app/common/model/Config.php new file mode 100644 index 0000000..a9a8682 --- /dev/null +++ b/app/common/model/Config.php @@ -0,0 +1,83 @@ +find()) return false; + return self::removeArrayItem('config_group', $key); + } + + /** + * 添加系统快捷配置入口 + * @throws Throwable + */ + public static function addQuickEntrance(string $key, string $value): bool + { + return self::addArrayItem('config_quick_entrance', $key, $value); + } + + /** + * 删除系统快捷配置入口 + * @throws Throwable + */ + public static function removeQuickEntrance(string $key): bool + { + return self::removeArrayItem('config_quick_entrance', $key); + } + + /** + * 为Array类型的配置项添加元素 + * @throws Throwable + */ + public static function addArrayItem(string $name, string $key, string $value): bool + { + $configRow = adminConfigModel::where('name', $name)->find(); + foreach ($configRow->value as $item) { + if ($item['key'] == $key) { + return false; + } + } + $configRow->value = array_merge($configRow->value, [['key' => $key, 'value' => $value]]); + $configRow->save(); + return true; + } + + /** + * 删除Array类型配置项的一个元素 + * @throws Throwable + */ + public static function removeArrayItem(string $name, string $key): bool + { + $configRow = adminConfigModel::where('name', $name)->find(); + $configRowValue = $configRow->value; + foreach ($configRowValue as $iKey => $item) { + if ($item['key'] == $key) { + unset($configRowValue[$iKey]); + } + } + $configRow->value = $configRowValue; + $configRow->save(); + return true; + } + +} \ No newline at end of file diff --git a/app/common/model/CourseCollect.php b/app/common/model/CourseCollect.php new file mode 100644 index 0000000..5899ab5 --- /dev/null +++ b/app/common/model/CourseCollect.php @@ -0,0 +1,14 @@ + rand(000000, 999999), + 'username' => '哎呀' . rand(000000, 999999), + ]); + } + + // 查询username + public static function GetByusername($username) + { + // 全表扫描username + $dbmap = config('database.db_map'); + foreach ($dbmap as $dbname) { + if(!in_array($dbname, config('database.unset_db_map'))) { + $connect = Db::connect($dbname); + $data = $connect->name('sys_user')->where(['username' => $username])->find(); + if($data) { + // 如果查到直接跳出循环并保存缓存 + Cache::set('admin_info_' . $username, $data['user_id']); + break; + } + } + } + return $data; + } + + + public static function GetByQrcode($qd_rcode) + { + // 全表扫描username + return DatabaseRoute::getAllDbData('sys_user', function ($query) use ($qd_rcode) { + return $query->where([ + 'qd_code' => $qd_rcode + ]); + })->find(); + } + + public static function updateSysMoney($userId, $money, $type) + { + $count = Db::name('sys_user_money')->where([ + 'user_id' => $userId + ])->count(); + if (!$count) { + Db::name('sys_user_money')->where([ + 'user_id' => $userId + ])->insert([ + 'id' => Random::generateRandomPrefixedId(19), + 'user_id' => $userId, + 'money' => 0 + ]); + } + $model = Db::name('sys_user_money'); + if ($type) { + $model->inc('money', floatval($money))->where([ + 'user_id' => $userId + ])->update(); + }else{ + $model->dec('money', floatval($money))->where([ + 'user_id' => $userId + ])->update(); + } + } + + +} \ No newline at end of file diff --git a/app/common/model/User.php b/app/common/model/User.php new file mode 100644 index 0000000..09d7f28 --- /dev/null +++ b/app/common/model/User.php @@ -0,0 +1,51 @@ +where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']); + } + + public function getMoneyAttr($value): string + { + return bcdiv($value, 100, 2); + } + + /** + * 用户的余额是不可以直接进行修改的,请通过 UserMoneyLog 模型插入记录来实现自动修改余额 + * 此处定义上 money 的修改器仅为防止直接对余额的修改造成数据错乱 + */ + public function setMoneyAttr($value): string + { + return bcmul($value, 100, 2); + } +} \ No newline at end of file diff --git a/app/common/model/UserMoneyLog.php b/app/common/model/UserMoneyLog.php new file mode 100644 index 0000000..c465e71 --- /dev/null +++ b/app/common/model/UserMoneyLog.php @@ -0,0 +1,41 @@ +find(); - try { - if(empty($get['courseId'])) { - return json('参数不完整'); - } - $courseId = $get['courseId']; - // 获取短剧详情 - $dd_b = Db::connect('duanju_slave'); - $db_name = $dd_b->name('course'); - $bean = $db_name->where(['course_id' => $courseId])->find(); - if(!$bean) { - return json('短剧不存在'); - } - - - $courseCollect = DatabaseRoute::getDb('course_collect', $user_id) - ->where(['course_id' => $course_id]) - ->where(['user_id' => $user_id]) - ->where(['classify' => 3]) - ->limit(1) - ->find(); - - - - // 是否追剧 - $collect = DatabaseRoute::getDb('course_collect', $user_id) - ->where(['course_id' => $course_id]) - ->where(['classify' => 1]) - ->limit(1) - ->find(); - - - - - - $db = Db::connect(config('think-orm.search_library')); - $userVip = $db->name('user_vip')->where(['user_id' => $user['user_id']])->find(); - - if ($userVip) { - $user['member'] = $userVip['is_vip']; - $user['end_time'] = $userVip['end_time']; - } - - - - - $userInfo = $user; - - - - - - - if (!empty($userInfo['member']) && $userInfo['member'] == 2) { - $isVip = true; - }else{ - $isVip = false; - } - - - - - - - // 查询用户是否购买了整集 - $courseUser = DatabaseRoute::getDb('course_user', $user_id) - ->where(['course_id' => $course_id]) - ->where(['classify' => 1]) - ->find(); - - - - // 每天购买超过上限,获得免费时间段资格 - $freeWatch = Test::checkFreeWatchPayCount($user['user_id']); - - $startSort = 0; - $endSort = 5; - $dn_course_details = DatabaseRoute::getDb('course_details', ['course_id' => $courseId]); - $sort = null; - if (is_null($sort)) { - - if (!empty($courseCollect)) { - $courseDetails = $dn_course_details->field('sort') - ->where('course_details_id', $courseCollect['course_details_id']) - ->limit(1) - ->find(); - $sort = $courseDetails['sort']; - } - } - - if ($freeWatch || !empty($courseUser)) { - $courseDetailsSetVos = Test::courseSets($courseId, 2, null); - } else { - $courseDetailsSetVos = Test::courseSets($courseId, 1, $bean['wholesale_price']); - } - - // 调整集数范围 - if (!is_null($sort) && $sort > 2) { - $startSort = $sort - 3; - $endSort = $sort + 3; - if (count($courseDetailsSetVos) < $endSort) { - $startSort = count($courseDetailsSetVos) - 5; - $endSort = count($courseDetailsSetVos) + 1; - } - } - - // 已购买剧集ID集合 - $detailsId = []; - if (!$freeWatch) { - $det_db = Db::connect(DatabaseRoute::getConnection('course_user', ['user_id' => $user['user_id']])); - $detailsId = $det_db->name('course_user')->where(['course_id' => $courseId, 'classify' => 2])->column('course_details_id'); - $det_db->close(); - $detailsId = array_flip(array_flip($detailsId)); // 去重 - \support\Log::info('啦啦啦' . date('Y-m-d H:i:s')); - } - // 处理剧集列表 - $current = null; - foreach ($courseDetailsSetVos as &$s) { - $s['wholesalePrice'] = (int) $s['wholesalePrice']; - // 当前播放集 - if (!empty($courseCollect) && $s['courseDetailsId'] == $courseCollect['course_details_id']) { - $s['current'] = 1; - $current = &$s; - } - - // 非免费用户的权限控制 - if ( - !$freeWatch && - $s['sort'] > 3 && - (empty($detailsId) || !in_array($s['courseDetailsId'], $detailsId)) && - empty($courseUser) && - !$isVip - ) { - $s['videoUrl'] = null; - } - - // 检查是否已点赞 - if ($s['sort'] > $startSort && $s['sort'] < $endSort) { - $isGood_db = Db::connect(DatabaseRoute::getConnection('course_collect', ['user_id' => $user['user_id']])); - $isGood = $isGood_db->name('course_collect') - ->where('course_details_id', $s['courseDetailsId']) - ->where('classify', 2) - ->limit(1) - ->count(); - $s['isGood'] = empty($isGood) || $isGood == 0 ? 0 : 1; - } - } - - // 如果没有当前播放集,默认第一集 - if (empty($current) && !empty($courseDetailsSetVos)) { - $courseDetailsSetVos[0]['current'] = 1; - $current = &$courseDetailsSetVos[0]; - } - Test::setCourseView($bean); - - $price = ($freeWatch ? 0 : ($bean['price'] ?? 0)); - $price = bccomp($price, '0', 2) <= 0 ? 0 : $price; - // 返回结果 - $map = [ - 'current' => $current, - 'price' => $price, - 'title' => $bean['title'], - 'collect' => empty($collect) || $collect == 0 ? 0 : 1, - 'list' => $courseDetailsSetVos - ]; - \support\Log::info('即将返回' . date('Y-m-d H:i:s')); - return json($map); - } catch (\Exception $e) { - return json($e->getMessage()); - } } public function view(Request $request) { - return view('index/view', ['name' => 'webman']); } public function json(Request $request) { - return json(['code' => 0, 'msg' => 'ok']); } } diff --git a/app/czg/app/controller/CourseController.php b/app/czg/app/controller/CourseController.php new file mode 100644 index 0000000..af47a0f --- /dev/null +++ b/app/czg/app/controller/CourseController.php @@ -0,0 +1,22 @@ +request->get(); + $res = Course::courseSets($get, $this->auth->getUser()); + return $this->ApiDataReturn($res); + } + +} diff --git a/app/czg/app/model/AlibabaSms.php b/app/czg/app/model/AlibabaSms.php new file mode 100644 index 0000000..3ac75b9 --- /dev/null +++ b/app/czg/app/model/AlibabaSms.php @@ -0,0 +1,93 @@ + $accessKeyId, + "accessKeySecret" => $accessKeySecret + ]); + $config->endpoint = "dysmsapi.aliyuncs.com"; + return new Dysmsapi($config); + } + + /** + * @param array $args + * @return void + */ + public static function main($args, $access_key_id, $access_key_secret){ + + $client = self::createClient($access_key_id, $access_key_secret); + $sendSmsRequest = new SendSmsRequest($args); + $runtime = new RuntimeOptions([]); + try { + // 复制代码运行请自行打印 API 的返回值 + $res = $client->sendSmsWithOptions($sendSmsRequest, $runtime); + if($res->body->code == 'OK') { + return true; + }else { + return false; + } + } + catch (Exception $error) { + if (!($error instanceof TeaError)) { + $error = new TeaError([], $error->getMessage(), $error->getCode(), $error); + } + // 如有需要,请打印 error + Utils::assertAsString($error->message); + Log::write('短信发送错误--' . $error->message); + return false; + } + } + + + public static function sms($mobile, $event, $code = null) + { + $code = $code?:Random::numeric(config('captcha.length')); + $time = time(); + $ip = request()->ip(); + $sms = Sms::create(['event' => $event, 'mobile' => $mobile, 'code' => $code, 'ip' => $ip, 'createtime' => $time]); + if (!$sms) { + return false; + } + $ret = AlibabaSms::main([ + 'templateCode' => config('alibaba.registerCode'), + 'templateParam' => json_encode(['code' => $code]), + 'phoneNumbers' => $mobile, + 'signName' => config('alibaba.sign'), + ]); + if($ret) { + return true; + }else { + $sms->delete(); + return false; + } + } + +} + + + diff --git a/app/czg/app/model/Announcement.php b/app/czg/app/model/Announcement.php new file mode 100644 index 0000000..682902a --- /dev/null +++ b/app/czg/app/model/Announcement.php @@ -0,0 +1,21 @@ +name('announcement')->where(['type' => $type, 'state' => 1])->select()->toArray(); + $res_data = convertToCamelCase($res_data); + return returnSuccessData($res_data); + } + +} \ No newline at end of file diff --git a/app/czg/app/model/Banner.php b/app/czg/app/model/Banner.php new file mode 100644 index 0000000..593726f --- /dev/null +++ b/app/czg/app/model/Banner.php @@ -0,0 +1,13 @@ + $cashOut['user_id']]))->name('cash_out'); + // 构建查询条件 + if ($isApp) { + // APP端:只查询用户自己的提现记录(用户类型为1) + $query = $db_name_cash_out->where('user_id', $cashOut['user_id']) + ->where('user_type', 1); + } else { + // 管理端:根据用户ID或系统用户ID查询 + if (!empty($cashOut['user_id'])) { + $query = $db_name_cash_out->where('user_id', $cashOut['user_id']); + } else { + if (empty($cashOut['sys_user_id'])) { + // 无有效用户ID,返回空结果 + return returnErrorData('无效用户'); + } else { + $query = $db_name_cash_out->where('user_id', $cashOut['sys_user_id']) + ->where('user_type', 2); + } + } + } + $count = $query->count(); + // 执行分页查询 + $cashOutList = $query->limit(page($get['page'], $get['limit']), $get['limit'])->order('create_at', 'desc')->select()->toArray(); + if (!$isApp) { + // 管理端:补充用户信息和统计数据 + $userIdList = array_column($cashOutList, 'user_id'); + if (!empty($userIdList)) { + // 查询用户提现总数和总金额 + $cashoutSumMap = self::selectSumByUserIdList($userIdList, 1); + $cashoutVerifySumMap = self::selectSumByUserIdList($userIdList, 3); + // 查询用户名称 + $userinfoMap = $user; + + // 合并数据到结果集 + foreach ($cashOutList as &$item) { + $userId = $item['user_id']; + // 设置用户名 + $item['userName'] = $userinfoMap[$userId] ?? ''; + // 设置提现统计 + if (isset($cashoutSumMap[$userId])) { + $item['count'] = $cashoutSumMap[$userId]['count']; + $item['total'] = $cashoutSumMap[$userId]['total']; + } + // 设置审核通过的提现统计 + if (isset($cashoutVerifySumMap[$userId])) { + $item['verifyCount'] = $cashoutVerifySumMap[$userId]['count']; + $item['verifyTotal'] = $cashoutVerifySumMap[$userId]['total']; + } + } + unset($item); + } + } + + if ($isApp) { + // APP端:对敏感信息进行脱敏处理 + foreach ($cashOutList as &$item) { + if (!empty($item['bank_name'])) { + // 银行卡号脱敏 + $item['zhifubao'] = bankCard($item['zhifubao']); + } elseif (filter_var($item['zhifubao'], FILTER_VALIDATE_EMAIL)) { + // 邮箱脱敏 + $item['zhifubao'] = email($item['zhifubao']); + } elseif (preg_match('/^1[3-9]\d{9}$/', $item['zhifubao'])) { + // 手机号脱敏 + $item['zhifubao'] = maskPhoneNumber($item['zhifubao']); + } + } + unset($item); + } + + return returnSuccessData([ + 'currPage' => $get['page'], + 'pageSize' => $get['limit'], + 'list' => convertToCamelCase($cashOutList), + 'totalCount' => $count, + 'totalPage' => ceil($count / $get['limit']), + ]); + } + + + public static function selectSumByUserIdList($userIdList, $state) + { + + $result = DatabaseRoute::getAllDbData('cash_out', function($query) use ($userIdList, $state) { + return $query + ->field([ + 'user_id', + 'ROUND(SUM(money), 2) AS total', + 'COUNT(*) AS count' + ]) + ->where('state', $state) + ->whereIn('user_id', $userIdList) + ->group('user_id'); + })->select()->toArray(); + $resultMap = []; + foreach ($result as $item) { + $resultMap[$item['user_id']] = $item; + } + + return $resultMap; + } + + +} \ No newline at end of file diff --git a/app/czg/app/model/CommonInfo.php b/app/czg/app/model/CommonInfo.php new file mode 100644 index 0000000..e3c966f --- /dev/null +++ b/app/czg/app/model/CommonInfo.php @@ -0,0 +1,32 @@ +where([ + 'type' => $code + ])->find()->toArray(); + cache('common_info:'.$code, $val, 60 * 60 * 24); + return $val; + } + + public function getByCodeToInt(int $code) { + $val = $this->getByCode($code); + if (!$val || empty($val['value'])) { + throw new SysException('代码获取失败, code: {}', $code); + } + + return intval($val['value']); + } + +} \ No newline at end of file diff --git a/app/czg/app/model/Course.php b/app/czg/app/model/Course.php new file mode 100644 index 0000000..94992fc --- /dev/null +++ b/app/czg/app/model/Course.php @@ -0,0 +1,443 @@ +name('course'); + $db = $db->where(['status' => 1]); + $count = $db->count(); + $data_db = $data_db->limit($page, $data['limit']); + if(!empty($data['sort'])) { + if($data['sort'] == 1) { + $data_db = $data_db->order('week_pay','desc'); + }elseif ($data['sort'] == 2) { + $data_db = $data_db->order('week_view','desc'); + } + }else { + $data_db = $data_db->order('create_time','desc'); + } + $list = $data_db->select()->toArray(); + foreach ($list as $k => &$v) { + $v['course_id'] = (string)$v['course_id']; + // 如果没有剧集了,给下架 + $crous_detail_db_count = DatabaseRoute::getDb('course_details', ['course_id' => $v['course_id']])->where(['course_id' => $v['course_id']])->count(); + if(!$crous_detail_db_count) { + Course::where(['course_id' => $v['course_id']])->update(['status' => 2]); + } + } + + $return = [ + 'currPage' => 1, + 'list' => convertToCamelCase($list), + 'pageSize' => $data['limit'], + 'totalCount' => $count, + 'totalPage' => ceil($count / $data['limit']), + ]; + if($list) { + Cach::set('index_data_' . $data['page'], json_encode($return, true)); + } + return returnSuccessData($return); + } + + public static function selectByUserId($get, $user_id) + { + + if(empty($get['classify'])) { + return returnErrorData('参数不完整'); + } + $db = Db::connect(DatabaseRoute::getConnection('course_collect', ['user_id' => $user_id])); + if($get['classify'] == 1) { + // 追剧 + $result = $result2 = $db->name('course_collect') + ->alias('c1') + ->join('course_collect c2', 'c1.course_id = c2.course_id AND c2.user_id = ' . $user_id .' AND c2.classify = ' . $get['classify']) + ->where('c1.classify', 3) + ->where('c1.user_id', $user_id); + $count = $result2->count(); + $result = $result->field([ + 'c1.course_id' => 'courseId', + 'c1.course_details_id' => 'courseDetailsId', + 'c1.update_time' => 'updateTime' + ]) + ->limit(page($get['page'], $get['limit']), $get['limit']) + ->select(); + }elseif($get['classify'] == 2 || $get['classify'] == 3) { + // 点赞 观看历史 + $result = $result2 = $db->name('course_collect') + ->alias('c1') + ->where('c1.classify', $get['classify']) + ->where('c1.user_id', $user_id); + $count = $result2->count(); + $result = $result->field([ + 'c1.course_id' => 'courseId', + 'c1.course_details_id' => 'courseDetailsId', + 'c1.update_time' => 'updateTime' + ])->limit(page($get['page'], $get['limit']), $get['limit'])->select(); + } + $is_arr = self::sacouresdata($result); + $db = Db::connect(config('database.search_library')); + $course = $db->name('course')->whereIn('course_id', $is_arr['course_id'])->select(); + $course_arr = self::sacouresdata($result, $course); + // 拿详情 + $course_data = self::sacouresjidata($course_arr['course_list']); + return returnSuccessData([ + 'currPage' => $get['page'], + 'pageSize' => $get['limit'], + 'totalCount' => $count, + 'totalPage' => ceil($count / $get['limit']), + 'records' => $course_data, + ]); + + } + + + // 拿短剧详情 + public static function sacouresdata($result, $course = []) + { + $data['course_id'] = []; + $data['course_details_id'] = []; + $data['course_list'] = []; + foreach ($result as $k => $v) { + if(empty($course)) { + $data['course_id'][] = (string)$v['courseId']; + $data['course_details_id'][] = (string)$v['courseDetailsId']; + }else { + foreach ($course as $courseKey => $courseValue) { + if($courseValue['course_id'] == $v['courseId']) { + $data['course_list'][] = [ + 'courseId' => (string)$courseValue['course_id'], + 'courseDetailsId' => (string)$v['courseDetailsId'], + 'courseLabel' => $courseValue['course_label'], + 'courseLabelIds' => $courseValue['course_label_ids'], + 'courseType' => $courseValue['course_type'], + 'createTime' => $courseValue['create_time'], + 'payNum' => $courseValue['pay_num'], + 'title' => $courseValue['title'], + 'titleImg' => $courseValue['title_img'], + 'updateTime' => $courseValue['update_time'], + ]; + } + } + } + } + + return $data; + } + + // 拿剧集详情 + public static function sacouresjidata($result) + { + foreach ($result as $k => $v) { + $db_name = DatabaseRoute::getConnection('course_details', ['course_id' => $v['courseId']]); + $db = Db::connect($db_name); + $course_details = $db->name('course_details')->where(['course_details_id' => $v['courseDetailsId']])->find(); + $result[$k]['courseDetailsCount'] = $db->name('course_details')->where(['course_id' => $v['courseId']])->count(); + if($course_details) { + $result[$k]['courseDetailsName'] = $course_details['course_details_name']; + }else { + $result[$k]['courseDetailsName'] = ''; + } + $result[$k]['db_name'] = $db_name; + + } + return $result; + } + + public static function collectVideoSummary($user_id) + { + $db_name = DatabaseRoute::getDb('course_collect', $user_id)->where('user_id', $user_id) + ->field([ + 'COUNT(CASE WHEN classify = 1 THEN 1 END)' => 'collectCount', + 'COUNT(CASE WHEN classify = 2 THEN 1 END)' => 'likeCount' + ]) + ->find(); + return returnSuccessData($db_name); + } + + + /** + * 获取推荐视频 + */ + public static function selectCourseDetailsList($get, $user_id) + { + if(empty($get['page']) || empty($get['limit']) || empty($get['randomNum'])) { + return returnErrorData('参数不完整'); + } + $sale = config('database.db_map'); + foreach ($sale as $k => $v) { + if(in_array($v, config('database.unset_db_map'))) { + unset($sale[$k]); + } + } + $dbname = $sale[array_rand($sale)]; + $db = Db::connect($dbname)->name('course_details'); + $where = ['good' => 1, 'is_price' => 2]; + $course_detail_count = $db->where($where)->count(); + if(!$course_detail_count) { + return returnErrorData('暂无视频'); + } + $page = rand(1, $course_detail_count); + $size = 5; + $currPage = ceil($page / $size); + $res = $db->where($where)->limit($page, $size)->select()->toArray(); + + if($user_id) { + $db_name = DatabaseRoute::getConnection('course_collect', ['user_id' => $user_id]); + $user_db = Db::connect($db_name)->name('course_collect'); + foreach ($res as $k => $v) { + $res[$k]['isCollect'] = $user_db->where(['classify' => 1, 'user_id' => $user_id])->count(); + $res[$k]['isGood'] = $user_db->where(['classify' => 2, 'user_id' => $user_id])->count(); + } + } + $res = apiconvertToCamelCase($res); + return returnSuccessData([ + 'currPage' => $currPage, + 'pageSize' => $size, + 'list' => $res, + 'records' => null, + 'totalCount' => $course_detail_count, + 'totalPage' => ceil($course_detail_count / $size), + ]); + } + + public static function getRedEnvelopeTips($user_id) + { + $db = DatabaseRoute::getDb('orders', $user_id); + $count = $db->where('create_time', '>', date('Y-m-d 00:00:00'))->where(['user_id' => $user_id, 'status' => 1, 'pay_way' => 9])->count(); + $totalCount = CommonInfo::where(['type' => 901])->find()->value; + $string = '每日前' . $totalCount . '次付款均可获取抽奖机会,抽奖保底抽中付款金额等额红包,红包可直接提现。当前为第' . $count + 1 . '次付款'; + return returnSuccessData($string); + + } + + // 查看短剧 + public static function viewCourse($get) + { + if(empty($get['courseId']) || empty($get['courseDetailsId']) || empty($get['type'])) { + return returnErrorData('参数不完整'); + } + + // 获取短剧详情 + $course = Course::where(['course_id' => $get['courseId']])->find(); + if(!$course) { + return returnErrorData('短剧不存在'); + } + + // 获取短剧集详情 + $course_detail_db = DatabaseRoute::getDb('course_details', ['course_id' => $get['courseId']]) + ->where(['course_id' => $get['courseId']]) + ->where(['course_details_id' => $get['courseDetailsId']]) + ->find(); + if(!$course_detail_db) { + return returnErrorData('短剧集不存在'); + } + + $db = Db::connect(DatabaseRoute::getConnection('course_details', ['course_id' => $get['courseId']], true)); + if($get['type'] == 'start') { + $db->name('course_details') + ->where(['course_id' => $get['courseId']]) + ->where(['course_details_id' => $get['courseDetailsId']]) + ->inc('view_count') + ->update(); + }elseif($get['type'] == 'end') { + $db->name('course_details') + ->where(['course_id' => $get['courseId']]) + ->where(['course_details_id' => $get['courseDetailsId']]) + ->inc('play_complete_count') + ->update(); + } + + return returnSuccessData(); + } + + // 根据id查询短剧集数列表 + public static function courseSets($get, $user, $sort = null) + { + try { + if(empty($get['courseId'])) { + return returnErrorData('参数不完整'); + } + $courseId = $get['courseId']; + // 获取短剧详情 + $dd_b = Db::connect(config('database.search_library')); + $db_name = $dd_b->name('course'); + $bean = $db_name->where(['course_id' => $courseId])->find(); + $dd_b->close(); + if(!$bean) { + return returnErrorData('短剧不存在'); + } + $courseCollect = CourseCollect::Watchhistory($user['user_id'], $courseId); + Db::connect()->close(); + // 是否追剧 + $collect = CourseCollect::isfollowthedrama($user['user_id'], $courseId); + Db::connect()->close(); + $userInfo = TbUser::selectUserByIdNew($user); + if (!empty($userInfo['member']) && $userInfo['member'] == 2) { + $isVip = true; + }else{ + $isVip = false; + } + // 查询用户是否购买了整集 + $courseUser = CourseCollect::selectCourseUser($user['user_id'], $courseId); + Db::connect()->close(); + // 每天购买超过上限,获得免费时间段资格 + $freeWatch = CourseCollect::checkFreeWatchPayCount($user['user_id']); + $startSort = 0; + $endSort = 5; + $dn_course_details = DatabaseRoute::getDb('course_details', ['course_id' => $courseId]); + if (is_null($sort)) { + if (!empty($courseCollect)) { + $courseDetails = $dn_course_details->field('sort') + ->where('course_details_id', $courseCollect['course_details_id']) + ->limit(1) + ->find(); + $sort = $courseDetails['sort']; + } + } + Db::connect()->close(); + if ($freeWatch || !empty($courseUser)) { + $courseDetailsSetVos = CourseDetails::courseSets($courseId, 2, null); + } else { + $courseDetailsSetVos = CourseDetails::courseSets($courseId, 1, $bean['wholesale_price']); + } + // 调整集数范围 + if (!is_null($sort) && $sort > 2) { + $startSort = $sort - 3; + $endSort = $sort + 3; + if (count($courseDetailsSetVos) < $endSort) { + $startSort = count($courseDetailsSetVos) - 5; + $endSort = count($courseDetailsSetVos) + 1; + } + } + + // 已购买剧集ID集合 + $detailsId = []; + if (!$freeWatch) { + $det_db = Db::connect(DatabaseRoute::getConnection('course_user', ['user_id' => $user['user_id']])); + $detailsId = $det_db->name('course_user')->where(['course_id' => $courseId, 'classify' => 2])->column('course_details_id'); + $det_db->close(); + $detailsId = array_flip(array_flip($detailsId)); // 去重 + } + // 处理剧集列表 + $current = null; + foreach ($courseDetailsSetVos as &$s) { + $s['wholesalePrice'] = (int) $s['wholesalePrice']; + // 当前播放集 + if (!empty($courseCollect) && $s['courseDetailsId'] == $courseCollect['course_details_id']) { + $s['current'] = 1; + $current = &$s; + } + + // 非免费用户的权限控制 + if ( + !$freeWatch && + $s['sort'] > 3 && + (empty($detailsId) || !in_array($s['courseDetailsId'], $detailsId)) && + empty($courseUser) && + !$isVip + ) { + $s['videoUrl'] = null; + } + + // 检查是否已点赞 + if ($s['sort'] > $startSort && $s['sort'] < $endSort) { + $isGood_db = Db::connect(DatabaseRoute::getConnection('course_collect', ['user_id' => $user['user_id']])); + $isGood = $isGood_db->name('course_collect') + ->where('course_details_id', $s['courseDetailsId']) + ->where('classify', 2) + ->limit(1) + ->count(); + $isGood_db->close(); + $s['isGood'] = empty($isGood) || $isGood == 0 ? 0 : 1; + } + } + // 如果没有当前播放集,默认第一集 + if (empty($current) && !empty($courseDetailsSetVos)) { + $courseDetailsSetVos[0]['current'] = 1; + $current = &$courseDetailsSetVos[0]; + } + self::setCourseView($bean); + + $price = ($freeWatch ? 0 : ($bean['price'] ?? 0)); + $price = bccomp($price, '0', 2) <= 0 ? 0 : $price; + // 返回结果 + $map = [ + 'current' => $current, + 'price' => $price, + 'title' => $bean['title'], + 'collect' => empty($collect) || $collect == 0 ? 0 : 1, + 'list' => $courseDetailsSetVos + ]; + return returnSuccessData($map); + } catch (\Exception $e) { + Log::info("请求剧集异常: " . $e->getMessage() . '/' . $e->getLine() . '/'); + return returnErrorData($e->getMessage()); + } + } + + + public static function setCourseView($course) + { + // 1. 更新总播放量 + if (empty($course['view_counts'])) { + $viewCounts = 1; + } else { + $viewCounts = $course['view_counts'] + 1; + } + + // 2. 检查是否允许更新周播放量(假设ApiAccessLimitUtil为自定义工具类) + $allowUpdateWeekView = Common::isAccessAllowed( + (string)$course['course_id'], + "updateWeekCourseView", + 1, + 600 + ); + + // 3. 获取并更新周播放量 + $weekView = $course['week_view'] ?? 0; + if ($allowUpdateWeekView) { + // 从Redis获取周播放量(假设redisServiceImpl为自定义服务类) + $weekView = Common::getCourseWeekViewCount($course['course_id']); + } + + $db_name = Db::connect(config('database.z_library'))->name('course'); + // 4. 执行数据库更新 + $db_name->where(['course_id' => $course['course_id']])->update([ + 'view_counts' => $viewCounts, + 'week_view' => $weekView + ]); + Db::connect()->close(); + } + + + +} \ No newline at end of file diff --git a/app/czg/app/model/CourseCollect.php b/app/czg/app/model/CourseCollect.php new file mode 100644 index 0000000..723a6cf --- /dev/null +++ b/app/czg/app/model/CourseCollect.php @@ -0,0 +1,84 @@ +where(['course_id' => $course_id]) + ->where(['user_id' => $user_id]) + ->where(['classify' => 3]) + ->limit(1) + ->find(); + } + + + // 是否追剧 + public static function isfollowthedrama($user_id, $course_id) + { + return DatabaseRoute::getDb('course_collect', $user_id) + ->where(['course_id' => $course_id]) + ->where(['classify' => 1]) + ->limit(1) + ->find(); + } + + // 查询用户是否购买了整集 + public static function selectCourseUser($user_id, $course_id) + { + return DatabaseRoute::getDb('course_user', $user_id) + ->where(['course_id' => $course_id]) + ->where(['classify' => 1]) + ->find(); + } + + + /** + * 校验用户是否达到免费播放购买次数 + */ + public static function checkFreeWatchPayCount($userId) + { + $isExpire = RedisUtils::getFreeWatchTimeIsExpire($userId); + + if (!$isExpire) { + $count = DatabaseRoute::getDb('orders', $userId)->where([ + 'status' => 1, + 'pay_way' => 9, + ['create_time', '>', date('Y-m-d 00:00:00')], + ])->count(); + $needCount = (new CommonInfo())->getByCode(916); + $freeTime = (new CommonInfo())->getByCode(917); + Db::connect()->close(); + if (!$needCount || !$freeTime) { + return false; + } + + if ($count >= intval($needCount['value'])) { + RedisUtils::setFreeWatchTime($userId, intval($freeTime['value']) * 60, false); + RedisUtils::getFreeWatchTimeIsExpire($userId); + $isExpire = false; + }else{ + $isExpire = true; + } + } + + return !$isExpire; + } + + + + +} \ No newline at end of file diff --git a/app/czg/app/model/CourseDetails.php b/app/czg/app/model/CourseDetails.php new file mode 100644 index 0000000..5a4d605 --- /dev/null +++ b/app/czg/app/model/CourseDetails.php @@ -0,0 +1,53 @@ + $courseId])); + $courseDetailsSetVos = $db->name('course_details') + ->alias('c') + ->field([ + 'c.course_id' => 'courseId', + 'c.course_details_id' => 'courseDetailsId', + 'c.course_details_name' => 'courseDetailsName', + 'c.video_url' => 'videoUrl', + 'c.price' => 'price', + 'c.sort' => 'sort', + 'c.is_price' => 'isPrice', + 'c.title_img' => 'titleImg', + 'c.good_num' => 'goodNum', + ]) + ->where('c.course_id', $courseId) + ->order('c.sort', 'asc') + ->select() + ->toArray(); + $db->close(); + foreach ($courseDetailsSetVos as $k => &$v) { + $v['courseId'] = (string) $v['courseId']; + $v['courseDetailsId'] = (string) $v['courseDetailsId']; + if(empty($wholesalePrice)) { + $v['wholesalePrice'] = 0; + }else { + $v['wholesalePrice'] = $wholesalePrice; + } + if($isPrice != 1) { + $v['isPrice'] = 2; + } + } + return $courseDetailsSetVos; + } + +} \ No newline at end of file diff --git a/app/czg/app/model/DiscSpinningRecord.php b/app/czg/app/model/DiscSpinningRecord.php new file mode 100644 index 0000000..b141d30 --- /dev/null +++ b/app/czg/app/model/DiscSpinningRecord.php @@ -0,0 +1,128 @@ +where([ + 'source' => 'order', + 'draw_day' => date('Y-m-d') + ])->count(); + } + + + public static function countTaskDisc($userId, $type) + { + $countTaskDisc = 0; + $signCount = null; + + // 检查类型参数 + if (empty($type) || $type === "1") { + return 0; + } + + $db_name = \think\facade\Db::connect(config('database.search_library')); + $task = $db_name->name('task_center')->where(['type' => 2]); + // 构建查询条件 + $sourceType = null; + if ($type === "2") { + $task =$task->where('number', '>', 1); + $task =$task->where('number', '<', 8); + $sourceType = "taskW"; + } elseif ($type === "3") { + $task =$task->where('number', '>', 7); + $task =$task->where('number', '<', 32); + $sourceType = "taskM"; + } + + // 检查是否已有抽奖记录 + $spCount = DatabaseRoute::getDb('disc_spinning_record', $userId)->where(['source' => $sourceType])->count(); + if (!empty($spCount) && $spCount > 0) { + return 0; + } + + // 获取任务列表 + $taskCenters = $task->select(); + foreach ($taskCenters as $k => $taskCenter) { + // 获取任务奖励配置 + $rewardMap_arr = $db_name->name('task_center_reward')->field('type,number')->where(['task_id' => $taskCenter['id']])->select()->toArray(); + if (empty($rewardMap_arr)) { + continue; + } + $number = 0; + $rewardMap = []; + foreach ($rewardMap_arr as $tk => $tv) { + $number += $tv['number']; + $t_type = $tv['type']; + } + $rewardMap[$t_type] = $number; + $taskWRedisMap = []; + if ($type === "2") { + // 周任务处理逻辑 + $taskWCount = UserSignRecord::getTaskWCount($userId, $rewardMap[9]); + if (!empty($taskWCount)) { + foreach ($taskWCount as $key => $value) { + if ($value > 0) { + $countTaskDisc += $value; + $taskWRedisMap[$key] = $value; + } + } + } + if (!empty($taskWRedisMap)) { + Cache::set("date:spinning:draw:taskW" . $userId, json_encode($taskWRedisMap), todayAfterSecond()); + } + } elseif ($type === "3") { + // 月任务处理逻辑 + if ($signCount === null) { + $signCount = UserSignRecord::getUserSignCount($userId); + } + + if ($signCount >= $taskCenter['number']) { + if (isset($rewardMap[9])) { + $spinningCount = DatabaseRoute::getDb('disc_spinning_record', $userId)->where(['source' => 'taskM', 'source_id' => $taskCenter['id']])->count(); + $countTaskDisc = $rewardMap[9] - ($spinningCount ?? 0); + + if ($countTaskDisc > 0) { + $taskWRedisMap[$taskCenter['id']] = $countTaskDisc; + } + } + } + if (!empty($taskWRedisMap)) { + Cache::set("date:spinning:draw:taskM" . $userId, json_encode($taskWRedisMap), todayAfterSecond()); + } + } + } + return $countTaskDisc; + } + + + public static function selectOrdersCountStatisticsByDay($user_id, $limit) + { + $db = Db::connect(DatabaseRoute::getConnection('orders', ['user_id' => $user_id])); + $count = $db->name('orders') + ->alias('o') + ->leftJoin('disc_spinning_record record', 'o.orders_id = record.source_id AND record.source = "order"') + ->where('o.user_id', $user_id) + ->where('o.status', 1) + ->where('o.pay_way', 9) + ->whereraw('o.create_time > DATE_FORMAT(NOW(), "%Y-%m-%d 00:00:00")') + ->whereNull('record.source_id') + ->order('o.create_time') + ->count(); + if ($count == null) { + return 0; + } + if ($count <= $limit) { + return $count; + } + return $limit; + } + +} \ No newline at end of file diff --git a/app/czg/app/model/HelpClassify.php b/app/czg/app/model/HelpClassify.php new file mode 100644 index 0000000..5fdfa5e --- /dev/null +++ b/app/czg/app/model/HelpClassify.php @@ -0,0 +1,20 @@ +hasMany('HelpWord', 'help_classify_id', 'help_classify_id'); + } + +} \ No newline at end of file diff --git a/app/czg/app/model/HelpWord.php b/app/czg/app/model/HelpWord.php new file mode 100644 index 0000000..3223e6a --- /dev/null +++ b/app/czg/app/model/HelpWord.php @@ -0,0 +1,16 @@ +create_time = date('Y-m-d H:i:s'); + $inviter_model->state = 0; + $inviter_model->money = 0.00; + $inviter_model->user_id = $inviter['user_id']; + $inviter_model->invitee_user_id = $user_id; + $inviter_model->user_type = 1; + $inviter_model->save(); + // 同步二级 + if(!empty($inviter['inviter_user_id'])) { + $inviter_level_two = new self; + $inviter_level_two->create_time = date('Y-m-d H:i:s'); + $inviter_level_two->state = 0; + $inviter_level_two->money = 0.00; + $inviter_level_two->user_id = $inviter['inviter_user_id']; + $inviter_level_two->invitee_user_id = $user_id; + $inviter_level_two->user_type = 2; + $inviter_level_two->save(); + } + $where = $sale = ['user_id' => $inviter['user_id']]; + Common::saveDbData('tb_user', + $sale, + ['invite_count' => $inviter['invite_count'] + 1 ], + 'update', + $where + ); + // 金币 + $money = CommonInfo::where(['type' => 911])->find()->value; + if($money > 0 && $inviter['user_id'] != 1) { + $for_money = formatTo4Decimal($money); + $insert_data = [ + 'id' => Random::generateRandomPrefixedId(19), + 'user_id' => $inviter['user_id'], + 'type' => 1, + 'classify' => 1, + 'state' => 2, + 'money_type' => 2, + 'title' => "[分享奖励金币]", + 'content' => '获取金币:' . $money, + 'money' => $for_money, + 'create_time' => date('Y-m-d H:i:s'), + ]; + $a = Common::saveDbData('user_money_details', $sale, $insert_data); + + $usermoney = Common::saveDbData('user_money', $sale, [], 'find', $where); + if($usermoney) { + $user_money_update_data = [ + 'money' => !empty($usermoney['money'])? $usermoney['money'] + $for_money:$for_money, + 'invite_income_coin' => !empty($usermoney['invite_income_coin'])? $usermoney['invite_income_coin'] + $for_money:$for_money, + ]; + // 更新邀请人钱包 + Common::saveDbData('user_money', + $sale, + $user_money_update_data, + 'update', + $where + ); + } + return true; + } + } + + + public static function updateInviteMoneySum($userId, $money) + { + $count = DatabaseRoute::getDb('invite_money', $userId)->count(); + if (!$count) { + DatabaseRoute::getDb('invite_money', $userId, true)->insert([ + 'cash_out' => 0, + 'user_id' => $userId, + 'money' => 0, + 'money_sum' => 0 + ]); + } + + $money = floatval($money); + $model = DatabaseRoute::getDb('invite_money', $userId, true, true)->inc('money', $money)->inc('money_sum', $money); + $model->update(); + + } + + + // 我的收益 + public static function selectInviteMoney($user):array + { + $inviteMoney = InviteMoney::selectInviteMoney($user['user_id']); + $inviteCount = TbUser::GetByuserInvite($user['invitation_code']); + $inviteSignCount = InviteAchievement::GetByInviteAchievementInvite($user['user_id']); + $userMoney = UserMoney::selectUserMoneyfind($user['user_id']); + + return returnSuccessData([ + 'inviteMoney' => $inviteMoney, + 'inviteCount' => $inviteCount, + 'inviteSignCount' => $inviteSignCount, + 'earning' => [ + 'inviteGoldMoney' => $userMoney['invite_income_coin'], + 'inviteMoney' => $userMoney['invite_income_money'], + ], + ]); + } + + // 查看我邀请的人员列表(查看所有邀请列表) + public static function selectInviteByUserIdLists($user, $get, $os) + { + if(empty($get['page']) || empty($get['limit'])) { + return returnErrorData('参数不完整'); + } + // 拿到下级列表 + $junior_list = TbUser::GetByuserInviteselect($user['invitation_code'], $get['page'], $get['limit']); + $return = [ + 'currPage' => 1, + 'pageSize' => $get['limit'], + 'totalCount' => 0, + 'totalPage' => 0, + ]; + if(empty($junior_list)) { + $return['list'] = []; + if($os == 'admin') { + return returnSuccessData(['pageUtils' => $return]); + }else { + return returnSuccessData($return); + } + } + // 下级user_id集合 + $junior_user_list = extract_user_ids($junior_list); + Log::write('下级user_id集合'. json_encode($junior_user_list)); + $ach_select = DatabaseRoute::getDb('invite_achievement', $user['user_id']) + ->where('count', '>=', 3) + ->where(['user_id' => $user['user_id']]) + ->whereIn('target_user_id', $junior_user_list) + ->select() + ->toArray(); + Log::write('签到集合'. json_encode($ach_select)); + foreach ($ach_select as $k => &$v) { + $v['user_id'] = (string) $v['user_id']; + } + // 下级user_id集合 + $ach_user_list = extract_target_user_ids($ach_select); + Log::write('签到user_id集合---'. json_encode($ach_user_list)); + $commonInfoCount = CommonInfo::where(['type' => 913])->find()->value; + $date = date('Y-m-d 00:00:00'); + + foreach ($junior_list as $k => $v) { + $count = DatabaseRoute::getDb('orders', $v['user_id'])->where(['user_id' => $v['user_id'], 'status' => 1, 'pay_way' => 9])->where('create_time', '>', $date)->count(); + $return['list'][] = [ + 'userId' => $v['user_id'], + 'avatar' => $v['avatar'], + 'userName' => $v['user_name'], + 'recordNum' => in_array($v['user_id'], $ach_user_list)?1:0, + 'userTag' => $count >= $commonInfoCount ? 1 : 0, + ]; + } + if($os == 'admin') { + return returnSuccessData(['pageUtils' => $return]); + }else { + return returnSuccessData($return); + } + } + + + + +} \ No newline at end of file diff --git a/app/czg/app/model/InviteAchievement.php b/app/czg/app/model/InviteAchievement.php new file mode 100644 index 0000000..0f9f3df --- /dev/null +++ b/app/czg/app/model/InviteAchievement.php @@ -0,0 +1,19 @@ +where(['state' => 1, 'user_id' => $user_id])->count(); + return $count; + } +} \ No newline at end of file diff --git a/app/czg/app/model/InviteMoney.php b/app/czg/app/model/InviteMoney.php new file mode 100644 index 0000000..63990d9 --- /dev/null +++ b/app/czg/app/model/InviteMoney.php @@ -0,0 +1,30 @@ + $user_id]; + $db_name = DatabaseRoute::getConnection('invite_money', $sale, true); + $db = Db::connect($db_name)->name('invite_money'); + $money = $db->where($where)->find(); + if(!$money) { + $money = [ + 'user_id' => $user_id, + 'money_sum' => 0.00, + 'money' => 0.00, + 'cash_out' => 0.00, + ]; + $db->insert($money); + } + return convertToCamelCase($money); + } +} \ No newline at end of file diff --git a/app/czg/app/model/MessageInfo.php b/app/czg/app/model/MessageInfo.php new file mode 100644 index 0000000..79d2590 --- /dev/null +++ b/app/czg/app/model/MessageInfo.php @@ -0,0 +1,60 @@ +name('message_info'); + if(!empty($data['user_id'])) { + $where = [ + 'state' => $data['state'], + 'user_id' => $data['user_id'], + ]; + }else { + $where = [ + 'state' => $data['state'], + ]; + } + $list = $db->where($where)->limit($page, $data['limit'])->select()->toArray(); + $list = convertToCamelCase($list); + $count = $db->where($where)->count(); + $return = [ + 'currPage' => 1, + 'list' => $list, + 'pageSize' => $data['limit'], + 'totalCount' => $count, + 'totalPage' => ceil($count / $data['limit']), + ]; + return returnSuccessData($return); + } + + + + public static function sendMessage($data, $user_id) + { + if(empty($data['content']) || empty($data['state']) || empty($data['title'])) { + return returnErrorData('参数不完整'); + } + $data['user_id'] = $user_id; + $data['create_at'] = date('Y-m-d H:i:s'); + MessageInfo::create($data); + return returnSuccessData(); + } + +} \ No newline at end of file diff --git a/app/czg/app/model/Msg.php b/app/czg/app/model/Msg.php new file mode 100644 index 0000000..d9a0482 --- /dev/null +++ b/app/czg/app/model/Msg.php @@ -0,0 +1,117 @@ + 79])->find(); + if($commonInfo && $commonInfo->value == 2) { + return self::AlibabaSendMsg($event, $phone); + }else { + return returnErrorData('配置错误'); + } + } + + public static function AlibabaSendMsg($event, $phone):array + { + + $accessKeyId = CommonInfo::where(['type' => 85])->find()->value; + $accessSecret = CommonInfo::where(['type' => 86])->find()->value; + $sign = CommonInfo::where(['type' => 81])->find()->value; + + switch ($event) { + case "login": + $value = CommonInfo::where(['type' => 82])->find()->value; + break; + case "forget": + $value = CommonInfo::where(['type' => 83])->find()->value; + break; + case "bindWx": + $value = CommonInfo::where(['type' => 84])->find()->value; + break; + case "bindIos": + $value = CommonInfo::where(['type' => 84])->find()->value; + break; + default: + $value = CommonInfo::where(['type' => 82])->find()->value; + break; + } + + $code = Random::build('numeric', 6); + $ret = AlibabaSms::main([ + 'templateCode' => $value, + 'templateParam' => json_encode(['code' => $code]), + 'phoneNumbers' => $phone, + 'signName' => $sign, + ], $accessKeyId, $accessSecret); +// $ret = true; + + if($ret) { + // 保存数据库 + Msg::create([ + 'phone' => $phone, + 'code' => $code, + ]); + return ['code' => 0, 'message' => 'ok', 'msg' => 'login']; + }else { + return returnErrorData('发送失败'); + } + } + + public static function checkCode($phone, $code) + { + if($code != 9876) { + return self::where(['phone' => $phone, 'code' => $code])->order('id','desc')->find(); + } + return true; + } + + public static function delCode($phone, $code) + { + return self::where(['phone' => $phone, 'code' => $code])->delete(); + } + + + +} \ No newline at end of file diff --git a/app/czg/app/model/Orders.php b/app/czg/app/model/Orders.php new file mode 100644 index 0000000..7d83250 --- /dev/null +++ b/app/czg/app/model/Orders.php @@ -0,0 +1,602 @@ +where([ +// 'user_id' => $userId +// ])->find(); + if (empty($userInfo['qd_code'])) { + return; + } + + $sysUser = DatabaseRoute::getMasterDb('sys_user')->where([ + 'qd_code' => $userInfo['qd_code'] + ])->find(); + if (!$sysUser) { + return; + } + $order['sys_user_id'] = $sysUser['user_id']; + } + + /** + * 加入短剧到我的列表 + * @param $order array 订单 + */ + public static function insertOrders($order) + { + // 短剧订单 + if ($order['orders_type'] == 1) { + // 单集购买 + if (!empty($order['course_details_ids'])) { + $insertDataIst = []; + $courseDetailList = json_decode($order['course_details_ids'], true); + foreach ($courseDetailList as $courseDetailId) { + $insertDataIst[] = [ + 'course_id' => $order['course_id'], + 'course_details_id' => $courseDetailId, + 'classify' => 2, + 'user_id' => $order['user_id'], + 'order_id' => $order['orders_id'], + 'create_time' => getNormalDate() + ]; + } + + DatabaseRoute::getDb('course_user', $order['user_id'], true)->insertAll()($insertDataIst); + + Log::info("添加短剧到我的列表成功: " . json_encode($insertDataIst)); + }else{ + DatabaseRoute::getDb('course_user', $order['user_id'], true)->insert([ + 'course_id' => $order['course_id'], + 'course_details_id' => $order['course_details_id'], + 'classify' => $order['course_details_id'] ? 2 : 1, + 'user_id' => $order['user_id'], + 'order_id' => $order['orders_id'], + 'create_time' => getNormalDate() + ]); + } + // 会员订单 + }else{ + $dateFormat = 'Y-m-d H:i:s'; + // 查询用户是否是会员 + $userVip = DatabaseRoute::getDb('user_vip', $order['user_id'])->where([ + 'user_id' => $order['user_id'] + ])->find(); + $cal = new DateTime(); + if ($userVip) { + //未到期 + // 判断会员是否未到期(isVip == 2) + if ($userVip['isVip'] == 2) { + // 设置会员到期时间 + $endTime = new DateTime($userVip['endTime']); + $cal->setTimestamp($endTime->getTimestamp()); // 当前时间 + self::setDateByType($cal, $order['vip_name_type']); + } else { + // 到期会员续费 + $cal->setTimestamp(time()); // 当前时间 + self::setDateByType($cal, $order['vip_name_type']); + } + + $userVip['is_vip'] = 2; + $userVip['create_time'] = getNormalDate(); + $userVip['end_time'] = $cal->format($dateFormat); + $userVip['vip_type'] = 2; + + DatabaseRoute::getDb('user_vip', $order['user_id'], true)->where([ + 'user_id' => $order['user_id'] + ])->update($userVip); + Log::info("会员续费成功: " . json_encode($userVip)); + + }else{ + $cal->setTimestamp(time()); // 当前时间 + self::setDateByType($cal, $order['vip_name_type']); + + // 开通会员 + DatabaseRoute::getDb('user_vip', $order['user_id'], true)->insert([ + 'user_id' => $order['user_id'], + 'create_time' => getNormalDate(), + 'is_vip' => 2, + 'end_time' => $cal->format($dateFormat) + ]); + Log::info("会员续费成功: " . json_encode($userVip)); + } + } + + } + + private static function setDateByType(DateTime $cal, $type) + { + switch ($type) { + case 0: $cal->modify('+1 month'); break; + case 1: $cal->modify('+3 months'); break; + case 2: $cal->modify('+1 year'); break; + } + } + + public static function updateOrderStatus($payDetail, $order, $userId) { + if ($payDetail['state'] == 1) { + // TODO 测试 +// return; + } + + $userInfo = DatabaseRoute::getDb('tb_user', $userId, true)->find(); + self::fillSysUserId($order, $userInfo); + + DatabaseRoute::getDb('pay_details', $userId, true)->where([ + 'id' => $payDetail['id'] + ])->update([ + 'state' => 1, + 'pay_time' => getNormalDate(), + 'trade_no' => $payDetail['trade_no'], + 'third_order_no' => $payDetail['third_order_no'], + ]); + + DatabaseRoute::getDb('orders', $userId, true)->where([ + 'orders_id' => $order['orders_id'] + ])->update([ + 'pay_way' => 9, + 'status' => 1, + 'pay_time' => getNormalDate(), + 'sys_user_id' => $order['sys_user_id'] + ]); + +// // 短剧插入 +// self::insertOrders($order); +// +// // 用户信息及上级信息 +// $userInfo = DatabaseRoute::getDb('tb_user', $order['user_id'])->where([ +// 'user_id' => $order['user_id'] +// ])->find(); + + +// $byUser = TbUser::getByUserIdOrInviterCode($userInfo['inviter_user_id'], $userInfo['inviter_code']); +// 记录上级用户奖励信息 +// Log::info("上级用户: ".json_encode($byUser)); +// if ($byUser) { +// $inviteAchievement = DatabaseRoute::getAllDbData('invite_achievement', function ($query) use ($byUser, $userInfo) { +// return $query->where([ +// 'target_user_id' => $userInfo['user_id'] +// ]); +// })->find(); +// if ($inviteAchievement) { +// Log::info("修改邀请统计"); +// DatabaseRoute::getDb('invite_achievement', $byUser['user_id'], true)->where([ +// 'user_id' => $inviteAchievement['user_id'], +// 'id' => $inviteAchievement['id'] +// ])->update([ +// 'count' => $inviteAchievement['count'] + 1, +// 'update_time' => getNormalDate() +// ]); +// }else{ +// Log::info("新增邀请统计"); +// DatabaseRoute::getDb('invite_achievement', $byUser['user_id'], true)->insert([ +// 'state' => 0, +// 'count' => 1, +// 'create_time' => getNormalDate(), +// 'target_user_id' => $userInfo['user_id'], +// 'user_id' => $byUser['user_id'] +// ]); +// } +// +// +// // TODO 异步领取奖励 +//// pushQueue(ActivitiesQueue::class, [ +//// 'userInfo' => $userInfo, +//// 'sourceUser' => $byUser +//// ], 1); +// DatabaseRoute::transactionXa(function () use ($userInfo, $byUser, $userId) { +// self::activities($userInfo, $byUser); +// }); +// } + + + // 推广奖励发放 +// if ($userInfo['inviter_code'] || !$byUser || $byUser['user_id'] == 1) { +// $sysUser = SysUser::GetByQrcode($userInfo['qd_code']); +// if ($sysUser) { +// $rateMoney = $sysUser['qd_rate']; +// SysUser::updateSysMoney($sysUser['user_id'], $rateMoney, 1); +// +// DatabaseRoute::getDb('sys_user_money_details', $sysUser['user_id'], true)->insert([ +// 'sys_user_id' => $sysUser['user_id'], +// 'user_id' => $sysUser['user_id'], +// 'type' => 1, +// 'money' => $rateMoney, +// 'create_time' => getNormalDate(), +// 'state' => 2, +// 'classify' => 10, +// 'title' => "[渠道用户]用户名称:{$userInfo['user_name']}", +// 'money_type' => 1, +// 'content' => '总佣金'.$rateMoney.',到账佣金'.$rateMoney +// ]); +// } +// } + + // 增加剧集支付次数 +// self::incrWeekPayCount($order['course_id']); + + } + + public static function incrWeekPayCount($courseId) + { + RedisUtils::incrWeekCounter("setWeekPayCount:", $courseId); + $count = RedisUtils::getWeekCounter("setWeekPayCount:", $courseId); + DatabaseRoute::getMasterDb('course', true)->where([ + 'course_id' => $courseId + ])->update([ + 'week_pay' => $count + ]); + + } + + /** + * 推广奖励 一级二级佣金 废弃 + */ + public static function updateInvite($userInfo, $userId, $price) + { + if ($userInfo['user_id'] == 1) { + return []; + } + + if ($userInfo && $userId && $price) { + $invite = DatabaseRoute::getMasterDb('invite')->where([ + 'user_id' => $userInfo['user_id'], + 'invitee_user_id' => $userId + ])->where(function ($query) { + $query->where([ + 'user_type' => 1 + ])->whereOrNotNull('user_type'); + })->find(); + if (!$invite) { + $invite = [ + 'state' => 0, + 'money' => 0, + 'user_id' => $userInfo['user_id'], + 'invitee_user_id' => $userId, + 'create_time' => getNormalDate(), + 'user_type' => 1, + ]; + DatabaseRoute::getMasterDb('invite', true)->insert($invite); + } + + $sourceUser = TbUser::selectUserById($userId); +// if (bccomp($userInfo['rate'], "0", 2) > 0) { +// $rateMoney = $userInfo['rate']; +// Db::name('invite')->where([ +// 'id' => $invite['id'] +// ])->update([ +// 'user_type' => 1, +// 'state' => 1, +// 'money' => $invite['money'] + $rateMoney +// ]); +// +// Invite::updateInviteMoneySum($userInfo['user_id'], $rateMoney); +// +// } + + + } + + } + + public static function activities($user, $sourceUser) + { + Log::info("活动领取开始: 用户{$user['user_name']}, 上级{$sourceUser['user_name']}"); + // 查询上级用户 + $inviteAchievement = DatabaseRoute::getAllDbData('invite_achievement', function ($query) use ($user) { + return $query->where([ + 'target_user_id' => $user['user_id'] + ]); + })->find(); + + // 首次达标 + $commonModel = (new CommonInfo()); + $signCount = $commonModel->getByCodeToInt(913); + Log::info("活动领取: 用户{$user['user_name']}, 上级{$sourceUser['user_name']}, 达标次数{$inviteAchievement['count']}"); + // 首次达标发放奖励 +// if ($inviteAchievement['state'] == 0 && $inviteAchievement['count'] >= $signCount) { + if (true) { + Log::info('开始领取达标奖励'); + $amount = $commonModel->getByCode(912)['value']; + // 记录资金明细 + DatabaseRoute::getDb('user_money_details', $sourceUser['user_id'], true)->insert([ + 'classify' => 6, + 'money' => $amount, + 'user_id' => $sourceUser['user_id'], + 'create_time' => getNormalDate(), + 'content' => "分享达标{$amount}元", + 'title' => '分享达标奖励', + 'state' => 2, + 'type' => 1, + 'money_type' => 1, + ]); + + Invite::updateInviteMoneySum($sourceUser['user_id'], $amount); + + // 增加上级用户钱 + if(DatabaseRoute::getDb('user_money', $sourceUser['user_id'])->count() == 0) { + DatabaseRoute::getDb('user_money', $sourceUser['user_id'], true)->insert([ + 'user_id' => $sourceUser['user_id'], + 'money' => $amount, + 'amount' => $amount + ]); + }else{ + DatabaseRoute::getDb('user_money', $sourceUser['user_id'], true, true)->inc('amount', $amount)->inc('invite_income_money', $amount)->update(); + } + + DatabaseRoute::getDb('invite_achievement', $inviteAchievement['user_id'], true, true)->update([ + 'state' => 1 + ]); + + + // 代理发放佣金 + if ($user['qd_code'] && $user['qd_code'] != "666666") { + $sysUser = DatabaseRoute::getAllDbData('sys_user', function ($query) use ($user) { + return $query->whereNull('sys_user_id')->where([ + 'qd_code' => $user['qd_code'] + ]); + })->find(); + + if ($sysUser) { + // 查询代理奖励金额 + $qdAward = $commonModel->getByCode(915)['value']; + if (bccomp($qdAward, "0", 2) > 0) { + DatabaseRoute::getDb('sys_user_money_details', $sysUser['user_id'], true)->insert([ + 'user_id' => $sysUser['user_id'], + 'sys_user_id' => $sysUser['sys_user_id'], + 'title' => '[分享达标额外奖励]', + 'classify' => 6, + 'type' =>1, + 'state' => 2, + 'money' => $qdAward, + 'content' => '推广人员首次达标,额外奖励现金红包'.$qdAward, + 'money_type' => 1, + 'create_time' => getNormalDate() + ]); + + DatabaseRoute::getMasterDb('sys_user_money', true)->where([ + 'user_id' => $sysUser['user_id'] + ])->inc('money', $qdAward)->inc('invite_income_money', $qdAward)->update(); + } + } + } + + }else{ + Log::info('未达标或已领取跳过领取'.json_encode($inviteAchievement)); + } + + + // 拉人奖励 + self::calcUserInviteAmount($user, $sourceUser, $signCount); + self::calcInviteStandardAward($user, $sourceUser); + + } + + /** + * 计算用户邀请奖励金额 + */ + private static function calcUserInviteAmount($user, $sourceUser, $signCount) + { + // 检查实名 + $user = DatabaseRoute::getDb('user_info', $sourceUser['user_id'])->find(); + if (!$user || empty($user['cert_no'])) { + Log::info("邀请用户{$sourceUser['user_name']}未实名认证, 不发放奖励"); + return; + } + + // 查询用户当天完成订单 + $orderCount = DatabaseRoute::getDb('orders', $user['user_id'])->where([ + 'status' => 1, + 'pay_way' => 9, + ['create_time', '>=', date('Y-m-d 00:00:00')], + ])->count(); + if ($orderCount < $signCount) { + Log::info("用户{$sourceUser['user_name']}未完成{$signCount}个订单, 不发放奖励"); + return; + } + + // 查询当天是否已经给过上级奖励 + $count = DatabaseRoute::getDb('user_money_details', $sourceUser['user_id'])->where([ + 'classify' => 6, + 'by_user_id' => $user['id'], + ['create_time', '>=', date('Y-m-d 00:00:00')] + ])->count(); + if ($count > 0) { + Log::info("上级用户奖励已发放,{$sourceUser['user_id']}"); + return; + } + + + // 给上级用户达标奖励 + if (empty($sourceUser['invite_amount'])) { + DatabaseRoute::getDb('user_money', $sourceUser['user_id'], true, true)->update([ + 'invite_amount' => '0.1' + ]); + } + + DatabaseRoute::getDb('user_money_details', $sourceUser['user_id'], true)->insert([ + 'classify' => 6, + 'money' => '0.1', + 'user_id' => $sourceUser['user_id'], + 'by_user_id' => $user['id'], + 'create_time' => getNormalDate(), + 'content' => '下级签到奖励0.1元', + 'title' => '签到奖励', + 'state' => 2, + 'type' => 1, + 'money_type' => 1, + ]); + + // 发放奖励 + DatabaseRoute::getDb('user_money', $sourceUser['user_id'], true, true)->inc('amount', 0.1)->update(); + Log::info("用户: {$user['user_id']}, 上级: {$sourceUser['user_id']}, 签到奖励0.1元"); + + } + + + /** + * 计算分享达标奖励 + */ + private static function calcInviteStandardAward($userInfo, $sourceUser) + { + runWithLock("userAward".$sourceUser['user_id'], 500, function () use ($sourceUser, $userInfo) { + // 查询邀请用户人数 + $byUserIdList = DatabaseRoute::getDb('invite_achievement', $sourceUser['user_id'])->where([ + 'state' => 1, + ])->column('target_user_id'); + +// 去重(替代 array_unique) + $uniqueMap = []; + foreach ($byUserIdList as $id) { + $uniqueMap[$id] = true; + } + $byUserIdList = array_keys($uniqueMap); + + if (empty($byUserIdList)) { + return; + } + +// 查询邀请用户的 cert_no,并排除自己,去重 + $targetCertNo = $userInfo['cert_no'] ?? null; + + $collect = []; + $chunkSize = 2000; // 每批处理1000条,视数据库配置可调高 + + foreach (array_chunk($byUserIdList, $chunkSize) as $chunk) { + $partial = DatabaseRoute::getAllDbData('user_info', function ($builder) use ($chunk, $targetCertNo) { + $builder = $builder->whereIn('user_id', $chunk)->whereNotNull('account_no'); + if (!empty($targetCertNo)) { + $builder = $builder->where('cert_no', '<>', $targetCertNo); + } + return $builder; + })->column('cert_no'); + + // 合并本批结果 + if (!empty($partial)) { + $collect = array_merge($collect, $partial); + } + } + // 去重(用更快方式) + $collect = array_keys(array_flip($collect ?? [])); + + $inviteCount = count($collect); + + // 查询所有已开启的奖励 + $completAward = DatabaseRoute::getMasterDb('complet_award', true)->where([ + 'id' => 1 + ])->find(); + if (!$completAward) { + Log::info("分享达标未配置"); + return; + } + + if ($inviteCount < $completAward['invite_count']) { + return; + } + // 查询是否开启分享循环奖励 + $isLoop = (new CommonInfo())->getByCodeToInt(932); + $inviteAchievement = DatabaseRoute::getDb('invite_achievement', $sourceUser['inviter_user_id'])->where([ + 'target_user_id' => $userInfo['user_id'] + ])->find(); + if (!$inviteAchievement) { + $inviteAchievement = [ + 'user_id' => $sourceUser['inviter_user_id'], + 'target_user_id' => $sourceUser['id'], + 'give_award_count' => 0 + ]; + DatabaseRoute::getDb('invite_achievement', $sourceUser['user_id'], true)->insert($inviteAchievement); + } + $awardCount = $inviteAchievement['give_award_count']; + // 如果未开启循环奖励,并且已经发放过奖励,则跳过 + if ($isLoop != 1 && $awardCount > 0) { + return; + } + + // 计算获取奖励次数 邀请达标人员 / 邀请人数 + $awardNum = intval($inviteCount / $completAward['invite_count']); + if ($isLoop != 1) { + $awardNum = 1; + } + + if ($awardNum - $awardCount <= 0 ) { + return; + } + + for ($i = 0; $i < $awardNum - $awardCount; $i++) { + switch ($completAward['type']) { + case 1: + DatabaseRoute::getDb('user_money_details', $sourceUser['user_id'], true)->insert([ + 'user_id' => $sourceUser['user_id'], + 'title' => '[分享达标额外奖励]', + 'classify' => 6, + 'type' => 1, + 'state' => 2, + 'money' => $completAward['award_number'], + 'content' => "邀请人员已有{$completAward['invite_count']}人达标,额外奖励金币{$completAward['award_number']}", + 'money_type' => 2, + 'source_id' => $completAward['id'] + ]); + + DatabaseRoute::getDb('user_money', $sourceUser['user_id'], true, true)->inc('money', $completAward['award_number'])->update(); + break; + case 2: + DatabaseRoute::getDb('user_money_details', $sourceUser['user_id'], true)->insert([ + 'user_id' => $sourceUser['user_id'], + 'title' => '[分享达标额外奖励]', + 'classify' => 6, + 'type' => 1, + 'state' => 2, + 'money' => $completAward['award_number'], + 'content' => "邀请人员已有{$completAward['invite_count']}人达标,额外奖励现金红包{$completAward['award_number']}", + 'money_type' => 1, + 'source_id' => $completAward['id'] + ]); + + DatabaseRoute::getDb('user_money', $sourceUser['user_id'], true, true)->inc('amount', $completAward['award_number'])->update(); + } + + //更新邀请达标奖励次数 + DatabaseRoute::getDb('invite_achievement', $inviteAchievement['user_id'], true, true)->where([ + 'id' => $inviteAchievement['id'], + ])->update([ + 'user_id' => $inviteAchievement['user_id'], + 'give_award_count' => $inviteAchievement['give_award_count'] + ($awardNum - $awardCount) + ]); + } + + }); + } + + public static function selectOrdersByDay(int $userId) + { + return DatabaseRoute::getDb('orders', $userId, false, false, false)->alias('o') + ->leftJoin('disc_spinning_record r', 'o.orders_id = r.source_id') + ->where('o.user_id', $userId) + ->where('o.status', 1) + ->where('o.pay_way', 9) + ->whereraw('o.create_time > DATE_FORMAT(NOW(), "%Y-%m-%d 00:00:00")') + ->whereNull('r.source_id') + ->order('o.create_time') + ->find(); // LIMIT 1 + } + +} \ No newline at end of file diff --git a/app/czg/app/model/TaskCenter.php b/app/czg/app/model/TaskCenter.php new file mode 100644 index 0000000..1992af7 --- /dev/null +++ b/app/czg/app/model/TaskCenter.php @@ -0,0 +1,17 @@ +name('task_center')->where(['shows' => 1])->order('type', 'asc')->order('sort', 'asc')->select()->toArray(); + $data = []; + $day_date = date('Y-m-d 00:00:00'); + foreach ($rask_arr as $k => $task) { + $task_reward_arr = $db->name('task_center_reward')->field('type,number')->where(['task_id' => $task['id']])->select()->toArray(); + $todaySign = true; + $signCount = null; + // 默认值 + $rask_arr[$k]['disabled'] = true; + $rask_arr[$k]['discNumber'] = 0; + if(empty($task_reward_arr)) { + continue; + } + $number = 0; + $task_reward = []; + foreach ($task_reward_arr as $tk => $tv) { + $number += $tv['number']; + $t_type = $tv['type']; + } + $task_reward[$t_type] = $number; + + switch ($task['type']) { + //签到任务 + case 2: + + if($task['number'] == 1) { + $order_db = DatabaseRoute::getDb('orders', $user_id); + $dayOrderNum = $order_db->where(['status' => 1, 'pay_way' => 9])->where('create_time', '>', $day_date)->count(); + if($dayOrderNum < 3) { + $rask_arr[$k]['discNumber'] = $dayOrderNum; + $rask_arr[$k]['number'] = 3; + $todaySign = false; + }elseif (UserSignRecord::getTaskCenterRecordCount($user_id, $task['id'], $day_date) > 0) { + $rask_arr[$k]['buttonTitle'] = '已领取'; + $rask_arr[$k]['number'] = null; + }else { + $rask_arr[$k]['discNumber'] = 0; + $rask_arr[$k]['number'] = null; + $rask_arr[$k]['jumpType'] = 0; + } + }else { + // 周任务 + if($task['number'] > 1 && $task['number'] < 8) { + if(!empty($task_reward[9])) { + $disc_spi_count = DatabaseRoute::getDb('disc_spinning_record', $user_id)->where(['source' => 'taskW'])->count(); + if($disc_spi_count > 0) { + continue 2; + } + $isBreak = false; + // 抽奖次数 + $taskWCount = UserSignRecord::getTaskWCount($user_id, $task_reward[9]); + if($taskWCount) { + foreach ($taskWCount as $taskWCount_K => $taskWCount_v) { + if($taskWCount_v > 0) { + $isBreak = true; + break; + } + } + if($isBreak) { + $rask_arr[$k]['discNumber'] = null; + $rask_arr[$k]['number'] = null; + break; + } + } + } + $wSignCount = UserSignRecord::getWSignCount($user_id); + $wSignCount_s = $todaySign ? 1 : 0; + if(!$wSignCount || ($wSignCount + $wSignCount_s) < $rask_arr[$k]['number']) { + $rask_arr[$k]['discNumber'] = !$wSignCount?0:$wSignCount; + $rask_arr[$k]['disabled'] = false; + }else { + continue 2; + } + }elseif ($task['number'] > 7 && $task['number'] < 32) { + if(!$signCount) { + $signCount = UserSignRecord::getUserSignCount($user_id); + } + if($signCount + ($todaySign ? 1 : 0) < $rask_arr[$k]['number']) { + $rask_arr[$k]['discNumber'] = $signCount; + $rask_arr[$k]['disabled'] = false; + }else { + if(!empty($task_reward[9])) { + $spinningCount = DatabaseRoute::getDb('disc_spinning_record', $user_id)->where(['source' => 'taskW', 'source_id' => $tv['id']])->count(); + if ($spinningCount == null || $task_reward[9] - $spinningCount > 0) { + $rask_arr[$k]['discNumber'] = null; + $rask_arr[$k]['number'] = null; + break; + } else { + continue 2; + } + }else { + if(UserSignRecord::getTaskCenterRecordCount($user_id, $task['id'], $day_date) > 0) { + $rask_arr[$k]['buttonTitle'] = '已领取'; + $rask_arr[$k]['disabled'] = false; + $rask_arr[$k]['number'] = null; + $rask_arr[$k]['discNumber'] = null; + }else { + $rask_arr[$k]['number'] = null; + $rask_arr[$k]['discNumber'] = null; + } + } + } + } + } + break; + // 一次性任务 + case 3: + if($task['id'] == 1) { +// $inviteAchievement = Db::connect(DatabaseRoute::getConnection('invite_achievement', ['user_id' => $user_id]))->name('invite_achievement') +// ->where(['target_user_id' => $user_id])->find(); + $inviteAchievement = DatabaseRoute::getDb('invite_achievement', $target_user_id) + ->where('target_user_id', $user_id) + ->find(); + + if($inviteAchievement && !empty($inviteAchievement['tasks'])) { + $splitTasks = explode(',', $inviteAchievement['tasks']); + $isOver = false; + foreach ($splitTasks as $tasks) { + $isEqual = trim($tasks) === '1'; + if($isEqual) { + $isOver = true; + break; + } + } + if($isOver) { + unset($rask_arr[$k]); + continue 2; + } + } + $userinfo = DatabaseRoute::getDb('user_info', $user_id)->find(); + if($userinfo && !empty($userinfo['cert_name']) && !empty($userinfo['cert_no'])) { + $users = UserInfo::getUsersByNameAndCertNo($userinfo['cert_name'], $userinfo['cert_no']); + if(UserSignRecord::getTaskCenterRecordCountUserIdAll($users, $task['id']) > 0) { + if($inviteAchievement) { +// Db::connect(DatabaseRoute::getConnection('invite_achievement', ['user_id' => $user_id], true))->name('invite_achievement') +// ->where(['id' => $inviteAchievement['id']]) +// ->update(['tasks' => empty($inviteAchievement['tasks']) ? '1' : $inviteAchievement['tasks'] . ',1']); + DatabaseRoute::getDb('invite_achievement', $user_id, true, true) + ->where(['id' => $inviteAchievement['id']]) + ->update(['tasks' => empty($inviteAchievement['tasks']) ? '1' : $inviteAchievement['tasks'] . ',1']); + } + unset($rask_arr[$k]); + continue 2; + } + } + if(UserSignRecord::getTaskCenterRecordCount($user_id, $task['id'], null) > 0) { + unset($rask_arr[$k]); + continue 2; + } + $sumOrderNum = 0; + if($inviteAchievement) { + $sumOrderNum = $inviteAchievement['count']; + } + if($sumOrderNum != null && $sumOrderNum < $rask_arr[$k]['number']) { + $rask_arr[$k]['discNumber'] = $sumOrderNum; + }else { + $rask_arr[$k]['discNumber'] = null; + $rask_arr[$k]['number'] = null; + $rask_arr[$k]['jumpType'] = 0; + } + } + break; + } + } + return returnSuccessData(convertToCamelCase($rask_arr)); + } + + public static function addBlackUser(int $userId, string $behavior) + { + Log::info("异常用户id, 异常操作: {$userId},{$behavior}"); + $db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userId], true)); + $userInfo = $db->name('user_info')->where('user_id', $userId)->find(); + + if (!empty($userInfo) && !empty($userInfo['cert_no'])) { + Db::name('tb_user_blacklist')->insert([ + 'real_name' => $userInfo['cert_name'], + 'id_card_no' => $userInfo['cert_no'] + ]); + } + $db->name('tb_user')->where('user_id', $userId) + ->update([ + 'status' => 0, + 'platform' => $behavior, + 'update_time' => date('Y-m-d H:i:s') + ]); + } + + // 任务领取 + public static function taskReceive($userId, $id, $target_id) + { + + $user_id_slave_db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userId])); + $user_id_master_db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userId], true)); + + // 查询任务中心记录 + $taskCenter = Db::name('task_center')->find($id); + if (empty($taskCenter) || $taskCenter['shows'] != 1) { + return ['code' => -1, 'msg' => '领取失败']; + } + + // 查询邀请成就记录 + $inviteAchievement = DatabaseRoute::getDb('invite_achievement', $target_id) + ->where('target_user_id', $userId) + ->find(); + + // 处理类型为2的任务 + if ($taskCenter['type'] == 2) { + // 统计今日订单数量 + $todayStart = date('Y-m-d') . ' 00:00:00'; + $dayOrderNum = $user_id_slave_db->name('orders') + ->where('user_id', $userId) + ->where('create_time', '>=', $todayStart) + ->count(); + + if ($taskCenter['number'] == 1) { + // 查询昨日签到记录 + $yesterday = date('Y-m-d', strtotime('-1 day')); + $yesterdaySign = $user_id_slave_db->name('user_sign_record') + ->where('user_id', $userId) + ->where('sign_day', $yesterday) + ->find(); + + // 构建今日签到记录 + $signRecord = [ + 'user_id' => $userId, + 'sign_day' => date('Y-m-d'), + 'create_time' => date('Y-m-d H:i:s'), + 'day' => 1 // 默认连续1天 + ]; + + // 计算连续签到天数 + if (!empty($yesterdaySign) && $yesterdaySign['day'] != 7) { + $signRecord['day'] = $yesterdaySign['day'] + 1; + } + + // 检查领取条件 + if ($dayOrderNum < 3) { + return ['code' => -1, 'msg' => '领取失败,未达成领取条件']; + } + + // 检查是否已领取 + $recordCount = $user_id_slave_db->name('task_center_record') + ->where('user_id', $userId) + ->where('task_id', $taskCenter['id']) + ->where('create_time', '>=', $todayStart) + ->count(); + + if ($recordCount > 0) { + return ['code' => -1, 'msg' => '不可重复领取']; + } + + // 保存签到记录 + $user_id_master_db->name('user_sign_record')->insert($signRecord); + } else { + return ['code' => -1, 'msg' => '异常领取,已记录']; + + } + } + // 处理类型为3且id=1的任务 + elseif ($taskCenter['type'] == 3 && $taskCenter['id'] == 1) { + $sumOrderNum = 0; + + // 检查是否已领取过该任务 + if (!empty($inviteAchievement) && !empty($inviteAchievement['tasks'])) { + $tasks = explode(',', $inviteAchievement['tasks']); + if (in_array('1', array_map('trim', $tasks))) { + return ['code' => -1, 'msg' => '不可重复领取']; + } + } + + // 获取订单总数 + if (!empty($inviteAchievement)) { + $sumOrderNum = $inviteAchievement['count']; + } + + // 检查订单数量是否达标 + if ($sumOrderNum !== null && $sumOrderNum < $taskCenter['number']) { + return ['code' => -1, 'msg' => '领取失败,未达成领取条件']; + } else { + // 检查用户实名信息 + $userInfo = $user_id_slave_db->name('user_info') + ->where('user_id', $userId) + ->find(); + + if (empty($userInfo) || empty($userInfo['cert_no']) || empty($userInfo['cert_name'])) { + return ['code' => -1, 'msg' => '请实名后领取']; + } + + // 检查同一实名是否已领取 + $users = $user_id_slave_db->name('user_info') + ->where('cert_name', $userInfo['cert_name']) + ->where('cert_no', $userInfo['cert_no']) + ->select() + ->toArray(); + + $courseIds = array_column($users, 'user_id'); + $recordCount = $user_id_slave_db->name('task_center_record') + ->whereIn('user_id', $courseIds) + ->where('task_id', $taskCenter['id']) + ->count(); + + if ($recordCount > 0) { + return ['code' => -1, 'msg' => '同一实名算一个新用户,不可重复领取']; + } + } + } else { + return ['code' => -1, 'msg' => '异常领取,已记录']; + } + + // 处理奖励发放 + $records = []; + $targetId = null; + + // 查询任务奖励列表 + $rewards = Db::name('task_center_reward') + ->where('task_id', $id) + ->select() + ->toArray(); + + foreach ($rewards as $reward) { + switch ($reward['type']) { + // 金币奖励 + case 1: + $moneyDetail = [ + 'user_id' => $userId, + 'title' => '[任务中心]', + 'classify' => 7, + 'type' => 1, + 'state' => 2, + 'money' => $reward['number'], + 'content' => $taskCenter['title'] . "任务完成,金币奖励" . $reward['number'], + 'money_type' => 2, + 'source_id' => $reward['task_id'], + 'create_time' => date('Y-m-d H:i:s') + ]; + + // 更新用户金币 + UserMoney::updateMoney($userId, $reward['number']); + // 保存金币明细 + $targetId = $user_id_master_db->name('user_money_details')->insertGetId($moneyDetail); + break; + + // 现金奖励 + case 2: + $cashDetail = [ + 'user_id' => $userId, + 'title' => '[任务中心]', + 'classify' => 7, + 'type' => 1, + 'state' => 2, + 'money' => $reward['number'], + 'content' => $taskCenter['title'] . "任务完成,现金奖励" . $reward['number'], + 'money_type' => 1, + 'source_id' => $reward['task_id'], + 'create_time' => date('Y-m-d H:i:s') + ]; + + // 更新用户现金 + $user_id_master_db->name('user_money')->where('user_id', $userId)->inc('amount', $reward['number'])->update(); + UserMoney::updateAmount($userId, $reward['number']); + // 保存现金明细 + $targetId = $user_id_master_db->name('user_money_details')->insertGetId($cashDetail); + break; + } + + // 构建任务记录 + $records[] = [ + 'user_id' => $userId, + 'task_id' => $id, + 'type' => $reward['type'], + 'number' => $reward['number'], + 'name' => $taskCenter['title'], + 'target_id' => $targetId, + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s') + ]; + } + + // 批量保存任务记录 + if (!empty($records)) { + $user_id_master_db->name('task_center_record')->insertAll($records); + } + + // 更新邀请成就任务记录 + if (!empty($inviteAchievement) && $id == 1) { + $tasks = $inviteAchievement['tasks'] ?? ''; + $newTasks = empty($tasks) ? '1' : $tasks . ',1'; + + $user_id_master_db->name('invite_achievement') + ->where('user_id', $inviteAchievement['user_id']) + ->where('id', $inviteAchievement['id']) + ->update(['tasks' => $newTasks]); + } + return returnSuccessData(); + } + + + +} \ No newline at end of file diff --git a/app/czg/app/model/TbUser.php b/app/czg/app/model/TbUser.php new file mode 100644 index 0000000..fdc966e --- /dev/null +++ b/app/czg/app/model/TbUser.php @@ -0,0 +1,309 @@ +name('tb_user')->where([$field => $username])->find(); + if($data) { + return $data; + } + } + } + return null; + } + + + public static function GetByuserInvite($inviter_code) + { +// // 全表扫描username +// $dbmap = config('database.db_map'); +// $count = 0; +// foreach ($dbmap as $dbname) { +// if(!in_array($dbname, config('database.unset_db_map'))) { +// $connect = Db::connect($dbname); +// $data = $connect->name('tb_user')->where(['inviter_code' => $inviter_code])->count(); +// $count += $data; +// } +// } +// + $count = DatabaseRoute::getAllDbData('tb_user', function ($query) use ($inviter_code) { + return $query->where(['inviter_code' => $inviter_code]); + })->count(); + return $count; + } + + public static function GetByuserInviteselect($invitation_code, $page, $limit) + { + $data_list = []; + $data_arr = DatabaseRoute::paginateAllDb('tb_user', function ($query) use ($invitation_code) { + return $query->where(['inviter_code' => $invitation_code])->field('user_id,avatar,user_name,user_id'); + }, $page, $limit); + if(!empty($data_arr['list'])) { + foreach ($data_arr['list'] as $k => $v) { + $data_list[] = [ + 'user_id' => (string) $v['user_id'], + 'avatar' => $v['avatar'], + 'user_name' => $v['user_name'], + ]; + } + } + return $data_list; + + +// $dbmap = config('database.db_map'); +// $data_list = []; +// foreach ($dbmap as $dbname) { +// +// if(!in_array($dbname, config('database.unset_db_map'))) { +// $connect = Db::connect($dbname); +// $data_arr = $connect->name('tb_user')->where(['inviter_code' => $invitation_code])->field('user_id,avatar,user_name,user_id')->limit(page($page, $limit), $limit)->select()->toArray(); +// if($data_arr) { +// foreach ($data_arr as $k => $v) { +// $data_list[] = [ +// 'user_id' => $v['user_id'], +// 'avatar' => $v['avatar'], +// 'user_name' => $v['user_name'], +// ]; +// } +// } +// } +// } +// Log::write('下级列表:' . json_encode($data_arr)); +// return $data_list; + + + } + + public static function getByUserIdOrInviterCode($userId, $inviterCode) + { + $user = $userId ? DatabaseRoute::getDb('tb_user', $userId)->where([ + 'user_id' => $userId + ])->find() : null; + return $user ?? self::GetByinvitationCode($inviterCode); + } + + public static function GetByinvitationCode($invitation_code) + { + // 全表扫描username + $dbmap = config('database.db_map'); + foreach ($dbmap as $dbname) { + if(!in_array($dbname, config('database.unset_db_map'))) { + $connect = Db::connect($dbname); + $data = $connect->name('tb_user')->where(['invitation_code' => $invitation_code])->find(); + if($data) { + return $data; + } + } + } + return null; + } + + + public static function GetByinviterCodeCount($inviter_code) + { + // 全表扫描username + $dbmap = config('database.db_map'); + foreach ($dbmap as $dbname) { + if(!in_array($dbname, config('database.unset_db_map'))) { + $connect = Db::connect($dbname); + $data = $connect->name('tb_user')->where(['inviter_code' => $inviter_code])->count(); + if($data) { + return $data; + } + } + } + return null; + } + + public static function register($data):array + { + if(empty($data['password']) || empty($data['phone']) || empty($data['msg']) || empty($data['platform'])) { + return returnErrorData('参数不完整'); + } + $toUser = self::GetByusername($data['phone']); + if($toUser) { + return returnErrorData('此号码已注册'); + } + if(!Msg::checkCode($data['phone'], $data['msg'])) { + return returnErrorData('验证码错误'); + } + if(!empty($data['inviterCode'])) { + $inviter = self::GetByinvitationCode($data['inviterCode']); + if(!$inviter) { + return returnErrorData('邀请码不正确'); + } + }else { + $data['inviterCode'] = CommonInfo::where(['type' => 88])->find()->value; + $inviter = self::GetByinvitationCode($data['inviterCode']); + } + + if(empty($data['qdCode'])) { + $data['qdCode'] = $inviter['qd_code']; + }else { + $sys_user = SysUser::GetByQrcode($data['qdCode']); + if(!$sys_user) { + return returnErrorData('渠道码错误'); + } + } + $user_id = Random::generateRandomPrefixedId(19); + Db::startTrans(); + try { + $insert = [ + 'user_id' => $user_id, + 'user_name' => maskPhoneNumber($data['phone']), + 'qd_code' => $data['qdCode'], + 'phone' => $data['phone'], + 'platform' => $data['platform'], + 'create_time' => date('Y-m-d H:i:s'), + 'sys_phone' => !empty($data['sys_phone'])?:'', + 'password' => sha256Hex($data['password']), + 'status' => 1, + 'update_time' => date('Y-m-d H:i:s'), + 'rate' => CommonInfo::where(['type' => 420])->find()->value, + 'two_rate' => CommonInfo::where(['type' => 421])->find()->value, + 'invitation_code' => toSerialCode($user_id), + 'inviter_code' => $data['inviterCode'], + 'inviter_user_id' => $inviter['user_id'], + ]; + $connect_name = DatabaseRoute::getConnection('tb_user', ['user_id' => $user_id], true); + $db = Db::connect($connect_name); + $db->name('tb_user')->insertGetId($insert); + $user = $db->name('tb_user')->where(['user_id' => $user_id])->find(); + // 删除验证码 + if(Msg::delCode($data['phone'], $data['msg'])) { + if($inviter) { + // 关于上级的处理 + Invite::saveBody($user_id, $inviter); + } + Db::commit(); + return returnSuccessData([], ['user' => apiconvertToCamelCase($user)]); + } + + }catch (Exception $exception) { + Db::rollback(); + return returnErrorData($exception->getMessage()); + } + return returnErrorData('error'); + } + + + + + + + + public static function CheckPassword($userpasswoed, $formpassword):bool + { + $hash = hash('sha256', $formpassword); + return $hash === $userpasswoed; + } + + public static function checkEnable($user) + { +// $user = DatabaseRoute::getDb('tb_user', $userId)->where([ +// 'user_id' => $userId +// ])->find(); + if (!$user) { + throw new SysException("用户不存在"); + } + + if ($user['status'] != 1) { + throw new SysException("用户已禁用"); + } + + return $user; + } + + // 忘记密码 + public static function forgetPwd($phone, $pwd, $msg):array + { + if(empty($phone) || empty($pwd) || empty($msg)) { + return returnErrorData('参数不完整'); + } + $user = TbUser::GetByusername($phone); + if($user) { + if(Msg::checkCode($phone, $msg)) { + $pwd = sha256Hex($pwd); + $where = $sale = ['user_id' => $user['user_id']]; + Db::startTrans(); + try { + Common::saveDbData('tb_user', $sale, ['password' => $pwd], 'update', $where); + Msg::delCode($phone, $msg); + Db::commit(); + return returnSuccessData(); + }catch (Exception $e) { + Db::rollback(); + return returnErrorData($e->getMessage()); + } + + }else { + return returnErrorData('验证码错误'); + } + } + return returnErrorData('error'); + } + + public static function selectUserById($userId) + { + $user = DatabaseRoute::getDb('tb_user', $userId)->find(); + if ($user) { + $userVip = Db::name('user_vip')->where([ + 'user_id' => $userId + ])->find(); + if ($userVip) { + $user['member'] = $userVip['is_vip']; + $user['end_time'] = $userVip['end_time']; + } + } + + return $user; + + } + + public static function selectUserByIdNew($user) + { + $db = Db::connect(config('database.search_library')); + $userVip = $db->name('user_vip')->where(['user_id' => $user['user_id']])->find(); + $db->close(); + if ($userVip) { + $user['member'] = $userVip['is_vip']; + $user['end_time'] = $userVip['end_time']; + } + return $user; + + } + + /** + * 校验用户实名 + */ + public static function checkReal($userId) + { + $count = DatabaseRoute::getDb('user_info', $userId)->whereNotNull('cert_name')->whereNotNull('cert_no')->count(); + return $count > 0; + } + + + + + + +} \ No newline at end of file diff --git a/app/czg/app/model/TbUserBlacklist.php b/app/czg/app/model/TbUserBlacklist.php new file mode 100644 index 0000000..258ab01 --- /dev/null +++ b/app/czg/app/model/TbUserBlacklist.php @@ -0,0 +1,34 @@ +where([ + 'user_id' => $userId + ])->find(); + if ($user && $user['cert_no']) { + $this->insert([ + 'real_name' => $user['real_name'], + 'cert_no' => $user['cert_no'], + ]); + } + + DatabaseRoute::getDb('tb_user', $userId, true)->where([ + 'user_id' => $userId + ])->update([ + 'status' => 0 + ]); + + } +} \ No newline at end of file diff --git a/app/czg/app/model/UniAdCallbackRecord.php b/app/czg/app/model/UniAdCallbackRecord.php new file mode 100644 index 0000000..bd27bb7 --- /dev/null +++ b/app/czg/app/model/UniAdCallbackRecord.php @@ -0,0 +1,180 @@ +get($redisKey, 0); + // 确保返回整数类型 + return (int)$remainTime; + } + + + public static function adCallBack(array $callBackDTO) + { + $respData = []; + + $db = Db::connect(config('database.search_library')); + // 检查是否重复回调 + $record = $db->name('uni_ad_callback_record') + ->where('trans_id', $callBackDTO['trans_id']) + ->find(); + + if ($record) { + Log::write("回调重复, trans_id: {$record['trans_id']}"); + $respData['isValid'] = false; + return $respData; + } + + // 准备记录数据 + $recordData = [ + 'user_id' => (int)$callBackDTO['user_id'], + 'platform' => $callBackDTO['platform'], + 'trans_id' => $callBackDTO['trans_id'], + 'adpid' => $callBackDTO['adpid'], + 'provider' => $callBackDTO['provider'], + 'sign' => $callBackDTO['sign'], + 'extra' => $callBackDTO['extra'], + 'create_time' => date('Y-m-d H:i:s'), + 'is_ended' => 1 + ]; + $security = 'cbc34e14ee6d64738557c96623dd6da89f77cac8ae519c6a5c87191d614d386a'; + // 签名验证 + $flag = self::validateSign($security, $callBackDTO['trans_id'], $callBackDTO['sign']); + if (!$flag) { + $recordData['err_msg'] = "签名验证失败"; + Log::write(json_encode($recordData)); + Db::name('uni_ad_callback_record')->insert($recordData); + $respData['isValid'] = false; + return $respData; + } + + // 检查用户是否存在 + $userEntity = Db::name('user_entity') + ->where('id', $callBackDTO['user_id']) + ->find(); + + if (!$userEntity) { + $recordData['err_msg'] = "用户不存在"; + Log::warning(self::getBaseErrInfo($recordData)); + Db::name('uni_ad_callback_record')->insert($recordData); + $respData['isValid'] = false; + return $respData; + } + + // 根据extra字段处理不同逻辑 + if (!str_contains($callBackDTO['extra'], "cash")) { + // 获取配置信息 + $info = Db::name('common_info') + ->where('id', 921) + ->find(); + + if (!$info || empty($info['value'])) { + $recordData['err_msg'] = "CommonInfo时长时间未配置"; + Log::warning(self::getBaseErrInfo($recordData)); + Db::name('uni_ad_callback_record')->insert($recordData); + $respData['isValid'] = false; + return $respData; + } + + // 设置免费观看时间(分钟转秒) + self::setFreeWatchTime($recordData['user_id'], (int)$info['value'] * 60, true); + } else { + // 设置可提现标志 + $recordId = Db::name('uni_ad_callback_record')->insertGetId($recordData); + self::setCanCashFlag($userEntity['id'], $recordId); + return ['isValid' => true]; // 提前返回,避免重复插入 + } + + // 保存记录 + Db::name('uni_ad_callback_record')->insert($recordData); + + // 返回成功响应 + $respData['isValid'] = true; + return $respData; + } + + /** + * 验证签名 + * @param string $securityKey 安全密钥 + * @param string $transId 交易ID + * @param string $sign 签名 + * @return bool 验证结果 + */ + public static function validateSign(string $securityKey, string $transId, string $sign): bool + { + // 实际签名验证逻辑(根据业务需求实现) + $expectedSign = self::generateSign($securityKey, $transId); + return $expectedSign === $sign; + } + + /** + * 设置免费观看时间 + * @param int $userId 用户ID + * @param int $duration 时长(秒) + * @param bool $isPermanent 是否永久 + */ + public static function setFreeWatchTime(int $userId, int $duration, bool $isPermanent) + { + $key = $isPermanent ? "free_watch:permanent:{$userId}" : "free_watch:normal:{$userId}"; + Cache::set($key, $duration, $duration); // 缓存时间等于有效时长 + } + + /** + * 设置可提现标志 + * @param int $userId 用户ID + * @param int $recordId 记录ID + */ + public static function setCanCashFlag(int $userId, int $recordId) + { + Cache::set("can_cash:{$userId}", $recordId, 86400); // 缓存24小时 + } + + /** + * 获取基础错误信息 + * @param array $record 记录数据 + * @return string 错误信息 + */ + public static function getBaseErrInfo(array $record): string + { + return "广告回调错误: [user_id={$record['user_id']}, trans_id={$record['trans_id']}, err_msg={$record['err_msg']}]"; + } + + /** + * 生成签名 + * @param string $secret 安全密钥 + * @param string $transId 交易ID + * @return string 生成的签名(十六进制字符串) + */ + public static function generateSign(string $secret, string $transId): string + { + // 生成待加密的字符串 + $data = $secret . ':' . $transId; + + // 使用SHA-256生成签名(返回十六进制字符串) + return hash('sha256', $data); + } + + + + + +} \ No newline at end of file diff --git a/app/czg/app/model/UserInfo.php b/app/czg/app/model/UserInfo.php new file mode 100644 index 0000000..76bd8a5 --- /dev/null +++ b/app/czg/app/model/UserInfo.php @@ -0,0 +1,53 @@ +name('user_info')->where(['cert_name' => $cert_name, 'cert_no' => $cert_no])->field('user_id')->select()->toArray(); +// if($data_arr) { +// foreach ($data_arr as $k => $v) { +// $data[] = $v['user_id']; +// } +// } +// } +// } + + + $data_arr = DatabaseRoute::getAllDbData('user_info', function ($query) use ($cert_no, $cert_name) { + return $query->where(['cert_name' => $cert_name, 'cert_no' => $cert_no])->field('user_id'); + })->select()->toArray(); + if($data_arr) { + foreach ($data_arr as $k => $v) { + $data[] = $v['user_id']; + } + } + return $data; + + } + + public static function getByUserIdOrSave(int $userId) + { + $userInfo = DatabaseRoute::getDb('user_info', $userId)->find(); + if (!$userInfo) { + $id = DatabaseRoute::getDb('user_info', $userId, true)->insertGetId([ + 'user_id' => $userId + ]); + $userInfo['id'] = $id; + $userInfo['user_id'] = $userId; + } + return $userInfo; + } +} \ No newline at end of file diff --git a/app/czg/app/model/UserMoney.php b/app/czg/app/model/UserMoney.php new file mode 100644 index 0000000..3d17512 --- /dev/null +++ b/app/czg/app/model/UserMoney.php @@ -0,0 +1,121 @@ + $user_id]; + $money = DatabaseRoute::getDb('user_money', $user_id)->where($where)->find(); + if(!$money) { + $database_name = DatabaseRoute::getConnection('user_money', $sale, true); + $money = [ + 'user_id' => $user_id, + 'money' => 0.00, + 'amount' => 0.00, + ]; + Db::connect($database_name)->name('user_money')->insert($money); + } + if($money['money'] == 0) { + $money['money'] = 0; + } + if($money['amount'] == 0) { + $money['amount'] = 0; + } + $money['amount'] = sprintf('%.2f', $money['amount']); + $money['money'] = sprintf('%.2f', $money['money']); + $money = apiconvertToCamelCase($money); + return returnSuccessData($money); + } + public static function selectUserMoneyfind($user_id) + { + $db = DatabaseRoute::getDb('user_money', $user_id); + $data = $db->where([ 'user_id' => $user_id])->find(); + return $data; + } + + + public static function queryUserMoneyDetails($user_id, $get) + { + $user_where = $sale = ['user_id' => $user_id]; + $where = []; + if(!empty($get['classify'])) { + $where['classify'] = $get['classify']; + } + if(!empty($get['type'])) { + $where['type'] = $get['type']; + } + if(!empty($get['moneyType'])) { + $where['money_type'] = $get['moneyType']; + } + if(!empty($get['classify'])) { + $where['classify'] = $get['classify']; + } + $money = DatabaseRoute::getDb('user_money_details', $user_id)->where($user_where)->where($where); + if(!empty($get['viewType']) && $get['viewType'] == 1) { + $money = $money->whereIn('classify', [1,6]); + } + $count = $money->count(); + $money = $money->order('create_time','desc')->limit(page($get['page'], $get['limit']), $get['limit'])->select()->toArray(); + foreach ($money as $k => &$v) { + $v['money'] = sprintf('%.2f', $v['money']); + } + return returnSuccessData([ + 'currPage' => $get['page'], + 'pageSize' => $get['limit'], + 'records' => apiconvertToCamelCase($money), + 'totalCount' => $count, + 'totalPage' => ceil($count / $get['limit']), + ]); + } + + public static function updateAmount($userId, $money, $isIncr=true) + { + $userMoney = self::selectUserMoneyfind($userId); + if (!$userMoney) { + DatabaseRoute::getDb('user_money', $userId, true)->insert([ + 'user_id' => $userId, + 'money' => 0, + 'amount' => 0 + ]); + } + $money = floatval($money); + $model = DatabaseRoute::getDb('user_money', $userId, true, true); + if ($isIncr) { + $model->inc('amount', $money); + }else{ + $model->dec('amount', $money); + } + $model->update(); + } + + public static function updateMoney($userId, $money, $isIncr=true) + { + $userMoney = self::selectUserMoneyfind($userId); + if (!$userMoney) { + DatabaseRoute::getDb('user_money', $userId, true)->insert([ + 'user_id' => $userId, + 'money' => 0, + 'amount' => 0 + ]); + } + $money = floatval($money); + $model = DatabaseRoute::getDb('user_money', $userId, true, true); + if ($isIncr) { + $model->inc('money', $money); + }else{ + $model->dec('money', $money); + } + $model->update(); + } + +} \ No newline at end of file diff --git a/app/czg/app/model/UserMoneyDetails.php b/app/czg/app/model/UserMoneyDetails.php new file mode 100644 index 0000000..c927af4 --- /dev/null +++ b/app/czg/app/model/UserMoneyDetails.php @@ -0,0 +1,15 @@ +name('user_prize_exchange'); + + if (!is_null($foreignId)) { + $query = $query->where('foreign_id', $foreignId); + } + + if (!empty($foreignType)) { + $query = $query->where('foreign_type', $foreignType); + } + + if (!is_null($userId)) { + $query = $query->where('user_id', $userId); + } + + if (!empty($userName)) { + $query = $query->where('user_name', 'like', "%{$userName}%"); + } + + if (!empty($prizeName)) { + $query = $query->where('prize_name', 'like', "%{$prizeName}%"); + } + + if (!is_null($status)) { + $query = $query->where('status', $status); + } + + if (!empty($phone)) { + $query = $query->where('phone', 'like', "%{$phone}%"); + } + + if (!empty($remark)) { + $query = $query->where('remark', 'like', "%{$remark}%"); + } + + if (!empty($beginDate)) { + $query = $query->where('create_time', '>=', "{$beginDate} 00:00:00"); + } + + if (!empty($endDate)) { + $query = $query->where('create_time', '<=', "{$endDate} 23:59:59"); + } + $count = $query->count(); + // 设置排序 + $query = $query->order('id', 'desc'); + + // 分页参数 + $pageNum = isset($params['page']) ? (int)$params['page'] : 1; + $pageSize = isset($params['limit']) ? (int)$params['limit'] : 10; + + // 执行分页查询 + $list = $query->limit(page($pageNum, $pageSize), $pageSize)->select()->toArray(); + + return returnSuccessData([ + 'currPage' => $pageNum, + 'pageSize' => $pageSize, + 'list' => ($list), + 'totalCount' => $count, + 'totalPage' => ceil($count / $pageSize), + ]); + } +} \ No newline at end of file diff --git a/app/czg/app/model/UserSignRecord.php b/app/czg/app/model/UserSignRecord.php new file mode 100644 index 0000000..637d03b --- /dev/null +++ b/app/czg/app/model/UserSignRecord.php @@ -0,0 +1,233 @@ +where(['day' => 7])->where('create_time', '>', $day)->order('create_time', 'acs') + ->field('id') + ->select() + ->toArray(); + $taskWCount = Db::connect(DatabaseRoute::getConnection('user_sign_record', ['user_id' => $user_id])); + $taskWCount = + $taskWCount + ->name('user_sign_record') + ->alias('sign') + ->field([ + 'sign.id as id', + "{$wCount} - COUNT(CASE WHEN spRecord.source_id IS NOT NULL THEN 1 END) AS `day`" + ]) + ->leftJoin('disc_spinning_record spRecord', 'sign.id = spRecord.source_id') + ->where('sign.day', 7) + ->where('sign.user_id', $user_id) + ->where('sign.create_time', '>', $day) + ->whereIn('sign.id', $noRecordTasks) + ->group('sign.id') + ->order('sign.create_time', 'asc') + ->select(); + if($taskWCount) { + $days = 0; + $task_reward = []; + foreach ($taskWCount as $tk => $tv) { + $task_reward[][$tv['id']] = $days += $tv['day']; + } + return $task_reward; + } + return null; + + } + + + public static function getWSignCount($user_id) + { + $user_sign_record_db = DatabaseRoute::getDb('user_sign_record', $user_id); + $noRecordTasks = $user_sign_record_db->where('create_time', '>', date('Y-m-d 00:00:00', strtotime('-1 day')))->order('create_time', 'desc') + ->field('day') + ->find(); + return $noRecordTasks?$noRecordTasks['day']:null; + } + + public static function getUserSignCount($user_id) + { + $user_sign_record_db = DatabaseRoute::getDb('user_sign_record', $user_id); + $noRecordTasks = $user_sign_record_db->where('sign_day', '>', date('Y-m') . '-00')->order('create_time', 'asc') + ->count(); + return $noRecordTasks; + } + + public static function getTaskCenterRecordCount($user_id, $task_id, $day_date) + { + $data = DatabaseRoute::getDb('task_center_record', $user_id)->where(['task_id' => $task_id]); + if($day_date) { + $data = $data->where('create_time', '>', $day_date); + } + return $data->count(); + } + public static function getTaskCenterRecordCountUserIdAll($user_id_s, $task_id):string|int + { + $count = 0; + foreach ($user_id_s as $k => $user_id) { + $count += DatabaseRoute::getDb('task_center_record', $user_id)->where(['task_id' => $task_id])->count(); + } + return $count; + } + + public static function getUserSignData($user_id, $user) + { + $dto = [ + 'userId' => $user_id, + 'username' => $user['user_name'], + 'mobile' => $user['phone'], + 'signDays' => 0, + 'enable' => 1, + 'isReceived' => 0, + ]; + $config = CommonInfo::where(['type' => 918])->find()->value; + if(!$config) { + return returnErrorData('签到活动配置不存在'); + } + // 活动天数 + $parts = explode(',', $config); + $activeDays = isset($parts[0]) ? (int)$parts[0] : 0; + // 当前日期 + $beginDay = date('Y-m-d'); + // 签到记录 + $recordList = []; + // 连续签到日期 + $flowDays = buildFlowDays($beginDay, $activeDays); + $list = DatabaseRoute::getDb('user_sign_record', $user_id)->order('sign_day', 'asc')->order('create_time', 'asc')->order('id', 'asc')->select()->toArray(); + // 第x天 + $index = 1; + // 连续签到天数 + $signDays = 0; + if(!$list) { + foreach ($flowDays as $k => $day) { + $recordList[] = [ + 'showText' => '第' . $index . '天', + 'status' => 0, + 'signDay' => $day, + 'signDate' => '', + ]; + $index ++; + } + $dto['recordList'] = $recordList; + return returnSuccessData($dto); + } + $beginSignDay = $list[0]['sign_day']; + $enDay = date('Y-m-d', strtotime('+' . $activeDays - 1 . 'days', strtotime(date('Y-m-d')))); + + $flowDays = buildFlowDaysTwo($beginSignDay, $enDay); + // 需要移除的记录 + $removeList = []; + $signMap = []; + foreach ($list as $record) { + $signMap[$record['sign_day']] = $record['create_time']; + } + + foreach ($flowDays as $k => $day) { + $date = !empty($signMap[$day])?$signMap[$day]:''; + if($date) { + $recordList[$k] = [ + 'signDay' => $day, + 'status' => 1, + 'signDate' => $date, + 'showText' => '已签到', + ]; + $signDays ++; + }else { + $recordList[$k] = [ + 'signDay' => $day, + 'status' => 0, + 'signDate' => '', + ]; + $daysBetween = daysBetween($day); + if($daysBetween > 0) { + $signDays = 0; + $recordList[$k]['showText'] = '未签到'; + }elseif($daysBetween == 0) { + $recordList[$k]['showText'] = '待签到'; + }else { + $recordList[$k]['showText'] = '第' . $k + 1 .'天'; + } + } + if($signDays == $activeDays) { + break; + } + + // 1. 检查记录日期是否等于结束日期 + + if ($day === $enDay) { + // 2. 反转记录列表 + $tempList = array_reverse($recordList); + // 3. 计算今天且状态为"1"的记录数 + $today = date('Y-m-d'); + $isInclude = 0; + + foreach ($tempList as $item) { + if ($item['signDay'] === $today && $item['status'] == 1) { + $isInclude++; + } + } + // 4. 确定要移除的记录数量 + $removeSize = $signDays; + if ($isInclude > 0) { + $removeSize = $signDays - 1; + } + // 5. 添加要移除的记录到 removeList + for ($i = 0; $i < $removeSize; $i++) { + if (isset($tempList[$i])) { // 确保索引存在 + $removeList[] = $tempList[$i]; + } + } + break; + } + } + // 1. 移除元素 + $recordList = array_udiff($recordList, $removeList, function($a, $b) { + return ($a === $b) ? 0 : 1; + }); + // 2. 第一次反转 + $recordList = array_reverse($recordList); + // 3. 截取前 activeDays 条记录 + $recordList = array_slice($recordList, 0, $activeDays, true); + // 4. 第二次反转 + $recordList = array_reverse($recordList); + $index = 1; + foreach ($recordList as $k => $v) { + $recordList[$k]['showText'] = sprintf($v['showText'], '第' . $index . '天'); + $index ++; + } + $dto['recordList'] = $recordList; + $dto['signDays'] = $signDays; + if($signDays >= $activeDays) { + $dto['enable'] = 0; + } + + $count = DatabaseRoute::getDb('user_money_details', $user_id)->where('type', 1) + ->where('classify', 7) + ->where('money_type', 1) + ->where('title', 'like', '[连续签到%') + ->where('title', 'like', '%天]') + ->count(); + $dto['isReceived'] = $count > 0 ? 1 : 0; + return returnSuccessData($dto); + } + +} \ No newline at end of file diff --git a/app/czg/app/model/WithDraw.php b/app/czg/app/model/WithDraw.php new file mode 100644 index 0000000..f31e421 --- /dev/null +++ b/app/czg/app/model/WithDraw.php @@ -0,0 +1,243 @@ + 0, + 'money' => $amount, + 'user_id' => $userId, + 'user_type' => $isSys ? 2 : 1, + 'state' => 0, + 'rate' => 0, + 'create_at' => getNormalDate(), + 'withdraw_type' => 1, + 'order_number' => uuid(), + ]; + + $moneyDetails = [ + 'user_id' => $userId, + 'sys_user_id' => $userId, + 'title' => '[提现]', + 'content' => "提现:{$amount}元", + 'type' => 2, + 'state' => 2, + 'classify' => 4, + 'money' => $amount, + 'create_time' => getNormalDate(), + 'money_type' => 1, + ]; + + $userMoney = []; + $flag = true; + + // 系统用户提现 + if ($isSys) { + $user = DatabaseRoute::getDb('sys_user', $userId)->find(); + $msgCount = Db::name('msg')->where([ + 'phone' => $user['mobile'], + 'code' => $msg + ])->count(); + if (!$msgCount) { + throw new SysException("验证码不正确"); + } + + if (empty($user['zhi_fu_bao']) || empty($user['zhi_fu_bao_name'])) { + throw new SysException([ + 'code' => 9999, + 'msg' => '请先绑定支付宝账号' + ]); + } + $cashInfo['zhifubao'] = $user['zhi_fu_bao']; + $cashInfo['zhifubao_name'] = $user['zhi_fu_bao_name']; + $cashInfo['bank_name'] = ''; + + // 校验余额 + $userMoney = Db::name('sys_user_money')->where([ + 'user_id' => $userId + ])->find(); + }else{ + $userMoney = DatabaseRoute::getDb('user_money', $userId)->find(); + $user = TbUser::selectUserById($userId); + if ($user['status'] != 1) { + throw new SysException([ + 'code' => 9999, + 'msg' => $user['status'] == 0 ? '账号不存在': '账号已被禁用,请联系客服处理' + ]); + } + + $userInfo = DatabaseRoute::getDb('user_info', $userId)->find(); + if (!$userInfo || empty($userInfo['cert_name'])) { + throw new SysException([ + 'code' => 9991, + 'msg' => '请先实名认证' + ]); + } + + if (empty($userInfo['account_no']) || empty($userInfo['mobile'])) { + throw new SysException([ + 'code' => 9991, + 'msg' => '需重新完成实名认证后才可提现' + ]); + } + + if (empty($userInfo['bank_name'] && !empty($userInfo['resp_json']))) { + DatabaseRoute::getDb('user_info', $userId, true, true)->update([ + 'bank_name' => self::getBankName($userInfo['resp_json']) + ]); + } + + if (empty($userInfo['bank_name'])) { + DatabaseRoute::getDb('user_info', $userId, true, true)->delete(); + throw new SysException([ + 'code' => 9991, + 'msg' => '需重新完成实名认证后才可提现' + ]); + } + + if ($isAlipay) { + $cashInfo['zhifubao'] = $user['zhi_fu_bao']; + $cashInfo['zhifubao_name'] = $user['zhi_fu_bao_name']; + + if ($userInfo['cert_name'] != $user['zhi_fu_bao_name']) { + throw new SysException([ + 'code' => 9991, + 'msg' => '认证名称不一致,请重新确认!' + ]); + } + }else{ + $cashInfo['zhifubao'] = $userInfo['account_no']; + $cashInfo['zhifubao_name'] = $userInfo['cert_name']; + } + + $cashInfo['bank_name'] = $userInfo['bank_name']; + $cashInfo['id_card_no'] = $userInfo['cert_no']; + $cashInfo['province'] = $userInfo['province']; + $cashInfo['city'] = $userInfo['city']; + $cashInfo['bank_branch'] = $userInfo['bank_branch']; + + // 校验黑名单用户 + $count = Db::name('tb_withdraw_blacklist')->where([ + 'real_name' => $cashInfo['zhifubao_name'] + ])->count(); + + $blackCount = Db::name('tb_user_blacklist')->where([ + 'id_card_no' => trim($userInfo['cert_no']) + ])->count(); + if ($blackCount) { + $cashInfo['state'] = 2; + $cashInfo['out_at'] = getNormalDate(); + $moneyDetails['content'] = "刷单用户禁止提现:$amount"; + $flag = false; + }else if($count) { + $cashInfo['state'] = 3; + $cashInfo['content'] = "提现=$amount"; + $moneyDetails['relation_id'] = '提现黑名单用户,请谨慎审核!'; + $flag = false; + } + } + + $info = (new CommonInfo())->getByCode(112); + if (!$info || bccomp($amount, $info['value'], 2) < 0) { + throw new SysException("不满足最低提现金额,请重新输入!"); + } + + // 校验余额 + if (bccomp($userMoney['amount'], $amount, 2) < 0) { + throw new SysException("可提现余额不足"); + } + + self::checkCanCash($userId, 1, $amount); + + + if($flag) { + $cashInfo['state'] = 4; + $resp = WuYouPayUtils::extractOrder($isAlipay, $cashInfo['order_number'], $userId, $amount, $isSys, $cashInfo['zhifubao'], $cashInfo['zhifubao_name'], + $cashInfo['bank_name'], !empty($cashInfo['province'])?$cashInfo['province']:'', !empty($cashInfo['city'])?$cashInfo['city']:'', !empty($cashInfo['bank_branch'])?$cashInfo['bank_branch']:''); + if (isset($resp['status']) && ($resp['status'] == 2 || $resp['status'] == 10000)) { + $cashInfo['content'] = "成功提现:$amount"; + $cashInfo['state'] = 1; + $cashInfo['out_at'] = getNormalDate(); + } + + if (!empty($resp['error_msg'])){ + throw new SysException($resp['error_msg']); + } + } + // 插入提现记录 + unset($cashInfo['id_card_no']); + unset($cashInfo['content']); + DatabaseRoute::getDb('cash_out', $userId, true)->insert($cashInfo); + + DatabaseRoute::getDb($isSys ? 'sys_user_money_details' : 'user_money_details', $userId, true)->insert($moneyDetails); + if ($isSys) { + Db::name('sys_user_money')->where([ + 'user_id' => $userId + ])->dec('amount', floatval($amount))->update(); + }else{ + DatabaseRoute::getDb('user_money', $userId, true)->where([ + 'user_id' => $userId + ])->dec('amount', floatval($amount))->update(); + } + + + } + + private static function checkCanCash($userId, $type, $money) + { + + if ($type == 1) { + // 查询当日体现次数 + $count = DatabaseRoute::getDb('cash_out', $userId)->where([ + ['state', 'in', [1, 3]], + 'withdraw_type' => 1, + ['create_at', '>=', date('Y-m-d 00:00:00')], + ])->count(); + + $commonInfo = (new CommonInfo())->getByCode(922); + if (!$commonInfo) { + throw new SysException("【922】每日提现次数上限未配置"); + } + + if ($count >= intval($commonInfo['value'])) { + throw new SysException("超过当日提现限制次数{$commonInfo['value']}次,请明天再试!"); + } + + $commonInfo = (new CommonInfo())->getByCode(923); + if (!$commonInfo) { + throw new SysException("【923】单次提现超额未配置"); + } + if (bccomp($money, $commonInfo['value']) > 0) { + throw new SysException("单次提现超额"); + } + } + + } + + private static function getBankName($json) + { + + $resp = json_decode($json); + $result = $resp['result']; + $errorCode = $resp['error_code']; + $respCode = $resp['respCode']; + if ($errorCode == 0 && $respCode == '0') { + return $result['bancardInfor'] ? $result['bancardInfor']['bankName'] : ''; + } + return ''; + + } + +} \ No newline at end of file diff --git a/app/enums/ErrEnums.php b/app/enums/ErrEnums.php new file mode 100644 index 0000000..838bded --- /dev/null +++ b/app/enums/ErrEnums.php @@ -0,0 +1,16 @@ +500 + }; + } + +} \ No newline at end of file diff --git a/app/exception/CzgException.php b/app/exception/CzgException.php new file mode 100644 index 0000000..9f75407 --- /dev/null +++ b/app/exception/CzgException.php @@ -0,0 +1,35 @@ + '', + 'args' => [], + 'code' => 500 + ], ...$args) + { + if ($data instanceof ErrEnums) { + parent::__construct($data->value, $data->code()); + + }else if (is_string($data)) { + if (!empty($args)) { + $data = format($data, $args); + + } + parent::__construct($data, 500); + } + else{ + $val = $data['msg']; + if (isset($data['args'])) { + $val = format($data['msg'], $data['args']); + } + parent::__construct($val, isset($data['code']) ?? 500); + } + } + +} \ No newline at end of file diff --git a/app/exception/HttpResponseException.php b/app/exception/HttpResponseException.php new file mode 100644 index 0000000..a576e45 --- /dev/null +++ b/app/exception/HttpResponseException.php @@ -0,0 +1,22 @@ +response = $response; + } + + // 获取响应对象 + public function getResponse() + { + return $this->response; + } +} diff --git a/app/exception/SysException.php b/app/exception/SysException.php new file mode 100644 index 0000000..4f2a0ad --- /dev/null +++ b/app/exception/SysException.php @@ -0,0 +1,35 @@ + '', + 'args' => [], + 'code' => 500 + ], ...$args) + { + if ($data instanceof ErrEnums) { + parent::__construct($data->value, $data->code()); + + }else if (is_string($data)) { + if (!empty($args)) { + $data = format($data, $args); + + } + parent::__construct($data, 500); + } + else{ + $val = $data['msg']; + if (isset($data['args'])) { + $val = format($data['msg'], $data['args']); + } + parent::__construct($val, isset($data['code']) ?? 500); + } + } + +} \ No newline at end of file diff --git a/app/utils/AliUtils.php b/app/utils/AliUtils.php new file mode 100644 index 0000000..c028548 --- /dev/null +++ b/app/utils/AliUtils.php @@ -0,0 +1,94 @@ + $accountNo, + 'name' => $name, + 'idCardCode' => $idCard, + 'bankPreMobile' => $bankPreMobile + ]); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15 + ]); + + $response = curl_exec($ch); + curl_close($ch); + + Log::info("阿里云四要素: {$idCard}, {$name}, {$accountNo}, {$bankPreMobile}, 结果: {$response}"); + + $ret = json_decode($response, true); + if (!isset($ret['error_code']) || !isset($ret['result'])) { + throw new SysException("阿里云接口返回格式错误".$response); + } + + $errorCode = $ret['error_code']; + $result = $ret['result']; + $respCode = $result['respCode'] ?? ''; + + if ($errorCode == 0 && $respCode === "0") { + // 验证通过 + } else { + throw new SysException($result['respMsg'] ?? '认证失败'); + } + + $bankName = $result['bancardInfor']['bankName'] ?? null; + + return [ + 'bankName' => $bankName, + 'respJson' => $response + ]; + } + + private static function isValidIdCard($idCard) + { + // 简单校验身份证号 + return preg_match('/^\d{17}[\dXx]$/', $idCard); + } +} \ No newline at end of file diff --git a/app/utils/JwtUtils.php b/app/utils/JwtUtils.php new file mode 100644 index 0000000..7ac0e26 --- /dev/null +++ b/app/utils/JwtUtils.php @@ -0,0 +1,86 @@ +secret = base64_decode($this->secret); + } + + + /** + * 生成 JWT Token + */ + public function generateToken($userId, string $type): string + { + $now = time(); + $payload = [ + 'sub' => (string)$userId, + 'type' => $type, + 'iat' => $now, + 'exp' => $now + $this->expire + ]; + + return JWT::encode($payload, $this->secret, 'HS512'); + } + + /** + * 从 token 中解析出 Claims + */ + public function getClaimByToken(string $token): ?object + { + try { + return JWT::decode($token, new Key($this->secret, 'HS512')); + } catch (\Exception $e) { + error_log('Token 验证失败: ' . $e->getMessage()); + return null; + } + } + + /** + * 判断 token 是否过期(通过传入的 exp 字段) + */ + public function isTokenExpired(int $exp): bool + { + return $exp < time(); + } + + // Getter/Setter + public function getSecret(): string + { + return $this->secret; + } + + public function getExpire(): int + { + return $this->expire; + } + + public function getHeader(): string + { + return $this->header; + } + + public function setSecret(string $secret): void + { + $this->secret = $secret; + } + + public function setExpire(int $expire): void + { + $this->expire = $expire; + } + + public function setHeader(string $header): void + { + $this->header = $header; + } +} diff --git a/app/utils/RedisUtils.php b/app/utils/RedisUtils.php new file mode 100644 index 0000000..4d8b051 --- /dev/null +++ b/app/utils/RedisUtils.php @@ -0,0 +1,230 @@ +set($key, (string)$orderId, 60); + + // 获取 Redis 原生实例 + $redis = Cache::store('redis')->handler(); + + // 使用 scan 非阻塞统计 key 数量 + $pattern = "createOrder:{$userId}:*"; + $iterator = null; + $count = 0; + + while ($keys = $redis->scan($iterator, $pattern, 100)) { + $count += count($keys); + } + + // 超过 22 返回 true + return $count > 22; + } + + /** + * 通用的“按周统计”计数器(自动按课程 ID、业务类型区分) + * + * @param string $prefix 前缀标识,如 'course:week_play:' + * @param int|string $id 主体 ID(如课程 ID) + * @param int $expireSeconds 默认过期时间(秒),默认 7 天 + * @return int 返回当前统计值 + */ + public static function incrWeekCounter(string $prefix, $id, int $expireSeconds = 604800): int + { + $key = $prefix . date('oW') . ':' . $id; // oW 为当前“ISO 年 + 周数”,确保每周一个 key + $count = Cache::store('redis')->inc($key); + + // 第一次设置时设定过期时间 + if ($count === 1) { + Cache::store('redis')->expire($key, $expireSeconds); + } + + return $count; + } + + + public static function getconfig() + { + return Cache::getConfig(); + } + + /** + * 获取指定“周统计”计数值 + * + * @param string $prefix 前缀 + * @param int|string $id 主体 ID + * @return int + */ + public static function getWeekCounter(string $prefix, $id): int + { + $key = $prefix . date('oW') . ':' . $id; + return (int)Cache::store('redis')->get($key, 0); + } + + public static function isCanCash($userId) + { + $val = cache(self::CAN_CASH . $userId); + return !empty($val); + } + + public static function checkRealCount(int $userId) + { + $key = 'updateAuthCertInfo:' . $userId; + $val = cache($key); + if (!empty($val)) { + throw new SysException("实名修改失败: 每月可修改次数已用完,请联系管理员"); + } + } + + public static function setRealCount(int $userId) + { + $key = 'updateAuthCertInfo:' . $userId; + cache($key, 2628000); + } + + private static function getFreeWatchKey($userId, $permanently = false) + { + if ($permanently) { + return "free:watch:" . $userId; + } + + return "free:watch:" . date('Y-m-d') . ':' . $userId; + } + + public static function isExpiredSet($key) + { + $redis = Cache::store('redis')->handler(); + + // 获取 key 的剩余过期时间(单位:秒) + $ttl = $redis->ttl($key); + + if ($ttl == -1) { + return false; + } + return $ttl !== -2; + } + + public static function getFreeWatchTimeIsExpire($userId) + { + $freeWatchKey = self::getFreeWatchKey($userId, true); + $permanentlyFreeWatch = cache($freeWatchKey); + + $watchKey = self::getFreeWatchKey($userId, false); + $payFreeWatchInfo = cache($watchKey); + + $expireTime = -1; + $jsonObject = null; + + if (!empty($payFreeWatchInfo)) { + $jsonObject = json_decode($payFreeWatchInfo, true); + $expireTime = isset($jsonObject['expireTime']) ? $jsonObject['expireTime'] : -1; + } + + $now = time(); // 当前时间戳 + + if ((!empty($permanentlyFreeWatch) && RedisUtils::isExpiredSet($freeWatchKey)) || + (!empty($permanentlyFreeWatch) && $now >= $expireTime)) { + + if (empty($permanentlyFreeWatch)) { + return null; + } + + if (!RedisUtils::isExpiredSet($freeWatchKey)) { + cache($freeWatchKey, intval($permanentlyFreeWatch)); + } + return false; + } else { + if (empty($payFreeWatchInfo)) { + return null; + } + + $second = isset($jsonObject['second']) ? intval($jsonObject['second']) : 0; + + if ($expireTime == -1) { + $jsonObject['expireTime'] = $now + $second; + + $tomorrowZero = strtotime(date('Y-m-d', strtotime('+1 day'))); + $expire = $tomorrowZero - $now; + + cache($watchKey, json_encode($jsonObject), $expire); + return false; + } else { + return $now > $expireTime; + } + } + } + + + public static function setFreeWatchTime($userId, $second, $isPermanently = false) + { + $now = time(); + $tomorrow = strtotime(date('Y-m-d', strtotime('+1 day'))); + $freeWatchKey = self::getFreeWatchKey($userId, $isPermanently); + + if ($isPermanently) { + $data = cache($freeWatchKey); + if (empty($data)) { + // 永久的,但不设置过期时间(-1) + cache($freeWatchKey, $second); + } else { + $expire = Cache::store('redis')->handler()->ttl($freeWatchKey); + if ($expire === -1 || $expire === false || $expire === null) { + $expire = -1; + } else { + $expire += intval($second); + } + + $newValue = intval($data) + intval($second); + if ($expire > 0) { + cache($freeWatchKey, $newValue, $expire); + } else { + cache($freeWatchKey, $newValue); + } + } + } else { + // 非永久,临时有效期到明天零点 + $expire = $tomorrow - $now; + + $jsonObject = [ + 'expireTime' => -1, + 'second' => intval($second) + ]; + + // 如果不存在才设置 + if (!Cache::store('redis')->has($freeWatchKey)) { + cache($freeWatchKey, json_encode($jsonObject), $expire); + } + } + } + + public static function setCanCashFlag(mixed $userId, int $param) + { + cache("cash:canCash:".$userId, $param,300); + } + +} \ No newline at end of file diff --git a/app/utils/WuYouPayUtils.php b/app/utils/WuYouPayUtils.php new file mode 100644 index 0000000..2500a17 --- /dev/null +++ b/app/utils/WuYouPayUtils.php @@ -0,0 +1,192 @@ + $value) { + $signStr .= $key . '=' . $value . '&'; + } + + $signStr .= 'key=' . self::$secret; + + return strtoupper(md5($signStr)); + } + + private static function getBaseParams() + { + self::boot(); + return [ + 'mch_id' => self::$mchId, + 'timestamp' => time(), + ]; + } + + public static function payOrderTest($orderNo, $userId, $amount, $userAgent, $allId, $payType) { + return http_post('192.168.1.21:8080/api/delay', [ + 'callbacks' => 'CODE_SUCCESS', + 'order_sn' => uuid(), + 'out_trade_no' => "{$orderNo}-{$userId}", + 'pay_time' => time(), + 'sign' => '', + 'total' => $amount + + ]); + } + + public static function payOrder($orderNo, $userId, $amount, $userAgent, $allId, $payType) + { + $payConfig = (new CommonInfo())->getByCode(926)['value'] ?? '0'; + if ($payConfig !== '1') { + throw new SysException('暂无支付渠道'); + } + + $params = self::getBaseParams(); + $params['type'] = '6001'; + $params['is_code'] = '1'; + $params['out_trade_no'] = "{$orderNo}-{$userId}"; + $params['total'] = $amount; + $params['notify_url'] = self::$notifyUrl; + + $params['sign'] = self::getParamsSign($params); + if ($payType === 'h5') { + $params['return_url'] = self::$h5BaseUrl . $allId; + } + + + return self::requestPost(self::$payUrl, $params, $userAgent); + } + + /** + * 发起 POST 请求 + * @param string $url 请求地址 + * @param array $params 请求参数 + * @param string $userAgent User-Agent + * @return array|null + */ + private static function requestPost(string $url, array $params, string $userAgent): ?array + { + Log::info("[支付]请求地址:{$url},请求参数:".json_encode($params).",User-Agent:{$userAgent}"); + $curl = curl_init(); + + curl_setopt_array($curl, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($params), + CURLOPT_HTTPHEADER => [ + 'User-Agent: ' . $userAgent, + 'Content-Type: application/x-www-form-urlencoded', +// 'Content-Type: application/json', + ] + ]); + + $response = curl_exec($curl); + curl_close($curl); + Log::info("[支付]返回结果:{$response}"); + + return json_decode($response, true); + } + + public static function queryExtractOrder($outOrderNo, $userId, $isUser, $amount) + { + $params = self::getBaseParams(); + $params['out_trade_no'] = sprintf('%s-%s:%s', $outOrderNo, $userId, $isUser ? 'us' : 'dl'); + $params['total'] = $amount; + $params['sign'] = self::getParamsSign($params); + + return self::requestPost(self::$extractUrl, $params, 'WuYouPay'); + } + + public static function extractOrder($isAliPay, $outOrderNo, $userId, $amount, $isUser, $account, $userName, $bankName, $province, $city, $bankBranch) + { + + $params = self::getBaseParams(); + + $params['out_trade_no'] = sprintf('%s-%s:%s', $outOrderNo, $userId, $isUser ? 'us' : 'dl'); + $params['total'] = $amount; + $params['bank_card'] = $account; + $params['bank_account_name'] = $userName; + + if ($isAliPay) { + $params['bank_name'] = '1'; + $params['bank_branch'] = '1'; + $params['province'] = '1'; + $params['city'] = '1'; + } else { + $params['bank_name'] = $bankName; + $params['bank_branch'] = !empty($bankBranch) ? $bankBranch : '1'; + $params['province'] = !empty($province) ? $province : '1'; + $params['city'] = !empty($city) ? $city : '1'; + } + + $params['notify_url'] = self::$extractNotifyUrl; + + $params['sign'] = self::getParamsSign($params); + + $params['business_type'] = 0; + if ($isAliPay) { + $params['business_attr'] = 'alipay'; + } else { + $params['business_attr'] = 'unionpay'; + } + + // 发送请求(POST) + return self::requestPost(self::$extractUrl, $params, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36'); + + } + + public static function queryOrder($trade_no, $user_id, string $amount, string $string) + { + $params = self::getBaseParams(); + $params['out_trade_no'] = "{$trade_no}-{$user_id}"; + $params['total'] = $amount; + $params['sign'] = self::getParamsSign($params); + + return self::requestPost(self::$queryUrl, $params, 'WuYouPay'); + + + } +} diff --git a/composer.json b/composer.json index 6fac653..10ea97d 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "monolog/monolog": "^2.0", "webman/think-orm": "^2.1", "vlucas/phpdotenv": "^5.6", - "webman/think-cache": "^2.1" + "webman/think-cache": "^2.1", + "firebase/php-jwt": "^6.11" }, "suggest": { "ext-event": "For better performance. " diff --git a/composer.lock b/composer.lock index d8dbd95..58dcb8c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,77 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c2aa9244f1a0947409551191f357b155", + "content-hash": "e2c855a6c24bd55772806a6c331bf958", "packages": [ + { + "name": "firebase/php-jwt", + "version": "v6.11.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/8f718f4dfc9c5d5f0c994cdfd103921b43592712", + "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.0" + }, + "time": "2025-01-23T05:11:06+00:00" + }, { "name": "graham-campbell/result-type", "version": "1.1.x-dev", diff --git a/config/buildadmin.php b/config/buildadmin.php new file mode 100644 index 0000000..70eaee3 --- /dev/null +++ b/config/buildadmin.php @@ -0,0 +1,87 @@ + '*', + // 是否开启会员登录验证码 + 'user_login_captcha' => true, + // 是否开启管理员登录验证码 + 'admin_login_captcha' => true, + // 会员登录失败可重试次数,false则无限 + 'user_login_retry' => 10, + // 管理员登录失败可重试次数,false则无限 + 'admin_login_retry' => 10, + // 开启管理员单处登录它处失效 + 'admin_sso' => false, + // 开启会员单处登录它处失效 + 'user_sso' => false, + // 会员登录态保持时间(非刷新token,3天) + 'user_token_keep_time' => 60 * 60 * 24 * 3, + // 管理员登录态保持时间(非刷新token,3天) + 'admin_token_keep_time' => 60 * 60 * 24 * 3, + // 开启前台会员中心 + 'open_member_center' => true, + // 模块纯净安装(安装时移动模块文件而不是复制) + 'module_pure_install' => true, + // 点选验证码配置 + 'click_captcha' => [ + // 模式:text=文字,icon=图标(若只有icon则适用于国际化站点) + 'mode' => ['text', 'icon'], + // 长度 + 'length' => 2, + // 混淆点长度 + 'confuse_length' => 2, + ], + // 代理服务器IP(\think\Request 类将尝试获取这些代理服务器发送过来的真实IP) + 'proxy_server_ip' => [], + // Token 配置 + 'token' => [ + // 默认驱动方式 + 'default' => 'mysql', + // 加密key + 'key' => '8uaHLJ4wg7cWpOFGyt6oPn0RZEBbDTqh', + // 加密方式 + 'algo' => 'ripemd160', + // 驱动 + 'stores' => [ + 'mysql' => [ + 'type' => 'Mysql', + // 留空表示使用默认的 Mysql 数据库,也可以填写其他数据库连接配置的`name` + 'name' => '', + // 存储token的表名 + 'table' => 'token', + // 默认 token 有效时间 + 'expire' => 0, + ], + 'redis' => [ + 'type' => 'Redis', + 'host' => '127.0.0.1', + 'port' => 6379, + 'password' => '', + // Db索引,非 0 以避免数据被意外清理 + 'select' => 1, + 'timeout' => 0, + // 默认 token 有效时间 + 'expire' => 2592000, + 'persistent' => false, + 'prefix' => 'tk:', + ], + ] + ], + // 自动写入管理员操作日志 + 'auto_write_admin_log' => true, + // 缺省头像图片路径 + 'default_avatar' => '/static/images/avatar.png', + // 内容分发网络URL,末尾不带`/` + 'cdn_url' => '', + // 内容分发网络URL参数,将自动添加 `?`,之后拼接到 cdn_url 的结尾(例如 `imageMogr2/format/heif`) + 'cdn_url_params' => '', + // 版本号 + 'version' => 'v2.3.3', + // 中心接口地址(用于请求模块市场的数据等用途) + 'api_url' => 'https://buildadmin.com', + 'run_api_url' => 'https://playlet.test.sxczgkj.com', +]; \ No newline at end of file diff --git a/config/route.php b/config/route.php index a5064fc..a004849 100644 --- a/config/route.php +++ b/config/route.php @@ -18,4 +18,3 @@ use Webman\Route; - diff --git a/extend/alei/Export.php b/extend/alei/Export.php new file mode 100644 index 0000000..256aa47 --- /dev/null +++ b/extend/alei/Export.php @@ -0,0 +1,231 @@ + '', + //导出文件名 + 'file_name' => '导出文件', + //首行是否加粗 + 'header_bold' => true, + //垂直居中 + 'header_vertical_center' => true, + //水平居中 + 'header_horizontal_center' => true, + //首列高度 + 'header_row_height' => 30 + ]; + + private $column; + + private $data; + + /** + * @params Array|String $option_or_file_name 导出选项或者文件名 + * @params String $file_path 文件保存路径 + */ + public function __construct($option_or_file_name = null, $file_path = null) + { + if(!is_null($option_or_file_name)){ + if(is_array($option_or_file_name)) { + //设置导出选项 + $this->option = array_merge($this->option, $option_or_file_name); + }else if(is_string($option_or_file_name) && strlen($option_or_file_name) > 0){ + //设置保存的文件名 + $this->option['file_name'] = $option_or_file_name; + } + } + + //设置保存的文件路径 + if (!is_null($file_path) && is_string($file_path) && strlen($file_path) > 0) { + $this->option['file_path'] = $file_path; + } + } + + + /** + * 设置列参数 + * @param String field 字段名 + * @param String title 标题 + * @param Int width 列宽 + * @param Function|String formater [格式化器,参数1:字段值,参数2:行数据,参数3:行号] 或 [预定义方法:'time':格式化时间] + * @param String type 类型 default 'text' + * @param Boole vertical_center 是否垂直居中,default false + * @param Boole horizontal_center 是否水平居中,default false + */ + public function setColumn($column) + { + + $this->column = $column; + } + + /** + * 格式化行参数 + */ + private function formatRowData($data) + { + $tmp = []; + foreach ($this->column as $key => $column) { + if (isset($data[$column['field']])) { + $tmp[$column['field']] = $data[$column['field']]; + } else { + $tmp[$column['field']] = ''; + } + if (isset($column['formater'])) { + if (gettype($column['formater']) === 'object' && is_callable($column['formater'])) { + $tmp[$column['field']] = $column['formater']($tmp[$column['field']], $data, $key); + } else if (is_string($column['formater'])) { + //格式化 + switch($column['formater']){ + case 'time': //时间戳转时间 + $format_time = $column['format_time'] ?? 'Y-m-d H:i:s'; + if (empty($tmp[$column['field']])) { + $tmp[$column['field']] = ''; + } else { + $tmp[$column['field']] = date($format_time, $tmp[$column['field']]); + } + break; + default : + } + } + } + } + return $tmp; + } + + + /** + * 设置数据 + */ + public function setData($list) + { + if (empty($this->column)) throw new \think\Exception('Please set the column parameters first!'); + $data = []; + foreach ($list as $key => $val) { + $data[] = $this->formatRowData($val); + unset($list[$key]); + } + + $this->data = $data; + } + + + /** + * 渲染表格并返回 + * @params $column 列参数 + * @params $data 导出数据 + */ + public function build() + { + if (empty($this->column)) { + throw new \think\Exception('Please set the column parameters first!'); + } + + $spreadsheet = new Spreadsheet(); + // 1获取活动工作薄 + $sheet = $spreadsheet->getActiveSheet(); + + //最后一列列号 + $end_row = count($this->column); + + // 首行选择器 + $first_cell = [1, 1, $end_row, 1]; + + //设置背景色 + $sheet->getStyle($first_cell)->getFill()->setFillType(Fill::FILL_SOLID); + $sheet->getStyle($first_cell)->getFill()->getStartColor()->setARGB('FFeeeeee'); + + //首行加边框 + $sheet->getStyle($first_cell)->getBorders()->applyFromArray([ + 'allBorders' => [ + 'borderStyle' => Border::BORDER_HAIR, + 'color' => ['argb' => '666666'] + ] + ]); + + //加粗 + if ($this->option['header_bold']) { + $sheet->getStyle($first_cell)->getFont()->setBold(true); + } + + //垂直居中 + if ($this->option['header_vertical_center']) { + $sheet->getStyle($first_cell)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER); + } + + //水平居中 + if ($this->option['header_horizontal_center']) { + $sheet->getStyle($first_cell)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); + } + + //首行高度 + $sheet->getRowDimension(1)->setRowHeight($this->option['header_row_height']); + + + foreach ($this->column as $key => $column) { + $sheet->setCellValue([$key + 1, 1], $column['title']); + $sheet->getColumnDimensionByColumn($key + 1)->setAutoSize(true); + } + + + foreach ($this->data as $key => $row) { + foreach ($this->column as $k => $column) { + $value = $row[$column['field']]; + $cell = [$k + 1, $key + 2]; + + //换行 + if (mb_strpos($value, PHP_EOL) !== false) { + $sheet->getStyle($cell)->getAlignment()->setWrapText(true); + } + + //垂直居中 + if (!empty($column['vertical_center'])) { + $sheet->getStyle($cell)->getAlignment()->setVertical(Alignment::VERTICAL_CENTER); + } + + //水平居中 + if (!empty($column['horizontal_center'])) { + $sheet->getStyle($cell)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); + } + + //设置文字 + $sheet->setCellValueExplicit($cell, $value, DataType::TYPE_STRING); + } + unset($this->data[$key]); + } + + + //设置工作表标题 + $sheet->setTitle($this->option['file_name']); + + $this->option['file_path'] = $this->option['file_path'] == '' ? '' : ($this->option['file_path'] . '/' ); + + $url = '/export/' . $this->option['file_path'] . $this->option['file_name'] . '.xlsx'; + $filePath = public_path() . str_replace('/', DIRECTORY_SEPARATOR, $url); + + if(is_file($filePath) ){ + //如果文件已经导出过则删除旧文件 +// unlink($filePath); + }else if (!is_dir(dirname($filePath))) { + mkdir(dirname($filePath), 0700, true); + } + // 保存电子表格 + $writer = new Xlsx($spreadsheet); + $writer->save($filePath); + return $url; + } + +} \ No newline at end of file diff --git a/extend/ba/Auth.php b/extend/ba/Auth.php new file mode 100644 index 0000000..582fb9b --- /dev/null +++ b/extend/ba/Auth.php @@ -0,0 +1,316 @@ + 'admin_group', // 用户组数据表名 + 'auth_group_access' => 'admin_group_access', // 用户-用户组关系表 + 'auth_rule' => 'admin_rule', // 权限规则表 + ]; + + protected object $db; + + /** + * 子菜单规则数组 + * @var array + */ + protected array $children = []; + + /** + * 构造方法 + * @param array $config + */ + public function __construct(array $config = []) + { + $this->config = array_merge($this->config, $config); + $this->db = Db::connect(config('database.search_library')); + } + + /** + * 魔术方法-获取当前配置 + * @param $name + * @return mixed + */ + public function __get($name): mixed + { + return $this->config[$name]; + } + + /** + * 获取菜单规则列表 + * @access public + * @param int $uid 用户ID + * @return array + * @throws Throwable + */ + public function getMenus(int $uid): array + { + $this->children = []; + $originAuthRules = $this->getOriginAuthRules($uid); + foreach ($originAuthRules as $rule) { + $this->children[$rule['pid']][] = $rule; + } + + // 没有根菜单规则 + if (!isset($this->children[0])) return []; + + return $this->getChildren($this->children[0]); + } + + /** + * 获取传递的菜单规则的子规则 + * @param array $rules 菜单规则 + * @return array + */ + private function getChildren(array $rules): array + { + foreach ($rules as $key => $rule) { + if (array_key_exists($rule['id'], $this->children)) { + $rules[$key]['children'] = $this->getChildren($this->children[$rule['id']]); + } + } + return $rules; + } + + /** + * 检查是否有某权限 + * @param string $name 菜单规则的 name,可以传递两个,以','号隔开 + * @param int $uid 用户ID + * @param string $relation 如果出现两个 name,是两个都通过(and)还是一个通过即可(or) + * @param string $mode 如果不使用 url 则菜单规则name匹配到即通过 + * @return bool + * @throws Throwable + */ + public function check(string $name, int $uid, string $relation = 'or', string $mode = 'url'): bool + { + // 获取用户需要验证的所有有效规则列表 + $ruleList = $this->getRuleList($uid); + if (in_array('*', $ruleList)) { + return true; + } + + if ($name) { + $name = strtolower($name); + if (str_contains($name, ',')) { + $name = explode(',', $name); + } else { + $name = [$name]; + } + } + $list = []; //保存验证通过的规则名 + if ('url' == $mode) { + $REQUEST = json_decode(strtolower(json_encode(request()->param(), JSON_UNESCAPED_UNICODE)), true); + } + foreach ($ruleList as $rule) { + $query = preg_replace('/^.+\?/U', '', $rule); + if ('url' == $mode && $query != $rule) { + parse_str($query, $param); //解析规则中的param + $intersect = array_intersect_assoc($REQUEST, $param); + $rule = preg_replace('/\?.*$/U', '', $rule); + if (in_array($rule, $name) && $intersect == $param) { + // 如果节点相符且url参数满足 + $list[] = $rule; + } + } elseif (in_array($rule, $name)) { + $list[] = $rule; + } + } + if ('or' == $relation && !empty($list)) { + return true; + } + $diff = array_diff($name, $list); + if ('and' == $relation && empty($diff)) { + return true; + } + + return false; + } + + /** + * 获得权限规则列表 + * @param int $uid 用户id + * @return array + * @throws Throwable + */ + public function getRuleList(int $uid): array + { + // 读取用户规则节点 + $ids = $this->getRuleIds($uid); + if (empty($ids)) return []; + + $originAuthRules = $this->getOriginAuthRules($uid); + + // 用户规则 + $rules = []; + if (in_array('*', $ids)) { + $rules[] = "*"; + } + foreach ($originAuthRules as $rule) { + $rules[$rule['id']] = strtolower($rule['name']); + } + return array_unique($rules); + } + + /** + * 获得权限规则原始数据 + * @param int $uid 用户id + * @return array + * @throws Throwable + */ + public function getOriginAuthRules(int $uid): array + { + $ids = $this->getRuleIds($uid); + if (empty($ids)) return []; + + $where = []; + $where[] = ['status', '=', '1']; + // 如果没有 * 则只获取用户拥有的规则 + if (!in_array('*', $ids)) { + $where[] = ['id', 'in', $ids]; + } + $rules = Db::name($this->config['auth_rule']) + ->withoutField(['remark', 'status', 'weigh', 'update_time', 'create_time']) + ->where($where) + ->order('weigh desc,id asc') + ->select() + ->toArray(); + foreach ($rules as $key => $rule) { + if (!empty($rule['keepalive'])) { + $rules[$key]['keepalive'] = $rule['name']; + } + } + + return $rules; + } + + /** + * 获取权限规则ids + * @param int $uid + * @return array + * @throws Throwable + */ + public function getRuleIds(int $uid): array + { + // 用户的组别和规则ID + $groups = $this->getGroups($uid); + $ids = []; + foreach ($groups as $g) { + $ids = array_merge($ids, explode(',', trim($g['rules'], ','))); + } + return array_unique($ids); + } + + /** + * 获取用户所有分组和对应权限规则 + * @param int $uid + * @return array + * @throws Throwable + */ + public function getGroups(int $uid): array + { + $dbName = $this->config['auth_group_access'] ?: 'user'; + if ($this->config['auth_group_access']) { + $userGroups = Db::name($dbName) + ->alias('aga') + ->join($this->config['auth_group'] . ' ag', 'aga.group_id = ag.id', 'LEFT') + ->field('aga.uid,aga.group_id,ag.id,ag.pid,ag.name,ag.rules') + ->where("aga.uid='$uid' and ag.status='1'") + ->select() + ->toArray(); + } else { + $userGroups = Db::name($dbName) + ->alias('u') + ->join($this->config['auth_group'] . ' ag', 'u.group_id = ag.id', 'LEFT') + ->field('u.id as uid,u.group_id,ag.id,ag.name,ag.rules') + ->where("u.id='$uid' and ag.status='1'") + ->select() + ->toArray(); + } + + return $userGroups; + } + + + + /** + * 获取角色系统规则 + * @param 角色 + */ + public function getMenu($role):array + { + $role_menu = $this->db->name('sys_role_menu')->where('role_id', $role['role_id'])->field('menu_id')->select()->toArray(); + $ids = []; + foreach ($role_menu as $k => $v) { + $ids[] = $v['menu_id']; + } + return $this->db->name('sys_menu')->whereIn('menu_id', $ids)->select()->toArray(); + } + + + + /** + * 获取用户角色 + * @return void + */ + public function getUserRole($uid):array + { + return $this->db->name('sys_user_role')->where('user_id', $uid)->find(); + } + + + + /** + * 获取用户所有菜单 + */ + public function getUserMenus($uid):array + { + return $this->getMenu($this->getUserRole($uid)); + } + + + /** + * 验证权限 + * @return void + */ + public function checkMenus($url, $uid):bool + { + $menus = $this->getUserMenus($uid); + $file = root_path() . '/jsondata/peion.json'; + // 检查数组 + $par_info = get_file_info($file); + $par_info = json_decode($par_info, true); + // 超管拥有全部权限 + if($uid == 1) { + return true; + } + foreach ($par_info as $k => $v) { + if(!empty($v['peion']) && strpos($url, $v['peion']) !== false) { + if($v['url'] == '*') { + return true; + } + foreach ($menus as $mk => $mv) { + if($mv['url'] == $v['url'] || $mv['perms'] == $v['url']) { + return true; + } + } + } + } + return false; + } + + + +} \ No newline at end of file diff --git a/extend/ba/Captcha.php b/extend/ba/Captcha.php new file mode 100644 index 0000000..d7a3bbb --- /dev/null +++ b/extend/ba/Captcha.php @@ -0,0 +1,443 @@ + +// +---------------------------------------------------------------------- +// | 妙码生花在 2022-2-26 进行修订,通过Mysql保存验证码而不是Session以更好的支持API访问 +// | 使用Cache不能清理过期验证码,且一旦执行清理缓存操作,验证码将失效 +// +---------------------------------------------------------------------- + +namespace ba; + +use GdImage; +use Throwable; +use think\Response; +use think\facade\Db; + +/** + * 验证码类(图形验证码、继续流程验证码) + * @property string $seKey 验证码加密密钥 + * @property string $codeSet 验证码字符集合 + * @property int $expire 验证码过期时间(s) + * @property bool $useZh 使用中文验证码 + * @property string $zhSet 中文验证码字符串 + * @property bool $useImgBg 使用背景图片 + * @property int $fontSize 验证码字体大小(px) + * @property bool $useCurve 是否画混淆曲线 + * @property bool $useNoise 是否添加杂点 + * @property int $imageH 验证码图片高度 + * @property int $imageW 验证码图片宽度 + * @property int $length 验证码位数 + * @property string $fontTtf 验证码字体,不设置随机获取 + * @property array $bg 背景颜色 + * @property bool $reset 验证成功后是否重置 + */ +class Captcha +{ + protected array $config = [ + // 验证码加密密钥 + 'seKey' => 'BuildAdmin', + // 验证码字符集合 + 'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY', + // 验证码过期时间(s) + 'expire' => 600, + // 使用中文验证码 + 'useZh' => false, + // 中文验证码字符串 + 'zhSet' => '们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测士身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非搞亚磨族火段算适讲按值美态黄易彪服早班麦削信排台声该击素张密害侯草何树肥继右属市严径螺检左页抗苏显苦英快称坏移约巴材省黑武培著河帝仅针怎植京助升王眼她抓含苗副杂普谈围食射源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功套友限项余倒卷创律雨让骨远帮初皮播优占死毒圈伟季训控激找叫云互跟裂粮粒母练塞钢顶策双留误础吸阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺灭版烈零室轻血倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送奴侧润盖挥距触星松送获兴独官混纪依未突架宽冬章湿偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告卵箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞幼哪剥迫旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末阴丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆烂森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩伤飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶缸夹念兰映沟乙吗儒杀汽磷艰晶插埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司危括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖殖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀割摆贡呈劲财仪沉炼麻罪祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜妇恶脂庄擦险赞钟摇典柄辩竹谷卖乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐援扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛亡答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释乳巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟陷枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑污冰柬嘴啥饭塑寄赵喊垫丹渡耳刨虎笔稀昆浪萨茶滴浅拥穴覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷狠忽灾闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳穷塘燥泡袋朗喂铝软渠颗惯贸粪综墙趋彼届墨碍启逆卸航衣孙龄岭骗休借', + // 使用背景图片 + 'useImgBg' => false, + // 验证码字体大小(px) + 'fontSize' => 25, + // 是否画混淆曲线 + 'useCurve' => true, + // 是否添加杂点 + 'useNoise' => true, + // 验证码图片高度 + 'imageH' => 0, + // 验证码图片宽度 + 'imageW' => 0, + // 验证码位数 + 'length' => 4, + // 验证码字体,不设置随机获取 + 'fontTtf' => '', + // 背景颜色 + 'bg' => [243, 251, 254], + // 验证成功后是否重置 + 'reset' => true, + ]; + + /** + * 验证码图片实例 + * @var GdImage|resource|null + */ + private $image = null; + + /** + * 验证码字体颜色 + * @var bool|int|null + */ + private bool|int|null $color = null; + + /** + * 架构方法 设置参数 + * @param array $config 配置参数 + * @throws Throwable + */ + public function __construct(array $config = []) + { + $this->config = array_merge($this->config, $config); + + // 清理过期的验证码 + Db::name('captcha') + ->where('expire_time', '<', time()) + ->delete(); + } + + /** + * 使用 $this->name 获取配置 + * @param string $name 配置名称 + * @return mixed 配置值 + */ + public function __get(string $name): mixed + { + return $this->config[$name]; + } + + /** + * 设置验证码配置 + * @param string $name 配置名称 + * @param mixed $value 配置值 + * @return void + */ + public function __set(string $name, mixed $value): void + { + if (isset($this->config[$name])) { + $this->config[$name] = $value; + } + } + + /** + * 检查配置 + * @param string $name 配置名称 + * @return bool + */ + public function __isset(string $name): bool + { + return isset($this->config[$name]); + } + + /** + * 验证验证码是否正确 + * @param string $code 用户验证码 + * @param string $id 验证码标识 + * @return bool 用户验证码是否正确 + * @throws Throwable + */ + public function check(string $code, string $id): bool + { + $key = $this->authCode($this->seKey, $id); + $seCode = Db::name('captcha')->where('key', $key)->find(); + + // 验证码为空 + if (empty($code) || empty($seCode)) { + return false; + } + + // 验证码过期 + if (time() > $seCode['expire_time']) { + Db::name('captcha')->where('key', $key)->delete(); + return false; + } + + if ($this->authCode(strtoupper($code), $id) == $seCode['code']) { + $this->reset && Db::name('captcha')->where('key', $key)->delete(); + return true; + } + + return false; + } + + /** + * 创建一个逻辑验证码可供后续验证(非图形) + * @param string $id 验证码标识 + * @param string|bool $captcha 验证码,不传递则自动生成 + * @return string 生成的验证码,发送出去或做它用... + * @throws Throwable + */ + public function create(string $id, string|bool $captcha = false): string + { + $nowTime = time(); + $key = $this->authCode($this->seKey, $id); + $captchaTemp = Db::name('captcha')->where('key', $key)->find(); + if ($captchaTemp) { + // 重复的为同一标识创建验证码 + Db::name('captcha')->where('key', $key)->delete(); + } + $captcha = $this->generate($captcha); + $code = $this->authCode($captcha, $id); + Db::name('captcha') + ->insert([ + 'key' => $key, + 'code' => $code, + 'captcha' => $captcha, + 'create_time' => $nowTime, + 'expire_time' => $nowTime + $this->expire + ]); + return $captcha; + } + + /** + * 获取验证码数据 + * @param string $id 验证码标识 + * @return array + * @throws Throwable + */ + public function getCaptchaData(string $id): array + { + $key = $this->authCode($this->seKey, $id); + $seCode = Db::name('captcha')->where('key', $key)->find(); + return $seCode ?: []; + } + + /** + * 输出图形验证码并把验证码的值保存的Mysql中 + * @param string $id 要生成验证码的标识 + * @return Response + * @throws Throwable + */ + public function entry(string $id): Response + { + $nowTime = time(); + // 图片宽(px) + $this->imageW || $this->imageW = $this->length * $this->fontSize * 1.5 + $this->length * $this->fontSize / 2; + // 图片高(px) + $this->imageH || $this->imageH = $this->fontSize * 2.5; + // 建立一幅 $this->imageW x $this->imageH 的图像 + $this->image = imagecreate($this->imageW, $this->imageH); + // 设置背景 + imagecolorallocate($this->image, $this->bg[0], $this->bg[1], $this->bg[2]); + + // 验证码字体随机颜色 + $this->color = imagecolorallocate($this->image, mt_rand(1, 150), mt_rand(1, 150), mt_rand(1, 150)); + // 验证码使用随机字体 + $ttfPath = public_path() . 'static' . DIRECTORY_SEPARATOR . 'fonts' . DIRECTORY_SEPARATOR . ($this->useZh ? 'zhttfs' : 'ttfs') . DIRECTORY_SEPARATOR; + + if (empty($this->fontTtf)) { + $dir = dir($ttfPath); + $ttfFiles = []; + while (false !== ($file = $dir->read())) { + if ('.' != $file[0] && str_ends_with($file, '.ttf')) { + $ttfFiles[] = $file; + } + } + $dir->close(); + $this->fontTtf = $ttfFiles[array_rand($ttfFiles)]; + } + $this->fontTtf = $ttfPath . $this->fontTtf; + + if ($this->useImgBg) { + $this->background(); + } + + if ($this->useNoise) { + // 绘杂点 + $this->writeNoise(); + } + if ($this->useCurve) { + // 绘干扰线 + $this->writeCurve(); + } + + $key = $this->authCode($this->seKey, $id); + $captcha = Db::name('captcha')->where('key', $key)->find(); + + // 绘验证码 + if ($captcha && $nowTime <= $captcha['expire_time']) { + $this->writeText($captcha['captcha']); + } else { + $captcha = $this->writeText(); + + // 保存验证码 + $code = $this->authCode(strtoupper(implode('', $captcha)), $id); + Db::name('captcha')->insert([ + 'key' => $key, + 'code' => $code, + 'captcha' => strtoupper(implode('', $captcha)), + 'create_time' => $nowTime, + 'expire_time' => $nowTime + $this->expire + ]); + } + + ob_start(); + // 输出图像 + imagepng($this->image); + $content = ob_get_clean(); + imagedestroy($this->image); + + return response($content, 200, ['Content-Length' => strlen($content)])->contentType('image/png'); + } + + /** + * 绘验证码 + * @param string $captcha 验证码 + * @return array|string 验证码 + */ + private function writeText(string $captcha = ''): array|string + { + $code = []; // 验证码 + $codeNX = 0; // 验证码第N个字符的左边距 + if ($this->useZh) { + // 中文验证码 + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = $captcha ? $captcha[$i] : iconv_substr($this->zhSet, floor(mt_rand(0, mb_strlen($this->zhSet, 'utf-8') - 1)), 1, 'utf-8'); + imagettftext($this->image, $this->fontSize, mt_rand(-40, 40), $this->fontSize * ($i + 1) * 1.5, $this->fontSize + mt_rand(10, 20), (int)$this->color, $this->fontTtf, $code[$i]); + } + } else { + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = $captcha ? $captcha[$i] : $this->codeSet[mt_rand(0, strlen($this->codeSet) - 1)]; + $codeNX += mt_rand((int)($this->fontSize * 1.2), (int)($this->fontSize * 1.6)); + imagettftext($this->image, $this->fontSize, mt_rand(-40, 40), $codeNX, (int)($this->fontSize * 1.6), (int)$this->color, $this->fontTtf, $code[$i]); + } + } + return $captcha ?: $code; + } + + /** + * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数) + * 正弦型函数解析式:y=Asin(ωx+φ)+b + * 各常数值对函数图像的影响: + * A:决定峰值(即纵向拉伸压缩的倍数) + * b:表示波形在Y轴的位置关系或纵向移动距离(上加下减) + * φ:决定波形与X轴位置关系或横向移动距离(左加右减) + * ω:决定周期(最小正周期T=2π/∣ω∣) + */ + private function writeCurve(): void + { + $py = 0; + + // 曲线前部分 + $A = mt_rand(1, $this->imageH / 2); // 振幅 + $b = mt_rand(-$this->imageH / 4, $this->imageH / 4); // Y轴方向偏移量 + $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量 + $T = mt_rand($this->imageH, $this->imageW * 2); // 周期 + $w = (2 * M_PI) / $T; + + $px1 = 0; // 曲线横坐标起始位置 + $px2 = mt_rand($this->imageW / 2, $this->imageW * 0.8); // 曲线横坐标结束位置 + + for ($px = $px1; $px <= $px2; $px = $px + 1) { + if (0 != $w) { + $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b + $i = (int)($this->fontSize / 5); + while ($i > 0) { + imagesetpixel($this->image, $px + $i, $py + $i, (int)$this->color); // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出(不用这while循环)性能要好很多 + $i--; + } + } + } + + // 曲线后部分 + $A = mt_rand(1, $this->imageH / 2); // 振幅 + $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量 + $T = mt_rand($this->imageH, $this->imageW * 2); // 周期 + $w = (2 * M_PI) / $T; + $b = $py - $A * sin($w * $px + $f) - $this->imageH / 2; + $px1 = $px2; + $px2 = $this->imageW; + + for ($px = $px1; $px <= $px2; $px = $px + 1) { + if (0 != $w) { + $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b + $i = (int)($this->fontSize / 5); + while ($i > 0) { + imagesetpixel($this->image, $px + $i, $py + $i, (int)$this->color); + $i--; + } + } + } + } + + /** + * 绘杂点,往图片上写不同颜色的字母或数字 + */ + private function writeNoise(): void + { + $codeSet = '2345678abcdefhijkmnpqrstuvwxyz'; + for ($i = 0; $i < 10; $i++) { + //杂点颜色 + $noiseColor = imagecolorallocate($this->image, mt_rand(150, 225), mt_rand(150, 225), mt_rand(150, 225)); + for ($j = 0; $j < 5; $j++) { + // 绘制 + imagestring($this->image, 5, mt_rand(-10, $this->imageW), mt_rand(-10, $this->imageH), $codeSet[mt_rand(0, 29)], $noiseColor); + } + } + } + + /** + * 绘制背景图片 + * + * 注:如果验证码输出图片比较大,将占用比较多的系统资源 + */ + private function background(): void + { + $path = Filesystem::fsFit(public_path() . 'static/images/captcha/image/'); + $dir = dir($path); + + $bgs = []; + while (false !== ($file = $dir->read())) { + if ('.' != $file[0] && str_ends_with($file, '.jpg')) { + $bgs[] = $path . $file; + } + } + $dir->close(); + + $gb = $bgs[array_rand($bgs)]; + + list($width, $height) = @getimagesize($gb); + // Resample + $bgImage = @imagecreatefromjpeg($gb); + @imagecopyresampled($this->image, $bgImage, 0, 0, 0, 0, $this->imageW, $this->imageH, $width, $height); + @imagedestroy($bgImage); + } + + + /** + * 加密验证码 + * @param string $str 验证码字符串 + * @param string $id 验证码标识 + */ + private function authCode(string $str, string $id): string + { + $key = substr(md5($this->seKey), 5, 8); + $str = substr(md5($str), 8, 10); + return md5($key . $str . $id); + } + + /** + * 生成验证码随机字符 + * @param bool|string $captcha + * @return string + */ + private function generate(bool|string $captcha = false): string + { + $code = []; // 验证码 + if ($this->useZh) { + // 中文验证码 + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = $captcha ? $captcha[$i] : iconv_substr($this->zhSet, floor(mt_rand(0, mb_strlen($this->zhSet, 'utf-8') - 1)), 1, 'utf-8'); + } + } else { + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = $captcha ? $captcha[$i] : $this->codeSet[mt_rand(0, strlen($this->codeSet) - 1)]; + } + } + $captcha = $captcha ?: implode('', $code); + return strtoupper($captcha); + } +} \ No newline at end of file diff --git a/extend/ba/ClickCaptcha.php b/extend/ba/ClickCaptcha.php new file mode 100644 index 0000000..e1181e7 --- /dev/null +++ b/extend/ba/ClickCaptcha.php @@ -0,0 +1,338 @@ + '飞机', + 'apple' => '苹果', + 'banana' => '香蕉', + 'bell' => '铃铛', + 'bicycle' => '自行车', + 'bird' => '小鸟', + 'bomb' => '炸弹', + 'butterfly' => '蝴蝶', + 'candy' => '糖果', + 'crab' => '螃蟹', + 'cup' => '杯子', + 'dolphin' => '海豚', + 'fire' => '火', + 'guitar' => '吉他', + 'hexagon' => '六角形', + 'pear' => '梨', + 'rocket' => '火箭', + 'sailboat' => '帆船', + 'snowflake' => '雪花', + 'wolf head' => '狼头', + ]; + + /** + * 配置 + * @var array + */ + private array $config = [ + // 透明度 + 'alpha' => 36, + // 中文字符集 + 'zhSet' => '们以我到他会作时要动国产的是工就年阶义发成部民可出能方进在和有大这主中为来分生对于学级地用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所起政好十战无农使前等反体合斗路图把结第里正新开论之物从当两些还天资事队点育重其思与间内去因件利相由压员气业代全组数果期导平各基或月然如应形想制心样都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极已根共直团统式转别造切九你取西持总料连任志观调么山程百报更见必真保热委手改管处己将修支识象先老光专什六型具示复安带每东增则完风回南劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单坚据速防史拉世设达尔场织历花求传断况采精金界品判参层止边清至万确究书术状须离再目海权且青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿胜细影济白格效置推空配叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非亚磨族段算适讲按值美态易彪服早班麦削信排台声该击素张密害侯何树肥继右属市严径螺检左页抗苏显苦英快称坏移巴材省黑武培著河帝仅针怎植京助升王眼她抓苗副杂普谈围食源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功友限项余倒卷创律雨让骨远帮初皮播优占圈伟季训控激找叫云互跟粮粒母练塞钢顶策双留误础阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺版烈零室轻倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送侧润盖挥距触星松送获兴独官混纪依未突架宽冬章偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞哪旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶念兰映沟乙吗儒汽磷艰晶埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀摆贡呈劲财仪沉炼麻祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜脂庄擦险赞钟摇典柄辩竹谷乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼峰零柴簧午跳居尚秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑冰柬嘴啥饭塑寄赵喊垫丹渡耳虎笔稀昆浪萨茶滴浅拥覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷忽闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳塘燥泡袋朗喂铝软渠颗惯贸综墙趋彼届墨碍启逆卸航衣孙龄岭休借', + ]; + + /** + * 构造方法 + * @param array $config 点击验证码配置 + * @throws Throwable + */ + public function __construct(array $config = []) + { + $clickConfig = Config::get('buildadmin.click_captcha'); + $this->config = array_merge($clickConfig, $this->config, $config); + + // 清理过期的验证码 + Db::name('captcha')->where('expire_time', '<', time())->delete(); + } + + /** + * 创建图形验证码 + * @param string $id 验证码ID,开发者自定义 + * @return array 返回验证码图片的base64编码和验证码文字信息 + */ + public function creat(string $id): array + { + $imagePath = Filesystem::fsFit(public_path() . $this->bgPaths[mt_rand(0, count($this->bgPaths) - 1)]); + $fontPath = Filesystem::fsFit(public_path() . $this->fontPaths[mt_rand(0, count($this->fontPaths) - 1)]); + $randPoints = $this->randPoints($this->config['length'] + $this->config['confuse_length']); + + $lang = Lang::getLangSet(); + + foreach ($randPoints as $v) { + $tmp['size'] = rand(15, 30); + if (isset($this->iconDict[$v])) { + // 图标 + $tmp['icon'] = true; + $tmp['name'] = $v; + $tmp['text'] = $lang == 'zh-cn' ? "<{$this->iconDict[$v]}>" : "<$v>"; + $iconInfo = getimagesize(Filesystem::fsFit(public_path() . 'static/images/captcha/click/icons/' . $v . '.png')); + $tmp['width'] = $iconInfo[0]; + $tmp['height'] = $iconInfo[1]; + } else { + // 字符串文本框宽度和长度 + $fontArea = imagettfbbox($tmp['size'], 0, $fontPath, $v); + $textWidth = $fontArea[2] - $fontArea[0]; + $textHeight = $fontArea[1] - $fontArea[7]; + $tmp['icon'] = false; + $tmp['text'] = $v; + $tmp['width'] = $textWidth; + $tmp['height'] = $textHeight; + } + $textArr['text'][] = $tmp; + } + // 图片宽高和类型 + $imageInfo = getimagesize($imagePath); + $textArr['width'] = $imageInfo[0]; + $textArr['height'] = $imageInfo[1]; + // 随机生成验证点位置 + foreach ($textArr['text'] as &$v) { + list($x, $y) = $this->randPosition($textArr['text'], $textArr['width'], $textArr['height'], $v['width'], $v['height'], $v['icon']); + $v['x'] = $x; + $v['y'] = $y; + $text[] = $v['text']; + } + unset($v); + // 创建图片的实例 + $image = imagecreatefromstring(file_get_contents($imagePath)); + foreach ($textArr['text'] as $v) { + if ($v['icon']) { + $this->iconCover($image, $v); + } else { + //字体颜色 + $color = imagecolorallocatealpha($image, 239, 239, 234, 127 - intval($this->config['alpha'] * (127 / 100))); + // 绘画文字 + imagettftext($image, $v['size'], 0, $v['x'], $v['y'], $color, $fontPath, $v['text']); + } + } + $nowTime = time(); + $textArr['text'] = array_splice($textArr['text'], 0, $this->config['length']); + $text = array_splice($text, 0, $this->config['length']); + Db::name('captcha') + ->replace() + ->insert([ + 'key' => md5($id), + 'code' => md5(implode(',', $text)), + 'captcha' => json_encode($textArr, JSON_UNESCAPED_UNICODE), + 'create_time' => $nowTime, + 'expire_time' => $nowTime + $this->expire + ]); + + // 输出图片 + while (ob_get_level()) { + ob_end_clean(); + } + if (!ob_get_level()) ob_start(); + switch ($imageInfo[2]) { + case 1:// GIF + imagegif($image); + $content = ob_get_clean(); + break; + case 2:// JPG + imagejpeg($image); + $content = ob_get_clean(); + break; + case 3:// PNG + imagepng($image); + $content = ob_get_clean(); + break; + default: + $content = ''; + break; + } + imagedestroy($image); + return [ + 'id' => $id, + 'text' => $text, + 'base64' => 'data:' . $imageInfo['mime'] . ';base64,' . base64_encode($content), + 'width' => $textArr['width'], + 'height' => $textArr['height'], + ]; + } + + /** + * 检查验证码 + * @param string $id 开发者自定义的验证码ID + * @param string $info 验证信息 + * @param bool $unset 验证成功是否删除验证码 + * @return bool + * @throws Throwable + */ + public function check(string $id, string $info, bool $unset = true): bool + { + $key = md5($id); + $captcha = Db::name('captcha')->where('key', $key)->find(); + if ($captcha) { + // 验证码过期 + if (time() > $captcha['expire_time']) { + Db::name('captcha')->where('key', $key)->delete(); + return false; + } + $textArr = json_decode($captcha['captcha'], true); + list($xy, $w, $h) = explode(';', $info); + $xyArr = explode('-', $xy); + $xPro = $w / $textArr['width'];// 宽度比例 + $yPro = $h / $textArr['height'];// 高度比例 + foreach ($xyArr as $k => $v) { + $xy = explode(',', $v); + $x = $xy[0]; + $y = $xy[1]; + if ($x / $xPro < $textArr['text'][$k]['x'] || $x / $xPro > $textArr['text'][$k]['x'] + $textArr['text'][$k]['width']) { + return false; + } + $phStart = $textArr['text'][$k]['icon'] ? $textArr['text'][$k]['y'] : $textArr['text'][$k]['y'] - $textArr['text'][$k]['height']; + $phEnd = $textArr['text'][$k]['icon'] ? $textArr['text'][$k]['y'] + $textArr['text'][$k]['height'] : $textArr['text'][$k]['y']; + if ($y / $yPro < $phStart || $y / $yPro > $phEnd) { + return false; + } + } + if ($unset) Db::name('captcha')->where('key', $key)->delete(); + return true; + } else { + return false; + } + } + + /** + * 绘制Icon + */ + protected function iconCover($bgImg, $iconImgData): void + { + $iconImage = imagecreatefrompng(Filesystem::fsFit(public_path() . 'static/images/captcha/click/icons/' . $iconImgData['name'] . '.png')); + $trueColorImage = imagecreatetruecolor($iconImgData['width'], $iconImgData['height']); + imagecopy($trueColorImage, $bgImg, 0, 0, $iconImgData['x'], $iconImgData['y'], $iconImgData['width'], $iconImgData['height']); + imagecopy($trueColorImage, $iconImage, 0, 0, 0, 0, $iconImgData['width'], $iconImgData['height']); + imagecopymerge($bgImg, $trueColorImage, $iconImgData['x'], $iconImgData['y'], 0, 0, $iconImgData['width'], $iconImgData['height'], $this->config['alpha']); + imagedestroy($iconImage); + imagedestroy($trueColorImage); + } + + /** + * 随机生成验证点元素 + * @param int $length + * @return array + */ + public function randPoints(int $length = 4): array + { + $arr = []; + // 文字 + if (in_array('text', $this->config['mode'])) { + for ($i = 0; $i < $length; $i++) { + $arr[] = mb_substr($this->config['zhSet'], mt_rand(0, mb_strlen($this->config['zhSet'], 'utf-8') - 1), 1, 'utf-8'); + } + } + + // 图标 + if (in_array('icon', $this->config['mode'])) { + $icon = array_keys($this->iconDict); + shuffle($icon); + $icon = array_slice($icon, 0, $length); + $arr = array_merge($arr, $icon); + } + + shuffle($arr); + return array_slice($arr, 0, $length); + } + + /** + * 随机生成位置布局 + * @param array $textArr 点位数据 + * @param int $imgW 图片宽度 + * @param int $imgH 图片高度 + * @param int $fontW 文字宽度 + * @param int $fontH 文字高度 + * @param bool $isIcon 是否是图标 + * @return array + */ + private function randPosition(array $textArr, int $imgW, int $imgH, int $fontW, int $fontH, bool $isIcon): array + { + $x = rand(0, $imgW - $fontW); + $y = rand($fontH, $imgH - $fontH); + // 碰撞验证 + if (!$this->checkPosition($textArr, $x, $y, $fontW, $fontH, $isIcon)) { + $position = $this->randPosition($textArr, $imgW, $imgH, $fontW, $fontH, $isIcon); + } else { + $position = [$x, $y]; + } + return $position; + } + + /** + * 碰撞验证 + * @param array $textArr 验证点数据 + * @param int $x x轴位置 + * @param int $y y轴位置 + * @param int $w 验证点宽度 + * @param int $h 验证点高度 + * @param bool $isIcon 是否是图标 + * @return bool + */ + public function checkPosition(array $textArr, int $x, int $y, int $w, int $h, bool $isIcon): bool + { + $flag = true; + foreach ($textArr as $v) { + if (isset($v['x']) && isset($v['y'])) { + $flagX = false; + $flagY = false; + $historyPw = $v['x'] + $v['width']; + if (($x + $w) < $v['x'] || $x > $historyPw) { + $flagX = true; + } + + $currentPhStart = $isIcon ? $y : $y - $h; + $currentPhEnd = $isIcon ? $y + $v['height'] : $y; + $historyPhStart = $v['icon'] ? $v['y'] : ($v['y'] - $v['height']); + $historyPhEnd = $v['icon'] ? ($v['y'] + $v['height']) : $v['y']; + if ($currentPhEnd < $historyPhStart || $currentPhStart > $historyPhEnd) { + $flagY = true; + } + if (!$flagX && !$flagY) { + $flag = false; + } + } + } + return $flag; + } +} \ No newline at end of file diff --git a/extend/ba/Date.php b/extend/ba/Date.php new file mode 100644 index 0000000..869e5aa --- /dev/null +++ b/extend/ba/Date.php @@ -0,0 +1,195 @@ +. + * @param string $remote timezone that to find the offset of + * @param string|null $local timezone used as the baseline + * @param string|int|null $now UNIX timestamp or date string + * @return int + * @throws Throwable + * @example $seconds = self::offset('America/Chicago', 'GMT'); + */ + public static function offset(string $remote, string $local = null, string|int $now = null): int + { + if ($local === null) { + // Use the default timezone + $local = date_default_timezone_get(); + } + if (is_int($now)) { + // Convert the timestamp into a string + $now = date(DateTimeInterface::RFC2822, $now); + } + // Create timezone objects + $zone_remote = new DateTimeZone($remote); + $zone_local = new DateTimeZone($local); + // Create date objects from timezones + $time_remote = new DateTime($now, $zone_remote); + $time_local = new DateTime($now, $zone_local); + // Find the offset + return $zone_remote->getOffset($time_remote) - $zone_local->getOffset($time_local); + } + + /** + * 计算两个时间戳之间相差的时间 + * + * $span = self::span(60, 182, 'minutes,seconds'); // array('minutes' => 2, 'seconds' => 2) + * $span = self::span(60, 182, 'minutes'); // 2 + * + * @param int $remote timestamp to find the span of + * @param int|null $local timestamp to use as the baseline + * @param string $output formatting string + * @return bool|array|string associative list of all outputs requested|when only a single output is requested + * @from https://github.com/kohana/ohanzee-helpers/blob/master/src/Date.php + */ + public static function span(int $remote, int $local = null, string $output = 'years,months,weeks,days,hours,minutes,seconds'): bool|array|string + { + // Normalize output + $output = trim(strtolower($output)); + if (!$output) { + // Invalid output + return false; + } + // Array with the output formats + $output = preg_split('/[^a-z]+/', $output); + // Convert the list of outputs to an associative array + $output = array_combine($output, array_fill(0, count($output), 0)); + // Make the output values into keys + extract(array_flip($output), EXTR_SKIP); + if ($local === null) { + // Calculate the span from the current time + $local = time(); + } + // Calculate timespan (seconds) + $timespan = abs($remote - $local); + if (isset($output['years'])) { + $timespan -= self::YEAR * ($output['years'] = (int)floor($timespan / self::YEAR)); + } + if (isset($output['months'])) { + $timespan -= self::MONTH * ($output['months'] = (int)floor($timespan / self::MONTH)); + } + if (isset($output['weeks'])) { + $timespan -= self::WEEK * ($output['weeks'] = (int)floor($timespan / self::WEEK)); + } + if (isset($output['days'])) { + $timespan -= self::DAY * ($output['days'] = (int)floor($timespan / self::DAY)); + } + if (isset($output['hours'])) { + $timespan -= self::HOUR * ($output['hours'] = (int)floor($timespan / self::HOUR)); + } + if (isset($output['minutes'])) { + $timespan -= self::MINUTE * ($output['minutes'] = (int)floor($timespan / self::MINUTE)); + } + // Seconds ago, 1 + if (isset($output['seconds'])) { + $output['seconds'] = $timespan; + } + if (count($output) === 1) { + // Only a single output was requested, return it + return array_pop($output); + } + // Return array + return $output; + } + + /** + * 格式化 UNIX 时间戳为人易读的字符串 + * + * @param int $remote Unix 时间戳 + * @param ?int $local 本地时间戳 + * @return string 格式化的日期字符串 + */ + public static function human(int $remote, ?int $local = null): string + { + $timeDiff = (is_null($local) ? time() : $local) - $remote; + $tense = $timeDiff < 0 ? 'after' : 'ago'; + $timeDiff = abs($timeDiff); + $chunks = [ + [60 * 60 * 24 * 365, 'year'], + [60 * 60 * 24 * 30, 'month'], + [60 * 60 * 24 * 7, 'week'], + [60 * 60 * 24, 'day'], + [60 * 60, 'hour'], + [60, 'minute'], + [1, 'second'], + ]; + + $count = 0; + $name = ''; + for ($i = 0, $j = count($chunks); $i < $j; $i++) { + $seconds = $chunks[$i][0]; + $name = $chunks[$i][1]; + if (($count = floor($timeDiff / $seconds)) != 0) { + break; + } + } + return __("%d $name%s $tense", [$count, $count > 1 ? 's' : '']); + } + + /** + * 获取一个基于时间偏移的Unix时间戳 + * + * @param string $type 时间类型,默认为day,可选minute,hour,day,week,month,quarter,year + * @param int $offset 时间偏移量 默认为0,正数表示当前type之后,负数表示当前type之前 + * @param string $position 时间的开始或结束,默认为begin,可选前(begin,start,first,front),end + * @param int|null $year 基准年,默认为null,即以当前年为基准 + * @param int|null $month 基准月,默认为null,即以当前月为基准 + * @param int|null $day 基准天,默认为null,即以当前天为基准 + * @param int|null $hour 基准小时,默认为null,即以当前年小时基准 + * @param int|null $minute 基准分钟,默认为null,即以当前分钟为基准 + * @return int 处理后的Unix时间戳 + */ + public static function unixTime(string $type = 'day', int $offset = 0, string $position = 'begin', int $year = null, int $month = null, int $day = null, int $hour = null, int $minute = null): int + { + $year = is_null($year) ? date('Y') : $year; + $month = is_null($month) ? date('m') : $month; + $day = is_null($day) ? date('d') : $day; + $hour = is_null($hour) ? date('H') : $hour; + $minute = is_null($minute) ? date('i') : $minute; + $position = in_array($position, ['begin', 'start', 'first', 'front']); + + return match ($type) { + 'minute' => $position ? mktime($hour, $minute + $offset, 0, $month, $day, $year) : mktime($hour, $minute + $offset, 59, $month, $day, $year), + 'hour' => $position ? mktime($hour + $offset, 0, 0, $month, $day, $year) : mktime($hour + $offset, 59, 59, $month, $day, $year), + 'day' => $position ? mktime(0, 0, 0, $month, $day + $offset, $year) : mktime(23, 59, 59, $month, $day + $offset, $year), + // 使用固定的 this week monday 而不是 $offset weeks monday 的语法才能确保准确性 + 'week' => $position ? strtotime('this week monday', mktime(0, 0, 0, $month, $day + ($offset * 7), $year)) : strtotime('this week sunday 23:59:59', mktime(0, 0, 0, $month, $day + ($offset * 7), $year)), + 'month' => $position ? mktime(0, 0, 0, $month + $offset, 1, $year) : mktime(23, 59, 59, $month + $offset, self::daysInMonth($month + $offset, $year), $year), + 'quarter' => $position ? + mktime(0, 0, 0, 1 + ((ceil(date('n', mktime(0, 0, 0, $month, $day, $year)) / 3) + $offset) - 1) * 3, 1, $year) : + mktime(23, 59, 59, (ceil(date('n', mktime(0, 0, 0, $month, $day, $year)) / 3) + $offset) * 3, self::daysInMonth((ceil(date('n', mktime(0, 0, 0, $month, $day, $year)) / 3) + $offset) * 3, $year), $year), + 'year' => $position ? mktime(0, 0, 0, 1, 1, $year + $offset) : mktime(23, 59, 59, 12, 31, $year + $offset), + default => mktime($hour, $minute, 0, $month, $day, $year), + }; + } + + /** + * 获取给定月份的天数 (28 到 31) + */ + public static function daysInMonth(int $month, ?int $year = null): int + { + return (int)date('t', mktime(0, 0, 0, $month, 1, $year)); + } +} \ No newline at end of file diff --git a/extend/ba/Depends.php b/extend/ba/Depends.php new file mode 100644 index 0000000..9bd078e --- /dev/null +++ b/extend/ba/Depends.php @@ -0,0 +1,212 @@ +json)) { + throw new Exception($this->json . ' file does not exist!'); + } + if ($this->jsonContent && !$realTime) return $this->jsonContent; + $content = @file_get_contents($this->json); + $this->jsonContent = json_decode($content, true); + if (!$this->jsonContent) { + throw new Exception($this->json . ' file read failure!'); + } + return $this->jsonContent; + } + + /** + * 设置 json 文件内容 + * @param array $content + * @throws Throwable + */ + public function setContent(array $content = []): void + { + if (!$content) $content = $this->jsonContent; + if (!isset($content['name'])) { + throw new Exception('Depend content file content is incomplete'); + } + $content = json_encode($content, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + $result = @file_put_contents($this->json, $content . PHP_EOL); + if (!$result) { + throw new Exception('File has no write permission:' . $this->json); + } + } + + /** + * 获取依赖项 + * @param bool $devEnv 是否是获取开发环境依赖 + * @return array + * @throws Throwable + */ + public function getDepends(bool $devEnv = false): array + { + try { + $content = $this->getContent(); + } catch (Throwable) { + return []; + } + + if ($this->type == 'npm') { + return $devEnv ? $content['devDependencies'] : $content['dependencies']; + } else { + return $devEnv ? $content['require-dev'] : $content['require']; + } + } + + /** + * 是否存在某个依赖 + * @param string $name 依赖名称 + * @param bool $devEnv 是否是获取开发环境依赖 + * @return bool|string false或者依赖版本号 + * @throws Throwable + */ + public function hasDepend(string $name, bool $devEnv = false): bool|string + { + $depends = $this->getDepends($devEnv); + return $depends[$name] ?? false; + } + + /** + * 添加依赖 + * @param array $depends 要添加的依赖数组["xxx" => ">=7.1.0",] + * @param bool $devEnv 是否添加为开发环境依赖 + * @param bool $cover 覆盖模式 + * @return void + * @throws Throwable + */ + public function addDepends(array $depends, bool $devEnv = false, bool $cover = false): void + { + $content = $this->getContent(true); + $dKey = $devEnv ? ($this->type == 'npm' ? 'devDependencies' : 'require-dev') : ($this->type == 'npm' ? 'dependencies' : 'require'); + if (!$cover) { + foreach ($depends as $key => $item) { + if (isset($content[$dKey][$key])) { + throw new Exception($key . ' depend already exists!'); + } + } + } + $content[$dKey] = array_merge($content[$dKey], $depends); + $this->setContent($content); + } + + /** + * 删除依赖 + * @param array $depends 要删除的依赖数组["php", "w7corp/easyWechat"] + * @param bool $devEnv 是否为开发环境删除依赖 + * @return void + * @throws Throwable + */ + public function removeDepends(array $depends, bool $devEnv = false): void + { + $content = $this->getContent(true); + $dKey = $devEnv ? ($this->type == 'npm' ? 'devDependencies' : 'require-dev') : ($this->type == 'npm' ? 'dependencies' : 'require'); + foreach ($depends as $item) { + if (isset($content[$dKey][$item])) { + unset($content[$dKey][$item]); + } + } + $this->setContent($content); + } + + /** + * 获取 composer.json 的 config 字段 + */ + public function getComposerConfig(): array + { + try { + $content = $this->getContent(); + } catch (Throwable) { + return []; + } + return $content['config']; + } + + /** + * 设置 composer.json 的 config 字段 + * @throws Throwable + */ + public function setComposerConfig(array $config, bool $cover = true): void + { + $content = $this->getContent(true); + + // 配置冲突检查 + if (!$cover) { + foreach ($config as $key => $item) { + if (is_array($item)) { + foreach ($item as $configKey => $configItem) { + if (isset($content['config'][$key][$configKey]) && $content['config'][$key][$configKey] != $configItem) { + throw new Exception(__('composer config %s conflict', [$configKey])); + } + } + } elseif (isset($content['config'][$key]) && $content['config'][$key] != $item) { + throw new Exception(__('composer config %s conflict', [$key])); + } + } + } + + foreach ($config as $key => $item) { + if (is_array($item)) { + foreach ($item as $configKey => $configItem) { + $content['config'][$key][$configKey] = $configItem; + } + } else { + $content['config'][$key] = $item; + } + } + $this->setContent($content); + } + + /** + * 删除 composer 配置项 + * @throws Throwable + */ + public function removeComposerConfig(array $config): void + { + if (!$config) return; + $content = $this->getContent(true); + foreach ($config as $key => $item) { + if (isset($content['config'][$key])) { + if (is_array($item)) { + foreach ($item as $configKey => $configItem) { + if (isset($content['config'][$key][$configKey])) unset($content['config'][$key][$configKey]); + } + + // 没有子级配置项了 + if (!$content['config'][$key]) { + unset($content['config'][$key]); + } + } else { + unset($content['config'][$key]); + } + } + } + $this->setContent($content); + } +} \ No newline at end of file diff --git a/extend/ba/Exception.php b/extend/ba/Exception.php new file mode 100644 index 0000000..ce2edce --- /dev/null +++ b/extend/ba/Exception.php @@ -0,0 +1,17 @@ +error(__($e->getMessage()), $e->getData(), $e->getCode()); + */ +class Exception extends E +{ + public function __construct(protected $message, protected $code = 0, protected $data = []) + { + parent::__construct($message, $code); + } +} \ No newline at end of file diff --git a/extend/ba/Filesystem.php b/extend/ba/Filesystem.php new file mode 100644 index 0000000..1f4c988 --- /dev/null +++ b/extend/ba/Filesystem.php @@ -0,0 +1,248 @@ +isDir()) { + self::delDir($fileInfo->getRealPath()); + } else { + @unlink($fileInfo->getRealPath()); + } + } + if ($delSelf) { + @rmdir($dir); + } + return true; + } + + /** + * 删除一个路径下的所有相对空文件夹(删除此路径中的所有空文件夹) + * @param string $path 相对于根目录的文件夹路径 如`c:BuildAdmin/a/b/` + * @return void + */ + public static function delEmptyDir(string $path): void + { + $path = str_replace(root_path(), '', rtrim(self::fsFit($path), DIRECTORY_SEPARATOR)); + $path = array_filter(explode(DIRECTORY_SEPARATOR, $path)); + for ($i = count($path) - 1; $i >= 0; $i--) { + $dirPath = root_path() . implode(DIRECTORY_SEPARATOR, $path); + if (!is_dir($dirPath)) { + unset($path[$i]); + continue; + } + if (self::dirIsEmpty($dirPath)) { + self::delDir($dirPath); + unset($path[$i]); + } else { + break; + } + } + } + + /** + * 检查目录/文件是否可写 + * @param $path + * @return bool + */ + public static function pathIsWritable($path): bool + { + if (DIRECTORY_SEPARATOR == '/' && !@ini_get('safe_mode')) { + return is_writable($path); + } + + if (is_dir($path)) { + $path = rtrim($path, '/') . '/' . md5(mt_rand(1, 100) . mt_rand(1, 100)); + if (($fp = @fopen($path, 'ab')) === false) { + return false; + } + + fclose($fp); + @chmod($path, 0777); + @unlink($path); + + return true; + } elseif (!is_file($path) || ($fp = @fopen($path, 'ab')) === false) { + return false; + } + + fclose($fp); + return true; + } + + /** + * 路径分隔符根据当前系统分隔符适配 + * @param string $path 路径 + * @return string 转换后的路径 + */ + public static function fsFit(string $path): string + { + return str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); + } + + /** + * 解压Zip + * @param string $file ZIP文件路径 + * @param string $dir 解压路径 + * @return string 解压后的路径 + * @throws Throwable + */ + public static function unzip(string $file, string $dir = ''): string + { + if (!file_exists($file)) { + throw new Exception("Zip file not found"); + } + + $zip = new ZipFile(); + try { + $zip->openFile($file); + } catch (Throwable $e) { + $zip->close(); + throw new Exception('Unable to open the zip file', 0, ['msg' => $e->getMessage()]); + } + + $dir = $dir ?: substr($file, 0, strripos($file, '.zip')); + if (!is_dir($dir)) { + @mkdir($dir, 0755); + } + + try { + $zip->extractTo($dir); + } catch (Throwable $e) { + throw new Exception('Unable to extract ZIP file', 0, ['msg' => $e->getMessage()]); + } finally { + $zip->close(); + } + return $dir; + } + + /** + * 创建ZIP + * @param array $files 文件路径列表 + * @param string $fileName ZIP文件名称 + * @return bool + * @throws Throwable + */ + public static function zip(array $files, string $fileName): bool + { + $zip = new ZipFile(); + try { + foreach ($files as $v) { + if (is_array($v) && isset($v['file']) && isset($v['name'])) { + $zip->addFile(root_path() . str_replace(root_path(), '', Filesystem::fsFit($v['file'])), $v['name']); + } else { + $saveFile = str_replace(root_path(), '', Filesystem::fsFit($v)); + $zip->addFile(root_path() . $saveFile, $saveFile); + } + } + $zip->saveAsFile($fileName); + } catch (Throwable $e) { + throw new Exception('Unable to package zip file', 0, ['msg' => $e->getMessage(), 'file' => $fileName]); + } finally { + $zip->close(); + } + if (file_exists($fileName)) { + return true; + } else { + return false; + } + } + + /** + * 递归创建目录 + * @param string $dir 目录路径 + * @return bool + */ + public static function mkdir(string $dir): bool + { + if (!is_dir($dir)) { + return mkdir($dir, 0755, true); + } + return false; + } + + /** + * 获取一个目录内的文件列表 + * @param string $dir 目录路径 + * @param array $suffix 要获取的文件列表的后缀 + * @return array + */ + public static function getDirFiles(string $dir, array $suffix = []): array + { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY + ); + + $fileList = []; + foreach ($files as $file) { + if ($file->isDir()) { + continue; + } + if (!empty($suffix) && !in_array($file->getExtension(), $suffix)) { + continue; + } + $filePath = $file->getRealPath(); + $name = str_replace($dir, '', $filePath); + $name = str_replace(DIRECTORY_SEPARATOR, "/", $name); + $fileList[$name] = $name; + } + return $fileList; + } + + /** + * 将一个文件单位转为字节 + * @param string $unit 将b、kb、m、mb、g、gb的单位转为 byte + * @return int byte + */ + public static function fileUnitToByte(string $unit): int + { + preg_match('/([0-9.]+)(\w+)/', $unit, $matches); + if (!$matches) { + return 0; + } + $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3]; + return (int)($matches[1] * pow(1024, $typeDict[strtolower($matches[2])] ?? 0)); + } +} diff --git a/extend/ba/Random.php b/extend/ba/Random.php new file mode 100644 index 0000000..defb4d2 --- /dev/null +++ b/extend/ba/Random.php @@ -0,0 +1,87 @@ + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + 'alnum' => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', + 'numeric' => '0123456789', + 'noZero' => '123456789', + default => '', + }; + return substr(str_shuffle(str_repeat($pool, ceil($len / strlen($pool)))), 0, $len); + case 'unique': + case 'md5': + return md5(uniqid(mt_rand())); + case 'encrypt': + case 'sha1': + return sha1(uniqid(mt_rand(), true)); + } + return ''; + } + + + /** + * 生成带随机前缀的唯一ID + * @param int $length ID总长度 + * @return string 唯一ID + */ + public static function generateRandomPrefixedId(int $length = 16): string { + $length = min($length, 18); + $prefixLength = 4; + $timestamp = floor(microtime(true) * 1000); // 毫秒级时间戳 + $randomPart = ''; + + // 生成随机前缀 + for ($i = 0; $i < $prefixLength; $i++) { + $randomPart .= random_int(0, 9); + } + + // 计算剩余长度用于时间戳 + $timestampLength = $length - $prefixLength; + $timestampStr = (string) $timestamp; + + // 截取或补零时间戳部分 + if (strlen($timestampStr) > $timestampLength) { + $timestampStr = substr($timestampStr, -$timestampLength); + } else { + $timestampStr = str_pad($timestampStr, $timestampLength, '0', STR_PAD_LEFT); + } + + return $randomPart . $timestampStr; + } + + + +} \ No newline at end of file diff --git a/extend/ba/TableManager.php b/extend/ba/TableManager.php new file mode 100644 index 0000000..a649eb9 --- /dev/null +++ b/extend/ba/TableManager.php @@ -0,0 +1,186 @@ +getAdapter($config['adapter'], $config); + if ($prefixWrapper) return $factory->getWrapper('prefix', $adapter); + return $adapter; + } + + /** + * 数据表名 + * @param string $table 表名,带不带前缀均可 + * @param bool $fullName 是否返回带前缀的表名 + * @param ?string $connection 连接配置标识 + * @return string 表名 + * @throws Exception + */ + public static function tableName(string $table, bool $fullName = true, ?string $connection = null): string + { + $connection = self::getConnectionConfig($connection); + $pattern = '/^' . $connection['prefix'] . '/i'; + return ($fullName ? $connection['prefix'] : '') . (preg_replace($pattern, '', $table)); + } + + /** + * 数据表列表 + * @param ?string $connection 连接配置标识 + * @throws Exception + */ + public static function getTableList(?string $connection = null): array + { + $tableList = []; + $config = self::getConnectionConfig($connection); + $connection = self::getConnection($connection); + $tables = Db::connect($connection)->query("SELECT TABLE_NAME,TABLE_COMMENT FROM information_schema.TABLES WHERE table_schema = ? ", [$config['database']]); + foreach ($tables as $row) { + $tableList[$row['TABLE_NAME']] = $row['TABLE_NAME'] . ($row['TABLE_COMMENT'] ? ' - ' . $row['TABLE_COMMENT'] : ''); + } + return $tableList; + } + + /** + * 获取数据表所有列 + * @param string $table 数据表名 + * @param bool $onlyCleanComment 只要干净的字段注释信息 + * @param ?string $connection 连接配置标识 + * @throws Throwable + */ + public static function getTableColumns(string $table, bool $onlyCleanComment = false, ?string $connection = null): array + { + if (!$table) return []; + + $table = self::tableName($table, true, $connection); + $config = self::getConnectionConfig($connection); + $connection = self::getConnection($connection); + + // 从数据库中获取表字段信息 + // Phinx 目前无法正确获取到列注释信息,故使用 sql + $sql = "SELECT * FROM `information_schema`.`columns` " + . "WHERE TABLE_SCHEMA = ? AND table_name = ? " + . "ORDER BY ORDINAL_POSITION"; + $columnList = Db::connect($connection)->query($sql, [$config['database'], $table]); + + $fieldList = []; + foreach ($columnList as $item) { + if ($onlyCleanComment) { + $fieldList[$item['COLUMN_NAME']] = ''; + if ($item['COLUMN_COMMENT']) { + $comment = explode(':', $item['COLUMN_COMMENT']); + $fieldList[$item['COLUMN_NAME']] = $comment[0]; + } + continue; + } + $fieldList[$item['COLUMN_NAME']] = $item; + } + return $fieldList; + } + + /** + * 系统是否存在多个数据库连接配置 + */ + public static function isMultiDatabase(): bool + { + return count(Config::get("database.connections")) > 1; + } + + /** + * 获取数据库连接配置标识 + * @param ?string $source + * @return string 连接配置标识 + */ + public static function getConnection(?string $source = null): string + { + if (!$source || $source === 'default') { + return Config::get('database.default'); + } + return $source; + } + + /** + * 获取某个数据库连接的配置数组 + * @param ?string $connection 连接配置标识 + * @throws Exception + */ + public static function getConnectionConfig(?string $connection = null): array + { + $connection = self::getConnection($connection); + $connection = config("database.connections.$connection"); + if (!is_array($connection)) { + throw new Exception('Database connection configuration error'); + } + + // 分布式 + if ($connection['deploy'] == 1) { + $keys = ['type', 'hostname', 'database', 'username', 'password', 'hostport', 'charset', 'prefix']; + foreach ($connection as $key => $item) { + if (in_array($key, $keys)) { + $connection[$key] = is_array($item) ? $item[0] : explode(',', $item)[0]; + } + } + } + return $connection; + } + + /** + * 获取 Phinx 适配器需要的数据库配置 + * @param ?string $connection 连接配置标识 + * @return array + * @throws Throwable + */ + protected static function getPhinxDbConfig(?string $connection = null): array + { + $config = self::getConnectionConfig($connection); + $connection = self::getConnection($connection); + $db = Db::connect($connection); + + // 数据库为懒连接,执行 sql 命令为 $db 实例连接数据库 + $db->query('SELECT 1'); + + $table = Config::get('database.migration_table', 'migrations'); + return [ + 'adapter' => $config['type'], + 'connection' => $db->getPdo(), + 'name' => $config['database'], + 'table_prefix' => $config['prefix'], + 'migration_table' => $config['prefix'] . $table, + ]; + } +} \ No newline at end of file diff --git a/extend/ba/Terminal.php b/extend/ba/Terminal.php new file mode 100644 index 0000000..9aa637e --- /dev/null +++ b/extend/ba/Terminal.php @@ -0,0 +1,509 @@ + +// +---------------------------------------------------------------------- + +namespace ba; + +use Throwable; +use think\Response; +use think\facade\Config; +use app\admin\library\Auth; +use app\admin\library\module\Manage; +use think\exception\HttpResponseException; +use app\common\library\token\TokenExpirationException; + +class Terminal +{ + /** + * @var ?Terminal 对象实例 + */ + protected static ?Terminal $instance = null; + + /** + * @var string 当前执行的命令 $command 的 key + */ + protected string $commandKey = ''; + + /** + * @var array proc_open 的参数 + */ + protected array $descriptorsPec = []; + + /** + * @var resource|bool proc_open 返回的 resource + */ + protected $process = false; + + /** + * @var array proc_open 的管道 + */ + protected array $pipes = []; + + /** + * @var int proc执行状态:0=未执行,1=执行中,2=执行完毕 + */ + protected int $procStatusMark = 0; + + /** + * @var array proc执行状态数据 + */ + protected array $procStatusData = []; + + /** + * @var string 命令在前台的uuid + */ + protected string $uuid = ''; + + /** + * @var string 扩展信息 + */ + protected string $extend = ''; + + /** + * @var string 命令执行输出文件 + */ + protected string $outputFile = ''; + + /** + * @var string 命令执行实时输出内容 + */ + protected string $outputContent = ''; + + /** + * @var string 自动构建的前端文件的 outDir(相对于根目录) + */ + protected static string $distDir = 'web' . DIRECTORY_SEPARATOR . 'dist'; + + /** + * @var array 状态标识 + */ + protected array $flag = [ + // 连接成功 + 'link-success' => 'command-link-success', + // 执行成功 + 'exec-success' => 'command-exec-success', + // 执行完成 + 'exec-completed' => 'command-exec-completed', + // 执行出错 + 'exec-error' => 'command-exec-error', + ]; + + /** + * 初始化 + */ + public static function instance(): Terminal + { + if (is_null(self::$instance)) { + self::$instance = new static(); + } + return self::$instance; + } + + /** + * 构造函数 + */ + public function __construct() + { + $this->uuid = request()->param('uuid', ''); + $this->extend = request()->param('extend', ''); + + // 初始化日志文件 + $outputDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'terminal'; + $this->outputFile = $outputDir . DIRECTORY_SEPARATOR . 'exec.log'; + if (!is_dir($outputDir)) { + mkdir($outputDir, 0755, true); + } + file_put_contents($this->outputFile, ''); + + /** + * 命令执行结果输出到文件而不是管道 + * 因为输出到管道时有延迟,而文件虽然需要频繁读取和对比内容,但是输出实时的 + */ + $this->descriptorsPec = [0 => ['pipe', 'r'], 1 => ['file', $this->outputFile, 'w'], 2 => ['file', $this->outputFile, 'w']]; + } + + /** + * 获取命令 + * @param string $key 命令key + * @return array|bool + */ + public static function getCommand(string $key): bool|array + { + if (!$key) { + return false; + } + + $commands = Config::get('terminal.commands'); + if (stripos($key, '.')) { + $key = explode('.', $key); + if (!array_key_exists($key[0], $commands) || !is_array($commands[$key[0]]) || !array_key_exists($key[1], $commands[$key[0]])) { + return false; + } + $command = $commands[$key[0]][$key[1]]; + } else { + if (!array_key_exists($key, $commands)) { + return false; + } + $command = $commands[$key]; + } + if (!is_array($command)) { + $command = [ + 'cwd' => root_path(), + 'command' => $command, + ]; + } else { + $command['cwd'] = root_path() . $command['cwd']; + } + + if (str_contains($command['command'], '%')) { + $args = request()->param('extend', ''); + $args = explode('~~', $args); + + array_unshift($args, $command['command']); + $command['command'] = call_user_func_array('sprintf', $args); + } + + $command['cwd'] = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $command['cwd']); + return $command; + } + + /** + * 执行命令 + * @param bool $authentication 是否鉴权 + * @throws Throwable + */ + public function exec(bool $authentication = true): void + { + $this->sendHeader(); + + while (ob_get_level()) { + ob_end_clean(); + } + if (!ob_get_level()) ob_start(); + + $this->commandKey = request()->param('command'); + + $command = self::getCommand($this->commandKey); + if (!$command) { + $this->execError('The command was not allowed to be executed', true); + } + + if ($authentication) { + try { + $token = get_auth_token(); + $auth = Auth::instance(); + $auth->init($token); + + if (!$auth->isLogin() || !$auth->isSuperAdmin()) { + $this->execError("You are not super administrator or not logged in", true); + } + } catch (TokenExpirationException) { + $this->execError(__('Token expiration')); + } + } + + $this->beforeExecution(); + $this->outputFlag('link-success'); + + if (!empty($command['notes'])) { + $this->output('> ' . __($command['notes']), false); + } + $this->output('> ' . $command['command'], false); + + $this->process = proc_open($command['command'], $this->descriptorsPec, $this->pipes, $command['cwd']); + if (!is_resource($this->process)) { + $this->execError('Failed to execute', true); + } + while ($this->getProcStatus()) { + $contents = file_get_contents($this->outputFile); + if (strlen($contents) && $this->outputContent != $contents) { + $newOutput = str_replace($this->outputContent, '', $contents); + $this->checkOutput($contents, $newOutput); + if (preg_match('/\r\n|\r|\n/', $newOutput)) { + $this->output($newOutput); + $this->outputContent = $contents; + } + } + + // 输出执行状态信息 + if ($this->procStatusMark === 2) { + $this->output('exitCode: ' . $this->procStatusData['exitcode']); + if ($this->procStatusData['exitcode'] === 0) { + if ($this->successCallback()) { + $this->outputFlag('exec-success'); + } else { + $this->output('Error: Command execution succeeded, but callback execution failed'); + $this->outputFlag('exec-error'); + } + } else { + $this->outputFlag('exec-error'); + } + } + + usleep(500000); + } + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + proc_close($this->process); + $this->outputFlag('exec-completed'); + } + + /** + * 获取执行状态 + * @throws Throwable + */ + public function getProcStatus(): bool + { + $this->procStatusData = proc_get_status($this->process); + if ($this->procStatusData['running']) { + $this->procStatusMark = 1; + return true; + } elseif ($this->procStatusMark === 1) { + $this->procStatusMark = 2; + return true; + } else { + return false; + } + } + + /** + * 输出 EventSource 数据 + * @param string $data + * @param bool $callback + */ + public function output(string $data, bool $callback = true): void + { + $data = self::outputFilter($data); + $data = [ + 'data' => $data, + 'uuid' => $this->uuid, + 'extend' => $this->extend, + 'key' => $this->commandKey, + ]; + $data = json_encode($data, JSON_UNESCAPED_UNICODE); + if ($data) { + $this->finalOutput($data); + if ($callback) $this->outputCallback($data); + @ob_flush();// 刷新浏览器缓冲区 + } + } + + + /** + * 检查输出 + * @param string $outputs 全部输出内容 + * @param string $rowOutput 当前输出内容(行) + */ + public function checkOutput(string $outputs, string $rowOutput): void + { + if (str_contains($rowOutput, '(Y/n)')) { + $this->execError('Interactive output detected, please manually execute the command to confirm the situation.', true); + } + } + + /** + * 输出状态标记 + * @param string $flag + */ + public function outputFlag(string $flag): void + { + $this->output($this->flag[$flag], false); + } + + /** + * 输出后回调 + */ + public function outputCallback($data): void + { + + } + + /** + * 成功后回调 + * @return bool + * @throws Throwable + */ + public function successCallback(): bool + { + if (stripos($this->commandKey, '.')) { + $commandKeyArr = explode('.', $this->commandKey); + $commandPKey = $commandKeyArr[0] ?? ''; + } else { + $commandPKey = $this->commandKey; + } + + if ($commandPKey == 'web-build') { + if (!self::mvDist()) { + $this->output('Build succeeded, but move file failed. Please operate manually.'); + return false; + } + } elseif ($commandPKey == 'web-install' && $this->extend) { + [$type, $value] = explode(':', $this->extend); + if ($type == 'module-install' && $value) { + Manage::instance($value)->dependentInstallComplete('npm'); + } + } elseif ($commandPKey == 'composer' && $this->extend) { + [$type, $value] = explode(':', $this->extend); + if ($type == 'module-install' && $value) { + Manage::instance($value)->dependentInstallComplete('composer'); + } + } elseif ($commandPKey == 'nuxt-install' && $this->extend) { + [$type, $value] = explode(':', $this->extend); + if ($type == 'module-install' && $value) { + Manage::instance($value)->dependentInstallComplete('nuxt_npm'); + } + } + return true; + } + + /** + * 执行前埋点 + */ + public function beforeExecution(): void + { + if ($this->commandKey == 'test.pnpm') { + @unlink(root_path() . 'public' . DIRECTORY_SEPARATOR . 'npm-install-test' . DIRECTORY_SEPARATOR . 'pnpm-lock.yaml'); + } elseif ($this->commandKey == 'web-install.pnpm') { + @unlink(root_path() . 'web' . DIRECTORY_SEPARATOR . 'pnpm-lock.yaml'); + } + } + + /** + * 输出过滤 + */ + public static function outputFilter($str): string + { + $str = trim($str); + $preg = '/\[(.*?)m/i'; + $str = preg_replace($preg, '', $str); + $str = str_replace(["\r\n", "\r", "\n"], "\n", $str); + return mb_convert_encoding($str, 'UTF-8', 'UTF-8,GBK,GB2312,BIG5'); + } + + /** + * 执行错误 + */ + public function execError($error, $break = false): void + { + $this->output('Error:' . $error); + $this->outputFlag('exec-error'); + if ($break) $this->break(); + } + + /** + * 退出执行 + */ + public function break(): void + { + throw new HttpResponseException(Response::create()->contentType('text/event-stream')); + } + + /** + * 执行一个命令并以字符串的方式返回执行输出 + * 代替 exec 使用,这样就只需要解除 proc_open 的函数禁用了 + * @param $commandKey + * @return string|bool + */ + public static function getOutputFromProc($commandKey): bool|string + { + if (!function_exists('proc_open') || !function_exists('proc_close')) { + return false; + } + $command = self::getCommand($commandKey); + if (!$command) { + return false; + } + $descriptorsPec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open($command['command'], $descriptorsPec, $pipes, null, null); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + $info .= stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + return self::outputFilter($info); + } + return ''; + } + + public static function mvDist(): bool + { + $distPath = root_path() . self::$distDir . DIRECTORY_SEPARATOR; + $indexHtmlPath = $distPath . 'index.html'; + $assetsPath = $distPath . 'assets'; + if (!file_exists($indexHtmlPath) || !file_exists($assetsPath)) { + return false; + } + + $toIndexHtmlPath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'index.html'; + $toAssetsPath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'assets'; + @unlink($toIndexHtmlPath); + Filesystem::delDir($toAssetsPath); + + if (rename($indexHtmlPath, $toIndexHtmlPath) && rename($assetsPath, $toAssetsPath)) { + Filesystem::delDir($distPath); + return true; + } else { + return false; + } + } + + public static function changeTerminalConfig($config = []): bool + { + // 不保存在数据库中,因为切换包管理器时,数据库资料可能还未配置 + $oldPackageManager = Config::get('terminal.npm_package_manager'); + $newPackageManager = request()->post('manager', $config['manager'] ?? $oldPackageManager); + + if ($oldPackageManager == $newPackageManager) { + return true; + } + + $buildConfigFile = config_path() . 'terminal.php'; + $buildConfigContent = @file_get_contents($buildConfigFile); + $buildConfigContent = preg_replace("/'npm_package_manager'(\s+)=>(\s+)'$oldPackageManager'/", "'npm_package_manager'\$1=>\$2'$newPackageManager'", $buildConfigContent); + $result = @file_put_contents($buildConfigFile, $buildConfigContent); + return (bool)$result; + } + + /** + * 最终输出 + */ + public function finalOutput(string $data): void + { + $app = app(); + if (!empty($app->worker) && !empty($app->connection)) { + $app->connection->send(new \Workerman\Protocols\Http\ServerSentEvents(['event' => 'message', 'data' => $data])); + } else { + echo 'data: ' . $data . "\n\n"; + } + } + + /** + * 发送响应头 + */ + public function sendHeader(): void + { + $headers = array_merge(request()->allowCrossDomainHeaders ?? [], [ + 'X-Accel-Buffering' => 'no', + 'Content-Type' => 'text/event-stream', + 'Cache-Control' => 'no-cache', + ]); + + $app = app(); + if (!empty($app->worker) && !empty($app->connection)) { + $app->connection->send(new \Workerman\Protocols\Http\Response(200, $headers, "\r\n")); + } else { + foreach ($headers as $name => $val) { + header($name . (!is_null($val) ? ':' . $val : '')); + } + } + } +} \ No newline at end of file diff --git a/extend/ba/Tree.php b/extend/ba/Tree.php new file mode 100644 index 0000000..8eac879 --- /dev/null +++ b/extend/ba/Tree.php @@ -0,0 +1,145 @@ +assembleChild()方法组装 + * @param array $arr 要改为树状的数组 + * @param string $field '树枝'字段 + * @param int $level 递归数组层次,无需手动维护 + * @param bool $superiorEnd 递归上一级树枝是否结束,无需手动维护 + * @return array + */ + public static function getTreeArray(array $arr, string $field = 'name', int $level = 0, bool $superiorEnd = false): array + { + $level++; + $number = 1; + $total = count($arr); + foreach ($arr as $key => $item) { + $prefix = ($number == $total) ? self::$icon[2] : self::$icon[1]; + if ($level == 2) { + $arr[$key][$field] = str_pad('', 4) . $prefix . $item[$field]; + } elseif ($level >= 3) { + $arr[$key][$field] = str_pad('', 4) . ($superiorEnd ? '' : self::$icon[0]) . str_pad('', ($level - 2) * 4) . $prefix . $item[$field]; + } + + if (isset($item['children']) && $item['children']) { + $arr[$key]['children'] = self::getTreeArray($item['children'], $field, $level, $number == $total); + } + $number++; + } + return $arr; + } + + /** + * 递归合并树状数组(根据children多维变二维方便渲染) + * @param array $data 要合并的数组 ['id' => 1, 'pid' => 0, 'title' => '标题1', 'children' => ['id' => 2, 'pid' => 1, 'title' => ' └标题1-1']] + * @return array [['id' => 1, 'pid' => 0, 'title' => '标题1'], ['id' => 2, 'pid' => 1, 'title' => ' └标题1-1']] + */ + public static function assembleTree(array $data): array + { + $arr = []; + foreach ($data as $v) { + $children = $v['children'] ?? []; + unset($v['children']); + $arr[] = $v; + if ($children) { + $arr = array_merge($arr, self::assembleTree($children)); + } + } + return $arr; + } + + /** + * 递归的根据指定字段组装 children 数组 + * @param array $data 数据源 例如:[['id' => 1, 'pid' => 0, title => '标题1'], ['id' => 2, 'pid' => 1, title => '标题1-1']] + * @param string $pid 存储上级id的字段 + * @param string $pk 主键字段 + * @return array ['id' => 1, 'pid' => 0, 'title' => '标题1', 'children' => ['id' => 2, 'pid' => 1, 'title' => '标题1-1']] + */ + public function assembleChild(array $data, string $pid = 'pid', string $pk = 'id'): array + { + if (!$data) return []; + + $pks = []; + $topLevelData = []; // 顶级数据 + $this->children = []; // 置空子级数据 + foreach ($data as $item) { + $pks[] = $item[$pk]; + + // 以pid组成children + $this->children[$item[$pid]][] = $item; + } + // 上级不存在的就是顶级,只获取它们的 children + foreach ($data as $item) { + if (!in_array($item[$pid], $pks)) { + $topLevelData[] = $item; + } + } + + if (count($this->children) > 0) { + foreach ($topLevelData as $key => $item) { + $topLevelData[$key]['children'] = $this->getChildren($this->children[$item[$pk]] ?? [], $pk); + } + return $topLevelData; + } else { + return $data; + } + } + + /** + * 获取 children 数组 + * 辅助 assembleChild 组装 children + * @param array $data + * @param string $pk + * @return array + */ + protected function getChildren(array $data, string $pk = 'id'): array + { + if (!$data) return []; + foreach ($data as $key => $item) { + if (array_key_exists($item[$pk], $this->children)) { + $data[$key]['children'] = $this->getChildren($this->children[$item[$pk]], $pk); + } + } + return $data; + } +} \ No newline at end of file diff --git a/extend/ba/Version.php b/extend/ba/Version.php new file mode 100644 index 0000000..8a29bc3 --- /dev/null +++ b/extend/ba/Version.php @@ -0,0 +1,133 @@ + $v2[$i]) { + return false; + } + if ($v1[$i] < $v2[$i]) { + return true; + } + } + if (count($v1) != count($v2)) { + return !(count($v1) > count($v2)); + } + return false; + } + + /** + * 是否是一个数字版本号 + * @param $version + * @return bool + */ + public static function checkDigitalVersion($version): bool + { + if (!$version) { + return false; + } + if (strtolower($version[0]) == 'v') { + $version = substr($version, 1); + } + + $rule1 = '/\.{2,10}/'; // 是否有两个的`.` + $rule2 = '/^\d+(\.\d+){0,10}$/'; + if (!preg_match($rule1, (string)$version)) { + return !!preg_match($rule2, (string)$version); + } + return false; + } + + /** + * @return string + */ + public static function getCnpmVersion(): string + { + $execOut = Terminal::getOutputFromProc('version.cnpm'); + if ($execOut) { + $preg = '/cnpm@(.+?) \(/is'; + preg_match($preg, $execOut, $result); + return $result[1] ?? ''; + } else { + return ''; + } + } + + /** + * 获取依赖版本号 + * @param string $name 支持:npm、cnpm、yarn、pnpm、node + * @return string + */ + public static function getVersion(string $name): string + { + if ($name == 'cnpm') { + return self::getCnpmVersion(); + } elseif (in_array($name, ['npm', 'yarn', 'pnpm', 'node'])) { + $execOut = Terminal::getOutputFromProc('version.' . $name); + if ($execOut) { + if (strripos($execOut, 'npm WARN') !== false) { + $preg = '/\d+(\.\d+){0,2}/'; + preg_match($preg, $execOut, $matches); + if (isset($matches[0]) && self::checkDigitalVersion($matches[0])) { + return $matches[0]; + } + } + $execOut = preg_split('/\r\n|\r|\n/', $execOut); + // 检测两行,第一行可能会是个警告消息 + for ($i = 0; $i < 2; $i++) { + if (isset($execOut[$i]) && self::checkDigitalVersion($execOut[$i])) { + return $execOut[$i]; + } + } + } else { + return ''; + } + } + return ''; + } +} \ No newline at end of file