Parcourir la source

0721-0137-h01

Zory il y a 2 semaines
Parent
commit
0ab817b424
55 fichiers modifiés avec 2080 ajouts et 80 suppressions
  1. 1 0
      .idea/php.xml
  2. 1 0
      .idea/poem-api.iml
  3. 3 69
      README.md
  4. 82 0
      app/command/PoemContent.php
  5. 3 2
      app/command/PoemData.php
  6. 22 1
      app/controller/IndexController.php
  7. 112 0
      app/controller/common/Login.php
  8. 76 0
      app/controller/common/Menu.php
  9. 208 0
      app/extra/basic/Base.php
  10. 136 0
      app/extra/basic/Service.php
  11. 137 0
      app/extra/tools/CodeExtend.php
  12. 72 0
      app/extra/tools/DataExtend.php
  13. 80 0
      app/extra/tools/OssRegionExtend.php
  14. 123 0
      app/extra/tools/UploadExtend.php
  15. 57 0
      app/functions.php
  16. 40 0
      app/middleware/AuthMiddleware.php
  17. 49 0
      app/model/poem/PoemContent.php
  18. 48 0
      app/model/system/SystemConfig.php
  19. 45 0
      app/model/system/SystemData.php
  20. 53 0
      app/model/system/SystemMenu.php
  21. 59 0
      app/model/system/SystemUser.php
  22. 2 1
      composer.json
  23. 49 1
      composer.lock
  24. 11 0
      config/plugin/tinywan/captcha/app.php
  25. 3 0
      resource/translations/en/messages.php
  26. 46 0
      resource/translations/zh_CN/messages.php
  27. 1 0
      vendor/composer/autoload_psr4.php
  28. 5 0
      vendor/composer/autoload_static.php
  29. 51 0
      vendor/composer/installed.json
  30. 15 6
      vendor/composer/installed.php
  31. 8 0
      vendor/tinywan/captcha/.gitignore
  32. 31 0
      vendor/tinywan/captcha/.php-cs-fixer.php
  33. 21 0
      vendor/tinywan/captcha/LICENSE
  34. 51 0
      vendor/tinywan/captcha/README.md
  35. BIN
      vendor/tinywan/captcha/assets/bgs/1.jpg
  36. BIN
      vendor/tinywan/captcha/assets/bgs/2.jpg
  37. BIN
      vendor/tinywan/captcha/assets/bgs/3.jpg
  38. BIN
      vendor/tinywan/captcha/assets/bgs/4.jpg
  39. BIN
      vendor/tinywan/captcha/assets/bgs/5.jpg
  40. BIN
      vendor/tinywan/captcha/assets/bgs/6.jpg
  41. BIN
      vendor/tinywan/captcha/assets/bgs/7.jpg
  42. BIN
      vendor/tinywan/captcha/assets/bgs/8.jpg
  43. BIN
      vendor/tinywan/captcha/assets/ttfs/1.ttf
  44. BIN
      vendor/tinywan/captcha/assets/ttfs/2.ttf
  45. BIN
      vendor/tinywan/captcha/assets/ttfs/3.ttf
  46. BIN
      vendor/tinywan/captcha/assets/ttfs/4.ttf
  47. BIN
      vendor/tinywan/captcha/assets/ttfs/5.ttf
  48. BIN
      vendor/tinywan/captcha/assets/ttfs/6.ttf
  49. BIN
      vendor/tinywan/captcha/assets/zhttfs/1.otf
  50. BIN
      vendor/tinywan/captcha/captcha.png
  51. 29 0
      vendor/tinywan/captcha/composer.json
  52. 250 0
      vendor/tinywan/captcha/src/Captcha.php
  53. 16 0
      vendor/tinywan/captcha/src/Exception/CaptchaException.php
  54. 73 0
      vendor/tinywan/captcha/src/Install.php
  55. 11 0
      vendor/tinywan/captcha/src/config/plugin/tinywan/captcha/app.php

+ 1 - 0
.idea/php.xml

@@ -112,6 +112,7 @@
       <path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
       <path value="$PROJECT_DIR$/vendor/psr/cache" />
       <path value="$PROJECT_DIR$/vendor/overtrue/pinyin" />
+      <path value="$PROJECT_DIR$/vendor/tinywan/captcha" />
     </include_path>
   </component>
   <component name="PhpProjectSharedConfiguration" php_language_level="8.2">

+ 1 - 0
.idea/poem-api.iml

@@ -84,6 +84,7 @@
       <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-exporter" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/tinywan/captcha" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/tinywan/storage" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/topthink/think-container" />
       <excludeFolder url="file://$MODULE_DIR$/vendor/topthink/think-helper" />

+ 3 - 69
README.md

@@ -1,70 +1,4 @@
-<div style="padding:18px;max-width: 1024px;margin:0 auto;background-color:#fff;color:#333">
-<h1>webman</h1>
-
-基于<a href="https://www.workerman.net" target="__blank">workerman</a>开发的超高性能PHP框架
-
-
-<h1>学习</h1>
-
-<ul>
-  <li>
-    <a href="https://www.workerman.net/webman" target="__blank">主页 / Home page</a>
-  </li>
-  <li>
-    <a href="https://webman.workerman.net" target="__blank">文档 / Document</a>
-  </li>
-  <li>
-    <a href="https://www.workerman.net/doc/webman/install.html" target="__blank">安装 / Install</a>
-  </li>
-  <li>
-    <a href="https://www.workerman.net/questions" target="__blank">问答 / Questions</a>
-  </li>
-  <li>
-    <a href="https://www.workerman.net/apps" target="__blank">市场 / Apps</a>
-  </li>
-  <li>
-    <a href="https://www.workerman.net/sponsor" target="__blank">赞助 / Sponsors</a>
-  </li>
-  <li>
-    <a href="https://www.workerman.net/doc/webman/thanks.html" target="__blank">致谢 / Thanks</a>
-  </li>
-</ul>
-
-<div style="float:left;padding-bottom:30px;">
-
-  <h1>赞助商</h1>
-
-  <h4>特别赞助</h4>
-  <a href="https://www.crmeb.com/?form=workerman" target="__blank">
-    <img src="https://www.workerman.net/img/sponsors/6429/20230719111500.svg" width="200">
-  </a>
-
-  <h4>铂金赞助</h4>
-  <a href="https://www.fadetask.com/?from=workerman" target="__blank"><img src="https://www.workerman.net/img/sponsors/1/20230719084316.png" width="200"></a>
-  <a href="https://www.yilianyun.net/?from=workerman" target="__blank" style="margin-left:20px;"><img src="https://www.workerman.net/img/sponsors/6218/20230720114049.png" width="200"></a>
-
-
-</div>
-
-
-<div style="float:left;padding-bottom:30px;clear:both">
-
-  <h1>请作者喝咖啡</h1>
-
-<img src="https://www.workerman.net/img/wx_donate.png" width="200">
-<img src="https://www.workerman.net/img/ali_donate.png" width="200">
-<br>
-<b>如果您觉得webman对您有所帮助,欢迎捐赠。</b>
-
-
-</div>
-
-
-<div style="clear: both">
-<h1>LICENSE</h1>
-The webman is open-sourced software licensed under the MIT.
-</div>
-
-</div>
-
+````
+composer require tinywan/captcha
 
+````

+ 82 - 0
app/command/PoemContent.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace app\command;
+
+use app\model\dynasty\DynastyAuthor;
+use QL\QueryList;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class PoemContent extends Command
+{
+
+    protected static string $defaultName = 'poem:content';
+
+    protected static string $defaultDescription = '采集作品详情';
+
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        try {
+            $nextPage = 1; $done = 0; $subPage = 1;
+            while (is_numeric($nextPage)) {
+                $output->writeln("开始" . getDateFull());
+                $resp = (new \app\model\poem\PoemData)->where("is_done",0)->page($nextPage,10)->select();
+                if ($resp->isEmpty()) {
+                    $output->writeln("没有更多任务啦!===".getDateFull());
+                    $nextPage = null;
+                    return self::SUCCESS;
+                }
+                $model = (new \app\model\poem\PoemContent);
+                foreach ($resp as $val) {
+                    $output->writeln("标题:{$val['title']}==作者:{$val['author_name']}===链接地址:https://www.gushiji.cc/gushi/{$val['poem_id']}.html====".getDateFull());
+                    $ql = QueryList::getInstance()->get("https://www.gushiji.cc/gushi/{$val['poem_id']}.html")->rules([
+                        "data"  => [".shicicontent","text"],
+                        "tags"  => [".relatedCategory>ul>li>a","texts"],
+                        "notes" => [".shici-content","texts"],
+                    ])->query()->getData(function ($item,$key){
+                        foreach ($item['notes'] as $k=>$v){
+                            if ($k == 0) {
+                                $note = splitTransAndNote($v);
+                            } else {
+                                $note = $v;
+                            }
+                            $item['notes'][$k] = $note;
+                        }
+                        return $item;
+                    });
+                    $respData = $ql->all();
+                    $poemData = $model->where("poem_id",$val['poem_id'])->findOrEmpty();
+                    if ($poemData->isEmpty()) {
+                        $output->writeln("【新增】标题:{$val['title']}====".getDateFull());
+                        $poemData->insertGetId([
+                            'poem_id'   => $val['poem_id'],
+                            "content"   => $respData['data'],
+                            "trans"     => $respData['notes'][0]['trans']??'',
+                            "note"      => $respData['notes'][0]['note']??'',
+                            "review"    => $respData['notes'][1]??'',
+                            "background"    => $respData['notes'][2]??'',
+                        ]);
+                    } else {
+                        $output->writeln("【更新】标题:{$val['title']}====".getDateFull());
+                        $poemData->save([
+                            "content"   => $respData['data'],
+                            "trans"     => $respData['notes'][0]['trans']??'',
+                            "note"      => $respData['notes'][0]['note']??'',
+                            "review"    => $respData['notes'][1]??'',
+                            "background"    => $respData['notes'][2]??''
+                        ]);
+                    }
+                    (new \app\model\poem\PoemData)->where('id',$val['id'])->save(['is_done'=>1,"tags" => json_encode($respData['tags'])]);
+                }
+                $nextPage = $nextPage+1;
+                $output->writeln("进入下一篇=={$nextPage}==".getDateFull());
+            }
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+        }
+        return self::SUCCESS;
+    }
+
+}

