Repository: putyy/chatgpt Branch: main Commit: 089b26b3f5ec Files: 210 Total size: 548.9 KB Directory structure: gitextract_w1gfey8a/ ├── .gitignore ├── LICENSE ├── MineAdmin/ │ ├── php/ │ │ └── app/ │ │ └── Ai/ │ │ ├── Api/ │ │ │ ├── BaseApi.php │ │ │ ├── Chat.php │ │ │ ├── Common.php │ │ │ ├── Login.php │ │ │ ├── Order.php │ │ │ ├── User.php │ │ │ ├── Wallet.php │ │ │ └── Websocket.php │ │ ├── Command/ │ │ │ └── InitMenuCommand.php │ │ ├── Constants/ │ │ │ ├── OrderConst.php │ │ │ ├── RedisConst.php │ │ │ ├── ResponseCodeConst.php │ │ │ ├── UploadSceneConst.php │ │ │ ├── VipConst.php │ │ │ └── WalletConst.php │ │ ├── Controller/ │ │ │ ├── AiChatMessageController.php │ │ │ ├── AiChatSessionController.php │ │ │ ├── AiChatgptPromptsController.php │ │ │ ├── AiImageMaterialController.php │ │ │ ├── AiMineMenuController.php │ │ │ ├── AiMineMenuGroupController.php │ │ │ ├── AiOpenaiKeyController.php │ │ │ ├── AiOrderController.php │ │ │ ├── AiPayKamiController.php │ │ │ ├── AiQuickIssueController.php │ │ │ ├── AiSettingController.php │ │ │ └── AiUserController.php │ │ ├── Crontab/ │ │ │ └── CheckVipOver.php │ │ ├── Database/ │ │ │ ├── Migrations/ │ │ │ │ ├── 2023_05_04_145048_create_ai_user_table.php │ │ │ │ ├── 2023_05_09_095456_create_ai_user_wallet_table.php │ │ │ │ ├── 2023_05_09_095504_create_ai_user_wallet_log_table.php │ │ │ │ ├── 2023_05_09_095525_create_ai_user_relation_table.php │ │ │ │ ├── 2023_05_09_095540_create_ai_order_table.php │ │ │ │ ├── 2023_05_09_095543_create_ai_order_vip_table.php │ │ │ │ ├── 2023_05_09_102806_create_ai_mine_menu_group_table.php │ │ │ │ ├── 2023_05_09_102817_create_ai_mine_menu_table.php │ │ │ │ ├── 2023_05_09_154733_create_ai_chatgpt_prompts_table.php │ │ │ │ ├── 2023_05_10_165842_create_ai_chat_message_table.php │ │ │ │ ├── 2023_05_10_171603_create_ai_chat_session_table.php │ │ │ │ ├── 2023_05_12_152504_create_ai_quick_issue_table.php │ │ │ │ ├── 2023_05_19_164456_create_ai_pay_kami_table.php │ │ │ │ ├── 2023_05_25_152855_create_ai_openai_key_table.php │ │ │ │ ├── 2023_05_26_111034_create_ai_order_kami_table.php │ │ │ │ └── 2023_06_14_141605_create_ai_image_material_table.php │ │ │ └── Seeders/ │ │ │ ├── ai_chatgpt_prompts.php │ │ │ ├── ai_mine_menu.php │ │ │ ├── ai_mine_menu_group.php │ │ │ └── ai_user.php │ │ ├── Dto/ │ │ │ ├── AiChatMessageDto.php │ │ │ ├── AiChatSessionDto.php │ │ │ ├── AiChatgptPromptsDto.php │ │ │ ├── AiImageMaterialDto.php │ │ │ ├── AiMineMenuDto.php │ │ │ ├── AiMineMenuGroupDto.php │ │ │ ├── AiOpenaiKeyDto.php │ │ │ ├── AiOrderDto.php │ │ │ ├── AiPayKamiDto.php │ │ │ ├── AiQuickIssueDto.php │ │ │ └── AiUserDto.php │ │ ├── Factory/ │ │ │ └── AiRedisFactory.php │ │ ├── Mapper/ │ │ │ ├── AiChatMessageMapper.php │ │ │ ├── AiChatSessionMapper.php │ │ │ ├── AiChatgptPromptsMapper.php │ │ │ ├── AiImageMaterialMapper.php │ │ │ ├── AiMineMenuGroupMapper.php │ │ │ ├── AiMineMenuMapper.php │ │ │ ├── AiOpenaiKeyMapper.php │ │ │ ├── AiOrderMapper.php │ │ │ ├── AiPayKamiMapper.php │ │ │ ├── AiQuickIssueMapper.php │ │ │ └── AiUserMapper.php │ │ ├── Middleware/ │ │ │ └── AuthMiddleware.php │ │ ├── Model/ │ │ │ ├── AiChatMessage.php │ │ │ ├── AiChatSession.php │ │ │ ├── AiChatgptPrompts.php │ │ │ ├── AiImageMaterial.php │ │ │ ├── AiMineMenu.php │ │ │ ├── AiMineMenuGroup.php │ │ │ ├── AiOpenaiKey.php │ │ │ ├── AiOrder.php │ │ │ ├── AiOrderKami.php │ │ │ ├── AiOrderVip.php │ │ │ ├── AiPayKami.php │ │ │ ├── AiQuickIssue.php │ │ │ ├── AiUser.php │ │ │ ├── AiUserRelation.php │ │ │ ├── AiUserWallet.php │ │ │ └── AiUserWalletLog.php │ │ ├── Request/ │ │ │ ├── AiChatMessageRequest.php │ │ │ ├── AiChatSessionRequest.php │ │ │ ├── AiChatgptPromptsRequest.php │ │ │ ├── AiImageMaterialRequest.php │ │ │ ├── AiMineMenuGroupRequest.php │ │ │ ├── AiMineMenuRequest.php │ │ │ ├── AiOpenaiKeyRequest.php │ │ │ ├── AiOrderRequest.php │ │ │ ├── AiPayKamiRequest.php │ │ │ ├── AiQuickIssueRequest.php │ │ │ └── AiUserRequest.php │ │ ├── Service/ │ │ │ ├── AiChatMessageService.php │ │ │ ├── AiChatSessionService.php │ │ │ ├── AiChatgptPromptsService.php │ │ │ ├── AiImageMaterialService.php │ │ │ ├── AiLoginService.php │ │ │ ├── AiMineMenuGroupService.php │ │ │ ├── AiMineMenuService.php │ │ │ ├── AiOpenaiKeyService.php │ │ │ ├── AiOrderService.php │ │ │ ├── AiPayKamiService.php │ │ │ ├── AiQuickIssueService.php │ │ │ ├── AiSettingService.php │ │ │ ├── AiUserService.php │ │ │ ├── AiVipService.php │ │ │ ├── AiWalletService.php │ │ │ ├── HelperService.php │ │ │ └── QiniuService.php │ │ └── config.json │ └── vue/ │ └── src/ │ ├── api/ │ │ └── ai/ │ │ ├── aiChatMessage.js │ │ ├── aiChatSession.js │ │ ├── aiChatgptPrompts.js │ │ ├── aiImageMaterial.js │ │ ├── aiMineMenu.js │ │ ├── aiMineMenuGroup.js │ │ ├── aiOpenaiKey.js │ │ ├── aiOrder.js │ │ ├── aiPayKami.js │ │ ├── aiQuickIssue.js │ │ ├── aiSetting.js │ │ └── aiUser.js │ ├── components/ │ │ └── putyy/ │ │ └── pt-upload.vue │ ├── config/ │ │ ├── pt-const.js │ │ └── pt-scene.js │ ├── utils/ │ │ └── pt-upload.js │ └── views/ │ └── ai/ │ ├── chatMessage/ │ │ └── index.vue │ ├── chatSession/ │ │ └── index.vue │ ├── chatgptPrompts/ │ │ └── index.vue │ ├── imageMaterial/ │ │ └── index.vue │ ├── mineMenu/ │ │ └── index.vue │ ├── mineMenuGroup/ │ │ └── index.vue │ ├── openKey/ │ │ ├── components/ │ │ │ └── add.vue │ │ └── index.vue │ ├── order/ │ │ └── index.vue │ ├── payKami/ │ │ ├── components/ │ │ │ └── add.vue │ │ └── index.vue │ ├── quickIssue/ │ │ └── index.vue │ ├── setting/ │ │ └── index.vue │ └── user/ │ ├── components/ │ │ └── openVip.vue │ └── index.vue ├── README.md └── UniApp/ ├── .gitignore ├── App.vue ├── README.md ├── androidPrivacy.json ├── common/ │ ├── api.ts │ ├── const.ts │ ├── func.ts │ └── utils/ │ ├── jump.ts │ ├── request.ts │ └── services.ts ├── components/ │ ├── AgreementPopup.vue │ ├── FooterCommon.vue │ ├── Nothing.vue │ ├── OpenVipPopup.vue │ ├── QrCodePopup.vue │ └── Search.vue ├── config.example.ts ├── index.html ├── logic/ │ └── user.ts ├── main.js ├── manifest.json ├── pages/ │ ├── chatgpt/ │ │ ├── channel.vue │ │ └── room.vue │ ├── login.vue │ ├── notice.vue │ └── user/ │ ├── friend.vue │ ├── mine.vue │ ├── order.vue │ ├── perfect.vue │ ├── wallet.vue │ ├── walletList.vue │ └── withdrawalList.vue ├── pages.json ├── store/ │ ├── index.ts │ └── websocket.ts ├── tsconfig.json ├── types/ │ └── global.d.ts ├── uni.promisify.adaptor.js ├── uni.scss └── uni_modules/ ├── bt-cropper_3.0.1/ │ ├── changelog.md │ ├── components/ │ │ └── bt-cropper/ │ │ ├── bt-cropper.vue │ │ ├── iconfont.css │ │ ├── js/ │ │ │ └── touchs.js │ │ ├── utils/ │ │ │ └── tools.js │ │ └── {ages.json │ ├── package.json │ └── readme.md └── mp-html/ ├── README.md ├── changelog.md ├── components/ │ └── mp-html/ │ ├── emoji/ │ │ └── index.js │ ├── highlight/ │ │ ├── config.js │ │ └── index.js │ ├── markdown/ │ │ └── index.js │ ├── mp-html.vue │ ├── node/ │ │ └── node.vue │ └── parser.js ├── package.json └── static/ └── app-plus/ └── mp-html/ ├── js/ │ └── handler.js └── local.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.idea/ /.vscode/ /.hbuilderx/ /test ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MineAdmin/php/app/Ai/Api/BaseApi.php ================================================ success($this->funCallback($callback)); } public function funCallback(callable $callback) { Db::beginTransaction(); try { $res = $callback(); Db::commit(); } catch (\Throwable $throwable) { Db::rollBack(); throw $throwable; } return $res; } } ================================================ FILE: MineAdmin/php/app/Ai/Api/Chat.php ================================================ success([ 'list' => $this->chatgptPromptsService->getList([ 'select' => 'id,act,prompt', 'orderBy' => 'sort', 'orderType' => 'desc', ], false) ]); } #[GetMapping("model-list")] public function modelList(): ResponseInterface { return $this->success(AiChatgptPromptsService::MODEL_LIST); } #[GetMapping("session")] public function session(): ResponseInterface { return $this->success($this->chatSessionService->session($this->request->all())); } #[PostMapping("session-close")] public function sessionClose(): ResponseInterface { return $this->success([ 'sid' => $this->chatSessionService->sessionClose($this->request->all()) ]); } #[GetMapping("messages")] public function messages(): ResponseInterface { // next、prev $sid = $this->request->query('sid'); $session = $this->chatSessionService->mapper->session([ 'id' => $sid, 'uid' => $this->loginService->getId() ]); if (empty($session)) { return $this->success([ 'list' => [] ]); } return $this->success([ 'list' => $this->chatMessageService->messages($this->request->all()) ]); } #[GetMapping("session-history")] public function sessionHistory(): ResponseInterface { return $this->success([ 'list' => $this->chatSessionService->sessionHistory($this->request->all()) ]); } #[PostMapping("session-share")] public function sessionShare(): ResponseInterface { $model = $this->chatSessionService->mapper->session([ 'id' => $this->request->post('id'), 'uid' => $this->loginService->getId() ]); if (empty($model) || ($this->request->post('is_share', false) && $model->share === 2)) { return $this->success(); } $model->share = $model->share === 1 ? 2 : 1; $model->save(); return $this->success(); } #[PostMapping("session-delete")] public function sessionDelete(): ResponseInterface { $model = $this->chatSessionService->mapper->session([ 'id' => $this->request->post('id'), 'uid' => $this->loginService->getId() ]); if (empty($model)) { return $this->success(); } $model->delete(); $this->chatMessageService->mapper->getModel()::where([ 'sid' => $this->request->post('id') ])->delete(); return $this->success(); } #[GetMapping("session-share-list")] public function sessionShareList(): ResponseInterface { return $this->success([ 'list' => $this->chatSessionService->sessionShareList($this->request->all()) ]); } #[GetMapping("session-share-message-list")] public function sessionShareMessageList(): ResponseInterface { $sid = (int)$this->request->query('sid'); $session = $this->chatSessionService->read($sid); if (empty($session) || $session->share === 1) { return $this->success(['list' => []]); } return $this->success([ 'list' => $this->chatMessageService->shareMessageList($this->request->all()) ]); } #[GetMapping("quick-issue")] public function quickIssue(): ResponseInterface { return $this->success([ 'list' => $this->quickIssueService->getList([ 'select' => 'id,title,content', 'orderBy' => 'id', 'orderType' => 'desc', ], false) ]); } // #[GetMapping("test")] public function test() { $token = $this->request->query('x-token', null); if ($token) { $token = 'Bearer ' . $token; //todo 验证 } $opts = [ 'model' => 'gpt-3.5-turbo', 'messages' => [], 'temperature' => 1.0, 'max_tokens' => 150, 'frequency_penalty' => 0, 'presence_penalty' => 0, 'stream' => true, ]; $opts['messages'][] = ['role' => 'system', 'content' => "你是一个AI助手,我需要你模拟一名温柔贴心的女朋友来回答我的问题。"]; $opts['messages'][] = [ 'role' => 'user', 'content' => 'php如何计算1+1?', ]; $response = ApplicationContext::getContainer()->get(\Hyperf\HttpServer\Contract\ResponseInterface::class); $eventStream = new EventStream($response->getConnection(), $response); $openAiService = ApplicationContext::getContainer()->get(AiOpenaiKeyService::class); $key = $openAiService->openAiKey(); $openAI = new OpenAi($key); $baseUrl = $openAiService->openaiProxy(); $baseUrl && $openAI->setBaseURL($baseUrl); $openAI->setHeader(["Content-Type" => "text/event-stream"]); // 本次所有结果 $replyContent = ''; $openAI->chat($opts, function ($curl_info, $data) use ($eventStream, &$replyContent) { $datas = explode('data: ', $data); foreach ($datas as $dataStr) { $arrayData = json_decode($dataStr, true); if ($arrayData) { if (isset($arrayData['choices'][0]['delta']['content'])) { $replyContent .= ($content = $arrayData['choices'][0]['delta']['content'] ?? ''); $eventStream->write("data: " . json_encode([ 'status' => 1, 'content' => $content, ]) . PHP_EOL . PHP_EOL ); } elseif (!empty($arrayData['choices'][0]['finish_reason'])) { $eventStream->write("data: " . json_encode([ 'status' => 2, 'content' => '', ]) . PHP_EOL . PHP_EOL ); } elseif (isset($arrayData['error'])) { $eventStream->write("data: " . json_encode([ 'status' => 0, 'content' => $arrayData['error']['message'], ]) . PHP_EOL . PHP_EOL ); } } } return \strlen($data); }); return "data: " . PHP_EOL . PHP_EOL; } } ================================================ FILE: MineAdmin/php/app/Ai/Api/Common.php ================================================ [ 'image' => array_filter(UploadSceneConst::ImageScene, function ($key) { return str_starts_with($key, 'ai_'); }, ARRAY_FILTER_USE_KEY), // 远端访问主域名, 用于前端上传时判断是否本地文件 'domain' => config('file.storage.qiniu.host'), ], 'tutorials' => [], 'agreement' => [ 'user' => $this->settingService->agreementUser(), ], 'ads' => [], ]; $data['other'] = [ 'copyright' => 'Copyright © 1999 - ' . date('Y') . ' ByAi', 'version' => Login::VERSIONS, 'customer_info' => $this->settingService->customer(), ]; return $this->success($data); } #[GetMapping("upload-token")] public function uploadToken(): ResponseInterface { return $this->success($this->qiniuService->token($this->request->query('scenes'))); } #[GetMapping("vip-config")] public function vipConfig(): ResponseInterface { [$config, $user] = $this->vipService->config($this->loginService->getId()); $configCp = array_column(VipConst::config(), null, 'level'); $data = [ 'equity' => [ [ [ 'text' => '特权', 'color' => '', ], [ 'text' => $configCp[VipConst::VIP]['name'], 'color' => '', ], [ 'text' => $configCp[VipConst::VIP_ONE]['name'], 'color' => '', ], [ 'text' => $configCp[VipConst::VIP_TWO]['name'], 'color' => '', ], ], [ [ 'text' => '使用时长(月)', 'color' => '', ], [ 'text' => $configCp[VipConst::VIP]['length'], 'color' => '', ], [ 'text' => $configCp[VipConst::VIP_ONE]['length'], 'color' => '', ], [ 'text' => $configCp[VipConst::VIP_TWO]['length'], 'color' => 'red', ], ], [ [ 'text' => 'VIP抵扣包', 'color' => '', ], [ 'text' => $configCp[VipConst::VIP]['wrap_vip'], 'color' => '', ], [ 'text' => $configCp[VipConst::VIP_ONE]['wrap_vip'], 'color' => '', ], [ 'text' => $configCp[VipConst::VIP_TWO]['wrap_vip'], 'color' => 'red', ], ], [ [ 'text' => '推广赚(%)', 'color' => '', ], [ 'text' => $configCp[VipConst::VIP]['income'], 'color' => '', ], [ 'text' => $configCp[VipConst::VIP_ONE]['income'], 'color' => '', ], [ 'text' => $configCp[VipConst::VIP_TWO]['income'], 'color' => 'red', ], ], [ [ 'text' => '聊天上下文', 'color' => '', ], [ 'text' => '不支持', 'color' => '', ], [ 'text' => '支持', 'color' => '', ], [ 'text' => '支持', 'color' => 'red', ], ], [ [ 'text' => '每日次数', 'color' => '', ], [ 'text' => '19', 'color' => '', ], [ 'text' => '无限', 'color' => '', ], [ 'text' => '无限', 'color' => 'red', ], ], [ [ 'text' => '总价值', 'color' => '', ], [ 'text' => '1000+', 'color' => '', ], [ 'text' => '3000+', 'color' => '', ], [ 'text' => '5000+', 'color' => 'red', ], ], ], 'config' => $config, 'user' => $user->toArray(), ]; return $this->success($data); } } ================================================ FILE: MineAdmin/php/app/Ai/Api/Login.php ================================================ request->query('user_name'); $vid = (int)$this->request->query('vid'); $password = $this->request->query('password'); if (!$user_name || !$password){ return $this->error('参数错误!'); } /** * @var AiUser $user */ $user = AiUser::where(['mobile'=>$user_name, 'is_lock'=>1])->first(); $password = md5($password); if (!empty($user) && $password !== $user->password){ return $this->error('密码错误!'); } if (empty($user)){ if (empty($vid)) { return $this->error('账号不存在!'); }else{ /** * @var AiUser $pUser */ $pUser = AiUser::where(['id'=>$vid, 'is_lock'=>1])->select(['id'])->first(); if (empty($pUser)) { return $this->error('邀请用户有误!'); } // 注册 Db::beginTransaction(); try { $user = new AiUser(); $user->nick_name = ''; $user->head_img = ''; $user->mobile = $user_name; $user->password = $password; $user->save(); AiUserRelation::insert([ 'uid' => $user->id, 'from_uid' => $pUser->id, ]); AiUserWallet::insert([ 'uid' => $user->id, ]); Db::commit(); }catch (\Throwable $exception){ Db::rollBack(); return $this->error('注册失败!'); } } } $data = []; $data['version'] = self::VERSIONS; $data['id'] = $user->id; $data['vip'] = $user->vip; $token = $this->loginService->getToken($data); return $this->success([ 'token' => $token, 'version' => self::VERSIONS, 'check_status' => false, 'login_time' => time(), ]); }catch (\Throwable $exception){ return $this->error($exception->getMessage()); } } } ================================================ FILE: MineAdmin/php/app/Ai/Api/Order.php ================================================ request->all(); $param['ord_type'] = [OrderConst::OPEN_VIP, OrderConst::MARKET]; return $this->success([ 'list' => $this->orderService->orderList($param) ]); } #[PostMapping("kami-open-vip")] public function kamiOpenVip(): ResponseInterface { $this->orderService->kamiOpenVip($this->request->all()); return $this->success(); } } ================================================ FILE: MineAdmin/php/app/Ai/Api/User.php ================================================ userService->read($this->loginService->getId()); return $this->success([ 'head_img' => $user->head_img, 'nick_name' => $user->nick_name, 'uid' => $user->id, 'mobile' => $user->mobile, 'vip' => $user->vip, 'vip_name' => VipConst::getDesc($user->vip), 'vip_ent_at' => $user->vip_ent_at ? strtotime($user->vip_ent_at) : 0, 'amount' => $user->wallet->balance ? HelperService::decode100($user->wallet->balance) : 0, 'friend_num' => $this->userService->friendNum($user->id), 'menus' => $this->mineMenuGroupService->mineMenus(), 'banner' => $this->imageMaterialService->mine(1), ]); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws \Throwable */ #[PostMapping("edit")] public function edit(): ResponseInterface { $post = $this->request->post(); if (!$this->checkParameter($post, ['mobile', 'head_img', 'nick_name'])) { throw new NormalStatusException('参数错误', ResponseCodeConst::PARAM_FAILED); } return $this->funCallbackRes(function () use ($post) { $this->userService->update($this->loginService->getId(), [ 'nick_name' => $post['nick_name'], 'head_img' => HelperService::buildSavePath($post['head_img']), 'mobile' => $post['mobile'], ]); return ''; }); } #[GetMapping("friends")] public function friends(): ResponseInterface { return $this->success([ 'list'=>$this->userService->friendList($this->request->all()) ]); } } ================================================ FILE: MineAdmin/php/app/Ai/Api/Wallet.php ================================================ success($this->walletService->info()); } #[GetMapping("change-log-list")] public function changeLogList(): ResponseInterface { return $this->success([ 'list'=>$this->walletService->changeLogList($this->request->all()) ]); } #[GetMapping("withdrawal-list")] public function withdrawalList(): ResponseInterface { $param = $this->request->all(); $param['ord_type'] = [OrderConst::WITHDRAWAL]; return $this->success([ 'list'=>$this->orderService->orderList($param) ]); } #[PostMapping("withdrawal")] public function withdrawal(): ResponseInterface { $this->walletService->withdrawal($this->request->all()); return $this->success(); } } ================================================ FILE: MineAdmin/php/app/Ai/Api/Websocket.php ================================================ opcode == Opcode::PING) { // 如果使用协程 Server,在判断是 PING 帧后,需要手动处理,返回 PONG 帧。 // 异步风格 Server,可以直接通过 Swoole 配置处理,详情请见 https://wiki.swoole.com/#/websocket_server?id=open_websocket_ping_frame $server->push('', Opcode::PONG); return; } $data = \json_decode($frame->data); // todo {model: "1:gpt3.5问答模式 2文心一言问答模式 3通义千问问答模式", role_id: 1, message:"",} switch ($data->type){ case 'ping': $server->push($frame->fd, '{"type":"ping","content":"ok"}'); break; case 'message': $this->messageHandle($frame->fd, $data); break; } } public function messageHandle($fd, $data) { switch ($data->model->index){ case 0: case 1: // chatgpt 3.5 // todo 存入消息,请求openapi 返回结果 结果入表 \Hyperf\Coroutine\go(function () use($fd, $data){ $this->chatgpt($fd, $data); }); break; } } protected function chatgpt($fd, $data){ try { /** * @var WebSocketServer $server */ $server = $this->container->get(ServerFactory::class)->getServer()->getServer(); // todo 验证会话id是否属于当前用户uid if (mb_strlen($data->content) > 2048) { $server->push($fd, '{"type":"error","content":"您输入的内容过长","status":true}'); return; } $user = $this->redis->hGetAll(RedisConst::FD_TO_USER . $fd); // 免费会员限制会话 $time = time(); $todayKey = RedisConst::USER_SPOKE_TODAY . date('Ymd'); $incr = $this->redis->zIncrBy($todayKey, 1, $user['uid']); $this->redis->expire($todayKey, 86400); if ($user['vip'] === VipConst::FREE && $incr > 10) { $server->push($fd, '{"type":"error","content":"您今日已经超过免费会员次数了哦!","status":true}'); return; } $prompt = $this->chatgptPromptsService->mapper->first(['id'=>$data->prompt_id], ['id', 'act', 'prompt']); $opts = [ 'model' => AiChatgptPromptsService::MODEL_LIST[$data->model->index]['text'], 'messages' => [], 'temperature' => $data->model->temperature, 'frequency_penalty' => $data->model->frequency_penalty, 'presence_penalty' => $data->model->presence_penalty, 'stream' => true, ]; if (AiChatgptPromptsService::MODEL_LIST[$data->model->index]['is_vip'] && !$user['vip']) { $opts['model'] = AiChatgptPromptsService::MODEL_LIST[0]['text']; $opts['max_tokens'] = 2048; } else if (!$user['vip']) { $opts['max_tokens'] = 2048; } $opts['messages'][] = ['role' => 'system', 'content' => $prompt->prompt]; if (!empty($data->context) && $user['vip'] > VipConst::FREE){ $i = 0; foreach ($data->context as $msg) { $opts['messages'][] = [ 'role' => 'user', 'content' => $msg->content, ]; $opts['messages'][] = [ 'role' => 'assistant', 'content' => $msg->reply_content, ]; if (++$i > 6) { break; } } } $opts['messages'][] = [ 'role' => 'user', 'content' => $data->content, ]; $key = $this->openAiService->openAiKey(); $openAI = new OpenAi($key); $baseUrl = $this->settingService->openaiProxy(); $baseUrl && $openAI->setBaseURL($baseUrl); $openAI->setHeader(["Content-Type"=>"text/event-stream"]); // 本次所有结果 $replyContent = ''; $openAI->chat($opts, function ($curl_info, $data) use ($fd, $server, &$replyContent) { $datas = explode('data: ', $data); foreach ($datas as $dataStr) { $arrayData = json_decode($dataStr, true); if ($arrayData) { if (isset($arrayData['choices'][0]['delta']['content'])) { $replyContent .= ($content = $arrayData['choices'][0]['delta']['content'] ?? ''); $server->push($fd, json_encode([ 'type' => 'message', 'content' => $content, 'status' => false, ], JSON_UNESCAPED_UNICODE)); } elseif (!empty($arrayData['choices'][0]['finish_reason'])) { $server->push($fd, '{"type":"message","content":"","status":true}'); } elseif (isset($arrayData['error'])) { $server->push($fd, '{"type":"error","content":"' . ($arrayData['error']['message'] ?? '') . '","status":true}'); } } } return \strlen($data); }); $date = date('Y-m-d H:i:s', $time); $this->chatMessageService->save([ 'role' => 2, 'sid' => $data->sid ?: 1, 'content' => htmlspecialchars($data->content), 'reply_content' => $replyContent ? htmlspecialchars($replyContent) : '', 'reply_at' => $date, 'created_at' => $date, ]); $server->push($fd, '{"type":"message","content":"","status":true}'); }catch (\Throwable $exception){ $server->push($fd, '{"type":"error","content":"'.$exception->getMessage().'","status":true}'); } } public function onClose($server, int $fd, int $reactorId): void { try { // 解除绑定fd $user = $this->redis->hGetAll(RedisConst::FD_TO_USER . $fd); if (!empty($user['uid'])) { $this->redis->zRem(RedisConst::USER_TO_FD, $user['uid']); } $this->redis->del(RedisConst::FD_TO_USER . $fd); }catch (\Throwable $exception){ } } /** * @param Response|Server $server * @param Request $request * @throws \RedisException */ public function onOpen($server, $request): void { try { $token = $request->get['token'] ?? ''; if (empty($token)){ $server->push($request->fd, '{"type":"login","content":"token无效1"}'); $server->close(); return; } if (false === $this->loginService->check($token, 'ai')) { $server->push($request->fd, '{"type":"login","content":"token无效2"}'); $server->close(); return; } $userInfo = $this->loginService->getInfo($token); if (empty($userInfo['id'])){ $server->push($request->fd, '{"type":"login","content":"token无效3"}'); $server->close(); return; } $this->redis->zAdd(RedisConst::USER_TO_FD, $request->fd, $userInfo['id']); $this->redis->hMSet(RedisConst::FD_TO_USER . $request->fd, [ 'uid' => $userInfo['id'], 'vip' => $userInfo['vip'] ?? 0, ]); $this->redis->expire(RedisConst::USER_TO_FD, 86400); $this->redis->expire(RedisConst::FD_TO_USER . $request->fd, 86400); $server->push($request->fd, '{"type":"opened","content":"ok"}'); } catch (\Throwable $exception) { $server->push($request->fd, '{"type":"error","content":"' . $exception->getMessage() . '"}'); } } } ================================================ FILE: MineAdmin/php/app/Ai/Command/InitMenuCommand.php ================================================ 0, 'level' => 0, 'code' => 'aiapp',])->first(); if (!empty($isInit)) { $this->line('已经执行过了', 'info'); return; } $tableName = env('DB_PREFIX') . SystemMenu::getModel()->getTable(); $sql = "INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (0, '0', 'AI系统', 'aiapp', 'IconFire', 'ai', NULL, NULL, 2, 'M', 1, 96, 1, 1, now(), now(), NULL, NULL); SET @topid := LAST_INSERT_ID(); SET @toplevel := CONCAT('0', ',', @topid); -- chatgptPrompts -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, 'chatgpt角色', 'ai:chatgptPrompts', 'IconUser', 'ai/chatgptPrompts', 'ai/chatgptPrompts/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色列表', 'ai:chatgptPrompts:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色更新', 'ai:chatgptPrompts:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色保存', 'ai:chatgptPrompts:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色读取', 'ai:chatgptPrompts:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色删除', 'ai:chatgptPrompts:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); -- chatMessage -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '聊天数据', 'ai:chatMessage', 'IconNotification', 'ai/chatMessage', 'ai/chatMessage/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '聊天数据列表', 'ai:chatMessage:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '聊天数据读取', 'ai:chatMessage:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '聊天数据删除', 'ai:chatMessage:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); -- chatSession -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '问答会话', 'ai:chatSession', 'IconSend', 'ai/chatSession', 'ai/chatSession/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '问答会话列表', 'ai:chatSession:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '问答会话读取', 'ai:chatSession:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '问答会话删除', 'ai:chatSession:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); -- mineMenuGroup -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, 'Mine菜单分组', 'ai:mineMenuGroup', 'IconMenuUnfold', 'ai/mineMenuGroup', 'ai/mineMenuGroup/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组列表', 'ai:mineMenuGroup:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组保存', 'ai:mineMenuGroup:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组更新', 'ai:mineMenuGroup:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组读取', 'ai:mineMenuGroup:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组删除', 'ai:mineMenuGroup:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); -- mineMenu -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, 'Mine菜单', 'ai:mineMenu', 'IconSelectAll', 'ai/mineMenu', 'ai/mineMenu/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单列表', 'ai:mineMenu:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单保存', 'ai:mineMenu:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单更新', 'ai:mineMenu:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单读取', 'ai:mineMenu:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单删除', 'ai:mineMenu:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); -- order -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '订单列表', 'ai:order', 'icon-home', 'ai/order', 'ai/order/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '订单表列表', 'ai:order:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '订单表读取', 'ai:order:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '订单表删除', 'ai:order:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); -- quickIssue -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '快捷问题', 'ai:quickIssue', 'IconExclamationCircle', 'ai/quickIssue', 'ai/quickIssue/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题列表', 'ai:quickIssue:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题更新', 'ai:quickIssue:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题保存', 'ai:quickIssue:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题读取', 'ai:quickIssue:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题删除', 'ai:quickIssue:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); -- user -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '用户列表', 'ai:user', 'IconUserGroup', 'ai/user', 'ai/user/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户主表列表', 'ai:user:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户主表更新', 'ai:user:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户主表读取', 'ai:user:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户主表删除', 'ai:user:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户开通VIP', 'ai:user:open-vip', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户锁定', 'ai:user:lock', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL); -- payKami -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '卡密', 'ai:payKami', 'icon-home', 'ai/payKami', 'ai/payKami/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '卡密列表', 'ai:payKami:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '卡密读取', 'ai:payKami:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '卡密更新', 'ai:payKami:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '卡密删除', 'ai:payKami:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '创建卡密', 'ai:payKami:add', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL); -- setting -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '设置', 'ai:setting', 'IconSettings', 'ai/setting', 'ai/setting/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL); -- openKey -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, 'openai_key', 'ai:openKey', 'icon-home', 'ai/openKey', 'ai/openKey/index', NULL, 2, 'M', 1, 0, 1, NULL, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key列表', 'ai:openKey:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key保存', 'ai:openKey:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key更新', 'ai:openKey:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key读取', 'ai:openKey:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key删除', 'ai:openKey:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key刷新缓存', 'ai:openKey:refresh-cache-list', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key批量添加', 'ai:openKey:batch-add', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL); -- imageMaterial -- INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '图片素材', 'ai:imageMaterial', 'icon-home', 'ai/imageMaterial', 'ai/imageMaterial/index', NULL, '2', 'M', '1', 0, 1, NULL, now(), now(), NULL, NULL); SET @id := LAST_INSERT_ID(); SET @level := CONCAT('0',',', @topid, ',', @id); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '列表'), CONCAT('ai:imageMaterial',':index'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '保存'), CONCAT('ai:imageMaterial',':save'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '更新'), CONCAT('ai:imageMaterial',':update'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '读取'), CONCAT('ai:imageMaterial',':read'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL); INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '删除'), CONCAT('ai:imageMaterial',':delete'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL); "; Db::unprepared($sql); $this->line('大功告成', 'info'); } } ================================================ FILE: MineAdmin/php/app/Ai/Constants/OrderConst.php ================================================ '1010', 'ai_mine_menu_icon' => '1011', 'ai_customer_wx_img' => '1012', 'ai_customer_head_img' => '1013', 'ai_image_materialg' => '1014', ]; // 2开头,例如 2001 2002 const AudioScene = [ ]; // 3开头,例如 3001 3002 const VideoScene = [ ]; public static function hasScene(string $scene): bool { if (in_array($scene, self::ImageScene) || in_array($scene, self::AudioScene) || in_array($scene, self::VideoScene)) { return true; } return false; } public static function isOnly(string $scene): bool { return in_array($scene, ['1010']); } } ================================================ FILE: MineAdmin/php/app/Ai/Constants/VipConst.php ================================================ 'VIP', // VIP名称 'price' => '199', // 现价 'price_pay' => '199', // 支付金额 'price_old' => '198', // 原价 'level' => 10, // 等级 'length' => 12, // 时长(月) 'income' => 0, // 收益% 'wrap_vip' => 0, // VIP抵扣包 'is_default' => false,// 默认选中 'is_choose' => false,// 是否可选 'btn_text' => '立即开通', ], [ 'name' => '一星', 'price' => '299', 'price_pay' => '299', 'price_old' => '1198', 'level' => 20, 'length' => 36, 'income' => 35, 'wrap_vip' => 10, 'is_default' => false, 'is_choose' => false, 'btn_text' => '立即开通', ], [ 'name' => '二星', 'price' => '399', 'price_pay' => '399', 'price_old' => '2198', 'level' => 30, 'length' => 60, 'income' => 50, 'wrap_vip' => 20, 'is_default' => false, 'is_choose' => false, 'btn_text' => '立即开通', ], ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Constants/WalletConst.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:chatMessage:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:chatMessage:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiChatSessionController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:chatSession:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:chatSession:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiChatgptPromptsController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 更新 * @param int $id * @param AiChatgptPromptsRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PutMapping("update/{id}"), Permission("ai:chatgptPrompts:update"), OperationLog] public function update(int $id, AiChatgptPromptsRequest $request): ResponseInterface { return $this->service->update($id, $request->all()) ? $this->success() : $this->error(); } /** * 新增 * @param AiChatgptPromptsRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("save"), Permission("ai:chatgptPrompts:save"), OperationLog] public function save(AiChatgptPromptsRequest $request): ResponseInterface { return $this->success(['id' => $this->service->save($request->all())]); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:chatgptPrompts:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:chatgptPrompts:delete"), OperationLog] public function delete(): ResponseInterface { $ids = $this->request->input('ids', []); if (in_array(1, $ids)) { $this->error('id为1的角色不能删除!'); } return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiImageMaterialController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 新增 * @param AiImageMaterialRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("save"), Permission("ai:imageMaterial:save"), OperationLog] public function save(AiImageMaterialRequest $request): ResponseInterface { return $this->success(['id' => $this->service->save($request->all())]); } /** * 更新 * @param int $id * @param AiImageMaterialRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PutMapping("update/{id}"), Permission("ai:imageMaterial:update"), OperationLog] public function update(int $id, AiImageMaterialRequest $request): ResponseInterface { return $this->service->update($id, $request->all()) ? $this->success() : $this->error(); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:imageMaterial:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:imageMaterial:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiMineMenuController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 新增 * @param AiMineMenuRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("save"), Permission("ai:mineMenu:save"), OperationLog] public function save(AiMineMenuRequest $request): ResponseInterface { return $this->success(['id' => $this->service->save($request->all())]); } /** * 更新 * @param int $id * @param AiMineMenuRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PutMapping("update/{id}"), Permission("ai:mineMenu:update"), OperationLog] public function update(int $id, AiMineMenuRequest $request): ResponseInterface { return $this->service->update($id, $request->all()) ? $this->success() : $this->error(); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:mineMenu:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:mineMenu:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiMineMenuGroupController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 新增 * @param AiMineMenuGroupRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("save"), Permission("ai:mineMenuGroup:save"), OperationLog] public function save(AiMineMenuGroupRequest $request): ResponseInterface { return $this->success(['id' => $this->service->save($request->all())]); } /** * 更新 * @param int $id * @param AiMineMenuGroupRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PutMapping("update/{id}"), Permission("ai:mineMenuGroup:update"), OperationLog] public function update(int $id, AiMineMenuGroupRequest $request): ResponseInterface { return $this->service->update($id, $request->all()) ? $this->success() : $this->error(); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:mineMenuGroup:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:mineMenuGroup:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiOpenaiKeyController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 新增 * @param AiOpenaiKeyRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("save"), Permission("ai:openKey:save"), OperationLog] public function save(AiOpenaiKeyRequest $request): ResponseInterface { return $this->success(['id' => $this->service->save($request->all())]); } /** * 更新 * @param int $id * @param AiOpenaiKeyRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PutMapping("update/{id}"), Permission("ai:openKey:update"), OperationLog] public function update(int $id, AiOpenaiKeyRequest $request): ResponseInterface { return $this->service->update($id, $request->all()) ? $this->success() : $this->error(); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:openKey:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:openKey:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } /** * 批量添加 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("batch-add"), Permission("ai:openKey:batch-add"), OperationLog] public function batchAdd(): ResponseInterface { return $this->service->batchAdd($this->request->all()) ? $this->success() : $this->error(); } /** * 缓存列表 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("refresh-cache-list"), Permission("ai:openKey:refresh-cache-list"), OperationLog] public function refreshCache(): ResponseInterface { return $this->service->cacheAll() ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiOrderController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:order:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:order:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiPayKamiController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:payKami:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 更新 * @param int $id * @param AiPayKamiRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PutMapping("update/{id}"), Permission("ai:payKami:update"), OperationLog] public function update(int $id, AiPayKamiRequest $request): ResponseInterface { return $this->service->update($id, $request->all()) ? $this->success() : $this->error(); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:payKami:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("add"), Permission("ai:payKami:add"), OperationLog] public function add(): ResponseInterface { $this->service->add($this->request->all()); return $this->success(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiQuickIssueController.php ================================================ success($this->service->getPageList($this->request->all())); } /** * 更新 * @param int $id * @param AiQuickIssueRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PutMapping("update/{id}"), Permission("ai:quickIssue:update"), OperationLog] public function update(int $id, AiQuickIssueRequest $request): ResponseInterface { return $this->service->update($id, $request->all()) ? $this->success() : $this->error(); } /** * 新增 * @param AiQuickIssueRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("save"), Permission("ai:quickIssue:save"), OperationLog] public function save(AiQuickIssueRequest $request): ResponseInterface { return $this->success(['id' => $this->service->save($request->all())]); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:quickIssue:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:quickIssue:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiSettingController.php ================================================ success(array_merge([ 'app_close_message' => $this->settingService->appClose(), 'agreement_user' => $this->settingService->agreementUser(), 'openai_proxy' => $this->settingService->openaiProxy(), ], $this->settingService->customer())); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ #[PostMapping("save"), Permission("ai:setting, ai:setting:save")] public function save(): ResponseInterface { $this->settingService->setAppClose($this->request->post('app_close_message', '')); $this->settingService->setAgreementUser($this->request->post('agreement_user', '')); $this->settingService->setCustomer($this->request->post('customer')); $this->settingService->setOpenaiProxy($this->request->post('openai_proxy')); return $this->success(); } #[Inject] protected QiniuService $qiniuService; #[GetMapping("upload-token")] public function uploadToken(): ResponseInterface { return $this->success($this->qiniuService->token($this->request->query('scenes'))); } } ================================================ FILE: MineAdmin/php/app/Ai/Controller/AiUserController.php ================================================ success($this->service->userList($this->request->all())); } /** * 更新 * @param int $id * @param AiUserRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PutMapping("update/{id}"), Permission("ai:user:update"), OperationLog] public function update(int $id, AiUserRequest $request): ResponseInterface { return $this->service->update($id, $request->all()) ? $this->success() : $this->error(); } /** * 读取数据 * @param int $id * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[GetMapping("read/{id}"), Permission("ai:user:read")] public function read(int $id): ResponseInterface { return $this->success($this->service->read($id)); } /** * 单个或批量删除数据到回收站 * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[DeleteMapping("delete"), Permission("ai:user:delete"), OperationLog] public function delete(): ResponseInterface { return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error(); } /** * 更新 * @param int $id * @param AiUserRequest $request * @return ResponseInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ #[PostMapping("open-vip/{id}"), Permission("ai:user:open-vip"), OperationLog] public function openVip(): ResponseInterface { $this->orderService->adminOpenVip($this->request->all()); return $this->success(); } #[PostMapping("lock/{id}"), Permission("ai:user:lock"), OperationLog] public function lock(int $id): ResponseInterface { $this->service->lock($id); return $this->success(); } } ================================================ FILE: MineAdmin/php/app/Ai/Crontab/CheckVipOver.php ================================================ ', $lastId)->where('vip','>', 0)->limit(1000)->get(); /** * @var AiUser $user */ $uidArr = []; foreach ($userList as $user) { if ($user->vip_ent_at && $time > strtotime($user->vip_ent_at)) { $uidArr[] = $user->id; } $lastId = $user->id; } $uidArr && AiUser::whereIn('id', $uidArr)->update([ 'vip' => 0 ]); if (count($userList) < 1000) { break; } } } catch (\Throwable $exception){ } return 'success'; } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_04_145048_create_ai_user_table.php ================================================ engine = 'Innodb'; $table->comment('用户主表'); $table->increments('id')->comment('主键'); $table->addColumn('string', 'nick_name', ['length' => 50, 'comment' => '昵称'])->default('')->nullable(false); $table->addColumn('string', 'head_img', ['length' => 100, 'comment' => '头像'])->default('')->nullable(false); $table->addColumn('string', 'mobile', ['length' => 20, 'comment' => '手机号'])->default('')->nullable(false); $table->addColumn('tinyInteger', 'vip', ['length' => 1, 'comment' => 'vip等级'])->index()->default(0)->nullable(false); $table->addColumn('timestamp', 'vip_ent_at', ['precision' => 0, 'comment' => 'vip到期时间'])->nullable(); $table->addColumn('tinyInteger', 'is_lock', ['length' => 1, 'comment' => '是否锁定:1正常,2锁定'])->default(1)->nullable(false); $table->addColumn('string', 'password', ['length' => 32, 'comment' => '密码'])->default('')->nullable(false); $table->addColumn('integer', 'updated_by', ['comment' => '更新者'])->default(0)->nullable(); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_user'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095456_create_ai_user_wallet_table.php ================================================ engine = 'Innodb'; $table->comment('用户钱包'); $table->addColumn('integer', 'uid')->primary()->comment('用户UID'); $table->addColumn('integer', 'balance')->comment('余额')->unsigned()->default(0)->nullable(false); $table->addColumn('integer', 'balance_total')->comment('总收入')->unsigned()->default(0)->nullable(false); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_user_wallet'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095504_create_ai_user_wallet_log_table.php ================================================ engine = 'Innodb'; $table->comment('钱包变动记录'); $table->increments('id')->comment('主键'); $table->addColumn('integer', 'uid')->index()->comment('用户UID')->nullable(false); $table->addColumn('integer', 'oid')->comment('订单ID')->index()->default(0)->nullable(false); $table->addColumn('tinyInteger', 'direction', ['length' => 1, 'comment' => '类型:1收入,2支出'])->default(1)->nullable(false); $table->addColumn('integer', 'balance')->comment('金额')->default(0)->nullable(false); $table->addColumn('tinyInteger', 'scene', ['length' => 1, 'comment' => '变动场景: 1推广获益'])->default(1)->nullable(false); $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default("")->nullable(false); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_user_wallet_log'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095525_create_ai_user_relation_table.php ================================================ engine = 'Innodb'; $table->comment('用户关系'); $table->addColumn('integer', 'uid')->primary()->comment('用户UID'); $table->addColumn('integer', 'from_uid', ['comment' => '上级'])->index()->default(1)->nullable(false); $table->addColumn('integer', 'market_uid', ['comment' => '上级营销部'])->index()->default(0)->nullable(false); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_user_relation'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095540_create_ai_order_table.php ================================================ engine = 'Innodb'; $table->comment('订单表'); $table->increments('id')->comment('主键'); $table->addColumn('integer', 'uid', ['comment' => '用户UID'])->index()->default(0)->nullable(false); $table->addColumn('integer', 'from_uid', ['comment' => '上级UID'])->default(0)->nullable(false); $table->addColumn('integer', 'market_uid', ['comment' => '营销部UID'])->default(0)->nullable(false); $table->addColumn('string', 'ord_sn', ['length' => 32, 'comment' => '订单号'])->unique()->default('')->nullable(false); $table->addColumn('tinyInteger', 'ord_type', ['length' => 1, 'comment' => '订单类型: 1开通VIP 2提现 3成为营销部'])->default(1)->nullable(false); $table->addColumn('tinyInteger', 'pay_type', ['length' => 1, 'comment' => '支付方式: 1微信 2后台付费'])->default(1)->nullable(false); $table->addColumn('tinyInteger', 'status', ['length' => 1, 'comment' => '订单状态 1未支付(待处理) 2已支付(已完成) 3失败'])->default(1)->nullable(false); $table->addColumn('integer', 'total_price', ['length' => 8, 'comment' => '总金额'])->default(0)->nullable(false); $table->addColumn('integer', 'amount_price', ['length' => 8, 'comment' => '实际金额'])->default(0)->nullable(false); $table->addColumn('string', 'content', ['length' => 255, 'comment' => '订单描述'])->default('')->nullable(false); $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default("")->nullable(false); $table->addColumn('bigInteger', 'created_by', ['comment' => '创建者'])->default(0)->nullable(); $table->addColumn('bigInteger', 'updated_by', ['comment' => '更新者'])->default(0)->nullable(); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'pay_at', ['precision' => 0, 'comment' => '订单完成时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_order'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095543_create_ai_order_vip_table.php ================================================ engine = 'Innodb'; $table->comment('vip订单'); $table->integer('oid')->primary()->comment('订单ID'); $table->addColumn('tinyInteger', 'vip_level', ['length'=>1, 'comment' => 'vip等级'])->default(1)->nullable(false); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_order_vip'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_102806_create_ai_mine_menu_group_table.php ================================================ engine = 'Innodb'; $table->comment('个人中心菜单分组'); $table->bigIncrements('id')->comment('主键'); $table->addColumn('string', 'name', ['length' => 100, 'comment' => '分组名称'])->default('')->nullable(false); $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false); $table->addColumn('tinyInteger', 'is_lock', ['length' => 1, 'comment' => '是否锁定:1正常,锁定'])->default(1)->nullable(false); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_mine_menu_group'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_102817_create_ai_mine_menu_table.php ================================================ engine = 'Innodb'; $table->comment('个人中心菜单'); $table->bigIncrements('id')->comment('主键'); $table->addColumn('integer', 'gid', ['comment' => '分组ID'])->index()->default(0)->nullable(false); $table->addColumn('string', 'name', ['length' => 50, 'comment' => '菜单名称'])->default('')->nullable(false); $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false); $table->addColumn('tinyInteger', 'use_vip', ['length' => 2, 'comment' => '使用权限限制 0全部'])->default(0)->nullable(false); $table->addColumn('tinyInteger', 'click_type', ['length' => 1, 'comment' => '点击类型 1跳转 2调用函数'])->default(1)->nullable(false); $table->addColumn('string', 'click_func', ['length' => 50, 'comment' => '函数标识 小程序端提前封装'])->default('')->nullable(false); $table->addColumn('string', 'path', ['length' => 100, 'comment' => '打开的页面路径'])->default('')->nullable(false); $table->addColumn('string', 'app_id', ['length' => 100, 'comment' => '小程序appid'])->default('')->nullable(false); $table->addColumn('string', 'extra_data', ['length' => 100, 'comment' => '需要传递给目标小程序的数据 json'])->default('')->nullable(false); $table->addColumn('string', 'env_version', ['length' => 100, 'comment' => '要打开的小程序版本'])->default('')->nullable(false); $table->addColumn('string', 'short_link', ['length' => 100, 'comment' => '小程序链接'])->default('')->nullable(false); $table->addColumn('string', 'icon', ['length' => 255, 'comment' => '菜单图标'])->default('')->nullable(false); $table->addColumn('tinyInteger', 'is_lock', ['length' => 1, 'comment' => '是否锁定:1正常,2锁定'])->default(1)->nullable(false); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_mine_menu'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_154733_create_ai_chatgpt_prompts_table.php ================================================ engine = 'Innodb'; $table->comment('chatgpt角色'); $table->increments('id')->comment('主键'); $table->addColumn('string', 'act', ['length'=>100, 'comment' => '角色名称'])->default('')->nullable(false); $table->addColumn('text', 'prompt', ['comment' => '角色说明'])->nullable(false); $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false); $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default('')->nullable(false); $table->addColumn('integer', 'created_by', ['comment' => '创建者'])->default(0)->nullable(false); $table->addColumn('integer', 'updated_by', ['comment' => '更新者'])->default(0)->nullable(false); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_chatgpt_prompts'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_10_165842_create_ai_chat_message_table.php ================================================ engine = 'Innodb'; $table->comment('聊天数据'); $table->bigIncrements('id')->comment('主键'); $table->addColumn('integer', 'sid', ['comment' => '会话ID'])->index()->default(0)->nullable(false); $table->addColumn('text', 'content', ['comment' => '内容'])->nullable(false); $table->addColumn('text', 'reply_content', ['comment' => '回复内容'])->nullable(false); $table->addColumn('timestamp', 'reply_at', ['precision' => 0, 'comment' => '回复时间'])->nullable(); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_chat_message'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_10_171603_create_ai_chat_session_table.php ================================================ engine = 'Innodb'; $table->comment('问答会话'); $table->increments('id')->comment('主键'); $table->addColumn('integer', 'uid', ['comment' => '用户uid'])->index()->default(0)->nullable(false); $table->addColumn('integer', 'prompt_id', ['comment' => '模型ID'])->index()->default(0)->nullable(false); $table->addColumn('tinyInteger', 'close', ['length' => 1, 'comment' => '是否关闭:1正常,2关闭'])->default(1)->nullable(false); $table->addColumn('tinyInteger', 'share', ['length' => 1, 'comment' => '是否分享:1关闭,2公开'])->default(1)->nullable(false); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_chat_session'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_12_152504_create_ai_quick_issue_table.php ================================================ engine = 'Innodb'; $table->comment('快捷问题'); $table->increments('id')->comment('主键'); $table->addColumn('string', 'title', ['length' => 100, 'comment' => '问题标题'])->default('')->nullable(false); $table->addColumn('string', 'content', ['length' => 255, 'comment' => '问题描述'])->default('')->nullable(false); $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_quick_issue'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_19_164456_create_ai_pay_kami_table.php ================================================ engine = 'Innodb'; $table->comment('卡密'); $table->increments('id')->comment('主键'); $table->addColumn('integer', 'uid', ['comment' => '绑定用户'])->default(0)->nullable(false); $table->addColumn('integer', 'price', ['comment' => '价格'])->default(0)->nullable(false); $table->addColumn('string', 'code', ['length' => 32, 'comment' => '卡密号'])->unique()->default('')->nullable(false); $table->addColumn('tinyInteger', 'status', ['length' => 1, 'comment' => '状态 1未使用 2已使用'])->default(1)->nullable(false); $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default('')->nullable(false); $table->addColumn('bigInteger', 'created_by', ['comment' => '创建者'])->default(0)->nullable(false); $table->addColumn('bigInteger', 'updated_by', ['comment' => '更新者'])->default(0)->nullable(false); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'use_at', ['precision' => 0, 'comment' => '使用时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_pay_carmi'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_25_152855_create_ai_openai_key_table.php ================================================ engine = 'Innodb'; $table->comment('openai_key'); $table->increments('id')->comment('主键'); $table->addColumn('string', 'openai_key', ['length' => 255, 'comment' => 'openai_key'])->nullable(); $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->nullable(); $table->addColumn('bigInteger', 'created_by', ['comment' => '创建者'])->nullable(); $table->addColumn('bigInteger', 'updated_by', ['comment' => '更新者'])->nullable(); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_openai_key'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_05_26_111034_create_ai_order_kami_table.php ================================================ engine = 'Innodb'; $table->comment('订单关联卡密'); $table->integer('oid')->primary()->comment('订单ID'); $table->integer('kid')->comment('卡密ID'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_order_kami'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Migrations/2023_06_14_141605_create_ai_image_material_table.php ================================================ engine = 'Innodb'; $table->comment('图片素材'); $table->increments('id')->comment('主键'); $table->addColumn('tinyInteger', 'scene', ['length' => 1, 'comment' => '使用场景'])->default(1)->nullable(false); $table->addColumn('string', 'img_url', ['length' => 255, 'comment' => '图片地址'])->default('')->nullable(false); $table->addColumn('string', 'url', ['length' => 255, 'comment' => '跳转地址'])->default('')->nullable(false); $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default('')->nullable(false); $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false); $table->addColumn('integer', 'created_by', ['comment' => '创建者'])->default(0)->nullable(false); $table->addColumn('integer', 'updated_by', ['comment' => '更新者'])->default(0)->nullable(false); $table->addColumn('timestamp', 'start_at', ['precision' => 0, 'comment' => '使用开始时间'])->nullable(); $table->addColumn('timestamp', 'end_at', ['precision' => 0, 'comment' => '使用结束时间'])->nullable(); $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable(); $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable(); $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('ai_image_material'); } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Seeders/ai_chatgpt_prompts.php ================================================ 1])->first(); if (!empty($isInit)){ echo '大功告成'.PHP_EOL; return; } $tableName = env('DB_PREFIX') . \App\Ai\Model\AiChatgptPrompts::getModel()->getTable(); $sql = [ "INSERT INTO `{$tableName}`(`id`, `act`, `prompt`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (1, 'Ai助手', '你是一个Ai智能助手,我需要你模拟一名温柔贴心的女朋友来回答我的问题。', 0, 1, 1, now(), now(), NULL, '')", ]; foreach ($sql as $item) { Db::insert($item); } echo '大功告成'.PHP_EOL; } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Seeders/ai_mine_menu.php ================================================ truncate(); $tableName = env('DB_PREFIX') . \App\Ai\Model\AiMineMenu::getModel()->getTable(); $sql = [ "INSERT INTO `{$tableName}`(`id`, `gid`, `name`, `sort`, `use_vip`, `click_type`, `click_func`, `path`, `app_id`, `extra_data`, `env_version`, `short_link`, `icon`, `is_lock`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 1, '会员特权', 4, 0, 2, '1002', '', '', '', '', '', 'https://www.putyy.com/uploads/images/vip.png', 1, now(), now(), NULL);", "INSERT INTO `{$tableName}`(`id`, `gid`, `name`, `sort`, `use_vip`, `click_type`, `click_func`, `path`, `app_id`, `extra_data`, `env_version`, `short_link`, `icon`, `is_lock`, `created_at`, `updated_at`, `deleted_at`) VALUES (2, 1, '联系客服', 3, 0, 2, '1001', '', '', '', '', '', 'https://www.putyy.com/uploads/images/kf.png', 2, now(), now(), NULL);", "INSERT INTO `{$tableName}`(`id`, `gid`, `name`, `sort`, `use_vip`, `click_type`, `click_func`, `path`, `app_id`, `extra_data`, `env_version`, `short_link`, `icon`, `is_lock`, `created_at`, `updated_at`, `deleted_at`) VALUES (3, 1, '清空缓存', 0, 0, 2, '1003', '', '', '', '', '', 'https://www.putyy.com/uploads/images/clear.png', 1, now(), now(), NULL);", "INSERT INTO `{$tableName}`(`id`, `gid`, `name`, `sort`, `use_vip`, `click_type`, `click_func`, `path`, `app_id`, `extra_data`, `env_version`, `short_link`, `icon`, `is_lock`, `created_at`, `updated_at`, `deleted_at`) VALUES (4, 1, '订单列表', 0, 0, 1, '', '/pages/user/order', '', '', '', '', 'https://www.putyy.com/uploads/images/list.png', 1, now(), now(), NULL);", ]; foreach ($sql as $item){ Db::insert($item); } echo '大功告成'.PHP_EOL; } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Seeders/ai_mine_menu_group.php ================================================ truncate(); $tableName = env('DB_PREFIX') . \App\Ai\Model\AiMineMenuGroup::getModel()->getTable(); Db::insert("INSERT INTO `$tableName`(`id`, `name`, `sort`, `is_lock`, `created_at`, `updated_at`) VALUES (1, '会员服务', 1, 1, now(), now());"); echo '大功告成'.PHP_EOL; } } ================================================ FILE: MineAdmin/php/app/Ai/Database/Seeders/ai_user.php ================================================ * @Link https://gitee.com/xmo/MineAdmin */ declare(strict_types=1); use Hyperf\Database\Seeders\Seeder; use Hyperf\DbConnection\Db; class AiUser extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $isInit = \App\Ai\Model\AiUser::where(['id'=>1])->first(); if (!empty($isInit)){ echo '大功告成'.PHP_EOL; return; } $tableName = env('DB_PREFIX') . \App\Ai\Model\AiUser::getModel()->getTable(); Db::insert("INSERT INTO `{$tableName}`(`id`, `nick_name`, `head_img`, `mobile`, `vip`, `vip_ent_at`, `password`, `is_lock`, `updated_by`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 'ChatGPT', '', '123456789', 30, '2036-03-11 16:02:58', 'e10adc3949ba59abbe56e057f20f883e', 1, 1, now(), now(), NULL);"); \App\Ai\Model\AiUserRelation::insert([ 'uid' => 1, 'from_uid' => 1, ]); \App\Ai\Model\AiUserWallet::insert([ 'uid' => 1, ]); echo '大功告成'.PHP_EOL; } } ================================================ FILE: MineAdmin/php/app/Ai/Dto/AiChatMessageDto.php ================================================ model = AiChatMessage::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 会话ID if (isset($params['sid']) && $params['sid'] !== '') { $query->where('sid', '=', $params['sid']); } // 内容 if (isset($params['content']) && $params['content'] !== '') { $query->where('content', 'like', "%{$params['content']}%"); } // 内容 if (isset($params['reply_content']) && $params['reply_content'] !== '') { $query->where('reply_content', 'like', "%{$params['reply_content']}%"); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiChatSessionMapper.php ================================================ model = AiChatSession::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 主键 if (isset($params['id']) && $params['id'] !== '') { $query->where('id', '=', $params['id']); } // 用户uid if (isset($params['uid']) && $params['uid'] !== '') { $query->where('uid', '=', $params['uid']); } // 模型ID if (isset($params['prompt_id']) && $params['prompt_id'] !== '') { $query->where('prompt_id', '=', $params['prompt_id']); } // 是否关闭:1正常,2关闭 if (isset($params['close']) && $params['close'] !== '') { $query->where('close', '=', $params['close']); } // 是否分享:1关闭,2公开 if (isset($params['share']) && $params['share'] !== '') { $query->where('share', '=', $params['share']); } return $query; } public function session($where): Model|Builder|null { return $this->model::where($where)->orderBy('id', 'desc')->first(); } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiChatgptPromptsMapper.php ================================================ model = AiChatgptPrompts::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 角色名称 if (isset($params['act']) && $params['act'] !== '') { $query->where('act', 'like', '%'.$params['act'].'%'); } // 角色说明 if (isset($params['prompt']) && $params['prompt'] !== '') { $query->where('prompt', 'like', '%'.$params['prompt'].'%'); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiImageMaterialMapper.php ================================================ model = AiImageMaterial::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 使用场景 if (isset($params['scene']) && $params['scene'] !== '') { $query->where('scene', '=', $params['scene']); } // 备注 if (isset($params['remark']) && $params['remark'] !== '') { $query->where('remark', 'like', '%'.$params['remark'].'%'); } // 使用开始时间 if (isset($params['start_at']) && is_array($params['start_at']) && count($params['start_at']) == 2) { $query->whereBetween( 'start_at', [ $params['start_at'][0], $params['start_at'][1] ] ); } // 使用结束时间 if (isset($params['end_at']) && is_array($params['end_at']) && count($params['end_at']) == 2) { $query->whereBetween( 'end_at', [ $params['end_at'][0], $params['end_at'][1] ] ); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiMineMenuGroupMapper.php ================================================ model = AiMineMenuGroup::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 分组名称 if (isset($params['name']) && $params['name'] !== '') { $query->where('name', 'like', '%'.$params['name'].'%'); } // 排序 if (isset($params['sort']) && $params['sort'] !== '') { $query->where('sort', '=', $params['sort']); } // 是否锁定:1正常,锁定 if (isset($params['is_lock']) && $params['is_lock'] !== '') { $query->where('is_lock', '=', $params['is_lock']); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } // 更新时间 if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) { $query->whereBetween( 'updated_at', [ $params['updated_at'][0], $params['updated_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiMineMenuMapper.php ================================================ model = AiMineMenu::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 分组名称 if (isset($params['name']) && $params['name'] !== '') { $query->where('name', 'like', '%'.$params['name'].'%'); } // 是否锁定:1正常,2锁定 if (isset($params['is_lock']) && $params['is_lock'] !== '') { $query->where('is_lock', '=', $params['is_lock']); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } // 更新时间 if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) { $query->whereBetween( 'updated_at', [ $params['updated_at'][0], $params['updated_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiOpenaiKeyMapper.php ================================================ model = AiOpenaiKey::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // openai_key if (isset($params['openai_key']) && $params['openai_key'] !== '') { $query->where('openai_key', 'like', '%'.$params['openai_key'].'%'); } // 备注 if (isset($params['remark']) && $params['remark'] !== '') { $query->where('remark', 'like', '%'.$params['remark'].'%'); } // 主键 if (isset($params['id']) && $params['id'] !== '') { $query->where('id', '=', $params['id']); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } // 更新时间 if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) { $query->whereBetween( 'updated_at', [ $params['updated_at'][0], $params['updated_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiOrderMapper.php ================================================ model = AiOrder::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 用户UID if (isset($params['uid']) && $params['uid'] !== '') { $query->where('uid', '=', $params['uid']); } // 订单号 if (isset($params['ord_sn']) && $params['ord_sn'] !== '') { $query->where('ord_sn', 'like', '%'.$params['ord_sn'].'%'); } // 订单类型: 1开通VIP 2提现 3成为营销部 if (isset($params['ord_type']) && $params['ord_type'] !== '') { $query->where('ord_type', '=', $params['ord_type']); } // 支付方式: 1微信 2后台付费 if (isset($params['pay_type']) && $params['pay_type'] !== '') { $query->where('pay_type', '=', $params['pay_type']); } // 订单状态 1未支付(待处理) 2已支付(已完成) 3失败 if (isset($params['status']) && $params['status'] !== '') { $query->where('status', '=', $params['status']); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } // 订单完成时间 if (isset($params['pay_at']) && is_array($params['pay_at']) && count($params['pay_at']) == 2) { $query->whereBetween( 'pay_at', [ $params['pay_at'][0], $params['pay_at'][1] ] ); } // 更新时间 if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) { $query->whereBetween( 'updated_at', [ $params['updated_at'][0], $params['updated_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiPayKamiMapper.php ================================================ model = AiPayKami::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 主键 if (isset($params['id']) && $params['id'] !== '') { $query->where('id', '=', $params['id']); } // 绑定用户 if (isset($params['uid']) && $params['uid'] !== '') { $query->where('uid', '=', $params['uid']); } // 卡密号 if (isset($params['code']) && $params['code'] !== '') { $query->where('code', 'like', '%'.$params['code'].'%'); } // 状态 1未使用 2已使用 if (isset($params['status']) && $params['status'] !== '') { $query->where('status', '=', $params['status']); } // 备注 if (isset($params['remark']) && $params['remark'] !== '') { $query->where('remark', 'like', '%'.$params['remark'].'%'); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } // 使用时间 if (isset($params['use_at']) && is_array($params['use_at']) && count($params['use_at']) == 2) { $query->whereBetween( 'use_at', [ $params['use_at'][0], $params['use_at'][1] ] ); } // 更新时间 if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) { $query->whereBetween( 'updated_at', [ $params['updated_at'][0], $params['updated_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiQuickIssueMapper.php ================================================ model = AiQuickIssue::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 问题标题 if (isset($params['title']) && $params['title'] !== '') { $query->where('title', 'like', '%'.$params['title'].'%'); } // 问题描述 if (isset($params['content']) && $params['content'] !== '') { $query->where('content', 'like', '%'.$params['content'].'%'); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Mapper/AiUserMapper.php ================================================ model = AiUser::class; } /** * 搜索处理器 * @param Builder $query * @param array $params * @return Builder */ public function handleSearch(Builder $query, array $params): Builder { // 昵称 if (isset($params['nick_name']) && $params['nick_name'] !== '') { $query->where('nick_name', 'like', '%'.$params['nick_name'].'%'); } // 手机号 if (isset($params['mobile']) && $params['mobile'] !== '') { $query->where('mobile', 'like', '%'.$params['mobile'].'%'); } // vip等级 if (isset($params['vip']) && $params['vip'] !== '') { $query->where('vip', '=', $params['vip']); } // vip到期时间 if (isset($params['vip_ent_at']) && is_array($params['vip_ent_at']) && count($params['vip_ent_at']) == 2) { $query->whereBetween( 'vip_ent_at', [ $params['vip_ent_at'][0], $params['vip_ent_at'][1] ] ); } // 是否锁定:1正常,2锁定 if (isset($params['is_lock']) && $params['is_lock'] !== '') { $query->where('is_lock', '=', $params['is_lock']); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } // 更新时间 if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) { $query->whereBetween( 'updated_at', [ $params['updated_at'][0], $params['updated_at'][1] ] ); } return $query; } } ================================================ FILE: MineAdmin/php/app/Ai/Middleware/AuthMiddleware.php ================================================ container = $container; $this->response = $response; $this->request = $request; $this->loginService = $container->get(AiLoginService::class); $this->settingService = $container->get(AiSettingService::class); $this->logger = $container->get(StdoutLoggerInterface::class); $this->userService = $container->get(AiUserService::class); } /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (false === $this->loginService->check(null, 'ai')) { throw new NormalStatusException('error', ResponseCodeConst::INVALID_TOKEN); } $data = $this->loginService->getInfo(); if (($data['version'] ?? '') != Login::VERSIONS) { throw new NormalStatusException('error', ResponseCodeConst::CLEAR_ALL_CACHE); } // 检测系统是否关站 if ($closeMsg = $this->settingService->appClose()) { throw new NormalStatusException($closeMsg, ResponseCodeConst::APP_CLOSE); } // 拦截刚删除的用户、锁定的账户 if ($this->userService->isJustDelete($data['id'])) { throw new NormalStatusException('账号不存在', ResponseCodeConst::CLEAR_ALL_CACHE); } if ($this->userService->isJustLock($data['id'])) { throw new NormalStatusException('账号被锁定', ResponseCodeConst::ACCOUNT_LOCK); } Context::set(self::class . '_login_data', $data); return $handler->handle($request); } } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiChatMessage.php ================================================ 'integer', 'sid' => 'integer']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiChatSession.php ================================================ 'integer', 'uid' => 'integer', 'prompt_id' => 'integer', 'close' => 'integer', 'share' => 'integer']; public function prompt(): HasOne { return $this->hasOne(AiChatgptPrompts::class, 'id', 'prompt_id'); } public function firstMessage(): HasOne { return $this->hasOne(AiChatMessage::class, 'sid', 'id'); } public function user(): HasOne { return $this->hasOne(AiUser::class, 'id', 'uid'); } } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiChatgptPrompts.php ================================================ 'integer', 'sort' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiImageMaterial.php ================================================ 'integer', 'scene' => 'integer', 'sort' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; public function setImgUrlAttribute($value) { $this->attributes['img_url'] = HelperService::buildSavePath($value); } public function getImgUrlAttribute($value) { return HelperService::buildSourceUrl($value); } } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiMineMenu.php ================================================ 'integer', 'gid' => 'integer', 'sort' => 'integer', 'use_vip' => 'integer', 'click_type' => 'integer', 'is_lock' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; public function setIconAttribute($value) { $this->attributes['icon'] = HelperService::buildSavePath($value); } public function getIconAttribute($value) { return HelperService::buildSourceUrl($value); } } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiMineMenuGroup.php ================================================ 'integer', 'sort' => 'integer', 'is_lock' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; public function menus(): HasMany { return $this->hasMany(AiMineMenu::class, 'gid', 'id'); } } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiOpenaiKey.php ================================================ 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiOrder.php ================================================ 'integer', 'uid' => 'integer', 'from_uid' => 'integer', 'market_uid' => 'integer', 'ord_type' => 'integer', 'pay_type' => 'integer', 'status' => 'integer', 'total_price' => 'integer', 'amount_price' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiOrderKami.php ================================================ 'integer', 'kid' => 'integer']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiOrderVip.php ================================================ 'integer', 'vip_level' => 'integer', 'created_at' => 'datetime']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiPayKami.php ================================================ 'integer', 'uid' => 'integer', 'price' => 'integer', 'status' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiQuickIssue.php ================================================ 'integer', 'sort' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiUser.php ================================================ 'integer', 'vip' => 'integer', 'is_lock' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; public function parentRelation(): HasOne { return $this->hasOne(AiUserRelation::class, 'uid', 'id'); } public function wallet(): HasOne { return $this->hasOne(AiUserWallet::class, 'uid', 'id'); } public function setHeadImgAttribute($value) { $this->attributes['head_img'] = HelperService::buildSavePath($value); } public function getHeadImgAttribute($value) { return HelperService::buildSourceUrl($value); } } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiUserRelation.php ================================================ 'integer', 'from_uid' => 'integer', 'market_uid' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiUserWallet.php ================================================ 'integer']; } ================================================ FILE: MineAdmin/php/app/Ai/Model/AiUserWalletLog.php ================================================ 'integer', 'uid' => 'integer', 'oid' => 'integer', 'direction' => 'integer', 'balance' => 'integer', 'scene' => 'integer', 'created_at' => 'datetime']; } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiChatMessageRequest.php ================================================ '主键', 'sid' => '会话ID', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiChatSessionRequest.php ================================================ 'required', //角色说明 验证 'prompt' => 'required', //排序 验证 'sort' => 'required', ]; } /** * 更新数据验证规则 * return array */ public function updateRules(): array { return [ //角色名称 验证 'act' => 'required', //角色说明 验证 'prompt' => 'required', //排序 验证 'sort' => 'required', ]; } /** * 字段映射名称 * return array */ public function attributes(): array { return [ 'id' => '主键', 'act' => '角色名称', 'prompt' => '角色说明', 'sort' => '排序', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiImageMaterialRequest.php ================================================ 'required', //图片地址 验证 'img_url' => 'required', //跳转地址 验证 'url' => 'required', //备注 验证 'remark' => 'required', //排序 验证 'sort' => 'required', //使用开始时间 验证 'start_at' => 'required', //使用结束时间 验证 'end_at' => 'required', ]; } /** * 更新数据验证规则 * return array */ public function updateRules(): array { return [ //使用场景 验证 'scene' => 'required', //图片地址 验证 'img_url' => 'required', //跳转地址 验证 'url' => 'required', //备注 验证 'remark' => 'required', //排序 验证 'sort' => 'required', //使用开始时间 验证 'start_at' => 'required', //使用结束时间 验证 'end_at' => 'required', ]; } /** * 字段映射名称 * return array */ public function attributes(): array { return [ 'id' => '主键', 'scene' => '使用场景', 'img_url' => '图片地址', 'url' => '跳转地址', 'remark' => '备注', 'sort' => '排序', 'created_by' => '创建者', 'updated_by' => '更新者', 'start_at' => '使用开始时间', 'end_at' => '使用结束时间', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiMineMenuGroupRequest.php ================================================ '主键', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiMineMenuRequest.php ================================================ '主键', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiOpenaiKeyRequest.php ================================================ '主键', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiOrderRequest.php ================================================ '主键', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiPayKamiRequest.php ================================================ '主键', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiQuickIssueRequest.php ================================================ 'required', //问题描述 验证 'content' => 'required', // 验证 'sort' => 'required', ]; } /** * 更新数据验证规则 * return array */ public function updateRules(): array { return [ //问题标题 验证 'title' => 'required', //问题描述 验证 'content' => 'required', // 验证 'sort' => 'required', ]; } /** * 字段映射名称 * return array */ public function attributes(): array { return [ 'id' => '主键', 'title' => '问题标题', 'content' => '问题描述', 'sort' => '', ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Request/AiUserRequest.php ================================================ 'required', //头像 验证 'head_img' => 'required', //手机号 验证 'mobile' => 'required', //vip等级 验证 'vip' => 'required', ]; } /** * 字段映射名称 * return array */ public function attributes(): array { return [ 'id' => '主键', 'nick_name' => '昵称', 'head_img' => '头像', 'mobile' => '手机号', 'vip' => 'vip等级' ]; } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiChatMessageService.php ================================================ mapper = $mapper; } public function messages(array $param): array { $slide = $param['slide'] ?? 'prev'; $last_id = $param['last_id'] ?? 0; $model = $this->mapper->getModel(); $model = $model->where('sid', '=', $param['sid']); if ($slide === 'prev') { // 上一页 if ($last_id) { $model = $model->where('id', '<', $last_id); } } else { if ($last_id) { $model = $model->where('id', '>', $last_id); } } return array_values($model->orderBy('id', 'desc')->limit(10)->get()->sortBy('id', SORT_ASC)->toArray()); } public function shareMessageList(array $param): array { $model = $this->mapper->getModel(); $model = $model->where('sid', '=', $param['sid']); $last_id = $param['last_id'] ?? 0; if ($last_id) { $model = $model->where('id', '>', $last_id); } return $model->orderBy('id')->limit(10)->get()->toArray(); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiChatSessionService.php ================================================ mapper = $mapper; $this->loginService = $loginService; } /** * 获取会话 * @param array $param * @return array */ public function session(array $param): array { $where = []; $where['uid'] = $this->loginService->getId(); $where['close'] = 1; if ($prompt_id = ($param['prompt_id'] ?? 0)) { $where['prompt_id'] = $prompt_id; } /** * @var $res AiChatSession */ $res = $this->mapper->session($where); $isNew = false; if (empty($res)) { // 没有回话则创建会话 $res = new \stdClass(); $res->prompt_id = $prompt_id ?: 1; $res->id = $this->mapper->save([ 'uid' => $this->loginService->getId(), 'prompt_id' => $res->prompt_id, 'created_at' => date("Y-m-d H:i:s") ]); $isNew = true; } return [ 'sid' => $res->id, 'prompt_id' => $res->prompt_id, 'is_new' => $isNew ]; } public function sessionClose(array $param): int { if (empty($param['sid']) || empty($param['prompt_id'])) { throw new NormalStatusException('参数错误', ResponseCodeConst::PARAM_FAILED); } $this->mapper->updateByCondition(['id' => $param['sid'], 'uid' => $this->loginService->getId()], [ 'close' => 2 ]); return $this->mapper->save([ 'uid' => $this->loginService->getId(), 'prompt_id' => $param['prompt_id'], 'created_at' => date("Y-m-d H:i:s") ]); } public function sessionHistory(array $param): array { $where = []; $where['uid'] = $this->loginService->getId(); $model = $this->mapper->getModel(); $id = $param['last_id'] ?? 0; if ($id) { $model = $model->where('id', '<', $id); } return $model->with('firstMessage')->where($where)->orderBy('id', 'desc')->limit(15)->get()->toArray(); } public function sessionShareList(array $param): array { $where = []; $where['share'] = 2; $model = $this->mapper->getModel(); $id = $param['last_id'] ?? 0; if ($id) { $model = $model->where('id', '<', $id); } return $model->with('firstMessage')->with(['user' => function ($query) { $query->select(['id', 'nick_name', 'head_img']); }]) ->where($where) ->orderBy('id', 'desc') ->limit(10) ->get() ->toArray(); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiChatgptPromptsService.php ================================================ 'gpt-3.5-turbo-0613', 'value' => 0, 'is_vip' => false, // 每次对话上下文最多包含1500单词左右 'max_tokens' => 2048, 'max_tokens_text' => '每次对话上下文最多包含1500单词左右', // 温度设置(较高的温度会导致更多种类和不可预测的输出) 'temperature' => 0.5, // 在生成句子的时候加入惩罚项来限制重复单词 'frequency_penalty' => 0, // 减少总体上使用频率较高的单词/短语的概率 'presence_penalty' => 0, // 上下文长度 'context_length' => 3, ], [ 'text' => 'gpt-3.5-turbo-16k', 'value' => 1, 'is_vip' => true, 'temperature' => 1.0, 'max_tokens' => -1, 'max_tokens_text' => '每次对话上下文最多包含12000单词左右', 'frequency_penalty' => 0, 'presence_penalty' => 0, 'context_length' => 3, ], ]; public function __construct(AiChatgptPromptsMapper $mapper) { $this->mapper = $mapper; } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiImageMaterialService.php ================================================ mapper = $mapper; } public function mine(int $scene): array { $time = time(); return $this->mapper->getModel() ->where('scene', '=', $scene) ->orderByDesc('sort') ->orderByDesc('id') ->select(['id','img_url','start_at','end_at','url']) ->get() ->filter(function ($item) use ($time){ return strtotime($item->start_at) < $time && strtotime($item->end_at) > $time; }) ->toArray(); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiLoginService.php ================================================ jwt */ $this->jwt = make(JWT::class)->setScene('ai'); } /** * 获取Token * @param array $claims * @return string * @throws InvalidArgumentException */ public function getToken(array $claims): string { return $this->jwt->getToken($claims); } /** * 验证token * @param string|null $token * @param string $scene * @return bool */ public function check(?string $token = null, string $scene = 'default'): bool { try { if ($this->jwt->checkToken($token, $scene, true, true, true)) { return true; } } catch (InvalidArgumentException $e) { // throw new TokenException(t('jwt.no_token')); return false; } catch (\Throwable $e) { // throw new TokenException(t('jwt.no_login')); return false; } return false; } /** * 获取JWT对象 * @return Jwt */ public function getJwt(): Jwt { return $this->jwt; } /** * 获取当前登录用户信息 * @param string|null $token * @return array */ public function getInfo(?string $token = null): array { $info = Context::get(self::class.'_info'); if (empty($info)){ $info = $this->jwt->getParserData($token); Context::set(self::class.'_info', $info); } return $info; } /** * 获取当前登录用户ID * @return int */ public function getId(): int { return $this->getInfo()['id']; } /** * 刷新token * @return string */ public function refresh(): string { return $this->jwt->refreshToken(); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiMineMenuGroupService.php ================================================ mapper = $mapper; } public function mineMenus(): array { return $this->mapper->getModel()::with(['menus' => function ($query) { $query->orderByDesc('sort')->orderByDesc('id'); }]) ->orderByDesc('sort') ->orderByDesc('id') ->get() ->toArray(); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiMineMenuService.php ================================================ mapper = $mapper; } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiOpenaiKeyService.php ================================================ mapper = $mapper; $this->redis = $redis; } public function batchAdd(array $data): bool { $keys = explode("\n", $data['content']); if (empty($keys)) { return false; } $saveAll = []; $time = date('Y-m-d H:i:s'); foreach ($keys as $key=>$v) { $saveAll[$key]['openai_key'] = trim($v); $saveAll[$key]['remark'] = $data['remark']; $saveAll[$key]['created_at'] = $time; $saveAll[$key]['updated_at'] = $time; } $model = new AiOpenaiKey(); $model->insert($saveAll); return true; } public function cacheAll(): bool { $this->redis->del('ai_openai_key_cache'); $all = $this->mapper->get(); if (empty($all)) return false; for ($i = 0; $i < 10; $i++) { foreach ($all as $item) { $this->redis->rPush('ai_openai_key_cache', $item['openai_key']); } } return true; } public function openAiKey() { $key = $this->redis->lPop('ai_openai_key_cache'); $this->redis->rPush('ai_openai_key_cache', $key); return $key; } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiOrderService.php ================================================ mapper = $mapper; $this->vipService = $vipService; $this->loginService = $loginService; } public function orderList(array $param): array { $create_time1 = $create_time2 = ''; if (!empty($param['create_time'])) { list($create_time1, $create_time2) = explode(',', $param['create_time']); } $last_id = $param['last_id'] ?? 0; $keywords = $param['keyword'] ?? ''; $model = new AiOrder(); $model = $model->where([ 'uid' => $this->loginService->getId(), ])->whereIn('ord_type', $param['ord_type']); if ($create_time1 && $create_time2) { $model = $model->where('created_at', '>', $create_time1 . ' 00:00:00') ->where('created_at', '<', $create_time2 . ' 00:00:00'); } if ($last_id) { $model = $model->where('id', '<', $last_id); } if ($keywords) { $model = $model->where('content', 'like', "%$keywords%"); } $ord_type = OrderConst::getGroupValueDescArr('ord_type'); $status = OrderConst::getGroupValueDescArr('status'); $pay_type = OrderConst::getGroupValueDescArr('pay_type'); return $model->limit(15) ->select(explode(',', 'id,uid,ord_sn,ord_type,pay_type,status,amount_price,content,created_at,pay_at')) ->orderBy('id', 'desc')->get() ->each(function ($item) use ($ord_type, $status, $pay_type) { $item->status_text = $status[$item->status]; $item->ord_type_text = $ord_type[$item->ord_type]; $item->pay_type_text = $pay_type[$item->pay_type]; $item->amount_price_text = HelperService::decode100($item->amount_price); }) ->toArray(); } public function kamiOpenVip(array $param) { /** * @var AiPayKami $kami */ $kami = AiPayKami::where(['code' => $param['kami_code']])->first(); if (empty($kami)) { throw new NormalStatusException('卡密不存在', ResponseCodeConst::PARAM_FAILED); } if (2 === $kami->status) { throw new NormalStatusException('卡密已失效', ResponseCodeConst::PARAM_FAILED); } if ($kami->uid && $kami->uid != $this->loginService->getId()) { throw new NormalStatusException('您无权使用该卡密', ResponseCodeConst::PARAM_FAILED); } /** * @var AiUser $user */ [$vipConfigAll, $user] = $this->vipService->config($this->loginService->getId()); unset($user->vip_name); $vipConfigAll = array_column($vipConfigAll, null, 'level'); if (empty($vipConfigAll[$param['level']])) { throw new NormalStatusException('VIP信息有误', ResponseCodeConst::PARAM_FAILED); } $vipConfig = $vipConfigAll[$param['level']]; if ($kami->price && $kami->price < $vipConfig['price_pay']) { throw new NormalStatusException('卡密金额不足', ResponseCodeConst::PARAM_FAILED); } if ($user->vip > $vipConfig['level']) { throw new NormalStatusException('VIP信息有误1', ResponseCodeConst::PARAM_FAILED); } Db::beginTransaction(); try { $price = HelperService::encode100($vipConfig['price_pay']); $aiOrder = AiOrder::create([ 'uid' => $user->id, 'from_uid' => $user->parentRelation->from_uid, 'market_uid' => $user->parentRelation->market_uid, 'ord_sn' => HelperService::createOrderCode($user->parentRelation->from_uid), 'ord_type' => OrderConst::OPEN_VIP, 'pay_type' => $price ? OrderConst::KAMI_PAY : OrderConst::KAMI_FREE, 'status' => OrderConst::SUCCESS_PAY, 'total_price' => $price, 'amount_price' => $price, 'content' => '购买<<' . $vipConfig['name'] . '>>', 'remark' => '卡密支付', ]); $oid = $aiOrder->id; AiOrderVip::create([ 'oid' => $oid, 'vip_level' => $vipConfig['level'], ]); AiOrderKami::create([ 'oid' => $oid, 'kid' => $kami->id, ]); if ($price && !empty($user->parentRelation->from_uid)) { /** * @var AiUser $parentInfo */ $parentInfo = AiUser::first(['id' => $user->parentRelation->from_uid]); $pVipConfig = $vipConfigAll[$parentInfo->vip] ?? []; if (!empty($pVipConfig) && ($parentInfo->vip ?? 0) > VipConst::VIP && $parentInfo->vip >= $vipConfig['level'] && $pVipConfig['income'] > 0) { $income = HelperService::encode100($vipConfig['price_pay'] * ($pVipConfig['income'] / 100)); AiUserWallet::where(['uid' => $user->parentRelation->from_uid])->update([ 'balance' => Db::raw('`balance`+' . $income), 'balance_total' => Db::raw('`balance_total`+' . $income), ]); AiUserWalletLog::create([ 'uid' => $user->parentRelation->from_uid, 'oid' => $oid, 'direction' => WalletConst::IN, 'scene' => WalletConst::PROMOTION, 'balance' => $income, 'remark' => 'user-'.$user->id.':购买<<' . $vipConfig['name'] . '>>获得' . $vipConfig['income'] . '%', 'created_at' => date('Y-m-d H:i:s'), ]); } } $user->vip = $vipConfig['level']; $vip_ent_at = $user->vip_ent_at ? strtotime($user->vip_ent_at) : 0; $time = time(); if ($vip_ent_at && $vip_ent_at > $time) { $vip_ent_at += $vipConfig['length'] * 2592000; } else { $vip_ent_at = $time + $vipConfig['length'] * 2592000; } $user->vip_ent_at = date('Y-m-d H:i:s', $vip_ent_at); $user->save(); Db::commit(); }catch (\Throwable $exception){ Db::rollBack(); throw new NormalStatusException($exception->getMessage(), ResponseCodeConst::PARAM_FAILED); } } public function adminOpenVip(array $data) { /** * @var AiUser $user */ $user = $this->mapper->first(['id' => $data['id']]); if (empty($user)) { throw new NormalStatusException('用户不存在', ResponseCodeConst::PARAM_FAILED); } if ($user->vip > (int)$data['vip']) { throw new NormalStatusException('不能开通比用户原等级小的VIP', ResponseCodeConst::PARAM_FAILED); } $vipConfigAll = array_column(VipConst::config(), null, 'level'); $vipConfig = $vipConfigAll[$data['vip']] ?? []; if (empty($vipConfig)) { throw new NormalStatusException('vip信息有误', ResponseCodeConst::PARAM_FAILED); } Db::beginTransaction(); try { $price = $data['price'] ? HelperService::encode100($data['price']) : 0; $aiOrder = AiOrder::create([ 'uid' => $user->id, 'from_uid' => $user->parentRelation->from_uid, 'market_uid' => $user->parentRelation->market_uid, 'ord_sn' => HelperService::createOrderCode($user->parentRelation->from_uid), 'ord_type' => OrderConst::OPEN_VIP, 'pay_type' => $price ? OrderConst::ADMIN_PAY : OrderConst::ADMIN_FREE, 'status' => OrderConst::SUCCESS_PAY, 'total_price' => $price, 'amount_price' => $price, 'content' => '购买<<' . $vipConfig['name'] . '>>', 'remark' => $data['remark'], ]); $oid = $aiOrder->id; AiOrderVip::create([ 'oid' => $oid, 'vip_level' => $vipConfig['level'], ]); if ($price && !empty($user->parentRelation->from_uid)) { /** * @var AiUser $parentInfo */ $parentInfo = AiUser::first(['id' => $user->parentRelation->from_uid]); $pVipConfig = $vipConfigAll[$parentInfo->vip] ?? []; if (!empty($pVipConfig) && ($parentInfo->vip ?? 0) > VipConst::VIP && $parentInfo->vip >= $vipConfig['level'] && $pVipConfig['income'] > 0) { $income = HelperService::encode100($data['price'] * ($pVipConfig['income'] / 100)); AiUserWallet::where(['uid' => $user->parentRelation->from_uid])->update([ 'balance' => Db::raw('`balance`+' . $income), 'balance_total' => Db::raw('`balance_total`+' . $income), ]); AiUserWalletLog::create([ 'uid' => $user->parentRelation->from_uid, 'oid' => $oid, 'direction' => WalletConst::IN, 'scene' => WalletConst::PROMOTION, 'balance' => $income, 'remark' => 'user-'.$user->id.':购买<<' . $vipConfig['name'] . '>>获得' . $vipConfig['income'] . '%', 'created_at' => date('Y-m-d H:i:s'), ]); } } $user->vip = $vipConfig['level']; $vip_ent_at = $user->vip_ent_at ? strtotime($user->vip_ent_at) : 0; $time = time(); if ($vip_ent_at && $vip_ent_at > $time) { $vip_ent_at += $vipConfig['length'] * 2592000; } else { $vip_ent_at = $time + $vipConfig['length'] * 2592000; } $user->vip_ent_at = date('Y-m-d H:i:s', $vip_ent_at); $user->save(); Db::commit(); }catch (\Throwable $exception){ Db::rollBack(); throw new NormalStatusException($exception->getMessage(), ResponseCodeConst::PARAM_FAILED); } } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiPayKamiService.php ================================================ mapper = $mapper; } public function add(array $data){ $num = $data['number']; $num = $num ?? 100; if ($num <= 0 || $num >= 100) { throw new NormalStatusException('数量必须小于或等于100', ResponseCodeConst::PARAM_FAILED); } $time = date('Y-m-d H:i:s'); $code_arr = HelperService::randStrArr(32, (int)$num); $saveAll = []; $data['price'] = $data['price'] ? HelperService::encode100($data['price']) : 0; $data['uid'] = $data['uid'] ?: 0; $data['remark'] = $data['remark'] ?: ''; foreach ($code_arr as $key=>$code){ $saveAll[$key]['uid'] = $data['uid']; $saveAll[$key]['price'] = $data['price']; $saveAll[$key]['code'] = $code; $saveAll[$key]['status'] = 1; $saveAll[$key]['remark'] = $data['remark']; $saveAll[$key]['created_at'] = $time; $saveAll[$key]['updated_at'] = $time; } $model = new AiPayKami(); $model->insert($saveAll); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiQuickIssueService.php ================================================ mapper = $mapper; } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiSettingService.php ================================================ redis = $redis; } const CACHE_KEY = 'ai_setting_'; protected array $customer = [ 'head_img' => '', 'mobile' => '', 'user_name' => '', 'work_time' => '', 'wx_img_url' => '', 'wx_no' => '' ]; public function customer(): array { $data = $this->redis->get(self::CACHE_KEY . 'customer'); if ($data) { $data = \json_decode($data, true); } $data = array_merge($this->customer, $data ?: []); return $data; } public function setCustomer($data): array { $this->redis->set(self::CACHE_KEY . 'customer', \json_encode($data, JSON_UNESCAPED_UNICODE)); return array_merge($this->customer, $data); } public function appClose() { return $this->redis->get(self::CACHE_KEY . 'app_close_message') ?: '' ; } public function setAppClose(string $v) { // 关站消息,有则为关站 return $this->redis->set(self::CACHE_KEY . 'app_close_message', $v); } public function agreementUser() { return $this->redis->get(self::CACHE_KEY . 'agreement') ?: ''; } public function setAgreementUser($v) { return $this->redis->set(self::CACHE_KEY . 'agreement', $v); } public function openaiProxy() { return $this->redis->get(self::CACHE_KEY . 'openai_proxy') ?: ''; } public function setOpenaiProxy($v) { return $this->redis->set(self::CACHE_KEY . 'openai_proxy', $v); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiUserService.php ================================================ mapper = $mapper; $this->redis = $redis; $this->loginService = $loginService; $this->vipService = $vipService; } public function userList(array $params): array { $relation = new AiUserRelation(); $wallet = new AiUserWallet(); $user = $this->mapper->getModel(); $query = Db::table($user->getTable().' as a') ->leftJoin($relation->getTable().' as b', 'a.id', '=', 'b.uid') ->leftJoin($wallet->getTable().' as c', 'a.id', '=', 'c.uid') ->leftJoin($user->getTable().' as d', 'b.from_uid', '=', 'd.id') ->select(['a.*', 'b.from_uid', 'b.market_uid', 'c.balance', 'c.balance_total', 'd.nick_name as parent_nick_name']); // 昵称 if (isset($params['nick_name']) && $params['nick_name'] !== '') { $query->where('a.nick_name', 'like', '%'.$params['nick_name'].'%'); } // 手机号 if (isset($params['mobile']) && $params['mobile'] !== '') { $query->where('a.mobile', 'like', '%'.$params['mobile'].'%'); } // vip等级 if (isset($params['vip']) && $params['vip'] !== '') { $query->where('a.vip', '=', $params['vip']); } // vip到期时间 if (isset($params['vip_ent_at']) && is_array($params['vip_ent_at']) && count($params['vip_ent_at']) == 2) { $query->whereBetween( 'a.vip_ent_at', [ $params['vip_ent_at'][0], $params['vip_ent_at'][1] ] ); } // 是否锁定:1正常,2锁定 if (isset($params['is_lock']) && $params['is_lock'] !== '') { $query->where('a.is_lock', '=', $params['is_lock']); } // 创建时间 if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) { $query->whereBetween( 'a.created_at', [ $params['created_at'][0], $params['created_at'][1] ] ); } // 更新时间 if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) { $query->whereBetween( 'a.updated_at', [ $params['updated_at'][0], $params['updated_at'][1] ] ); } // 昵称 if (isset($params['from_uid']) && $params['from_uid'] !== '') { $query->where('b.from_uid', '=', $params['from_uid']); } if ($params['orderBy'] ?? false) { if (is_array($params['orderBy'])) { foreach ($params['orderBy'] as $key => $order) { $query->orderBy($order, 'a.'.$params['orderType'][$key] ?? 'asc'); } } else { $query->orderBy($params['orderBy'], 'a.'.$params['orderType'] ?? 'asc'); } } $query = $query->paginate((int)$params['pageSize'] ?? $this->mapper->getModel()::PAGE_SIZE, ['*'], 'page', (int)$params['page'] ?? 1); $res = $this->mapper->setPaginate($query); foreach ($res['items'] as $k=>$v) { $res['items'][$k]->head_img = HelperService::buildSourceUrl($v->head_img); $res['items'][$k]->balance = $v->balance ? HelperService::decode100($v->balance) : '0'; $res['items'][$k]->balance_total = $v->balance_total ? HelperService::decode100($v->balance_total) : '0'; } return $res; } public function update(mixed $id, array $data): bool { /** * @var AiUser $user */ $user = $this->mapper->first(['mobile' => $data['mobile']]); if (!empty($user) && $user->id != $id) { throw new NormalStatusException('手机号已有用户使用', ResponseCodeConst::PARAM_FAILED); } return $this->mapper->update($id, $data); } /** * 单个或批量软删除数据 * @param array $ids * @return bool */ public function delete(array $ids): bool { if (!empty($ids)){ $this->mapper->delete($ids); foreach ($ids as $id) { $this->redis->sAdd('ai_user_delete', $id); } $this->redis->expire('ai_user_delete', 86400); } return true; } public function isJustDelete(int $id): bool { return $this->redis->sIsMember('ai_user_delete', $id); } public function lock(int $id) { $user = $this->read($id); if ($user) { $user->is_lock = $user->is_lock === 1 ? 2 : 1; $user->save(); if ($user->is_lock === 2) { $this->redis->sAdd('ai_user_lock', $id); $this->redis->expire('ai_user_lock', 86400); } else { $this->redis->sRem('ai_user_lock', $id); } } } public function isJustLock(int $id): bool { return $this->redis->sIsMember('ai_user_lock', $id); } public function friendNum(int $uid): int { return AiUserRelation::where(['from_uid' => $uid])->count('uid'); } public function friendList(array $param){ $register_time1 = $register_time2 = ''; if (!empty($param['register_time'])) { list($register_time1, $register_time2) = explode(',', $param['register_time']); } $keywords = $param['keyword'] ?? ''; $last_id = $param['last_id'] ?? 0; $uid = $this->loginService->getId(); $model = new AiUserRelation(); $userModel = new AiUser(); if (($param['cate_id'] ?? '1') === '2') { $model = $model->where('market_uid', '=', $uid)->where('from_uid','<>',$uid); }else{ $model = $model->where('from_uid','=',$uid); } $model = $model->leftJoin($userModel->getTable().' as u', 'uid', '=', 'u.id'); if ($last_id) { $model = $model->where('u.id', '<', $last_id); } $vip = $param['vip'] ?? -1; if ($vip > -1) { $model = $model->where('u.vip', '=', $vip); } if ($register_time1 && $register_time2) { $model = $model->where('u.created_at', '>', $register_time1.' 00:00:00')->where('u.created_at', '<', $register_time2.' 00:00:00'); } if ($keywords) { $model = $model->where('u.nick_name', 'like', "%$keywords%"); } return $model->orderBy('u.id', 'desc') ->where('u.is_lock','=', 1) ->select(['u.id','u.nick_name','u.head_img','u.mobile','u.vip','u.created_at']) ->limit(10) ->get() ->each(function ($user) { $user->head_img = HelperService::buildSourceUrl($user->head_img); $user->vip_name = VipConst::getDesc($user->vip); }) ->toArray(); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiVipService.php ================================================ $uid, 'ord_type' => 1, 'status' => 2 ]) ->whereIn('pay_type', [OrderConst::WX_PAY, OrderConst::ADMIN_PAY, OrderConst::KAMI_PAY]) ->select(['amount_price']) ->orderBy('id', 'desc') ->first(); /** * @var AiUser $user */ $user = AiUser::where(['id'=>$uid])->select(['id', 'vip', 'vip_ent_at'])->first(); $user->vip_name = VipConst::getDesc($user->vip); $config = VipConst::config(); foreach ($config as $k=>&$item) { if ($user->vip <= $item['level']) { // 当前等级小于等于配置等级低 $item['is_choose'] = true; } if ($user->vip === $item['level']) { // 当前用户等级等于配置等级时为续费 $item['btn_text'] = '立即续费'; } if ($user->vip >0 && $user->vip < $item['level']) { if (!empty($order)) { // 升级补差价 抵扣最近订单金额 $item['price_pay'] = HelperService::decode100($order->amount_price) > $item['price'] ? '¥1' : $item['price']; } $item['btn_text'] = '立即升级'; } if ($user->vip === 0 && $k === 1){ $item['is_default'] = true; } if ($user->vip === 10 && $k === 1){ $item['is_default'] = true; } if ($user->vip === 20 && $k === 2){ $item['is_default'] = true; } if ($user->vip === 30 && $k === 2) { $item['is_default'] = true; } } return [$config, $user]; } } ================================================ FILE: MineAdmin/php/app/Ai/Service/AiWalletService.php ================================================ loginService = $loginService; } public function info(): array { $res = AiUserWallet::where(['uid' => $this->loginService->getId()])->first(); return [ 'balance' => isset($res->balance) ? HelperService::decode100($res->balance) : 0, 'balance_total' => isset($res->balance_total) ? HelperService::decode100($res->balance_total) : 0, ]; } public function withdrawal(array $param): void { /** * @var AiUserWallet $userWallet */ $userWallet = AiUserWallet::where(['uid' => $this->loginService->getId()])->first(); if (empty($userWallet)) { throw new NormalStatusException('账户信息错误', ResponseCodeConst::PARAM_FAILED); } $amount_price = HelperService::encode100((int)$param['amount']); if ($userWallet->balance < $amount_price){ throw new NormalStatusException('账户余额不足', ResponseCodeConst::PARAM_FAILED); } /** * @var AiUser $user */ $user = AiUser::where(['id'=>$userWallet->uid])->first(); Db::beginTransaction(); try { AiOrder::create([ 'uid' => $userWallet->uid, 'from_uid' => $user->parentRelation->from_uid, 'market_uid' => $user->parentRelation->market_uid, 'ord_sn' => HelperService::createOrderCode($user->parentRelation->from_uid), 'ord_type' => OrderConst::WITHDRAWAL, 'pay_type' => OrderConst::WX_PAY, 'status' => OrderConst::WAIT_PAY, 'total_price' => $amount_price, 'amount_price' => $amount_price, 'content' => '提现<<' . HelperService::decode100($amount_price) . '元>>', 'remark' => '', ]); AiUserWallet::where(['uid' => $userWallet->uid])->update([ 'balance' => Db::raw('`balance`-' . $amount_price), ]); Db::commit(); }catch (\Throwable $exception){ Db::rollBack(); throw new NormalStatusException($exception->getMessage(), ResponseCodeConst::PARAM_FAILED); } } public function changeLogList(array $param): array { $create_time1 = $create_time2 = ''; if (!empty($param['create_time'])) { list($create_time1, $create_time2) = explode(',', $param['create_time']); } $keywords = $param['keyword'] ?? ''; $direction = (int)$param['direction'] ?? 0; $last_id = $param['last_id'] ?? 0; $model = new AiUserWalletLog(); $model = $model->where([ 'uid' => $this->loginService->getId() ]); if ($direction){ $model = $model->where('direction', '=', $direction === WalletConst::IN ? WalletConst::IN : WalletConst::OUT); } if ($create_time1 && $create_time2) { $model = $model->where('created_at', '>', $create_time1.' 00:00:00') ->where('created_at', '<', $create_time2.' 00:00:00'); } if ($keywords) { $model = $model->where('remark', 'like', "%$keywords%"); } if ($last_id) { $model = $model->where('id', '<', $last_id); } $scene = WalletConst::getGroupValueDescArr('scene'); return $model->limit(15)->orderBy('id', 'desc')->get() ->each(function ($item) use ($scene){ $item->scene_text = $scene[$item->scene]; $item->balance_text = HelperService::decode100($item->balance); }) ->toArray(); } } ================================================ FILE: MineAdmin/php/app/Ai/Service/HelperService.php ================================================ 0) { // 用户唯一 $filename = 'upload/' . $scene . '/' . md5(config('app_name') . '-' . $scene . '-' . $uid); } else { $filename = 'upload/' . $scene . '/' . date('Ymd') . uniqid() . mt_rand(100000000, 999999999); } return $filename . $suffix; } public static function buildSourceUrl(?string $filename): string { if (empty($filename)) { return ''; } if (strpos($filename, 'http') === 0 || strpos($filename, '//') === 0) { return $filename; } // upload/1000/a.png $type = (int)substr($filename, 7, 1) - 1; $config = config('file.storage.qiniu'); $domain = [$config['image_domain'], $config['audio_domain'], $config['video_domain']]; $schema = self::isMini() ? 'https' : 'http'; switch ($type) { case 2: return $schema . '://' . $domain[$type - 1] . '/' . config('app_env') . '/' . $filename; case 1: default: return $schema . '://' . ($domain[$type - 1] ?? $domain[0]) . '/' . config('app_env') . '/' . $filename; } } /** * 是否小程序请求 * @return bool */ public static function isMini(): bool { return isset(Context::get(AuthMiddleware::class . '_login_data')['mini_openid']); } public static function decode100(string|int $price, string $percent = '0.01') : string { return bcmul((string)$price, $percent, 2); } public static function encode100(string|int|float $price, string $hundred = '100') : string { return bcmul((string)$price, $hundred, 0); } public static function createOrderCode(string|int $uid=0) : string { mt_srand(); return date("YmdHis") . $uid . rand(100000, 999999); } public static function randStrArr(int $len = 6, int $number = 1): array { mt_srand(); $res = []; $str = 'a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9'; $arr = explode(' ', $str); do { $rand_keys = array_rand($arr, $len); shuffle($rand_keys); $code = ''; foreach ($rand_keys as $index => $key) { $code .= $arr[$key]; } array_push($res, $code); --$number; } while ($number); return $res; } } ================================================ FILE: MineAdmin/php/app/Ai/Service/QiniuService.php ================================================ $scene) { if (!UploadSceneConst::hasScene($scene)) { throw new NormalStatusException('场景不存在~~', ResponseCodeConst::PARAM_FAILED); } $i = substr($scene, 0, 1) - 1; $list[$key]['path'] = $env . '/' . HelperService::makeFileName($scene, $suffix[$i] ?? $suffix[0], UploadSceneConst::isOnly($scene) ? $uid : 0); $list[$key]['domain'] = $schema . '://' . ($domain[$i] ?? $domain[0]); $list[$key]['scene'] = $scene; /** * https://developer.qiniu.com/kodo/1671/region-endpoint-fq */ $list[$key]['service_url'] = $config['upload_domain']; $returnBody = '{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)","url":"' . $list[$key]['domain'] . '/$(key)' . (UploadSceneConst::isOnly($scene) ? '?v=' . $time : '') . '"}'; $list[$key]['token'] = $auth->uploadToken($bucket[$i] ?? $bucket[0], $list[$key]['path'], 900, ['returnBody' => $returnBody]); } } return $list; } } ================================================ FILE: MineAdmin/php/app/Ai/config.json ================================================ { "name": "Ai", "label": "ai", "description": "ai", "installed": true, "enabled": true, "version": "1.0.0", "order": 0 } ================================================ FILE: MineAdmin/vue/src/api/ai/aiChatMessage.js ================================================ import { request } from '@/utils/request.js' /** * 聊天数据 API JS */ export default { /** * 获取聊天数据分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/chatMessage/index', method: 'get', params }) }, /** * 读取聊天数据 * @returns */ read (data = {}) { return request({ url: 'ai/chatMessage/read', method: 'get', data }) }, /** * 将聊天数据删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/chatMessage/delete', method: 'delete', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiChatSession.js ================================================ import { request } from '@/utils/request.js' /** * 问答会话 API JS */ export default { /** * 获取问答会话分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/chatSession/index', method: 'get', params }) }, /** * 读取问答会话 * @returns */ read (data = {}) { return request({ url: 'ai/chatSession/read', method: 'get', data }) }, /** * 将问答会话删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/chatSession/delete', method: 'delete', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiChatgptPrompts.js ================================================ import { request } from '@/utils/request.js' /** * chatgpt角色 API JS */ export default { /** * 获取chatgpt角色分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/chatgptPrompts/index', method: 'get', params }) }, /** * 更新chatgpt角色数据 * @returns */ update (id, data = {}) { return request({ url: 'ai/chatgptPrompts/update/' + id, method: 'put', data }) }, /** * 添加chatgpt角色 * @returns */ save (data = {}) { return request({ url: 'ai/chatgptPrompts/save', method: 'post', data }) }, /** * 读取chatgpt角色 * @returns */ read (data = {}) { return request({ url: 'ai/chatgptPrompts/read', method: 'get', data }) }, /** * 将chatgpt角色删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/chatgptPrompts/delete', method: 'delete', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiImageMaterial.js ================================================ import { request } from '@/utils/request.js' /** * 图片素材 API JS */ export default { /** * 获取图片素材分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/imageMaterial/index', method: 'get', params }) }, /** * 添加图片素材 * @returns */ save (data = {}) { return request({ url: 'ai/imageMaterial/save', method: 'post', data }) }, /** * 更新图片素材数据 * @returns */ update (id, data = {}) { return request({ url: 'ai/imageMaterial/update/' + id, method: 'put', data }) }, /** * 读取图片素材 * @returns */ read (data = {}) { return request({ url: 'ai/imageMaterial/read', method: 'get', data }) }, /** * 将图片素材删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/imageMaterial/delete', method: 'delete', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiMineMenu.js ================================================ import { request } from '@/utils/request.js' /** * 个人中心菜单 API JS */ export default { /** * 获取个人中心菜单分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/mineMenu/index', method: 'get', params }) }, /** * 添加个人中心菜单 * @returns */ save (data = {}) { return request({ url: 'ai/mineMenu/save', method: 'post', data }) }, /** * 更新个人中心菜单数据 * @returns */ update (id, data = {}) { return request({ url: 'ai/mineMenu/update/' + id, method: 'put', data }) }, /** * 读取个人中心菜单 * @returns */ read (data = {}) { return request({ url: 'ai/mineMenu/read', method: 'get', data }) }, /** * 将个人中心菜单删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/mineMenu/delete', method: 'delete', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiMineMenuGroup.js ================================================ import { request } from '@/utils/request.js' /** * 个人中心菜单分组 API JS */ export default { /** * 获取个人中心菜单分组分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/mineMenuGroup/index', method: 'get', params }) }, /** * 添加个人中心菜单分组 * @returns */ save (data = {}) { return request({ url: 'ai/mineMenuGroup/save', method: 'post', data }) }, /** * 更新个人中心菜单分组数据 * @returns */ update (id, data = {}) { return request({ url: 'ai/mineMenuGroup/update/' + id, method: 'put', data }) }, /** * 读取个人中心菜单分组 * @returns */ read (data = {}) { return request({ url: 'ai/mineMenuGroup/read', method: 'get', data }) }, /** * 将个人中心菜单分组删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/mineMenuGroup/delete', method: 'delete', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiOpenaiKey.js ================================================ import { request } from '@/utils/request.js' /** * openai_key API JS */ export default { /** * 获取openai_key分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/openKey/index', method: 'get', params }) }, /** * 添加openai_key * @returns */ save (data = {}) { return request({ url: 'ai/openKey/save', method: 'post', data }) }, /** * 更新openai_key数据 * @returns */ update (id, data = {}) { return request({ url: 'ai/openKey/update/' + id, method: 'put', data }) }, /** * 读取openai_key * @returns */ read (data = {}) { return request({ url: 'ai/openKey/read', method: 'get', data }) }, /** * 将openai_key删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/openKey/delete', method: 'delete', data }) }, batchAdd (data) { return request({ url: 'ai/openKey/batch-add', method: 'post', data }) }, refreshCache () { return request({ url: 'ai/openKey/refresh-cache-list', method: 'post' }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiOrder.js ================================================ import { request } from '@/utils/request.js' /** * 订单表 API JS */ export default { /** * 获取订单表分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/order/index', method: 'get', params }) }, /** * 读取订单表 * @returns */ read (data = {}) { return request({ url: 'ai/order/read', method: 'get', data }) }, /** * 将订单表删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/order/delete', method: 'delete', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiPayKami.js ================================================ import { request } from '@/utils/request.js' /** * 卡密 API JS */ export default { /** * 获取卡密分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/payKami/index', method: 'get', params }) }, /** * 读取卡密 * @returns */ read (data = {}) { return request({ url: 'ai/payKami/read', method: 'get', data }) }, /** * 更新卡密数据 * @returns */ update (id, data = {}) { return request({ url: 'ai/payKami/update/' + id, method: 'put', data }) }, /** * 将卡密删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/payKami/delete', method: 'delete', data }) }, /** * 将卡密删除,有软删除则移动到回收站 * @returns */ add (data) { return request({ url: 'ai/payKami/add', method: 'post', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiQuickIssue.js ================================================ import { request } from '@/utils/request.js' /** * 快捷问题 API JS */ export default { /** * 获取快捷问题 分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/quickIssue/index', method: 'get', params }) }, /** * 更新快捷问题 数据 * @returns */ update (id, data = {}) { return request({ url: 'ai/quickIssue/update/' + id, method: 'put', data }) }, /** * 添加快捷问题 * @returns */ save (data = {}) { return request({ url: 'ai/quickIssue/save', method: 'post', data }) }, /** * 读取快捷问题 * @returns */ read (data = {}) { return request({ url: 'ai/quickIssue/read', method: 'get', data }) }, /** * 将快捷问题 删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/quickIssue/delete', method: 'delete', data }) }, } ================================================ FILE: MineAdmin/vue/src/api/ai/aiSetting.js ================================================ import { request } from '@/utils/request.js' /** * 用户主表 API JS */ export default { /** * 获取用户主表分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/setting/index', method: 'get', params }) }, /** * 更新用户主表数据 * @returns */ save (data = {}) { return request({ url: 'ai/setting/save', method: 'post', data }) }, getQiniuToken(scenes= "") { let params = {scenes: scenes} return request({ url: 'ai/setting/upload-token', method: 'get', params }) } } ================================================ FILE: MineAdmin/vue/src/api/ai/aiUser.js ================================================ import { request } from '@/utils/request.js' /** * 用户主表 API JS */ export default { /** * 获取用户主表分页列表 * @returns */ getList (params = {}) { return request({ url: 'ai/user/index', method: 'get', params }) }, /** * 更新用户主表数据 * @returns */ update (id, data = {}) { return request({ url: 'ai/user/update/' + id, method: 'put', data }) }, /** * 读取用户主表 * @returns */ read (data = {}) { return request({ url: 'ai/user/read', method: 'get', data }) }, /** * 将用户主表删除,有软删除则移动到回收站 * @returns */ deletes (data) { return request({ url: 'ai/user/delete', method: 'delete', data }) }, /** * 将用户主表删除,有软删除则移动到回收站 * @returns */ openVip(id, data) { return request({ url: 'ai/user/open-vip/' + id, method: 'post', data }) }, /** * 锁定 or 解锁 * @returns */ lock(id) { return request({ url: 'ai/user/lock/' + id, method: 'post' }) }, } ================================================ FILE: MineAdmin/vue/src/components/putyy/pt-upload.vue ================================================ ================================================ FILE: MineAdmin/vue/src/config/pt-const.js ================================================ export default { is_lock: [ { label: "正常", value: 1 }, { label: "锁定", value: 2 }, ], ai_vip: [ { label: "免费会员", value: 0 }, { label: "VIP", value: 10 }, { label: "1星VIP", value: 20 }, { label: "2星VIP", value: 30 }, ], }; ================================================ FILE: MineAdmin/vue/src/config/pt-scene.js ================================================ export default { ai_head_img: '1010', ai_mine_menu_icon: '1011', ai_customer_wx_img: '1012', ai_customer_head_img: '1013', ai_image_materialg: '1014', }; ================================================ FILE: MineAdmin/vue/src/utils/pt-upload.js ================================================ import ai from "@/api/ai/aiSetting"; import * as qiniu from "qiniu-js"; let uploadQiniu = (blob,key,token)=>{ return new Promise(function (resolve, reject) { let observable = qiniu.upload(blob, key, token) let observer = { next (res) { }, error (err) { reject(err) }, complete (data) { resolve(data) } } observable.subscribe(observer) }) } export const uploadQiniuHandler = async (files) => { let sceneArr = [] for (let dataIndex in files) { sceneArr.push(files[dataIndex].ptScene) } if (sceneArr.length <= 0) { return true } // 获取token let sceneResArr = [] await ai.getQiniuToken(sceneArr.join("-")).then((res)=>{ sceneResArr = res.data }) for (let key in sceneResArr) { for (let dataIndex in files) { if (sceneResArr[key].scene === files[dataIndex].ptScene) { await uploadQiniu(files[dataIndex].file, sceneResArr[key].path,sceneResArr[key].token).then((res) => { files[dataIndex].source_url = res.url }) } } } return files } ================================================ FILE: MineAdmin/vue/src/views/ai/chatMessage/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/chatSession/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/chatgptPrompts/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/imageMaterial/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/mineMenu/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/mineMenuGroup/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/openKey/components/add.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/openKey/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/order/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/payKami/components/add.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/payKami/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/quickIssue/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/setting/index.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/user/components/openVip.vue ================================================ ================================================ FILE: MineAdmin/vue/src/views/ai/user/index.vue ================================================ ================================================ FILE: README.md ================================================ # uniapp、hyperf MineAdmin 实现的 chatgpt应用,支持小程序、H5、App! ## 效果图 #### App ![](images/1.jpg) ![](images/2.jpg) ![](images/3.jpg) #### 后台系统 . ![admin.png](images/admin.png) ## 技术栈 具体依赖看项目代码吧! ### 前端 > uniapp vue3 pug scss 等 ### 后端 > swoole hyperf MineAdmin 等 ## 功能说明 > 客户端: 问答上下文、快捷提问、角色自定义、历史会话、公开频道、模型设置、VIP系统、邀请好友、分佣系统、联系客服、钱包系统、提现、订单、好友管理等 > 后台:chatgpt角色自定义、快捷提问管理、聊天数据管理、个人中心菜单管理、订单管理、用户管理、卡密管理、设置(openai地址、客服信息、用户协议等设置)、图片素材(个人中心banner)、openai_key管理(自动轮训) ## 开始安装 > 下载本项目 ```shell git clone https://github.com/putyy/chatgpt.git ``` ## 安装 MineAdmin #### 1. 按照官方文档进行安装 [安装文档](https://doc.mineadmin.com/guide/install/) #### 2. 安装本项目mineadmin-php > 安装composer依赖包 >> composer require easyswoole/oss putyy/php-constants orhanerday/open-ai --ignore-platform-reqs >复制本项目 ./MineAdmin/php/app/ai 下所有子文件夹,粘贴到到mineadmin-php app/ai目录下 >> cp -r ./MineAdmin/php/app/ai/ ./you-mineadmin-php/app/ai > 执行以下命令添加本项目需要的数据表及初始化数据 >> php bin/hyperf.php mine:migrate-run ai >> >> php bin/hyperf.php mine:seeder-run ai >> >> php bin/hyperf.php ai:init-menu > 生成Ai api需要的jwt key >>php bin/hyperf.php mine:jwt-gen --jwtSecret=JWT_AI_SECRET > 修改mineadmin后端 jwt 配置文件,位置: config/autoload/jwt.php,新增如下内容: > ```php > return [ > // ...... > 'scene' => [ > // 新增如下 > 'ai' => [ > 'secret' => env('JWT_AI_SECRET', ''), // 非对称加密使用字符串,请使用自己加密的字符串 > 'login_type' => 'sso', > 'sso_key' => 'id', > 'ttl' => 86400, > 'blacklist_cache_ttl' => 86400, > ], > // ...... > ] > // ...... > ] > ``` > 修改mineadmin后端 route 配置文件,位置: config/routes.php,新增如下内容: > ```php > Router::addServer('message', function () { > // ...... > // 新增如下内容 > Router::get('/ws-chat', 'App\Ai\Api\Websocket', [ > 'middleware' => [ ] > ]); > // ...... > }) > ``` > > > 修改mineadmin后端 file 配置文件,位置: config/autoload/file.php,新增如下内容: > ```php > return [ > 'storage'=>[ > 'qiniu'=>[ > // ...... > 'accessKey' => '七牛云accessKey', > 'secretKey' => '七牛云secretKey', > 'host' => env('QINIU_HOST', '你的七牛云访问主域名,例: baidu.com'), > // 具体查看 https://developer.qiniu.com/kodo/1671/region-endpoint-fq > 'upload_domain' => env('QINIU_UPLOAD_DOMAIN', 'https://up-cn-east-2.qiniup.com'), > 'image_bucket' => env('QINIU_IMAGE_BUCKET', '七牛云图片空间'), > 'image_domain' => env('QINIU_IMAGE_DOMAIN', '七牛云图片域名'), > // 以下未用到 不用配置 > 'audio_bucket' => env('QINIU_AUDIO_BUCKET'), > 'video_bucket' => env('QINIU_VIDEO_BUCKET'), > 'audio_domain' => env('QINIU_AUDIO_DOMAIN'), > 'video_domain' => env('QINIU_VIDEO_DOMAIN'), > // ...... > ] > ] > ] >``` > 4. 安装本项目mineadmin-vue >> 复制本项目 ./MineAdmin/vue/src 下所有子文件夹,粘贴到到mineadmin-vue src目录下 >>> cp -r ./MineAdmin/vue/src/ ./you-mineadmin-vue/src >> >> 安装 qiniu-js >>> yarn add qiniu-js --save >> >> 运行 >>> yarn run dev #### 打开后台系统 > 添加openai api key: Ai系统->openai_key->新增, 返回列表点击顶部刷新缓存 > > 设置站点相关信息: Ai系统->设置 ### 大功告成,其他功能自行探索! . ### 安装 uniapp #### 1. 按照uniapp官方文档安装好环境 #### 2. 用HBuilderX打开本项目UniApp文件夹 #### 3. 按照以下说明修改配置文件(相关文件都在UniApp文件夹内) > uniapp开发者中心 [点击获取appid](https://dev.dcloud.net.cn/pages/app/list) ```shell # 修改 ./manifest.json 文件中的appid { "name" : "应用名称", "appid" : "你的应用ID", "description" : "", ...... } # 复制 ./config.example.ts => ./common/config.ts 文件, 修改对应配置 let config = [ { wsUrl: 'ws://开发环境的域名ws/ws-chat', baseURL: 'https://开发环境的域名/api/ai/api/' }, { wsUrl: 'ws://线上域名/ws/ws-chat', baseURL: 'https://线上域名/api/ai/api/' } ] ``` . #### 4. 安装以下插件(点击打开,页面最右侧导入HBuilderX),已安装过则忽略 > [uni-ui](https://ext.dcloud.net.cn/plugin?id=55) > > [compile-typescript](https://ext.dcloud.net.cn/plugin?name=compile-typescript) > > [compile-node-sass](https://ext.dcloud.net.cn/plugin?name=compile-node-sass) > > [pug-language](https://ext.dcloud.net.cn/plugin?name=pug-language) > > [compile-pug-cli](https://ext.dcloud.net.cn/plugin?name=compile-pug-cli) #### 6. HBuilderX最顶部: 运行->运行到浏览器 #### 大功告成! ## 其他 ## uniapp打包app、h5、小程序参考uniapp官方文档,使用HBuilderX配置manifest.json 之后进行打包操作! . ## nginx配置文件可以参考如下 ```nginx # 前端 location / { index index.html index.htm; try_files $uri $uri/ /index.html; } # PHP后端代理,这里的 /prod/ 要跟前端 .env.production 的 VITE_APP_PROXY_PREFIX 值一致 location /api/ { if ($request_method = 'OPTIONS') { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, DELETE'; add_header Access-Control-Allow-Headers 'DNT,Keep-Alive,User-Agent,Cache-Control,Content-Type,Authorization,X-Token'; return 204; } # 将客户端的 Host 和 IP 信息一并转发到对应节点 proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 将协议架构转发到对应节点,如果使用非https请改为http proxy_set_header X-scheme https; # 执行代理访问真实服务器 proxy_pass http://127.0.0.1:9501/; } location /ws/ { # WebSocket Header proxy_http_version 1.1; proxy_set_header Upgrade websocket; proxy_set_header Connection "Upgrade"; # 将客户端的 Host 和 IP 信息一并转发到对应节点 proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; # 客户端与服务端无交互 60s 后自动断开连接,请根据实际业务场景设置 proxy_read_timeout 60s ; # 执行代理访问真实服务器 proxy_pass http://127.0.0.1:9502/; } # ^~ 不能去掉,/upload/ 中的 upload 可以改成其他名称 location ^~ /upload/ { # 将客户端的 Host 和 IP 信息一并转发到对应节点 proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 将协议架构转发到对应节点,如果使用非https请改为http proxy_set_header X-scheme https; # 执行代理访问真实服务器 proxy_pass http://127.0.0.1:9501/; } ``` #### MineAdmin-vue下的 > .env.development ```env VITE_APP_BASE_URL = http://you.domain.com/api VITE_APP_UPLOAD_URL = http://you.domain.com/upload VITE_APP_WS_URL = ws://you.domain.com/ws/message.io ``` > .env.production ```env VITE_APP_BASE_URL = / VITE_APP_UPLOAD_URL = http://you.domain.com/upload VITE_APP_WS_URL = ws://you.domain.com/ws/message.io ``` #### uniapp下的./common/config.ts文件如下 ``` let config = [ { wsUrl: 'ws://you.domain.com/ws/ws-chat', baseURL: 'https://you.domain.com/api/ai/api/' }, { wsUrl: 'ws://you.domain.com/ws/ws-chat', baseURL: 'https://you.domain.com/api/ai/api/' } ] ``` ## 免责声明 #### 使用本软件不得用于开发违反国家有关政策的相关软件和应用,若因使用本软件造成的一切法律责任均与本人无关! ================================================ FILE: UniApp/.gitignore ================================================ /.idea/ /.vscode/ /.hbuilderx/ /uni_modules/* !/uni_modules/bt-cropper_3.0.1/ !/uni_modules/mp-html/ /node_modules/ /unpackage/ /common/config.ts /.vite/ ================================================ FILE: UniApp/App.vue ================================================ ================================================ FILE: UniApp/README.md ================================================ # chatGPT UniApp ================================================ FILE: UniApp/androidPrivacy.json ================================================ { "prompt" : "template" } ================================================ FILE: UniApp/common/api.ts ================================================ import request from './utils/services' export function getLogin(data: ptAny) { return request.get('login/index', data) } export function getInit() { return request.get('public/init') } export function getMine() { return request.get('user/mine') } export function getFriends(data: ptAny) { return request.get('user/friends', data) } export function postUserDataInfo(data: ptAny) { return request.post('user/edit', data) } export function getQiniuToken(scenes: string) { return request.get('public/upload-token', {scenes: scenes}) } export function getAiRoles() { return request.get('chat/roles') } export function getAiModelList() { return request.get('chat/model-list') } export function getAiSession(data: ptAny) { return request.get('chat/session', data) } export function getAiSessionHistory(data: ptAny) { return request.get('chat/session-history', data) } export function postAiSessionClose(data: ptAny) { return request.post('chat/session-close', data) } export function postAiSessionShare(data: ptAny) { return request.post('chat/session-share', data) } export function postAiSessionDelete(data: ptAny) { return request.post('chat/session-delete', data) } export function getAiSessionShareList(data: ptAny) { return request.get('chat/session-share-list', data) } export function getAiSessionShareMessageList(data: ptAny) { return request.get('chat/session-share-message-list', data) } export function getAiMessages(data: ptAny) { return request.get('chat/messages', data) } export function getAiQuickIssue() { return request.get('chat/quick-issue') } export function getVipConfig() { return request.get('public/vip-config') } export function getOrderList(data: ptAny) { return request.get('order/list', data) } export function postKamiOpenVip(data: ptAny) { return request.post('order/kami-open-vip', data) } export function getWalletInfo() { return request.get('wallet/index') } export function getChangeLogList(data: ptAny) { return request.get('wallet/change-log-list', data) } export function getWithdrawalList(data: ptAny) { return request.get('wallet/withdrawal-list', data) } export function postWithdrawal(data: ptAny) { return request.post('wallet/withdrawal', data) } ================================================ FILE: UniApp/common/const.ts ================================================ export const xTokenCacheKey = "app-x-token"; // 缓存登录token export const AppNoticeCacheKey = "app-notice"; // 缓存提示信息 export const AppInitCacheKey = "app-init"; // 缓存公共信息 export const AppInitOptionCacheKey = "app-init-options"; // 缓存初始化参数 export const UserMineCacheKey = "app-user-mine"; // 个人中心缓存 export const AiRolesCacheKey = "app-ai-room-roles"; // ai问答角色缓存 export const AiBaseCacheKey = "app-ai-room-base"; // ai问答基础信息缓存 export const AiQuickIssueCacheKey = "app-ai-room-quick-issue"; // ai问答快捷问题列表缓存 export const AiLoginCacheKey = "app-ai-login"; // 缓存登录名密码 export const AiModelCacheKey = "app-ai-model"; // 模型设置缓存 export const AiModelListCacheKey = "app-ai-model-list"; // 模型设置缓存 ================================================ FILE: UniApp/common/func.ts ================================================ /** * uploadCos. * 上传到七牛云 * @param {any} qiniuInfo 七牛云预生成的信息 * @param {String} filePath 文件的临时路径 * @returns {Promise} */ export function uploadQiniu(qiniuInfo: utilsType.qiniuInfo, filePath: string) { return new Promise(async (resolve, reject) => { // @ts-ignore await uni.uploadFile({ url: qiniuInfo.service_url, filePath: filePath, name: 'file', formData: { token: qiniuInfo.token, key: qiniuInfo.path }, success: (res: ptAny) => { resolve(JSON.parse(res.data)) }, fail: (err: ptAny) => { reject(err) } }) }) } ================================================ FILE: UniApp/common/utils/jump.ts ================================================ export const byNavigateTo = (page: string, params?: ptAny) => { // @ts-ignore uni.navigateTo({ url: page }).then((r: ptAny) => { }) } export const byRedirectTo = (page: string, params?: ptAny) => { // @ts-ignore uni.redirectTo({ url: page }).then((r: ptAny) => { }) } export const byReLaunch = (page: string, params?: ptAny) => { // @ts-ignore uni.reLaunch({ url: page }).then((r: ptAny) => { }) } export const byNavigateBack = (delta: number) => { // @ts-ignore uni.navigateBack({ delta: delta }).then((r: ptAny) => { }) } ================================================ FILE: UniApp/common/utils/request.ts ================================================ class RequestService { private before: ptAny[]; private after: ptAny[]; constructor() { // @ts-ignore this.before = [] this.after = [] } static handleIntercept(handles: ptAny, data: utilsType.requestConfig | ptAny) { return handles.reduce((old: ptAny, current: ptAny) => { return current(old) }, data) } use(before: ptAny, after: ptAny) { typeof before === 'function' && this.before.push(before) typeof after === 'function' && this.after.push(after) } get(url: ptAny, data?: ptAny) { return this.request({ url, data, method: 'GET' }) } post(url: ptAny, data?: ptAny) { return this.request({ url, data, method: 'POST' }) } request(config: utilsType.requestConfig) { let _config = RequestService.handleIntercept(this.before, config) // @ts-ignore return new Promise((resolve, reject) => { // @ts-ignore uni.request({ ..._config, ...{ success: (res: ptAny) => { let response = RequestService.handleIntercept(this.after, res) if (response && response.statusCode === 200) { resolve(response.data) } else { reject(response.data) } }, fail: (err: any) => { console.log("fail", err) reject(err) } } }) }) } } const request = new RequestService() export default request ================================================ FILE: UniApp/common/utils/services.ts ================================================ import Request from './request' import {AppNoticeCacheKey, xTokenCacheKey} from "../const"; import {useIndexStore} from '../../store' import {getConfig} from "../config"; Request.use( (config: utilsType.requestConfig) => { if (!config.header) { config.header = {} } // @ts-ignore let authorization = uni.getStorageSync(xTokenCacheKey) config.header['Authorization'] = 'Bearer ' + (authorization ? authorization.token : '') if (config.url.slice(0, 8) !== "https://") { config.url = getConfig().baseURL + config.url } return config }, (response: ptAny) => { switch (response.data.code) { case 10006: // 参数错误 // @ts-ignore uni.showToast({ title: response.data.message, icon: 'none', duration: 1500 }) break case 10001: // token 无效 useIndexStore().authorization(true) break case 10002: // 账号被锁定 // @ts-ignore uni.setStorageSync(AppNoticeCacheKey, response.data) // @ts-ignore uni.redirectTo({ url: '/pages/notice' }) break case 10003: // 跳转到登录页 // @ts-ignore uni.redirectTo({ url: '/pages/login' }) break case 10004: // 关站维护 // @ts-ignore uni.setStorageSync(AppNoticeCacheKey, response.data) // @ts-ignore uni.redirectTo({ url: '/pages/notice' }) break case 10005: // 清空所有缓存数据 // @ts-ignore uni.clearStorageSync() break } return response }) export default Request ================================================ FILE: UniApp/components/AgreementPopup.vue ================================================ ================================================ FILE: UniApp/components/FooterCommon.vue ================================================ ================================================ FILE: UniApp/components/Nothing.vue ================================================ ================================================ FILE: UniApp/components/OpenVipPopup.vue ================================================ ================================================ FILE: UniApp/components/QrCodePopup.vue ================================================ ================================================ FILE: UniApp/components/Search.vue ================================================ ================================================ FILE: UniApp/config.example.ts ================================================ let config = [ { wsUrl: 'ws://开发环境的域名ws/ws-chat', baseURL: 'https://开发环境的域名/api/ai/api/' }, { wsUrl: 'ws://线上域名/ws/ws-chat', baseURL: 'https://线上域名/api/ai/api/' } ] // @ts-ignore let index = process.env.NODE_ENV === 'development' ? 0 : 1 export const getConfig = () => { return config[index] } ================================================ FILE: UniApp/index.html ================================================
================================================ FILE: UniApp/logic/user.ts ================================================ import {UserMineCacheKey} from "../common/const" import {getMine} from "../common/api" export function userService() { const initMine = async () => { // @ts-ignore let cache = uni.getStorageSync(UserMineCacheKey) if (cache) { return cache } let data = {} await getMine().then((res: ptAny) => { if (res.code === 200) { data = res.data // @ts-ignore uni.setStorageSync(UserMineCacheKey, res.data) } }) return data } const updateMineUserCache = (data: ptAny) => { // @ts-ignore let cache = uni.getStorageSync(UserMineCacheKey) if (cache) { cache.head_img = data.head_img cache.nick_name = data.nick_name cache.mobile = data.mobile // @ts-ignore uni.setStorageSync(UserMineCacheKey, cache) } } return {initMine, updateMineUserCache} } ================================================ FILE: UniApp/main.js ================================================ import App from './App' // #ifndef VUE3 import Vue from 'vue' import './uni.promisify.adaptor' Vue.config.productionTip = false App.mpType = 'app' const app = new Vue({ ...App }) app.$mount() // #endif // #ifdef VUE3 import { createSSRApp } from 'vue' import * as Pinia from 'pinia'; export function createApp() { const app = createSSRApp(App) app.use(Pinia.createPinia()) return { app, Pinia, } } // #endif ================================================ FILE: UniApp/manifest.json ================================================ { "name" : "ChatGPT", "appid" : "", "description" : "ChatGPT AI问答", "versionName" : "1.0.3", "versionCode" : "100", "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { "usingComponents" : true, "nvueStyleCompiler" : "uni-app", "compilerVersion" : 3, "splashscreen" : { "alwaysShowBeforeRender" : true, "waiting" : true, "autoclose" : true, "delay" : 0 }, /* 模块配置 */ "modules" : {}, /* 应用发布信息 */ "distribute" : { /* android打包配置 */ "android" : { "permissions" : [ "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" ] }, /* ios打包配置 */ "ios" : { "dSYMs" : false }, /* SDK配置 */ "sdkConfigs" : { "ad" : {} }, "icons" : { "android" : { "hdpi" : "./static/icon/72x72.png", "xhdpi" : "./static/icon/96x96.png", "xxhdpi" : "./static/icon/144x144.png", "xxxhdpi" : "./static/icon/192x192.png" }, "ios" : { "appstore" : "./static/icon/1024x1024.png", "ipad" : { "app" : "./static/icon/76x76.png", "app@2x" : "./static/icon/152x152.png", "notification" : "./static/icon/20x20.png", "notification@2x" : "./static/icon/40x40.png", "proapp@2x" : "./static/icon/167x167.png", "settings" : "./static/icon/29x29.png", "settings@2x" : "./static/icon/58x58.png", "spotlight" : "./static/icon/40x40.png", "spotlight@2x" : "./static/icon/80x80.png" }, "iphone" : { "app@2x" : "./static/icon/120x120.png", "app@3x" : "./static/icon/180x180.png", "notification@2x" : "./static/icon/40x40.png", "notification@3x" : "./static/icon/60x60.png", "settings@2x" : "./static/icon/58x58.png", "settings@3x" : "./static/icon/87x87.png", "spotlight@2x" : "./static/icon/80x80.png", "spotlight@3x" : "./static/icon/120x120.png" } } }, "splashscreen" : { "androidStyle" : "default", "android" : { "hdpi" : "./static/icon/init-app.9.png", "xhdpi" : "./static/icon/init-app.9.png", "xxhdpi" : "./static/icon/init-app.9.png" }, "iosStyle" : "common", "useOriginalMsgbox" : true } } }, /* 快应用特有相关 */ "quickapp" : {}, /* 小程序特有相关 */ "mp-weixin" : { "appid" : "", "setting" : { "urlCheck" : false }, "usingComponents" : true }, "mp-alipay" : { "usingComponents" : true }, "mp-baidu" : { "usingComponents" : true }, "mp-toutiao" : { "usingComponents" : true }, "uniStatistics" : { "enable" : false }, "vueVersion" : "3", "h5" : { "devServer" : { "https" : false } }, "fallbackLocale" : "zh-Hans" } ================================================ FILE: UniApp/pages/chatgpt/channel.vue ================================================ ================================================ FILE: UniApp/pages/chatgpt/room.vue ================================================ ================================================ FILE: UniApp/pages/login.vue ================================================ ================================================ FILE: UniApp/pages/notice.vue ================================================ ================================================ FILE: UniApp/pages/user/friend.vue ================================================ ================================================ FILE: UniApp/pages/user/mine.vue ================================================ ================================================ FILE: UniApp/pages/user/order.vue ================================================ ================================================ FILE: UniApp/pages/user/perfect.vue ================================================ ================================================ FILE: UniApp/pages/user/wallet.vue ================================================ ================================================ FILE: UniApp/pages/user/walletList.vue ================================================ ================================================ FILE: UniApp/pages/user/withdrawalList.vue ================================================ ================================================ FILE: UniApp/pages.json ================================================ { "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages { "path": "pages/user/mine", "style": { "navigationBarTitleText": "个人中心" } }, { "path": "pages/user/perfect", "style": { "navigationBarTitleText": "编辑资料" } }, { "path": "pages/user/friend", "style": { "navigationBarTitleText": "我的好友" } }, { "path": "pages/user/order", "style": { "navigationBarTitleText": "我的订单" } }, { "path": "pages/user/wallet", "style": { "navigationBarTitleText": "钱包" } }, { "path": "pages/user/walletList", "style": { "navigationBarTitleText": "收支明细" } }, { "path": "pages/user/withdrawalList", "style": { "navigationBarTitleText": "提现列表" } }, { "path": "pages/chatgpt/room", "style": { "navigationBarTitleText": "Ai问答", "enablePullDownRefresh":true } }, { "path": "pages/chatgpt/channel", "style": { "navigationBarTitleText": "公开频道" } }, { "path": "pages/login", "style": { "navigationBarTitleText": "login" } }, { "path": "pages/notice", "style": { "navigationBarTitleText": "notice" } } ], "globalStyle": { "navigationBarTextStyle": "black", "navigationBarTitleText": "AI大脑", "navigationBarBackgroundColor": "#F8F8F8", // #ifdef H5 "navigationStyle": "custom", // #endif // #ifndef H5 "navigationStyle": "default", // #endif "backgroundColor": "#F8F8F8" }, "uniIdRouter": {} } ================================================ FILE: UniApp/store/index.ts ================================================ // @ts-ignore import {defineStore} from 'pinia' // @ts-ignore import {ref} from "vue" import { getInit } from "../common/api" import {AppInitCacheKey, xTokenCacheKey} from "../common/const" export const useIndexStore = defineStore('index', () => { const scene = ref({ // 场景值 ads: { // 广告 }, agreement: { // 协议 user: '' }, upload: { // 上传 image : { byv_default: "1000", byv_head_img: "1002", byv_home_back: "1005", byv_real_name: "1001", byv_wx_er: "1003", byv_wx_video_er: "1004", }, domain: '', }, tutorials: { // 教程 }, }) const appInfo = ref({ copyright: '', // 版权信息 version: '', }) const customerInfo = ref({ // 客服信息 customer_id: 0, // id head_img: '', // 头像 mobile: '', // 头像 user_name: '', // 头像 work_time: '', // 头像 wx_img_url: '', // 昵称 wx_no: '', // 昵称 }) const setInit = async ()=>{ // @ts-ignore let initInfo = uni.getStorageSync(AppInitCacheKey) if (initInfo) { scene.value = initInfo.scene customerInfo.value = initInfo.other.customer_info appInfo.value.copyright = initInfo.other.copyright appInfo.value.version = initInfo.other.version return Promise.resolve() } else { return getInit().then((res: ptAny) => { if (res.code === 200) { // @ts-ignore uni.setStorageSync(AppInitCacheKey, res.data) scene.value = res.data.scene customerInfo.value = res.data.other.customer_info appInfo.value.copyright = res.data.other.copyright appInfo.value.version = res.data.other.version } }) } } const authorization = async (isForce?: boolean) => { // @ts-ignore let xTokenCache = uni.getStorageSync(xTokenCacheKey) if (xTokenCache && !isForce) { await setInit().then(async (r: ptAny) => { }) return } // @ts-ignore uni.reLaunch({url: "/pages/login"}) } return { scene, appInfo,customerInfo,setInit, authorization } }); ================================================ FILE: UniApp/store/websocket.ts ================================================ // @ts-ignore import {defineStore} from 'pinia' // @ts-ignore import {ref} from "vue" import {getConfig} from '../common/config' import { xTokenCacheKey } from '../common/const' export const useWebsocketStore = defineStore('websocket', () => { // @ts-ignore const websocketTask = ref() const heartbeatTime = ref(0) const isOpenSocket = ref(false) const isReconnect = ref(true) const onMessageHandles = ref({ ping: (res: ptAny) => { console.log("websocket-ping", res) }, opened: (res: ptAny) => { console.log("websocket-opened", res) }, close: (res: ptAny) => { console.log("websocket-close", res) }, error: (res: ptAny) => { console.log("websocket-error", res) }, login: (res: ptAny) => { // @ts-ignore uni.reLaunch({url: "/pages/login"}) }, }) const websocketInit = () => { if (isOpenSocket.value) { return } // @ts-ignore let authorization = uni.getStorageSync(xTokenCacheKey) // @ts-ignore websocketTask.value = uni.connectSocket({ url: getConfig().wsUrl + "?token=" + (authorization ? authorization.token : ''), complete: () => { } }) websocketTask.value.onOpen((res: ptAny) => { heartbeatTime.value && clearInterval(heartbeatTime.value) isOpenSocket.value = true heartbeatTime.value = setInterval(() => { websocketTask.value.send({data: "{\"type\":\"ping\"}"}); }, 55000) }) websocketTask.value.onMessage((res: ptAny) => { let data = JSON.parse(res.data) if(onMessageHandles.value.hasOwnProperty(data.type)){ onMessageHandles.value[data.type](data) }else{ console.log("找不到onMessageHandles") } }) websocketTask.value.onClose(() => { isOpenSocket.value = false isReconnect.value && reconnect(); }) } const reconnect = () => { websocketTask.value.close({}) if (!isOpenSocket.value) { setTimeout(() => { websocketInit(); }, 2000) } } const send = (data: ptAny) => { websocketTask.value.send({data:JSON.stringify(data)}) } const close = () => { websocketTask.value.close({}) isOpenSocket.value = false websocketTask.value = null } const bindMessageHandle = (handle: ptAny)=>{ onMessageHandles.value[handle.type] = handle.event } return { websocketInit, send, close, bindMessageHandle } }) ================================================ FILE: UniApp/tsconfig.json ================================================ // tsconfig.json { "compilerOptions": { "target": "esnext", "module": "esnext", "strict": true, "jsx": "preserve", "moduleResolution": "node", "esModuleInterop": true, "sourceMap": true, "skipLibCheck": true, "importHelpers": true, "allowSyntheticDefaultImports": true, "useDefineForClassFields": true, "resolveJsonModule": true, "lib": [ "esnext", "dom" ], "types": [ "@dcloudio/types", "./types" ] }, "exclude": [ "node_modules", "unpackage", "src/**/*.nvue" ] } ================================================ FILE: UniApp/types/global.d.ts ================================================ type ptAny = any declare namespace utilsType { interface result{ code: number; data: any; message: string; } interface requestConfig { url: string header?: Record data?: any method: 'GET' | 'POST' | 'PUT' | 'DELETE' } interface qiniuInfo { domain: string service_url: string scene: string | number token: string path: string } } ================================================ FILE: UniApp/uni.promisify.adaptor.js ================================================ uni.addInterceptor({ returnValue (res) { if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { return res; } return new Promise((resolve, reject) => { res.then((res) => res[0] ? reject(res[0]) : resolve(res[1])); }); }, }); ================================================ FILE: UniApp/uni.scss ================================================ /** * 这里是uni-app内置的常用样式变量 * * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App * */ /** * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 * * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 */ /* 颜色变量 */ /* 行为相关颜色 */ $uni-color-primary: #007aff; $uni-color-success: #4cd964; $uni-color-warning: #f0ad4e; $uni-color-error: #dd524d; /* 文字基本颜色 */ $uni-text-color:#333;//基本色 $uni-text-color-inverse:#fff;//反色 $uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 $uni-text-color-placeholder: #808080; $uni-text-color-disable:#c0c0c0; /* 背景颜色 */ $uni-bg-color:#ffffff; $uni-bg-color-grey:#f8f8f8; $uni-bg-color-hover:#f1f1f1;//点击状态颜色 $uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 /* 边框颜色 */ $uni-border-color:#c8c7cc; /* 尺寸变量 */ /* 文字尺寸 */ $uni-font-size-sm:12px; $uni-font-size-base:14px; $uni-font-size-lg:16; /* 图片尺寸 */ $uni-img-size-sm:20px; $uni-img-size-base:26px; $uni-img-size-lg:40px; /* Border Radius */ $uni-border-radius-sm: 2px; $uni-border-radius-base: 3px; $uni-border-radius-lg: 6px; $uni-border-radius-circle: 50%; /* 水平间距 */ $uni-spacing-row-sm: 5px; $uni-spacing-row-base: 10px; $uni-spacing-row-lg: 15px; /* 垂直间距 */ $uni-spacing-col-sm: 4px; $uni-spacing-col-base: 8px; $uni-spacing-col-lg: 12px; /* 透明度 */ $uni-opacity-disabled: 0.3; // 组件禁用态的透明度 /* 文章场景相关 */ $uni-color-title: #2C405A; // 文章标题颜色 $uni-font-size-title:20px; $uni-color-subtitle: #555555; // 二级标题颜色 $uni-font-size-subtitle:26px; $uni-color-paragraph: #3F536E; // 文章段落颜色 $uni-font-size-paragraph:15px; $theme-color: rgb(85 170 239); ================================================ FILE: UniApp/uni_modules/bt-cropper_3.0.1/changelog.md ================================================ ## 3.0.1(2022-11-03) 修复 撤销和重做不生效的问题 ## 3.0.0(2022-11-03) 使用wxs重构代码,性能大提升 新增 支持蒙版裁剪,可以裁剪任何形状的图形(详情见demo示例) 新增 支持在弹窗中使用(详情见demo示例) 移除 由于插槽会导致许多问题,实际上开发者自己封装组件反而更简单,所以3.0版本以后移除插槽,2.0迁移教程见 demo:全屏裁剪 ## 2.0.3(2022-08-21) 修复 在vue3 程序中报错的问题 新增 新增了图片初始化完成和加载失败的事件 ## 2.0.2(2022-08-18) 新增 增加了原像素裁剪功能,即使用用户在裁剪框取景的大小作为输出像素,换句话说,输出的图片分辨率与输入图片分辨率一样 新增 增加了change事件,会在图像和裁剪框位置变化后触发 新增 增加了compress参数 压缩图片,压缩图片是为了提升流畅度,所以只会针对用户拖动的那张图片进行压缩,最终输出的图像品质并不会受到影响 修复 用户在没有传入图像时报错的问题 修复 ios在某些机型上拖动出现残留的问题 ## 2.0.1(2022-07-20) 修复:ios打包成app的时候有几率裁剪不成功的问题 ## 2.0.0(2022-07-13) 更新了2.0版本,增加了图片放大功能 ## bt-cropper 图片裁切 ======== ### 2022年7月13日 发布2.0版本 * 1.完全重构了代码,并且优化了性能 * 2.支持app * 3.修复demo在H5的情况下高度错误的问题 ================================================ FILE: UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/bt-cropper.vue ================================================ ================================================ FILE: UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/iconfont.css ================================================ @font-face { font-family: "iconfont"; /* Project id 3311610 */ src: url('//at.alicdn.com/t/font_3311610_7wh8injedpd.woff2?t=1649382821379') format('woff2'), url('//at.alicdn.com/t/font_3311610_7wh8injedpd.woff?t=1649382821379') format('woff'), url('//at.alicdn.com/t/font_3311610_7wh8injedpd.ttf?t=1649382821379') format('truetype'); } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-reset:before { content: "\e611"; } .icon-move:before { content: "\e67b"; } ================================================ FILE: UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/js/touchs.js ================================================ var startTouchs = []; var touchType = '' var startDistance = 0; var touchCenter = []; var cropperRect = null; var imageRect = null; var directionX = 0; var directionY = 0; var ratio = 0; // 操作时改变的对象 var changes = { imageRect: null, cropperRect: null } export default { computed: { imageStyle() { const imageRect = this.imageRect if (imageRect) { return { left: imageRect.left + 'px', top: imageRect.top + 'px', width: imageRect.width + 'px', height: imageRect.height + 'px' } } else { return {} } }, cropperStyle() { const cropperRect = this.cropperRect if (cropperRect) { return { left: cropperRect.left + 'px', top: cropperRect.top + 'px', width: cropperRect.width + 'px', height: cropperRect.height + 'px' } } else { return {} } } }, methods: { touchStart() { let ev; if (arguments.length == 3) { directionX = arguments[0]; directionY = arguments[1]; ev = arguments[2]; touchType = "controller"; } else { touchType = "image"; ev = arguments[0]; } startTouchs = ev.touches; changes = { imageRect: this.imageRect, cropperRect: this.cropperRect }; ratio = this.ratio; cropperRect = { ...changes.cropperRect } imageRect = { ...changes.imageRect } if (startTouchs.length == 2) { const imageRect = this.imageRect var x1 = startTouchs[0].clientX var y1 = startTouchs[0].clientY var x2 = startTouchs[1].clientX var y2 = startTouchs[1].clientY var distance = Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2) startDistance = Math.sqrt(distance) var leftPercent = ((x1 + x2) / 2 - imageRect.left) / imageRect.width var topPercent = ((y1 + y2) / 2 - imageRect.top) / imageRect.height touchCenter = [leftPercent, topPercent] } }, touchMove(ev) { if(startTouchs.length!==ev.touches.length) return var touches = ev.touches; var changeX1 = touches[0].clientX - startTouchs[0].clientX; var changeY1 = touches[0].clientY - startTouchs[0].clientY; if (startTouchs.length == 1) { if (touchType === 'image') { changes.imageRect.left = imageRect.left + changeX1; changes.imageRect.top = imageRect.top + changeY1; // console.log(startTouchs.length,ev.touches.length) } else if (touchType === 'controller') { var changeX = changeX1 * directionX; var changeY = changeY1 * directionY; // 比例缩放控制 if (ratio !== 0) { if (directionX * directionY !== 0) { if (changeX / ratio > changeY) { changeY = changeX / ratio changeX = changeY * ratio } else { changeX = changeY * ratio changeY = changeX / ratio } } else { if (directionX == 0) { changeX = changeY * ratio } else { changeY = changeX / ratio } } } var width = cropperRect.width + changeX var height = cropperRect.height + changeY var imageRight = imageRect.left + imageRect.width var imageBottom = imageRect.top + imageRect.height if (directionX != -1) { if (cropperRect.left + width > imageRight) { width = imageRight - cropperRect.left if (ratio !== 0) { height = width / ratio } } } else { var cLeft = cropperRect.left - changeX if (cLeft < imageRect.left) { width = cropperRect.left + cropperRect.width - imageRect.left if (ratio !== 0) { height = width / ratio } } } // 判断是否触底 if (directionY != -1) { if (cropperRect.top + height > imageBottom) { height = imageBottom - cropperRect.top if (ratio !== 0) { width = height * ratio } } } else { var cTop = cropperRect.top - changeY if (cTop < imageRect.top) { height = cropperRect.top + cropperRect.height - imageRect.top if (ratio !== 0) { width = height * ratio } } } if (directionX == -1) { changes.cropperRect.left = cropperRect.left + cropperRect.width - width } if (directionY == -1) { changes.cropperRect.top = cropperRect.top + cropperRect.height - height } // 边界控制 changes.cropperRect.width = width changes.cropperRect.height = height } } else if (touches.length == 2 && startTouchs.length == 2) { var changeX2 = touches[0].clientX - touches[1].clientX; var changeY2 = touches[0].clientY - touches[1].clientY; var distance = Math.pow(changeX2, 2) + Math.pow(changeY2, 2) distance = Math.sqrt(distance) // 放大比例 var scaleRate = distance / startDistance this.imageScale(scaleRate) } }, touchEnd(ev) { // console.log('end',ev) if(ev.touches.length!==0) return if (touchType === "image") { var cropperLeft = cropperRect.left var cropperRight = cropperRect.left + cropperRect.width var cropperTop = cropperRect.top var cropperBottom = cropperTop + cropperRect.height var rate = changes.imageRect.width / changes.imageRect.height var cropperRate = cropperRect.width / cropperRect.height if (changes.imageRect.width < cropperRect.width || changes.imageRect.height < cropperRect.height) { var scale = 1 if (rate < cropperRate) { scale = cropperRect.width / changes.imageRect.width } else { scale = cropperRect.height / changes.imageRect.height } imageRect.width = changes.imageRect.width imageRect.height = changes.imageRect.height this.imageScale(scale) } // 边界控制start if (cropperLeft < changes.imageRect.left) { changes.imageRect.left = cropperLeft } if (cropperRight > changes.imageRect.left + changes.imageRect.width) { changes.imageRect.left = cropperRight - changes.imageRect.width } if (cropperTop < changes.imageRect.top) { changes.imageRect.top = cropperTop } if (cropperBottom > changes.imageRect.top + changes.imageRect.height) { changes.imageRect.top = cropperBottom - changes.imageRect.height } // 边界控制end } this.updateData({ cropperRect: changes.cropperRect, imageRect: changes.imageRect, }) touchType = "" startTouchs = [] return false; }, imageScale(scaleRate) { var cw = imageRect.width * (scaleRate - 1) var ch = imageRect.height * (scaleRate - 1) changes.imageRect = { width: imageRect.width + cw, height: imageRect.height + ch, left: imageRect.left - cw * (touchCenter[0]), top: imageRect.top - ch * (touchCenter[1]) } } } } ================================================ FILE: UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/utils/tools.js ================================================ export function getTouchPoints(touchs) { return Array.from(touchs).map(ev => { return [ev.clientX, ev.clientY] }) } // 函数防抖 export function debounce(fn, wait = 200) { var timer = null; return function (){ if (timer !== null) { clearTimeout(timer); } timer = setTimeout(fn.bind(this), wait); } } /** * @description 睡眠 * @param {number} time 等待时间毫秒数 */ export function sleep(time = 200) { return new Promise(resolve => { setTimeout(resolve, time) }) } const systemInfo = uni.getSystemInfoSync(); export function parseUnit(size){ if(typeof size == 'number' || !isNaN(Number(size))){ return uni.upx2px(size) }else if(typeof size === 'string') { if(size.endsWith('rpx')){ return parseUnit(size.replace('rpx','')) }else if(size.endsWith('px')){ return Number(size.replace('px','')) }else if(size.endsWith('vw')){ return Number(size.replace('vw',''))*systemInfo.screenWidth/100 }else if(size.endsWith('vh')){ return Number(size.replace('vh',''))*systemInfo.screenHeight/100 } } return 0 } ================================================ FILE: UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/{ages.json ================================================ ================================================ FILE: UniApp/uni_modules/bt-cropper_3.0.1/package.json ================================================ { "id": "bt-cropper", "displayName": "bt-cropper图片裁剪插件", "version": "3.0.1", "description": "一款好用的图片裁剪插件", "keywords": [ "图片", "图片裁剪", "图片裁剪", "头像裁剪", "cropper" ], "repository": "", "engines": { "HBuilderX": "^3.2.1" }, "dcloudext": { "sale": { "regular": { "price": "0.00" }, "sourcecode": { "price": "0.00" } }, "contact": { "qq": "1097122362" }, "declaration": { "ads": "无", "data": "插件不采集任何数据", "permissions": "无" }, "npmurl": "", "type": "component-vue" }, "uni_modules": { "dependencies": [], "encrypt": [], "platforms": { "cloud": { "tcb": "y", "aliyun": "y" }, "client": { "Vue": { "vue2": "y", "vue3": "y" }, "App": { "app-vue": "y", "app-nvue": "n" }, "H5-mobile": { "Safari": "y", "Android Browser": "y", "微信浏览器(Android)": "y", "QQ浏览器(Android)": "y" }, "H5-pc": { "Chrome": "n", "IE": "n", "Edge": "n", "Firefox": "n", "Safari": "n" }, "小程序": { "微信": "y", "阿里": "u", "百度": "u", "字节跳动": "y", "QQ": "y" }, "快应用": { "华为": "n", "联盟": "n" } } } } } ================================================ FILE: UniApp/uni_modules/bt-cropper_3.0.1/readme.md ================================================ ## bt-cropper 图片裁切 > **组件名:bt-cropper** 图片裁切组件,在页面中裁切图片,输出裁切后的图片,支持app,小程序,H5 ### [在线体验](https://static-a3b890b4-7cb2-4b29-aa78-e652572bdef6.bspapp.com/#/) > **注意事项** > 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。 > - 组件需要依赖 `sass` 插件 ,请自行手动安装 > - 只测试了头条小程序,app-vue 安卓,微信小程序和H5 大部分平台应该都没问题了 > - 包裹层或裁剪器需要手动指定高度和宽度,推荐手动指定裁剪器的大小,尤其是头条小程序,js有时候获取不到容器的大小 > - 如使用过程中有任何问题,或者您有一些好的建议,欢迎联系作者微信:1097122362 ### 安装方式 本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。 ### 基本用法 **示例** ```html ``` ```javascript export default { methods:{ crop(){ // 通过组件定义的ref调用cropper方法,返回一个promise对象 this.$refs.cropper.crop().then(([err,res])=>{ if(!err){ console.log(res) }else{ console.err(err) } }) } } } ``` ### 限定裁切比例 bt-cropper,指定ratio即可设置裁切框的宽高比,如果你想让用户自由缩放,将ratio设置为0即可 **示例** ```html ``` ## API ### cropper Props |属性名|类型|默认值|说明| |:-:|:-:|:-:|:-:| |ratio|number|0|裁切图像的宽高比,0表示自由比例| |dWidth|number|0|生成的图片的宽度,单位:px,如果传入0的话就是按原像素的比例裁剪,也就是说,输出图片的清晰度和输入图片的清晰度一样| |imageSrc|String|''|原图的路径,支持本地路径和网络路径,如果是网络路径,小程序要注意配置下载域名,H5要注意跨域问题| |mask|String|''| 裁剪的蒙版url,配合蒙版可以裁剪出任何形状的图形 (示例见全屏裁剪demo) | |fileType|String|'jpg'|目标文件的类型,只支持 'jpg' 或 'png'。默认为 'jpg'| |quality|Number|1|图片的质量,取值范围为 (0, 1],不在范围内时当作1.0处理| |showGrid|Boolean|false|是否显示中心网格线,默认不显示| |initPosition|object|null|图片自定义的初始的位置,内容格式见change事件| |autoZoom|Boolean|true|是否开启操作结束后自动放大到窗口大小| |containerSize|object|null|手动指定容器大小,如果裁剪器放在大小会移动或缩放的dom中,则必须手动指定大小,可以带上单位,如果不带单位默认px,支持的单位有:rpx,px,vw,vm,示例:{width:100,height:1100}或者:{width:'100rpx',height:'100rpx'}| |canvas2d|Boolean|false| 是开启新版的canvas | ### cropper Methods |方法名称|说明|参数| |:-:|:-:|:-:| |crop|裁剪图片|开始绘制并开始裁剪图片,返回Promise对象| |init|初始化|-| |resetImage|重置裁剪框和图片的位置和大小到初始的位置和大小|-| |redo|撤销,最多可以回退10步|-| |resume|重做|-| ### cropper Events |方法名称|说明|返回值| |:-:|:-:|:-:| |change|当裁剪框和图片的相对位置发生变化的时候触发,返回裁剪框与图片的相对位置|ev={left:number,top:number,width:number,height:number}| |loadFail|当图片加载失败时触发| - | |cropStart|当裁开始时触发| - | ## 帮助 在使用中如遇到无法解决的问题,请提 [Issues](https://gitee.com/xiaojiang1996/better-uni-cropper/issues) 或者加我 微信:1097122362。 ================================================ FILE: UniApp/uni_modules/mp-html/README.md ================================================ ## 为减小组件包的大小,默认组件包中不包含编辑、latex 公式等扩展功能,需要使用扩展功能的请参考下方的 插件扩展 栏的说明 ## 功能介绍 - 全端支持(含 `v3、NVUE`) - 支持丰富的标签(包括 `table`、`video`、`svg` 等) - 支持丰富的事件效果(自动预览图片、链接处理等) - 支持设置占位图(加载中、出错时、预览时) - 支持锚点跳转、长按复制等丰富功能 - 支持大部分 *html* 实体 - 丰富的插件(关键词搜索、内容编辑、`latex` 公式等) - 效率高、容错性强且轻量化 查看 [功能介绍](https://jin-yufeng.gitee.io/mp-html/#/overview/feature) 了解更多 ## 使用方法 - `uni_modules` 方式 1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下 2. 在需要使用页面的 `(n)vue` 文件中添加 ```html ``` ```javascript export default { data() { return { html: '
Hello World!
' } } } ``` 3. 需要更新版本时在 `HBuilder X` 中右键 `uni_modules/mp-html` 目录选择 `从插件市场更新` 即可 - 源码方式 1. 从 [github](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 或 [gitee](https://gitee.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 下载源码 插件市场的 **非 uni_modules 版本** 无法更新,不建议从插件市场获取 2. 在需要使用页面的 `(n)vue` 文件中添加 ```html ``` ```javascript import mpHtml from '@/components/mp-html/mp-html' export default { // HBuilderX 2.5.5+ 可以通过 easycom 自动引入 components: { mpHtml }, data() { return { html: '
Hello World!
' } } } ``` - npm 方式 1. 在项目根目录下执行 ```bash npm install mp-html ``` 2. 在需要使用页面的 `(n)vue` 文件中添加 ```html ``` ```javascript import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html' export default { // 不可省略 components: { mpHtml }, data() { return { html: '
Hello World!
' } } } ``` 3. 需要更新版本时执行以下命令即可 ```bash npm update mp-html ``` 使用 *cli* 方式运行的项目,通过 *npm* 方式引入时,需要在 *vue.config.js* 中配置 *transpileDependencies*,详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687) 如果在 **nvue** 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下,否则无法运行 查看 [快速开始](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart) 了解更多 ## 组件属性 | 属性 | 类型 | 默认值 | 说明 | |:---:|:---:|:---:|---| | container-style | String | | 容器的样式([2.1.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v210)) | | content | String | | 用于渲染的 html 字符串 | | copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 | | domain | String | | 主域名(用于链接拼接) | | error-img | String | | 图片出错时的占位图链接 | | lazy-load | Boolean | false | 是否开启图片懒加载 | | loading-img | String | | 图片加载过程中的占位图链接 | | pause-video | Boolean | true | 是否在播放一个视频时自动暂停其他视频 | | preview-img | Boolean | true | 是否允许图片被点击时自动预览 | | scroll-table | Boolean | false | 是否给每个表格添加一个滚动层使其能单独横向滚动 | | selectable | Boolean | false | 是否开启文本长按复制 | | set-title | Boolean | true | 是否将 title 标签的内容设置到页面标题 | | show-img-menu | Boolean | true | 是否允许图片被长按时显示菜单 | | tag-style | Object | | 设置标签的默认样式 | | use-anchor | Boolean | false | 是否使用锚点链接 | 查看 [属性](https://jin-yufeng.gitee.io/mp-html/#/basic/prop) 了解更多 ## 组件事件 | 名称 | 触发时机 | |:---:|---| | load | dom 树加载完毕时 | | ready | 图片加载完毕时 | | error | 发生渲染错误时 | | imgtap | 图片被点击时 | | linktap | 链接被点击时 | | play | 音视频播放时 | 查看 [事件](https://jin-yufeng.gitee.io/mp-html/#/basic/event) 了解更多 ## api 组件实例上提供了一些 `api` 方法可供调用 | 名称 | 作用 | |:---:|---| | in | 将锚点跳转的范围限定在一个 scroll-view 内 | | navigateTo | 锚点跳转 | | getText | 获取文本内容 | | getRect | 获取富文本内容的位置和大小 | | setContent | 设置富文本内容 | | imgList | 获取所有图片的数组 | | pauseMedia | 暂停播放音视频([2.2.2+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v222)) | | setPlaybackRate | 设置音视频播放速率([2.4.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v240)) | 查看 [api](https://jin-yufeng.gitee.io/mp-html/#/advanced/api) 了解更多 ## 插件扩展 除基本功能外,本组件还提供了丰富的扩展,可按照需要选用 | 名称 | 作用 | |:---:|---| | audio | 音乐播放器 | | editable | 富文本 **编辑**([示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip)) | | emoji | 解析 emoji | | highlight | 代码块高亮显示 | | markdown | 渲染 markdown | | search | 关键词搜索 | | style | 匹配 style 标签中的样式 | | txv-video | 使用腾讯视频 | | img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) | | latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) | 从插件市场导入的包中 **不含有** 扩展插件,使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包: 1. 获取完整组件包 ```bash npm install mp-html ``` 2. 编辑 `tools/config.js` 中的 `plugins` 项,选择需要的插件 3. 生成新的组件包 在 `node_modules/mp-html` 目录下执行 ```bash npm install npm run build:uni-app ``` 4. 拷贝 `dist/uni-app` 中的内容到项目根目录 查看 [插件](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin) 了解更多 ## 关于 nvue `nvue` 使用原生渲染,不支持部分 `css` 样式,为实现和 `html` 相同的效果,组件内部通过 `web-view` 进行渲染,性能上差于原生,根据 `weex` 官方建议,`web` 标签仅应用在非常规的降级场景。因此,如果通过原生的方式(如 `richtext`)能够满足需要,则不建议使用本组件,如果有较多的富文本内容,则可以直接使用 `vue` 页面 由于渲染方式与其他端不同,有以下限制: 1. 不支持 `lazy-load` 属性 2. 视频不支持全屏播放 3. 如果在 `flex-direction: row` 的容器中使用,需要给组件设置宽度或设置 `flex: 1` 占满剩余宽度 纯 `nvue` 模式下,[此问题](https://ask.dcloud.net.cn/question/119678) 修复前,不支持通过 `uni_modules` 引入,需要本地引入(将 [dist/uni-app](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 中的内容拷贝到项目根目录下) ## 立即体验 ![富文本插件](https://mp-html.oss-cn-hangzhou.aliyuncs.com/qrcode.jpg) ## 问题反馈 遇到问题时,请先查阅 [常见问题](https://jin-yufeng.gitee.io/mp-html/#/question/faq) 和 [issue](https://github.com/jin-yufeng/mp-html/issues) 中是否已有相同的问题 可通过 [issue](https://github.com/jin-yufeng/mp-html/issues/new/choose) 、插件问答或发送邮件到 [mp_html@126.com](mailto:mp_html@126.com) 提问,不建议在评论区提问(不方便回复) 提问请严格按照 [issue 模板](https://github.com/jin-yufeng/mp-html/issues/new/choose) ,描述清楚使用环境、`html` 内容或可复现的 `demo` 项目以及复现方式,对于 **描述不清**、**无法复现** 或重复的问题将不予回复 欢迎加入 `QQ` 交流群: 群1(已满):`699734691` 群2:`778239129` 查看 [问题反馈](https://jin-yufeng.gitee.io/mp-html/#/question/feedback) 了解更多 ================================================ FILE: UniApp/uni_modules/mp-html/changelog.md ================================================ ## v2.4.2(2023-05-14) 1. `A` `editable` 插件支持修改文字颜色 [详细](https://github.com/jin-yufeng/mp-html/issues/254) 2. `F` 修复了 `svg` 中有 `style` 不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/505) 3. `F` 修复了使用旧版编译器可能报错 `Bad attr nodes` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/472) 4. `F` 修复了 `app` 端可能出现无法读取 `lazyLoad` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/513) 5. `F` 修复了 `editable` 插件在点击换图时未拼接 `domain` 的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/497) by [@TwoKe945](https://github.com/TwoKe945) 6. `F` 修复了 `latex` 插件部分情况下不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/515) 7. `F` 修复了 `editable` 插件点击音视频时其他标签框不消失的问题 ## v2.4.1(2022-12-25) 1. `F` 修复了没有图片时 `ready` 事件可能不触发的问题 2. `F` 修复了加载过程中可能出现 `Root label not found` 错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/470) 3. `F` 修复了 `audio` 插件退出页面可能会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/457) 4. `F` 修复了 `vue3` 运行到 `app` 在 `HBuilder X 3.6.10` 以上报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/480) 5. `F` 修复了 `nvue` 端链接中包含 `%22` 时可能无法显示的问题 6. `F` 修复了 `vue3` 使用 `highlight` 插件可能报错的问题 ## v2.4.0(2022-08-27) 1. `A` 增加了 [setPlaybackRate](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#setPlaybackRate) 的 `api`,可以设置音视频的播放速率 [详细](https://github.com/jin-yufeng/mp-html/issues/452) 2. `A` 示例小程序代码开源 [详细](https://github.com/jin-yufeng/mp-html-demo) 3. `U` 优化 `ready` 事件触发时机,未设置懒加载的情况下基本可以准确触发 [详细](https://github.com/jin-yufeng/mp-html/issues/195) 4. `U` `highlight` 插件在编辑状态下不进行高亮处理,便于编辑 5. `F` 修复了 `flex` 布局下图片大小可能不正确的问题 6. `F` 修复了 `selectable` 属性没有设置 `force` 也可能出现渲染异常的问题 7. `F` 修复了表格中的图片大小可能不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/448) 8. `F` 修复了含有合并单元格的表格可能无法设置竖直对齐的问题 9. `F` 修复了 `editable` 插件在 `scroll-view` 中使用时工具条位置可能不正确的问题 10. `F` 修复了 `vue3` 使用 [search](advanced/plugin#search) 插件可能导致错误换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/449) ## v2.3.2(2022-08-13) 1. `A` 增加 [latex](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#latex) 插件,可以渲染数学公式 [详细](https://github.com/jin-yufeng/mp-html/pull/447) by [@Zeng-J](https://github.com/Zeng-J) 2. `U` 优化根节点下有很多标签的长内容渲染速度 3. `U` `highlight` 插件适配 `lang-xxx` 格式 4. `F` 修复了 `table` 标签设置 `border` 属性后可能无法修改边框样式的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/439) by [@zouxingjie](https://github.com/zouxingjie) 5. `F` 修复了 `editable` 插件输入连续空格无效的问题 6. `F` 修复了 `vue3` 图片设置 `inline` 会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/438) 7. `F` 修复了 `vue3` 使用 `table` 可能报错的问题 ## v2.3.1(2022-05-20) 1. `U` `app` 端支持使用本地图片 2. `U` 优化了微信小程序 `selectable` 属性在 `ios` 端的处理 [详细](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable) 3. `F` 修复了 `editable` 插件不在顶部时 `tooltip` 位置可能错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/430) 4. `F` 修复了 `vue3` 运行到微信小程序可能报错丢失内容的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/414) 5. `F` 修复了 `vue3` 部分标签可能被错误换行的问题 6. `F` 修复了 `editable` 插件 `app` 端插入视频无法预览的问题 ## v2.3.0(2022-04-01) 1. `A` 增加了 `play` 事件,音视频播放时触发,可用于与页面其他音视频进行互斥播放 [详细](basic/event#play) 2. `U` `show-img-menu` 属性支持控制预览时是否长按弹出菜单 3. `U` 优化 `wxs` 处理,提高渲染性能 [详细](https://developers.weixin.qq.com/community/develop/article/doc/0006cc2b204740f601bd43fa25a413) 4. `U` `video` 标签支持 `object-fit` 属性 5. `U` 增加支持一些常用实体编码 [详细](https://github.com/jin-yufeng/mp-html/issues/418) 6. `F` 修复了图片仅设置高度可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/410) 7. `F` 修复了 `video` 标签高度设置为 `auto` 不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/411) 8. `F` 修复了使用 `grid` 布局时可能样式错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/413) 9. `F` 修复了含有合并单元格的表格部分情况下显示异常的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/417) 10. `F` 修复了 `editable` 插件连续插入内容时顺序不正确的问题 11. `F` 修复了 `uni-app` 包 `vue3` 使用 `audio` 插件报错的问题 12. `F` 修复了 `uni-app` 包 `highlight` 插件使用自定义的 `prism.min.js` 报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/416) ## v2.2.2(2022-02-26) 1. `A` 增加了 [pauseMedia](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#pauseMedia) 的 `api`,可用于暂停播放音视频 [详细](https://github.com/jin-yufeng/mp-html/issues/317) 2. `U` 优化了长内容的加载速度 3. `U` 适配 `vue3` [#389](https://github.com/jin-yufeng/mp-html/issues/389)、[#398](https://github.com/jin-yufeng/mp-html/pull/398) by [@zhouhuafei](https://github.com/zhouhuafei)、[#400](https://github.com/jin-yufeng/mp-html/issues/400) 4. `F` 修复了小程序端图片高度设置为百分比时可能不显示的问题 5. `F` 修复了 `highlight` 插件部分情况下可能显示不完整的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/403) ## v2.2.1(2021-12-24) 1. `A` `editable` 插件增加上下移动标签功能 2. `U` `editable` 插件支持在文本中间光标处插入内容 3. `F` 修复了 `nvue` 端设置 `margin` 后可能导致高度不正确的问题 4. `F` 修复了 `highlight` 插件使用压缩版的 `prism.css` 可能导致背景失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/367) 5. `F` 修复了编辑状态下使用 `emoji` 插件内容为空时可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/371) 6. `F` 修复了使用 `editable` 插件后将 `selectable` 属性设置为 `force` 不生效的问题 ## v2.2.0(2021-10-12) 1. `A` 增加 `customElements` 配置项,便于添加自定义功能性标签 [详细](https://github.com/jin-yufeng/mp-html/issues/350) 2. `A` `editable` 插件增加切换音视频自动播放状态的功能 [详细](https://github.com/jin-yufeng/mp-html/pull/341) by [@leeseett](https://github.com/leeseett) 3. `A` `editable` 插件删除媒体标签时触发 `remove` 事件,便于删除已上传的文件 4. `U` `editable` 插件 `insertImg` 方法支持同时插入多张图片 [详细](https://github.com/jin-yufeng/mp-html/issues/342) 5. `U` `editable` 插入图片和音视频时支持拼接 `domian` 主域名 6. `F` 修复了内部链接参数中包含 `://` 时被认为是外部链接的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/356) 7. `F` 修复了部分 `svg` 标签名或属性名大小写不正确时不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/351) 8. `F` 修复了 `nvue` 页面运行到非 `app` 平台时可能样式错误的问题 ## v2.1.5(2021-08-13) 1. `A` 增加支持标签的 `dir` 属性 2. `F` 修复了 `ruby` 标签文字与拼音没有居中对齐的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/325) 3. `F` 修复了音视频标签内有 `a` 标签时可能无法播放的问题 4. `F` 修复了 `externStyle` 中的 `class` 名包含下划线或数字时可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326) 5. `F` 修复了 `h5` 端引入 `externStyle` 可能不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326) ## v2.1.4(2021-07-14) 1. `F` 修复了 `rt` 标签无法设置样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/318) 2. `F` 修复了表格中有单元格同时合并行和列时可能显示不正确的问题 3. `F` 修复了 `app` 端无法关闭图片长按菜单的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/322) 4. `F` 修复了 `editable` 插件只能添加图片链接不能修改的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/312) by [@leeseett](https://github.com/leeseett) ## v2.1.3(2021-06-12) 1. `A` `editable` 插件增加 `insertTable` 方法 2. `U` `editable` 插件支持编辑表格中的空白单元格 [详细](https://github.com/jin-yufeng/mp-html/issues/310) 3. `F` 修复了 `externStyle` 中使用伪类可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/298) 4. `F` 修复了多个组件同时使用时 `tag-style` 属性时可能互相影响的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/305) by [@woodguoyu](https://github.com/woodguoyu) 5. `F` 修复了包含 `linearGradient` 的 `svg` 可能无法显示的问题 6. `F` 修复了编译到头条小程序时可能报错的问题 7. `F` 修复了 `nvue` 端不触发 `click` 事件的问题 8. `F` 修复了 `editable` 插件尾部插入时无法撤销的问题 9. `F` 修复了 `editable` 插件的 `insertHtml` 方法只能在末尾插入的问题 10. `F` 修复了 `editable` 插件插入音频不显示的问题 ## v2.1.2(2021-04-24) 1. `A` 增加了 [img-cache](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#img-cache) 插件,可以在 `app` 端缓存图片 [详细](https://github.com/jin-yufeng/mp-html/issues/292) by [@PentaTea](https://github.com/PentaTea) 2. `U` 支持通过 `container-style` 属性设置 `white-space` 来保留连续空格和换行符 [详细](https://jin-yufeng.gitee.io/mp-html/#/question/faq#space) 3. `U` 代码风格符合 [standard](https://standardjs.com) 标准 4. `U` `editable` 插件编辑状态下支持预览视频 [详细](https://github.com/jin-yufeng/mp-html/issues/286) 5. `F` 修复了 `svg` 标签内嵌 `svg` 时无法显示的问题 6. `F` 修复了编译到支付宝和头条小程序时部分区域不可复制的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/291) ## v2.1.1(2021-04-09) 1. 修复了对 `p` 标签设置 `tag-style` 可能不生效的问题 2. 修复了 `svg` 标签中的文本无法显示的问题 3. 修复了使用 `editable` 插件编辑表格时可能报错的问题 4. 修复了使用 `highlight` 插件运行到头条小程序时可能没有样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/280) 5. 修复了使用 `editable` 插件 `editable` 属性为 `false` 时会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/284) 6. 修复了 `style` 插件连续子选择器失效的问题 7. 修复了 `editable` 插件无法修改图片和字体大小的问题 ## v2.1.0.2(2021-03-21) 修复了 `nvue` 端使用可能报错的问题 ## v2.1.0(2021-03-20) 1. `A` 增加了 [container-style](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#container-style) 属性 [详细](https://gitee.com/jin-yufeng/mp-html/pulls/1) 2. `A` 增加支持 `strike` 标签 3. `A` `editable` 插件增加 `placeholder` 属性 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable) 4. `A` `editable` 插件增加 `insertHtml` 方法 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable) 5. `U` 外部样式支持标签名选择器 [详细](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart#setting) 6. `F` 修复了 `nvue` 端部分情况下可能不显示的问题 ## v2.0.5(2021-03-12) 1. `U` [linktap](https://jin-yufeng.gitee.io/mp-html/#/basic/event#linktap) 事件增加返回内部文本内容 `innerText` [详细](https://github.com/jin-yufeng/mp-html/issues/271) 2. `U` [selectable](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable) 属性设置为 `force` 时能够在微信 `iOS` 端生效(文本块会变成 `inline-block`) [详细](https://github.com/jin-yufeng/mp-html/issues/267) 3. `F` 修复了部分情况下竖向无法滚动的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/182) 4. `F` 修复了多次修改富文本数据时部分内容可能不显示的问题 5. `F` 修复了 [腾讯视频](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#txv-video) 插件可能无法播放的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/265) 6. `F` 修复了 [highlight](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#highlight) 插件没有设置高亮语言时没有应用默认样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/276) by [@fuzui](https://github.com/fuzui) ================================================ FILE: UniApp/uni_modules/mp-html/components/mp-html/emoji/index.js ================================================ /** * @fileoverview emoji 插件 */ const reg = /\[(\S+?)\]/g const data = { 笑脸: '😄', 生病: '😷', 破涕为笑: '😂', 吐舌: '😝', 脸红: '😳', 恐惧: '😱', 失望: '😔', 无语: '😒', 眨眼: '😉', 酷: '😎', 哭: '😭', 痴迷: '😍', 吻: '😘', 思考: '🤔', 困惑: '😕', 颠倒: '🙃', 钱: '🤑', 惊讶: '😲', 白眼: '🙄', 叹气: '😤', 睡觉: '😴', 书呆子: '🤓', 愤怒: '😡', 面无表情: '😑', 张嘴: '😮', 量体温: '🤒', 呕吐: '🤮', 光环: '😇', 幽灵: '👻', 外星人: '👽', 机器人: '🤖', 捂眼镜: '🙈', 捂耳朵: '🙉', 捂嘴: '🙊', 婴儿: '👶', 男孩: '👦', 女孩: '👧', 男人: '👨', 女人: '👩', 老人: '👴', 老妇人: '👵', 警察: '👮', 王子: '🤴', 公主: '🤴', 举手: '🙋', 跑步: '🏃', 家庭: '👪', 眼睛: '👀', 鼻子: '👃', 耳朵: '👂', 舌头: '👅', 嘴: '👄', 心: '❤️', 心碎: '💔', 雪人: '☃️', 情书: '💌', 大便: '💩', 闹钟: '⏰', 眼镜: '👓', 雨伞: '☂️', 音乐: '🎵', 话筒: '🎤', 游戏机: '🎮', 喇叭: '📢', 耳机: '🎧', 礼物: '🎁', 电话: '📞', 电脑: '💻', 打印机: '🖨️', 手电筒: '🔦', 灯泡: '💡', 书本: '📖', 信封: '✉️', 药丸: '💊', 口红: '💄', 手机: '📱', 相机: '📷', 电视: '📺', 中: '🀄', 垃圾桶: '🚮', 厕所: '🚾', 感叹号: '❗', 禁: '🈲', 可: '🉑', 彩虹: '🌈', 旋风: '🌀', 雷电: '⚡', 雪花: '❄️', 星星: '⭐', 水滴: '💧', 玫瑰: '🌹', 加油: '💪', 左: '👈', 右: '👉', 上: '👆', 下: '👇', 手掌: '🖐️', 好的: '👌', 好: '👍', 差: '👎', 胜利: '✌', 拳头: '👊', 挥手: '👋', 鼓掌: '👏', 猴子: '🐒', 狗: '🐶', 狼: '🐺', 猫: '🐱', 老虎: '🐯', 马: '🐎', 独角兽: '🦄', 斑马: '🦓', 鹿: '🦌', 牛: '🐮', 猪: '🐷', 羊: '🐏', 长颈鹿: '🦒', 大象: '🐘', 老鼠: '🐭', 蝙蝠: '🦇', 刺猬: '🦔', 熊猫: '🐼', 鸽子: '🕊️', 鸭子: '🦆', 兔子: '🐇', 老鹰: '🦅', 青蛙: '🐸', 蛇: '🐍', 龙: '🐉', 鲸鱼: '🐳', 海豚: '🐬', 足球: '⚽', 棒球: '⚾', 篮球: '🏀', 排球: '🏐', 橄榄球: '🏉', 网球: '🎾', 骰子: '🎲', 鸡腿: '🍗', 蛋糕: '🎂', 啤酒: '🍺', 饺子: '🥟', 汉堡: '🍔', 薯条: '🍟', 意大利面: '🍝', 干杯: '🥂', 筷子: '🥢', 糖果: '🍬', 奶瓶: '🍼', 爆米花: '🍿', 邮局: '🏤', 医院: '🏥', 银行: '🏦', 酒店: '🏨', 学校: '🏫', 城堡: '🏰', 火车: '🚂', 高铁: '🚄', 地铁: '🚇', 公交: '🚌', 救护车: '🚑', 消防车: '🚒', 警车: '🚓', 出租车: '🚕', 汽车: '🚗', 货车: '🚛', 自行车: '🚲', 摩托: '🛵', 红绿灯: '🚥', 帆船: '⛵', 游轮: '🛳️', 轮船: '⛴️', 飞机: '✈️', 直升机: '🚁', 缆车: '🚠', 警告: '⚠️', 禁止: '⛔' } function Emoji () { } Emoji.prototype.onUpdate = function (content) { return content.replace(reg, ($, $1) => { if (data[$1]) return data[$1] return $ }) } Emoji.prototype.onGetContent = function (content) { for (const item in data) { content = content.replace(new RegExp(data[item], 'g'), '[' + item + ']') } return content } export default Emoji ================================================ FILE: UniApp/uni_modules/mp-html/components/mp-html/highlight/config.js ================================================ export default { copyByLongPress: false, // 是否需要长按代码块时显示复制代码内容菜单 showLanguageName: false, // 是否在代码块右上角显示语言的名称 showLineNumber: false // 是否显示行号 } ================================================ FILE: UniApp/uni_modules/mp-html/components/mp-html/highlight/index.js ================================================ /** * @fileoverview highlight 插件 * Include prismjs (https://prismjs.com) */ import prism from './prism.min' import config from './config' import Parser from '../parser' function Highlight (vm) { this.vm = vm } Highlight.prototype.onParse = function (node, vm) { if (node.name === 'pre') { if (vm.options.editable) { node.attrs.class = (node.attrs.class || '') + ' hl-pre' return } let i for (i = node.children.length; i--;) { if (node.children[i].name === 'code') break } if (i === -1) return const code = node.children[i] let className = code.attrs.class + ' ' + node.attrs.class i = className.indexOf('language-') if (i === -1) { i = className.indexOf('lang-') if (i === -1) { className = 'language-text' i = 9 } else { i += 5 } } else { i += 9 } let j for (j = i; j < className.length; j++) { if (className[j] === ' ') break } const lang = className.substring(i, j) if (code.children.length) { const text = this.vm.getText(code.children).replace(/&/g, '&') if (!text) return if (node.c) { node.c = undefined } if (prism.languages[lang]) { code.children = (new Parser(this.vm).parse( // 加一层 pre 保留空白符 '
' + prism.highlight(text, prism.languages[lang], lang).replace(/token /g, 'hl-') + '
'))[0].children } node.attrs.class = 'hl-pre' code.attrs.class = 'hl-code' if (config.showLanguageName) { node.children.push({ name: 'div', attrs: { class: 'hl-language', style: 'user-select:none' }, children: [{ type: 'text', text: lang }] }) } if (config.copyByLongPress) { node.attrs.style += (node.attrs.style || '') + ';user-select:none' node.attrs['data-content'] = text vm.expose() } if (config.showLineNumber) { const line = text.split('\n').length; const children = [] for (let k = line; k--;) { children.push({ name: 'span', attrs: { class: 'span' } }) } node.children.push({ name: 'span', attrs: { class: 'line-numbers-rows' }, children }) } } } } export default Highlight ================================================ FILE: UniApp/uni_modules/mp-html/components/mp-html/markdown/index.js ================================================ /** * @fileoverview markdown 插件 * Include marked (https://github.com/markedjs/marked) * Include github-markdown-css (https://github.com/sindresorhus/github-markdown-css) */ import marked from './marked.min' let index = 0 function Markdown (vm) { this.vm = vm vm._ids = {} } Markdown.prototype.onUpdate = function (content) { if (this.vm.markdown) { return marked(content) } } Markdown.prototype.onParse = function (node, vm) { if (vm.options.markdown) { // 中文 id 需要转换,否则无法跳转 if (vm.options.useAnchor && node.attrs && /[\u4e00-\u9fa5]/.test(node.attrs.id)) { const id = 't' + index++ this.vm._ids[node.attrs.id] = id node.attrs.id = id } if (node.name === 'p' || node.name === 'table' || node.name === 'tr' || node.name === 'th' || node.name === 'td' || node.name === 'blockquote' || node.name === 'pre' || node.name === 'code') { node.attrs.class = `md-${node.name} ${node.attrs.class || ''}` } } } export default Markdown ================================================ FILE: UniApp/uni_modules/mp-html/components/mp-html/mp-html.vue ================================================ ================================================ FILE: UniApp/uni_modules/mp-html/components/mp-html/node/node.vue ================================================