diff --git a/app/admin/common.php b/app/admin/common.php new file mode 100644 index 0000000..41231d2 --- /dev/null +++ b/app/admin/common.php @@ -0,0 +1,48 @@ + config('buildadmin.api_url'), + 'timeout' => 30, + 'connect_timeout' => 30, + 'verify' => false, + 'http_errors' => false, + 'headers' => [ + 'X-REQUESTED-WITH' => 'XMLHttpRequest', + 'Referer' => dirname(request()->root(true)), + 'User-Agent' => 'BuildAdminClient', + ] + ]); + } +} + +function convertKeysCamelToSnakeRecursive(array $array): array +{ + $converted = []; + foreach ($array as $key => $value) { + $newKey = strtolower(preg_replace('/(?setTitle(__('upload')); + $file = $this->request->file('file'); + $driver = $this->request->param('driver', 'local'); + $topic = $this->request->param('topic', 'default'); + try { + $upload = new Upload(); + $attachment = $upload + ->setFile($file) + ->setDriver($driver) + ->setTopic($topic) + ->upload(null, $this->auth->id); + unset($attachment['create_time'], $attachment['quote']); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + + $this->success(__('File uploaded successfully'), [ + 'file' => $attachment ?? [] + ]); + } + + /** + * 获取省市区数据 + * @throws Throwable + */ + public function area(): void + { + $this->success('', get_area()); + } + + public function buildSuffixSvg(): Response + { + $suffix = $this->request->param('suffix', 'file'); + $background = $this->request->param('background'); + $content = build_suffix_svg((string)$suffix, (string)$background); + return response($content, 200, ['Content-Length' => strlen($content)])->contentType('image/svg+xml'); + } + + /** + * 获取已脱敏的数据库连接配置列表 + * @throws Throwable + */ + public function getDatabaseConnectionList(): void + { + $quickSearch = $this->request->get("quickSearch/s", ''); + $connections = config('database.connections'); + $desensitization = []; + foreach ($connections as $key => $connection) { + $connection = TableManager::getConnectionConfig($key); + $desensitization[] = [ + 'type' => $connection['type'], + 'database' => substr_replace($connection['database'], '****', 1, strlen($connection['database']) > 4 ? 2 : 1), + 'key' => $key, + ]; + } + + if ($quickSearch) { + $desensitization = array_filter($desensitization, function ($item) use ($quickSearch) { + return preg_match("/$quickSearch/i", $item['key']); + }); + $desensitization = array_values($desensitization); + } + + $this->success('', [ + 'list' => $desensitization, + ]); + } + + /** + * 获取表主键字段 + * @param ?string $table + * @param ?string $connection + * @throws Throwable + */ + public function getTablePk(?string $table = null, ?string $connection = null): void + { + if (!$table) { + $this->error(__('Parameter error')); + } + + $table = TableManager::tableName($table, true, $connection); + if (!TableManager::phinxAdapter(false, $connection)->hasTable($table)) { + $this->error(__('Data table does not exist')); + } + + $tablePk = Db::connect(TableManager::getConnection($connection)) + ->table($table) + ->getPk(); + $this->success('', ['pk' => $tablePk]); + } + + /** + * 获取数据表列表 + * @throws Throwable + */ + public function getTableList(): void + { + $quickSearch = $this->request->get("quickSearch/s", ''); + $connection = $this->request->request('connection');// 数据库连接配置标识 + $samePrefix = $this->request->request('samePrefix/b', true);// 是否仅返回项目数据表(前缀同项目一致的) + $excludeTable = $this->request->request('excludeTable/a', []);// 要排除的数据表数组(表名无需带前缀) + + $outTables = []; + $dbConfig = TableManager::getConnectionConfig($connection); + $tables = TableManager::getTableList($connection); + + if ($quickSearch) { + $tables = array_filter($tables, function ($comment) use ($quickSearch) { + return preg_match("/$quickSearch/i", $comment); + }); + } + + $pattern = '/^' . $dbConfig['prefix'] . '/i'; + foreach ($tables as $table => $comment) { + if ($samePrefix && !preg_match($pattern, $table)) continue; + + $table = preg_replace($pattern, '', $table); + if (!in_array($table, $excludeTable)) { + $outTables[] = [ + 'table' => $table, + 'comment' => $comment, + 'connection' => $connection, + 'prefix' => $dbConfig['prefix'], + ]; + } + } + + $this->success('', [ + 'list' => $outTables, + ]); + } + + /** + * 获取数据表字段列表 + * @throws Throwable + */ + public function getTableFieldList(): void + { + $table = $this->request->param('table'); + $clean = $this->request->param('clean', true); + $connection = $this->request->request('connection'); + if (!$table) { + $this->error(__('Parameter error')); + } + + $connection = TableManager::getConnection($connection); + $tablePk = Db::connect($connection)->name($table)->getPk(); + $this->success('', [ + 'pk' => $tablePk, + 'fieldList' => TableManager::getTableColumns($table, $clean, $connection), + ]); + } + + public function changeTerminalConfig(): void + { + AdminLog::instance()->setTitle(__('Change terminal config')); + if (Terminal::changeTerminalConfig()) { + $this->success(); + } else { + $this->error(__('Failed to modify the terminal configuration. Please modify the configuration file manually:%s', ['/config/buildadmin.php'])); + } + } + + public function clearCache(): void + { + AdminLog::instance()->setTitle(__('Clear cache')); + $type = $this->request->post('type'); + if ($type == 'tp' || $type == 'all') { + Cache::clear(); + } else { + $this->error(__('Parameter error')); + } + Event::trigger('cacheClearAfter', $this->app); + $this->success(__('Cache cleaned~')); + } + + /** + * 终端 + * @throws Throwable + */ + public function terminal(): void + { + (new Terminal())->exec(); + } +} \ No newline at end of file diff --git a/app/admin/controller/Alioss.php b/app/admin/controller/Alioss.php new file mode 100644 index 0000000..c4ed224 --- /dev/null +++ b/app/admin/controller/Alioss.php @@ -0,0 +1,116 @@ +file('file'); + if(empty($file)) { + $this->error('参数不能为空'); + } + $commoninfo = Db::connect(config('database.search_library')); + $endpoint = $commoninfo->name('common_info')->where(['type' => 68])->find()['value']; + $accessKeyId = $commoninfo->name('common_info')->where(['type' => 69])->find()['value']; + $secretAccessKey = $commoninfo->name('common_info')->where(['type' => 70])->find()['value']; + $bucket = $commoninfo->name('common_info')->where(['type' => 71])->find()['value']; + $befor_url = $commoninfo->name('common_info')->where(['type' => 72])->find()['value']; + + putenv('OSS_ACCESS_KEY_ID=' . $accessKeyId); + putenv('OSS_ACCESS_KEY_SECRET='. $secretAccessKey); + $provider = new EnvironmentVariableCredentialsProvider(); + + $object = date('Ymd') . '/' . uniqid() . '.' .$file->getOriginalExtension(); + try{ + $config = array( + "provider" => $provider, + "endpoint" => $endpoint, + "signatureVersion" => OssClient::OSS_SIGNATURE_VERSION_V1, + ); + $ossClient = new OssClient($config); + // 以二进制模式读取文件内容 + $handle = fopen($file->getRealPath(), 'rb'); + $content = stream_get_contents($handle); + fclose($handle); + + // 上传二进制内容,明确指定Content-Type + $options = [ + OssClient::OSS_CONTENT_LENGTH => strlen($content), + OssClient::OSS_CONTENT_TYPE => $file->getMime(), + // 禁用字符编码转换 + 'Content-Encoding' => 'binary', + ]; + $res = $ossClient->putObject($bucket, $object, $content, $options); + Log::write('上传文件结果' . json_encode($res)); + if(!empty($res['info'])) { + return $this->ApiDataReturn(['data' => $befor_url . '/' . $object, 'msg' => 'success', 'code' => 0]); + }else { + $this->error('上传失败'); + } + } catch(OssException $e) { + $this->error($e->getMessage()); + } + } + + // 获取阿里云oss存储相关配置 + public function getCredentials() + { + $commoninfo = Db::connect(config('database.search_library')); + $configItems = $commoninfo->name('common_info')->whereIn('type', [66, 67, 69, 70]) + ->column('value', 'type'); + AlibabaCloud::accessKeyClient($configItems[69], $configItems[70]) + ->regionId('cn-nanjing') + ->asDefaultClient(); + try { + $result = AlibabaCloud::rpc()->product('Sts') + ->options([ + "signatureVersion" => OssClient::OSS_SIGNATURE_VERSION_V1 + ]) + ->version('2015-04-01') + ->action('AssumeRole') + ->method('POST') + ->host($configItems[66]) // Endpoint + ->scheme('https') + ->options([ + 'query' => [ + 'RoleArn' => $configItems[67], // 角色ARN + 'RoleSessionName' => 'test', // 会话名称 + 'DurationSeconds' => 3600, // 凭证有效期(秒) + ], + ]) + ->request(); + // 获取响应中的凭证信息。 + $credentials = $result['Credentials']; + $this->n_success(['data' => [ + 'accessKeyId' => $credentials['AccessKeyId'], + 'accessKeySecret' => $credentials['AccessKeySecret'], + 'expiration' => $credentials['Expiration'], + 'securityToken' => $credentials['SecurityToken'], + ]]); + } catch (ClientException $e) { + // 处理客户端异常。 + echo $e->getErrorMessage() . PHP_EOL; + } catch (ServerException $e) { + // 处理服务端异常。 + echo $e->getErrorMessage() . PHP_EOL; + } + + + } + + + +} \ No newline at end of file diff --git a/app/admin/controller/Announcement.php b/app/admin/controller/Announcement.php new file mode 100644 index 0000000..4f3a1a7 --- /dev/null +++ b/app/admin/controller/Announcement.php @@ -0,0 +1,70 @@ +request->param(); + $where = []; + if (!empty($params['title'])) { + $where[] = ['title', '=', $params['title']]; + } + + if (!empty($params['type'])) { + $where[] = ['type', '=', $params['type']]; + } + + if (!empty($params['state'])) { + $where[] = ['state', '=', $params['state']]; + } + + if (!empty($params['id'])) { + $where[] = ['id', '=', $params['id']]; + } + + $this->successWithData(apiconvertToCamelCase(Db::name('announcement')->where($where)->select()->toArray())); + } + + public function save() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $params['create_time'] = getNormalDate(); + Db::name('announcement')->insert($params); + $this->success(); + } + + public function update() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + Db::name('announcement')->where([ + 'id' => $params['id'] + ])->update($params); + + $this->success(); + } + + public function delete() + { + $id = $this->request->post('id'); + Db::name('announcement')->delete([ + 'id' => $id + ]); + + $this->success(); + } + + + + +} \ No newline at end of file diff --git a/app/admin/controller/Appinfo.php b/app/admin/controller/Appinfo.php new file mode 100644 index 0000000..204678b --- /dev/null +++ b/app/admin/controller/Appinfo.php @@ -0,0 +1,48 @@ +request->param(); + $list = Db::name('app')->paginate([ + 'page' => $params['page'], + 'list_rows' => $params['limit'] + ]); + $this->successWithData([ + 'records' => convertToCamelCase($list->items()), // 当前页数据 + 'totalCount' => $list->total(), // 总记录数 + 'currPage' => $list->currentPage(), + 'last_page' => $list->lastPage(), + 'pageSize' => $params['limit'] + ]); + } + + public function save() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + if (empty($params['id'])) { + $params['create_at'] = getNormalDate(); + Db::name('app')->insert($params); + }else { + Db::name('app')->where([ + 'id' => $params['id'] + ])->update($params); + } + + $this->success(); + + } + + + +} \ No newline at end of file diff --git a/app/admin/controller/Banner.php b/app/admin/controller/Banner.php new file mode 100644 index 0000000..7721f9e --- /dev/null +++ b/app/admin/controller/Banner.php @@ -0,0 +1,99 @@ +request->param(); + $info = cache('banner'.$params['page'].$params['limit']); + if ($info) { + $this->successWithData($info); + } + $info = DatabaseRoute::paginateDb('banner', function ($query) use ($params) { + if (!empty($params['classify'])) { + $query->where([ + 'classify' => $params['classify'] + ]); + } + + if (!empty($params['state'])) { +// $query->where([ +// 'state' => $params['state'] +// ]); + } + return $query; + }, $params['page'], $params['limit']); + cache('banner'.$params['page'].$params['limit'], $info, -1); + $this->successWithData($info); + + } + + public function updateBannerById() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + unset($params['course']); + Db::name('banner')->where([ + 'id' => $params['id'] + ])->update($params); + deleteRedisKeysByPattern('banner*'); + $this->success(); + } + + public function insertBanner() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $params['state'] = 2; + $params['create_time'] = getNormalDate(); + unset($params['id']); + Db::name('banner')->insert($params); + deleteRedisKeysByPattern('banner*'); + $this->success(); + + } + + public function deleteBannerById() + { + $params = $this->request->param(); + $params['ids'] = explode(',', $params['ids']); + foreach ($params['ids'] as $id) { + Db::name('banner')->delete([ + 'id' => $id + ]); + } + deleteRedisKeysByPattern('banner*'); + $this->success(); + } + + + // 隐藏banner图 + public function updateBannerStateById() + { + $get = $this->request->get(); + if(empty($get['id'])) { + $this->error('参数不玩这'); + } + $id = $get['id']; + $banner = Db::connect(get_slave_connect_name())->name('banner')->where(['id' => $id])->find(); + if(!$banner) { + $this->error('记录不存在'); + } + Db::connect(get_master_connect_name())->name('banner')->where(['id' => $id])->update([ + 'state' => $banner['state'] == 1 ? 2 : 1, + ]); + $this->success(); + } + + + +} \ No newline at end of file diff --git a/app/admin/controller/Cash.php b/app/admin/controller/Cash.php new file mode 100644 index 0000000..713067d --- /dev/null +++ b/app/admin/controller/Cash.php @@ -0,0 +1,270 @@ +request->param(); + $this->successWithData(DatabaseRoute::paginateAllDb('pay_details', function ($query) use ($params) { + $query->alias('s') + ->leftJoin('tb_user u', 'u.user_id = s.user_id') + ->field([ + 's.id', 's.classify', + 's.order_id' => 'orderId', + 's.money', + 's.user_id' => 'userId', + 's.pay_diamond' => 'payDiamond', + 's.diamond', + 's.state', + 's.create_time' => 'createTime', + 's.pay_time' => 'payTime', + 'u.user_name' => 'userName', + 'u.phone', + ]); + // 动态条件拼接 + if (!empty($params['startTime']) && !empty($params['endTime'])) { + $query->whereBetween('s.create_time', [$params['startTime'], $params['endTime']]); + } + + if (!empty($params['userName'])) { + $query->whereLike('u.user_name', '%' . $params['userName'] . '%'); + } + + if (!empty($params['orderId'])) { + $query->whereLike('s.order_id', '%' . $params['orderId'] . '%'); + } + + if (isset($params['userId'])) { + $query->where('u.user_id', $params['userId']); + } + + if (isset($params['state']) && $params['state'] !== -1) { + $query->where('s.state', $params['state']); + } else { + $query->where('s.state', '<>', -1); + } + + + return $query; + }, $params['page'], $params['limit'], 's.create_time')); + } + + + public function statisticsIncomeMoney() + { + $get = $this->request->get(); + // 调用统计服务方法 + $sumMoney = \app\admin\model\Cash::statisticsIncomeMoney($get['time'], $get['flag'], null); + $courseMoney = \app\admin\model\Cash::statisticsIncomeMoney($get['time'], $get['flag'], 1); + $vipMoney = \app\admin\model\Cash::statisticsIncomeMoney($get['time'], $get['flag'], 2); + + // 构建返回数据 + $map = [ + 'sumMoney' => is_null($sumMoney) ? 0.00 : $sumMoney, + 'courseMoney' => is_null($courseMoney) ? 0.00 : $courseMoney, + 'vipMoney' => is_null($vipMoney) ? 0.00 : $vipMoney + ]; + $this->n_success(['data' => $map]); ; + } + + // 财务提现统计 + public function statisticsCashMoney() + { + $get = $this->request->get(); + $time = $get['time']; + $flag = $get['flag']; + // 验证时间格式并转换为时间戳 + $timestamp = strtotime($time); + if ($timestamp === false) { + $this->error('无效的时间格式,请使用 yyyy-MM-dd 格式'); + } + + // 初始化开始和结束时间(默认按日) + $startTime = date('Y-m-d 00:00:00', $timestamp); + $endTime = date('Y-m-d 23:59:59', $timestamp); + + // 根据flag调整时间范围 + if ($flag == 2) { + // 按月:当月第一天 00:00:00 至 当月最后一天 23:59:59 + $startTime = date('Y-m-01 00:00:00', $timestamp); // 当月1号 + $lastDay = date('t', $timestamp); // 获取当月总天数(t是原生格式化符) + $endTime = date('Y-m-' . $lastDay . ' 23:59:59', $timestamp); + } elseif ($flag == 3) { + // 按年:当年第一天 00:00:00 至 当年最后一天 23:59:59 + $startTime = date('Y-01-01 00:00:00', $timestamp); // 当年1月1号 + $endTime = date('Y-12-31 23:59:59', $timestamp); // 当年12月31号 + } + + $sumMoney = DatabaseRoute::getAllDbData('cash_out', function($query) use ($startTime, $endTime) { + $query->where(['state' => 1]); + $query->whereBetween('create_at', [$startTime, $endTime]); + return $query; + })->sum('money'); + + $countMoney = DatabaseRoute::getAllDbData('cash_out', function($query) use ($startTime, $endTime) { + $query->whereBetween('create_at', [$startTime, $endTime]); + return $query; + })->count(); + + $stayMoney = DatabaseRoute::getAllDbData('cash_out', function($query) use ($startTime, $endTime) { + $query->where(['state' => 0]); + $query->whereBetween('create_at', [$startTime, $endTime]); + return $query; + })->count(); + + $map = [ + // 金额字段,若为null则赋值0.00(保留两位小数) + 'sumMoney' => is_null($sumMoney) ? 0.00 : (float)$sumMoney, + // 计数字段,若为null则赋值0(整数) + 'countMoney' => is_null($countMoney) ? 0 : (int)$countMoney, + // 待处理金额字段,处理逻辑同上 + 'stayMoney' => is_null($stayMoney) ? 0 : (int)$stayMoney + ]; + $this->n_success(['data' => $map]); + } + + // 查询所有用户充值信息列表 + public function selectUserRecharge() + { + $get = $this->request->get(); + if(!empty($get['state']) && $get['state'] == -1) { + $get['state'] = null; + } + $get['startTime'] = date('Y-m-d 00:00:00', strtotime($get['startTime'])); + $get['endTime'] = date('Y-m-d 23:59:59', strtotime($get['endTime'])); + $this->n_success(['data' => \app\admin\model\Cash::selectPayDetails($get)]); + } + + + public function payMember() + { + $get = $this->request->get(); + $time = $get['time'];$flag = $get['flag'];$payClassify = empty($get['payClassify'])?null:$get['payClassify']; + // 1. 统计总支付金额(不指定分类) + $sumMoney = PayDetails::selectSumPayByClassify(null, $flag, $time, $payClassify); + + // 2. 按支付渠道分别统计(1-8对应不同渠道) + $channelMoneys = [ + 1 => PayDetails::selectSumPayByClassify(1, $flag, $time, $payClassify), // 微信App + 2 => PayDetails::selectSumPayByClassify(2, $flag, $time, $payClassify), // 微信公众号 + 3 => PayDetails::selectSumPayByClassify(3, $flag, $time, $payClassify), // 微信小程序 + 4 => PayDetails::selectSumPayByClassify(4, $flag, $time, $payClassify), // 支付宝App + 5 => PayDetails::selectSumPayByClassify(5, $flag, $time, $payClassify), // 支付宝H5 + 6 => PayDetails::selectSumPayByClassify(6, $flag, $time, $payClassify), // 抖音 + 7 => PayDetails::selectSumPayByClassify(7, $flag, $time, $payClassify), // 苹果 + 8 => PayDetails::selectSumPayByClassify(8, $flag, $time, $payClassify) // 快手 + ]; + + // 3. 构建结果映射(处理null值,默认为0.00) + $result = [ + 'sumMoney' => $sumMoney ?? 0.00, + 'weiXinAppMoney' => $channelMoneys[1] ?? 0.00, + 'weiXinGZHMoney' => $channelMoneys[2] ?? 0.00, + 'weiXinXCXMoney' => $channelMoneys[3] ?? 0.00, + 'zhiFuBaoAppMoney' => $channelMoneys[4] ?? 0.00, + 'zhiFuBaoH5Money' => $channelMoneys[5] ?? 0.00, + 'dyMoney' => $channelMoneys[6] ?? 0.00, + 'iosMoney' => $channelMoneys[7] ?? 0.00, + 'ksMoney' => $channelMoneys[8] ?? 0.00 + ]; + $this->n_success(['data' => $result]); + } + + public static function send($userInfo, $title, $content) + { + if (!empty($userInfo['client_id'])) { + + } + + Db::name('message_info')->insert([ + 'content' => $content, + 'title' => $title, + 'state' => 5, + 'is_see' => 0, + 'user_name' => $userInfo['user_name'], + 'user_id' => $userInfo['user_id'] + ]); + + + } + + + public function sendMsg() + { + $params = $this->request->param(); + if ($params['flag'] == 1) { + $userInfo = DatabaseRoute::getAllDbData('tb_user', function ($query) use ($params) { + return $query->where([ + 'phone' => $params['phone'] + ]); + })->find(); + + if (!$userInfo) { + $this->error('用户不存在'); + } + $this->send($userInfo, $params['title'], $params['content']); + + }else{ + $userList = DatabaseRoute::getAllDbData('tb_user', function ($query) use ($params) { + return $query; + })->list(); + $chunks = array_chunk($userList, ceil(count($userList) / 3)); + pushQueue($chunks[0]); + pushQueue($chunks[1]); + pushQueue($chunks[2]); + + } + + $this->success(); + + } + + + // 用户提现记录 + public function selectPayDetails() + { + $get = $this->request->get(); + $cashOut = []; + if(isset($get['userId'])) { + $cashOut['userId'] = $get['userId']; + } + $res = \app\admin\model\Cash::selectCashOutList($get['page'], $get['limit'], $cashOut); + $this->n_success(['data' => $res]); + + } + + + /** + * 提现接口 + */ + public function withdraw() + { + $get = $this->request->get(); + $userId = $get['userId'] ?? null; + $money = $get['money'] ?? null; + $msg = $get['msg'] ?? null; + // 验证验证码是否为空 + if (empty($msg)) { + $this->error('请输入验证码'); + } + debounce("withdraw:".$userId, 30); + runWithLock("lock:withdraw:{$userId}", 300, function () use ($userId, $money, $msg) { + WithDraw::goWithDraw($userId, $money, $msg, true, true); + }); + $this->success('提现成功,将在三个工作日内到账,请耐心等待!'); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/CashOutAudit.php b/app/admin/controller/CashOutAudit.php new file mode 100644 index 0000000..d43c2ae --- /dev/null +++ b/app/admin/controller/CashOutAudit.php @@ -0,0 +1,325 @@ +request->get(); + $cashOut = []; + if (!empty($params)) { + $cashOut = array_intersect_key($params, array_flip([ + 'user_id', 'user_name', 'zhifubao_name', 'zhifubao', + 'bank_name', 'user_type', 'state', 'start_time', 'end_time' + ])); + } + $cashOutList = DatabaseRoute::paginateAllDb('cash_out', function ($query) use($cashOut) { + if (!empty($cashOut['user_id'])) { + $query->where('user_id', $cashOut['user_id']); + } + // 用户名模糊查询(对应Java的userName匹配zhifubaoName) + if (!empty($cashOut['user_name'])) { + $query->whereLike('zhifubao_name', "%{$cashOut['user_name']}%"); + } + // 支付宝姓名模糊查询 + if (!empty($cashOut['zhifubao_name'])) { + $query->whereLike('zhifubao_name', "%{$cashOut['zhifubao_name']}%"); + } + // 支付宝账号模糊查询 + if (!empty($cashOut['zhifubao'])) { + $query->whereLike('zhifubao', "%{$cashOut['zhifubao']}%"); + } + // 银行名称模糊查询 + if (!empty($cashOut['bank_name'])) { + $query->whereLike('bank_name', "%{$cashOut['bank_name']}%"); + } + // 用户类型条件 + if (isset($cashOut['user_type']) && $cashOut['user_type'] !== '') { + $query->where('user_type', $cashOut['user_type']); + } + // 状态条件 + if (isset($cashOut['state']) && $cashOut['state'] !== '') { + $query->where('state', $cashOut['state']); + } + // 时间范围条件 + $startTime = isset($cashOut['start_time']) ? $cashOut['start_time'] : ''; + $endTime = isset($cashOut['end_time']) ? $cashOut['end_time'] : ''; + if ($startTime && $endTime) { + $query->whereBetween('create_at', $startTime, $endTime); + } elseif ($startTime) { + $query->where('create_at', '>=', $startTime); + } elseif ($endTime) { + $query->where('create_at', '<=', $endTime); + } + return $query; + }, $params['page'], $params['limit'], 'create_at'); + + // 补充关联数据 + if (!empty($cashOutList['list'])) { + // 提取所有用户ID + $userIdList = Collection::make($cashOutList['list'])->column('user_id'); + $userIdList = array_unique(array_filter($userIdList)); // 去重并过滤空值 + if($userIdList) { + $countInfoList_n = []; + foreach ($userIdList as $k => $user_id) { + $db_name = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $user_id])); + $userNameMap[] = $db_name->name('tb_user')->where(['user_id' => $user_id])->field('user_id, user_name') + ->select() + ->column('user_name', 'user_id'); + + $countInfoList[] = $db_name + ->query("SELECT user_id as userId, + ROUND(SUM(CASE WHEN state = 1 THEN money ELSE 0 END), 2) AS total, + COUNT(CASE WHEN state = 1 THEN 1 END) AS count, + ROUND(SUM(CASE WHEN state = 3 THEN money ELSE 0 END), 2) AS verifyTotal, + COUNT(CASE WHEN state = 3 THEN 1 END) AS verifyCount + FROM cash_out + WHERE user_id=" . $user_id . " + GROUP BY user_id")[0]; + } + $countInfoMap = []; + foreach ($countInfoList as $item) { + + $countInfoMap[$item['userId']] = $item; + } + + // 补充字段到结果集 + foreach ($cashOutList['list'] as &$item) { + // 补充用户名 + $item['user_name'] = $userNameMap[$item['user_id']] ?? ''; + + // 补充统计信息 + if (isset($countInfoMap[$item['user_id']])) { + $countInfo = $countInfoMap[$item['user_id']]; + $item['total'] = $countInfo['total']; + $item['count'] = $countInfo['count']; + $item['verify_count'] = $countInfo['verifyCount']; + $item['verify_total'] = $countInfo['verifyTotal']; + } else { + $item['total'] = 0; + $item['count'] = 0; + $item['verify_count'] = 0; + $item['verify_total'] = 0; + } + } + unset($item); // 解除引用 + } + } + $this->n_success(['page' => $cashOutList]); + } + + // + public function alipaytransfersummaryquery() + { + $get = $this->request->get(); + // 校验支付宝账户名 + if (empty($get['alipayAccountName'])) { + $this->error('支付宝账号不能为空'); + } + $alipayAccountName = $get['alipayAccountName']; + // 初始化返回数据结构 + $data = [ + 'alipayAccountName' => $alipayAccountName, + 'sum' => 0.00, + 'count' => 0, + 'list' => [] + ]; + + $cashOutList = DatabaseRoute::getAllDbData('cash_out', function ($query) use($alipayAccountName) { + return $query->where('state', 1) + ->where('zhifubao_name', $alipayAccountName); + })->select()->toArray(); + // 无记录则直接返回 + if (empty($cashOutList)) { + $this->n_success($data); + } + // 按用户ID分组(替代Java的Collectors.groupingBy) + $groupedByUserId = []; + foreach ($cashOutList as $record) { + $userId = $record['user_id']; + $groupedByUserId[$userId][] = $record; + } + + + // 处理每个用户的汇总数据 + foreach ($groupedByUserId as $userId => $userRecords) { + // 计算该用户的提现总金额 + $subSum = array_sum(array_column($userRecords, 'money')); + + // 初始化用户记录 + $record = [ + 'userId' => $userId, + 'userName' => '未知', + 'inviterCode' => '', + 'phone' => '', + 'zhiFuBaoName' => implode(' / ', array_unique(array_column($userRecords, 'zhifubao_name'))), + 'zhiFuBao' => implode(' / ', array_unique(array_column($userRecords, 'zhifubao'))), + 'subTotal' => $subSum, + 'subCount' => count($userRecords) + ]; + + // 查询用户信息 + $userEntity = Db::name('user') + ->where('user_id', $userId) + ->find(); + + // 补充用户信息 + if ($userEntity) { + $record['userId'] = $userEntity['user_id']; + $record['userName'] = $userEntity['user_name']; + $record['inviterCode'] = $userEntity['inviter_code']; + $record['phone'] = $userEntity['phone']; + } + + // 添加到结果列表 + $data['list'][] = $record; + + // 更新总统计 + $data['sum'] += $subSum; + $data['count'] += count($userRecords); + } + $this->n_success($data); + } + + + public function audit() + { + $cashOut = $this->request->post(); + // 1. 验证审核状态 + $isAgree = $cashOut['isAgree'] ?? null; + + if ($isAgree === null) { + $this->error("请选择同意或者拒绝!"); + } + // 拒绝时必须填写原因 + if ($isAgree == 0 && trim($cashOut['refund'] ?? '') === '') { + $this->error("请输入拒绝原因!"); + } + + $db = Db::connect(DatabaseRoute::getConnection('cash_out', ['user_id' => $cashOut['userId']], true)); + + // 2. 查询提现申请实体 + $entity = $db->name('cash_out') + ->where('user_id', $cashOut['userId']) + ->where('id', $cashOut['id']) + ->find(); + if (!$entity) { + $this->error("提现申请不存在!"); + } + + // 3. 验证申请状态(必须为待审核状态:state=3) + if ($entity['state'] != 3) { + $this->error("提现申请状态无效,请刷新后重试!"); + } + + // 4. 根据审核结果更新状态 + if ($isAgree == 1) { + $entity['state'] = 0; // 同意 + } else { + $entity['state'] = 2; // 拒绝 + $entity['refund'] = $cashOut['refund']; // 拒绝原因 + } + + // 5. 验证用户/代理信息是否存在 + $isUser = true; + if ($entity['user_type'] == 2) { + $isUser = false; // 代理用户 + } + + if ($isUser) { + // 普通用户 + $user = $db->name('tb_user')->where('user_id', $entity['user_id'])->find(); + if (!$user) { + $this->error("提现用户信息不存在!"); + } + } else { + // 代理用户 + $sysUser = $db->name('sys_user')->where('user_id', $entity['user_id'])->find(); + if (!$sysUser) { + $this->error("提现代理信息不存在!"); + } + } + + // 6. 拒绝时退回金额并返回 + if ($isAgree == 0) { + \app\admin\model\Cash::backCashAmount($entity, $db); + $this->success(); + } + + // 7. 同意时处理提现逻辑 + $isHistoryData = false; + // 历史数据判断(无银行名称的普通用户) + if ($isUser && trim($entity['bank_name'] ?? '') === '') { + $isHistoryData = true; + } + + // 更新状态为处理中 + $entity['state'] = 4; + $db->name('cash_out') + ->where('user_id', $entity['user_id']) + ->where('id', $entity['id']) + ->update([ + 'state' => 4 + ]); + + // 生成订单号(为空时) + if (trim($entity['order_number'] ?? '') === '') { + $entity['order_number'] = Random::generateRandomPrefixedId(19); + $db->name('cash_out') + ->where('id', $entity['id']) + ->update(['order_number' => $entity['order_number']]); + } + + // 8. 调用支付接口执行提现 + $baseResp = WuYouPayUtils::extractOrder( + $isHistoryData, + $entity['order_number'], + $entity['user_id'], + $entity['money'], + $isUser, + $entity['zhifubao'] ?? '', + $entity['zhifubao_name'] ?? '', + $entity['bank_name'] ?? '', + empty($entity['province']) ? '1' : $entity['province'], + empty($entity['city']) ? '1' : $entity['city'], + empty($entity['bank_branch']) ? '1' : $entity['bank_branch'] + ); + + // 9. 根据支付结果更新状态 + $updateData = []; + if (isset($baseResp['status']) && ($baseResp['status'] == 2 || $baseResp['status'] == 10000)) { + $entity['state'] = 1; // 提现成功 + } elseif (!empty($baseResp['error_msg'])) { + $entity['state'] = 2; // 提现失败 + if (strpos($baseResp['error_msg'], '收款人账户号出款属性不匹配') !== false) { + $entity['refund'] = "提现失败,请检查收款账号与收款人姓名后重试。"; + } else { + $entity['refund'] = $baseResp['error_msg']; + } + \app\admin\model\Cash::backCashAmount($entity, $db); // 失败时退回金额 + } elseif (!empty($baseResp['msg'])) { + $entity['state'] = 2; // 提现失败 + $entity['refund'] = "提现失败,请检查收款账号与收款人姓名后重试。"; + \app\admin\model\Cash::backCashAmount($entity, $db); // 失败时退回金额 + } + // 调用用户ID更新方法(原updateByUserId) + \app\admin\model\Cash::updateByUserId($entity, $db); + } + + + + +} \ No newline at end of file diff --git a/app/admin/controller/Common.php b/app/admin/controller/Common.php new file mode 100644 index 0000000..bc0cd27 --- /dev/null +++ b/app/admin/controller/Common.php @@ -0,0 +1,44 @@ +request->param(); + $this->successWithData(Db::name('common_info')->where([ + 'condition_from' => $condition['condition'] + ])->select()->toArray()); + } + + public function update() + { + $params = $this->request->post(); + $params['create_at'] = getNormalDate(); + Db::name('common_info')->where([ + 'id' => $params['id'] + ])->update(convertKeysCamelToSnakeRecursive($params)); + + $info = Db::name('common_info')->where([ + 'id' => $params['id'] + ])->find(); + cache('common_info:'.$info['type'], null); + $this->success(); + } + + public function type() + { + $get = $this->request->param(); + if(empty($get['num'])) { + $this->error('type 不能为空'); + } + $data = convertToCamelCase(Db::connect(config('database.search_library'))->name('common_info')->where('type', $get['num'])->find()); + $this->success('ok', $data); + } +} \ No newline at end of file diff --git a/app/admin/controller/CompletAward.php b/app/admin/controller/CompletAward.php new file mode 100644 index 0000000..1a10262 --- /dev/null +++ b/app/admin/controller/CompletAward.php @@ -0,0 +1,34 @@ +request->post(); + $db = Db::connect(get_master_connect_name()); + $post['create_time'] = date('Y-m-d H:i:s'); + $data = []; + foreach ($post as $k => $v) { + $data[toSnakeCase($k)] = $v; + } + unset($data['invite_img']); + unset($data['id']); + $db->name('complet_award')->insert($data); + $this->success(); + } + +} \ No newline at end of file diff --git a/app/admin/controller/Course.php b/app/admin/controller/Course.php new file mode 100644 index 0000000..9f71b71 --- /dev/null +++ b/app/admin/controller/Course.php @@ -0,0 +1,139 @@ +request->param(); + $res = \app\api\model\Course::selectCourse($params); + $this->successWithData($res['data']); + } + + public function insertCourse() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $params['is_delete'] = 0; + $params['create_time'] = getNormalDate(); + $params['course_id'] = Random::generateRandomPrefixedId(); + $params['banner_id'] = empty($params['banner_id']) ? null : $params['banner_id']; + if ($params['course_type'] == 2 || $params['course_type'] == 3) { + $copy = unserialize(serialize($params)); + unset($copy['remark']); + $id = Db::name('course')->insert($copy); + DatabaseRoute::getDb('course_details', $id, true)->insert([ + 'course_id' => $id, + 'video_url' => $params['remark'] ?? '', + 'view_count' => 0, + 'play_complete_count' => 0, + ]); + }else { + unset($params['remark']); + $id = Db::name('course')->insert($params); + } + + $this->success(); + } + + public function updateCourse() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + unset($params['banner_id']); + Db::name('course')->where([ + 'course_id' => $params['course_id'] + ])->update($params); + $this->success(); + } + + public function selectCourseById() + { + $params = $this->request->param(); + $where = [ + 'course_id' => $params['id'] + ]; + if (!empty($params['good'])) { + if ($params['good'] == 1) { + $where['good'] = $params['good']; + }else{ + $where['good'] = $params['good']; + } + } + $this->successWithData(DatabaseRoute::paginateDb('course_details', function ($query) use($where) { + $query->where($where); + return $query->order('sort', false); + }, $params['page'], $params['limit'], [ + 'course_id' => $params['id'] + ])); + + } + + public function updateDelete() + { + $params = $this->request->param(); + Db::name('course')->where([ + 'course_id' => $params['id'] + ])->delete(); + $this->success(); + } + + public function updateCourseStatus() + { + $params = $this->request->param(); + $params['ids'] = explode(',', $params['ids']); + foreach ($params['ids'] as $id) { + Db::name('course')->where([ + 'course_id' => $id + ])->update([ + 'status' => $params['status'] + ]); + } + + $this->success(); + } + + public function updateCourseDetails() + { + $param = $this->request->param(); + $param['ids'] = explode(',', $param['ids']); + foreach ($param['ids'] as $id) { + DatabaseRoute::getAllDbData('course_details', function ($query) use ($id, $param) { + return $query->where([ + 'course_details_id' => $id + ]); + })->update([ + 'is_price' => bccomp($param['price'],0, 4) == 0 ? 2 : 1, + 'content' => $param['content'], + 'title_img' => $param['titleImg'], + ]); + } + + $this->success(); + } + + public function deleteCourseDetailsByIds() + { + $params = $this->request->param(); + $params['ids'] = explode(',', $params['ids']); + foreach ($params['ids'] as $id) { + DatabaseRoute::getAllDbData('course_details', function ($query) use ($id) { + return $query->where([ + 'course_details_id' => $id + ]); + })->delete(); + } + + $this->success(); + } +} \ No newline at end of file diff --git a/app/admin/controller/CourseClassification.php b/app/admin/controller/CourseClassification.php new file mode 100644 index 0000000..7a65bd3 --- /dev/null +++ b/app/admin/controller/CourseClassification.php @@ -0,0 +1,62 @@ +request->param(); + if (!isset($params['page'])) { + $params['page'] = 1; + } + if (!isset($params['limit'])) { + $params['limit'] = 10; + } + + $this->n_success(['data' => DatabaseRoute::paginateDb('course_classification', function ($query) use ($params) { + if (isset($params['classificationName'])) { + $query->whereLike('classification_name', '%' . $params['classificationName'] . '%'); + } + return $query->where([ + 'is_delete' => 0 + ]); + }, $params['page'], $params['limit'])]); + } + + public function insertCourseClassification() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $params['is_delete'] = 0; + Db::name('course_classification')->insert($params); + $this->success(); + } + + public function updateCourseClassification() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + Db::name('course_classification')->where([ + 'classification_id' => $params['classification_id'] + ])->update($params); + $this->success(); + } + + public function updateDelete() + { + $params = $this->request->param(); + Db::name('course_classification')->where([ + 'classification_id' => $params['id'] + ])->delete(); + $this->success(); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/CourseCollect.php b/app/admin/controller/CourseCollect.php new file mode 100644 index 0000000..45251cb --- /dev/null +++ b/app/admin/controller/CourseCollect.php @@ -0,0 +1,23 @@ +request->get(); + return $this->ApiDataReturn(\app\api\model\Course::selectByUserId($get, $this->auth->user_id)); + } + + + +} \ No newline at end of file diff --git a/app/admin/controller/CourseDetails.php b/app/admin/controller/CourseDetails.php new file mode 100644 index 0000000..1e9b274 --- /dev/null +++ b/app/admin/controller/CourseDetails.php @@ -0,0 +1,56 @@ +request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $params['create_time'] = getNormalDate(); + $params['course_details_id'] = Random::generateRandomPrefixedId(); + if (empty($params['good_num'])) { + $params['good_num'] = 0; + } + DatabaseRoute::getDb('course_details', [ + 'course_id' => $params['course_id'] + ], true)->insert($params); + $this->success(); + } + + public function updateCourseDetails() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $db = Db::connect(DatabaseRoute::getConnection('course_details', ['course_id' => $params['course_id']], true)); + $course_details_id = $params['course_details_id']; + unset($params['course_id']); + unset($params['course_details_id']); + $db->name('course_details')->where('course_details_id', $course_details_id)->update($params); + $this->success(); + } + + public function deleteCourseDetails() + { + $ids = $this->request->param('ids'); + $ids = explode(',', $ids); + foreach ($ids as $id) { + DatabaseRoute::deleteAllDbDirect('course_details', function ($query) use ($id) { + return $query->where([ + 'course_details_id' => $id + ]); + }); + } + $this->success(); + } + +} \ No newline at end of file diff --git a/app/admin/controller/Dashboard.php b/app/admin/controller/Dashboard.php new file mode 100644 index 0000000..1aed884 --- /dev/null +++ b/app/admin/controller/Dashboard.php @@ -0,0 +1,20 @@ +success('', [ + 'remark' => get_route_remark() + ]); + } +} \ No newline at end of file diff --git a/app/admin/controller/DiscSpinning.php b/app/admin/controller/DiscSpinning.php new file mode 100644 index 0000000..e702c23 --- /dev/null +++ b/app/admin/controller/DiscSpinning.php @@ -0,0 +1,126 @@ +request->get(); + $db = Db::connect(get_slave_connect_name()); + $count = $db->name('disc_spinning')->where(['disc_type' => $get['source']])->count(); + $res = $db->name('disc_spinning')->where(['disc_type' => $get['source']])->limit(page($get['page'], $get['limit']), $get['limit'])->order('disc_type', 'asc')->order('odds', 'asc')->select()->toArray(); + $this->n_success(['data' => [ + 'totalCount' => $count, + 'pageSize' => $get['limit'], + 'totalPage' => ceil($count / $get['limit']), + 'currPage' => $get['page'], + 'list' => null, + 'records' => $res + ]]); + } + + + // 修改大转盘 + public function updateDiscSpinning() + { + $post = $this->request->post(); +// if(empty($post['name']) || empty($post['url']) || empty($post['type']) || empty($post['odds']) || empty($post['discType']) || empty($post['id'])) { +// $this->error('参数不完整'); +// } + + // 查询指定类型的奖品列表(按type和id升序排列) + $prizes = Db::name('disc_spinning') + ->where('disc_type', $post['discType']) + ->order('type', 'asc') + ->order('id', 'asc') + ->select() + ->toArray(); + + // 处理奖品概率累加 + $upPrizes = []; + $number = 0; // 概率累加值(PHP使用浮点数,需注意精度问题) + + foreach ($prizes as $key => $prize) { + // 如果是当前编辑的奖品,使用传入的数据覆盖 + if ($prize['id'] == $post['id']) { + $prizes[$key] = $post; + $prize = $post; + } + + // 累加概率 + $number += $prize['odds']; + $prizes[$key]['number'] = $number; // 记录累加后的概率值 + + // 添加到结果列表 + $upPrizes[] = convertKeysCamelToSnakeRecursive($prizes[$key]); + } + + // 验证概率总和是否超过100 + if ($number > 100) { + $this->error("中奖概率总和 不可超过100"); + } + + \app\admin\model\DiscSpinning::updateBatchById($upPrizes); + $this->success(); + } + + // 删除大转盘 + public function deleteDiscSpinning() + { + $post = $this->request->post(); + if(empty($post['id'])) { + $this->error('id不能为空'); + } + $db = Db::connect(get_master_connect_name()); + if($db->name('disc_spinning')->where(['id' => $post['id']])->delete()) { + $this->success(); + } + $this->error(); + } + + // 转盘添加抽奖项 + public function insertDiscSpinning() + { + $post = $this->request->post(); + if(empty($post['name']) || empty($post['url']) || empty($post['type']) || empty($post['odds']) || empty($post['discType'])) { + $this->error('参数不完整'); + } + // 查询指定类型的奖品列表(按type和id升序排列) + $prizes = Db::name('disc_spinning') + ->where('disc_type', $post['discType']) + ->order('type', 'asc') + ->order('id', 'asc') + ->select() + ->toArray(); + // 计算当前奖品总概率 + $totalOdds = 0; + foreach ($prizes as &$prize) { + $totalOdds += $prize['odds']; + $prize['number'] = $totalOdds; + } + unset($prize); // 释放引用 + // 计算新增奖品后的总概率 + $newTotalOdds = $totalOdds + $post['odds']; + // 验证概率总和是否超过100 + if ($newTotalOdds > 100) { + $this->error("中奖概率总和 不可超过100"); + } + // 设置创建时间(使用当前时间戳) + $post['create_time'] = date('Y-m-d H:i:s'); + $post['number'] = $post['odds']; + \app\admin\model\DiscSpinning::updateBatchById($prizes); + Db::name('disc_spinning')->insert($post); + $this->success(); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/DiscSpinningAmount.php b/app/admin/controller/DiscSpinningAmount.php new file mode 100644 index 0000000..f36acbf --- /dev/null +++ b/app/admin/controller/DiscSpinningAmount.php @@ -0,0 +1,122 @@ +request->get(); + if(empty($get['page'])) { + $get['page'] = 1; + } + if(empty($get['limit'])) { + $get['limit'] = 20; + } + $db = Db::connect(get_slave_connect_name()); + $count = $db->name('disc_spinning_amount')->where('type', $get['type'])->count(); + // 构建查询条件 + $res = $db->name('disc_spinning_amount') + ->where('type', $get['type']) + ->order('status', 'desc') + ->order('num', 'asc') + ->order('random', 'asc') + ->order('max_amount', 'asc') + ->limit(page($get['page'], $get['limit'])) + ->select() + ->toArray(); + foreach ($res as $k => &$v) { + $scaleFactor = 100; + $intValue = (int)round($v['max_amount'] * $scaleFactor, 0); + $v['max_amount'] = $intValue / $scaleFactor; + + $intValue = (int)round($v['random'] * $scaleFactor, 0); + $v['random'] = $intValue / $scaleFactor; + + } + // 转换为前端需要的格式 + $data = [ + 'totalCount' => $count, + 'pageSize' => $get['limit'], + 'totalPage' => ceil($count / $get['limit']), + 'currPage' => $get['page'], + 'list' => null, + 'records' => $res + ]; + $this->n_success(['data' => $data]); + } + + + // 添加现金红包 抽奖配置 + public function insertDiscSpinningAmount() + { + $post = $this->request->post(); + // 保存数据到数据库 + $result = Db::name('disc_spinning_amount')->insert([ + 'name' => $post['name'], + 'random' => $post['random'], + 'max_amount' => $post['maxAmount'], + 'type' => $post['type'], + 'status' => $post['status'], + 'num' => $post['num'] ?? 0, + ]); + if (!$result) { + $this->error("保存转盘金额配置失败"); + } + // 删除Redis缓存(使用Cache门面) + $cacheKey = "spinning:amount:{$post['type']}"; + Cache::delete($cacheKey); + $this->success(); + } + + // 修改现金红包 抽奖配置 + public function updateDiscSpinningAmount() + { + $post = $this->request->post(); + // 保存数据到数据库 + Db::name('disc_spinning_amount')->where(['id' => $post['id']])->update([ + 'name' => $post['name'], + 'random' => $post['random'], + 'max_amount' => $post['maxAmount'], + 'type' => $post['type'], + 'status' => $post['status'], + 'num' => $post['num'], + ]); +// if (!$result) { +// $this->error("保存转盘金额配置失败"); +// } + // 删除Redis缓存(使用Cache门面) + $cacheKey = "spinning:amount:{$post['type']}"; + Cache::delete($cacheKey); + $this->success(); + } + + + + public function deleteDiscSpinningAmount() + { + $post = $this->request->param(); + // 保存数据到数据库 + $result = Db::name('disc_spinning_amount')->where(['id' => $post['id']])->delete(); + if (!$result) { + $this->error("保存转盘金额配置失败"); + } + // 删除Redis缓存(使用Cache门面) + $cacheKey = "spinning:amount"; + Cache::delete($cacheKey); + $this->success(); + } + + + +} \ No newline at end of file diff --git a/app/admin/controller/ExtSys.php b/app/admin/controller/ExtSys.php new file mode 100644 index 0000000..22f4bb5 --- /dev/null +++ b/app/admin/controller/ExtSys.php @@ -0,0 +1,78 @@ + $code + ])->find(); + if($commonInfo) { + $commonInfo = $commonInfo->toArray(); + } + cache('common_info:'.$code, $commonInfo, 60 * 60 * 24); + } + if (!$commonInfo) { + $this->success(); + } + + $this->successWithData([ + 'imageUrl' => $commonInfo['value'], + 'tips' => $commonInfo['max'] + ]); + + } + + public function friendConfigSave() + { + $params = $this->request->post(); + if (empty($params['imageUrl'])) { + $this->error('顶部图片地址不能为空'); + } + + if (empty($params['tips'])) { + $this->error('邀请文案不能为空'); + } + + if (strlen($params['tips']) > 20) { + $this->error('邀请文案不能大于20个字符'); + } + + $commonInfo = (new CommonInfo())->getByCode(999); + if ($commonInfo) { + Db::name('common_info')->where([ + 'id' => $commonInfo['id'] + ])->update([ + 'max' => $params['tips'], + 'value' => $params['imageUrl'] + ]); + }else{ + Db::name('common_info')->insert([ + 'code' => 999, + 'max' => $params['tips'], + 'value' => $params['imageUrl'], + 'min' => '邀请好友配置', + 'is_app_use' => 1, + 'create_at' => getNormalDate() + ]); + } + cache('common_info:999', Db::name('common_info')->where([ + 'type' => 999 + ])->find()); + $this->success(); + + } + + +} \ No newline at end of file diff --git a/app/admin/controller/HelpWord.php b/app/admin/controller/HelpWord.php new file mode 100644 index 0000000..eae3b06 --- /dev/null +++ b/app/admin/controller/HelpWord.php @@ -0,0 +1,119 @@ +request->get(); + $page = $get['page'] ?? 1; + $limit = $get['limit'] ?? 10; + $types = $get['types'] ?? null; + $helpClassifyName = $get['helpClassifyName'] ?? null; + $parentId = $get['parentId'] ?? null; + $db = Db::connect(get_slave_connect_name()); + // 构建查询 + $query = $db->name('help_classify'); + $query = $query->when($types !== null, function ($query) use ($types) { + return $query->where('types', $types); + }); + $query = $query->when(!empty($helpClassifyName), function ($query) use ($helpClassifyName) { + return $query->where('help_classify_name', $helpClassifyName); + }); + + $query = $query->when($parentId !== null, function ($query) use ($parentId) { + return $query->where('parent_id', $parentId); + }); + $count = $query->count(); + $res = $query->order('sort', 'asc')->limit(page($page, $limit), $limit)->select()->toArray(); + $data = [ + 'totalCount' => $count, + 'pageSize' => $limit, + 'totalPage' => ceil($count / $limit), + 'currPage' => $page, + 'list' => $res, + 'records' => $res + ]; + $this->n_success(['data' => $data]); + } + + // 删除帮助分类 + public function deleteHelpClassify() + { + $post = $this->request->param(); + if(empty($post['helpClassifyId'])) { + $this->error('参数不完整'); + } + + $db = Db::connect(get_master_connect_name()); + $db->name('help_classify')->where(['help_classify_id' => $post['helpClassifyId']])->delete(); + $this->success(); + + + } + + // 添加帮助分类 + public function insertHelpClassify() + { + $post = $this->request->post(); + $createTime = date('Y-m-d H:i:s'); + if(empty($post['helpClassifyName']) || !isset($post['sort']) || empty($post['state']) || empty($post['types'])) { + $this->error('参数不完整'); + } + Db::name('help_classify')->insert([ + 'help_classify_id' => Random::generateRandomPrefixedId(), + 'help_classify_name' => $post['helpClassifyName'], + 'sort' => $post['sort'], + 'types' => $post['types'], + 'create_time' => $createTime, + ]); + $this->success(); + } + + + public function selectHelpWordList() + { + $get = $this->request->get(); + $helpClassifyId = $get['helpClassifyId'] ?? null; + $helpWordTitle = $get['helpWordTitle'] ?? null; + $page = $get['page'] ?? null; + $limit = $get['limit'] ?? null; + $query = Db::name('help_word'); + // 条件筛选 + if (!is_null($helpClassifyId)) { + $query->where('help_classify_id', $helpClassifyId); + } + if (!empty($helpWordTitle)) { + $query->where('help_word_title', $helpWordTitle); + } + // 排序和分页 + $list = $query->order('sort', 'asc') + ->page($page, $limit) + ->limit(page($page, $limit), $limit) + ->select() + ->toArray(); + // 获取总数(用于分页信息) + $count = $query->count(); + $this->n_success(['data' => [ + 'totalCount' => $count, + 'pageSize' => $get['limit'], + 'totalPage' => ceil($count / $get['limit']), + 'currPage' => $get['page'], + 'list' => $list, + ]]); + + + } + + + + +} \ No newline at end of file diff --git a/app/admin/controller/Indetcode.php b/app/admin/controller/Indetcode.php new file mode 100644 index 0000000..f63dfe7 --- /dev/null +++ b/app/admin/controller/Indetcode.php @@ -0,0 +1,16 @@ +request->get(); + if(empty($data['uuid']))$this->error('参数不完整'); + $this->success('ok', SysCaptcha::getCode($data['uuid'])); + } +} \ No newline at end of file diff --git a/app/admin/controller/Index.php b/app/admin/controller/Index.php new file mode 100644 index 0000000..45427eb --- /dev/null +++ b/app/admin/controller/Index.php @@ -0,0 +1,133 @@ +auth->getInfo(); + $adminInfo['super'] = $this->auth->isSuperAdmin(); + unset($adminInfo['token'], $adminInfo['refresh_token']); + + $menus = $this->auth->getMenus(); + if (!$menus) { + $this->error(__('No background menu, please contact super administrator!')); + } + $this->success('', [ + 'adminInfo' => $adminInfo, + 'menus' => $menus, + 'siteConfig' => [ + 'siteName' => get_sys_config('site_name'), + 'version' => get_sys_config('version'), + 'apiUrl' => Config::get('buildadmin.api_url'), + 'upload' => keys_to_camel_case(get_upload_config(), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types']), + 'cdnUrl' => full_url(), + 'cdnUrlParams' => Config::get('buildadmin.cdn_url_params'), + ], + 'terminal' => [ + 'phpDevelopmentServer' => str_contains($_SERVER['SERVER_SOFTWARE'], 'Development Server'), + 'npmPackageManager' => Config::get('terminal.npm_package_manager'), + ] + ]); + } + + /** + * 管理员登录 + * @return void + * @throws Throwable + */ + public function login(): void + { + // 检查登录态 + if ($this->auth->isLogin()) { + $this->success(__('You have already logged in. There is no need to log in again~'), [ + 'type' => $this->auth::LOGGED_IN + ], $this->auth::LOGIN_RESPONSE_CODE); + } + + $captchaSwitch = Config::get('buildadmin.admin_login_captcha'); + + // 检查提交 + if ($this->request->isPost()) { + $username = $this->request->post('username'); + $password = $this->request->post('password'); + $keep = $this->request->post('keep'); + + $rule = [ + 'username|' . __('Username') => 'require|length:3,30', + 'password|' . __('Password') => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$', + ]; + $data = [ + 'username' => $username, + 'password' => $password, + ]; + if ($captchaSwitch) { + $rule['captchaId|' . __('CaptchaId')] = 'require'; + $rule['captchaInfo|' . __('Captcha')] = 'require'; + + $data['captchaId'] = $this->request->post('captchaId'); + $data['captchaInfo'] = $this->request->post('captchaInfo'); + } + $validate = Validate::rule($rule); + if (!$validate->check($data)) { + $this->error($validate->getError()); + } + + if ($captchaSwitch) { + $captchaObj = new ClickCaptcha(); + if (!$captchaObj->check($data['captchaId'], $data['captchaInfo'])) { + $this->error(__('Captcha error')); + } + } + + AdminLog::instance()->setTitle(__('Login')); + + $res = $this->auth->login($username, $password, (bool)$keep); + if ($res === true) { + $this->success(__('Login succeeded!'), [ + 'userInfo' => $this->auth->getInfo() + ]); + } else { + $msg = $this->auth->getError(); + $msg = $msg ?: __('Incorrect user name or password!'); + $this->error($msg); + } + } + + $this->success('', [ + 'captcha' => $captchaSwitch + ]); + } + + /** + * 管理员注销 + * @return void + */ + public function logout(): void + { + if ($this->request->isPost()) { + $refreshToken = $this->request->post('refreshToken', ''); + if ($refreshToken) Token::delete((string)$refreshToken); + $this->auth->logout(); + $this->success(); + } + } +} diff --git a/app/admin/controller/Integral.php b/app/admin/controller/Integral.php new file mode 100644 index 0000000..aca9f47 --- /dev/null +++ b/app/admin/controller/Integral.php @@ -0,0 +1,83 @@ +request->get(); + if(empty($get['userId'])) { + $this->error('userId is not empty'); + } + + $data = Db::connect(get_slave_connect_name())->name('user_integral')->where(['user_id' => $get['userId']])->find(); + $data = [ + 'user_id' => (string)$get['userId'], + 'integral_num' => !empty($data['integral_num'])?$data['integral_num']:'0', + ]; + $this->n_success(['data' => convertToCamelCase($data)]); + } + + public function details() + { + $get = $this->request->get(); + if(empty($get['userId'])) { + $this->error('userId is not empty'); + } + $this->n_success(['data' => UserIntegral::selectUserIntegralDetailsByUserId($get['page'], $get['limit'], $get['userId'])]); + } + + public function updateUserIntegral() + { + $post = $this->request->post(); + $userId = $post['userId'] ?? $this->error('参数不完整'); + $type = $post['type'] ?? $this->error('参数不完整'); + $integral = $post['integral'] ?? $this->error('参数不完整'); + + + Db::startTrans(); + try { + $db = Db::connect(get_master_connect_name()); + if(UserIntegral::updateIntegral($type, $userId, $integral, $db)) { + // 记录积分变动明细 + $details['classify'] = 2; + $details['content'] = $type == 1 + ? "系统增加积分:{$integral}" + : "系统减少积分:{$integral}"; + + $currentTime = time(); + $details['create_time'] = date('Y-m-d H:i:s', $currentTime); + + $details['num'] = $integral; + $details['type'] = $type; + $details['user_id'] = $userId; + $db->name('user_integral_details')->insert($details); + Db::commit(); + } + $this->success(); + }catch (Exception $e) { + Db::rollback(); + $this->error($e->getMessage()); + } + + + + + + + + } + + + +} \ No newline at end of file diff --git a/app/admin/controller/Invite.php b/app/admin/controller/Invite.php new file mode 100644 index 0000000..414ec03 --- /dev/null +++ b/app/admin/controller/Invite.php @@ -0,0 +1,29 @@ +request->get(); + if(empty($get['userId'])) { + $this->error('userId 不能为空'); + } + $user = DatabaseRoute::getDb('tb_user', $get['userId'])->find(); + if(empty($user)) { + $this->error('用户不存在'); + } + return $this->ApiDataReturn(\app\api\model\Invite::selectInviteByUserIdLists($user, $get, 'admin')); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/InviteAward.php b/app/admin/controller/InviteAward.php new file mode 100644 index 0000000..a156782 --- /dev/null +++ b/app/admin/controller/InviteAward.php @@ -0,0 +1,31 @@ +order('invite_count', 'asc')->select(); + $this->successWithData(buildPageInfo(convertToCamelCase($inviteList->toArray()), true)); + } + + public function deleteInviteAward() + { + $post = $this->request->param(); + $inviteAwardId = !empty($post['inviteAwardId'])?$post['inviteAwardId']:$this->error('参数不完整'); + $db = Db::connect(get_master_connect_name()); + if($db->name('invite_award')->where(['invite_award_id' => $inviteAwardId])->delete()) { + $this->success(); + } + $this->error(); + } + +} \ No newline at end of file diff --git a/app/admin/controller/Login.php b/app/admin/controller/Login.php new file mode 100644 index 0000000..a23f63e --- /dev/null +++ b/app/admin/controller/Login.php @@ -0,0 +1,28 @@ +request->get(); + return $this->ApiDataReturn(Msg::sendMsg($get['phone'], $get['event'])); + } +} \ No newline at end of file diff --git a/app/admin/controller/Message.php b/app/admin/controller/Message.php new file mode 100644 index 0000000..b25aafb --- /dev/null +++ b/app/admin/controller/Message.php @@ -0,0 +1,85 @@ +request->param(); + $pageInfo = DatabaseRoute::paginateDb('message_info', function ($query) use ($params) { + if (!empty($params['state'])) { + $query->where([ + 'state' => $params['state'] + ]); + } + return $query; + }, $params['page'], $params['limit']); + foreach ($pageInfo['list'] as &$info) { + if (!empty($info['user_id'])) { + $info['userEntity'] = DatabaseRoute::getDb('tb_user', $info['user_id'])->find(); + } + } + + $this->successWithData($pageInfo); + } + + public function save() + { + $params = $this->request->post(); + $params['create_at'] = getNormalDate(); + Db::name('message_info')->insert(convertKeysCamelToSnakeRecursive($params)); + $this->success(); + } + + public function update() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + Db::name('message_info')->where([ + 'id' => $params['id'] + ])->update($params); + + $this->success(); + + } + + public function delete() + { + $params = $this->request->param(); + Db::name('message_info')->delete([ + 'id' => $params['id'] + ]); + + $this->success(); + + } + + + public function selectMessageByUserId() + { + $get = $this->request->get(); + $this->n_success(['data' => MessageInfo::selectMessageList($get)]); + } + + public function selectMessageByType() + { + $get = $this->request->get(); + $this->n_success(['data' => MessageInfo::selectMessageList($get)]); + } + + + + + + +} \ No newline at end of file diff --git a/app/admin/controller/Module.php b/app/admin/controller/Module.php new file mode 100644 index 0000000..7d7d0db --- /dev/null +++ b/app/admin/controller/Module.php @@ -0,0 +1,156 @@ +success('', [ + 'sysVersion' => Config::get('buildadmin.version'), + 'installed' => Server::installedList(root_path() . 'modules' . DIRECTORY_SEPARATOR), + ]); + } + + public function state(): void + { + $uid = $this->request->get("uid/s", ''); + if (!$uid) { + $this->error(__('Parameter error')); + } + $this->success('', [ + 'state' => Manage::instance($uid)->getInstallState() + ]); + } + + public function install(): void + { + AdminLog::instance()->setTitle(__('Install module')); + $uid = $this->request->get("uid/s", ''); + $token = $this->request->get("token/s", ''); + $orderId = $this->request->get("orderId/d", 0); + if (!$uid) { + $this->error(__('Parameter error')); + } + $res = []; + try { + $res = Manage::instance($uid)->install($token, $orderId); + } catch (Exception $e) { + $this->error(__($e->getMessage()), $e->getData(), $e->getCode()); + } catch (Throwable $e) { + $this->error(__($e->getMessage())); + } + $this->success('', [ + 'data' => $res, + ]); + } + + public function dependentInstallComplete(): void + { + $uid = $this->request->get("uid/s", ''); + if (!$uid) { + $this->error(__('Parameter error')); + } + try { + Manage::instance($uid)->dependentInstallComplete('all'); + } catch (Exception $e) { + $this->error(__($e->getMessage()), $e->getData(), $e->getCode()); + } catch (Throwable $e) { + $this->error(__($e->getMessage())); + } + $this->success(); + } + + public function changeState(): void + { + AdminLog::instance()->setTitle(__('Change module state')); + $uid = $this->request->post("uid/s", ''); + $state = $this->request->post("state/b", false); + if (!$uid) { + $this->error(__('Parameter error')); + } + $info = []; + try { + $info = Manage::instance($uid)->changeState($state); + } catch (Exception $e) { + $this->error(__($e->getMessage()), $e->getData(), $e->getCode()); + } catch (Throwable $e) { + $this->error(__($e->getMessage())); + } + $this->success('', [ + 'info' => $info, + ]); + } + + public function uninstall(): void + { + AdminLog::instance()->setTitle(__('Unload module')); + $uid = $this->request->get("uid/s", ''); + if (!$uid) { + $this->error(__('Parameter error')); + } + try { + Manage::instance($uid)->uninstall(); + } catch (Exception $e) { + $this->error(__($e->getMessage()), $e->getData(), $e->getCode()); + } catch (Throwable $e) { + $this->error(__($e->getMessage())); + } + $this->success(); + } + + public function update(): void + { + AdminLog::instance()->setTitle(__('Update module')); + $uid = $this->request->get("uid/s", ''); + $token = $this->request->get("token/s", ''); + $orderId = $this->request->get("orderId/d", 0); + if (!$token || !$uid) { + $this->error(__('Parameter error')); + } + try { + Manage::instance($uid)->update($token, $orderId); + } catch (Exception $e) { + $this->error(__($e->getMessage()), $e->getData(), $e->getCode()); + } catch (Throwable $e) { + $this->error(__($e->getMessage())); + } + $this->success(); + } + + public function upload(): void + { + AdminLog::instance()->setTitle(__('Upload install module')); + $file = $this->request->get("file/s", ''); + $token = $this->request->get("token/s", ''); + if (!$file) $this->error(__('Parameter error')); + if (!$token) $this->error(__('Please login to the official website account first')); + + $info = []; + try { + $info = Manage::instance()->upload($token, $file); + } catch (Exception $e) { + $this->error(__($e->getMessage()), $e->getData(), $e->getCode()); + } catch (Throwable $e) { + $this->error(__($e->getMessage())); + } + $this->success('', [ + 'info' => $info + ]); + } +} \ No newline at end of file diff --git a/app/admin/controller/MoneyDetails.php b/app/admin/controller/MoneyDetails.php new file mode 100644 index 0000000..0ee2969 --- /dev/null +++ b/app/admin/controller/MoneyDetails.php @@ -0,0 +1,52 @@ +request->get(); + if(empty($get['userId'])) { + $this->error('userId is not empty'); + } + + $this->n_success(['data' => User::selectUserMoneyByUserId($get['userId'])]); + + + } + + public function queryUserMoneyDetails() + { + $get = $this->request->get(); + $page = $get['page']; + $limit = $get['limit']; + $userId = $get['userId'] ?? ''; + $classify = $get['classify'] ?? null; + + $this->n_success(['data' => SysUserMoneyDetails::queryUserMoneyDetails($page, $limit, $get['sysUserId'] ?? '', $userId, $classify, null, null, null)]); + + + } + + public function selectSysUserMoney() + { + $get = $this->request->get(); + if(empty($get['userId'])) { + $this->error('userId is not empty'); + } + $this->n_success(['data' => SysUserMoney::selectSysUserMoneyByUserId($get['userId'])]); + + + } +} \ No newline at end of file diff --git a/app/admin/controller/Order.php b/app/admin/controller/Order.php new file mode 100644 index 0000000..e3601d1 --- /dev/null +++ b/app/admin/controller/Order.php @@ -0,0 +1,498 @@ +where([ + 'orders_id' => $id + ]); + }); + } + $this->success(); + } + + public function selectOrders() + { + $params = $this->request->param(); + $this->successWithData(DatabaseRoute::paginateAllDb('orders', function ($query) use ($params) { + // 动态拼接查询条件 + if (!empty($params['userName'])) { + $query->whereLike('u.user_name', '%' . $params['userName'] . '%'); + } + + if (!empty($params['qdCode'])) { + $query->where('s.qd_code', $params['qdCode']); + } + + if (!empty($params['sysUserName'])) { + $query->whereLike('s.username', '%' . $params['sysUserName'] . '%'); + } + + if (!empty($params['ordersNo'])) { + $query->whereLike('o.orders_no', '%' . $params['ordersNo'] . '%'); + } + + if (isset($params['status']) && $params['status'] !== -1) { + $query->where('o.status', $params['status']); + } + + if (!empty($params['userId'])) { + $query->where('o.user_id', $params['userId']); + } + + if (!empty($params['ordersType'])) { + $query->where('o.orders_type', $params['ordersType']); + } + + if (!empty($params['courseId'])) { + $query->where('o.course_id', $params['courseId']); + } + + if (!empty($params['sysUserId'])) { + $query->where('o.sys_user_id', $params['sysUserId']); + } + + // 时间范围 + if (!empty($params['startTime']) && !empty($params['endTime'])) { + $query->whereBetween('o.create_time', [$params['startTime'], $params['endTime']]); + } elseif (!empty($params['startTime'])) { + $query->where('o.create_time', '>=', $params['startTime']); + } elseif (!empty($params['endTime'])) { + $query->where('o.create_time', '<=', $params['endTime']); + } + + // 排序 + $query->order('o.create_time', 'desc'); + return $query->alias('o') + ->field('o.*, u.user_name as userName, s.username as sysUserName, s.qd_code as qdCode') + ->leftJoin('tb_user u', 'o.user_id = u.user_id') + ->leftJoin('sys_user s', 's.user_id = o.sys_user_id'); + }, $params['page'], $params['limit'])); + } + + private function sumOrder($params) + { + // 总收益 + return DatabaseRoute::getAllDbData('orders', function ($query) use ($params) { + // 条件拼接 + if (!empty($params['sysUserId']) && $params['sysUserId'] != 1) { + $query->where('sys_user_id', $params['sysUserId']); + } + + if (isset($params['status'])) { + $query->where('status', $params['status']); + } + + if (isset($params['courseId'])) { + $query->where('course_id', $params['courseId']); + } + + if (isset($params['ordersType'])) { + $query->where('orders_type', $params['ordersType']); + } + + if (isset($params['flag']) && !empty($params['time'])) { + switch ((int)$params['flag']) { + case 1: + // 按日 + $query->whereRaw("DATE_FORMAT(create_time, '%Y-%m-%d') = DATE_FORMAT(:time, '%Y-%m-%d')", ['time' => $params['time']]); + break; + case 2: + // 按月 + $query->whereRaw("DATE_FORMAT(create_time, '%Y-%m') = DATE_FORMAT(:time, '%Y-%m')", ['time' => $params['time']]); + break; + case 3: + // 按年 + $query->whereRaw("DATE_FORMAT(create_time, '%Y') = DATE_FORMAT(:time, '%Y')", ['time' => $params['time']]); + break; + } + } + + $query->where('pay_way', 9) + ->where('status', 1); // 强制条件 + return $query; + })->sum('pay_money'); + } + + + public function selectCourseOrdersMoneyCount() + { + $params = $this->request->param(); + + $params['ordersType'] = 1; + $params['time'] = date('Y-m-d 00:00:00'); + // 总收益 + $sumMoney = $this->sumOrder($params); + $params['flag'] = 3; + $yearMoney = $this->sumOrder($params); + $params['flag'] = 2; + $monthMoney = $this->sumOrder($params); + $params['flag'] = 1; + $dayMoney = $this->sumOrder($params); + $this->successWithData([ + 'sumMoney' => $sumMoney ?? 0, + 'yearMoney' => $yearMoney ?? 0, + 'monthMoney' => $monthMoney ?? 0, + 'dayMoney' => $dayMoney ?? 0 + ]); + } + + + // 订单数量统计 + public function selectOrdersCountStatisticsByYear() + { + $get = $this->request->get(); + $startTime = $get['startTime']; + $endTime = $get['endTime']; + + // 初始化结果数组 + $ordersCountList = []; + $ordersDaiFuKuanCountList = []; + $ordersYiZhiFuCountList = []; + $ordersYiTuiKuanLunCountList = []; + $dateList = []; + + // 日期处理 + $currentDate = strtotime($startTime); + $endDate = strtotime($endTime); + + // 循环遍历日期范围 + while ($currentDate <= $endDate) { + $date = date('Y-m-d', $currentDate); + + // 总订单数 + $ordersCount = \app\admin\model\Order::selectOrdersCountStatisticsByYear(1, $date, null); + $ordersCountList[] = $ordersCount; + + // 0待支付 + $ordersDaiFuKuanCount = \app\admin\model\Order::selectOrdersCountStatisticsByYear(1, $date, 0); + $ordersDaiFuKuanCountList[] = $ordersDaiFuKuanCount; + + // 1已支付 + $ordersJinXinCount = \app\admin\model\Order::selectOrdersCountStatisticsByYear(1, $date, 1); + $ordersYiZhiFuCountList[] = $ordersJinXinCount; + + // 2已退款 + $ordersQuXiaoCount = \app\admin\model\Order::selectOrdersCountStatisticsByYear(1, $date, 2); + $ordersYiTuiKuanLunCountList[] = $ordersQuXiaoCount; + + // 记录日期 + $dateList[] = $date; + + // 日期加1天 + $currentDate = strtotime('+1 day', $currentDate); + } + $result = [ + 'ordersCountList' => $ordersCountList, + 'ordersDaiFuKuanCountList' => $ordersDaiFuKuanCountList, + 'ordersYiZhiFuCountList' => $ordersYiZhiFuCountList, + 'ordersYiTuiKuanLunCountList' => $ordersYiTuiKuanLunCountList, + 'year' => $dateList + ]; + $this->n_success(['data' => $result]); + } + + // 统计分销金币 + public function selectFenXiaoMoney() + { + $get = $this->request->get(); + $admin = $this->auth->getAdmin(); + $sysUserId = $admin['user_id']; + $flag = $get['flag']; + $time = $get['time']; + + // 1. 分别查询一级、二级和渠道分销金额 + $oneMoney = \app\admin\model\Order::selectFenXiaoMoney(1, $sysUserId, $flag, $time); + $twoMoney = \app\admin\model\Order::selectFenXiaoMoney(2, $sysUserId, $flag, $time); + $qdMoney = \app\admin\model\Order::selectFenXiaoMoney(3, $sysUserId, $flag, $time); + + // 2. 计算总分销金额(使用BCMath确保精度) + $sumMoney = bcadd( + bcadd($oneMoney, $twoMoney, 2), // 一级 + 二级 + $qdMoney, // + 渠道 + 2 // 精度保留两位小数 + ); + + $result = [ + 'oneMoney' => $oneMoney, + 'twoMoney' => $twoMoney, + 'qdMoney' => $qdMoney, + 'sumMoney' => $sumMoney + ]; + $this->n_success(['data' => $result]); + } + + // 订单统计 + public function selectOrdersCount() + { + $get = $this->request->get(); + $admin = $this->auth->getAdmin(); + $sysUserId = $admin['user_id']; + $flag = $get['flag']; + $time = $get['time']; + + // 1. 短剧订单统计(数量) + $sumCourseOrdersCount = \app\admin\model\Order::selectOrdersCount(null, 1, $flag, $time, $sysUserId); + $daiCourseKeOrdersCount = \app\admin\model\Order::selectOrdersCount(0, 1, $flag, $time, $sysUserId); + $wanCourseKeOrdersCount = \app\admin\model\Order::selectOrdersCount(1, 1, $flag, $time, $sysUserId); + $tuiCourseOrdersCount = \app\admin\model\Order::selectOrdersCount(2, 1, $flag, $time, $sysUserId); + + // 2. 短剧订单统计(金额) + $sumCourseOrdersMoney = \app\admin\model\Order::selectOrdersMoney(null, 1, $flag, $time, null, $sysUserId); + $daiCourseOrdersMoney = \app\admin\model\Order::selectOrdersMoney(0, 1, $flag, $time, null, $sysUserId); + $wanCourseOrdersMoney = \app\admin\model\Order::selectOrdersMoney(1, 1, $flag, $time, null, $sysUserId); + $tuiCourseOrdersMoney = \app\admin\model\Order::selectOrdersMoney(2, 1, $flag, $time, null, $sysUserId); + + // 3. 会员订单统计(数量) + $sumMemberOrdersCount = \app\admin\model\Order::selectOrdersCount(null, 2, $flag, $time, $sysUserId); + $daiMemberKeOrdersCount = \app\admin\model\Order::selectOrdersCount(0, 2, $flag, $time, $sysUserId); + $wanMemberKeOrdersCount = \app\admin\model\Order::selectOrdersCount(1, 2, $flag, $time, $sysUserId); + $tuiMemberOrdersCount = \app\admin\model\Order::selectOrdersCount(2, 2, $flag, $time, $sysUserId); + + // 4. 会员订单统计(金额) + $sumMemberOrdersMoney = \app\admin\model\Order::selectOrdersMoney(null, 2, $flag, $time, null, $sysUserId); + $daiMemberOrdersMoney = \app\admin\model\Order::selectOrdersMoney(0, 2, $flag, $time, null, $sysUserId); + $wanMemberOrdersMoney = \app\admin\model\Order::selectOrdersMoney(1, 2, $flag, $time, null, $sysUserId); + $tuiMemberOrdersMoney = \app\admin\model\Order::selectOrdersMoney(2, 2, $flag, $time, null, $sysUserId); + + // 5. 提现统计 + $timestamp = strtotime($time); // 将日期字符串转为时间戳 + $beginOfDay = date('Y-m-d 00:00:00', $timestamp); // 当天开始时间 + $endOfDay = date('Y-m-d 23:59:59', $timestamp); // 当天结束时间 + + $cashCount = DatabaseRoute::getAllDbData('cash_out', function ($query) use($sysUserId, $beginOfDay) { + if (!is_null($sysUserId)) { + $query->where(['sys_user_id' => $sysUserId]); + } + $query->where('state', 1)->where('create_at', '>', $beginOfDay); + return $query; + })->count(); + + + $cashSum = DatabaseRoute::getAllDbData('cash_out', function ($query) use($sysUserId, $beginOfDay, $endOfDay) { + if (!is_null($sysUserId)) { + $query->where(['sys_user_id' => $sysUserId]); + } + $query->whereBetween('create_at', [$beginOfDay, $endOfDay]); + return $query; + })->sum('money') ?? 0; + + + + $cashSum = number_format($cashSum, 2, '.', ''); // 保留两位小数 + + // 6. 奖励金额统计 + $signInAwardMoney = \app\admin\model\Order::selectSignInAwardMoney($flag, $time, $sysUserId); + $shareAwardMoney = \app\admin\model\Order::selectShareAwardMoney($flag, $time, $sysUserId); + $newUserTaskDoneAwardMoney = \app\admin\model\Order::selectNewUserTaskDoneAwardMoney($flag, $time, $sysUserId); + $inviteTaskDoneAwardMoney = \app\admin\model\Order::selectInviteTaskDoneAwardMoney($flag, $time, $sysUserId); + + // 7. 组装结果 + $result = [ + // 短剧订单数量 + 'sumCourseOrdersCount' => $sumCourseOrdersCount ?? 0, + 'daiCourseKeOrdersCount' => $daiCourseKeOrdersCount ?? 0, + 'wanCourseKeOrdersCount' => $wanCourseKeOrdersCount ?? 0, + 'tuiCourseOrdersCount' => $tuiCourseOrdersCount ?? 0, + // 短剧订单金额 + 'sumCourseOrdersMoney' => $sumCourseOrdersMoney ?? 0.00, + 'daiCourseOrdersMoney' => $daiCourseOrdersMoney ?? 0.00, + 'wanCourseOrdersMoney' => $wanCourseOrdersMoney ?? 0.00, + 'tuiCourseOrdersMoney' => $tuiCourseOrdersMoney ?? 0.00, + // 会员订单数量 + 'sumMemberOrdersCount' => $sumMemberOrdersCount ?? 0, + 'daiMemberKeOrdersCount' => $daiMemberKeOrdersCount ?? 0, + 'wanMemberKeOrdersCount' => $wanMemberKeOrdersCount ?? 0, + 'tuiMemberOrdersCount' => $tuiMemberOrdersCount ?? 0, + // 会员订单金额 + 'sumMemberOrdersMoney' => $sumMemberOrdersMoney ?? 0.00, + 'daiMemberOrdersMoney' => $daiMemberOrdersMoney ?? 0.00, + 'wanMemberOrdersMoney' => $wanMemberOrdersMoney ?? 0.00, + 'tuiMemberOrdersMoney' => $tuiMemberOrdersMoney ?? 0.00, + // 提现数据 + 'cashCount' => $cashCount ?? 0, + 'cashSum' => $cashSum, + // 奖励金额 + 'signInAwardMoney' => $signInAwardMoney ?? 0.00, + 'shareAwardMoney' => $shareAwardMoney ?? 0.00, + 'newUserTaskDoneAwardMoney' => $newUserTaskDoneAwardMoney ?? 0.00, + 'inviteTaskDoneAwardMoney' => $inviteTaskDoneAwardMoney ?? 0.00, + ]; + + $this->n_success(['data' => $result]); + } + + public function queryByTradeNo() + { + $get = $this->request->get(); + if (empty($get['outTradeNo'])) { + $this->error('参数不能为空'); + } + $outTradeNo = $get['outTradeNo']; + $tradeNo = null; + $userId = null; + try { + // 解析外部交易号(提现回调格式:xxx-xxx:xxx) + if (strpos($outTradeNo, '-') !== false && strpos($outTradeNo, ':') !== false) { + $parts = explode('-', $outTradeNo); + $tradeNo = $parts[0]; + $userIdPart = explode(':', $parts[1])[0]; + $userId = (int)$userIdPart; + } + // 解析支付回调格式(xxx-xxx) + elseif (strpos($outTradeNo, '-') !== false) { + $parts = explode('-', $outTradeNo); + $tradeNo = $parts[0]; + $userId = (int)$parts[1]; + } + } catch (\Exception $e) { + $this->error("交易订单号不合法"); + } + + // 验证解析结果 + if (empty($tradeNo) || $userId === null) { + $this->error("交易订单号不合法"); + } + + // 初始化汇总数据 + $data = []; + + // 查询用户基本信息 + $user = TbUser::selectUserById($userId); + $data['user_info'] = $user ?: []; + + // 查询用户实名认证信息 + $realNameAuth = \app\api\model\UserInfo::getByUserIdOrSave($userId); + $data['auth_info'] = $realNameAuth ?: []; + + // 初始化提现统计(success:成功, fail:失败, auditing:审核中, other:其他) + $withdrawTotal = [ + 'success' => ['total' => 0.00, 'count' => 0], + 'fail' => ['total' => 0.00, 'count' => 0], + 'auditing' => ['total' => 0.00, 'count' => 0], + 'other' => ['total' => 0.00, 'count' => 0], + ]; + + // 初始化支付统计(success:成功, fail:失败, unpaid:未支付) + $payTotal = [ + 'success' => ['total' => 0.00, 'count' => 0], + 'fail' => ['total' => 0.00, 'count' => 0], + 'unpaid' => ['total' => 0.00, 'count' => 0], + ]; + + + // 查询提现记录并统计 + $cashOutList = DatabaseRoute::getDb('cash_out', $userId)->select()->toArray(); + if (!empty($cashOutList)) { + // 按状态分组统计总金额和数量 + $cashOutSum = []; // 金额统计:[状态 => 总金额] + $cashOutCount = []; // 数量统计:[状态 => 总条数] + + foreach ($cashOutList as $cash) { + $state = $cash['state']; + $money = (float)$cash['money']; + + // 累加金额 + if (!isset($cashOutSum[$state])) { + $cashOutSum[$state] = 0.00; + } + $cashOutSum[$state] += $money; + + // 累加数量 + if (!isset($cashOutCount[$state])) { + $cashOutCount[$state] = 0; + } + $cashOutCount[$state]++; + } + + // 更新成功/失败/审核中状态的统计 + $withdrawTotal['success'] = [ + 'total' => $cashOutSum[1] ?? 0.00, + 'count' => $cashOutCount[1] ?? 0 + ]; + $withdrawTotal['fail'] = [ + 'total' => $cashOutSum[2] ?? 0.00, + 'count' => $cashOutCount[2] ?? 0 + ]; + $withdrawTotal['auditing'] = [ + 'total' => $cashOutSum[3] ?? 0.00, + 'count' => $cashOutCount[3] ?? 0 + ]; + + // 统计其他状态(排除1/2/3) + $otherStates = [1, 2, 3]; + $otherMoney = 0.00; + $otherCount = 0; + foreach ($cashOutList as $cash) { + if (!in_array($cash['state'], $otherStates)) { + $otherMoney += (float)$cash['money']; + $otherCount++; + } + } + $withdrawTotal['other'] = [ + 'total' => $otherMoney, + 'count' => $otherCount + ]; + } + $data['withdraw_total'] = $withdrawTotal; + + // 查询支付记录并统计 + $payDetailsList = DatabaseRoute::getDb('pay_details', $userId)->select()->toArray(); + if (!empty($payDetailsList)) { + // 按状态分组统计总金额和数量 + $paySum = []; // 金额统计:[状态 => 总金额] + $payCount = []; // 数量统计:[状态 => 总条数] + + foreach ($payDetailsList as $pay) { + $state = $pay['state']; + $money = (float)$pay['money']; + + // 累加金额 + if (!isset($paySum[$state])) { + $paySum[$state] = 0.00; + } + $paySum[$state] += $money; + + // 累加数量 + if (!isset($payCount[$state])) { + $payCount[$state] = 0; + } + $payCount[$state]++; + } + + // 更新支付统计(1:成功, 2:失败, 0:未支付) + $payTotal['success'] = [ + 'total' => $paySum[1] ?? 0.00, + 'count' => $payCount[1] ?? 0 + ]; + $payTotal['fail'] = [ + 'total' => $paySum[2] ?? 0.00, + 'count' => $payCount[2] ?? 0 + ]; + $payTotal['unpaid'] = [ + 'total' => $paySum[0] ?? 0.00, + 'count' => $payCount[0] ?? 0 + ]; + } + $data['pay_total'] = $payTotal; + $this->n_success(['data' => $data]); + } + + + + + +} \ No newline at end of file diff --git a/app/admin/controller/PayClassify.php b/app/admin/controller/PayClassify.php new file mode 100644 index 0000000..e944ece --- /dev/null +++ b/app/admin/controller/PayClassify.php @@ -0,0 +1,63 @@ +select()->toArray(); + foreach ($infos as $k => &$v) { + $v['pay_classify_id'] = (string) $v['pay_classify_id']; + } + $this->successWithData([ + 'currPage' => 1, + 'pageSize' => count($infos), + 'totalCount' => count($infos), + 'totalPage' => 1, + 'list' => convertToCamelCase($infos) + ]); + } + + public function updatePayClassify() + { + $params = $this->request->post(); + if (empty($params['payClassifyId'])) { + $this->error('参数错误'); + } + unset($params['memberId']); + Db::name('pay_classify')->where([ + 'pay_classify_id' => $params['payClassifyId'] + ])->update(convertKeysCamelToSnakeRecursive($params)); + $this->success(); + } + + public function insertPayClassify() + { + $params = $this->request->post(); + $params['create_time'] = getNormalDate(); + unset($params['memberId']); + unset($params['payClassifyId']); + Db::name('pay_classify')->insert(convertKeysCamelToSnakeRecursive($params)); + $this->success(); + } + + public function deletePayClassify() + { + $params = $this->request->get(); + if (empty($params['payClassifyId'])) { + $this->error('参数错误'); + } + Db::name('pay_classify')->delete([ + 'pay_classify_id' => $params['payClassifyId'] + ]); + $this->success(); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/Sdk.php b/app/admin/controller/Sdk.php new file mode 100644 index 0000000..898a366 --- /dev/null +++ b/app/admin/controller/Sdk.php @@ -0,0 +1,38 @@ +request->get(); + if(empty($get['userId'])) { + $this->error('userId 不能为空'); + } + $sdkINfo = [ + 'sdkRemarks' => $get['sdkRemarks'] ?? null, + 'status' => $get['status'] ?? null, + 'typeId' => $get['typeId'] ?? null, + 'nickName' => $get['nickName'] ?? null, + 'sdkContent' => $get['sdkContent'] ?? null, + ]; + $this->n_success(['data' => [ + 'list' => [], + 'totalCount' => 0, + 'totalPage' => 0, + 'currPage' => 1, + 'pageSize' => 0, + ]]); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/Sys.php b/app/admin/controller/Sys.php new file mode 100644 index 0000000..881dc57 --- /dev/null +++ b/app/admin/controller/Sys.php @@ -0,0 +1,242 @@ +request->post(); + if(empty($data['username']) || empty($data['password']) || empty($data['uuid']) || empty($data['captcha']) || empty($data['adminType'])){ + $this->n_error('参数不完整'); + } + $uuid = $data['uuid']; + $connect = Db::connect(get_slave_connect_name()); + $captcha = $connect->name('sys_captcha')->where(['uuid' => $uuid, 'code' => $data['captcha']])->find(); + if(!$captcha) { + $this->n_error('验证码错误', [], 500); + } + $ext_time = strtotime($captcha['expire_time']); + if(time() > $ext_time) { + $this->n_error('验证码已经过期', [], 500); + } + $res = $this->auth->login($data['username'], $data['password']); + if (isset($res) && $res === true) { + $user = $this->auth->getAdmin(); + if($data['adminType'] == 1 && $user['is_channel'] != null && $user['is_channel'] == 1) { + $this->n_error('代理账号请登录代理端', [], 500); + } + if($data['adminType'] == 2 && $user['is_channel'] == null) { + $this->n_error('管理员请登录管理端', [], 500); + } + $this->n_success([ + 'token' => $this->auth->getToken() + ]); + } else { + $msg = $this->auth->getError(); + $msg = $msg ?: __('Check in failed, please try again or contact the website administrator~'); + $this->n_error($msg); + } + } + + /** + * 管理员注销 + * @return void + */ + public function logout(): void + { + if ($this->request->isPost()) { + $refreshToken = $this->request->post('refreshToken', ''); + if ($refreshToken) Token::delete((string)$refreshToken); + $this->auth->logout(); + $this->success(); + } + } + + + /** + * 邀请好友奖励分页 + * @return void + */ + public function invitefriendaward() + { + $params = $this->request->get(); + $subQuery = DatabaseRoute::getAllDbData('user_money_details', function ($query) use ($params) { + return $query->field('user_id, SUM(money) as awardAmount, SUM(IF(title = "签到奖励", 1, 0)) as signInNum') + ->where('classify', 6)->group('user_id'); + })->buildSql(); + + $result = DatabaseRoute::paginateAllDb('tb_user', function ($query) use ($subQuery , $params) { + $query->alias('t1') + ->field([ + 't1.user_id as userId', + 't1.user_name as userName', + 't1.phone', + 't1.avatar', + 'IFNULL(t2.signInNum, 0) as signInNum', + 'IFNULL(t2.awardAmount, 0) as awardAmount' + ]) + ->leftJoin("{$subQuery} t2", 't1.user_id = t2.user_id'); + if(!empty($params['keywords'])) { + $query->where('t1.user_name', $params['keywords'])->whereOr('t1.phone', $params['keywords']); + } + $query->order('t2.signInNum', 'desc'); + $query->order('t1.user_id', 'asc'); + return $query; + }, (int)$params['page'], (int)$params['limit']); + $this->n_success(['data' => $result]); + } + /** + * 奖励详情 + * @return void + */ + public function invitefrienddetail() + { + $params = $this->request->get(); + $userId = $params['userId']; + $result = DatabaseRoute::paginateAllDb('user_money_details', function ($query) use ($params, $userId) { + $query->alias('t1') + ->field([ + 't1.by_user_id AS userId', + 't1.money AS amount', + 't2.phone AS userPhone', + 't1.create_time AS createTime' + ]) + ->leftJoin('tb_user t2', 't1.by_user_id = t2.user_id') + ->where('t1.user_id', $userId) + ->where('t1.classify', 6) + ->whereNotNull('t1.by_user_id') + ->whereNotNull('t2.user_id') + ->order('t1.create_time', 'desc') + ->order('t1.by_user_id', 'asc'); + return $query; + }, (int)$params['page'], (int)$params['limit'], 'createTime'); + $this->successWithData($result); + } + + /** + * 邀请好友奖励-签到人数分页 + * @return void + */ + public function signindetailpage() + { + $params = $this->request->get(); + $userId = $params['userId']; + // 先获取总数 + $count = DatabaseRoute::getAllDbData('user_money_details', function ($query) use($userId) { + return $query->alias('t') + ->field('t.by_user_id') + ->where('t.user_id', $userId) + ->where('t.classify', 6) + ->where('t.title', '签到奖励') + ->group('t.by_user_id'); + })->count(); + $v_db_name = config('database.connections.' . get_slave_connect_name() . '.database'); + $result = DatabaseRoute::paginateAllDbBySqlAutoCount(function () use($userId, $v_db_name) { + return "select + t1.by_user_id as userId, + t1.createTime, + t2.user_name as userName, + t2.phone, + t3.cert_name as realName, + t3.cert_no as idCardNo, + t3.bank_name as bankName, + t3.account_no as bankCardNo, + t3.mobile, + t3.province, + t3.city, + t3.bank_branch as bankBranch + from ( + SELECT + t.by_user_id, + MIN( t.create_time ) as createTime + FROM + " . $v_db_name . ".v_user_money_details t + WHERE 1=1 + AND t.user_id = ".$userId." + AND t.classify = 6 + AND t.title = '签到奖励' + GROUP BY t.by_user_id + ) t1 + LEFT JOIN " . $v_db_name . ".v_tb_user t2 on t1.by_user_id = t2.user_id + LEFT JOIN " . $v_db_name . ".v_user_info t3 on t1.by_user_id = t3.user_id + order by t1.createTime desc,t1.by_user_id asc"; + }, $params['page'], $params['limit'], null, $count); + $this->successWithData($result); + } + + /** + * 抽奖次数查询-分页 + * @return void + */ + public function lotterypage() + { + $params = $this->request->get(); + $keywords = $params['keywords']; + $v_db_name = config('database.connections.' . get_slave_connect_name() . '.database'); + $result = DatabaseRoute::paginateAllDb('tb_user', function ($query) use($keywords, $v_db_name) { + return $query->alias('t1') + ->field([ + 't1.user_id as userId', + 't1.avatar', + 't1.user_name as userName', + 't1.phone', + // 今日解锁订单数 + '(SELECT COUNT(1) FROM '. $v_db_name .'.v_orders WHERE STATUS = 1 AND pay_way = 9 AND user_id = t1.user_id AND pay_time >= DATE_FORMAT(CURDATE(), "%Y-%m-%d 00:00:00") AND pay_time <= DATE_FORMAT(CURDATE(), "%Y-%m-%d 23:59:59")) as todayUnlocked', + // 今日抽奖次数 + '(SELECT COUNT(1) FROM '. $v_db_name .'.v_disc_spinning_record WHERE user_id = t1.user_id AND DATE_FORMAT(create_time, "%Y-%m-%d") = CURDATE()) as todayDrawCount' + ]) + ->where(function ($query) use ($keywords) { + $query->where('t1.user_name', $keywords) + ->whereOr('t1.phone', $keywords); + }) + ->order('todayUnlocked', 'desc') + ->order('t1.user_id', 'asc'); + }, $params['page'], $params['limit']); + + $this->n_success(['data' => $result]); + } + + /** + * 抽奖详情-分页 + * @return void + */ + public function lotterydetailpage() + { + $params = $this->request->get(); + $userId = $params['userId']; + // 先获取总数 + $count = Db::connect(get_slave_connect_name())->query("select + count(*) count + from v_disc_spinning_record t1 + where t1.user_id = ".$userId); + $count = $count[0]['count']; + $v_db_name = config('database.connections.' . get_slave_connect_name() . '.database'); + $result = DatabaseRoute::paginateAllDbBySqlAutoCount(function () use($userId, $v_db_name) { + return "select + t1.id, + t1.name, + t1.number, + t1.create_time + from ". $v_db_name .".v_disc_spinning_record t1 + where t1.user_id = " . $userId . " + order by t1.id desc"; + }, $params['page'], $params['limit'], null, $count); + $this->successWithData($result); + } + + // 发送验证码 + public function sendMsg() + { + $get = $this->request->get(); + return $this->ApiDataReturn(Msg::sendMsg($get['phone'], $get['event'])); + } +} \ No newline at end of file diff --git a/app/admin/controller/TaskCenter.php b/app/admin/controller/TaskCenter.php new file mode 100644 index 0000000..1d83921 --- /dev/null +++ b/app/admin/controller/TaskCenter.php @@ -0,0 +1,56 @@ +request->param(); + $this->successWithData(DatabaseRoute::paginateDb('task_center', function ($query) use ($params) { + return $query->order('sort'); + }, $params['page'], $params['limit'], null, true)); + } + + public function updateTaskCenter() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $params['update_time'] = getNormalDate(); + unset($params['disc_number']); + unset($params['disabled']); + Db::name('task_center')->where([ + 'id' => $params['id'] + ])->update($params); + $this->success(); + + } + + public function insertTaskCenter() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $params['create_time'] = getNormalDate(); + unset($params['is_trusted']); + Db::name('task_center')->insert($params); + $this->success(); + } + + public function deleteTaskCenter() + { + $id = $this->request->param('id'); + Db::name('task_center')->delete([ + 'id' => $id + ]); + + $this->success(); + } + +} \ No newline at end of file diff --git a/app/admin/controller/TaskCenterReward.php b/app/admin/controller/TaskCenterReward.php new file mode 100644 index 0000000..a27cd4c --- /dev/null +++ b/app/admin/controller/TaskCenterReward.php @@ -0,0 +1,69 @@ +request->param(); + $this->successWithData(DatabaseRoute::paginateDb('task_center_reward', function ($query) use ($params) { + if (!empty($params['taskId'])) { + $query->where([ + 'task_id' => $params['taskId'] + ]); + } + return $query->order('id', false); + + }, $params['page'] ?? 1, $params['limit'] ?? 10)); + } + + public function updateTaskCenterReward() + { + $params = $this->request->param(); + $params = convertKeysCamelToSnakeRecursive($params); + $info = Db::name('task_center_reward')->where([ + 'id' => $params['id'] + ]); + + if (!empty($info['total_number']) && !empty($params['total_number']) && $info['total_number'] != $params['total_number']) { + $surplusNumber = $params['total_number'] - $info['total_number']; + $surplusNumber = $surplusNumber > 0 ?? $params['total_number']; + $params['total_number'] = $surplusNumber; + } + + $params['update_time'] = getNormalDate(); + Db::name('task_center_reward')->where([ + 'id' => $params['id'] + ])->update($params); + + $this->success(); + } + + public function insertTaskCenterReward() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + $params['surplus_number'] = $params['total_number']; + unset($params['is_trusted']); + unset($params['id']); + Db::name('task_center_reward')->insert($params); + $this->success(); + } + + public function deleteTaskCenterReward() + { + $id = $this->request->param('id'); + Db::name('task_center_reward')->delete([ + 'id' => $id + ]); + $this->success(); + } +} \ No newline at end of file diff --git a/app/admin/controller/UrlAddress.php b/app/admin/controller/UrlAddress.php new file mode 100644 index 0000000..b9b9e2d --- /dev/null +++ b/app/admin/controller/UrlAddress.php @@ -0,0 +1,97 @@ +order('num', 'asc')->limit(1)->find(); + if ($info) { + $info['num'] = $info['num'] ? $info['num'] + 1 : 1; + Db::name('url_address')->where([ + 'url_id' => $info['url_id'] + ])->update([ + 'num' => $info['num'] + ]); + } + + $this->successWithData(convertToCamelCase($info)); + + } + + public function selectUrlAddressList() + { + $get = $this->request->get(); + $page = $get['page']; // 页码,默认1 + $limit = $get['limit']; // 每页条数,默认10 + $urlAddress = $get['urlAddress']; // URL地址关键词 + $status = $get['status']; // 状态值 + $db = Db::connect(get_master_connect_name()); + // 构建查询 + $query = $db->name('url_address'); + if(!empty($urlAddress)) { + $query->where('url_address', 'like', "%{$urlAddress}%"); + } + if(!empty($status)) { + $query->where('status', $status); + } + $count = $query->count(); + $info = $query->limit(page($page, $limit), $limit)->select()->toArray(); + $this->n_success(['data' => [ + 'totalCount' => $count, + 'pageSize' => $get['limit'], + 'totalPage' => ceil($count / $get['limit']), + 'currPage' => $get['page'], + 'list' => $info, + 'records' => null + ]]); + } + + + public function updateUrlAddress() + { + $post = $this->request->post(); + + $url_id = $post['urlId'] ?? null; + $data['num'] = $post['num'] ?? null; + $data['url_address'] = $post['urlAddress'] ?? null; + $data['status'] = $post['status'] ?? null; + Db::name('url_address')->where(['url_id' => $url_id])->update($data); + $this->success(); + } + + + // 创建域名 + public function insertUrlAddress() + { + $post = $this->request->post(); + $data['num'] = $post['num'] ?? null; + $data['url_address'] = $post['urlAddress'] ?? null; + $data['status'] = $post['status'] ?? null; + $data['create_time'] = date('Y-m-d H:i:s'); + Db::name('url_address')->insert($data); + $this->success(); + } + + public function deleteUrlAddress() + { + $param = $this->request->get(); + if(empty($param['addressId'])) { + $this->error('参数错误'); + } + Db::name('url_address')->where(['url_id' => $param['addressId']])->delete(); + $this->success(); + } + + + + + + +} \ No newline at end of file diff --git a/app/admin/controller/UserInfo.php b/app/admin/controller/UserInfo.php new file mode 100644 index 0000000..49a5523 --- /dev/null +++ b/app/admin/controller/UserInfo.php @@ -0,0 +1,84 @@ +request->param(); + $result = DatabaseRoute::paginateAllDb('user_info', function ($query) use ($params) { + if (!empty($params['phone'])) { + $user = DatabaseRoute::getAllDbData('tb_user', function ($q) use ($params) { + return $q->where('phone', $params['phone']); + })->find(); + $userId = $user ? $user['user_id'] : -99999; + $query->where('user_id', $userId); + } + + if (!empty($name)) { + $query->whereLike('cert_name', "%{$name}%"); + } + + return $query; + }, $params['page'], $params['limit'], 'id', 'id'); + + // 用户信息补全 + $userInfoList = $result['list']; + $userIds = array_column($userInfoList, 'user_id'); + + if (!empty($userIds)) { + $userMap = DatabaseRoute::getAllDbData('tb_user', function ($query) use ($params, $userIds) { + return $query + ->whereIn('user_id', $userIds); + })->select(); + + foreach ($userInfoList as &$item) { + $user = $userMap[$item['user_id']] ?? null; + $item['name'] = $user['user_name'] ?? null; + $item['phone'] = $user['phone'] ?? null; + } + unset($item); + + $result['list'] = $userInfoList; + } + + $this->successWithData($result); + } + + + public function update() + { + $params = $this->request->put(); + if (empty($params['userId'])) { + $this->error('参数错误'); + } + + $params['update_time'] = getNormalDate(); + DatabaseRoute::getDb('user_info', $params['userId'], true, true)->update([ + 'cert_name' => $params['certName'] ?? '', + 'cert_no' => $params['certNo'] ?? '', + 'account_no' => $params['accountNo'] ?? '', + 'mobile' => $params['mobile'] ?? '', + 'bank_name' => $params['bankName'] ?? '', + ]); + $this->success(); + } + + public function delete() + { + $params = $this->request->delete(); + if (empty($params['userId'])) { + $this->error('参数错误'); + } + DatabaseRoute::getDb('user_info', $params['userId'], true, true)->delete(); + $this->success(); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/UserPrizeExchange.php b/app/admin/controller/UserPrizeExchange.php new file mode 100644 index 0000000..5e9b986 --- /dev/null +++ b/app/admin/controller/UserPrizeExchange.php @@ -0,0 +1,84 @@ +request->param(); + $this->successWithData(DatabaseRoute::paginateDb('user_prize_exchange', function ($query) use ($params) { + if (!empty($params['foreignId'])) { + $query->where('foreign_id', $params['foreignId']); + } + if (!empty($params['foreignType'])) { + $query->where('foreign_type', $params['foreignType']); + } + if (!empty($params['userId'])) { + $query->where('user_id', $params['userId']); + } + if (!empty($params['userName'])) { + $query->where('user_name', 'like', "%". $params['userName']."%"); + } + if (!empty($params['prizeName'])) { + $query->where('prize_name', 'like', "%" . $params['prizeName'] . "%"); + } + if (!empty($params['status'])) { + $query->where('status', $params['status']); + } + if (!empty($params['phone'])) { + $query->where('phone', 'like', "%". $params['phone'] ."%"); + } + if (!empty($params['remark'])) { + $query->where('remark', 'like', "%" . $params['remark'] . "%"); + } + + if (!empty($params['beginDate'])) { + $query->where('create_time', '>=', $params['beginDate'] . ' 00:00:00'); + } + if (!empty($params['endDate'])) { + $query->where('create_time', '<=', $params['endDate'] . ' 23:59:59'); + } + $query->order('id', false); + })); + } + + public function deliver() + { + $params = $this->request->post(); + if (empty($params['id'])) { + $this->error('兑奖id不能为空'); + } + + $info = Db::name('user_prize_exchange')->where([ + 'id' => $params['id'] + ])->find(); + if (!$info) { + $this->error('兑奖订单不存在'); + } + + Db::name('user_prize_exchange')->where([ + 'id' => $params['id'] + ])->update([ + 'status' => 1, + 'address' => $params['address'] ?? '', + 'remark' => $params['remark'] ?? '', + 'update_time' => getNormalDate() + ]); + + $this->success(); + + + } + + + +} \ No newline at end of file diff --git a/app/admin/controller/VipDetails.php b/app/admin/controller/VipDetails.php new file mode 100644 index 0000000..58e4149 --- /dev/null +++ b/app/admin/controller/VipDetails.php @@ -0,0 +1,128 @@ +request->post(); + $userId = $data['userId'] ?? null; + $num = $data['num'] ?? null; + $userVip = \app\admin\model\User::selectUserVipByUserId($userId); + + // 获取当前时间戳 + $currentTime = time(); + + // 如果用户已有 VIP 记录 + if ($userVip) { + // 如果当前是 VIP 会员(假设 is_vip=2 表示 VIP) + if ($userVip['is_vip'] == 2) { + // 从现有结束时间开始叠加(转换为时间戳) + $endTime = strtotime($userVip['end_time']); + } else { + // 非 VIP 从当前时间开始 + $endTime = $currentTime; + } + } else { + // 新建 VIP 记录 + $userVip = [ + 'user_id' => $userId, + 'create_time' => date('Y-m-d H:i:s', $currentTime) + ]; + $endTime = $currentTime; + } + + // 设置 VIP 类型和状态 + $userVip['vip_type'] = 1; + $userVip['is_vip'] = 2; + + // 计算新的结束时间(增加指定天数,每天 86400 秒) + $endTime += $num * 86400; + $userVip['end_time'] = date('Y-m-d H:i:s', $endTime); + $db = Db::connect(get_master_connect_name())->name('user_vip'); + // 保存或更新记录 + if (isset($userVip['vip_id'])) { + // 更新记录 + $db->where('vip_id', $userVip['vip_id']) + ->update($userVip); + } else { + // 插入新记录 + $db->insert($userVip); + } + $this->success(); + + } + + + public function selectVipDetailsList() + { + $get = $this->request->get(); + $page = $get['page'] ?? null; + $limit = $get['limit'] ?? null; + $db = Db::connect(get_slave_connect_name())->name('vip_details'); + $count = $db->count(); + $data = $db->limit(page($page, $limit), $limit)->select()->toArray(); + $this->n_success(['data' => [ + 'currPage' => $get['page'], + 'pageSize' => $get['limit'], + 'list' => array_map(function ($item) { + $item['id'] = (string)$item['id']; + return $item; + }, $data), + 'totalCount' => $count, + 'totalPage' => ceil($count / $get['limit']), + ]]); + } + + + public function insertVipDetails() + { + $post = $this->request->post(); + $data['vip_name_type'] = $post['vipNameType'] ?? null; + $data['money'] = $post['money'] ?? null; + $data['pay_diamond'] = $post['payDiamond'] ?? null; + $db = Db::connect(get_master_connect_name()); + $res = $db->name('vip_details')->insert($data); + if($res) { + $this->success('成功'); + }else { + $this->error('失败'); + } + } + + + public function updateVipDetails() + { + $post = $this->request->post(); + $id = $post['id'] ?? null; + $data['vip_name_type'] = $post['vipNameType'] ?? null; + $data['money'] = $post['money'] ?? null; + $data['pay_diamond'] = $post['payDiamond'] ?? null; + $db = Db::connect(get_master_connect_name()); + $res = $db->name('vip_details')->where(['id' => $id])->update($data); + $this->success('成功'); + } + + public function deleteVipDetails() + { + $post = $this->request->param(); + if(empty($post['id'])) { + $this->error('参数不完整'); + } + $id = $post['id']; + $db = Db::connect(get_master_connect_name()); + $res = $db->name('vip_details')->where(['id' => $id])->delete(); + $this->success('成功'); + } + + + + +} \ No newline at end of file diff --git a/app/admin/controller/auth/Admin.php b/app/admin/controller/auth/Admin.php new file mode 100644 index 0000000..c8f820e --- /dev/null +++ b/app/admin/controller/auth/Admin.php @@ -0,0 +1,261 @@ +model = new AdminModel(); + } + + /** + * 查看 + * @throws Throwable + */ + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + list($where, $alias, $limit, $order) = $this->queryBuilder(); + $res = $this->model + ->withoutField('login_failure,password,salt') + ->withJoin($this->withJoinTable, $this->withJoinType) + ->alias($alias) + ->where($where) + ->order($order) + ->paginate($limit); + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + 'remark' => get_route_remark(), + ]); + } + + /** + * 添加 + * @throws Throwable + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + if ($this->modelValidate) { + try { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + $validate = new $validate(); + $validate->scene('add')->check($data); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + } + + $passwd = $data['password']; // 密码将被排除不直接入库 + $data = $this->excludeFields($data); + $result = false; + if ($data['group_arr']) $this->checkGroupAuth($data['group_arr']); + $this->model->startTrans(); + try { + $result = $this->model->save($data); + if ($data['group_arr']) { + $groupAccess = []; + foreach ($data['group_arr'] as $datum) { + $groupAccess[] = [ + 'uid' => $this->model->id, + 'group_id' => $datum, + ]; + } + Db::name('admin_group_access')->insertAll($groupAccess); + } + $this->model->commit(); + + if (!empty($passwd)) { + $this->model->resetPassword($this->model->id, $passwd); + } + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + $this->error(__('Parameter error')); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) { + $this->error(__('You have no permission')); + } + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + if ($this->modelValidate) { + try { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + $validate = new $validate(); + $validate->scene('edit')->check($data); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + } + + if ($this->auth->id == $data['id'] && $data['status'] == 'disable') { + $this->error(__('Please use another administrator account to disable the current account!')); + } + + if (!empty($data['password'])) { + $this->model->resetPassword($row->id, $data['password']); + } + + $groupAccess = []; + if ($data['group_arr']) { + $checkGroups = []; + foreach ($data['group_arr'] as $datum) { + if (!in_array($datum, $row->group_arr)) { + $checkGroups[] = $datum; + } + $groupAccess[] = [ + 'uid' => $id, + 'group_id' => $datum, + ]; + } + $this->checkGroupAuth($checkGroups); + } + + Db::name('admin_group_access') + ->where('uid', $id) + ->delete(); + + $data = $this->excludeFields($data); + $result = false; + $this->model->startTrans(); + try { + $result = $row->save($data); + if ($groupAccess) Db::name('admin_group_access')->insertAll($groupAccess); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + } + + unset($row['salt'], $row['login_failure']); + $row['password'] = ''; + $this->success('', [ + 'row' => $row + ]); + } + + /** + * 删除 + * @throws Throwable + */ + public function del(): void + { + $where = []; + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds) { + $where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds]; + } + + $ids = $this->request->param('ids/a', []); + $where[] = [$this->model->getPk(), 'in', $ids]; + $data = $this->model->where($where)->select(); + + $count = 0; + $this->model->startTrans(); + try { + foreach ($data as $v) { + if ($v->id != $this->auth->id) { + $count += $v->delete(); + Db::name('admin_group_access') + ->where('uid', $v['id']) + ->delete(); + } + } + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($count) { + $this->success(__('Deleted successfully')); + } else { + $this->error(__('No rows were deleted')); + } + } + + /** + * 检查分组权限 + * @throws Throwable + */ + private function checkGroupAuth(array $groups): void + { + if ($this->auth->isSuperAdmin()) { + return; + } + $authGroups = $this->auth->getAllAuthGroups('allAuthAndOthers'); + foreach ($groups as $group) { + if (!in_array($group, $authGroups)) { + $this->error(__('You have no permission to add an administrator to this group!')); + } + } + } +} \ No newline at end of file diff --git a/app/admin/controller/auth/AdminLog.php b/app/admin/controller/auth/AdminLog.php new file mode 100644 index 0000000..ace3b80 --- /dev/null +++ b/app/admin/controller/auth/AdminLog.php @@ -0,0 +1,54 @@ +model = new AdminLogModel(); + } + + /** + * 查看 + * @throws Throwable + */ + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + list($where, $alias, $limit, $order) = $this->queryBuilder(); + if (!$this->auth->isSuperAdmin()) { + $where[] = ['admin_id', '=', $this->auth->id]; + } + $res = $this->model + ->withJoin($this->withJoinTable, $this->withJoinType) + ->alias($alias) + ->where($where) + ->order($order) + ->paginate($limit); + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + 'remark' => get_route_remark(), + ]); + } +} \ No newline at end of file diff --git a/app/admin/controller/auth/Group.php b/app/admin/controller/auth/Group.php new file mode 100644 index 0000000..9b3f4ab --- /dev/null +++ b/app/admin/controller/auth/Group.php @@ -0,0 +1,379 @@ +model = new AdminGroup(); + $this->tree = Tree::instance(); + + $isTree = $this->request->param('isTree', true); + $this->initValue = $this->request->get("initValue/a", []); + $this->initValue = array_filter($this->initValue); + $this->keyword = $this->request->request("quickSearch", ''); + + // 有初始化值时不组装树状(初始化出来的值更好看) + $this->assembleTree = $isTree && !$this->initValue; + + $this->adminGroups = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id'); + } + + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + $this->success('', [ + 'list' => $this->getGroups(), + 'group' => $this->adminGroups, + 'remark' => get_route_remark(), + ]); + } + + /** + * 添加 + * @throws Throwable + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $data = $this->handleRules($data); + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + $validate->scene('add')->check($data); + } + } + $result = $this->model->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + $this->error(__('Parameter error')); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + $this->checkAuth($id); + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $adminGroup = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id'); + if (in_array($data['id'], $adminGroup)) { + $this->error(__('You cannot modify your own management group!')); + } + + $data = $this->excludeFields($data); + $data = $this->handleRules($data); + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + $validate->scene('edit')->check($data); + } + } + $result = $row->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + } + + // 读取所有pid,全部从节点数组移除,父级选择状态由子级决定 + $pidArr = AdminRule::field('pid') + ->distinct() + ->where('id', 'in', $row->rules) + ->select() + ->toArray(); + $rules = $row->rules ? explode(',', $row->rules) : []; + foreach ($pidArr as $item) { + $ruKey = array_search($item['pid'], $rules); + if ($ruKey !== false) { + unset($rules[$ruKey]); + } + } + $row->rules = array_values($rules); + $this->success('', [ + 'row' => $row + ]); + } + + /** + * 删除 + * @throws Throwable + */ + public function del(): void + { + $ids = $this->request->param('ids/a', []); + $data = $this->model->where($this->model->getPk(), 'in', $ids)->select(); + foreach ($data as $v) { + $this->checkAuth($v->id); + } + $subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id'); + foreach ($subData as $key => $subDatum) { + if (!in_array($key, $ids)) { + $this->error(__('Please delete the child element first, or use batch deletion')); + } + } + + $adminGroup = Db::name('admin_group_access')->where('uid', $this->auth->id)->column('group_id'); + $count = 0; + $this->model->startTrans(); + try { + foreach ($data as $v) { + if (!in_array($v['id'], $adminGroup)) { + $count += $v->delete(); + } + } + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($count) { + $this->success(__('Deleted successfully')); + } else { + $this->error(__('No rows were deleted')); + } + } + + /** + * 远程下拉 + * @return void + * @throws Throwable + */ + public function select(): void + { + $data = $this->getGroups([['status', '=', 1]]); + + if ($this->assembleTree) { + $data = $this->tree->assembleTree($this->tree->getTreeArray($data)); + } + $this->success('', [ + 'options' => $data + ]); + } + + /** + * 权限节点入库前处理 + * @throws Throwable + */ + private function handleRules(array &$data): array + { + if (!empty($data['rules']) && is_array($data['rules'])) { + $superAdmin = true; + $checkedRules = []; + $allRuleIds = AdminRule::column('id'); + + // 遍历检查权限ID是否存在(以免传递了可预测的未来权限ID号) + foreach ($data['rules'] as $postRuleId) { + if (in_array($postRuleId, $allRuleIds)) { + $checkedRules[] = $postRuleId; + } + } + + // 正在建立超管级分组? + foreach ($allRuleIds as $ruleId) { + if (!in_array($ruleId, $checkedRules)) { + $superAdmin = false; + } + } + + if ($superAdmin && $this->auth->isSuperAdmin()) { + // 允许超管建立超管级分组 + $data['rules'] = '*'; + } else { + // 当前管理员所拥有的权限节点 + $ownedRuleIds = $this->auth->getRuleIds(); + + // 禁止添加`拥有自己全部权限`的分组 + if (!array_diff($ownedRuleIds, $checkedRules)) { + $this->error(__('Role group has all your rights, please contact the upper administrator to add or do not need to add!')); + } + + // 检查分组权限是否超出了自己的权限(超管的 $ownedRuleIds 为 ['*'],不便且可以不做此项检查) + if (array_diff($checkedRules, $ownedRuleIds) && !$this->auth->isSuperAdmin()) { + $this->error(__('The group permission node exceeds the range that can be allocated')); + } + + $data['rules'] = implode(',', $checkedRules); + } + } else { + unset($data['rules']); + } + return $data; + } + + /** + * 获取分组 + * @param array $where + * @return array + * @throws Throwable + */ + private function getGroups(array $where = []): array + { + $pk = $this->model->getPk(); + $initKey = $this->request->get("initKey/s", $pk); + + // 下拉选择时只获取:拥有所有权限并且有额外权限的分组 + $absoluteAuth = $this->request->get('absoluteAuth/b', false); + + if ($this->keyword) { + $keyword = explode(' ', $this->keyword); + foreach ($keyword as $item) { + $where[] = [$this->quickSearchField, 'like', '%' . $item . '%']; + } + } + + if ($this->initValue) { + $where[] = [$initKey, 'in', $this->initValue]; + } + + if (!$this->auth->isSuperAdmin()) { + $authGroups = $this->auth->getAllAuthGroups($this->authMethod, $where); + if (!$absoluteAuth) $authGroups = array_merge($this->adminGroups, $authGroups); + $where[] = ['id', 'in', $authGroups]; + } + $data = $this->model->where($where)->select()->toArray(); + + // 获取第一个权限的名称供列表显示-s + foreach ($data as &$datum) { + if ($datum['rules']) { + if ($datum['rules'] == '*') { + $datum['rules'] = __('Super administrator'); + } else { + $rules = explode(',', $datum['rules']); + if ($rules) { + $rulesFirstTitle = AdminRule::where('id', $rules[0])->value('title'); + $datum['rules'] = count($rules) == 1 ? $rulesFirstTitle : $rulesFirstTitle . '等 ' . count($rules) . ' 项'; + } + } + } else { + $datum['rules'] = __('No permission'); + } + } + // 获取第一个权限的名称供列表显示-e + + // 如果要求树状,此处先组装好 children + return $this->assembleTree ? $this->tree->assembleChild($data) : $data; + } + + /** + * 检查权限 + * @param $groupId + * @return void + * @throws Throwable + */ + private function checkAuth($groupId): void + { + $authGroups = $this->auth->getAllAuthGroups($this->authMethod, []); + if (!$this->auth->isSuperAdmin() && !in_array($groupId, $authGroups)) { + $this->error(__($this->authMethod == 'allAuth' ? 'You need to have all permissions of this group to operate this group~' : 'You need to have all the permissions of the group and have additional permissions before you can operate the group~')); + } + } + +} \ No newline at end of file diff --git a/app/admin/controller/auth/Rule.php b/app/admin/controller/auth/Rule.php new file mode 100644 index 0000000..adbe210 --- /dev/null +++ b/app/admin/controller/auth/Rule.php @@ -0,0 +1,274 @@ + 'desc']; + + protected string|array $quickSearchField = 'title'; + + /** + * @var object + * @phpstan-var AdminRule + */ + protected object $model; + + /** + * @var Tree + */ + protected Tree $tree; + + /** + * 远程select初始化传值 + * @var array + */ + protected array $initValue; + + /** + * 搜索关键词 + * @var string + */ + protected string $keyword; + + /** + * 是否组装Tree + * @var bool + */ + protected bool $assembleTree; + + /** + * 开启模型验证 + * @var bool + */ + protected bool $modelValidate = false; + + public function initialize(): void + { + parent::initialize(); + + // 防止 URL 中的特殊符号被默认的 filter 函数转义 + $this->request->filter('clean_xss'); + + $this->model = new AdminRule(); + $this->tree = Tree::instance(); + $isTree = $this->request->param('isTree', true); + $this->initValue = $this->request->get('initValue/a', []); + $this->initValue = array_filter($this->initValue); + $this->keyword = $this->request->request('quickSearch', ''); + $this->assembleTree = $isTree && !$this->initValue; // 有初始化值时不组装树状(初始化出来的值更好看) + } + + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + $this->success('', [ + 'list' => $this->getMenus(), + 'remark' => get_route_remark(), + ]); + } + + /** + * 添加 + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + if ($this->dataLimit && $this->dataLimitFieldAutoFill) { + $data[$this->dataLimitField] = $this->auth->id; + } + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('add'); + $validate->check($data); + } + } + $result = $this->model->save($data); + + // 检查所有非超管的分组是否应该拥有此权限 + if (!empty($data['pid'])) { + $groups = AdminGroup::where('rules', '<>', '*')->select(); + foreach ($groups as $group) { + $rules = explode(',', $group->rules); + if (in_array($data['pid'], $rules) && !in_array($this->model->id, $rules)) { + $rules[] = $this->model->id; + $group->rules = implode(',', $rules); + $group->save(); + } + } + } + + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + $this->error(__('Parameter error')); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $id = $this->request->param($this->model->getPk()); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) { + $this->error(__('You have no permission')); + } + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('edit'); + $validate->check($data); + } + } + if (isset($data['pid']) && $data['pid'] > 0) { + // 满足意图并消除副作用 + $parent = $this->model->where('id', $data['pid'])->find(); + if ($parent['pid'] == $row['id']) { + $parent->pid = 0; + $parent->save(); + } + } + $result = $row->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + } + + $this->success('', [ + 'row' => $row + ]); + } + + /** + * 删除 + * @throws Throwable + */ + public function del(): void + { + $ids = $this->request->param('ids/a', []); + + // 子级元素检查 + $subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id'); + foreach ($subData as $key => $subDatum) { + if (!in_array($key, $ids)) { + $this->error(__('Please delete the child element first, or use batch deletion')); + } + } + + parent::del(); + } + + /** + * 重写select方法 + * @throws Throwable + */ + public function select(): void + { + $data = $this->getMenus([['type', 'in', ['menu_dir', 'menu']], ['status', '=', 1]]); + + if ($this->assembleTree) { + $data = $this->tree->assembleTree($this->tree->getTreeArray($data, 'title')); + } + $this->success('', [ + 'options' => $data + ]); + } + + /** + * 获取菜单列表 + * @throws Throwable + */ + protected function getMenus($where = []): array + { + $pk = $this->model->getPk(); + $initKey = $this->request->get("initKey/s", $pk); + + $ids = $this->auth->getRuleIds(); + + // 如果没有 * 则只获取用户拥有的规则 + if (!in_array('*', $ids)) { + $where[] = ['id', 'in', $ids]; + } + + if ($this->keyword) { + $keyword = explode(' ', $this->keyword); + foreach ($keyword as $item) { + $where[] = [$this->quickSearchField, 'like', '%' . $item . '%']; + } + } + + if ($this->initValue) { + $where[] = [$initKey, 'in', $this->initValue]; + } + + // 读取用户组所有权限规则 + $rules = $this->model + ->where($where) + ->order($this->queryOrderBuilder()) + ->select() + ->toArray(); + + // 如果要求树状,此处先组装好 children + return $this->assembleTree ? $this->tree->assembleChild($rules) : $rules; + } +} \ No newline at end of file diff --git a/app/admin/controller/crud/Crud.php b/app/admin/controller/crud/Crud.php new file mode 100644 index 0000000..217c45d --- /dev/null +++ b/app/admin/controller/crud/Crud.php @@ -0,0 +1,992 @@ +request->post('type', ''); + $table = $this->request->post('table', []); + $fields = $this->request->post('fields', [], 'clean_xss,htmlspecialchars_decode_improve'); + + if (!$table || !$fields || !isset($table['name']) || !$table['name']) { + $this->error(__('Parameter error')); + } + + try { + // 记录日志 + $crudLogId = Helper::recordCrudStatus([ + 'table' => $table, + 'fields' => $fields, + 'status' => 'start', + ]); + + // 表名称 + $tableName = TableManager::tableName($table['name'], false, $table['databaseConnection']); + + if ($type == 'create' || $table['rebuild'] == 'Yes') { + // 数据表存在则删除 + TableManager::phinxTable($tableName, [], true, $table['databaseConnection'])->drop()->save(); + } + + // 处理表设计 + [$tablePk] = Helper::handleTableDesign($table, $fields); + + // 表注释 + $tableComment = mb_substr($table['comment'], -1) == '表' ? mb_substr($table['comment'], 0, -1) . '管理' : $table['comment']; + + // 生成文件信息解析 + $modelFile = Helper::parseNameData($table['isCommonModel'] ? 'common' : 'admin', $tableName, 'model', $table['modelFile']); + $validateFile = Helper::parseNameData($table['isCommonModel'] ? 'common' : 'admin', $tableName, 'validate', $table['validateFile']); + $controllerFile = Helper::parseNameData('admin', $tableName, 'controller', $table['controllerFile']); + $webViewsDir = Helper::parseWebDirNameData($tableName, 'views', $table['webViewsDir']); + $webLangDir = Helper::parseWebDirNameData($tableName, 'lang', $table['webViewsDir']); + + // 语言翻译前缀 + $this->webTranslate = implode('.', $webLangDir['lang']) . '.'; + + // 快速搜索字段 + if (!in_array($tablePk, $table['quickSearchField'])) { + $table['quickSearchField'][] = $tablePk; + } + $quickSearchFieldZhCnTitle = []; + + // 模型数据 + $this->modelData['append'] = []; + $this->modelData['methods'] = []; + $this->modelData['fieldType'] = []; + $this->modelData['createTime'] = ''; + $this->modelData['updateTime'] = ''; + $this->modelData['beforeInsertMixins'] = []; + $this->modelData['beforeInsert'] = ''; + $this->modelData['afterInsert'] = ''; + $this->modelData['connection'] = $table['databaseConnection']; + $this->modelData['name'] = $tableName; + $this->modelData['className'] = $modelFile['lastName']; + $this->modelData['namespace'] = $modelFile['namespace']; + $this->modelData['relationMethodList'] = []; + + // 控制器数据 + $this->controllerData['use'] = []; + $this->controllerData['attr'] = []; + $this->controllerData['methods'] = []; + $this->controllerData['filterRule'] = ''; + $this->controllerData['className'] = $controllerFile['lastName']; + $this->controllerData['namespace'] = $controllerFile['namespace']; + $this->controllerData['tableComment'] = $tableComment; + $this->controllerData['modelName'] = $modelFile['lastName']; + $this->controllerData['modelNamespace'] = $modelFile['namespace']; + + // index.vue数据 + $this->indexVueData['enableDragSort'] = false; + $this->indexVueData['defaultItems'] = []; + $this->indexVueData['tableColumn'] = [ + [ + 'type' => 'selection', + 'align' => 'center', + 'operator' => 'false', + ], + ]; + $this->indexVueData['dblClickNotEditColumn'] = ['undefined']; + $this->indexVueData['optButtons'] = ['edit', 'delete']; + $this->indexVueData['defaultOrder'] = ''; + + // form.vue数据 + $this->formVueData['bigDialog'] = false; + $this->formVueData['formFields'] = []; + $this->formVueData['formValidatorRules'] = []; + $this->formVueData['imports'] = []; + + // 语言包数据 + $this->langTsData = [ + 'en' => [], + 'zh-cn' => [], + ]; + + // 简化的字段数据 + $fieldsMap = []; + + foreach ($fields as $key => $field) { + + $fieldsMap[$field['name']] = $field['designType']; + + // 分析字段 + Helper::analyseField($field); + + Helper::getDictData($this->langTsData['en'], $field, 'en'); + Helper::getDictData($this->langTsData['zh-cn'], $field, 'zh-cn'); + + // 快速搜索字段 + if (in_array($field['name'], $table['quickSearchField'])) { + $quickSearchFieldZhCnTitle[] = $this->langTsData['zh-cn'][$field['name']] ?? $field['name']; + } + + // 不允许双击编辑的字段 + if ($field['designType'] == 'switch') { + $this->indexVueData['dblClickNotEditColumn'][] = $field['name']; + } + + // 列字典数据 + $columnDict = $this->getColumnDict($field); + + // 表单项 + if (in_array($field['name'], $table['formFields'])) { + $this->formVueData['formFields'][] = $this->getFormField($field, $columnDict, $table['databaseConnection']); + } + + // 表格列 + if (in_array($field['name'], $table['columnFields'])) { + $this->indexVueData['tableColumn'][] = $this->getTableColumn($field, $columnDict); + } + + // 关联表数据解析 + if (in_array($field['designType'], ['remoteSelect', 'remoteSelects'])) { + $this->parseJoinData($field, $table); + } + + // 模型方法 + $this->parseModelMethods($field, $this->modelData); + + // 控制器/模型等文件的一些杂项属性解析 + $this->parseSundryData($field, $table); + + if (!in_array($field['name'], $table['formFields'])) { + $this->controllerData['attr']['preExcludeFields'][] = $field['name']; + } + } + + // 快速搜索提示 + $this->langTsData['en']['quick Search Fields'] = implode(',', $table['quickSearchField']); + $this->langTsData['zh-cn']['quick Search Fields'] = implode('、', $quickSearchFieldZhCnTitle); + $this->controllerData['attr']['quickSearchField'] = $table['quickSearchField']; + + // 开启字段排序 + $weighKey = array_search('weigh', $fieldsMap); + if ($weighKey !== false) { + $this->indexVueData['enableDragSort'] = true; + $this->modelData['afterInsert'] = Helper::assembleStub('mixins/model/afterInsert', [ + 'field' => $weighKey + ]); + } + + // 表格的操作列 + $this->indexVueData['tableColumn'][] = [ + 'label' => "t('Operate')", + 'align' => 'center', + 'width' => $this->indexVueData['enableDragSort'] ? 140 : 100, + 'render' => 'buttons', + 'buttons' => 'optButtons', + 'operator' => 'false', + ]; + if ($this->indexVueData['enableDragSort']) { + array_unshift($this->indexVueData['optButtons'], 'weigh-sort'); + } + + // 写入语言包代码 + Helper::writeWebLangFile($this->langTsData, $webLangDir); + + // 写入模型代码 + Helper::writeModelFile($tablePk, $fieldsMap, $this->modelData, $modelFile); + + // 写入控制器代码 + Helper::writeControllerFile($this->controllerData, $controllerFile); + + // 写入验证器代码 + $validateContent = Helper::assembleStub('mixins/validate/validate', [ + 'namespace' => $validateFile['namespace'], + 'className' => $validateFile['lastName'], + ]); + Helper::writeFile($validateFile['parseFile'], $validateContent); + + // 写入index.vue代码 + $this->indexVueData['tablePk'] = $tablePk; + $this->indexVueData['webTranslate'] = $this->webTranslate; + Helper::writeIndexFile($this->indexVueData, $webViewsDir, $controllerFile); + + // 写入form.vue代码 + Helper::writeFormFile($this->formVueData, $webViewsDir, $fields, $this->webTranslate); + + // 生成菜单 + Helper::createMenu($webViewsDir, $tableComment); + + Helper::recordCrudStatus([ + 'id' => $crudLogId, + 'status' => 'success', + ]); + } catch (Exception $e) { + Helper::recordCrudStatus([ + 'id' => $crudLogId ?? 0, + 'status' => 'error', + ]); + $this->error($e->getMessage()); + } catch (Throwable $e) { + Helper::recordCrudStatus([ + 'id' => $crudLogId ?? 0, + 'status' => 'error', + ]); + if (env('app_debug', false)) throw $e; + $this->error($e->getMessage()); + } + + $this->success('', [ + 'crudLog' => CrudLog::find($crudLogId), + ]); + } + + /** + * 从log开始 + * @throws Throwable + */ + public function logStart(): void + { + $id = $this->request->post('id'); + $type = $this->request->post('type', ''); + + if ($type == 'Cloud history') { + // 云端 历史记录 + $client = get_ba_client(); + $response = $client->request('GET', '/api/v6.Crud/info', [ + 'query' => [ + 'id' => $id, + 'server' => 1, + 'ba-user-token' => $this->request->post('token', ''), + ] + ]); + $body = $response->getBody(); + $statusCode = $response->getStatusCode(); + $content = $body->getContents(); + if ($content == '' || stripos($content, '系统发生错误') !== false || $statusCode != 200) { + $this->error(__('Failed to load cloud data')); + } + $json = json_decode($content, true); + if (json_last_error() != JSON_ERROR_NONE) { + $this->error(__('Failed to load cloud data')); + } + if (is_array($json)) { + if ($json['code'] != 1) { + $this->error($json['msg']); + } + + $info = $json['data']['info']; + } + } else { + // 本地记录 + $info = CrudLog::find($id)->toArray(); + } + + if (!isset($info) || !$info) { + $this->error(__('Record not found')); + } + + // 数据表是否有数据 + $connection = TableManager::getConnection($info['table']['databaseConnection'] ?? ''); + $tableName = TableManager::tableName($info['table']['name'], false, $connection); + $adapter = TableManager::phinxAdapter(true, $connection); + if ($adapter->hasTable($tableName)) { + $info['table']['empty'] = Db::connect($connection) + ->name($tableName) + ->limit(1) + ->select() + ->isEmpty(); + } else { + $info['table']['empty'] = true; + } + + AdminLog::instance()->setTitle(__('Log start')); + + $this->success('', [ + 'table' => $info['table'], + 'fields' => $info['fields'], + 'sync' => $info['sync'], + ]); + } + + /** + * 删除CRUD记录和生成的文件 + * @throws Throwable + */ + public function delete(): void + { + $id = $this->request->post('id'); + $info = CrudLog::find($id)->toArray(); + if (!$info) { + $this->error(__('Record not found')); + } + $webLangDir = Helper::parseWebDirNameData($info['table']['name'], 'lang', $info['table']['webViewsDir']); + $files = [ + $webLangDir['en'] . '.ts', + $webLangDir['zh-cn'] . '.ts', + $info['table']['webViewsDir'] . '/' . 'index.vue', + $info['table']['webViewsDir'] . '/' . 'popupForm.vue', + $info['table']['controllerFile'], + $info['table']['modelFile'], + $info['table']['validateFile'], + ]; + try { + foreach ($files as &$file) { + $file = Filesystem::fsFit(root_path() . $file); + if (file_exists($file)) { + unlink($file); + } + Filesystem::delEmptyDir(dirname($file)); + } + + // 删除菜单 + Menu::delete(Helper::getMenuName($webLangDir), true); + + Helper::recordCrudStatus([ + 'id' => $id, + 'status' => 'delete', + ]); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + $this->success(__('Deleted successfully')); + } + + /** + * 获取文件路径数据 + * @throws Throwable + */ + public function getFileData(): void + { + $table = $this->request->get('table'); + $commonModel = $this->request->get('commonModel/b'); + + if (!$table) { + $this->error(__('Parameter error')); + } + + try { + $modelFile = Helper::parseNameData($commonModel ? 'common' : 'admin', $table, 'model'); + $validateFile = Helper::parseNameData($commonModel ? 'common' : 'admin', $table, 'validate'); + $controllerFile = Helper::parseNameData('admin', $table, 'controller'); + $webViewsDir = Helper::parseWebDirNameData($table, 'views'); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + + // 模型和控制器文件和文件列表 + $adminModelFiles = Filesystem::getDirFiles(root_path() . 'app' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR); + $commonModelFiles = Filesystem::getDirFiles(root_path() . 'app' . DIRECTORY_SEPARATOR . 'common' . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR); + $adminControllerFiles = get_controller_list(); + + $modelFileList = []; + $controllerFiles = []; + foreach ($adminModelFiles as $item) { + $item = Filesystem::fsFit('app/admin/model/' . $item); + $modelFileList[$item] = $item; + } + foreach ($commonModelFiles as $item) { + $item = Filesystem::fsFit('app/common/model/' . $item); + $modelFileList[$item] = $item; + } + + $outExcludeController = [ + 'Addon.php', + 'Ajax.php', + 'Dashboard.php', + 'Index.php', + 'Module.php', + 'Terminal.php', + 'routine/AdminInfo.php', + 'routine/Config.php', + ]; + foreach ($adminControllerFiles as $item) { + if (in_array($item, $outExcludeController)) { + continue; + } + $item = Filesystem::fsFit('app/admin/controller/' . $item); + $controllerFiles[$item] = $item; + } + + $this->success('', [ + 'modelFile' => $modelFile['rootFileName'], + 'controllerFile' => $controllerFile['rootFileName'], + 'validateFile' => $validateFile['rootFileName'], + 'controllerFileList' => $controllerFiles, + 'modelFileList' => $modelFileList, + 'webViewsDir' => $webViewsDir['views'], + ]); + } + + /** + * 检查是否已有CRUD记录 + * @throws Throwable + */ + public function checkCrudLog(): void + { + $table = $this->request->get('table'); + $connection = $this->request->get('connection'); + $connection = $connection ?: config('database.default'); + + $crudLog = Db::name('crud_log') + ->where('table_name', $table) + ->where('connection', $connection) + ->order('create_time desc') + ->find(); + $this->success('', [ + 'id' => ($crudLog && $crudLog['status'] == 'success') ? $crudLog['id'] : 0, + ]); + } + + /** + * 解析字段数据 + * @throws Throwable + */ + public function parseFieldData(): void + { + AdminLog::instance()->setTitle(__('Parse field data')); + $type = $this->request->post('type'); + $table = $this->request->post('table'); + $connection = $this->request->post('connection'); + $connection = TableManager::getConnection($connection); + + $table = TableManager::tableName($table, true, $connection); + $connectionConfig = TableManager::getConnectionConfig($connection); + + if ($type == 'db') { + $sql = 'SELECT * FROM `information_schema`.`tables` ' + . 'WHERE TABLE_SCHEMA = ? AND table_name = ?'; + $tableInfo = Db::connect($connection)->query($sql, [$connectionConfig['database'], $table]); + if (!$tableInfo) { + $this->error(__('Record not found')); + } + + // 数据表是否有数据 + $adapter = TableManager::phinxAdapter(false, $connection); + if ($adapter->hasTable($table)) { + $empty = Db::connect($connection) + ->table($table) + ->limit(1) + ->select() + ->isEmpty(); + } else { + $empty = true; + } + + $this->success('', [ + 'columns' => Helper::parseTableColumns($table, false, $connection), + 'comment' => $tableInfo[0]['TABLE_COMMENT'] ?? '', + 'empty' => $empty, + ]); + } + } + + /** + * 生成前检查 + * @throws Throwable + */ + public function generateCheck(): void + { + $table = $this->request->post('table'); + $connection = $this->request->post('connection'); + $webViewsDir = $this->request->post('webViewsDir', ''); + $controllerFile = $this->request->post('controllerFile', ''); + + if (!$table) { + $this->error(__('Parameter error')); + } + + AdminLog::instance()->setTitle(__('Generate check')); + + try { + $webViewsDir = Helper::parseWebDirNameData($table, 'views', $webViewsDir); + $controllerFile = Helper::parseNameData('admin', $table, 'controller', $controllerFile)['rootFileName']; + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + + // 数据表是否存在 + $tableList = TableManager::getTableList($connection); + $tableExist = array_key_exists(TableManager::tableName($table, true, $connection), $tableList); + + // 控制器是否存在 + $controllerExist = file_exists(root_path() . $controllerFile); + + // 菜单规则是否存在 + $menuName = Helper::getMenuName($webViewsDir); + $menuExist = AdminRule::where('name', $menuName)->value('id'); + + if ($controllerExist || $tableExist || $menuExist) { + $this->error('', [ + 'menu' => $menuExist, + 'table' => $tableExist, + 'controller' => $controllerExist, + ], -1); + } + $this->success(); + } + + /** + * CRUD 设计记录上传成功标记 + * @throws Throwable + */ + public function uploadCompleted(): void + { + $syncIds = $this->request->post('syncIds/a', []); + $cancelSync = $this->request->post('cancelSync/b', false); + $crudLogModel = new CrudLog(); + + if ($cancelSync) { + $logData = $crudLogModel->where('id', 'in', array_keys($syncIds))->select(); + foreach ($logData as $logDatum) { + if ($logDatum->sync == $syncIds[$logDatum->id]) { + $logDatum->sync = 0; + $logDatum->save(); + } + } + $this->success(); + } + + $saveData = []; + foreach ($syncIds as $key => $syncId) { + $saveData[] = [ + 'id' => $key, + 'sync' => $syncId, + ]; + } + $crudLogModel->saveAll($saveData); + $this->success(); + } + + /** + * 关联表数据解析 + * @param $field + * @param $table + * @throws Throwable + */ + private function parseJoinData($field, $table): void + { + $dictEn = []; + $dictZhCn = []; + + if ($field['form']['relation-fields'] && $field['form']['remote-table']) { + $columns = Helper::parseTableColumns($field['form']['remote-table'], true, $table['databaseConnection']); + $relationFields = explode(',', $field['form']['relation-fields']); + $tableName = TableManager::tableName($field['form']['remote-table'], false, $table['databaseConnection']); + $rnPattern = '/(.*)(_ids|_id)$/'; + if (preg_match($rnPattern, $field['name'])) { + $relationName = parse_name(preg_replace($rnPattern, '$1', $field['name']), 1, false); + } else { + $relationName = parse_name($field['name'] . '_table', 1, false); + } + + // 建立关联模型代码文件 + if (!$field['form']['remote-model'] || !file_exists(root_path() . $field['form']['remote-model'])) { + $joinModelFile = Helper::parseNameData('admin', $tableName, 'model', $field['form']['remote-model']); + if (!file_exists(root_path() . $joinModelFile['rootFileName'])) { + $joinModelData['append'] = []; + $joinModelData['methods'] = []; + $joinModelData['fieldType'] = []; + $joinModelData['createTime'] = ''; + $joinModelData['updateTime'] = ''; + $joinModelData['beforeInsertMixins'] = []; + $joinModelData['beforeInsert'] = ''; + $joinModelData['afterInsert'] = ''; + $joinModelData['connection'] = $table['databaseConnection']; + $joinModelData['name'] = $tableName; + $joinModelData['className'] = $joinModelFile['lastName']; + $joinModelData['namespace'] = $joinModelFile['namespace']; + $joinTablePk = 'id'; + $joinFieldsMap = []; + foreach ($columns as $column) { + $joinFieldsMap[$column['name']] = $column['designType']; + $this->parseModelMethods($column, $joinModelData); + if ($column['primaryKey']) $joinTablePk = $column['name']; + } + $weighKey = array_search('weigh', $joinFieldsMap); + if ($weighKey !== false) { + $joinModelData['afterInsert'] = Helper::assembleStub('mixins/model/afterInsert', [ + 'field' => $joinFieldsMap[$weighKey] + ]); + } + Helper::writeModelFile($joinTablePk, $joinFieldsMap, $joinModelData, $joinModelFile); + } + $field['form']['remote-model'] = $joinModelFile['rootFileName']; + } + + if ($field['designType'] == 'remoteSelect') { + // 关联预载入方法 + $this->controllerData['attr']['withJoinTable'][$relationName] = $relationName; + + // 模型方法代码 + $relationData = [ + 'relationMethod' => $relationName, + 'relationMode' => 'belongsTo', + 'relationPrimaryKey' => $field['form']['remote-pk'] ?? 'id', + 'relationForeignKey' => $field['name'], + 'relationClassName' => str_replace(['.php', '/'], ['', '\\'], '\\' . $field['form']['remote-model']) . "::class", + ]; + $this->modelData['relationMethodList'][$relationName] = Helper::assembleStub('mixins/model/belongsTo', $relationData); + + // 查询时显示的字段 + if ($relationFields) { + $this->controllerData['relationVisibleFieldList'][$relationData['relationMethod']] = $relationFields; + } + } elseif ($field['designType'] == 'remoteSelects') { + $this->modelData['append'][] = $relationName; + $this->modelData['methods'][] = Helper::assembleStub('mixins/model/getters/remoteSelectLabels', [ + 'field' => parse_name($relationName, 1), + 'className' => str_replace(['.php', '/'], ['', '\\'], '\\' . $field['form']['remote-model']), + 'primaryKey' => $field['form']['remote-pk'] ?? 'id', + 'foreignKey' => $field['name'], + 'labelFieldName' => $field['form']['remote-field'] ?? 'name', + ]); + } + + foreach ($relationFields as $relationField) { + if (!array_key_exists($relationField, $columns)) continue; + $relationFieldPrefix = $relationName . '.'; + $relationFieldLangPrefix = strtolower($relationName) . '__'; + Helper::getDictData($dictEn, $columns[$relationField], 'en', $relationFieldLangPrefix); + Helper::getDictData($dictZhCn, $columns[$relationField], 'zh-cn', $relationFieldLangPrefix); + + // 不允许双击编辑的字段 + if ($columns[$relationField]['designType'] == 'switch') { + $this->indexVueData['dblClickNotEditColumn'][] = $field['name']; + } + + // 列字典数据 + $columnDict = $this->getColumnDict($columns[$relationField], $relationFieldLangPrefix); + + // 表格列 + $columns[$relationField]['designType'] = $field['designType']; + $columns[$relationField]['table']['render'] = 'tags'; + if ($field['designType'] == 'remoteSelects') { + $columns[$relationField]['table']['operator'] = 'false'; + $this->indexVueData['tableColumn'][] = $this->getTableColumn($columns[$relationField], $columnDict, $relationFieldPrefix, $relationFieldLangPrefix); + + // 额外生成一个公共搜索,渲染为远程下拉的列 + unset($columns[$relationField]['table']['render']); + $columns[$relationField]['table']['label'] = "t('" . $this->webTranslate . $relationFieldLangPrefix . $columns[$relationField]['name'] . "')"; + $columns[$relationField]['name'] = $field['name']; + $columns[$relationField]['table']['show'] = 'false'; + $columns[$relationField]['table']['operator'] = 'FIND_IN_SET'; + $columns[$relationField]['table']['comSearchRender'] = 'remoteSelect'; + + $columns[$relationField]['table']['remote'] = [ + 'pk' => $this->getRemoteSelectPk($field), + 'field' => $field['form']['remote-field'] ?? 'name', + 'remoteUrl' => $this->getRemoteSelectUrl($field), + 'multiple' => 'true', + ]; + $this->indexVueData['tableColumn'][] = $this->getTableColumn($columns[$relationField], $columnDict, '', $relationFieldLangPrefix); + } else { + $columns[$relationField]['table']['operator'] = 'LIKE'; + $this->indexVueData['tableColumn'][] = $this->getTableColumn($columns[$relationField], $columnDict, $relationFieldPrefix, $relationFieldLangPrefix); + } + } + } + $this->langTsData['en'] = array_merge($this->langTsData['en'], $dictEn); + $this->langTsData['zh-cn'] = array_merge($this->langTsData['zh-cn'], $dictZhCn); + } + + /** + * 解析模型方法(设置器、获取器等) + */ + private function parseModelMethods($field, &$modelData): void + { + // fieldType + if ($field['designType'] == 'array') { + $modelData['fieldType'][$field['name']] = 'json'; + } elseif (!in_array($field['name'], ['create_time', 'update_time', 'updatetime', 'createtime']) && $field['designType'] == 'datetime' && (in_array($field['type'], ['int', 'bigint']))) { + $modelData['fieldType'][$field['name']] = 'timestamp:Y-m-d H:i:s'; + } + + // beforeInsertMixins + if ($field['designType'] == 'spk') { + $modelData['beforeInsertMixins']['snowflake'] = Helper::assembleStub('mixins/model/mixins/beforeInsertWithSnowflake', []); + } + + // methods + $fieldName = parse_name($field['name'], 1); + if (in_array($field['designType'], $this->dtStringToArray)) { + $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/stringToArray', [ + 'field' => $fieldName + ]); + $modelData['methods'][] = Helper::assembleStub('mixins/model/setters/arrayToString', [ + 'field' => $fieldName + ]); + } elseif ($field['designType'] == 'array') { + $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/jsonDecode', [ + 'field' => $fieldName + ]); + } elseif ($field['designType'] == 'time') { + $modelData['methods'][] = Helper::assembleStub('mixins/model/setters/time', [ + 'field' => $fieldName + ]); + } elseif ($field['designType'] == 'editor') { + $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/htmlDecode', [ + 'field' => $fieldName + ]); + } elseif ($field['designType'] == 'spk') { + $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/string', [ + 'field' => $fieldName + ]); + } elseif (in_array($field['type'], ['float', 'decimal', 'double'])) { + $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/float', [ + 'field' => $fieldName + ]); + } + + if ($field['designType'] == 'city') { + $modelData['append'][] = $field['name'] . '_text'; + $modelData['methods'][] = Helper::assembleStub('mixins/model/getters/cityNames', [ + 'field' => $fieldName . 'Text', + 'originalFieldName' => $field['name'], + ]); + } + } + + /** + * 控制器/模型等文件的一些杂项属性解析 + */ + private function parseSundryData($field, $table): void + { + if ($field['designType'] == 'editor') { + $this->formVueData['bigDialog'] = true; // 加宽 dialog + $this->controllerData['filterRule'] = "\n" . Helper::tab(2) . '$this->request->filter(\'clean_xss\');'; // 修改变量过滤规则 + } + + // 默认排序字段 + if ($table['defaultSortField'] && $table['defaultSortType']) { + $defaultSortField = "{$table['defaultSortField']},{$table['defaultSortType']}"; + if ($defaultSortField == 'id,desc') { + $this->controllerData['attr']['defaultSortField'] = ''; + } else { + $this->controllerData['attr']['defaultSortField'] = $defaultSortField; + $this->indexVueData['defaultOrder'] = Helper::buildDefaultOrder($table['defaultSortField'], $table['defaultSortType']); + } + } + + // 自定义了权重字段名称 + if ($field['originalDesignType'] == 'weigh' && $field['name'] != 'weigh') { + $this->controllerData['attr']['weighField'] = $field['name']; + } + } + + /** + * 组装前台表单的数据 + * @throws Throwable + */ + private function getFormField($field, $columnDict, ?string $dbConnection = null): array + { + // 表单项属性 + $formField = [ + ':label' => 't(\'' . $this->webTranslate . $field['name'] . '\')', + 'type' => $field['designType'], + 'v-model' => 'baTable.form.items!.' . $field['name'], + 'prop' => $field['name'], + ]; + + // 不同输入框的属性处理 + if ($columnDict || in_array($field['designType'], ['radio', 'checkbox', 'select', 'selects'])) { + $formField[':input-attr']['content'] = $columnDict; + } elseif ($field['designType'] == 'textarea') { + $formField[':input-attr']['rows'] = (int)($field['form']['rows'] ?? 3); + $formField['@keyup.enter.stop'] = ''; + $formField['@keyup.ctrl.enter'] = 'baTable.onSubmit(formRef)'; + } elseif ($field['designType'] == 'remoteSelect' || $field['designType'] == 'remoteSelects') { + $formField[':input-attr']['pk'] = $this->getRemoteSelectPk($field); + $formField[':input-attr']['field'] = $field['form']['remote-field'] ?? 'name'; + $formField[':input-attr']['remoteUrl'] = $this->getRemoteSelectUrl($field); + } elseif ($field['designType'] == 'number') { + $formField[':input-attr']['step'] = (int)($field['form']['step'] ?? 1); + } elseif ($field['designType'] == 'icon') { + $formField[':input-attr']['placement'] = 'top'; + } elseif ($field['designType'] == 'editor') { + $formField['@keyup.enter.stop'] = ''; + $formField['@keyup.ctrl.enter'] = 'baTable.onSubmit(formRef)'; + } + + // placeholder + if (!in_array($field['designType'], ['image', 'images', 'file', 'files', 'switch'])) { + if (in_array($field['designType'], ['radio', 'checkbox', 'datetime', 'year', 'date', 'time', 'select', 'selects', 'remoteSelect', 'remoteSelects', 'city', 'icon'])) { + $formField[':placeholder'] = "t('Please select field', { field: t('" . $this->webTranslate . $field['name'] . "') })"; + } else { + $formField[':placeholder'] = "t('Please input field', { field: t('" . $this->webTranslate . $field['name'] . "') })"; + } + } + + // 默认值 + if ($field['defaultType'] == 'INPUT') { + $this->indexVueData['defaultItems'][$field['name']] = $field['default']; + } + + // 部分生成类型的默认值需要额外处理 + if ($field['designType'] == 'editor') { + $this->indexVueData['defaultItems'][$field['name']] = ($field['defaultType'] == 'INPUT' && $field['default']) ? $field['default'] : ''; + } elseif ($field['designType'] == 'array') { + $this->indexVueData['defaultItems'][$field['name']] = "[]"; + } elseif ($field['defaultType'] == 'INPUT' && in_array($field['designType'], $this->dtStringToArray) && str_contains($field['default'], ',')) { + $this->indexVueData['defaultItems'][$field['name']] = Helper::buildSimpleArray(explode(',', $field['default'])); + } elseif ($field['defaultType'] == 'INPUT' && in_array($field['designType'], ['number', 'float'])) { + $this->indexVueData['defaultItems'][$field['name']] = (float)$field['default']; + } + + // 无意义的默认值 + if (isset($field['default']) && in_array($field['designType'], ['switch', 'number', 'float', 'remoteSelect']) && $field['default'] == 0) { + unset($this->indexVueData['defaultItems'][$field['name']]); + } + + return $formField; + } + + private function getRemoteSelectPk($field): string + { + $pk = $field['form']['remote-pk'] ?? 'id'; + if (!str_contains($pk, '.')) { + if ($field['form']['remote-source-config-type'] == 'crud' && $field['form']['remote-model']) { + $alias = parse_name(basename(str_replace('\\', '/', $field['form']['remote-model']), '.php')); + } else { + $alias = $field['form']['remote-primary-table-alias'] ?? ''; + } + } + return !empty($alias) ? "$alias.$pk" : $pk; + } + + private function getRemoteSelectUrl($field): string + { + if ($field['form']['remote-source-config-type'] == 'crud' && $field['form']['remote-controller']) { + $pathArr = []; + $controller = explode(DIRECTORY_SEPARATOR, $field['form']['remote-controller']); + $controller = str_replace('.php', '', $controller); + $redundantDir = [ + 'app' => 0, + 'admin' => 1, + 'controller' => 2, + ]; + foreach ($controller as $key => $item) { + if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) { + $pathArr[] = $item; + } + } + $url = count($pathArr) > 1 ? implode('.', $pathArr) : $pathArr[0]; + return '/admin/' . $url . '/index'; + } + return $field['form']['remote-url']; + } + + private function getTableColumn($field, $columnDict, $fieldNamePrefix = '', $translationPrefix = ''): array + { + $column = [ + 'label' => "t('" . $this->webTranslate . $translationPrefix . $field['name'] . "')", + 'prop' => $fieldNamePrefix . $field['name'] . ($field['designType'] == 'city' ? '_text' : ''), + 'align' => 'center', + ]; + + // 模糊搜索增加一个placeholder + if (isset($field['table']['operator']) && $field['table']['operator'] == 'LIKE') { + $column['operatorPlaceholder'] = "t('Fuzzy query')"; + } + + // 合并前端预设的字段表格属性 + if (isset($field['table']) && $field['table']) { + $column = array_merge($column, $field['table']); + } + + // 需要值替换的渲染类型 + $columnReplaceValue = ['tag', 'tags', 'switch']; + if (!in_array($field['designType'], ['remoteSelect', 'remoteSelects']) && ($columnDict || (isset($field['table']['render']) && in_array($field['table']['render'], $columnReplaceValue)))) { + $column['replaceValue'] = $columnDict; + } + + if (isset($column['render']) && $column['render'] == 'none') { + unset($column['render']); + } + return $column; + } + + private function getColumnDict($column, $translationPrefix = ''): array + { + $dict = []; + // 确保字典中无翻译也可以识别到该值 + if (in_array($column['type'], ['enum', 'set'])) { + $dataType = str_replace(' ', '', $column['dataType']); + $columnData = substr($dataType, stripos($dataType, '(') + 1, -1); + $columnData = explode(',', str_replace(["'", '"'], '', $columnData)); + foreach ($columnData as $columnDatum) { + $dict[$columnDatum] = $column['name'] . ' ' . $columnDatum; + } + } + $dictData = []; + Helper::getDictData($dictData, $column, 'zh-cn', $translationPrefix); + if ($dictData) { + unset($dictData[$translationPrefix . $column['name']]); + foreach ($dictData as $key => $item) { + $keyName = str_replace($translationPrefix . $column['name'] . ' ', '', $key); + $dict[$keyName] = "t('" . $this->webTranslate . $key . "')"; + } + } + return $dict; + } +} \ No newline at end of file diff --git a/app/admin/controller/crud/Log.php b/app/admin/controller/crud/Log.php new file mode 100644 index 0000000..59624fa --- /dev/null +++ b/app/admin/controller/crud/Log.php @@ -0,0 +1,37 @@ +model = new CrudLog(); + + if (!$this->auth->check('crud/crud/index')) { + $this->error(__('You have no permission'), [], 401); + } + } + +} \ No newline at end of file diff --git a/app/admin/controller/routine/AdminInfo.php b/app/admin/controller/routine/AdminInfo.php new file mode 100644 index 0000000..945e5e3 --- /dev/null +++ b/app/admin/controller/routine/AdminInfo.php @@ -0,0 +1,90 @@ +auth->setAllowFields($this->authAllowFields); + $this->model = $this->auth->getAdmin(); + } + + public function index(): void + { + $info = $this->auth->getInfo(); + $this->success('', [ + 'info' => $info + ]); + } + + public function edit(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + if (!empty($data['avatar'])) { + $row->avatar = $data['avatar']; + if ($row->save()) { + $this->success(__('Avatar modified successfully!')); + } + } + + // 数据验证 + if ($this->modelValidate) { + try { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + $validate = new $validate(); + $validate->scene('info')->check($data); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + } + + if (!empty($data['password'])) { + $this->model->resetPassword($this->auth->id, $data['password']); + } + + $data = $this->excludeFields($data); + $result = false; + $this->model->startTrans(); + try { + $result = $row->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + } + } +} \ No newline at end of file diff --git a/app/admin/controller/routine/Attachment.php b/app/admin/controller/routine/Attachment.php new file mode 100644 index 0000000..776a115 --- /dev/null +++ b/app/admin/controller/routine/Attachment.php @@ -0,0 +1,59 @@ +model = new AttachmentModel(); + } + + /** + * 删除 + * @throws Throwable + */ + public function del(): void + { + $where = []; + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds) { + $where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds]; + } + + $ids = $this->request->param('ids/a', []); + $where[] = [$this->model->getPk(), 'in', $ids]; + $data = $this->model->where($where)->select(); + + $count = 0; + try { + foreach ($data as $v) { + $count += $v->delete(); + } + } catch (Throwable $e) { + $this->error(__('%d records and files have been deleted', [$count]) . $e->getMessage()); + } + if ($count) { + $this->success(__('%d records and files have been deleted', [$count])); + } else { + $this->error(__('No rows were deleted')); + } + } +} \ No newline at end of file diff --git a/app/admin/controller/routine/Config.php b/app/admin/controller/routine/Config.php new file mode 100644 index 0000000..64db402 --- /dev/null +++ b/app/admin/controller/routine/Config.php @@ -0,0 +1,246 @@ + 'config/app.php', + 'webAdminBase' => 'web/src/router/static/adminBase.ts', + 'backendEntranceStub' => 'app/admin/library/stubs/backendEntrance.stub', + ]; + + public function initialize(): void + { + parent::initialize(); + $this->model = new ConfigModel(); + } + + public function index(): void + { + $configGroup = get_sys_config('config_group'); + $config = $this->model->order('weigh desc')->select()->toArray(); + + $list = []; + $newConfigGroup = []; + foreach ($configGroup as $item) { + $list[$item['key']]['name'] = $item['key']; + $list[$item['key']]['title'] = __($item['value']); + $newConfigGroup[$item['key']] = $list[$item['key']]['title']; + } + foreach ($config as $item) { + if (array_key_exists($item['group'], $newConfigGroup)) { + $item['title'] = __($item['title']); + $list[$item['group']]['list'][] = $item; + } + } + + $this->success('', [ + 'list' => $list, + 'remark' => get_route_remark(), + 'configGroup' => $newConfigGroup ?? [], + 'quickEntrance' => get_sys_config('config_quick_entrance'), + ]); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $all = $this->model->select(); + foreach ($all as $item) { + if ($item['type'] == 'editor') { + $this->request->filter('clean_xss'); + break; + } + } + if ($this->request->isPost()) { + $this->modelValidate = false; + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + + $configValue = []; + foreach ($all as $item) { + if (array_key_exists($item->name, $data)) { + $configValue[] = [ + 'id' => $item->id, + 'type' => $item->getData('type'), + 'value' => $data[$item->name] + ]; + + // 自定义后台入口 + if ($item->name == 'backend_entrance') { + $backendEntrance = get_sys_config('backend_entrance'); + if ($backendEntrance == $data[$item->name]) continue; + + if (!preg_match("/^\/[a-zA-Z0-9]+$/", $data[$item->name])) { + $this->error(__('Backend entrance rule')); + } + + // 修改 adminBaseRoutePath + $adminBaseFilePath = Filesystem::fsFit(root_path() . $this->filePath['webAdminBase']); + $adminBaseContent = @file_get_contents($adminBaseFilePath); + if (!$adminBaseContent) $this->error(__('Configuration write failed: %s', [$this->filePath['webAdminBase']])); + + $adminBaseContent = str_replace("export const adminBaseRoutePath = '$backendEntrance'", "export const adminBaseRoutePath = '{$data[$item->name]}'", $adminBaseContent); + $result = @file_put_contents($adminBaseFilePath, $adminBaseContent); + if (!$result) $this->error(__('Configuration write failed: %s', [$this->filePath['webAdminBase']])); + + // 去除后台入口开头的斜杠 + $oldBackendEntrance = ltrim($backendEntrance, '/'); + $newBackendEntrance = ltrim($data[$item->name], '/'); + + // 设置应用别名映射 + $appMap = config('app.app_map'); + $adminMapKey = array_search('admin', $appMap); + if ($adminMapKey !== false) { + unset($appMap[$adminMapKey]); + } + if ($newBackendEntrance != 'admin') { + $appMap[$newBackendEntrance] = 'admin'; + } + $appConfigFilePath = Filesystem::fsFit(root_path() . $this->filePath['appConfig']); + $appConfigContent = @file_get_contents($appConfigFilePath); + if (!$appConfigContent) $this->error(__('Configuration write failed: %s', [$this->filePath['appConfig']])); + + $appMapStr = ''; + foreach ($appMap as $newAppName => $oldAppName) { + $appMapStr .= "'$newAppName' => '$oldAppName', "; + } + $appMapStr = rtrim($appMapStr, ', '); + $appMapStr = "[$appMapStr]"; + + $appConfigContent = preg_replace("/'app_map'(\s+)=>(\s+)(.*)\/\/ 域名/s", "'app_map'\$1=>\$2$appMapStr,\n // 域名", $appConfigContent); + $result = @file_put_contents($appConfigFilePath, $appConfigContent); + if (!$result) $this->error(__('Configuration write failed: %s', [$this->filePath['appConfig']])); + + // 建立API入口文件 + $oldBackendEntranceFile = Filesystem::fsFit(public_path() . $oldBackendEntrance . '.php'); + $newBackendEntranceFile = Filesystem::fsFit(public_path() . $newBackendEntrance . '.php'); + if (file_exists($oldBackendEntranceFile)) @unlink($oldBackendEntranceFile); + + if ($newBackendEntrance != 'admin') { + $backendEntranceStub = @file_get_contents(Filesystem::fsFit(root_path() . $this->filePath['backendEntranceStub'])); + if (!$backendEntranceStub) $this->error(__('Configuration write failed: %s', [$this->filePath['backendEntranceStub']])); + + $result = @file_put_contents($newBackendEntranceFile, $backendEntranceStub); + if (!$result) $this->error(__('Configuration write failed: %s', [$newBackendEntranceFile])); + } + } + } + } + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('edit'); + $validate->check($data); + } + } + $result = $this->model->saveAll($configValue); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('The current page configuration item was updated successfully')); + } else { + $this->error(__('No rows updated')); + } + + } + } + + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('add'); + $validate->check($data); + } + } + $result = $this->model->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + $this->error(__('Parameter error')); + } + + /** + * 发送邮件测试 + * @throws Throwable + */ + public function sendTestMail(): void + { + $data = $this->request->post(); + $mail = new Email(); + try { + $mail->Host = $data['smtp_server']; + $mail->SMTPAuth = true; + $mail->Username = $data['smtp_user']; + $mail->Password = $data['smtp_pass']; + $mail->SMTPSecure = $data['smtp_verification'] == 'SSL' ? PHPMailer::ENCRYPTION_SMTPS : PHPMailer::ENCRYPTION_STARTTLS; + $mail->Port = $data['smtp_port']; + + $mail->setFrom($data['smtp_sender_mail'], $data['smtp_user']); + + $mail->isSMTP(); + $mail->addAddress($data['testMail']); + $mail->isHTML(); + $mail->setSubject(__('This is a test email') . '-' . get_sys_config('site_name')); + $mail->Body = __('Congratulations, receiving this email means that your email service has been configured correctly'); + $mail->send(); + } catch (PHPMailerException) { + $this->error($mail->ErrorInfo); + } + $this->success(__('Test mail sent successfully~')); + } +} \ No newline at end of file diff --git a/app/admin/controller/security/DataRecycle.php b/app/admin/controller/security/DataRecycle.php new file mode 100644 index 0000000..33201c5 --- /dev/null +++ b/app/admin/controller/security/DataRecycle.php @@ -0,0 +1,150 @@ +model = new DataRecycleModel(); + } + + /** + * 添加 + * @throws Throwable + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? ''); + $data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as'])); + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('add'); + $validate->check($data); + } + } + $result = $this->model->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + // 放在add方法内,就不需要额外添加权限节点了 + $this->success('', [ + 'controllers' => $this->getControllerList(), + ]); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? ''); + $data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as'])); + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('edit'); + $validate->check($data); + } + } + $result = $row->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + } + + $this->success('', [ + 'row' => $row + ]); + } + + protected function getControllerList(): array + { + $outExcludeController = [ + 'Addon.php', + 'Ajax.php', + 'Module.php', + 'Terminal.php', + 'Dashboard.php', + 'Index.php', + 'routine/AdminInfo.php', + 'user/MoneyLog.php', + 'user/ScoreLog.php', + ]; + $outControllers = []; + $controllers = get_controller_list(); + foreach ($controllers as $key => $controller) { + if (!in_array($controller, $outExcludeController)) { + $outControllers[$key] = $controller; + } + } + return $outControllers; + } +} \ No newline at end of file diff --git a/app/admin/controller/security/DataRecycleLog.php b/app/admin/controller/security/DataRecycleLog.php new file mode 100644 index 0000000..68907d8 --- /dev/null +++ b/app/admin/controller/security/DataRecycleLog.php @@ -0,0 +1,106 @@ +model = new DataRecycleLogModel(); + } + + /** + * 还原 + * @throws Throwable + */ + public function restore(): void + { + $ids = $this->request->param('ids/a', []); + $data = $this->model->where('id', 'in', $ids)->select(); + if (!$data) { + $this->error(__('Record not found')); + } + + $count = 0; + $this->model->startTrans(); + try { + foreach ($data as $row) { + $recycleData = json_decode($row['data'], true); + if (is_array($recycleData) && Db::connect(TableManager::getConnection($row->connection))->name($row->data_table)->insert($recycleData)) { + $row->delete(); + $count++; + } + } + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + + if ($count) { + $this->success(); + } else { + $this->error(__('No rows were restore')); + } + } + + /** + * 详情 + * @throws Throwable + */ + public function info(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model + ->withJoin($this->withJoinTable, $this->withJoinType) + ->where('data_recycle_log.id', $id) + ->find(); + if (!$row) { + $this->error(__('Record not found')); + } + $data = $this->jsonToArray($row['data']); + if (is_array($data)) { + foreach ($data as $key => $item) { + $data[$key] = $this->jsonToArray($item); + } + } + $row['data'] = $data; + + $this->success('', [ + 'row' => $row + ]); + } + + protected function jsonToArray($value = '') + { + if (!is_string($value)) { + return $value; + } + $data = json_decode($value, true); + if (($data && is_object($data)) || (is_array($data) && !empty($data))) { + return $data; + } + return $value; + } +} \ No newline at end of file diff --git a/app/admin/controller/security/SensitiveData.php b/app/admin/controller/security/SensitiveData.php new file mode 100644 index 0000000..555889e --- /dev/null +++ b/app/admin/controller/security/SensitiveData.php @@ -0,0 +1,204 @@ +model = new SensitiveDataModel(); + } + + /** + * 查看 + * @throws Throwable + */ + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + list($where, $alias, $limit, $order) = $this->queryBuilder(); + $res = $this->model + ->withJoin($this->withJoinTable, $this->withJoinType) + ->alias($alias) + ->where($where) + ->order($order) + ->paginate($limit); + + foreach ($res->items() as $item) { + if ($item->data_fields) { + $fields = []; + foreach ($item->data_fields as $key => $field) { + $fields[] = $field ?: $key; + } + $item->data_fields = $fields; + } + } + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + 'remark' => get_route_remark(), + ]); + } + + /** + * 添加重写 + * @throws Throwable + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? ''); + $data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as'])); + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('add'); + $validate->check($data); + } + } + + if (is_array($data['fields'])) { + $data['data_fields'] = []; + foreach ($data['fields'] as $field) { + $data['data_fields'][$field['name']] = $field['value']; + } + } + + $result = $this->model->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + // 放在add方法内,就不需要额外添加权限节点了 + $this->success('', [ + 'controllers' => $this->getControllerList(), + ]); + } + + /** + * 编辑重写 + * @throws Throwable + */ + public function edit(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $data['controller_as'] = str_ireplace('.php', '', $data['controller'] ?? ''); + $data['controller_as'] = strtolower(str_ireplace(['\\', '.'], '/', $data['controller_as'])); + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('edit'); + $validate->check($data); + } + } + + if (is_array($data['fields'])) { + $data['data_fields'] = []; + foreach ($data['fields'] as $field) { + $data['data_fields'][$field['name']] = $field['value']; + } + } + + $result = $row->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + } + + $this->success('', [ + 'row' => $row, + 'controllers' => $this->getControllerList(), + ]); + } + + protected function getControllerList(): array + { + $outExcludeController = [ + 'Addon.php', + 'Ajax.php', + 'Dashboard.php', + 'Index.php', + 'Module.php', + 'Terminal.php', + 'auth/AdminLog.php', + 'routine/AdminInfo.php', + 'routine/Config.php', + 'user/MoneyLog.php', + 'user/ScoreLog.php', + ]; + $outControllers = []; + $controllers = get_controller_list(); + foreach ($controllers as $key => $controller) { + if (!in_array($controller, $outExcludeController)) { + $outControllers[$key] = $controller; + } + } + return $outControllers; + } +} \ No newline at end of file diff --git a/app/admin/controller/security/SensitiveDataLog.php b/app/admin/controller/security/SensitiveDataLog.php new file mode 100644 index 0000000..dc41595 --- /dev/null +++ b/app/admin/controller/security/SensitiveDataLog.php @@ -0,0 +1,117 @@ +model = new SensitiveDataLogModel(); + } + + /** + * 查看 + * @throws Throwable + */ + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + list($where, $alias, $limit, $order) = $this->queryBuilder(); + $res = $this->model + ->withJoin($this->withJoinTable, $this->withJoinType) + ->alias($alias) + ->where($where) + ->order($order) + ->paginate($limit); + + foreach ($res->items() as $item) { + $item->id_value = $item['primary_key'] . '=' . $item->id_value; + } + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + 'remark' => get_route_remark(), + ]); + } + + /** + * 详情 + * @throws Throwable + */ + public function info(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model + ->withJoin($this->withJoinTable, $this->withJoinType) + ->where('sensitive_data_log.id', $id) + ->find(); + if (!$row) { + $this->error(__('Record not found')); + } + + $this->success('', [ + 'row' => $row + ]); + } + + /** + * 回滚 + * @throws Throwable + */ + public function rollback(): void + { + $ids = $this->request->param('ids/a', []); + $data = $this->model->where('id', 'in', $ids)->select(); + if (!$data) { + $this->error(__('Record not found')); + } + + $count = 0; + $this->model->startTrans(); + try { + foreach ($data as $row) { + if (Db::connect(TableManager::getConnection($row->connection))->name($row->data_table)->where($row->primary_key, $row->id_value)->update([ + $row->data_field => $row->before + ])) { + $row->delete(); + $count++; + } + } + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + + if ($count) { + $this->success(); + } else { + $this->error(__('No rows were rollback')); + } + } +} \ No newline at end of file diff --git a/app/admin/controller/sys/Config.php b/app/admin/controller/sys/Config.php new file mode 100644 index 0000000..f582cf9 --- /dev/null +++ b/app/admin/controller/sys/Config.php @@ -0,0 +1,28 @@ +request->get(); + $db = Db::connect(get_slave_connect_name()); + $count = $db->name('sys_config')->count(); + $config_list= $db->name('sys_config')->where(['status' => 1])->limit(page($get['page'], $get['limit']), $get['limit'])->select()->toArray(); + $this->n_success(['page' => [ + 'totalCount' => $count, + 'pageSize' => $get['limit'], + 'totalPage' => ceil($count / $get['limit']), + 'currPage' => $get['page'], + 'list' => $config_list, + ]]); + } + +} \ No newline at end of file diff --git a/app/admin/controller/sys/Log.php b/app/admin/controller/sys/Log.php new file mode 100644 index 0000000..731a3bc --- /dev/null +++ b/app/admin/controller/sys/Log.php @@ -0,0 +1,24 @@ +request->param(); + $this->result('成功', DatabaseRoute::paginateDb('sys_log', function ($query) { + return $query->order('id', 'desc'); + }, $params['page'], $params['limit']), 0, null, [], [], 'page'); + } + + + +} \ No newline at end of file diff --git a/app/admin/controller/sys/Menu.php b/app/admin/controller/sys/Menu.php new file mode 100644 index 0000000..c5ba8b1 --- /dev/null +++ b/app/admin/controller/sys/Menu.php @@ -0,0 +1,128 @@ +auth->getAdmin(); + $return = SysMenu::getMenuList($this->auth->getUserMenus($admin['user_id'])); + + $this->n_success([ + 'menuList' => convertToCamelCase($return['menuList']), + 'permissions' => $return['permissions'], + ]); + } + + public function list() + { + $menuList = Db::name('sys_menu')->order('order_num', 'asc')->select()->toArray(); + + // 收集所有 parent_id + $parentIds = array_column($menuList, 'parent_id'); + $parentIds = array_unique(array_filter($parentIds)); + + // 批量查询父级菜单 + $parentMap = []; + if (!empty($parentIds)) { + $parents = Db::name('sys_menu')->whereIn('menu_id', $parentIds)->column('name', 'menu_id'); + $parentMap = $parents; + } + + // 设置 parentName + foreach ($menuList as &$menu) { + $menu['menu_id'] = (string) $menu['menu_id']; + $menu['parent_id'] = (string) $menu['parent_id']; + + $menu['parent_name'] = $parentMap[$menu['parent_id']] ?? ''; + } + unset($menu); + + return $this->ApiDataReturn(convertToCamelCase($menuList)); + } + + public function info() + { + $data = Db::name('sys_menu')->where([ + 'menu_id' => $this->request->param('id') + ])->find(); + $data['menu_id'] = (string) $data['menu_id']; + $data['parent_id'] = (string) $data['parent_id']; + $data = convertToCamelCase($data); + $this->n_success(['menu' => $data]); + } + + + + public function save() + { + $menu = $this->request->post(); + SysMenu::verifyForm($menu); + $menu = convertKeysCamelToSnakeRecursive($menu); + Db::connect(get_master_connect_name())->name('sys_menu')->insert($menu); + $this->success(); + } + + + + public function selectInfo() + { + $list = Db::name('sys_menu')->where([ + 'type' => ['!=', 2] + ])->order('order_num')->select()->toArray(); + + $list[] = [ + 'menu_id' => 0, + 'name' => '一级菜单', + 'parent_id' => -1, + 'open' => true, + ]; + $list = convertToCamelCase($list); + $this->n_success(['menuList' => $list]); +// $this->successWithData([ +// 'menuList' => $list +// ]); + } + + public function update() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + Db::name('sys_menu')->where([ + 'menu_id' => $params['menu_id'] + ])->update($params); + $this->success(); + } + + public function delete() + { + $post = $this->request->post(); + if(empty($post['menuId'])) { + $this->error('参数错误'); + } + $menuId = $post['menuId']; + if($menuId <= 31) { + $this->error('系统菜单,不能删除'); + } + + // 判断是否有子菜单或按钮 + $menuList = Db::connect(get_slave_connect_name())->name('sys_menu')->where(['parent_id' => $menuId])->order('order_num', 'asc')->select()->toArray(); + if($menuList) { + $this->error('请先删除子菜单或按钮'); + } + + if(Db::name('sys_menu')->where(['menu_id' => $menuId])->delete()) { + $this->success('删除成功'); + } + $this->error('操作失败'); + } + +} \ No newline at end of file diff --git a/app/admin/controller/sys/Oss.php b/app/admin/controller/sys/Oss.php new file mode 100644 index 0000000..d02b36e --- /dev/null +++ b/app/admin/controller/sys/Oss.php @@ -0,0 +1,43 @@ +request->param(); + $data = DatabaseRoute::paginateDb('sys_oss', function ($query) { + return $query->order('id', 'desc'); + }, $params['page'], $params['limit']); + $this->n_success(['page' => $data]); + } + + public function config() + { + $data = Db::connect(get_slave_connect_name())->name('sys_config')->where(['param_key' => 'CLOUD_STORAGE_CONFIG_KEY'])->column('param_value')[0]; + $data = json_decode($data, true); + $this->n_success(['config' => $data]); + + } + + + public function saveConfig() + { + $post = $this->request->post(); + if(is_array($post)) { + $post = json_encode($post); + }else { + $this->error('参数错误'); + } + Db::name('sys_config')->where(['param_key' => 'CLOUD_STORAGE_CONFIG_KEY'])->update(['param_value' => $post]); + $this->success(); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/sys/Role.php b/app/admin/controller/sys/Role.php new file mode 100644 index 0000000..e440a40 --- /dev/null +++ b/app/admin/controller/sys/Role.php @@ -0,0 +1,110 @@ +request->param(); + $data = DatabaseRoute::paginateDb('sys_role', function ($query) { + return $query; + }, $prams['page'], $prams['limit']); + $this->n_success(['page' => $data]); + } + + private function saveRoleMenu($roleId, $params) + { + if (!empty($params['menu_id_list'])) { + Db::name('sys_role_menu')->where([ + 'role_id' => $roleId + ])->delete(); + + foreach ($params['menu_id_list'] as $menuId) { + Db::name('sys_role_menu')->insert([ + 'role_id' => $roleId, + 'menu_id' => $menuId + ]); + } + } + + } + + public function save() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + + $id = Db::name('sys_role')->insert([ + 'role_name' => $params['role_name'], + 'create_time' => getNormalDate(), + 'remark' => $params['remark'] ?? '' + ]); + + $this->saveRoleMenu($id, $params); + $this->success(); + } + + public function update() + { + $params = $this->request->post(); + $params = convertKeysCamelToSnakeRecursive($params); + Db::name('sys_role')->where([ + 'role_id' => $params['role_id'] + ])->update([ + 'role_name' => $params['role_name'], + 'remark' => $params['remark'] ?? '' + ]); + + $this->saveRoleMenu($params['role_id'], $params); + } + + public function delete() + { + $params = $this->request->post(); + Db::name('sys_role')->where([ + ['role_id', 'in', $params] + ])->delete(); + $this->success(); + } + + public function selects() + { + + $this->n_success(['list' => convertToCamelCase(array_map(function ($item) { + $item['role_id'] = (string)$item['role_id']; + return $item; + }, Db::connect(get_slave_connect_name())->name('sys_role')->select()->toArray()))]); + } + + + // 角色信息 + public function info() + { + $get = $this->request->get(); + if(empty($get['roleId'])) { + $this->error('roleId 不能为空'); + } + $db = Db::connect(get_slave_connect_name()); + + $role = $db->name('sys_role')->where(['role_id' => $get['roleId']])->find(); + if (!$role) { + $this->error('角色不存在'); + } + $role = apiconvertToCamelCase($role); + $menuIdList = $db->name('sys_role_menu')->where(['role_id' => $get['roleId']])->column('menu_id'); + + $role['menuIdList'] = $menuIdList; + $this->n_success(['role' => $role]); + + } + + +} \ No newline at end of file diff --git a/app/admin/controller/sys/User.php b/app/admin/controller/sys/User.php new file mode 100644 index 0000000..4976cbf --- /dev/null +++ b/app/admin/controller/sys/User.php @@ -0,0 +1,232 @@ +request->get(); + if (empty($get['userId'])) { + $info = $this->auth->getAdmin(); + } else { + $info = DatabaseRoute::getDb('sys_user', $get['userId'])->find(); + } + $info = convertToCamelCase($info); + $info['password'] = null; +// $info['salt'] = null; + $roleIdList = array_column(Db::name('sys_user_role')->where([ + 'user_id' => $info['userId'] + ])->select()->toArray(), 'role_id'); + foreach ($roleIdList as $k => &$v) { + $v = (string) $v; + } + $info['roleIdList'] = $roleIdList; + $this->n_success(['user' => $info]); + } + + public function list() + { + $params = $this->request->param(); + $data = DatabaseRoute::paginateAllDb('sys_user', function ($query) use ($params) { + if (!empty($params['username'])) { + $query->whereLike('username', '%' . $params['username'] . '%'); + } + + if (!empty($params['isChannel'])) { + $query->where('is_channel', $params['isChannel']); + } + return $query; + }, $params['page'], $params['limit']); + $data['list'] = convertToCamelCase($data['list']); + $this->n_success(['page' => $data]); + + } + + public function detail() + { + $id = $this->request->param('id'); + $sysInfo = DatabaseRoute::getDb('sys_user', $id)->find(); + if (!$sysInfo) { + $this->error('用户不存在'); + } + $userRoleList = Db::name('sys_user_role')->where([ + 'user_id' => $sysInfo['user_id'] + ])->select()->toArray(); + $sysInfo = convertToCamelCase($sysInfo); + $sysInfo['roleIdList'] =array_map('strval', array_column($userRoleList, 'role_id')); + $this->successWithData([ + 'user' => $sysInfo + ]); + + } + + public function updateRoleInfo($params) + { + if (!empty($params['role_id_list'])) { + Db::name('sys_user_role')->where([ + 'user_id' => $params['user_id'] + ])->delete(); + + foreach ($params['role_id_list'] as $roleId) { + Db::name('sys_user_role')->insert([ + 'user_id' => $params['user_id'], + 'role_id' => $roleId + ]); + } + } + } + + public function update() + { + $params = $this->request->post(); + if (empty($params['userId'])) { + $this->error('参数错误'); + } + $params = convertKeysCamelToSnakeRecursive($params); + if (isset($params['password']) && $params['password'] == '') { + unset($params['password']); + } else if (!empty($params['password'])) { + $params['password'] = shiro_simple_hash_hex_salt('sha256', $params['password'], $params['salt']); + } + $this->updateRoleInfo($params); + unset($params['role_id_list']); + + DatabaseRoute::getDb('sys_user', $params['user_id'], true, true)->where([ + 'user_id' => $params['user_id'] + ])->update($params); + + $this->success(); + } + + public function save() + { + $params = $this->request->post(); + + $params = convertKeysCamelToSnakeRecursive($params); + $params['create_time'] = getNormalDate(); + $params['salt'] = Random::generateRandomPrefixedId(20); + $params['password'] = shiro_simple_hash_hex_salt('sha256', $params['password'], $params['salt']); + $params['user_id'] = Random::generateRandomPrefixedId(); + + $this->updateRoleInfo($params); + + unset($params['role_id_list']); + unset($params['money']); + DatabaseRoute::getDb('sys_user', $params['user_id'], true)->insert($params); + if (!empty($params['is_channel']) && $params['is_channel'] == 1 && empty($params['qd_code'])) { + $params['qd_code'] = InvitationCodeUtil::toRegisteredCode($params['user_id']); + DatabaseRoute::getDb('sys_user', $params['user_id'], true, true)->update($params); + } + + $this->success(); + } + + public function delete() + { + $params = $this->request->post(); + if (empty($params)) { + $this->error('参数有误'); + } + + foreach ($params as $id) { + DatabaseRoute::getDb('sys_user', $id, true, true)->delete(); + } + + $this->success(); + + } + + + public function selectInviteUserList() + { + $params = $this->request->param(); + $params = convertKeysCamelToSnakeRecursive($params); + + // 1. 分页查询被邀请人分组:按 inviter_code 聚合 + $inviteListPageInfo = DatabaseRoute::paginateAllDb('tb_user', function ($query) use ($params) { + if (!empty($params['user_name'])) { + $query->where('user_name', 'like', '%' . $params['user_name'] . '%'); + } + + if (!empty($params['phone'])) { + $query->where('phone', 'like', '%' . $params['phone'] . '%'); + } + + return $query->fieldRaw('ANY_VALUE(inviter_code) AS inviter_code, COUNT(*) AS counts') + ->group('inviter_code') + ->order('counts', 'desc'); + }, $params['page'], $params['limit'], 'counts', null, true); + + $records = $inviteListPageInfo['records']; + if (empty($records)) { + $this->successWithData($inviteListPageInfo); // 无数据 + } + + $inviteCodeList = array_column($records, 'inviter_code'); + $countMap = array_column($records, 'counts', 'inviter_code'); + + // 2. 查询邀请人信息(部分 code 可能查不到 => 被删) + $userInfoList = DatabaseRoute::getAllDbData('tb_user', function ($query) use ($inviteCodeList) { + return $query->whereIn('invitation_code', $inviteCodeList); + })->select()->toArray(); + + // 3. 查询金额信息(注意先判断 user_id 是否存在) + $userIdList = array_column($userInfoList, 'user_id'); + $userMoneyList = []; + if (!empty($userIdList)) { + $userMoneyList = DatabaseRoute::getAllDbData('user_money', function ($query) use ($userIdList) { + return $query->whereIn('user_id', $userIdList); + })->select()->toArray(); + } + + $userMoneyMap = []; + foreach ($userMoneyList as $money) { + $userMoneyMap[$money['user_id']] = $money; + } + + // 4. 构建 inviter_code => user 映射 + $inviterMap = []; + foreach ($userInfoList as $user) { + $code = $user['invitation_code']; + $uid = $user['user_id']; + + $user['money'] = $userMoneyMap[$uid]['invite_income_money'] ?? 0; + $user['counts'] = $countMap[$code] ?? 0; + + $inviterMap[$code] = $user; + } + + // 5. 最终组装记录:保留所有 inviter_code,即使用户不存在 + $finalRecords = []; + foreach ($inviteCodeList as $code) { + if (isset($inviterMap[$code])) { + $finalRecords[] = $inviterMap[$code]; + } else { + // 🛠️ 如果邀请人被删,构造一条匿名信息 + $finalRecords[] = [ + 'user_id' => null, + 'user_name' => '已删除', + 'phone' => '-', + 'money' => 0, + 'counts' => $countMap[$code], + 'invitation_code' => $code, + ]; + } + } + + $inviteListPageInfo['records'] = $finalRecords; + $this->successWithData($inviteListPageInfo); + } + + +} \ No newline at end of file diff --git a/app/admin/controller/user/Group.php b/app/admin/controller/user/Group.php new file mode 100644 index 0000000..8ca7996 --- /dev/null +++ b/app/admin/controller/user/Group.php @@ -0,0 +1,163 @@ +model = new UserGroup(); + } + + /** + * 添加 + * @throws Throwable + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $data = $this->handleRules($data); + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + $validate->scene('add')->check($data); + } + } + $result = $this->model->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + $this->error(__('Parameter error')); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $data = $this->handleRules($data); + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + $validate->scene('edit')->check($data); + } + } + $result = $row->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + } + + // 读取所有pid,全部从节点数组移除,父级选择状态由子级决定 + $pidArr = UserRule::field('pid') + ->distinct(true) + ->where('id', 'in', $row->rules) + ->select() + ->toArray(); + $rules = $row->rules ? explode(',', $row->rules) : []; + foreach ($pidArr as $item) { + $ruKey = array_search($item['pid'], $rules); + if ($ruKey !== false) { + unset($rules[$ruKey]); + } + } + $row->rules = array_values($rules); + $this->success('', [ + 'row' => $row + ]); + } + + /** + * 权限规则入库前处理 + * @param array $data 接受到的数据 + * @return array + * @throws Throwable + */ + private function handleRules(array &$data): array + { + if (is_array($data['rules']) && $data['rules']) { + $rules = UserRule::select(); + $super = true; + foreach ($rules as $rule) { + if (!in_array($rule['id'], $data['rules'])) { + $super = false; + } + } + + if ($super) { + $data['rules'] = '*'; + } else { + $data['rules'] = implode(',', $data['rules']); + } + } else { + unset($data['rules']); + } + return $data; + } +} \ No newline at end of file diff --git a/app/admin/controller/user/MoneyLog.php b/app/admin/controller/user/MoneyLog.php new file mode 100644 index 0000000..20f86f1 --- /dev/null +++ b/app/admin/controller/user/MoneyLog.php @@ -0,0 +1,50 @@ +model = new UserMoneyLog(); + } + + /** + * 添加 + * @param int $userId + * @throws Throwable + */ + public function add(int $userId = 0): void + { + if ($this->request->isPost()) { + parent::add(); + } + + $user = User::where('id', $userId)->find(); + if (!$user) { + $this->error(__("The user can't find it")); + } + $this->success('', [ + 'user' => $user + ]); + } +} \ No newline at end of file diff --git a/app/admin/controller/user/Rule.php b/app/admin/controller/user/Rule.php new file mode 100644 index 0000000..8b22011 --- /dev/null +++ b/app/admin/controller/user/Rule.php @@ -0,0 +1,260 @@ + 'desc']; + + protected string|array $quickSearchField = 'title'; + + /** + * 远程select初始化传值 + * @var array + */ + protected array $initValue; + + /** + * 是否组装Tree + * @var bool + */ + protected bool $assembleTree; + + /** + * 搜索关键词 + * @var string + */ + protected string $keyword; + + public function initialize(): void + { + parent::initialize(); + + // 防止 URL 中的特殊符号被默认的 filter 函数转义 + $this->request->filter('clean_xss'); + + $this->model = new UserRule(); + $this->tree = Tree::instance(); + $isTree = $this->request->param('isTree', true); + $this->initValue = $this->request->get("initValue/a", []); + $this->initValue = array_filter($this->initValue); + $this->keyword = $this->request->request('quickSearch', ''); + $this->assembleTree = $isTree && !$this->initValue; // 有初始化值时不组装树状(初始化出来的值更好看) + } + + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + $this->success('', [ + 'list' => $this->getRules(), + 'remark' => get_route_remark(), + ]); + } + + /** + * 添加 + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + if ($this->dataLimit && $this->dataLimitFieldAutoFill) { + $data[$this->dataLimitField] = $this->auth->id; + } + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('add'); + $validate->check($data); + } + } + $result = $this->model->save($data); + + if (!empty($data['pid'])) { + $groups = UserGroup::where('rules', '<>', '*')->select(); + foreach ($groups as $group) { + $rules = explode(',', $group->rules); + if (in_array($data['pid'], $rules) && !in_array($this->model->id, $rules)) { + $rules[] = $this->model->id; + $group->rules = implode(',', $rules); + $group->save(); + } + } + } + + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + $this->error(__('Parameter error')); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $id = $this->request->param($this->model->getPk()); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) { + $this->error(__('You have no permission')); + } + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('edit'); + $validate->check($data); + } + } + if (isset($data['pid']) && $data['pid'] > 0) { + // 满足意图并消除副作用 + $parent = $this->model->where('id', $data['pid'])->find(); + if ($parent['pid'] == $row['id']) { + $parent->pid = 0; + $parent->save(); + } + } + $result = $row->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + + } + + $this->success('', [ + 'row' => $row + ]); + } + + /** + * 删除 + * @throws Throwable + */ + public function del(): void + { + $ids = $this->request->param('ids/a', []); + + // 子级元素检查 + $subData = $this->model->where('pid', 'in', $ids)->column('pid', 'id'); + foreach ($subData as $key => $subDatum) { + if (!in_array($key, $ids)) { + $this->error(__('Please delete the child element first, or use batch deletion')); + } + } + + parent::del(); + } + + /** + * 远程下拉 + * @throws Throwable + */ + public function select(): void + { + $data = $this->getRules([['status', '=', 1]]); + + if ($this->assembleTree) { + $data = $this->tree->assembleTree($this->tree->getTreeArray($data, 'title')); + } + $this->success('', [ + 'options' => $data + ]); + } + + /** + * 获取菜单规则 + * @throws Throwable + */ + private function getRules(array $where = []): array + { + $pk = $this->model->getPk(); + $initKey = $this->request->get("initKey/s", $pk); + + if ($this->keyword) { + $keyword = explode(' ', $this->keyword); + foreach ($keyword as $item) { + $where[] = [$this->quickSearchField, 'like', '%' . $item . '%']; + } + } + + if ($this->initValue) { + $where[] = [$initKey, 'in', $this->initValue]; + } + + $data = $this->model + ->where($where) + ->order($this->queryOrderBuilder()) + ->select() + ->toArray(); + + return $this->assembleTree ? $this->tree->assembleChild($data) : $data; + } + +} \ No newline at end of file diff --git a/app/admin/controller/user/ScoreLog.php b/app/admin/controller/user/ScoreLog.php new file mode 100644 index 0000000..d8f98db --- /dev/null +++ b/app/admin/controller/user/ScoreLog.php @@ -0,0 +1,50 @@ +model = new UserScoreLog(); + } + + /** + * 添加 + * @param int $userId + * @throws Throwable + */ + public function add(int $userId = 0): void + { + if ($this->request->isPost()) { + parent::add(); + } + + $user = User::where('id', $userId)->find(); + if (!$user) { + $this->error(__("The user can't find it")); + } + $this->success('', [ + 'user' => $user + ]); + } +} \ No newline at end of file diff --git a/app/admin/controller/user/User.php b/app/admin/controller/user/User.php new file mode 100644 index 0000000..b791a4d --- /dev/null +++ b/app/admin/controller/user/User.php @@ -0,0 +1,542 @@ +model = new UserModel(); + } + + /** + * 查看 + * @throws Throwable + */ + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + list($where, $alias, $limit, $order) = $this->queryBuilder(); + $res = $this->model + ->withoutField('password,salt') + ->withJoin($this->withJoinTable, $this->withJoinType) + ->alias($alias) + ->where($where) + ->order($order) + ->paginate($limit); + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + 'remark' => get_route_remark(), + ]); + } + + /** + * 添加 + * @throws Throwable + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $result = false; + $passwd = $data['password']; // 密码将被排除不直接入库 + $data = $this->excludeFields($data); + + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('add'); + $validate->check($data); + } + } + $result = $this->model->save($data); + $this->model->commit(); + + if (!empty($passwd)) { + $this->model->resetPassword($this->model->id, $passwd); + } + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + $this->error(__('Parameter error')); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + if ($this->request->isPost()) { + $password = $this->request->post('password', ''); + if ($password) { + $this->model->resetPassword($id, $password); + } + parent::edit(); + } + + unset($row->salt); + $row->password = ''; + $this->success('', [ + 'row' => $row + ]); + } + + /** + * 重写select + * @throws Throwable + */ + public function select(): void + { + list($where, $alias, $limit, $order) = $this->queryBuilder(); + $res = $this->model + ->withoutField('password,salt') + ->withJoin($this->withJoinTable, $this->withJoinType) + ->alias($alias) + ->where($where) + ->order($order) + ->paginate($limit); + + foreach ($res as $re) { + $re->nickname_text = $re->username . '(ID:' . $re->id . ')'; + } + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + 'remark' => get_route_remark(), + ]); + } + + // 短剧分析 + public function courseMessage() + { + $get = $this->request->get(); + $admin = $this->auth->getAdmin(); + $pageUtils = \app\admin\model\User::queryCourseOrder($get['page'], $get['limit'], $get['type'], completeStartTime($get['date']), $admin['user_id']); + $this->n_success(['data' => $pageUtils]); + } + + // 用户分析 + public function userMessage() + { + $get = $this->request->get(); + $admin = $this->auth->getAdmin(); + // 补全开始时间(调用之前实现的函数) + $date = completeStartTime($get['date']); // 假设已实现该函数 + $qdCode = $admin['qd_code']; + $sumUserCount = \app\admin\model\User::queryUserCount($get['type'], $date, null, $qdCode); + $h5Count = \app\admin\model\User::queryUserCount($get['type'], $date, "h5", $qdCode); + $appCount = \app\admin\model\User::queryUserCount($get['type'], $date, "app", $qdCode); + $wxCount = \app\admin\model\User::queryUserCount($get['type'], $date, "小程序", $qdCode); + $dyCount = \app\admin\model\User::queryUserCount($get['type'], $date, "抖音", $qdCode); + $giveMemberCount = \app\admin\model\User::userMessage($date, $get['type'], $qdCode, 1); + $moneyMemberCount = \app\admin\model\User::userMessage($date, $get['type'], $qdCode, 2); + $memberCount = \app\admin\model\User::userMessage($date, $get['type'], $qdCode, null); + $userCount = $sumUserCount - $memberCount; + $this->n_success(['data' => [ + 'sumUserCount' => $sumUserCount, + 'h5Count' => $h5Count, + 'appCount' => $appCount, + 'wxCount' => $wxCount, + 'dyCount' => $dyCount, + 'memberCount' => $memberCount, + 'giveMemberCount' => $giveMemberCount, + 'moneyMemberCount' => $moneyMemberCount, + 'userCount' => $userCount, + ]]); + } + + // 当前在线人数统计 + public function selectUserOnLineCount() + { + $admin = $this->auth->getAdmin(); + $qdCode = $admin['qd_code']; + $selectUserOnLineCount = DatabaseRoute::getAllDbData('tb_user', function($query)use($qdCode) { + if($query) { + $query->where(['qd_code' => $qdCode]); + } + return $query->where('on_line_time', '>=', Db::raw('DATE_SUB(NOW(), INTERVAL 10 MINUTE)')); + })->count(); + $this->n_success(['data' => $selectUserOnLineCount]); + } + + // 用户统计 + public function homeMessage() + { + $admin = $this->auth->getAdmin(); + $qdCode = $admin['qd_code']; + $data = []; + $data['totalUsers'] = \app\admin\model\User::queryUserCount(0, null, null, $qdCode); + $data['newToday'] = \app\admin\model\User::queryUserCount(1, null, null, $qdCode); + $data['newMonth'] = \app\admin\model\User::queryUserCount(2, null, null, $qdCode); + $data['newYear'] = \app\admin\model\User::queryUserCount(3, null, null, $qdCode); + $data['totalRevenue'] = \app\admin\model\User::queryPayMoney(0, $qdCode); + $data['todayRevenue'] = \app\admin\model\User::queryPayMoney(1, $qdCode); + $data['monthRevenue'] = \app\admin\model\User::queryPayMoney(2, $qdCode); + $data['yearRevenue'] = \app\admin\model\User::queryPayMoney(3, $qdCode); + $map = \app\admin\model\User::queryPayAndExtractInfo(); + $data['todayPayAmount'] = isset($map['payAmount']) + ? round((float)$map['payAmount'], 2, PHP_ROUND_HALF_UP) + : 0.00; + $data['todayPayCount'] = isset($map['payCount']) + ? (int)$map['payCount'] + : 0; + $data['todayExtractAmount'] = isset($map['extractAmount']) + ? round((float)$map['extractAmount'], 2, PHP_ROUND_HALF_UP) + : 0.00; + + $data['todayExtractCount'] = isset($map['extractCount']) + ? (int)$map['extractCount'] + : 0; + $this->n_success(['data' => $data]); + } + + // 用户增长折线图 + public function selectUserCountStatisticsByTime() + { + $get = $this->request->get(); + $startTime = $get['startTime']; + $endTime = $get['endTime']; + + // 初始化结果数组 + $userCountList = []; + $dateList = []; + + // 日期处理 + $currentDate = strtotime($startTime); + $endDate = strtotime($endTime); + + // 循环遍历日期范围 + while ($currentDate <= $endDate) { + $date = date('Y-m-d', $currentDate); + + // 查询当日用户注册数量 + $userCount = \app\admin\model\User::queryUserCount(1, $date, null, null); + + // 记录数据 + $userCountList[] = $userCount; + $dateList[] = $date; + + // 日期加1天 + $currentDate = strtotime('+1 day', $currentDate); + } + + // 构建结果数据 + $result = [ + 'userCountList' => $userCountList, + 'year' => $dateList // 注:原Java代码中使用year变量,但实际存储的是日期列表 + ]; + + $this->n_success(['data' => $result]); + + } + + + + // 查询所有用户列表 + public function selectUserList() + { + $params = $this->request->get(); + $vipType = $params['vipType'] ?? null; + $member = $params['member'] ?? null; + $status = $params['status'] ?? null; + $page = $params['page'] ?? null; + $limit = $params['limit'] ?? null; + $phone = $params['phone'] ?? null; + $sysUserName = $params['sysUserName'] ?? null; + $userName = $params['userName'] ?? null; + $sex = $params['sex'] ?? null; + $platform = $params['platform'] ?? null; + $sysPhone = $params['sysPhone'] ?? null; + $inviterCode = $params['inviterCode'] ?? null; + $invitationCode = $params['invitationCode'] ?? null; + $qdCode = $params['qdCode'] ?? null; + $startTime = $params['startTime'] ?? null; + $endTime = $params['endTime'] ?? null; + $delegate = $params['delegate'] ?? null; + $this->n_success(['data' => \app\admin\model\User::selectUserPage( + $page, $limit, $phone, $sex, $platform, $sysPhone, $status, $member, + $inviterCode, $userName, $invitationCode, $startTime, $endTime, $qdCode, $sysUserName, $vipType, $delegate + )]); + } + + // 修改用户状态 + public function updateUserStatusByUserId() + { + $get = $this->request->get(); + if(empty($get['userId'])) { + $this->error('userId 不能为空'); + } + if(empty($get['status']) && $get['status'] == null) { + $this->error('status 不能为空'); + } + $status = $get['status']; + $userId = $get['userId']; + $db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userId], true)); + // 查询用户是否存在 + $user = $db->name('tb_user')->where(['user_id' => $userId])->find(); + if (is_null($user)) { + $this->error('用户不存在'); + } + + // 根据状态执行不同操作 + switch ($status) { + case 1: + // 状态1:设置状态为1,并调用upUserBlack方法(拉黑) + \app\admin\model\User::upUserBlack($user, 1, $db); + break; + case 2: + // 状态2:直接更新状态为2 + $db->name('tb_user')->where('user_id', $userId) + ->update(['status' => 2]); + break; + case 0: + // 状态0:调用upUserBlack方法(解封) + \app\admin\model\User::upUserBlack($user, 0, $db); + break; + default: + // 无效状态 + $this->error('状态不正确'); + } + $this->success(); + } + + + // 更新用户邀请奖励金额 + public function inviteAmount() + { + $userInviteDTO = $this->request->post(); + // 验证用户ID不能为空 + if (empty($userInviteDTO['userId'])) { + $this->error('用户id不能为空'); + } + + // 验证邀请奖励金额必须大于0 + $inviteAmount = $userInviteDTO['inviteAmount'] ?? null; + if (is_null($inviteAmount) || bccomp($inviteAmount, 0) <= 0) { + $this->error('邀请奖励金额必须大于0'); + } + + $db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userInviteDTO['userId']], true))->name('tb_user'); + // 查询用户是否存在 + $userEntity = $db->where(['user_id' => $userInviteDTO['userId']])->find($userInviteDTO['userId']); + if (is_null($userEntity)) { + $this->error('用户不存在'); + } + // 更新用户邀请奖励金额 + $db->where(['user_id' => $userInviteDTO['userId']])->update(['invite_amount' => $inviteAmount]); + $this->success(); + } + + + public function updatePwd() + { + $get = $this->request->get(); + if(empty($get['userId']) || empty($get['pwd'])) { + $this->error('参数不完整'); + } + $userId = $get['userId']; + $pwd = $get['pwd']; + $db = Db::connect(DatabaseRoute::getConnection('sys_user', ['user_id' => $userId])); + $user = $db->name('sys_user')->where(['user_id' => $userId])->find(); + if(!$user) { + $this->error('用户不存在'); + } + $db = Db::connect(DatabaseRoute::getConnection('sys_user', ['user_id' => $userId], true)); + $user = $db->name('sys_user')->where(['user_id' => $userId])->update(['password' => shiro_simple_hash_hex_salt('sha256', $pwd, $user['salt'])]); + if($user) { + $this->success(); + } + $this->error(); + } + + public function deleteUserByUserId() + { + $post = $this->request->post(); + if(empty($post['userId'])) { + $this->error('userId 不能为空'); + } + $userId = $post['userId']; + $db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userId], true)); + $user = $db->name('tb_user')->where(['user_id' => $userId])->delete(); + if($user) { + $this->success(); + } + $this->error('操作失败'); + } + + + public function getuserinfo() + { + $userId = $this->request->get('userId'); + if(empty($userId)) { + $this->error('userId 不能为空'); + } + + $db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userId])); + $user = $db->name('tb_user')->where(['user_id' => $userId])->find(); + if(!$user) { + $this->error('用户不存在'); + } + + $inviteMoney = \app\admin\model\User::selectInviteMoneyByUserId($userId, $db); + if(empty($inviteMoney)) { + $inviteMoney = [ + 'user_id' => $userId, + 'money_sum' => 0.00, + 'money' => 0.00, + 'cash_out' => 0.00, + ]; + Db::connect(DatabaseRoute::getConnection('invite_money', ['user_id' => $userId], true), true)->name('invite_money')->insert($inviteMoney); + } + $money = $inviteMoney['money']; + + // 获取当前日期(格式:YYYY-MM-DD HH:mm:ss) + $date = date('Y-m-d H:i:s'); + + // 查询本月充值(复用之前的instantselectSumPay方法) + $consume = \app\admin\model\User::instantselectSumPay(date('Y-m'), $userId, $db); + + // 查询本月提现(假设monthIncome方法已实现) + $income = \app\admin\model\User::monthIncome(date('Y-m'), $userId, $db); + + // 查询邀请人数(复用之前的countUsersByInviterCode方法) + $count = \app\admin\model\User::queryInviterCount($user['invitation_code']); + + // 查询VIP信息 + $userVip = \app\admin\model\User::selectUserVipByUserId($userId); + if ($userVip) { + $user['member'] = $userVip['is_vip']; + $user['end_time'] = $userVip['end_time']; + $user['vip_type'] = $userVip['vip_type']; + } + + // 组装结果数据 + $resultData = [ + 'userEntity' => $user, + 'money' => $money, + 'consume' => $consume, + 'income' => $income, + 'count' => $count + ]; + $this->n_success(['data' => $resultData]); + + } + + public function userListExcel() + { + $get = $this->request->get(); + $startTime = $get['startTime'] ?? null; + $endTime = $get['endTime'] ?? null; + $this->n_success(\app\admin\model\User::userListExcel($startTime, $endTime)); + } + + + + // 获取用户详细信息 + public function selectUserByInvitationCode() + { + $get = $this->request->get(); + if(empty($get['invitationCode'])) { + $this->error('参数不完整'); + } + $invitationCode = $get['invitationCode']; + $userEntity = TbUser::GetByusername($invitationCode, 'invitation_code'); + if(empty($userEntity)) { + $this->error('用户信息不存在'); + } + $userId = $userEntity['user_id']; + + $db = Db::connect(DatabaseRoute::getConnection('invite_money', ['user_id' => $userId])); + // 查询用户钱包 + $inviteMoney = \app\admin\model\User::selectInviteMoneyByUserId($userId, $db); + $money = $inviteMoney['money']; + // 获取当前时间(格式:Y-m-d H:i:s) + $currentDate = date('Y-m-d H:i:s'); + // 查询本月充值总额 + $consume = \app\admin\model\User::instantselectSumPay($currentDate, $userId, $db); + $income = \app\admin\model\User::monthIncome($currentDate, $userId, $db); + //查询邀请人数 + $count = \app\admin\model\User::queryInviterCount($userEntity['invitation_code']); + $userVip = \app\admin\model\User::selectUserVipByUserId($userId); + if ($userVip) { + $userEntity['member'] = $userVip['is_vip']; + $userEntity['end_time'] = $userVip['end_time']; + $userEntity['vip_type'] = $userVip['vip_type']; + } + $data = [ + 'userEntity' => $userEntity, + 'money' => $money, + 'consume' => $consume, + 'income' => $income, + 'count' => $count + ]; + $this->n_success(['data' => $data]); + } + + +} \ No newline at end of file diff --git a/app/admin/event.php b/app/admin/event.php new file mode 100644 index 0000000..8e86bde --- /dev/null +++ b/app/admin/event.php @@ -0,0 +1,16 @@ + [ + ], + 'listen' => [ + 'AppInit' => [], + 'HttpRun' => [], + 'HttpEnd' => [], + 'LogLevel' => [], + 'LogWrite' => [], + 'backendInit' => [app\common\event\Security::class], + ], + 'subscribe' => [ + ], +]; \ No newline at end of file diff --git a/app/admin/lang/en.php b/app/admin/lang/en.php new file mode 100644 index 0000000..b16526f --- /dev/null +++ b/app/admin/lang/en.php @@ -0,0 +1,38 @@ + 'Please login first', + 'You have no permission' => 'You have no permission to operate', + 'Username' => 'Username', + 'Password' => 'Password', + 'Nickname' => 'Nickname', + 'Email' => 'Email', + 'Mobile' => 'Mobile Number', + 'Captcha' => 'Captcha', + 'CaptchaId' => 'Captcha Id', + 'Please enter the correct verification code' => 'Please enter the correct Captcha!', + 'Captcha error' => 'Captcha error!', + 'Parameter %s can not be empty' => 'Parameter %s can not be empty', + 'Record not found' => 'Record not found', + 'No rows were added' => 'No rows were added', + 'No rows were deleted' => 'No rows were deleted', + 'No rows updated' => 'No rows updated', + 'Update successful' => 'Update successful!', + 'Added successfully' => 'Added successfully!', + 'Deleted successfully' => 'Deleted successfully!', + 'Parameter error' => 'Parameter error!', + 'File uploaded successfully' => 'File uploaded successfully', + 'No files were uploaded' => 'No files were uploaded', + 'The uploaded file format is not allowed' => 'The uploaded file format is no allowance.', + 'The uploaded image file is not a valid image' => 'The uploaded image file is not a valid image', + 'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => 'The uploaded file is too large (%sMiB), maximum file size:%sMiB', + 'No files have been uploaded or the file size exceeds the upload limit of the server' => 'No files have been uploaded or the file size exceeds the server upload limit.', + 'Unknown' => 'Unknown', + 'Account not exist' => 'Account does not exist', + 'Account disabled' => 'Account is disabled', + 'Token login failed' => 'Token login failed', + 'Username is incorrect' => 'Incorrect username', + 'Please try again after 1 day' => 'The number of login failures exceeds the limit, please try again after 24 hours', + 'Password is incorrect' => 'Wrong password', + 'You are not logged in' => 'You are not logged in', + 'Cache cleaned~' => 'The cache has been cleaned up, please refresh the page.', +]; \ No newline at end of file diff --git a/app/admin/lang/en/ajax.php b/app/admin/lang/en/ajax.php new file mode 100644 index 0000000..e2cf328 --- /dev/null +++ b/app/admin/lang/en/ajax.php @@ -0,0 +1,9 @@ + 'Failed to switch package manager, please modify the configuration file manually:%s', + 'Failed to modify the terminal configuration. Please modify the configuration file manually:%s' => 'Failed to modify the terminal configuration, please modify the configuration file manually:%s', + 'upload' => 'Upload files', + 'Change terminal config' => 'Modify terminal configuration', + 'Clear cache' => 'Clear cache', + 'Data table does not exist' => 'Data table does not exist', +]; \ No newline at end of file diff --git a/app/admin/lang/en/auth/admin.php b/app/admin/lang/en/auth/admin.php new file mode 100644 index 0000000..b1a3073 --- /dev/null +++ b/app/admin/lang/en/auth/admin.php @@ -0,0 +1,5 @@ + 'Administrator Grouping ', + 'Please use another administrator account to disable the current account!' => 'Disable the current account, please use another administrator account!', +]; \ No newline at end of file diff --git a/app/admin/lang/en/auth/group.php b/app/admin/lang/en/auth/group.php new file mode 100644 index 0000000..19aa20f --- /dev/null +++ b/app/admin/lang/en/auth/group.php @@ -0,0 +1,6 @@ + 'Super administrator', + 'No permission' => 'No permission', + 'You cannot modify your own management group!' => 'You cannot modify your own management group!', +]; diff --git a/app/admin/lang/en/auth/menu.php b/app/admin/lang/en/auth/menu.php new file mode 100644 index 0000000..94d9d4c --- /dev/null +++ b/app/admin/lang/en/auth/menu.php @@ -0,0 +1,6 @@ + 'Rule type', + 'title' => 'Rule title', + 'name' => 'Rule name', +]; \ No newline at end of file diff --git a/app/admin/lang/en/crud/crud.php b/app/admin/lang/en/crud/crud.php new file mode 100644 index 0000000..522c32c --- /dev/null +++ b/app/admin/lang/en/crud/crud.php @@ -0,0 +1,7 @@ + 'Field %s failed to be renamed because the field does not exist in the data table', + 'del-field fail not exist' => 'Failed to delete field %s because the field does not exist in the data table', + 'change-field-attr fail not exist' => 'Description Failed to modify the properties of field %s because the field does not exist in the data table', + 'add-field fail exist' => 'Failed to add field %s because the field already exists in the data table', +]; \ No newline at end of file diff --git a/app/admin/lang/en/dashboard.php b/app/admin/lang/en/dashboard.php new file mode 100644 index 0000000..57b0f72 --- /dev/null +++ b/app/admin/lang/en/dashboard.php @@ -0,0 +1,4 @@ + "Open source equals mutual assistance, and needs everyone's support. There are many ways to support it, such as using, recommending, writing tutorials, protecting the ecology, contributing code, answering questions, sharing experiences, donation, sponsorship and so on. Welcome to join us!", +]; \ No newline at end of file diff --git a/app/admin/lang/en/index.php b/app/admin/lang/en/index.php new file mode 100644 index 0000000..e7fb229 --- /dev/null +++ b/app/admin/lang/en/index.php @@ -0,0 +1,9 @@ + 'No background menu, please contact the super administrator!', + 'You have already logged in. There is no need to log in again~' => 'You have already logged in, no need to log in again.', + 'Login succeeded!' => 'Login successful!', + 'Incorrect user name or password!' => 'Incorrect username or password!', + 'Login' => 'Login', + 'Logout' => 'Logout', +]; \ No newline at end of file diff --git a/app/admin/lang/en/routine/admininfo.php b/app/admin/lang/en/routine/admininfo.php new file mode 100644 index 0000000..924bf10 --- /dev/null +++ b/app/admin/lang/en/routine/admininfo.php @@ -0,0 +1,6 @@ + 'Please enter the correct username', + 'Please input correct password' => 'Please enter the correct password', + 'Avatar modified successfully!' => 'Profile picture modified successfully!', +]; \ No newline at end of file diff --git a/app/admin/lang/en/routine/attachment.php b/app/admin/lang/en/routine/attachment.php new file mode 100644 index 0000000..6761720 --- /dev/null +++ b/app/admin/lang/en/routine/attachment.php @@ -0,0 +1,5 @@ + '%d records and files have been deleted', + 'remark_text' => 'When the same file is uploaded multiple times, only one copy will be saved to the disk and an attachment record will be added; Deleting an attachment record will automatically delete the corresponding file!', +]; \ No newline at end of file diff --git a/app/admin/lang/en/routine/config.php b/app/admin/lang/en/routine/config.php new file mode 100644 index 0000000..6707b30 --- /dev/null +++ b/app/admin/lang/en/routine/config.php @@ -0,0 +1,23 @@ + 'Basic configuration', + 'Mail' => 'Mail configuration', + 'Config group' => 'Configure grouping', + 'Site Name' => 'Site name', + 'Config Quick entrance' => 'Quick configuration entrance', + 'Record number' => 'Record Number', + 'Version number' => 'Version Number', + 'time zone' => 'Time zone', + 'No access ip' => 'No access IP', + 'smtp server' => 'SMTP server', + 'smtp port' => 'SMTP port', + 'smtp user' => 'SMTP username', + 'smtp pass' => 'SMTP password', + 'smtp verification' => 'SMTP verification mode', + 'smtp sender mail' => 'SMTP sender mailbox', + 'Variable name' => 'variable name', + 'Test mail sent successfully~' => 'Test message sent successfully', + 'This is a test email' => 'This is a test email', + 'Congratulations, receiving this email means that your email service has been configured correctly' => "Congratulations, when you receive this email, it means that your mail service is configures correctly. This is the email subject, you can use HtmlL! in the main body.", + 'Backend entrance rule' => 'The background entry must start with / and contain only numbers and letters.', +]; \ No newline at end of file diff --git a/app/admin/lang/en/security/datarecycle.php b/app/admin/lang/en/security/datarecycle.php new file mode 100644 index 0000000..e1491ee --- /dev/null +++ b/app/admin/lang/en/security/datarecycle.php @@ -0,0 +1,7 @@ + 'Rule Name', + 'Controller' => 'Controller', + 'Data Table' => 'Corresponding data table', + 'Primary Key' => 'Data table primary key', +]; \ No newline at end of file diff --git a/app/admin/lang/en/security/datarecyclelog.php b/app/admin/lang/en/security/datarecyclelog.php new file mode 100644 index 0000000..5baf49c --- /dev/null +++ b/app/admin/lang/en/security/datarecyclelog.php @@ -0,0 +1,4 @@ + 'No records have been restored', +]; \ No newline at end of file diff --git a/app/admin/lang/en/security/sensitivedata.php b/app/admin/lang/en/security/sensitivedata.php new file mode 100644 index 0000000..4cb11da --- /dev/null +++ b/app/admin/lang/en/security/sensitivedata.php @@ -0,0 +1,8 @@ + 'Rule name', + 'Controller' => 'Controller', + 'Data Table' => 'Corresponding data table', + 'Primary Key' => 'Data table primary key', + 'Data Fields' => 'Sensitive data fields', +]; \ No newline at end of file diff --git a/app/admin/lang/en/security/sensitivedatalog.php b/app/admin/lang/en/security/sensitivedatalog.php new file mode 100644 index 0000000..53f3e10 --- /dev/null +++ b/app/admin/lang/en/security/sensitivedatalog.php @@ -0,0 +1,4 @@ + 'No records have been roll-back', +]; \ No newline at end of file diff --git a/app/admin/lang/en/user/moneylog.php b/app/admin/lang/en/user/moneylog.php new file mode 100644 index 0000000..d7d23a9 --- /dev/null +++ b/app/admin/lang/en/user/moneylog.php @@ -0,0 +1,8 @@ + 'User', + 'money' => 'Change amount', + 'memo' => 'Change Notes', + "The user can't find it" => "User does not exist", + 'Change note cannot be blank' => 'Change Notes cannot be empty', +]; \ No newline at end of file diff --git a/app/admin/lang/en/user/scorelog.php b/app/admin/lang/en/user/scorelog.php new file mode 100644 index 0000000..aed6a51 --- /dev/null +++ b/app/admin/lang/en/user/scorelog.php @@ -0,0 +1,8 @@ + 'User', + 'score' => 'Change points', + 'memo' => 'Change Notes', + "The user can't find it" => 'User does not exist', + 'Change note cannot be blank' => 'Change notes cannot be empty', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn.php b/app/admin/lang/zh-cn.php new file mode 100644 index 0000000..72b49d5 --- /dev/null +++ b/app/admin/lang/zh-cn.php @@ -0,0 +1,62 @@ + '请先登录!', + 'You have no permission' => '没有权限操作!', + 'Username' => '用户名', + 'Password' => '密码', + 'Nickname' => '昵称', + 'Email' => '邮箱', + 'Mobile' => '手机号', + 'Captcha' => '验证码', + 'CaptchaId' => '验证码ID', + 'Please enter the correct verification code' => '请输入正确的验证码!', + 'Captcha error' => '验证码错误!', + 'Parameter %s can not be empty' => '参数%s不能为空', + 'Record not found' => '记录未找到', + 'No rows were added' => '未添加任何行', + 'No rows were deleted' => '未删除任何行', + 'No rows updated' => '未更新任何行', + 'Update successful' => '更新成功!', + 'Added successfully' => '添加成功!', + 'Deleted successfully' => '删除成功!', + 'Parameter error' => '参数错误!', + 'Please use the %s field to sort before operating' => '请使用 %s 字段排序后再操作', + 'File uploaded successfully' => '文件上传成功!', + 'No files were uploaded' => '没有文件被上传', + 'The uploaded file format is not allowed' => '上传的文件格式未被允许', + 'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像', + 'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM', + 'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!', + 'Topic format error' => '上传存储子目录格式错误!', + 'Driver %s not supported' => '不支持的驱动:%s', + 'Unknown' => '未知', + // 权限类语言包-s + 'Account not exist' => '帐户不存在', + 'Account disabled' => '帐户已禁用', + 'Token login failed' => '令牌登录失败', + 'Username is incorrect' => '用户名不正确', + 'Please try again after 1 day' => '登录失败次数超限,请在1天后再试', + 'Password is incorrect' => '密码不正确', + 'You are not logged in' => '你没有登录', + // 权限类语言包-e + // 时间格式化-s + '%d second%s ago' => '%d秒前', + '%d minute%s ago' => '%d分钟前', + '%d hour%s ago' => '%d小时前', + '%d day%s ago' => '%d天前', + '%d week%s ago' => '%d周前', + '%d month%s ago' => '%d月前', + '%d year%s ago' => '%d年前', + '%d second%s after' => '%d秒后', + '%d minute%s after' => '%d分钟后', + '%d hour%s after' => '%d小时后', + '%d day%s after' => '%d天后', + '%d week%s after' => '%d周后', + '%d month%s after' => '%d月后', + '%d year%s after' => '%d年后', + // 时间格式化-e + 'Cache cleaned~' => '缓存已清理,请刷新后台~', + 'Please delete the child element first, or use batch deletion' => '请首先删除子元素,或使用批量删除!', + 'Configuration write failed: %s' => '配置写入失败:%s', + 'Token expiration' => '登录态过期,请重新登录!', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/ajax.php b/app/admin/lang/zh-cn/ajax.php new file mode 100644 index 0000000..2994d4c --- /dev/null +++ b/app/admin/lang/zh-cn/ajax.php @@ -0,0 +1,12 @@ + '开始进行数据库迁移', + 'Start formatting the web project code' => '开始格式化前端代码(失败无影响,代码编辑器内按需的手动格式化即可)', + 'Start installing the composer dependencies' => '开始安装服务端依赖', + 'Start executing the build command of the web project' => '开始执行 web 工程的 build 命令,成功后会自动将构建产物移动至 根目录/public 目录下', + 'Failed to modify the terminal configuration. Please modify the configuration file manually:%s' => '修改终端配置失败,请手动修改配置文件:%s', + 'upload' => '上传文件', + 'Change terminal config' => '修改终端配置', + 'Clear cache' => '清理缓存', + 'Data table does not exist' => '数据表不存在~', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/auth/admin.php b/app/admin/lang/zh-cn/auth/admin.php new file mode 100644 index 0000000..efa0622 --- /dev/null +++ b/app/admin/lang/zh-cn/auth/admin.php @@ -0,0 +1,6 @@ + '管理员分组', + 'Please use another administrator account to disable the current account!' => '请使用另外的管理员账户禁用当前账户!', + 'You have no permission to add an administrator to this group!' => '您没有权限向此分组添加管理员!', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/auth/group.php b/app/admin/lang/zh-cn/auth/group.php new file mode 100644 index 0000000..1b5db16 --- /dev/null +++ b/app/admin/lang/zh-cn/auth/group.php @@ -0,0 +1,13 @@ + '组别名称', + 'Please select rules' => '请选择权限', + 'Super administrator' => '超级管理员', + 'No permission' => '无权限', + 'You cannot modify your own management group!' => '不能修改自己所在的管理组!', + 'You need to have all permissions of this group to operate this group~' => '您需要拥有该分组的所有权限才可以操作该分组~', + 'You need to have all the permissions of the group and have additional permissions before you can operate the group~' => '您需要拥有该分组的所有权限且还有额外权限时,才可以操作该分组~', + 'Role group has all your rights, please contact the upper administrator to add or do not need to add!' => '角色组拥有您的全部权限,请联系上级管理员添加或无需添加!', + 'The group permission node exceeds the range that can be allocated' => '分组权限节点超出可分配范围,请刷新重试~', + 'Remark lang' => '为保障系统安全,角色组本身的上下级关系仅供参考,系统的实际上下级划分是根据`权限多寡`来确定的,两位管理员的权限节点:相同被认为是`同级`、包含且有额外权限才被认为是`上级`,同级不可管理同级,上级可为下级分配自己拥有的权限节点;若有特殊情况管理员需转`上级`,可建立一个虚拟权限节点', +]; diff --git a/app/admin/lang/zh-cn/auth/rule.php b/app/admin/lang/zh-cn/auth/rule.php new file mode 100644 index 0000000..4c08979 --- /dev/null +++ b/app/admin/lang/zh-cn/auth/rule.php @@ -0,0 +1,6 @@ + '规则类型', + 'title' => '规则标题', + 'name' => '规则名称', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/crud/crud.php b/app/admin/lang/zh-cn/crud/crud.php new file mode 100644 index 0000000..36ca330 --- /dev/null +++ b/app/admin/lang/zh-cn/crud/crud.php @@ -0,0 +1,11 @@ + 'CRUD代码生成-解析字段数据', + 'Log start' => 'CRUD代码生成-从历史记录开始', + 'Generate check' => 'CRUD代码生成-生成前预检', + 'change-field-name fail not exist' => '字段 %s 改名失败,数据表内不存在该字段', + 'del-field fail not exist' => '字段 %s 删除失败,数据表内不存在该字段', + 'change-field-attr fail not exist' => '修改字段 %s 的属性失败,数据表内不存在该字段', + 'add-field fail exist' => '添加字段 %s 失败,数据表内已经存在该字段', + 'Failed to load cloud data' => '加载云端数据失败,请稍后重试!', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/dashboard.php b/app/admin/lang/zh-cn/dashboard.php new file mode 100644 index 0000000..e4eaff2 --- /dev/null +++ b/app/admin/lang/zh-cn/dashboard.php @@ -0,0 +1,4 @@ + '开源等于互助;开源需要大家一起来支持,支持的方式有很多种,比如使用、推荐、写教程、保护生态、贡献代码、回答问题、分享经验、打赏赞助等;欢迎您加入我们!', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/index.php b/app/admin/lang/zh-cn/index.php new file mode 100644 index 0000000..b896075 --- /dev/null +++ b/app/admin/lang/zh-cn/index.php @@ -0,0 +1,9 @@ + '无后台菜单,请联系超级管理员!', + 'You have already logged in. There is no need to log in again~' => '您已经登录过了,无需重复登录~', + 'Login succeeded!' => '登录成功!', + 'Incorrect user name or password!' => '用户名或密码不正确!', + 'Login' => '登录', + 'Logout' => '注销登录', +]; diff --git a/app/admin/lang/zh-cn/module.php b/app/admin/lang/zh-cn/module.php new file mode 100644 index 0000000..d63d099 --- /dev/null +++ b/app/admin/lang/zh-cn/module.php @@ -0,0 +1,29 @@ + '订单找不到啦!', + 'Module already exists' => '模块已存在!', + 'package download failed' => '包下载失败!', + 'package check failed' => '包检查失败!', + 'No permission to write temporary files' => '没有权限写入临时文件!', + 'Zip file not found' => '找不到压缩包文件', + 'Unable to open the zip file' => '无法打开压缩包文件', + 'Unable to extract ZIP file' => '无法提取ZIP文件', + 'Unable to package zip file' => '无法打包zip文件', + 'Basic configuration of the Module is incomplete' => '模块基础配置不完整', + 'Module package file does not exist' => '模块包文件不存在', + 'Module file conflicts' => '模块文件存在冲突,请手动处理!', + 'Configuration file has no write permission' => '配置文件无写入权限', + 'The current state of the module cannot be set to disabled' => '模块当前状态无法设定为禁用', + 'The current state of the module cannot be set to enabled' => '模块当前状态无法设定为启用', + 'Module file updated' => '模块文件有更新', + 'Please disable the module first' => '请先禁用模块', + 'Please disable the module before updating' => '更新前请先禁用模块', + 'The directory required by the module is occupied' => '模块所需目录已被占用', + 'Install module' => '安装模块', + 'Unload module' => '卸载模块', + 'Update module' => '更新模块', + 'Change module state' => '改变模块状态', + 'Upload install module' => '上传安装模块', + 'Please login to the official website account first' => '请先使用BuildAdmin官网账户登录到模块市场~', + 'composer config %s conflict' => 'composer 配置项 %s 存在冲突', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/routine/admininfo.php b/app/admin/lang/zh-cn/routine/admininfo.php new file mode 100644 index 0000000..1f2612c --- /dev/null +++ b/app/admin/lang/zh-cn/routine/admininfo.php @@ -0,0 +1,6 @@ + '请输入正确的用户名', + 'Please input correct password' => '请输入正确的密码', + 'Avatar modified successfully!' => '头像修改成功!', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/routine/attachment.php b/app/admin/lang/zh-cn/routine/attachment.php new file mode 100644 index 0000000..22ee5a0 --- /dev/null +++ b/app/admin/lang/zh-cn/routine/attachment.php @@ -0,0 +1,5 @@ + '同一文件被多次上传时,只会保存一份至磁盘和增加一条附件记录;删除附件记录,将自动删除对应文件!', + '%d records and files have been deleted' => '删除了%d条记录和文件', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/routine/config.php b/app/admin/lang/zh-cn/routine/config.php new file mode 100644 index 0000000..22657d0 --- /dev/null +++ b/app/admin/lang/zh-cn/routine/config.php @@ -0,0 +1,25 @@ + '基础配置', + 'Mail' => '邮件配置', + 'Config group' => '配置分组', + 'Site Name' => '站点名称', + 'Backend entrance' => '自定义后台入口', + 'Config Quick entrance' => '快捷配置入口', + 'Record number' => '备案号', + 'Version number' => '版本号', + 'time zone' => '时区', + 'No access ip' => '禁止访问IP', + 'smtp server' => 'SMTP 服务器', + 'smtp port' => 'SMTP 端口', + 'smtp user' => 'SMTP 用户名', + 'smtp pass' => 'SMTP 密码', + 'smtp verification' => 'SMTP 验证方式', + 'smtp sender mail' => 'SMTP 发件人邮箱', + 'Variable name' => '变量名', + 'Test mail sent successfully~' => '测试邮件发送成功~', + 'This is a test email' => '这是一封测试邮件', + 'Congratulations, receiving this email means that your email service has been configured correctly' => '恭喜您,收到此邮件代表您的邮件服务已配置正确;这是邮件主体 在主体中可以使用Html!', + 'The current page configuration item was updated successfully' => '当前页配置项更新成功!', + 'Backend entrance rule' => '后台入口请以 / 开头,且只包含数字和字母。', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/security/datarecycle.php b/app/admin/lang/zh-cn/security/datarecycle.php new file mode 100644 index 0000000..7b21cfb --- /dev/null +++ b/app/admin/lang/zh-cn/security/datarecycle.php @@ -0,0 +1,8 @@ + '规则名称', + 'Controller' => '控制器', + 'Data Table' => '对应数据表', + 'Primary Key' => '数据表主键', + 'Remark lang' => '在此定义需要回收的数据,实现数据自动统一回收', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/security/datarecyclelog.php b/app/admin/lang/zh-cn/security/datarecyclelog.php new file mode 100644 index 0000000..4b8d0b2 --- /dev/null +++ b/app/admin/lang/zh-cn/security/datarecyclelog.php @@ -0,0 +1,4 @@ + '没有记录被还原', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/security/sensitivedata.php b/app/admin/lang/zh-cn/security/sensitivedata.php new file mode 100644 index 0000000..9367d1c --- /dev/null +++ b/app/admin/lang/zh-cn/security/sensitivedata.php @@ -0,0 +1,9 @@ + '规则名称', + 'Controller' => '控制器', + 'Data Table' => '对应数据表', + 'Primary Key' => '数据表主键', + 'Data Fields' => '敏感数据字段', + 'Remark lang' => '在此定义需要保护的敏感字段,随后系统将自动监听该字段的修改操作,并提供了敏感字段的修改回滚功能', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/security/sensitivedatalog.php b/app/admin/lang/zh-cn/security/sensitivedatalog.php new file mode 100644 index 0000000..7e62651 --- /dev/null +++ b/app/admin/lang/zh-cn/security/sensitivedatalog.php @@ -0,0 +1,4 @@ + '没有记录被回滚', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/user/moneylog.php b/app/admin/lang/zh-cn/user/moneylog.php new file mode 100644 index 0000000..da8d81d --- /dev/null +++ b/app/admin/lang/zh-cn/user/moneylog.php @@ -0,0 +1,8 @@ + '用户', + 'money' => '变更金额', + 'memo' => '变更备注', + "The user can't find it" => '用户找不到啦', + 'Change note cannot be blank' => '变更备注不能为空', +]; \ No newline at end of file diff --git a/app/admin/lang/zh-cn/user/scorelog.php b/app/admin/lang/zh-cn/user/scorelog.php new file mode 100644 index 0000000..7822169 --- /dev/null +++ b/app/admin/lang/zh-cn/user/scorelog.php @@ -0,0 +1,8 @@ + '用户', + 'score' => '变更积分', + 'memo' => '变更备注', + "The user can't find it" => '用户找不到啦', + 'Change note cannot be blank' => '变更备注不能为空', +]; \ No newline at end of file diff --git a/app/admin/library/Auth.php b/app/admin/library/Auth.php new file mode 100644 index 0000000..74f4c74 --- /dev/null +++ b/app/admin/library/Auth.php @@ -0,0 +1,517 @@ +setKeepTime((int)Config::get('buildadmin.admin_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->adminAuth)) { + $request->adminAuth = new static($options); + } + return $request->adminAuth; + } + + /** + * 根据Token初始化管理员登录态 + * @param string $token + * @return bool + * @throws Throwable + */ + public function init(string $token): bool + { +// $tokenData = Token::get($token); + $jwtUtil = (new JwtUtils()); + + $tokenData = $jwtUtil->getClaimByToken($token); + + if ($tokenData) { + /** + * 过期检查,过期则抛出 @see TokenExpirationException + */ +// Token::tokenExpirationCheck($tokenData); + +// $userId = intval($tokenData['user_id']); + $userId = $tokenData->sub; + if (!isset($tokenData->type) || ($tokenData->type == self::TOKEN_TYPE && $userId > 0)) { + $this->user_id_slave_connect_db = Db::connect(DatabaseRoute::getConnection('sys_user', ['user_id' => $userId])); + $this->model = $this->user_id_slave_connect_db->name('sys_user')->where('user_id', $userId)->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->loginSuccessful(); + return true; + } + } + $this->setError('Token login failed'); + $this->reset(); + return false; + } + + /** + * 管理员登录 + * @param string $username 用户名 + * @param string $password 密码 + * @param bool $keep 是否保持登录 + * @return bool + * @throws Throwable + */ + public function login(string $username, string $password, bool $keep = false): bool + { + + $this->model = SysUser::GetByusername($username); + if (!$this->model) { + $this->setError('未注册, 请先注册'); + return false; + } + if ($this->model['status'] != 1) { + $this->setError('账号已被禁用,请联系客服处理!'); + return false; + } + + if(empty($this->model['password'])) { + $this->setError('当前账号未绑定密码,请前往忘记密码中进行重置!'); + return false; + } + $pwd = shiro_simple_hash_hex_salt('sha256', $password, $this->model['salt']); + // 验证密码 + if($pwd == $this->model['password']) { + $this->loginSuccessful(); + return true; + }else { + $this->setError('账号或密码不正确!'); + return false; + } + } + + /** + * 设置刷新Token + * @param int $keepTime + */ + public function setRefreshToken(int $keepTime = 0): void + { + $this->refreshToken = Random::uuid(); + Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime); + } + + /** + * 管理员登录成功 + * @return bool + */ + public function loginSuccessful(): bool + { + if (!$this->model) return false; + try { + $this->loginEd = true; + if (!$this->token) { +// $this->token = Random::uuid(); + $this->token = (new JwtUtils())->generateToken($this->model['user_id'], 'admin'); +// Token::set($this->token, self::TOKEN_TYPE, $this->model['user_id'], $this->keepTime); + } + } catch (Throwable $e) { + $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(); + } + + /** + * 退出登录 + * @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 Admin + */ + public function getAdmin() + { + return $this->model; + } + + /** + * 获取管理员Token + * @return string + */ + public function getToken(): string + { + return $this->token; + } + + /** + * 获取管理员刷新Token + * @return string + */ + public function getRefreshToken(): string + { + return $this->refreshToken; + } + + /** + * 获取管理员信息 - 只输出允许输出的字段 + * @return array + */ + public function getInfo(): array + { + if (!$this->model) return []; + $info = $this->model->toArray(); + $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; + } + + + public function checkmenus($url, $uid): bool + { + return parent::checkmenus($url, $uid ?: $this->user_id); + } + public function getUserMenus($uid):array + { + return parent::getUserMenus($uid); + } + public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool + { + return parent::check($name, $uid ?: $this->id, $relation, $mode); + } + + public function getGroups(int $uid = 0): array + { + return parent::getGroups($uid ?: $this->id); + } + + public function getRuleList(int $uid = 0): array + { + return parent::getRuleList($uid ?: $this->id); + } + + public function getRuleIds(int $uid = 0): array + { + return parent::getRuleIds($uid ?: $this->id); + } + + public function getMenus(int $uid = 0): array + { + return parent::getMenus($uid ?: $this->id); + } + + /** + * 是否是超级管理员 + * @throws Throwable + */ + public function isSuperAdmin(): bool + { + return in_array('*', $this->getRuleIds()); + } + + /** + * 获取管理员所在分组的所有子级分组 + * @return array + * @throws Throwable + */ + public function getAdminChildGroups(): array + { + $groupIds = Db::name('admin_group_access') + ->where('uid', $this->id) + ->select(); + $children = []; + foreach ($groupIds as $group) { + $this->getGroupChildGroups($group['group_id'], $children); + } + return array_unique($children); + } + + /** + * 获取一个分组下的子分组 + * @param int $groupId 分组ID + * @param array $children 存放子分组的变量 + * @return void + * @throws Throwable + */ + public function getGroupChildGroups(int $groupId, array &$children): void + { + $childrenTemp = AdminGroup::where('pid', $groupId) + ->where('status', 1) + ->select(); + foreach ($childrenTemp as $item) { + $children[] = $item['id']; + $this->getGroupChildGroups($item['id'], $children); + } + } + + /** + * 获取分组内的管理员 + * @param array $groups + * @return array 管理员数组 + */ + public function getGroupAdmins(array $groups): array + { + return Db::name('admin_group_access') + ->where('group_id', 'in', $groups) + ->column('uid'); + } + + /** + * 获取拥有 `所有权限` 的分组 + * @param string $dataLimit 数据权限 + * @param array $groupQueryWhere 分组查询条件(默认查询启用的分组:[['status','=',1]]) + * @return array 分组数组 + * @throws Throwable + */ + public function getAllAuthGroups(string $dataLimit, array $groupQueryWhere = [['status', '=', 1]]): array + { + // 当前管理员拥有的权限 + $rules = $this->getRuleIds(); + $allAuthGroups = []; + $groups = AdminGroup::where($groupQueryWhere)->select(); + foreach ($groups as $group) { + if ($group['rules'] == '*') { + continue; + } + $groupRules = explode(',', $group['rules']); + + // 及时break, array_diff 等没有 in_array 快 + $all = true; + foreach ($groupRules as $groupRule) { + if (!in_array($groupRule, $rules)) { + $all = false; + break; + } + } + if ($all) { + if ($dataLimit == 'allAuth' || ($dataLimit == 'allAuthAndOthers' && array_diff($rules, $groupRules))) { + $allAuthGroups[] = $group['id']; + } + } + } + return $allAuthGroups; + } + + /** + * 设置错误消息 + * @param $error + * @return Auth + */ + public function setError($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::get('buildadmin.admin_token_keep_time')); + return true; + } +} \ No newline at end of file diff --git a/app/admin/library/crud/Helper.php b/app/admin/library/crud/Helper.php new file mode 100644 index 0000000..ee922f6 --- /dev/null +++ b/app/admin/library/crud/Helper.php @@ -0,0 +1,1291 @@ + [ + 'user' => ['user', 'user'], + 'admin' => ['auth', 'admin'], + 'admin_group' => ['auth', 'group'], + 'attachment' => ['routine', 'attachment'], + 'admin_rule' => ['auth', 'rule'], + ], + 'model' => [], + 'validate' => [], + ]; + + /** + * 子级菜单数组(权限节点) + * @var array + */ + protected static array $menuChildren = [ + ['type' => 'button', 'title' => '查看', 'name' => '/index', 'status' => 1], + ['type' => 'button', 'title' => '添加', 'name' => '/add', 'status' => 1], + ['type' => 'button', 'title' => '编辑', 'name' => '/edit', 'status' => 1], + ['type' => 'button', 'title' => '删除', 'name' => '/del', 'status' => 1], + ['type' => 'button', 'title' => '快速排序', 'name' => '/sortable', 'status' => 1], + ]; + + /** + * 输入框类型的识别规则 + * @var array + */ + protected static array $inputTypeRule = [ + // 开关组件 + [ + 'type' => ['tinyint', 'int', 'enum'], + 'suffix' => ['switch', 'toggle'], + 'value' => 'switch', + ], + [ + 'column_type' => ['tinyint(1)', 'char(1)', 'tinyint(1) unsigned'], + 'suffix' => ['switch', 'toggle'], + 'value' => 'switch', + ], + // 富文本-识别规则和textarea重合,优先识别为富文本 + [ + 'type' => ['longtext', 'text', 'mediumtext', 'smalltext', 'tinytext', 'bigtext'], + 'suffix' => ['content', 'editor'], + 'value' => 'editor', + ], + // textarea + [ + 'type' => ['varchar'], + 'suffix' => ['textarea', 'multiline', 'rows'], + 'value' => 'textarea', + ], + // Array + [ + 'suffix' => ['array'], + 'value' => 'array', + ], + // 时间选择器-字段类型为int同时以['time', 'datetime']结尾 + [ + 'type' => ['int'], + 'suffix' => ['time', 'datetime'], + 'value' => 'timestamp', + ], + [ + 'type' => ['datetime', 'timestamp'], + 'value' => 'datetime', + ], + [ + 'type' => ['date'], + 'value' => 'date', + ], + [ + 'type' => ['year'], + 'value' => 'year', + ], + [ + 'type' => ['time'], + 'value' => 'time', + ], + // 单选select + [ + 'suffix' => ['select', 'list', 'data'], + 'value' => 'select', + ], + // 多选select + [ + 'suffix' => ['selects', 'multi', 'lists'], + 'value' => 'selects', + ], + // 远程select + [ + 'suffix' => ['_id'], + 'value' => 'remoteSelect', + ], + // 远程selects + [ + 'suffix' => ['_ids'], + 'value' => 'remoteSelects', + ], + // 城市选择器 + [ + 'suffix' => ['city'], + 'value' => 'city', + ], + // 单图上传 + [ + 'suffix' => ['image', 'avatar'], + 'value' => 'image', + ], + // 多图上传 + [ + 'suffix' => ['images', 'avatars'], + 'value' => 'images', + ], + // 文件上传 + [ + 'suffix' => ['file'], + 'value' => 'file', + ], + // 多文件上传 + [ + 'suffix' => ['files'], + 'value' => 'files', + ], + // icon选择器 + [ + 'suffix' => ['icon'], + 'value' => 'icon', + ], + // 单选框 + [ + 'column_type' => ['tinyint(1)', 'char(1)', 'tinyint(1) unsigned'], + 'suffix' => ['status', 'state', 'type'], + 'value' => 'radio', + ], + // 数字输入框 + [ + 'suffix' => ['number', 'int', 'num'], + 'value' => 'number', + ], + [ + 'type' => ['bigint', 'int', 'mediumint', 'smallint', 'tinyint', 'decimal', 'double', 'float'], + 'value' => 'number', + ], + // 富文本-低权重 + [ + 'type' => ['longtext', 'text', 'mediumtext', 'smalltext', 'tinytext', 'bigtext'], + 'value' => 'textarea', + ], + // 单选框-低权重 + [ + 'type' => ['enum'], + 'value' => 'radio', + ], + // 多选框 + [ + 'type' => ['set'], + 'value' => 'checkbox', + ], + // 颜色选择器 + [ + 'suffix' => ['color'], + 'value' => 'color', + ], + ]; + + /** + * 预设WEB端文件位置 + * @var array + */ + protected static array $parseWebDirPresets = [ + 'lang' => [], + 'views' => [ + 'user' => ['user', 'user'], + 'admin' => ['auth', 'admin'], + 'admin_group' => ['auth', 'group'], + 'attachment' => ['routine', 'attachment'], + 'admin_rule' => ['auth', 'rule'], + ], + ]; + + /** + * 添加时间字段 + * @var string + */ + protected static string $createTimeField = 'create_time'; + + /** + * 更新时间字段 + * @var string + */ + protected static string $updateTimeField = 'update_time'; + + /** + * 属性的类型对照表 + * @var array + */ + protected static array $attrType = [ + 'controller' => [ + 'preExcludeFields' => 'array|string', + 'quickSearchField' => 'string|array', + 'withJoinTable' => 'array', + 'defaultSortField' => 'string|array', + 'weighField' => 'string', + ], + ]; + + /** + * 获取字段字典数据 + * @param array $dict 存储字典数据的变量 + * @param array $field 字段数据 + * @param string $lang 语言 + * @param string $translationPrefix 翻译前缀 + */ + public static function getDictData(array &$dict, array $field, string $lang, string $translationPrefix = ''): array + { + if (!$field['comment']) return []; + $comment = str_replace([',', ':'], [',', ':'], $field['comment']); + if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false) { + [$fieldTitle, $item] = explode(':', $comment); + $dict[$translationPrefix . $field['name']] = $lang == 'en' ? $field['name'] : $fieldTitle; + foreach (explode(',', $item) as $v) { + $valArr = explode('=', $v); + if (count($valArr) == 2) { + [$key, $value] = $valArr; + $dict[$translationPrefix . $field['name'] . ' ' . $key] = $lang == 'en' ? $field['name'] . ' ' . $key : $value; + } + } + } else { + $dict[$translationPrefix . $field['name']] = $lang == 'en' ? $field['name'] : $comment; + } + return $dict; + } + + /** + * 记录CRUD状态 + * @param array $data CRUD记录数据 + * @return int 记录ID + */ + public static function recordCrudStatus(array $data): int + { + if (isset($data['id'])) { + CrudLog::where('id', $data['id']) + ->update([ + 'status' => $data['status'], + ]); + return $data['id']; + } + + $connection = $data['table']['databaseConnection'] ?: config('database.default'); + $log = CrudLog::create([ + 'table_name' => $data['table']['name'], + 'comment' => $data['table']['comment'], + 'table' => $data['table'], + 'fields' => $data['fields'], + 'connection' => $connection, + 'status' => $data['status'], + ]); + return $log->id; + } + + /** + * 获取 Phinx 的字段类型数据 + * @param string $type 字段类型 + * @param array $field 字段数据 + * @return array + */ + public static function getPhinxFieldType(string $type, array $field): array + { + if ($type == 'tinyint') { + if ( + (isset($field['dataType']) && $field['dataType'] == 'tinyint(1)') || + ($field['default'] == '1' && $field['defaultType'] == 'INPUT') + ) { + $type = 'boolean'; + } + } + $phinxFieldTypeMap = [ + // 数字 + 'tinyint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_TINY], + 'smallint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_SMALL], + 'mediumint' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => MysqlAdapter::INT_MEDIUM], + 'int' => ['type' => AdapterInterface::PHINX_TYPE_INTEGER, 'limit' => null], + 'bigint' => ['type' => AdapterInterface::PHINX_TYPE_BIG_INTEGER, 'limit' => null], + 'boolean' => ['type' => AdapterInterface::PHINX_TYPE_BOOLEAN, 'limit' => null], + // 文本 + 'varchar' => ['type' => AdapterInterface::PHINX_TYPE_STRING, 'limit' => null], + 'tinytext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_TINY], + 'mediumtext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_MEDIUM], + 'longtext' => ['type' => AdapterInterface::PHINX_TYPE_TEXT, 'limit' => MysqlAdapter::TEXT_LONG], + 'tinyblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_TINY], + 'mediumblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_MEDIUM], + 'longblob' => ['type' => AdapterInterface::PHINX_TYPE_BLOB, 'limit' => MysqlAdapter::BLOB_LONG], + ]; + return array_key_exists($type, $phinxFieldTypeMap) ? $phinxFieldTypeMap[$type] : ['type' => $type, 'limit' => null]; + } + + /** + * 分析字段limit和精度 + * @param string $type 字段类型 + * @param array $field 字段数据 + * @return array ['limit' => 10, 'precision' => null, 'scale' => null] + */ + public static function analyseFieldLimit(string $type, array $field): array + { + $fieldType = [ + 'decimal' => ['decimal', 'double', 'float'], + 'values' => ['enum', 'set'], + ]; + + $dataTypeLimit = self::dataTypeLimit($field['dataType'] ?? ''); + if (in_array($type, $fieldType['decimal'])) { + if ($dataTypeLimit) { + return ['precision' => $dataTypeLimit[0], 'scale' => $dataTypeLimit[1] ?? 0]; + } + $scale = isset($field['precision']) ? intval($field['precision']) : 0; + return ['precision' => $field['length'] ?? 10, 'scale' => $scale]; + } elseif (in_array($type, $fieldType['values'])) { + foreach ($dataTypeLimit as &$item) { + $item = str_replace(['"', "'"], '', $item); + } + return ['values' => $dataTypeLimit]; + } elseif ($dataTypeLimit && $dataTypeLimit[0]) { + return ['limit' => $dataTypeLimit[0]]; + } elseif (isset($field['length'])) { + return ['limit' => $field['length']]; + } + return []; + } + + public static function dataTypeLimit(string $dataType): array + { + preg_match("/\((.*?)\)/", $dataType, $matches); + if (isset($matches[1]) && $matches[1]) { + return explode(',', trim($matches[1], ',')); + } + return []; + } + + public static function analyseFieldDefault(array $field): mixed + { + return match ($field['defaultType']) { + 'EMPTY STRING' => '', + 'NULL' => null, + default => $field['default'], + }; + } + + public static function searchArray($fields, callable $myFunction): array|bool + { + foreach ($fields as $key => $field) { + if (call_user_func($myFunction, $field, $key)) { + return $field; + } + } + return false; + } + + /** + * 获取 Phinx 格式的字段数据 + * @param array $field + * @return array + */ + public static function getPhinxFieldData(array $field): array + { + $conciseType = self::analyseFieldType($field); + $phinxTypeData = self::getPhinxFieldType($conciseType, $field); + + $phinxColumnOptions = self::analyseFieldLimit($conciseType, $field); + if (!is_null($phinxTypeData['limit'])) { + $phinxColumnOptions['limit'] = $phinxTypeData['limit']; + } + + // 无默认值字段 + $noDefaultValueFields = [ + 'text', 'blob', 'geometry', 'geometrycollection', 'json', 'linestring', 'longblob', 'longtext', 'mediumblob', + 'mediumtext', 'multilinestring', 'multipoint', 'multipolygon', 'point', 'polygon', 'tinyblob', + ]; + if ($field['defaultType'] != 'NONE' && !in_array($conciseType, $noDefaultValueFields)) { + $phinxColumnOptions['default'] = self::analyseFieldDefault($field); + } + + $phinxColumnOptions['null'] = (bool)$field['null']; + $phinxColumnOptions['comment'] = $field['comment']; + $phinxColumnOptions['signed'] = !$field['unsigned']; + $phinxColumnOptions['identity'] = $field['autoIncrement']; + return [ + 'type' => $phinxTypeData['type'], + 'options' => $phinxColumnOptions, + ]; + } + + /** + * 表字段排序 + * @param string $tableName 表名 + * @param array $fields 字段数据 + * @param array $designChange 前端字段改变数据 + * @param ?string $connection 数据库连接标识 + * @return void + * @throws Throwable + */ + public static function updateFieldOrder(string $tableName, array $fields, array $designChange, ?string $connection = null): void + { + if ($designChange) { + $table = TableManager::phinxTable($tableName, [], false, $connection); + foreach ($designChange as $item) { + if (!$item['sync']) continue; + + if (!empty($item['after'])) { + + $fieldName = in_array($item['type'], ['add-field', 'change-field-name']) ? $item['newName'] : $item['oldName']; + + $field = self::searchArray($fields, function ($field) use ($fieldName) { + return $field['name'] == $fieldName; + }); + + $phinxFieldData = self::getPhinxFieldData($field); + + // 字段顺序调整 + if ($item['after'] == 'FIRST FIELD') { + $phinxFieldData['options']['after'] = MysqlAdapter::FIRST; + } else { + $phinxFieldData['options']['after'] = $item['after']; + } + $table->changeColumn($fieldName, $phinxFieldData['type'], $phinxFieldData['options']); + } + } + $table->update(); + } + } + + /** + * 表设计处理 + * @param array $table 表数据 + * @param array $fields 字段数据 + * @return array + * @throws Throwable + */ + public static function handleTableDesign(array $table, array $fields): array + { + $name = TableManager::tableName($table['name'], true, $table['databaseConnection']); + $comment = $table['comment'] ?? ''; + $designChange = $table['designChange'] ?? []; + $adapter = TableManager::phinxAdapter(false, $table['databaseConnection']); + + $pk = self::searchArray($fields, function ($item) { + return $item['primaryKey']; + }); + $pk = $pk ? $pk['name'] : ''; + + if ($adapter->hasTable($name)) { + // 更新表 + if ($designChange) { + $tableManager = TableManager::phinxTable($name, [], false, $table['databaseConnection']); + $tableManager->changeComment($comment)->update(); + + // 改名和删除操作优先 + $priorityOpt = false; + foreach ($designChange as $item) { + + if (!$item['sync']) continue; + + if (in_array($item['type'], ['change-field-name', 'del-field']) && !$tableManager->hasColumn($item['oldName'])) { + // 字段不存在 + throw new BaException(__($item['type'] . ' fail not exist', [$item['oldName']])); + } + + if ($item['type'] == 'change-field-name') { + $priorityOpt = true; + $tableManager->renameColumn($item['oldName'], $item['newName']); + } elseif ($item['type'] == 'del-field') { + $priorityOpt = true; + $tableManager->removeColumn($item['oldName']); + } + } + + // 保存需要优先执行的操作,避免先改名再改属性时找不到字段 + if ($priorityOpt) { + $tableManager->update(); + } + + // 修改字段属性和添加字段操作 + foreach ($designChange as $item) { + + if (!$item['sync']) continue; + + if ($item['type'] == 'change-field-attr') { + + if (!$tableManager->hasColumn($item['oldName'])) { + // 字段不存在 + throw new BaException(__($item['type'] . ' fail not exist', [$item['oldName']])); + } + + $phinxFieldData = self::getPhinxFieldData(self::searchArray($fields, function ($field) use ($item) { + return $field['name'] == $item['oldName']; + })); + $tableManager->changeColumn($item['oldName'], $phinxFieldData['type'], $phinxFieldData['options']); + } elseif ($item['type'] == 'add-field') { + + if ($tableManager->hasColumn($item['newName'])) { + // 字段已经存在 + throw new BaException(__($item['type'] . ' fail exist', [$item['newName']])); + } + + $phinxFieldData = self::getPhinxFieldData(self::searchArray($fields, function ($field) use ($item) { + return $field['name'] == $item['newName']; + })); + $tableManager->addColumn($item['newName'], $phinxFieldData['type'], $phinxFieldData['options']); + } + } + $tableManager->update(); + + // 表更新结构完成再处理字段排序 + self::updateFieldOrder($name, $fields, $designChange, $table['databaseConnection']); + } + } else { + // 创建表 + $tableManager = TableManager::phinxTable($name, [ + 'id' => false, + 'comment' => $comment, + 'row_format' => 'DYNAMIC', + 'primary_key' => $pk, + 'collation' => 'utf8mb4_unicode_ci', + ], false, $table['databaseConnection']); + foreach ($fields as $field) { + $phinxFieldData = self::getPhinxFieldData($field); + $tableManager->addColumn($field['name'], $phinxFieldData['type'], $phinxFieldData['options']); + } + $tableManager->create(); + } + + return [$pk]; + } + + /** + * 解析文件数据 + * @throws Throwable + */ + public static function parseNameData($app, $table, $type, $value = ''): array + { + $pathArr = []; + if ($value) { + $value = str_replace('.php', '', $value); + $value = str_replace(['.', '/', '\\', '_'], '/', $value); + $pathArrTemp = explode('/', $value); + $redundantDir = [ + 'app' => 0, + $app => 1, + $type => 2, + ]; + foreach ($pathArrTemp as $key => $item) { + if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) { + $pathArr[] = $item; + } + } + } elseif (isset(self::$parseNamePresets[$type]) && array_key_exists($table, self::$parseNamePresets[$type])) { + $pathArr = self::$parseNamePresets[$type][$table]; + } else { + $table = str_replace(['.', '/', '\\', '_'], '/', $table); + $pathArr = explode('/', $table); + } + $originalLastName = array_pop($pathArr); + $pathArr = array_map('strtolower', $pathArr); + $lastName = ucfirst($originalLastName); + + // 类名不能为内部关键字 + if (in_array(strtolower($originalLastName), self::$reservedKeywords)) { + throw new Exception('Unable to use internal variable:' . $lastName); + } + + $appDir = app()->getBasePath() . $app . DIRECTORY_SEPARATOR; + $namespace = "app\\$app\\$type" . ($pathArr ? '\\' . implode('\\', $pathArr) : ''); + $parseFile = $appDir . $type . DIRECTORY_SEPARATOR . ($pathArr ? implode(DIRECTORY_SEPARATOR, $pathArr) . DIRECTORY_SEPARATOR : '') . $lastName . '.php'; + $rootFileName = $namespace . "/$lastName" . '.php'; + + return [ + 'lastName' => $lastName, + 'originalLastName' => $originalLastName, + 'path' => $pathArr, + 'namespace' => $namespace, + 'parseFile' => Filesystem::fsFit($parseFile), + 'rootFileName' => Filesystem::fsFit($rootFileName), + ]; + } + + public static function parseWebDirNameData($table, $type, $value = ''): array + { + $pathArr = []; + if ($value) { + $value = str_replace(['.', '/', '\\', '_'], '/', $value); + $pathArrTemp = explode('/', $value); + $redundantDir = [ + 'web' => 0, + 'src' => 1, + 'views' => 2, + 'lang' => 2, + 'backend' => 3, + 'pages' => 3, + 'en' => 4, + 'zh-cn' => 4, + ]; + foreach ($pathArrTemp as $key => $item) { + if (!array_key_exists($item, $redundantDir) || $key !== $redundantDir[$item]) { + $pathArr[] = $item; + } + } + } elseif (array_key_exists($table, self::$parseWebDirPresets[$type])) { + $pathArr = self::$parseWebDirPresets[$type][$table]; + } else { + $table = str_replace(['.', '/', '\\', '_'], '/', $table); + $pathArr = explode('/', $table); + } + $originalLastName = array_pop($pathArr); + $pathArr = array_map('strtolower', $pathArr); + $lastName = lcfirst($originalLastName); + + $webDir['path'] = $pathArr; + $webDir['lastName'] = $lastName; + $webDir['originalLastName'] = $originalLastName; + if ($type == 'views') { + $webDir['views'] = "web/src/views/backend" . ($pathArr ? '/' . implode('/', $pathArr) : '') . "/$lastName"; + } elseif ($type == 'lang') { + $webDir['lang'] = array_merge($pathArr, [$lastName]); + $langDir = ['en', 'zh-cn']; + foreach ($langDir as $item) { + $webDir[$item] = "web/src/lang/backend/$item" . ($pathArr ? '/' . implode('/', $pathArr) : '') . "/$lastName"; + } + } + foreach ($webDir as &$item) { + if (is_string($item)) $item = Filesystem::fsFit($item); + } + return $webDir; + } + + /** + * 获取菜单name、path + * @param array $webDir + * @return string + */ + public static function getMenuName(array $webDir): string + { + return ($webDir['path'] ? implode('/', $webDir['path']) . '/' : '') . $webDir['originalLastName']; + } + + /** + * 获取基础模板文件路径 + * @param string $name + * @return string + */ + public static function getStubFilePath(string $name): string + { + return app_path() . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'crud' . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . Filesystem::fsFit($name) . '.stub'; + } + + /** + * 多维数组转字符串 + */ + public static function arrayToString(array|string $value): string + { + if (!is_array($value)) { + return $value; + } + foreach ($value as &$item) { + $item = self::arrayToString($item); + } + return implode(PHP_EOL, $value); + } + + /** + * 组装模板 + * @param string $name + * @param array $data + * @param bool $escape + * @return string + */ + public static function assembleStub(string $name, array $data, bool $escape = false): string + { + foreach ($data as &$datum) { + $datum = self::arrayToString($datum); + } + $search = $replace = []; + foreach ($data as $k => $v) { + $search[] = "{%$k%}"; + $replace[] = $v; + } + $stubPath = self::getStubFilePath($name); + $stubContent = file_get_contents($stubPath); + $content = str_replace($search, $replace, $stubContent); + return $escape ? self::escape($content) : $content; + } + + /** + * 获取转义编码后的值 + * @param array|string $value + * @return string + */ + public static function escape(array|string $value): string + { + if (is_array($value)) { + $value = json_encode($value, JSON_UNESCAPED_UNICODE); + } + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); + } + + public static function tab(int $num = 1): string + { + return str_pad('', 4 * $num); + } + + /** + * 根据数据表解析字段数据 + * @throws Throwable + */ + public static function parseTableColumns(string $table, bool $analyseField = false, ?string $connection = null): array + { + $connection = TableManager::getConnection($connection); + $connectionConfig = TableManager::getConnectionConfig($connection); + + // 从数据库中获取表字段信息 + $sql = 'SELECT * FROM `information_schema`.`columns` ' + . 'WHERE TABLE_SCHEMA = ? AND table_name = ? ' + . 'ORDER BY ORDINAL_POSITION'; + + $columns = []; + $tableColumn = Db::connect($connection)->query($sql, [$connectionConfig['database'], TableManager::tableName($table, true, $connection)]); + + foreach ($tableColumn as $item) { + $isNullAble = $item['IS_NULLABLE'] == 'YES'; + if (str_contains($item['COLUMN_TYPE'], '(')) { + $dataType = substr_replace($item['COLUMN_TYPE'], '', stripos($item['COLUMN_TYPE'], ')') + 1); + } else { + $dataType = str_replace(' unsigned', '', $item['COLUMN_TYPE']); + } + + // 默认值和默认值类型分析 + $default = ''; + if ($isNullAble && is_null($item['COLUMN_DEFAULT'])) { + $defaultType = 'NULL'; + } elseif ($item['COLUMN_DEFAULT'] == '' && in_array($item['DATA_TYPE'], ['varchar', 'char'])) { + $defaultType = 'EMPTY STRING'; + } elseif (!$isNullAble && is_null($item['COLUMN_DEFAULT'])) { + $defaultType = 'NONE'; + } else { + $defaultType = 'INPUT'; + $default = $item['COLUMN_DEFAULT']; + } + + $column = [ + 'name' => $item['COLUMN_NAME'], + 'type' => $item['DATA_TYPE'], + 'dataType' => $dataType, + 'default' => $default, + 'defaultType' => $defaultType, + 'null' => $isNullAble, + 'primaryKey' => $item['COLUMN_KEY'] == 'PRI', + 'unsigned' => (bool)stripos($item['COLUMN_TYPE'], 'unsigned'), + 'autoIncrement' => stripos($item['EXTRA'], 'auto_increment') !== false, + 'comment' => $item['COLUMN_COMMENT'], + 'designType' => self::getTableColumnsDataType($item), + 'table' => [], + 'form' => [], + ]; + if ($analyseField) { + self::analyseField($column); + } else { + self::handleTableColumn($column); + } + $columns[$item['COLUMN_NAME']] = $column; + } + return $columns; + } + + /** + * 解析到的表字段的额外处理 + */ + public static function handleTableColumn(&$column): void + { + // 预留 + } + + /** + * 分析字段数据类型 + * @param array $field 字段数据 + * @return string 字段类型 + */ + public static function analyseFieldType(array $field): string + { + $dataType = (isset($field['dataType']) && $field['dataType']) ? $field['dataType'] : $field['type']; + if (stripos($dataType, '(') !== false) { + $typeName = explode('(', $dataType); + return trim($typeName[0]); + } + return trim($dataType); + } + + /** + * 分析字段的完整数据类型定义 + * @param array $field 字段数据 + * @return string + */ + public static function analyseFieldDataType(array $field): string + { + if (!empty($field['dataType'])) return $field['dataType']; + + $conciseType = self::analyseFieldType($field); + $limit = self::analyseFieldLimit($conciseType, $field); + + if (isset($limit['precision'])) { + $dataType = "$conciseType({$limit['precision']}, {$limit['scale']})"; + } elseif (isset($limit['values'])) { + $values = implode(',', $limit['values']); + $dataType = "$conciseType($values)"; + } else { + $dataType = "$conciseType({$limit['limit']})"; + } + return $dataType; + } + + /** + * 分析字段 + */ + public static function analyseField(&$field): void + { + $field['type'] = self::analyseFieldType($field); + $field['originalDesignType'] = $field['designType']; + + // 表单项类型转换对照表 + $designTypeComparison = [ + 'pk' => 'string', + 'weigh' => 'number', + 'timestamp' => 'datetime', + 'float' => 'number', + ]; + if (array_key_exists($field['designType'], $designTypeComparison)) { + $field['designType'] = $designTypeComparison[$field['designType']]; + } + + // 是否开启了多选 + $supportMultipleComparison = ['select', 'image', 'file', 'remoteSelect']; + if (in_array($field['designType'], $supportMultipleComparison)) { + $multiKey = $field['designType'] == 'remoteSelect' ? 'select-multi' : $field['designType'] . '-multi'; + if (isset($field['form'][$multiKey]) && $field['form'][$multiKey]) { + $field['designType'] = $field['designType'] . 's'; + } + } + } + + public static function getTableColumnsDataType($column) + { + if (stripos($column['COLUMN_NAME'], 'id') !== false && stripos($column['EXTRA'], 'auto_increment') !== false) { + return 'pk'; + } elseif ($column['COLUMN_NAME'] == 'weigh') { + return 'weigh'; + } elseif (in_array($column['COLUMN_NAME'], ['createtime', 'updatetime', 'create_time', 'update_time'])) { + return 'timestamp'; + } + foreach (self::$inputTypeRule as $item) { + $typeBool = true; + $suffixBool = true; + $columnTypeBool = true; + if (isset($item['type']) && $item['type'] && !in_array($column['DATA_TYPE'], $item['type'])) { + $typeBool = false; + } + if (isset($item['suffix']) && $item['suffix']) { + $suffixBool = self::isMatchSuffix($column['COLUMN_NAME'], $item['suffix']); + } + if (isset($item['column_type']) && $item['column_type'] && !in_array($column['COLUMN_TYPE'], $item['column_type'])) { + $columnTypeBool = false; + } + if ($typeBool && $suffixBool && $columnTypeBool) { + return $item['value']; + } + } + return 'string'; + } + + /** + * 判断是否符合指定后缀 + * + * @param string $field 字段名称 + * @param string|array $suffixArr 后缀 + * @return bool + */ + protected static function isMatchSuffix(string $field, string|array $suffixArr): bool + { + $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr); + foreach ($suffixArr as $v) { + if (preg_match("/$v$/i", $field)) { + return true; + } + } + return false; + } + + /** + * 创建菜单 + * @throws Throwable + */ + public static function createMenu($webViewsDir, $tableComment): void + { + $menuName = self::getMenuName($webViewsDir); + if (AdminRule::where('name', $menuName)->value('id')) { + return; + } + + // 组装权限节点数据 + $menuChildren = self::$menuChildren; + foreach ($menuChildren as &$item) { + $item['name'] = $menuName . $item['name']; + } + + // 组件路径 + $componentPath = str_replace(['\\', 'web/src'], ['/', '/src'], $webViewsDir['views'] . '/' . 'index.vue'); + + // 菜单数组 + $menus = [ + 'type' => 'menu', + 'title' => $tableComment ?: $webViewsDir['originalLastName'], + 'name' => $menuName, + 'path' => $menuName, + 'menu_type' => 'tab', + 'keepalive' => 1, + 'component' => $componentPath, + 'children' => $menuChildren, + ]; + $paths = array_reverse($webViewsDir['path']); + foreach ($paths as $path) { + $menus = [ + 'type' => 'menu_dir', + 'title' => $path, + 'name' => $path, + 'path' => $path, + 'children' => [$menus], + ]; + } + + // 创建菜单 + Menu::create([$menus], 0, 'ignore'); + } + + public static function writeWebLangFile($langData, $webLangDir): void + { + foreach ($langData as $lang => $langDatum) { + $langTsContent = ''; + foreach ($langDatum as $key => $item) { + $quote = self::getQuote($item); + $keyStr = self::formatObjectKey($key); + $langTsContent .= self::tab() . $keyStr . ": $quote$item$quote,\n"; + } + $langTsContent = "export default {\n" . $langTsContent . "}\n"; + self::writeFile(root_path() . $webLangDir[$lang] . '.ts', $langTsContent); + } + } + + public static function writeFile($path, $content): bool|int + { + $path = Filesystem::fsFit($path); + if (!is_dir(dirname($path))) { + mkdir(dirname($path), 0755, true); + } + return file_put_contents($path, $content); + } + + public static function buildModelAppend($append): string + { + if (!$append) return ''; + $append = self::buildFormatSimpleArray($append); + return "\n" . self::tab() . "// 追加属性" . "\n" . self::tab() . "protected \$append = $append;\n"; + } + + public static function buildModelFieldType(array $fieldType): string + { + if (!$fieldType) return ''; + $maxStrLang = 0; + foreach ($fieldType as $key => $item) { + $strLang = strlen($key); + $maxStrLang = max($strLang, $maxStrLang); + } + + $str = ''; + foreach ($fieldType as $key => $item) { + $str .= self::tab(2) . "'$key'" . str_pad('=>', ($maxStrLang - strlen($key) + 3), ' ', STR_PAD_LEFT) . " '$item',\n"; + } + return "\n" . self::tab() . "// 字段类型转换" . "\n" . self::tab() . "protected \$type = [\n" . rtrim($str, "\n") . "\n" . self::tab() . "];\n"; + } + + public static function writeModelFile(string $tablePk, array $fieldsMap, array $modelData, array $modelFile): void + { + if ($modelData['connection'] && $modelData['connection'] != config('database.default')) { + $modelData['connection'] = "\n" . self::tab() . "// 数据库连接配置标识\n" . self::tab() . 'protected $connection = ' . "'{$modelData['connection']}';\n"; + } else { + $modelData['connection'] = ''; + } + + $modelData['pk'] = $tablePk == 'id' ? '' : "\n" . self::tab() . "// 表主键\n" . self::tab() . 'protected $pk = ' . "'$tablePk';\n"; + $modelData['autoWriteTimestamp'] = array_key_exists(self::$createTimeField, $fieldsMap) || array_key_exists(self::$updateTimeField, $fieldsMap) ? 'true' : 'false'; + if ($modelData['autoWriteTimestamp'] == 'true') { + $modelData['createTime'] = array_key_exists(self::$createTimeField, $fieldsMap) ? '' : "\n" . self::tab() . "protected \$createTime = false;"; + $modelData['updateTime'] = array_key_exists(self::$updateTimeField, $fieldsMap) ? '' : "\n" . self::tab() . "protected \$updateTime = false;"; + } + $modelMethodList = isset($modelData['relationMethodList']) ? array_merge($modelData['methods'], $modelData['relationMethodList']) : $modelData['methods']; + $modelData['methods'] = $modelMethodList ? "\n" . implode("\n", $modelMethodList) : ''; + $modelData['append'] = self::buildModelAppend($modelData['append']); + $modelData['fieldType'] = self::buildModelFieldType($modelData['fieldType']); + + // 生成雪花ID? + if (isset($modelData['beforeInsertMixins']['snowflake'])) { + // beforeInsert 组装 + $modelData['beforeInsert'] = Helper::assembleStub('mixins/model/beforeInsert', [ + 'setSnowFlakeIdCode' => $modelData['beforeInsertMixins']['snowflake'] + ]); + } + if ($modelData['afterInsert'] && $modelData['beforeInsert']) { + $modelData['afterInsert'] = "\n" . $modelData['afterInsert']; + } + + $modelFileContent = self::assembleStub('mixins/model/model', $modelData); + self::writeFile($modelFile['parseFile'], $modelFileContent); + } + + public static function writeControllerFile(array $controllerData, array $controllerFile): void + { + if (isset($controllerData['relationVisibleFieldList']) && $controllerData['relationVisibleFieldList']) { + $relationVisibleFields = '$res->visible(['; + foreach ($controllerData['relationVisibleFieldList'] as $cKey => $controllerDatum) { + $relationVisibleFields .= "'$cKey' => ['" . implode("','", $controllerDatum) . "'], "; + } + $relationVisibleFields = rtrim($relationVisibleFields, ', '); + $relationVisibleFields .= ']);'; + // 重写index + $controllerData['methods']['index'] = self::assembleStub('mixins/controller/index', [ + 'relationVisibleFields' => $relationVisibleFields + ]); + $controllerData['use']['Throwable'] = "\nuse Throwable;"; + unset($controllerData['relationVisibleFieldList']); + } + $controllerAttr = ''; + foreach ($controllerData['attr'] as $key => $item) { + $attrType = ''; + if (array_key_exists($key, self::$attrType['controller'])) { + $attrType = self::$attrType['controller'][$key]; + } + if (is_array($item)) { + $controllerAttr .= "\n" . self::tab() . "protected $attrType \$$key = ['" . implode("', '", $item) . "'];\n"; + } elseif ($item) { + $controllerAttr .= "\n" . self::tab() . "protected $attrType \$$key = '$item';\n"; + } + } + $controllerData['attr'] = $controllerAttr; + $controllerData['initialize'] = self::assembleStub('mixins/controller/initialize', [ + 'modelNamespace' => $controllerData['modelNamespace'], + 'modelName' => $controllerData['modelName'], + 'filterRule' => $controllerData['filterRule'], + ]); + $contentFileContent = self::assembleStub('mixins/controller/controller', $controllerData); + self::writeFile($controllerFile['parseFile'], $contentFileContent); + } + + public static function writeFormFile($formVueData, $webViewsDir, $fields, $webTranslate): void + { + $fieldHtml = "\n"; + $formVueData['bigDialog'] = $formVueData['bigDialog'] ? "\n" . self::tab(2) . 'width="70%"' : ''; + foreach ($formVueData['formFields'] as $field) { + $fieldHtml .= self::tab(5) . " $attr) { + if (is_array($attr)) { + $fieldHtml .= ' ' . $key . '="' . self::getJsonFromArray($attr) . '"'; + } else { + $fieldHtml .= ' ' . $key . '="' . $attr . '"'; + } + } + $fieldHtml .= " />\n"; + } + $formVueData['formFields'] = rtrim($fieldHtml, "\n"); + + // 表单验证规则 + foreach ($fields as $field) { + if (isset($field['form']['validator'])) { + foreach ($field['form']['validator'] as $item) { + $message = ''; + if (isset($field['form']['validatorMsg']) && $field['form']['validatorMsg']) { + $message = ", message: '{$field['form']['validatorMsg']}'"; + } + $formVueData['formValidatorRules'][$field['name']][] = "buildValidatorData({ name: '$item', title: t('$webTranslate{$field['name']}')$message })"; + } + } + } + + if ($formVueData['formValidatorRules']) { + $formVueData['imports'][] = "import { buildValidatorData } from '/@/utils/validate'"; + } + + $formVueData['importExpand'] = self::buildImportExpand($formVueData['imports']); + $formVueData['formItemRules'] = self::buildFormValidatorRules($formVueData['formValidatorRules']); + $formVueContent = self::assembleStub('html/form', $formVueData); + self::writeFile(root_path() . $webViewsDir['views'] . '/' . 'popupForm.vue', $formVueContent); + } + + public static function buildImportExpand(array $imports): string + { + $importExpand = ''; + foreach ($imports as $import) { + $importExpand .= "\n$import"; + } + return $importExpand; + } + + public static function buildFormValidatorRules(array $formValidatorRules): string + { + $rulesHtml = ""; + foreach ($formValidatorRules as $key => $formItemRule) { + $rulesArrHtml = ''; + foreach ($formItemRule as $item) { + $rulesArrHtml .= $item . ', '; + } + $rulesHtml .= self::tab() . $key . ': [' . rtrim($rulesArrHtml, ', ') . "],\n"; + } + return $rulesHtml ? "\n" . $rulesHtml : ''; + } + + public static function writeIndexFile($indexVueData, $webViewsDir, $controllerFile): void + { + $indexVueData['optButtons'] = self::buildSimpleArray($indexVueData['optButtons']); + $indexVueData['defaultItems'] = self::getJsonFromArray($indexVueData['defaultItems']); + $indexVueData['tableColumn'] = self::buildTableColumn($indexVueData['tableColumn']); + $indexVueData['dblClickNotEditColumn'] = self::buildSimpleArray($indexVueData['dblClickNotEditColumn']); + $controllerFile['path'][] = $controllerFile['originalLastName']; + $indexVueData['controllerUrl'] = '\'/admin/' . ($controllerFile['path'] ? implode('.', $controllerFile['path']) : '') . '/\''; + $indexVueData['componentName'] = ($webViewsDir['path'] ? implode('/', $webViewsDir['path']) . '/' : '') . $webViewsDir['originalLastName']; + $indexVueContent = self::assembleStub('html/index', $indexVueData); + self::writeFile(root_path() . $webViewsDir['views'] . '/' . 'index.vue', $indexVueContent); + } + + public static function buildTableColumn($tableColumnList): string + { + $columnJson = ''; + foreach ($tableColumnList as $column) { + $columnJson .= self::tab(3) . '{'; + foreach ($column as $key => $item) { + $columnJson .= self::buildTableColumnKey($key, $item); + } + $columnJson = rtrim($columnJson, ','); + $columnJson .= ' }' . ",\n"; + } + return rtrim($columnJson, "\n"); + } + + public static function buildTableColumnKey($key, $item): string + { + $key = self::formatObjectKey($key); + if (is_array($item)) { + $itemJson = ' ' . $key . ': {'; + foreach ($item as $ik => $iItem) { + $itemJson .= self::buildTableColumnKey($ik, $iItem); + } + $itemJson = rtrim($itemJson, ','); + $itemJson .= ' }'; + } elseif ($item === 'false' || $item === 'true') { + $itemJson = ' ' . $key . ': ' . $item . ','; + } elseif (in_array($key, ['label', 'width', 'buttons'], true) || str_starts_with($item, "t('") || str_starts_with($item, "t(\"")) { + $itemJson = ' ' . $key . ': ' . $item . ','; + } else { + $itemJson = ' ' . $key . ': \'' . $item . '\','; + } + return $itemJson; + } + + public static function formatObjectKey(string $keyName): string + { + if (preg_match("/^[a-zA-Z_][a-zA-Z0-9_]+$/", $keyName)) { + return $keyName; + } else { + $quote = self::getQuote($keyName); + return "$quote$keyName$quote"; + } + } + + public static function getQuote(string $value): string + { + return stripos($value, "'") === false ? "'" : '"'; + } + + public static function buildFormatSimpleArray($arr, int $tab = 2): string + { + if (!$arr) return '[]'; + $str = '[' . PHP_EOL; + foreach ($arr as $item) { + if ($item == 'undefined' || $item == 'false' || is_numeric($item)) { + $str .= self::tab($tab) . $item . ',' . PHP_EOL; + } else { + $quote = self::getQuote($item); + $str .= self::tab($tab) . "$quote$item$quote," . PHP_EOL; + } + } + return $str . self::tab($tab - 1) . ']'; + } + + public static function buildSimpleArray($arr): string + { + if (!$arr) return '[]'; + $str = ''; + foreach ($arr as $item) { + if ($item == 'undefined' || $item == 'false' || is_numeric($item)) { + $str .= $item . ', '; + } else { + $quote = self::getQuote($item); + $str .= "$quote$item$quote, "; + } + } + return '[' . rtrim($str, ", ") . ']'; + } + + public static function buildDefaultOrder(string $field, string $type): string + { + if ($field && $type) { + $defaultOrderStub = [ + 'prop' => $field, + 'order' => $type, + ]; + $defaultOrderStub = self::getJsonFromArray($defaultOrderStub); + if ($defaultOrderStub) { + return "\n" . self::tab(2) . "defaultOrder: " . $defaultOrderStub . ','; + } + } + return ''; + } + + public static function getJsonFromArray($arr) + { + if (is_array($arr)) { + $jsonStr = ''; + foreach ($arr as $key => $item) { + $keyStr = ' ' . self::formatObjectKey($key) . ': '; + if (is_array($item)) { + $jsonStr .= $keyStr . self::getJsonFromArray($item) . ','; + } elseif ($item === 'false' || $item === 'true') { + $jsonStr .= $keyStr . ($item === 'false' ? 'false' : 'true') . ','; + } elseif ($item === null) { + $jsonStr .= $keyStr . 'null,'; + } elseif (str_starts_with($item, "t('") || str_starts_with($item, "t(\"") || $item == '[]' || in_array(gettype($item), ['integer', 'double'])) { + $jsonStr .= $keyStr . $item . ','; + } elseif (isset($item[0]) && $item[0] == '[' && str_ends_with($item, ']')) { + $jsonStr .= $keyStr . $item . ','; + } else { + $quote = self::getQuote($item); + $jsonStr .= $keyStr . "$quote$item$quote,"; + } + } + return $jsonStr ? '{' . rtrim($jsonStr, ',') . ' }' : '{}'; + } else { + return $arr; + } + } + +} \ No newline at end of file diff --git a/app/admin/library/crud/stubs/html/form.stub b/app/admin/library/crud/stubs/html/form.stub new file mode 100644 index 0000000..bbf7b8e --- /dev/null +++ b/app/admin/library/crud/stubs/html/form.stub @@ -0,0 +1,63 @@ + + + + + diff --git a/app/admin/library/crud/stubs/html/index.stub b/app/admin/library/crud/stubs/html/index.stub new file mode 100644 index 0000000..53ad5fe --- /dev/null +++ b/app/admin/library/crud/stubs/html/index.stub @@ -0,0 +1,69 @@ + + + + + diff --git a/app/admin/library/crud/stubs/mixins/controller/controller.stub b/app/admin/library/crud/stubs/mixins/controller/controller.stub new file mode 100644 index 0000000..7a5213f --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/controller/controller.stub @@ -0,0 +1,24 @@ +request->param('select')) { + $this->select(); + } + + /** + * 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则) + * 2. 以下的别名设置了主表别名,同时便于拼接查询参数等 + * 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理 + */ + list($where, $alias, $limit, $order) = $this->queryBuilder(); + $res = $this->model + ->withJoin($this->withJoinTable, $this->withJoinType) + ->alias($alias) + ->where($where) + ->order($order) + ->paginate($limit); + {%relationVisibleFields%} + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + 'remark' => get_route_remark(), + ]); + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/controller/initialize.stub b/app/admin/library/crud/stubs/mixins/controller/initialize.stub new file mode 100644 index 0000000..d8888f9 --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/controller/initialize.stub @@ -0,0 +1,6 @@ + + public function initialize(): void + { + parent::initialize(); + $this->model = new \{%modelNamespace%}\{%modelName%}();{%filterRule%} + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/afterInsert.stub b/app/admin/library/crud/stubs/mixins/model/afterInsert.stub new file mode 100644 index 0000000..58d0763 --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/afterInsert.stub @@ -0,0 +1,12 @@ + + protected static function onAfterInsert($model): void + { + if (is_null($model->{%field%})) { + $pk = $model->getPk(); + if (strlen($model[$pk]) >= 19) { + $model->where($pk, $model[$pk])->update(['{%field%}' => $model->count()]); + } else { + $model->where($pk, $model[$pk])->update(['{%field%}' => $model[$pk]]); + } + } + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/beforeInsert.stub b/app/admin/library/crud/stubs/mixins/model/beforeInsert.stub new file mode 100644 index 0000000..6eebf2d --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/beforeInsert.stub @@ -0,0 +1,5 @@ + + protected static function onBeforeInsert($model): void + { +{%setSnowFlakeIdCode%} + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/belongsTo.stub b/app/admin/library/crud/stubs/mixins/model/belongsTo.stub new file mode 100644 index 0000000..995118d --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/belongsTo.stub @@ -0,0 +1,5 @@ + + public function {%relationMethod%}(): \think\model\relation\BelongsTo + { + return $this->{%relationMode%}({%relationClassName%}, '{%relationForeignKey%}', '{%relationPrimaryKey%}'); + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/getters/cityNames.stub b/app/admin/library/crud/stubs/mixins/model/getters/cityNames.stub new file mode 100644 index 0000000..191d63c --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/getters/cityNames.stub @@ -0,0 +1,7 @@ + + public function get{%field%}Attr($value, $row): string + { + if ($row['{%originalFieldName%}'] === '' || $row['{%originalFieldName%}'] === null) return ''; + $cityNames = \think\facade\Db::name('area')->whereIn('id', $row['{%originalFieldName%}'])->column('name'); + return $cityNames ? implode(',', $cityNames) : ''; + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/getters/float.stub b/app/admin/library/crud/stubs/mixins/model/getters/float.stub new file mode 100644 index 0000000..482115c --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/getters/float.stub @@ -0,0 +1,5 @@ + + public function get{%field%}Attr($value): ?float + { + return is_null($value) ? null : (float)$value; + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/getters/htmlDecode.stub b/app/admin/library/crud/stubs/mixins/model/getters/htmlDecode.stub new file mode 100644 index 0000000..47ea946 --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/getters/htmlDecode.stub @@ -0,0 +1,5 @@ + + public function get{%field%}Attr($value): string + { + return !$value ? '' : htmlspecialchars_decode($value); + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/getters/jsonDecode.stub b/app/admin/library/crud/stubs/mixins/model/getters/jsonDecode.stub new file mode 100644 index 0000000..6ae96a7 --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/getters/jsonDecode.stub @@ -0,0 +1,5 @@ + + public function get{%field%}Attr($value): array + { + return !$value ? [] : json_decode($value, true); + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/getters/remoteSelectLabels.stub b/app/admin/library/crud/stubs/mixins/model/getters/remoteSelectLabels.stub new file mode 100644 index 0000000..2767dd8 --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/getters/remoteSelectLabels.stub @@ -0,0 +1,7 @@ + + public function get{%field%}Attr($value, $row): array + { + return [ + '{%labelFieldName%}' => {%className%}::whereIn('{%primaryKey%}', $row['{%foreignKey%}'])->column('{%labelFieldName%}'), + ]; + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/getters/string.stub b/app/admin/library/crud/stubs/mixins/model/getters/string.stub new file mode 100644 index 0000000..1622c4b --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/getters/string.stub @@ -0,0 +1,5 @@ + + public function get{%field%}Attr($value): string + { + return (string)$value; + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/getters/stringToArray.stub b/app/admin/library/crud/stubs/mixins/model/getters/stringToArray.stub new file mode 100644 index 0000000..43f7543 --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/getters/stringToArray.stub @@ -0,0 +1,9 @@ + + public function get{%field%}Attr($value): array + { + if ($value === '' || $value === null) return []; + if (!is_array($value)) { + return explode(',', $value); + } + return $value; + } \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/mixins/beforeInsertWithSnowflake.stub b/app/admin/library/crud/stubs/mixins/model/mixins/beforeInsertWithSnowflake.stub new file mode 100644 index 0000000..4904e08 --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/mixins/beforeInsertWithSnowflake.stub @@ -0,0 +1,2 @@ + $pk = $model->getPk(); + $model->$pk = \app\common\library\SnowFlake::generateParticle(); \ No newline at end of file diff --git a/app/admin/library/crud/stubs/mixins/model/model.stub b/app/admin/library/crud/stubs/mixins/model/model.stub new file mode 100644 index 0000000..cd77f11 --- /dev/null +++ b/app/admin/library/crud/stubs/mixins/model/model.stub @@ -0,0 +1,18 @@ + [], + 'edit' => [], + ]; + +} diff --git a/app/admin/library/module/Manage.php b/app/admin/library/module/Manage.php new file mode 100644 index 0000000..b5c362c --- /dev/null +++ b/app/admin/library/module/Manage.php @@ -0,0 +1,962 @@ +installDir = root_path() . 'modules' . DIRECTORY_SEPARATOR; + $this->backupsDir = $this->installDir . 'backups' . DIRECTORY_SEPARATOR; + if (!is_dir($this->installDir)) { + mkdir($this->installDir, 0755, true); + } + if (!is_dir($this->backupsDir)) { + mkdir($this->backupsDir, 0755, true); + } + + if ($uid) { + $this->uid = $uid; + $this->modulesDir = $this->installDir . $uid . DIRECTORY_SEPARATOR; + } + } + + public function getInstallState() + { + if (!is_dir($this->modulesDir)) { + return self::UNINSTALLED; + } + $info = $this->getInfo(); + if ($info && isset($info['state'])) { + return $info['state']; + } + + // 目录已存在,但非正常的模块 + return Filesystem::dirIsEmpty($this->modulesDir) ? self::UNINSTALLED : self::DIRECTORY_OCCUPIED; + } + + /** + * 下载模块文件 + * @param string $token 官网会员token + * @param int $orderId 订单号 + * @return string 下载文件路径 + * @throws Throwable + */ + public function download(string $token, int $orderId): string + { + if (!$orderId) { + throw new Exception('Order not found'); + } + // 下载 - 系统版本号要求、已安装模块的互斥和依赖检测 + $zipFile = Server::download($this->uid, $this->installDir, [ + 'sysVersion' => Config::get('buildadmin.version'), + 'nuxtVersion' => Server::getNuxtVersion(), + 'ba-user-token' => $token, + 'order_id' => $orderId, + 'installed' => Server::getInstalledIds($this->installDir), + ]); + + // 删除旧版本代码 + Filesystem::delDir($this->modulesDir); + + // 解压 + Filesystem::unzip($zipFile); + + // 删除下载的zip + @unlink($zipFile); + + // 检查是否完整 + $this->checkPackage(); + + // 设置为待安装状态 + $this->setInfo([ + 'state' => self::WAIT_INSTALL, + ]); + + return $zipFile; + } + + /** + * 上传安装 + * @param string $file 已经上传完成的文件 + * @return array 模块的基本信息 + * @throws Throwable + */ + public function upload(string $token, string $file): array + { + $file = Filesystem::fsFit(root_path() . 'public' . $file); + if (!is_file($file)) { + // 包未找到 + throw new Exception('Zip file not found'); + } + + $copyTo = $this->installDir . 'uploadTemp' . date('YmdHis') . '.zip'; + copy($file, $copyTo); + + // 解压 + $copyToDir = Filesystem::unzip($copyTo); + $copyToDir .= DIRECTORY_SEPARATOR; + + // 删除zip + @unlink($file); + @unlink($copyTo); + + // 读取ini + $info = Server::getIni($copyToDir); + if (empty($info['uid'])) { + Filesystem::delDir($copyToDir); + // 基本配置不完整 + throw new Exception('Basic configuration of the Module is incomplete'); + } + + // 安装预检 - 系统版本号要求、已安装模块的互斥和依赖检测 + try { + Server::installPreCheck([ + 'uid' => $info['uid'], + 'sysVersion' => Config::get('buildadmin.version'), + 'nuxtVersion' => Server::getNuxtVersion(), + 'moduleVersion' => $info['version'], + 'ba-user-token' => $token, + 'installed' => Server::getInstalledIds($this->installDir), + 'server' => 1, + ]); + } catch (Throwable $e) { + Filesystem::delDir($copyToDir); + throw $e; + } + + $this->uid = $info['uid']; + $this->modulesDir = $this->installDir . $info['uid'] . DIRECTORY_SEPARATOR; + + $upgrade = false; + if (is_dir($this->modulesDir)) { + $oldInfo = $this->getInfo(); + if ($oldInfo && !empty($oldInfo['uid'])) { + $versions = explode('.', $oldInfo['version']); + if (isset($versions[2])) { + $versions[2]++; + } + $nextVersion = implode('.', $versions); + $upgrade = Version::compare($nextVersion, $info['version']); + if (!$upgrade) { + Filesystem::delDir($copyToDir); + // 模块已经存在 + throw new Exception('Module already exists'); + } + } + + if (!Filesystem::dirIsEmpty($this->modulesDir) && !$upgrade) { + Filesystem::delDir($copyToDir); + // 模块目录被占 + throw new Exception('The directory required by the module is occupied'); + } + } + + $newInfo = ['state' => self::WAIT_INSTALL]; + if ($upgrade) { + $newInfo['update'] = 1; + + // 清理旧版本代码 + Filesystem::delDir($this->modulesDir); + } + + // 放置新模块 + rename($copyToDir, $this->modulesDir); + + // 检查新包是否完整 + $this->checkPackage(); + + // 设置为待安装状态 + $this->setInfo($newInfo); + + return $info; + } + + /** + * 更新 + * @throws Throwable + */ + public function update(string $token, int $orderId): void + { + $state = $this->getInstallState(); + if ($state != self::DISABLE) { + throw new Exception('Please disable the module before updating'); + } + + $this->download($token, $orderId); + + // 标记需要执行更新脚本,并在安装请求执行(当前请求未自动加载到新文件不方便执行) + $info = $this->getInfo(); + $info['update'] = 1; + $this->setInfo([], $info); + } + + /** + * 安装模块 + * @param string $token 用户token + * @param int $orderId 订单号 + * @return array 模块基本信息 + * @throws Throwable + */ + public function install(string $token, int $orderId): array + { + $state = $this->getInstallState(); + if ($state == self::INSTALLED || $state == self::DIRECTORY_OCCUPIED || $state == self::DISABLE) { + throw new Exception('Module already exists'); + } + + if ($state == self::UNINSTALLED) { + $this->download($token, $orderId); + } + + // 导入sql + Server::importSql($this->modulesDir); + + // 如果是更新,先执行更新脚本 + $info = $this->getInfo(); + if (isset($info['update']) && $info['update']) { + Server::execEvent($this->uid, 'update'); + unset($info['update']); + $this->setInfo([], $info); + } + + // 执行安装脚本 + Server::execEvent($this->uid, 'install'); + + // 启用插件 + $this->enable('install'); + + return $info; + } + + /** + * 卸载 + * @throws Throwable + */ + public function uninstall(): void + { + $info = $this->getInfo(); + if ($info['state'] != self::DISABLE) { + throw new Exception('Please disable the module first', 0, [ + 'uid' => $this->uid, + ]); + } + + // 执行卸载脚本 + Server::execEvent($this->uid, 'uninstall'); + + Filesystem::delDir($this->modulesDir); + } + + /** + * 修改模块状态 + * @param bool $state 新状态 + * @return array 模块基本信息 + * @throws Throwable + */ + public function changeState(bool $state): array + { + $info = $this->getInfo(); + if (!$state) { + $canDisable = [ + self::INSTALLED, + self::CONFLICT_PENDING, + self::DEPENDENT_WAIT_INSTALL, + ]; + if (!in_array($info['state'], $canDisable)) { + throw new Exception('The current state of the module cannot be set to disabled', 0, [ + 'uid' => $this->uid, + 'state' => $info['state'], + ]); + } + return $this->disable(); + } + + if ($info['state'] != self::DISABLE) { + throw new Exception('The current state of the module cannot be set to enabled', 0, [ + 'uid' => $this->uid, + 'state' => $info['state'], + ]); + } + $this->setInfo([ + 'state' => self::WAIT_INSTALL, + ]); + return $info; + } + + /** + * 启用 + * @param string $trigger 触发启用的标志,比如:install=安装 + * @throws Throwable + */ + public function enable(string $trigger): void + { + // 安装 WebBootstrap + Server::installWebBootstrap($this->uid, $this->modulesDir); + + // 建立 .runtime + Server::createRuntime($this->modulesDir); + + // 冲突检查 + $this->conflictHandle($trigger); + + // 执行启用脚本 + Server::execEvent($this->uid, 'enable'); + + $this->dependUpdateHandle(); + } + + /** + * 禁用 + * @return array 模块基本信息 + * @throws Throwable + */ + public function disable(): array + { + $update = request()->post("update/b", false); + $confirmConflict = request()->post("confirmConflict/b", false); + $dependConflictSolution = request()->post("dependConflictSolution/a", []); + + $info = $this->getInfo(); + $zipFile = $this->backupsDir . $this->uid . '-install.zip'; + $zipDir = false; + if (is_file($zipFile)) { + try { + $zipDir = $this->backupsDir . $this->uid . '-install' . DIRECTORY_SEPARATOR; + Filesystem::unzip($zipFile, $zipDir); + } catch (Exception) { + // skip + } + } + + $conflictFile = Server::getFileList($this->modulesDir, true); + $dependConflict = $this->disableDependCheck(); + if (($conflictFile || !self::isEmptyArray($dependConflict)) && !$confirmConflict) { + $dependConflictTemp = []; + foreach ($dependConflict as $env => $item) { + foreach ($item as $depend => $v) { + $dependConflictTemp[] = [ + 'env' => $env, + 'depend' => $depend, + 'dependTitle' => $depend . ' ' . $v, + 'solution' => 'delete', + ]; + } + } + throw new Exception('Module file updated', -1, [ + 'uid' => $this->uid, + 'conflictFile' => $conflictFile, + 'dependConflict' => $dependConflictTemp, + ]); + } + + // 执行禁用脚本 + Server::execEvent($this->uid, 'disable', ['update' => $update]); + + // 是否需要备份依赖? + $delNpmDepend = false; + $delNuxtNpmDepend = false; + $delComposerDepend = false; + foreach ($dependConflictSolution as $env => $depends) { + if (!$depends) continue; + if ($env == 'require' || $env == 'require-dev') { + $delComposerDepend = true; + } elseif ($env == 'dependencies' || $env == 'devDependencies') { + $delNpmDepend = true; + } elseif ($env == 'nuxtDependencies' || $env == 'nuxtDevDependencies') { + $delNuxtNpmDepend = true; + } + } + + // 备份 + $dependJsonFiles = [ + 'composer' => 'composer.json', + 'webPackage' => 'web' . DIRECTORY_SEPARATOR . 'package.json', + 'webNuxtPackage' => 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json', + ]; + $dependWaitInstall = []; + if ($delComposerDepend) { + $conflictFile[] = $dependJsonFiles['composer']; + $dependWaitInstall[] = [ + 'pm' => false, + 'command' => 'composer.update', + 'type' => 'composer_dependent_wait_install', + ]; + } + if ($delNpmDepend) { + $conflictFile[] = $dependJsonFiles['webPackage']; + $dependWaitInstall[] = [ + 'pm' => true, + 'command' => 'web-install', + 'type' => 'npm_dependent_wait_install', + ]; + } + if ($delNuxtNpmDepend) { + $conflictFile[] = $dependJsonFiles['webNuxtPackage']; + $dependWaitInstall[] = [ + 'pm' => true, + 'command' => 'nuxt-install', + 'type' => 'nuxt_npm_dependent_wait_install', + ]; + } + if ($conflictFile) { + // 如果是模块自带文件需要备份,加上模块目录前缀 + $overwriteDir = Server::getOverwriteDir(); + foreach ($conflictFile as $key => $item) { + $paths = explode(DIRECTORY_SEPARATOR, $item); + if (in_array($paths[0], $overwriteDir) || in_array($item, $dependJsonFiles)) { + $conflictFile[$key] = $item; + } else { + $conflictFile[$key] = Filesystem::fsFit(str_replace(root_path(), '', $this->modulesDir . $item)); + } + if (!is_file(root_path() . $conflictFile[$key])) { + unset($conflictFile[$key]); + } + } + $backupsZip = $this->backupsDir . $this->uid . '-disable-' . date('YmdHis') . '.zip'; + Filesystem::zip($conflictFile, $backupsZip); + } + + // 删除依赖 + $serverDepend = new Depends(root_path() . 'composer.json', 'composer'); + $webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json'); + $webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'); + foreach ($dependConflictSolution as $env => $depends) { + if (!$depends) continue; + $dev = !(stripos($env, 'dev') === false); + if ($env == 'require' || $env == 'require-dev') { + $serverDepend->removeDepends($depends, $dev); + } elseif ($env == 'dependencies' || $env == 'devDependencies') { + $webDep->removeDepends($depends, $dev); + } elseif ($env == 'nuxtDependencies' || $env == 'nuxtDevDependencies') { + $webNuxtDep->removeDepends($depends, $dev); + } + } + + // 删除 composer.json 中的 config + $composerConfig = Server::getConfig($this->modulesDir, 'composerConfig'); + if ($composerConfig) { + $serverDepend->removeComposerConfig($composerConfig); + } + + // 配置了不删除的文件 + $protectedFiles = Server::getConfig($this->modulesDir, 'protectedFiles'); + foreach ($protectedFiles as &$protectedFile) { + $protectedFile = Filesystem::fsFit(root_path() . $protectedFile); + } + // 模块文件列表 + $moduleFile = Server::getFileList($this->modulesDir); + + // 删除模块文件 + foreach ($moduleFile as &$file) { + // 纯净模式下,模块文件将被删除,此处直接检查模块目录中是否有该文件并恢复(不检查是否开启纯净模式,因为开关可能被调整) + $moduleFilePath = Filesystem::fsFit($this->modulesDir . $file); + $file = Filesystem::fsFit(root_path() . $file); + if (!file_exists($file)) continue; + if (!file_exists($moduleFilePath)) { + if (!is_dir(dirname($moduleFilePath))) { + mkdir(dirname($moduleFilePath), 0755, true); + } + copy($file, $moduleFilePath); + } + + if (in_array($file, $protectedFiles)) { + continue; + } + if (file_exists($file)) { + unlink($file); + } + Filesystem::delEmptyDir(dirname($file)); + } + + // 恢复备份文件 + if ($zipDir) { + $unrecoverableFiles = [ + Filesystem::fsFit(root_path() . 'composer.json'), + Filesystem::fsFit(root_path() . 'web/package.json'), + Filesystem::fsFit(root_path() . 'web-nuxt/package.json'), + ]; + foreach ( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($zipDir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ) as $item + ) { + $backupsFile = Filesystem::fsFit(root_path() . str_replace($zipDir, '', $item->getPathname())); + + // 在模块包中,同时不在 $protectedFiles 列表的文件不恢复,这些文件可能是模块升级时备份的 + if (in_array($backupsFile, $moduleFile) && !in_array($backupsFile, $protectedFiles)) { + continue; + } + + if ($item->isDir()) { + if (!is_dir($backupsFile)) { + mkdir($backupsFile, 0755, true); + } + } elseif (!in_array($backupsFile, $unrecoverableFiles)) { + copy($item, $backupsFile); + } + } + } + + // 删除解压后的备份文件 + Filesystem::delDir($zipDir); + + // 卸载 WebBootstrap + Server::uninstallWebBootstrap($this->uid); + + $this->setInfo([ + 'state' => self::DISABLE, + ]); + + if ($update) { + $token = request()->post("token/s", ''); + $order = request()->post("order/d", 0); + $this->update($token, $order); + throw new Exception('update', -3, [ + 'uid' => $this->uid, + ]); + } + + if ($dependWaitInstall) { + throw new Exception('dependent wait install', -2, [ + 'uid' => $this->uid, + 'wait_install' => $dependWaitInstall, + ]); + } + return $info; + } + + /** + * 处理依赖和文件冲突,并完成与前端的冲突处理交互 + * @throws Throwable + */ + public function conflictHandle(string $trigger): bool + { + $info = $this->getInfo(); + if ($info['state'] != self::WAIT_INSTALL && $info['state'] != self::CONFLICT_PENDING) { + return false; + } + $fileConflict = Server::getFileList($this->modulesDir, true);// 文件冲突 + $dependConflict = Server::dependConflictCheck($this->modulesDir);// 依赖冲突 + $installFiles = Server::getFileList($this->modulesDir);// 待安装文件 + $depends = Server::getDepend($this->modulesDir);// 待安装依赖 + + $coverFiles = [];// 要覆盖的文件-备份 + $discardFiles = [];// 抛弃的文件-复制时不覆盖 + $serverDep = new Depends(root_path() . 'composer.json', 'composer'); + $webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json'); + $webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'); + if ($fileConflict || !self::isEmptyArray($dependConflict)) { + $extend = request()->post('extend/a', []); + if (!$extend) { + // 发现冲突->手动处理->转换为方便前端使用的格式 + $fileConflictTemp = []; + foreach ($fileConflict as $key => $item) { + $fileConflictTemp[$key] = [ + 'newFile' => $this->uid . DIRECTORY_SEPARATOR . $item, + 'oldFile' => $item, + 'solution' => 'cover', + ]; + } + $dependConflictTemp = []; + foreach ($dependConflict as $env => $item) { + $dev = !(stripos($env, 'dev') === false); + foreach ($item as $depend => $v) { + $oldDepend = ''; + if (in_array($env, ['require', 'require-dev'])) { + $oldDepend = $depend . ' ' . $serverDep->hasDepend($depend, $dev); + } elseif (in_array($env, ['dependencies', 'devDependencies'])) { + $oldDepend = $depend . ' ' . $webDep->hasDepend($depend, $dev); + } elseif (in_array($env, ['nuxtDependencies', 'nuxtDevDependencies'])) { + $oldDepend = $depend . ' ' . $webNuxtDep->hasDepend($depend, $dev); + } + $dependConflictTemp[] = [ + 'env' => $env, + 'newDepend' => $depend . ' ' . $v, + 'oldDepend' => $oldDepend, + 'depend' => $depend, + 'solution' => 'cover', + ]; + } + } + $this->setInfo([ + 'state' => self::CONFLICT_PENDING, + ]); + throw new Exception('Module file conflicts', -1, [ + 'fileConflict' => $fileConflictTemp, + 'dependConflict' => $dependConflictTemp, + 'uid' => $this->uid, + 'state' => self::CONFLICT_PENDING, + ]); + } + + // 处理冲突 + if ($fileConflict && isset($extend['fileConflict'])) { + foreach ($installFiles as $ikey => $installFile) { + if (isset($extend['fileConflict'][$installFile])) { + if ($extend['fileConflict'][$installFile] == 'discard') { + $discardFiles[] = $installFile; + unset($installFiles[$ikey]); + } else { + $coverFiles[] = $installFile; + } + } + } + } + if (!self::isEmptyArray($dependConflict) && isset($extend['dependConflict'])) { + foreach ($depends as $fKey => $fItem) { + foreach ($fItem as $cKey => $cItem) { + if (isset($extend['dependConflict'][$fKey][$cKey])) { + if ($extend['dependConflict'][$fKey][$cKey] == 'discard') { + unset($depends[$fKey][$cKey]); + } + } + } + } + } + } + + // 如果有依赖更新,增加要备份的文件 + if ($depends) { + foreach ($depends as $key => $item) { + if (!$item) { + continue; + } + if ($key == 'require' || $key == 'require-dev') { + $coverFiles[] = 'composer.json'; + continue; + } + if ($key == 'dependencies' || $key == 'devDependencies') { + $coverFiles[] = 'web' . DIRECTORY_SEPARATOR . 'package.json'; + } + if ($key == 'nuxtDependencies' || $key == 'nuxtDevDependencies') { + $coverFiles[] = 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'; + } + } + } + + // 备份将被覆盖的文件 + if ($coverFiles) { + $backupsZip = $trigger == 'install' ? $this->backupsDir . $this->uid . '-install.zip' : $this->backupsDir . $this->uid . '-cover-' . date('YmdHis') . '.zip'; + Filesystem::zip($coverFiles, $backupsZip); + } + + if ($depends) { + $npm = false; + $composer = false; + $nuxtNpm = false; + + // composer config 更新 + $composerConfig = Server::getConfig($this->modulesDir, 'composerConfig'); + if ($composerConfig) { + $serverDep->setComposerConfig($composerConfig); + } + + foreach ($depends as $key => $item) { + if (!$item) { + continue; + } + if ($key == 'require') { + $composer = true; + $serverDep->addDepends($item, false, true); + } elseif ($key == 'require-dev') { + $composer = true; + $serverDep->addDepends($item, true, true); + } elseif ($key == 'dependencies') { + $npm = true; + $webDep->addDepends($item, false, true); + } elseif ($key == 'devDependencies') { + $npm = true; + $webDep->addDepends($item, true, true); + } elseif ($key == 'nuxtDependencies') { + $nuxtNpm = true; + $webNuxtDep->addDepends($item, false, true); + } elseif ($key == 'nuxtDevDependencies') { + $nuxtNpm = true; + $webNuxtDep->addDepends($item, true, true); + } + } + if ($npm) { + $info['npm_dependent_wait_install'] = 1; + $info['state'] = self::DEPENDENT_WAIT_INSTALL; + } + if ($composer) { + $info['composer_dependent_wait_install'] = 1; + $info['state'] = self::DEPENDENT_WAIT_INSTALL; + } + if ($nuxtNpm) { + $info['nuxt_npm_dependent_wait_install'] = 1; + $info['state'] = self::DEPENDENT_WAIT_INSTALL; + } + if ($info['state'] != self::DEPENDENT_WAIT_INSTALL) { + // 无冲突 + $this->setInfo([ + 'state' => self::INSTALLED, + ]); + } else { + $this->setInfo([], $info); + } + } else { + // 无冲突 + $this->setInfo([ + 'state' => self::INSTALLED, + ]); + } + + // 复制文件 + $overwriteDir = Server::getOverwriteDir(); + foreach ($overwriteDir as $dirItem) { + $baseDir = $this->modulesDir . $dirItem; + $destDir = root_path() . $dirItem; + if (!is_dir($baseDir)) { + continue; + } + foreach ( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($baseDir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ) as $item + ) { + $destDirItem = Filesystem::fsFit($destDir . DIRECTORY_SEPARATOR . str_replace($baseDir, '', $item->getPathname())); + if ($item->isDir()) { + Filesystem::mkdir($destDirItem); + } elseif (!in_array(str_replace(root_path(), '', $destDirItem), $discardFiles)) { + Filesystem::mkdir(dirname($destDirItem)); + copy($item, $destDirItem); + } + } + // 纯净模式 + if (Config::get('buildadmin.module_pure_install')) { + Filesystem::delDir($baseDir); + } + } + return true; + } + + /** + * 依赖升级处理 + * @throws Throwable + */ + public function dependUpdateHandle(): void + { + $info = $this->getInfo(); + if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) { + $waitInstall = []; + if (isset($info['composer_dependent_wait_install'])) { + $waitInstall[] = 'composer_dependent_wait_install'; + } + if (isset($info['npm_dependent_wait_install'])) { + $waitInstall[] = 'npm_dependent_wait_install'; + } + if (isset($info['nuxt_npm_dependent_wait_install'])) { + $waitInstall[] = 'nuxt_npm_dependent_wait_install'; + } + if ($waitInstall) { + throw new Exception('dependent wait install', -2, [ + 'uid' => $this->uid, + 'state' => self::DEPENDENT_WAIT_INSTALL, + 'wait_install' => $waitInstall, + ]); + } else { + $this->setInfo([ + 'state' => self::INSTALLED, + ]); + } + } + } + + /** + * 依赖安装完成标记 + * @throws Throwable + */ + public function dependentInstallComplete(string $type): void + { + $info = $this->getInfo(); + if ($info['state'] == self::DEPENDENT_WAIT_INSTALL) { + if ($type == 'npm') { + unset($info['npm_dependent_wait_install']); + } + if ($type == 'nuxt_npm') { + unset($info['nuxt_npm_dependent_wait_install']); + } + if ($type == 'composer') { + unset($info['composer_dependent_wait_install']); + } + if ($type == 'all') { + unset($info['npm_dependent_wait_install'], $info['composer_dependent_wait_install'], $info['nuxt_npm_dependent_wait_install']); + } + if (!isset($info['npm_dependent_wait_install']) && !isset($info['composer_dependent_wait_install']) && !isset($info['nuxt_npm_dependent_wait_install'])) { + $info['state'] = self::INSTALLED; + } + $this->setInfo([], $info); + } + } + + /** + * 禁用依赖检查 + * @throws Throwable + */ + public function disableDependCheck(): array + { + // 读取模块所有依赖 + $depend = Server::getDepend($this->modulesDir); + if (!$depend) { + return []; + } + + // 读取所有依赖中,系统上已经安装的依赖 + $serverDep = new Depends(root_path() . 'composer.json', 'composer'); + $webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json'); + $webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'); + foreach ($depend as $key => $depends) { + $dev = !(stripos($key, 'dev') === false); + if ($key == 'require' || $key == 'require-dev') { + foreach ($depends as $dependKey => $dependItem) { + if (!$serverDep->hasDepend($dependKey, $dev)) { + unset($depends[$dependKey]); + } + } + $depend[$key] = $depends; + } elseif ($key == 'dependencies' || $key == 'devDependencies') { + foreach ($depends as $dependKey => $dependItem) { + if (!$webDep->hasDepend($dependKey, $dev)) { + unset($depends[$dependKey]); + } + } + $depend[$key] = $depends; + } elseif ($key == 'nuxtDependencies' || $key == 'nuxtDevDependencies') { + foreach ($depends as $dependKey => $dependItem) { + if (!$webNuxtDep->hasDepend($dependKey, $dev)) { + unset($depends[$dependKey]); + } + } + $depend[$key] = $depends; + } + } + return $depend; + } + + /** + * 检查包是否完整 + * @throws Throwable + */ + public function checkPackage(): bool + { + if (!is_dir($this->modulesDir)) { + throw new Exception('Module package file does not exist'); + } + $info = $this->getInfo(); + $infoKeys = ['uid', 'title', 'intro', 'author', 'version', 'state']; + foreach ($infoKeys as $value) { + if (!array_key_exists($value, $info)) { + Filesystem::delDir($this->modulesDir); + throw new Exception('Basic configuration of the Module is incomplete'); + } + } + return true; + } + + /** + * 获取模块基本信息 + */ + public function getInfo(): array + { + return Server::getIni($this->modulesDir); + } + + /** + * 设置模块基本信息 + * @throws Throwable + */ + public function setInfo(array $kv = [], array $arr = []): bool + { + if ($kv) { + $info = $this->getInfo(); + foreach ($kv as $k => $v) { + $info[$k] = $v; + } + return Server::setIni($this->modulesDir, $info); + } elseif ($arr) { + return Server::setIni($this->modulesDir, $arr); + } + throw new Exception('Parameter error'); + } + + + /** + * 检查多维数组是否全部为空 + */ + public static function isEmptyArray($arr): bool + { + foreach ($arr as $item) { + if (is_array($item)) { + $empty = self::isEmptyArray($item); + if (!$empty) return false; + } elseif ($item) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/app/admin/library/module/Server.php b/app/admin/library/module/Server.php new file mode 100644 index 0000000..90e19c3 --- /dev/null +++ b/app/admin/library/module/Server.php @@ -0,0 +1,590 @@ +get(self::$apiBaseUrl . 'download', ['query' => array_merge(['uid' => $uid, 'server' => 1], $extend)]); + $body = $response->getBody(); + $content = $body->getContents(); + if ($content == '' || stripos($content, '系统发生错误') !== false) { + throw new Exception('package download failed', 0); + } + if (str_starts_with($content, '{')) { + $json = (array)json_decode($content, true); + throw new Exception($json['msg'], $json['code'], $json['data'] ?? []); + } + } catch (TransferException $e) { + throw new Exception('package download failed', 0, ['msg' => $e->getMessage()]); + } + + if ($write = fopen($tmpFile, 'w')) { + fwrite($write, $content); + fclose($write); + return $tmpFile; + } + throw new Exception("No permission to write temporary files"); + } + + /** + * 安装预检 + * @throws Throwable + */ + public static function installPreCheck(array $query = []): bool + { + try { + $client = get_ba_client(); + $response = $client->get(self::$apiBaseUrl . 'preCheck', ['query' => $query]); + $body = $response->getBody(); + $statusCode = $response->getStatusCode(); + $content = $body->getContents(); + if ($content == '' || stripos($content, '系统发生错误') !== false || $statusCode != 200) { + return true; + } + if (str_starts_with($content, '{')) { + $json = json_decode($content, true); + if ($json && $json['code'] == 0) { + throw new Exception($json['msg'], $json['code'], $json['data'] ?? []); + } + } + } catch (TransferException $e) { + throw new Exception('package check failed', 0, ['msg' => $e->getMessage()]); + } + return true; + } + + public static function getConfig(string $dir, $key = ''): array + { + $configFile = $dir . 'config.json'; + if (!is_dir($dir) || !is_file($configFile)) { + return []; + } + $configContent = @file_get_contents($configFile); + $configContent = json_decode($configContent, true); + if (!$configContent) { + return []; + } + if ($key) { + return $configContent[$key] ?? []; + } + return $configContent; + } + + public static function getDepend(string $dir, string $key = ''): array + { + if ($key) { + return self::getConfig($dir, $key); + } + $configContent = self::getConfig($dir); + $dependKey = ['require', 'require-dev', 'dependencies', 'devDependencies', 'nuxtDependencies', 'nuxtDevDependencies']; + $dependArray = []; + foreach ($dependKey as $item) { + if (array_key_exists($item, $configContent) && $configContent[$item]) { + $dependArray[$item] = $configContent[$item]; + } + } + return $dependArray; + } + + /** + * 依赖冲突检查 + * @throws Throwable + */ + public static function dependConflictCheck(string $dir): array + { + $depend = self::getDepend($dir); + $serverDep = new Depends(root_path() . 'composer.json', 'composer'); + $webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json'); + $webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json'); + $sysDepend = [ + 'require' => $serverDep->getDepends(), + 'require-dev' => $serverDep->getDepends(true), + 'dependencies' => $webDep->getDepends(), + 'devDependencies' => $webDep->getDepends(true), + 'nuxtDependencies' => $webNuxtDep->getDepends(), + 'nuxtDevDependencies' => $webNuxtDep->getDepends(true), + ]; + + $conflict = []; + foreach ($depend as $key => $item) { + $conflict[$key] = array_uintersect_assoc($item, $sysDepend[$key], function ($a, $b) { + return $a == $b ? -1 : 0; + }); + } + return $conflict; + } + + /** + * 获取模块[冲突]文件列表 + * @param string $dir 模块目录 + * @param bool $onlyConflict 是否只获取冲突文件 + */ + public static function getFileList(string $dir, bool $onlyConflict = false): array + { + if (!is_dir($dir)) { + return []; + } + + $fileList = []; + $overwriteDir = self::getOverwriteDir(); + $moduleFileList = self::getRuntime($dir, 'files'); + + if ($moduleFileList) { + // 有冲突的文件 + if ($onlyConflict) { + // 排除的文件 + $excludeFile = [ + 'info.ini' + ]; + foreach ($moduleFileList as $file) { + // 如果是要安装到项目的文件,从项目根目录开始,如果不是,从模块根目录开始 + $path = Filesystem::fsFit(str_replace($dir, '', $file['path'])); + $paths = explode(DIRECTORY_SEPARATOR, $path); + $overwriteFile = in_array($paths[0], $overwriteDir) ? root_path() . $path : $dir . $path; + if (is_file($overwriteFile) && !in_array($path, $excludeFile) && (filesize($overwriteFile) != $file['size'] || md5_file($overwriteFile) != $file['md5'])) { + $fileList[] = $path; + } + } + } else { + // 要安装的文件 + foreach ($overwriteDir as $item) { + $baseDir = $dir . $item; + foreach ($moduleFileList as $file) { + if (!str_starts_with($file['path'], $baseDir)) continue; + $fileList[] = Filesystem::fsFit(str_replace($dir, '', $file['path'])); + } + } + } + return $fileList; + } + + foreach ($overwriteDir as $item) { + $baseDir = $dir . $item; + if (!is_dir($baseDir)) { + continue; + } + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($baseDir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($files as $file) { + if ($file->isFile()) { + $filePath = $file->getPathName(); + $path = str_replace($dir, '', $filePath); + $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); + + if ($onlyConflict) { + $overwriteFile = root_path() . $path; + if (is_file($overwriteFile) && (filesize($overwriteFile) != filesize($filePath) || md5_file($overwriteFile) != md5_file($filePath))) { + $fileList[] = $path; + } + } else { + $fileList[] = $path; + } + } + } + } + return $fileList; + } + + public static function getOverwriteDir(): array + { + return [ + 'app', + 'config', + 'database', + 'extend', + 'modules', + 'public', + 'vendor', + 'web', + 'web-nuxt', + ]; + } + + public static function importSql(string $dir): bool + { + $sqlFile = $dir . 'install.sql'; + $tempLine = ''; + if (is_file($sqlFile)) { + $lines = file($sqlFile); + foreach ($lines as $line) { + if (str_starts_with($line, '--') || $line == '' || str_starts_with($line, '/*')) { + continue; + } + + $tempLine .= $line; + if (str_ends_with(trim($line), ';')) { + $tempLine = str_ireplace('__PREFIX__', Config::get('database.connections.mysql.prefix'), $tempLine); + $tempLine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $tempLine); + try { + Db::execute($tempLine); + } catch (PDOException) { + // $e->getMessage(); + } + $tempLine = ''; + } + } + } + return true; + } + + public static function installedList(string $dir): array + { + if (!is_dir($dir)) { + return []; + } + $installedDir = scandir($dir); + $installedList = []; + foreach ($installedDir as $item) { + if ($item === '.' or $item === '..' || is_file($dir . $item)) { + continue; + } + $tempDir = $dir . $item . DIRECTORY_SEPARATOR; + if (!is_dir($tempDir)) { + continue; + } + $info = self::getIni($tempDir); + if (!isset($info['uid'])) { + continue; + } + $installedList[] = $info; + } + return $installedList; + } + + public static function getInstalledIds(string $dir): array + { + $installedIds = []; + $installed = self::installedList($dir); + foreach ($installed as $item) { + $installedIds[] = $item['uid']; + } + return $installedIds; + } + + /** + * 获取模块ini + * @param string $dir 模块目录路径 + */ + public static function getIni(string $dir): array + { + $infoFile = $dir . 'info.ini'; + $info = []; + if (is_file($infoFile)) { + $info = parse_ini_file($infoFile, true, INI_SCANNER_TYPED) ?: []; + if (!$info) return []; + } + return $info; + } + + /** + * 设置模块ini + * @param string $dir 模块目录路径 + * @param array $arr 新的ini数据 + * @return bool + * @throws Throwable + */ + public static function setIni(string $dir, array $arr): bool + { + $infoFile = $dir . 'info.ini'; + $ini = []; + foreach ($arr as $key => $val) { + if (is_array($val)) { + $ini[] = "[$key]"; + foreach ($val as $ikey => $ival) { + $ini[] = "$ikey = $ival"; + } + } else { + $ini[] = "$key = $val"; + } + } + if (!file_put_contents($infoFile, implode("\n", $ini) . "\n", LOCK_EX)) { + throw new Exception("Configuration file has no write permission"); + } + return true; + } + + public static function getClass(string $uid, string $type = 'event', string $class = null): string + { + $name = parse_name($uid); + if (!is_null($class) && strpos($class, '.')) { + $class = explode('.', $class); + $class[count($class) - 1] = parse_name(end($class), 1); + $class = implode('\\', $class); + } else { + $class = parse_name(is_null($class) ? $name : $class, 1); + } + $namespace = match ($type) { + 'controller' => '\\modules\\' . $name . '\\controller\\' . $class, + default => '\\modules\\' . $name . '\\' . $class, + }; + return class_exists($namespace) ? $namespace : ''; + } + + public static function execEvent(string $uid, string $event, array $params = []): void + { + $eventClass = self::getClass($uid); + if (class_exists($eventClass)) { + $handle = new $eventClass(); + if (method_exists($eventClass, $event)) { + $handle->$event($params); + } + } + } + + /** + * 分析 WebBootstrap 代码 + */ + public static function analysisWebBootstrap(string $uid, string $dir): array + { + $bootstrapFile = $dir . 'webBootstrap.stub'; + if (!file_exists($bootstrapFile)) return []; + $bootstrapContent = file_get_contents($bootstrapFile); + $pregArr = [ + 'mainTsImport' => '/#main.ts import code start#([\s\S]*?)#main.ts import code end#/i', + 'mainTsStart' => '/#main.ts start code start#([\s\S]*?)#main.ts start code end#/i', + 'appVueImport' => '/#App.vue import code start#([\s\S]*?)#App.vue import code end#/i', + 'appVueOnMounted' => '/#App.vue onMounted code start#([\s\S]*?)#App.vue onMounted code end#/i', + 'nuxtAppVueImport' => '/#web-nuxt\/app.vue import code start#([\s\S]*?)#web-nuxt\/app.vue import code end#/i', + 'nuxtAppVueStart' => '/#web-nuxt\/app.vue start code start#([\s\S]*?)#web-nuxt\/app.vue start code end#/i', + ]; + $codeStrArr = []; + foreach ($pregArr as $key => $item) { + preg_match($item, $bootstrapContent, $matches); + if (isset($matches[1]) && $matches[1]) { + $mainImportCodeArr = array_filter(preg_split('/\r\n|\r|\n/', $matches[1])); + if ($mainImportCodeArr) { + $codeStrArr[$key] = "\n"; + if (count($mainImportCodeArr) == 1) { + foreach ($mainImportCodeArr as $codeItem) { + $codeStrArr[$key] .= $codeItem . self::buildMarkStr('module-line-mark', $uid, $key); + } + } else { + $codeStrArr[$key] .= self::buildMarkStr('module-multi-line-mark-start', $uid, $key); + foreach ($mainImportCodeArr as $codeItem) { + $codeStrArr[$key] .= $codeItem . "\n"; + } + $codeStrArr[$key] .= self::buildMarkStr('module-multi-line-mark-end', $uid, $key); + } + } + } + unset($matches); + } + + return $codeStrArr; + } + + /** + * 安装 WebBootstrap + */ + public static function installWebBootstrap(string $uid, string $dir): void + { + $bootstrapCode = self::analysisWebBootstrap($uid, $dir); + if (!$bootstrapCode) { + return; + } + + $webPath = root_path() . 'web' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR; + $webNuxtPath = root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR; + $filePaths = [ + 'mainTsImport' => $webPath . 'main.ts', + 'mainTsStart' => $webPath . 'main.ts', + 'appVueImport' => $webPath . 'App.vue', + 'appVueOnMounted' => $webPath . 'App.vue', + 'nuxtAppVueImport' => $webNuxtPath . 'app.vue', + 'nuxtAppVueStart' => $webNuxtPath . 'app.vue', + ]; + + $marks = [ + 'mainTsImport' => self::buildMarkStr('import-root-mark'), + 'mainTsStart' => self::buildMarkStr('start-root-mark'), + 'appVueImport' => self::buildMarkStr('import-root-mark'), + 'appVueOnMounted' => self::buildMarkStr('onMounted-root-mark'), + 'nuxtAppVueImport' => self::buildMarkStr('import-root-mark'), + 'nuxtAppVueStart' => self::buildMarkStr('start-root-mark'), + ]; + + foreach ($bootstrapCode as $key => $item) { + if ($item && isset($marks[$key])) { + $content = file_get_contents($filePaths[$key]); + $markPos = stripos($content, $marks[$key]); + if ($markPos && strripos($content, self::buildMarkStr('module-line-mark', $uid, $key)) === false && strripos($content, self::buildMarkStr('module-multi-line-mark-start', $uid, $key)) === false) { + $content = substr_replace($content, $item, $markPos + strlen($marks[$key]), 0); + file_put_contents($filePaths[$key], $content); + } + } + } + } + + /** + * 卸载 WebBootstrap + */ + public static function uninstallWebBootstrap(string $uid): void + { + $webPath = root_path() . 'web' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR; + $webNuxtPath = root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR; + $filePaths = [ + 'mainTsImport' => $webPath . 'main.ts', + 'mainTsStart' => $webPath . 'main.ts', + 'appVueImport' => $webPath . 'App.vue', + 'appVueOnMounted' => $webPath . 'App.vue', + 'nuxtAppVueImport' => $webNuxtPath . 'app.vue', + 'nuxtAppVueStart' => $webNuxtPath . 'app.vue', + ]; + + $marksKey = [ + 'mainTsImport', + 'mainTsStart', + 'appVueImport', + 'appVueOnMounted', + 'nuxtAppVueImport', + 'nuxtAppVueStart', + ]; + + foreach ($marksKey as $item) { + if (!is_file($filePaths[$item])) { + continue; + } + $content = file_get_contents($filePaths[$item]); + $moduleLineMark = self::buildMarkStr('module-line-mark', $uid, $item); + $moduleMultiLineMarkStart = self::buildMarkStr('module-multi-line-mark-start', $uid, $item); + $moduleMultiLineMarkEnd = self::buildMarkStr('module-multi-line-mark-end', $uid, $item); + + // 寻找标记,找到则将其中内容删除 + $moduleLineMarkPos = strripos($content, $moduleLineMark); + if ($moduleLineMarkPos !== false) { + $delStartTemp = explode($moduleLineMark, $content); + $delStartPos = strripos(rtrim($delStartTemp[0], "\n"), "\n"); + $delEndPos = stripos($content, "\n", $moduleLineMarkPos); + $content = substr_replace($content, '', $delStartPos, $delEndPos - $delStartPos); + } + + $moduleMultiLineMarkStartPos = stripos($content, $moduleMultiLineMarkStart); + if ($moduleMultiLineMarkStartPos !== false) { + $moduleMultiLineMarkStartPos--; + $moduleMultiLineMarkEndPos = stripos($content, $moduleMultiLineMarkEnd); + $delLang = ($moduleMultiLineMarkEndPos + strlen($moduleMultiLineMarkEnd)) - $moduleMultiLineMarkStartPos; + $content = substr_replace($content, '', $moduleMultiLineMarkStartPos, $delLang); + } + + if ($moduleLineMarkPos || $moduleMultiLineMarkStartPos) { + file_put_contents($filePaths[$item], $content); + } + } + } + + /** + * 构建 WebBootstrap 需要的各种标记字符串 + * @param string $type + * @param string $uid 模块UID + * @param string $extend 扩展数据 + * @return string + */ + public static function buildMarkStr(string $type, string $uid = '', string $extend = ''): string + { + $nonTabKeys = ['mti', 'avi', 'navi', 'navs']; + $extend = match ($extend) { + 'mainTsImport' => 'mti', + 'mainTsStart' => 'mts', + 'appVueImport' => 'avi', + 'appVueOnMounted' => 'avo', + 'nuxtAppVueImport' => 'navi', + 'nuxtAppVueStart' => 'navs', + default => '', + }; + return match ($type) { + 'import-root-mark' => '// modules import mark, Please do not remove.', + 'start-root-mark' => '// modules start mark, Please do not remove.', + 'onMounted-root-mark' => '// Modules onMounted mark, Please do not remove.', + 'module-line-mark' => ' // Code from module \'' . $uid . "'" . ($extend ? "($extend)" : ''), + 'module-multi-line-mark-start' => (in_array($extend, $nonTabKeys) ? '' : Helper::tab()) . "// Code from module '$uid' start" . ($extend ? "($extend)" : '') . "\n", + 'module-multi-line-mark-end' => (in_array($extend, $nonTabKeys) ? '' : Helper::tab()) . "// Code from module '$uid' end", + default => '', + }; + } + + public static function getNuxtVersion() + { + $nuxtPackageJsonPath = Filesystem::fsFit(root_path() . 'web-nuxt/package.json'); + if (is_file($nuxtPackageJsonPath)) { + $nuxtPackageJson = file_get_contents($nuxtPackageJsonPath); + $nuxtPackageJson = json_decode($nuxtPackageJson, true); + if ($nuxtPackageJson && isset($nuxtPackageJson['version'])) { + return $nuxtPackageJson['version']; + } + } + return false; + } + + /** + * 创建 .runtime + */ + public static function createRuntime(string $dir): void + { + $runtimeFilePath = $dir . '.runtime'; + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY + ); + $filePaths = []; + foreach ($files as $file) { + if (!$file->isDir()) { + $pathName = $file->getPathName(); + if ($pathName == $runtimeFilePath) continue; + $filePaths[] = [ + 'path' => Filesystem::fsFit($pathName), + 'size' => filesize($pathName), + 'md5' => md5_file($pathName), + ]; + } + } + + file_put_contents($runtimeFilePath, json_encode([ + 'files' => $filePaths, + 'pure' => Config::get('buildadmin.module_pure_install'), + ])); + } + + /** + * 读取 .runtime + */ + public static function getRuntime(string $dir, string $key = ''): mixed + { + $runtimeFilePath = $dir . '.runtime'; + $runtimeContent = @file_get_contents($runtimeFilePath); + $runtimeContentArr = json_decode($runtimeContent, true); + if (!$runtimeContentArr) return []; + + if ($key) { + return $runtimeContentArr[$key] ?? []; + } else { + return $runtimeContentArr; + } + } +} \ No newline at end of file diff --git a/app/admin/library/stubs/backendEntrance.stub b/app/admin/library/stubs/backendEntrance.stub new file mode 100644 index 0000000..7b4b74a --- /dev/null +++ b/app/admin/library/stubs/backendEntrance.stub @@ -0,0 +1,24 @@ + +// +---------------------------------------------------------------------- + +// [ 应用入口文件 ] +namespace think; + +require __DIR__ . '/../vendor/autoload.php'; + +// 执行HTTP应用并响应 +$http = (new App())->http; + +$response = $http->name('admin')->run(); + +$response->send(); + +$http->end($response); diff --git a/app/admin/library/traits/Backend.php b/app/admin/library/traits/Backend.php new file mode 100644 index 0000000..06c6fbb --- /dev/null +++ b/app/admin/library/traits/Backend.php @@ -0,0 +1,301 @@ +preExcludeFields)) { + $this->preExcludeFields = explode(',', (string)$this->preExcludeFields); + } + + foreach ($this->preExcludeFields as $field) { + if (array_key_exists($field, $params)) { + unset($params[$field]); + } + } + return $params; + } + + /** + * 查看 + * @throws Throwable + */ + public function index(): void + { + if ($this->request->param('select')) { + $this->select(); + } + + list($where, $alias, $limit, $order) = $this->queryBuilder(); + $res = $this->model + ->field($this->indexField) + ->withJoin($this->withJoinTable, $this->withJoinType) + ->alias($alias) + ->where($where) + ->order($order) + ->paginate($limit); + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + 'remark' => get_route_remark(), + ]); + } + + /** + * 添加 + */ + public function add(): void + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + if ($this->dataLimit && $this->dataLimitFieldAutoFill) { + $data[$this->dataLimitField] = $this->auth->id; + } + + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('add'); + $validate->check($data); + } + } + $result = $this->model->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Added successfully')); + } else { + $this->error(__('No rows were added')); + } + } + + $this->error(__('Parameter error')); + } + + /** + * 编辑 + * @throws Throwable + */ + public function edit(): void + { + $pk = $this->model->getPk(); + $id = $this->request->param($pk); + $row = $this->model->find($id); + if (!$row) { + $this->error(__('Record not found')); + } + + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) { + $this->error(__('You have no permission')); + } + + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!$data) { + $this->error(__('Parameter %s can not be empty', [''])); + } + + $data = $this->excludeFields($data); + $result = false; + $this->model->startTrans(); + try { + // 模型验证 + if ($this->modelValidate) { + $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); + if (class_exists($validate)) { + $validate = new $validate(); + if ($this->modelSceneValidate) $validate->scene('edit'); + $data[$pk] = $row[$pk]; + $validate->check($data); + } + } + $result = $row->save($data); + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($result !== false) { + $this->success(__('Update successful')); + } else { + $this->error(__('No rows updated')); + } + } + + $this->success('', [ + 'row' => $row + ]); + } + + /** + * 删除 + * @throws Throwable + */ + public function del(): void + { + $where = []; + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds) { + $where[] = [$this->dataLimitField, 'in', $dataLimitAdminIds]; + } + + $ids = $this->request->param('ids/a', []); + $where[] = [$this->model->getPk(), 'in', $ids]; + $data = $this->model->where($where)->select(); + + $count = 0; + $this->model->startTrans(); + try { + foreach ($data as $v) { + $count += $v->delete(); + } + $this->model->commit(); + } catch (Throwable $e) { + $this->model->rollback(); + $this->error($e->getMessage()); + } + if ($count) { + $this->success(__('Deleted successfully')); + } else { + $this->error(__('No rows were deleted')); + } + } + + /** + * 排序 - 增量重排法 + * @throws Throwable + */ + public function sortable(): void + { + $pk = $this->model->getPk(); + $move = $this->request->param('move'); + $target = $this->request->param('target'); + $order = $this->request->param("order/s") ?: $this->defaultSortField; + $direction = $this->request->param('direction'); + + $dataLimitWhere = []; + $dataLimitAdminIds = $this->getDataLimitAdminIds(); + if ($dataLimitAdminIds) { + $dataLimitWhere[] = [$this->dataLimitField, 'in', $dataLimitAdminIds]; + } + + $moveRow = $this->model->where($dataLimitWhere)->find($move); + $targetRow = $this->model->where($dataLimitWhere)->find($target); + + if ($move == $target || !$moveRow || !$targetRow || !$direction) { + $this->error(__('Record not found')); + } + + // 当前是否以权重字段排序(只检查当前排序和默认排序字段,不检查有序保证字段) + if ($order && is_string($order)) { + $order = explode(',', $order); + $order = [$order[0] => $order[1] ?? 'asc']; + } + if (!array_key_exists($this->weighField, $order)) { + $this->error(__('Please use the %s field to sort before operating', [$this->weighField])); + } + + // 开始增量重排 + $order = $this->queryOrderBuilder(); + $weigh = $targetRow[$this->weighField]; + + // 波及行的权重值向上增加还是向下减少 + if ($order[$this->weighField] == 'desc') { + $updateMethod = $direction == 'up' ? 'dec' : 'inc'; + } else { + $updateMethod = $direction == 'up' ? 'inc' : 'dec'; + } + + // 与目标行权重相同的行 + $weighRowIds = $this->model + ->where($dataLimitWhere) + ->where($this->weighField, $weigh) + ->order($order) + ->column($pk); + $weighRowsCount = count($weighRowIds); + + // 单个 SQL 查询中完成大于目标权重行的修改 + $this->model->where($dataLimitWhere) + ->where($this->weighField, $updateMethod == 'dec' ? '<' : '>', $weigh) + ->whereNotIn($pk, [$moveRow->$pk]) + ->$updateMethod($this->weighField, $weighRowsCount) + ->save(); + + // 遍历与目标行权重相同的行,每出现一行权重值将额外 +1,保证权重相同行的顺序位置不变 + if ($direction == 'down') { + $weighRowIds = array_reverse($weighRowIds); + } + + $moveComplete = 0; + $weighRowIds = implode(',', $weighRowIds); + $weighRows = $this->model->where($dataLimitWhere) + ->where($pk, 'in', $weighRowIds) + ->orderRaw("field($pk,$weighRowIds)") + ->select(); + + // 权重相等行 + foreach ($weighRows as $key => $weighRow) { + // 跳过当前拖动行(相等权重数据之间的拖动时,被拖动行会出现在 $weighRows 内) + if ($moveRow[$pk] == $weighRow[$pk]) { + continue; + } + + if ($updateMethod == 'dec') { + $rowWeighVal = $weighRow[$this->weighField] - $key; + } else { + $rowWeighVal = $weighRow[$this->weighField] + $key; + } + + // 找到了目标行 + if ($weighRow[$pk] == $targetRow[$pk]) { + $moveComplete = 1; + $moveRow[$this->weighField] = $rowWeighVal; + $moveRow->save(); + } + + $rowWeighVal = $updateMethod == 'dec' ? $rowWeighVal - $moveComplete : $rowWeighVal + $moveComplete; + $weighRow[$this->weighField] = $rowWeighVal; + $weighRow->save(); + } + + $this->success(); + } + + /** + * 加载为select(远程下拉选择框)数据,默认还是走$this->index()方法 + * 必要时请在对应控制器类中重写 + */ + public function select(): void + { + + } +} \ No newline at end of file diff --git a/app/admin/middleware.php b/app/admin/middleware.php new file mode 100644 index 0000000..b89787e --- /dev/null +++ b/app/admin/middleware.php @@ -0,0 +1,6 @@ +where('uid', $row['id']) + ->column('group_id'); + } + + public function getGroupNameArrAttr($value, $row): array + { + $groupAccess = Db::name('admin_group_access') + ->where('uid', $row['id']) + ->column('group_id'); + return AdminGroup::whereIn('id', $groupAccess)->column('name'); + } + + public function getAvatarAttr($value): string + { + return full_url($value, false, config('buildadmin.default_avatar')); + } + + public function setAvatarAttr($value): string + { + return $value == full_url('', false, config('buildadmin.default_avatar')) ? '' : $value; + } + + public function getLastLoginTimeAttr($value): string + { + return $value ? date('Y-m-d H:i:s', $value) : ''; + } + + /** + * 重置用户密码 + * @param int|string $uid 管理员ID + * @param string $newPassword 新密码 + * @return int|Admin + */ + public function resetPassword(int|string $uid, string $newPassword): int|Admin + { + return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']); + } +} \ No newline at end of file diff --git a/app/admin/model/AdminGroup.php b/app/admin/model/AdminGroup.php new file mode 100644 index 0000000..15818b8 --- /dev/null +++ b/app/admin/model/AdminGroup.php @@ -0,0 +1,13 @@ +adminLog)) { + $request->adminLog = new static(); + } + return $request->adminLog; + } + + /** + * 设置标题 + * @param string $title + */ + public function setTitle(string $title): void + { + $this->title = $title; + } + + /** + * 设置日志内容 + * @param string|array $data + */ + public function setData(string|array $data): void + { + $this->data = $data; + } + + /** + * 设置忽略的链接正则列表 + * @param array|string $regex + */ + public function setUrlIgnoreRegex(array|string $regex = []): void + { + $regex = is_array($regex) ? $regex : [$regex]; + $this->urlIgnoreRegex = array_merge($this->urlIgnoreRegex, $regex); + } + + /** + * 设置需要进行数据脱敏的正则列表 + * @param array|string $regex + */ + public function setDesensitizationRegex(array|string $regex = []): void + { + $regex = is_array($regex) ? $regex : [$regex]; + $this->desensitizationRegex = array_merge($this->desensitizationRegex, $regex); + } + + /** + * 数据脱敏(只数组,根据数组 key 脱敏) + * @param array|string $data + * @return array|string + */ + protected function desensitization(array|string $data): array|string + { + if (!is_array($data) || !$this->desensitizationRegex) { + return $data; + } + foreach ($data as $index => &$item) { + foreach ($this->desensitizationRegex as $reg) { + if (preg_match($reg, $index)) { + $item = "***"; + } elseif (is_array($item)) { + $item = $this->desensitization($item); + } + } + } + return $data; + } + + /** + * 写入日志 + * @param string $title + * @param string|array|null $data + * @throws Throwable + */ + public function record(string $title = '', string|array $data = null): void + { + $auth = Auth::instance(); + $adminId = $auth->isLogin() ? $auth->user_id : 0; + $username = $auth->isLogin() ? $auth->username : request()->param('username', __('Unknown')); + + $controller = str_replace('.', '/', request()->controller(true)); + $action = request()->action(true); + $path = $controller . '/' . $action; + if ($this->urlIgnoreRegex) { + foreach ($this->urlIgnoreRegex as $item) { + if (preg_match($item, $path)) { + return; + } + } + } + $data = $data ?: $this->data; + if (!$data) { + $data = request()->param('', null, 'trim,strip_tags,htmlspecialchars'); + } + $data = $this->desensitization($data); + $title = $title ?: $this->title; + if (!$title) { + + $slave_db = Db::connect(config('database.search_library')); + $admin_rule = $slave_db->name('sys_menu'); + $controllerTitle = $admin_rule->where('url', $controller)->value('name'); + $title = $admin_rule->where('url', $path)->value('name'); + $title = $title ?: __('Unknown') . '(' . $action . ')'; + $title = $controllerTitle ? ($controllerTitle . '-' . $title) : $title; + } + + // 主库写入 + $master_db = Db::connect(config('database.z_library')); + $master_db->name('sys_log')->insert([ + 'username' => $username, + 'method' => substr(request()->url(), 0, 1500), + 'operation' => $title, + 'params' => !is_scalar($data) ? json_encode($data) : $data, + 'ip' => request()->ip(), + 'time' => 0, +// 'useragent' => substr(request()->server('HTTP_USER_AGENT'), 0, 255), + ]); + } + + public function admin(): BelongsTo + { + return $this->belongsTo(Admin::class); + } +} \ No newline at end of file diff --git a/app/admin/model/AdminRule.php b/app/admin/model/AdminRule.php new file mode 100644 index 0000000..62c9fc4 --- /dev/null +++ b/app/admin/model/AdminRule.php @@ -0,0 +1,21 @@ +where(['state' => 1]); + if(!empty($flag)) { + if($flag == 1) { + $query->whereTime('create_time', '>=', strtotime(date('Y-m-d 00:00:00', strtotime($time)))); + $query->whereTime('create_time', '<=', strtotime(date('Y-m-d 23:59:59', strtotime($time)))); + } + if($flag == 2) { + $query->whereTime('create_time', '>=', strtotime(date('Y-m-01 00:00:00', strtotime($time)))); + $query->whereTime('create_time', '<=', strtotime(date('Y-m-t 23:59:59', strtotime($time)))); + } + if($flag == 3) { + $query->whereTime('create_time', '>=', strtotime(date('Y-01-01 00:00:00', strtotime($time)))); + $query->whereTime('create_time', '<=', strtotime(date('Y-12-31 23:59:59', strtotime($time)))); + } + } + return $query; + })->sum('money'); + return $rest; + } + + + public static function selectPayDetails($get) + { + return DatabaseRoute::paginateAllDb('pay_details', function ($query)use($get){ + $query->alias('s') + ->field([ + 's.id', + 's.classify', + 's.order_id as orderId', + 's.money', + 's.user_id as userId', + 's.pay_diamond as payDiamond', + 's.diamond', + 's.state', + 's.create_time as createTime', + 's.pay_time as payTime', + 'u.user_name as userName', + 'u.phone' + ]) + ->leftJoin('tb_user u', 'u.user_id = s.user_id'); + // 添加动态查询条件 + if (!empty($get['startTime']) && !empty($get['endTime'])) { + $query->whereBetween('s.create_time', [$get['startTime'], $get['endTime']]); + } + if (!empty($get['userName'])) { + $query->where('u.user_name', 'like', "%{$get['userName']}%"); + } + if (!empty($get['orderId'])) { + $query->where('s.order_id', 'like', "%{$get['orderId']}%"); + } + if (isset($get['userId']) && $get['userId'] !== null) { + $query->where('u.user_id', $get['userId']); + } + if (isset($get['state']) && $get['state'] !== -1) { + $query->where('s.state', $get['state']); + } elseif (!isset($get['state']) || $get['state'] === -1) { + $query->where('s.state', '<>', -1); + } + return $query; + }, $get['page'], $get['limit'], 's.create_time'); + } + + + public static function selectCashOutList($page, $limit, $cashOut, $isApp = false):array + { + $cashOutList = DatabaseRoute::paginateAllDb('cash_out', function ($query)use($page, $limit, $cashOut, $isApp){ + // 根据请求端设置不同查询条件 + if ($isApp) { + // APP端:查询用户自身的提现记录(用户类型1) + $query->where('user_id', $cashOut['user_id'] ?? 0) + ->where('user_type', 1); + } else { + // 管理后台:根据条件查询 + if (isset($cashOut['user_id'])) { + $query->where('user_id', $cashOut['user_id']); + } else { + if (!isset($cashOut['sys_user_id'])) { + return $query; + } else { + // 查询系统用户的提现记录(用户类型2) + $query->where('user_id', $cashOut['sys_user_id']) + ->where('user_type', 2); + } + } + } + return $query; + }, $page, $limit, 'create_at'); + + if (!$isApp) { + // 管理后台:补充用户信息和统计数据 + $userIdList = []; + foreach ($cashOutList['list'] as $out) { + $userIdList[] = $out['user_id']; + } + + // 查询用户提现总数和总金额 + $cashoutSumMap = []; + $cashoutVerifySumMap = []; + $userinfoMap = []; + + if (!empty($userIdList)) { + // 获取已完成提现统计 + $cashoutSumList = \app\api\model\Cash::selectSumByUserIdList($userIdList, 1); + $cashoutSumMap = array_column($cashoutSumList, null, 'user_id'); + + // 获取审核中提现统计 + $cashoutVerifyList = \app\api\model\Cash::selectSumByUserIdList($userIdList, 3); + $cashoutVerifySumMap = array_column($cashoutVerifyList, null, 'user_id'); + + // 获取用户信息 + $userList = DatabaseRoute::getAllDbData('tb_user', function ($query) use($userIdList) { + return $query->whereIn('user_id', $userIdList) + ->field('user_id, user_name'); + })->select(); + + + $userinfoMap = array_column($userList->toArray(), 'user_name', 'user_id'); + } + + // 补充数据到提现记录 + foreach ($cashOutList['list'] as &$item) { + $info = $cashoutSumMap[$item['user_id']] ?? null; + $info2 = $cashoutVerifySumMap[$item['user_id']] ?? null; + + $item['user_name'] = $userinfoMap[$item['user_id']] ?? ''; + $item['count'] = $info ? $info['count'] : 0; + $item['total'] = $info ? $info['total'] : 0.00; + $item['verify_count'] = $info2 ? $info2['count'] : 0; + $item['verify_total'] = $info2 ? $info2['total'] : 0.00; + } + } + + if ($isApp) { + // APP端:对敏感信息进行脱敏处理 + foreach ($cashOutList['list'] 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']); + } + } + } + return $cashOutList; + } + + /** + * 退回提现金额 + * @param array $entity 提现实体 + * @throws Exception + */ + public static function backCashAmount($entity, $db) + { + // 开启事务确保数据一致性 + $db->startTrans(); + try { + if ($entity['user_type'] == 2) { + // 代理用户退款逻辑 + $detailsData = [ + 'user_id' => $entity['user_id'], + 'operate_id' => $entity['user_id'], + 'title' => "提现失败存入余额", + 'type' => 4, + 'money_type' => 1, + 'money' => $entity['money'], + 'content' => "提现失败存入余额{$entity['money']}元", + 'status' => 1, + 'create_time' => date('Y-m-d H:i:s') + ]; + + // 记录资金流水 + $db->name('sys_user_money_details')->insert($detailsData); + + // 更新代理账户余额(增加余额) + self::updateSysMoney(1, $entity['user_id'], $entity['money']); + } else { + // 普通用户退款逻辑 + self::updateByUserId($entity, $db); + + $detailsData = [ + 'user_id' => $entity['user_id'], + 'sys_user_id' => $entity['sys_user_id'] ?? null, + 'title' => "[提现退款]", + 'type' => 4, + 'money_type' => 1, + 'money' => $entity['money'], + 'content' => "提现失败,自动退款{$entity['money']}元", + 'status' => 1, + 'cash_out_id' => $entity['id'], + 'create_time' => date('Y-m-d H:i:s') + ]; + + // 记录资金流水 + $db->name('user_money_details')->insert($detailsData); + + // 归还用户余额 + self::updateAmount(1, $entity['user_id'], $entity['money'], $db); + } + + // 提交事务 + $db->commit(); + } catch (Exception $e) { + // 回滚事务 + $db->rollback(); + throw $e; + } + } + + public static function updateSysMoney($type, $userId, $money) + { + $query = Db::name('sys_user_money') + ->where('user_id', $userId); + + // 根据类型决定是增加还是减少余额 + if ($type == 1) { + // 增加余额 + return $query->inc('money', $money)->update(); + } elseif ($type == 2) { + // 减少余额 + return $query->dec('money', $money)->update(); + } + return 0; // 无效类型返回 0 + } + + + public static function updateAmount($type, $userId, $amount, $db) + { + User::selectUserMoneyByUserId($userId); + // 构建基础查询 + $query = $db->name('user_money') + ->where('user_id', $userId); + + // 根据类型执行增减操作 + if ($type == 1) { + // 增加金额:amount = amount + #{amount} + return $query->inc('amount', $amount)->update(); + } elseif ($type == 2) { + // 减少金额:amount = amount - #{amount} + // 可选:添加余额充足校验 + return $query->where('amount', '>=', $amount) // 确保余额不小于要减少的金额 + ->dec('amount', $amount) + ->update(); + } + + return 0; // 无效类型返回0 + } + + + public static function updateByUserId($entity, $db) + { + // 验证userId是否存在 + if (empty($entity['user_id'])) { + throw new Exception("cashOut修改失败: userId必须传递"); + } + + // 构建更新条件 + $conditions = [ + 'user_id' => $entity['user_id'], + 'id' => $entity['id'] + ]; + + // 过滤掉主键和条件字段,避免更新这些字段 + $updateData = $entity; + unset($updateData['user_id'], $updateData['id']); + + // 执行更新操作 + $db->name('cash_out') + ->where($conditions) + ->update($updateData); + } + + + +} \ No newline at end of file diff --git a/app/admin/model/Config.php b/app/admin/model/Config.php new file mode 100644 index 0000000..dd5d8de --- /dev/null +++ b/app/admin/model/Config.php @@ -0,0 +1,133 @@ +getData('type'), $model->needContent)) { + $model->content = null; + } else { + $model->content = json_encode(str_attr_to_array($model->getData('content'))); + } + if (is_array($model->rule)) { + $model->rule = implode(',', $model->rule); + } + if ($model->getData('extend') || $model->getData('inputExtend')) { + $extend = str_attr_to_array($model->getData('extend')); + $inputExtend = str_attr_to_array($model->getData('inputExtend')); + if ($inputExtend) $extend['baInputExtend'] = $inputExtend; + if ($extend) $model->extend = json_encode($extend); + } + $model->allow_del = 1; + } + + /** + * 写入后 + */ + public static function onAfterWrite(): void + { + // 清理配置缓存 + Cache::tag(self::$cacheTag)->clear(); + } + + public function getValueAttr($value, $row) + { + if (!isset($row['type']) || $value == '0') return $value; + if (in_array($row['type'], $this->jsonDecodeType)) { + return empty($value) ? [] : json_decode($value, true); + } elseif ($row['type'] == 'switch') { + return (bool)$value; + } elseif ($row['type'] == 'editor') { + return !$value ? '' : htmlspecialchars_decode($value); + } elseif (in_array($row['type'], ['city', 'remoteSelects'])) { + if (!$value) return []; + if (!is_array($value)) return explode(',', $value); + return $value; + } else { + return $value ?: ''; + } + } + + public function setValueAttr(mixed $value, $row): mixed + { + if (in_array($row['type'], $this->jsonDecodeType)) { + return $value ? json_encode($value) : ''; + } elseif ($row['type'] == 'switch') { + return $value ? '1' : '0'; + } elseif ($row['type'] == 'time') { + return $value ? date('H:i:s', strtotime($value)) : ''; + } elseif ($row['type'] == 'city') { + if ($value && is_array($value)) { + return implode(',', $value); + } + return $value ?: ''; + } elseif (is_array($value)) { + return implode(',', $value); + } + + return $value; + } + + public function getContentAttr($value, $row) + { + if (!isset($row['type'])) return ''; + if (in_array($row['type'], $this->needContent)) { + $arr = json_decode($value, true); + return $arr ?: []; + } else { + return ''; + } + } + + public function getExtendAttr($value) + { + if ($value) { + $arr = json_decode($value, true); + if ($arr) { + unset($arr['baInputExtend']); + return $arr; + } + } + return []; + } + + public function getInputExtendAttr($value, $row) + { + if ($row && $row['extend']) { + $arr = json_decode($row['extend'], true); + if ($arr && isset($arr['baInputExtend'])) { + return $arr['baInputExtend']; + } + } + return []; + } +} \ No newline at end of file diff --git a/app/admin/model/CrudLog.php b/app/admin/model/CrudLog.php new file mode 100644 index 0000000..ac18233 --- /dev/null +++ b/app/admin/model/CrudLog.php @@ -0,0 +1,24 @@ + 'array', + 'fields' => 'array', + ]; + +} \ No newline at end of file diff --git a/app/admin/model/DataRecycle.php b/app/admin/model/DataRecycle.php new file mode 100644 index 0000000..4770890 --- /dev/null +++ b/app/admin/model/DataRecycle.php @@ -0,0 +1,15 @@ +belongsTo(DataRecycle::class, 'recycle_id'); + } + + public function admin(): BelongsTo + { + return $this->belongsTo(Admin::class, 'admin_id'); + } +} \ No newline at end of file diff --git a/app/admin/model/DiscSpinning.php b/app/admin/model/DiscSpinning.php new file mode 100644 index 0000000..03b1551 --- /dev/null +++ b/app/admin/model/DiscSpinning.php @@ -0,0 +1,151 @@ + null]); + + // 执行更新(使用模型或Db类) + Db::name($modelName) + ->where($primaryKey, $entity[$primaryKey]) + ->update($updateData); + + // 每batchSize条记录刷新一次(提交部分事务) + $count++; + if ($count % $batchSize === 0) { + Db::commit(); + Db::startTrans(); // 重新开启事务 + } + } + + // 提交剩余事务 + Db::commit(); + return true; + } catch (Exception $e) { + // 回滚事务 + Db::rollback(); + throw $e; + } + } + + public static function receive1($receive) + { + $userId = $receive['user_id'] ?? 0; + $drawCount = self::countDraw($userId); + $maxDraws = Db::name('common_info') + ->where('type', 901) + ->value('value'); + // 校验是否超过限制 + if ($drawCount > $maxDraws) { + Log::write('超过限制' . $receive['id'] . '/' . $drawCount); + return false; // 超过次数限制,终止处理 + } + + // 查询抽奖记录 + $recordId = $receive['id'] ?? 0; + $db = Db::connect(DatabaseRoute::getConnection('disc_spinning_record', ['user_id' => $userId])); + $record = $db->name('disc_spinning_record')->find($recordId); + // 校验记录是否已处理 + if (!empty($record['target_id'])) { + Log::write('记录已处理无需继续处理' . $record['id'] . '/' . $record['target_id']); + return false; // 已处理,终止处理 + } + self::receiveAsync($record); + } + + + public static function countDraw($userId) + { + return DatabaseRoute::getDb('disc_spinning_record', $userId)->where('source', 'order')->where('draw_day', date('Y-m-d')) + ->count(); + } + + public static function receiveAsync($receive) + { + Log::write('正式补偿' . $receive['id']); + // 校验奖励类型(必须为2) + if (($receive['type'] ?? 0) != 2) { + Log::info("非现金转盘奖励,type={$receive['type']}"); + return false; + } + + $db_name = DatabaseRoute::getConnection('tb_user', ['user_id' => $receive['user_id']], true); + $db = Db::connect($db_name); + + // 获取用户信息 + $userInfo = $db->name('tb_user')->where('user_id', $receive['user_id'])->find(); + if (!$userInfo || $userInfo['status'] == 0) { + Log::info("用户状态无效,user_id={$receive['user_id']}"); + return false; + } + + // 开启事务确保数据一致性 + $db->startTrans(); + try { + // 创建资金流水记录 + $moneyDetails = [ + 'user_id' => $receive['user_id'], + 'title' => "[现金大转盘]", + 'type' => 1, + 'money_type' => 1, + 'money' => $receive['number'], + 'content' => "现金红包奖励{$receive['number']}元", + 'source_id' => $receive['id'], + 'create_time' => date('Y-m-d H:i:s', time() - 1) // 上一秒时间 + ]; + + $detailId = $db->name('user_money_details')->insertGetId($moneyDetails); + + // 更新奖励记录 + $a = $db->name('disc_spinning_record') + ->where('id', $receive['id']) + ->update([ + 'target' => "2", + 'target_id' => $detailId + ]); + Log::write('更新奖励' . $a); + Cash::updateAmount(1, $receive['user_id'], $receive['number'], $db); + // 提交事务 + $db->commit(); + return true; + } catch (\Exception $e) { + // 回滚事务 + $db->rollback(); + Log::error("现金转盘奖励处理失败:{$e->getMessage()}"); + } + } + + +} \ No newline at end of file diff --git a/app/admin/model/MessageInfo.php b/app/admin/model/MessageInfo.php new file mode 100644 index 0000000..82da0b7 --- /dev/null +++ b/app/admin/model/MessageInfo.php @@ -0,0 +1,63 @@ +name('message_info'); + + + + // 添加条件(仅当参数不为空时) + if ($userId !== null) { + $query = $query->where('user_id', $userId); + } + + if ($state !== null) { + $query = $query->where('state', $state); + } + + if ($type !== null) { + $query = $query->where('type', $type); + } + $total = $query->count(); + // 构建查询 + $messageList = $query->order('create_at', 'desc')->limit(page($pageNum, $pageSize), $pageSize)->select()->toArray(); + + // 关联用户信息 + foreach ($messageList as &$message) { + if ($message['user_id'] !== null) { + $user = DatabaseRoute::getDb('tb_user', $message['user_id'])->find(); + $message['user_entity'] = $user ?: null; + } + } + return [ + 'list' => $messageList, + 'totalCount' => $total, + 'totalPage' => (int)ceil($total / $pageSize), + 'currPage' => $pageNum, + 'pageSize' => $pageSize, + ]; + + } + +} \ No newline at end of file diff --git a/app/admin/model/Order.php b/app/admin/model/Order.php new file mode 100644 index 0000000..c8d2c04 --- /dev/null +++ b/app/admin/model/Order.php @@ -0,0 +1,490 @@ +where('status', $status); + } + // 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配(精确到天) + $query->whereRaw("date_format(create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配(精确到月) + $query->whereRaw("date_format(create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配(精确到年) + $query->whereRaw("date_format(create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + // 无效flag,不添加时间条件 + break; + } + } + return $query; + })->count(); + } + + + public static function selectFenXiaoMoney($type, $sysUserId, $flag, $time) + { + $result = DatabaseRoute::getAllDbData('orders', function($query)use($type, $sysUserId, $flag, $time) { + $query->where('status', 1); // 固定条件:订单状态为1(已完成) + // 2. 根据类型设置聚合字段(处理null值为0.00) + switch ($type) { + case 1: + $query->field('sum(ifnull(one_money, 0.00)) as total'); // 一级分销金额 + break; + case 2: + $query->field('sum(ifnull(two_money, 0.00)) as total'); // 二级分销金额 + break; + case 3: + $query->field('sum(ifnull(qd_money, 0.00)) as total'); // 渠道分销金额 + break; + default: + throw new \InvalidArgumentException("无效的金额类型:{$type}"); + } + + // 3. 添加系统用户ID条件 + if (!is_null($sysUserId)) { + $query->where('sys_user_id', $sysUserId); + } + + // 4. 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配 + $query->whereRaw("date_format(create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配 + $query->whereRaw("date_format(create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配 + $query->whereRaw("date_format(create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + throw new \InvalidArgumentException("无效的时间标志:{$flag}"); + } + } + return $query; + })->find(); + return $result['total'] ?? 0.00; + } + + + + public static function selectOrdersCount($status, $orderType, $flag, $time, $sysUserId) + { + return DatabaseRoute::getAllDbData('orders', function($query)use($status, $orderType, $flag, $time, $sysUserId) { + $query->where('pay_way', 9); + // 添加系统用户ID条件(排除sysUserId=1的情况) + if (!is_null($sysUserId) && $sysUserId != 1) { + $query->where('sys_user_id', $sysUserId); + } + + // 添加订单状态条件 + if (!is_null($status)) { + $query->where('status', $status); + } + + // 添加订单类型条件 + if (!is_null($orderType)) { + $query->where('orders_type', $orderType); + } + + // 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配(精确到天) + $query->whereRaw("date_format(create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配(精确到月) + $query->whereRaw("date_format(create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配(精确到年) + $query->whereRaw("date_format(create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + // 无效flag,不添加时间条件 + break; + } + } + + return $query; + })->count(); + } + public static function selectOrdersMoney($status, $orderType, $flag, $time, $courseId, $sysUserId) + { + $result = DatabaseRoute::getAllDbData('orders', function($query)use($status, $orderType, $flag, $time, $sysUserId, $courseId) { + // 初始化查询 + $query->where('pay_way', 9) // 固定条件:支付方式为9 + ->where('status', 1) // 固定条件:订单状态为1 + ->field('sum(pay_money) as total'); // 聚合计算总支付金额 + + // 添加系统用户ID条件 + if (!is_null($sysUserId) && $sysUserId != 1) { + $query->where('sys_user_id', $sysUserId); + } + + // 添加订单状态条件 + if (!is_null($status)) { + $query->where('status', $status); + } + + // 添加课程ID条件 + if (!is_null($courseId)) { + $query->where('course_id', $courseId); + } + + // 添加订单类型条件 + if (!is_null($orderType)) { + $query->where('orders_type', $orderType); + } + + // 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配(精确到天) + $query->whereRaw("date_format(create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配(精确到月) + $query->whereRaw("date_format(create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配(精确到年) + $query->whereRaw("date_format(create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + // 无效flag,不添加时间条件 + break; + } + } + return $query; + })->find(); + return (float)$result['total'] ?? 0.00; + } + public static function selectSignInAwardMoney($flag, $time, $sysUserId) + { + $db = Db::connect(get_slave_connect_name()); + // 初始化查询 + $query = $db->name('v_user_money_detail_temp') + ->alias('x') + ->where('x.classify', 6) // 固定条件:分类为6 + ->where('x.type', 1) // 固定条件:类型为1 + ->where('x.state', 2) // 固定条件:状态为2 + ->where('x.user_id', '<>', 1) // 固定条件:用户ID不为1 + ->where('x.title', '签到奖励') // 固定条件:标题为"签到奖励" + ->field('ifnull(sum(x.money), 0) as total'); // 聚合计算总金额 + + // 添加系统用户ID条件 + if (!is_null($sysUserId) && $sysUserId != 1) { + $query = $query->where('x.sys_user_id', $sysUserId); + } + // 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配(精确到天) + $query = $query->whereRaw("date_format(x.create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配(精确到月) + $query = $query->whereRaw("date_format(x.create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配(精确到年) + $query = $query->whereRaw("date_format(x.create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + // 无效flag,不添加时间条件 + break; + } + } + + // 执行查询并返回结果 + $result = $query->find(); + return (float)$result['total']; + } + + public static function selectShareAwardMoney($flag, $time, $sysUserId) + { + $db = Db::connect(get_slave_connect_name()); + // 初始化查询 + $query = $db->name('v_user_money_detail_temp') + ->alias('x') + ->where('x.classify', 6) // 固定条件:分类为6 + ->where('x.type', 1) // 固定条件:类型为1 + ->where('x.state', 2) // 固定条件:状态为2 + ->where('x.user_id', '<>', 1) // 固定条件:用户ID不为1 + ->where('x.title', '分享达标奖励') // 固定条件:标题为"分享达标奖励" + ->field('ifnull(sum(x.money), 0) as total'); // 聚合计算总金额 + + // 添加系统用户ID条件 + if (!is_null($sysUserId) && $sysUserId != 1) { + $query->where('x.sys_user_id', $sysUserId); + } + + // 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配(精确到天) + $query->whereRaw("date_format(x.create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配(精确到月) + $query->whereRaw("date_format(x.create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配(精确到年) + $query->whereRaw("date_format(x.create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + // 无效flag,不添加时间条件 + break; + } + } + + // 执行查询并返回结果 + $result = $query->find(); + return (float)$result['total']; + } + + public static function selectNewUserTaskDoneAwardMoney($flag, $time, $sysUserId) + { + $db = Db::connect(get_slave_connect_name()); + // 初始化查询 + $query = $db->name('v_user_money_detail_temp') + ->alias('x') + ->where('x.classify', 7) // 固定条件:分类为7 + ->where('x.type', 1) // 固定条件:类型为1 + ->where('x.state', 2) // 固定条件:状态为2 + ->where('x.user_id', '<>', 1) // 固定条件:用户ID不为1 + ->where('x.money_type', 1) // 新增条件:金额类型为1 + ->field('ifnull(sum(x.money), 0) as total'); // 聚合计算总金额 + + // 添加系统用户ID条件 + if (!is_null($sysUserId) && $sysUserId != 1) { + $query->where('x.sys_user_id', $sysUserId); + } + + // 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配(精确到天) + $query->whereRaw("date_format(x.create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配(精确到月) + $query->whereRaw("date_format(x.create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配(精确到年) + $query->whereRaw("date_format(x.create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + // 无效flag,不添加时间条件 + break; + } + } + + // 执行查询并返回结果 + $result = $query->find(); + return (float)$result['total']; + } + + + public static function selectInviteTaskDoneAwardMoney($flag, $time, $sysUserId) + { + $db = Db::connect(get_slave_connect_name()); + // 初始化查询 + $query = $db->name('v_user_money_detail_temp') + ->alias('x') + ->where('x.classify', 6) // 固定条件:分类为6 + ->where('x.type', 1) // 固定条件:类型为1 + ->where('x.state', 2) // 固定条件:状态为2 + ->where('x.user_id', '<>', 1) // 固定条件:用户ID不为1 + ->where('x.title', '[分享达标额外奖励]') // 固定条件:标题为"[分享达标额外奖励]" + ->field('ifnull(sum(x.money), 0) as total'); // 聚合计算总金额 + + // 添加系统用户ID条件 + if (!is_null($sysUserId) && $sysUserId != 1) { + $query->where('x.sys_user_id', $sysUserId); + } + + // 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配(精确到天) + $query->whereRaw("date_format(x.create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配(精确到月) + $query->whereRaw("date_format(x.create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配(精确到年) + $query->whereRaw("date_format(x.create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + // 无效flag,不添加时间条件 + break; + } + } + + // 执行查询并返回结果 + $result = $query->find(); + return (float)$result['total']; + } + + public static function executeExtractCallback(mixed $cashOut, ?array $baseResp) + { + $state = 0; + if (!empty($baseResp['status']) && $baseResp['status'] == 2) { + $state = 1; + }else if (!empty($baseResp['status']) && ($baseResp['status'] == 3 || $baseResp['status'] == 99999)) { + $state = 2; + } + + $reason = ''; + if (!empty($baseResp['reason']) && $baseResp['reason'] == '已驳回') { + $reason = '提现失败,请检查收款账号与收款人姓名后重试。'; + } + + if ($state == 1) { + $cashOut['state'] = 1; + $cashOut['out_at'] = getNormalDate(); + $cashOut['refund'] = null; + DatabaseRoute::getDb('cash_out', $cashOut['user_id'], true)->where([ + 'id' => $cashOut['id'] + ])->update($cashOut); + + $userMoney = UserMoney::selectUserMoneyfind($cashOut['user_id']); + if ($userMoney) { + DatabaseRoute::getDb('user_money', $cashOut['user_id'], true)->where([ + 'id' => $userMoney['id'] + ])->update([ + 'cash_count' => $userMoney['cash_count'] + 1, + 'cash_amount' => $userMoney['cash_amount'] ?? 0 + $cashOut['amount'], + ]); + } + return 1; + } + + if ($state == 2) { + // 设置失败状态 + $cashOut['out_at'] = date('Y-m-d H:i:s'); + $cashOut['state'] = 2; + $cashOut['refund'] = $reason; + + // 企业用户 + if ($cashOut['user_type'] === 2) { + // 判断是否已经返还过 + $count = DatabaseRoute::getDb('sys_user_money_details', $cashOut['user_id'])->where([ + 'user_id' => $cashOut['user_id'], + 'classify' => 4, + 'state' => 2, + 'money_type' => 1, + 'type' => 1, + 'source_id' => $cashOut['id'], + ])->count(); + + if ($count > 0) { + return 0; + } + + // 写入返还明细 + DatabaseRoute::getDb('sys_user_money_details', $cashOut['user_id'], true)->insert([ + 'user_id' => $cashOut['user_id'], + 'relation_id'=> $cashOut['user_id'], + 'title' => '提现失败存入余额', + 'classify' => 4, + 'money_type' => 1, + 'state' => 2, + 'money' => $cashOut['money'], + 'content' => '提现失败存入余额' . $cashOut['money'] . '元', + 'type' => 1, + ]); + + // 更新余额 + + $userMoney = SysUserMoney::selectSysUserMoneyByUserId($cashOut['user_id']); + Db::name('sys_user_money')->where([ + 'id' => $userMoney['id'] + ])->dec('money', $cashOut['money'])->update(); + + } else { + // 普通用户 + $count = DatabaseRoute::getDb('user_money_details', $cashOut['user_id'])->where([ + 'user_id' => $cashOut['user_id'], + 'classify' => 4, + 'state' => 2, + 'money_type' => 1, + 'type' => 1, + 'source_id' => $cashOut['id'], + ])->count(); + + if ($count > 0) { + return 0; + } + + // 写入返还明细 + DatabaseRoute::getDb('user_money_details', $cashOut['user_id'], true)->insert([ + 'user_id' => $cashOut['user_id'], + 'title' => '提现失败存入余额', + 'classify' => 4, + 'money_type' => 1, + 'state' => 2, + 'money' => $cashOut['money'], + 'content' => '提现失败存入余额' . $cashOut['money'] . '元', + 'type' => 1, + 'create_time'=> date('Y-m-d H:i:s'), + 'source_id' => $cashOut['id'], + ]); + + // 更新余额 + (new UserMoney())->updateAmount($cashOut['user_id'], $cashOut['money'], false); + } + + // 更新提现记录 + DatabaseRoute::getDb('cash_out', $cashOut['user_id'], true)->where('user_id', $cashOut->user_id) + ->where('id', $cashOut['id']) + ->update([ + 'out_at' => $cashOut['out_at'], + 'state' => $cashOut['state'], + 'refund' => $cashOut['refund'], + ]); + } + + return 0; + + } + +} \ No newline at end of file diff --git a/app/admin/model/PayDetails.php b/app/admin/model/PayDetails.php new file mode 100644 index 0000000..e227969 --- /dev/null +++ b/app/admin/model/PayDetails.php @@ -0,0 +1,59 @@ +where('state', 1) // 固定条件:state=1 + ->field('sum(money) as total'); // 聚合计算总金额 + // 添加分类条件 + if (!is_null($classify)) { + $query->where('classify', $classify); + } + + // 添加时间条件 + if (!is_null($flag)) { + switch ($flag) { + case 1: + // 按日匹配(精确到天) + $query->whereRaw("date_format(create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$time]); + break; + case 2: + // 按月匹配(精确到月) + $query->whereRaw("date_format(create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$time]); + break; + case 3: + // 按年匹配(精确到年) + $query->whereRaw("date_format(create_time, '%Y') = date_format(?, '%Y')", [$time]); + break; + default: + // 无效flag,不添加时间条件 + break; + } + } + + // 添加支付分类条件 + if (!is_null($payClassify) && $payClassify != 0) { + $query->where('pay_classify', $payClassify); + } + return $query; + })->find(); + return $result['total'] ?? 0.0; + } + + + +} \ No newline at end of file diff --git a/app/admin/model/SensitiveData.php b/app/admin/model/SensitiveData.php new file mode 100644 index 0000000..5bf6aeb --- /dev/null +++ b/app/admin/model/SensitiveData.php @@ -0,0 +1,19 @@ + 'array', + ]; +} \ No newline at end of file diff --git a/app/admin/model/SensitiveDataLog.php b/app/admin/model/SensitiveDataLog.php new file mode 100644 index 0000000..e6d4ecf --- /dev/null +++ b/app/admin/model/SensitiveDataLog.php @@ -0,0 +1,27 @@ +belongsTo(SensitiveData::class, 'sensitive_id'); + } + + public function admin(): BelongsTo + { + return $this->belongsTo(Admin::class, 'admin_id'); + } +} \ No newline at end of file diff --git a/app/admin/model/SysCaptcha.php b/app/admin/model/SysCaptcha.php new file mode 100644 index 0000000..cbc8079 --- /dev/null +++ b/app/admin/model/SysCaptcha.php @@ -0,0 +1,21 @@ + $uuid, + 'code' => $code_data['code'], + 'expire_time' => date('Y-m-d H:i:s', time() + config('captcha.expire_time')), + ]); + return ['img' => $code_data['img']]; + } +} \ No newline at end of file diff --git a/app/admin/model/SysMenu.php b/app/admin/model/SysMenu.php new file mode 100644 index 0000000..f16f0eb --- /dev/null +++ b/app/admin/model/SysMenu.php @@ -0,0 +1,103 @@ + $v) { + if(in_array($v['type'], [0, 1])) { + if($v['parent_id'] == 0) { + $one_menu[] = $v; + }else { + $t_menu[] = $v; + } + } + // 权限处理 + if($v['perms'] ) { + if(strpos($v['perms'], ',') !== false) { + foreach (explode(',', $v['perms']) as $item) { + $p_list[] = $item; + } + }else { + $p_list[] = $v['perms']; + } + } + } + foreach ($one_menu as $k => &$one_value) { + foreach ($t_menu as $two_k => $two_value) { + if($two_value['parent_id'] == $one_value['menu_id']) { + $one_value['list'][] = convertToCamelCase($two_value); + } + } + } + $return['menuList'] = convertToCamelCase($one_menu); + $return['permissions'] = $p_list; + return $return; + } + + + + public static function verifyForm($menu) + { + // 验证菜单名称 + if (empty($menu['name'])) { + throw new Exception("菜单名称不能为空"); + } + + // 验证上级菜单 + if (!isset($menu['parentId'])) { + throw new Exception("上级菜单不能为空"); + } + + // 菜单类型为菜单时,验证 URL + if ($menu['type'] == self::MENU_TYPE_MENU) { + if (empty($menu['url'])) { + throw new Exception("菜单URL不能为空"); + } + } + + // 获取上级菜单类型 + $parentType = self::MENU_TYPE_CATALOG; + if ($menu['parentId'] != 0) { + $parentMenu = Db::name('sys_menu')->where(['menu_id' => $menu['parentId']])->find(); + if (!$parentMenu) { + throw new Exception("上级菜单不存在"); + } + $parentType = $parentMenu['type']; + } + + // 目录、菜单类型验证 + if ($menu['type'] == self::MENU_TYPE_CATALOG || $menu['type'] == self::MENU_TYPE_MENU) { + if ($parentType != self::MENU_TYPE_CATALOG) { + throw new Exception("上级菜单只能为目录类型"); + } + return; + } + + // 按钮类型验证 + if ($menu['type'] == self::MENU_TYPE_BUTTON) { + if ($parentType != self::MENU_TYPE_MENU) { + throw new Exception("上级菜单只能为菜单类型"); + } + return; + } + } + +} \ No newline at end of file diff --git a/app/admin/model/SysUser.php b/app/admin/model/SysUser.php new file mode 100644 index 0000000..d692784 --- /dev/null +++ b/app/admin/model/SysUser.php @@ -0,0 +1,11 @@ +name('sys_user_money')->where('user_id', $userId)->find(); + // 若不存在,则创建新记录 + if (is_null($userMoney)) { + $userMoney = [ + 'user_id' => $userId, + 'money' => 0.00, + 'id' => Random::generateRandomPrefixedId(19), + ]; + $id = Db::connect(get_master_connect_name())->name('sys_user_money')->insert($userMoney); + } + return $db->name('sys_user_money')->where('user_id', $userId)->find(); + } + +} \ No newline at end of file diff --git a/app/admin/model/SysUserMoneyDetails.php b/app/admin/model/SysUserMoneyDetails.php new file mode 100644 index 0000000..954108a --- /dev/null +++ b/app/admin/model/SysUserMoneyDetails.php @@ -0,0 +1,85 @@ +name('sys_user_money_details')->where('user_id', $sysUserId)->count(); + // 系统用户模式:查询系统用户资金明细 + $list = $db->name('sys_user_money_details')->where('user_id', $sysUserId) + ->order('create_time', 'desc') + ->limit(page($page, $limit), $limit) + ->select() + ->toArray(); + return [ + 'list' => $list, + 'totalCount' => $total, + 'totalPage' => (int)ceil($total / $limit), + 'currPage' => $page, + 'pageSize' => $limit, + ]; + } + if($userId !== null) { + $db = DatabaseRoute::getDb('user_money_details', $userId)->name('user_money_details'); + $query = $db->where('user_id', $userId); + if ($classify !== null) { + $query = $query->where('classify', $classify); + } + + if ($type !== null) { + $query = $query->where('type', $type); + } + + if ($moneyType !== null) { + $query = $query->where('money_type', $moneyType); + } + if ($viewType == 1) { + // 视图类型1:特殊分类(1和6) + $query = $query->whereIn('classify', [1, 6]); + } + $total = $query->count(); + $query = $query->limit(page($page, $limit), $limit) + ->order('create_time', 'desc') + ->select() + ->toArray(); + return [ + 'list' => $query, + 'totalCount' => $total, + 'totalPage' => (int)ceil($total / $limit), + 'currPage' => $page, + 'pageSize' => $limit, + ]; + }else { + return DatabaseRoute::paginateAllDb('user_money_details', function ($query)use($page, $limit, $sysUserId, $userId, $classify, $type, $moneyType, $viewType) { + if ($classify !== null) { + $query->where('classify', $classify); + } + if ($type !== null) { + $query->where('type', $type); + } + if ($moneyType !== null) { + $query->where('money_type', $moneyType); + } + if ($viewType == 1) { + // 视图类型1:特殊分类(1和6) + $query->whereIn('classify', [1, 6]); + } + return $query; + }, $page, $limit); + } + } + +} \ No newline at end of file diff --git a/app/admin/model/User.php b/app/admin/model/User.php new file mode 100644 index 0000000..3bf52f6 --- /dev/null +++ b/app/admin/model/User.php @@ -0,0 +1,606 @@ +belongsTo(UserGroup::class, 'group_id'); + } + + /** + * 重置用户密码 + * @param int|string $uid 用户ID + * @param string $newPassword 新密码 + * @return int|User + */ + public function resetPassword(int|string $uid, string $newPassword): int|User + { + return $this->where(['id' => $uid])->update(['password' => hash_password($newPassword), 'salt' => '']); + } + + public static function queryCourseOrder($page, $limit, $type, $date, $sysUserId):array + { + // 1. 处理时间范围 + $timestamp = strtotime($date); + if ($type == 2) { + // 按月统计 + $startTime = date('Y-m-01 00:00:00', $timestamp); + $endTime = date('Y-m-t 23:59:59', $timestamp); + } elseif ($type == 3) { + // 按年统计 + $startTime = date('Y-01-01 00:00:00', $timestamp); + $endTime = date('Y-12-31 23:59:59', $timestamp); + } else { + // 按日统计 + $startTime = date('Y-m-d 00:00:00', $timestamp); + $endTime = date('Y-m-d 23:59:59', $timestamp); + } + + // 2. 查询课程订单统计数据(带分页) + $courseList = self::selectGroupCourseId($startTime, $endTime, $page, $limit); + + // 3. 提取课程ID集合 + $courseIds = array_column($courseList['list'], 'orders_id'); + + // 4. 批量查询课程信息并构建映射 + if (!empty($courseIds)) { + $courseMap = []; + $db = Db::connect(get_slave_connect_name()); + $courses = $db->name('course') + ->whereIn('course_id', $courseIds) + ->field('course_id, title') + ->select() + ->toArray(); + + // 构建ID到课程的映射 + foreach ($courses as $course) { + $courseMap[$course['course_id']] = $course; + } + + // 5. 关联课程名称到订单统计数据 + foreach ($courseList['list'] as &$item) { + if (isset($courseMap[$item['orders_id']])) { + $item['coursename'] = $courseMap[$item['orders_id']]['title']; + } + } + unset($item); // 释放引用 + $courseList['list'] = convertToCamelCase($courseList['list']); + } + return $courseList; + } + + + public static function selectGroupCourseId($startTime, $endTime, $page, $limit) + { + return DatabaseRoute::paginateAllDb('orders', function($query) use($startTime, $endTime) { + return $query->alias('o') + ->field([ + 'sum(o.pay_money) as coursemoney', + 'count(*) as coursenum', + 'o.course_id as courseId' + ]) + ->where('o.status', 1) + ->where('o.orders_type', 1) + ->whereBetween('o.create_time', [$startTime, $endTime]) + ->group('o.course_id'); + }, $page, $limit, 'coursenum'); + + } + + + + public static function queryUserCount($type, $date, $platform, $qdCode) + { + + // 检查日期是否为空 + if (empty($date)) { + // 格式化当前时间(注意:PHP时间格式符与Java略有不同) + $date = date('Y-m-d H:i:s'); + } + return DatabaseRoute::getAllDbData('tb_user', function($query) use($type, $date, $platform, $qdCode) { + // 处理时间条件 + $dateTime = strtotime($date); + if ($dateTime === false) { + throw new \InvalidArgumentException("无效的日期格式:{$date}"); + } + + switch ($type) { + case 1: + // 按日统计(精确到天) + $startDate = date('Y-m-d 00:00:00', $dateTime); + $endDate = date('Y-m-d 23:59:59', $dateTime); + $query->whereBetweenTime('create_time', $startDate, $endDate); + break; + case 2: + // 按月统计 + $startDate = date('Y-m-01 00:00:00', $dateTime); + $endDate = date('Y-m-t 23:59:59', $dateTime); + $query->whereBetweenTime('create_time', $startDate, $endDate); + break; + case 3: + // 按年统计 + $startDate = date('Y-01-01 00:00:00', $dateTime); + $endDate = date('Y-12-31 23:59:59', $dateTime); + $query->whereBetweenTime('create_time', $startDate, $endDate); + break; + default: + // 无效类型,不添加时间条件 + break; + } + + // 处理平台条件 + if (!is_null($platform)) { + $query->where('platform', $platform); + } + + // 处理渠道码条件 + if (!is_null($qdCode)) { + $query->where('qd_code', $qdCode); + } + return $query; + })->count(); + + } + + + public static function queryPayMoney($type, $qdCode) + { + $date = date('Y-m-d H:i:s'); + $result = DatabaseRoute::getAllDbData('pay_details', function($query) use($type, $date, $qdCode) { + $query->alias('p') + ->leftJoin('tb_user u', 'u.user_id = p.user_id') + ->where('p.state', 1) // 支付状态为1(成功) + ->field('sum(p.money) as total_money'); // 聚合计算总金额 + + // 根据类型添加时间条件(匹配date_format的逻辑) + switch ($type) { + case 1: + // 按日:日期部分完全匹配 + $query->whereRaw("date_format(p.create_time, '%Y-%m-%d') = date_format(?, '%Y-%m-%d')", [$date]); + break; + case 2: + // 按月:年月部分匹配 + $query->whereRaw("date_format(p.create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$date]); + break; + case 3: + // 按年:年份部分匹配 + $query->whereRaw("date_format(p.create_time, '%Y') = date_format(?, '%Y')", [$date]); + break; + default: + // 默认为空,可根据业务补充默认条件 + break; + } + + // 可选条件:渠道码 + if (!empty($qdCode)) { + $query->where('u.qd_code', $qdCode); + } + return $query; + })->find(); + return $result['total_money'] ?? 0.0; + + } + + + + public static function userMessage($date, $type, $qdCode, $vipType) + { + // 1. 处理时间条件 + $timestamp = strtotime($date); + if ($timestamp === false) { + throw new \InvalidArgumentException("无效的日期格式:{$date}"); + } + if ($type == 1) { + // 按日开始(00:00:00) + $startTime = date('Y-m-d 00:00:00', $timestamp); + } elseif ($type == 2) { + // 按月开始(当月1日 00:00:00) + $startTime = date('Y-m-01 00:00:00', $timestamp); + } else { + // 按年开始(当年1月1日 00:00:00) + $startTime = date('Y-01-01 00:00:00', $timestamp); + } + + $cachestr = 'userMessageUserIdList'; + $userIds = Cache::get($cachestr); + if(!$userIds) { + if (!empty($qdCode)) { + $userIds = DatabaseRoute::getAllDbData('tb_user', function($query) use($qdCode, $startTime) { + $query->where('qd_code', $qdCode); + // 2. 查询符合条件的用户ID集合 + $query->where('create_time', '>=', $startTime) + ->field('user_id'); + return $query; + })->column('user_id'); + } + // 3. 若用户ID集合为空,直接返回0 + if (empty($userIds)) { + return 0; + } + Cache::set($cachestr, json_encode($userIds)); + }else { + $userIds = json_decode($userIds, true); + } + $db = Db::connect(get_slave_connect_name()); + // 4. 查询符合条件的VIP用户数量 + $query = $db->name('user_vip'); + if($userIds) { + $query = $query->whereIn('user_id', $userIds); + } + $query = $query->where('is_vip', 2); + if (!is_null($vipType)) { + $query = $query->where('vip_type', $vipType); + } + return $query->count(); + } + + + /** + * 查询今日支付和提现统计信息 + * @return array 包含支付金额、支付次数、提现金额、提现次数的关联数组 + */ + public static function queryPayAndExtractInfo() + { + + // 1. 计算今日开始和结束时间 + $beginOfDay = date('Y-m-d 00:00:00'); // 今日0点 + $endOfDay = date('Y-m-d 23:59:59'); // 今日23点59分59秒 + + // 2. 查询今日支付信息 + $payInfo = self::queryPayInfo($beginOfDay, $endOfDay); + + // 3. 查询今日提现信息 + $extractInfo = self::queryExtractInfo($beginOfDay, $endOfDay); + + // 4. 合并结果 + return [ + 'payAmount' => $payInfo['totalMoney'] ?? null, + 'payCount' => $payInfo['totalCount'] ?? null, + 'extractAmount' => $extractInfo['totalMoney'] ?? null, + 'extractCount' => $extractInfo['totalCount'] ?? null + ]; + } + + + + /** + * 查询指定时间范围内的支付信息 + * @param string $begin 开始时间(格式:Y-m-d H:i:s) + * @param string $end 结束时间(格式:Y-m-d H:i:s) + * @return array 支付统计数据 + */ + private static function queryPayInfo($begin, $end) + { + return DatabaseRoute::getAllDbData('orders', function($query) use($begin, $end) { + return $query->where('create_time', 'between', [$begin, $end]) + ->where('status', 1) // 假设1表示支付成功 + ->where('pay_way', 9) // 假设1表示支付成功 + ->field([ + 'SUM(pay_money) as totalMoney', // 总支付金额 + 'COUNT(1) as totalCount' // 支付次数 + ]); + })->find() ?: []; + } + + /** + * 查询指定时间范围内的提现信息 + * @param string $begin 开始时间(格式:Y-m-d H:i:s) + * @param string $end 结束时间(格式:Y-m-d H:i:s) + * @return array 提现统计数据 + */ + private static function queryExtractInfo($begin, $end) + { + return DatabaseRoute::getAllDbData('cash_out', function($query) use($begin, $end) { + return $query->where('create_at', 'between', [$begin, $end]) + ->where('state', 1) + ->field([ + 'SUM(money) as totalMoney', // 总提现金额 + 'COUNT(1) as totalCount' // 提现次数 + ]); + })->find() ?: []; + } + + + public static function selectUserPage($page, $limit, $phone, $sex, $platform, $sysPhone, $status, $member, + $inviterCode, $userName, $invitationCode, $startTime, $endTime, $qdCode, $sysUserName, $vipType, $delegate) + { + + $result = DatabaseRoute::paginateAllDb('tb_user', function ($query) use($page, $limit, $phone, $sex, $platform, $sysPhone, $status, $member, + $inviterCode, $userName, $invitationCode, $startTime, $endTime, $qdCode, $sysUserName, $vipType, $delegate) { + + // 初始化查询 + $query->alias('u') + // 左连接sys_user表(别名s) + ->leftJoin('sys_user s', 's.qd_code = u.qd_code') + // 左连接user_money表(别名m) + ->leftJoin('user_money m', 'm.user_id = u.user_id') + // 固定条件:s.sys_user_id is null + ->where('s.sys_user_id', null) + // 字段选择(与原SQL一致) + ->field([ + 'u.*', + '1 as member', + 's.username as sysUserName', + 'm.cash_count as cashCount', + 'm.cash_amount as cashAmount', + 'm.amount as balance' + ]); + + // 搜索条件(user_id/phone/user_name匹配) + if (!is_null($phone) && $phone !== '') { + $query->where(function ($query) use ($phone) { + $query->where('u.user_id', $phone) + ->whereOr('u.phone', $phone) + ->whereOr('u.user_name', $phone); + }); + } + + // 系统用户名模糊查询 + if (!is_null($sysUserName) && $sysUserName !== '') { + $query->where('s.username', 'like', "%{$sysUserName}%"); + } + + // 用户名模糊查询 + if (!is_null($userName) && $userName !== '') { + $query->where('u.user_name', 'like', "%{$userName}%"); + } + + // 性别筛选(非0值) + if (!is_null($sex) && $sex != 0) { + $query->where('u.sex', $sex); + } + + // 平台筛选 + if (!is_null($platform) && $platform !== '') { + $query->where('u.platform', $platform); + } + + // 系统手机号筛选 + if (!is_null($sysPhone) && $sysPhone !== '') { + $query->where('u.sys_phone', $sysPhone); + } + + // 邀请人编码模糊查询 + if (!is_null($inviterCode) && $inviterCode !== '') { + $query->where('u.inviter_code', 'like', "%{$inviterCode}%"); + } + + // 邀请码模糊查询 + if (!is_null($invitationCode) && $invitationCode !== '') { + $query->where('u.invitation_code', 'like', "%{$invitationCode}%"); + } + + // 渠道码精确匹配 + if (!is_null($qdCode) && $qdCode !== '') { + $query->where('u.qd_code', $qdCode); + } + + // 时间范围筛选 + if (!is_null($startTime) && $startTime !== '' && !is_null($endTime) && $endTime !== '') { + // 开始时间和结束时间都存在 + $query->whereBetween('u.create_time', [$startTime, $endTime]); + } elseif (!is_null($startTime) && $startTime !== '') { + // 仅开始时间 + $query->where('u.create_time', '>=', $startTime); + } elseif (!is_null($endTime) && $endTime !== '') { + // 仅结束时间 + $query->where('u.create_time', '<=', $endTime); + } + + // 邀请人数筛选(delegate) + if (!is_null($delegate)) { + if ($delegate == 0) { + // 邀请人数=0 + $query->where('u.invite_count', 0); + } elseif ($delegate == 1) { + // 邀请人数>0 + $query->where('u.invite_count', '>', 0); + } + } + return $query; + }, $page, $limit, 'u.create_time'); + + return $result; + + + } + + public static function upUserBlack($user, $status, $db) + { + $userId = $user['user_id']; // 获取用户ID + + if ($status == 0) { + // 拉黑操作:设置状态为0并更新 + $db->name('tb_user')->where(['user_id' => $userId])->update(['status' => 0]); + return true; + } + + // 解除拉黑操作:设置状态为1,平台为h5并更新 + $db->name('tb_user')->where(['user_id' => $userId])->update(['status' => 1, 'platform' => 'h5']); + + // 查询用户信息(身份证号) + $userInfo = $db->name('user_info')->where('user_id', $userId)->find(); + + // 若存在身份证号,则删除黑名单记录 + if (!is_null($userInfo) && !empty($userInfo['cert_no'])) { + Db::connect(get_master_connect_name())->name('tb_user_blacklist') + ->where('id_card_no', $userInfo['cert_no']) + ->delete(); + } + return true; + + } + + public static function selectInviteMoneyByUserId($user_id, $db) + { + return $db->name('invite_money')->where(['user_id' => $user_id])->find(); + } + + public static function instantselectSumPay($date, $userId, $db) + { + $startTime = date('Y-m-01 00:00:00', strtotime($date)); + $endTime = date('Y-m-t 23:59:59', strtotime($date)); + $sumMoney = $db->name('pay_details') + ->where('create_time', '>', $startTime) // 创建时间大于开始时间 + ->where('create_time', '<', $endTime) // 创建时间小于结束时间 + ->where('state', 1) // 支付状态为1(成功) + ->where('user_id', $userId) // 指定用户ID + ->sum('money'); // 计算金额总和 + return $sumMoney !== null ? (float)$sumMoney : 0.00; + } + + + public static function monthIncome($date, $userId, $db) + { + // 构建查询 + $sumMoney = $db->name('user_money_details') + ->where('user_id', $userId) // 固定条件:用户ID匹配 + ->where('classify', 4) // 固定条件:分类为4 + ->where('type', 2) // 固定条件:类型为2 + ->where('state', 2) // 固定条件:状态为2 + // 时间条件:当月匹配(与原SQL的date_format逻辑一致) + ->whereRaw("date_format(create_time, '%Y-%m') = date_format(?, '%Y-%m')", [$date]) + ->sum('money'); // 计算金额总和 + + // 处理空值并保留两位小数 + return $sumMoney !== null ? number_format($sumMoney, 2) : '0.00'; + } + + public static function queryInviterCount($inviterCode) + { + return DatabaseRoute::getAllDbData('tb_user', function($query) use($inviterCode) { + return $query->where('inviter_code', $inviterCode); + })->count(); + + } + + + public static function selectUserVipByUserId($userId) + { + return Db::connect(get_slave_connect_name())->name('user_vip')->where(['user_id' => $userId])->find(); + + } + + public static function selectUserMoneyByUserId($userId) + { + $db_name = DatabaseRoute::getConnection('user_money', ['user_id' => $userId]); + $db = Db::connect($db_name); + // 查询用户钱包信息 + $userMoney = $db->name('user_money')->where('user_id', $userId)->find(); + // 若不存在,则创建新记录 + if (is_null($userMoney)) { + $db_name = DatabaseRoute::getConnection('user_money', ['user_id' => $userId], true); + $userMoney = [ + 'user_id' => $userId, + 'money' => 0.00, + 'amount' => 0.00, + ]; + Db::connect($db_name)->name('user_money')->insert($userMoney); + } + return $userMoney; + + } + + public static function userListExcel($startTime, $endTime, $page = 1, $limit = 20, $userEntity = []) + { + $list = DatabaseRoute::getAllDbData('tb_user', function ($query) use ($startTime, $endTime, $page, $limit, $userEntity) { + $query->alias('u'); + // 处理 phone 条件 + if (!empty($userEntity['phone'])) { + $query->where('u.phone', 'like', "%{$userEntity['phone']}%"); + } + + // 处理 userName 条件 + if (!empty($userEntity['userName'])) { + $query->where('u.user_name', 'like', "%{$userEntity['userName']}%"); + } + + // 处理 invitationCode 条件 + if (!empty($userEntity['invitationCode'])) { + $query->where('u.invitation_code', 'like', "%{$userEntity['invitationCode']}%"); + } + + // 处理 inviterCode 条件 + if (!empty($userEntity['inviterCode'])) { + $query->where('u.inviter_code', 'like', "%{$userEntity['inviterCode']}%"); + } + + // 处理 platform 条件 + if (!empty($userEntity['platform'])) { + $query->where('u.platform', $userEntity['platform']); + } + + // 处理时间范围条件 + if (!empty($startTime) && !empty($endTime)) { + $query->whereTime('u.create_time', 'between', [$startTime, $endTime]); + } elseif (!empty($startTime)) { + $query->whereTime('u.create_time', '>=', $startTime); + } elseif (!empty($endTime)) { + $query->whereTime('u.create_time', '<=', $endTime); + } + if (!empty($page) && !empty($limit)) { + $query->limit(page($page, $limit), $limit); + } + return $query; + })->select()->toArray(); + $export = new \alei\Export('用户列表'); + $export->setColumn([ + ['field' => 'user_id', 'title' => '用户ID'], + ['field' => 'user_name', 'title' => '用户名'], //格式化时间,时间戳格式化为时间日期格式,默认格式: “Y-m-d H:i:s” + ['field' => 'phone', 'title' => '手机号'], + ['field' => 'sys_phone', 'title' => '手机类型 1安卓 2ios'], + ['field' => 'jifen', 'title' => '积分'], + ['field' => 'invitation_code', 'title' => '邀请码'], + ['field' => 'inviter_code', 'title' => '邀请人邀请码'], + ['field' => 'zhi_fu_bao_name', 'title' => '支付宝名称'], + ['field' => 'zhi_fu_bao', 'title' => '支付宝账号'], + ['field' => 'cert_name', 'title' => '姓名'], + ['field' => 'cert_no', 'title' => '身份证号码'], + ['field' => 'create_time', 'title' => '创建时间'], + ]); + //设置数据 + $export->setData($list); + //生成表格,返回url + $url = $export->build(); //'export/文章.xlsx' + return ['data' => config('buildadmin.run_api_url') . $url]; + } + + + + +} \ No newline at end of file diff --git a/app/admin/model/UserGroup.php b/app/admin/model/UserGroup.php new file mode 100644 index 0000000..27f4168 --- /dev/null +++ b/app/admin/model/UserGroup.php @@ -0,0 +1,13 @@ +name('user_integral_details'); + // 添加用户ID条件(如果不为空) + if ($userId !== null) { + $query = $query->where('user_id', $userId); + } + $count = $query->count(); + $query = $query->order('create_time', 'desc')->limit(page($page, $limit), $limit)->select()->toArray(); + return [ + 'currPage' => $page, + 'pageSize' => $limit, + 'list' => convertToCamelCase($query), + 'totalCount' => $count, + 'totalPage' => ceil($count / $limit), + ]; + + } + + + + public static function updateIntegral($type, $userId, $num, $db) + { + $db = $db->name('user_integral'); + $query = $db->where('user_id', $userId); + // 根据类型决定是增加还是减少积分 + if ($type == 1) { + // 增加积分 + $query->inc('integral_num', $num); + } elseif ($type == 2) { + // 减少积分 + $query->where('integral_num', '>=', $num); + $query->dec('integral_num', $num); + } + + // 执行更新 + return $query->update(); + + } + + + +} \ No newline at end of file diff --git a/app/admin/model/UserMoneyLog.php b/app/admin/model/UserMoneyLog.php new file mode 100644 index 0000000..bff4062 --- /dev/null +++ b/app/admin/model/UserMoneyLog.php @@ -0,0 +1,80 @@ +user_id)->lock(true)->find(); + if (!$user) { + throw new Exception("The user can't find it"); + } + if (!$model->memo) { + throw new Exception("Change note cannot be blank"); + } + $model->before = $user->money; + + $user->money += $model->money; + $user->save(); + + $model->after = $user->money; + } + + public static function onBeforeDelete(): bool + { + return false; + } + + public function getMoneyAttr($value): string + { + return bcdiv($value, 100, 2); + } + + public function setMoneyAttr($value): string + { + return bcmul($value, 100, 2); + } + + public function getBeforeAttr($value): string + { + return bcdiv($value, 100, 2); + } + + public function setBeforeAttr($value): string + { + return bcmul($value, 100, 2); + } + + public function getAfterAttr($value): string + { + return bcdiv($value, 100, 2); + } + + public function setAfterAttr($value): string + { + return bcmul($value, 100, 2); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } +} \ No newline at end of file diff --git a/app/admin/model/UserRule.php b/app/admin/model/UserRule.php new file mode 100644 index 0000000..798c4ec --- /dev/null +++ b/app/admin/model/UserRule.php @@ -0,0 +1,26 @@ +getPk(); + $model->where($pk, $model[$pk])->update(['weigh' => $model[$pk]]); + } + + public function setComponentAttr($value) + { + if ($value) $value = str_replace('\\', '/', $value); + return $value; + } +} \ No newline at end of file diff --git a/app/admin/model/UserScoreLog.php b/app/admin/model/UserScoreLog.php new file mode 100644 index 0000000..770bcf4 --- /dev/null +++ b/app/admin/model/UserScoreLog.php @@ -0,0 +1,50 @@ +user_id)->lock(true)->find(); + if (!$user) { + throw new Exception("The user can't find it"); + } + if (!$model->memo) { + throw new Exception("Change note cannot be blank"); + } + $model->before = $user->score; + + $user->score += $model->score; + $user->save(); + + $model->after = $user->score; + } + + public static function onBeforeDelete(): bool + { + return false; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } +} \ No newline at end of file diff --git a/app/admin/route/route.php b/app/admin/route/route.php new file mode 100644 index 0000000..fdb4310 --- /dev/null +++ b/app/admin/route/route.php @@ -0,0 +1,100 @@ + 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:admin', + 'nickname' => 'require', + 'password' => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$', + 'email' => 'email|unique:admin', + 'mobile' => 'mobile|unique:admin', + 'group_arr' => 'require|array', + ]; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 字段描述 + */ + protected $field = [ + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'add' => ['username', 'nickname', 'password', 'email', 'mobile', 'group_arr'], + ]; + + /** + * 验证场景-前台自己修改自己资料 + */ + public function sceneInfo(): Admin + { + return $this->only(['nickname', 'password', 'email', 'mobile']) + ->remove('password', 'require'); + } + + /** + * 验证场景-编辑资料 + */ + public function sceneEdit(): Admin + { + return $this->only(['username', 'nickname', 'password', 'email', 'mobile', 'group_arr']) + ->remove('password', 'require'); + } + + public function __construct() + { + $this->field = [ + 'username' => __('Username'), + 'nickname' => __('Nickname'), + 'password' => __('Password'), + 'email' => __('Email'), + 'mobile' => __('Mobile'), + 'group_arr' => __('Group Name Arr'), + ]; + $this->message = array_merge($this->message, [ + 'username.regex' => __('Please input correct username'), + 'password.regex' => __('Please input correct password') + ]); + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/admin/validate/AdminGroup.php b/app/admin/validate/AdminGroup.php new file mode 100644 index 0000000..936ec2e --- /dev/null +++ b/app/admin/validate/AdminGroup.php @@ -0,0 +1,46 @@ + 'require', + 'rules' => 'require', + ]; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 字段描述 + */ + protected $field = [ + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'add' => ['name', 'rules'], + 'edit' => ['name', 'rules'], + ]; + + public function __construct() + { + $this->field = [ + 'name' => __('name'), + ]; + $this->message = [ + 'rules' => __('Please select rules'), + ]; + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/admin/validate/AdminRule.php b/app/admin/validate/AdminRule.php new file mode 100644 index 0000000..5abd477 --- /dev/null +++ b/app/admin/validate/AdminRule.php @@ -0,0 +1,46 @@ + 'require', + 'title' => 'require', + 'name' => 'require|unique:admin_rule', + ]; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 字段描述 + */ + protected $field = [ + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'add' => ['type', 'title', 'name'], + 'edit' => ['type', 'title', 'name'], + ]; + + public function __construct() + { + $this->field = [ + 'type' => __('type'), + 'title' => __('title'), + 'name' => __('name'), + ]; + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/admin/validate/Config.php b/app/admin/validate/Config.php new file mode 100644 index 0000000..63bb00c --- /dev/null +++ b/app/admin/validate/Config.php @@ -0,0 +1,41 @@ + 'require|unique:config', + ]; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 字段描述 + */ + protected $field = [ + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'add' => ['name'], + ]; + + public function __construct() + { + $this->field = [ + 'name' => __('Variable name'), + ]; + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/admin/validate/DataRecycle.php b/app/admin/validate/DataRecycle.php new file mode 100644 index 0000000..822285f --- /dev/null +++ b/app/admin/validate/DataRecycle.php @@ -0,0 +1,48 @@ + 'require', + 'controller' => 'require|unique:security_data_recycle', + 'data_table' => 'require', + 'primary_key' => 'require', + ]; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 字段描述 + */ + protected $field = [ + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'add' => ['name', 'controller', 'data_table', 'primary_key'], + 'edit' => ['name', 'controller', 'data_table', 'primary_key'], + ]; + + public function __construct() + { + $this->field = [ + 'name' => __('Name'), + 'controller' => __('Controller'), + 'data_table' => __('Data Table'), + 'primary_key' => __('Primary Key'), + ]; + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/admin/validate/SensitiveData.php b/app/admin/validate/SensitiveData.php new file mode 100644 index 0000000..10bf627 --- /dev/null +++ b/app/admin/validate/SensitiveData.php @@ -0,0 +1,50 @@ + 'require', + 'controller' => 'require|unique:security_sensitive_data', + 'data_table' => 'require', + 'primary_key' => 'require', + 'data_fields' => 'require', + ]; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 字段描述 + */ + protected $field = [ + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'add' => ['name', 'data_fields', 'controller', 'data_table', 'primary_key'], + 'edit' => ['name', 'data_fields', 'controller', 'data_table', 'primary_key'], + ]; + + public function __construct() + { + $this->field = [ + 'name' => __('Name'), + 'data_fields' => __('Data Fields'), + 'controller' => __('Controller'), + 'data_table' => __('Data Table'), + 'primary_key' => __('Primary Key'), + ]; + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/admin/validate/UserMoneyLog.php b/app/admin/validate/UserMoneyLog.php new file mode 100644 index 0000000..76bc080 --- /dev/null +++ b/app/admin/validate/UserMoneyLog.php @@ -0,0 +1,46 @@ + 'require', + 'money' => 'require', + 'memo' => 'require', + ]; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 字段描述 + */ + protected $field = [ + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'add' => ['user_id', 'money', 'memo'], + 'edit' => ['user_id', 'money', 'memo'], + ]; + + public function __construct() + { + $this->field = [ + 'user_id' => __('user_id'), + 'money' => __('money'), + 'memo' => __('memo'), + ]; + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/admin/validate/UserScoreLog.php b/app/admin/validate/UserScoreLog.php new file mode 100644 index 0000000..678d87a --- /dev/null +++ b/app/admin/validate/UserScoreLog.php @@ -0,0 +1,46 @@ + 'require', + 'score' => 'require', + 'memo' => 'require', + ]; + + /** + * 验证提示信息 + * @var array + */ + protected $message = []; + + /** + * 字段描述 + */ + protected $field = [ + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'add' => ['user_id', 'score', 'memo'], + 'edit' => ['user_id', 'score', 'memo'], + ]; + + public function __construct() + { + $this->field = [ + 'user_id' => __('user_id'), + 'score' => __('score'), + 'memo' => __('memo'), + ]; + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/api/common.php b/app/api/common.php new file mode 100644 index 0000000..de848d5 --- /dev/null +++ b/app/api/common.php @@ -0,0 +1,38 @@ + 'Login expired, please login again.', + 'Account not exist' => 'Account does not exist', + 'Account disabled' => 'Account is disabled', + 'Token login failed' => 'Token login failed', + 'Please try again after 1 day' => 'The number of failed login attempts has exceeded the limit, please try again after 24 hours.', + 'Password is incorrect' => 'Incorrect password', + 'You are not logged in' => 'You are not logged in.', + 'Unknown operation' => 'Unknown operation', + 'No action available, please contact the administrator~' => 'There is no action available, please contact the administrator~', + 'Please login first' => 'Please login first!', + 'You have no permission' => 'No permission to operate!', + 'Captcha error' => 'Captcha error!', +]; \ No newline at end of file diff --git a/app/api/lang/en/account.php b/app/api/lang/en/account.php new file mode 100644 index 0000000..3968db2 --- /dev/null +++ b/app/api/lang/en/account.php @@ -0,0 +1,16 @@ + 'Nickname', + 'birthday' => 'Birthday', + 'captcha' => 'Captcha', + 'Old password error' => 'Old password error', + 'Data updated successfully~' => 'Data updated successfully', + 'Please input correct password' => 'Please enter the correct password', + 'nicknameChsDash' => 'Usernames can only be Chinese characters, letters, numbers, underscores_ and dashes-.', + 'Password has been changed~' => 'Password has been changed~', + 'Password has been changed, please login again~' => 'Password has been changed, please login again~', + 'Account does not exist~' => 'Account does not exist', + 'Failed to modify password, please try again later~' => 'Failed to modify password, please try again later~', + 'Please enter the correct verification code' => 'Please enter the correct Captcha', + '%s has been registered' => '%s has been registered, please login directly.', +]; \ No newline at end of file diff --git a/app/api/lang/en/ems.php b/app/api/lang/en/ems.php new file mode 100644 index 0000000..46c7fb3 --- /dev/null +++ b/app/api/lang/en/ems.php @@ -0,0 +1,16 @@ + 'email format error', + 'user_register' => 'Member registration verification', + 'user_retrieve_pwd' => 'Retrieve password verification', + 'user_change_email' => 'Modify mailbox validation', + 'user_email_verify' => 'Member Email Verification', + 'Your verification code is: %s' => 'Your Captcha is: %s,valid for 10 minutes~', + 'Mail sent successfully~' => 'Mail sent successfully', + 'Account does not exist~' => 'Account does not exist', + 'Mail sending service unavailable' => 'The mail sending service is not working, please contact the webmaster to configure it.', + 'Frequent email sending' => 'Frequent email sending', + 'Email has been registered, please log in directly' => 'Email has been registered, please log in directly~', + 'The email has been occupied' => 'The email has been occupied', + 'Email not registered' => 'Email not registered', +]; \ No newline at end of file diff --git a/app/api/lang/en/install.php b/app/api/lang/en/install.php new file mode 100644 index 0000000..0175147 --- /dev/null +++ b/app/api/lang/en/install.php @@ -0,0 +1,44 @@ + 'Install the controller', + 'need' => 'Need', + 'Click to see how to solve it' => 'Click to see how to solve.', + 'Please check the config directory permissions' => 'Please check the Config directory permissions', + 'Please check the public directory permissions' => 'Please check the Public directory permissions', + 'open' => 'Open', + 'close' => 'Close', + 'The installation can continue, and some operations need to be completed manually' => 'You can continue to install, and some operations need to be completed manually ', + 'Allow execution' => 'Allow execution', + 'disabled' => 'Disabled', + 'Allow operation' => 'Allow operation', + 'Acquisition failed' => 'Access failed', + 'Click Install %s' => 'Click Install %s', + 'Writable' => 'Writable', + 'No write permission' => 'No write permissions', + 'already installed' => 'Installed', + 'Not installed' => 'Not installed', + 'File has no write permission:%s' => 'File has no write permission:%s', + 'The system has completed installation. If you need to reinstall, please delete the %s file first' => 'The system has been installed, if you need to reinstall, please delete the %s file first.', + 'Database connection failed:%s' => 'Database connection failure:%s', + 'Failed to install SQL execution:%s' => 'Installation SQL execution failed:%s', + 'unknown' => 'Unknown', + 'Database does not exist' => 'Database does not exist!', + 'No built front-end file found, please rebuild manually!' => 'No built front-end file found, please rebuild manually.', + 'Failed to move the front-end file, please move it manually!' => 'Failed to move the front-end file, please move manually!', + 'How to solve?' => 'How to solve?', + 'View reason' => 'View reasons', + 'Click to view the reason' => 'Click to see the reason', + 'PDO extensions need to be installed' => 'pdo_mysql extensions need to be installed.', + 'proc_open or proc_close functions in PHP Ini is disabled' => 'proc_open and proc_close functions in PHP.Ini is disabled.', + 'How to modify' => 'How to modify?', + 'Click to view how to modify' => 'Click to see how to modify.', + 'Security assurance?' => 'Security assurance?', + 'Using the installation service correctly will not cause any potential security problems. Click to view the details' => 'The correct use of the installation service will not cause any potential security issues. Click to view the details.', + 'Please install NPM first' => 'Please install NPM first.', + 'Installation error:%s' => 'Installation error:%s', + 'Failed to switch package manager. Please modify the configuration file manually:%s' => 'Package manager switch failed, please modify the configuration file manually:%s.', + 'Please upgrade %s version' => 'Please upgrade the %s version', + 'nothing' => 'Nothing', + 'The gd extension and freeType library need to be installed' => 'The gd2 extension and freeType library need to be installed', + 'The .env file with database configuration was detected. Please clean up and try again!' => 'The .env file with database configuration was detected. Please clean up and try again!', +]; \ No newline at end of file diff --git a/app/api/lang/en/user.php b/app/api/lang/en/user.php new file mode 100644 index 0000000..d857baf --- /dev/null +++ b/app/api/lang/en/user.php @@ -0,0 +1,13 @@ + 'Captcha', + 'captchaId' => 'Captcha ID', + 'Please input correct username' => 'Please enter the correct username.', + 'Please input correct password' => 'Please enter the correct password.', + 'Registration parameter error' => 'Wrong registration parameter', + 'Login succeeded!' => 'Login succeeded!', + 'Please enter the correct verification code' => 'Please enter the correct Captcha.', + 'You have already logged in. There is no need to log in again~' => 'You have already logged in, no need to log in again.', + 'Check in failed, please try again or contact the website administrator~' => 'Check in failed,please try again or contact the webmaster.', + 'Member center disabled' => 'The member centre has been disabled, please contact the webmaster to turn it on.', +]; \ No newline at end of file diff --git a/app/api/lang/zh-cn.php b/app/api/lang/zh-cn.php new file mode 100644 index 0000000..a125c99 --- /dev/null +++ b/app/api/lang/zh-cn.php @@ -0,0 +1,47 @@ + '%d秒前', + '%d minute%s ago' => '%d分钟前', + '%d hour%s ago' => '%d小时前', + '%d day%s ago' => '%d天前', + '%d week%s ago' => '%d周前', + '%d month%s ago' => '%d月前', + '%d year%s ago' => '%d年前', + '%d second%s after' => '%d秒后', + '%d minute%s after' => '%d分钟后', + '%d hour%s after' => '%d小时后', + '%d day%s after' => '%d天后', + '%d week%s after' => '%d周后', + '%d month%s after' => '%d月后', + '%d year%s after' => '%d年后', + // 时间格式化-e + // 文件上传-s + 'File uploaded successfully' => '文件上传成功!', + 'No files were uploaded' => '没有文件被上传', + 'The uploaded file format is not allowed' => '上传的文件格式未被允许', + 'The uploaded image file is not a valid image' => '上传的图片文件不是有效的图像', + 'The uploaded file is too large (%sMiB), Maximum file size:%sMiB' => '上传的文件太大(%sM),最大文件大小:%sM', + 'No files have been uploaded or the file size exceeds the upload limit of the server' => '没有文件被上传或文件大小超出服务器上传限制!', + 'Topic format error' => '上传存储子目录格式错误!', + 'Driver %s not supported' => '不支持的驱动:%s', + // 文件上传-e + 'Username' => '用户名', + 'Email' => '邮箱', + 'Mobile' => '手机号', + 'Password' => '密码', + 'Login expired, please login again.' => '登录过期,请重新登录。', + 'Account not exist' => '帐户不存在', + 'Account disabled' => '帐户已禁用', + 'Token login failed' => '令牌登录失败', + 'Please try again after 1 day' => '登录失败次数超限,请在1天后再试', + 'Password is incorrect' => '密码不正确', + 'You are not logged in' => '你没有登录', + 'Unknown operation' => '未知操作', + 'No action available, please contact the administrator~' => '没有可用操作,请联系管理员~', + 'Please login first' => '请先登录!', + 'You have no permission' => '没有权限操作!', + 'Parameter error' => '参数错误!', + 'Token expiration' => '登录态过期,请重新登录!', + 'Captcha error' => '验证码错误!', +]; \ No newline at end of file diff --git a/app/api/lang/zh-cn/account.php b/app/api/lang/zh-cn/account.php new file mode 100644 index 0000000..4f8be69 --- /dev/null +++ b/app/api/lang/zh-cn/account.php @@ -0,0 +1,22 @@ + '昵称', + 'birthday' => '生日', + 'captcha' => '验证码', + 'Old password error' => '旧密码错误', + 'Data updated successfully~' => '资料更新成功~', + 'Please input correct password' => '请输入正确的密码', + 'nicknameChsDash' => '用户名只能是汉字、字母、数字和下划线_及破折号-', + 'Password has been changed~' => '密码已修改~', + 'Password has been changed, please login again~' => '密码已修改,请重新登录~', + 'Account does not exist~' => '账户不存在~', + 'Failed to modify password, please try again later~' => '修改密码失败,请稍后重试~', + 'Please enter the correct verification code' => '请输入正确的验证码!', + '%s has been registered' => '%s已被注册,请直接登录~', + 'email format error' => '电子邮箱格式错误!', + 'mobile format error' => '手机号格式错误!', + 'You need to verify your account before modifying the binding information' => '您需要先通过账户验证才能修改绑定信息!', + 'Password error' => '密码错误!', + 'email is occupied' => '电子邮箱地址已被占用!', + 'mobile is occupied' => '手机号已被占用!', +]; \ No newline at end of file diff --git a/app/api/lang/zh-cn/ems.php b/app/api/lang/zh-cn/ems.php new file mode 100644 index 0000000..e75a328 --- /dev/null +++ b/app/api/lang/zh-cn/ems.php @@ -0,0 +1,18 @@ + '电子邮箱格式错误', + 'user_register' => '会员注册验证', + 'user_change_email' => '修改邮箱验证', + 'user_retrieve_pwd' => '找回密码验证', + 'user_email_verify' => '会员身份验证', + 'Your verification code is: %s' => '您的验证码是:%s,十分钟内有效~', + 'Mail sent successfully~' => '邮件发送成功~', + 'Account does not exist~' => '账户不存在~', + 'Mail sending service unavailable' => '邮件发送服务不可用,请联系网站管理员进行配置~', + 'Frequent email sending' => '频繁发送电子邮件', + 'Email has been registered, please log in directly' => '电子邮箱已注册,请直接登录~', + 'The email has been occupied' => '电子邮箱已被占用!', + 'Email not registered' => '电子邮箱未注册', + 'Please use the account registration email to send the verification code' => '请使用账户注册邮箱发送验证码!', + 'Password error' => '密码错误!', +]; \ No newline at end of file diff --git a/app/api/lang/zh-cn/install.php b/app/api/lang/zh-cn/install.php new file mode 100644 index 0000000..18117ea --- /dev/null +++ b/app/api/lang/zh-cn/install.php @@ -0,0 +1,44 @@ + '安装控制器', + 'need' => '需要', + 'Click to see how to solve it' => '点击查看如何解决', + 'Please check the config directory permissions' => '请检查 config 目录权限', + 'Please check the public directory permissions' => '请检查 public 目录权限', + 'open' => '开启', + 'close' => '关闭', + 'The installation can continue, and some operations need to be completed manually' => '可以继续安装,部分操作需手动完成', + 'Allow execution' => '允许执行', + 'disabled' => '已禁用', + 'Allow operation' => '允许操作', + 'Acquisition failed' => '获取失败', + 'Click Install %s' => '点击安装%s', + 'Writable' => '可写', + 'No write permission' => '无写权限', + 'already installed' => '已安装', + 'Not installed' => '未安装', + 'File has no write permission:%s' => '文件无写入权限:%s', + 'The system has completed installation. If you need to reinstall, please delete the %s file first' => '系统已完成安装。如果需要重新安装,请先删除 %s 文件', + 'Database connection failed:%s' => '数据库连接失败:%s', + 'Failed to install SQL execution:%s' => '安装SQL执行失败:%s', + 'unknown' => '未知', + 'Database does not exist' => '数据库不存在!', + 'No built front-end file found, please rebuild manually!' => '没有找到构建好的前端文件,请手动重新构建!', + 'Failed to move the front-end file, please move it manually!' => '移动前端文件失败,请手动移动!', + 'How to solve?' => '如何解决?', + 'View reason' => '查看原因', + 'Click to view the reason' => '点击查看原因', + 'PDO extensions need to be installed' => '需要安装 pdo_mysql 扩展', + 'proc_open or proc_close functions in PHP Ini is disabled' => 'proc_open和proc_close函数在php.ini中被禁用掉了', + 'How to modify' => '如何修改', + 'Click to view how to modify' => '点击查看如何修改', + 'Security assurance?' => '安全保证?', + 'Using the installation service correctly will not cause any potential security problems. Click to view the details' => '安装服务使用正确不会造成任何潜在安全问题,点击查看详情', + 'Please install NPM first' => '请先安装npm', + 'Installation error:%s' => '安装出错:%s', + 'Failed to switch package manager. Please modify the configuration file manually:%s' => '包管理器切换失败,请手动修改配置文件:%s', + 'Please upgrade %s version' => '请升级%s版本', + 'nothing' => '无', + 'The gd extension and freeType library need to be installed' => '需要gd2扩展和freeType库', + 'The .env file with database configuration was detected. Please clean up and try again!' => '检测到带有数据库配置的 .env 文件。请清理后再试一次!', +]; \ No newline at end of file diff --git a/app/api/lang/zh-cn/user.php b/app/api/lang/zh-cn/user.php new file mode 100644 index 0000000..3bb8591 --- /dev/null +++ b/app/api/lang/zh-cn/user.php @@ -0,0 +1,14 @@ + '验证码', + 'captchaId' => '验证码标识', + 'Register type' => '注册类型', + 'Please input correct username' => '请输入正确的用户名', + 'Please input correct password' => '请输入正确的密码', + 'Registration parameter error' => '注册参数错误', + 'Login succeeded!' => '登录成功', + 'Please enter the correct verification code' => '请输入正确的验证码', + 'You have already logged in. There is no need to log in again~' => '您已经登录过了,无需重复登录~', + 'Check in failed, please try again or contact the website administrator~' => '签入失败,请重试或联系网站管理员~', + 'Member center disabled' => '会员中心已禁用,请联系网站管理员开启。', +]; \ No newline at end of file diff --git a/app/api/middleware.php b/app/api/middleware.php new file mode 100644 index 0000000..8b6c89a --- /dev/null +++ b/app/api/middleware.php @@ -0,0 +1,5 @@ + ceil($count / $data['limit']), ]; if($list) { - Cach::set('index_data_' . $data['page'], json_encode($return, true)); + Cache::set('index_data_' . $data['page'], json_encode($return, true)); } return returnSuccessData($return); } @@ -282,7 +282,6 @@ class Course extends Model // 根据id查询短剧集数列表 public static function courseSets($get, $user, $sort = null) { - try { if(empty($get['courseId'])) { return returnErrorData('参数不完整'); @@ -330,6 +329,7 @@ class Course extends Model } else { $courseDetailsSetVos = CourseDetails::courseSets($courseId, 1, $bean['wholesale_price']); } +// return returnSuccessData(321); // 调整集数范围 if (!is_null($sort) && $sort > 2) { $startSort = $sort - 3; @@ -346,8 +346,10 @@ class Course extends Model $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(); +// Log::write('123'); $detailsId = array_flip(array_flip($detailsId)); // 去重 } +// return returnSuccessData(123); // 处理剧集列表 $current = null; foreach ($courseDetailsSetVos as &$s) { @@ -370,17 +372,18 @@ class Course extends Model } // 检查是否已点赞 - 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 ($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; +// } } +// return returnSuccessData(999); // 如果没有当前播放集,默认第一集 if (empty($current) && !empty($courseDetailsSetVos)) { $courseDetailsSetVos[0]['current'] = 1; @@ -400,7 +403,7 @@ class Course extends Model ]; return returnSuccessData($map); } catch (\Exception $e) { - Log::info("请求剧集异常: " . $e->getMessage() . '/' . $e->getLine() . '/'); + Log::write("请求剧集异常: " . $e->getMessage() . '/' . $e->getLine() . '/'); return returnErrorData($e->getMessage()); } } diff --git a/app/czg/app/model/CourseCollect.php b/app/api/model/CourseCollect.php similarity index 98% rename from app/czg/app/model/CourseCollect.php rename to app/api/model/CourseCollect.php index 723a6cf..a703fe6 100644 --- a/app/czg/app/model/CourseCollect.php +++ b/app/api/model/CourseCollect.php @@ -1,6 +1,6 @@ $order['sys_user_id'] ]); -// // 短剧插入 + // 短剧插入 self::insertOrders($order); // 用户信息及上级信息 @@ -196,9 +196,9 @@ class Orders extends BaseModel 'user_id' => $byUser['user_id'] ]); } -// -// -// // TODO 异步领取奖励 + + + // TODO 异步领取奖励 // pushQueue(ActivitiesQueue::class, [ // 'userInfo' => $userInfo, // 'sourceUser' => $byUser diff --git a/app/czg/app/model/TaskCenter.php b/app/api/model/TaskCenter.php similarity index 100% rename from app/czg/app/model/TaskCenter.php rename to app/api/model/TaskCenter.php diff --git a/app/czg/app/model/TaskCenterRecord.php b/app/api/model/TaskCenterRecord.php similarity index 100% rename from app/czg/app/model/TaskCenterRecord.php rename to app/api/model/TaskCenterRecord.php diff --git a/app/czg/app/model/TbUser.php b/app/api/model/TbUser.php similarity index 93% rename from app/czg/app/model/TbUser.php rename to app/api/model/TbUser.php index 81176e2..9cf8eb6 100644 --- a/app/czg/app/model/TbUser.php +++ b/app/api/model/TbUser.php @@ -1,6 +1,6 @@ name('tb_user')->where([$field => $username])->find(); if($data) { @@ -35,10 +35,10 @@ class TbUser extends BaseModel public static function GetByuserInvite($inviter_code) { // // 全表扫描username -// $dbmap = config('think-orm.db_map'); +// $dbmap = config('database.db_map'); // $count = 0; // foreach ($dbmap as $dbname) { -// if(!in_array($dbname, config('think-orm.unset_db_map'))) { +// 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; @@ -69,11 +69,11 @@ class TbUser extends BaseModel return $data_list; -// $dbmap = config('think-orm.db_map'); +// $dbmap = config('database.db_map'); // $data_list = []; // foreach ($dbmap as $dbname) { // -// if(!in_array($dbname, config('think-orm.unset_db_map'))) { +// 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) { @@ -104,9 +104,9 @@ class TbUser extends BaseModel public static function GetByinvitationCode($invitation_code) { // 全表扫描username - $dbmap = config('think-orm.db_map'); + $dbmap = config('database.db_map'); foreach ($dbmap as $dbname) { - if(!in_array($dbname, config('think-orm.unset_db_map'))) { + 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) { @@ -121,9 +121,9 @@ class TbUser extends BaseModel public static function GetByinviterCodeCount($inviter_code) { // 全表扫描username - $dbmap = config('think-orm.db_map'); + $dbmap = config('database.db_map'); foreach ($dbmap as $dbname) { - if(!in_array($dbname, config('think-orm.unset_db_map'))) { + 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) { @@ -281,7 +281,7 @@ class TbUser extends BaseModel public static function selectUserByIdNew($user) { - $db = Db::connect(config('think-orm.search_library')); + $db = Db::connect(config('database.search_library')); $userVip = $db->name('user_vip')->where(['user_id' => $user['user_id']])->find(); $db->close(); if ($userVip) { diff --git a/app/czg/app/model/TbUserBlacklist.php b/app/api/model/TbUserBlacklist.php similarity index 96% rename from app/czg/app/model/TbUserBlacklist.php rename to app/api/model/TbUserBlacklist.php index c668b88..258ab01 100644 --- a/app/czg/app/model/TbUserBlacklist.php +++ b/app/api/model/TbUserBlacklist.php @@ -1,6 +1,6 @@ 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user', + 'nickname' => 'require|chsDash', + 'birthday' => 'date', + 'email' => 'require|email|unique:user', + 'mobile' => 'require|mobile|unique:user', + 'password' => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$', + 'account' => 'require', + 'captcha' => 'require', + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'edit' => ['username', 'nickname', 'birthday'], + 'changePassword' => ['password'], + 'retrievePassword' => ['account', 'captcha', 'password'], + ]; + + public function __construct() + { + $this->field = [ + 'username' => __('Username'), + 'email' => __('Email'), + 'mobile' => __('Mobile'), + 'password' => __('Password'), + 'nickname' => __('nickname'), + 'birthday' => __('birthday'), + ]; + $this->message = array_merge($this->message, [ + 'nickname.chsDash' => __('nicknameChsDash'), + 'password.regex' => __('Please input correct password') + ]); + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/api/validate/CourseCollectValidate.php b/app/api/validate/CourseCollectValidate.php new file mode 100644 index 0000000..148dd88 --- /dev/null +++ b/app/api/validate/CourseCollectValidate.php @@ -0,0 +1,29 @@ + 'require', +// 'courseDetailsId' => 'require', + 'courseId' => 'require', + 'type' => 'require', + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'insertCourseCollect' => ['classify', 'courseId', 'type'] + ]; + + public function __construct() + { + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/api/validate/OrderValidate.php b/app/api/validate/OrderValidate.php new file mode 100644 index 0000000..71b0b3c --- /dev/null +++ b/app/api/validate/OrderValidate.php @@ -0,0 +1,27 @@ + 'require', + 'courseDetailsId' => 'require', + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'insertCourseOrders' => ['courseId'] + ]; + + public function __construct() + { + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/api/validate/User.php b/app/api/validate/User.php new file mode 100644 index 0000000..846feb0 --- /dev/null +++ b/app/api/validate/User.php @@ -0,0 +1,76 @@ + 'require|regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user', + 'password' => 'require|regex:^(?!.*[&<>"\'\n\r]).{6,32}$', + 'registerType' => 'require|in:email,mobile', + 'email' => 'email|unique:user|requireIf:registerType,email', + 'mobile' => 'mobile|requireIf:registerType,mobile', + // 注册邮箱或手机验证码 + 'captcha' => 'require', + // 登录点选验证码 + 'captchaId' => 'require', + 'captchaInfo' => 'require', + 'name' => 'require', + 'idNum' => 'require', + 'certName' => 'require', + 'certNum' => 'require', + 'accountNo' => 'require', + 'city' => 'require', + 'province' => 'require', + 'bankBranch' => 'require', + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'register' => ['username', 'password', 'registerType', 'email', 'mobile', 'captcha'], + 'realName' => ['certName', 'certNum', 'accountNo', 'mobile', 'province', 'city', 'bankBranch'], + ]; + + /** + * 登录验证场景 + */ + public function sceneLogin(): User + { + $fields = ['username', 'password']; + + // 根据系统配置的登录验证码开关调整验证场景的字段 + $userLoginCaptchaSwitch = Config::get('buildadmin.user_login_captcha'); + if ($userLoginCaptchaSwitch) { + $fields[] = 'captchaId'; + $fields[] = 'captchaInfo'; + } + + return $this->only($fields)->remove('username', ['regex', 'unique']); + } + + public function __construct() + { + $this->field = [ + 'username' => __('Username'), + 'email' => __('Email'), + 'mobile' => __('Mobile'), + 'password' => __('Password'), + 'captcha' => __('captcha'), + 'captchaId' => __('captchaId'), + 'captchaInfo' => __('captcha'), + 'registerType' => __('Register type'), + ]; + $this->message = array_merge($this->message, [ + 'username.regex' => __('Please input correct username'), + 'password.regex' => __('Please input correct password') + ]); + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/api/validate/WuyouValidate.php b/app/api/validate/WuyouValidate.php new file mode 100644 index 0000000..f955a45 --- /dev/null +++ b/app/api/validate/WuyouValidate.php @@ -0,0 +1,27 @@ + 'require', + 'orderId' => 'require', + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'payOrder' => ['payType', 'orderId'] + ]; + + public function __construct() + { + parent::__construct(); + } +} \ No newline at end of file diff --git a/app/common/controller/Frontend.php b/app/common/controller/Frontend.php new file mode 100644 index 0000000..1de932e --- /dev/null +++ b/app/common/controller/Frontend.php @@ -0,0 +1,209 @@ +auth->getUserInfo()['user_id']; + } + public function __construct() + { + $this->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); + } + }else { + if ($needLogin) { + $this->error('请登录', [ + 'type' => $this->auth::NEED_LOGIN + ], $this->auth::LOGIN_RESPONSE_CODE); + } + 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'] = 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; + + throw new MyBusinessException(json_encode($result)); + } + + + + /** + * 返回 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 MyBusinessException(json_encode($result)); + } + + public function ApiDataReturn($data) + { + return json($data); + } + + + + +} \ No newline at end of file diff --git a/app/common/model/Common.php b/app/common/model/Common.php index 796c8f6..7720f1e 100644 --- a/app/common/model/Common.php +++ b/app/common/model/Common.php @@ -45,7 +45,7 @@ class Common extends Model public static function getAppUseKv() { - $info = Db::connect(config('database.search_library'))->name('common_info')->where(['is_app_use' => 1])->field('id, value')->select(); + $info = Db::connect(config('think-orm.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']; diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index f09ff8e..fd81188 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -14,6 +14,180 @@ class IndexController { public function index(Request $request) { + \support\Log::info('来了' . date('Y-m-d H:i:s')); + $get['courseId'] = $course_id = '1877654905222135809'; + $user['user_id'] = $user_id = '14240'; + $user = DatabaseRoute::getDb('tb_user', $user_id)->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(); + $isGood_db->close(); + $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) diff --git a/app/czg/app/controller/AccountController.php b/app/czg/app/controller/AccountController.php new file mode 100644 index 0000000..24ccfe1 --- /dev/null +++ b/app/czg/app/controller/AccountController.php @@ -0,0 +1,255 @@ +auth->id) + ->where('create_time', 'BETWEEN', $tempToday0 . ',' . $tempToday24) + ->sum('score'); + + $userMoneyTemp = UserMoneyLog::where('user_id', $this->auth->id) + ->where('create_time', 'BETWEEN', $tempToday0 . ',' . $tempToday24) + ->sum('money'); + $money[$i] = bcdiv($userMoneyTemp, 100, 2); + } + + $this->success('', [ + 'days' => $days, + 'score' => $score, + 'money' => $money, + ]); + } + + /** + * 会员资料 + * @throws Throwable + */ + public function profile(): void + { + if ($this->request->isPost()) { + $data = $this->request->only(['id', 'avatar', 'username', 'nickname', 'gender', 'birthday', 'motto']); + if (!isset($data['birthday'])) $data['birthday'] = null; + + try { + $validate = new AccountValidate(); + $validate->scene('edit')->check($data); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + + $model = $this->auth->getUser(); + $model->startTrans(); + try { + $model->save($data); + $model->commit(); + } catch (Throwable $e) { + $model->rollback(); + $this->error($e->getMessage()); + } + + $this->success(__('Data updated successfully~')); + } + + $this->success('', [ + 'accountVerificationType' => get_account_verification_type() + ]); + } + + /** + * 通过手机号或邮箱验证账户 + * 此处检查的验证码是通过 api/Ems或api/Sms发送的 + * 验证成功后,向前端返回一个 email-pass Token或着 mobile-pass Token + * 在 changBind 方法中,通过 pass Token来确定用户已经通过了账户验证(用户未绑定邮箱/手机时通过账户密码验证) + * @throws Throwable + */ + public function verification(): void + { + $captcha = new Captcha(); + $params = $this->request->only(['type', 'captcha']); + if ($captcha->check($params['captcha'], ($params['type'] == 'email' ? $this->auth->email : $this->auth->mobile) . "user_{$params['type']}_verify")) { + $uuid = Random::uuid(); + Token::set($uuid, $params['type'] . '-pass', $this->auth->id, 600); + $this->success('', [ + 'type' => $params['type'], + 'accountVerificationToken' => $uuid, + ]); + } + $this->error(__('Please enter the correct verification code')); + } + + /** + * 修改绑定信息(手机号、邮箱) + * 通过 pass Token来确定用户已经通过了账户验证,也就是以上的 verification 方法,同时用户未绑定邮箱/手机时通过账户密码验证 + * @throws Throwable + */ + public function changeBind(): void + { + $captcha = new Captcha(); + $params = $this->request->only(['type', 'captcha', 'email', 'mobile', 'accountVerificationToken', 'password']); + $user = $this->auth->getUser(); + + if ($user[$params['type']]) { + if (!Token::check($params['accountVerificationToken'], $params['type'] . '-pass', $user->id)) { + $this->error(__('You need to verify your account before modifying the binding information')); + } + } elseif (!isset($params['password']) || !verify_password($params['password'], $user->password, ['salt' => $user->salt])) { + $this->error(__('Password error')); + } + + // 检查验证码 + if ($captcha->check($params['captcha'], $params[$params['type']] . "user_change_{$params['type']}")) { + if ($params['type'] == 'email') { + $validate = Validate::rule(['email' => 'require|email|unique:user'])->message([ + 'email.require' => 'email format error', + 'email.email' => 'email format error', + 'email.unique' => 'email is occupied', + ]); + if (!$validate->check(['email' => $params['email']])) { + $this->error(__($validate->getError())); + } + $user->email = $params['email']; + } elseif ($params['type'] == 'mobile') { + $validate = Validate::rule(['mobile' => 'require|mobile|unique:user'])->message([ + 'mobile.require' => 'mobile format error', + 'mobile.mobile' => 'mobile format error', + 'mobile.unique' => 'mobile is occupied', + ]); + if (!$validate->check(['mobile' => $params['mobile']])) { + $this->error(__($validate->getError())); + } + $user->mobile = $params['mobile']; + } + Token::delete($params['accountVerificationToken']); + $user->save(); + $this->success(); + } + $this->error(__('Please enter the correct verification code')); + } + + public function changePassword(): void + { + if ($this->request->isPost()) { + $model = $this->auth->getUser(); + $params = $this->request->only(['oldPassword', 'newPassword']); + + if (!verify_password($params['oldPassword'], $model->password, ['salt' => $model->salt])) { + $this->error(__('Old password error')); + } + + $model->startTrans(); + try { + $validate = new AccountValidate(); + $validate->scene('changePassword')->check(['password' => $params['newPassword']]); + $model->resetPassword($this->auth->id, $params['newPassword']); + $model->commit(); + } catch (Throwable $e) { + $model->rollback(); + $this->error($e->getMessage()); + } + + $this->auth->logout(); + $this->success(__('Password has been changed, please login again~')); + } + } + + /** + * 积分日志 + * @throws Throwable + */ + public function integral(): void + { + $limit = $this->request->request('limit'); + $integralModel = new UserScoreLog(); + $res = $integralModel->where('user_id', $this->auth->id) + ->order('create_time desc') + ->paginate($limit); + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + ]); + } + + /** + * 余额日志 + * @throws Throwable + */ + public function balance(): void + { + $limit = $this->request->request('limit'); + $moneyModel = new UserMoneyLog(); + $res = $moneyModel->where('user_id', $this->auth->id) + ->order('create_time desc') + ->paginate($limit); + + $this->success('', [ + 'list' => $res->items(), + 'total' => $res->total(), + ]); + } + + /** + * 找回密码 + * @throws Throwable + */ + public function retrievePassword(): void + { + $params = $this->request->only(['type', 'account', 'captcha', 'password']); + try { + $validate = new AccountValidate(); + $validate->scene('retrievePassword')->check($params); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + + if ($params['type'] == 'email') { + $user = User::where('email', $params['account'])->find(); + } else { + $user = User::where('mobile', $params['account'])->find(); + } + if (!$user) { + $this->error(__('Account does not exist~')); + } + + $captchaObj = new Captcha(); + if (!$captchaObj->check($params['captcha'], $params['account'] . 'user_retrieve_pwd')) { + $this->error(__('Please enter the correct verification code')); + } + + if ($user->resetPassword($user->id, $params['password'])) { + $this->success(__('Password has been changed~')); + } else { + $this->error(__('Failed to modify password, please try again later~')); + } + } +} \ No newline at end of file diff --git a/app/czg/app/controller/AdController.php b/app/czg/app/controller/AdController.php new file mode 100644 index 0000000..9b1b7c1 --- /dev/null +++ b/app/czg/app/controller/AdController.php @@ -0,0 +1,30 @@ +request->get(); + $info = Db::name('uni_ad_callback_record')->where([ + 'user_id' => $this->getUserId(), + 'extra' => $params['extraKey'] + ])->find(); + if (!$info && RedisUtils::isCanCash($this->getUserId()) && str_contains($params['extraKey'], 'cash')) { + RedisUtils::setCanCashFlag($this->getUserId(), -1); + } + $this->success(); + } + + + + +} \ No newline at end of file diff --git a/app/czg/app/controller/AliossController.php b/app/czg/app/controller/AliossController.php new file mode 100644 index 0000000..8801fc6 --- /dev/null +++ b/app/czg/app/controller/AliossController.php @@ -0,0 +1,70 @@ +file('file'); + if(empty($file)) { + $this->error('参数不能为空'); + } + $commoninfo = Db::connect(config('database.search_library')); + $endpoint = $commoninfo->name('common_info')->where(['type' => 68])->find()['value']; + $accessKeyId = $commoninfo->name('common_info')->where(['type' => 69])->find()['value']; + $secretAccessKey = $commoninfo->name('common_info')->where(['type' => 70])->find()['value']; + $bucket = $commoninfo->name('common_info')->where(['type' => 71])->find()['value']; + $befor_url = $commoninfo->name('common_info')->where(['type' => 72])->find()['value']; + + putenv('OSS_ACCESS_KEY_ID=' . $accessKeyId); + putenv('OSS_ACCESS_KEY_SECRET='. $secretAccessKey); + $provider = new EnvironmentVariableCredentialsProvider(); + + $object = date('Ymd') . '/' . uniqid() . '.' .$file->getOriginalExtension(); + try{ + $config = array( + "provider" => $provider, + "endpoint" => $endpoint, + "signatureVersion" => OssClient::OSS_SIGNATURE_VERSION_V1, + ); + $ossClient = new OssClient($config); + // 以二进制模式读取文件内容 + $handle = fopen($file->getRealPath(), 'rb'); + $content = stream_get_contents($handle); + fclose($handle); + + // 上传二进制内容,明确指定Content-Type + $options = [ + OssClient::OSS_CONTENT_LENGTH => strlen($content), + OssClient::OSS_CONTENT_TYPE => $file->getMime(), + // 禁用字符编码转换 + 'Content-Encoding' => 'binary', + ]; + $res = $ossClient->putObject($bucket, $object, $content, $options); + Log::write('上传文件结果' . json_encode($res)); + if(!empty($res['info'])) { + return $this->ApiDataReturn(['data' => $befor_url . '/' . $object, 'msg' => 'success', 'code' => 0]); + }else { + $this->error('上传失败'); + } + } catch(OssException $e) { + $this->error($e->getMessage()); + } + + + } + + +} \ No newline at end of file diff --git a/app/czg/app/controller/AnnouncementController.php b/app/czg/app/controller/AnnouncementController.php new file mode 100644 index 0000000..084f078 --- /dev/null +++ b/app/czg/app/controller/AnnouncementController.php @@ -0,0 +1,22 @@ +request->get(); + return $this->ApiDataReturn(\app\czg\app\model\Announcement::list($get['type'])); + } + + + + +} \ No newline at end of file diff --git a/app/czg/app/controller/BannerController.php b/app/czg/app/controller/BannerController.php new file mode 100644 index 0000000..6595224 --- /dev/null +++ b/app/czg/app/controller/BannerController.php @@ -0,0 +1,31 @@ +request->get(); + if(empty($get['classify'])) { + $this->error('参数不完整'); + } + $banner = \app\api\model\Banner::where(['classify' => $get['classify'], 'state' => 1])->order('sort', 'desc')->select()->toArray(); + $this->success('ok', convertToCamelCase($banner)); + } + + public function test() + { + $qdAward = (new CommonInfo())->getByCode(915)['value']; + $this->successWithData(bccomp($qdAward, "0") > 0); + } + +} \ No newline at end of file diff --git a/app/czg/app/controller/CashController.php b/app/czg/app/controller/CashController.php new file mode 100644 index 0000000..8139920 --- /dev/null +++ b/app/czg/app/controller/CashController.php @@ -0,0 +1,94 @@ +auth->getUser()['user_id']; + $comm = CommonInfo::where(['type' => 928])->find()->value; + if($comm == 0) { + $this->success('ok', true); + } + $redis_can_cash = Cache::get('cash:canCash:' . $user_id); + if($redis_can_cash) { + $this->success('ok', true); + } + } + + private function checkCanCash($userId) + { + $canCash = RedisUtils::isCanCash($userId); + if (!$canCash) { + $val = (new CommonInfo())->getByCode(928)['value']; + if ($val == '1') { + throw new SysException("您未观看激励广告,请先观看"); + } + } + } + + /** + * 提现接口 + */ + public function withdraw() + { + $amount = $this->request->get('amount'); + $isAlipay = $this->request->get('isAlipay/d'); + $userId = $this->getUserId(); + self::checkCanCash($userId); + $info = (new CommonInfo())->getByCode(930); + if (!$info) { + $this->error("当前时间段未开启提现功能"); + } + $timeScope = explode('~', $info['value']); // 如 08:30~17:30 + $today = date('Y-m-d'); + + // 拼接今日的开始和结束时间 + $beginTime = strtotime($today . ' ' . $timeScope[0]); + $endTime = strtotime($today . ' ' . $timeScope[1]); + $now = time(); + + // 判断是否在时间范围内 + if ($now < $beginTime || $now > $endTime) { + throw new SysException("提现时间为每天" . $info['value']); + } + + + + debounce("withdraw:".$userId, 30); + runWithLock("lock:withdraw:{$userId}", 300, function () use ($userId, $amount, $isAlipay) { + WithDraw::goWithDraw($userId, $amount, '', false, $isAlipay == 1); + }); + + $this->success('提现成功,将在三个工作日内到账,请耐心等待!'); + + } + + + + /** + * 查询提现记录列表 + */ + public function selectPayDetails() + { + $get = $this->request->get(); + $cashOut[ 'user_id'] = $this->auth->user_id; + return $this->ApiDataReturn(CashModel::selectPayDetails($cashOut, $get, true, $this->auth->getUser())); + } + + + + +} \ No newline at end of file diff --git a/app/czg/app/controller/CommonController.php b/app/czg/app/controller/CommonController.php new file mode 100644 index 0000000..19c2cec --- /dev/null +++ b/app/czg/app/controller/CommonController.php @@ -0,0 +1,110 @@ +request->request('id'); + $config = array( + 'codeSet' => '123456789', // 验证码字符集合 + 'fontSize' => 22, // 验证码字体大小(px) + 'useCurve' => false, // 是否画混淆曲线 + 'useNoise' => true, // 是否添加杂点 + 'length' => 4, // 验证码位数 + 'bg' => array(255, 255, 255), // 背景颜色 + ); + + $captcha = new Captcha($config); + return $captcha->entry($captchaId); + } + + /** + * 点选验证码 + */ + public function clickCaptcha(): void + { + $id = $this->request->request('id/s'); + $captcha = new ClickCaptcha(); + $this->success('', $captcha->creat($id)); + } + + /** + * 点选验证码检查 + * @throws Throwable + */ + public function checkClickCaptcha(): void + { + $id = $this->request->post('id/s'); + $info = $this->request->post('info/s'); + $unset = $this->request->post('unset/b', false); + $captcha = new ClickCaptcha(); + if ($captcha->check($id, $info, $unset)) $this->success(); + $this->error(); + } + + /** + * 刷新 token + * 无需主动删除原 token,由 token 驱动自行实现过期 token 清理,可避免并发场景下无法获取到过期 token 数据 + */ + public function refreshToken(): void + { + $refreshToken = $this->request->post('refreshToken'); + $refreshToken = Token::get($refreshToken); + + if (!$refreshToken || $refreshToken['expire_time'] < time()) { + $this->error(__('Login expired, please login again.')); + } + + $newToken = Random::uuid(); + + // 管理员token刷新 + if ($refreshToken['type'] == AdminAuth::TOKEN_TYPE . '-refresh') { + Token::set($newToken, AdminAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)Config::get('buildadmin.admin_token_keep_time')); + } + + // 会员token刷新 + if ($refreshToken['type'] == UserAuth::TOKEN_TYPE . '-refresh') { + Token::set($newToken, UserAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)Config::get('buildadmin.user_token_keep_time')); + } + + $this->success('', [ + 'type' => $refreshToken['type'], + 'token' => $newToken + ]); + } + + public function getAppUseKv() + { + return $this->resultApi(\app\common\model\Common::getAppUseKv()); + } + + public function type() + { + $type = $this->request->route('num'); + $data = convertToCamelCase(Db::connect(config('think-orm.search_library'))->name('common_info')->where('type', $type)->find()); + $this->success('ok', $data, 0); + } + + + + + +} \ No newline at end of file diff --git a/app/czg/app/controller/CourseCollectController.php b/app/czg/app/controller/CourseCollectController.php new file mode 100644 index 0000000..c93f7e4 --- /dev/null +++ b/app/czg/app/controller/CourseCollectController.php @@ -0,0 +1,153 @@ +getUserId(); + $model = DatabaseRoute::getDb('course_collect', $userId, true); + $info = $model->where([ + 'user_id' => $userId, + 'classify' => 3, + 'course_id' => $params['courseId'], + ])->order('create_time', 'desc')->limit(1)->find(); + if ($info) { + // 更新记录 + $model + ->where([ + 'course_collect_id' => $info['course_collect_id'], + 'user_id' => $userId, + ]) + ->update([ + 'update_time' => date('Y-m-d H:i:s'), + 'course_details_id' => $params['courseDetailsId'], + ]); + } else { + // 插入新记录 + $model->insert([ + 'user_id' => $userId, + 'course_id' => $params['courseId'], + 'course_details_id' => $params['courseDetailsId'], + 'classify' => 3, + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + ]); + } + } + + public function upGoodNum($params, $isAdd) + { + $courseDetails = $course_d = DatabaseRoute::getDb('course_details', ['course_id' => $params['courseId']]); + $courseDetails = $courseDetails->where([ + 'course_id' => $params['courseId'], + 'course_details_id' => $params['courseDetailsId'], + ])->find(); + + if (!$courseDetails) { + return; // 数据不存在,不处理 + } + + $goodNum = isset($courseDetails['good_num']) ? (int)$courseDetails['good_num'] : 0; + + if ($isAdd) { + $goodNum += 1; // 点赞 + } else { + $goodNum -= 1; // 取消点赞 + + } + + if ($goodNum < 0) { + return; // 防止出现负数 + } + Db::connect(DatabaseRoute::getConnection('course_details', ['course_id' => $params['courseId']], true))->name('course_details')->where([ + 'course_id' => $params['courseId'], + 'course_details_id' => $params['courseDetailsId'], + ])->update(['good_num' => $goodNum]); + } + + public function insertCourseCollect() + { + $userId = $this->getUserId(); + $data = $this->request->only(['classify', 'courseDetailsId', 'courseId', 'type']); + // classify 1收藏 2点赞 3订单 + (new CourseCollectValidate())->scene('insertCourseCollect')->check($data); + if ($data['classify'] === 3) { + $this->insertOrder($data); + } else { + $where = [ + 'user_id' => $userId, + 'classify' => $data['classify'], + 'course_id' => $data['courseId'], + ]; + + if ($data['classify'] === 2) { + if(empty($data['courseDetailsId'])) { + $this->error('courseDetailsId不能为空', [], -1); + } + $where[] = ['course_details_id', 'eq', $data['courseDetailsId']]; + } + $model = DatabaseRoute::getDb('course_collect', $this->getUserId()); + $info = $model->where($where)->find(); + if ($data['type'] == 1) { + if ($info == null) { + $model = DatabaseRoute::getDb('course_collect', $this->getUserId(), true); + $model->insert([ + 'user_id' => $this->getUserId(), + 'create_time' => getNormalDate(), + 'update_time' => getNormalDate(), + 'course_id' => $data['courseId'], + 'course_details_id' => !empty($data['courseDetailsId'])?$data['courseDetailsId']:null, + 'classify' => $data['classify'], + ]); + } + }else{ + if ($info) { + DatabaseRoute::getDb('course_collect', $this->getUserId(), true)->where([ + 'course_collect_id' => $info['course_collect_id'] + ])->delete(); + } + } + } + + if ($data['classify'] == 2) { + $this->upGoodNum($data, $data['type']); + } + + $this->success(); + } + + + // app查询收藏短剧信息 + public function selectByUserId() + { + $get = $this->request->get(); + return $this->ApiDataReturn(\app\api\model\Course::selectByUserId($get, $this->auth->user_id)); + + } + + + // 我的追剧和我的喜欢数量 + public function collectVideoSummary() + { + return $this->ApiDataReturn(\app\api\model\Course::collectVideoSummary($this->auth->user_id)); + } + + + +} \ No newline at end of file diff --git a/app/czg/app/controller/CourseController.php b/app/czg/app/controller/CourseController.php index 0def9d7..432b3c3 100644 --- a/app/czg/app/controller/CourseController.php +++ b/app/czg/app/controller/CourseController.php @@ -1,16 +1,51 @@ request->get(); + $res = Course::selectCourse($post); + return $this->ApiDataReturn($res); + } + + + // 推荐视频 + public function selectCourseDetailsList() + { + if($this->auth->isLogin()) { + $user_id = $this->auth->user_id; + }else { + $user_id = 0; + } + $post = $this->request->get(); + $res = Course::selectCourseDetailsList($post, $user_id); + return $this->ApiDataReturn($res); + } + + // 获取抽奖红包提示 + public function getRedEnvelopeTips() + { + $user_id = $this->auth->user_id; + $res = Course::getRedEnvelopeTips($user_id); + return $this->ApiDataReturn($res); + } + + // 查看视频 + public function viewCourse() + { + $get = $this->request->get(); + $res = Course::viewCourse($get); + return $this->ApiDataReturn($res); + } + + // 根据id查询短剧集数列表 public function courseSets() { $get = $this->request->get(); diff --git a/app/czg/app/controller/DiscSpinningController.php b/app/czg/app/controller/DiscSpinningController.php new file mode 100644 index 0000000..96705d9 --- /dev/null +++ b/app/czg/app/controller/DiscSpinningController.php @@ -0,0 +1,316 @@ +request->get(); + $userInfo = $this->auth->getUser(); + Log::info('抽奖'. json_encode($userInfo)); + + debounce("user:disc-spinning:draw:".$userInfo['user_id'], 1); + $userId = $userInfo['user_id']; + $resp = []; + runWithLock("user:disc-spinning:limit:user:lock:".$this->getUserId(), 60, function () use ($params, $userId, &$resp, $userInfo) { + DatabaseRoute::transactionXa(function () use ($params, $userId, &$resp, $userInfo) { + // 查询抽奖次数 + $count = DiscSpinningRecord::countDraw($userId); + // 免费两次之后校验实名 + if ($count == 2 && !TbUser::checkEnable($userInfo)) { + $this->error('请实名认证后继续抽奖'); + } + + Log::info("用户id: $userId, 抽奖次数: $count"); + // 订单抽奖 + if (!isset($params['source']) || $params['source'] == 1) { + + $drawCount = (new CommonInfo())->getByCodeToInt(901); + if ($count >= $drawCount) { + $this->error('当日可抽奖次数已超限'); + } + + // 校验订单笔数 + $orders = Orders::selectOrdersByDay($userId); + if (!$orders) { + throw new SysException("无可用抽奖机会"); + } + $params['sourceId'] = $orders['orders_id']; + }else{ + $this->error("八嘎"); + } + + if (!isset($params['sourceId'])) { + throw new CzgException("异常请求"); + } + + $draws = self::draws($count + 1, $orders['pay_money'], $params['sourceId'], $userId, $params['source']); + $resp = $draws; +// $this->receive($draws); + }); + }); + pushQueue(DiscReceiveQueue::class, [ + 'draws' => $resp + ]); + $resp = convertToCamelCase($resp); + $resp['img'] = $resp['imgUrl']; + $this->successWithData($resp); + } + + public static function draws($drawCount, $orderAmount, $sourceId, $userId, $source) + { + $result = [ + 'name' => '谢谢惠顾', + 'source_id' => $sourceId, + 'user_id' => $userId, + 'img_url' => '', + 'type' => 1, + 'number' => 1, + 'draw_day' => date('Y-m-d'), + 'create_time' => date('Y-m-d H:i:s'), + 'source' => $source == 1 ? 'order' : 'task' + ]; + + // 查询奖项 + $prizes = Db::name('disc_spinning')->where([ + 'disc_type' => $source + ])->order('number', 'asc')->select()->toArray(); + + if (empty($prizes)) { + DatabaseRoute::getDb('disc_spinning_record', $userId, true, true)->insert($result); + return $result; + } + + // 获取最大概率 + $maxNumber = array_reduce($prizes, function ($carry, $prize) { + return bccomp($prize['number'], $carry) > 0 ? $prize['number'] : $carry; + }, '0'); + + // 最大值为 0,直接谢谢惠顾 + if (bccomp($maxNumber, '0') === 0) { + $record = $result; + DatabaseRoute::getDb('disc_spinning_record', $userId, true, true)->insert($record); + return $record; + } + + // 获取随机数(整数) + if ($maxNumber > 1) { + $randomNum = rand(0, (int)$maxNumber - 1); + } else { + $randomNum = rand(0, (int)$maxNumber); // 或者其它默认值 + } + + // 查询奖励金额列表(模拟 redis 逻辑) + $amounts = Db::name('disc_spinning_amount')->where([ + 'status' => 1, + 'type' => $source + ])->order('max_amount', 'asc')->select()->toArray(); + + // 按 num 分组 + $amountMaps = []; + foreach ($amounts as $item) { + $num = isset($item['num']) ? intval($item['num']) : 0; + $amountMaps[$num][] = $item; + } + + // 按照 drawCount 向下查找匹配组 + $filteredAmounts = []; + if (!empty($amountMaps)) { + for ($i = $drawCount; $i >= 0; $i--) { + if (isset($amountMaps["{$i}"])) { + $filteredAmounts = $amountMaps["{$i}"]; + break; + } + } + } + + // 抽奖逻辑 + $finalPrize = null; + foreach ($prizes as $prize) { + if (bccomp((string)$randomNum, $prize['number']) < 0) { + if ($prize['type'] == 2) { + // 金额类奖品 + $maxAmount = (new CommonInfo())->getByCodeToInt(900); // 最大金额限制 + $baseRandom = mt_rand() / mt_getrandmax(); + $baseRandom = bccomp($baseRandom, "1", 2) ? 0.99 : $baseRandom; + $baseAmount = 0; + $resultAmount = 0; + + foreach ($filteredAmounts as $amount) { + if ($baseRandom < $amount['random']) { + $resultAmount = $baseAmount + (mt_rand() / mt_getrandmax()) * $amount['max_amount']; + break; + } + $baseAmount = $amount['max_amount']; + } + + if ($resultAmount < 0.01) { + $resultAmount = 0.01; + } + $randomFactor = mt_rand(50, 90) / 100; // 生成 0.5 到 0.9 的随机数 + $resultAmount = $resultAmount * $randomFactor; + $resultAmount += $orderAmount; + if ($resultAmount > $maxAmount) { + $resultAmount = $maxAmount; + } + + + $finalPrize = [ + 'name' => $prize['name'], + 'type' => 2, + 'number' => round($resultAmount, 2), + 'id' => $prize['id'], + 'url' => $prize['url'] ?? '' + ]; + break; + } else { + // 非金额奖品 + if ($source != 1) { + $finalPrize = [ + 'name' => $prize['name'], + 'type' => $prize['type'], + 'number' => 1, + 'id' => $prize['id'], + 'url' => $prize['url'] ?? '' + ]; + break; + } + } + } + } + + if (!$finalPrize) { + // 没抽中任何奖品,默认“谢谢惠顾” + $finalPrize = [ + 'name' => '谢谢惠顾', + 'type' => 1, + 'number' => 1, + 'id' => null, + 'url' => '' + ]; + } + + // 构建记录 + $record = [ + 'id' => Random::generateRandomPrefixedId(19), + 'name' => $finalPrize['name'], + 'source_id' => $sourceId, + 'user_id' => $userId, + 'img_url' => $finalPrize['url'] ?? '', + 'type' => $finalPrize['type'], + 'number' => $finalPrize['number'], + 'draw_day' => date('Y-m-d'), + 'create_time' => date('Y-m-d H:i:s'), + 'source' => $source == 1 ? 'order' : 'task' + ]; + + // 保存 + DatabaseRoute::getDb('disc_spinning_record', $userId, true)->insert($record); + + $record['discSpinningId'] = $finalPrize['id']; + return $record; + } + + + + /** + * 获取大转盘抽奖次数 + */ + public function drawCount() + { + $get = $this->request->get(); + if(empty($get['source'])) { + $soure = 1; + }else { + $soure = $get['source']; + } + $user = $this->auth->getUser(); + $drawCount = (int)CommonInfo::where(['type' => 901])->find()->value; + $data['sum'] = $drawCount; + if(!empty($soure) && $soure != 1) { + if($soure == 2) { + $sourceType = 'taskW'; + }else { + $sourceType = 'taskM'; + } + $spinningCount = DatabaseRoute::getDb('disc_spinning_record', $user['user_id'])->where(['source' => $sourceType])->count(); + if($spinningCount != null && $spinningCount > 0) { + $data['count'] = 0; + }else { + $i = DiscSpinningRecord::countTaskDisc($user['user_id'], $soure); + $data['count'] = $i>0?1:0; + } + }else { + $i = DatabaseRoute::getDb('disc_spinning_record', $user['user_id'])->where(['source' => 'order'])->where([ + 'draw_day' => ['between', date('Y-m-d 00:00:00'), date('Y-m-d 11:59:59')] + ])->count(); + if($drawCount - $i > 0) { + $data['count'] = DiscSpinningRecord::selectOrdersCountStatisticsByDay($user['user_id'], $drawCount - $i); + }else { + $data['count'] = 0; + } + } + $this->success('ok', $data); + } + + + // 查询大转盘 + public function selectDiscSpinning() + { + $get = $this->request->get(); + if(empty($get['source'])) { + $this->error('参数不完整'); + } + $page = 1; + $limit = 20; + $db = Db::connect(config('database.search_library')); + $list = $db->name('disc_spinning') + ->where(['disc_type' => $get['source']]); + $count = $list->count(); + $list = $list->order('disc_type', 'asc') + ->order('odds', 'asc') + ->limit(page($page, $limit), $limit) + ->select()->toArray(); + foreach ($list as $k => &$v) { + $v['odds'] = null; + $v['number'] = null; + $v['discType'] = null; + $v['img'] = $v['url']; + } + $this->success('ok', [ + 'currPage' => $page, + 'pageSize' => $limit, + 'records' => convertToCamelCase($list), + 'totalCount' => $count, + 'totalPage' => ceil($count / $limit), + ]); + } +} \ No newline at end of file diff --git a/app/czg/app/controller/DiscSpinningRecordController.php b/app/czg/app/controller/DiscSpinningRecordController.php new file mode 100644 index 0000000..429e8fd --- /dev/null +++ b/app/czg/app/controller/DiscSpinningRecordController.php @@ -0,0 +1,41 @@ +request->get(); + if(empty($get['source']) || empty($get['page']) || empty($get['limit'])) { + $this->error('参数不完整'); + } + $user = $this->auth->getUser(); + $db = Db::connect(DatabaseRoute::getConnection('disc_spinning_record', ['user_id' => $user['user_id']])); + $list = $db->name('disc_spinning_record') + ->where(['user_id' => $user['user_id']]); + $count = $list->count(); + $list = $list->order('create_time', 'asc') + ->limit(page($get['page'], $get['limit']), $get['limit']) + ->select()->toArray(); + $this->success('ok', [ + 'currPage' => $get['page'], + 'pageSize' => $get['limit'], + 'records' => convertToCamelCase($list), + 'totalCount' => $count, + 'totalPage' => ceil($count / $get['limit']), + ]); + } + +} \ No newline at end of file diff --git a/app/czg/app/controller/EmsController.php b/app/czg/app/controller/EmsController.php new file mode 100644 index 0000000..823551c --- /dev/null +++ b/app/czg/app/controller/EmsController.php @@ -0,0 +1,108 @@ +request->post(['email', 'event', 'captchaId', 'captchaInfo']); + $mail = new Email(); + if (!$mail->configured) { + $this->error(__('Mail sending service unavailable')); + } + + $validate = Validate::rule([ + 'email' => 'require|email', + 'event' => 'require', + 'captchaId' => 'require', + 'captchaInfo' => 'require' + ])->message([ + 'email' => 'email format error', + 'event' => 'Parameter error', + 'captchaId' => 'Captcha error', + 'captchaInfo' => 'Captcha error' + ]); + if (!$validate->check($params)) { + $this->error(__($validate->getError())); + } + + // 检查验证码 + $captchaObj = new Captcha(); + $clickCaptcha = new ClickCaptcha(); + if (!$clickCaptcha->check($params['captchaId'], $params['captchaInfo'])) { + $this->error(__('Captcha error')); + } + + // 检查频繁发送 + $captcha = $captchaObj->getCaptchaData($params['email'] . $params['event']); + if ($captcha && time() - $captcha['create_time'] < 60) { + $this->error(__('Frequent email sending')); + } + + // 检查邮箱 + $userInfo = User::where('email', $params['email'])->find(); + if ($params['event'] == 'user_register' && $userInfo) { + $this->error(__('Email has been registered, please log in directly')); + } elseif ($params['event'] == 'user_change_email' && $userInfo) { + $this->error(__('The email has been occupied')); + } elseif (in_array($params['event'], ['user_retrieve_pwd', 'user_email_verify']) && !$userInfo) { + $this->error(__('Email not registered')); + } + + // 通过邮箱验证账户 + if ($params['event'] == 'user_email_verify') { + if (!$this->auth->isLogin()) { + $this->error(__('Please login first')); + } + if ($this->auth->email != $params['email']) { + $this->error(__('Please use the account registration email to send the verification code')); + } + // 验证账户密码 + $password = $this->request->post('password'); + if (!verify_password($password, $this->auth->password, ['salt' => $this->auth->salt])) { + $this->error(__('Password error')); + } + } + + // 生成一个验证码 + $code = $captchaObj->create($params['email'] . $params['event']); + $subject = __($params['event']) . '-' . get_sys_config('site_name'); + $body = __('Your verification code is: %s', [$code]); + + try { + $mail->isSMTP(); + $mail->addAddress($params['email']); + $mail->isHTML(); + $mail->setSubject($subject); + $mail->Body = $body; + $mail->send(); + } catch (PHPMailerException) { + $this->error($mail->ErrorInfo); + } + + $this->success(__('Mail sent successfully~')); + } +} \ No newline at end of file diff --git a/app/czg/app/controller/HelpWordController.php b/app/czg/app/controller/HelpWordController.php new file mode 100644 index 0000000..4bbfd7f --- /dev/null +++ b/app/czg/app/controller/HelpWordController.php @@ -0,0 +1,34 @@ +request->get(); + if(empty($get['types'])) { + $this->error('参数不完整'); + } + $word = new HelpClassify; + $word->setConnection(Config('think-orm.search_library')); + $result = $word + ->with('helpword') + ->where('types', $get['types'])->select()->toArray(); + $result = convertToCamelCase($result); + foreach ($result as $k => $v) { + $result[$k]['helpWordList'] = convertToCamelCase($v['helpword']); + unset($result[$k]['helpword']); + } + $this->success('ok', $result); + } + + +} \ No newline at end of file diff --git a/app/czg/app/controller/IndexController.php b/app/czg/app/controller/IndexController.php new file mode 100644 index 0000000..ea2e48c --- /dev/null +++ b/app/czg/app/controller/IndexController.php @@ -0,0 +1,84 @@ +auth->isLogin()) { +// $rules = []; +// $userMenus = $this->auth->getMenus(); +// +// // 首页加载的规则,验权,但过滤掉会员中心菜单 +// foreach ($userMenus as $item) { +// if ($item['type'] == 'menu_dir') { +// $menus[] = $item; +// } elseif ($item['type'] != 'menu') { +// $rules[] = $item; +// } +// } +// $rules = array_values($rules); +// } else { +// // 若是从前台会员中心内发出的请求,要求必须登录,否则会员中心异常 +// $requiredLogin = $this->request->get('requiredLogin/b', false); +// if ($requiredLogin) { +// +// // 触发可能的 token 过期异常 +// try { +// $token = get_auth_token(['ba', 'user', 'token']); +// $this->auth->init($token); +// } catch (TokenExpirationException) { +// $this->error(__('Token expiration'), [], 409); +// } +// +// $this->error(__('Please login first'), [ +// 'type' => $this->auth::NEED_LOGIN +// ], $this->auth::LOGIN_RESPONSE_CODE); +// } +// +// $rules = Db::name('user_rule') +// ->where('status', 1) +// ->where('no_login_valid', 1) +// ->where('type', 'in', ['route', 'nav', 'button']) +// ->order('weigh', 'desc') +// ->select() +// ->toArray(); +// $rules = Tree::instance()->assembleChild($rules); +// } +// +// $this->success('', [ +// 'site' => [ +// 'siteName' => get_sys_config('site_name'), +// 'version' => get_sys_config('version'), +// 'cdnUrl' => full_url(), +// 'upload' => keys_to_camel_case(get_upload_config(), ['max_size', 'save_name', 'allowed_suffixes', 'allowed_mime_types']), +// 'recordNumber' => get_sys_config('record_number'), +// 'cdnUrlParams' => Config::get('buildadmin.cdn_url_params'), +// ], +// 'openMemberCenter' => Config::get('buildadmin.open_member_center'), +// 'userInfo' => $this->auth->getUserInfo(), +// 'rules' => $rules, +// 'menus' => $menus, +// ]); + } +} \ No newline at end of file diff --git a/app/czg/app/controller/InviteController.php b/app/czg/app/controller/InviteController.php new file mode 100644 index 0000000..2e0e022 --- /dev/null +++ b/app/czg/app/controller/InviteController.php @@ -0,0 +1,29 @@ +auth->getUser()); + return $this->ApiDataReturn($res); + } + + + + // 查看我邀请的人员列表(查看所有邀请列表) + public function selectInviteByUserIdLists() + { + $get = $this->request->get(); + $res = \app\api\model\Invite::selectInviteByUserIdLists($this->auth->getUser(), $get, 'api'); + return $this->ApiDataReturn($res); + } + +} \ No newline at end of file diff --git a/app/czg/app/controller/LoginController.php b/app/czg/app/controller/LoginController.php index bc3cc95..62c4c9b 100644 --- a/app/czg/app/controller/LoginController.php +++ b/app/czg/app/controller/LoginController.php @@ -16,7 +16,7 @@ use app\common\facade\Token; use app\common\controller\Frontend; use app\api\validate\User as UserValidate; -class LoginController extends BaseController +class LoginControllerController extends BaseController { protected array $noNeedLogin = ['*']; @@ -50,7 +50,7 @@ class LoginController extends BaseController // 发送验证码 public function sendMsg() { - $get = $this->request->route(); + $get = $this->request->route->param(); return $this->ApiDataReturn(Msg::sendMsg($get['phone'], $get['event'])); } diff --git a/app/czg/app/controller/MessageController.php b/app/czg/app/controller/MessageController.php new file mode 100644 index 0000000..44e8f95 --- /dev/null +++ b/app/czg/app/controller/MessageController.php @@ -0,0 +1,30 @@ +ApiDataReturn(MessageModel::getList($this->request->get())); + } + public function sendMessage() + { + $post = $this->request->post(); + return $this->ApiDataReturn(MessageModel::sendMessage($post, $this->auth->user_id)); + } + + + public function selectMessageByUserId() + { + $get = $this->request->get(); + $get['user_id'] = $this->getUserId(); + return $this->ApiDataReturn(MessageModel::getList($get)); + } + +} \ No newline at end of file diff --git a/app/czg/app/controller/MoneyDetailsController.php b/app/czg/app/controller/MoneyDetailsController.php new file mode 100644 index 0000000..c67182d --- /dev/null +++ b/app/czg/app/controller/MoneyDetailsController.php @@ -0,0 +1,23 @@ +ApiDataReturn(UserMoney::selectUserMoney($this->auth->user_id)); + } + public function queryUserMoneyDetails() + { + $get = $this->request->get(); + return $this->ApiDataReturn(UserMoney::queryUserMoneyDetails($this->auth->user_id, $get)); + } + + + +} \ No newline at end of file diff --git a/app/czg/app/controller/OrderController.php b/app/czg/app/controller/OrderController.php index 3cb302c..751e2fb 100644 --- a/app/czg/app/controller/OrderController.php +++ b/app/czg/app/controller/OrderController.php @@ -6,8 +6,8 @@ use app\api\model\Orders; use app\api\model\UserMoney; use app\common\controller\BaseController; use app\common\library\DatabaseRoute; -use app\czg\app\model\CommonInfo; -use app\czg\app\model\TbUser; +use app\api\model\CommonInfo; +use app\api\model\TbUser; use app\enums\ErrEnums; use app\exception\SysException; use app\utils\RedisUtils; diff --git a/app/czg/app/controller/TaskCenterController.php b/app/czg/app/controller/TaskCenterController.php new file mode 100644 index 0000000..5e35b0f --- /dev/null +++ b/app/czg/app/controller/TaskCenterController.php @@ -0,0 +1,38 @@ +auth->getUser()['user_id']; + return $this->ApiDataReturn(TaskCenterRecord::selectTaskCenter($user_id, $this->auth->getUser()['inviter_user_id'])); + } + + public function taskReceive() + { + $get = $this->request->get(); + if(empty($get['id'])) { + $this->error('参数错误'); + } + $id = $get['id']; + $userId = $this->auth->getUser()['user_id']; + if($id !== null && $id == 19) { + TaskCenterRecord::addBlackUser($userId, '任务中心领取'); + } + return $this->ApiDataReturn(TaskCenterRecord::taskReceive($userId, $id, $this->auth->getUser()['inviter_user_id'])); + } + + +} \ No newline at end of file diff --git a/app/czg/app/controller/UniCallBackController.php b/app/czg/app/controller/UniCallBackController.php new file mode 100644 index 0000000..9bcd08b --- /dev/null +++ b/app/czg/app/controller/UniCallBackController.php @@ -0,0 +1,42 @@ + $this->request->param('adpid'), + 'provider' => $this->request->param('provider'), + 'platform' => $this->request->param('platform'), + 'sign' => $this->request->param('sign'), + 'trans_id' => $this->request->param('trans_id'), + 'user_id' => $this->request->param('user_id'), + 'extra' => $this->request->param('extra', '') // 可选参数,默认空字符串 + ]; + // 记录日志 + Log::write("接收到uni-ad广告完播回调,回调信息: " . json_encode($params)); + // 调用服务处理回调 + $result = UniAdCallbackRecord::adCallBack($params); + // 返回成功响应 + $this->successWithData($result); + } + + + +} \ No newline at end of file diff --git a/app/czg/app/controller/UserController.php b/app/czg/app/controller/UserController.php new file mode 100644 index 0000000..b49186c --- /dev/null +++ b/app/czg/app/controller/UserController.php @@ -0,0 +1,389 @@ +auth->user_id); +// Db::connect(DatabaseRoute::getConnection('course_collect', ['user_id' => 10])) +// ->name('course_collect') +// ->where('user_id', 10) +// ->find(); + + // 查询 +// $user = new SysUser(); +// // 查询或者删除前设置分库连接 +// $user->setConnection($user::findbefore($user, ['user_id' => 1072634324253292306])); +// $user = $user->where(['user_id' => 1072634324253292306]); +//// $res = $user->delete(); +// $data = $user->find(); +// p($data); + + + +// SysUser::update(['username' => 99999], ['user_id' => 130677]); +// SysUser::create([ +// 'user_id' => rand(000000, 999999), +// 'username' => '哎呀' . rand(000000, 999999), +// ]); + + } + + + + public function bindAlipay() + { + $get = $this->request->param(); + $user = $this->auth->getUser(); + // 获取请求参数 + $userId = $user['user_id']; + $zhiFuBao = $get['zhiFuBao'] ?? null; + $certName = $get['certName'] ?? null; + + // 验证参数不能为空 + if (empty($zhiFuBao) || empty($certName)) { + $this->error("支付宝账号或姓名不能为空"); + } + + // 包含*号直接返回成功(业务特殊处理) + if (str_contains($zhiFuBao, '*') || str_contains($certName, '*')) { + $this->success(); + } + + $slave_db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userId])); + // 查询用户信息 + $userEntity = $slave_db->name('tb_user') + ->where('user_id', $userId) + ->find(); + + // 检查是否与原有信息一致 + if ($zhiFuBao == $userEntity['zhi_fu_bao'] && $certName == $userEntity['zhi_fu_bao_name']) { + $this->success(); + } + + // 检查支付宝账号是否已被其他用户绑定 + $count = $slave_db->name('tb_user') + ->where('user_id', '<>', $userId) + ->where('zhi_fu_bao_name', $certName) + ->where('zhi_fu_bao', $zhiFuBao) + ->count(); + + if ($count > 0) { + $this->error("支付宝信息修改失败: 此支付宝账号已被绑定"); + } + + // 检查实名认证信息是否匹配 + $userInfo = $slave_db->name('user_info') + ->where('user_id', $userId) + ->find(); + + if ($userInfo) { + if (!empty($userInfo['cert_name']) && $certName != $userInfo['cert_name']) { + $this->error("支付宝信息修改失败: 姓名与实名认证信息不相符"); + } + } + + // 获取配置的限制次数 + $limitConfig = Db::name('common_info') + ->where('type', 924) + ->value('value'); + + // 获取当前时间戳(秒) + $currentTime = time(); + // 获取本月最后一天的日期(如:2024-08-31) + $lastDayOfMonth = date('Y-m-t'); + // 拼接本月最后一刻的时间(23:59:59) + $endOfMonth = $lastDayOfMonth . ' 23:59:59'; + // 转换为时间戳 + $endTime = strtotime($endOfMonth); + // 计算剩余秒数(若当前时间已过本月最后一刻,返回0) + $remainingSeconds = max(0, $endTime - $currentTime); + + // 检查支付宝账号每月绑定次数限制 + if (!\app\common\model\Common::isAccessAllowed($zhiFuBao, $certName, (int)$limitConfig, $remainingSeconds, true)) { + $this->error("支付宝信息修改失败: 相同支付宝账号每月可绑定次数已用完"); + } + + // 检查用户每月修改次数限制 + $updateLimitConfig = Db::name('common_info') + ->where('id', 925) + ->value('value'); + + if (!\app\common\model\Common::isAccessAllowed((string)$userId, "updateZFB", (int)$updateLimitConfig, $remainingSeconds)) { + $this->error("支付宝信息修改失败: 每月可修改次数已用完,请联系管理员"); + } + + // 更新用户支付宝信息 + $master_db = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $userId], true)); + $master_db->name('tb_user') + ->where('user_id', $userId) + ->update([ + 'zhi_fu_bao' => $zhiFuBao, + 'zhi_fu_bao_name' => $certName, + // 可添加更新时间字段 + 'update_time' => date('Y-m-d H:i:s') + ]); + $this->success('ok', 1); + } + + + + + public function updatePhone() + { + $user_id = $this->auth->user_id; + $post = $this->request->post(); + if(empty($post['phone']) || empty($post['msg'])) { + $this->error('参数不正常'); + } + + // 验证码 + if(Msg::checkCode($post['phone'], $post['msg'])) { + $user = TbUser::GetByusername($post['phone']); + if($user) { + $this->error('手机号已经存在'); + } + $db = DatabaseRoute::getDb('tb_user', $user_id, true)->where(['user_id' => $user_id])->update(['phone' => $post['phone']]); + if($db) { + $this->success('操作成功'); + }else { + $this->error('操作失败'); + } + }else { + $this->error('验证码错误'); + } + } + + + + + + /** + * 会员签入(登录和注册) + * @throws Throwable + */ + public function checkIn(): void + { + $openMemberCenter = Config::get('buildadmin.open_member_center'); + if (!$openMemberCenter) { + $this->error(__('Member center disabled')); + } + + // 检查登录态 + if ($this->auth->isLogin()) { + $this->success(__('You have already logged in. There is no need to log in again~'), [ + 'type' => $this->auth::LOGGED_IN + ], $this->auth::LOGIN_RESPONSE_CODE); + } + + $userLoginCaptchaSwitch = Config::get('buildadmin.user_login_captcha'); + + if ($this->request->isPost()) { + $params = $this->request->post(['tab', 'email', 'mobile', 'username', 'password', 'keep', 'captcha', 'captchaId', 'captchaInfo', 'registerType']); + + // 提前检查 tab ,然后将以 tab 值作为数据验证场景 + if (!in_array($params['tab'] ?? '', ['login', 'register'])) { + $this->error(__('Unknown operation')); + } + + $validate = new UserValidate(); + try { + $validate->scene($params['tab'])->check($params); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + + if ($params['tab'] == 'login') { + if ($userLoginCaptchaSwitch) { + $captchaObj = new ClickCaptcha(); + if (!$captchaObj->check($params['captchaId'], $params['captchaInfo'])) { + $this->error(__('Captcha error')); + } + } + $res = $this->auth->login($params['username'], $params['password'], !empty($params['keep'])); + } elseif ($params['tab'] == 'register') { + $captchaObj = new Captcha(); + if (!$captchaObj->check($params['captcha'], $params[$params['registerType']] . 'user_register')) { + $this->error(__('Please enter the correct verification code')); + } + $res = $this->auth->register($params['username'], $params['password'], $params['mobile'], $params['email']); + } + + if (isset($res) && $res === true) { + $this->success(__('Login succeeded!'), [ + 'userInfo' => $this->auth->getUserInfo(), + 'routePath' => '/user' + ]); + } else { + $msg = $this->auth->getError(); + $msg = $msg ?: __('Check in failed, please try again or contact the website administrator~'); + $this->error($msg); + } + } + + $this->success('', [ + 'userLoginCaptchaSwitch' => $userLoginCaptchaSwitch, + 'accountVerificationType' => get_account_verification_type() + ]); + } + + public function logout(): void + { + if ($this->request->isPost()) { + $refreshToken = $this->request->post('refreshToken', ''); + if ($refreshToken) Token::delete((string)$refreshToken); + $this->auth->logout(); + $this->success(); + } + } + + public function selectNewApp() + { + $this->n_success(['data' => apiconvertToCamelCase(Db::connect(get_slave_connect_name())->name('app')->order('version', 'desc')->select()->toArray())]); + } + + /** + * 实名接口 + */ + public function realNameAuth() + { + $params = $this->request->post(); +// (new UserValidate())->scene('realName')->check($params); + $params['certNum'] = trim($params['certNum']); + $params['certName'] = trim($params['certName']); + $userId = $this->getUserId(); + $regex = '/^[0-9Xx*]+$/'; + if (!preg_match($regex, $params['certNum'])) { + $this->success(); + } + + $count = DatabaseRoute::getDb('user_info', $userId)->where([ + 'cert_name' => $params['certName'], + 'cert_no' => $params['certNum'], + ])->count(); + if ($count) { + $this->error('已完成实名认证,无需重复操作'); + } + + $count = DatabaseRoute::getAllDbData('tb_user', function ($query) use ($params) { + return $query->where([ + 'cert_name' => $params['certName'], + 'cert_no' => $params['certNum'], + ]); + })->count(); + if ($count >= 3) { + $this->error('实名认证失败: 1个身份证号码最多绑定3个账号'); + } + + RedisUtils::checkRealCount($userId); + + $resp = AliUtils::verify($params['certName'], $params['certNum'], $params['accountNo'], $params['mobile']); + $userInfo = UserInfo::getByUserIdOrSave($userId); + $userInfo['cert_name'] = $params['certName']; + $userInfo['cert_no'] = $params['certNum']; + $userInfo['account_no'] = $params['accountNo']; + $userInfo['mobile'] = $params['mobile']; + $userInfo['bank_name'] = $resp['bankName']; + $userInfo['province'] = $params['province'] ?? ''; + $userInfo['city'] = $params['city'] ?? ''; + $userInfo['bank_branch'] = $params['bankBranch']; + $userInfo['resp_json'] = $resp['respJson']; + $userInfo['update_time'] = getNormalDate(); + DatabaseRoute::getDb('user_info', $userId, true, true)->update($userInfo); + + RedisUtils::setRealCount($userId); + + // 校验实名黑名单 + $count = Db::name('tb_user_blacklist')->where([ + 'id_card_no' => $params['certNum'] + ])->count(); + if ($count) { + DatabaseRoute::getDb('tb_user', $userId, true, true)->update([ + 'status' => 0, + 'plat_form' => '异常行为用户:实名信息异常 在黑名单' + ]); + throw new SysException("异常行为: 您的实名信息存在异常行为"); + } + + $this->successWithData(1); + } + + public function selectUserById() + { + $user = $this->auth->getUser(); + // 获取用户详情 + $userInfo = DatabaseRoute::getDb('user_info', $user['user_id'])->find(); + $userInfo['resp_json'] = null; // 移除敏感字段 + + // 对用户详情中的敏感信息进行脱敏 + if (!empty($userInfo['account_no'])) { + $userInfo['account_no'] = bankCard($userInfo['account_no']); + } + + if (!empty($userInfo['mobile'])) { + $userInfo['mobile'] = maskPhoneNumber($userInfo['mobile']); + } + + if (!empty($userInfo['cert_no'])) { + $userInfo['cert_no'] = idCardNum($userInfo['cert_no'], 3, 2); + } + + // 对支付宝信息进行脱敏 + if (!empty($user['zhi_fu_bao'])) { + if (filter_var($user['zhi_fu_bao'], FILTER_VALIDATE_EMAIL)) { + $user['zhi_fu_bao'] = email($user['zhi_fu_bao']); + } else { + $user['zhi_fu_bao'] = maskPhoneNumber($user['zhi_fu_bao']); + } + } + + // 合并用户信息和用户详情 + $map = array_merge($user, $userInfo); + $map['user_id'] = (string)$map['user_id']; // 确保用户ID为字符串类型 + + // 如果支付宝姓名为空,使用实名认证姓名 + if (empty($user['zhi_fu_bao_name']) && !empty($userInfo['cert_name'])) { + $map['zhi_fu_bao_name'] = $userInfo['cert_name']; + } + $this->success('ok', convertToCamelCase($map)); + } + + public function updateUsers() + { + $post = $this->request->post(); + $user = $this->auth->getUser(); + $db_name = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $user['user_id']], true)); + $db_name->name('tb_user')->where(['user_id' => $user['user_id']])->update(['user_name' => $post['userName'], 'avatar' => $post['avatar']]); + $this->success('ok'); + } + +} \ No newline at end of file diff --git a/app/czg/app/controller/UserPrizeExchangeController.php b/app/czg/app/controller/UserPrizeExchangeController.php new file mode 100644 index 0000000..4bf49d2 --- /dev/null +++ b/app/czg/app/controller/UserPrizeExchangeController.php @@ -0,0 +1,24 @@ +request->get(); + return $this->ApiDataReturn(\app\api\model\UserPrizeExchange::pages($get, $this->auth->user_id)); + } + +} \ No newline at end of file diff --git a/app/czg/app/controller/UserSignRecordController.php b/app/czg/app/controller/UserSignRecordController.php new file mode 100644 index 0000000..d21cc86 --- /dev/null +++ b/app/czg/app/controller/UserSignRecordController.php @@ -0,0 +1,29 @@ +auth->user_id; + $res = \app\api\model\UserSignRecord::getUserSignData($user_id, $this->auth->getUser()); + return $this->ApiDataReturn($res); + } + + + + +} \ No newline at end of file diff --git a/app/czg/app/controller/Wuyou.php b/app/czg/app/controller/WuyouController.php similarity index 98% rename from app/czg/app/controller/Wuyou.php rename to app/czg/app/controller/WuyouController.php index 679bc08..82c06f9 100644 --- a/app/czg/app/controller/Wuyou.php +++ b/app/czg/app/controller/WuyouController.php @@ -7,14 +7,15 @@ use app\common\controller\BaseController; use app\common\library\DatabaseRoute; use app\exception\SysException; use app\utils\RedisUtils; +use app\api\validate\WuyouValidate; use app\utils\WuYouPayUtils; use ba\Random; -use Orders; +use app\api\model\Orders; use support\Log; use think\facade\Db; use Throwable; -class Wuyou extends BaseController +class WuyouController extends BaseController { protected array $noNeedLogin = ['payOrder', 'index', 'test', 'notify', 'queryOrder']; diff --git a/app/czg/app/controller/user/User.php b/app/czg/app/controller/user/User.php new file mode 100644 index 0000000..d16b781 --- /dev/null +++ b/app/czg/app/controller/user/User.php @@ -0,0 +1,292 @@ +auth->user_id); +// Db::connect(DatabaseRoute::getConnection('course_collect', ['user_id' => 10])) +// ->name('course_collect') +// ->where('user_id', 10) +// ->find(); + + // 查询 +// $user = new SysUser(); +// // 查询或者删除前设置分库连接 +// $user->setConnection($user::findbefore($user, ['user_id' => 1072634324253292306])); +// $user = $user->where(['user_id' => 1072634324253292306]); +//// $res = $user->delete(); +// $data = $user->find(); +// p($data); + + + +// SysUser::update(['username' => 99999], ['user_id' => 130677]); +// SysUser::create([ +// 'user_id' => rand(000000, 999999), +// 'username' => '哎呀' . rand(000000, 999999), +// ]); + + } + + + + public function updatePhone() + { + $user_id = $this->auth->user_id; + $post = $this->request->post(); + if(empty($post['phone']) || empty($post['msg'])) { + $this->error('参数不正常'); + } + + // 验证码 + if(Msg::checkCode($post['phone'], $post['msg'])) { + $user = TbUser::GetByusername($post['phone']); + if($user) { + $this->error('手机号已经存在'); + } + $db = DatabaseRoute::getDb('tb_user', $user_id, true)->where(['user_id' => $user_id])->update(['phone' => $post['phone']]); + if($db) { + $this->success('操作成功'); + }else { + $this->error('操作失败'); + } + }else { + $this->error('验证码错误'); + } + } + + + + + + /** + * 会员签入(登录和注册) + * @throws Throwable + */ + public function checkIn(): void + { + $openMemberCenter = Config::get('buildadmin.open_member_center'); + if (!$openMemberCenter) { + $this->error(__('Member center disabled')); + } + + // 检查登录态 + if ($this->auth->isLogin()) { + $this->success(__('You have already logged in. There is no need to log in again~'), [ + 'type' => $this->auth::LOGGED_IN + ], $this->auth::LOGIN_RESPONSE_CODE); + } + + $userLoginCaptchaSwitch = Config::get('buildadmin.user_login_captcha'); + + if ($this->request->isPost()) { + $params = $this->request->post(['tab', 'email', 'mobile', 'username', 'password', 'keep', 'captcha', 'captchaId', 'captchaInfo', 'registerType']); + + // 提前检查 tab ,然后将以 tab 值作为数据验证场景 + if (!in_array($params['tab'] ?? '', ['login', 'register'])) { + $this->error(__('Unknown operation')); + } + + $validate = new UserValidate(); + try { + $validate->scene($params['tab'])->check($params); + } catch (Throwable $e) { + $this->error($e->getMessage()); + } + + if ($params['tab'] == 'login') { + if ($userLoginCaptchaSwitch) { + $captchaObj = new ClickCaptcha(); + if (!$captchaObj->check($params['captchaId'], $params['captchaInfo'])) { + $this->error(__('Captcha error')); + } + } + $res = $this->auth->login($params['username'], $params['password'], !empty($params['keep'])); + } elseif ($params['tab'] == 'register') { + $captchaObj = new Captcha(); + if (!$captchaObj->check($params['captcha'], $params[$params['registerType']] . 'user_register')) { + $this->error(__('Please enter the correct verification code')); + } + $res = $this->auth->register($params['username'], $params['password'], $params['mobile'], $params['email']); + } + + if (isset($res) && $res === true) { + $this->success(__('Login succeeded!'), [ + 'userInfo' => $this->auth->getUserInfo(), + 'routePath' => '/user' + ]); + } else { + $msg = $this->auth->getError(); + $msg = $msg ?: __('Check in failed, please try again or contact the website administrator~'); + $this->error($msg); + } + } + + $this->success('', [ + 'userLoginCaptchaSwitch' => $userLoginCaptchaSwitch, + 'accountVerificationType' => get_account_verification_type() + ]); + } + + public function logout(): void + { + if ($this->request->isPost()) { + $refreshToken = $this->request->post('refreshToken', ''); + if ($refreshToken) Token::delete((string)$refreshToken); + $this->auth->logout(); + $this->success(); + } + } + + + public static function selectInviteCount() + { + + } + + /** + * 实名接口 + */ + public function realNameAuth() + { + $params = $this->request->post(); +// (new UserValidate())->scene('realName')->check($params); + $params['certNum'] = trim($params['certNum']); + $params['certName'] = trim($params['certName']); + $userId = $this->getUserId(); + $regex = '/^[0-9Xx*]+$/'; + if (!preg_match($regex, $params['certNum'])) { + $this->success(); + } + + $count = DatabaseRoute::getDb('user_info', $userId)->where([ + 'cert_name' => $params['certName'], + 'cert_no' => $params['certNum'], + ])->count(); + if ($count) { + $this->error('已完成实名认证,无需重复操作'); + } + + $count = DatabaseRoute::getAllDbData('tb_user', function ($query) use ($params) { + return $query->where([ + 'cert_name' => $params['certName'], + 'cert_no' => $params['certNum'], + ]); + })->count(); + if ($count >= 3) { + $this->error('实名认证失败: 1个身份证号码最多绑定3个账号'); + } + + RedisUtils::checkRealCount($userId); + + $resp = AliUtils::verify($params['certName'], $params['certNum'], $params['accountNo'], $params['mobile']); + $userInfo = UserInfo::getByUserIdOrSave($userId); + $userInfo['cert_name'] = $params['certName']; + $userInfo['cert_no'] = $params['certNum']; + $userInfo['account_no'] = $params['accountNo']; + $userInfo['mobile'] = $params['mobile']; + $userInfo['bank_name'] = $resp['bankName']; + $userInfo['province'] = $params['province'] ?? ''; + $userInfo['city'] = $params['city'] ?? ''; + $userInfo['bank_branch'] = $params['bankBranch']; + $userInfo['resp_json'] = $resp['respJson']; + $userInfo['update_time'] = getNormalDate(); + DatabaseRoute::getDb('user_info', $userId, true, true)->update($userInfo); + + RedisUtils::setRealCount($userId); + + // 校验实名黑名单 + $count = Db::name('tb_user_blacklist')->where([ + 'id_card_no' => $params['certNum'] + ])->count(); + if ($count) { + DatabaseRoute::getDb('tb_user', $userId, true, true)->update([ + 'status' => 0, + 'plat_form' => '异常行为用户:实名信息异常 在黑名单' + ]); + throw new SysException("异常行为: 您的实名信息存在异常行为"); + } + + $this->successWithData(1); + } + + public function selectUserById() + { + $user = $this->auth->getUser(); + // 获取用户详情 + $userInfo = DatabaseRoute::getDb('user_info', $user['user_id'])->find(); + $userInfo['resp_json'] = null; // 移除敏感字段 + + // 对用户详情中的敏感信息进行脱敏 + if (!empty($userInfo['account_no'])) { + $userInfo['account_no'] = bankCard($userInfo['account_no']); + } + + if (!empty($userInfo['mobile'])) { + $userInfo['mobile'] = maskPhoneNumber($userInfo['mobile']); + } + + if (!empty($userInfo['cert_no'])) { + $userInfo['cert_no'] = idCardNum($userInfo['cert_no'], 3, 2); + } + + // 对支付宝信息进行脱敏 + if (!empty($user['zhi_fu_bao'])) { + if (filter_var($user['zhi_fu_bao'], FILTER_VALIDATE_EMAIL)) { + $user['zhi_fu_bao'] = email($user['zhi_fu_bao']); + } else { + $user['zhi_fu_bao'] = maskPhoneNumber($user['zhi_fu_bao']); + } + } + + // 合并用户信息和用户详情 + $map = array_merge($user, $userInfo); + $map['user_id'] = (string)$map['user_id']; // 确保用户ID为字符串类型 + + // 如果支付宝姓名为空,使用实名认证姓名 + if (empty($user['zhi_fu_bao_name']) && !empty($userInfo['cert_name'])) { + $map['zhi_fu_bao_name'] = $userInfo['cert_name']; + } + $this->success('ok', convertToCamelCase($map)); + } + + public function updateUsers() + { + $post = $this->request->post(); + $user = $this->auth->getUser(); + $db_name = Db::connect(DatabaseRoute::getConnection('tb_user', ['user_id' => $user['user_id']])); + $db_name->name('tb_user')->where(['user_id' => $user['user_id']])->update(['user_name' => $post['userName'], 'avatar' => $post['avatar']]); + $this->success('ok'); + } + +} \ No newline at end of file diff --git a/app/queue/ActivitiesQueue.php b/app/queue/ActivitiesQueue.php new file mode 100644 index 0000000..12d8032 --- /dev/null +++ b/app/queue/ActivitiesQueue.php @@ -0,0 +1,20 @@ +run($job, $data); + Log::info("消息队列执行成功:" . static::class); + $job->delete(); + } catch (\Throwable $e) { + Log::error("消息队列执行异常:" . $e->getMessage()); + Log::info($e->getTraceAsString()); + $job->release(10); // 或 $job->fail() + } + + $end = microtime(true); + Log::info("消息队列执行完毕, 耗时:" . ($end - $start) . 's'); + } + + // 子类实现具体逻辑 + abstract public function run(Job $job, $data); +} \ No newline at end of file diff --git a/app/queue/DiscCompensateJob.php b/app/queue/DiscCompensateJob.php new file mode 100644 index 0000000..47fcb3e --- /dev/null +++ b/app/queue/DiscCompensateJob.php @@ -0,0 +1,26 @@ +delete(); // 处理成功删除任务 + } catch (\Exception $e) { + if ($job->attempts() < 3) { + $job->release(5); // 重试3次,间隔5秒 + } else { + $job->delete(); + Log::error("大转盘补偿任务最终失败:ID={$data['id']}"); + } + } + } +} \ No newline at end of file diff --git a/app/queue/DiscReceiveQueue.php b/app/queue/DiscReceiveQueue.php new file mode 100644 index 0000000..e946ea0 --- /dev/null +++ b/app/queue/DiscReceiveQueue.php @@ -0,0 +1,50 @@ +find(); + if ($user['status'] != 1) { + return; + } + + $id = DatabaseRoute::getDb('user_money_details', $drawsInfo['user_id'], true)->insert([ + 'user_id' => $drawsInfo['user_id'], + 'title' => '现金大转盘', + 'classify' => 5, + 'type' => 1, + 'state' => 2, + 'money' => $drawsInfo['number'], + 'content' => "现金红包奖励{$drawsInfo['number']}元", + 'money_type' => 1, + 'create_time' => getNormalDate(), + 'source_id' => $drawsInfo['id'] + ]); + + DatabaseRoute::getDb('disc_spinning_record', $drawsInfo['user_id'], true, true, false)->where([ + 'id' => $drawsInfo['id'] + ])->update([ + 'target' => 2, + 'target_id' => $id + ]); + + UserMoney::updateAmount($drawsInfo['user_id'], $drawsInfo['number']); + + } +} \ No newline at end of file diff --git a/app/queue/UserPushQueue.php b/app/queue/UserPushQueue.php new file mode 100644 index 0000000..1bd9b3d --- /dev/null +++ b/app/queue/UserPushQueue.php @@ -0,0 +1,24 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, { "name": "firebase/php-jwt", "version": "v6.11.0", @@ -144,6 +316,600 @@ ], "time": "2025-02-09T22:43:44+00:00" }, + { + "name": "illuminate/bus", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/bus.git", + "reference": "5f7cd1f99b2ff7dd0ef20aead81da1390c4bc8e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/bus/zipball/5f7cd1f99b2ff7dd0ef20aead81da1390c4bc8e3", + "reference": "5f7cd1f99b2ff7dd0ef20aead81da1390c4bc8e3", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "illuminate/collections": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/pipeline": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2" + }, + "suggest": { + "illuminate/queue": "Required to use closures when chaining jobs (^7.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Bus\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Bus package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/collections", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "856b1da953e46281ba61d7c82d337072d3ee1825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/856b1da953e46281ba61d7c82d337072d3ee1825", + "reference": "856b1da953e46281ba61d7c82d337072d3ee1825", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "illuminate/conditionable": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/macroable": "^11.0", + "php": "^8.2" + }, + "suggest": { + "symfony/var-dumper": "Required to use the dump method (^7.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "319b717e0587bd7c8a3b44464f0e27867b4bcda9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/319b717e0587bd7c8a3b44464f0e27867b4bcda9", + "reference": "319b717e0587bd7c8a3b44464f0e27867b4bcda9", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^8.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/container", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "79bf9149ad7ddd7e14326ebcdd41197d2c4ee36a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/79bf9149ad7ddd7e14326ebcdd41197d2c4ee36a", + "reference": "79bf9149ad7ddd7e14326ebcdd41197d2c4ee36a", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "illuminate/contracts": "^11.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/4b2a67d1663f50085bc91e6371492697a5d2d4e8", + "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/events", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/events.git", + "reference": "b72dab66d8e05d22dc5aa949efec150bbc73e827" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/events/zipball/b72dab66d8e05d22dc5aa949efec150bbc73e827", + "reference": "b72dab66d8e05d22dc5aa949efec150bbc73e827", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "illuminate/bus": "^11.0", + "illuminate/collections": "^11.0", + "illuminate/container": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/macroable": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Events\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Events package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", + "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-06-28T20:10:30+00:00" + }, + { + "name": "illuminate/pipeline", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pipeline.git", + "reference": "f73bb7cab13ac8ef91094dc46976f5e992eea127" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/f73bb7cab13ac8ef91094dc46976f5e992eea127", + "reference": "f73bb7cab13ac8ef91094dc46976f5e992eea127", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "illuminate/contracts": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pipeline\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pipeline package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/redis", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/redis.git", + "reference": "d39281045e514cf9c2f3621eac158479f4178197" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/redis/zipball/d39281045e514cf9c2f3621eac158479f4178197", + "reference": "d39281045e514cf9c2f3621eac158479f4178197", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "illuminate/collections": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/macroable": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2" + }, + "suggest": { + "ext-redis": "Required to use the phpredis connector (^4.0|^5.0|^6.0).", + "predis/predis": "Required to use the predis connector (^2.3)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Redis\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Redis package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:54:20+00:00" + }, + { + "name": "illuminate/support", + "version": "v11.45.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "9732f41d7a9836a2c466ab06460efc732aeb417a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/9732f41d7a9836a2c466ab06460efc732aeb417a", + "reference": "9732f41d7a9836a2c466ab06460efc732aeb417a", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^11.0", + "illuminate/conditionable": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/macroable": "^11.0", + "nesbot/carbon": "^2.72.6|^3.8.4", + "php": "^8.2", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "spatie/once": "*" + }, + "suggest": { + "illuminate/filesystem": "Required to use the Composer class (^11.0).", + "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the Composer class (^7.0).", + "symfony/uid": "Required to use Str::ulid() (^7.0).", + "symfony/var-dumper": "Required to use the dd function (^7.0).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-11T20:47:08+00:00" + }, { "name": "monolog/monolog", "version": "2.10.0", @@ -252,6 +1018,118 @@ ], "time": "2024-11-12T12:43:37+00:00" }, + { + "name": "nesbot/carbon", + "version": "3.9.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "ced71f79398ece168e24f7f7710462f462310d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ced71f79398ece168e24f7f7710462f462310d4d", + "reference": "ced71f79398ece168e24f7f7710462f462310d4d", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.57.2", + "kylekatarnls/multi-tester": "^2.5.3", + "ondrejmirtes/better-reflection": "^6.25.0.4", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.11.2", + "phpunit/phpunit": "^10.5.20", + "squizlabs/php_codesniffer": "^3.9.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-05-01T19:51:51+00:00" + }, { "name": "nikic/fast-route", "version": "v1.3.0", @@ -389,6 +1267,60 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -561,6 +1493,159 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.31.0", @@ -818,6 +1903,274 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, { "name": "topthink/think-container", "version": "v3.0.1", @@ -1128,6 +2481,128 @@ ], "time": "2024-07-20T21:52:34+00:00" }, + { + "name": "voku/portable-ascii", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "16c17671a804bb92602822113dd91fbc8a35d2af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/16c17671a804bb92602822113dd91fbc8a35d2af", + "reference": "16c17671a804bb92602822113dd91fbc8a35d2af", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.2" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T00:49:12+00:00" + }, + { + "name": "webman/redis", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/webman-php/redis.git", + "reference": "83893f0931e07906ba9dbf976126ad8338f40624" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/redis/zipball/83893f0931e07906ba9dbf976126ad8338f40624", + "reference": "83893f0931e07906ba9dbf976126ad8338f40624", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "illuminate/redis": "^10.0 || ^11.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Redis\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman redis", + "support": { + "issues": "https://github.com/webman-php/redis/issues", + "source": "https://github.com/webman-php/redis/tree/v2.1.2" + }, + "time": "2025-02-14T11:36:24+00:00" + }, { "name": "webman/think-cache", "version": "v2.1.1", @@ -1420,7 +2895,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=8.1" + "php": ">=8.1", + "ext-bcmath": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/config/process.php b/config/process.php index d6d0c4d..e078a00 100644 --- a/config/process.php +++ b/config/process.php @@ -26,8 +26,8 @@ return [ 'user' => '', 'group' => '', 'reusePort' => false, -// 'eventLoop' => '', - 'eventLoop' => \Workerman\Events\Swoole::class, + 'eventLoop' => '', +// 'eventLoop' => \Workerman\Events\Swoole::class, 'context' => [], 'constructor' => [ 'requestClass' => Request::class, diff --git a/config/redis.php b/config/redis.php new file mode 100644 index 0000000..143ce5f --- /dev/null +++ b/config/redis.php @@ -0,0 +1,30 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +return [ + 'default' => [ + 'password' => '222222', + 'host' => '127.0.0.1', + 'port' => 6379, + 'database' => 0, + // Connection pool, supports only Swoole or Swow drivers. + 'pool' => [ + 'max_connections' => 5, + 'min_connections' => 1, + 'wait_timeout' => 3, + 'idle_timeout' => 60, + 'heartbeat_interval' => 50, + ], + ] +]; diff --git a/config/route.php b/config/route.php index 0984b69..ba7f61b 100644 --- a/config/route.php +++ b/config/route.php @@ -14,7 +14,8 @@ use Webman\Route; -Route::any('/czg/app/wuyou/queryOrder/{orderId}', [app\czg\app\controller\Wuyou::class, 'queryOrder']); +Route::any('/czg/app/wuyou/queryOrder/{orderId}', [app\czg\app\controller\WuyouController::class, 'queryOrder']); +Route::any('/czg/app/Login/sendMsg/{phone}/{event}', [app\czg\app\controller\LoginController::class, 'sendMsg']);