+ 3 - 2
app/command/PoemData.php

@@ -22,7 +22,7 @@ class PoemData extends Command
             $nextPage = 1; $done = 0; $subPage = 1;
             while (is_numeric($nextPage)) {
                 $output->writeln("开始" . getDateFull());
-                $resp = (new DynastyAuthor)->page($nextPage,10)->select();
+                $resp = (new DynastyAuthor)->where('is_skip',0)->where("is_done",0)->page($nextPage,10)->select();
                 if ($resp->isEmpty()) {
                     $output->writeln("没有更多任务啦!===".getDateFull());
                     $nextPage = null;
@@ -53,7 +53,8 @@ class PoemData extends Command
                         });
                         $respData = $ql->all();
                         if ($subPage > $respData['page']) {
-                            $output->writeln("作者:{$val['name']}==没有作品啦==".getDateFull());
+                            (new DynastyAuthor)->where('id',$val['id'])->save(['is_done'=>1]);
+                            $output->writeln("作者:{$val['name']}===ID:{$val['id']}==没有作品啦==".getDateFull());
                             $subPage = 1;
                             break;
                         }

+ 22 - 1
app/controller/IndexController.php

@@ -11,6 +11,28 @@ class IndexController
 {
     public function index(Request $request)
     {
+        $ql = QueryList::getInstance()->get("https://www.gushiji.cc/gushi/ed7378.html")->rules([
+            "data"  => [".shicicontent","text"],
+            "tags"  => [".relatedCategory>ul>li>a","texts"],
+            "notes" => [".shici-content","texts"],
+        ])->query()->getData(function ($item,$key){
+            foreach ($item['notes'] as $k=>$v){
+                if ($k == 0) {
+                    $note = splitTransAndNote($v);
+                } else {
+                    $note = $v;
+                }
+                $item['notes'][$k] = $note;
+            }
+            return $item;
+        });
+        $resp = $ql->all();
+        return json($resp);
+    }
+
+    protected function getPoem()
+    {
+
         $ql = QueryList::getInstance()->get("https://www.gushiji.cc/dianji/")->rules([
 //            "name"      => ['.gushi-info>h3','texts'],
 //            "link"      => ['.gushi-info>h3>a','attrs(href)'],
@@ -44,7 +66,6 @@ class IndexController
         }
         return json($resp);
     }
-
     protected function getAuthor()
     {
         $ql = QueryList::getInstance()->get("https://www.gushiji.cc/shiren/")->rules([

+ 112 - 0
app/controller/common/Login.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasStore;
+use app\model\system\SystemConfig;
+use app\model\system\SystemUser;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\{GetMapping, Controller, Middleware, PostMapping};
+use support\{Request,Response};
+use Shopwwi\WebmanAuth\Auth;
+use think\facade\Db;
+use Tinywan\Captcha\Captcha;
+
+#[Controller("/api/login"),Middleware(AuthMiddleware::class)]
+class Login extends Base
+{
+
+    /**
+     * @var array|string[]
+     */
+    protected array $noNeedLogin = ["getSystemData","setLogin"];
+
+    #[Inject]
+    protected SystemConfig $config;
+
+    /**
+     * 登陆
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('user')]
+    public function setLogin(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "username.require"  => trans("empty.user"),
+                "password.require"  => trans("empty.passwd"),
+                "code.require"      => trans("empty.code"),
+                "key.require"       => trans("empty.data"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if (Captcha::check($param['code'],$param['key']) === false) return errorTrans("error.captcha");
+            $map = ["is_deleted" => 0,"username" => $param['username']];
+            [$state,$msg,$user] = $this->checkLogin($map,2,$param);
+            if (!$state) return error($msg);
+            return successTrans("success.login",get_object_vars((new Auth)->guard("admin")->login($user)));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 登录验证处理
+     * @param array $map
+     * @param int $type
+     * @param array $param
+     * @return array
+     */
+    protected function checkLogin(array $map = [],int $type = 1,array $param = []): array
+    {
+        $user = (new SystemUser)->where($map)->findOrEmpty();
+        if ($user->isEmpty()) return [0,trans("error.user-empty"),[]];
+        if ($user['status'] <> 1) return [0,trans("error.user-status"),[]];
+        if (md5($param['password'].$user['salt']) <> $user['password']) return [0,trans("error.passwd"),[]];
+        $user->login_at = getDateFull();
+        $user->login_ip = request()->getRealIp();
+        $user->login_num = Db::raw("login_num+1");
+        $user->save();
+        return [1,'success',$user->toArray()];
+    }
+
+    #[GetMapping('profile')]
+    public function getLoginUser(): Response
+    {
+        try {
+            $userData = (new Auth)->guard("admin")->user()->toArray();
+            if (isset($userData['password'])) unset($userData['password']);
+            $agent = (new SaasStore)->where("poi_id",$userData['store_id'])->findOrEmpty();
+            if (empty($agent['vip_end']))
+            {
+                $userData['vip_end'] = 0;
+            } else {
+                $userData['vip_end'] = strtotime($agent['vip_end']);
+            }
+            return successTrans("success.data",[
+                "username"  => $userData['username'],
+                "userId"    => $userData['id'],
+                "truename"  => $userData['truename'],
+                "super"     => $userData['is_super']
+            ]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[GetMapping('data')]
+    public function getSystemData(): Response
+    {
+        try {
+            $captcha = Captcha::base64();
+            $service = $this->config->where("type","service")->column("value","name");
+            return successTrans("success.data",compact("captcha",'service'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 76 - 0
app/controller/common/Menu.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\extra\tools\DataExtend;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemMenu;
+use app\service\system\MenuService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use LinFly\Annotation\Attributes\Route\PostMapping;
+use support\Request;
+use support\Response;
+
+#[Controller(prefix: "/api/menu"),Middleware(AuthMiddleware::class)]
+class Menu extends Base
+{
+
+    #[Inject]
+    protected MenuService $service;
+
+    /**
+     * 获取菜单
+     */
+    #[GetMapping("list")]
+    public function getMenuList(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "form.default"  => $request->user['type'],
+                "type.default"  => 1
+            ]);
+            $hide = 0;
+            $menu = $this->service->getMenuList($request->user['is_super'],$hide,$param['form']);
+            $permissionsData = [];
+            foreach ($menu as $val) {
+                if ($val['type'] == 'button') {
+                    $permissionsData[] = $val['name'];
+                }
+            }
+            $menu = $this->filterMenu(DataExtend::arr2tree($menu),$param['type']);
+            if($param['type'] == 1) {
+                $permissions = $permissionsData;
+                $dashboardGrid = [];
+                return successTrans("success.data",compact('menu','permissions','dashboardGrid'));
+            }
+            return successTrans("success.data",$menu);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 更新
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("save")]
+    public function saveMenuData(Request $request): Response
+    {
+        try {
+            $state = (new SystemMenu)->setAutoData($request->post());
+            if(!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 208 - 0
app/extra/basic/Base.php

@@ -0,0 +1,208 @@
+<?php
+
+namespace app\extra\basic;
+
+use app\model\saas\SaasStore;
+use support\Response;
+use think\contract\Arrayable;
+use think\Validate;
+
+class Base
+{
+
+
+
+    const DEFAULT_SUCCESS_MESSAGE = 'success';
+    const DEFAULT_FAIL_MESSAGE = 'fail';
+
+    const DEFAULT_SUCCESS_CODE = 200;
+    const DEFAULT_FAIL_CODE = 400;
+
+
+    public function getShopId()
+    {
+        return request()->header("shop",0);
+    }
+
+
+    /**
+     * 保证金检测
+     */
+    protected function checkDeposit(string $poiId = ""): array
+    {
+        if ($poiId == 0) return [1,'ok'];
+        $deposit = (new SaasStore)->where("poi_id",$poiId)->findOrEmpty();
+        if ($deposit->isEmpty()) return [0,"非法操作"];
+        if ($deposit['deposit'] == 0) return [0,'抖音来客保证金未缴纳,请联系渠道进行保证金缴纳,充值成功后自动开放权限'];
+        return [1,'ok'];
+    }
+
+    protected function getDyConfig(): array
+    {
+        return ["appid" => sConf('wechat.mini_appid'),'secret' => sConf('wechat.mini_secret')];
+    }
+
+    /**
+     * 短信通道
+     * @return array[]
+     */
+    protected function getSmsChannel(): array
+    {
+        return [
+            [
+                "name"  => "阿里云",
+                "type"  => "aliyun",
+                "url"   => "https://dysms.console.aliyun.com/dysms.htm"
+            ],
+            [
+                "name"  => "腾讯云",
+                "type"  => "qcloud",
+                "url"   => "https://console.cloud.tencent.com/smsv2"
+            ],
+            [
+                "name"  => "七牛云",
+                "type"  => "qiniu",
+                "url"   => "https://portal.qiniu.com/sms/dashboard"
+            ]
+        ];
+    }
+
+    /**
+     *  快捷输入并验证( 支持 规则 # 别名 )
+     * @param array $rules 验证规则( 验证信息数组 )
+     * @param array|string $input 输入方式 ( post. 或 get. )
+     * @param callable|null $callable 异常处理操作
+     */
+    protected function _valid(array $rules, array|string $input = '', ?callable $callable = null)
+    {
+        if (is_string($input)) {
+            $type = trim($input, '.') ?: 'get';
+            $input = request()->$type();
+        }
+        [$data, $rule, $info] = [[], [], []];
+        foreach ($rules as $name => $message) if (is_numeric($name)) {
+            [$name, $alias] = explode('#', $message . '#');
+            $data[$name] = $input[($alias ?: $name)] ?? null;
+        } elseif (!str_contains($name, '.')) {
+            $data[$name] = $message;
+        } elseif (preg_match('|^(.*?)\.(.*?)#(.*?)#?$|', $name . '#', $matches)) {
+            [, $_key, $_rule, $alias] = $matches;
+            if (in_array($_rule, ['value', 'default'])) {
+                if ($_rule === 'value') $data[$_key] = $message;
+                elseif ($_rule === 'default') $data[$_key] = $input[($alias ?: $_key)] ?? $message;
+            } else {
+                $info[explode(':', $name)[0]] = $message;
+                $data[$_key] = $data[$_key] ?? ($input[($alias ?: $_key)] ?? null);
+                $rule[$_key] = isset($rule[$_key]) ? ($rule[$_key] . '|' . $_rule) : $_rule;
+            }
+        }
+        $validate = new Validate();
+        if ($validate->rule($rule)->message($info)->check($data)) {
+            return $data;
+        } elseif (is_callable($callable)) {
+            return call_user_func($callable, $validate->getError(), $data);
+        } else {
+            return $validate->getError();
+        }
+    }
+    /**
+     * @param $data
+     * @return array|string|null
+     */
+    private function parseData($data)
+    {
+        if ($data instanceof Arrayable)
+            return $data->toArray();
+        else
+            return $data;
+    }
+
+    /**
+     * @param string $message
+     * @param array|null $data
+     * @return Response
+     */
+    public function success(string $message = self::DEFAULT_SUCCESS_MESSAGE, array $data = null): Response
+    {
+        $message = $this->parseData($message);
+        if (is_array($message)) {
+            $data = $message;
+            $message = self::DEFAULT_SUCCESS_MESSAGE;
+        } else {
+            $data = $this->parseData($data);
+        }
+        return $this->make(self::DEFAULT_SUCCESS_CODE, $message, $data);
+    }
+
+
+    /**
+     * @param int $status
+     * @param string $msg
+     * @param null $data
+     * @return Response
+     */
+    public function make(int $status, string $msg, $data = null)
+    {
+        $content = compact('status', 'msg');
+        if (!is_null($data))
+            $content['data'] = $this->parseData($data);
+        return json($content);
+    }
+
+    public function encode($msg = self::DEFAULT_SUCCESS_MESSAGE, $data = null)
+    {
+        $msg = $this->parseData($msg);
+        if (is_array($msg)) {
+            $data = $msg;
+            $msg = self::DEFAULT_SUCCESS_MESSAGE;
+        } else {
+            $data = $this->parseData($data);
+        }
+        $status = self::DEFAULT_SUCCESS_CODE;
+        $encode = true;
+        $content = compact('status','encode', 'msg');
+        if (is_null($data) || env('APP_DEBUG', false))
+            return $this->success($msg, $data['data'] ?? $data);
+        if (!is_array($data) || !isset($data['data'])) {
+            $res['data'] = $data;
+        } else {
+            $res = $data;
+        }
+        $res['status'] = $status;
+        $res['msg'] = $msg;
+        $content['data'] =  base64_encode(gzdeflate(json_encode($res,JSON_UNESCAPED_UNICODE),9));
+        return json($content);
+    }
+
+
+    /**
+     * 菜单信息格式化
+     * @param array $menus
+     * @param int $type
+     * @param int $mch
+     * @return array
+     */
+    protected function filterMenu(array $menus,int $type,int $mch = 0): array
+    {
+        foreach ($menus as &$menu) {
+            $menu['meta'] = [
+                "title"     => $menu['title'],
+                "icon"      => $menu['icon']??'',
+                "type"      => $menu['type'],
+                "hidden"    => !($menu['hide'] == 1),
+            ];
+            if (!empty($menu['children'])) {
+                $menu['children'] = $this->filterMenu($menu['children'],$type,$mch);
+            }
+            if ($mch > 0) {
+                $menu['component'] = ($mch==1?'merchant/':'store/').$menu['name'];
+            } else {
+                $menu['component'] = $menu['name'];
+            }
+            $menu['isMenu'] = $menu['status'];
+            if($type == 1) unset($menu['title'],$menu['icon'],$menu['type'],$menu['pid'],$menu['id']);
+        }
+        return $menus;
+    }
+
+}

+ 136 - 0
app/extra/basic/Service.php

@@ -0,0 +1,136 @@
+<?php
+
+namespace app\extra\basic;
+
+use app\model\system\SystemConfig;
+use Overtrue\EasySms\Strategies\OrderStrategy;
+
+class Service
+{
+
+    /**
+     * @var null
+     */
+    protected $mode = null;
+
+
+    /**
+     * 白名单
+     * @var array|string[]
+     */
+    protected array $mobileWhite = ["18665619195","18665619196","18665619197","18665619198","18665619199","18665619191","18665619192","18665619193","18665619194"];
+
+
+    /**
+     * 短信配置
+     * @param int $type 1 系统配置 2 代理配置
+     * @return array
+     */
+    protected function smsConfig(int $type = 1): array
+    {
+        $sms = (new SystemConfig)->where("type","sms")->column("value","name");
+        return [
+            "config" => [
+                'enable' => true,
+                'timeout' => 5.0,
+                "default"   => [
+                    "strategy"  => OrderStrategy::class,
+                    "gateways"  => [$sms['sms_type']]
+                ],
+                "gateways"  => [
+                    'errorlog' => [
+                        'file' => runtime_path().'/tmp/easy-sms.log',
+                    ],
+                    'aliyun' => [
+                        'access_key_id' => $sms['AccessKeyId'],
+                        'access_key_secret' => $sms['AccessKeySecret'],
+                        'sign_name' => trim($sms['sign']),
+                    ],
+                ]
+            ],
+            "template"  => $sms['login']
+        ];
+    }
+    /**
+     * 默认排序筛选
+     * @param array $param
+     * @param string $prefix
+     * @param array $filter
+     * @return string[]
+     */
+    public function defaultSort(array $param = [],string $prefix = "",array $filter = []): array
+    {
+        if (!empty($prefix)) $prefix = $prefix.".";
+        if (isset($param['order']) && $param['order'] == 'descending'){
+            $orderBy = ["{$prefix}{$param['field']}" => "desc"];
+        } else if (isset($param['order']) && $param['order'] == 'ascending'){
+            $orderBy = ["{$prefix}{$param['field']}" => "asc"];
+        } else {
+            $orderBy = ["{$prefix}create_at" => "desc"];
+        }
+        return $orderBy;
+    }
+
+
+    /**
+     * @param array $param 参数
+     * @param array $filter 过滤器
+     * @param string $prefix 链表前缀
+     * @param string $join 链表表名
+     * @param string $field 字段
+     * @param string $forKey 链表首字母
+     * @param string $localKey 被链表首字母
+     */
+    protected function searchVal(array $param = [],array $filter = [],string $prefix = "",string $join = "",string $field = "",string $forKey = "",string $localKey = "")
+    {
+        $orderBy = $this->defaultSort($param,$prefix);
+        $commonFilter = [];
+        // 起止时间
+        if (!empty($param['create'])) {
+            $times = between_time($param['create']);
+            $start = date('Y-m-d',$times['start_time']);
+            $end = date('Y-m-d',($times['end_time'] + 86400));
+            $commonFilter[] = ['create_at', '>=', $start ];
+            $commonFilter[] = ['create_at', '<', $end ];
+        }
+        $filter = array_merge($filter,$commonFilter);
+        if (!empty($join)) {
+            $this->mode =  $this->mode->alias('a')->join("{$join} {$prefix}","a.{$forKey} = {$prefix}.{$localKey}")->field($field);
+        }
+        return $this->mode->order($orderBy)->where($filter);
+    }
+
+
+    /**
+     *
+     */
+    public function getList(array $param = [],$with = null,bool $page = true,array $append = [],$withAttr = null)
+    {
+        $model = $this->searchVal($param,$this->searchFilter($param));
+        // 支持多种类型的 $with 参数
+        if ($with !== null) {
+            if (is_callable($with)) {
+                $with($model);
+            } elseif (is_string($with) || is_array($with)) {
+                $model->with($with);
+            }
+        }
+        // 3. 动态设置修改器(withAttr)
+        if ($withAttr !== null) {
+            $model->withAttr($withAttr);
+        }
+        if (!empty($append)) {
+            $model->append($append);
+        }
+        if ($page) {
+            $resp = $model->paginate([
+                "list_rows" => $param['pageSize'],
+                "page"      => $param['page']
+            ]);
+        } else {
+            $resp = $model->select()->toArray();
+        }
+        return $resp;
+    }
+
+}

+ 137 - 0
app/extra/tools/CodeExtend.php

@@ -0,0 +1,137 @@
+<?php
+
+namespace app\extra\tools;
+
+class CodeExtend
+{
+
+    /**
+     * 生成随机编码
+     * @param integer $size 编码长度
+     * @param integer $type 编码类型(1纯数字,2纯字母,3数字字母)
+     * @param string $prefix 编码前缀
+     * @return string
+     */
+    public static function random(int $size = 10, int $type = 1, string $prefix = ''): string
+    {
+        $numbs = '0123456789';
+        $chars = 'abcdefghijklmnopqrstuvwxyz';
+        if ($type === 1) $chars = $numbs;
+        if ($type === 3) $chars = "{$numbs}{$chars}";
+        $code = $prefix . $chars[rand(1, strlen($chars) - 1)];
+        while (strlen($code) < $size) $code .= $chars[rand(0, strlen($chars) - 1)];
+        return $code;
+    }
+
+    /**
+     * 生成日期编码
+     * @param integer $size 编码长度
+     * @param string $prefix 编码前缀
+     * @return string
+     */
+    public static function uniqidDate(int $size = 16, string $prefix = ''): string
+    {
+        if ($size < 14) $size = 14;
+        $code = $prefix . date('Ymd') . (date('H') + date('i')) . date('s');
+        while (strlen($code) < $size) $code .= rand(0, 9);
+        return $code;
+    }
+
+    /**
+     * 生成数字编码
+     * @param integer $size 编码长度
+     * @param string $prefix 编码前缀
+     * @return string
+     */
+    public static function uniqidNumber(int $size = 12, string $prefix = ''): string
+    {
+        $time = strval(time());
+        if ($size < 10) $size = 10;
+        $code = $prefix . (intval($time[0]) + intval($time[1])) . substr($time, 2) . rand(0, 9);
+        while (strlen($code) < $size) $code .= rand(0, 9);
+        return $code;
+    }
+
+    /**
+     * 文本转码
+     * @param string $text 文本内容
+     * @param string $target 目标编码
+     * @return string
+     */
+    public static function text2utf8(string $text, string $target = 'UTF-8'): string
+    {
+        [$first2, $first4] = [substr($text, 0, 2), substr($text, 0, 4)];
+        if ($first4 === chr(0x00) . chr(0x00) . chr(0xFE) . chr(0xFF)) $ft = 'UTF-32BE';
+        elseif ($first4 === chr(0xFF) . chr(0xFE) . chr(0x00) . chr(0x00)) $ft = 'UTF-32LE';
+        elseif ($first2 === chr(0xFE) . chr(0xFF)) $ft = 'UTF-16BE';
+        elseif ($first2 === chr(0xFF) . chr(0xFE)) $ft = 'UTF-16LE';
+        return mb_convert_encoding($text, $target, $ft ?? mb_detect_encoding($text));
+    }
+
+    /**
+     * 数据解密处理
+     * @param mixed $data 加密数据
+     * @param string $skey 安全密钥
+     * @return string
+     */
+    public static function encrypt($data, string $skey): string
+    {
+        $iv = static::random(16, 3);
+        $value = openssl_encrypt(serialize($data), 'AES-256-CBC', $skey, 0, $iv);
+        return static::enSafe64(json_encode(['iv' => $iv, 'value' => $value]));
+    }
+
+    /**
+     * 数据加密处理
+     * @param string $data 解密数据
+     * @param string $skey 安全密钥
+     * @return mixed
+     */
+    public static function decrypt(string $data, string $skey)
+    {
+        $attr = json_decode(static::deSafe64($data), true);
+        return unserialize(openssl_decrypt($attr['value'], 'AES-256-CBC', $skey, 0, $attr['iv']));
+    }
+
+    /**
+     * Base64Url 安全编码
+     * @param string $text 待加密文本
+     * @return string
+     */
+    public static function enSafe64(string $text): string
+    {
+        return rtrim(strtr(base64_encode($text), '+/', '-_'), '=');
+    }
+
+    /**
+     * Base64Url 安全解码
+     * @param string $text 待解密文本
+     * @return string
+     */
+    public static function deSafe64(string $text): string
+    {
+        return base64_decode(str_pad(strtr($text, '-_', '+/'), strlen($text) % 4, '='));
+    }
+
+    /**
+     * 压缩数据对象
+     * @param mixed $data
+     * @return string
+     */
+    public static function enzip($data): string
+    {
+        return static::enSafe64(gzcompress(serialize($data)));
+    }
+
+    /**
+     * 解压数据对象
+     * @param string $string
+     * @return mixed
+     */
+    public static function dezip(string $string)
+    {
+        return unserialize(gzuncompress(static::deSafe64($string)));
+    }
+
+
+}

+ 72 - 0
app/extra/tools/DataExtend.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace app\extra\tools;
+
+class DataExtend
+{
+
+    /**
+     * 一维数组转多维数据树
+     * @param array $its 待处理数据
+     * @param string $cid 自己的主键
+     * @param string $pid 上级的主键
+     * @param string $sub 子数组名称
+     * @return array
+     */
+    public static function arr2tree(array $its, string $cid = 'id', string $pid = 'pid', string $sub = 'children'): array
+    {
+        [$tree, $its] = [[], array_column($its, null, $cid)];
+        foreach ($its as $it) isset($its[$it[$pid]]) ? $its[$it[$pid]][$sub][] = &$its[$it[$cid]] : $tree[] = &$its[$it[$cid]];
+        return $tree;
+    }
+
+    /**
+     * 一维数组转数据树表
+     * @param array $its 待处理数据
+     * @param string $cid 自己的主键
+     * @param string $pid 上级的主键
+     * @param string $path 当前 PATH
+     * @return array
+     */
+    public static function arr2table(array $its, string $cid = 'id', string $pid = 'pid', string $path = 'path'): array
+    {
+        $call = function (array $its, callable $call, array &$data = [], string $parent = '') use ($cid, $pid, $path) {
+            foreach ($its as $it) {
+                $ts = $it['sub'] ?? [];
+                unset($it['sub']);
+                $it[$path] = "{$parent}-{$it[$cid]}";
+                $it['spc'] = count($ts);
+                $it['spt'] = substr_count($parent, '-');
+                $it['spl'] = str_repeat('ㅤ├ㅤ', $it['spt']);
+                $it['sps'] = ",{$it[$cid]},";
+                array_walk_recursive($ts, function ($val, $key) use ($cid, &$it) {
+                    if ($key === $cid) $it['sps'] .= "{$val},";
+                });
+                $it['spp'] = arr2str(str2arr(strtr($parent . $it['sps'], '-', ',')));
+                $data[] = $it;
+                if (empty($ts)) continue;
+                $call($ts, $call, $data, $it[$path]);
+            }
+            return $data;
+        };
+        return $call(static::arr2tree($its, $cid, $pid), $call);
+    }
+
+    /**
+     * 获取数据树子ID集合
+     * @param array $list 数据列表
+     * @param mixed $value 起始有效ID值
+     * @param string $ckey 当前主键ID名称
+     * @param string $pkey 上级主键ID名称
+     * @return array
+     */
+    public static function getArrSubIds(array $list, $value = 0, string $ckey = 'id', string $pkey = 'pid'): array
+    {
+        $ids = [intval($value)];
+        foreach ($list as $vo) if (intval($vo[$pkey]) > 0 && intval($vo[$pkey]) === intval($value)) {
+            $ids = array_merge($ids, static::getArrSubIds($list, intval($vo[$ckey]), $ckey, $pkey));
+        }
+        return $ids;
+    }
+
+}

+ 80 - 0
app/extra/tools/OssRegionExtend.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace app\extra\tools;
+
+class OssRegionExtend
+{
+    /**
+     * 获取存储区域
+     * @return array
+     */
+    public static function OssRegion(): array
+    {
+        return [
+            'oss-cn-hangzhou.aliyuncs.com'       => '华东 1(杭州)',
+            'oss-cn-shanghai.aliyuncs.com'       => '华东 2(上海)',
+            'oss-cn-nanjing.aliyuncs.com'        => '华东 5(南京本地地域)',
+            'oss-cn-fuzhou.aliyuncs.com'         => '华东 6(福州本地地域)',
+            'oss-cn-qingdao.aliyuncs.com'        => '华北 1(青岛)',
+            'oss-cn-beijing.aliyuncs.com'        => '华北 2(北京)',
+            'oss-cn-zhangjiakou.aliyuncs.com'    => '华北 3(张家口)',
+            'oss-cn-huhehaote.aliyuncs.com'      => '华北 5(呼和浩特)',
+            'oss-cn-wulanchabu.aliyuncs.com'     => '华北 6(乌兰察布)',
+            'oss-cn-shenzhen.aliyuncs.com'       => '华南 1(深圳)',
+            'oss-cn-heyuan.aliyuncs.com'         => '华南 2(河源)',
+            'oss-cn-guangzhou.aliyuncs.com'      => '华南 3(广州)',
+            'oss-cn-chengdu.aliyuncs.com'        => '西南 1(成都)',
+            'oss-cn-hongkong.aliyuncs.com'       => '中国(香港)',
+            'oss-us-west-1.aliyuncs.com'         => '美国(硅谷)',
+            'oss-us-east-1.aliyuncs.com'         => '美国(弗吉尼亚)',
+            'oss-ap-northeast-1.aliyuncs.com'    => '日本(东京)',
+            'oss-ap-northeast-2.aliyuncs.com'    => '韩国(首尔)',
+            'oss-ap-southeast-1.aliyuncs.com'    => '新加坡',
+            'oss-ap-southeast-2.aliyuncs.com'    => '澳大利亚(悉尼)',
+            'oss-ap-southeast-3.aliyuncs.com'    => '马来西亚(吉隆坡)',
+            'oss-ap-southeast-5.aliyuncs.com'    => '印度尼西亚(雅加达)',
+            'oss-ap-southeast-6.aliyuncs.com'    => '菲律宾(马尼拉)',
+            'oss-ap-southeast-7.aliyuncs.com'    => '泰国(曼谷)',
+            'oss-ap-south-1.aliyuncs.com'        => '印度(孟买)',
+            'oss-eu-central-1.aliyuncs.com'      => '德国(法兰克福)',
+            'oss-eu-west-1.aliyuncs.com'         => '英国(伦敦)',
+            'oss-me-east-1.aliyuncs.com'         => '阿联酋(迪拜)',
+            'oss-rg-china-mainland.aliyuncs.com' => '无地域属性(中国内地)'
+        ];
+    }
+
+    /**
+     * 获取存储区域
+     * @return array
+     */
+    public static function CosRegion(): array
+    {
+        return [
+            'ap-guangzhou'                => '华南地区(广州)',
+            'up-cn-east-2.qiniup.com'      => '华东-浙江2',
+            'up-z1.qiniup.com'             => '华北-河北',
+            'up-z2.qiniup.com'             => '华南-广东',
+            'up-na0.qiniup.com'            => '北美-洛杉矶',
+            'up-as0.qiniup.com'            => '亚太-新加坡',
+            'up-ap-northeast-1.qiniup.com' => '亚太-首尔',
+        ];
+    }
+
+    /**
+     * 获取存储区域
+     * @return array
+     */
+    public static function QiniuRegion(): array
+    {
+        return [
+            'up.qiniup.com'                => '华东-浙江',
+            'up-cn-east-2.qiniup.com'      => '华东-浙江2',
+            'up-z1.qiniup.com'             => '华北-河北',
+            'up-z2.qiniup.com'             => '华南-广东',
+            'up-na0.qiniup.com'            => '北美-洛杉矶',
+            'up-as0.qiniup.com'            => '亚太-新加坡',
+            'up-ap-northeast-1.qiniup.com' => '亚太-首尔',
+        ];
+    }
+
+}

+ 123 - 0
app/extra/tools/UploadExtend.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace app\extra\tools;
+
+use app\service\system\UploadService;
+
+use Tinywan\Storage\Storage;
+
+/**
+ * @see Storage
+ * @mixin Storage
+ *
+ * @method static array uploadFile(array $config = [])  上传文件
+ * @method static array uploadBase64(string $base64, string $extension = 'png') 上传Base64文件
+ * @method static array uploadServerFile(string $file_path)  上传服务端文件
+ */
+
+class UploadExtend
+{
+    /**
+     * 本地对象存储.
+     */
+    public const MODE_LOCAL = 'local';
+
+    /**
+     * 阿里云对象存储.
+     */
+    public const MODE_OSS = 'oss';
+
+    /**
+     * 腾讯云对象存储.
+     */
+    public const MODE_COS = 'cos';
+
+    /**
+     * 七牛云对象存储.
+     */
+    public const MODE_QINIU = 'qiniu';
+
+    /**
+     * S3对象存储.
+     */
+    public const MODE_S3 = 's3';
+
+
+    protected array $config = [];
+
+    /**
+     * Support Storage
+     */
+    static $allowStorage = [
+        self::MODE_LOCAL,
+        self::MODE_OSS,
+        self::MODE_COS,
+        self::MODE_QINIU,
+        self::MODE_S3
+    ];
+
+    /**
+     * 存储磁盘
+     * @param string|null $name
+     * @param bool $_is_file_upload
+     * @return mixed
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function disk(string $name = null, bool $_is_file_upload = true)
+    {
+        $storage = $name ?? self::getDefaultStorage();
+        $config = self::getConfig($storage);
+        return new $config['adapter'](array_merge(
+            $config, ['_is_file_upload' => $_is_file_upload]
+        ));
+    }
+
+    /**
+     * 默认存储
+     * @return mixed
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function getDefaultStorage()
+    {
+        return self::getConfig('default');
+    }
+
+    /**
+     * 获取存储配置
+     * @param string|null $name 名称
+     * @return mixed
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function getConfig(string $name = null)
+    {
+        $config = (new UploadService)->setConfigVal($name);
+        if (!is_null($name)) {
+            return $config['storage'][$name];
+        }
+        return $config['storage']["default"];
+    }
+
+
+    /**
+     * 配置信息
+     * @param array $config
+     */
+    public function config(array $config)
+    {
+        $this->$config = $config;
+        return $this;
+    }
+
+    /**
+     * @param $name
+     * @param $arguments
+     * @return mixed
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function __callStatic($name, $arguments)
+    {
+        return static::disk()->{$name}(...$arguments);
+    }
+
+
+}

+ 57 - 0
app/functions.php

@@ -72,3 +72,60 @@ if(!function_exists('format_money')){
         return number_format($str, $len, ".", $append);
     }
 }
+
+if (!function_exists("clearAllBlank"))
+{
+
+    /**
+     * 全局替换所有空白为空
+     * @param string $str
+     * @return string
+     */
+    function clearAllBlank(string $str): string
+    {
+        return preg_replace('/[\s\x{3000}]+/u', '', $str);
+    }
+}
+if (!function_exists("splitTransAndNote")) {
+
+    /**
+     * 拆分译文和注释
+     * @param string $text 原始混合文本
+     * @return array ['trans' => 译文内容, 'note' => 注释内容]
+     */
+    function splitTransAndNote(string $text): array
+    {
+        // 正则分割:匹配【译文】【注释】边界(兼容有无中括号两种写法)
+        $pattern = '/(译文|注释)/';
+        $parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+        $trans = '';
+        $note = '';
+
+        // 遍历分割结果组装内容
+        $flag = '';
+        foreach ($parts as $item) {
+            if ($item === '译文') {
+                $flag = 'trans';
+            } elseif ($item === '注释') {
+                $flag = 'note';
+            } else {
+                $item = trim($item);
+                if ($flag === 'trans' && $item !== '') {
+                    $trans = $item;
+                } elseif ($flag === 'note' && $item !== '') {
+                    $note = $item;
+                }
+            }
+        }
+
+        // 清除多余空白、换行
+        $trans = preg_replace('/\s+/', '', $trans);
+        $note = preg_replace('/\s+/', PHP_EOL, $note); // 注释每条换行展示,可读性更好
+
+        return [
+            'trans' => $trans,
+            'note'  => $note
+        ];
+    }
+}

+ 40 - 0
app/middleware/AuthMiddleware.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace app\middleware;
+
+use Shopwwi\WebmanAuth\Auth;
+use Shopwwi\WebmanAuth\JWT;
+use Webman\Http\Request;
+use Webman\Http\Response;
+use Webman\MiddlewareInterface;
+
+class AuthMiddleware implements MiddlewareInterface
+{
+
+    public function process(Request $request, callable $handler): Response
+    {
+        try {
+            $controller = new \ReflectionClass($request->controller);
+            $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin']??[];
+            if (empty($noNeedLogin) || !in_array($request->action, $noNeedLogin)) {
+                $type = $request->header('api-type','');
+                if (empty($type)) return json(['code'=> 0,'msg'=> trans("error.param")]);
+                $token =  $request->header("Authorization","");
+                if (empty($token)) return json(['code'=> 0,'msg'=> trans("error.request")]);
+                $guard = "admin";
+                if ($type == 'mini') {
+                    $guard = "user";
+                }
+                (new JWT)->guard($guard)->verify();
+                $user = (new Auth)->guard($guard)->user();
+                if (empty($user)) return json(['code'=>401,'msg'=> trans("error.login")]);
+                $request->user = $user->toArray();
+            }
+        } catch (\Throwable $throwable) {
+            return json(['code'=> 401,'msg'=> $throwable->getMessage()]);
+        }
+        $response = $request->method() == 'OPTIONS' ? response('',204) : $handler($request);
+        return $response;
+    }
+
+}

+ 49 - 0
app/model/poem/PoemContent.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace app\model\poem;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poem_id 
+ * @property mixed $content 诗词内容
+ * @property string $trans 译文
+ * @property string $note 注释
+ * @property string $review 赏析
+ * @property string $background 写作背景
+ * @property integer $is_done
+ */
+class PoemContent extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "poem_content";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 48 - 0
app/model/system/SystemConfig.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property string $type 配置分类
+ * @property string $name 配置名称
+ * @property string $value 配置内容
+ * @property string $desc 字段名称
+ * @property integer $status 
+ * @property mixed $update_at 更新时间
+ */
+class SystemConfig extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_config";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 45 - 0
app/model/system/SystemData.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $name 
+ * @property mixed $content 
+ * @property integer $is_default 是否使用
+ */
+class SystemData extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_data";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 53 - 0
app/model/system/SystemMenu.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $from 1管理后台2代理
+ * @property integer $pid 
+ * @property string $name 
+ * @property string $path 
+ * @property string $title 
+ * @property string $type 
+ * @property string $icon 
+ * @property integer $hide 
+ * @property integer $status 
+ * @property integer $sort 
+ * @property mixed $create_at
+ */
+class SystemMenu extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_menu";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 59 - 0
app/model/system/SystemUser.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $goup_id 
+ * @property string $username 用户名
+ * @property string $truename 真实姓名
+ * @property string $password 密码
+ * @property mixed $salt 密钥串
+ * @property string $contact_name 
+ * @property string $contact_mobile 
+ * @property integer $status 状态
+ * @property integer $is_deleted 删除状态
+ * @property integer $is_super 
+ * @property string $remark 备注
+ * @property string $login_ip 登录IP
+ * @property mixed $login_at 登录时间
+ * @property integer $login_num 
+ * @property string $create_ip 
+ * @property mixed $updated_at 更新时间
+ * @property mixed $create_at 创建时间
+ */
+class SystemUser extends Model
+{
+    /**
+     * The connection name for the model.
+     *
+     * @var string|null
+     */
+    protected $connection = 'mysql';
+    
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected string $table = "system_user";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string $primaryKey = "id";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool $timestamps = false;
+
+
+}

+ 2 - 1
composer.json

@@ -47,7 +47,8 @@
     "symfony/translation": "^7.4",
     "php-di/php-di": "^7.0",
     "jaeger/querylist": "^4.4",
-    "overtrue/pinyin": "^6.0"
+    "overtrue/pinyin": "^6.0",
+    "tinywan/captcha": "^0.0.4"
   },
   "suggest": {
     "ext-event": "For better performance. "

+ 49 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "d0ea14f19b87b3f732c3ee59e1af7881",
+    "content-hash": "b3238d168bfc27da402f6e11a158d6c0",
     "packages": [
         {
             "name": "aliyuncs/oss-sdk-php",
@@ -4479,6 +4479,54 @@
             },
             "time": "2026-06-27T08:41:53+00:00"
         },
+        {
+            "name": "tinywan/captcha",
+            "version": "v0.0.4",
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.cloud.tencent.com/repository/composer/tinywan/captcha/v0.0.4/tinywan-captcha-v0.0.4.zip",
+                "reference": "5d24139730f2adcce162366f69d207d39fa89558",
+                "shasum": ""
+            },
+            "require": {
+                "ext-gd": "*",
+                "ext-mbstring": "*",
+                "ext-redis": "*",
+                "php": ">=7.2"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^3.6",
+                "phpstan/phpstan": "^1.4",
+                "workerman/webman": "^1.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Tinywan\\Captcha\\": "src"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Tinywan",
+                    "email": "756684177@qq.com"
+                }
+            ],
+            "description": "base64 image verification captcha library for webman plugin",
+            "keywords": [
+                "base64",
+                "captcha",
+                "plugin",
+                "webman"
+            ],
+            "support": {
+                "issues": "https://github.com/Tinywan/webman-captcha/issues",
+                "source": "https://github.com/Tinywan/webman-captcha/tree/v0.0.4"
+            },
+            "time": "2022-08-08T13:52:15+00:00"
+        },
         {
             "name": "tinywan/storage",
             "version": "v1.1.2",

Fichier diff supprimé car celui-ci est trop grand
+ 11 - 0
config/plugin/tinywan/captcha/app.php


+ 3 - 0
resource/translations/en/messages.php

@@ -0,0 +1,3 @@
+<?php
+
+return [];

+ 46 - 0
resource/translations/zh_CN/messages.php

@@ -0,0 +1,46 @@
+<?php
+
+
+return [
+    "empty" => [
+        "mobile"    => "请输入手机号码",
+        "user"      => "请输入用户名",
+        "passwd"    => "请输入登陆密码",
+        "code"      => "请输入验证码",
+        "data"      => "数据不存在",
+        "require"   => "参数不能为空",
+        "agent"     => "未绑定代理信息"
+    ],
+    "error"     => [
+        "exist"         => "数据已存在",
+        "data"          => "操作失败",
+        "status-qrcode" => "请先开启后再生成二维码",
+        "captcha"       => "验证码输入错误",
+        "mobile"        => "手机号码格式错误",
+        "idcard"        => "身份证号码格式错误",
+        "email"         => "邮箱格式错误",
+        "sms-repeat"    => "请勿重复获取",
+        "sms"           => "获取验证码失败,请联系管理员",
+        "mobile-empty"  => "手机号未注册,请开通后再重试",
+        "mobile-exist"  => "手机号已被注册",
+        "user-empty"    => "登录账号不存在",
+        "user-exist"    => "登录账号已存在,请更换",
+        "user-status"   => "该账号已被冻结,请联系管理员",
+        "passwd"        => "登录密码错误",
+        "param"         => "不合法的参数",
+        "request"       => "不合法的请求格式",
+        "login"         => "登录已过期,请重新登录",
+        "sms-err"       => "短信验证码错误",
+        "agent"         => "代理状态异常,请联系管理员",
+        "agent-out"     => "权限已到期,请联系管理员",
+        "agent-no-exist"   => "代理不存在",
+        "store-no-exist"   => "门店不存在",
+    ],
+    "success"   => [
+        "data"  => "操作成功",
+        "sms"   => "验证码已成功发送至%mobile%,请注意查收",
+        "login" => "登录成功",
+        "print" => "已成功发起打印测试,请留意打印机是否正常工作",
+        "print-state" => "已成功发起状态查询任务,请等待1-3分钟再刷新数据"
+    ],
+];

+ 1 - 0
vendor/composer/autoload_psr4.php

@@ -28,6 +28,7 @@ return array(
     'Webman\\Console\\' => array($vendorDir . '/webman/console/src'),
     'Webman\\' => array($vendorDir . '/workerman/webman-framework/src'),
     'Tinywan\\Storage\\' => array($vendorDir . '/tinywan/storage/src'),
+    'Tinywan\\Captcha\\' => array($vendorDir . '/tinywan/captcha/src'),
     'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'),
     'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
     'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),

+ 5 - 0
vendor/composer/autoload_static.php

@@ -88,6 +88,7 @@ class ComposerStaticInit691f538563ac6695008ddc51b7722c80
         'T' => 
         array (
             'Tinywan\\Storage\\' => 16,
+            'Tinywan\\Captcha\\' => 16,
         ),
         'S' => 
         array (
@@ -308,6 +309,10 @@ class ComposerStaticInit691f538563ac6695008ddc51b7722c80
         array (
             0 => __DIR__ . '/..' . '/tinywan/storage/src',
         ),
+        'Tinywan\\Captcha\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/tinywan/captcha/src',
+        ),
         'Symfony\\Polyfill\\Php85\\' => 
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php85',

+ 51 - 0
vendor/composer/installed.json

@@ -4710,6 +4710,57 @@
             },
             "install-path": "../symfony/var-exporter"
         },
+        {
+            "name": "tinywan/captcha",
+            "version": "v0.0.4",
+            "version_normalized": "0.0.4.0",
+            "dist": {
+                "type": "zip",
+                "url": "https://mirrors.cloud.tencent.com/repository/composer/tinywan/captcha/v0.0.4/tinywan-captcha-v0.0.4.zip",
+                "reference": "5d24139730f2adcce162366f69d207d39fa89558",
+                "shasum": ""
+            },
+            "require": {
+                "ext-gd": "*",
+                "ext-mbstring": "*",
+                "ext-redis": "*",
+                "php": ">=7.2"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^3.6",
+                "phpstan/phpstan": "^1.4",
+                "workerman/webman": "^1.0"
+            },
+            "time": "2022-08-08T13:52:15+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Tinywan\\Captcha\\": "src"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Tinywan",
+                    "email": "756684177@qq.com"
+                }
+            ],
+            "description": "base64 image verification captcha library for webman plugin",
+            "keywords": [
+                "base64",
+                "captcha",
+                "plugin",
+                "webman"
+            ],
+            "support": {
+                "issues": "https://github.com/Tinywan/webman-captcha/issues",
+                "source": "https://github.com/Tinywan/webman-captcha/tree/v0.0.4"
+            },
+            "install-path": "../tinywan/captcha"
+        },
         {
             "name": "tinywan/storage",
             "version": "v1.1.2",

+ 15 - 6
vendor/composer/installed.php

@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'workerman/webman',
-        'pretty_version' => '1.0.0+no-version-set',
-        'version' => '1.0.0.0',
-        'reference' => null,
+        'pretty_version' => 'dev-main',
+        'version' => 'dev-main',
+        'reference' => 'dcdfcf7578d804fd932511fb4c17588293ef9dbc',
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -789,6 +789,15 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'tinywan/captcha' => array(
+            'pretty_version' => 'v0.0.4',
+            'version' => '0.0.4.0',
+            'reference' => '5d24139730f2adcce162366f69d207d39fa89558',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../tinywan/captcha',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'tinywan/storage' => array(
             'pretty_version' => 'v1.1.2',
             'version' => '1.1.2.0',
@@ -952,9 +961,9 @@
             'dev_requirement' => false,
         ),
         'workerman/webman' => array(
-            'pretty_version' => '1.0.0+no-version-set',
-            'version' => '1.0.0.0',
-            'reference' => null,
+            'pretty_version' => 'dev-main',
+            'version' => 'dev-main',
+            'reference' => 'dcdfcf7578d804fd932511fb4c17588293ef9dbc',
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

+ 8 - 0
vendor/tinywan/captcha/.gitignore

@@ -0,0 +1,8 @@
+vendor/
+*.log
+.DS_Store
+vendor
+.idea
+.vscode
+.phpunit*
+composer.lock

+ 31 - 0
vendor/tinywan/captcha/.php-cs-fixer.php

@@ -0,0 +1,31 @@
+<?php
+
+$finder = PhpCsFixer\Finder::create()
+    ->exclude('tests')
+    ->exclude('vendor')
+    ->in(__DIR__);
+
+return (new PhpCsFixer\Config())
+    ->setUsingCache(false)
+    ->setRules([
+        'combine_consecutive_unsets' => true,   //多个unset,合并成一个
+        'class_attributes_separation' => true,
+        'heredoc_to_nowdoc'                     => true,     //删除配置中多余的空行和/或者空行。
+        'no_unreachable_default_argument_value' => false, //在函数参数中,不能有默认值在非缺省值之前的参数。有风险
+        'no_useless_else'                       => true,  //删除无用的else
+        'no_useless_return'                     => true,  //删除函数末尾无用的return
+        'no_empty_phpdoc'                       => true,  // 删除空注释
+        'no_empty_statement'                    => true,  //删除多余的分号
+        'no_leading_namespace_whitespace'       => true,  //删除namespace声明行包含前导空格
+        'no_spaces_inside_parenthesis'          => true,  //删除括号后内两端的空格
+        'no_trailing_whitespace'                => true,  //删除非空白行末尾的空白
+        'no_unused_imports'                     => true,  //删除未使用的use语句
+        'no_whitespace_before_comma_in_array'   => true,  //删除数组声明中,每个逗号前的空格
+        'no_whitespace_in_blank_line'           => true,  //删除空白行末尾的空白
+        'ordered_class_elements' => true,
+        'ordered_imports' => ['sort_algorithm' => 'alpha'],
+        'line_ending' => true,
+        'single_quote' => true,
+        'array_syntax' => ['syntax' => 'short'],
+    ])
+    ->setFinder($finder);

+ 21 - 0
vendor/tinywan/captcha/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022  ShaoBo Wan(無尘)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 51 - 0
vendor/tinywan/captcha/README.md

@@ -0,0 +1,51 @@
+# webman captcha plugin
+
+[![Latest Stable Version](http://poser.pugx.org/tinywan/captcha/v)](https://packagist.org/packages/tinywan/captcha) 
+[![Total Downloads](http://poser.pugx.org/tinywan/captcha/downloads)](https://packagist.org/packages/tinywan/captcha) 
+[![Daily Downloads](http://poser.pugx.org/tinywan/captcha/d/daily)](https://packagist.org/packages/tinywan/captcha)
+[![Latest Unstable Version](http://poser.pugx.org/tinywan/captcha/v/unstable)](https://packagist.org/packages/tinywan/captcha) 
+[![License](http://poser.pugx.org/tinywan/captcha/license)](https://packagist.org/packages/tinywan/captcha)
+[![PHP Version Require](http://poser.pugx.org/tinywan/captcha/require/php)](https://packagist.org/packages/tinywan/captcha)
+
+base64 image verification captcha library for webman plugin
+
+## 安装
+
+```shell
+composer require tinywan/captcha
+```
+
+## 使用
+
+### 1、生成base64
+
+```php
+var_export(Tinywan\Captcha\Captcha::base64());  // 就这么简单
+```
+
+**以上输出base64**
+```php
+array (
+  'key' => '$2y$10$QCLukWC.DMmPuMArKEJNaeBaSeBnzFOM3nv8bL4jBJFJBIe.8YngC',
+  'base64' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPoAAAA+BAMAAADwuxusAAAAG1BMVEXz+/5Bd1vc6umauaxtmIODqJiwycBXh2/G2tUJ+xU0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACo0lEQVRYhe1Xz7OTMBCmSaE5uj76eEfQ6dRjcUbH44tv/HFs1T8AxovHph70WMbq8882hBKSQKXEPrnkO0CSDfl2s5vd4HkODg4ODg4ODg6ngF6NyZ7fjMmePh6TfTMOO6Frwf7IHH//EGxMGIkhO/YRuy5fdGvMsw6EJ38TpoJsBnKgiOJSqdvLsD+lED4/LU5CQTmXAxhuO9mvbcgxlPigD/6OZXMKZXujWEav+APWxjqFDTviln/8BNKt1SBEcjcCWPEnU9y8hD2fsZJ9srJmX8I89rwFzNVBBIo6JTFRyHhny2c06m6Eb5IrC3bKDeG4F85s6H826pQn2xfbXyOdcw0a9qXwgg07hqNDaagLFtKxBV92okmnkJFK6UpVVhJPtN07D0ltw1Td2xIyzmaRGdCI7QJ1M4ryWxt2Wn+DmJE6Z7W5mJtJ9cxWhAEoXQJcudlwdgK7unnY6yK/Xh/BGhnny4fPKjs3Pmu0PR++ud+qKD626DZQvFwNfY3ULuHRMx3OPtNi2RDVrfxmGhnCBHSuHPYW7BOxLN53iHK52mTeOk2Bnh94f+ebGvYjEYvkHUc1AJlb/ShvFfONEWNptLBmh9emAG2aiCDQKimt3ILhbjh7lUV4rs907h8UlBPODLHXkdXTCMw5vagKGNe8CZlnv16842k+jJWV2wu3qrkPw9nxcX9l0sFMFNzoLj75TaVQOxLeDmb3mLAhgLqA5gDf3rz80sPdcbHyyHByLxfXpERmk/t2AHaBtC4XVsDw3TveVgQQFer0IT+dpQYh5Z7HSnH3of+OgugZk84CYVFWqJakp1O/BIT7y7Dza0TEVEsI60/YyqXzX3EA/Va1bKru/8DBCDRq1tOHhfEvgy8VU3YY998cZWOyOzg4ONjjD4tjUV+t8cERAAAAAElFTkSuQmCC',
+)
+```
+> 通过网址:https://tool.jisuapi.com/base642pic.html BASE64转图片
+
+**转图片效果如图**
+
+![captcha.png](captcha.png)
+
+### 2、验证验证码
+
+```php
+use Tinywan\Captcha\Captcha;
+use support\Request;
+
+$code = $request->get('code');
+$key = $request->get('key');
+if (false === Tinywan\Captcha\Captcha::check($code, $key)) {
+	// 验证失败
+};
+// 验证通过
+```

BIN
vendor/tinywan/captcha/assets/bgs/1.jpg


BIN
vendor/tinywan/captcha/assets/bgs/2.jpg


BIN
vendor/tinywan/captcha/assets/bgs/3.jpg


BIN
vendor/tinywan/captcha/assets/bgs/4.jpg


BIN
vendor/tinywan/captcha/assets/bgs/5.jpg


BIN
vendor/tinywan/captcha/assets/bgs/6.jpg


BIN
vendor/tinywan/captcha/assets/bgs/7.jpg


BIN
vendor/tinywan/captcha/assets/bgs/8.jpg


BIN
vendor/tinywan/captcha/assets/ttfs/1.ttf


BIN
vendor/tinywan/captcha/assets/ttfs/2.ttf


BIN
vendor/tinywan/captcha/assets/ttfs/3.ttf


BIN
vendor/tinywan/captcha/assets/ttfs/4.ttf


BIN
vendor/tinywan/captcha/assets/ttfs/5.ttf


BIN
vendor/tinywan/captcha/assets/ttfs/6.ttf


BIN
vendor/tinywan/captcha/assets/zhttfs/1.otf


BIN
vendor/tinywan/captcha/captcha.png


+ 29 - 0
vendor/tinywan/captcha/composer.json

@@ -0,0 +1,29 @@
+{
+  "name": "tinywan/captcha",
+  "description": "base64 image verification captcha library for webman plugin",
+  "keywords": ["base64", "captcha", "webman", "plugin"],
+  "type": "library",
+  "license": "MIT",
+  "authors": [
+    {
+      "name": "Tinywan",
+      "email": "756684177@qq.com"
+    }
+  ],
+  "require": {
+    "php": ">=7.2",
+    "ext-redis": "*",
+    "ext-gd": "*",
+    "ext-mbstring": "*"
+  },
+  "autoload": {
+    "psr-4": {
+      "Tinywan\\Captcha\\": "src"
+    }
+  },
+  "require-dev": {
+    "workerman/webman": "^1.0",
+    "friendsofphp/php-cs-fixer": "^3.6",
+    "phpstan/phpstan": "^1.4"
+  }
+}

+ 250 - 0
vendor/tinywan/captcha/src/Captcha.php

@@ -0,0 +1,250 @@
+<?php
+/**
+ * @desc Captcha.php 描述信息
+ * @author Tinywan(ShaoBo Wan)
+ * @date 2022/3/24 21:36
+ */
+declare(strict_types=1);
+
+namespace Tinywan\Captcha;
+
+use support\Redis;
+
+class Captcha
+{
+    /**
+     * @desc: 验证验证码是否正确
+     * @param string $code 用户验证码
+     * @param string $key 用户验证码key
+     * @return bool
+     * @author Tinywan(ShaoBo Wan)
+     */
+    public static function check(string $code, string $key): bool
+    {
+        $config = config('plugin.tinywan.captcha.app.captcha');
+        $cacheKey = $config['prefix'] . $key;
+        if (!Redis::exists($cacheKey)) {
+            return false;
+        }
+        $hash = Redis::hGet($cacheKey, 'key');
+        $code = mb_strtolower($code, 'UTF-8');
+        $res = password_verify($code, $hash);
+        if ($res) {
+            Redis::del($cacheKey);
+        }
+        return $res;
+    }
+
+    /**
+     * 输出验证码并把验证码的值保存的session中
+     * @access public
+     * @param array $_config
+     * @return array
+     * @throws \Exception
+     */
+    public static function base64(array $_config = [])
+    {
+        $config = config('plugin.tinywan.captcha.app.captcha');
+        if (!empty($_config)) {
+            $config = array_merge($config, $_config);
+        }
+        $generator = self::generate($config);
+        // 图片宽(px)
+        $config['imageW'] || $config['imageW'] = $config['length'] * $config['fontSize'] * 1.5 + $config['length'] * $config['fontSize'] / 2;
+        // 图片高(px)
+        $config['imageH'] || $config['imageH'] = $config['fontSize'] * 2.5;
+        // 建立一幅 $config['imageW'] x $config['imageH'] 的图像
+        $im = imagecreate((int)$config['imageW'], (int) $config['imageH']);
+        // 设置背景
+        imagecolorallocate($im, $config['bg'][0], $config['bg'][1], $config['bg'][2]);
+
+        // 验证码字体随机颜色
+        $color = imagecolorallocate($im, mt_rand(1, 150), mt_rand(1, 150), mt_rand(1, 150));
+
+        // 验证码使用随机字体
+        $ttfPath = __DIR__ . '/../assets/' . ($config['useZh'] ? 'zhttfs' : 'ttfs') . '/';
+
+        if (empty($config['fontttf'])) {
+            $dir  = dir($ttfPath);
+            $ttfs = [];
+            while (false !== ($file = $dir->read())) {
+                if (substr($file, -4) == '.ttf' || substr($file, -4) == '.otf') {
+                    $ttfs[] = $file;
+                }
+            }
+            $dir->close();
+            $config['fontttf'] = $ttfs[array_rand($ttfs)];
+        }
+
+        $fontttf = $ttfPath . $config['fontttf'];
+
+        if ($config['useImgBg']) {
+            self::background($config, $im);
+        }
+
+        if ($config['useNoise']) {
+            // 绘杂点
+            self::writeNoise($config, $im);
+        }
+        if ($config['useCurve']) {
+            // 绘干扰线
+            self::writeCurve($config, $im, $color);
+        }
+
+        // 绘验证码
+        $text = $config['useZh'] ? preg_split('/(?<!^)(?!$)/u', $generator['value']) : str_split($generator['value']); // 验证码
+
+        foreach ($text as $index => $char) {
+            $x     = $config['fontSize'] * ($index + 1) * mt_rand((int)1.2, (int)1.6) * ($config['math'] ? 1 : 1.5);
+            $y     = $config['fontSize'] + mt_rand(10, 20);
+            $angle = $config['math'] ? 0 : mt_rand(-40, 40);
+            imagettftext($im, $config['fontSize'], $angle, (int) $x, (int) $y, $color, $fontttf, $char);
+        }
+
+        ob_start();
+        imagepng($im);
+        $content = ob_get_clean();
+        imagedestroy($im);
+
+        return [
+            'key' => $generator['key'],
+            'base64' => 'data:image/png;base64,' . base64_encode($content),
+        ];
+    }
+
+    /**
+     * @desc: 创建验证码
+     * @param array $config
+     * @return array
+     * @throws \Exception
+     * @author Tinywan(ShaoBo Wan)
+     */
+    protected static function generate(array $config): array
+    {
+        $bag = '';
+        if ($config['math']) {
+            $config['useZh']  = false;
+            $config['length'] = 5;
+            $x   = random_int(10, 30);
+            $y   = random_int(1, 9);
+            $bag = "{$x} + {$y} = ";
+            $key = $x + $y;
+            $key .= '';
+        } else {
+            if ($config['useZh']) {
+                $characters = preg_split('/(?<!^)(?!$)/u', $config['zhSet']);
+            } else {
+                $characters = str_split($config['codeSet']);
+            }
+
+            for ($i = 0; $i < $config['length']; $i++) {
+                $bag .= $characters[rand(0, count($characters) - 1)];
+            }
+
+            $key = mb_strtolower($bag, 'UTF-8');
+        }
+
+        $config = config('plugin.tinywan.captcha.app.captcha');
+        $hash = password_hash($key, PASSWORD_BCRYPT, ['cost' => 10]);
+        Redis::multi();
+        Redis::hMSet($config['prefix'] . $hash, ['key' => $hash]);
+        Redis::expire($config['prefix'] . $hash, $config['expire'] ?? 60);
+        Redis::exec();
+        return ['value' => $bag, 'key' => $hash];
+    }
+
+    /**
+     * @desc: 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
+     * @param array $config
+     */
+    protected static function writeCurve(array $config, $im, $color): void
+    {
+        $py = 0;
+        // 曲线前部分
+        $A = mt_rand(1, (int) ($config['imageH'] / 2)); // 振幅
+        $b = mt_rand(-(int) ($config['imageH'] / 4), (int) ($config['imageH'] / 4)); // Y轴方向偏移量
+        $f = mt_rand(-(int) ($config['imageH'] / 4), (int) ($config['imageH'] / 4)); // X轴方向偏移量
+        $T = mt_rand((int) $config['imageH'], (int) ($config['imageW'] * 2)); // 周期
+        $w = (2 * M_PI) / $T;
+
+        $px1 = 0; // 曲线横坐标起始位置
+        $px2 = mt_rand((int) ($config['imageW'] / 2), (int)$config['imageW']); // 曲线横坐标结束位置
+
+        for ($px = $px1; $px <= $px2; $px = $px + 1) {
+            if (0 != $w) {
+                $py = $A * sin($w * $px + $f) + $b + $config['imageH'] / 2; // y = Asin(ωx+φ) + b
+                $i  = (int) ($config['fontSize'] / 5);
+                while ($i > 0) {
+                    imagesetpixel($im, (int) $px + $i, (int) $py + $i, (int)$color); // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出(不用这while循环)性能要好很多
+                    $i--;
+                }
+            }
+        }
+
+        // 曲线后部分
+        $A   = mt_rand(1, (int) ($config['imageH'] / 2)); // 振幅
+        $f   = mt_rand(-(int) ($config['imageH'] / 4), (int) ($config['imageH'] / 4)); // X轴方向偏移量
+        $T   = mt_rand((int) $config['imageH'], (int) ($config['imageW'] * 2)); // 周期
+        $w   = (2 * M_PI) / $T;
+        $b   = $py - $A * sin($w * $px + $f) - $config['imageH'] / 2;
+        $px1 = $px2;
+        $px2 = $config['imageW'];
+
+        for ($px = $px1; $px <= $px2; $px = $px + 1) {
+            if (0 != $w) {
+                $py = $A * sin($w * $px + $f) + $b + $config['imageH'] / 2; // y = Asin(ωx+φ) + b
+                $i  = (int) ($config['fontSize'] / 5);
+                while ($i > 0) {
+                    imagesetpixel($im, (int) $px + $i, (int) $py + $i, (int)$color);
+                    $i--;
+                }
+            }
+        }
+    }
+
+    /**
+     * @desc: 画杂点  往图片上写不同颜色的字母或数字
+     * @param array $config
+     * @param $im
+     * @author Tinywan(ShaoBo Wan)
+     */
+    protected static function writeNoise(array $config, $im): void
+    {
+        $codeSet = 'tinywan20222345678abcdefhijkmnpqrstuvwxyz';
+        for ($i = 0; $i < 10; $i++) {
+            //杂点颜色
+            $noiseColor = imagecolorallocate($im, mt_rand(150, 225), mt_rand(150, 225), mt_rand(150, 225));
+            for ($j = 0; $j < 5; $j++) {
+                // 绘杂点
+                imagestring($im, 5, mt_rand(-10, (int) $config['imageW']), mt_rand(-10, (int) $config['imageH']), $codeSet[mt_rand(0, 29)], (int) $noiseColor);
+            }
+        }
+    }
+
+    /**
+     * @desc: 绘制背景图片 注:如果验证码输出图片比较大,将占用比较多的系统资源
+     * @param array $config
+     * @param $im
+     * @author Tinywan(ShaoBo Wan)
+     */
+    protected static function background(array $config, $im): void
+    {
+        $path = __DIR__ . '/../assets/bgs/';
+        $dir  = dir($path);
+
+        $bgs = [];
+        while (false !== ($file = $dir->read())) {
+            if ('.' != $file[0] && substr($file, -4) == '.jpg') {
+                $bgs[] = $path . $file;
+            }
+        }
+        $dir->close();
+
+        $gb = $bgs[array_rand($bgs)];
+
+        [$width, $height] = @getimagesize($gb);
+        $bgImage = @imagecreatefromjpeg($gb);
+        @imagecopyresampled($im, $bgImage, 0, 0, 0, 0, (int) $config['imageW'], (int) $config['imageH'], $width, $height);
+        @imagedestroy($bgImage);
+    }
+}

+ 16 - 0
vendor/tinywan/captcha/src/Exception/CaptchaException.php

@@ -0,0 +1,16 @@
+<?php
+/**
+ * @desc CaptchaException.php 描述信息
+ * @author Tinywan(ShaoBo Wan)
+ * @date 2022/3/24 21:51
+ */
+declare(strict_types=1);
+
+
+namespace Tinywan\Captcha\Exception;
+
+
+class CaptchaException extends \RuntimeException
+{
+
+}

+ 73 - 0
vendor/tinywan/captcha/src/Install.php

@@ -0,0 +1,73 @@
+<?php
+namespace Tinywan\Captcha;
+
+class Install
+{
+    const WEBMAN_PLUGIN = true;
+
+    /**
+     * @var array
+     */
+    protected static $pathRelation =  [
+  'config/plugin/tinywan/captcha' => 'config/plugin/tinywan/captcha',
+];
+
+    /**
+     * Install
+     * @return void
+     */
+    public static function install()
+    {
+        static::installByRelation();
+    }
+
+    /**
+     * Uninstall
+     * @return void
+     */
+    public static function uninstall()
+    {
+        self::uninstallByRelation();
+    }
+
+    /**
+     * installByRelation
+     * @return void
+     */
+    public static function installByRelation()
+    {
+        foreach (static::$pathRelation as $source => $dest) {
+            if ($pos = strrpos($dest, '/')) {
+                $parent_dir = base_path().'/'.substr($dest, 0, $pos);
+                if (!is_dir($parent_dir)) {
+                    mkdir($parent_dir, 0777, true);
+                }
+            }
+            //symlink(__DIR__ . "/$source", base_path()."/$dest");
+            copy_dir(__DIR__ . "/$source", base_path()."/$dest");
+            echo "Create $dest
+";
+        }
+    }
+
+    /**
+     * uninstallByRelation
+     * @return void
+     */
+    public static function uninstallByRelation()
+    {
+        foreach (static::$pathRelation as $source => $dest) {
+            $path = base_path()."/$dest";
+            if (!is_dir($path) && !is_file($path)) {
+                continue;
+            }
+            echo "Remove $dest
+";
+            if (is_file($path) || is_link($path)) {
+                unlink($path);
+                continue;
+            }
+            remove_dir($path);
+        }
+    }
+}

Fichier diff supprimé car celui-ci est trop grand
+ 11 - 0
vendor/tinywan/captcha/src/config/plugin/tinywan/captcha/app.php


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff