Zory 1 tydzień temu
commit
2634991b1e
100 zmienionych plików z 10049 dodań i 0 usunięć
  1. 20 0
      .gitignore
  2. 18 0
      Dockerfile
  3. 21 0
      LICENSE
  4. 45 0
      README.md
  5. 133 0
      app/command/Model.php
  6. 24 0
      app/command/OrderDelay.php
  7. 48 0
      app/controller/IndexController.php
  8. 116 0
      app/controller/admin/Category.php
  9. 84 0
      app/controller/admin/Complaint.php
  10. 71 0
      app/controller/admin/Config.php
  11. 84 0
      app/controller/admin/Deposit.php
  12. 136 0
      app/controller/admin/Finance.php
  13. 107 0
      app/controller/admin/Goods.php
  14. 54 0
      app/controller/admin/Order.php
  15. 274 0
      app/controller/admin/Plan.php
  16. 209 0
      app/controller/admin/Prints.php
  17. 155 0
      app/controller/admin/Star.php
  18. 283 0
      app/controller/admin/Store.php
  19. 67 0
      app/controller/admin/StoreCategory.php
  20. 118 0
      app/controller/admin/StoreUser.php
  21. 53 0
      app/controller/admin/Upload.php
  22. 196 0
      app/controller/api/Auth.php
  23. 66 0
      app/controller/api/Complaint.php
  24. 94 0
      app/controller/api/Goods.php
  25. 174 0
      app/controller/api/Home.php
  26. 127 0
      app/controller/api/Notify.php
  27. 336 0
      app/controller/api/Order.php
  28. 333 0
      app/controller/api/Service.php
  29. 160 0
      app/controller/api/Solution.php
  30. 37 0
      app/controller/api/User.php
  31. 138 0
      app/controller/common/Login.php
  32. 76 0
      app/controller/common/Menu.php
  33. 67 0
      app/controller/merchant/Category.php
  34. 85 0
      app/controller/merchant/Complaint.php
  35. 52 0
      app/controller/merchant/Deposit.php
  36. 20 0
      app/controller/merchant/Finance.php
  37. 287 0
      app/controller/merchant/Goods.php
  38. 151 0
      app/controller/merchant/Order.php
  39. 36 0
      app/controller/merchant/OrderPrice.php
  40. 282 0
      app/controller/merchant/Plan.php
  41. 157 0
      app/controller/merchant/Star.php
  42. 80 0
      app/controller/merchant/User.php
  43. 90 0
      app/controller/notify/Life.php
  44. 37 0
      app/controller/service/Heart.php
  45. 60 0
      app/controller/service/Quick.php
  46. 223 0
      app/controller/service/Service.php
  47. 208 0
      app/extra/basic/Base.php
  48. 29 0
      app/extra/basic/Model.php
  49. 136 0
      app/extra/basic/Service.php
  50. 113 0
      app/extra/dyLife/BasicLife.php
  51. 66 0
      app/extra/dyLife/Crypt.php
  52. 82 0
      app/extra/dyLife/SignUtil.php
  53. 39 0
      app/extra/dyLife/Token.php
  54. 301 0
      app/extra/dyLife/data/BaseData.php
  55. 68 0
      app/extra/dyLife/data/OrderData.php
  56. 114 0
      app/extra/dyMini/Pay.php
  57. 82 0
      app/extra/dyMini/PlanLive.php
  58. 27 0
      app/extra/dyMini/Qrcode.php
  59. 73 0
      app/extra/dyMini/Relation.php
  60. 137 0
      app/extra/tools/CodeExtend.php
  61. 72 0
      app/extra/tools/DataExtend.php
  62. 80 0
      app/extra/tools/OssRegionExtend.php
  63. 133 0
      app/extra/tools/PrintExtend.php
  64. 123 0
      app/extra/tools/UploadExtend.php
  65. 796 0
      app/functions.php
  66. 40 0
      app/middleware/AuthMiddleware.php
  67. 42 0
      app/middleware/StaticFile.php
  68. 29 0
      app/model/Test.php
  69. 45 0
      app/model/saas/SaasBankCompany.php
  70. 49 0
      app/model/saas/SaasCategory.php
  71. 59 0
      app/model/saas/SaasChatMsg.php
  72. 43 0
      app/model/saas/SaasChatSet.php
  73. 70 0
      app/model/saas/SaasChatStore.php
  74. 60 0
      app/model/saas/SaasComplaint.php
  75. 53 0
      app/model/saas/SaasDepositLog.php
  76. 102 0
      app/model/saas/SaasGoods.php
  77. 73 0
      app/model/saas/SaasGoodsSku.php
  78. 62 0
      app/model/saas/SaasLivePlan.php
  79. 49 0
      app/model/saas/SaasLivePlanGoods.php
  80. 49 0
      app/model/saas/SaasLivePlanStar.php
  81. 73 0
      app/model/saas/SaasOrder.php
  82. 50 0
      app/model/saas/SaasOrderAddress.php
  83. 44 0
      app/model/saas/SaasOrderDelay.php
  84. 55 0
      app/model/saas/SaasOrderPrice.php
  85. 47 0
      app/model/saas/SaasOrderVerify.php
  86. 54 0
      app/model/saas/SaasPrint.php
  87. 46 0
      app/model/saas/SaasQuick.php
  88. 68 0
      app/model/saas/SaasStar.php
  89. 51 0
      app/model/saas/SaasStore.php
  90. 50 0
      app/model/saas/SaasStoreAccount.php
  91. 51 0
      app/model/saas/SaasStoreCategory.php
  92. 47 0
      app/model/saas/SaasStoreExpress.php
  93. 69 0
      app/model/saas/SaasStoreOpen.php
  94. 50 0
      app/model/saas/SaasTransactionLog.php
  95. 47 0
      app/model/saas/SaasUserOpen.php
  96. 48 0
      app/model/system/SystemConfig.php
  97. 45 0
      app/model/system/SystemData.php
  98. 52 0
      app/model/system/SystemMenu.php
  99. 48 0
      app/model/system/SystemOplog.php
  100. 66 0
      app/model/system/SystemUser.php

+ 20 - 0
.gitignore

@@ -0,0 +1,20 @@
+.DS_Store
+/vendor
+/runtime
+
+# local env files
+.env.local
+.env
+.env.*.local
+
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+/composer.lock

+ 18 - 0
Dockerfile

@@ -0,0 +1,18 @@
+FROM php:8.3.22-cli-alpine
+
+RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
+
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
+  && apk update --no-cache \
+  && docker-php-source extract
+
+# install extensions
+RUN docker-php-ext-install pdo pdo_mysql -j$(nproc) pcntl
+
+# enable opcache and pcntl
+RUN docker-php-ext-enable opcache pcntl
+RUN docker-php-source delete \
+    rm -rf /var/cache/apk/*
+
+RUN mkdir -p /app
+WORKDIR /app

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/webman/contributors)
+
+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.

+ 45 - 0
README.md

@@ -0,0 +1,45 @@
+# 交易小程序
+```angular2html
+composer require yzh52521/easyhttp
+composer require "linqiao/php-addtran:^0.1"
+composer require webman/event
+composer require phpoffice/phpspreadsheet
+composer require workerman/crontab
+composer require vlucas/phpdotenv
+composer require webman/console
+composer require webman/redis-queue
+composer require -W webman/think-cache
+composer require kkokk/poster
+composer require isszz/webman-hashids
+composer require linfly/annotation
+composer require hhink/webman-sms
+composer require hzdad/codecheck
+composer require php-di/php-di
+composer require shopwwi/webman-auth
+composer require tinywan/storage
+composer require aliyuncs/oss-sdk-php
+composer require qcloud/cos-sdk-v5
+composer require qiniu/php-sdk
+composer require luckycmc/webman-province-city-area
+composer require tinywan/captcha:^2.1
+```
+
+## API核销流程
+
+#### 通过抖音订单ID获取encrypted_code
+```angular2html
+https://developer.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/life.capacity.fulfilment/certificate.query
+获取到 encrypted_code
+```
+
+### 发起核销
+```angular2html
+https://developer.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/life.capacity.fulfilment/certificate.verify
+
+{
+    "verify_token": "4600000194202606129883397878", // 可随机生成
+    "poi_id": "7644106137976965139", // POIID
+    "encrypted_codes": ["CgYIASAHKAESLgosV9U88cYt5F3q0PunWznwayRS5aK20VokyuBMWnPrWm1mvAmszlN02+Ks93AaAA=="], // 第一步获取的encrypted_code
+    "order_id": "1110177134099228984" // 订单ID
+}
+```

+ 133 - 0
app/command/Model.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace app\command;
+
+use support\think\Db;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use think\db\exception\BindParamException;
+use Webman\Console\Util;
+
+class Model extends Command
+{
+
+
+    protected static string $defaultName = 'model:all';
+    protected static string $defaultDescription = '统一生成Model';
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $output->writeln("生成Model开始====>".getDateFull());
+        try {
+            $table = Db::query("SHOW TABLES");
+        } catch (BindParamException $e) {
+
+        }
+        $connection = config("think-orm.default");
+        $database = config("think-orm.connections.$connection.database");
+        foreach ($table as $val) {
+            $dir = explode("_",$val["Tables_in_{$database}"]);
+            $dirPath = app_path("model").DIRECTORY_SEPARATOR.$dir[0];
+            $namespace = str_replace('/', '\\', 'app/model/' . $dir[0]);
+            $class = Util::nameToClass($val["Tables_in_{$database}"]);
+            $file = $dirPath . DIRECTORY_SEPARATOR . "$class.php";
+            if (file_exists($file)) { // 存在,跳过
+                $output->writeln("已存在,跳过====>".getDateFull()."====>".$file);
+            } else {
+                $path = pathinfo($file, PATHINFO_DIRNAME);
+                if (!is_dir($path)) {
+                    mkdir($path, 0777, true);
+                }
+                $properties = '';
+                $pk = 'id';
+                foreach (Db::query("select COLUMN_NAME,DATA_TYPE,COLUMN_KEY,COLUMN_COMMENT from INFORMATION_SCHEMA.COLUMNS where table_name = '".$val["Tables_in_{$database}"]."' and table_schema = '$database' ORDER BY ordinal_position") as $item) {
+                    if ($item['COLUMN_KEY'] === 'PRI') {
+                        $pk = $item['COLUMN_NAME'];
+                        $item["COLUMN_COMMENT"] .= "(主键)";
+                    }
+                    $type = $this->getType($item["DATA_TYPE"]);
+                    $properties .= " * @property $type \${$item["COLUMN_NAME"]} {$item["COLUMN_COMMENT"]}\n";
+                }
+                $properties = rtrim($properties) ?: ' *';
+                $table_val = $val["Tables_in_{$database}"];
+                $model_content = <<<EOF
+<?php
+
+namespace $namespace;
+
+use app\\extra\\basic\\Model;
+
+
+/**
+$properties
+ */
+class $class 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 = "$table_val";
+    
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected string \$primaryKey = "$pk";
+    
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public bool \$timestamps = false;
+
+
+}
+
+EOF;
+                file_put_contents($file, $model_content);
+                $output->writeln("生成成功====>".getDateFull()."====>".$file);
+            }
+        }
+        return self::SUCCESS;
+    }
+
+
+    protected function getType(string $type): string
+    {
+        if (str_contains($type, 'int')) {
+            return 'integer';
+        }
+        switch ($type) {
+            case 'varchar':
+            case 'string':
+            case 'text':
+            case 'date':
+            case 'time':
+            case 'guid':
+            case 'datetimetz':
+            case 'datetime':
+            case 'decimal':
+            case 'enum':
+                return 'string';
+            case 'boolean':
+                return 'integer';
+            case 'float':
+                return 'float';
+            default:
+                return 'mixed';
+        }
+    }
+
+}

+ 24 - 0
app/command/OrderDelay.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace app\command;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class OrderDelay extends Command
+{
+
+
+    protected static string $defaultName = 'order:delay';
+
+    protected static string $defaultDescription = '超出核销时段订单第二天定时进行核销';
+
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+
+        return self::SUCCESS;
+    }
+
+}

+ 48 - 0
app/controller/IndexController.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace app\controller;
+
+use support\Request;
+use Webman\Push\Api;
+
+class IndexController
+{
+    public function index(Request $request)
+    {
+//        $api = new Api('http://127.0.0.1:3232', config('plugin.webman.push.app.app_key'),config('plugin.webman.push.app.app_secret'));
+//        $api->trigger("user-_000KmfiVkio30K9VxoeLUUY3_hngEWcgR7u","message",[
+//            "type"  =>  "text",
+//            "time"  =>  time() * 1000,
+//            "msgId" => time(),
+//            "content"   => "哈稍等哈就说客家话",
+//            "source" => 2,
+//            "avatar"    => "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png",
+//            "poi_id"    => "7644106137976965139",
+//            "openid"  => "_000KmfiVkio30K9VxoeLUUY3_hngEWcgR7u",
+//            "service_id"  => "105"
+//        ]);
+//        echo "asd";
+//        $param = [
+//            "events" => [
+//                [
+//                    "channel"   => "private-user-1",
+//                    "name"      => "channel_added"
+//                ]
+//            ]
+//        ];
+//        $key = hash_hmac('sha256', json_encode($param), config('plugin.webman.push.app.app_secret'), false);
+//        echo $key."\n";
+        return view('index');
+    }
+
+    public function view(Request $request)
+    {
+        return view('index/view', ['name' => 'webman']);
+    }
+
+    public function json(Request $request)
+    {
+        return json(['code' => 0, 'msg' => 'ok']);
+    }
+
+}

+ 116 - 0
app/controller/admin/Category.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\tools\DataExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasCategory;
+use app\service\saas\CategoryService;
+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;
+use Webman\RedisQueue\Redis;
+
+
+#[Controller("/api/category"),Middleware(AuthMiddleware::class)]
+class Category extends Base
+{
+
+    #[Inject]
+    protected CategoryService $service;
+
+    #[Inject]
+    protected SaasCategory $model;
+
+    /**
+     * 数据列表
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping('list')]
+    public function getStoreCategory(Request $request): Response
+    {
+        try {
+            $data = $this->service->setModel()->getList($request->all(),null,false);
+            $tree = DataExtend::arr2tree($data,"category_id","parent_id");
+            return successTrans(100010,$tree,200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    /**
+     * 同步门店分类
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping('sync')]
+    public function syncStore(Request $request): Response
+    {
+        try {
+            Redis::send("sync-category",[
+                "appid"     => sConf("wechat.appid"),
+                "secret"    => sConf("wechat.secret"),
+                "account"   => sConf("wechat.shop_id"),
+            ]);
+            return success("发起成功");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+    /**
+     * 编辑代理信息
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("edit")]
+    public function setListData(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $factory = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($factory->isEmpty()) return errorTrans("error.data");
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 批量操作
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("batch")]
+    public function setGoodsBatch(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => "参数错误",
+                "value.require"     => "参数错误",
+                "field.require"     => "参数错误",
+                "type.require"      => "参数错误"
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param['type'] == "batch") {
+                $state = $this->model->where("id","in",$param['id'])->save([$param['field'] => $param['value']]);
+            } else {
+                $state = $this->model->where("id",$param['id'])->save([$param['field'] => $param['value']]);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+}

+ 84 - 0
app/controller/admin/Complaint.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace app\controller\admin;
+
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasComplaint;
+use app\service\saas\ComplaintService;
+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("/api/complaint"),Middleware(AuthMiddleware::class)]
+class Complaint extends Base
+{
+
+
+    #[Inject]
+    protected ComplaintService $service;
+
+    #[Inject]
+    protected SaasComplaint $model;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $data = $this->service->setModel()->getList($param,['store' => function($query){
+                $query->field("poi_id,poi_name,poi_address,nick_name");
+            },'orders' => function($query){
+                $query->field("out_order_no,product_id");
+            }]);
+            return successTrans("success.data",pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping('detail')]
+    public function getDataDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->model->where("id",$param["id"])->with(['store' => function($query){
+                $query->field("poi_id,poi_name,poi_address,poi_city");
+            }])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("empty.data");
+            return successTrans("success.data",$data->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[PostMapping('save')]
+    public function setDataDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+                "remark.require"    => trans("empty.require")
+            ],$request->method());
+            $data = $this->model->where("id",$param["id"])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("empty.data");
+            if ($data['status'] <> 0) return error("请勿重复回复");
+            $data->remark = $param['remark'];
+            $state = $data->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+}

+ 71 - 0
app/controller/admin/Config.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\tools\OssRegionExtend;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemConfig;
+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("/api/config"),Middleware(AuthMiddleware::class)]
+class Config extends Base
+{
+
+    #[GetMapping('list')]
+    public function getConfigList(Request $request): Response
+    {
+        try {
+            $type = $request->get("type","service");
+            $data = (new SystemConfig)->where("type",$type)->where("status",1)->field("name,value")->select()->toArray();
+            $result = [];
+            foreach ($data as $item) {
+                $result[$item['name']] = $item['value'];
+            }
+            if ($type == "sms") {
+                $result['channel'] = $this->getSmsChannel();
+                $result['login_sms'] = '您的验证码为:${code},请勿泄露于他人!';
+            }
+            return successTrans("success.data",$result);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("save")]
+    public function setConfigData(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (isset($param['data']['channel'])) unset($param['data']['channel']);
+            foreach ($param['data'] as $k => $v){
+                if(is_array($v)) $v = implode(",",$v);
+                sConf($param['type'].'.'.$k, $v);
+            }
+            return successTrans("success.data",[]);
+        } catch (\Throwable $exception){
+            return error($exception->getMessage());
+        }
+    }
+
+    #[GetMapping("regin")]
+    public function getConfigRegin(): Response
+    {
+        try {
+            return successTrans(100010,[
+                "oss"       => OssRegionExtend::OssRegion(),
+                "cos"       => OssRegionExtend::CosRegion(),
+                "qiniu"     => OssRegionExtend::QiniuRegion(),
+            ]);
+        } catch (\Exception $exception) {
+            return error($exception->getMessage());
+        }
+    }
+
+}

+ 84 - 0
app/controller/admin/Deposit.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasDepositLog;
+use app\model\saas\SaasStore;
+use app\service\saas\DepositService;
+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("/api/deposit"),Middleware(AuthMiddleware::class)]
+class Deposit extends Base
+{
+
+    #[Inject]
+    protected DepositService $service;
+
+    #[Inject]
+    protected SaasDepositLog $model;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $data = $this->service->setModel()->getList($param,['poi' => function($query){
+                $query->field("poi_id,poi_name,poi_address,nick_name");
+            }]);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[PostMapping("save")]
+    public function setDepositData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "poi_id.require"    => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+                "money.require"     => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $store = (new SaasStore)->where("poi_id",$param['poi_id'])->findOrEmpty();
+            if ($store->isEmpty()) return error("该商家不存在");
+            switch ($param['type']) {
+                case "1": // 新增金额
+                case "3":
+                case "5":
+                    $store->deposit = $store['deposit'] + ($param['money'] * 100);
+                    break;
+                default:
+                    if (($param['money'] * 100) > $store['deposit']) return error("商家保证金余额不足");
+                    $store->deposit = $store['deposit'] - ($param['money'] * 100);
+                    break;
+            }
+            $store->save();
+            $state = $this->model->insertGetId([
+                "poi_id"    => $param['poi_id'],
+                "order_sn"  => strtoupper(CodeExtend::random(18,3)),
+                "money"     => $param['money'] * 100,
+                "type"      => $param['type'],
+                "status"    => 1
+            ]);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 136 - 0
app/controller/admin/Finance.php

@@ -0,0 +1,136 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasBankCompany;
+use app\model\saas\SaasStoreOpen;
+use app\service\saas\StoreOpenService;
+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("/api/finance"),Middleware(AuthMiddleware::class)]
+class Finance extends Base
+{
+
+    #[Inject]
+    protected StoreOpenService $storeOpen;
+
+    #[GetMapping("open/list")]
+    public function getOpenList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            if (!empty($param['status'])) $param['status'] = intval($param['status']+1);
+            $data = $this->storeOpen->setModel()->getList($param,['poi' => function($query){
+                $query->field("poi_id,poi_name,poi_address,nick_name");
+            }]);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 三级城市信息
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping("city")]
+    public function getCityJson(Request $request): Response
+    {
+        try {
+            $data = json_decode(file_get_contents(base_path()."/city.json"),true);
+            return successTrans("success.data",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[GetMapping("code")]
+    public function getCodeData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "name.require"  => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = (new SaasBankCompany)->where("name","like","%{$param['name']}%")->select()->toArray();
+            if (empty($data)) return error("无数据");
+            return successTrans("success.data",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[GetMapping("open")]
+    public function getCodeOpen(Request $request): Response
+    {
+        try {
+            $data = (new SaasStoreOpen)->where("poi_id",$request->user['store_id'])->select()->toArray();
+            return successTrans("success.data",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 虚拟户进件提交
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("save")]
+    public function setOpenData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "company_name.require"      => trans("empty.require"),
+                "company_sub_name.require"  => trans("empty.require"),
+                "company_number.require"    => trans("empty.require"),
+                "company_license.require"   => trans("empty.require"),
+                "city.require"              => trans("empty.require"),
+                "city_name.require"         => trans("empty.require"),
+                "address.require"           => trans("empty.require"),
+                "legal_mobile.require"      => trans("empty.require"),
+                "legal_mobile.mobile"       => trans("error.mobile"),
+                "legal_name.require"        => trans("empty.require"),
+                "legal_idcard.require"      => trans("empty.require"),
+                "legal_idcard.idCard"       => trans("error.idcard"),
+                "email.require"             => trans("empty.require"),
+                "email.email"               => trans("error.email"),
+                "legal_id_front.require"    => trans("empty.require"),
+                "legal_id_back.require"     => trans("empty.require"),
+                "bank_img.require"          => trans("empty.require"),
+                "bank_number.require"       => trans("empty.require"),
+                "bank_code.require"         => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            if (!empty($param['city'])) $param['city_code'] = json_encode($param['city']);
+            if (!empty($param['city_name'])) $param['city_name'] = trim($param['city_name']);
+            $data = (new SaasStoreOpen)->where("poi_id",$request->user['store_id'])->findOrEmpty();
+            $param['last_at'] = getDateFull();
+            if ($data->isEmpty())
+            {
+                $param['poi_id'] = $request->user['store_id'];
+                $state = $data->setAutoData($param);
+            } else {
+                if ($data['status'] == 1) return error("无需重复进件");
+                $state = $data->save($param);
+            }
+            if (!$state) return errorTrans("error.data");
+            return success("提交成功,请耐心等待审核");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 107 - 0
app/controller/admin/Goods.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\dyMini\Qrcode;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasGoods;
+use app\model\saas\SaasGoodsSku;
+use app\model\saas\SaasStore;
+use app\service\saas\GoodsService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+#[Controller("/api/goods"),Middleware(AuthMiddleware::class)]
+class Goods extends Base
+{
+
+    #[Inject]
+    protected GoodsService $service;
+
+    #[Inject]
+    protected SaasGoods $model;
+
+    #[Inject]
+    protected SaasGoodsSku $skuModel;
+
+    #[Inject]
+    protected SaasStore $store;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            if (!empty($param['status'])) $param['status'] = intval($param['status']+1);
+            $productType = $this->service->productType();
+            $data = $this->service->setModel()->getList($param,['poi' => function($query){
+                $query->field("poi_id,poi_name,poi_address,nick_name");
+            }],true,['types'],['types' => function($query,$resp) use($productType){
+                $productTypeArr = [];
+                foreach ($productType as $val) {
+                    $productTypeArr[$val['key']] = $val['name'];
+                }
+                return $productTypeArr[$resp['product_type']]??'';
+            }]);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    /**
+     * 获取商品二维码
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping("qrcode")]
+    public function getQrcode(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("error.data");
+            $qrcode = (new Qrcode)->config($this->getDyConfig())->token()->getQrcodeV2("pages/goods/detail?goods={$data['product_id']}&spm={$data['id']}","douyin");
+            if (!isset($qrcode['img'])) return error("生成二维码失败");
+            return successTrans("success.data",compact('qrcode'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 商品详情
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping('detail')]
+    public function getGoodDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"            => trans("empty.require"),
+                "product_id.require"    => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $detail = $this->model->where("id",$param["id"])->with(['skuSpecs'])->findOrEmpty();
+            if ($detail->isEmpty()) return errorTrans("empty.data");
+            if ($detail['poi_id'] <> $request->user['store_id']) return errorTrans("error.param");
+            $detail['sold_start_times'] = [(string)$detail['sold_start_time'],(string)$detail['sold_end_time']];
+            $detail['use_times'] = [(string)$detail['use_time_start'],(string)$detail['use_time_end']];
+            return successTrans('success.data',$detail->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+}

+ 54 - 0
app/controller/admin/Order.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\service\saas\GoodsService;
+use app\service\saas\OrderService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/api/order"),Middleware(AuthMiddleware::class)]
+class Order extends Base
+{
+
+    #[Inject]
+    protected OrderService $service;
+
+    #[Inject]
+    protected GoodsService $goodsService;
+
+    #[GetMapping('list')]
+    public function getOrderList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $productType = $this->goodsService->productType();
+            $data = $this->service->setModel()->getList($param,['product' => function($query) use($productType){
+                $query->field("product_id,product_name,product_type")->append(['types'])->withAttr(['types' => function($query,$resp) use($productType){
+                    $productTypeArr = [];
+                    foreach ($productType as $val) {
+                        $productTypeArr[$val['key']] = $val['name'];
+                    }
+                    return $productTypeArr[$resp['product_type']]??'';
+                }]);
+            },'user' => function($query){
+                $query->field("openid,nickname,avatar");
+            },'poi' => function($query){
+                $query->field("poi_id,poi_name,poi_address,nick_name");
+            }],true,['end_time'],["end_time" => function ($resp,$next) {
+                return timeDiff(strtotime("+30 minutes",strtotime($next['create_at'])),time());
+            }]);
+            return successTrans("success.data",pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 274 - 0
app/controller/admin/Plan.php

@@ -0,0 +1,274 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\dyMini\PlanLive;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasLivePlan;
+use app\model\saas\SaasLivePlanGoods;
+use app\model\saas\SaasLivePlanStar;
+use app\service\saas\GoodsService;
+use app\service\saas\PlanService;
+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("/api/plan"),Middleware(AuthMiddleware::class)]
+class Plan extends Base
+{
+
+    #[Inject]
+    protected PlanService $service;
+
+    #[Inject]
+    protected GoodsService $goodsService;
+
+    #[Inject]
+    protected SaasLivePlan $model;
+
+    #[GetMapping('list')]
+    public function getPlanList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $data = $this->service->setModel()->getList($param,['poi' => function($query){
+                $query->field("poi_id,poi_name,poi_address,nick_name");
+            }]);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 发布佣金计划
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('save')]
+    public function addPlanData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "plan_name.require"         => trans("empty.require"),
+                "merchant_phone.require"    => trans("empty.require"),
+                "merchant_phone.mobile"     => trans("error.mobile"),
+                "status.require"            => trans("empty.require"),
+                "goods.require"             => trans("empty.require"),
+                "star.require"              => trans("empty.require"),
+                "plan_id.default"           => 0,
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $goodsData = is_string($param['goods']) ? json_decode($param['goods'],true) : $param['goods'];
+            $starData = is_string($param['star']) ? json_decode($param['star'],true) : $param['star'];
+            $plan = $this->model->where("plan_id",$param['plan_id'])->findOrEmpty();
+            $liveDyStar = $liveDyGoods = [];
+            foreach ($goodsData as $k => $v) {
+                $liveDyGoods[$k] = [
+                    "commission_rate"   => round($v['rate'] * 100),
+                    "product_id"        => $v['goods_id'],
+                ];
+            }
+            foreach ($starData as $k => $v) {
+                $liveDyStar[$k] = $v['unique_id'];
+            }
+            $resp = (new PlanLive)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->planData($param['plan_id'],['name' => $param['plan_name'],'mobile' => $param['merchant_phone'],'goods' => $liveDyGoods,'star' => $liveDyStar]);
+            if (isset($resp['msg'])) return error($resp['msg']);
+            if (!isset($resp['plan_id'])) return error("发布直播计划失败");
+            if ($plan->isEmpty()) { // 新增
+                $state = $plan->insertGetId([
+                    "plan_id"           => $resp['plan_id'],
+                    "poi_id"            => $request->user['store_id'],
+                    "plan_name"         => $param['plan_name'],
+                    "merchant_phone"    => $param['merchant_phone'],
+                    "status"            => $param['status'],
+                ]);
+            } else { // 编辑
+                (new SaasLivePlanGoods)->where("plan_id",$resp['plan_id'])->delete();
+                (new SaasLivePlanStar)->where("plan_id",$resp['plan_id'])->delete();
+                $state = $plan->save([
+                    "plan_name"         => $param['plan_name'],
+                    "merchant_phone"    => $param['merchant_phone'],
+                    "status"            => $param['status'],
+                ]);
+            }
+            $goodsAll = $starAll = [];
+            foreach ($liveDyGoods as $k => $v) {
+                $goodsAll[$k] = [
+                    "plan_id"           => $resp['plan_id'],
+                    "goods_id"          => $v['product_id'],
+                    "commission_rate"   => $v['commission_rate'] / 100,
+                ];
+            }
+            (new SaasLivePlanGoods)->insertAll($goodsAll);
+            foreach ($liveDyStar as $k => $v) {
+                $starAll[$k] = [
+                    "plan_id"   => $resp['plan_id'],
+                    "unique_id" => $v
+                ];
+            }
+            (new SaasLivePlanStar)->insertAll($starAll);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping('detail')]
+    public function detailPlan(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"         => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $productType = $this->goodsService->productType();
+            $plan = $this->model->where("id",$param['id'])->with(['goods' => function ($query) use($productType){
+                $query->with(['goods' => function($subQue) use($productType){
+                    $subQue->field("goods_id,product_name,image_list,product_id,price,line_price,limit_use_rule,use_num_per_consume,status,product_type")->append(['types'])->withAttr(['types' => function($query,$resp) use($productType){
+                        $productTypeArr = [];
+                        foreach ($productType as $val) {
+                            $productTypeArr[$val['key']] = $val['name'];
+                        }
+                        return $productTypeArr[$resp['product_type']]??'';
+                    }]);
+                }]);
+            },'star' => function ($query) {
+                $query->with(['star' => function($subQue){
+                    $subQue->field("unique_id,avatar_url,show_nickname,fans_count,fans_tag_list,local_fans_count,talent_item_level_display,talent_live_level_display,content_level_display,credit_score_display,city_name,rank_display,status");
+                }]);
+            }])->findOrEmpty();
+            if ($plan->isEmpty()) return errorTrans("empty.data");
+            return successTrans("success.data",$plan->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping('goods')]
+    public function setGoodsRate(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+                "data.require"      => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $plan = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($plan->isEmpty()) return errorTrans("empty.data");
+            $planData = is_string($param['data']) ? json_decode($param['data'],true) : $param['data'];
+            $editData = [];
+            $editPlanData = [
+                'name'      => $plan['plan_name'],
+                'mobile'    => $plan['merchant_phone'],
+            ];
+            if ($param['type'] == 'goods') {
+                foreach ($planData as $k => $v) {
+                    $commission_rate = $v['commission_rate'] * 100;
+                    echo $commission_rate."\n";
+                    $editData[$k] = [
+                        "commission_rate"   => round($commission_rate),
+                        "product_id"        => $v['goods_id'],
+                    ];
+                }
+                $editPlanData['goods'] = $editData;
+            }
+            if ($param['type'] == 'star') {
+                foreach ($planData as $k => $v) {
+                    $editData[$k] = $v['unique_id'];
+                }
+                $editPlanData['star'] = $editData;
+            }
+            $resp = (new PlanLive)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->planData($plan['plan_id'],$editPlanData);
+            if (isset($resp['msg'])) return error($resp['msg']);
+            $goodsAll = $starAll = [];
+            $state = false;
+            if ($param['type'] == 'goods') {
+                (new SaasLivePlanGoods)->where("plan_id",$plan['plan_id'])->delete();
+                foreach ($planData as $k => $v) {
+                    $goodsAll[$k] = [
+                        "plan_id"           => $resp['plan_id'],
+                        "goods_id"          => $v['goods_id'],
+                        "commission_rate"   => $v['commission_rate'],
+                    ];
+                }
+                $state = (new SaasLivePlanGoods)->insertAll($goodsAll);
+            }
+            if ($param['type'] == 'star') {
+                (new SaasLivePlanStar)->where("plan_id",$plan['plan_id'])->delete();
+                foreach ($planData as $k => $v) {
+                    $starAll[$k] = [
+                        "plan_id"   => $resp['plan_id'],
+                        "unique_id" => $v
+                    ];
+                }
+                $state = (new SaasLivePlanStar)->insertAll($starAll);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping('close')]
+    public function closePlan(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $plan = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($plan->isEmpty()) return errorTrans("empty.data");
+            $resp = (new PlanLive)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->planClose($plan['plan_id']);
+            if (isset($resp['msg'])) return error($resp['msg']);
+            $plan->status = 2;
+            $state = $plan->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping('del')]
+    public function delPlan(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $plan = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($plan->isEmpty()) return errorTrans("empty.data");
+            if  ($plan['status'] <> 2) return error("该计划暂不支持删除");
+            $state = $plan->delete();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 209 - 0
app/controller/admin/Prints.php

@@ -0,0 +1,209 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\tools\PrintExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasPrint;
+use app\service\saas\PrintService;
+use app\validate\saas\PrintValidate;
+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;
+use Webman\RedisQueue\Redis;
+
+#[Controller("/api/prints"),Middleware(AuthMiddleware::class)]
+class Prints extends Base
+{
+
+    #[Inject]
+    protected PrintService $service;
+
+    #[Inject]
+    protected PrintValidate $validate;
+
+    #[Inject]
+    protected SaasPrint $model;
+
+
+    #[GetMapping('list')]
+    public function getStoreList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $list = $this->service->setModel()->getList($param,['poi']);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 查询打印机状态
+     * @return Response
+     */
+    #[PostMapping('refresh')]
+    public function refreshState(): Response
+    {
+        try {
+            Redis::send("print-state",[]);
+            return successTrans("success.print-state");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 打印测试
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('test')]
+    public function testPrint(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "sn.require"    => trans("empty.require")
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $print = $this->model->where("sn",$param["sn"])->findOrEmpty();
+            if ($print->isEmpty()) return errorTrans("empty.data");
+            $resp = (new PrintExtend)->config(['appid' => sConf('wechat.ex_appid'),'secret' => sConf('wechat.ex_secret')])->printTemplate(sConf('wechat.ex_print'),$param['sn'],[
+                [
+                    "express"   => [
+                        [
+                            "j_mobile" => "180****9980",
+                            "j_address" =>"安徽省阜阳市颍州区xxx镇xxx村xxx",
+                            "express_num" => "SF3270714680386",
+                            "name" => "测试的商品标题名称测试的商品标题名称测试的商品标题名称测试的商品标题名称测试的商品标题名称",
+                            "time" => getDateFull(),
+                            "type" => "特快",
+                            "s_name" => "王*二",
+                            "s_address" => "安徽省阜阳市颍州区xxx镇xxx村xxx",
+                            "s_mobile" => "******00982",
+                            "j_name" => "王麻子",
+                            "express_type" => "饰品",
+                            "order" => "1033999288123123"
+                        ]
+                    ]
+                ]
+            ]);
+            if (!$resp['status']) return error($resp['message']);
+            return successTrans("success.print");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 新增/编辑打印机
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('save')]
+    public function save(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (!$this->validate->check($param)) return error($this->validate->getError());
+            if (!isset($param['id'])) {
+                $resp = (new PrintExtend)->config(['appid' => sConf('wechat.ex_appid'),'secret' => sConf('wechat.ex_secret')])->bindDevice($param['sn'],$param['name']);
+                if (!$resp['status']) return error($resp['message']);
+            }
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 新增/编辑打印机
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('edit')]
+    public function edit(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (!$this->validate->scene('edit')->check($request->post())) return error($this->validate->getError());
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('batch')]
+    public function setBatchData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+                "value.require"     => trans("empty.require"),
+                "field.require"     => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param['type'] == "batch") {
+                $state = $this->model->where("id","in",$param['id'])->save([$param['field'] => $param['value']]);
+            } else {
+                $state = $this->model->where("id",$param['id'])->save([$param['field'] => $param['value']]);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 删除
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('del')]
+    public function delUser(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require"),
+                "type.default"  => "one",
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param['type'] == "batch") {
+                $state = $this->model->where("id","in",$param['id'])->delete();
+            } else {
+                $data = $this->model->where("id",$param['id'])->findOrEmpty();
+                if ($data->isEmpty()) return errorTrans("empty.data");
+                // 删除其他相关数据
+                $state = $data->delete();
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+}

Plik diff jest za duży
+ 155 - 0
app/controller/admin/Star.php


+ 283 - 0
app/controller/admin/Store.php

@@ -0,0 +1,283 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\dyMini\Qrcode;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasStore;
+use app\model\saas\SaasStoreCategory;
+use app\model\saas\SaasStoreExpress;
+use app\service\saas\GoodsService;
+use app\service\saas\StoreService;
+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\Container;
+use support\Request;
+use support\Response;
+use Webman\RedisQueue\Redis;
+
+
+/**
+ * POI门店
+ */
+#[Controller("/api/store"),Middleware(AuthMiddleware::class)]
+class Store extends Base
+{
+
+
+    #[Inject]
+    protected StoreService $service;
+
+    #[Inject]
+    protected GoodsService $goodsService;
+
+    #[Inject]
+    protected SaasStore $model;
+
+
+    /**
+     * 数据列表
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping('list')]
+    public function getStoreData(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $data = $this->service->setModel()->getList($param,null,true,['type'],['type' => function($query,$resp){
+                if (empty($resp['product_type'])) return [];
+                $productType = json_decode($resp['product_type'],true);
+                $typeData = [];
+                foreach ($this->goodsService->productType() as $key => $value) {
+                    if (in_array($value['key'],$productType)) {
+                        $typeData[] = $value['name'];
+                    }
+                }
+                return $typeData;
+            }]);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    /**
+     * 同步POI门店
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping('sync')]
+    public function syncStore(Request $request): Response
+    {
+        try {
+            Redis::send("sync-store",[
+                "appid"     => sConf("wechat.appid"),
+                "secret"    => sConf("wechat.secret"),
+                "account"   => sConf("wechat.shop_id"),
+            ]);
+            return success("发起成功");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 编辑代理信息
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("edit")]
+    public function setListData(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $factory = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($factory->isEmpty()) return errorTrans("error.data");
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 批量操作
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("batch")]
+    public function setGoodsBatch(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => "参数错误",
+                "value.require"     => "参数错误",
+                "field.require"     => "参数错误",
+                "type.require"      => "参数错误"
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param['type'] == "batch") {
+                $state = $this->model->where("id","in",$param['id'])->save([$param['field'] => $param['value']]);
+            } else {
+                $state = $this->model->where("id",$param['id'])->save([$param['field'] => $param['value']]);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping("cate")]
+    public function getCategory(): Response
+    {
+        try {
+            $data = $this->goodsService->productType();
+            return successTrans("success",compact('data'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 新增店铺品类授权
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("cate")]
+    public function addCategory(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "poi_id.require"        => trans("empty.require"),
+                "category.require"      => trans("empty.require"),
+                "product_type.require"  => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $storeMode = (new SaasStoreCategory);
+            $inData = [];
+            foreach ($param['category'] as $key => $value) {
+                if (!isset($value[2])) return error("请选择正确的品类");
+                $lastId = $value[2];
+                $storeCategory = $storeMode->where("category_id",$lastId)->where("poi_id",$param['poi_id'])->with(['cate'])->findOrEmpty();
+                if (!$storeCategory->isEmpty()) return error("【{$storeCategory['cate']['name']}】已存在,请勿重复添加");
+                $inData[$key] = [
+                    "category_id"   => $value[2],
+                    "poi_id"        => $param['poi_id'],
+                    "product_type"  => json_encode($param["product_type"]),
+                ];
+            }
+            $state = $storeMode->insertAll($inData);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 删除店铺品类授权
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("cate/del")]
+    public function delCategory(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "poi_id.require"    => trans("empty.require"),
+                "id.require"        => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = (new SaasStoreCategory)->where(['poi_id' => $param['poi_id'],'category_id' => $param['id']])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("error.data");
+            $state = $data->delete();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 获取门店二维码
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping("qrcode")]
+    public function getQrcode(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("error.data");
+            $qrcode = (new Qrcode)->config($this->getDyConfig())->token()->getQrcodeV2("pages/index/index?store={$data['poi_id']}","douyin");
+            if (!isset($qrcode['img'])) return error("生成二维码失败");
+            return successTrans("success.data",compact('qrcode'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 检测权限是否开通
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping("express")]
+    public function getExpressData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "poi.require"        => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->model->where("poi_id",$param['poi'])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("error.data");
+            $express = (new SaasStoreExpress)->where("poi_id",$param['poi'])->findOrEmpty();
+            $return = $express->toArray();
+            if ($express->isEmpty()) {
+                $return = null;
+            }
+            return successTrans("success.data",compact('return'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("express/save")]
+    public function setExpressData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.default"        => 0,
+                "status.require"    => trans("empty.require"),
+                "poi_id.require"    => trans("empty.require"),
+                "type.default"      => "sfexpress",
+                "rule.default"      => [],
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->model->where("poi_id",$param['poi_id'])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("error.data");
+            $express = (new SaasStoreExpress)->where("poi_id",$param['poi_id'])->findOrEmpty();
+            $param['rule'] = json_encode($param['rule']);
+            $state = $express->setAutoData($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 67 - 0
app/controller/admin/StoreCategory.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\tools\DataExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasStoreCategory;
+use app\service\saas\CategoryService;
+use app\service\saas\GoodsService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/api/store/category"),Middleware(AuthMiddleware::class)]
+class StoreCategory extends Base
+{
+
+    protected array $noNeedLogin = ["getCategoryList"];
+
+    #[Inject]
+    protected CategoryService $service;
+
+    #[Inject]
+    protected GoodsService $goodsService;
+
+    #[Inject]
+    protected SaasStoreCategory $model;
+
+    #[GetMapping('list')]
+    public function getCategoryList(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "poi.require"   => trans("empty.require")
+            ]);
+            if (!is_array($param)) return error($param);
+            $data = $this->service->setModel()->getList([],null,false);
+            $tree = DataExtend::arr2tree($data,"category_id","parent_id");
+            $storeCate = $this->model->where("poi_id",$param['poi'])->column('product_type',"category_id");
+            if (empty($storeCate)) return successTrans(100010,[],200);
+            $storeIds = [];
+            $storeTypes = [];
+            foreach ($storeCate as $k=>$v){
+                $storeIds[] = $k;
+                $productType = !empty($v) ? json_decode($v,true) : [];
+                $typeData = [];
+                foreach ($this->goodsService->productType() as $key => $value) {
+                    if (in_array($value['key'],$productType)) {
+                        $typeData[] = $value;
+                    }
+                }
+                $storeTypes[$k] = $typeData;
+            }
+            $resp = filterTreeByTargets($tree,$storeIds);
+            addTypesRecursive($resp,$storeTypes);
+            return successTrans(100010,$resp,200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 118 - 0
app/controller/admin/StoreUser.php

@@ -0,0 +1,118 @@
+<?php
+
+namespace app\controller\admin;
+
+
+use app\extra\basic\Base;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemUser;
+use app\service\system\UserService;
+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("/api/store/user"),Middleware(AuthMiddleware::class)]
+class StoreUser extends Base
+{
+
+    #[Inject]
+    protected UserService $service;
+
+    #[Inject]
+    protected SystemUser $model;
+
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['type'] = 2;
+            $data = $this->service->setModel()->getList($param,"account");
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[GetMapping('service')]
+    public function getDataListService(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['type'] = 3;
+            $data = $this->service->setModel()->getList($param,"account");
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+
+    /**
+     * 账户列表
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("save")]
+    public function setUserData(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if(!isset($param['id'])) // 新增
+            {
+                $param['salt'] = CodeExtend::random(6,3);
+                $param['password'] = md5($param['password'].$param['salt']);
+                $param['create_ip'] = $request->getRealIp();
+                $user = $this->model->where("username",$param['username'])->findOrEmpty();
+                if (!$user->isEmpty()) return errorTrans(20011);
+            }
+            if (isset($param['app_id'])) {
+                $param['app_id'] = json_encode($param['app_id']);
+            }
+            if(isset($param['role_path']) && is_array($param['role_path'])){
+                $parent = $param['role_path'];
+                $param['role_path'] = implode(",",$parent);
+                $param['role_id'] = $parent[count($parent) - 1];
+            }
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans(100011);
+            return successTrans(100010);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+    /**
+     * 修改密码
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("passwd")]
+    public function setUserPasswd(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans(20010),
+                "password.require"  => trans(20010)
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $user = $this->model->where("username",$param['id'])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("empty.error");
+            $user->password = md5($param['password'].$user['salt']);
+            $state = $user->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+}

+ 53 - 0
app/controller/admin/Upload.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\tools\UploadExtend;
+use app\middleware\AuthMiddleware;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use LinFly\Annotation\Attributes\Route\PostMapping;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/api/upload"),Middleware(AuthMiddleware::class)]
+class Upload extends Base
+{
+
+    #[PostMapping("data")]
+    public function upload2image(Request $request): Response
+    {
+        try {
+            [$state,$msg] = $this->checkDeposit($request->user['store_id']);
+            if (!$state) return error($msg);
+            $resp = UploadExtend::uploadFile();
+            if (!isset($resp[0]['url'])) return errorTrans(40010);
+            return successTrans("success.data",[
+                "fileName"  => $resp[0]['origin_name'],
+                "src"       => $resp[0]['url'],
+            ],200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("file")]
+    public function upload2file(Request $request): Response
+    {
+        try {
+            $resp = UploadExtend::disk(UploadExtend::MODE_LOCAL)->uploadFile();
+            if (!isset($resp[0]['url'])) return errorTrans(40010);
+            return successTrans("success.data",[
+                "fileName"  => $resp[0]['origin_name'],
+                "src"       => $resp[0]['url'],
+            ],200);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 196 - 0
app/controller/api/Auth.php

@@ -0,0 +1,196 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\dyLife\Crypt;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasUserOpen;
+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 Shopwwi\WebmanAuth\Facade\Auth as AuthFacade;
+use support\Response;
+
+
+#[Controller("/dy/auth"),Middleware(AuthMiddleware::class)]
+class Auth extends Base
+{
+
+    protected array $noNeedLogin = ["log2mobile","log2nickname","loginMobileAuth",'loginNicknameAuth'];
+
+    #[Inject]
+    protected SaasUserOpen $model;
+
+    /**
+     * 授权获取手机号-非登录
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("mobile/auth")]
+    public function loginMobileAuth(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "login.require"     => '获取登录参数失败',
+                "code.default"      => '',
+                "encryptedData.default"      => '',
+                "iv.default"        => '',
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $sessionKey = (new Crypt)->config($this->getDyConfig())->getSessionKey($param['login']);
+            if (empty($sessionKey)) return error("授权登录失败");
+            if (!empty($param['code'])) {
+                $mobileStr = (new Crypt)->config($this->getDyConfig())->token()->getMobile($param['code']);
+                echo  "sdasd\n";
+                print_r($mobileStr);
+            } else {
+                $data = $this->decrypt($param['encryptedData'],$sessionKey['session_key'],$param['iv']);
+                $mobileStr = $data['purePhoneNumber'];
+            }
+            print_r($mobileStr);
+            if (empty($mobileStr)) return error("授权信息获取失败");
+            return $this->encode("success",['mobile' => $mobileStr,'openid' => $sessionKey['openid']]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 授权昵称等信息-实际登录
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("nickname/auth")]
+    public function loginNicknameAuth(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "avatar.require"        => trans("empty.require"),
+                "nickname.require"      => trans("empty.require"),
+                "mobile.require"        => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $mobileData = json_decode($param['mobile'],true);
+            $mobile = [];
+            if (!empty($param['mobile'])) {
+                if (!isset($mobileData['openid'])) return error("授权失败");
+                if (isValidMobile($mobileData['mobile'])) {
+                    $mobile = ["purePhoneNumber" => $mobileData['mobile']];
+                } else {
+                    $mobile = $this->decrypt2code(sConf('wechat.min_private_key'), $mobileData['mobile']);
+                }
+            }
+            if (empty($mobile)) return error("授权登录失败");
+            $userInfo = [
+                "openid"    => $mobileData['openid'],
+                "nickname"  => $param['nickname']?removeEmoji2($param['nickname']):'',
+                "avatar"    => $param['avatar']
+            ];
+            $map = ["openid" => $mobileData['openid']];
+            if (!empty($mobile['purePhoneNumber'])) {
+                $userInfo['mobile'] = $mobile['purePhoneNumber'];
+            }
+            $user = $this->model->where($map)->findOrEmpty();
+            if ($user->isEmpty()) {
+                $userInfo["create_ip"] = $request->getRealIp();
+                $user->insertGetId($userInfo);
+            }
+            $userAuth = get_object_vars(AuthFacade::guard("user")->login(['openid' => $mobileData['openid']]));
+            return success("ok",$userAuth);
+
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("nickname")]
+    public function log2nickname(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $sessionKey = (new Crypt)->config($this->getDyConfig())->getSessionKey($param['login']);
+            if (empty($sessionKey)) return error("授权登录失败");
+            $userInfo = [
+                "openid"    => $sessionKey['openid'],
+                "nickname"  => $param['nickname']?removeEmoji2($param['nickname']):'',
+                "avatar"    => $param['avatar']??'',
+                "create_ip" => $request->getRealIp()
+            ];
+            $map = ["openid" => $sessionKey['openid']];
+            $user = $this->model->where($map)->findOrEmpty();
+            if ($user->isEmpty()) {
+                $user->insertGetId($userInfo);
+            } else {
+                $user->nickname = $param['nickname']?removeEmoji2($param['nickname']):'';
+                $user->avatar = $param['avatar']??'';
+                $user->save();
+            }
+            $userAuth = get_object_vars(AuthFacade::guard("user")->login(['openid' => $sessionKey['openid']]));
+            return success("ok",$userAuth);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[PostMapping("mobile")]
+    public function log2mobile(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $sessionKey = (new Crypt)->config($this->getDyConfig())->getSessionKey($param['login']);
+            if (empty($sessionKey)) return error("授权登录失败");
+            $mobile = [];
+            if (!empty($param['code'])) {
+                $mobileStr = (new Crypt)->config($this->getDyConfig())->token()->getMobile($param['code']);
+                if (!empty($mobileStr)) {
+                    $mobile = $this->decrypt2code(sConf('wechat.min_private_key'), $mobileStr);
+                }
+            }
+            $userInfo = [
+                "openid"    => $sessionKey['openid'],
+                "create_ip" => $request->getRealIp()
+            ];
+            $map = ["openid" => $sessionKey['openid']];
+            if (!empty($mobile['purePhoneNumber'])) {
+                $userInfo['mobile'] = $mobile['purePhoneNumber'];
+            }
+            $user = $this->model->where($map)->findOrEmpty();
+            if ($user->isEmpty()) {
+                $user->insertGetId($userInfo);
+            }
+            $userAuth = get_object_vars(AuthFacade::guard("user")->login(['openid' => $sessionKey['openid']]));
+            return success("ok",$userAuth);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    protected function decrypt2code($private_key, $ciphertext_str) {
+        // 解码 base64 密文
+        $ciphertext = base64_decode($ciphertext_str);
+
+        // 使用私钥解密
+        openssl_private_decrypt($ciphertext, $plaintext, $private_key, OPENSSL_PKCS1_PADDING);
+
+        if ($plaintext === false) {
+            return [];
+        }
+        return json_decode($plaintext,true);
+    }
+
+
+    protected function decrypt($encrypted_data, $session_key, $iv) {
+        $data = base64_decode($encrypted_data);
+        $key = base64_decode($session_key);
+        $iv_decoded = base64_decode($iv);
+        // 使用 AES-256-CBC 模式解密
+        $decrypted = openssl_decrypt($data, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv_decoded);
+
+        return json_decode($decrypted,true);
+    }
+}

+ 66 - 0
app/controller/api/Complaint.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace app\controller\api;
+
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasComplaint;
+use app\service\saas\ComplaintService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+/**
+ * 投诉
+ */
+#[Controller("/dy/complaint"),Middleware(AuthMiddleware::class)]
+class Complaint extends Base
+{
+
+    #[Inject]
+    protected ComplaintService $service;
+
+    #[Inject]
+    protected SaasComplaint $model;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            if (!empty($param['size'])) {
+                $param['pageSize'] = $param['size'];
+            }
+            $param['openid'] = $request->user['openid'];
+            $data = $this->service->setModel()->getList($param);
+            return successTrans("success.data",pageFormat($data));
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[GetMapping('detail')]
+    public function getDataDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->model->where("id",$param["id"])->with(['store' => function($query){
+                $query->field("poi_id,poi_name,poi_address,poi_city");
+            }])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("empty.data");
+            if ($data['openid'] <> $request->user['openid']) return error("非法操作");
+            $data['mobile'] = hide_mobile($data['mobile']);
+            return successTrans("success.data",$data->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+}

+ 94 - 0
app/controller/api/Goods.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasGoods;
+use app\model\saas\SaasGoodsSku;
+use app\model\saas\SaasOrder;
+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("/dy/goods"),Middleware(AuthMiddleware::class)]
+class Goods extends Base
+{
+
+    #[Inject]
+    protected SaasGoods $model;
+
+    protected array $noNeedLogin = ["getGoodsData"];
+
+    #[GetMapping("data")]
+    public function getGoodsData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "goods.require"     => trans("empty.require"),
+                "spm.default"       => "",
+                "option.default"    => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            echo getDateFull()."===>商品详情\n";
+            echo json_encode($param)."\n";
+            $data = $this->model->where("product_id",$param['goods'])->with(['poi' => function ($query) {
+                $query->field("poi_id,poi_name,poi_address,longitude,latitude,service_mobile,start_at,end_at,status");
+            },'skuSpecs'])->append(['otherImg'])->withAttr(['otherImg' => function ($qu,$resp) {
+                $img = json_decode($resp['image_list'],true);
+                return array_map(function ($v) {
+                    return $v['url'];
+                }, $img);
+            }])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("empty.data");
+            if ($data['poi']['status'] <> 1) return error("该店铺已关闭,请联系管理员");
+            if (!empty($data['content'])) {
+                $data['content'] = filterHtmlTags($data['content'],[
+                    'img' => [
+                        'remove' => ['width', 'height'],
+                        'style'  => ['width' => '100%']
+                    ],
+                    'p' => [
+                        'style'  => ['margin' => '0']
+                    ]
+                ]);
+            }
+            return $this->encode("success",$data->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[PostMapping("add_order")]
+    public function addOrder(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            if (!empty($param['spm'])) $param['goods_id'] = $param['spm'];
+            $goods = (new SaasGoods)->where([ "product_id" => $param['product_id'], "id" => $param['goods_id'] ])->findOrEmpty();
+            $sku = (new SaasGoodsSku)->where([ "product_id" => $param['product_id'], "sku_id" => $param['sku_id'] ])->findOrEmpty();
+            $priceData = empty($sku['price']) ? $goods['price'] * 100 : $sku['price'] * 100;
+            $param['line_price'] = empty($sku['line_price']) ? $goods['price'] * 100 : $sku['line_price'] * 100;
+            $param['price'] = $priceData * $param['number'];
+            $param['order_sn'] = "AL".CodeExtend::uniqidDate(18).rand(100,999);
+            $param['openid'] = $request->user['openid'];
+            $param['sku_name'] = $param['name'];
+            $param['poi_id'] = $goods['poi_id'];
+            $param['goods_id'] = $goods['id'];
+            $param['life_goods_id'] = $sku['goods_id']?$sku['goods_id']:$goods['goods_id'];
+            $state = (new SaasOrder)->setAutoData($param);
+            if (!$state) return error("提交订单失败");
+            return success("提交成功",['order' => $param['order_sn']]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 174 - 0
app/controller/api/Home.php

@@ -0,0 +1,174 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\dyLife\Crypt;
+use app\extra\dyLife\data\BaseData;
+use app\extra\tools\UploadExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasGoods;
+use app\model\saas\SaasStore;
+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;
+use Webman\RedisQueue\Redis;
+
+
+#[Controller("/dy/home"),Middleware(AuthMiddleware::class)]
+class Home extends Base
+{
+
+    /**
+     * @var array|string[]
+     */
+    protected array $noNeedLogin = ["getHomeData","getLicense","payBtnMobile","getMoreGoods","getCityJson"];
+
+    #[GetMapping("data")]
+    public function getHomeData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "store.require" => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $banner = [
+                [
+                    "cover" => "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/dc7fd761c2e2af9c340f20677f8f115d942f0bde.png"
+                ]
+            ];
+            $store = (new SaasStore)->where("poi_id",$param['store'])->field("poi_name,start_at,end_at,longitude,latitude,service_mobile,poi_id,poi_city,poi_address,poi_logo")->findOrEmpty();
+            if (!$store->isEmpty()) {
+                $store['poi_logo'] = empty($store['poi_logo']) ? sConf("service.logo") : $store['poi_logo'];
+            }
+            $goods = (new SaasGoods)->where("poi_id",$param['store'])->where("status",1)->field("product_id,product_name,image_list,category,price,line_price,sale_stock,id")->limit(10)->order('id','desc')->select()->toArray();
+            return success("ok",compact('banner','store','goods'));
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[GetMapping("goods")]
+    public function getMoreGoods(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "store.require" => trans("empty.require"),
+                "page.default" => 1,
+                "size.default" => 10
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $goods = (new SaasGoods)->where("poi_id",$param['store'])->where("status",1)->field("product_id,product_name,image_list,category,price,line_price,sale_stock,id")->limit(10)->order('id','desc')->paginate([
+                "list_rows" => $param['size'],
+                "page"      => $param['page']
+            ]);
+            return successTrans(100010,pageFormat($goods),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[GetMapping("license")]
+    public function getLicense(): Response
+    {
+        try {
+            return success("ok",['img' => sConf("service.license")]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 三级城市信息
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping("city")]
+    public function getCityJson(Request $request): Response
+    {
+        try {
+            $data = json_decode(file_get_contents(base_path()."/city.json"),true);
+            return successTrans("success.data",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 生服组件解密手机号码
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("mobile")]
+    public function payBtnMobile(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $mobile = [];
+            if (!empty($param['code'])) {
+                $sessionKey = (new Crypt)->config($this->getDyConfig())->token()->getSessionKey($param['code']);
+                if (!empty($sessionKey)) {
+                    $mobileStr = $this->decrypt2code($param['encryptedData'], $sessionKey['session_key'],$param['iv']);
+                    if (!empty($mobileStr)) $mobile = json_decode($mobileStr,true);
+                }
+            }
+            if (empty($mobile)) return error("获取失败");
+            return successTrans("success.data",['mobile' => $mobile['purePhoneNumber']]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 解密微信用户手机号等加密数据
+     *
+     * @param string $encrypted_data Base64 编码的密文
+     * @param string $session_key    Base64 编码的会话密钥
+     * @param string $iv             Base64 编码的初始向量
+     * @return string 解密后的原始字符串(通常为 JSON)
+     * @throws Exception
+     */
+    protected function decrypt2code($encrypted_data, $session_key, $iv)
+    {
+        // 1. Base64 解码
+        $data = base64_decode($encrypted_data, true);
+        $key  = base64_decode($session_key, true);
+        $iv   = base64_decode($iv, true);
+
+        if ($data === false || $key === false || $iv === false) {
+            throw new \Exception('Base64 解码失败');
+        }
+
+        // 2. 根据密钥长度选择 AES 算法
+        $key_len = strlen($key);
+        switch ($key_len) {
+            case 16:
+                $method = 'AES-128-CBC';
+                break;
+            case 24:
+                $method = 'AES-192-CBC';
+                break;
+            case 32:
+                $method = 'AES-256-CBC';
+                break;
+            default:
+                throw new \Exception("无效的密钥长度: {$key_len} 字节");
+        }
+
+        // 3. 解密(OPENSSL_RAW_DATA 表示返回二进制数据,不进行 Base64 编码)
+        $decrypted = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
+
+        if ($decrypted === false) {
+            throw new \Exception('解密失败: ' . openssl_error_string());
+        }
+
+        return $decrypted;
+    }
+
+}

+ 127 - 0
app/controller/api/Notify.php

@@ -0,0 +1,127 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\model\saas\SaasOrder;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\PostMapping;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/notify")]
+class Notify extends Base
+{
+
+    #[PostMapping("douyin")]
+    public function getOrderNotify(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            echo getDateFull()."===支付返回\n";
+            $data = !empty($param['msg']) ? json_decode($param['msg'],true) : [];
+            print_r($data);
+            $orderSn = $data['out_order_no'];
+            if ($data['status'] <> "SUCCESS") return json(['err_no' => 0,'err_tips' => "success"]);
+            if (empty($data)) return json(['err_no' => 0,'err_tips' => "success"]);
+            $order = (new SaasOrder)->where("out_order_no",$orderSn)->findOrEmpty();
+            if ($order->isEmpty()) return json(['err_no' => 0,'err_tips' => "success"]);
+            if ($order['status'] <> 0) return json(['err_no' => 0,'err_tips' => "success"]);
+            $order->status = 1;
+            $order->pay_at = getDateFull();
+            $order->pay_type = $data['pay_channel'];
+            $order->pay_sn = $data['channel_pay_id'];
+            $order->pay_money = $data['total_amount'];
+            $order->save();
+            return json(['err_no' => 0,'err_tips' => "success"]);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[PostMapping("price")]
+    public function getOrderNotifyPrice(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            echo getDateFull()."===支付返回\n";
+            $data = !empty($param['msg']) ? json_decode($param['msg'],true) : [];
+            print_r($data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping("mini")]
+    public function getOrderNotifyMini(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            echo getDateFull()."===支付返回\n";
+            $data = !empty($param['msg']) ? json_decode($param['msg'],true) : [];
+            print_r($data);
+            $orderSn = $data['cp_extra'];
+            if ($data['status'] <> "SUCCESS") return json(['err_no' => 0,'err_tips' => "success"]);
+            if (empty($data)) return json(['err_no' => 0,'err_tips' => "success"]);
+            $order = (new SaasOrder)->where("order_sn",$orderSn)->findOrEmpty();
+            if ($order->isEmpty()) return json(['err_no' => 0,'err_tips' => "success"]);
+            if ($order['status'] <> 0) return json(['err_no' => 0,'err_tips' => "success"]);
+            $order->status = 1;
+            $order->pay_at = getDateFull();
+            $order->pay_type = $data['way'];
+            $order->pay_sn = $data['cp_orderno'];
+            $order->pay_money = $data['total_amount'];
+            $order->save();
+            return json(['err_no' => 0,'err_tips' => "success"]);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[PostMapping("refund")]
+    public function getOrderRefund(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            echo getDateFull()."===退款返回\n";
+            $data = !empty($param['msg']) ? json_decode($param['msg'],true) : [];
+            print_r($data);
+            $orderSn = $data['order_id'];
+            if ($data['status'] <> "SUCCESS") return json(['err_no' => 0,'err_tips' => "success"]);
+            if (empty($data)) return json(['err_no' => 0,'err_tips' => "success"]);
+            $order = (new SaasOrder)->where("order_sn",$orderSn)->findOrEmpty();
+            if ($order['status'] <> 4) return json(['err_no' => 0,'err_tips' => "success"]);
+            $order->status = 3;
+            $order->refund_at = getDateFull();
+            $order->save();
+            return json(['err_no' => 0,'err_tips' => "success"]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping("refund/mini")]
+    public function getOrderRefundMini(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            echo getDateFull()."===小程序退款返回\n";
+            $data = !empty($param['msg']) ? json_decode($param['msg'],true) : [];
+            print_r($data);
+            $orderSn = $data['cp_extra'];
+            if ($data['status'] <> "SUCCESS") return json(['err_no' => 0,'err_tips' => "success"]);
+            if (empty($data)) return json(['err_no' => 0,'err_tips' => "success"]);
+            $order = (new SaasOrder)->where("order_sn",$orderSn)->findOrEmpty();
+            if ($order['status'] <> 4) return json(['err_no' => 0,'err_tips' => "success"]);
+            $order->status = 3;
+            $order->refund_at = getDateFull();
+            $order->save();
+            return json(['err_no' => 0,'err_tips' => "success"]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+}

+ 336 - 0
app/controller/api/Order.php

@@ -0,0 +1,336 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\dyLife\Crypt;
+use app\extra\dyMini\Pay;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasComplaint;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderPrice;
+use app\service\saas\OrderService;
+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("/dy/order"),Middleware(AuthMiddleware::class)]
+class Order extends Base
+{
+
+
+
+    #[Inject]
+    protected SaasOrder $model;
+
+    #[Inject]
+    protected OrderService $service;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            if (!empty($param['size'])) {
+                $param['pageSize'] = $param['size'];
+            }
+            $param['openid'] = $request->user['openid'];
+            $data = $this->service->setModel()->getList($param,['product' => function($query){
+                $query->field("product_id,product_name");
+            }]);
+            return successTrans("success.data",pageFormat($data));
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[GetMapping('chat')]
+    public function getDataChat(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            if (!empty($param['size'])) {
+                $param['pageSize'] = $param['size'];
+            }
+            $param['openid'] = $request->user['openid'];
+            $param['statusIn'] = [1,2];
+            $data = $this->service->setModel()->getList($param,['product' => function($query){
+                $query->field("product_id,product_name");
+            }]);
+            return successTrans("success.data",pageFormat($data));
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[PostMapping("del")]
+    public function delOrderData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require" => trans("empty.require"),
+                "type.require"  => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = $this->model->where("order_sn",$param['order'])->with(['product','poi'])->findOrEmpty();
+            if ($order->isEmpty()) return errorTrans("empty.data");
+            if ($order['openid'] <> $request->user['openid']) return errorTrans("empty.data");
+            switch ($param['type']) {
+                case "del": // 删除订单
+                    if ($order['status'] <> 5) return error("该订单不支持删除");
+                    $state = $order->delete();
+                    break;
+                case "cancel": // 取消订单
+                    if ($order['status'] <> 0) return error("该订单不支持取消");
+                    $order->status = 5;
+                    $state = $order->save();
+                    break;
+                default:
+                    $state = false;
+                    break;
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping("refund")]
+    public function orderRefund(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require" => trans("empty.require"),
+                "refund.require" => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = $this->model->where("order_sn",$param['order'])->with(['product','poi'])->findOrEmpty();
+            if ($order->isEmpty()) return errorTrans("empty.data");
+            if ($order['openid'] <> $request->user['openid']) return errorTrans("empty.data");
+            if ($order['status'] <> 1) return error("当前订单不支持退款");
+            $reason = explode(",",$param['refund']);
+            $payParam = [
+                "order_sn"      => $order['order_sn'],
+                "total"         => $order['pay_money'],
+                "reason"        => $reason[0]??'不想要了',
+                "out_refund_no" => $order['pay_sn'],
+                "notify_url"    => "https://tran.jsshuita.cn/notify/refund"
+            ];
+            $byteAuthorization = (new Pay)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+                "salt"      => sConf("wechat.mch_salt"),
+            ])->createRefund($payParam);
+            if (!$byteAuthorization) return error("发起退款失败");
+            $order->status = 4;
+            $order->refund_apply = getDateFull();
+            $state = $order->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("提交申请成功");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[GetMapping('detail')]
+    public function getDataDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"     => trans("empty.require"),
+                "option.default"    => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            echo getDateFull()."===>订单详情\n";
+            echo json_encode($param)."\n";
+            $order = $this->model->where("out_order_no",$param['order'])->with(['product','poi'])->findOrEmpty();
+            if ($order->isEmpty()) return errorTrans("empty.data");
+            $order['end_time'] = timeDiff(strtotime("+30 minutes",strtotime($order['create_at'])),time());
+            return $this->encode("ok",$order->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[GetMapping("confirm")]
+    public function confirmOrder(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"     => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = $this->model->where("order_sn",$param['order'])->with(['product','poi'])->findOrEmpty();
+            if ($order->isEmpty()) return errorTrans("empty.data");
+            if ($order['status'] <> 0) return errorTrans("empty.data");
+            return $this->encode("ok",$order->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 差价支付
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("pricePay")]
+    public function orderPricePay(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"     => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = (new SaasOrderPrice)->where("order_sn",$param['order'])->findOrEmpty();
+            $payParam = [
+                "order_sn"      => $order['order_sn'],
+                "total"         => $order['price'],
+                "name"          => $order['name'],
+                "notify_url"    => "https://tran.jsshuita.cn/notify/price"
+            ];
+            $byteAuthorization = (new Pay)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+                "salt"      => sConf("wechat.mch_salt"),
+            ])->createOrder($payParam);
+            return success("ok",['pay' => $byteAuthorization]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 发起支付
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("toPay")]
+    public function orderPay(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"     => trans("empty.require"),
+                "mobile.default"    => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = $this->model->where("order_sn",$param['order'])->with(['product','poi'])->findOrEmpty();
+            $payParam = [
+                "order_sn"      => $order['order_sn'],
+                "total"         => $order['price'],
+                "name"          => $order['product']['product_name'],
+                "notify_url"    => "https://tran.jsshuita.cn/notify/douyin"
+            ];
+            $byteAuthorization = (new Pay)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+                "salt"      => sConf("wechat.mch_salt"),
+            ])->createOrder($payParam);
+            if (!empty($param['mobile'])) {
+                $order->mobile = $param['mobile'];
+                $order->save();
+            }
+            return success("ok",['pay' => $byteAuthorization]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 交易组件获取手机号码
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("mobile")]
+    public function getOrderMobile(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $mobile = [];
+            if (!empty($param['code'])) {
+                $mobileStr = (new Crypt)->config($this->getDyConfig())->token()->getMobile($param['code']);
+                if (!empty($mobileStr)) {
+                    $mobile = $this->decrypt2code(sConf('wechat.min_private_key'), $mobileStr);
+                }
+            }
+            $mobileStr = "";
+            if (!empty($mobile['purePhoneNumber'])) {
+                $mobileStr = $mobile['purePhoneNumber'];
+            }
+            return success("ok",['mobile' => $mobileStr]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 订单投诉
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("complaint")]
+    public function setOrderComplaint(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "content.require"   => trans("empty.require"),
+                "mobile.default"    => "",
+                "order.require"     => trans("empty.require"),
+                "text.require"      => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = (new SaasOrder)->where("out_order_no",$param['order'])->findOrEmpty();
+            if ($order->isEmpty()) return error("该笔交易不存在");
+            if ($order['openid'] <> $request->user['openid']) return error("非法操作");
+            $state = (new SaasComplaint)->insertGetId([
+                "openid"    => $request->user['openid'],
+                "poi_id"    => $order['poi_id'],
+                "content"   => $param['content'],
+                "question"  => $param['text'],
+                "order_sn"  => $param['order'],
+                "mobile"    => $param['mobile']??$order['mobile'],
+            ]);
+            if (!$state) return error("提交失败");
+            return success("提交成功,请耐心等待");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    protected function decrypt2code($private_key, $ciphertext_str) {
+        // 解码 base64 密文
+        $ciphertext = base64_decode($ciphertext_str);
+
+        // 使用私钥解密
+        openssl_private_decrypt($ciphertext, $plaintext, $private_key, OPENSSL_PKCS1_PADDING);
+
+        if ($plaintext === false) {
+            return [];
+        }
+        return json_decode($plaintext,true);
+    }
+
+
+    protected function decrypt($encrypted_data, $session_key, $iv) {
+        $data = base64_decode($encrypted_data);
+        $key = base64_decode($session_key);
+        $iv_decoded = base64_decode($iv);
+        // 使用 AES-256-CBC 模式解密
+        $decrypted = openssl_decrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv_decoded);
+
+        return $decrypted;
+    }
+
+}

+ 333 - 0
app/controller/api/Service.php

@@ -0,0 +1,333 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\tools\UploadExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasChatMsg;
+use app\model\saas\SaasChatStore;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderAddress;
+use app\model\saas\SaasOrderDelay;
+use app\model\saas\SaasStore;
+use app\model\system\SystemUser;
+use app\service\saas\ChatStoreService;
+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;
+use Webman\Push\Api;
+use Webman\RedisQueue\Redis;
+use function DI\get;
+
+
+#[Controller("/dy/service"),Middleware(AuthMiddleware::class)]
+class Service extends Base
+{
+
+    #[Inject]
+    protected ChatStoreService $service;
+
+    /**
+     * 获取客服列表-已聊过的
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping("list")]
+    public function getServiceList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            if (!empty($param['size'])) {
+                $param['pageSize'] = $param['size'];
+            }
+            $param['openid'] = $request->user['openid'];
+            $list = $this->service->setModel()->getList($param,null,true,['last','avatar'],['last' => function($data,$resp) use($param){
+                $last = (new SaasChatMsg)->where("poi_id",$resp['poi_id'])->where("openid",$param['openid'])->order("create_at desc")->field("content,create_at,type")->findOrEmpty();
+                if ($last->isEmpty()) {
+                    return ['type' => "text","content"=>"无",'num' => 0,"create_at"=> formatTime(date("Y-m-d H:i:s",time()))];
+                }
+                return ["content"=> $last['content'],'num' => 0,"create_at"=> formatTime($last['create_at']),'type' => $last['type']];
+            },'avatar' => function(){
+                return "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png";
+            }]);
+            return successTrans("success.data",pageFormat($list));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 分配客服
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("shareout")]
+    public function shareUser2Mer(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $store = (new SaasStore)->where("poi_id",$param['poi'])->findOrEmpty();
+            if ($store->isEmpty()) return error("店铺不存在");
+            $chatStore = (new SaasChatStore)->where(['poi_id'=> $store['poi_id'],"openid" => $request->user['openid']])->findOrEmpty();
+            $userAvatar = $request->user['avatar'];
+            // 获取最新的10条聊天记录
+            $msg = (new SaasChatMsg)->where(['openid' => $request->user['openid'],'poi_id' => $store['poi_id']])->append(['avatar','time'])->withAttr(['avatar' => function($data,$resp) use($userAvatar){
+                if ($resp['source'] == 1) { // 用户
+                    return $userAvatar??'';
+                } else {
+                    return "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png";
+                }
+            },'time' => function($resp,$data){
+                return strtotime($data['create_at']) * 1000;
+            }])->order("id","desc")->paginate([
+                "list_rows" => 10,
+                "page"      => 1
+            ]);
+            // 获取在线客服
+            $service = (new SystemUser)->where("store_id",$param['poi'])->where("type",3)->where("is_line",1)->findOrEmpty();
+            if ($service->isEmpty()) return $this->encode("ok",['store' => $store,'code' => 3,'sendId' => $request->user['openid'],'msg' => pageFormatMsg($msg),"serviceId" => 0]); // 无客服在线
+            if ($chatStore->isEmpty()) {
+                $chatStore->insertGetId([
+                    "poi_id"    => $store['poi_id'],
+                    "poi_name"  => $store['poi_name'],
+                    "openid"    => $request->user['openid'],
+                    "service_id"    => $service['id'],
+                    "order"     => $param['order']??'',
+                    "goods"     => $param['goods']??'',
+                    "last_at"   => getDateFull()
+                ]);
+            } else {
+                $chatStore->service_id = $service['id'];
+                $chatStore->order = $param['order']??'';
+                $chatStore->goods = $param['goods']??'';
+                $chatStore->last_at = getDateFull();
+                $chatStore->save();
+            }
+            return $this->encode("ok",['store' => $store,'code' => 1,'sendId' => $request->user['openid'],'msg' => pageFormatMsg($msg),"serviceId" => $service['id'],'avatar' => $userAvatar]); // 客服在线
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping("msg")]
+    public function getMessageList(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "poi.require"   => trans("empty.require"),
+                "page.default"  => 1,
+                "size.default"  => 10
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $userAvatar = $request->user['avatar'];
+            $msg = (new SaasChatMsg)->where(['openid' => $request->user['openid'],'poi_id' => $param['poi']])->append(['avatar','time'])->withAttr(['avatar' => function($data,$resp) use($userAvatar){
+                if ($resp['source'] == 1) {
+                    return $userAvatar??'';
+                } else {
+                    return "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png";
+                }
+            },'time' => function($resp,$data){
+                return strtotime($data['create_at']) * 1000;
+            }])->order("id","desc")->paginate([
+                "list_rows" => $param['size']??10,
+                "page"      => $param['page']??1,
+            ]);
+            return $this->encode("ok",pageFormat($msg));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 发送消息
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("send")]
+    public function sendMsg(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "content.require"   => trans("empty.require"),
+                "groupId.require"   => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+                "sendId.require"    => trans("empty.require"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            (new SaasChatMsg)->insertGetId([
+                "source"    => 1,
+                "openid"    => $request->user['openid'],
+                "content"   => is_array($param['content'])?json_encode($param['content']):$param['content'],
+                "type"      => $param['type'],
+                "msgId"     => time(),
+                "poi_id"    => $param['groupId'],
+                "service_id" => $param['sendId'],
+            ]);
+            $chatStore = (new SaasChatStore)->where(['openid' => $request->user['openid'],"service_id" => $param['sendId']])->with(['user' => function($query){
+                $query->field("openid,nickname,avatar,mobile,create_at");
+            }])->append(['last','avatar'])->withAttr(['last' => function($data,$resp) use($param){
+                $last = (new SaasChatMsg)->where(["poi_id" => $resp['poi_id'],"service_id" => $param['sendId'],"openid" => $resp['openid']])->order("create_at desc")->field("content,create_at,type")->findOrEmpty();
+                if ($last->isEmpty()) {
+                    return ['type' => "text","content"=>"无",'num' => 0,'time' => time(),"create_at"=> formatTime(date("Y-m-d H:i:s",time()))];
+                }
+                $total = (new SaasChatMsg)->where(["poi_id" => $resp['poi_id'],"service_id" => $param['sendId'],"openid" => $resp['openid'],'is_read' => 0])->count();
+                return ["content"=> str_cut_ellipsis($last['content']),'num' => ($total),"create_at"=> formatTime($last['create_at']),"time"=> strtotime($last['create_at']),'type' => $last['type']];
+            },'avatar' => function(){
+                return "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png";
+            }])->findOrEmpty();
+            if (!$chatStore->isEmpty())
+            {
+                $chatStore->save(['last_at' => getDateFull()]);
+            }
+            $chatStore['last_at'] = getDateFull();
+            $api = new Api('http://127.0.0.1:3232', config('plugin.webman.push.app.app_key'),config('plugin.webman.push.app.app_secret'));
+            $api->trigger("service-{$param['sendId']}","message",[
+                "type"  =>  $param['type'],
+                "time"  =>  time() * 1000,
+                "msgId" => time(),
+                "content"   => is_array($param['content'])?json_encode($param['content']):$param['content'],
+                "source" => 1,
+                "avatar"    => "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png",
+                "poi_id"    => $param['groupId'],
+                "openid"  => $request->user['openid'],
+                "service_id"  => $param['sendId'],
+                "create_at"  => formatTime(time()),
+                "last_at"   => getDateFull(),
+                "user"  => [
+                    "nickname"  => $request->user['nickname'],
+                    "avatar"    => $request->user['avatar']
+                ],
+                "item"  => $chatStore->toArray()
+            ]);
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 发送图片消息
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("send/img")]
+    public function sendImgMsg(Request $request): Response
+    {
+        try {
+            $resp = UploadExtend::uploadFile();
+            if (!isset($resp[0]['url'])) return error("发送失败");
+            $param = $this->_valid([
+                "groupId.require"   => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+                "sendId.require"    => trans("empty.require"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $param['content'] = $resp[0]['url'];
+            $state = (new SaasChatMsg)->insertGetId([
+                "source"    => 1,
+                "openid"    => $request->user['openid'],
+                "content"   => is_array($param['content'])?json_encode($param['content']):$param['content'],
+                "type"      => $param['type'],
+                "msgId"     => time(),
+                "poi_id"    => $param['groupId'],
+                "service_id" => $param['sendId'],
+            ]);
+            $api = new Api('http://127.0.0.1:3232', config('plugin.webman.push.app.app_key'),config('plugin.webman.push.app.app_secret'));
+            $api->trigger("service-{$param['sendId']}","message",[
+                "type"  =>  $param['type'],
+                "time"  =>  time() * 1000,
+                "msgId" => time(),
+                "content"   => is_array($param['content'])?json_encode($param['content']):$param['content'],
+                "source" => 1,
+                "avatar"    => "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png",
+                "poi_id"    => $param['groupId'],
+                "openid"  => $request->user['openid'],
+                "service_id"  => $param['sendId'],
+                "create_at"  => formatTime(time()),
+                "user"  => [
+                    "nickname" => $request->user['nickname']
+                ]
+            ]);
+            return successTrans("success.data",['path' => $param['content']],200);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 收集完善用户收货地址
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("send/address")]
+    public function setOrderAddress(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "city.require"      => trans("empty.require"),
+                "mobile.require"    => trans("empty.require"),
+                "mobile.mobile"     => trans("error.mobile"),
+                "address.require"   => trans("empty.require"),
+                "nickname.require"  => trans("empty.require"),
+                "order.require"     => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            echo getDateFull()."===地址信息\n";
+            print_r($param);
+            $cityJson = json_decode($param['city'],true);
+            $cityCode = [];
+            $city_text = "";
+            foreach ($cityJson as $k => $v){
+                $cityCode[$k] = $v['value'];
+                $city_text .= $v['text'];
+            }
+            $param['city_text'] = $city_text;
+            $param['city_code'] = json_encode($cityCode);
+            $orderData = json_decode($param['order'],true);
+            $order = (new SaasOrder)->where("out_order_no",$orderData["order"])->with(['poi'])->findOrEmpty();
+            if ($order->isEmpty()) return error("关联订单错误");
+            if ($order['openid'] <> $request->user['openid']) return error("关联订单错误");
+            if ($order['status'] <> 1) return error("关联订单错误");
+            $startTime = $order['poi']['order_start']??'09:00';
+            $endTime = $order['poi']['order_end']??'22:30';
+            $startTimeStamp = strtotime("Y-m-d {$startTime}:00");
+            $endTimeStamp = strtotime("Y-m-d {$endTime}:00");
+            $address = (new SaasOrderAddress)->where(['openid' => $request->user['openid'],'order_sn' => $orderData['order']])->findOrEmpty();
+            if (!$address->isEmpty()) return error("请勿重复提交");
+            $address->insertGetId([
+                "openid"    => $request->user['openid'],
+                "order_sn"  => $orderData['order'],
+                "city_code" => $param['city_code'],
+                "city_text" => $param['city_text'],
+                "mobile"    => $param['mobile'],
+                "address"   => $param['address'],
+                "nickname"  => $param['nickname'],
+            ]);
+            $order->status = 2;
+            $order->express_status = 1;
+            $order->save();
+            // 判断是否在核销范围
+//            if (time() > $startTimeStamp && time() < $endTimeStamp) { // 自动核销,订单状态改成待发货
+//                Redis::send("order-done",['order' => $orderData['order']]);
+//            } else { // 写入待核销定时任务队列中
+//                (new SaasOrderDelay)->insertGetId([
+//                    "order_sn"  => $orderData['order'],
+//                ]);
+//            }
+            return success("提交成功");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+}

+ 160 - 0
app/controller/api/Solution.php

@@ -0,0 +1,160 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\dyLife\Token;
+use app\extra\dyMini\Relation;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasGoods;
+use app\model\saas\SaasGoodsSku;
+use app\model\saas\SaasOrder;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\RequestMapping;
+use support\Request;
+use support\Response;
+
+
+/**
+ * 解决方案回传
+ */
+
+#[Controller("/solution")]
+class Solution extends Base
+{
+
+
+    #[RequestMapping(path: "notify")]
+    public function getSolutionData(Request $request): Response
+    {
+        try {
+            $data = $request->all();
+            echo getDateFull()."===扩展回调\n";
+            print_r($data);
+            if (isset($data['operate_type'])) {
+                if ($data['status'] == "PASS") { // 创建商品回调
+                    $goods = (new SaasGoods)->where("goods_id",$data['product_id'])->findOrEmpty();
+                    if ($goods->isEmpty()) return json(['err_no' => 0,"err_tips" => "success"]);
+                    $goods->status = 1;
+                    $goods->save();
+                }
+                if ($data['status'] == "FAIL") { // 审核拒绝
+                    $goods = (new SaasGoods)->where("goods_id",$data['product_id'])->findOrEmpty();
+                    if ($goods->isEmpty()) return json(['err_no' => 0,"err_tips" => "success"]);
+                    $goods->status = 4;
+                    $goods->reason = $data['reason'];
+                    $goods->save();
+                }
+            }
+            if (isset($data['type']))
+            {
+                $msgData = json_decode($data['msg'],true);
+                switch ($data['type'])
+                {
+                    case "pre_create_refund": // 退款回调
+                        $order = (new SaasOrder)->where("order_sn",$msgData['order_id'])->findOrEmpty();
+                        if ($order->isEmpty()) return json(['err_no' => 0,"err_tips" => "success"]);
+                        if ($order['status'] <> 1) return json(['err_no' => 0,"err_tips" => "success"]);
+                        $order->status = 4; // 退款中
+                        $order->refund_msg = json_encode($msgData['refund_reason']);
+                        $order->refund_apply = getDateFull();
+                        $order->save();
+                        return json(['err_no' => 0,"err_tips" => "success","data" => [
+                            "out_refund_no" => "R".$order['out_order_no'],
+                            "order_entry_schema"    => [
+                                "path"      => "pages/order/detail",
+                                "params"    => json_encode([
+                                    "order" => $order['out_order_no']
+                                ])
+                            ],
+                            "notify_url"    => "https://tran.jsshuita.cn/notify/refund"
+                        ]]);
+                        break;
+                    case "pre_create_order": // 预下单
+                        echo getDateFull()."====预下单\n";
+                        print_r($msgData);
+                        if (!isset($msgData['item_order_info_list'][0]['goods_id'])) return json(['err_no' => 0,"err_tips" => "success"]);
+                        if (empty($msgData['cp_extra'])) return json(['err_no' => 0,"err_tips" => "success"]);
+                        $cp_extra = json_decode($msgData['cp_extra'],true);
+                        $skuGoods = [];
+                        if ($cp_extra['product_id'] <> $cp_extra['sku_id']) { // 多规格下单
+                            $skuGoods = (new SaasGoodsSku)->where(['product_id' => $cp_extra['product_id'],'sku_id' => $cp_extra['sku_id']])->findOrEmpty();
+                        }
+                        $goods = (new SaasGoods)->where("goods_id",$msgData['item_order_info_list'][0]['goods_id'])->findOrEmpty();
+                        if ($goods->isEmpty()) return json(['err_no' => 0,"err_tips" => "success"]);
+                        $order = (new SaasOrder)->where("order_sn",$msgData['order_id'])->findOrEmpty();
+                        if (!$order->isEmpty()) return json(['err_no' => 0,"err_tips" => "success"]);
+                        $orderSn = "AL".CodeExtend::uniqidDate(18).rand(100,999);
+                        $goodsImg = is_string($goods['image_list'])?json_decode($goods['image_list'],true):$goods['image_list'];
+                        $order->insertGetId([
+                            "out_order_no"  => $orderSn,
+                            "order_sn"  => $msgData['order_id'],
+                            "openid"    => $msgData['open_id'],
+                            "goods_id"  => $goods['id'],
+                            "life_goods_id" => $goods['goods_id'],
+                            "product_id"    => $goods['product_id'],
+                            "sku_id"        => $cp_extra['sku_id'],
+                            "number"        => $msgData['goods'][0]['quantity']??1,
+                            "price"         => $msgData['item_order_info_list'][0]['price']?:$goods['price'],
+                            "custom_price"  => empty($skuGoods)?$goods['custom_price']:$skuGoods['custom_price'], // 补差价金额
+                            "line_price"    => $goods['line_price']*100,
+                            "poi_id"        => $goods['poi_id'],
+                            "img"           => $goodsImg[0]['url'],
+                            "sku_name"      => $cp_extra['name'],
+                            "mobile"        => $msgData['phone_num']??'',
+                            "pay_money"     => $msgData['total_amount']??0,
+                        ]);
+                        return json(['err_no' => 0,"err_tips" => "success","data" => [
+                            "order_entry_schema"    => [
+                                "path"      => "pages/order/detail",
+                                "params"    => json_encode([
+                                    "order" => $orderSn
+                                ])
+                            ],
+                            "out_order_no"  => $orderSn,
+                            "pay_expire_seconds"    => 1800,
+                            "pay_notify_url"        => "https://tran.jsshuita.cn/notify/douyin"
+                        ]]);
+                        break;
+                    default:
+                        break;
+                }
+            }
+            return json(['err_no' => 0,"err_tips" => "success"]);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            echo $throwable->getLine()."\n";
+            echo $throwable->getFile()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping("token")]
+    public function setTestToken(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "code.require"  => ""
+            ]);
+            if (!is_array($param)) return error($param);
+            $data = (new Token)->config($this->getDyConfig())->token()->getAccessTokenUser($param['code']);
+            return success("ok",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping("test")]
+    public function setTest()
+    {
+        try {
+            $data = (new Relation)->config($this->getDyConfig())->token()->createOrderFirst(['1865253598118939']);
+            return success("ok",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 37 - 0
app/controller/api/User.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasUserOpen;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/dy/user"),Middleware(AuthMiddleware::class)]
+class User extends Base
+{
+
+
+    #[Inject]
+    protected SaasUserOpen $model;
+
+    #[GetMapping("data")]
+    public function getUserData(Request $request): Response
+    {
+        try {
+            $data = $this->model->where("openid",$request->user['openid'])->field("openid,nickname,avatar")->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("error.data");
+            if (empty($data['avatar'])) $data['avatar'] = "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png";
+            return success("ok",$data->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,138 @@
+<?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 ($user['type'] > 1) {
+            $typeUser = $this->getTypeUser($user['store_id']);
+            if (empty($typeUser)) return [0,trans("empty.agent"),[]];
+            if ($typeUser['status'] <> 1) return [0,trans("error.agent"),[]];
+            if (time() > strtotime($typeUser['vip_end'])) return [0,trans("error.agent-out"),[]];
+            $user['shop_name'] = $typeUser['poi_name'];
+        }
+        if ($type == 2) {
+            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()];
+    }
+
+
+    /**
+     * 获取代理信息
+     * @param int $agentId
+     * @return array
+     */
+    protected function getTypeUser(int $agentId = 0): array
+    {
+        return (new SaasStore)->where("poi_id",$agentId)->findOrEmpty()->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'],
+                "vip_at"    => $userData['vip_end'],
+                "agent_id"  => $userData['agent_id'],
+                "store_id"  => $userData['store_id'],
+                "super"     => $userData['is_super'],
+                "shop"      => $agent['poi_name']??'',
+                "trade"     => $agent['trade_type']??0,
+                "type"      => $userData['type']
+            ]);
+        } 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());
+        }
+    }
+
+}

+ 67 - 0
app/controller/merchant/Category.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\tools\DataExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasStoreCategory;
+use app\service\saas\CategoryService;
+use app\service\saas\GoodsService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/api/merchant/category"),Middleware(AuthMiddleware::class)]
+class Category extends Base
+{
+
+
+    #[Inject]
+    protected CategoryService $service;
+
+    #[Inject]
+    protected GoodsService $goodsService;
+
+    #[Inject]
+    protected SaasStoreCategory $model;
+
+    #[GetMapping('list')]
+    public function getCategoryList(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "poi.default"   => $request->user['store_id']
+            ]);
+            if (!is_array($param)) return error($param);
+            $data = $this->service->setModel()->getList([],null,false);
+            $tree = DataExtend::arr2tree($data,"category_id","parent_id");
+            $storeCate = $this->model->where("poi_id",$param['poi'])->column('product_type',"category_id");
+            if (empty($storeCate)) return successTrans(100010,[],200);
+            $storeIds = [];
+            $storeTypes = [];
+            foreach ($storeCate as $k=>$v){
+                $storeIds[] = $k;
+                $productType = !empty($v) ? json_decode($v,true) : [];
+                $typeData = [];
+                foreach ($this->goodsService->productType() as $key => $value) {
+                    if (in_array($value['key'],$productType)) {
+                        $typeData[] = $value;
+                    }
+                }
+                $storeTypes[$k] = $typeData;
+            }
+            $resp = filterTreeByTargets($tree,$storeIds);
+            addTypesRecursive($resp,$storeTypes);
+            return successTrans(100010,$resp,200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+}

+ 85 - 0
app/controller/merchant/Complaint.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasComplaint;
+use app\service\saas\ComplaintService;
+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("/api/merchant/complaint"),Middleware(AuthMiddleware::class)]
+class Complaint extends Base
+{
+
+    #[Inject]
+    protected ComplaintService $service;
+
+    #[Inject]
+    protected SaasComplaint $model;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            $data = $this->service->setModel()->getList($param,['orders' => function($query){
+                $query->field("out_order_no,product_id");
+            }]);
+            return successTrans("success.data",pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping('detail')]
+    public function getDataDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = $this->model->where("id",$param["id"])->with(['store' => function($query){
+                $query->field("poi_id,poi_name,poi_address,poi_city");
+            }])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("empty.data");
+            return successTrans("success.data",$data->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[PostMapping('save')]
+    public function setDataDetail(Request $request): Response
+    {
+        try {
+            [$state,$msg] = $this->checkDeposit($request->user['store_id']);
+            if (!$state) return error($msg);
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+                "remark.require"    => trans("empty.require")
+            ],$request->method());
+            $data = $this->model->where("id",$param["id"])->findOrEmpty();
+            if ($data->isEmpty()) return errorTrans("empty.data");
+            if ($data['status'] <> 0) return error("请勿重复回复");
+            $data->status = 1;
+            $data->remark = $param['remark'];
+            $state = $data->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+}

+ 52 - 0
app/controller/merchant/Deposit.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasStore;
+use app\service\saas\DepositService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+/**
+ * 保证金
+ */
+
+#[Controller("/api/merchant/deposit"),Middleware(AuthMiddleware::class)]
+class Deposit extends Base
+{
+
+    #[Inject]
+    protected DepositService $service;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            $data = $this->service->setModel()->getList($param);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[GetMapping('data')]
+    public function getDepositData(Request $request): Response
+    {
+        try {
+            $deposit = (new SaasStore)->where("poi_id",$request->user['store_id'])->value("deposit");
+            return success('ok',compact("deposit"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 20 - 0
app/controller/merchant/Finance.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/api/merchant/finance"),Middleware(AuthMiddleware::class)]
+class Finance extends Base
+{
+
+
+
+}

+ 287 - 0
app/controller/merchant/Goods.php

@@ -0,0 +1,287 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\dyLife\data\BaseData;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\middleware\DepositMiddleware;
+use app\model\saas\SaasGoods;
+use app\model\saas\SaasGoodsSku;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasStore;
+use app\service\saas\GoodsService;
+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("/api/merchant/goods"),Middleware(AuthMiddleware::class)]
+class Goods extends Base
+{
+
+    #[Inject]
+    protected GoodsService $service;
+
+    #[Inject]
+    protected SaasGoods $model;
+
+    #[Inject]
+    protected SaasGoodsSku $skuModel;
+
+    #[Inject]
+    protected SaasStore $store;
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            if (!empty($param['status'])) $param['status'] = intval($param['status']+1);
+            $productType = $this->service->productType();
+            $data = $this->service->setModel()->getList($param,null,true,['types'],['types' => function($query,$resp) use($productType){
+                $productTypeArr = [];
+                foreach ($productType as $val) {
+                    $productTypeArr[$val['key']] = $val['name'];
+                }
+                return $productTypeArr[$resp['product_type']]??'';
+            }]);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * 商品详情
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping('detail')]
+    public function getGoodDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"            => trans("empty.require"),
+                "product_id.require"    => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $detail = $this->model->where("id",$param["id"])->with(['skuSpecs'])->findOrEmpty();
+            if ($detail->isEmpty()) return errorTrans("empty.data");
+            if ($detail['poi_id'] <> $request->user['store_id']) return errorTrans("error.param");
+            $detail['sold_start_times'] = [(string)$detail['sold_start_time'],(string)$detail['sold_end_time']];
+            $detail['use_times'] = null;
+            if ($detail['use_date_type'] == 2) {
+                $detail['use_times'] = [(string)$detail['use_time_start'],(string)$detail['use_time_end']];
+            }
+            return successTrans('success.data',$detail->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("template")]
+    public function getTemplate(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "product_type.require"  => trans("empty.require"),
+                "category.require"      => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = (new BaseData)->config([
+                "appid"     => sConf("wechat.appid"),
+                "secret"    => sConf("wechat.secret"),
+                "account"   => sConf("wechat.shop_id"),
+            ])->token()->getStoreCategoryTemplate($param['category'][2],$param['product_type']);
+            return success("ok",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping('type')]
+    public function getProductType(Request $request): Response
+    {
+        try {
+            $poiId = $request->user['store_id'];
+            $productType = $this->store->where("poi_id", $poiId)->value("product_type");
+            if (empty($productType)) return error("未配置");
+            $typeData = [];
+            foreach ($this->service->productType() as $key => $value) {
+                if (in_array($value['key'],json_decode($productType,true))) {
+                    $typeData[] = $value;
+                }
+            }
+            return success('ok', $typeData);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * 新增商品
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("save")]
+    public function saveGoods(Request $request): Response
+    {
+        try {
+            [$state,$msg] = $this->checkDeposit($request->user['store_id']);
+            if (!$state) return error($msg);
+            $param = $request->post();
+            if (empty($param['auto_renew'])) $param['auto_renew'] = 0;
+            $param['image_list'] = empty($param['image_list']) ? [] : json_encode($param['image_list']);
+            $param['detail_image_list'] = empty($param['detail_image_list']) ? [] : json_encode($param['detail_image_list']);
+            $param['environment_image_list'] = empty($param['environment_image_list']) ? [] : json_encode($param['environment_image_list']);
+//            $param['notification'] = empty($param['notification']) ? [] : json_encode($param['notification']);
+            if (empty($param['specs'])) return error("至少添加一组搭配信息");
+            if (is_array($param['specs'])) $param['specs'] = json_encode($param['specs']);
+            if (is_array($param['category'])) {
+                $param['category_id'] = $param['category'][2]??0;
+                $param['category'] = json_encode($param['category']);
+            }
+            if ($param['day_duration'] < 30) return error("顾客可消费日期不可低于30天");
+            if (!isset($param['id'])) {
+                $param['product_id'] = strtoupper(CodeExtend::random(18,3));
+            }
+            $param['price'] = $param['price']*100;
+            $param['poi_id'] = $request->user['store_id'];
+            if (!empty($param['no_use_weeks']) && count($param['no_use_weeks']) == 7) return error("至少需要保留一天可用");
+            $skuData = [];
+            if (is_array($param['skuSpecs'])) {
+                foreach ($param['skuSpecs'] as $key => $value) {
+                    $skuData[$key] = $value;
+                    $skuData[$key]['product_id'] = $param['product_id'];
+                    $skuData[$key]['specs'] = json_encode($value['specs']);
+                }
+            }
+            if (!isset($param['id'])) {
+                $param['spu_id'] = CodeExtend::uniqidDate(18);
+            } else {
+                $param['status'] = 0;
+            }
+            unset($param['skuSpecs']);
+            $param['line_price'] = $param['line_price'] * 100;
+//            if (empty($param['notification']))  return error("请最少添加一组添加使用规则");
+//            foreach (json_decode($param['notification'],true) as $key => $value) {
+//                if (empty($value['title'])) return error("规则标题不能为空");
+//                if (empty($value['content'])) return error("规则内容不能为空");
+//            }
+            $store = (new SaasStore)->where("poi_id",$request->user['store_id'])->findOrEmpty();
+            if ($store->isEmpty()) return errorTrans("error.data");
+            // 获取attr_key_value_map参数
+            $valMap = (new BaseData)->config([
+                "appid"     => sConf("wechat.appid"),
+                "secret"    => sConf("wechat.secret"),
+                "account"   => sConf("wechat.shop_id"),
+            ])->token()->getStoreCategoryTemplate($param['category_id'],$param['product_type']);
+            if (empty($valMap['product_attrs'])) return errorTrans("error.data");
+            $data = (new BaseData)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->createGoodsData($param,$skuData,$store->toArray(),$valMap);
+//            return errorTrans("error.data");
+            if (empty($data['product_id'])) return error($data['description']);
+            $param['goods_id'] = $data['product_id'];
+//            return errorTrans("error.data");
+            $stateSku = false;
+            if (!empty($skuData)) {
+                [$stateSku,$msg] = $this->skuData($skuData);
+                if (!$stateSku) return error($msg);
+            }
+            $state = $this->model->setAutoData($param);
+            if (!$state && !$stateSku) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getLine()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getMessage()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * SKU编辑
+     * @param array $skuData
+     * @return array
+     */
+    protected function skuData(array $skuData = []): array
+    {
+        try {
+            if (empty($skuData)) return [0,trans('empty.require')];
+            foreach ($skuData as $vo) {
+                $sku = $this->skuModel->where("sku_id",$vo['sku_id'])->findOrEmpty();
+                if ($sku->isEmpty()) {
+                    $sku->insertGetId($vo);
+                } else {
+                    $sku->update($vo);
+                }
+            }
+            return [1,'success'];
+        } catch (\Throwable $throwable) {
+            return [0,$throwable->getMessage()];
+        }
+    }
+
+
+    /**
+     * 商品下架
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("off")]
+    public function offGood2Life(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"            => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $detail = $this->model->where("id",$param["id"])->with(['skuSpecs'])->findOrEmpty();
+            if ($detail->isEmpty()) return errorTrans("empty.data");
+            if ($detail['poi_id'] <> $request->user['store_id']) return errorTrans("error.param");
+            $store = (new SaasStore)->where("poi_id",$request->user['store_id'])->findOrEmpty();
+            if ($store->isEmpty()) return errorTrans("error.data");
+            $data = (new BaseData)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->goodsOffOn(2,$store['store_id'],$detail['goods_id']);
+            if ($data['error_code'] <> 0) return error("操作失败,请稍后重试");
+            $detail->status = 2;
+            $detail->save();
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("del")]
+    public function delGoods(Request $request)
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"            => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $detail = $this->model->where("id",$param["id"])->findOrEmpty();
+            if ($detail->isEmpty()) return errorTrans("empty.data");
+            if ($detail['poi_id'] <> $request->user['store_id']) return error("非法操作");
+            $state = $detail->delete();
+            if (!$state) return errorTrans("error.data");
+            (new SaasOrder)->where("product_id",$detail["product_id"])->delete();
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+}

+ 151 - 0
app/controller/merchant/Order.php

@@ -0,0 +1,151 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\dyLife\data\OrderData;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasStore;
+use app\service\saas\GoodsService;
+use app\service\saas\OrderService;
+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("/api/merchant/order"),Middleware(AuthMiddleware::class)]
+class Order extends Base
+{
+
+
+    #[Inject]
+    protected OrderService $service;
+
+    #[Inject]
+    protected SaasOrder $model;
+
+    #[Inject]
+    protected GoodsService $goodsService;
+
+    #[GetMapping('list')]
+    public function getOrderList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            $productType = $this->goodsService->productType();
+            $data = $this->service->setModel()->getList($param,['product' => function($query) use($productType){
+                $query->field("product_id,product_name,product_type")->append(['types'])->withAttr(['types' => function($query,$resp) use($productType){
+                    $productTypeArr = [];
+                    foreach ($productType as $val) {
+                        $productTypeArr[$val['key']] = $val['name'];
+                    }
+                    return $productTypeArr[$resp['product_type']]??'';
+                }]);
+            },'user' => function($query){
+                $query->field("openid,nickname,avatar");
+            }],true,['end_time'],["end_time" => function ($resp,$next) {
+                return timeDiff(strtotime("+30 minutes",strtotime($next['create_at'])),time());
+            }]);
+            return successTrans("success.data",pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping('detail')]
+    public function getOrderDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require" => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = $this->model->where("out_order_no",$param['order'])->with(['product','poi','user','address'])->findOrEmpty();
+            if ($order->isEmpty()) return errorTrans("empty.data");
+            if ($order['poi_id'] <> $request->user['store_id'] && $request->user['is_super'] == 0) return errorTrans("empty.data");
+            $order['end_time'] = timeDiff(strtotime("+30 minutes",strtotime($order['create_at'])),time());
+            return successTrans("success.data",$order->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 手动核销
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('done')]
+    public function doneOrder(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = $this->model->where("order_sn",$param['id'])->with(['product','poi','user'])->findOrEmpty();
+            if ($order->isEmpty()) return errorTrans("empty.data");
+            if ($order['poi_id'] <> $request->user['store_id']) return errorTrans("empty.data");
+            if ($order['status'] <> 1) return error("该订单状态不支持核销");
+            $store = (new SaasStore)->where("poi_id",$request->user['store_id'])->findOrEmpty();
+            if ($store->isEmpty()) return errorTrans("error.data");
+            // 获取到订单encrypted_code
+            $orderCode = (new OrderData)->config([
+                "appid"     => sConf("wechat.appid"),
+                "secret"    => sConf("wechat.secret"),
+                "account"   => sConf("wechat.shop_id"),
+            ])->token()->getCertificate($order['order_sn'],$store['store_id']);
+            if (empty($orderCode['certificates'])) return error("获取订单数据失败");
+            if (!isset($orderCode['certificates'][0]['encrypted_code'])) return error("获取订单数据失败");
+            if ($orderCode['certificates'][0]['status'] <> 1) return error("该订单状态不支持核销");
+            // 核销订单
+            $orderDone = (new OrderData)->config([
+                "appid"     => sConf("wechat.appid"),
+                "secret"    => sConf("wechat.secret"),
+                "account"   => sConf("wechat.shop_id"),
+            ])->token()->verifyCertificate([
+                "account_id"    => $store['store_id'],
+                "pay_sn"        => $order['pay_sn'],
+                "encrypted_code"    => $orderCode['certificates'][0]['encrypted_code'],
+                "order_id"      => $order['order_sn'],
+                "poi_id"        => $order['poi_id']
+            ]);
+            if (empty($orderDone['verify_results'])) return error("核销失败");
+            if (!isset($orderDone['verify_results'][0]['verify_id'])) return error("核销失败");
+            if (!isset($orderDone['verify_results'][0]['certificate_id'])) return error("核销失败");
+            $order->status = 2;
+            $order->verify_id = $orderDone['verify_results'][0]['verify_id'];
+            $order->certificate_id = $orderDone['verify_results'][0]['certificate_id'];
+            $order->done_at = getDateFull();
+            $state = $order->save();
+            if (!$state) return error("核销失败");
+            return success("核销成功");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 三级城市信息
+     * @param Request $request
+     * @return Response
+     */
+    #[GetMapping("city")]
+    public function getCityJson(Request $request): Response
+    {
+        try {
+            $data = json_decode(file_get_contents(base_path()."/city.json"),true);
+            return successTrans("success.data",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 36 - 0
app/controller/merchant/OrderPrice.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\controller\merchant;
+
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\service\saas\OrderPriceService;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+#[Controller("/api/merchant/order/price"),Middleware(AuthMiddleware::class)]
+class OrderPrice extends Base
+{
+
+    #[Inject]
+    protected OrderPriceService $service;
+
+    #[GetMapping('list')]
+    public function getOrderList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            $data = $this->service->setModel()->getList($param);
+            return successTrans("success.data",pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 282 - 0
app/controller/merchant/Plan.php

@@ -0,0 +1,282 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\dyMini\PlanLive;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasLivePlan;
+use app\model\saas\SaasLivePlanGoods;
+use app\model\saas\SaasLivePlanStar;
+use app\service\saas\GoodsService;
+use app\service\saas\PlanService;
+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;
+use function DI\get;
+use function Laravel\Prompts\number;
+
+
+#[Controller("/api/merchant/plan"),Middleware(AuthMiddleware::class)]
+class Plan extends Base
+{
+
+    #[Inject]
+    protected PlanService $service;
+
+    #[Inject]
+    protected GoodsService $goodsService;
+
+    #[Inject]
+    protected SaasLivePlan $model;
+
+    #[GetMapping('list')]
+    public function getPlanList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            $data = $this->service->setModel()->getList($param);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 发布佣金计划
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping('save')]
+    public function addPlanData(Request $request): Response
+    {
+        try {
+            [$state,$msg] = $this->checkDeposit($request->user['store_id']);
+            if (!$state) return error($msg);
+            $param = $this->_valid([
+                "plan_name.require"         => trans("empty.require"),
+                "merchant_phone.require"    => trans("empty.require"),
+                "merchant_phone.mobile"     => trans("error.mobile"),
+                "status.require"            => trans("empty.require"),
+                "goods.require"             => trans("empty.require"),
+                "star.require"              => trans("empty.require"),
+                "plan_id.default"           => 0,
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $goodsData = is_string($param['goods']) ? json_decode($param['goods'],true) : $param['goods'];
+            $starData = is_string($param['star']) ? json_decode($param['star'],true) : $param['star'];
+            $plan = $this->model->where("plan_id",$param['plan_id'])->findOrEmpty();
+            $liveDyStar = $liveDyGoods = [];
+            foreach ($goodsData as $k => $v) {
+                $liveDyGoods[$k] = [
+                    "commission_rate"   => round($v['rate'] * 100),
+                    "product_id"        => $v['goods_id'],
+                ];
+            }
+            foreach ($starData as $k => $v) {
+                $liveDyStar[$k] = $v['unique_id'];
+            }
+            $resp = (new PlanLive)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->planData($param['plan_id'],['name' => $param['plan_name'],'mobile' => $param['merchant_phone'],'goods' => $liveDyGoods,'star' => $liveDyStar]);
+            if (isset($resp['msg'])) return error($resp['msg']);
+            if (!isset($resp['plan_id'])) return error("发布直播计划失败");
+            if ($plan->isEmpty()) { // 新增
+                $state = $plan->insertGetId([
+                    "plan_id"           => $resp['plan_id'],
+                    "poi_id"            => $request->user['store_id'],
+                    "plan_name"         => $param['plan_name'],
+                    "merchant_phone"    => $param['merchant_phone'],
+                    "status"            => $param['status'],
+                ]);
+            } else { // 编辑
+                (new SaasLivePlanGoods)->where("plan_id",$resp['plan_id'])->delete();
+                (new SaasLivePlanStar)->where("plan_id",$resp['plan_id'])->delete();
+                $state = $plan->save([
+                    "plan_name"         => $param['plan_name'],
+                    "merchant_phone"    => $param['merchant_phone'],
+                    "status"            => $param['status'],
+                ]);
+            }
+            $goodsAll = $starAll = [];
+            foreach ($liveDyGoods as $k => $v) {
+                $goodsAll[$k] = [
+                    "plan_id"           => $resp['plan_id'],
+                    "goods_id"          => $v['product_id'],
+                    "commission_rate"   => $v['commission_rate'] / 100,
+                ];
+            }
+            (new SaasLivePlanGoods)->insertAll($goodsAll);
+            foreach ($liveDyStar as $k => $v) {
+                $starAll[$k] = [
+                    "plan_id"   => $resp['plan_id'],
+                    "unique_id" => $v
+                ];
+            }
+            (new SaasLivePlanStar)->insertAll($starAll);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[GetMapping('detail')]
+    public function detailPlan(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"         => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $productType = $this->goodsService->productType();
+            $plan = $this->model->where("id",$param['id'])->with(['goods' => function ($query) use($productType){
+                $query->with(['goods' => function($subQue) use($productType){
+                    $subQue->field("goods_id,product_name,image_list,product_id,price,line_price,limit_use_rule,use_num_per_consume,status,product_type")->append(['types'])->withAttr(['types' => function($query,$resp) use($productType){
+                        $productTypeArr = [];
+                        foreach ($productType as $val) {
+                            $productTypeArr[$val['key']] = $val['name'];
+                        }
+                        return $productTypeArr[$resp['product_type']]??'';
+                    }]);
+                }]);
+            },'star' => function ($query) {
+                $query->with(['star' => function($subQue){
+                    $subQue->field("unique_id,avatar_url,show_nickname,fans_count,fans_tag_list,local_fans_count,talent_item_level_display,talent_live_level_display,content_level_display,credit_score_display,city_name,rank_display,status");
+                }]);
+            }])->findOrEmpty();
+            if ($plan->isEmpty()) return errorTrans("empty.data");
+            if ($plan['poi_id'] <> $request->user['store_id']) return errorTrans("empty.data");
+            return successTrans("success.data",$plan->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping('goods')]
+    public function setGoodsRate(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+                "data.require"      => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $plan = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($plan->isEmpty()) return errorTrans("empty.data");
+            if ($plan['poi_id'] <> $request->user['store_id']) return error("非法操作"); // return errorTrans("empty.data");
+            $planData = is_string($param['data']) ? json_decode($param['data'],true) : $param['data'];
+            $editData = [];
+            $editPlanData = [
+                'name'      => $plan['plan_name'],
+                'mobile'    => $plan['merchant_phone'],
+            ];
+            if ($param['type'] == 'goods') {
+                foreach ($planData as $k => $v) {
+                    $commission_rate = $v['commission_rate'] * 100;
+                    echo $commission_rate."\n";
+                    $editData[$k] = [
+                        "commission_rate"   => round($commission_rate),
+                        "product_id"        => $v['goods_id'],
+                    ];
+                }
+                $editPlanData['goods'] = $editData;
+            }
+            if ($param['type'] == 'star') {
+                foreach ($planData as $k => $v) {
+                    $editData[$k] = $v['unique_id'];
+                }
+                $editPlanData['star'] = $editData;
+            }
+            $resp = (new PlanLive)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->planData($plan['plan_id'],$editPlanData);
+            if (isset($resp['msg'])) return error($resp['msg']);
+            $goodsAll = $starAll = [];
+            $state = false;
+            if ($param['type'] == 'goods') {
+                (new SaasLivePlanGoods)->where("plan_id",$plan['plan_id'])->delete();
+                foreach ($planData as $k => $v) {
+                    $goodsAll[$k] = [
+                        "plan_id"           => $resp['plan_id'],
+                        "goods_id"          => $v['goods_id'],
+                        "commission_rate"   => $v['commission_rate'],
+                    ];
+                }
+                $state = (new SaasLivePlanGoods)->insertAll($goodsAll);
+            }
+            if ($param['type'] == 'star') {
+                (new SaasLivePlanStar)->where("plan_id",$plan['plan_id'])->delete();
+                foreach ($planData as $k => $v) {
+                    $starAll[$k] = [
+                        "plan_id"   => $resp['plan_id'],
+                        "unique_id" => $v
+                    ];
+                }
+                $state = (new SaasLivePlanStar)->insertAll($starAll);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping('close')]
+    public function closePlan(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $plan = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($plan->isEmpty()) return errorTrans("empty.data");
+            if ($plan['poi_id'] <> $request->user['store_id']) return error("非法操作");
+            $resp = (new PlanLive)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->planClose($plan['plan_id']);
+            if (isset($resp['msg'])) return error($resp['msg']);
+            $plan->status = 2;
+            $state = $plan->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[PostMapping('del')]
+    public function delPlan(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $plan = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($plan->isEmpty()) return errorTrans("empty.data");
+            if ($plan['poi_id'] <> $request->user['store_id']) return error("非法操作");
+            if  ($plan['status'] <> 2) return error("该计划暂不支持删除");
+            $state = $plan->delete();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+}

Plik diff jest za duży
+ 157 - 0
app/controller/merchant/Star.php


+ 80 - 0
app/controller/merchant/User.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasStore;
+use app\model\system\SystemUser;
+use app\service\system\UserService;
+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("/api/merchant/user"),Middleware(AuthMiddleware::class)]
+class User extends Base
+{
+    #[Inject]
+    protected UserService $service;
+
+    #[Inject]
+    protected SystemUser $model;
+
+
+    #[GetMapping('list')]
+    public function getDataList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['type'] = 3;
+            $param['store_id'] = $request->user['store_id'];
+            $data = $this->service->setModel()->getList($param,"account");
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * 账户列表
+     * @param Request $request
+     * @return Response
+     */
+    #[PostMapping("save")]
+    public function setUserData(Request $request): Response
+    {
+        try {
+            [$state,$msg] = $this->checkDeposit($request->user['store_id']);
+            if (!$state) return error($msg);
+            $param = $request->post();
+            if(!isset($param['id'])) // 新增
+            {
+                $param['salt'] = CodeExtend::random(6,3);
+                $param['password'] = md5($param['password'].$param['salt']);
+                $param['create_ip'] = $request->getRealIp();
+                $param['store_id'] = $request->user['store_id'];
+                $user = $this->model->where("username",$param['username'])->findOrEmpty();
+                if (!$user->isEmpty()) return errorTrans(20011);
+            }
+            if (isset($param['app_id'])) {
+                $param['app_id'] = json_encode($param['app_id']);
+            }
+            if(isset($param['role_path']) && is_array($param['role_path'])){
+                $parent = $param['role_path'];
+                $param['role_path'] = implode(",",$parent);
+                $param['role_id'] = $parent[count($parent) - 1];
+            }
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans(100011);
+            return successTrans(100010);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+}

+ 90 - 0
app/controller/notify/Life.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace app\controller\notify;
+
+use app\extra\basic\Base;
+use app\extra\dyMini\PlanLive;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\PostMapping;
+use support\Request;
+use support\Response;
+
+
+/**
+ * 生活应用解决方案接入
+ */
+#[Controller("/life")]
+class Life extends Base
+{
+
+
+    #[GetMapping("test")]
+    public function test2data(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"     => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $data = (new PlanLive)->config([
+                "appid"     => sConf("wechat.mini_appid"),
+                "secret"    => sConf("wechat.mini_secret"),
+            ])->token()->planOrder($param['order']);
+            return success('success',$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("spi")]
+    public function getLifeSpi(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            echo getDateFull()."===自研应用回调===\n";
+            print_r($param);
+            return error("ddd");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+    #[PostMapping("hook")]
+    public function getLifeHook(Request $request): Response
+    {
+        try {
+            $data = $request->all();
+            echo getDateFull()."===自研应用回调WebHook===\n";
+            print_r($data);
+            if (isset($data['event']) && $data['event'] == "verify_webhook") {
+                return json(['challenge' => $data['content']['challenge']]);
+            }
+            return error("ddd");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+
+    #[PostMapping("mini")]
+    public function getMiniHook(Request $request): Response
+    {
+        try {
+            $data = $request->all();
+            echo getDateFull()."===小程序WebHook===\n";
+            print_r($data);
+            if (isset($data['event']) && $data['event'] == "verify_webhook") {
+                return json(['challenge' => $data['content']['challenge']]);
+            }
+            return error("ddd");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 37 - 0
app/controller/service/Heart.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\controller\service;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemUser;
+use LinFly\Annotation\Attributes\Route\Controller;
+use LinFly\Annotation\Attributes\Route\GetMapping;
+use LinFly\Annotation\Attributes\Route\Middleware;
+use support\Request;
+use support\Response;
+
+
+#[Controller("/api/service/heart"),Middleware(AuthMiddleware::class)]
+class Heart extends Base
+{
+
+
+    #[GetMapping("beat")]
+    public function setHeartBeat(Request $request): Response
+    {
+        try {
+            $user = (new SystemUser)->where("id",$request->user['id'])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("empty.data");
+            if ($user['is_line'] == 0) {
+                $user->is_line = 1;
+            }
+            $user->last_active_at = getDateFull();
+            $user->save();
+            return success("ok");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 60 - 0
app/controller/service/Quick.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace app\controller\service;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasQuick;
+use app\service\saas\QuickService;
+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("/api/service/quick"),Middleware(AuthMiddleware::class)]
+class Quick extends Base
+{
+
+    #[Inject]
+    protected QuickService $service;
+
+    #[Inject]
+    protected SaasQuick $model;
+
+    #[GetMapping('list')]
+    public function getQuickList(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $param['service_id'] = $request->user['id'];
+            $data = $this->service->setModel()->getList($param);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[PostMapping("save")]
+    public function setQuickData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "content.require"   => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $state = $this->model->insertGetId([
+                "service_id"    => $request->user['id'],
+                "content"       => $param['content']
+            ]);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 223 - 0
app/controller/service/Service.php

@@ -0,0 +1,223 @@
+<?php
+
+namespace app\controller\service;
+
+use app\extra\basic\Base;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasChatMsg;
+use app\model\saas\SaasChatStore;
+use app\model\saas\SaasOrderPrice;
+use app\service\saas\ChatStoreService;
+use app\service\saas\GoodsService;
+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;
+use Webman\Push\Api;
+
+
+#[Controller("/api/service/chat"),Middleware(AuthMiddleware::class)]
+class Service extends Base
+{
+
+    #[Inject]
+    protected ChatStoreService $service;
+
+    #[Inject]
+    protected SaasChatStore $model;
+
+    #[Inject]
+    protected GoodsService $goodsService;
+
+
+//    protected array $noNeedLogin = ["getServiceData"];
+
+    #[GetMapping('list')]
+    public function getServiceData(Request $request): Response
+    {
+
+        try {
+            $param = $request->all();
+            $param['poi_id'] = $request->user['store_id'];
+            $param['service_id'] = $request->user['id'];
+            $param['order'] = "descending";
+            $param['field'] = "last_at";
+            $data = $this->service->setModel()->getList($param,['user' => function($query){
+                $query->field("openid,nickname,avatar,mobile,create_at");
+            }],true,['last','avatar'],['last' => function($data,$resp) use($param){
+                $last = (new SaasChatMsg)->where(["poi_id" => $resp['poi_id'],"service_id" => $param['service_id'],"openid" => $resp['openid']])->order("create_at desc")->field("content,create_at,type")->findOrEmpty();
+                if ($last->isEmpty()) {
+                    return ['type' => "text","content"=>"无",'num' => 0,'time' => time(),"create_at"=> formatTime(date("Y-m-d H:i:s",time()))];
+                }
+                $total = (new SaasChatMsg)->where(["poi_id" => $resp['poi_id'],"service_id" => $param['service_id'],"openid" => $resp['openid'],'is_read' => 0])->count();
+                return ["content"=> str_cut_ellipsis($last['content']),'num' => $total,"create_at"=> formatTime($last['create_at']),"time"=> strtotime($last['create_at']),'type' => $last['type']];
+            },'avatar' => function(){
+                return "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png";
+            }]);
+            return successTrans(100010,pageFormat($data),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[GetMapping('msg')]
+    public function getMessageData(Request $request): Response
+    {
+        try {
+            $param = $request->all();
+            $msg = (new SaasChatMsg)->where(['openid' => $param['openid'],'poi_id' => $request->user['store_id']])->append(['avatar','time'])->withAttr(['avatar' => function(){
+                return "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png";
+            },'time' => function($resp,$data){
+                return strtotime($data['create_at']) * 1000;
+            }])->with(['nick' => function ($query) {
+                $query->field("id,truename");
+            },'user' => function ($query) {
+                $query->field("openid,nickname,avatar");
+            }])->order("id","desc")->paginate([
+                "list_rows" => $param['size'] ?? 10,
+                "page"      => $param['page'] ?? 1,
+            ]);
+            return successTrans(100010,pageFormatMsg($msg),200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[GetMapping('order')]
+    public function getMsgOrder(Request $request)
+    {
+        try {
+            $param = $this->_valid([
+                "msgId.require" => trans("empty.require")
+            ]);
+            if (!is_array($param)) return error($param);
+            $productType = $this->goodsService->productType();
+            $chatStore = $this->model->where("id",$param['msgId'])->with(['orders' => function($query) use($productType){
+                $query->field("*")->with(['product' => function($query) use($productType){
+                    $query->field("product_id,product_name,product_type")->append(['types'])->withAttr(['types' => function($query,$resp) use($productType){
+                        $productTypeArr = [];
+                        foreach ($productType as $val) {
+                            $productTypeArr[$val['key']] = $val['name'];
+                        }
+                        return $productTypeArr[$resp['product_type']]??'';
+                    }]);
+                }]);
+            }])->findOrEmpty();
+            if ($chatStore->isEmpty()) return error("error");
+            return success("ok",['order' => $chatStore['orders']]);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    #[PostMapping('send')]
+    public function sendMessageData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "content.require"   => trans("empty.require"),
+                "groupId.require"   => trans("empty.require"),
+                "openid.require"    => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $str = preg_replace('/\s+/','',$param['content']);
+            if (empty($str)) return error("请勿发送空消息");
+            $state = (new SaasChatMsg)->insertGetId([
+                "source"    => 2,
+                "openid"    => $param['openid'],
+                "content"   => is_array($param['content'])?json_encode($param['content']):$param['content'],
+                "type"      => $param['type'],
+                "msgId"     => md5($param['openid'].time()),
+                "poi_id"    => $param['groupId'],
+                "service_id" => $request->user['id'],
+            ]);
+            (new SaasChatMsg)->where(['openid' => $param['openid'],'service_id' => $request->user['id']])->save(['is_read' => 1]);
+            $api = new Api('http://127.0.0.1:3232', config('plugin.webman.push.app.app_key'),config('plugin.webman.push.app.app_secret'));
+            $api->trigger("user-{$param['openid']}","message",[
+                "type"  =>  $param['type'],
+                "time"  =>  time() * 1000,
+                "msgId" => md5($param['openid'].time()),
+                "content"   => is_array($param['content'])?json_encode($param['content']):$param['content'],
+                "source" => 2,
+                "avatar"    => "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png",
+                "poi_id"    => $param['groupId'],
+                "openid"  => $param['openid'],
+                "service_id"  => $request->user['id'],
+                "create_at"  => formatTime(time())
+            ]);
+            return successTrans("success.data",['time' => formatTime(time())]);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[PostMapping('send/price')]
+    public function sendPriceData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "name.require"      => trans("empty.require"),
+                "price.require"     => trans("empty.require"),
+                "groupId.require"   => trans("empty.require"),
+                "openid.require"    => trans("empty.require"),
+                "type.require"      => trans("empty.require"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $orderSn = CodeExtend::uniqidDate(18,"P");
+            $msgId = md5($param['openid'].time());
+            $priceState = (new SaasOrderPrice)->insertGetId([
+                "openid"        => $param['openid'],
+                "poi_id"        => $param['groupId'],
+                "service_id"    => $request->user['id'],
+                "name"          => $param['name'],
+                "price"         => $param['price'] * 100,
+                "image"         => "https://jymini.oss-cn-guangzhou.aliyuncs.com/mini/chajia.jpg",
+                "order_sn"      => $orderSn,
+                "msgId"         => $msgId,
+            ]);
+            if (!$priceState) return errorTrans("error.data");
+            $msgData = [
+                "img"   => "https://jymini.oss-cn-guangzhou.aliyuncs.com/mini/chajia.jpg",
+                "name"  => $param['name'],
+                "order" => $orderSn,
+                "price" => $param['price'],
+                "msgId" => $msgId,
+                "status"    => 0
+            ];
+            $state = (new SaasChatMsg)->insertGetId([
+                "source"    => 2,
+                "openid"    => $param['openid'],
+                "content"   => json_encode($msgData),
+                "type"      => $param['type'],
+                "msgId"     => $msgId,
+                "poi_id"    => $param['groupId'],
+                "service_id" => $request->user['id'],
+            ]);
+            if (!$state) return errorTrans("error.data");
+            $api = new Api('http://127.0.0.1:3232', config('plugin.webman.push.app.app_key'),config('plugin.webman.push.app.app_secret'));
+            $api->trigger("user-{$param['openid']}","message",[
+                "type"  =>  $param['type'],
+                "time"  =>  time() * 1000,
+                "msgId" => $msgId,
+                "content"   => json_encode($msgData),
+                "source" => 2,
+                "avatar"    => "https://washmy.oss-cn-guangzhou.aliyuncs.com/storage/20260511/23627b2cb8f73a8a90b95c31d726e6ecad0cbb9a.png",
+                "poi_id"    => $param['groupId'],
+                "openid"  => $param['openid'],
+                "service_id"  => $request->user['id'],
+                "create_at"  => formatTime(time()),
+            ]);
+            return successTrans("success.data",$msgData);
+        } 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;
+    }
+
+}

+ 29 - 0
app/extra/basic/Model.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\extra\basic;
+
+class Model extends \think\Model
+{
+
+    /**
+     * 数据操作
+     * @param array $data
+     * @param string $id
+     * @return bool|int
+     */
+    public function setAutoData(array $data = [],string $id = "id"): bool|int
+    {
+        try {
+            if (isset($data[$id]) && $this->where($id,$data[$id])->count()) {
+                $state = $this->where($id,$data[$id])->strict(false)->update($data);
+            } else {
+                $state = $this->strict(false)->insertGetId($data);
+            }
+        } catch (\Throwable $throwable) {
+            echo $throwable->getMessage()."\n";
+            return false;
+        }
+        return $state;
+    }
+
+}

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

+ 113 - 0
app/extra/dyLife/BasicLife.php

@@ -0,0 +1,113 @@
+<?php
+
+namespace app\extra\dyLife;
+
+use support\think\Cache;
+use yzh52521\EasyHttp\Http;
+
+class BasicLife
+{
+    /**
+     * 网关
+     * @var string
+     */
+    protected string $gateway = "https://open.douyin.com/";
+    /**
+     * 配置信息
+     * @var array
+     */
+    protected array $config = [];
+
+    /**
+     * 默认header
+     * @var string[]
+     */
+    protected array $header = [];
+
+    /**
+     * AccessToken前缀
+     * @var string
+     */
+    protected string $prefix = "dy_[appid]_access_token";
+
+    /**
+     * 设置配置信息
+     * @param array $config
+     * @return $this
+     */
+    public function config(array $config = []): static
+    {
+        $this->config = $config;
+        return $this;
+    }
+
+    /**
+     * 释放授权
+     * @return $this
+     */
+    public function token(): static
+    {
+        $this->header = [
+            "access-token" => $this->getAccessToken()
+        ];
+        return $this;
+    }
+
+
+    public function getAccessToken()
+    {
+        try {
+            $accessToken = Cache::get($this->getPrefix());
+            if (!empty($accessToken)) return $accessToken;
+            $result = (new Token)->config($this->config)->getAccessToken();
+            if (empty($result)) return "获取Token失败";
+            Cache::set($this->getPrefix(),$result['access_token'],$result['expires_in']);
+            return $result['access_token'];
+        } catch (\Throwable $throwable) {
+            return "";
+        }
+    }
+
+    /**
+     * @return string
+     */
+    protected function getPrefix(): string
+    {
+        return str_replace("[appid]",$this->config['appid'],$this->prefix);
+    }
+
+
+    /**
+     * @param string $url
+     * @param array $data
+     * @param string $field
+     * @return array
+     */
+    public function curlPostApi(string $url = "", array $data = [], string $field = "data"): array
+    {
+        $result = Http::asJson()->withHeaders($this->header)->post($this->gateway.$url,$data)->array();
+        if (isset($result['err_no']) && $result['err_no'] <> 0) return ['msg' => $result['err_msg']];
+        if(!empty($result[$field]))
+        {
+            return $result[$field];
+        }
+        return [];
+    }
+
+    /**
+     * @param string $url
+     * @param array $data
+     * @param string $field
+     * @return array
+     */
+    public function curlGetApi(string $url = "", array $data = [], string $field = "data"): array
+    {
+        $result = Http::asJson()->withProxy("")->withHeaders($this->header)->get($this->gateway.$url,$data)->array();
+        if(!empty($result[$field]))
+        {
+            return $result[$field];
+        }
+        return [];
+    }
+
+}

+ 66 - 0
app/extra/dyLife/Crypt.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace app\extra\dyLife;
+
+use yzh52521\EasyHttp\Http;
+
+class Crypt extends BasicLife
+{
+
+    /**
+     * 获取小程序登录sessionKey
+     * @param string $code
+     * @return array
+     */
+    public function getSessionKey(string $code = ""): array
+    {
+        $url = "https://developer.toutiao.com/api/apps/v2/jscode2session";
+        $param = [
+            "appid"     => $this->config['appid'],
+            "secret"    => $this->config['secret'],
+            "code"      => $code
+        ];
+        $resp = Http::asJson()->post($url,$param)->array();
+        if ($resp['err_no'] == 0) {
+            return $resp['data'];
+        }
+        return [];
+    }
+
+    /**
+     * 查询标签组信息 - 用于支付
+     * @param int $goods_type
+     * @return array
+     */
+    public function getTagGroupId(int $goods_type): array
+    {
+        $url = $this->gateway."api/trade_basic/v1/developer/tag_query/";
+        $param = [
+            "goods_type"      => $goods_type
+        ];
+        $resp = Http::asJson()->withHeaders($this->header)->post($url,$param)->array();
+        if ($resp['err_no'] == 0) {
+            return $resp['data'];
+        }
+        return [];
+    }
+
+    /**
+     * 小程序获取手机号码
+     * @param string $code
+     * @return string
+     */
+    public function getMobile(string $code = ""):string
+    {
+        $url = $this->gateway."api/apps/v1/get_phonenumber_info/";
+        $param = [
+            "code"      => $code
+        ];
+        $resp = Http::asJson()->withHeaders($this->header)->post($url,$param)->array();
+        if ($resp['err_no'] == 0) {
+            return $resp['data'];
+        }
+        return "";
+    }
+
+}

+ 82 - 0
app/extra/dyLife/SignUtil.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace app\extra\dyLife;
+
+class SignUtil
+{
+
+    private $secret;
+    private $key;
+    private $iv;
+
+    public function init(string $secret = "") {
+        $this->secret = $secret;
+        $this->key = $this->parseSecret($secret);
+        $this->iv = substr($this->key, 16);
+        return $this;
+    }
+
+    /**
+     * @Description AES解密
+     * @param string $data base64后的密文
+     * @return string 明文
+     */
+    public function decryptAES($data) {
+        try {
+            $encrypted = $this->decode($data); // 先用base64解密
+            $decrypted = openssl_decrypt(
+                $encrypted,
+                'AES-256-CBC',
+                $this->key,
+                OPENSSL_RAW_DATA,
+                $this->iv
+            );
+            return $decrypted;
+        } catch (\Exception $e) {
+//            error_log($e->getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * base64编码
+     */
+    public function encode($data) {
+        return base64_encode($data);
+    }
+
+    /**
+     * base64解码
+     */
+    public function decode($data) {
+        return base64_decode($data);
+    }
+
+    private function parseSecret($secret) {
+        $secret = $this->fillSecret($secret);
+        $secret = $this->cutSecret($secret);
+        return $secret;
+    }
+
+    private function cutSecret($secret) {
+        if (strlen($secret) <= 32) {
+            return $secret;
+        }
+        $rightCnt = (int)((strlen($secret) - 32) / 2);
+        $leftCnt = strlen($secret) - 32 - $rightCnt;
+        return substr($secret, $leftCnt, 32);
+    }
+
+    private function fillSecret($secret) {
+        if (strlen($secret) >= 32) {
+            return $secret;
+        }
+        $rightCnt = (int)((32 - strlen($secret)) / 2);
+        $leftCnt = 32 - strlen($secret) - $rightCnt;
+        $sb = str_repeat('#', $leftCnt);
+        $sb .= $secret;
+        $sb .= str_repeat('#', $rightCnt);
+        return $sb;
+    }
+
+}

+ 39 - 0
app/extra/dyLife/Token.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace app\extra\dyLife;
+
+use yzh52521\EasyHttp\Http;
+
+class Token extends BasicLife
+{
+
+
+    /**
+     * 获取AccessToken
+     * https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/basic-abilities/interface-request-credential/non-user-authorization/get-client_token
+     * @param string $grantType
+     * @return array
+     */
+    public function getAccessToken(string $grantType = 'client_credential'): array
+    {
+        $data = [
+            "client_key"        => $this->config['appid'],
+            "client_secret"     => $this->config['secret'],
+            "grant_type"        => $grantType
+        ];
+        return $this->curlPostApi("oauth/client_token/",$data);
+    }
+
+
+    public function getAccessTokenUser(string $code = "", string $grantType = 'authorization_code'): array
+    {
+        $data = [
+            "client_key"        => $this->config['appid'],
+            "client_secret"     => $this->config['secret'],
+            "code"              => $code,
+            "grant_type"        => $grantType
+        ];
+        return $this->curlPostApi("oauth/access_token/",$data);
+    }
+
+}

+ 301 - 0
app/extra/dyLife/data/BaseData.php

@@ -0,0 +1,301 @@
+<?php
+
+namespace app\extra\dyLife\data;
+
+use app\extra\dyLife\BasicLife;
+use yzh52521\EasyHttp\Http;
+use function DI\string;
+
+class BaseData extends BasicLife
+{
+
+    /**
+     * 创建团购商品
+     * https://bytedance.larkoffice.com/docx/doxcnnyH289B98IgcPiLWxpSChc
+     * https://partner.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/goods/save
+     * https://open.douyin.com/goodlife/v1/goods/product/save/
+     */
+    public function createGoodsData(array $data = [],array $sku = [],array $store = [],array $valMap = []): array
+    {
+        $product_attrs = $sku_attrs = [];
+        foreach ($valMap['product_attrs'] as $val) {
+            $keyVal = $this->getProductAttrs($val['key'],$data);
+            if (!empty($keyVal)) {
+                $product_attrs[$val['key']] = $keyVal;
+            }
+        }
+        foreach ($valMap['sku_attrs'] as $val) {
+            $keyVal = $this->getProductAttrs($val['key'],$data);
+            if (!empty($keyVal)) {
+                $sku_attrs[$val['key']] = $keyVal;
+            }
+        }
+        $param = [
+            "account_id"    => $store['store_id'], // 商家id
+            "product" => [
+                "account_name"  => $store['poi_name'], // 商家名
+                "biz_line"      => 5, // 业务线 固定 5 小程序
+                "category_id"   => $data['category_id']??0, // 品类id
+                "product_name"  => $data['product_name'], // 商品名
+                "product_type"  => $data['product_type'], // 商品类型
+                "attr_key_value_map"    => $product_attrs,
+                "out_id"        => $data['product_id'], // 第三方id
+                "out_url"       => json_encode([
+                    "app_id"    => sConf("wechat.mini_appid"),
+                    "params"    => json_encode(['goods' => $data['product_id']]),
+                    "path"      => "pages/goods/detail"
+                ]), // 第三方跳转链接,小程序商品必填
+                "pois"          => [
+                    [
+                        "poi_id"    => $store['poi_id'], // 门店POI
+                    ]
+                ],
+                "sold_end_time" => strtotime($data['sold_end_time']." 00:00:00"), // 售卖结束时间
+                "sold_start_time"   => strtotime($data['sold_start_time']." 00:00:00"), // 售卖开始时间
+                "spu_id"        => (string)$data['spu_id'], // spuid
+                "account_id"    => $store['store_id'], // 商家id
+            ]
+        ];
+        if (empty($sku)) { // 单sku
+            $param['sku'] = [
+                "attr_key_value_map"    => $sku_attrs,
+                "actual_amount" => $data['price'], // 售价 实际支付价格
+                "sku_name"      => $data['product_name'], // sku名
+                "origin_amount" => $data['line_price'],
+                "status"        => 1,
+                "stock"         => [
+                    "limit_type"    => $data['limit_use_rule'], // 库存上限类型,为2时stock_qty和avail_qty字段无意义 1 有限库存 2 无限库存
+                    "avail_qty"     => $data['use_num_per_consume']??0, // 可用库存,limit_type=2时无意义,始终保证stock_qty=avail_qty+frozen_qty+sold_qty
+                ]
+            ];
+        } else { // 多sku
+
+        }
+        return $this->curlPostApi("goodlife/v1/goods/product/save/",$param);
+    }
+
+
+    /**
+     * 参数处理
+     * @param string $key
+     * @param array $data
+     * @return string
+     */
+    protected function getProductAttrs(string $key = "",array $data = []): string
+    {
+        switch ($key)
+        {
+            case "trade_url": // 小程序提单页跳转
+                return json_encode([
+                    "app_id"    => sConf("wechat.mini_appid"),
+                    "params"    => json_encode(['goods' => $data['product_id']]),
+                    "path"      => "pages/goods/detail"
+                ]);
+                break;
+            case "commodity": // 商品搭配
+                $specs = json_decode($data['specs'],true);
+                $commodity = [];
+                foreach ($specs as $key=>$val) {
+                    $commodity[$key] = [
+                        "group_name"    => $val['name'],
+                        "total_count"   => count($val['list']),
+                        "option_count"  => $val['num']==0?count($val['list']):$val['num']
+                    ];
+                    foreach ($val['list'] as $sKey=>$sub) {
+                        $commodity[$key]['item_list'][$sKey] = [
+                            "name"  => $sub['name'],
+                            "price" => $sub['price'] * 100,
+                            "count" => 1,
+                            "unit"  => "份"
+                        ];
+                    }
+                }
+                $return = json_encode($commodity);
+                break;
+            case "customer_reserved_info":
+                if ($data['customer_reserved_info'] == 1) { // 不需要留资
+                    $return = json_encode([
+                        "allow" => false
+                    ]);
+                } else {
+                    $return = json_encode([
+                        "allow"             => true,
+                        "allow_tel"         => true,
+                        "require_for_tel"   => true
+                    ]);
+                }
+                break;
+            case "appointment": // 预约规则
+                if ($data['booking_type'] == 1) { // 无需预约
+                    $return = json_encode([
+                        "need_appointment"  => false
+                    ]);
+                } else {
+                    if ($data['booking_unit'] == 1) {
+                        $return = json_encode([
+                            "need_appointment"  => true,
+                            "ahead_day_num"     => intval($data['booking_date'])
+                        ]);
+                    } else {
+                        $return = json_encode([
+                            "need_appointment"  => true,
+                            "ahead_hour_num"    => intval($data['booking_date'])
+                        ]);
+                    }
+                }
+                break;
+            case "EntryType": // 入口类型 1:H5 2:小程序 3:抖音 4:lynx
+                $return = "2";
+                break;
+            case "market_price": // 划线价格
+                $return = (string)$data['line_price'];
+                break;
+            case "can_no_use_date": // 不可使用日期
+                if ($data['can_no_use_date'] == 1) {
+                    $return = json_encode(["enable" => false]);
+                } else { // 指定日期
+                    $noUse = ["enable" => true];
+                    if ($data['can_no_use_week']) { // 每周几不可用
+                        $noUse['days_of_week'] = $data['no_use_weeks'];
+                    }
+                    if ($data['can_no_use_holiday']) { // 节假日不可用
+                        $noUse['holidays'] = $data['no_use_weeks'];
+                    }
+                    if ($data['can_no_use_day']) { // 指定某天不可用
+                        $noUse['date_list'] = $data['no_use_days'];
+                    }
+                    $return = json_encode($noUse);
+                }
+                break;
+            case "image_list": // 封面图 图片比例:375:280
+                $image_list = json_decode($data['image_list'],true);
+                $return = json_encode([
+                    [
+                        "url"   => $image_list[0]['url']
+                    ]
+                ]);
+                break;
+            case "limit_use_rule": // 限制使用规则
+                if ($data['rec_person_type']==1) {
+                    $return = json_encode(["is_limit_use" => false]);
+                } else {
+                    $return = json_encode(['is_limit_use' => true, 'use_num_per_consume' => $data['rec_person_num_max']?intval($data['rec_person_num_max']):0]);
+                }
+                break;
+            case "show_channel":
+                $return = (string) $data['show_channel'];
+                break;
+            case "use_date": // 使用日期 券码的可以核销日期,履约核销强依赖
+                if ($data['use_date_type'] == 1) { // 指定天数
+                    $return = json_encode([
+                        "use_date_type" => 2,
+                        "day_duration" => intval($data['day_duration'])
+                    ]);
+                } else { // 指定日期
+                    $return = json_encode([
+                        "use_date_type" => 1,
+                        "use_start_date" => $data['use_time_start'],
+                        "use_end_date" => $data['use_time_end']
+                    ]);
+                }
+                break;
+            case "use_time": // 使用时间 用户可以消费的时间
+                $return = json_encode([
+                    "use_time_type" => 1
+                ]);
+                break;
+            case "Notification": // 使用规则
+//                $return = $data['notification'];
+                $return = json_encode([
+                    [
+                        "title"     => "适用门店",
+                        "content"   => "全场通用",
+                    ]
+                ]);
+                break;
+            case "RefundPolicy": // 退款政策 1-允许退款 2-不可退款 3-有条件退
+                $return = "1";
+                break;
+            case "refund_need_merchant_confirm": // 退款是否需商家审核
+                $return = "true";
+                break;
+            case "code_source_type": // 券码生成方式 "1-抖音码 2-三方码 3-预导码
+                $return = $data['code_source_type']?(string)$data['code_source_type']:"1";
+                break;
+            case "settle_type": // 收款方式 1-总店结算 2-分店结算
+                $return = $data['code_source_type']?(string)$data['code_source_type']:"2";
+                break;
+            case "limit_rule": // 限制购买
+                if ($data['limit_rule_type'] == 2) {
+                    $return = json_encode(['is_limit' => true,"total_buy_num" => intval($data['limit_rule'])]);
+                } else {
+                    $return = json_encode(['is_limit' => false]);
+                }
+                break;
+            case "auto_renew":
+                $return = $data['auto_renew']==1?'true':'false';
+                break;
+            default:
+                $return = "";
+                break;
+        }
+        return $return;
+    }
+
+    /**
+     * 获取来客绑定的所有门店
+     * 一页最多50条
+     */
+    public function getStoreData(string $account,int $page = 1,int $size = 50): array
+    {
+        $data = [
+            "account_id"    => $account,
+            "page"          => $page,
+            "size"          => $size
+        ];
+        return $this->curlGetApi("goodlife/v1/shop/poi/query/",$data);
+    }
+
+
+    /**
+     * 获取来客绑定的所有分类
+     */
+    public function getStoreCategoryData(string $account): array
+    {
+        $data = [
+            "account_id"            => $account,
+            "query_category_type"   => 1
+        ];
+        return $this->curlGetApi("goodlife/v1/goods/category/get/",$data);
+    }
+
+
+
+    /**
+     * 查下商品模板
+     */
+    public function getStoreCategoryTemplate(string $category_id,int $product_type = 1): array
+    {
+        $data = compact("category_id","product_type");
+        return $this->curlGetApi("goodlife/v1/goods/template/get/",$data);
+    }
+
+    /**
+     * https://open.douyin.com/goodlife/v1/goods/product/operate/
+     * 商品上下架
+     * @param int $type  1-上线 2-下线 3 -删除(商品删除以后无法恢复,需要创建新的商品)
+     * @param string $account
+     * @param string $productId
+     * @return array
+     */
+    public function goodsOffOn(int $type = 1,string $account = "",string $productId = ""): array
+    {
+        $data = [
+            "op_type"       => $type,
+            "account_id"    => $account,
+            "product_id"    => $productId
+        ];
+        return $this->curlPostApi("goodlife/v1/goods/product/operate/",$data);
+    }
+
+}

+ 68 - 0
app/extra/dyLife/data/OrderData.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace app\extra\dyLife\data;
+
+use app\extra\dyLife\BasicLife;
+
+/**
+ * 订单相关
+ */
+class OrderData extends BasicLife
+{
+
+    /**
+     * 通过订单ID获取 encrypted_code
+     * https://developer.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/life.capacity.fulfilment/certificate.query
+     * @param string $orderId
+     * @param string $accountId
+     * data['certificates'][0]['encrypted_code']
+     * @return array
+     */
+    public function getCertificate(string $orderId = "",string $accountId = ""): array
+    {
+        $param = [
+            "account_id"    => $accountId,
+            "order_id"      => $orderId
+        ];
+        return $this->curlPostApi("goodlife/v1/fulfilment/certificate/query/",$param);
+    }
+
+    /**
+     * 发起核销
+     * https://developer.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/life.capacity.fulfilment/certificate.verify
+     * @param array $data
+     * data['verify_results'][0]
+     * certificate_id、verify_id
+     * @return array
+     */
+    public function verifyCertificate(array $data = []): array
+    {
+        $param = [
+            "account_id"        => $data['account_id'],
+            "verify_token"      => $data['pay_sn'],
+            "encrypted_codes"   => [$data['encrypted_code']],
+            "order_id"          => $data['order_id'],
+            "poi_id"            => $data['poi_id']
+        ];
+        return $this->curlPostApi("goodlife/v1/fulfilment/certificate/verify/",$param);
+    }
+
+    /**
+     * 撤销核销
+     * https://developer.open-douyin.com/docs/resource/zh-CN/local-life/develop/OpenAPI/general-capabilities/life.capacity.fulfilment/certificate.cancel
+     * @param array $data
+     * data['cancel_results'][0]
+     * result_code
+     * @return array
+     */
+    public function cancelCertificate(array $data = []): array
+    {
+        $param = [
+            "certificate_id"    => $data['certificate_id'],
+            "verify_id"         => $data['verify_id'],
+            "account_id"        => $data['account_id']
+        ];
+        return $this->curlPostApi("goodlife/v1/fulfilment/certificate/verify/",$param);
+    }
+
+}

+ 114 - 0
app/extra/dyMini/Pay.php

@@ -0,0 +1,114 @@
+<?php
+
+namespace app\extra\dyMini;
+
+use app\extra\dyLife\BasicLife;
+use yzh52521\EasyHttp\Http;
+
+class Pay extends BasicLife
+{
+
+    public function createOrder(array $data = [])
+    {
+        $url = "https://developer.toutiao.com/api/apps/ecpay/v1/create_order";
+        $param = [
+            "out_order_no"  => $data['order_sn'].rand(1000,9999),
+            "total_amount"  => (int) $data['total'],
+            "subject"       => $data['name'],
+            "body"          => $data['name'],
+            "valid_time"    => 1800,
+            "notify_url"    => $data['notify_url'],
+            "cp_extra"      => $data['order_sn']
+        ];
+        $param['sign'] = $this->requestSign($param);
+        $param['app_id'] = $this->config['appid'];
+        $resp = Http::asJson()->post($url,$param)->array();
+        if ($resp['err_no'] == 0) {
+            return $resp['data'];
+        }
+        return [];
+    }
+
+
+    public function createRefund(array $data = [])
+    {
+        $url = "https://developer.toutiao.com/api/apps/ecpay/v1/create_refund";
+        $param = [
+            "out_order_no"  => $data['out_refund_no'],
+            "out_refund_no" => $data['out_refund_no'],
+//            "out_refund_no" => $data['order_sn'].rand(1000,9999),
+            "refund_amount" => (int) $data['total'],
+            "reason"        => $data['reason'],
+            "notify_url"    => $data['notify_url'],
+            "cp_extra"      => $data['order_sn']
+        ];
+        $param['sign'] = $this->requestSign($param);
+        $param['app_id'] = $this->config['appid'];
+        $resp = Http::asJson()->post($url,$param)->array();
+        if ($resp['err_no'] == 0) return true;
+        return false;
+    }
+
+    /**
+     * 生成签名
+     */
+    private function requestSign($params): string
+    {
+        $rList = [];
+        foreach($params as $k =>$v) {
+            if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
+                continue;
+
+            $value = trim(strval($v));
+            if (is_array($v)) {
+                $value = arrayToStr($v);
+            }
+
+            $len = strlen($value);
+            if ($len > 1 && substr($value, 0,1)=="\"" && substr($value, $len-1)=="\"")
+                $value = substr($value,1, $len-1);
+            $value = trim($value);
+            if ($value == "" || $value == "null")
+                continue;
+            $rList[] = $value;
+        }
+        $rList[] = $this->config['salt'];
+        sort($rList, SORT_STRING);
+        return md5(implode('&', $rList));
+    }
+
+    /**
+     * @param $privateKeyStr
+     * @param $data
+     * @param $appId
+     * @param $nonceStr
+     * @param $timestamp
+     * @param $keyVersion
+     * @return string|null
+     * @throws \Exception
+     */
+    public function getByteAuthorization(string $privateKeyStr, string $data, string $appId, string $nonceStr,string $timestamp, string $keyVersion) {
+        $byteAuthorization = '';
+        // 读取私钥
+        $privateKey = openssl_pkey_get_private($privateKeyStr);
+        if (!$privateKey) {
+            throw new \Exception("Invalid private key");
+        }
+        // 生成签名
+        $signature = $this->getSignature("POST", "/requestOrder", $timestamp, $nonceStr, $data, $privateKeyStr);
+        if ($signature === false) {
+            return null;
+        }
+        // 构造 byteAuthorization
+        $byteAuthorization = sprintf("SHA256-RSA2048 appid=%s,nonce_str=%s,timestamp=%s,key_version=%s,signature=%s", $appId, $nonceStr, $timestamp, $keyVersion, $signature);
+        return $byteAuthorization;
+    }
+
+    public function getSignature(string $method, string $url, string $timestamp, string $nonce, $data, $privateKey) {
+//        printf("method:%s\n url:%s\n timestamp:%s\n nonce:%s\n data:%s", $method, $url, $timestamp, $nonce, $data);
+        $targetStr = $method. "\n" . $url. "\n" . $timestamp. "\n" . $nonce. "\n" . $data. "\n";
+        openssl_sign($targetStr, $sign, $privateKey, OPENSSL_ALGO_SHA256);
+        return base64_encode($sign);
+    }
+
+}

+ 82 - 0
app/extra/dyMini/PlanLive.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace app\extra\dyMini;
+
+use app\extra\dyLife\BasicLife;
+use yzh52521\EasyHttp\Http;
+
+class PlanLive extends BasicLife
+{
+
+    /**
+     * 发布/修改直播间定向佣金计划
+     * @param int $planId
+     * @param array $data
+     * @return array
+     */
+    public function planData(int $planId = 0,array $data = []): array
+    {
+        $param = [
+            "plan_id"           => $planId, // 大于0 编辑
+            "plan_name"         => $data['name'],
+            "merchant_phone"    => $data['mobile']
+        ];
+        if (isset($data['star'])) {
+            $param['douyin_id_list'] = $data['star'];
+        }
+        if (isset($data['goods'])) {
+            $param['product_list'] = $data['goods'];
+        }
+        return $this->curlPostApi("api/match/v2/poi/save_live_oriented_plan/",$param);
+    }
+
+
+    /**
+     * 取消计划
+     * @param int $planId
+     * @return array
+     */
+    public function planClose(int $planId = 0): array
+    {
+        $param = [
+            "plan_update_list"  => [
+                [
+                    "plan_id"   => $planId,
+                    "status"    => 3
+                ]
+            ]
+        ];
+        return $this->curlPostApi("api/match/v2/poi/update_oriented_plan_status/",$param);
+    }
+
+    /**
+     * 查下是否为cps订单
+     * @param string $order
+     * @return array
+     */
+    public function planOrder(string $order = ""): array
+    {
+        $param = [
+            "order_id"           => $order,
+        ];
+        return $this->curlPostApi("api/apps/trade/v2/order/query_cps/",$param);
+    }
+
+    /**
+     * 修改商品佣金
+     * @param array $data
+     * @return array
+     */
+    public function setPlanGoods(array $data = []): array
+    {
+        $param = [
+            "commission_rate"   => $data['commission_rate'] * 100,
+            "spu_id"            => $data['goods_id'],
+            "content_type"      => 2,
+            "plan_id"           => $data['plan_id']
+        ];
+        echo json_encode($param)."\n";
+        return $this->curlPostApi("api/match/v2/poi/save_common_plan/",$param);
+    }
+
+}

+ 27 - 0
app/extra/dyMini/Qrcode.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\extra\dyMini;
+
+use app\extra\dyLife\BasicLife;
+
+class Qrcode extends BasicLife
+{
+
+    /**
+     * 获取二维码(方形码/抖音圆码/异形码)
+     * @param string $path
+     * @param string $appName
+     * @return array 返回base64图片
+     */
+    public function getQrcodeV2(string $path = "",string $appName = "toutiao"): array
+    {
+        $param = [
+            "app_name"  => $appName,
+            "appid"     => $this->config['appid'],
+            "path"      => $path,
+            "is_circle_code"  => true
+        ];
+        return $this->curlPostApi("api/apps/v1/qrcode/create/",$param);
+    }
+
+}

+ 73 - 0
app/extra/dyMini/Relation.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace app\extra\dyMini;
+
+use app\extra\dyLife\BasicLife;
+use yzh52521\EasyHttp\Http;
+
+class Relation extends BasicLife
+{
+
+    /**
+     * 新增测试实体
+     * @param array $goodsId
+     * @return array
+     */
+    public function createTestRelation(array $goodsId = []): array
+    {
+        $param = [
+            "operator"      => "小谢",
+            "ref_id_list"   => $goodsId,
+            "type"          => "goods"
+        ];
+        return $this->curlPostApi("api/industry/v1/solution/add_app_test_relation/",$param);
+    }
+
+
+    public function getTestRelation(array $goodsId = []): array
+    {
+        $param = [
+            "type"          => "goods"
+        ];
+        return $this->curlPostApi("api/industry/v1/solution/query_app_test_relation/",$param,'ref_id_list');
+    }
+
+    /**
+     * 预下单
+     * @param array $data
+     * @return array
+     */
+    public function createOrderFirst(array $data = []): array
+    {
+        $param = [
+            "sku_list"  => [
+                [
+                    "quantity" => 1,
+                      "sku_id" => "424144008809807427",
+                      "sku_id_type" => 2,
+                      "price" =>  100,
+                      "discount_amount" => 0,
+                      "goods_info" => [
+                            "date_rule" => "周一至周日可用",
+                            "goods_id" => "1865253598118939",
+                            "goods_id_type" => 1
+                      ]
+                ]
+            ],
+            "app_id" => sConf('wechat.mini_appid'),
+            "total_amount"  => 1,
+            "discount_amount" => 0,
+            "phone_num" => "13265000719",
+            "contact_name" => "谢",
+            "extra" => "AL202605156234065268123",
+            "open_id" => "_000KmfiVkio30K9VxoeLUUY3_hngEWcgR7u",
+            "out_order_no" => "AL202605156234065268123",
+            "order_entry_schema" => [
+                "params" => json_encode(["order" => "AL202605156234065268123"]),
+                "path" => "pages/order/detail"
+            ]
+        ];
+        return $this->curlPostApi("api/apps/trade/v2/order/create_order/",$param);
+    }
+
+}

+ 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' => '亚太-首尔',
+        ];
+    }
+
+}

+ 133 - 0
app/extra/tools/PrintExtend.php

@@ -0,0 +1,133 @@
+<?php
+
+namespace app\extra\tools;
+
+use yzh52521\EasyHttp\Http;
+
+class PrintExtend
+{
+
+    /**
+     * 配置信息
+     * @var array
+     */
+    protected array $config = [];
+
+    /**
+     * 网关
+     * @var string
+     */
+    protected string $gateway = "https://cloud.kuaimai.com/";
+
+
+    /**
+     * 发起打印
+     * @param int $templateId
+     * @param string $sn
+     * @param array $data
+     * @param int $num
+     * [{"express":[{"j_mobile":"180****9980","j_address":"安徽省阜阳市颍州区xxx镇xxx村xxx","express_num":"SF3270714680386","name":"飞天茅台53度500ml","time":"2025-12-04 12:18:09","type":"特快","s_name":"王*二","s_address":"安徽省阜阳市颍州区xxx镇xxx村xxx","s_mobile":"******00982","j_name":"王麻子","express_type":"饰品","order":"1033999288123123"}]}]
+     * @return array
+     */
+    public function printTemplate(int $templateId,string $sn = "",array $data = [],int $num = 1): array
+    {
+        $param = [
+            "appId"         => $this->config['appid'],
+            "sn"            => $sn,
+            "templateId"    => $templateId,
+            "printTimes"    => $num,
+            "renderDataArray" => json_encode($data),
+            "timestamp"     => getDateFull()
+        ];
+        $param['sign'] = $this->getPaySign($param);
+        return Http::asJson()->post($this->gateway."api/cloud/print/tsplTemplatePrint", $param)->array();
+    }
+
+    /**
+     * 打印机状态查询下
+     * @param array $data 最多100台
+     * @return array
+     */
+    public function deviceState(array $data = []): array
+    {
+        $param = [
+            "appId"         => $this->config['appid'],
+            "snsStr"        => json_encode($data),
+            "timestamp"     => getDateFull()
+        ];
+        $param['sign'] = $this->getPaySign($param);
+        return Http::asJson()->post($this->gateway."api/cloud/device/batchStatus", $param)->array();
+    }
+
+    /**
+     * 绑定设备
+     * @param string $sn
+     * @param string $name
+     * @return array status true为成功,false为失败 message
+     */
+    public function bindDevice(string $sn = "",string $name = ""): array
+    {
+        $param = [
+            "appId"     => $this->config['appid'],
+            "sn"        => $sn,
+            "noteName"  => $name,
+            "timestamp" => getDateFull()
+        ];
+        $param['sign'] = $this->getPaySign($param);
+        return Http::asJson()->post($this->gateway."api/cloud/device/bindDevice", $param)->array();
+    }
+
+    /**
+     * 解绑设备
+     * @param string $sn
+     * @param string $deviceKey
+     * @return array status true为成功,false为失败 message
+     */
+    public function unbindDevice(string $sn = "",string $deviceKey = ""): array
+    {
+        $param = [
+            "appId"     => $this->config['appid'],
+            "sn"        => $sn,
+            "deviceKey" => $deviceKey,
+            "timestamp" => getDateFull()
+        ];
+        $param['sign'] = $this->getPaySign($param);
+        return Http::asJson()->post($this->gateway."api/cloud/device/unbindDevice", $param)->array();
+    }
+
+
+
+    /**
+     * @param array $data
+     * @return $this
+     */
+    public function config(array $data = [])
+    {
+        $this->config = $data;
+        return $this;
+    }
+
+
+    /**
+     * 生成签名
+     * @param array $data 参与签名的数据
+     * @param string $signType 参与签名的类型
+     * @param string $buff 参与签名字符串前缀
+     * @return string
+     */
+    public function getPaySign(array $data, string $signType = 'MD5', string $buff = ''): string
+    {
+        ksort($data);
+        if (isset($data['sign'])) unset($data['sign']);
+        foreach ($data as $k => $v) {
+            if ('' === $v || null === $v) continue;
+            $buff .= "{$k}{$v}";
+        }
+        $buff .= $this->config['secret'];
+        if (strtoupper($signType) === 'MD5') {
+            return md5($this->config['secret'].$buff);
+        }
+        return strtoupper(hash_hmac('SHA256', $buff, $this->config['secret']));
+    }
+
+}

+ 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);
+    }
+
+
+}

+ 796 - 0
app/functions.php

@@ -0,0 +1,796 @@
+<?php
+
+
+use app\service\system\SystemService;
+use support\Response;
+use Webman\Event\Event;
+
+if (!function_exists("between_time"))
+{
+    /**
+     * 格式化起止时间(为了兼容前端RangePicker组件)
+     * 2020-04-01T08:15:08.891Z => 1585670400
+     * @param array $times
+     * @param bool $isWithTime 是否包含时间
+     * @return array
+     */
+    function between_time(array $times, bool $isWithTime = false): array
+    {
+        foreach ($times as &$time) {
+            $time = trim($time, '&quot;');
+            $time = str2date($time, $isWithTime);
+        }
+        return ['start_time' => current($times), 'end_time' => next($times)];
+    }
+}
+if (!function_exists("str2date"))
+{
+    /**
+     * 日期转换时间戳
+     * 例如: 2020-04-01 08:15:08 => 1585670400
+     * @param string $date
+     * @param bool $isWithTime 是否包含时间
+     * @return int
+     */
+    function str2date(string $date, bool $isWithTime = false): int
+    {
+        if (!$isWithTime) {
+            $date = date('Y-m-d', strtotime($date));
+        }
+        return strtotime($date);
+    }
+}
+
+if (!function_exists("isValidMobile"))
+{
+    /**
+     * 验证是否为有效的中国大陆手机号码
+     * @param string $mobile
+     * @return bool
+     */
+    function isValidMobile(string $mobile): bool
+    {
+        // 去除空格、换行等
+        $mobile = trim($mobile);
+        // 正则:1 开头,第二位 3-9,后面 9 位数字,总共 11 位
+        $pattern = '/^1[3-9]\d{9}$/';
+        return preg_match($pattern, $mobile) === 1;
+    }
+}
+
+if(!function_exists("hide_mobile")){
+
+    /**
+     * 手机号码脱敏
+     * @param string $mobile
+     * @param int $len 4 末尾4位 6 末尾2位
+     * @return string
+     */
+    function hide_mobile(string $mobile,int $len = 6): string
+    {
+        return substr_replace($mobile, ($len==4 ? '****' : '******'), 3, $len);
+    }
+}
+
+if (!function_exists('hide_str')) {
+    /**
+     * 将一个字符串部分字符用*替代隐藏
+     * @param string $string 待转换的字符串
+     * @param int $begin 起始位置,从0开始计数,当$type=4时,表示左侧保留长度
+     * @param int $len 需要转换成*的字符个数,当$type=4时,表示右侧保留长度
+     * @param int $type 转换类型:0,从左向右隐藏;1,从右向左隐藏;2,从指定字符位置分割前由右向左隐藏;3,从指定字符位置分割后由左向右隐藏;4,保留首末指定字符串中间用***代替
+     * @param string $glue 分割符
+     * @return string   处理后的字符串
+     */
+    function hide_str(string $string, int $begin = 3, int $len = 4, int $type = 0, string $glue = "@"): string
+    {
+        $array = array();
+        if ($type == 0 || $type == 1 || $type == 4) {
+            $strlen = $length = mb_strlen($string);
+            while ($strlen) {
+                $array[] = mb_substr($string, 0, 1, "utf8");
+                $string = mb_substr($string, 1, $strlen, "utf8");
+                $strlen = mb_strlen($string);
+            }
+        }
+        if ($type == 0) {
+            for ($i = $begin; $i < ($begin + $len); $i++) {
+                if (isset($array[$i])) {
+                    $array[$i] = "*";
+                }
+            }
+            $string = implode("", $array);
+        } elseif ($type == 1) {
+            $array = array_reverse($array);
+            for ($i = $begin; $i < ($begin + $len); $i++) {
+                if (isset($array[$i])) {
+                    $array[$i] = "*";
+                }
+            }
+            $string = implode("", array_reverse($array));
+        } elseif ($type == 2) {
+            $array = explode($glue, $string);
+            if (isset($array[0])) {
+                $array[0] = hide_str($array[0], $begin, $len, 1);
+            }
+            $string = implode($glue, $array);
+        } elseif ($type == 3) {
+            $array = explode($glue, $string);
+            if (isset($array[1])) {
+                $array[1] = hide_str($array[1], $begin, $len, 0);
+            }
+            $string = implode($glue, $array);
+        } elseif ($type == 4) {
+            $left = $begin;
+            $right = $len;
+            $tem = array();
+            for ($i = 0; $i < ($length - $right); $i++) {
+                if (isset($array[$i])) {
+                    $tem[] = $i >= $left ? "" : $array[$i];
+                }
+            }
+            $tem[] = '*****';
+            $array = array_chunk(array_reverse($array), $right);
+            $array = array_reverse($array[0]);
+            for ($i = 0; $i < $right; $i++) {
+                if (isset($array[$i])) {
+                    $tem[] = $array[$i];
+                }
+            }
+            $string = implode("", $tem);
+        }
+        return $string;
+    }
+}
+
+if (!function_exists('list_sort_by')) {
+    /**
+     *----------------------------------------------------------
+     * 对查询结果集进行排序
+     *----------------------------------------------------------
+     * @access public
+     *----------------------------------------------------------
+     * @param array $list 查询结果
+     * @param string $field 排序的字段名
+     * @param string $sortBy 排序类型
+     * @switch string  asc正向排序 desc逆向排序 nat自然排序
+     *----------------------------------------------------------
+     * @return array
+     *----------------------------------------------------------
+     */
+    function list_sort_by(array $list, string $field, string $sortBy = 'asc'): array
+    {
+        if (!empty($list)) {
+            $refer = $resultSet = array();
+            foreach ($list as $i => $data)
+                $refer[$i] = &$data[$field];
+            switch ($sortBy) {
+                case 'asc': // 正向排序
+                    asort($refer);
+                    break;
+                case 'desc':// 逆向排序
+                    arsort($refer);
+                    break;
+                case 'nat': // 自然排序
+                    natcasesort($refer);
+                    break;
+            }
+            foreach ($refer as $key => $val)
+                $resultSet[] = &$list[$key];
+            return $resultSet;
+        }
+        return [];
+    }
+}
+
+
+if (!function_exists('supplement_id')) {
+    /**
+     * 用户ID风格
+     * @param string $id
+     * @return string
+     */
+    function supplement_id(string $id): string
+    {
+        $len = strlen($id);
+        $buf = '000000';
+        return $len < 6 ? substr($buf, 0, (6 - $len)) . $id : $id;
+    }
+}
+if (!function_exists('createOrderId')) {
+    /**
+     * 生成订单号
+     * @param string $letter
+     * @param int $length
+     * @return string
+     */
+    function createOrderId(string $letter = '', int $length = 3): string
+    {
+        $gradual = 0;
+        $orderId = date('YmdHis') . mt_rand(10000000, 99999999);
+        $lengths = strlen($orderId);
+
+        // 循环处理随机数
+        for ($i = 0; $i < $lengths; $i++) {
+            $gradual += (int)(substr($orderId, $i, 1));
+        }
+
+        if (empty($letter)) {
+            $letter = get_order_letter($length);
+        }
+
+        $code = (100 - $gradual % 100) % 100;
+        return $letter . $orderId . str_pad((string)$code, 2, '0', STR_PAD_LEFT);
+    }
+}
+
+if (!function_exists('get_order_letter')) {
+    /**
+     * 生成订单短ID
+     * @param int $length
+     * @return string
+     */
+    function get_order_letter(int $length = 2): string
+    {
+        $letter_all = range('A', 'Z');
+        shuffle($letter_all);
+        $letter_array = array_diff($letter_all, ['I', 'O']);
+        $letter = array_rand(array_flip($letter_array), $length);
+        return implode('', $letter);
+    }
+}
+
+/**
+ * 过滤emoji表情
+ * @param string $str
+ * @return string
+ */
+if (!function_exists('removeEmoji')) {
+    function removeEmoji(string $str = '') : string
+    {
+        $str = preg_replace('/[\x{1F600}-\x{1F64F}]/u', '', $str);
+        $str = preg_replace('/[\x{1F300}-\x{1F5FF}]/u', '', $str);
+        $str = preg_replace('/[\x{1F680}-\x{1F6FF}]/u', '', $str);
+        $str = preg_replace('/[\x{2600}-\x{26FF}]/u', '', $str);
+        return preg_replace('/[\x{2700}-\x{27BF}]/u', '', $str);
+    }
+}
+
+if (!function_exists('object_array')) {
+    /**
+     * @param $array
+     * @return array
+     */
+    function object_array($array): array
+    {
+        if(is_object($array)) {
+            $array = (array)$array;
+        }
+        if(is_array($array)) {
+            foreach($array as $key=>$value) {
+                $array[$key] = object_array($value);
+            }
+        }
+        return $array;
+    }
+}
+
+if (!function_exists("getDateFull"))
+{
+    /**
+     * @param string $format
+     * @param string $time
+     * @return string
+     */
+    function getDateFull(string $format = "Y-m-d H:i:s",string $time = ""): string
+    {
+        if (empty($time)) $time = time();
+        return date($format??'Y-m-d H:i:s',$time);
+    }
+}
+
+if (!function_exists("events"))
+{
+    /**
+     * 事件发布
+     * @param $name
+     * @param $data
+     * @return array|mixed|null
+     */
+    function events($name,$data): mixed
+    {
+        return Event::dispatch($name,$data);
+    }
+}
+
+if (!function_exists("success")) {
+    /**
+     * @param string $msg
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function success(string $msg = "", array $data = [], int $code = 1): Response
+    {
+        return json(compact('code', 'data', 'msg'));
+    }
+}
+
+if (!function_exists("successTrans")) {
+    /**
+     * 消息返回
+     * @param string $message
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function successTrans(string $message, array $data = [], int $code = 1): Response
+    {
+        $msg = trans($message);
+        return json(compact("msg", "code", "data"));
+    }
+}
+
+if (!function_exists("error")) {
+    /**
+     * @param $msg
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function error($msg, array $data = [], int $code = 0): Response
+    {
+        return json(compact('code', 'data', 'msg'));
+    }
+}
+
+if (!function_exists("errorTrans")) {
+    /**
+     * 消息返回
+     * @param string $message
+     * @param array $data
+     * @param int $code
+     * @return Response
+     */
+    function errorTrans(string $message = "", array $data = [], int $code = 0): Response
+    {
+        $msg = trans($message);
+        return json(compact("msg", "code", "data"));
+    }
+}
+
+if (!function_exists("pageFormat")) {
+    /**
+     * @param $data
+     * @param int $size
+     * @return array {page:"",pageSize:"",rows:[],total:""}
+     */
+    function pageFormat($data, int $size = 10): array
+    {
+        if (empty($data)) return [];
+        return [
+            'total'     => $data->total(),
+            'page'      => $data->currentPage(),
+            'pageSize'  => $size,
+            'rows'      => $data->items()
+        ];
+    }
+}
+
+if (!function_exists("pageFormatMsg")) {
+    /**
+     * @param $data
+     * @param int $size
+     * @return array {page:"",pageSize:"",rows:[],total:""}
+     */
+    function pageFormatMsg($data, int $size = 10): array
+    {
+        if (empty($data)) return [];
+        $rowsData = [];
+        if ($data->total() > 0) {
+            foreach ($data as $key=>$row) {
+                $rowsData[$key] = $row->toArray();
+            }
+        }
+        return [
+            'total'     => $data->total(),
+            'page'      => $data->currentPage(),
+            'pageSize'  => $size,
+            'rows'      => list_sort_by($rowsData,"id","asc")
+        ];
+    }
+}
+
+
+if(!function_exists('format_money')){
+    function format_money($str,$len = '2',$append = ""): string
+    {
+        if (empty($str)) {
+            return "0.00";
+        }
+        return number_format($str, $len, ".", $append);
+    }
+}
+
+
+if (!function_exists('sConf')) {
+
+    /**
+     * 获取或配置系统参数
+     * @param string $name 参数名称
+     * @param string|null $value 参数内容
+     */
+    function sConf(string $name = '', string $value = null)
+    {
+        if (is_null($value) && is_string($name)) {
+            return SystemService::get($name);
+        } else {
+            return SystemService::set($name, $value);
+        }
+    }
+}
+
+if (!function_exists('sData')) {
+
+    /**
+     * JSON 数据读取与存储
+     * @param string $name 数据名称
+     * @param mixed $value 数据内容
+     */
+    function sData(string $name,mixed $value = null)
+    {
+        if (is_null($value)) {
+            return SystemService::getData($name);
+        } else {
+            return SystemService::setData($name, $value);
+        }
+    }
+}
+
+
+if (!function_exists('sOplog')) {
+    /**
+     * 写入系统日志
+     * @param string $action 日志行为
+     * @param string $content 日志内容
+     * @param string $userName
+     * @return boolean
+     */
+    function sOplog(string $action, string $content, string $userName): bool
+    {
+        return SystemService::setOplog($action, $content,$userName);
+    }
+}
+
+
+
+if(!function_exists('store_region')){
+    /**
+     * 返回附近司机数据
+     * @param $latitude
+     * @param $longitude
+     * @param $type 1 返回查询影响记录数;2 返回数据数组
+     */
+    function store_region($latitude,$longitude,$type = 1,$where = 'status = 1',$field="*"){
+        $sql = "select ".$field." from(
+SELECT id,poi_id,poi_name,status,poi_address,longitude,latitude,
+ROUND(6378.138*2*ASIN(SQRT(POW(SIN(({$latitude}*PI()/180-latitude*PI()/180)/2),2)+COS({$latitude}*PI()/180)*COS(latitude*PI()/180)*POW(SIN(({$longitude}*PI()/180-longitude*PI()/180)/2),2)))*1000) AS juli
+FROM saas_store_shop where ".$where.") as tmp_table_name order by juli asc";
+        if($type == 1){
+            return \think\facade\Db::execute($sql);
+        }
+        return \think\facade\Db::query($sql);
+    }
+}
+
+if(!function_exists('getHourlyTimeSlots')){
+    function getHourlyTimeSlots($startHour = 9, $endHour = 22, $format = 'H:i'): array
+    {
+        $slots = [];
+
+        for ($hour = $startHour; $hour < $endHour; $hour++) {
+            $startTime = sprintf('%02d:00', $hour);
+            $endTime = sprintf('%02d:00', $hour + 1);
+
+            $slots[] = [
+                'start' => $startTime,
+                'end' => $endTime,
+                'display' => $startTime . ' - ' . $endTime
+            ];
+        }
+
+        return $slots;
+    }
+}
+
+if (!function_exists("strToUniqueNumberV4"))
+{
+    /**
+     * 名称加密
+     * @param string $str
+     * @param string $salt
+     * @return string
+     */
+    function strToUniqueNumberV4(string $str = "", string $salt = ''): string
+    {
+        // 添加盐值增加唯一性
+        $str = $str . $salt;
+        // 使用多种哈希组合
+        $crc = abs(crc32($str));
+        $md5 = hexdec(substr(md5($str), 0, 8));
+        $sha1 = hexdec(substr(sha1($str), 0, 8));
+        // 组合并取模
+        $number = ($crc + $md5 + $sha1) % 1000000000000;
+        // 使用时间戳微调确保唯一性(针对同一字符串)
+        static $lastStr = '';
+        static $counter = 0;
+        if ($str === $lastStr) {
+            $counter++;
+            $number = ($number + $counter) % 1000000000000;
+        } else {
+            $lastStr = $str;
+            $counter = 0;
+        }
+        return str_pad($number, 12, '0', STR_PAD_LEFT);
+    }
+}
+
+if (!function_exists("extractNodesClean"))
+{
+    /**
+     * 多维数组转N个一维
+     * @param $data
+     * @param array $result
+     * @return array
+     */
+    function extractNodesClean($data, array &$result = []): array
+    {
+        foreach ($data as $node) {
+            // 复制节点并删除 sub_tree_infos
+            $cleanNode = $node;
+            unset($cleanNode['sub_tree_infos']);
+            $result[] = $cleanNode;
+
+            // 递归处理子节点
+            if (isset($node['sub_tree_infos']) && is_array($node['sub_tree_infos'])) {
+                extractNodesClean($node['sub_tree_infos'], $result);
+            }
+        }
+        return $result;
+    }
+}
+if (!function_exists("formatTime"))
+{
+    /**
+     * 格式化时间:今天、昨天、前天、正常日期
+     * @param string $timestamp 时间戳 或 日期字符串
+     * @return string
+     */
+    function formatTime(string $timestamp): string
+    {
+        // 先统一转成时间戳
+        $time = is_numeric($timestamp) ? $timestamp : strtotime($timestamp);
+
+        $today = strtotime('today');       // 今天 00:00
+        $yesterday = strtotime('yesterday'); // 昨天 00:00
+        $beforeYesterday = strtotime('-2 days'); // 前天 00:00
+
+        if ($time >= $today) {
+            return '今天';
+        } elseif ($time >= $yesterday) {
+            return '昨天';
+        } elseif ($time >= $beforeYesterday) {
+            return '前天';
+        } else {
+            // 其他时间返回正常格式
+            return date('Y-m-d', $time);
+        }
+    }
+}
+if(! function_exists ('timeDiff') ) {
+    function timeDiff($begin_time, $end_time): array
+    {
+        if ($begin_time < $end_time) {
+            return ["day" => 0, "hour" => 0, "min" => 0, "sec" => 0];
+        } else {
+            $starttime = $end_time;
+            $endtime = $begin_time;
+        }
+
+        //计算天数
+        $timediff = $endtime - $starttime;
+        $days = intval($timediff / 86400);
+        //计算小时数
+        $remain = $timediff % 86400;
+        $hours = intval($remain / 3600);
+        //计算分钟数
+        $remain = $remain % 3600;
+        $mins = intval($remain / 60);
+        //计算秒数
+        $secs = $remain % 60;
+        return ["day" => $days, "hour" => $hours, "min" => $mins, "sec" => $secs];
+    }
+}
+
+if(! function_exists('filterTreeByTargets'))
+{
+
+    /**
+     * 过滤树,仅保留包含任何目标 category_id 的节点及其祖先
+     *
+     * @param array $tree       原始树
+     * @param array $targetIds  目标 category_id 列表
+     * @return array 过滤后的树(保持原结构)
+     */
+    function filterTreeByTargets($tree, $targetIds) {
+        $filtered = [];
+        foreach ($tree as $node) {
+            // 检查当前节点或子树中是否包含任一目标 ID
+            $contains = false;
+            if (in_array($node['category_id'], $targetIds)) {
+                $contains = true;
+            } else {
+                // 递归检查子节点
+                $children = [];
+                if (!empty($node['children']) && is_array($node['children'])) {
+                    $children = filterTreeByTargets($node['children'], $targetIds);
+                    if (!empty($children)) {
+                        $contains = true;
+                    }
+                }
+            }
+
+            if ($contains) {
+                // 保留当前节点,并合并过滤后的子节点
+                $newNode = $node;
+                if (!empty($node['children']) && is_array($node['children'])) {
+                    $newNode['children'] = filterTreeByTargets($node['children'], $targetIds);
+                } else {
+                    $newNode['children'] = [];
+                }
+                $filtered[] = $newNode;
+            }
+        }
+        return $filtered;
+    }
+}
+
+if(! function_exists('addTypesRecursive'))
+{
+    /**
+     * 为树中每个节点添加 types 字段
+     */
+    function addTypesRecursive(&$nodes, $typeMap) {
+        foreach ($nodes as &$node) {
+            $node['types'] = $typeMap[$node['category_id']] ?? '';
+            if (isset($node['children']) && is_array($node['children'])) {
+                addTypesRecursive($node['children'], $typeMap);
+            }
+        }
+    }
+}
+
+
+if(! function_exists('emojiToUnicode'))
+{
+    /**
+     * @param string $str
+     * @return string
+     */
+    function emojiToUnicode(string $str): string
+    {
+        return trim(json_encode($str, JSON_UNESCAPED_SLASHES), '"');
+    }
+}
+
+if(! function_exists('unicodeToEmoji'))
+{
+    function unicodeToEmoji(string $str): string
+    {
+        return json_decode("\"{$str}\"");
+    }
+}
+
+if(! function_exists('removeEmoji2'))
+{
+    /**
+     * 清除字符串中所有emoji表情
+     * @param string $str
+     * @return string
+     */
+    function removeEmoji2(string $str): string
+    {
+        // 匹配绝大部分emoji Unicode区间
+        $pattern = '/[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}\x{1F1E0}-\x{1F1FF}\x{2600}-\x{26FF}\x{2700}-\x{27BF}\xFE00-\xFE0F\x{1F900}-\x{1F9FF}]/u';
+        return preg_replace($pattern, '', $str);
+    }
+}
+if(! function_exists('str_cut_ellipsis'))
+{
+    function str_cut_ellipsis(string $str, int $len = 20, string $dot = '...'): string
+    {
+        if (mb_strlen($str, 'UTF-8') <= $len) {
+            return $str;
+        }
+        return mb_substr($str, 0, $len, 'UTF-8') . $dot;
+    }
+}
+
+if (! function_exists('filterHtmlTags'))
+{
+    /**
+     * 批量修改 HTML 中指定标签的属性和样式
+     *
+     * @param string $html      原始 HTML 内容
+     * @param array  $rules     规则数组,键为标签名(小写),值为配置:
+     * [
+     *  'remove' => ['width', 'height'], // 要移除的属性名列表
+     *   'style'  => ['width' => '100%', 'margin' => '0'] // 要强制设置/覆盖的样式
+     * ]
+     * @return string 处理后的 HTML
+     */
+    function filterHtmlTags($html, $rules) {
+        // 匹配所有标签的开始部分:<标签名 属性...> 或 <标签名 属性.../> 或 <标签名>
+        $pattern = '/<(\w+)(?:\s+([^>]*?))?\/?>/i';
+        return preg_replace_callback($pattern, function($matches) use ($rules) {
+            $tag = strtolower($matches[1]);
+            // 如果该标签不在规则中,直接返回原匹配
+            if (!isset($rules[$tag])) {
+                return $matches[0];
+            }
+            $rule = $rules[$tag];
+            // 解析属性(如果有)
+            $attrStr = isset($matches[2]) ? $matches[2] : '';
+            $attrArray = [];
+            if ($attrStr) {
+                // 匹配 属性名="值" 或 属性名='值' 或 属性名=值
+                preg_match_all('/([a-zA-Z-]+)\s*=\s*("([^"]*)"|\'([^\']*)\'|([^\s>]+))/', $attrStr, $matchesAttr, PREG_SET_ORDER);
+                foreach ($matchesAttr as $m) {
+                    $name = strtolower($m[1]);
+                    $value = isset($m[3]) ? $m[3] : (isset($m[4]) ? $m[4] : $m[5]);
+                    $attrArray[$name] = $value;
+                }
+            }
+
+            // 1. 移除指定属性
+            if (isset($rule['remove'])) {
+                foreach ($rule['remove'] as $attr) {
+                    unset($attrArray[$attr]);
+                }
+            }
+
+            // 2. 处理样式
+            if (isset($rule['style']) && is_array($rule['style'])) {
+                $oldStyle = isset($attrArray['style']) ? $attrArray['style'] : '';
+                $styles = [];
+                if ($oldStyle) {
+                    // 简单解析,按分号分割
+                    $parts = array_filter(array_map('trim', explode(';', $oldStyle)));
+                    foreach ($parts as $part) {
+                        if (strpos($part, ':') !== false) {
+                            list($key, $value) = array_map('trim', explode(':', $part, 2));
+                            $styles[$key] = $value;
+                        }
+                    }
+                }
+                // 强制覆盖(或添加)样式
+                foreach ($rule['style'] as $key => $value) {
+                    $styles[$key] = $value;
+                }
+                // 重组 style
+                $newStyle = '';
+                foreach ($styles as $k => $v) {
+                    $newStyle .= $k . ':' . $v . ';';
+                }
+                $attrArray['style'] = $newStyle;
+            }
+
+            // 重建属性字符串
+            $newAttr = '';
+            foreach ($attrArray as $k => $v) {
+                $newAttr .= ' ' . $k . '="' . htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . '"';
+            }
+            return '<' . $tag . $newAttr . '>';
+        }, $html);
+    }
+}

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

+ 42 - 0
app/middleware/StaticFile.php

@@ -0,0 +1,42 @@
+<?php
+/**
+ * This file is part of webman.
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the MIT-LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @author    walkor<walkor@workerman.net>
+ * @copyright walkor<walkor@workerman.net>
+ * @link      http://www.workerman.net/
+ * @license   http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+
+namespace app\middleware;
+
+use Webman\MiddlewareInterface;
+use Webman\Http\Response;
+use Webman\Http\Request;
+
+/**
+ * Class StaticFile
+ * @package app\middleware
+ */
+class StaticFile implements MiddlewareInterface
+{
+    public function process(Request $request, callable $handler): Response
+    {
+        // Access to files beginning with. Is prohibited
+        if (strpos($request->path(), '/.') !== false) {
+            return response('<h1>403 forbidden</h1>', 403);
+        }
+        /** @var Response $response */
+        $response = $handler($request);
+        // Add cross domain HTTP header
+        /*$response->withHeaders([
+            'Access-Control-Allow-Origin'      => '*',
+            'Access-Control-Allow-Credentials' => 'true',
+        ]);*/
+        return $response;
+    }
+}

+ 29 - 0
app/model/Test.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\model;
+
+use support\Model;
+
+class Test extends Model
+{
+    /**
+     * The table associated with the model.
+     *
+     * @var string
+     */
+    protected $table = 'test';
+
+    /**
+     * The primary key associated with the table.
+     *
+     * @var string
+     */
+    protected $primaryKey = 'id';
+
+    /**
+     * Indicates if the model should be timestamped.
+     *
+     * @var bool
+     */
+    public $timestamps = false;
+}

+ 45 - 0
app/model/saas/SaasBankCompany.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property string $name 
+ * @property string $sub_name 
+ * @property string $bank_code
+ */
+class SaasBankCompany 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 = "saas_bank_company";
+    
+    /**
+     * 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;
+
+
+}

+ 49 - 0
app/model/saas/SaasCategory.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $category_id 
+ * @property integer $level 
+ * @property string $name 
+ * @property mixed $parent_id 
+ * @property mixed $is_publish_block 是否屏蔽发品,true为屏蔽发品
+ * @property mixed $is_leaf 是否是叶子结点
+ * @property mixed $create_at
+ */
+class SaasCategory 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 = "saas_category";
+    
+    /**
+     * 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/saas/SaasChatMsg.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use app\model\system\SystemUser;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $openid 
+ * @property integer $service_id 
+ * @property string $content 
+ * @property string $type 
+ * @property integer $status 1已读
+ * @property mixed $create_at
+ */
+class SaasChatMsg 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 = "saas_chat_msg";
+    
+    /**
+     * 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;
+
+    public function nick(): HasOne
+    {
+        return $this->hasOne(SystemUser::class,"id","service_id");
+    }
+
+    public function user(): HasOne
+    {
+        return $this->hasOne(SaasUserOpen::class,"openid","openid");
+    }
+
+}

+ 43 - 0
app/model/saas/SaasChatSet.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id
+ */
+class SaasChatSet 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 = "saas_chat_set";
+    
+    /**
+     * 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;
+
+
+}

+ 70 - 0
app/model/saas/SaasChatStore.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use app\model\system\SystemUser;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $openid 
+ * @property mixed $poi_id 店铺ID
+ * @property string $poi_name 店铺名称
+ * @property mixed $create_at
+ */
+class SaasChatStore 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 = "saas_chat_store";
+    
+    /**
+     * 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;
+
+
+    public function user(): HasOne
+    {
+        return $this->hasOne(SaasUserOpen::class,"openid","openid");
+    }
+
+
+    public function nick(): HasOne
+    {
+        return $this->hasOne(SystemUser::class,"id","service_id");
+    }
+
+
+    public function orders(): HasOne
+    {
+        return $this->hasOne(SaasOrder::class,"order_sn","order");
+    }
+
+    public function good(): HasOne
+    {
+        return $this->hasOne(SaasGoods::class,"product_id","goods");
+    }
+
+}

+ 60 - 0
app/model/saas/SaasComplaint.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $openid 
+ * @property mixed $poi_id 
+ * @property mixed $mobile 
+ * @property mixed $order_sn 
+ * @property string $question 
+ * @property string $content 
+ * @property integer $status 
+ * @property mixed $create_at
+ */
+class SaasComplaint 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 = "saas_complaint";
+    
+    /**
+     * 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;
+
+    public function store(): HasOne
+    {
+        return $this->hasOne(SaasStore::class,"poi_id","poi_id");
+    }
+
+    public function orders(): HasOne
+    {
+        return $this->hasOne(SaasOrder::class,"out_order_no","order_sn");
+    }
+
+}

+ 53 - 0
app/model/saas/SaasDepositLog.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 
+ * @property mixed $order_sn 
+ * @property integer $money 
+ * @property integer $status 1生效0待处理
+ * @property integer $type 
+ * @property mixed $create_at
+ */
+class SaasDepositLog 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 = "saas_deposit_log";
+    
+    /**
+     * 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;
+
+    public function poi(): HasOne
+    {
+        return $this->hasOne(SaasStore::class, 'poi_id', 'poi_id');
+    }
+
+}

+ 102 - 0
app/model/saas/SaasGoods.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasMany;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $product_id 本地商品ID
+ * @property mixed $goods_id 来客商品ID
+ * @property mixed $store_id 来客id
+ * @property mixed $poi_id 
+ * @property string $product_name 
+ * @property mixed $image_list 商品头图
+ * @property mixed $category 商品ID组
+ * @property mixed $category_id 
+ * @property integer $settle_type 收款方式
+ * @property integer $show_channel 投放渠道
+ * @property integer $limit_use_rule 库存类型
+ * @property integer $auto_renew 自动延期
+ * @property integer $use_date_type 顾客可消费日期类型
+ * @property integer $day_duration 指定天数
+ * @property integer $can_no_use_date 顾客不可消费日期类型
+ * @property integer $code_source_type 券码类型
+ * @property integer $limit_rule_type 限购规则
+ * @property integer $limit_rule 限购数量
+ * @property integer $booking_date 预约时间
+ * @property integer $booking_type 预约规则
+ * @property integer $booking_unit 预约单位
+ * @property integer $rec_person_type 使用张数限制
+ * @property integer $rec_person_num_max 限制数量
+ * @property integer $mult_sku 多SKU开关
+ * @property integer $product_type 商品类型
+ * @property string $sold_start_time 售卖日期
+ * @property string $sold_end_time 售卖日期
+ * @property integer $line_price 划线价
+ * @property integer $price 销售价
+ * @property mixed $detail_image_list 辅助图
+ * @property mixed $environment_image_list 环境图
+ * @property mixed $specs 商品搭配
+ * @property integer $status 状态
+ * @property mixed $create_at
+ */
+class SaasGoods 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 = "saas_goods";
+    
+    /**
+     * 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;
+
+    public function setPriceAttr($value): int
+    {
+        return $value * 100;
+    }
+
+    public function getPriceAttr($value): float
+    {
+        return $value / 100;
+    }
+
+    public function getLinePriceAttr($value): float
+    {
+        return $value / 100;
+    }
+
+    public function skuSpecs(): HasMany
+    {
+        return $this->hasMany(SaasGoodsSku::class, 'product_id', 'product_id');
+    }
+
+    public function poi(): HasOne
+    {
+        return $this->hasOne(SaasStore::class, 'poi_id', 'poi_id');
+    }
+
+}

+ 73 - 0
app/model/saas/SaasGoodsSku.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $goods_id 来客商品ID
+ * @property mixed $sku_id 
+ * @property mixed $product_id 本地商品ID
+ * @property string $name 
+ * @property string $image 
+ * @property integer $line_price 
+ * @property integer $price 
+ * @property mixed $specs 
+ * @property integer $is_default 
+ * @property mixed $create_at
+ */
+class SaasGoodsSku 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 = "saas_goods_sku";
+    
+    /**
+     * 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;
+
+
+    public function setPriceAttr($value): int
+    {
+        return $value * 100;
+    }
+
+    public function getPriceAttr($value): float
+    {
+        return $value / 100;
+    }
+
+
+    public function setLinePriceAttr($value): int
+    {
+        return $value * 100;
+    }
+
+    public function getLinePriceAttr($value): float
+    {
+        return $value / 100;
+    }
+
+}

+ 62 - 0
app/model/saas/SaasLivePlan.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasMany;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 门店ID
+ * @property string $plan_name 
+ * @property string $merchant_phone 
+ * @property integer $status 
+ * @property mixed $create_at
+ */
+class SaasLivePlan 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 = "saas_live_plan";
+    
+    /**
+     * 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;
+
+    public function goods(): HasMany
+    {
+        return $this->hasMany(SaasLivePlanGoods::class,'plan_id','plan_id');
+    }
+
+    public function star(): HasMany
+    {
+        return $this->hasMany(SaasLivePlanStar::class,'plan_id','plan_id');
+    }
+    public function poi(): HasOne
+    {
+        return $this->hasOne(SaasStore::class,'poi_id','poi_id');
+    }
+
+}

+ 49 - 0
app/model/saas/SaasLivePlanGoods.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $plan_id 
+ * @property mixed $goods_id
+ */
+class SaasLivePlanGoods 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 = "saas_live_plan_goods";
+    
+    /**
+     * 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;
+
+    public function goods(): HasOne
+    {
+        return $this->hasOne(SaasGoods::class,'goods_id','goods_id');
+    }
+
+}

+ 49 - 0
app/model/saas/SaasLivePlanStar.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $plan_id 
+ * @property string $unique_id 抖音号
+ */
+class SaasLivePlanStar 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 = "saas_live_plan_star";
+    
+    /**
+     * 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;
+
+    public function star(): HasOne
+    {
+        return $this->hasOne(SaasStar::class,'unique_id','unique_id');
+    }
+
+}

+ 73 - 0
app/model/saas/SaasOrder.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $order_sn 
+ * @property mixed $openid 
+ * @property mixed $product_id 
+ * @property mixed $sku_id 
+ * @property integer $number 
+ * @property string $img 
+ * @property integer $price 
+ * @property integer $line_price 
+ * @property string $sku_name 
+ * @property mixed $poi_id 
+ * @property mixed $mobile
+ */
+class SaasOrder 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 = "saas_order";
+    
+    /**
+     * 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;
+
+    public function product(): HasOne
+    {
+        return $this->hasOne(SaasGoods::class,"product_id","product_id");
+    }
+
+    public function poi(): HasOne
+    {
+        return $this->hasOne(SaasStore::class,"poi_id","poi_id");
+    }
+
+    public function user(): HasOne
+    {
+        return $this->hasOne(SaasUserOpen::class,"openid","openid");
+    }
+
+    public function address(): HasOne
+    {
+        return $this->hasOne(SaasOrderAddress::class,"order_sn","out_order_no");
+    }
+
+}

+ 50 - 0
app/model/saas/SaasOrderAddress.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $order_sn 
+ * @property mixed $openid 
+ * @property string $city_text 
+ * @property mixed $city_code 
+ * @property string $nickname 
+ * @property string $mobile 
+ * @property string $address 
+ * @property mixed $create_at
+ */
+class SaasOrderAddress 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 = "saas_order_address";
+    
+    /**
+     * 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;
+
+
+}

+ 44 - 0
app/model/saas/SaasOrderDelay.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $order_sn 
+ * @property mixed $create_at
+ */
+class SaasOrderDelay 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 = "saas_order_delay";
+    
+    /**
+     * 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;
+
+
+}

+ 55 - 0
app/model/saas/SaasOrderPrice.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property string $name 
+ * @property integer $price 
+ * @property string $image 
+ * @property mixed $order_sn 
+ * @property integer $status 
+ * @property mixed $msgId 
+ * @property mixed $create_at
+ */
+class SaasOrderPrice 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 = "saas_order_price";
+    
+    /**
+     * 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;
+
+
+    public function user(): HasOne
+    {
+        return $this->hasOne(SaasUserOpen::class,"openid","openid");
+    }
+
+}

+ 47 - 0
app/model/saas/SaasOrderVerify.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $order_sn 
+ * @property mixed $certificate_id 
+ * @property mixed $verify_id 
+ * @property integer $status 0核销成功1撤销成功
+ * @property mixed $create_at
+ */
+class SaasOrderVerify 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 = "saas_order_verify";
+    
+    /**
+     * 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;
+
+
+}

+ 54 - 0
app/model/saas/SaasPrint.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 
+ * @property string $sn 序列号
+ * @property string $device_key 
+ * @property string $name 打印机名称
+ * @property string $device_state 
+ * @property string $online 
+ * @property mixed $create_at
+ */
+class SaasPrint 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 = "saas_print";
+    
+    /**
+     * 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;
+
+    public function poi(): HasOne
+    {
+        return $this->hasOne(SaasStore::class,"poi_id","poi_id");
+    }
+
+}

+ 46 - 0
app/model/saas/SaasQuick.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $service_id 
+ * @property string $content 
+ * @property integer $type 1个人2团队
+ * @property mixed $create_at
+ */
+class SaasQuick 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 = "saas_quick";
+    
+    /**
+     * 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;
+
+
+}

+ 68 - 0
app/model/saas/SaasStar.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasMany;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 
+ * @property string $nick_name 昵称
+ * @property string $unique_id 抖音号
+ * @property string $avatar_url 
+ * @property string $city_name 
+ * @property mixed $uid 抖音唯一ID
+ * @property string $fans_count 
+ * @property mixed $fans_tag_list 粉丝标签
+ * @property string $talent_level_display 带货力
+ * @property string $talent_item_level_display 视频带货力
+ * @property string $talent_live_level_display 直播
+ * @property string $content_level_display 内容力
+ * @property string $credit_score_display 信用分
+ * @property string $rank_display 视频榜
+ * @property mixed $create_at
+ */
+class SaasStar 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 = "saas_star";
+    
+    /**
+     * 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;
+
+    public function getShowNicknameAttr($data): string
+    {
+        return unicodeToEmoji($data);
+    }
+
+    public function poi(): HasOne
+    {
+        return $this->hasOne(SaasStore::class,'poi_id','poi_id');
+    }
+
+}

+ 51 - 0
app/model/saas/SaasStore.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $store_id 来客id
+ * @property integer $poi_id 
+ * @property string $poi_name 
+ * @property string $poi_address 
+ * @property float $longitude 
+ * @property float $latitude 
+ * @property integer $is_default 
+ * @property integer $status 
+ * @property mixed $create_at
+ */
+class SaasStore 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 = "saas_store";
+    
+    /**
+     * 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;
+
+
+}

+ 50 - 0
app/model/saas/SaasStoreAccount.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 
+ * @property integer $type 1个人2企业
+ * @property string $truename 名字
+ * @property string $idcard 身份证或营业执照
+ * @property mixed $content 其他数据
+ * @property integer $status 
+ * @property mixed $member_id 三方ID
+ * @property mixed $create_at
+ */
+class SaasStoreAccount 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 = "saas_store_account";
+    
+    /**
+     * 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;
+
+
+}

+ 51 - 0
app/model/saas/SaasStoreCategory.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $product_type 
+ * @property integer $category_id 
+ * @property integer $poi_id 
+ * @property mixed $create_at
+ */
+class SaasStoreCategory 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 = "saas_store_category";
+    
+    /**
+     * 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;
+
+    public function cate(): HasOne
+    {
+        return $this->hasOne(SaasCategory::class, "category_id", "category_id");
+    }
+
+}

+ 47 - 0
app/model/saas/SaasStoreExpress.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 
+ * @property string $type 
+ * @property integer $status 0关闭1开通
+ * @property mixed $rule 权限信息
+ * @property mixed $create_at
+ */
+class SaasStoreExpress 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 = "saas_store_express";
+    
+    /**
+     * 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;
+
+
+}

+ 69 - 0
app/model/saas/SaasStoreOpen.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 
+ * @property string $company_name 
+ * @property mixed $company_number 
+ * @property string $company_sub_name 
+ * @property string $company_license 营业执照
+ * @property string $city_name 
+ * @property mixed $city_code 
+ * @property string $address 
+ * @property string $legal_name 法人姓名
+ * @property mixed $legal_mobile 
+ * @property mixed $legal_idcard 身份证号
+ * @property string $legal_id_front 正面
+ * @property string $legal_id_back 反面
+ * @property mixed $email 
+ * @property mixed $bank_number 对公帐号
+ * @property string $bank_code 开户行编码
+ * @property string $bank_img 开户许可证照片
+ * @property integer $status 
+ * @property string $remark 
+ * @property mixed $last_at 
+ * @property mixed $create_at
+ */
+class SaasStoreOpen 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 = "saas_store_open";
+    
+    /**
+     * 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;
+
+    public function poi(): HasOne
+    {
+        return $this->hasOne(SaasStore::class, 'poi_id', 'poi_id');
+    }
+
+
+}

+ 50 - 0
app/model/saas/SaasTransactionLog.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $poi_id 
+ * @property mixed $order_sn 
+ * @property integer $star_money 达人佣金
+ * @property float $star_rate 达人佣金比例
+ * @property integer $system_money 系统扣点
+ * @property float $system_rate 系统扣点比例
+ * @property mixed $star_plan 佣金计划ID
+ * @property mixed $create_at
+ */
+class SaasTransactionLog 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 = "saas_transaction_log";
+    
+    /**
+     * 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;
+
+
+}

+ 47 - 0
app/model/saas/SaasUserOpen.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property mixed $openid 
+ * @property string $nickname 
+ * @property mixed $mobile 
+ * @property integer $status 状态
+ * @property mixed $create_at
+ */
+class SaasUserOpen 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 = "saas_user_open";
+    
+    /**
+     * 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;
+
+
+}

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

@@ -0,0 +1,52 @@
+<?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 $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;
+
+
+}

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

@@ -0,0 +1,48 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property string $username 
+ * @property string $node 当前操作节点
+ * @property string $action 操作行为名称
+ * @property string $content 当前操作节点
+ * @property mixed $geoip 
+ * @property mixed $create_at
+ */
+class SystemOplog 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_oplog";
+    
+    /**
+     * 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;
+
+
+}

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

@@ -0,0 +1,66 @@
+<?php
+
+namespace app\model\system;
+
+use app\extra\basic\Model;
+use think\model\relation\HasOne;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $agent_id 代理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 $type 1管理员2代理子账号3店铺账号
+ * @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;
+
+
+    public function account(): HasOne
+    {
+        return $this->hasOne("app\model\saas\SaasStore", "poi_id", "store_id");
+    }
+
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików