zory 3 months ago
parent
commit
d9e3ee284a
100 changed files with 10706 additions and 1 deletions
  1. 20 0
      .gitignore
  2. 21 0
      LICENSE
  3. 20 1
      README.md
  4. 31 0
      app/command/ClearCart.php
  5. 106 0
      app/command/DayMoney.php
  6. 51 0
      app/command/DayPay.php
  7. 59 0
      app/command/DayTotal.php
  8. 133 0
      app/command/Model.php
  9. 169 0
      app/controller/Dashboard.php
  10. 129 0
      app/controller/admin/Combo.php
  11. 84 0
      app/controller/admin/Config.php
  12. 29 0
      app/controller/admin/Dashboard.php
  13. 192 0
      app/controller/admin/Shop.php
  14. 65 0
      app/controller/admin/Upload.php
  15. 185 0
      app/controller/admin/User.php
  16. 215 0
      app/controller/api/Card.php
  17. 505 0
      app/controller/api/Cart.php
  18. 193 0
      app/controller/api/Home.php
  19. 391 0
      app/controller/api/Notify.php
  20. 277 0
      app/controller/api/Order.php
  21. 263 0
      app/controller/api/Test.php
  22. 57 0
      app/controller/api/Uploads.php
  23. 82 0
      app/controller/api/User.php
  24. 46 0
      app/controller/common/Common.php
  25. 146 0
      app/controller/common/Login.php
  26. 75 0
      app/controller/common/Menu.php
  27. 110 0
      app/controller/exe/Login.php
  28. 155 0
      app/controller/exe/Prints.php
  29. 80 0
      app/controller/merchant/Dashboard.php
  30. 64 0
      app/controller/merchant/Detail.php
  31. 102 0
      app/controller/merchant/Discount.php
  32. 57 0
      app/controller/merchant/Member.php
  33. 163 0
      app/controller/merchant/Order.php
  34. 106 0
      app/controller/merchant/Price.php
  35. 160 0
      app/controller/merchant/Prints.php
  36. 67 0
      app/controller/merchant/Qrcode.php
  37. 104 0
      app/controller/merchant/Renew.php
  38. 165 0
      app/controller/merchant/Staff.php
  39. 74 0
      app/controller/merchant/Turnover.php
  40. 36 0
      app/controller/merchant/User.php
  41. 34 0
      app/controller/merchant/UserLog.php
  42. 89 0
      app/controller/notify/SfExpress.php
  43. 122 0
      app/controller/wap/Card.php
  44. 10 0
      app/controller/wap/Common.php
  45. 200 0
      app/controller/wap/Detail.php
  46. 104 0
      app/controller/wap/Discount.php
  47. 336 0
      app/controller/wap/Login.php
  48. 109 0
      app/controller/wap/Member.php
  49. 119 0
      app/controller/wap/Order.php
  50. 108 0
      app/controller/wap/Price.php
  51. 102 0
      app/controller/wap/Prints.php
  52. 70 0
      app/controller/wap/Turnover.php
  53. 37 0
      app/event/ClearUpload.php
  54. 54 0
      app/event/CreateOrder.php
  55. 43 0
      app/event/MergePdf.php
  56. 19 0
      app/event/SyncUser.php
  57. 222 0
      app/extra/basic/Base.php
  58. 30 0
      app/extra/basic/Model.php
  59. 103 0
      app/extra/basic/Service.php
  60. 158 0
      app/extra/jhfPay/Pay.php
  61. 134 0
      app/extra/jhfPay/Utils.php
  62. 98 0
      app/extra/service/basic/UploadService.php
  63. 36 0
      app/extra/service/saas/ComboLogService.php
  64. 35 0
      app/extra/service/saas/ComboService.php
  65. 34 0
      app/extra/service/saas/DisCountService.php
  66. 36 0
      app/extra/service/saas/MemberLogService.php
  67. 68 0
      app/extra/service/saas/MemberService.php
  68. 121 0
      app/extra/service/saas/OrderService.php
  69. 34 0
      app/extra/service/saas/PriceService.php
  70. 35 0
      app/extra/service/saas/PrintService.php
  71. 50 0
      app/extra/service/saas/QrcodeService.php
  72. 36 0
      app/extra/service/saas/ShopLogService.php
  73. 38 0
      app/extra/service/saas/ShopService.php
  74. 29 0
      app/extra/service/system/MenuService.php
  75. 35 0
      app/extra/service/system/MoneyLogService.php
  76. 183 0
      app/extra/service/system/SystemService.php
  77. 42 0
      app/extra/service/system/UserService.php
  78. 197 0
      app/extra/tools/CodeExtend.php
  79. 73 0
      app/extra/tools/DataExtend.php
  80. 80 0
      app/extra/tools/OssRegionExtend.php
  81. 122 0
      app/extra/tools/UploadExtend.php
  82. 116 0
      app/extra/weMini/BasicWeChat.php
  83. 198 0
      app/extra/weMini/BasicWePay.php
  84. 69 0
      app/extra/weMini/Crypt.php
  85. 145 0
      app/extra/weMini/Link.php
  86. 143 0
      app/extra/weMini/Order.php
  87. 188 0
      app/extra/weMini/Pay.php
  88. 88 0
      app/extra/weMini/PayExtra.php
  89. 24 0
      app/extra/weMini/Subscribe.php
  90. 21 0
      app/extra/weMini/crypt/errorCode.php
  91. 59 0
      app/extra/weMini/crypt/wxBizDataCrypt.php
  92. 161 0
      app/extra/wechat/WechatService.php
  93. 601 0
      app/functions.php
  94. 45 0
      app/middleware/AuthMiddleware.php
  95. 44 0
      app/middleware/DyMiddleware.php
  96. 33 0
      app/middleware/ExeMiddleware.php
  97. 42 0
      app/middleware/StaticFile.php
  98. 39 0
      app/middleware/WxMiddleware.php
  99. 68 0
      app/model/saas/SaasCart.php
  100. 50 0
      app/model/saas/SaasCombo.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

+ 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.

+ 20 - 1
README.md

@@ -1,2 +1,21 @@
-# xg-api
+# hx-mini-api
 
+### 跨域
+
+```
+文件
+config/plugin/linfly/annotation/route.php
+新增内容
+use Webman\Route;
+// 匹配所有options路由
+Route::options('[{path:.+}]', function (){
+    return response('',204);
+});
+```
+
+### 合并
+
+````
+git fetch origin
+git reset --hard origin/master
+````

+ 31 - 0
app/command/ClearCart.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace app\command;
+
+use app\model\saas\SaasCart;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ClearCart extends Command
+{
+
+
+    protected static string $defaultName = 'clear:cart';
+    protected static string $defaultDescription = '清理购物车前一天无效数据';
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $output->writeln("清理开始====>" . getDateFull());
+        $cartNum = (new SaasCart)->where("create_at","<",getDateFull())->count();
+        $output->writeln("共计需清理{$cartNum}条数据====>" . getDateFull());
+        $state = (new SaasCart)->where("create_at","<",getDateFull())->delete();
+        if (!$state) {
+            $output->writeln("清理失败====>" . getDateFull());
+        } else {
+            $output->writeln("清理完成====>" . getDateFull());
+        }
+        return self::SUCCESS;
+    }
+
+}

+ 106 - 0
app/command/DayMoney.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace app\command;
+
+use app\extra\jhfPay\Pay;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasShop;
+use app\model\saas\SaasUserBuy;
+use app\model\system\SystemUserMoney;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class DayMoney extends Command
+{
+
+    protected static string $defaultName = 'day:money';
+    protected static string $defaultDescription = '每日结算金额统计';
+
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $nextPage = 1;$done = 0;
+        $mode = (new SaasShop);
+        $orderMode = (new SaasOrder);
+        $moneyMode = (new SystemUserMoney);
+        $cardMode = (new SaasUserBuy);
+        $day = date("Y-m-d",strtotime("-1 day"));
+//        $respJhf = (new Pay)->config([
+//            "appid"  => sConf("wechat.jhf_appid"),
+//            "mch_id" => sConf("wechat.jhf_mch_id"),
+//            "aeskey" => sConf("wechat.jhf_aeskey"),
+//            "pubkey" => sConf("wechat.jhf_pubkey"),
+//            "prikey" => sConf("wechat.jhf_prikey"),
+//        ]);
+        while (is_numeric($nextPage)) {
+            $total = $mode->count();
+            $result = $mode->limit(($nextPage-1)*10,10)->with(['card'])->select();
+            if ($result->isEmpty()) {
+                $output->writeln('没有更多数据啦~==>'.getDateFull());
+                $nextPage = null;
+                return self::SUCCESS;
+            }
+            if ($done >= $total){
+                $output->writeln('没有更多数据啦!~==>'.getDateFull());
+                $nextPage = null;
+                return self::SUCCESS;
+            }
+            $moneyData = [];
+            foreach ($result as $key => $val)
+            {
+                ++$done;
+                if (empty($val['cash_rate'])) {
+                    $rate = 0.006;
+                } else {
+                    $rate = $val['cash_rate'] / 100;
+                }
+                $dayData = $moneyMode->where("agent_id",$val['shop_id'])->where("day",$day)->findOrEmpty();
+                if ($dayData->isEmpty()) {
+                    $dayMoney = $orderMode->where("shop_id",$val['shop_id'])->whereIn("status",[1,2,3])->whereDay("create_at","yesterday")->where("pay_type",1)->sum("money");
+                    $cardMoney = $cardMode->where("shop_id",$val['shop_id'])->where("status",1)->whereDay("create_at","yesterday")->sum("money");
+                    $status = 0;
+                    $memberId = $val['card']['member_id']??'';
+                    if ($dayMoney == 0) {
+                        $status = 1;
+                    }
+                    $totalMoney = $dayMoney + $cardMoney;
+                    $rateMoney = ($totalMoney==0)?0:round($totalMoney*$rate);
+                    $endMoney = round($totalMoney - $rateMoney,0);
+                    $moneyData[$key] = [
+                        "order_sn"      => strtoupper(md5($day.$val['shop_id'])),
+                        "agent_id"      => $val['shop_id'],
+                        "money"         => $endMoney,
+                        "re_money"      => $totalMoney, // 当日金额
+                        "order_money"   => $dayMoney,
+                        "user_money"    => $cardMoney, // 会员卡充值金额
+                        "rate"          => $rateMoney,
+                        "day"           => $day,
+                        "status"        => $status,
+                        "member_id"     => $memberId
+                    ];
+//                    if (!empty($memberId) && $totalMoney > 0) { // 发起余额分账
+//                        $shopName = $val['shop_name']??'';
+//                        $respJhf->createBalancePay([
+//                            "app_id" => sConf("wechat.jhf_appid"),
+//                            "order_no"  => strtoupper(md5($day.$val['shop_id'])),
+//                            "member_id" => $memberId,
+//                            "pay_amt"   => format_money($endMoney/100,2),
+//                            "description"   => "{$day}-{$shopName}-结算"
+//                        ]);
+//                    }
+                    echo getDateFull()."==={$val['shop_id']}=={$val['shop_name']}==当天营业额统计===¥{$totalMoney}====结算卡:{$memberId}==状态:{$status}===\n";
+                } else {
+                    echo getDateFull()."==={$val['shop_id']}=={$val['shop_name']}=={$day}==已完成统计===\n";
+                }
+            }
+            if (!empty($moneyData)) {
+                $moneyMode->insertAll(array_values($moneyData));
+            }
+            $nextPage = $nextPage+1;
+            $output->writeln("当前页码{$nextPage}==>".getDateFull());
+        }
+        return self::SUCCESS;
+    }
+
+}

+ 51 - 0
app/command/DayPay.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace app\command;
+
+use app\extra\jhfPay\Pay;
+use app\extra\tools\CodeExtend;
+use app\extra\tools\DataExtend;
+use app\model\system\SystemUserBankcard;
+use app\model\system\SystemUserMoney;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class DayPay extends Command
+{
+
+    protected static string $defaultName = 'day:pay';
+    protected static string $defaultDescription = '每日结算金额发放';
+
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $card = (new SystemUserBankcard)->select();
+        $day = date("Y-m-d",strtotime("-1 day"));
+        $cardData = unique_arr($card->toArray(),"member_id");
+        $respJhf = (new Pay)->config([
+            "appid"  => sConf("wechat.jhf_appid"),
+            "mch_id" => sConf("wechat.jhf_mch_id"),
+            "aeskey" => sConf("wechat.jhf_aeskey"),
+            "pubkey" => sConf("wechat.jhf_pubkey"),
+            "prikey" => sConf("wechat.jhf_prikey"),
+        ]);
+        if (!empty($cardData)) {
+            foreach ($cardData as $val)
+            {
+                $jsResp = $respJhf->createBalanceWithdraw([
+                    "app_id" => sConf("wechat.jhf_appid"),
+                    "order_no"  => $val['member_id'].CodeExtend::uniqidDate(14),
+                    "member_id" => $val['member_id'],
+                    "remark"    => "{$day}-结算",
+                    "notify_url"    => "https://panel.huiyinduo.cn/notify/withdraw",
+                ]);
+                echo getDateFull()."=={$val['user_name']}==开始结算\n";
+                echo json_encode($jsResp)."\n";
+            }
+        }
+        return self::SUCCESS;
+    }
+
+
+}

+ 59 - 0
app/command/DayTotal.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace app\command;
+
+use app\extra\jhfPay\Pay;
+use app\extra\tools\CodeExtend;
+use app\model\system\SystemUserMoney;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class DayTotal extends Command
+{
+
+    protected static string $defaultName = 'day:total';
+    protected static string $defaultDescription = '每日结算金额余额分账';
+
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $day = date("Y-m-d",strtotime("-1 day"));
+        $totalMoney = (new SystemUserMoney)->where("day",$day)->select();
+        $moneyData = [];
+        if ($totalMoney->isEmpty()) return self::SUCCESS;
+        $respJhf = (new Pay)->config([
+            "appid"  => sConf("wechat.jhf_appid"),
+            "mch_id" => sConf("wechat.jhf_mch_id"),
+            "aeskey" => sConf("wechat.jhf_aeskey"),
+            "pubkey" => sConf("wechat.jhf_pubkey"),
+            "prikey" => sConf("wechat.jhf_prikey"),
+        ]);
+        foreach ($totalMoney as $key=>$value) {
+            if (!empty($value['member_id'])) {
+//                echo "{$value['member_id']}==={$value['money']}\n";
+                if (isset($moneyData[$value['member_id']])) {
+                    $moneyData[$value['member_id']] += $value['money'];
+                } else {
+                    $moneyData[$value['member_id']] = $value['money'];
+                }
+            }
+        }
+        if (!empty($moneyData)) {
+            foreach ($moneyData as  $memberId=>$val){
+                $resp = $respJhf->createBalancePay([
+                    "app_id" => sConf("wechat.jhf_appid"),
+                    "order_no"  => CodeExtend::uniqidDate(18),
+                    "member_id" => $memberId,
+                    "pay_amt"   => format_money($val/100,2),
+                    "description"   => "{$day}-结算"
+                ]);
+                echo getDateFull()."===余额分账===\n";
+                echo json_encode($resp)."\n";
+                sleep(1);
+            }
+        }
+        return self::SUCCESS;
+    }
+
+}

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

+ 169 - 0
app/controller/Dashboard.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace app\controller;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderLife;
+use app\model\saas\SaasStore;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/dashboard"),Middleware(AuthMiddleware::class)]
+class Dashboard extends Base
+{
+
+
+    /**
+     *
+     * @return Response
+     */
+    #[Route(path: "manage",methods: "get")]
+    public function getManageData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "money.default" => 7,
+                "hide.default"  => 0,
+                "order.default"  => 7
+            ],$request->method());
+            $moneyDay = $param['money'];
+            $data = $moneys = $orders = [];
+            $orderDay = $param['order'];
+            $mode = (new SaasOrder);
+            if ($param['hide'] == 1) {
+                $data = [
+                    "userNum"   => $mode->whereDay("create_at")->where('status','in',[1,3])->sum("money"), // 今日营业额
+                    "rechargeNum"   => $mode->whereDay("create_at")->where('status','in',[1,3])->count(), // 今日订单数
+                    "rechargeMoney"   => $mode->whereDay("create_at")->where('status',1)->count(), // 手机自助打印待处理
+                ];
+                $fields = ['ROUND(sum(money)/100,2)' => 'total','substr(create_at,1,10)' => 'mday'];
+                $orderMoney = $mode->field($fields)->where('status','in',[1,3])->whereTime('create_at', '-'.$moneyDay.' days')->group('mday')->select()->column(null, 'mday');
+                for ($i = $moneyDay; $i >= 0; $i--) {
+                    $date = date('Y-m-d', strtotime("-{$i}days"));
+                    $moneys[] = [
+                        '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                        '订单金额' => ($orderMoney[$date] ?? [])['total'] ?? 0,
+                    ];
+                }
+                $field = ['count(1)' => 'count', 'substr(create_at,1,10)' => 'mday'];
+                $orderNum = $mode->field($field)->where('status','in',[1,3])->whereTime('create_at', '-'.$orderDay.' days')->group('mday')->select()->column(null, 'mday');
+                for ($i = $orderDay; $i >= 0; $i--) {
+                    $date = date('Y-m-d', strtotime("-{$i}days"));
+                    $orders[] = [
+                        '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                        '订单数量' => ($orderNum[$date] ?? [])['count'] ?? 0,
+                    ];
+                }
+            } else {
+                for ($i = $moneyDay; $i >= 0; $i--) {
+                    $date = date('Y-m-d', strtotime("-{$i}days"));
+                    $moneys[] = [
+                        '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                        '订单金额' => 888,
+                    ];
+                }
+                for ($i = $orderDay; $i >= 0; $i--) {
+                    $date = date('Y-m-d', strtotime("-{$i}days"));
+                    $orders[] = [
+                        '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                        '订单数量' => 888,
+                    ];
+                }
+            }
+            return success("ok",compact("data","orders","moneys"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "store",methods: "get")]
+    public function getStoreData(Request $request): Response
+    {
+        try {
+            $map = [ "agent_id" => $request->user['agent_id'] ];
+            $total = (new SaasOrderLife)->where($map)->where("status","in",[1,2,3,6])->whereDay("create_at")->field("ROUND(sum(pay_amount)/100,2) as money,count(id) as num")->find();
+            $today = [
+                "order_money"   => $total['money']??0, // 营业额
+                "order_num"     => $total['num']??0, // 订单数
+                "order_done"    => "", // 已核销
+                "order_send"    => "",  // 待发货
+                "store_num"     => (new SaasStore)->where($map)->count(), // 总店铺数量
+            ];
+            $moneys = $orders = [];
+            $fields = ['ROUND(sum(pay_amount)/100,2)' => 'total','substr(create_at,1,10)' => 'mday'];
+            $orderMoney = (new SaasOrderLife)->field($fields)->where($map)->where("status","in",[1,3,4,5])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $moneys[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单金额' => ($orderMoney[$date] ?? [])['total'] ?? 0,
+                ];
+            }
+            $field = ['count(1)' => 'count', 'substr(create_at,1,10)' => 'mday'];
+            $orderNum = (new SaasOrderLife)->field($field)->where($map)->where("status","in",[1,2,3,6])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $orders[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单数量' => ($orderNum[$date] ?? [])['count'] ?? 0,
+                ];
+            }
+            return success("ok",compact("today","moneys","orders"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "shop",methods: "get")]
+    public function getShopData(Request $request): Response
+    {
+        try {
+            $map = [ "store_id" => $request->user['store_id'],"agent_id" => $request->user['agent_id'] ];
+            $total = (new SaasOrderLife)->where($map)->where("status","in",[1,3,4,5])->whereDay("create_at")->field("ROUND(sum(pay_amount)/100,2) as money,count(id) as num")->find();
+            $today = [
+                "order_money"   => $total['money']??0, // 营业额
+                "order_num"     => $total['num']??0, // 订单数
+                "order_done"    => "", // 已核销
+                "order_send"    => "",  // 待发货
+            ];
+            $moneys = $orders = [];
+            $fields = ['ROUND(sum(pay_amount)/100,2)' => 'total','substr(create_at,1,10)' => 'mday'];
+            $orderMoney = (new SaasOrderLife)->field($fields)->where($map)->where("status","in",[1,3,4,5])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $moneys[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单金额' => ($orderMoney[$date] ?? [])['total'] ?? 0,
+                ];
+            }
+            $field = ['count(1)' => 'count', 'substr(create_at,1,10)' => 'mday'];
+            $orderNum = (new SaasOrderLife)->field($field)->where($map)->where("status","in",[1,3,4,5])->whereTime('create_at', '-15 days')->group('mday')->select()->column(null, 'mday');
+            for ($i = 15; $i >= 0; $i--) {
+                $date = date('Y-m-d', strtotime("-{$i}days"));
+                $orders[] = [
+                    '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                    '订单数量' => ($orderNum[$date] ?? [])['count'] ?? 0,
+                ];
+            }
+            return success("ok",compact("today","moneys","orders"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 129 - 0
app/controller/admin/Combo.php

@@ -0,0 +1,129 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\ComboService;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasCombo;
+use app\model\system\SystemUser;
+use app\validate\saas\ComboValidate;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use LinFly\Annotation\Route\Middleware;
+
+
+#[Controller(prefix: "/api/combo"),Middleware(AuthMiddleware::class)]
+class Combo extends Base
+{
+
+    #[Inject]
+    protected ComboValidate $validate;
+
+    #[Inject]
+    protected SaasCombo $model;
+
+    #[Inject]
+    protected ComboService $service;
+
+
+    #[Route(path: "list",methods: "get")]
+    public function getStoreList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['type'] = 1;
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 新增/编辑代理
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function save(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (!empty($param['money'])) $param['money'] = $param['money'] * 100;
+            if (!empty($param['old_money'])) $param['old_money'] = $param['old_money'] * 100;
+            if (!$this->validate->check($param)) 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
+     */
+    #[Route(path: "batch",methods: "post")]
+    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
+     */
+    #[Route(path: "del",methods: "post")]
+    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());
+        }
+    }
+
+}

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

@@ -0,0 +1,84 @@
+<?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\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use LinFly\Annotation\Route\Middleware;
+
+
+#[Controller(prefix: "/api/config"),Middleware(AuthMiddleware::class)]
+class Config extends Base
+{
+
+
+    /**
+     * 获取配置信息
+     */
+    #[Route(path: "list",methods: "get")]
+    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());
+        }
+    }
+
+
+    /**
+     * 保存通用配置
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    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());
+        }
+    }
+
+    /**
+     * 获取地区列表
+     * @return Response
+     */
+    #[Route(path: "regin",methods: "get")]
+    public function getConfigRegin(): Response
+    {
+        try {
+            return successTrans(100010,[
+                "oss"       => OssRegionExtend::OssRegion(),
+                "cos"       => OssRegionExtend::CosRegion(),
+                "qiniu"     => OssRegionExtend::QiniuRegion(),
+            ]);
+        } catch (\Exception $exception) {
+            return error($exception->getMessage());
+        }
+    }
+
+}

+ 29 - 0
app/controller/admin/Dashboard.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Response;
+use LinFly\Annotation\Route\Middleware;
+
+#[Controller(prefix: "/api/dashboard"),Middleware(AuthMiddleware::class)]
+class Dashboard extends Base
+{
+
+    /**
+     * @return Response
+     */
+    #[Route(path: "get",methods: "get")]
+    public function getDashData(): Response
+    {
+        try {
+            return successTrans("success.data",[]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 192 - 0
app/controller/admin/Shop.php

@@ -0,0 +1,192 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\ShopService;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasCombo;
+use app\model\saas\SaasShop;
+use app\model\saas\SaasStore;
+use app\model\saas\SaasStoreShop;
+use app\model\system\SystemUser;
+use app\validate\saas\ShopValidate;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use LinFly\Annotation\Route\Middleware;
+
+
+#[Controller(prefix: "/api/shop"),Middleware(AuthMiddleware::class)]
+class Shop extends Base
+{
+
+    #[Inject]
+    protected ShopValidate $validate;
+
+    #[Inject]
+    protected ShopService $service;
+
+    #[Inject]
+    protected SaasShop $model;
+
+    #[Route(path: "list",methods: "get")]
+    public function getStoreList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 新增/编辑代理
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function save(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (!isset($param['id'])) {
+                $param['shop_id'] = CodeExtend::random(16,1,date("md"));
+                if (!empty($param['username'])) {
+                    $userName = (new SystemUser)->where("username",$param['username'])->findOrEmpty();
+                    if (!$userName->isEmpty()) return errorTrans("error.user-exist");
+                }
+            }
+            if (!$this->validate->check($param)) return error($this->validate->getError());
+            $state = $this->model->setAutoData($param);
+            if (!$state) return errorTrans("error.data");
+            $param['truename'] = $param['shop_name'];
+//            $this->sceneUser($param,2,"id");
+            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
+     */
+    #[Route(path: "combo",methods: "post")]
+    public function comboData(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (empty($param['combo'])) return errorTrans("empty.require");
+            if (empty($param['shop_id'])) return errorTrans("empty.require");
+            $combo = (new SaasCombo)->where("id",$param['combo'])->findOrEmpty();
+            if ($combo->isEmpty()) return errorTrans("empty.data");
+            $shop = (new SaasShop)->where("shop_id",$param['shop_id'])->findOrEmpty();
+            if ($shop->isEmpty()) return errorTrans("empty.data");
+            if ($combo['unit'] == 1) { // 天
+                $shop->vip_end = date('Y-m-d H:i:s',strtotime("+{$combo['time']} day"));
+            } else {
+                $shop->vip_end = date('Y-m-d H:i:s',strtotime("+{$combo['time']} year"));
+            }
+            $state = $shop->save();
+            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
+     */
+    #[Route(path: "edit",methods: "post")]
+    public function edit(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (!$this->validate->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
+     */
+    #[Route(path: "batch",methods: "post")]
+    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
+     */
+    #[Route(path: "del",methods: "post")]
+    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());
+        }
+    }
+
+
+}

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

@@ -0,0 +1,65 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\tools\UploadExtend;
+use app\middleware\AuthMiddleware;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use Tinywan\Storage\Storage;
+
+
+#[Controller(prefix: "/api/upload"),Middleware(AuthMiddleware::class)]
+class Upload extends Base
+{
+
+
+    /**
+     * 上传文件
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "data",methods: "post")]
+    public function upload2image(Request $request): Response
+    {
+        try {
+            $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());
+        }
+    }
+
+
+
+    /**
+     * 上传文件
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "file",methods: "post")]
+    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());
+        }
+    }
+
+}

+ 185 - 0
app/controller/admin/User.php

@@ -0,0 +1,185 @@
+<?php
+
+namespace app\controller\admin;
+
+use app\extra\basic\Base;
+use app\extra\service\system\UserService;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemUser;
+use app\validate\saas\UserValidate;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use LinFly\Annotation\Route\Middleware;
+
+
+#[Controller(prefix: "/api/user"),Middleware(AuthMiddleware::class)]
+class User extends Base
+{
+
+    #[Inject]
+    protected UserService $service;
+
+    #[Inject]
+    protected SystemUser $model;
+
+    #[Inject]
+    protected UserValidate $validate;
+
+    protected string $scene = "edit";
+
+    #[Route(path: "list",methods: "get")]
+    public function getStoreList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+    /**
+     * 修改密码
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "passwd",methods: "post")]
+    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("id",$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());
+        }
+    }
+    /**
+     * 新增/编辑代理
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function save(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $param['salt'] = strtoupper(CodeExtend::random(10,3));
+            $param['password'] = md5($param['password'].$param['salt']);
+            if (!$this->validate->check($param)) return error($this->validate->getError());
+            $user = $this->model->where("username",$param['username'])->findOrEmpty();
+            if (!$user->isEmpty()) return errorTrans("error.user-exist");
+            $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
+     */
+    #[Route(path: "state",methods: "post")]
+    public function setUserState(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"        => trans(20010)
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $user = $this->model->where("id",$param['id'])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("empty.error");
+            if ($user['status'] == 1) {
+                $user->status = 0;
+            } else {
+                $user->status = 1;
+            }
+            $user->updated_at = getDateFull();
+            $state = $user->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "batch",methods: "post")]
+    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
+     */
+    #[Route(path: "del",methods: "post")]
+    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());
+        }
+    }
+
+
+}

+ 215 - 0
app/controller/api/Card.php

@@ -0,0 +1,215 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\jhfPay\Pay;
+use app\extra\tools\CodeExtend;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasCombo;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserBuy;
+use app\model\saas\SaasUserLog;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/wx_api/card"),Middleware(WxMiddleware::class)]
+class Card extends Base
+{
+
+    #[Route(path: "data",methods: "get")]
+    public function getCardData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => "参数错误"
+            ]);
+            if (!is_array($param)) return error($param);
+            $memberCard = (new SaasUser)->where("shop_id",$param['shop'])->where("openid",$request->user['openid'])->with(["shop" => function($query){
+                $query->field("shop_id,shop_name,user_card_price,user_card");
+            }])->findOrEmpty();
+            if ($memberCard->isEmpty()) return success("",[],2);
+            $memberCard['balance'] = format_money($memberCard['balance']/100,2);
+            $memberCard['end_at'] = date("Y-m-d",strtotime($memberCard['create_at']));
+            $isRecharge = (new SaasUserBuy)->where("shop_id",$param['shop'])->where("openid",$request->user['openid'])->where("status",1)->sum("money");
+            $cardPrice = [];
+            if ($memberCard['shop']['user_card'] < 3) {
+                if ($memberCard['shop']['user_card'] == 2) { // 自定义套餐
+                    $cardPrice = array_values($memberCard['shop']['user_card_price']);
+                } else {
+                    $cardPrice = (new SaasCombo)->where("type",2)->select();
+                }
+                $cardPrice = array_filter($cardPrice, function($item) use ($isRecharge) {
+                    if ($isRecharge > 0) {
+                        return $item['is_first'] != '1'; // 注意:这里使用松散比较,因为数据中有字符串'1'
+                    } else {
+                        return $item;
+                    }
+                });
+                foreach ($cardPrice as $key=>$val) {
+                    $cardPrice[$key] = $val;
+                    $cardPrice[$key]['money'] = $val['money'];
+                    $cardPrice[$key]['old_money'] = $val['old_money'];
+                }
+                $cardPrice = array_values($cardPrice);
+            }
+            $memberCard['card'] = array_values($cardPrice);
+            return success("ok",$memberCard->toArray());
+        } catch (\Throwable $throwable) {
+            echo getDateFull()."==会员卡报错\n";
+            echo $throwable->getLine()."\n";
+            echo $throwable->getFile()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 会员卡消费明细
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "log",methods: "get")]
+    public function getOrderList(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "page.require"  => "参数错误",
+                "size.require"  => "参数错误",
+                "shop.require"  => "参数错误",
+                "card.require"  => "参数错误"
+            ]);
+            if (!is_array($param)) return error($param);
+            $map = ["openid" => $request->user['openid'],"shop_id" => $param['shop']];
+            $resp = (new SaasUserLog)->where($map)->order("create_at desc")->paginate([
+                "list_rows" => $param['size'],
+                "page"      => $param['page']
+            ]);
+            return success("ok",$resp->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 下单
+     * @param Request $request
+     * @return Response|void|null
+     */
+    #[Route(path: "create",methods: "post")]
+    public function createOrder(Request $request)
+    {
+        try {
+            $param = $this->_valid([
+                "card.require"  => "参数错误",
+                "shop.require"  => "参数错误"
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $map = ["openid" => $request->user['openid'],"shop_id" => $param['shop']];
+            $card = (new SaasUser)->where($map)->with(['shop' => function($query){
+                $query->field("shop_id,shop_name");
+            }])->findOrEmpty();
+            if ($card->isEmpty()) return error("尚未开通会员卡");
+            return error("充值通道暂时关闭");
+            $orderSn = strtoupper(CodeExtend::random(12,3));
+            $buyCard = json_decode($param['card'],true);
+            $state = (new SaasUserBuy)->insertGetId([
+                "openid"        => $request->user['openid'],
+                "order_sn"      => $orderSn,
+                "shop_id"       => $param['shop'],
+                "card_no"       => $card['card_no'],
+                "money"         => $buyCard['money'] * 100,
+                "total_money"   => $buyCard['money'] * 100 + $buyCard['old_money'] * 100,
+                "remark"        => "赠送金额".$buyCard['old_money']
+            ]);
+            if ($state) {
+                $param_data = [];
+                $param_data["order_no"] = $orderSn;
+                $param_data["app_id"] = sConf("wechat.jhf_appid");
+                $param_data["pay_channel"] = "wx_lite";
+                $param_data["pay_amt"] = format_money($buyCard['money'],2);
+                $param_data["goods_title"] = $card['shop']['shop_name']."-会员卡充值";
+                $param_data["device_info"] = array("device_ip" => $request->getRealIp());
+                $param_data['notify_url'] = "https://panel.huiyinduo.cn/notify/recharge";
+                $param_data["expend"] = [
+                    "wx_app_id" => sConf("wechat.mini_appid"),
+                    "open_id" => $request->user['openid']
+                ];
+                $respJhf = (new Pay)->config([
+                    "appid"  => sConf("wechat.jhf_appid"),
+                    "mch_id" => sConf("wechat.jhf_mch_id"),
+                    "aeskey" => sConf("wechat.jhf_aeskey"),
+                    "pubkey" => sConf("wechat.jhf_pubkey"),
+                    "prikey" => sConf("wechat.jhf_prikey"),
+                ])->createPay($param_data);
+                if (isset($respJhf['code'])) {
+                    return error("发起支付失败");
+                }
+                // 创建JSAPI参数签名
+                $resp = json_decode($respJhf['expend']['pay_info'],true);
+                $resp['timestamp'] = $resp['timeStamp'];
+//                $wechat = new \WeChat\Pay($this->getWxConfig());
+//                $options = [
+//                    'body'             => $card['shop']['shop_name']."-会员卡充值",
+//                    'out_trade_no'     => $orderSn."-".CodeExtend::random(8),
+//                    "attach"            => $orderSn,
+//                    'total_fee'        => $buyCard['money'] * 100,
+//                    'openid'           => $request->user['openid'],
+//                    'trade_type'       => 'JSAPI',
+//                    'notify_url'       => 'https://panel.huiyinduo.cn/notify/recharge',
+//                    'spbill_create_ip' => $request->getRealIp(),
+//                ];
+//                // 生成预支付码
+//                $result = $wechat->createOrder($options);
+//                // 创建JSAPI参数签名
+//                $resp = $wechat->createParamsForJsApi($result['prepay_id']);
+                return success("ok",$resp);
+            }
+            return error("发起充值失败");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "del",methods: "post")]
+    public function delCard(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => "参数错误"
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $map = ["openid" => $request->user['openid'],"shop_id" => $param['shop']];
+            $card = (new SaasUser)->where($map)->findOrEmpty();
+            if ($card->isEmpty()) return error("会员卡不存在");
+            $state = $card->delete();
+            if ($state) return success("注销成功");
+            return error("注销失败");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 小程序配置
+     * @return array
+     */
+    protected function getWxConfig(): array
+    {
+        return [
+            'token'          => 'test',
+            'appid'          => sConf("wechat.mini_appid"),
+            'appsecret'      => sConf("wechat.mini_secret"),
+            'encodingaeskey' => 'BJIUzE0gqlWy0GxfPp4J1oPTBmOrNDIGPNav1YFH5Z5',
+            // 配置商户支付参数(可选,在使用支付功能时需要)
+            'mch_id'         => sConf("wechat.mch_id"),
+            'mch_key'        => sConf("wechat.mch_key")
+        ];
+    }
+
+}

+ 505 - 0
app/controller/api/Cart.php

@@ -0,0 +1,505 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\tools\CodeExtend;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasCart;
+use app\model\saas\SaasCombo;
+use app\model\saas\SaasDiscount;
+use app\model\saas\SaasPrice;
+use app\model\saas\SaasPrintClient;
+use app\model\saas\SaasShop;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserBuy;
+use app\model\saas\SaasWordChange;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use Qcloud\Cos\Client;
+use support\Request;
+use support\Response;
+use Webman\RedisQueue\Redis;
+use yzh52521\EasyHttp\Http;
+
+
+#[Controller(prefix: "/wx_api/cart"),Middleware(WxMiddleware::class)]
+class Cart extends Base
+{
+    protected array $noNeedLogin = [];
+    /**
+     * 颜色
+     * @var array|string[]
+     */
+    protected array $color = ["1" => "彩色", "2" => "黑白"];
+    /**
+     * 单双面
+     * @var array|string[]
+     */
+    protected array $duplex = ["1" => "单面", "2" => "双面"];
+    /**
+     * 打印方向
+     * @var array|string[]
+     */
+    protected array $direction = ["1" => "长边翻转","2" => "短边翻转"];
+//    protected array $direction = ["1" => "自适应","2" => "横向", "3" => "竖向"];
+    /**
+     * 配送方式
+     * @var array|string[]
+     */
+    protected array $package = ["1" => "店内打印", "2" => '远程自取' , "3" => "商家配送"];
+
+    protected array $types = [
+        '1_1_1' => ['name' => '彩色-单面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+        '1_1_2' => ['name' => '彩色-单面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+        '1_2_1' => ['name' => '彩色-双面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+        '2_1_1' => ['name' => '黑白-单面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+        '2_2_1' => ['name' => '黑白-双面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+        '2_1_2' => ['name' => '黑白-双面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+    ];
+
+    /**
+     * 获取打印购物车列表
+     * @return Response
+     */
+    #[Route(path: "list",methods: "get")]
+    public function getCartList(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => trans("empty.require"),
+                "print.require" => trans("empty.require"),
+                "type.default"  => 1
+            ]);
+            if (!is_array($param)) return error($param);
+            $cart = (new SaasCart)->where("shop_id",$param['shop'])->where("print_id",$param['print'])->where("openid",$request->user['openid'])->order("create_at desc")->select();
+            if ($cart->isEmpty()) return success('ok',['cart' => []]);
+            $totalAmount = $totalDiscount = 0;
+            foreach ($cart as $k=>$v){
+                $key = $v['color'] . '_' . $v['duplex'] . '_' . $v['source'];
+                if (isset($this->types[$key])) {
+                    $this->types[$key]['quantity'] += $v['page'];
+                    $this->types[$key]['amount'] += $v['money'];
+                }
+                $cart[$k] = $v;
+                $cart[$k]['money'] = format_money($v['money'] / 100,2);
+                $cart[$k]['name'] = msubstr($v['name'],0,12);
+                $cart[$k]['jobStatus'] = 'success';
+            }
+            $printData = (new SaasPrintClient)->where("shop_id",$param['shop'])->where("code",$param['print'])->select();
+            if ($printData->isEmpty()) return error('无可用打印机');
+            $rule = [];
+            foreach ($printData as $key=>$value) {
+                $rule[$key]['check'] = 0;
+                $nameColor = "";
+                if (empty($value['rule'])) return error("尚未配置打印机~");
+                foreach ($value['rule']['color'] as $key2=>$val) {
+                    $rule[$key]['color'][$key2][$val] = $this->color[$val];
+                    $nameColor .= $this->color[$val];
+                }
+                foreach ($value['rule']['direction'] as $key2=>$val) {
+                    $rule[$key]['direction'][$key2][$val] = $this->direction[$val];
+                }
+                foreach ($value['rule']['duplex'] as $key2=>$val) {
+                    $rule[$key]['duplex'][$key2][$val] = $this->duplex[$val];
+                }
+                foreach ($value['rule']['package'] as $key2=>$val) {
+                    $rule[$key]['package'][$key2][$val] = $this->package[$val];
+                }
+                foreach ($value['rule']['paper_size'] as $key2=>$val) {
+                    $rule[$key]['paper_size'][$key2][$val] = $val;
+                }
+                if ($param['print'] == $value['code']) {
+                    $rule[$key]['check'] = 1;
+                }
+                $rule[$key]['name'] = $nameColor."-".$value['name'];
+                $rule[$key]['code'] = $value['code'];
+            }
+            // 计算折扣
+            foreach ($this->types as $k=>$v) {
+                $discount = (new SaasDiscount)->where("shop_id",$param['shop'])->where("keys",$k)->where("number",'<',$v['quantity'])->findOrEmpty();
+                if (!$discount->isEmpty()) {
+                    $v['discount'] = round($v['amount'] * $discount['rate']);
+                    $this->types[$k]['discount'] = $v['discount'];
+                }
+                $totalAmount += $v['amount'];
+                $totalDiscount += $v['discount'];
+            }
+            $totalAmount = format_money($totalAmount / 100,2);
+            $totalDiscount = format_money($totalDiscount / 100,2);
+            if ($param['type'] <> 1) {
+                $shop = (new SaasShop)->where("shop_id",$param['shop'])->field("shop_name,shop_address,user_card,user_card_price")->find();
+                $isRecharge = (new SaasUserBuy)->where("shop_id",$param['shop'])->where("openid",$request->user['openid'])->where("status",1)->sum("money");;
+                $cardPrice = [];
+                if ($shop['user_card'] < 3) {
+                    if ($shop['user_card'] == 2) { // 自定义套餐
+                        $cardPrice = array_values($shop['user_card_price']);
+                    } else {
+                        $cardPrice = (new SaasCombo)->where("type",2)->field("id,name,ROUND(money/100,2) as money,ROUND(old_money/100,2) as old_money,is_first")->where("status",1)->select()->toArray();
+                    }
+                    $cardPrice = array_filter($cardPrice, function($item) use ($isRecharge) {
+                        if ($isRecharge > 0) {
+                            return $item['is_first'] != '1'; // 注意:这里使用松散比较,因为数据中有字符串'1'
+                        } else {
+                            return $item;
+                        }
+                    });
+                    foreach ($cardPrice as $key=>$val) {
+                        $cardPrice[$key] = $val;
+                        $cardPrice[$key]['money'] = $val['money'];
+                        $cardPrice[$key]['old_money'] = $val['old_money'];
+                    }
+                    $cardPrice = array_values($cardPrice);
+                }
+                $card = (new SaasUser)->where("openid",$request->user['openid'])->where("shop_id",$param['shop'])->field("ROUND(balance/100,2) as money")->findOrEmpty();
+                if ($card->isEmpty()) {
+                    $card = null;
+                }
+                $package = $this->package;
+                return success("",compact('rule','totalAmount','totalDiscount','shop','cardPrice','package','card'));
+            }
+            return success("",compact('cart','rule','totalAmount','totalDiscount'));
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * 删除购物
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "del",methods: "post")]
+    public function delCart(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => trans("empty.require"),
+                "id.require"    => trans("empty.require"),
+                "print.require" => trans("empty.require"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $cart = (new SaasCart)->where("id",$param['id'])->findOrEmpty();
+            if ($cart->isEmpty()) return error("操作失败");
+            if ($cart['openid'] <> $request->user['openid']) return error("操作失败");
+            $state = $cart->delete();
+            if (!$state) return error("操作失败");
+            return success("操作成功");
+        } catch (\Throwable $exception) {
+            return error($exception->getMessage());
+        }
+    }
+
+    /**
+     * 更新购物
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "update",methods: "post")]
+    public function updateCart(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $cart = (new SaasCart)->where("id",$param['id'])->where("openid",$request->user['openid'])->findOrEmpty();
+            if ($cart->isEmpty()) return error('数据格式错误');
+            if ($param['end_page'] > $cart['total_page']) return error('打印范围不能大于总页数');
+            // 查询默认打印机是否有额外收费规则
+            $printData = (new SaasPrintClient)->where("shop_id",$param['shop_id'])->where("code",$param['print'])->findOrEmpty();
+            $extraMoney = 0;
+            $moneyMode = (new SaasPrice)->where([
+                "shop_id"   => $param['shop_id'],
+                "paper_size" => $param['paper_size'],
+                "type"      => $param['source'],
+                "color"     => $param['color'],
+                "duplex"    => $param['duplex'],
+            ])->findOrEmpty();
+            if ($moneyMode->isEmpty()) return error("尚未设置收费规则");
+            if (!empty($printData['price']) && $printData['is_price'] == 1) {
+                $priceRule = isset($printData['price'][$moneyMode['id']]['price']) ? $printData['price'][$moneyMode['id']]['price'] : 0;
+                $extraMoney = $priceRule * 100;
+            }
+            if ($param['duplex'] == 2) {
+                $updateData['page'] = ceil(($param['end_page'] - $param['start_page'] + 1) / 2); // 双面
+            } else {
+                $updateData['page'] = $param['end_page'] - $param['start_page'] + 1;
+            }
+            $updateData['extra_money'] = $extraMoney;
+            $updateData['money'] = ($moneyMode['price']*100 + $extraMoney) * $updateData['page'] * $param['number'];
+            $updateData['single_money'] = $moneyMode['price']*100;
+            $updateData['single_id'] = $moneyMode['id'];
+            $updateData['paper_size'] = $param['paper_size'];
+            $updateData['number'] = $param['number'];
+            $updateData['duplex'] = $param['duplex'];
+            $updateData['direction'] = $param['direction']??1;
+            $updateData['color'] = $param['color'];
+            $updateData['start_page'] = $param['start_page'];
+            $updateData['end_page'] = $param['end_page'];
+            $state = $cart->save($updateData);
+            if (!$state) return error("数据操作失败");
+            return success("更新成功");
+        } catch (\Throwable $exception) {
+            return error($exception->getMessage());
+        }
+    }
+
+
+    /**
+     * 预览
+     * @return Response
+     */
+    #[Route(path: "preview",methods: "post")]
+    public function wordPreview(): Response
+    {
+        try {
+            return success("",[
+                "host"  => "https://".sConf("storage.cos_http_domain")."/",
+                "query" => "?ci-process=doc-preview&dstType=jpg&imageDpi=120&page="
+            ]);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    /**
+     * 图片打印
+     * @return Response
+     */
+    #[Route(path: "image",methods: "post")]
+    public function uploadMultiImage(): Response
+    {
+        try {
+            return success("");
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * 文档打印
+     * @return Response
+     */
+    #[Route(path: "word",methods: "post")]
+    public function uploadWord(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"      => trans("empty.require"),
+                "word.require"      => trans("empty.require"),
+                "print.require"     => trans("empty.require"),
+                "type.default"      => 1, // 1打印 2复印
+                "size.default"      => "", // 纸张类型
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $wordData = json_decode($param["word"], true);
+            $printData = (new SaasPrintClient)->where(['shop_id' => $param['shop'],'code' => $param['print']])->findOrEmpty();
+            if ($printData->isEmpty()) return error('无可用打印机');
+            if ($printData['status'] <> 1) return error('当前打印机不可用');
+            $paperRule = is_string($printData['rule'])?json_decode($printData['rule'],true):$printData['rule'];
+            $paperSize = count($paperRule['paper_size'])==1?$paperRule['paper_size'][0]:'A4';
+            $colorSize = count($paperRule['color'])==1?$paperRule['color'][0]:2;
+
+            $paperSize = empty($param['size'])?$paperSize:$param['size'];
+            $moneyMode = (new SaasPrice)->where(['shop_id' => $param['shop'],'paper_size' => $paperSize,'color' => $colorSize,'type' => $param['type']])->findOrEmpty();
+            if ($moneyMode->isEmpty()) return error("店铺未设置收费规则");
+            $extraMoney = 0;
+            // 计算额外收费
+            if (!empty($printData['price']) && $printData['is_price'] == 1) {
+                $priceRule = isset($printData['price'][$moneyMode['id']]['price']) ? $printData['price'][$moneyMode['id']]['price'] : 0;
+                $extraMoney = $priceRule * 100 ;
+            }
+            $cartData = [];
+            foreach ($wordData as $key=>$val) {
+                if ($val['total'] <= 0) {
+                    return error("请检查上传文档是否有显示页数");
+                }
+                $cartData[$key] = [
+                    "money"         => ($val['total'] * $moneyMode['price'] * 100) + ($extraMoney * $val['total']),
+                    "extra_money"   => $extraMoney,
+                    "number"        => 1,
+                    "duplex"        => 1,
+                    "color"         => $colorSize,
+                    "page"          => $val['total'],
+                    "total_page"    => $val['total'],
+                    "end_page"      => $val['total'],
+                    "name"          => $val['name'],
+                    "openid"        => $request->user['openid'],
+                    "shop_id"       => $param['shop'],
+                    "paper_size"    => $paperSize,
+                    "extension"     => $val['ext'],
+                    "source"        => $param['type'],
+                    "icon"          => "https://inmei-print.oss-cn-guangzhou.aliyuncs.com/extension/{$val['ext']}.png", // 图标
+                    "single_money"  => $moneyMode['price'] * 100,
+                    "single_id"     => $moneyMode['id'],
+                    "path"          => $val['cosKey'],
+                    "print_id"      => $param['print'],
+                    "print_name"    => $printData['name'],
+                ];
+            }
+            if (empty($cartData)) return error('上传失败');
+            $state = (new SaasCart)->insertAll($cartData);
+            if (!$state) return error("解析文档失败,请重试");
+            return success("ok");
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * 读取页码
+     * ?ci-process=doc-preview&page=1&dstType=jpg&imageDpi=120
+     * ?ci-process=doc-preview&page=2&sheet=1&excelPaperDirection=0
+     */
+    #[Route(path: "total",methods: ['post','get'])]
+    public function checkTotal(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "cosKey.require"  => trans("empty.require"),
+                "type.default"  => "",
+                "ext.default"  => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $suffix = pathinfo($param['cosKey'], PATHINFO_EXTENSION);
+            if (empty($suffix)) return error("empty.data.suffix");
+            return error("请重启小程序");
+            $cosClient = new Client([
+                'region' => sConf("storage.cos_region"),
+                'schema' => 'https', // 协议头部,默认为 http
+                'credentials' => array(
+                    'secretId' => sConf("storage.cos_access_key"),
+                    'secretKey' => sConf("storage.cos_secret_key"),
+                ),
+                "verify"    => false
+            ]);
+            $url = $cosClient->getObjectUrl(sConf("storage.cos_bucket"), $param['cosKey']);
+            $params = array(
+                'ci-process' => 'doc-preview',
+                'page' => 1,
+                'dstType' => 'jpg',
+                'imageDpi' => '120',
+            );
+            $query = http_build_query($params);
+            $path = $url.$query;
+            $resp = Http::get($path)->headers();
+            if (!isset($resp['X-Total-Page'])) return error("文档可能需要密码,请先删除后再确认");
+            $page = $resp['X-Total-Page']?$resp['X-Total-Page'][0]:1;
+            $path = $param['cosKey'];
+            return success("ok",compact('page','path'));
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    #[Route(path: "change_total",methods: ['post','get'])]
+    public function checkChangeStep(Request $request)
+    {
+        try {
+            $param = $this->_valid([
+                "cosKey.require"  => trans("empty.require"),
+                "type.default"  => "",
+                "ext.default"  => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $suffix = pathinfo($param['cosKey'], PATHINFO_EXTENSION);
+            if (empty($suffix)) return error("empty.data.suffix");
+            $change = (new SaasWordChange)->where("key",md5($param['cosKey']))->findOrEmpty();
+            $cosClient = new Client([
+                'region' => sConf("storage.cos_region"),
+                'schema' => 'https', // 协议头部,默认为 http
+                'credentials' => array(
+                    'secretId' => sConf("storage.cos_access_key"),
+                    'secretKey' => sConf("storage.cos_secret_key"),
+                ),
+                "verify"    => false
+            ]);
+            if ($change->isEmpty())
+            {
+                $url = $cosClient->getObjectUrl(sConf("storage.cos_bucket"), $param['cosKey']);
+                $params = array(
+                    'ci-process' => 'doc-preview',
+                    'page' => 1,
+                    'dstType' => 'jpg',
+                    'imageDpi' => '120',
+                );
+                $query = http_build_query($params);
+                $path = $url.$query;
+                $resp = Http::get($path)->headers();
+                if (!isset($resp['X-Total-Page'])) return error("文档可能需要密码,请先删除后再确认");
+                $page = $resp['X-Total-Page']?$resp['X-Total-Page'][0]:1;
+
+                $result = $cosClient->CreateDocProcessJobs([
+                    'Bucket' => sConf("storage.cos_bucket"), // 存储桶名称,由 BucketName-Appid 组成,可以在 COS 控制台查看 https://console.cloud.tencent.com/cos5/bucket
+                    'Tag' => 'DocProcess', //任务的 Tag:DocProcess 固定值
+                    'Input' => array(
+                        'Object' => $param['cosKey'] //待操作的文件对象
+                    ),
+                    'Operation' => array(
+                        'DocProcess' => array(
+                            'SheetId' => 0, //表格文件参数,转换第 X 个表,默认为1
+                            'StartPage' => 1, //从第 X 页开始转换,默认为1
+                            'Quality' => 100, //生成预览图的图片质量,取值范围 [1-100],默认值100
+                            'Zoom' => 100, //预览图片的缩放参数,取值范围[10-200], 默认值100
+                            "ImageDpi" => 300
+                        ),
+                        'Output' => array(
+                            'Region' => sConf("storage.cos_region"), //存储桶的地域
+                            'Bucket' => sConf("storage.cos_bucket"), // 存储结果的存储桶
+                            'Object' => 'out/'.date('Ymd').'/'.time().md5($param['cosKey']).'-${Number}.pdf', //输出文件路径
+                        ),
+                    ),
+                ])->toArray();
+                $change->insertGetId([
+                    "key"       => md5($param['cosKey']),
+                    "task_id"   => $result['JobsDetail']['JobId']??'',
+                    "total"     => $page
+                ]);
+                return error("error");
+            }
+            $result = $cosClient->describeDocProcessJob(array(
+                'Bucket' => sConf("storage.cos_bucket"), // 存储桶名称,由 BucketName-Appid 组成,可以在 COS 控制台查看 https://console.cloud.tencent.com/cos5/bucket
+                'Key' => $change['task_id'], // JobId
+            ))->toArray();
+            if ($result['JobsDetail']['State'] == 'Success') {
+                $change->status = 1;
+                $change->path = $result['JobsDetail']['Operation']['DocProcessResult']['PageInfo'][0]['TgtUri']??'';
+                $change->end_at = getDateFull();
+                $change->save();
+                $page = $change['total'];
+                $path = $change['path'];
+                return success("ok",compact("path","page"));
+            }
+            return error("Error");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+    /**
+     * 获取打印机支持的纸张
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "size",methods: ['post','get'])]
+    public function getPageSize(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"      => trans("empty.require"),
+                "print.require"     => trans("empty.require")
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $printData = (new SaasPrintClient)->where(['shop_id' => $param['shop'],'code' => $param['print']])->findOrEmpty();
+            if ($printData->isEmpty()) return error('无可用打印机');
+            $paperRule = is_string($printData['rule'])?json_decode($printData['rule'],true):$printData['rule'];
+            return success("ok",['size' => $paperRule['paper_size']]);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,193 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\tools\CodeExtend;
+use app\extra\weMini\Crypt;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasShop;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserOpen;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use LinFly\Annotation\Route\Middleware;
+use Shopwwi\WebmanAuth\Facade\Auth as AuthMode;
+use support\think\Db;
+
+
+#[Controller(prefix: "/wx_api/default"),Middleware(WxMiddleware::class)]
+class Home extends Base
+{
+
+
+    protected array $whiteShop = [
+        "10888572278827"    => "10888572278827", // 惠印多马村店
+        "10888566286841"    => "327545130062964", // 中站小学店
+        "10888590458813"    => "326945519114632", // 幸小印美店
+        "10888645729727"    => "326844775971796", // 焦作和平街店
+    ];
+
+    /**
+     * 跳出授权
+     * @var array|string[]
+     */
+    protected array $noNeedLogin = ['loginHome','changeData','changeDataSubmit'];
+
+
+    #[Route(path: "change_save",methods: "post")]
+    public function changeDataSubmit(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => "",
+                "old.require"  => "",
+                "openid.default"  => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $member = (new SaasUser)->where("openid",$param['openid'])->findOrEmpty();
+            if (!$member->isEmpty()) return error("无需多次操作");
+            $old = Db::connect("old")->table("inmei_member_card")->where("openid",$param['old'])->findOrEmpty();
+            if (isset($this->whiteShop[$param['shop']])) {
+                $shopId = $this->whiteShop[$param['shop']];
+            } else {
+                $shopId = $param['shop'];
+            }
+            $state = $member->insertGetId([
+                "openid"    => $param['openid'],
+                "shop_id"   => $shopId,
+                "card_no"   => strtoupper(md5($param['openid'].$shopId)),
+                "balance"   => $old['balance'],
+                "total_balance" => $old['total_balance']
+            ]);
+            if (!$state) return error("操作失败");
+            return success("转入成功");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[Route(path: "change",methods: "post")]
+    public function changeData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => "",
+                "code.require"  => "",
+                "openid.default"  => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $userinfo = (new Crypt([
+                "appid"     => sConf("wechat.mini_appid"),
+                "appsecret" => sConf("wechat.mini_secret")
+            ]))->session($param['code']);
+            if (!isset($userinfo['openid'])) return error("获取数据失败");
+            $openId = $userinfo['openid'];
+            $old = Db::connect("old")->table("inmei_member_card")->where("openid",$param['openid'])->where("shop_id",$param['shop'])->findOrEmpty();
+            if (empty($old)) $old = null;
+            if (isset($this->whiteShop[$param['shop']])) {
+                $shopId = $this->whiteShop[$param['shop']];
+            } else {
+                $shopId = $param['shop'];
+            }
+            $shop = (new SaasShop)->where("shop_id",$shopId)->findOrEmpty();
+            if ($shop->isEmpty()) $shop = null;
+            return success("ok",compact("shop",'old','openId'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 首页信息
+     * @return Response
+     */
+    #[Route(path: "home",methods: "post")]
+    public function loginHome(Request $request)
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => "",
+                "print.require" => "",
+                "code.require"  => "",
+                "card.default"  => "",
+                "oshop.default"  => "",
+                "oid.default"  => ""
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $userinfo = (new Crypt([
+                "appid"     => sConf("wechat.mini_appid"),
+                "appsecret" => sConf("wechat.mini_secret")
+            ]))->session($param['code']);
+            if (!isset($userinfo['openid'])) return error("获取数据失败");
+            $map = ['is_deleted' => 0, 'openid' => $userinfo['openid']];
+            $user = (new SaasUserOpen)->where($map)->findOrEmpty();
+            if ($user->isEmpty()) {
+                $user->insertGetId(['openid' => $userinfo['openid'],'oid' => ($param['oid'] == 'undefined'?'':$param['oid']),'update_at' => getDateFull(),"create_ip" => $request->getRealIp(),"nickname" => "微信用户","headimg" => "https://inmei-print.oss-cn-guangzhou.aliyuncs.com/logo.png"]);
+                $user = (new SaasUserOpen)->where($map)->findOrEmpty();
+            } else {
+                if (!empty($param['oid']) && empty($user['oid'])) {
+                    if ($param['oid'] !== 'undefined') {
+                        $user->oid = ($param['oid'] == 'undefined'?'':$param['oid']);
+                        $user->update_at = getDateFull();
+                        $user->save();
+                    }
+                }
+            }
+            $userAuth = get_object_vars(AuthMode::guard("member")->login($user->toArray()));
+            $shopData = [];
+            $cardState = 0;
+            if (!empty($param['shop']))
+            {
+                $shop = (new SaasShop)->where("shop_id", $param['shop'])->field("shop_name,start_at,end_at,vip_end,shop_status,shop_notice,line_time,status,shop_address")->findOrEmpty();
+                if ($shop->isEmpty()) return error("店铺已被关闭");
+                $currentTime = time();
+                $vipEndTime = strtotime($shop['vip_end'] ?? '');
+                $lineEndTime = strtotime("+10020000 min", strtotime($shop['line_time'] ?? ''));
+                $today = date('Y-m-d');
+                $startTime = strtotime("{$today} {$shop['start_at']}");
+                $endTime = strtotime("{$today} {$shop['end_at']}");
+                if ($currentTime > $vipEndTime){
+                    $status = ['text' => '已过期', 'val' => 0];
+                } else if ($shop['status'] == 2) {
+                    $status = ['text' => '已冻结', 'val' => 0];
+                } else if ($currentTime > $lineEndTime) {
+                    $status = ['text' => '已离线', 'val' => 0];
+                } else if ($currentTime > $endTime || $currentTime < $startTime) // 过了营业时间
+                {
+                    $status = ['text' => '休息中', 'val' => 0];
+                } else {
+                    $status = ['text' => '营业中', 'val' => 1];
+                }
+                $shop['status_text'] = $status['text'];
+                $shop['status_val'] = $status['val'];
+                $shop['time'] = ($shop['start_at'] == '00:00' && $shop['end_at'] == '23:59')
+                    ? '24小时营业'
+                    : date('H:i', $startTime) . '-' . date('H:i', $endTime);
+                if ($param['card'] == 1) { // 领取会员卡
+                    $cardInfo = (new SaasUser)->where("shop_id",$param['shop'])->where("openid",$userinfo['openid'])->findOrEmpty();
+                    if ($cardInfo->isEmpty()) {
+                        $cardInfo->insertGetId([
+                            "shop_id"   => $param['shop'],
+                            "openid"    => $userinfo['openid'],
+                            "card_no"   => CodeExtend::uniqidDate(16,"1088")
+                        ]);
+                        $cardState = 1;
+                    }
+                }
+                $shopData = $shop->toArray();
+            }
+            $share = sConf("wechat.share");
+            return success("ok",["shop" => $shopData,'user' => ['nickname' => $user['nickname']??'','avatar' => $user['avatar']??''],"token" => $userAuth,"card" => $cardState,"share" => $share]);
+        }catch (\Throwable $e){
+            echo $e->getMessage()."\n";
+            echo $e->getFile()."\n";
+            echo $e->getLine()."\n";
+            return error($e->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,391 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\jhfPay\Utils;
+use app\model\saas\SaasCart;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderDetail;
+use app\model\saas\SaasShop;
+use app\model\saas\SaasShopLog;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserBuy;
+use app\model\saas\SaasUserLog;
+use app\model\system\SystemUserMoney;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use think\facade\Db;
+use WeChat\Contracts\Tools;
+
+
+#[Controller(prefix: "/notify")]
+class Notify extends Base
+{
+
+
+    /**
+     * 提现银行卡回调
+     */
+    #[Route(path: "withdraw",methods: "post")]
+    public function notifyWithdraw(Request $request): Response
+    {
+        try {
+            echo getDateFull() . "===>提现银行卡异步返回\n";
+            $data = $this->jhfReturn($request->all(),"withdraw.succeeded");
+            if (empty($data)) return error("提现失败");
+            if ($data['status'] == "succeeded") // 成功了
+            {
+                $day = date("Y-m-d",strtotime("-1 day"));
+                $todayMoney = (new SystemUserMoney)->where("member_id",$data['member_id'])->where("day",$day)->select();
+                if ($todayMoney->count() > 0) {
+                    $state = (new SystemUserMoney)->where("member_id",$data['member_id'])->where("day",$day)->update(['status' => 1,"withdraw_id" => $data['withdraw_id']]);
+                }
+            }
+            return success("ok");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 退款回调
+     * @param Request $request
+     */
+    #[Route(path: "refund",methods: "post")]
+    public function notifyRefund(Request $request)
+    {
+        try {
+            echo getDateFull() . "===>退款申请返回\n";
+            $data = $this->jhfReturn($request->all(),"refund.succeeded");
+            if (empty($data)) return error("支付失败");
+//            $payResp = $request->rawBody();
+//            $wechat = new \WePay\Refund($this->getWxConfig());
+//            if (empty($payResp)) return $wechat->getNotifySuccessReply();
+//            $data = $wechat->getNotify($payResp);
+//            if (empty($data)) return $wechat->getNotifySuccessReply();
+//            $orderSn = explode("-",$data['attach']);
+            $order = (new SaasOrder)->where("order_sn",$data['attach'])->with(['shop' => function($query){
+                $query->field('shop_id,shop_name,shop_address');
+            }])->findOrEmpty();
+            if ($order->isEmpty()) return $this->getNotifySuccessReply();
+            if ($order['status'] <> 4) return $this->getNotifySuccessReply();
+            $order->status = 6;
+            $order->refund_at = getDateFull();
+            $order->save();
+            (new SaasOrderDetail)->where("order_sn",$order['order_sn'])->update(['status' => 6]);
+            $orderMoney = (($order['money'] > $order['discount']) ? $order['discount'] : $order['money']);
+            // 商家账户额度退款
+            $shop = (new SaasShop)->where("shop_id",$order['shop_id'])->with(['wx' => function($query){
+                $query->field("shop_id,openid,is_msg");
+            }])->findOrEmpty();
+            if ($shop->isEmpty()) return $this->getNotifySuccessReply();
+            $shop->balance = $shop['balance'] - $orderMoney;
+            $shop->total_balance = $shop['total_balance'] - $orderMoney;
+            $shop->save();
+            (new SaasShopLog)->insertGetId([
+                "shop_id"   => $order['shop_id'],
+                "money"     => $orderMoney,
+                "balance"   => $shop->balance,
+                "remark"    => "订单退款【{$order['order_sn']}】",
+                "type"      => 2
+            ]);// 推送消息-公众号
+            if (!empty($shop['wx'])) {
+                $obj = \We::WeChatTemplate([
+                    "appid" => sConf("wechat.appid"),
+                    "appsecret" => sConf("wechat.secret"),
+                    "token" => sConf("wechat.token"),
+                    "encodingaeskey" => sConf("wechat.aeskey")
+                ]);
+                foreach ($shop['wx'] as $val) {
+                    if ($val['is_msg'] == 1) {
+                        $obj->send([
+                            "touser" => $val['openid'],
+                            "template_id" => "WboxN-32rdHORBQHzaVvQb9kPN7rLaXAPS_BL2yfb5w",
+                            "data" => [
+                                "amount3" => ["value" => format_money($order['money']/100)],
+                                "character_string1" => ["value" => $order['old_order']],
+                                "time4" => ["value" => date('Y-m-d H:i')]
+                            ]
+                        ]);
+                    }
+                }
+            }
+
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 订单支付
+     */
+    #[Route(path: "wx",methods: "post")]
+    public function notifyWx(Request $request)
+    {
+        try {
+            echo getDateFull()."===>支付返回\n";
+            $data = $this->jhfReturn($request->all());
+            if (empty($data)) return error("支付失败");
+//            $payResp = $request->rawBody();
+//            $data = $this->payReturn($payResp);
+            $order = (new SaasOrder)->where("order_sn",$data['attach'])->with(['shop' => function($query){
+                $query->field('shop_id,shop_name,shop_address');
+            }])->findOrEmpty();
+            if ($order->isEmpty()) return "success";
+            if ($order['status'] <> 0) return "success"; // 已支付或者是其他状态
+            $order->status = 1;
+            $order->pay_at = getDateFull();
+            $order->transaction_id = $data['transaction_id']??'';
+            $order->payment_id = $data['payment_id']??'';
+            $order->notify_status = 1;
+            $order->pay_type = 1;
+            $order->save();
+            $shop = (new SaasShop)->where("shop_id",$order['shop_id'])->with(['wx' => function($query){
+                $query->field("shop_id,openid,is_msg");
+            }])->findOrEmpty();
+            if ($shop->isEmpty()) return "success";
+            $shop->balance = Db::raw("balance+".$order['money']);
+            $shop->total_balance = Db::raw("total_balance+".$order['money']);
+            $shop->save();
+            (new SaasShopLog)->insertGetId([
+                "shop_id"   => $order['shop_id'],
+                "money"     => $order['money'],
+                "balance"   => $shop->balance,
+                "remark"    => "新订单【{$order['order_sn']}】"
+            ]);
+            events("create-order",['shop' => $order['shop_id'],'print' => $order['print_id'],'openid' => $order['openid'],"order" => $order['order_sn']]);
+            // 推送消息-公众号
+            if (!empty($shop['wx'])) {
+                $obj = \We::WeChatTemplate([
+                    "appid" => sConf("wechat.appid"),
+                    "appsecret" => sConf("wechat.secret"),
+                    "token" => sConf("wechat.token"),
+                    "encodingaeskey" => sConf("wechat.aeskey")
+                ]);
+                foreach ($shop['wx'] as $val) {
+                    if ($val['is_msg'] == 1) {
+                        $obj->send([
+                            "touser" => $val['openid'],
+                            "template_id" => "D20ZEWmUNmXMHLwiTOzaEateX5NvM9zoCbp2YwbIHsI",
+                            "data" => [
+                                "thing20" => ["value" => $val['shop_name']],
+                                "character_string1" => ["value" => $order['order_sn']],
+                                "amount16" => ["value" => format_money($order['money'] / 100)],
+                                "time2" => ["value" => date('Y-m-d H:i')]
+                            ],
+                            "url" => "https://inmei.yunenv.cn/weixin/order/detail?id=" . $order['id']
+                        ]);
+                    }
+                }
+            }
+
+            return "success";
+        } catch (\Throwable $throwable) {
+            return "success";
+        }
+    }
+
+
+    /**
+     * 充值并支付
+     */
+    #[Route(path: "recharge",methods: "post")]
+    public function notifyPayRecharge(Request $request)
+    {
+        try {
+            echo getDateFull()."===>充值并支付支付返回\n";
+            $data = $this->jhfReturn($request->all());
+            if (empty($data)) return error("支付失败");
+            $orderBuy = (new SaasUserBuy)->where("order_sn",$data['attach'])->with(['orders'])->findOrEmpty();
+            if ($orderBuy->isEmpty()) return "success";
+            if ($orderBuy['status'] <> 0) return "success"; // 已支付或者是其他状态
+            $orderMoney = 0;
+            $logData[0] = [
+                "openid"    => $orderBuy['openid'],
+                "card_no"   => $orderBuy['card_no'],
+                "shop_id"   => $orderBuy['shop_id'],
+                "money"     => $orderBuy['total_money'],
+                "order_sn"  => $orderBuy['order_sn'],
+                "type"      => 2,
+                "remark"    => "充值",
+                "balance"   => $orderBuy['total_money']
+            ];
+            if (!empty($orderBuy['orders']))
+            {
+                $orderMoney = $orderBuy['orders']['money'];
+                $logData[1] = [
+                    "openid"    => $orderBuy['openid'],
+                    "card_no"   => $orderBuy['card_no'],
+                    "shop_id"   => $orderBuy['shop_id'],
+                    "order_sn"  => $orderBuy['order_sn'],
+                    "money"     => $orderMoney,
+                    "type"      => 1,
+                    "remark"    => "订单付款",
+                    "balance"   => $orderBuy['total_money'] - $orderMoney
+                ];
+            };
+            $orderBuy->status = 1;
+            $orderBuy->pay_at = getDateFull();
+            $orderBuy->payment_id = $data['payment_id']??'';
+            $orderBuy->transaction_id = $data['transaction_id']??'';
+            $orderBuy->save();
+            $shop = (new SaasShop)->where("shop_id",$orderBuy['shop_id'])->with(['wx' => function($query){
+                $query->field("shop_id,openid,is_msg");
+            }])->findOrEmpty();
+            if ($shop->isEmpty()) return "success";
+            // 开通vip账户
+            $card = (new SaasUser)->where("card_no",$orderBuy['card_no'])->findOrEmpty();
+            $balanceMoney = $orderBuy['total_money'] - $orderMoney;
+            if ($card->isEmpty()) {
+                $card->insertGetId([
+                    "openid"    => $orderBuy['openid'],
+                    "shop_id"   => $orderBuy['shop_id'],
+                    "card_no"   => $orderBuy['card_no'],
+                    "balance"   => $balanceMoney, // 余额
+                    "total_balance"   => $orderBuy['money'], // 累计充值
+                    "total_consume"   => $orderMoney // 累计消费
+                ]);
+            } else {
+                $card->total_consume = Db::raw("total_consume+{$orderMoney}");
+                $card->total_balance = Db::raw("total_balance+{$orderBuy['money']}");
+                $card->balance = Db::raw("balance+{$balanceMoney}");
+                $card->save();
+            }
+            if (!empty($orderBuy['orders']))
+            {
+                $order = (new SaasOrder)->where("order_sn",$orderBuy['order_sn'])->findOrEmpty();
+                if ($order->isEmpty()) return "success";
+                if ($order['status'] <> 0) return "success"; // 已支付或者是其他状态
+                $order->status = 1;
+                $order->pay_at = getDateFull();
+                $order->transaction_id = $data['transaction_id']??'';
+                $order->notify_status = 1;
+                $order->pay_type = 2;
+                $order->save();
+                events("create-order",['shop' => $order['shop_id'],'print' => $order['print_id'],'openid' => $order['openid'],"order" => $order['order_sn']]);
+            }
+            $shop->balance = Db::raw("balance+".$orderBuy['money']);
+            $shop->total_balance = Db::raw("total_balance+".$orderBuy['money']);
+            $shop->save();
+            (new SaasShopLog)->insertGetId([
+                "shop_id"   => $orderBuy['shop_id'],
+                "money"     => $orderBuy['money'],
+                "balance"   => $shop->balance,
+                "type"      => 1,
+                "remark"    => "会员卡充值",
+                "status"    => 1
+            ]);
+            // 推送消息-公众号
+            if (!empty($shop['wx'])) {
+                $obj = \We::WeChatTemplate([
+                    "appid" => sConf("wechat.appid"),
+                    "appsecret" => sConf("wechat.secret"),
+                    "token" => sConf("wechat.token"),
+                    "encodingaeskey" => sConf("wechat.aeskey")
+                ]);
+                foreach ($shop['wx'] as $val) {
+                    if ($val['is_msg'] == 1) {
+                        $obj->send([
+                            "touser" => $val['openid'],
+                            "template_id" => "v1TVHflG9h5BPRLb2hH10r8oOV5OFMKD2cmqIw1nH-w",
+                            "data" => [
+                                "thing1" => ["value" => '微信用户'.$orderBuy['openid']],
+                                "thing6" => ["value" => $val['shop_name']],
+                                "amount3" => ["value" => format_money($orderBuy['money'] / 100)],
+                                "character_string9" => ["value" => $orderBuy['order_sn']],
+                                "time4" => ["time5" => date('Y-m-d H:i')]
+                            ]
+                        ]);
+                    }
+                }
+            }
+            (new SaasUserLog)->insertAll($logData);
+            return "success";
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 会员卡充值
+     */
+    #[Route(path: "recharges",methods: "post")]
+    public function notifyDataRecharge(Request $request)
+    {
+        try {
+            echo getDateFull()."===>会员卡支付返回\n";
+            $data = $this->jhfReturn($request->all());
+            if (empty($data)) return error("支付失败");
+
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 第三方支付返回
+     * @param array $respData
+     * @return array
+     */
+    protected function jhfReturn(array $respData = [],string $return = "payment.succeeded"): array
+    {
+        if ($respData['type'] <> $return) return [];
+        $resCipher = Utils::aes_decrypt($respData['resCipher'], sConf("wechat.jhf_aeskey"));
+        $data = json_decode($resCipher,true);
+        if ($return == "withdraw.succeeded") {
+            print_r($data);
+        }
+        $data['attach'] = $data['order_no'];
+        $data['transaction_id'] = $data['out_trans_id']??'';
+        $data['payment_id'] = $data['payment_id']??'';
+        $data['withdraw_id'] = $data['withdraw_id']??'';
+        $data['member_id'] = $data['member_id']??'';
+        $data['status'] = $data['status']??'';
+        return $data;
+    }
+
+    protected function payReturn($payResp)
+    {
+        $wechat = new \WeChat\Pay($this->getWxConfig());
+        if (empty($payResp)) return $this->getNotifySuccessReply();
+        $data = Tools::xml2arr($payResp);
+        if (empty($data)) return $this->getNotifySuccessReply();
+        return $data;
+    }
+
+
+    /**
+     * 获取微信支付通知成功回复 XML
+     * @return string
+     */
+    protected function getNotifySuccessReply(): string
+    {
+//        return Tools::arr2xml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']);
+        return json(['return_code' => 'SUCCESS', 'return_msg' => 'OK']);
+    }
+
+
+    /**
+     * 小程序配置
+     * @return array
+     */
+    protected function getWxConfig(): array
+    {
+        return [
+            'token'          => 'test',
+            'appid'          => sConf("wechat.mini_appid"),
+            'appsecret'      => sConf("wechat.mini_secret"),
+            'encodingaeskey' => 'BJIUzE0gqlWy0GxfPp4J1oPTBmOrNDIGPNav1YFH5Z5',
+            // 配置商户支付参数(可选,在使用支付功能时需要)
+            'mch_id'         => sConf("wechat.mch_id"),
+            'mch_key'        => sConf("wechat.mch_key")
+        ];
+    }
+
+
+}

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

@@ -0,0 +1,277 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\jhfPay\Pay;
+use app\extra\tools\CodeExtend;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasCart;
+use app\model\saas\SaasDiscount;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderDetail;
+use app\model\saas\SaasPrintClient;
+use app\model\saas\SaasShop;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserBuy;
+use app\model\saas\SaasUserLog;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use think\facade\Db;
+
+
+#[Controller(prefix: "/wx_api/order"),Middleware(WxMiddleware::class)]
+class Order extends Base
+{
+    protected array $noNeedLogin = [];
+
+    protected array $types = [
+        '1_1_1' => ['name' => '彩色-单面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+        '1_2_1' => ['name' => '彩色-双面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+        '2_1_1' => ['name' => '黑白-单面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+        '2_2_1' => ['name' => '黑白-双面', 'amount' => 0, 'quantity' => 0,'discount' => 0],
+    ];
+
+    protected array $color = ["1" => "彩色", "2" => "黑白"];
+
+    protected array $duplex = ["1" => "单面", "2" => "双面"];
+
+    protected array $package = ["1" => "店内打印", "2" => '远程自取' , "3" => "商家配送"];
+
+    #[Route(path: "list",methods: "get")]
+    public function getOrderList(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "page.require"  => "参数错误",
+                "size.require"  => "参数错误",
+                "type.require"  => "参数错误",
+                "shop.require"  => "请选择店铺"
+            ]);
+            if (!is_array($param)) return error($param);
+            $map = ["openid" => $request->user['openid'],"shop_id" => $param['shop']];
+            $model = (new SaasOrder);
+            if ($param['type'] > 0) {
+                $map['status'] = $param['type'] - 1;
+            } else {
+                $model = $model->where("status",">",0);
+            }
+            $resp = $model->where($map)->append(["total"])->with(['shop' => function($query){
+                $query->field('shop_id,shop_name');
+            }])->withAttr(["total" => function($val,$resp){
+                return (new SaasOrderDetail)->where("order_sn",$resp['order_sn'])->sum("number");
+            }])->order("create_at desc")->paginate([
+                "list_rows" => $param['size'],
+                "page"      => $param['page']
+            ]);
+            return success("ok",$resp->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 订单详情
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "detail",methods: "get")]
+    public function getOrderDetail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "order.require"  => "参数错误"
+            ]);
+            if (!is_array($param)) return error($param);
+            $resp = (new SaasOrder)->where("openid",$request->user['openid'])->where("order_sn",$param['order'])->append(["total","subscribe"])->with(['shop' => function($query){
+                $query->field('shop_id,shop_name,shop_address');
+            },"detail" => function($query){
+                $query->field('order_sn,name,color,paper_size,duplex,number,page,extension,path,icon');
+            }])->withAttr(["total" => function($val,$resp){
+                return (new SaasOrderDetail)->where("order_sn",$resp['order_sn'])->sum("number");
+            },"subscribe" => function(){
+                return ["495E40hqOKoz5j_mWcf-UcmF6wkj_yIwCrTXicicH5w","UXJjDQ7NGstSwOxrKf_laGDmpID8Mm5MpXwFAd45d8U"];
+            }])->findOrEmpty();
+            if ($resp->isEmpty()) return error("订单数据错误");
+            $resp['package_name'] = $this->package[$resp['package']];
+            $resp['money'] = format_money($resp['money'] / 100);
+            return success("ok",$resp->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "create",methods: "post")]
+    public function createOrder(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => trans("empty.require"),
+                "print.require" => trans("empty.require"),
+                "pay.require"   => trans("empty.require"), // 1微信支付 2会员卡支付 3会员卡充值并支付
+                "printName.default" => "",
+                "express.require" => trans("empty.require"),
+                "card.default"      => "",
+                "gift.default"      => 0
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $cart = (new SaasCart)->where("shop_id",$param['shop'])->where("print_id",$param['print'])->where("openid",$request->user['openid'])->order("create_at desc")->select();
+            if ($cart->isEmpty()) return error('请重新下单进行支付');
+            $totalAmount = $totalDiscount = 0;
+            foreach ($cart as $k=>$v){
+                $key = $v['color'] . '_' . $v['duplex'] . '_' . $v['source'];
+                if (isset($this->types[$key])) {
+                    $this->types[$key]['quantity'] += $v['page'];
+                    $this->types[$key]['amount'] += $v['money'];
+                }
+                $cart[$k] = $v;
+                $cart[$k]['money'] = format_money($v['money'] / 100,2);
+                $cart[$k]['name'] = msubstr($v['name'],0,12);
+            }
+            $printData = (new SaasPrintClient)->where("shop_id",$param['shop'])->where("code",$param['print'])->select();
+            if ($printData->isEmpty()) return error('无可用打印机');
+            // 计算折扣
+            foreach ($this->types as $k=>$v) {
+                $discount = (new SaasDiscount)->where("shop_id",$param['shop'])->where("keys",$k)->where("number",'<',$v['quantity'])->findOrEmpty();
+                if (!$discount->isEmpty()) {
+                    $v['discount'] = round($v['amount'] * $discount['rate']);
+                    $this->types[$k]['discount'] = $v['discount'];
+                }
+                $totalAmount += $v['amount']; // 实际金额
+                $totalDiscount += $v['discount']; // 折扣后金额
+            }
+            $orderSn = CodeExtend::uniqidDate(16).date("is").rand(1,9);
+            $totalDay = (new SaasOrder)->where("shop_id",$param['shop'])->whereDay("create_at")->count();
+            $orderData = [
+                "shop_id"       => $param['shop'], // 所属店铺
+                "parent_id"     => $param['shop'], // 消费店铺
+                "openid"        => $request->user['openid'],
+                "order_sn"      => $orderSn,
+                "money"         => $totalAmount,
+                "discount"      => $totalDiscount==0?$totalAmount:$totalDiscount, // 跟原价相等无折扣
+                "print_name"    => $param['printName'],
+                "print_id"      => $param['print'],
+                "package"       => $param['express'],
+                "package_sn"    => date('md')."-".sprintf("%02d",($totalDay+1)),
+                "extra_money"   => 0,
+                "remark"        => $param['remark']??''
+            ];
+            $shop = (new SaasShop)->where("shop_id",$param['shop'])->findOrEmpty();
+            if ($param['pay'] == 2) { // 会员卡支付
+                $card = (new SaasUser)->where("openid",$request->user['openid'])->where("shop_id",$param['shop'])->findOrEmpty();
+                $payMoney = $totalDiscount > 0 ? $totalDiscount : $totalAmount;
+                if ($payMoney >= $card['balance']) {
+                    return error("卡内余额不足~");
+                }
+                // 直接支付
+                $card->balance = ($card['balance'] - $payMoney);
+                $card->total_consume = ($card['total_consume'] + $payMoney);
+                $card->save();
+                $orderData['pay_type'] = 2;
+                $orderData['status'] = 1;
+                $orderData['pay_at'] = getDateFull();
+                (new SaasOrder)->insertGetId($orderData);
+                (new SaasUserLog)->insertGetId([
+                    "openid"    => $request->user['openid'],
+                    "shop_id"   => $param['shop'],
+                    "order_sn"  => $orderSn,
+                    "money"     => $payMoney,
+                    "card_no"   => strtoupper(md5($request->user['openid'].$param['shop'])),
+                    "type"      => 1,
+                    "balance"   => $card['balance'] - $payMoney,
+                ]);
+                events("create-order",['shop' => $param['shop'],'print' => $param['print'],'openid' => $request->user['openid'],"order" => $orderSn]);
+                return success("支付成功",['type' => 2,'data' => []]);
+            }
+            $options = [
+                'body'              => "{$shop['shop_name']}-{$param['printName']}",
+                'out_trade_no'      => $orderSn."-".$orderData['package_sn'],
+                "attach"            => $orderSn,
+                'total_fee'         => $orderData['money'],
+                'openid'            => $request->user['openid'],
+                'trade_type'        => 'JSAPI',
+                'spbill_create_ip'  => $request->getRealIp(),
+                "notify_url"        => "https://panel.huiyinduo.cn/notify/wx"
+            ];
+            if ($orderData['money'] <= 0) {
+                return error("数据变动请重新上传再下单");
+            }
+            if ($param['pay'] == 3 && !empty($param['card'])) { // 开通会员卡并充值
+                $buyCard = json_decode($param['card'],true);
+                (new SaasUserBuy)->insertGetId([
+                    "shop_id"       => $param['shop'],
+                    "money"         => $buyCard['money'] * 100,
+                    "total_money"   => ($buyCard['money'] * 100) + ($buyCard['old_money'] * 100),
+                    "order_sn"      => $orderSn,
+                    "openid"        => $request->user['openid'],
+                    "card_no"       => strtoupper(md5($request->user['openid'].$param['shop']))
+                ]);
+                $options['body'] = $shop['shop_name']."-充值支付";
+                $options['total_fee'] = $buyCard['money'] * 100;
+                $options['notify_url'] = "https://panel.huiyinduo.cn/notify/recharge";
+            }
+            (new SaasOrder)->insertGetId($orderData);
+            $param_data["order_no"] = $orderSn;
+            $param_data["app_id"] = sConf("wechat.jhf_appid");
+            $param_data["pay_channel"] = "wx_lite";
+            $param_data["pay_amt"] = format_money($options['total_fee'] / 100 , 2);
+            $param_data["goods_title"] = $options['body'];
+            $param_data["device_info"] = array("device_ip" => $request->getRealIp());
+            $param_data['notify_url'] = $options['notify_url'];
+            $param_data["expend"] = [
+                "wx_app_id" => sConf("wechat.mini_appid"),
+                "open_id" => $request->user['openid']
+            ];
+            $respJhf = (new Pay)->config([
+                "appid"  => sConf("wechat.jhf_appid"),
+                "mch_id" => sConf("wechat.jhf_mch_id"),
+                "aeskey" => sConf("wechat.jhf_aeskey"),
+                "pubkey" => sConf("wechat.jhf_pubkey"),
+                "prikey" => sConf("wechat.jhf_prikey"),
+            ])->createPay($param_data);
+
+//            $wechat = new \WeChat\Pay($this->getWxConfig());
+//            // 生成预支付码
+//            echo getDateFull()."生成支付二维码\n";
+//            print_r($options);
+//            $result = $wechat->createOrder($options);
+            if (isset($respJhf['code'])) {
+                return error("发起支付失败");
+            }
+            // 创建JSAPI参数签名
+            $resp = json_decode($respJhf['expend']['pay_info'],true);
+            $resp['timestamp'] = $resp['timeStamp'];
+            return success("ok",['type' => 1,"data" => $resp]);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getLine()."\n";
+            echo $throwable->getFile()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 小程序配置
+     * @return array
+     */
+    protected function getWxConfig(): array
+    {
+        return [
+            'token'          => 'test',
+            'appid'          => sConf("wechat.mini_appid"),
+            'appsecret'      => sConf("wechat.mini_secret"),
+            'encodingaeskey' => 'BJIUzE0gqlWy0GxfPp4J1oPTBmOrNDIGPNav1YFH5Z5',
+            // 配置商户支付参数(可选,在使用支付功能时需要)
+            'mch_id'         => sConf("wechat.mch_id"),
+            'mch_key'        => sConf("wechat.mch_key")
+        ];
+    }
+
+
+}

+ 263 - 0
app/controller/api/Test.php

@@ -0,0 +1,263 @@
+<?php
+
+namespace app\controller\api;
+
+
+use app\extra\basic\Base;
+use app\extra\jhfPay\Pay;
+use app\extra\tools\CodeExtend;
+use app\model\saas\SaasCart;
+use app\model\saas\SaasOrderDetail;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserOpen;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\think\Db;
+use Webman\Push\Api;
+use Webman\RedisQueue\Redis;
+
+#[Controller(prefix: "/api/test")]
+class Test extends Base
+{
+
+
+    protected array $whiteShop = [
+        "10888572278827"    => "10888572278827", // 惠印多马村店
+        "10888566286841"    => "327545130062964", // 中站小学店
+        "10888590458813"    => "326945519114632", // 幸小印美店
+        "10888645729727"    => "326844775971796", // 焦作和平街店
+    ];
+
+
+    #[Route(path: "pay",methods: "get")]
+    public function testPay(Request $request)
+    {
+        try {
+            $respJhf = (new Pay)->config([
+                "appid"  => sConf("wechat.jhf_appid"),
+                "mch_id" => sConf("wechat.jhf_mch_id"),
+                "aeskey" => sConf("wechat.jhf_aeskey"),
+                "pubkey" => sConf("wechat.jhf_pubkey"),
+                "prikey" => sConf("wechat.jhf_prikey"),
+            ]);
+            $jsResp = $respJhf->createBalancePay([
+                "app_id" => sConf("wechat.jhf_appid"),
+                "order_no"  => CodeExtend::uniqidDate(18),
+                "member_id" => "300919564646719394",
+                "pay_amt"   => "77.73",
+                "description"   => "补发"
+            ]);
+//            $jsResp = $respJhf->createBalanceWithdraw([
+//                "app_id" => sConf("wechat.jhf_appid"),
+//                "order_no"  => "9D18B0EEB5E70548308CB77274755601",
+//                "member_id" => "974707386910290082",
+//                "remark"    => "2026-05-01-结算",
+//                "notify_url"    => "https://panel.huiyinduo.cn/notify/withdraw",
+//            ]);
+            print_r($jsResp);
+
+//            $param_data = array();
+//            $param_data["order_no"] = date("YmdHis").CodeExtend::random(8);
+//            $param_data["app_id"] = "app_6666000195741955";
+//            $param_data["pay_channel"] = "wx_lite";
+//            $param_data["pay_amt"] = "0.10";
+//            $param_data["goods_title"] = "智惠印打印";
+//            $param_data["device_info"] = array("device_ip" => '192.168.0.1');
+//            $param_data["expend"] = [
+//                "wx_app_id" => "wxbeeb0dcd7336612f",
+//                "open_id" => "omf322AlD9wFjm5Ucix9uKmRXd4I"
+//            ]; //expend参数根据支付渠道变化
+//            $param_data["notify_url"] = "https://panel.huiyinduo.cn/notify/wx"; //接收支付结果异步通知地址
+//            $resp = (new Pay)->config([
+//                "appid" => "app_6666000195741955",
+//                "mch_id" => "1120260422467651",
+//                "aeskey" => "54784acef7d89b50edff9d42832f124c",
+//                "pubkey" => "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOKBBFXirWIJth+SNJCY4mhbATbL60sKV66bRixHMVz8vpBqONio9X6A+Pm9LNutBe+hLpI1BMmFJk3Mb1/QEcklWptRGgHqIrBxR4b19qc/2/pSxyqlpaifYJFZhOg2+OcQ/fqpAmhNXN5uc1pcYvbvWTam0j+6+nBNQeAAku5QIDAQAB",
+//                "prikey" => "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOK1bos5glGB/tq/iyDaCLwAgVUwTLRFltG7j5QjS+CEBu/0t6wW3z2UfacMxWGRgkPQBWCDNSOlYV9gVOoxtfj3TS0zWK1t9OCO+7PaSI9BqkYumlylpRq09gplul6C7HXgrWx3WxuzbLXSaJ6wAeFr0ZY1KqPdhZ5OB7m0zNeXAgMBAAECgYAr1COg6udU1qrso2dEXKKfpgFa9NF/cIyt03L4krJSn3Ov5EG2FV3nS9PW/dMS/8yNS6Qeen9Feu2OQNNpy16AfDiy5cFn5MvLm/PHb7syBMaakuKogEDYKnoo/CPbI8kTYymZA0tDnad1BkeY3lb0Bx6ou4oRZ+TYc0QOJCqwUQJBAPE7QRhpR4VygkIcPORyIR7PhBKkxmMz+ZHt+E/ep7o5KpdDfBP95gUs7591BZDnyh1EBrkBh9G8WQVBCeV9Qk0CQQDwlpEsLyw9DdtgKTneoRfw+bnLhIfvkshoxfhWf3i4iCcIqAvZZbfInY2W37vBJrtpfirHOhpuLjV7R2fqSctzAkBCuuJx70WSm69+vDL3+r5AuKTPR3d9n7YM8Sg8Z9o8AG5Qs6FSIm0Lx3dtw8BLamMVn2jAqrS4hwKVGn2zVugNAkEAyjFgFEgY377TjX9YOTgdzNGzSc06CSfM8fDfAqLirAMQ+v9v5ebMi/eNVSz2uB97Be+YuBKmv85p+A9Mz+Pw7QJAb4fn1d5Tw3B7gku3XANH3RTfvNWBeXBpxpAlnaxJU39pVh4lh9UGoaWGoEKEdufHSJhJMUtaSpI2morVAfo0Ow==",
+//            ])->createPay($param_data);
+            return success("ok");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[Route(path: "s2",methods: "get")]
+    public function sendWs2()
+    {
+        try {
+            Redis::send("push-print",['type' => 'order' , 'order' => '202604205907640644072']);
+            return success("ok");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "s",methods: "get")]
+    public function sendWs()
+    {
+        try {
+            // return error('err');
+            $api = new Api('http://127.0.0.1:3232',
+                config('plugin.webman.push.app.app_key'),
+                config('plugin.webman.push.app.app_secret'));
+            $param = $this->_valid([
+                "color.default" => "color",
+                "size.default" => "A4",
+                "key.default" => "004356708646",
+                "shop.default" => "10888543912042",
+                "name.default" => "qiantai",
+                "id.default" => "29131",
+                "number.default" => 1,
+                "range.default" => "1",
+                "duplex.default"    => "simplex"
+            ]);
+            $printJobData = (new SaasOrderDetail)->where("paper_size",'A4')->where("extension",'docx')->limit(1)->order("create_at",'desc')->select();
+//            $printJobData = (new SaasOrderDetail)->where("id",$param['id'])->select();
+//            $printJobData = (new SaasOrderDetail)->whereDay("create_at","yesterday")->limit(1)->select();
+            if ($printJobData->isEmpty()) return error('err');
+            $printData = [];
+            foreach ($printJobData as $key=>$printJob) {
+                $range = "1";
+                // if ($printJob['end_page'] > $printJob['start_page']) {
+                //     $range = $printJob['start_page']."-".$printJob['end_page'];
+                // }
+                // if ($printJob['end_page'] == $printJob['start_page']) {
+                //     $range = (string) $printJob['end_page'];
+                // }
+                $color_mode = ($printJob['color']==1?'color':'monochrome');
+                $duplex = "simplex";
+                if ($printJob['paper_size'] == 'A4' && $printJob['duplex'] == 2) {
+                    $duplex = 'duplexlong'; // duplexlong
+                }
+                if ($printJob['paper_size'] == 'A3' && $printJob['duplex'] == 2) {
+                    $duplex = 'duplexlong'; // duplexshort
+                }
+                $printData[$key] = [
+                    "printerName"   => $param['name'],
+                    "copies"        => $param['number'],
+                    "landscape"     => false,
+                    "paperSize"     => $param['size'],
+                    "pageRange"     => $param['range'],
+                    "duplex"        => $param['duplex'],
+                    "colorMode"     => $param['color'],
+                    "jobId"         => $printJob['id'],
+                    "dpi"           => 300,
+                    "action"        => 'create',
+                    "scaleMode"     => 'fit',
+                    "remoteUrl"     => "https://zhy-1355132020.cos.ap-guangzhou.myqcloud.com/".$printJob['path'],
+//                    "remoteUrl"     => "https://zhy-1355132020.cos.ap-guangzhou.myqcloud.com/pdfs/20260501/12456-001_ac02a21c-3ad6-436f-9c13-ec415d26b21d.pdf", //竖版
+//                    "remoteUrl"     => "https://zhy-1355132020.cos.ap-guangzhou.myqcloud.com/ossmini/20260502/1777694521564_sopo6f.pdf", // 横版
+//                    "remoteUrl"     => "https://yunenv.oss-cn-shenzhen.aliyuncs.com/a3shu.pdf", // 横版
+//                    "remoteUrl"     => "https://inmei-print.oss-cn-guangzhou.aliyuncs.com/wxjiaoyi.pdf",
+//                    "remoteUrl"  => "https://zhy-1355132020.cos.ap-guangzhou.myqcloud.com/ossmini/20260504/1777898266812_rcyph1.docx",
+//                    "customFileName" => $printJob['order_sn']."_".$printJob['id'],
+                    "customFileName" => time()."_".$printJob['id'],
+                ];
+            }
+            $api->trigger("client-{$param['key']}-{$param['shop']}",'message',[
+                "type"  => "print",
+                "data"  => $printData
+            ]);
+//             if (!$printJob->isEmpty()) {
+//                 $range = "1";
+//                 // if ($printJob['end_page'] > $printJob['start_page']) {
+//                 //     $range = $printJob['start_page']."-".$printJob['end_page'];
+//                 // }
+//                 // if ($printJob['end_page'] == $printJob['start_page']) {
+//                 //     $range = (string) $printJob['end_page'];
+//                 // }
+//                 $color_mode = ($printJob['color']==1?'color':'monochrome');
+//                 $duplex = "simplex";
+//                 if ($printJob['paper_size'] == 'A4' && $printJob['duplex'] == 2) {
+//                     $duplex = 'duplexlong'; // duplexlong
+//                 }
+//                 if ($printJob['paper_size'] == 'A3' && $printJob['duplex'] == 2) {
+//                     $duplex = 'duplexshort'; // duplexshort
+//                 }
+//                 $api->trigger("client-005571234125-10888543912042",'message',[
+//                     "type"  => "print",
+//                     "data"  => [
+//                         [
+//                             "printerName"   => "750",
+//                             "copies"        => "1",
+//                             // "copies"        => (string)$printJob['number'],
+//                             "landscape"     => false,
+//                             "paperSize"     => "A4",
+//                             // "paperSize"     => $printJob['paper_size'],
+//                             "pageRange"     => $range,
+//                             "duplex"        => "duplexlong",
+//                             // "duplex"        => $duplex,
+//                             "monochrome"    => $color_mode,
+//                             "colorMode"     => "monochrome",// monochrome color
+//                             "jobId"         => $printJob['id']."-".time().rand(1,99),
+//                             "action"        => 'create',
+//                             "scaleMode"     => 'fit',
+//                             "remoteUrl"     => "https://cdn-zhy.huiyinduo.cn/".$printJob['path'],
+//                             "customFileName" => $printJob['order_sn']."_".$printJob['id'],    
+//                         ]
+// //                    "exe"       => ['-print-to',$printJob['print_name'],'-print-settings',"{$range},{$printJob['number']}x,{$color_mode},$duplex,fit,paper={$printJob['paper_size']}",'-silent'],
+//                     ]
+//                 ]);
+//             }
+            return success("ok",compact('printData'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[Route(path: "t",methods: "get")]
+    public function testData(Request $request): \support\Response
+    {
+        try {
+//            $data = (new SaasUserOpen)->whereNotNull("oid")->whereDay("update_at",'yesterday')->select();
+            $data = (new SaasUserOpen)->whereNotNull("oid")->whereDay("update_at")->select();
+            if ($data->isEmpty()) return error("ok");
+            $userData = [];
+            foreach ($data as $key=>$val) {
+                if (!empty($val['oid'])) {
+                    $old = Db::connect("old")->table("inmei_member_card")->where("openid",$val['oid'])->find();
+                    if (!empty($old)) {
+                        $shopId = $old['shop_id'];
+                        if (isset($this->whiteShop[$old['shop_id']])) {
+                            $shopId = $this->whiteShop[$old['shop_id']];
+                        }
+                        $card = (new SaasUser)->where("openid",$val['openid'])->findOrEmpty();
+                        if ($card->isEmpty()) {
+                            $userData[$key] = [
+                                "openid"        => $val['openid'],
+                                "shop_id"       => $shopId,
+                                "card_no"       => strtoupper(md5($val['openid'].$shopId)),
+                                "balance"       => $old['balance'],
+                                "total_balance" => $old['total_balance'],
+                                "remark"        => "迁移,原ID:{$old['openid']}",
+                            ];
+                        }
+                    }
+                }
+            }
+            if (!empty($userData)) {
+                (new SaasUser)->insertAll($userData);
+            }
+            return success("ok",array_values($userData));
+        } catch (\Throwable $throwable) {
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+    
+    #[Route(path: "tid",methods: "get")]
+    public function testDataId(Request $request): \support\Response
+    {
+        try {
+            $id = $request->get("id",16992);
+            Redis::send("push-print",['type' => 'id' , 'order' => $id]);
+            return success("ok");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getLine()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 57 - 0
app/controller/api/Uploads.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\extra\tools\UploadExtend;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasUpload;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use LinFly\Annotation\Route\Middleware;
+
+#[Controller(prefix: "/wx_api/upload"),Middleware(WxMiddleware::class)]
+class Uploads extends Base
+{
+
+
+    #[Route(path: "temp",methods: "post")]
+    public function getCosTemp()
+    {
+        try {
+
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 上传文件
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "data",methods: "post")]
+    public function uploadData(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $resp = UploadExtend::disk(UploadExtend::MODE_LOCAL)->uploadFile();
+            events("clear-upload",['key' => $param['key']]);
+            (new SaasUpload())->insertGetId([
+                "key"       => $param['key'],
+                "path"      => $resp[0]['url'],
+                "agent_id"  => $param['shop']
+            ]);
+            if (!isset($resp[0]['url'])) return errorTrans(40010);
+            return successTrans("success.data",[
+                "fileName"  => $resp[0]['origin_name'],
+                "src"       => $resp[0]['url'],
+            ],200);
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,82 @@
+<?php
+
+namespace app\controller\api;
+
+use app\extra\basic\Base;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasPrice;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserOpen;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use LinFly\Annotation\Route\Middleware;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/wx_api/user"),Middleware(WxMiddleware::class)]
+class User extends Base
+{
+
+    protected array $color = ["1" => "彩色", "2" => "黑白"];
+
+    protected array $duplex = ["1" => "单面", "2" => "双面"];
+
+    protected array $type = ["1" => "文档", "2" => "复印"];
+
+    #[Route(path: "data",methods: "get")]
+    public function getUserData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => trans("empty.require"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $user = $request->user;
+            if (empty($user)) return errorTrans("empty.data");
+
+            $member = (new SaasUserOpen)->where("openid",$user['openid'])->field("openid,headimg,nickname")->append(["coupon"])->withAttr(['coupon' => function(){
+                return 0;
+            }])->findOrEmpty();
+            $memberUser = (new SaasUser)->where(['shop_id' => $param['shop'],'openid' => $user['openid']])->field("openid,ROUND(balance/100,2) as f_balance,ROUND(total_balance/100,2) as f_total_balance,ROUND(total_consume/100,2) as f_total_consume,card_no")->findOrEmpty();
+            $member['vip'] = [
+                "f_balance"         => $memberUser['f_balance']??'0.00',
+                "f_total_consume"   => $memberUser['f_total_balance']??'0.00',
+            ];
+            return success("ok",$member->toArray());
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+
+    /**
+     * 价目表
+     */
+    #[Route(path: "price",methods: "get")]
+    public function getPriceData(): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.require"  => "参数错误"
+            ]);
+            if (!is_array($param)) return error($param);
+            $data = (new SaasPrice)->where("shop_id",$param['shop'])->field("paper_size,source,duplex,price,color,type")->select();
+            if ($data->isEmpty()) return error("尚未配置价目表");
+            $resp = [];
+            foreach ($data as $key=>$val)
+            {
+                $color = $this->color[$val['color']];
+                $type = $this->type[$val['type']];
+                $duplex = $this->duplex[$val['duplex']];
+                $resp[$val['paper_size']]['id'] = $val['paper_size'];
+                $resp[$val['paper_size']]['child'][$key]['name'] = $type.$color.$duplex;
+                $resp[$val['paper_size']]['child'][$key]['price'] = $val['price'];
+            }
+            return success("ok",$resp);
+        } catch (\Throwable $exception) {
+            return error($exception->getMessage());
+        }
+    }
+
+}

+ 46 - 0
app/controller/common/Common.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\model\system\SystemConfig;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Response;
+use Tinywan\Captcha\Captcha;
+
+/**
+ *
+ */
+#[Controller(prefix: "/api/service")]
+class Common extends Base
+{
+
+    /**
+     *
+     */
+    #[Route(path: "data",methods: "get")]
+    public function getServiceData(): Response
+    {
+        try {
+            $captcha = Captcha::base64();
+            $service = (new SystemConfig)->where("type","service")->column("value","name");
+            return successTrans("success.data",compact('captcha','service'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "test",methods: "get")]
+    public function testData()
+    {
+        try {
+            events("merge-pdf",['shop' => "326945519114632","order" => "202603263933413924333", "openid" => "otdgy3QxYqFjS_yl_YKUmLYMAtGI"]);
+            return success("ok");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,146 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasShop;
+use app\model\system\SystemUser;
+use Hzdad\Codecheck\Codecheck;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use Shopwwi\WebmanAuth\Auth;
+use support\Request;
+use support\Response;
+use think\facade\Db;
+use Tinywan\Captcha\Captcha;
+use LinFly\Annotation\Route\Middleware;
+
+
+#[Controller(prefix: "/api/login")]
+class Login extends Base
+{
+
+    /**
+     * 登陆
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "user",methods: "post")]
+    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 Request $request
+     * @return Response
+     */
+    #[Route(path: "mobile",methods: "post")]
+    public function setLogin2Mobile(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "mobile.require"    => trans("empty.mobile"),
+                "code.require"      => trans("empty.code"),
+                "scene.require"     => trans("empty.data"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $code = (new Codecheck)->mobile($param['mobile'])->scene($param['scene'])->code($param['code'])->check();
+            if (!$code) return errorTrans("error.captcha");
+            $map = ["is_deleted" => 0,"mobile" => $param['mobile']];
+            [$state,$msg,$user] = $this->checkLogin($map);
+            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['agent_id']);
+            if (empty($typeUser)) return [0,trans("empty.agent"),[]];
+            if ($typeUser['status'] <> 0) return [0,trans("error.agent"),[]];
+            if (time() > strtotime($typeUser['vip_end'])) return [0,trans("error.agent-out"),[]];
+            $user['shop_name'] = $typeUser['shop_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 SaasShop)->where("shop_id",$agentId)->findOrEmpty()->toArray();
+    }
+
+    /**
+     * @return Response
+     */
+    #[Route(path: "profile",methods: "get"),Middleware(AuthMiddleware::class)]
+    public function getLoginUser(): Response
+    {
+        try {
+            $userData = (new Auth)->guard("admin")->user()->toArray();
+            if (isset($userData['password'])) unset($userData['password']);
+            $agent = (new SaasShop)->where("shop_id",$userData['agent_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'],
+                "truename"  => $userData['truename'],
+                "vip_at"    => $userData['vip_end'],
+                "agent_id"  => $userData['agent_id'],
+                "super"     => $userData['is_super'],
+                "shop"      => $agent['shop_name']??'',
+                "type"      => $userData['type']
+            ]);
+        } catch (\Throwable $exception){
+            return error($exception->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,75 @@
+<?php
+
+namespace app\controller\common;
+
+use app\extra\basic\Base;
+use app\extra\service\system\MenuService;
+use app\extra\tools\DataExtend;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemMenu;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use LinFly\Annotation\Route\Middleware;
+
+
+#[Controller(prefix: "/api/menu"),Middleware(AuthMiddleware::class)]
+class Menu extends Base
+{
+
+    #[Inject]
+    protected MenuService $service;
+
+    /**
+     * 获取菜单
+     */
+    #[Route(path: "list",methods: "get")]
+    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
+     */
+    #[Route(path: "save",methods: "post")]
+    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());
+        }
+    }
+}

+ 110 - 0
app/controller/exe/Login.php

@@ -0,0 +1,110 @@
+<?php
+
+namespace app\controller\exe;
+
+use app\extra\basic\Base;
+use app\middleware\ExeMiddleware;
+use app\model\saas\SaasShop;
+use app\model\system\SystemUser;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use Shopwwi\WebmanAuth\Auth;
+use support\Request;
+use support\Response;
+use think\facade\Db;
+use Tinywan\Captcha\Captcha;
+use LinFly\Annotation\Route\Middleware;
+
+
+#[Controller(prefix: "/exe/login")]
+class Login extends Base
+{
+
+    /**
+     * 登陆
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "user",methods: "post")]
+    public function setLogin(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "username.require"  => trans("empty.user"),
+                "password.require"  => trans("empty.passwd"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $map = ["is_deleted" => 0,"username" => $param['username']];
+            [$state,$msg,$user] = $this->checkLogin($map,2,$param);
+            print_r($msg);
+            if (!$state) return error($msg);
+            return successTrans("success.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['agent_id']);
+            if (empty($typeUser)) return [0,trans("empty.agent"),[]];
+            if ($typeUser['status'] <> 0) return [0,trans("error.agent"),[]];
+            if (time() > strtotime($typeUser['vip_end'])) return [0,trans("error.agent-out"),[]];
+            $user['shop_name'] = $typeUser['shop_name'];
+        }
+        if ($type == 2) {
+            if (md5(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 SaasShop)->where("shop_id",$agentId)->findOrEmpty()->toArray();
+    }
+
+
+    /**
+     * @return Response
+     */
+    #[Route(path: "profile",methods: "get"),Middleware(ExeMiddleware::class)]
+    public function getLoginUser(Request $request): Response
+    {
+        try {
+            $agent = (new SaasShop)->where("shop_id",$request->uuid)->findOrEmpty();
+            return successTrans("success.data",[
+                "vip_at"    => empty($agent['vip_end'])?'无限期':date('Y-m-d',strtotime($agent['vip_end'])),
+                "agent_id"  => $agent['agent_id'],
+                "shop"      => $agent['shop_name']??'',
+                "address"   => $agent['shop_address']??'',
+                "sys"       => sConf("service.title"),
+                "logo"      => sConf("service.logo")
+            ]);
+        } catch (\Throwable $exception){
+            return error($exception->getMessage());
+        }
+    }
+
+
+}

+ 155 - 0
app/controller/exe/Prints.php

@@ -0,0 +1,155 @@
+<?php
+
+namespace app\controller\exe;
+
+use app\extra\basic\Base;
+use app\middleware\ExeMiddleware;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderDetail;
+use app\model\saas\SaasPrintClient;
+use app\model\saas\SaasShop;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/exe/prints"),Middleware(ExeMiddleware::class)]
+class Prints extends Base
+{
+
+    protected array $noNeedLogin = ["getPrintData"];
+
+    /**
+     * 回传打印机信息
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "set",methods: "post")]
+    public function getPrintList(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (empty($param)) return errorTrans("empty.data");
+            $shopId = $request->uuid;
+            $mode = (new SaasPrintClient);
+            $printIn = [];
+            foreach ($param as $key=>$val)
+            {
+                $param[$key] = $val;
+                $printId = strToUniqueNumberV4(trim($val['name']));
+                $param[$key]['key'] = "{$printId}-{$shopId}";
+                $param[$key]['userKey'] = $printId;
+                $printData = $mode->where("code",$printId)->where("shop_id",$shopId)->findOrEmpty();
+                if ($printData->isEmpty()) {
+                    $printIn[$key] = [
+                        "shop_id"       => $shopId,
+                        "code"          => $printId,
+                        "name"          => $val['name'],
+                        "ipaddress"     => $val['ipAddress'],
+                        "print_status"  => $val['status'],
+                    ];
+                } else {
+                    $printData->ipaddress = $val['ipAddress'];
+                    $printData->print_status = $val['status'];
+                    $printData->save();
+                }
+            }
+            if (!empty($printIn)) {
+                $mode->insertAll(array_values($printIn));
+            }
+            (new SaasShop)->where("shop_id",$shopId)->update(['line_time' => getDateFull()]);
+            return success("ok",$param);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 打印识别
+     * @return Response
+     */
+    #[Route(path: "error",methods: "post")]
+    public function getPrintError(Request $request): Response
+    {
+        try {
+            echo getDateFull()."===打印出错\n";
+            print_r($request->all());
+            $param = $this->_valid([
+                "data.require"      => trans("empty.data"),
+                "param.require"     => trans("empty.data"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            echo getDateFull()."===打印出错\n";
+            print_r($param);
+            $orderId = $param['param']['jobId'];
+            
+            return success("ok");
+        }  catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 打印回传
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "status",methods: "post")]
+    public function getPrintData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "data.require"      => trans("empty.data"),
+                "param.require"     => trans("empty.data"),
+            ],$request->method());
+            if (!is_array($param)) return error($param);
+            $orderId = $param['param']['jobId'];
+            $order = (new SaasOrderDetail)->where("id",$orderId)->with(['orders'])->findOrEmpty();
+            $total = ['t0' => 0, 't1' => 0, 't2' => 0, 't3' => 0, 'ta' => 0];
+            foreach ((new SaasOrderDetail)->where("id",$orderId)->field('create_at,status,count(1) total')->group('status,create_at')->cursor() as $vo)
+            {
+                [$total["t{$vo['status']}"] += $vo['total'], $total['ta'] += $vo['total']];
+            }
+            if (isset($param['data']['success'])) {
+                if ($param['data']['success']) { // 打印成功
+                    $order->status = 3;
+                    $order->save();
+                    if (($total['t3']+1) == $total['ta']) {
+                        (new SaasOrder)->where("order_sn",$order['order_sn'])->update(['status' => 3,'print_at' => getDateFull()]);
+                    }
+                } else { // 打印失败
+                    
+                }
+            }
+            
+            // $outPut = $param['data']['outputArray'];
+            // $logData = [];
+            // foreach ($outPut as $key=>$val) {
+            //     if ($val['type'] == 'stdout') {
+            //         $logData[$key] = [
+            //             "order_sn"  => $param['param']['customFileName'],
+            //             "msg"       => delNt($val['data'])
+            //         ];
+            //     } else if ($val['type'] == "exit") {
+            //         if ($val['code'] == 0) {
+            //             $order->status = 3;
+            //             $order->save();
+            //             if (($total['t3']+1) == $total['ta']) {
+            //                 (new SaasOrder)->where("order_sn",$order['order_sn'])->update(['status' => 3,'print_at' => getDateFull()]);
+            //             }
+            //         }
+            //     }
+            // }
+            // $logData = array_values($logData);
+            // if (!empty($logData)) {
+
+            // }
+            return success("ok");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,80 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasOrder;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/dashboard"),Middleware(AuthMiddleware::class)]
+class Dashboard extends Base
+{
+
+
+    #[Route(path: "data",methods: "get")]
+    public function getData(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "money.default" => 7,
+                "hide.default"  => 0,
+                "order.default"  => 7
+            ],$request->method());
+            $moneyDay = $param['money'];
+            $data = $moneys = $orders = [];
+            $orderDay = $param['order'];
+            $shopId = $request->user['agent_id'];
+            $mode = (new SaasOrder);
+            if ($param['hide'] == 1) {
+                $data = [
+                    "userNum"   => $mode->whereDay("create_at")->where("shop_id",$shopId)->where('status','in',[1,3])->sum("money"), // 今日营业额
+                    "rechargeNum"   => $mode->whereDay("create_at")->where("shop_id",$shopId)->where('status','in',[1,3])->count(), // 今日订单数
+                    "rechargeMoney"   => $mode->whereDay("create_at")->where("shop_id",$shopId)->where('status',1)->count(), // 手机自助打印待处理
+                ];
+                $fields = ['ROUND(sum(money)/100,2)' => 'total','substr(create_at,1,10)' => 'mday'];
+                $orderMoney = $mode->field($fields)->where("shop_id",$shopId)->where('status','in',[1,3])->whereTime('create_at', '-'.$moneyDay.' days')->group('mday')->select()->column(null, 'mday');
+                for ($i = $moneyDay; $i >= 0; $i--) {
+                    $date = date('Y-m-d', strtotime("-{$i}days"));
+                    $moneys[] = [
+                        '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                        '订单金额' => ($orderMoney[$date] ?? [])['total'] ?? 0,
+                    ];
+                }
+                $field = ['count(1)' => 'count', 'substr(create_at,1,10)' => 'mday'];
+                $orderNum = $mode->field($field)->where("shop_id",$shopId)->where('status','in',[1,3])->whereTime('create_at', '-'.$orderDay.' days')->group('mday')->select()->column(null, 'mday');
+                for ($i = $orderDay; $i >= 0; $i--) {
+                    $date = date('Y-m-d', strtotime("-{$i}days"));
+                    $orders[] = [
+                        '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                        '订单数量' => ($orderNum[$date] ?? [])['count'] ?? 0,
+                    ];
+                }
+            } else {
+                for ($i = $moneyDay; $i >= 0; $i--) {
+                    $date = date('Y-m-d', strtotime("-{$i}days"));
+                    $moneys[] = [
+                        '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                        '订单金额' => 888,
+                    ];
+                }
+                for ($i = $orderDay; $i >= 0; $i--) {
+                    $date = date('Y-m-d', strtotime("-{$i}days"));
+                    $orders[] = [
+                        '当天日期' => date('Y-m-d', strtotime("-{$i}days")),
+                        '订单数量' => 888,
+                    ];
+                }
+            }
+            return success("ok",compact("data","orders","moneys"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 64 - 0
app/controller/merchant/Detail.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\weMini\Link;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasShop;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/shop"),Middleware(AuthMiddleware::class)]
+class Detail extends Base
+{
+
+    #[Inject]
+    protected SaasShop $model;
+
+
+    #[Route(path: "detail",methods: "get")]
+    public function getDetail(Request $request): Response
+    {
+        try {
+            $qrcodePath = "/uploads/card/{$request->user['agent_id']}-pay.jpg";
+            if (!is_file(public_path().$qrcodePath)) {
+                (new Link([
+                    "appid"     => sConf("wechat.mini_appid"),
+                    "appsecret" => sConf("wechat.mini_secret")
+                ]))->createQrcodeWx("/pages/shop/pay","shop={$request->user['agent_id']}",$qrcodePath);
+            }
+            $qrcode = 'data:image/png;base64,'.base64_encode(file_get_contents(public_path().$qrcodePath));
+            $shop = $this->model->where("shop_id",$request->user['agent_id'])->field("shop_name,shop_mobile,start_at,end_at,vip_end,shop_status,shop_notice,line_time,status,shop_address")->withAttr(['vip_end'  => function($query,$data) {
+                return date("Y-m-d",strtotime($data['vip_end']));
+            },'qrcode' => function () use ($qrcode){
+                return $qrcode;
+            }])->findOrEmpty();
+            if ($shop->isEmpty()) return errorTrans("empty.data");
+            return success("ok",$shop->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[Route(path: "save",methods: "post")]
+    public function setData(Request $request)
+    {
+        try {
+            $param = $request->post();
+            $shop = $this->model->where("shop_id",$request->user['agent_id'])->findOrEmpty();
+            if ($shop->isEmpty()) return errorTrans("empty.data");
+            $state = $shop->save($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 102 - 0
app/controller/merchant/Discount.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\DisCountService;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasDiscount;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/discount"),Middleware(AuthMiddleware::class)]
+class Discount extends Base
+{
+
+    #[Inject]
+    protected SaasDiscount $model;
+
+    #[Inject]
+    protected DisCountService $service;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['agent_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function savePrice(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.default"        => 0,
+                "color.require"     => trans("empty.require"),
+                "duplex.require"    => trans("empty.require"),
+                "number.require"    => trans("empty.require"),
+                "rate.require"      => trans("empty.require"),
+                "shop_id.default"   => $request->user['agent_id'],
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $param['keys'] = $param['color']."_".$param['duplex']."_1";
+            $price = $this->model->where("id",$param["id"])->findOrEmpty();
+            if ($price->isEmpty())
+            {
+                $priceType = $this->model->where([
+                    "shop_id"   => $param["shop_id"],
+                    "keys"      => $param["keys"]
+                ])->findOrEmpty();
+                if (!$priceType->isEmpty()) return errorTrans("error.exist");
+                $state = $price->insertGetId($param);
+            } else {
+                $state = $price->save($param);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "del",methods: "post")]
+    public function delPrice(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require"),
+                "type.default"  => ""
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param["type"] == "batch") {
+                $state = $this->model->whereIn("id",$param["id"])->delete();
+            } else {
+                $price = $this->model->where("id",$param["id"])->findOrEmpty();
+                if ($price->isEmpty()) return errorTrans("empty.data");
+                if ($price['shop_id'] <> $request->user["agent_id"]) return errorTrans("empty.data");
+                $state = $price->delete();
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 57 - 0
app/controller/merchant/Member.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasCombo;
+use app\model\saas\SaasShop;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/member"),Middleware(AuthMiddleware::class)]
+class Member extends Base
+{
+
+
+    #[Route(path: "combo",methods: "get")]
+    public function getComboDetail(Request $request): Response
+    {
+        try {
+            $shop = (new SaasShop)->where("shop_id",$request->user['agent_id'])->findOrEmpty();
+            $combo = (new SaasCombo)->where("type",2)->where("status",1)->field("id,name,ROUND(money/100,2) as money,ROUND(old_money/100,2) as old_money,is_first")->select();
+            $param = [
+                "state"     => $shop['user_card'], // 1 默认 2 自定义 3关闭
+                "system"    => $combo,
+                "shop"      => empty($shop['user_card_price']) ? $combo :$shop['user_card_price']
+            ];
+            return success("ok",$param);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "combo",methods: "post")]
+    public function setCombo(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $shop = (new SaasShop)->where("shop_id",$request->user['agent_id'])->findOrEmpty();
+            $shop->user_card = $param['state'];
+            if (isset($param['shop'])) {
+                $shop->user_card_price = json_encode($param['shop']);
+            }
+            $state = $shop->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,163 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\OrderService;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderDetail;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserLog;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+use think\facade\Db;
+
+
+#[Controller(prefix: "/api/mer/order"),Middleware(AuthMiddleware::class)]
+class Order extends Base
+{
+
+    #[Inject]
+    protected OrderService $service;
+
+    #[Inject]
+    protected SaasOrder $model;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            if (empty($param['shop'])) {
+                $param['shop'] = $request->user['agent_id'];
+            }
+            if (empty($param['status'])) $param['statusGt'] = 1;
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 返回统计结果
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "fail",methods: "post")]
+    public function setOrderFail(Request $request): Response
+    {
+        try {
+            $param = $this->_valid(['order.require' => "error"],$request->method());
+            if (!is_array($param)) return error($param);
+            $order = $this->model->where("order_sn",$param['order'])->findOrEmpty();
+            if ($order->isEmpty()) return errorTrans("empty.data");
+            $order->status = 2;
+            $order->save();
+            (new SaasOrderDetail)->where("order_sn",$param['order'])->update(['status' => 3]);
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 返回统计结果
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "total",methods: "get")]
+    public function getTotalData(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            if (empty($param['shop'])) {
+                $param['shop'] = $request->user['agent_id'];
+            }
+            $total = $this->service->getTotal($param);
+            return successTrans("success.data",compact("total"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 申请退款
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "refund",methods: "post")]
+    public function setRefund(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("id",$param['id'])->where("shop_id",$request->user['agent_id'])->findOrEmpty();
+            if ($order->isEmpty()) return errorTrans("empty.data");
+            if ($order['pay_type'] == 1) { // 微信退款
+                $wechat = new \WeChat\Pay($this->getWxConfig());
+                $resp = $wechat->createRefund([
+                    "transaction_id"    => $order['transaction_id'],
+                    "out_refund_no"     => $order['order_sn']."-".date("is"),
+                    "total_fee"         => $order['discount'],
+                    "refund_fee"        => $order['discount'],
+                    "notify_url"        => 'https://panel.huiyinduo.cn/notify/refund',
+                ]);
+                $order->status = 4;
+                $state = $order->save();
+                (new SaasOrderDetail)->where("shop_id",$request->user['agent_id'])->where("order_sn",$order['order_sn'])->update(['status' => 4]);
+            } else { // 会员卡退款
+                $card = (new SaasUser)->where("card_no",md5($order['openid'].$request->user['agent_id']))->findOrEmpty();
+                if ($card->isEmpty()) return error("会员不存在");
+                $card->balance = Db::raw("balance+".$order['discount']);
+                $card->total_consume = Db::raw("total_consume-".$order['discount']);
+                $card->save();
+                (new SaasUserLog)->insertGetId([
+                    "order_sn"  => $order['order_sn'],
+                    "openid"    => $order['openid'],
+                    "shop_id"   => $order['shop_id'],
+                    "card_no"   => md5($order['openid'].$order['shop_id']),
+                    "money"     => $order['discount'],
+                    "type"      => 2,
+                    "balance"   => $card->balance,
+                    "remark"    => "订单退款"
+                ]);
+                $order->status = 6;
+                $state = $order->save();
+                (new SaasOrderDetail)->where("shop_id",$request->user['agent_id'])->where("order_sn",$order['order_sn'])->update(['status' => 6]);
+            }
+            if (!$state) return error("申请失败");
+            return success("申请成功");
+        } catch (\Throwable $throwable) {
+            echo $throwable->getLine()."\n";
+            echo $throwable->getFile()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+    protected function getWxConfig(): array
+    {
+        return [
+            'token'          => 'test',
+            'appid'          => sConf("wechat.mini_appid"),
+            'appsecret'      => sConf("wechat.mini_secret"),
+            'encodingaeskey' => 'BJIUzE0gqlWy0GxfPp4J1oPTBmOrNDIGPNav1YFH5Z5',
+            // 配置商户支付参数(可选,在使用支付功能时需要)
+            'mch_id'         => sConf("wechat.mch_id"),
+            'mch_key'        => sConf("wechat.mch_key"),
+            "ssl_cer"       => public_path().sConf("wechat.mch_ssl_cer"),
+            "ssl_key"       => public_path().sConf("wechat.mch_ssl_key"),
+        ];
+    }
+
+}

+ 106 - 0
app/controller/merchant/Price.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\PriceService;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasPrice;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/price"),Middleware(AuthMiddleware::class)]
+class Price extends Base
+{
+
+    #[Inject]
+    protected SaasPrice $model;
+
+    #[Inject]
+    protected PriceService $service;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['agent_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function savePrice(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.default"        => 0,
+                "type.require"      => trans("empty.require"),
+                "color.require"     => trans("empty.require"),
+                "paper_size.require"=> trans("empty.require"),
+                "duplex.require"    => trans("empty.require"),
+                "price.require"     => trans("empty.require"),
+                "shop_id.default"   => $request->user['agent_id'],
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $param['price'] = $param['price'] * 100;
+            $price = $this->model->where("id",$param["id"])->findOrEmpty();
+            if ($price->isEmpty())
+            {
+                $priceType = $this->model->where([
+                    "type"      => $param["type"],
+                    "shop_id"   => $param["shop_id"],
+                    "color"     => $param["color"],
+                    "duplex"    => $param["duplex"],
+                    "paper_size"=> $param["paper_size"],
+                ])->findOrEmpty();
+                if (!$priceType->isEmpty()) return errorTrans("error.exist");
+                $state = $price->insertGetId($param);
+            } else {
+                $state = $price->save($param);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "del",methods: "post")]
+    public function delPrice(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require"),
+                "type.default"  => ""
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param["type"] == "batch") {
+                $state = $this->model->whereIn("id",$param["id"])->delete();
+            } else {
+                $price = $this->model->where("id",$param["id"])->findOrEmpty();
+                if ($price->isEmpty()) return errorTrans("empty.data");
+                if ($price['shop_id'] <> $request->user["agent_id"]) return errorTrans("empty.data");
+                $state = $price->delete();
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 160 - 0
app/controller/merchant/Prints.php

@@ -0,0 +1,160 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\PrintService;
+use app\extra\weMini\Link;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasPrice;
+use app\model\saas\SaasPrintClient;
+use DI\Attribute\Inject;
+use Kkokk\Poster\Facades\Poster;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/prints"),Middleware(AuthMiddleware::class)]
+class Prints extends Base
+{
+
+    #[Inject]
+    protected PrintService $service;
+
+    #[Inject]
+    protected SaasPrintClient $model;
+
+
+    #[Route(path: "list",methods: "get")]
+    public function getPrintList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['agent_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 保存配置
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function savePrint(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"            => trans(20010),
+                "is_price.require"      => trans(20010),
+                "paper_size.require"    => trans(20010),
+                "color.require"         => trans(20010),
+                "direction.require"     => trans(20010),
+                "duplex.require"        => trans(20010),
+                "package.require"       => trans(20010),
+                "type.require"          => trans(20010)
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $printId = $param['id'];
+            $is_price = $param['is_price'];
+            unset($param['id'],$param['is_price']);
+            $print = (new SaasPrintClient)->where("id",$printId)->findOrEmpty();
+            if ($print->isEmpty()) return errorTrans("empty.data");
+            $print->is_price = $is_price;
+            $print->rule = json_encode($param);
+            $state = $print->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+    /**
+     * 保存配置
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "single",methods: "post")]
+    public function savePrintSingle(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (empty($param['id'])) return errorTrans("empty.require");
+            $print = (new SaasPrintClient)->where("id",$param['id'])->findOrEmpty();
+            if ($print->isEmpty()) return errorTrans("empty.data");
+            if (isset($param['status'])) {
+                $param['status'] = $print['status'] == 1 ? 2 : 1;
+            }
+            $state = $print->save($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 二维码
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "qrcode",methods: "post")]
+    public function printQrcode(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (empty($param['id'])) return errorTrans("empty.require");
+            $print = (new SaasPrintClient)->where("id",$param['id'])->findOrEmpty();
+            if ($print->isEmpty()) return errorTrans("empty.data");
+            if ($print['status'] <> 1) return errorTrans("error.status-qrcode");
+            $qrcodePath = public_path()."/uploads/card/{$print['shop_id']}-print-{$print['code']}.jpg";
+            if (!is_file($qrcodePath)) {
+                $pathBase = "/uploads/qrcode/".$print['shop_id']."-".$print['code'].".jpg";
+                $path = (new Link([
+                    "appid"     => sConf("wechat.mini_appid"),
+                    "appsecret" => sConf("wechat.mini_secret")
+                ]))->createQrcodeWx("/pages/index/index","shop={$print['shop_id']}&code={$print['code']}",$pathBase);
+                $bg = base_path()."/resource/img/store-qrcode.jpg";
+                $fontPath = base_path()."/resource/font/msyh.ttc";
+                if (!is_file($qrcodePath)) {
+                    Poster::config([
+                        "path"  => $qrcodePath
+                    ])->buildImDst($bg,425,578)
+                        // ->buildText(isset($print['shop']['shop_name'])?$print['shop']['shop_name']:'印美自助打印','center',390,18,[0,0,0,1],180,$fontPath)
+//                ->buildQr($link,105,160,0,0,217,217)
+                        // ->buildImage($qrcodeminiPath,105,180,0,0,217,217)
+                        ->buildImage($path,105,170,0,0,217,247)
+                        ->getPoster();;
+                }
+            }
+            return successTrans("success.data",['img' => 'data:image/png;base64,'.base64_encode(file_get_contents($qrcodePath))]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "price",methods: "get")]
+    public function getPrintPrice(Request $request): Response
+    {
+        try {
+            $price = (new SaasPrice)->where("shop_id",$request->user['agent_id'])->where("type",1)->field("id,paper_size,duplex,color")->select();
+            if ($price->isEmpty()) return errorTrans("empty.data");
+            return successTrans("success.data",$price->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,67 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\QrcodeService;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasOrderQrcode;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/qrcode"),Middleware(AuthMiddleware::class)]
+class Qrcode extends Base
+{
+
+
+    #[Inject]
+    protected QrcodeService $service;
+
+    #[Inject]
+    protected SaasOrderQrcode $model;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            if (empty($param['shop'])) {
+                $param['shop'] = $request->user['agent_id'];
+            }
+            if (empty($param['status'])) $param['statusGt'] = 1;
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            echo $throwable->getLine()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getMessage()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 返回统计结果
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "total",methods: "get")]
+    public function getTotalData(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            if (empty($param['shop'])) {
+                $param['shop'] = $request->user['agent_id'];
+            }
+            $total = $this->service->getTotal($param);
+            return successTrans("success.data",compact("total"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 104 - 0
app/controller/merchant/Renew.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\ComboLogService;
+use app\extra\tools\CodeExtend;
+use app\extra\weMini\Link;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasCombo;
+use app\model\saas\SaasComboLog;
+use app\model\saas\SaasShop;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/renew"),Middleware(AuthMiddleware::class)]
+class Renew extends Base
+{
+
+
+    #[Inject]
+    protected ComboLogService $service;
+
+
+    /**
+     *
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "detail",methods: "get")]
+    public function getRenewDetail(Request $request): Response
+    {
+        try {
+            $shop = (new SaasShop)->where("shop_id",$request->user['agent_id'])->field("shop_name,shop_mobile,start_at,end_at,vip_end,shop_status,shop_notice,line_time,status,shop_address")->withAttr(['vip_end'  => function($query,$data) {
+                return date("Y-m-d",strtotime($data['vip_end']));
+            }])->findOrEmpty();
+            if ($shop->isEmpty()) return errorTrans("empty.data");
+            $combo = (new SaasCombo)->where("type",1)->where("status",1)->order("sort",'desc')->field("ROUND(money/100,2) as f_money,ROUND(old_money/100,2) as f_old_money,id,unit,time,name")
+                ->append(['diff'])
+                ->withAttr(['diff' => function($query,$data){
+                    return $data['f_old_money'] - $data['f_money'];
+                }])->select();
+            $privacy = htmlspecialchars_decode(sConf("service.privacy"));
+            return success('ok',compact("shop",'combo','privacy'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "qrcode",methods: "post")]
+    public function getPayQrcode(Request $request)
+    {
+        try {
+            $param = $this->_valid([
+                "comboId.require"   => trans("empty.require")
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $combo = (new SaasCombo)->where("id",$param['comboId'])->where("status",1)->findOrEmpty();
+            if ($combo->isEmpty()) return errorTrans("empty.data");
+            $orderSn = CodeExtend::createNoncestr();
+            $state = (new SaasComboLog)->insertGetId([
+                "shop_id"   => $request->user['agent_id'],
+                "order_sn"  => $orderSn,
+                "time"      => $combo['unit']==1?$combo['time']:($combo['time']*365),
+                "money"     => $combo['money']
+            ]);
+            if (!$state) return errorTrans("error.data");
+            // 生成二维码
+            $qrcodePath = "/uploads/card/{$request->user['agent_id']}-renew-{$orderSn}.jpg";
+            (new Link([
+                "appid"     => sConf("wechat.mini_appid"),
+                "appsecret" => sConf("wechat.mini_secret")
+            ]))->createQrcodeWx("/pages/shop/renew","shop={$request->user['agent_id']}&order={$orderSn}",$qrcodePath);
+            $qrcode = 'data:image/png;base64,'.base64_encode(file_get_contents(public_path().$qrcodePath));
+            return successTrans("success.data",compact("qrcode"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[Route(path: "list",methods: "get")]
+    public function getRenewList(Request $request)
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['agent_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 165 - 0
app/controller/merchant/Staff.php

@@ -0,0 +1,165 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\system\UserService;
+use app\extra\tools\CodeExtend;
+use app\middleware\AuthMiddleware;
+use app\model\system\SystemUser;
+use app\validate\saas\UserValidate;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/staff"),Middleware(AuthMiddleware::class)]
+class Staff extends Base
+{
+
+
+    #[Inject]
+    protected SystemUser $model;
+
+
+    #[Inject]
+    protected UserService $service;
+
+
+    #[Inject]
+    protected UserValidate $validate;
+
+
+
+    #[Route(path: "list",methods: "get")]
+    public function getPrintList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['agent'] = $request->user['agent_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+
+    /**
+     * 修改密码
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "passwd",methods: "post")]
+    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("id",$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());
+        }
+    }
+    /**
+     * 新增/编辑用户
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function save(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $param['agent_id'] = $request->user['agent_id'];
+            $param['type'] = 2;
+            $param['salt'] = strtoupper(CodeExtend::random(10,3));
+            $param['password'] = md5($param['password'].$param['salt']);
+            $param['create_ip'] = request()->getRealIp();
+            if (!$this->validate->check($param)) return error($this->validate->getError());
+            $user = $this->model->where("username",$param['username'])->findOrEmpty();
+            if (!$user->isEmpty()) return errorTrans("error.user-exist");
+            $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
+     */
+    #[Route(path: "del",methods: "post")]
+    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");
+                if ($data['agent_id'] <> $request->user['agent_id']) return errorTrans("empty.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
+     */
+    #[Route(path: "batch",methods: "post")]
+    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());
+        }
+    }
+
+}

+ 74 - 0
app/controller/merchant/Turnover.php

@@ -0,0 +1,74 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\OrderService;
+use app\middleware\AuthMiddleware;
+use app\model\saas\SaasOrderQrcode;
+use app\model\saas\SaasUserBuy;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/turnover"),Middleware(AuthMiddleware::class)]
+class Turnover extends Base
+{
+
+
+    /**
+     *
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "today",methods: "get")]
+    public function getToday(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "shop.default"  => $request->user['agent_id']
+            ]);
+            $data = (new OrderService)->getTotalToday(['shop' => $param['shop']]);
+            $qrcode = (new SaasOrderQrcode)->whereDay("create_at")->where(["shop_id" => $param['shop'],'status' => 1])->field("sum(money) as money,count(1) as total")->find();
+            $card = (new SaasUserBuy)->whereDay("create_at")->where(["shop_id" => $param['shop'],'status' => 1])->field("sum(money) as money,count(1) as total")->find();
+            $total = [
+                [
+                    "name"  => "小程序订单",
+                    "type"  => "线上支付",
+                    "order" => $data['p1'],
+                    "money" => $data['p1m']
+                ],
+                [
+                    "name"  => "收款码",
+                    "type"  => "线上支付",
+                    "order" => $qrcode['total'],
+                    "money" => $qrcode['money']
+                ],
+                [
+                    "name"  => "会员卡充值",
+                    "type"  => "线上支付",
+                    "order" => $card['total'],
+                    "money" => $card['money']
+                ],
+                [
+                    "name"  => "会员卡支付",
+                    "type"  => "线上支付",
+                    "order" => $data['p2'],
+                    "money" => $data['p2m']
+                ]
+            ];
+            $today = [
+                "money"     => $data['p1m'],
+                "user"      => $data['p2m'],
+                "qrcode"    => $qrcode['money'],
+            ];
+            return success("ok",compact("total","today"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

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

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\MemberService;
+use app\middleware\AuthMiddleware;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/api/mer/user"),Middleware(AuthMiddleware::class)]
+class User extends Base
+{
+
+    #[Inject]
+    protected MemberService $service;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['agent_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 34 - 0
app/controller/merchant/UserLog.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace app\controller\merchant;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\MemberLogService;
+use app\middleware\AuthMiddleware;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+#[Controller(prefix: "/api/mer/userLog"),Middleware(AuthMiddleware::class)]
+class UserLog extends Base
+{
+    #[Inject]
+    protected MemberLogService $service;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['agent_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 89 - 0
app/controller/notify/SfExpress.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace app\controller\notify;
+
+use app\extra\basic\Base;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderExpress;
+use app\model\saas\SaasOrderLog;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/sf")]
+class SfExpress extends Base
+{
+
+    protected array $opCode = [
+        50 => "已揽件",
+        30 => "运输中",
+        31 => "运输中",
+        44 => "运输中",
+        80 => "已签收"
+    ];
+
+
+    /**
+     *
+     * @param Request $request
+     * @param $id
+     * @return Response
+     */
+    #[Route(path: "hook/{id}",methods: ['get','post'])]
+    public function getHook(Request $request,$id): Response
+    {
+        try {
+            if ($request->method() == "GET") {
+                $data = $request->get();
+            } else {
+                $data = $request->post();
+            }
+            if (!empty($data['orderState'])){
+                foreach ($data['orderState'] as $val) {
+                    $order = (new SaasOrder)->where("express_id",$val['waybillNo'])->findOrEmpty();
+                    if ($order->isEmpty()) return json(['code' => 0,"success" => true,"msg" => ""]);
+                    if ($val['empCode'] == 500963) {
+                        (new SaasOrderExpress)->insertGetId(['order_id' => $order['order_sn'],'express_id' => $val['waybillNo'],'content' => "顺丰快递",'title' => "系统已接收"]);
+                        (new SaasOrderLog)->insertGetId(['order_id' => $order['order_sn'],'remark' => "顺丰快递",'title' => "系统已接收"]);
+                    }
+
+                }
+            }
+            if (!empty($data['Body']['WaybillRoute'])){
+                foreach ($data['Body']['WaybillRoute'] as $val) {
+                    $order = (new SaasOrder)->where("express_id",$val['mailno'])->with(['store' => function($query){
+                        $query->field('store_id,store_name,appid,secret,express_time,order_end');
+                    }])->findOrEmpty();
+                    if ($order->isEmpty()) return json(['code' => 0,"success" => true,"msg" => ""]);
+                    if ($order['status'] >= 2) return json(['code' => 0,"success" => true,"msg" => ""]);
+                    (new SaasOrderExpress)->insertGetId(['order_id' => $order['order_sn'],'express_id' => $val['mailno'],'content' => $val['remark'],'title' => $this->opCode[$val['opCode']]??'']);
+                    (new SaasOrderLog)->insertGetId(['order_id' => $order['order_sn'],'remark' => $val['remark'],'title' => $this->opCode[$val['opCode']]??'' ]);
+                    if ($val['opCode'] == 50 && $order['store']['order_end'] == "2") { // 已揽收,发起核销
+                        // $val['acceptAddress'] 发货地址
+                        $this->orderDone($order->toArray(),1);
+                        $order->status = 2;
+                        $order->save();
+                    }
+                    if ($val['opCode'] == 80 && $order['store']['order_end'] == "3") { // 已签收
+                        // $val['acceptAddress'] 发货地址
+                        $this->orderDone($order->toArray(),2);
+                        $order->status = 2;
+                        $order->save();
+                    }
+                }
+            }
+            return json(['code' => 0,"success" => true,"msg" => ""]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    protected function orderDone(array $data = [],int $type = 1)
+    {
+
+    }
+
+}

+ 122 - 0
app/controller/wap/Card.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace app\controller\wap;
+
+use app\extra\basic\Base;
+use app\extra\jhfPay\Pay;
+use app\extra\service\system\MoneyLogService;
+use app\extra\tools\CodeExtend;
+use app\middleware\WxMiddleware;
+use app\model\system\SystemUserBankcard;
+use app\validate\saas\CardValidate;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/wap/card"),Middleware(WxMiddleware::class)]
+class Card extends Base
+{
+
+    #[Inject]
+    protected SystemUserBankcard $model;
+
+    #[Inject]
+    protected MoneyLogService $service;
+
+    #[Inject]
+    protected CardValidate $validate;
+
+    #[Route(path: "list",methods: "get")]
+    public function getCardList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $data = $this->model->where("agent_id",$request->user['shop_id'])->findOrEmpty();
+            if ($data->isEmpty())
+            {
+                $data = null;
+            } else {
+                $data['logo'] = "https://apimg.alipay.com/combo.png?d=cashier&t={$data['bank']}";
+                $data['user_name_hide'] = hide_str($data['user_name'],1);
+                $data['card_id_hide'] = hide_str($data['card_id'],3,10);
+            }
+            $city = json_decode(file_get_contents(public_path()."/city.json"),true);
+            if (empty($param['shop'])) {
+                $param['shop'] = $request->user['shop_id'];
+            }
+            $list = $this->service->getList($param);
+            $list = pageFormat($list);
+            return success("ok",compact("data",'city','list'));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 新增银行卡
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function setUserCard(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $card = $this->model->where("agent_id",$request->user['shop_id'])->findOrEmpty();
+            if (!$card->isEmpty()) return error("请勿重复添加");
+            if ($param['type'] == 2) {
+                $memberId = $this->model->where("member_id",$param['data_id'])->field("member_id,user_name,cert_id,tel_no,bank,card_id,valid_from,valid_until,card_prov_code,card_area_code,card_city_name")->findOrEmpty();
+                if ($memberId->isEmpty()) return errorTrans("empty.data");
+                $memberId['agent_id'] = $request->user['shop_id'];
+                $state = $card->strict(false)->insertGetId($memberId->toArray());
+            } else {
+                if (empty($param['card_city'])) return error("请选择开户地区");
+                $param['card_prov_code'] = $param['card_city'][0];
+                $param['card_area_code'] = $param['card_city'][1];
+                $param['card_city'] = json_encode($param['card_city']);
+                $resp = $this->validate->scene("add")->check($param);
+                if (!$resp) return error($this->validate->getError());
+                $bankAli = json_decode(file_get_contents("https://ccdcapi.alipay.com/validateAndCacheCardInfo.json?_input_charset=utf-8&cardNo={$param['card_id']}&cardBinCheck=true"),true);
+                if (!$bankAli['validated']) return error("银行卡号有误");
+                if ($param['valid_from'] >= $param['valid_until']) return error("身份证有效期有误");
+                $param['bank'] = $bankAli['bank'];
+                $param['agent_id'] = $request->user['shop_id'];
+                $param['member_id'] = CodeExtend::random(18);
+                $respJhf = (new Pay)->config([
+                    "appid"  => sConf("wechat.jhf_appid"),
+                    "mch_id" => sConf("wechat.jhf_mch_id"),
+                    "aeskey" => sConf("wechat.jhf_aeskey"),
+                    "pubkey" => sConf("wechat.jhf_pubkey"),
+                    "prikey" => sConf("wechat.jhf_prikey"),
+                ])->createMember([
+                    "app_id"    => sConf("wechat.jhf_appid"),
+                    "member_id" => $param['member_id'],
+                    "member_type"   => "01",
+                    "member_p"  => [
+                        "user_name"     => $param['user_name'],
+                        "cert_id"       => $param['cert_id'],
+                        "tel_no"        => $param['tel_no'],
+                        "card_id"       => $param['card_id'],
+                        "valid_from"    => $param['valid_from'],
+                        "valid_until"   => $param['valid_until'],
+                        "card_prov_code"    => $param['card_prov_code'],
+                        "card_area_code"    => $param['card_area_code']
+                    ]
+                ]);
+                if (isset($respJhf['code'])) return error($respJhf['error_msg']);
+                // 推送绑定
+                $state = $card->strict(false)->insertGetId($param);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 10 - 0
app/controller/wap/Common.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace app\controller\wap;
+
+use app\extra\basic\Base;
+
+class Common extends Base
+{
+
+}

+ 200 - 0
app/controller/wap/Detail.php

@@ -0,0 +1,200 @@
+<?php
+
+namespace app\controller\wap;
+
+
+use app\extra\basic\Base;
+use app\extra\service\saas\ShopLogService;
+use app\extra\weMini\Link;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasShop;
+use app\model\system\SystemUser;
+use app\model\system\SystemUserOpen;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use Shopwwi\WebmanAuth\Auth;
+use support\Request;
+use support\Response;
+
+#[Controller(prefix: "/wap/shop"),Middleware(WxMiddleware::class)]
+class Detail extends Base
+{
+
+
+    #[Inject]
+    protected SaasShop $model;
+
+    #[Inject]
+    protected ShopLogService $service;
+
+
+    #[Route(path: "detail",methods: "get")]
+    public function getDetail(Request $request): Response
+    {
+        try {
+            $shop = $this->model->where("shop_id",$request->user['shop_id'])->field("shop_name,shop_mobile,start_at,end_at,vip_end,shop_status,shop_notice,line_time,status,shop_address")->withAttr(['vip_end'  => function($query,$data) {
+                return date("Y-m-d",strtotime($data['vip_end']));
+            }])->findOrEmpty();
+            if ($shop->isEmpty()) return errorTrans("empty.data");
+            return success("ok",$shop->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    #[Route(path: "save",methods: "post")]
+    public function setData(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $shop = $this->model->where("shop_id",$request->user['shop_id'])->findOrEmpty();
+            if ($shop->isEmpty()) return errorTrans("empty.data");
+            $state = $shop->save($param);
+            if (!$state) return errorTrans("error.data");
+            // 更新店铺名称到账号信息
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "msg",methods: ['post','get'])]
+    public function getMsgType(Request $request): Response
+    {
+        try {
+            if ($request->method() == "GET") {
+                $msg = (new SystemUserOpen)->where("id",$request->user['id'])->value("is_msg");
+                return success("ok",compact('msg'));
+            }
+            $param = $this->_valid([
+                "msg.require"   => trans("empty.require")
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $msg = (new SystemUserOpen)->where("id",$request->user['id'])->findOrEmpty();
+            if ($msg->isEmpty()) return errorTrans("error.data");
+            $msg->is_msg = $param['msg'];
+            $state = $msg->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 绑定新门店
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "bind",methods: "post")]
+    public function bindUser(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "username.require"  => trans("empty.user"),
+                "password.require"  => trans("empty.passwd"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $map = ["is_deleted" => 0,"username" => $param['username']];
+            $user = (new SystemUser)->where($map)->with(['account'])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("error.user-empty");
+            if ($user['status'] <> 1) return errorTrans("error.user-status");
+            if (md5($param['password'].$user['salt']) <> $user['password']) return errorTrans("error.passwd");
+            $userOpen = (new SystemUserOpen)->where(['openid' => $request->user['openid'],'shop_id' => $user['agent_id']])->findOrEmpty();
+            if (!$userOpen->isEmpty()) return error("请勿重复绑定");
+            $state = $userOpen->insertGetId([
+                "uid"       => $user['id'],
+                "openid"    => $request->user['openid'],
+                "shop_id"   => $user['agent_id'],
+                "shop_name" => $user['account']['shop_name']??'',
+                'is_msg'    => 1
+            ]);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 解绑
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "unbind",methods: "post")]
+    public function unbindUser(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"  => trans("empty.data")
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $user = (new SystemUserOpen)->where("id",$param['id'])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("empty.data");
+            if ($user['openid'] <> $request->user['openid']) return errorTrans("error.request");
+            $state = $user->delete();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "change",methods: ['post','get'])]
+    public function changeAuth(Request $request): Response
+    {
+        try {
+            if ($request->method() == "GET") {
+                $data = (new SystemUserOpen)->where("openid",$request->user['openid'])->select();
+                $shop = $request->user['shop_id'];
+                return success("ok",compact("data","shop"));
+            }
+            $param = $this->_valid([
+                "id.require"    => trans("empty.data")
+            ],'post');
+            if (!is_array($param)) return error($param);
+            $user = (new SystemUserOpen)->where("id",$param['id'])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("empty.data");
+            if ($user['openid'] <> $request->user['openid']) return errorTrans("error.request");
+            $loginUser = (new SystemUser)->where(['id' => $user['uid']])->with(['account' => function($query){
+                $query->field("shop_id,shop_name");
+            }])->findOrEmpty();
+            return success("ok",['shop_name' => $loginUser['account']['shop_name'],'token' => get_object_vars((new Auth)->guard("mp")->login($user->toArray()))]); // 正常登陆进入
+
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 钱包记录
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "walletLog",methods: "get")]
+    public function walletLog(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['shop_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 104 - 0
app/controller/wap/Discount.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace app\controller\wap;
+
+
+use app\extra\basic\Base;
+use app\extra\service\saas\DisCountService;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasDiscount;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+#[Controller(prefix: "/wap/discount"),Middleware(WxMiddleware::class)]
+class Discount extends Base
+{
+
+
+
+    #[Inject]
+    protected SaasDiscount $model;
+
+    #[Inject]
+    protected DisCountService $service;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['shop_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function savePrice(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.default"        => 0,
+                "color.require"     => trans("empty.require"),
+                "duplex.require"    => trans("empty.require"),
+                "number.require"    => trans("empty.require"),
+                "rate.require"      => trans("empty.require"),
+                "shop_id.default"   => $request->user['agent_id'],
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $param['keys'] = $param['color']."_".$param['duplex']."_1";
+            $price = $this->model->where("id",$param["id"])->findOrEmpty();
+            if ($price->isEmpty())
+            {
+                $priceType = $this->model->where([
+                    "shop_id"   => $param["shop_id"],
+                    "keys"      => $param["keys"]
+                ])->findOrEmpty();
+                if (!$priceType->isEmpty()) return errorTrans("error.exist");
+                $state = $price->insertGetId($param);
+            } else {
+                $state = $price->save($param);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "del",methods: "post")]
+    public function delPrice(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require"),
+                "type.default"  => ""
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param["type"] == "batch") {
+                $state = $this->model->whereIn("id",$param["id"])->delete();
+            } else {
+                $price = $this->model->where("id",$param["id"])->findOrEmpty();
+                if ($price->isEmpty()) return errorTrans("empty.data");
+                if ($price['shop_id'] <> $request->user["agent_id"]) return errorTrans("empty.data");
+                $state = $price->delete();
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 336 - 0
app/controller/wap/Login.php

@@ -0,0 +1,336 @@
+<?php
+
+namespace app\controller\wap;
+
+use app\extra\basic\Base;
+use app\extra\wechat\WechatService;
+use app\middleware\WxMiddleware;
+use app\model\system\SystemConfig;
+use app\model\system\SystemUser;
+use app\model\system\SystemUserOpen;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use Shopwwi\WebmanAuth\Auth;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/wap/login"),Middleware(WxMiddleware::class)]
+class Login extends Base
+{
+
+    protected array $noNeedLogin = ["checkLogin","bindLogin"];
+
+
+    #[Route(path: "check",methods: "post")]
+    public function checkLogin(Request $request): Response
+    {
+        try {
+            $postData = $request->all();
+            if (empty($postData)) {
+                $source = $request->header("referer")."login";
+            } else {
+                $source = $request->header("referer")."login?".http_build_query($postData);
+            }
+            $userInfo = WechatService::getWebOauthInfo($source,1,false);
+//            $userInfo['openid'] = "otdgy3S5ZzPUD5DYOsHacX5KhZ9I";
+//            $userInfo['openid'] = "omf322AlD9wFjm5Ucix9uKmRXd4I";
+            if (empty($userInfo['openid'])) {
+                return success("ok",['url' => $userInfo['url'],'type' => 1]); // 跳转
+            } else {
+                $service = (new SystemConfig)->where("type","service")->column("value","name");
+                $user = (new SystemUserOpen)->where(['openid' => $userInfo['openid']])->findOrEmpty();
+                if ($user->isEmpty())
+                {
+                    return success("ok",['url' => '','shop_name' => "",'type' => 2,'service' => $service,'token' => ['access_token' => $userInfo['openid']]]); // 跳到绑定用户
+                }
+                $loginUser = (new SystemUser)->where(['id' => $user['uid']])->with(['account' => function($query){
+                    $query->field("shop_id,shop_name");
+                }])->findOrEmpty();
+                return success("ok",['url' => '','shop_name' => $loginUser['account']['shop_name'],'type' => 3,'service' => $service,'menu' => $this->getMenu(),'token' => get_object_vars((new Auth)->guard("mp")->login($user->toArray()))]); // 正常登陆进入
+            }
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "bind",methods: "post")]
+    public function bindLogin(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "username.require"  => trans("empty.user"),
+                "password.require"  => trans("empty.passwd"),
+                "open.require"      => trans("empty.passwd"),
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $map = ["is_deleted" => 0,"username" => $param['username']];
+            $user = (new SystemUser)->where($map)->with(['account' => function($query){
+                $query->field("shop_id,shop_name");
+            }])->findOrEmpty();
+            if ($user->isEmpty()) return errorTrans("error.user-empty");
+            if ($user['status'] <> 1) return errorTrans("error.user-status");
+            if (md5($param['password'].$user['salt']) <> $user['password']) return errorTrans("error.passwd");
+            $userOpen = (new SystemUserOpen)->where(['openid' => $param['open'],'shop_id' => $user['agent_id']])->findOrEmpty();
+            if (!$userOpen->isEmpty()) return error("请勿重复授权");
+            $openData = [
+                "uid"       => $user['id'],
+                "openid"    => $param['open'],
+                "shop_id"   => $user['agent_id'],
+                "shop_name" => $user['account']['shop_name']??'',
+                'is_msg'    => 1
+            ];
+            $state = $userOpen->insertGetId($openData);
+            $openData['id'] = $state;
+            if (!$state) return errorTrans("error.data");
+            return success("ok",['shop_name' => $user['account']['shop_name'],'menu' => $this->getMenu(),'token' => get_object_vars((new Auth)->guard("mp")->login($openData))]); // 正常登陆进入
+        } catch (\Throwable $th) {
+            return error($th->getMessage());
+        }
+    }
+
+    /**
+     * @return array[]
+     */
+    protected function getMenu(): array
+    {
+        return [
+            [
+                "path"  => "/dashboard",
+                "name"  => "dashboard",
+                "component" => "dashboard",
+                "meta"  => [
+                    "icon"  => "",
+                    "title" => "总揽",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 1
+            ],[
+                "path"  => "/shop/detail",
+                "name"  => "shop/detail",
+                "component" => "shop/detail",
+                "meta"  => [
+                    "icon"  => "",
+                    "title" => "店铺详情",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 1
+            ],[
+                "path"  => "/shop/change",
+                "name"  => "shop/change",
+                "component" => "shop/change",
+                "meta"  => [
+                    "icon"  => "",
+                    "title" => "切换店铺",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 1
+            ],[
+                "path"  => "/print",
+                "name"  => "print/index",
+                "component" => "print/index",
+                "meta"  => [
+                    "icon"  => "el-icon-printer",
+                    "title" => "打印机",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/print/price",
+                "name"  => "print/price",
+                "component" => "print/price",
+                "meta"  => [
+                    "icon"  => "el-icon-shopping-bag",
+                    "title" => "价格设置",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/print/discount",
+                "name"  => "print/discount",
+                "component" => "print/discount",
+                "meta"  => [
+                    "icon"  => "el-icon-price-tag",
+                    "title" => "折扣设置",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/order",
+                "name"  => "order/index",
+                "component" => "order/index",
+                "meta"  => [
+                    "icon"  => "el-icon-tickets",
+                    "title" => "实时订单",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/order/detail",
+                "name"  => "order/detail",
+                "component" => "order/detail",
+                "meta"  => [
+                    "icon"  => "el-icon-tickets",
+                    "title" => "订单详情",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 1
+            ],[
+                "path"  => "/turnover/index",
+                "name"  => "turnover/index",
+                "component" => "turnover/index",
+                "meta"  => [
+                    "icon"  => "el-icon-money",
+                    "title" => "营业额",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/member/index",
+                "name"  => "member/index",
+                "component" => "member/index",
+                "meta"  => [
+                    "icon"  => "el-icon-credit-card",
+                    "title" => "会员卡",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/member/list",
+                "name"  => "member/list",
+                "component" => "member/list",
+                "meta"  => [
+                    "icon"  => "el-icon-credit-card",
+                    "title" => "会员列表",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 1
+            ],[
+                "path"  => "/member/recharge",
+                "name"  => "member/recharge",
+                "component" => "member/recharge",
+                "meta"  => [
+                    "icon"  => "el-icon-credit-card",
+                    "title" => "充值套餐",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 1
+            ],[
+                "path"  => "/wallet/index",
+                "name"  => "wallet/index",
+                "component" => "wallet/index",
+                "meta"  => [
+                    "icon"  => "el-icon-wallet",
+                    "title" => "我的钱包",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/wallet/log",
+                "name"  => "wallet/log",
+                "component" => "wallet/log",
+                "meta"  => [
+                    "icon"  => "el-icon-wallet",
+                    "title" => "钱包记录",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 1
+            ],[
+                "path"  => "/wallet/card",
+                "name"  => "wallet/card",
+                "component" => "wallet/card",
+                "meta"  => [
+                    "icon"  => "el-icon-postcard",
+                    "title" => "结算卡",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/shop/bind",
+                "name"  => "shop/bind",
+                "component" => "shop/bind",
+                "meta"  => [
+                    "icon"  => "el-icon-connection",
+                    "title" => "绑定门店",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ],[
+                "path"  => "/shop/msg",
+                "name"  => "shop/msg",
+                "component" => "shop/msg",
+                "meta"  => [
+                    "icon"  => "el-icon-notification",
+                    "title" => "消息开关",
+                    "type"  => "menu",
+                    "color" => "#333"
+                ],
+                "hide"  => 0
+            ]
+        ];
+    }
+
+    /**
+     * 获取当前请求的完整 URL
+     * @param bool $withQuery 是否包含查询参数
+     * @return string
+     */
+    protected function getFullUrl($withQuery = true) {
+        // 1. 获取协议(支持代理)
+        $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
+        || (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443)
+            ? 'https' : 'http';
+
+        // 2. 获取主机名(支持代理)
+        $host = $_SERVER['HTTP_X_FORWARDED_HOST']
+            ?? $_SERVER['HTTP_HOST']
+            ?? $_SERVER['SERVER_NAME']
+            ?? 'localhost';
+
+        // 3. 获取端口(支持代理)
+        $port = $_SERVER['HTTP_X_FORWARDED_PORT']
+            ?? $_SERVER['SERVER_PORT']
+            ?? 80;
+
+        // 4. 获取请求URI(包含路径和查询参数)
+        $requestUri = $_SERVER['REQUEST_URI'] ?? '/';
+
+        // 5. 构建基础URL
+        $baseUrl = $protocol . '://' . $host;
+
+        // 6. 如果不是标准端口,添加端口号
+        $isStandardPort = ($protocol === 'http' && $port == 80)
+            || ($protocol === 'https' && $port == 443);
+
+        if (!$isStandardPort) {
+            $baseUrl .= ':' . $port;
+        }
+
+        // 7. 返回完整URL
+        return $baseUrl . $requestUri;
+    }
+
+
+
+}

+ 109 - 0
app/controller/wap/Member.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace app\controller\wap;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\MemberService;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasCombo;
+use app\model\saas\SaasShop;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/wap/member"),Middleware(WxMiddleware::class)]
+class Member extends Base
+{
+
+    #[Inject]
+    protected MemberService $service;
+
+    #[Route(path: "total",methods: "get")]
+    public function getMemberTotal(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['shop_id'];
+            $data = $this->service->getMpTotal($param);
+            return success("ok",$data);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "list",methods: "get")]
+    public function getMemberList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            if (empty($param['shop'])) {
+                $param['shop'] = $request->user['shop_id'];
+            }
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "combo",methods: "get")]
+    public function getComboDetail(Request $request): Response
+    {
+        try {
+            $shop = (new SaasShop)->where("shop_id",$request->user['shop_id'])->findOrEmpty();
+            $combo = (new SaasCombo)->where("type",2)->where("status",1)->field("id,name,ROUND(money/100,2) as money,ROUND(old_money/100,2) as old_money,is_first")->select();
+            $param = [
+                "state"     => $shop['user_card'], // 1 默认 2 自定义 3关闭
+                "system"    => $combo,
+                "shop"      => empty($shop['user_card_price']) ? $combo :$shop['user_card_price']
+            ];
+            return success("ok",$param);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "combo",methods: "post")]
+    public function setCombo(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            $shop = (new SaasShop)->where("shop_id",$request->user['shop_id'])->findOrEmpty();
+            $shop->user_card = $param['state'];
+            if (isset($param['shop'])) {
+                $shop->user_card_price = json_encode($param['shop']);
+            }
+            $state = $shop->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 钱包余额
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "wallet",methods: "get")]
+    public function getWalletData(Request $request): Response
+    {
+        try {
+            $detail = (new SaasShop)->where("shop_id",$request->user['shop_id'])->column("balance,total_balance");
+            if (!isset($detail[0])) return errorTrans("empty.data");
+            return success("ok",$detail[0]);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 119 - 0
app/controller/wap/Order.php

@@ -0,0 +1,119 @@
+<?php
+
+namespace app\controller\wap;
+
+use app\extra\basic\Base;
+use app\extra\jhfPay\Pay;
+use app\extra\service\saas\OrderService;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderDetail;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+#[Controller(prefix: "/wap/order"),Middleware(WxMiddleware::class)]
+class Order extends Base
+{
+
+    #[Inject]
+    protected OrderService $service;
+
+    #[Inject]
+    protected SaasOrder $model;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            if (empty($param['shop'])) {
+                $param['shop'] = $request->user['shop_id'];
+            }
+            if (!empty($param['order'])) {
+                $orderType = explode("-",$param['order']);
+                if (count($orderType)>1) { // 搜索取件码
+                    $param['sn'] = $param['order'];
+                } else {
+                    $param['orderid'] = $param['order'];
+                }
+            }
+            if (empty($param['status'])) $param['statusGt'] = 1;
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "detail",methods: "get")]
+    public function getOrderDetail(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(['detail','shop' => function($query){
+                $query->field("shop_id,shop_name");
+            }])->findOrEmpty();
+            if ($detail->isEmpty()) return errorTrans("error.data");
+            if ($detail['shop_id'] <> $request->user['shop_id']) return errorTrans("error.data");
+            $startTime = strtotime(date('Y-m-d 00:00:00'));
+            $endTime = strtotime(date('Y-m-d 23:59:58'));
+            $orderTime = strtotime($detail['create_at']);
+            $detail['today'] = (($orderTime > $startTime && $orderTime < $endTime) ? 1 : 0);
+            return success("ok",$detail->toArray());
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "refund",methods: "post")]
+    public function orderRefund(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("error.data");
+            if ($detail['shop_id'] <> $request->user['shop_id']) return errorTrans("error.data");
+            if (!in_array($detail['status'],[1,2,3])) return error("该订单不支持退款");
+            if ($detail['pay_type'] == 2) {
+                return error("会员卡退款暂时停用");
+            }
+            $respJhf = (new Pay)->config([
+                "appid"  => sConf("wechat.jhf_appid"),
+                "mch_id" => sConf("wechat.jhf_mch_id"),
+                "aeskey" => sConf("wechat.jhf_aeskey"),
+                "pubkey" => sConf("wechat.jhf_pubkey"),
+                "prikey" => sConf("wechat.jhf_prikey"),
+            ])->createRefund([
+                "payment_id"    => $detail['payment_id'],
+                "order_no"      => $detail['order_sn'],
+                "notify_url"    => "https://panel.huiyinduo.cn/notify/refund",
+                "refund_amt"    => format_money($detail['money']/100,2)
+            ]);
+            if (isset($respJhf['code'])) {
+                return error("发起退款失败");
+            }
+            $detail->status = 4;
+            $state = $detail->save();
+            if (!$state) return errorTrans("error.data");
+            (new SaasOrderDetail)->where("order_sn",$detail['order_sn'])->update(['status' => 4]);
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+
+}

+ 108 - 0
app/controller/wap/Price.php

@@ -0,0 +1,108 @@
+<?php
+
+namespace app\controller\wap;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\PriceService;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasPrice;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/wap/price"),Middleware(WxMiddleware::class)]
+class Price extends Base
+{
+
+
+
+    #[Inject]
+    protected SaasPrice $model;
+
+    #[Inject]
+    protected PriceService $service;
+
+    #[Route(path: "list",methods: "get")]
+    public function getPriceList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['shop_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function savePrice(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.default"        => 0,
+                "type.require"      => trans("empty.require"),
+                "color.require"     => trans("empty.require"),
+                "paper_size.require"=> trans("empty.require"),
+                "duplex.require"    => trans("empty.require"),
+                "price.require"     => trans("empty.require"),
+                "shop_id.default"   => $request->user['agent_id'],
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $param['price'] = $param['price'] * 100;
+            $price = $this->model->where("id",$param["id"])->findOrEmpty();
+            if ($price->isEmpty())
+            {
+                $priceType = $this->model->where([
+                    "type"      => $param["type"],
+                    "shop_id"   => $param["shop_id"],
+                    "color"     => $param["color"],
+                    "duplex"    => $param["duplex"],
+                    "paper_size"=> $param["paper_size"],
+                ])->findOrEmpty();
+                if (!$priceType->isEmpty()) return errorTrans("error.exist");
+                $state = $price->insertGetId($param);
+            } else {
+                $state = $price->save($param);
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    #[Route(path: "del",methods: "post")]
+    public function delPrice(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"    => trans("empty.require"),
+                "type.default"  => ""
+            ],"post");
+            if (!is_array($param)) return error($param);
+            if ($param["type"] == "batch") {
+                $state = $this->model->whereIn("id",$param["id"])->delete();
+            } else {
+                $price = $this->model->where("id",$param["id"])->findOrEmpty();
+                if ($price->isEmpty()) return errorTrans("empty.data");
+                if ($price['shop_id'] <> $request->user["agent_id"]) return errorTrans("empty.data");
+                $state = $price->delete();
+            }
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 102 - 0
app/controller/wap/Prints.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace app\controller\wap;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\PrintService;
+use app\middleware\WxMiddleware;
+use app\model\saas\SaasPrintClient;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/wap/prints"),Middleware(WxMiddleware::class)]
+class Prints extends Base
+{
+
+
+    #[Inject]
+    protected PrintService $service;
+
+    #[Inject]
+    protected SaasPrintClient $model;
+
+
+    #[Route(path: "list",methods: "get")]
+    public function getPrintList(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            $param['shop'] = $request->user['shop_id'];
+            $list = $this->service->getList($param);
+            return successTrans("success.data",pageFormat($list),200);
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+    /**
+     * 保存配置
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "save",methods: "post")]
+    public function savePrint(Request $request): Response
+    {
+        try {
+            $param = $this->_valid([
+                "id.require"            => trans(20010),
+                "is_price.require"      => trans(20010),
+                "paper_size.require"    => trans(20010),
+                "color.require"         => trans(20010),
+                "direction.require"     => trans(20010),
+                "duplex.require"        => trans(20010),
+                "package.require"       => trans(20010),
+                "type.require"          => trans(20010)
+            ],"post");
+            if (!is_array($param)) return error($param);
+            $printId = $param['id'];
+            $is_price = $param['is_price'];
+            unset($param['id'],$param['is_price']);
+            $print = (new SaasPrintClient)->where("id",$printId)->findOrEmpty();
+            if ($print->isEmpty()) return errorTrans("empty.data");
+            $print->is_price = $is_price;
+            $print->rule = json_encode($param);
+            $state = $print->save();
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+
+    /**
+     * 保存配置
+     * @param Request $request
+     * @return Response
+     */
+    #[Route(path: "single",methods: "post")]
+    public function savePrintSingle(Request $request): Response
+    {
+        try {
+            $param = $request->post();
+            if (empty($param['id'])) return errorTrans("empty.require");
+            $print = (new SaasPrintClient)->where("id",$param['id'])->findOrEmpty();
+            if ($print->isEmpty()) return errorTrans("empty.data");
+            if (isset($param['status'])) {
+                $param['status'] = $print['status'] == 1 ? 2 : 1;
+            }
+            $state = $print->save($param);
+            if (!$state) return errorTrans("error.data");
+            return successTrans("success.data");
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 70 - 0
app/controller/wap/Turnover.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace app\controller\wap;
+
+use app\extra\basic\Base;
+use app\extra\service\saas\OrderService;
+use app\middleware\WxMiddleware;
+use DI\Attribute\Inject;
+use LinFly\Annotation\Route\Controller;
+use LinFly\Annotation\Route\Middleware;
+use LinFly\Annotation\Route\Route;
+use support\Request;
+use support\Response;
+
+
+#[Controller(prefix: "/wap/turnover"),Middleware(WxMiddleware::class)]
+class Turnover extends Base
+{
+
+    #[Inject]
+    protected OrderService $service;
+
+    #[Route(path: "total",methods: "get")]
+    public function getTotal(Request $request): Response
+    {
+        try {
+            $param = $request->get();
+            if (empty($param['shop']))
+            {
+                $param['shop'] = $request->user['shop_id'];
+            }
+            $data = $this->service->getTotalDate($param);
+            $total = [
+                [
+                    "name"  => "小程序订单",
+                    "type"  => "线上支付",
+                    "order" => $data['total']['p1'],
+                    "money" => $data['total']['p1m']
+                ],
+                [
+                    "name"  => "收款码",
+                    "type"  => "线上支付",
+                    "order" => $data['qrcode']['total'],
+                    "money" => $data['qrcode']['money']
+                ],
+                [
+                    "name"  => "会员卡充值",
+                    "type"  => "线上支付",
+                    "order" => $data['card']['total'],
+                    "money" => $data['card']['money']
+                ],
+                [
+                    "name"  => "会员卡支付",
+                    "type"  => "线上支付",
+                    "order" => $data['total']['p2'],
+                    "money" => $data['total']['p2m']
+                ]
+            ];
+            $today = [
+                "money"     => $data['total']['p1m'],
+                "user"      => $data['total']['p2m'],
+                "qrcode"    => $data['qrcode']['money'],
+            ];
+            return success("ok",compact("today","total"));
+        } catch (\Throwable $throwable) {
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 37 - 0
app/event/ClearUpload.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\event;
+
+use app\model\saas\SaasUpload;
+
+class ClearUpload
+{
+
+    /**
+     * 清理上传文件
+     * @param array $param
+     * @return bool
+     */
+    public function clearData(array $param = []): bool
+    {
+        try {
+            if (empty($param['key'])) return true;
+            $uploadData = (new SaasUpload)->where("key",$param["key"])->select();
+            if ($uploadData->isEmpty()) return true;
+            foreach ($uploadData as $file) {
+                if (file_exists(public_path().$file['path'])) {
+                    unlink(public_path().$file['path']);
+                }
+            }
+            $uploadData->delete(); // 清理数据库
+            return true;
+        } catch (\Throwable $th) {
+            echo "清理上传文件报错\n";
+            echo $th->getLine()."\n";
+            echo $th->getFile()."\n";
+            echo $th->getMessage()."\n";
+            return false;
+        }
+    }
+
+}

+ 54 - 0
app/event/CreateOrder.php

@@ -0,0 +1,54 @@
+<?php
+
+namespace app\event;
+
+use app\model\saas\SaasCart;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderDetail;
+use Webman\RedisQueue\Redis;
+
+class CreateOrder
+{
+
+    public function createOrder(array $data = [])
+    {
+        try {
+            $cart = (new SaasCart)->where("shop_id",$data['shop'])->where("openid",$data['openid'])->where("print_id",$data['print'])->order("create_at desc")->select();
+            if ($cart->isEmpty()) return success('ok',['cart' => []]);
+            $order = (new SaasOrder)->where("order_sn",$data['order'])->findOrEmpty();
+            $orderDetail = [];
+            $print_name = !empty($order['print_name']) ? explode("-",$order['print_name']) : [];
+            $printNameOut = $print_name[1] ?? '';
+            echo "订单号:{$data['order']}====打印机名:{$printNameOut}====打印机id:{$order['print_id']}\n";
+            foreach ($cart->toArray() as $key=>$val)
+            {
+                unset($val['id']);
+                $orderDetail[$key] = $val;
+                $orderDetail[$key]['order_sn'] = $data['order'];
+                $orderDetail[$key]['print_id'] = $order['print_id']?$order['print_id']:$val['print_id'];
+                $orderDetail[$key]['print_name'] = $print_name[1] ?? $val['print_name'];
+                $orderDetail[$key]['status'] = 1;
+//                if ($val['extension'] == "pdf") {
+//                    $orderDetail[$key]['status'] = 1;
+//                } else {
+//                    $orderDetail[$key]['status'] = 0;
+//                }
+            }
+            $state = (new SaasOrderDetail)->strict(false)->insertAll($orderDetail);
+            Redis::send("push-print",['type' => 'order' , 'order' => $data['order']]);
+//            events("merge-pdf",$data);
+            if (empty($printNameOut)) {
+                $order->print_name = $cart[0]['print_name']??'';
+                $order->save();
+            }
+            (new SaasCart)->where("shop_id",$data['shop'])->where("openid",$data['openid'])->delete();
+        } catch (\Throwable $throwable) {
+            echo "写入订单详情错误\n";
+            echo $throwable->getLine()."\n";
+            echo $throwable->getFile()."\n";
+            echo $throwable->getMessage()."\n";
+            return error($throwable->getMessage());
+        }
+    }
+
+}

+ 43 - 0
app/event/MergePdf.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace app\event;
+
+use app\model\saas\SaasOrderDetail;
+use Webman\RedisQueue\Redis;
+use yzh52521\EasyHttp\Http;
+
+class MergePdf
+{
+
+    public function imgMergePdf(array $data = [])
+    {
+        if (empty($data['shop'])) return true;
+        $detail = (new SaasOrderDetail)->where(["shop_id" => $data['shop'],"openid" => $data['openid'],"order_sn" => $data['order'],'status' => 0])->select();
+        if ($detail->isEmpty()) return true;
+        foreach ($detail as $key=>$val) {
+            if ($val['extension'] !== 'pdf') {
+                $pathUri = [];
+                for ($i = $val['start_page']; $i <= $val['end_page']; $i ++) {
+//                for ($i = 1; $i <= $val['total_page']; $i ++) {
+                    $pathUri[$i] = "https://".sConf("storage.cos_http_domain")."/".$val['path']."?ci-process=doc-preview&dstType=png&imageDpi=300&page={$i}";
+                }
+                $resp = Http::asJson()->withHeaders(['x-api-key' => "your-api-key-here"])->post("http://127.0.0.1:3000/api/images-to-pdf",[
+                    "imageUrls" => array_values($pathUri),
+                    "pdfName"   => $data['order']."_".$val['id'],
+                    "options"   => [
+                        "compression"   => false,
+                        "quality"       => 100
+                    ]
+                ])->array();
+                echo getDateFull()."==={$data['order']}===word转码\n";
+                print_r($resp);
+                if ($resp['success']) {
+                    (new SaasOrderDetail)->where("id",$val['id'])->update(['path' => $resp['data']['pdfUrl'],'old_path' => $val['path'], 'status' => 1]);
+                    // 推送打印
+                    Redis::send("push-print",['type' => 'id' , 'order' => $val['id']]);
+                }
+            }
+        }
+    }
+
+}

+ 19 - 0
app/event/SyncUser.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace app\event;
+
+use support\think\Db;
+
+class SyncUser
+{
+
+    public function syncUserData(array $data = [])
+    {
+        if (empty($data['openid'])) return true;
+        if (empty($data['oid'])) return true;
+        $old = Db::connect("old")->table("inmei_member_card")->where("openid",$data['oid'])->findOrEmpty();
+        if (empty($old)) return true;
+
+    }
+
+}

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

@@ -0,0 +1,222 @@
+<?php
+
+namespace app\extra\basic;
+
+use app\extra\tools\CodeExtend;
+use app\model\system\SystemUser;
+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;
+
+
+    protected function getParent(array $data)
+    {
+        if ($data['parent_id'] > 0) {
+            return $data['parent_id'];
+        }
+        return $data['user_id'];
+    }
+
+    public function getShopId()
+    {
+        return request()->header("shop",0);
+    }
+
+    public function getAccountId(array $data)
+    {
+        return $data['account_id'];
+    }
+
+    protected function getDyConfig(): array
+    {
+        return ["appid" => sConf('dy.appid'),'secret' => sConf('dy.secret')];
+    }
+
+    /**
+     * 写入新用户
+     * @param array $param
+     * @return bool
+     */
+    protected function sceneUser(array $param = [],int $type = 1,$field = "agent_id"): bool
+    {
+        if (!isset($param['id']))
+        {
+            $param['salt'] = strtoupper(CodeExtend::random(10,3));
+            $param['password'] = md5($param['password'].$param['salt']);
+            $param['create_ip'] = request()->getRealIp() ?: '127.0.0.1';
+            $param['type'] = $type;
+        }
+        return (new SystemUser)->setAutoData($param,$field);
+    }
+
+    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|array|Arrayable $message
+     * @param array|Arrayable|null $data
+     * @return Json
+     */
+    public function success($message = self::DEFAULT_SUCCESS_MESSAGE, $data = null)
+    {
+        $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 $message
+     * @param array|Arrayable|null $data
+     */
+    public function make(int $status, string $message, $data = null)
+    {
+        $content = compact('status', 'message');
+        if (!is_null($data))
+            $content['data'] = $this->parseData($data);
+        return json($content);
+    }
+
+    public function encode($message = self::DEFAULT_SUCCESS_MESSAGE, $data = null)
+    {
+        $message = $this->parseData($message);
+        if (is_array($message)) {
+            $data = $message;
+            $message = self::DEFAULT_SUCCESS_MESSAGE;
+        } else {
+            $data = $this->parseData($data);
+        }
+        $status = self::DEFAULT_SUCCESS_CODE;
+        $encode = true;
+        $content = compact('status','encode', 'message');
+        if (is_null($data) || env('APP_DEBUG', false))
+            return $this->success($message, $data['data'] ?? $data);
+        if (!is_array($data) || !isset($data['data'])) {
+            $res['data'] = $data;
+        } else {
+            $res = $data;
+        }
+        $res['status'] = $status;
+        $res['message'] = $message;
+        $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']
+            ];
+            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;
+    }
+
+
+
+}

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

@@ -0,0 +1,30 @@
+<?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) {
+            return false;
+        }
+        return $state;
+    }
+
+}

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

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

+ 158 - 0
app/extra/jhfPay/Pay.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace app\extra\jhfPay;
+
+use yzh52521\EasyHttp\Http;
+
+class Pay
+{
+
+    protected string $gateway = "https://payapi.juhefu.com/";
+
+    protected array $config = [];
+
+    public function config(array $config = [])
+    {
+        $this->config = $config;
+        return $this;
+    }
+
+    /**
+     * 分账完之后立马提现
+     * @param array $param
+     * @return array
+     */
+    public function createBalanceWithdraw(array $param = []): array
+    {
+        return $this->paramData($param,"api/member/balance_withdraw_a");
+    }
+
+    /**
+     * 基于余额分账
+     * @param array $param
+     * @return array
+     */
+    public function createBalancePay(array $param = []): array
+    {
+        return $this->paramData($param,"api/account/balance_pay");
+    }
+
+    /**
+     * 绑定用户结算卡
+     * @param array $param
+     * @return array
+     */
+    public function createMember(array $param = []): array
+    {
+        return $this->paramData($param,"api/member/create_user_a");
+    }
+
+    /**
+     * 更新结算卡信息
+     * @param array $param
+     * @return array
+     */
+    public function updateMember(array $param = []): array
+    {
+        return $this->paramData($param,"api/member/update_account_a");
+    }
+
+    /**
+     * 账户可用余额查询
+     * @param array $param
+     * @return array [balance,freeze_balance,divide_balance,total]
+     */
+    public function getBalance(array $param = []): array
+    {
+        return $this->paramData($param,"api/member/balance_query_a");
+    }
+
+    /**
+     * 创建支付
+     * @param array $param
+     * @return array
+     */
+    public function createPay(array $param = []): array
+    {
+        return $this->paramData($param,"api/payment/create_payment");
+    }
+
+
+    /**
+     * 创建退款
+     * @param array $param
+     * @return array
+     */
+    public function createRefund(array $param = []): array
+    {
+        return $this->paramData($param,"api/payment/payment_refund");
+    }
+
+    protected function paramData(array $param = [],string $url = ""): array
+    {
+        //转为json格式业务报文
+        $encryptData = json_encode($param, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
+        //对业务报文进行aes加密
+        $reqCipher = Utils::aes_encrypt($encryptData, $this->config['aeskey']);
+        //报文拼接签名字符串并进行SHA256withRSA签名
+        $param = $this->nestedSort($param);
+        $signData = Json_encode($param,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
+        $signData = Utils::rsa_sign($signData, $this->config['prikey']);
+        $time = microtime(true);
+        $timestamp = (int)$time; // 整数部分为时间戳
+        $microseconds = ($time - $timestamp) * 1000; // 小数部分转换为毫秒
+        $formattedTime = date("YmdHis", $timestamp) . sprintf('%03d', $microseconds);
+        $post_data = [
+            "merId"     => $this->config['mch_id'],
+            "sign"      => $signData,
+            "reqCipher" => $reqCipher,
+            "reqTime"   => $formattedTime
+        ];
+        $resp = Http::asJson()->post($this->gateway.$url,$post_data)->array();
+        if (array_key_exists("error_msg", $resp)) //失败
+        {
+            return ['code' => 0,"error_code"=>$resp["error_code"], "error_msg"=>$resp["error_msg"]];
+        }
+        $resCipher = Utils::aes_decrypt($resp['resCipher'], $this->config['aeskey']);
+        // 返回 [expend][pay_info]
+        return json_decode($resCipher,true);
+    }
+
+    /**
+     * @param $array
+     * @return array
+     */
+    protected function ksort_recursive(&$array): array
+    {
+        // 先对当前层数组按键名排序
+        ksort($array);
+        // 遍历数组中的每个元素
+        foreach ($array as &$value) {
+            // 如果元素是数组,递归调用排序函数
+            if (is_array($value)) {
+                $this->ksort_recursive($value);
+            }
+        }
+        unset($value); // 解除引用
+    }
+
+    /**
+     * 对多维数组进行递归字母顺序排序(每层按键名排序)
+     *
+     * @param array $array 待排序的数组
+     * @return array 排序后的数组
+     */
+    protected function nestedSort(array $array): array
+    {
+        // 对当前层的键进行排序
+        ksort($array);
+        // 递归处理子数组
+        foreach ($array as $key => &$value) {
+            if (is_array($value)) {
+                $value = $this->nestedSort($value);
+            }
+        }
+        return $array;
+    }
+
+}

+ 134 - 0
app/extra/jhfPay/Utils.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace app\extra\jhfPay;
+
+use InvalidArgumentException;
+use RuntimeException;
+
+class Utils
+{
+    /**
+     * AES加密函数(ECB模式,32位密钥)
+     *
+     * @param string $plaintext 待加密的明文
+     * @param string $key 32字节(256位)密钥
+     * @return string 加密后的Base64编码字符串
+     */
+    public static function aes_encrypt($plaintext, $key) {
+        // 验证密钥长度(32字节 = 256位)
+        if (strlen($key) !== 32) {
+            throw new InvalidArgumentException('密钥长度必须为32字节');
+        }
+
+        // 执行加密(ECB模式,PKCS#7填充)
+        $ciphertext = openssl_encrypt(
+            $plaintext,
+            'AES-256-ECB',
+            $key,
+            OPENSSL_RAW_DATA
+        );
+
+        // 返回Base64编码结果
+        return base64_encode($ciphertext);
+    }
+
+    /**
+     * AES解密函数(ECB模式,32位密钥)
+     *
+     * @param string $ciphertext 待解密的Base64编码密文
+     * @param string $key 32字节(256位)密钥
+     * @return string 解密后的明文
+     */
+    public static function aes_decrypt($ciphertext, $key) {
+        // 验证密钥长度(32字节 = 256位)
+        if (strlen($key) !== 32) {
+            throw new InvalidArgumentException('密钥长度必须为32字节');
+        }
+
+        // 解码Base64密文
+        $ciphertext = base64_decode($ciphertext);
+
+        // 执行解密(ECB模式,PKCS#7填充)
+        return openssl_decrypt(
+            $ciphertext,
+            'AES-256-ECB',
+            $key,
+            OPENSSL_RAW_DATA
+        );
+    }
+
+    /**
+     * RSA签名函数(SHA256withRSA,1024位密钥)
+     *
+     * @param string $data 待签名的数据
+     * @param string $privateKeyText 私钥纯文本内容(不带PEM头和尾)
+     * @return string 签名后的Base64编码字符串
+     */
+    public static function rsa_sign(string $data, string $privateKeyText): string
+    {
+
+        // 将纯文本私钥转换为PEM格式
+        $privateKeyPem = "-----BEGIN PRIVATE KEY-----\n" .
+            chunk_split($privateKeyText, 64, "\n") .
+            "-----END PRIVATE KEY-----";
+        // 创建私钥资源
+        $privateKeyResource = openssl_pkey_get_private($privateKeyPem);
+        if (!$privateKeyResource) {
+            throw new InvalidArgumentException('私钥格式错误: ' . openssl_error_string());
+        }
+        // 生成签名(SHA256withRSA)
+        $success = openssl_sign($data, $signature, $privateKeyResource, OPENSSL_ALGO_SHA256);
+        // 释放资源
+        unset($privateKey);
+//        openssl_free_key($privateKeyResource);
+
+        if (!$success) {
+            throw new RuntimeException('签名失败: ' . openssl_error_string());
+        }
+
+        // 返回Base64编码的签名
+        return base64_encode($signature);
+    }
+
+    /**
+     * RSA验签函数(SHA256withRSA,1024位密钥)
+     *
+     * @param string $data 原始数据
+     * @param string $signature 签名的Base64编码字符串
+     * @param string $publicKeyText 公钥纯文本内容(不带PEM头和尾)
+     * @return bool 验签结果
+     */
+    public static function rsa_verify(string $data, string $signature, string $publicKeyText): bool
+    {
+        // 将纯文本公钥转换为PEM格式
+        $publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" .
+            chunk_split($publicKeyText, 64, "\n") .
+            "-----END PUBLIC KEY-----";
+
+        // 创建公钥资源
+        $publicKeyResource = openssl_pkey_get_public($publicKeyPem);
+
+        if (!$publicKeyResource) {
+            throw new InvalidArgumentException('公钥格式错误: ' . openssl_error_string());
+        }
+
+        // 解码Base64签名
+        $signature = base64_decode($signature);
+
+        // 验证签名(SHA256withRSA)
+        $result = openssl_verify($data, $signature, $publicKeyResource, OPENSSL_ALGO_SHA256);
+
+        // 释放资源
+        openssl_free_key($publicKeyResource);
+
+        // 返回验证结果(1=成功,0=失败,-1=错误)
+        if ($result === 1) {
+            return true;
+        } elseif ($result === 0) {
+            return false;
+        } else {
+            throw new RuntimeException('验签过程发生错误: ' . openssl_error_string());
+        }
+    }
+
+}

+ 98 - 0
app/extra/service/basic/UploadService.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace app\extra\service\basic;
+
+use app\model\system\SystemConfig;
+use Tinywan\Storage\Adapter\CosAdapter;
+use Tinywan\Storage\Adapter\LocalAdapter;
+use Tinywan\Storage\Adapter\QiniuAdapter;
+
+class UploadService
+{
+    /**
+     * 配置
+     * @return array
+     */
+    public function setConfigVal(string $type = ""): array
+    {
+        $data = (new SystemConfig)->where("type","storage")->column("value","name");
+        $config = [];
+        $config['storage']['default'] = $data['type'];
+        $config['storage']['include'] = !empty($data['allow_exts']) ? explode(",",$data['allow_exts']) : [];
+        $config['storage']['exclude'] = !empty($data['noallow_exts']) ? explode(",",$data['noallow_exts']) : [];
+        $config['storage']['nums'] = 10;
+        $config['storage']['single_limit'] = 1024 * 1024 * 200;
+        $config['storage']['total_limit'] = 1024 * 1024 * 200;
+        if (empty($type)) $type = $data['type'];
+        switch ($type)
+        {
+            case "local":
+                $config['storage']['local'] = [
+                    'adapter' => LocalAdapter::class,
+                    'root' => public_path().'/uploads/storage/',
+                    'dirname' => function () {
+                        return date('Ymd');
+                    },
+                    'domain' => "",
+                    'uri' => '/uploads/storage/', // 如果 domain + uri 不在 public 目录下,请做好软链接,否则生成的url无法访问
+                    'algo' => 'sha1',
+                ];
+                break;
+            case "qiniu":
+                $config['storage']['qiniu'] = [
+                    'adapter' => QiniuAdapter::class,
+                    'accessKey' => $data['qiniu_access_key'],
+                    'secretKey' => $data['qiniu_secret_key'],
+                    'bucket' => $data['qiniu_bucket'],
+                    'dirname' => 'storage/'.date("Ymd"),
+                    'domain' => $this->setDomain($data['oss_http_protocol'],$data['qiniu_http_domain']),
+                ];
+                break;
+            case "oss":
+                $config['storage']['oss'] = [
+                    'adapter' => \Tinywan\Storage\Adapter\OssAdapter::class,
+                    'accessKeyId' => $data['oss_access_key'],
+                    'accessKeySecret' => $data['oss_secret_key'],
+                    'bucket' => $data['oss_bucket'],
+                    'dirname' => function () {
+                        return 'storage/'.date("Ymd");
+                    },
+                    'domain' => $this->setDomain($data['oss_http_protocol'],$data['oss_http_domain']),
+                    'endpoint' => $data['oss_region'],
+                    'algo' => 'sha1',
+
+                ];
+                break;
+            case "cos":
+                $config['storage']['cos'] = [
+                    'adapter' => CosAdapter::class,
+                    'secretId' => $data['cos_access_key'],
+                    'secretKey' => $data['cos_secret_key'],
+                    'bucket' => $data['cos_bucket'],
+                    'dirname' => 'storage/'.date("Ymd"),
+                    'domain' => $this->setDomain($data['oss_http_protocol'],$data['cos_http_domain']),
+                    'region' => $data['cos_region'],
+                ];
+                break;
+            default:
+                break;
+        }
+        return $config;
+    }
+
+    /**
+     * 协议域名
+     * @param string $type
+     * @param string $domain
+     * @return string
+     */
+    protected function setDomain(string $type,string $domain): string
+    {
+        if ($type == "auto") {
+            return "//".$domain;
+        }
+        return $type."://".$domain;
+    }
+
+
+}

+ 36 - 0
app/extra/service/saas/ComboLogService.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasComboLog;
+
+class ComboLogService extends Service
+{
+
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasComboLog();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        !empty($param['name']) && $filter[] = ["name", 'like', "%{$param['name']}%"];
+        return $filter;
+    }
+
+}

+ 35 - 0
app/extra/service/saas/ComboService.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasCombo;
+
+class ComboService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasCombo();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['name']) && $filter[] = ["name", 'like', "%{$param['name']}%"];
+        !empty($param['type']) && $filter[] = ["type", '=', $param['type']];
+        return $filter;
+    }
+
+}

+ 34 - 0
app/extra/service/saas/DisCountService.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasDiscount;
+
+class DisCountService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasDiscount();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['name']) && $filter[] = ["name", 'like', "%{$param['name']}%"];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        return $filter;
+    }
+
+
+}

+ 36 - 0
app/extra/service/saas/MemberLogService.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasUserLog;
+
+class MemberLogService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasUserLog();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['type']) && $filter[] = ["type", '=', $param['type']];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['statusGt']) && $filter[] = ["status", '>', ($param['statusGt']-1)];
+        !empty($param['orderid']) && $filter[] = ["card_no", 'like', "%{$param['orderid']}%"];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        return $filter;
+    }
+
+}

+ 68 - 0
app/extra/service/saas/MemberService.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasUser;
+use app\model\saas\SaasUserBuy;
+
+class MemberService extends Service
+{
+
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasUser();
+        return $this->searchVal($param,$this->searchFilter($param))->with(['shop'])->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+    /**
+     * @param array $param
+     * @return array
+     */
+    public function getMpTotal(array $param = []): array
+    {
+        $this->mode = new SaasUser();
+        $commonFilter = [];
+        $filter = $this->searchFilter($param);
+        // 起止时间
+        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);
+        $userTotal = $this->mode->field("count(1) as number,sum(balance) as total")->where($filter)->findOrEmpty();
+        $filter[] = ['status','=',1];
+        $buyTotal = (new SaasUserBuy)->field("count(1) as number,sum(money) as total")->where($filter)->findOrEmpty();
+        return [
+            "user"          => $userTotal['number'],
+            "userMoney"     => $userTotal['total'],
+            "recharge"      => $buyTotal['number'],
+            "rechargeMoney" => $buyTotal['total'],
+        ];
+    }
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['type']) && $filter[] = ["type", '=', $param['type']];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['statusGt']) && $filter[] = ["status", '>', ($param['statusGt']-1)];
+        !empty($param['orderid']) && $filter[] = ["card_no", 'like', "%{$param['orderid']}%"];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        return $filter;
+    }
+
+}

+ 121 - 0
app/extra/service/saas/OrderService.php

@@ -0,0 +1,121 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasOrder;
+use app\model\saas\SaasOrderQrcode;
+use app\model\saas\SaasUserBuy;
+
+class OrderService extends Service
+{
+
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasOrder();
+        return $this->searchVal($param,$this->searchFilter($param))->field("*")->with(['detail','shop' => function($query){
+            $query->field("shop_id,shop_name");
+        }])->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+    public function getTotal(array $param = []): array
+    {
+        $this->mode = new SaasOrder();
+        $total = ['t0' => 0, 't1' => 0, 't2' => 0, 't3' => 0, 't4' => 0, 't5' => 0, 't6' => 0, 'ta' => 0];
+        $where = [];
+        if (!empty($param['shop'])) {
+            $where[] = ['shop_id','=',$param['shop']];
+        }
+        $where[] = ['status',">",0];
+        foreach ($this->searchVal($param,$where)->field('create_at,status,count(1) total')->group('status,create_at')->cursor() as $vo)
+        {
+            [$total["t{$vo['status']}"] += $vo['total'], $total['ta'] += $vo['total']];
+        }
+        return $total;
+    }
+
+    /**
+     * 手机端
+     * @param array $param
+     * @return int[]
+     */
+    public function getTotalDate(array $param = []): array
+    {
+        $this->mode = new SaasOrder();
+        $commonFilter = [];
+        $filter = $this->searchFilter($param);
+        // 起止时间
+        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);
+        $total = ['ta' => 0,'t0' => 0, 't1' => 0, 't2' => 0, 't3' => 0, 't4' => 0, 't5' => 0, 't6' => 0, 'tm' => 0, 'p1' => 0, 'p2' => 0, 'p1m' => 0, 'p2m' => 0];
+        foreach ($this->mode->where($filter)->whereIn("status",[1,2,3])->field('create_at,pay_type,status,sum(discount) as money,count(1) as total')->group('status,create_at,pay_type')->cursor() as $vo)
+        {
+            $total["t{$vo['status']}"] += $vo['total'];
+            $total['ta'] += $vo['total'];
+            $total['tm'] += $vo['money'];
+            if ($vo['pay_type'] == 1) {
+                $total["p1"] += $vo['total'];
+                $total["p1m"] += $vo['money'];
+            }
+            if ($vo['pay_type'] == 2) {
+                $total["p2"] += $vo['total'];
+                $total["p2m"] += $vo['money'];
+            }
+        }
+        $filter[] = ['status','=',1];
+        $qrcode = (new SaasOrderQrcode)->where($filter)->field("sum(money) as money,count(1) as total")->find();
+        $card = (new SaasUserBuy)->where($filter)->field("sum(money) as money,count(1) as total")->find();
+        return compact("total",'qrcode','card');
+    }
+
+
+    public function getTotalToday(array $param = []): array
+    {
+        $this->mode = new SaasOrder();
+        $total = ['ta' => 0,'t0' => 0, 't1' => 0, 't2' => 0, 't3' => 0, 't4' => 0, 't5' => 0, 't6' => 0, 'tm' => 0, 'p1' => 0, 'p2' => 0, 'p1m' => 0, 'p2m' => 0];
+        $where = [];
+        if (!empty($param['shop'])) {
+            $where[] = ['shop_id','=',$param['shop']];
+        }
+        foreach ($this->mode->whereDay("create_at")->where($where)->whereIn("status",[1,2,3])->field('create_at,pay_type,status,sum(discount) as money,count(1) as total')->group('status,create_at,pay_type')->cursor() as $vo)
+        {
+            $total["t{$vo['status']}"] += $vo['total'];
+            $total['ta'] += $vo['total'];
+            $total['tm'] += $vo['money'];
+            if ($vo['pay_type'] == 1) {
+                $total["p1"] += $vo['total'];
+                $total["p1m"] += $vo['money'];
+            }
+            if ($vo['pay_type'] == 2) {
+                $total["p2"] += $vo['total'];
+                $total["p2m"] += $vo['money'];
+            }
+        }
+        return $total;
+    }
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['statusGt']) && $filter[] = ["status", '>', ($param['statusGt']-1)];
+        !empty($param['orderid']) && $filter[] = ["order_sn", 'like', "%{$param['orderid']}%"];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        return $filter;
+    }
+
+}

+ 34 - 0
app/extra/service/saas/PriceService.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasPrice;
+
+class PriceService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasPrice();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['name']) && $filter[] = ["name", 'like', "%{$param['name']}%"];
+        !empty($param['type']) && $filter[] = ["type", '=', $param['type']];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        return $filter;
+    }
+
+}

+ 35 - 0
app/extra/service/saas/PrintService.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasPrintClient;
+
+class PrintService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasPrintClient();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['name']) && $filter[] = ["name", 'like', "%{$param['name']}%"];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        return $filter;
+    }
+
+}

+ 50 - 0
app/extra/service/saas/QrcodeService.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasOrderQrcode;
+
+class QrcodeService extends Service
+{
+
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasOrderQrcode();
+        return $this->searchVal($param,$this->searchFilter($param))->with(['shop' => function($query){
+            $query->field("shop_id,shop_name");
+        }])->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+    public function getTotal(array $param = []): array
+    {
+        $this->mode = new SaasOrderQrcode();
+        $total = ['t0' => 0, 't1' => 0, 'ta' => 0];
+        foreach ($this->searchVal($param,[['status',">",0]])->field('create_at,status,count(1) total')->group('status,create_at')->cursor() as $vo)
+        {
+            [$total["t{$vo['status']}"] += $vo['total'], $total['ta'] += $vo['total']];
+        }
+        return $total;
+    }
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['statusGt']) && $filter[] = ["status", '>', ($param['statusGt']-1)];
+        !empty($param['orderid']) && $filter[] = ["order_sn", 'like', "%{$param['orderid']}%"];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        return $filter;
+    }
+
+
+
+}

+ 36 - 0
app/extra/service/saas/ShopLogService.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasShopLog;
+
+class ShopLogService extends Service
+{
+
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasShopLog();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['shop']) && $filter[] = ["shop_id", '=', $param['shop']];
+        !empty($param['type']) && $filter[] = ["type", '=', $param['type']];
+        return $filter;
+    }
+
+}

+ 38 - 0
app/extra/service/saas/ShopService.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace app\extra\service\saas;
+
+use app\extra\basic\Service;
+use app\model\saas\SaasAgent;
+use app\model\saas\SaasShop;
+
+class ShopService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SaasShop();
+        return $this->searchVal($param,$this->searchFilter($param))->where("is_deleted",0)->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['agent']) && $filter[] = ["agent_id", '=', $param['agent']];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['type']) && $filter[] = ["store_type", '=', $param['type']];
+        !empty($param['name']) && $filter[] = ["shop_name", 'like', "%{$param['name']}%"];
+        !empty($param['poi']) && $filter[] = ["poi_id", 'like', "%{$param['poi']}%"];
+        return $filter;
+    }
+
+}

+ 29 - 0
app/extra/service/system/MenuService.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace app\extra\service\system;
+
+use app\extra\basic\Service;
+use app\model\inmei\InmeiMenu;
+use app\model\system\SystemMenu;
+
+class MenuService extends Service
+{
+
+    /**
+     * @param int $super
+     * @param int $hide
+     * @param int $type
+     * @return array
+     */
+    public function getMenuList(int $super = 0,int $hide = 0,int $type = 1): array
+    {
+        $model = new SystemMenu();
+        try {
+            $data = $model->where("status",1)->where("from",$type)->order("sort","asc")->select();
+        } catch (\Throwable $throwable) {
+            return [];
+        }
+        return $data->isEmpty()?[]:$data->toArray();
+    }
+
+}

+ 35 - 0
app/extra/service/system/MoneyLogService.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace app\extra\service\system;
+
+use app\extra\basic\Service;
+use app\model\system\SystemUserMoney;
+
+class MoneyLogService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SystemUserMoney();
+        return $this->searchVal($param,$this->searchFilter($param))->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['shop']) && $filter[] = ["agent_id", '=', $param['shop']];
+        !empty($param['name']) && $filter[] = ["name", 'like', "%{$param['name']}%"];
+        return $filter;
+    }
+
+}

+ 183 - 0
app/extra/service/system/SystemService.php

@@ -0,0 +1,183 @@
+<?php
+
+namespace app\extra\service\system;
+
+use app\extra\basic\Service;
+use app\model\system\SystemConfig;
+use app\model\system\SystemData;
+use app\model\system\SystemOplog;
+use support\think\Cache;
+
+class SystemService extends Service
+{
+
+    /**
+     * 配置缓存数据
+     * @var array
+     */
+    private static array $config = [];
+
+
+    public static function get(string $name = '', string $default = '')
+    {
+        try {
+            $cacheConfig = Cache::get("SystemConfig");
+            if (empty($cacheConfig))
+            {
+                $config = (new SystemConfig)->select();
+                self::setConfig($config);
+                Cache::set("SystemConfig",json_encode($config));
+            } else {
+                $config = json_decode($cacheConfig,true);
+                self::setConfig($config);
+            }
+
+            [$type, $field, $outer] = static::_parse($name);
+            if (empty($name)) {
+                return static::$config;
+            } elseif (isset(static::$config[$type])) {
+                $group = static::$config[$type];
+                if ($outer !== 'raw') foreach ($group as $kk => $vo) {
+                    $group[$kk] = htmlspecialchars(strval($vo));
+                }
+                return $field ? ($group[$field] ?? $default) : $group;
+            } else {
+                return $default;
+            }
+
+        } catch (\Exception $exception)
+        {
+            return false;
+        }
+
+    }
+
+    protected static function setConfig($data)
+    {
+        foreach($data as $item)
+        {
+            static::$config[$item['type']][$item['name']] = $item['value'];
+        }
+    }
+
+    public static function set(string $name, $value = '')
+    {
+        static::$config = [];
+        [$type, $field] = static::_parse($name);
+        if (is_array($value)) {
+            $count = 0;
+            foreach ($value as $kk => $vv) {
+                $count += static::set("{$field}.{$kk}", $vv);
+            }
+            return $count;
+        } else try {
+            Cache::delete("SystemConfig");
+            $map = ['type' => $type, 'name' => $field];
+            $data = array_merge($map, ['value' => $value]);
+            $query = (new SystemConfig)->where($map);
+            return (clone $query)->count() > 0 ? $query->update($data) : $query->insert($data);
+        } catch (\Throwable $exception) {
+            return false;
+        }
+    }
+
+    /**
+     * 解析缓存名称
+     * @param string $rule 配置名称
+     * @return array
+     */
+    private static function _parse(string $rule): array
+    {
+        $type = 'base';
+        if (stripos($rule, '.') !== false) {
+            [$type, $rule] = explode('.', $rule, 2);
+        }
+        [$field, $outer] = explode('|', "{$rule}|");
+        return [$type, $field, strtolower($outer)];
+    }
+
+
+    /**
+     * 读取数据内容
+     * @param string $name
+     * @param array $default
+     * @return array|mixed
+     */
+    public static function getData(string $name, array $default = [])
+    {
+        try {
+            $value = (new SystemData)->where("name",$name)->value("content");
+            if (!empty($value)) return json_decode($value,true);
+            return $default;
+        } catch (\Throwable $exception)
+        {
+            return $default;
+        }
+    }
+
+    /**
+     * 保存数据内容
+     * @param string $name 数据名称
+     * @param mixed $value 数据内容
+     * @return bool
+     */
+    public static function setData(string $name, $value)
+    {
+        try {
+            $data = ['name' => $name, 'content' => json_encode($value, 64 | 256)];
+            return (new SystemData)->setAutoData($data,"name");
+        } catch (\Throwable $exception) {
+            echo $exception->getFile()."\n";
+            echo $exception->getLine()."\n";
+            echo $exception->getMessage()."\n";
+            return false;
+        }
+    }
+
+
+    /**
+     * 写入系统日志内容
+     * @param string $action
+     * @param string $content
+     * @param string $userName
+     * @return boolean
+     */
+    public static function setOplog(string $action, string $content, string $userName): bool
+    {
+        return (new SystemOplog)->insert(static::getOplog($action, $content,$userName)) !== false;
+    }
+
+    /**
+     * 获取系统日志内容
+     * @param string $action
+     * @param string $content
+     * @param string $userName
+     * @return array
+     */
+    public static function getUserOplog(string $action, string $content, string $userName): array
+    {
+        return [
+            'username'  => $userName,
+            'action'    => $action, 'content' => $content,
+            'geoip'     => request()->getRealIp() ?: '127.0.0.1',
+        ];
+    }
+
+    /**
+     * 获取系统日志内容
+     * @param string $action
+     * @param string $content
+     * @param string $userName
+     * @return array
+     */
+    public static function getOplog(string $action, string $content, string $userName): array
+    {
+        return [
+            'node'      => request()->uri(),
+            'action'    => $action, 'content' => $content,
+            'geoip'     => request()->getRealIp() ?: '127.0.0.1',
+            'username'  => $userName ?: '-'
+        ];
+    }
+
+}

+ 42 - 0
app/extra/service/system/UserService.php

@@ -0,0 +1,42 @@
+<?php
+
+namespace app\extra\service\system;
+
+use app\extra\basic\Service;
+use app\model\system\SystemUser;
+
+class UserService extends Service
+{
+
+    /**
+     * 列表
+     * @param array $param
+     */
+    public function getList(array $param = [])
+    {
+        $this->mode = new SystemUser();
+        return $this->searchVal($param,$this->searchFilter($param))->with(['account' => function ($query) {
+            $query->field("shop_id,shop_name");
+        }])->paginate([
+            "list_rows" => $param['pageSize'],
+            "page"      => $param['page']
+        ]);
+    }
+
+
+
+    protected function searchFilter(array $param = []): array
+    {
+        $filter = [];
+        !empty($param['agent']) && $filter[] = ["agent_id", '=', $param['agent']];
+        !empty($param['store']) && $filter[] = ["store_id", '=', $param['store']];
+        !empty($param['factory']) && $filter[] = ["factory_id", '=', $param['factory']];
+        !empty($param['status']) && $filter[] = ["status", '=', ($param['status']-1)];
+        !empty($param['mobile']) && $filter[] = ["mobile", 'like', "%{$param['mobile']}%"];
+        !empty($param['username']) && $filter[] = ["username", 'like', "%{$param['username']}%"];
+        !empty($param['name']) && $filter[] = ["username", 'like', "%{$param['name']}%"];
+        !empty($param['type']) && $filter[] = ["type", '=', $param['type']];
+        return $filter;
+    }
+
+}

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

@@ -0,0 +1,197 @@
+<?php
+
+namespace app\extra\tools;
+
+class CodeExtend
+{
+
+    /**
+     * 数组转XML内容
+     * @param array $data
+     * @return string
+     */
+    public static function arr2xml($data)
+    {
+        return "<xml>" . self::_arr2xml($data) . "</xml>";
+    }
+
+    /**
+     * 解析XML内容到数组
+     * @param string $xml
+     * @return array
+     */
+    public static function xml2arr($xml)
+    {
+        // 使用simplexml_load_string并禁用外部实体加载
+        $xmlObject = simplexml_load_string(
+            $xml,
+            'SimpleXMLElement',
+            LIBXML_NOCDATA | LIBXML_NOBLANKS | LIBXML_NOENT
+        );
+
+        if ($xmlObject === false) {
+            throw new \RuntimeException("Failed to parse XML");
+        }
+        return json_decode(json_encode($xmlObject), true);
+    }
+    /**
+     * XML内容生成
+     * @param array $data 数据
+     * @param string $content
+     * @return string
+     */
+    private static function _arr2xml($data, $content = '')
+    {
+        foreach ($data as $key => $val) {
+            is_numeric($key) && $key = 'item';
+            $content .= "<{$key}>";
+            if (is_array($val) || is_object($val)) {
+                $content .= self::_arr2xml($val);
+            } elseif (is_string($val)) {
+                $content .= '<![CDATA[' . preg_replace("/[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]/", '', $val) . ']]>';
+            } else {
+                $content .= $val;
+            }
+            $content .= "</{$key}>";
+        }
+        return $content;
+    }
+
+    public static function createNoncestr($length = 32, $str = "")
+    {
+        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
+        for ($i = 0; $i < $length; $i++) {
+            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
+        }
+        return $str;
+    }
+
+    /**
+     * 生成随机编码
+     * @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)));
+    }
+
+}

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

@@ -0,0 +1,73 @@
+<?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' => '亚太-首尔',
+        ];
+    }
+
+}

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

@@ -0,0 +1,122 @@
+<?php
+
+namespace app\extra\tools;
+
+use app\extra\service\basic\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('plugin.tinywan.storage.app.storage.' . $name, self::MODE_LOCAL);
+        }
+        return $config['storage']["default"];
+//        return config('plugin.tinywan.storage.app.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);
+    }
+
+}

+ 116 - 0
app/extra/weMini/BasicWeChat.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace app\extra\weMini;
+
+use think\Exception;
+use WeChat\Contracts\DataArray;
+use WeChat\Contracts\Tools;
+use yzh52521\EasyHttp\Http;
+
+class BasicWeChat
+{
+
+    /**
+     * 当前微信配置
+     * @var DataArray
+     */
+    public $config;
+
+    /**
+     * 访问AccessToken
+     * @var string
+     */
+    public $access_token = '';
+
+    /**
+     * 当前请求方法参数
+     * @var array
+     */
+    protected $currentMethod = [];
+
+    /**
+     * 当前模式
+     * @var bool
+     */
+    protected $isTry = false;
+
+    /**
+     * 静态缓存
+     * @var static
+     */
+    protected static $cache;
+
+    /**
+     * 注册代替函数
+     * @var string
+     */
+    protected $GetAccessTokenCallback;
+
+    public function __construct(array $options)
+    {
+        if (empty($options['appid'])) {
+            throw new Exception("Missing Config -- [appid]");
+        }
+        if (empty($options['appsecret'])) {
+            throw new Exception("Missing Config -- [appsecret]");
+        }
+        if (isset($options['GetAccessTokenCallback']) && is_callable($options['GetAccessTokenCallback'])) {
+            $this->GetAccessTokenCallback = $options['GetAccessTokenCallback'];
+        }
+        $this->config = new DataArray($options);
+    }
+
+    public function getAccessToken()
+    {
+        if (!empty($this->access_token)) {
+            return $this->access_token;
+        }
+        $cache = $this->config->get('appid') . '_access_token';
+        $this->access_token = Tools::getCache($cache);
+        if (!empty($this->access_token)) {
+            return $this->access_token;
+        }
+        // 处理开放平台授权公众号获取AccessToken
+        if (!empty($this->GetAccessTokenCallback) && is_callable($this->GetAccessTokenCallback)) {
+            $this->access_token = call_user_func_array($this->GetAccessTokenCallback, [$this->config->get('appid'), $this]);
+            if (!empty($this->access_token)) {
+                Tools::setCache($cache, $this->access_token, 7000);
+            }
+            return $this->access_token;
+        }
+        list($appid, $secret) = [$this->config->get('appid'), $this->config->get('appsecret')];
+        $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appid}&secret={$secret}";
+        $result = Http::get($url)->array();
+        if (!empty($result['access_token'])) {
+            Tools::setCache($cache, $result['access_token'], 7000);
+        }
+        return $this->access_token = $result['access_token'];
+    }
+
+    public function setAccessToken($accessToken)
+    {
+        if (!is_string($accessToken)) {
+            throw new InvalidArgumentException("Invalid AccessToken type, need string.");
+        }
+        $cache = $this->config->get('appid') . '_access_token';
+        Tools::setCache($cache, $this->access_token = $accessToken);
+    }
+
+    /**
+     * 清理删除 AccessToken
+     * @return bool
+     */
+    public function delAccessToken()
+    {
+        $this->access_token = '';
+        return Tools::delCache($this->config->get('appid') . '_access_token');
+    }
+
+    protected function registerApi(&$url, $method, $arguments = [])
+    {
+        $this->currentMethod = ['method' => $method, 'arguments' => $arguments];
+        if (empty($this->access_token)) $this->access_token = $this->getAccessToken();
+        return $url = str_replace('ACCESS_TOKEN', urlencode($this->access_token), $url);
+    }
+
+}

+ 198 - 0
app/extra/weMini/BasicWePay.php

@@ -0,0 +1,198 @@
+<?php
+
+namespace app\extra\weMini;
+
+use GuzzleHttp\Client;
+use WeChat\Contracts\DataArray;
+use WeChat\Contracts\Tools;
+use WeChat\Exceptions\InvalidArgumentException;
+use WeChat\Exceptions\InvalidResponseException;
+use yzh52521\EasyHttp\Http;
+
+class BasicWePay
+{
+
+
+    /**
+     * 商户配置
+     * @var DataArray
+     */
+    protected $config;
+
+    /**
+     * 当前请求数据
+     * @var DataArray
+     */
+    protected $params;
+
+    /**
+     * 静态缓存
+     * @var static
+     */
+    protected static $cache;
+
+    /**
+     * WeChat constructor.
+     * @param array $options
+     */
+    public function __construct(array $options)
+    {
+        if (empty($options['appid'])) {
+            throw new InvalidArgumentException("Missing Config -- [appid]");
+        }
+        if (empty($options['mch_id'])) {
+            throw new InvalidArgumentException("Missing Config -- [mch_id]");
+        }
+        if (empty($options['mch_key'])) {
+            throw new InvalidArgumentException("Missing Config -- [mch_key]");
+        }
+        if (!empty($options['cache_path'])) {
+            Tools::$cache_path = $options['cache_path'];
+        }
+        $this->config = new DataArray($options);
+        // 商户基础参数
+        $this->params = new DataArray([
+            'appid'     => $this->config->get('appid'),
+            'mch_id'    => $this->config->get('mch_id'),
+            'nonce_str' => Tools::createNoncestr(),
+        ]);
+        // 商户参数支持
+        if ($this->config->get('sub_appid')) {
+            $this->params->set('sub_appid', $this->config->get('sub_appid'));
+        }
+        if ($this->config->get('sub_mch_id')) {
+            $this->params->set('sub_mch_id', $this->config->get('sub_mch_id'));
+        }
+    }
+
+    /**
+     * 静态创建对象
+     * @param array $config
+     * @return static
+     */
+    public static function instance(array $config)
+    {
+        $key = md5(get_called_class() . serialize($config));
+        print_r($key);
+        if (isset(self::$cache[$key])) return self::$cache[$key];
+        return self::$cache[$key] = new static($config);
+    }
+
+    /**
+     * 获取微信支付通知
+     * @param string|array $xml
+     * @return array
+     * @throws \WeChat\Exceptions\InvalidResponseException
+     */
+    public function getNotify($xml = '')
+    {
+        $data = is_array($xml) ? $xml : Tools::xml2arr(empty($xml) ? Tools::getRawInput() : $xml);
+        if (isset($data['sign']) && $this->getPaySign($data) === $data['sign']) {
+            return $data;
+        }
+        throw new InvalidResponseException('Invalid Notify.', '0');
+    }
+
+    /**
+     * 获取微信支付通知回复内容
+     * @return string
+     */
+    public function getNotifySuccessReply()
+    {
+        return Tools::arr2xml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']);
+    }
+
+    /**
+     * 生成支付签名
+     * @param array $data 参与签名的数据
+     * @param string $signType 参与签名的类型
+     * @param string $buff 参与签名字符串前缀
+     * @return string
+     */
+    public function getPaySign(array $data, $signType = 'MD5', $buff = '')
+    {
+        ksort($data);
+        if (isset($data['sign'])) unset($data['sign']);
+        foreach ($data as $k => $v) {
+            if ('' === $v || null === $v) continue;
+            $buff .= "{$k}={$v}&";
+        }
+        $buff .= ("key=" . $this->config->get('mch_key'));
+        if (strtoupper($signType) === 'MD5') {
+            return strtoupper(md5($buff));
+        }
+        return strtoupper(hash_hmac('SHA256', $buff, $this->config->get('mch_key')));
+    }
+
+    /**
+     * 转换短链接
+     * @param string $longUrl 需要转换的URL,签名用原串,传输需URLencode
+     * @return array
+     */
+    public function shortUrl($longUrl)
+    {
+        $url = 'https://api.mch.weixin.qq.com/tools/shorturl';
+        return $this->callPostApi($url, ['long_url' => $longUrl]);
+    }
+
+    /**
+     * 数组直接转xml数据输出
+     * @param array $data
+     * @param bool $isReturn
+     * @return string|void
+     */
+    public function toXml(array $data, $isReturn = false)
+    {
+        $xml = Tools::arr2xml($data);
+        if ($isReturn) return $xml;
+        echo $xml;
+    }
+
+    /**
+     * 以 Post 请求接口
+     * @param string $url 请求
+     * @param array $data 接口参数
+     * @param bool $isCert 是否需要使用双向证书
+     * @param string $signType 数据签名类型 MD5|SHA256
+     * @param bool $needSignType 是否需要传签名类型参数
+     * @param bool $needNonceStr
+     * @return array
+     */
+    protected function callPostApi($url, array $data, $isCert = false, $signType = 'HMAC-SHA256', $needSignType = true, $needNonceStr = true)
+    {
+        $option = [];
+        if ($isCert) {
+            $option['ssl_p12'] = $this->config->get('ssl_p12');
+            $option['ssl_cer'] = $this->config->get('ssl_cer');
+            $option['ssl_key'] = $this->config->get('ssl_key');
+            if (is_string($option['ssl_p12']) && file_exists($option['ssl_p12'])) {
+                $content = file_get_contents($option['ssl_p12']);
+                if (openssl_pkcs12_read($content, $certs, $this->config->get('mch_id'))) {
+//                    $option['ssl_key'] = Tools::pushFile(md5($certs['pkey']) . '.pem', $certs['pkey']);
+//                    $option['ssl_cer'] = Tools::pushFile(md5($certs['cert']) . '.pem', $certs['cert']);
+                } else return ['code' => 0 ,"msg" => "P12 certificate does not match MCH_ID --- ssl_p12"];
+            }
+            if (empty($option['ssl_cer']) || !file_exists($option['ssl_cer'])) {
+                return ['code' => 0 ,"msg" => "Missing Config -- ssl_cer"];
+            }
+            if (empty($option['ssl_key']) || !file_exists($option['ssl_key'])) {
+                return ['code' => 0 ,"msg" => "Missing Config -- ssl_key"];
+            }
+        }
+        $params = $this->params->merge($data);
+        if (!$needNonceStr) unset($params['nonce_str']);
+        if ($needSignType) $params['sign_type'] = strtoupper($signType);
+        $params['sign'] = $this->getPaySign($params, $signType);
+        $client = new Client();
+        $resp = $client->post($url,[
+            "header"    => $client,
+            'body' => Tools::arr2xml($params)
+        ]);
+        $result = Tools::xml2arr($resp->getBody());
+        return $result;
+//        if ($result['return_code'] !== 'SUCCESS') {
+//            return $result;
+//        }
+    }
+
+}

+ 69 - 0
app/extra/weMini/Crypt.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace app\extra\weMini;
+
+use app\extra\weMini\crypt\wxBizDataCrypt;
+use WeChat\Exceptions\InvalidDecryptException;
+use WeChat\Exceptions\InvalidResponseException;
+use yzh52521\EasyHttp\Http;
+
+class Crypt extends BasicWeChat
+{
+    /**
+     * 数据签名校验
+     * @param string $iv
+     * @param string $sessionKey
+     * @param string $encryptedData
+     * @return bool|array
+     */
+    public function decode($iv, $sessionKey, $encryptedData)
+    {
+        $pc = new WXBizDataCrypt($this->config->get('appid'), $sessionKey);
+        $errCode = $pc->decryptData($encryptedData, $iv, $data);
+        if ($errCode == 0) {
+            return json_decode($data, true);
+        }
+        return false;
+    }
+
+
+    /**
+     * 登录凭证校验
+     * @param string $code 登录时获取的 code
+     * @return array
+     * @throws \WeChat\Exceptions\LocalCacheException
+     */
+    public function session($code)
+    {
+        $appid = $this->config->get('appid');
+        $secret = $this->config->get('appsecret');
+        $url = "https://api.weixin.qq.com/sns/jscode2session?appid={$appid}&secret={$secret}&js_code={$code}&grant_type=authorization_code";
+        return Http::get($url)->array();
+    }
+
+    /**
+     * 换取用户信息
+     * @param string $code 用户登录凭证(有效期五分钟)
+     * @param string $iv 加密算法的初始向量
+     * @param string $encryptedData 加密数据( encryptedData )
+     * @return array
+     * @throws \WeChat\Exceptions\InvalidDecryptException
+     * @throws \WeChat\Exceptions\InvalidResponseException
+     * @throws \WeChat\Exceptions\LocalCacheException
+     */
+    public function userInfo($code, $iv, $encryptedData)
+    {
+        $result = $this->session($code);
+        if (empty($result['session_key'])) {
+            throw new InvalidResponseException('Code 换取 SessionKey 失败', 403);
+        }
+        $userinfo = $this->decode($iv, $result['session_key'], $encryptedData);
+        if (empty($userinfo)) {
+            throw new InvalidDecryptException('用户信息解析失败', 403);
+        }
+        return array_merge($result, $userinfo);
+    }
+
+
+
+}

+ 145 - 0
app/extra/weMini/Link.php

@@ -0,0 +1,145 @@
+<?php
+
+namespace app\extra\weMini;
+
+use yzh52521\EasyHttp\Http;
+
+class Link extends BasicWeChat
+{
+
+
+
+    public function createQrcodeWx(string $path = "",string $query = "", string $filePath = "")
+    {
+        $url = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token={$this->getAccessToken()}";
+        $data = [
+            "path"  => $path."?".$query,
+            'width' => 430
+        ];
+
+        // 初始化cURL
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_POST, true);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_HEADER, false);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+
+        // 执行请求
+        $response = curl_exec($ch);
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+
+        // 检查请求是否成功
+        if ($httpCode != 200) {
+            return false;
+        }
+
+        // 检查返回的是否是JSON(错误情况)
+        $decode = json_decode($response, true);
+        if ($decode && isset($decode['errcode'])) {
+            error_log("微信接口返回错误: " . $decode['errmsg']);
+            return false;
+        }
+
+        // 保存为JPG文件
+        $filePath = public_path().$filePath;
+        $result = file_put_contents($filePath, $response);
+
+        if ($result === false) {
+            error_log("文件保存失败: " . $filePath);
+            return false;
+        }
+        return $filePath;
+//        echo $url;
+//        print_r($param);
+//        return Http::asJson()->post($url,$param);
+    }
+
+    public function createQrcode(string $path = "",string $query = "", string $filePath = "")
+    {
+        $url = "https://api.weixin.qq.com/wxa/getwxacode?access_token={$this->getAccessToken()}";
+        $data = [
+            "path"  => $path."?".$query,
+            'width' => 430
+        ];
+
+        // 初始化cURL
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_POST, true);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($ch, CURLOPT_HEADER, false);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+
+        // 执行请求
+        $response = curl_exec($ch);
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+
+        // 检查请求是否成功
+        if ($httpCode != 200) {
+            return false;
+        }
+
+        // 检查返回的是否是JSON(错误情况)
+        $decode = json_decode($response, true);
+        if ($decode && isset($decode['errcode'])) {
+            error_log("微信接口返回错误: " . $decode['errmsg']);
+            return false;
+        }
+
+        // 保存为JPG文件
+        $filePath = public_path().$filePath;
+        $result = file_put_contents($filePath, $response);
+
+        if ($result === false) {
+            error_log("文件保存失败: " . $filePath);
+            return false;
+        }
+        return $filePath;
+//        echo $url;
+//        print_r($param);
+//        return Http::asJson()->post($url,$param);
+    }
+
+
+    public function createLink(string $path = "",string $query = "",string $version = "release")
+    {
+        $url = "https://api.weixin.qq.com/wxa/generate_urllink?access_token={$this->getAccessToken()}";
+        $param = [
+            "path"          => $path,
+            "query"         => $query,
+            "env_version"   => $version
+        ];
+        return Http::asJson()->post($url,$param)->array();
+    }
+
+
+    public function createLinkJump(string $path = "",string $query = "",string $version = "release")
+    {
+        $url = "https://api.weixin.qq.com/wxa/generatescheme?access_token={$this->getAccessToken()}";
+        $param = [
+            "jump_wxa"  => [
+                "path"          => $path,
+                "query"         => $query,
+                "env_version"   => $version
+            ],
+        ];
+        print_r($param);
+        return Http::asJson()->post($url,$param)->array();
+    }
+
+
+    public function createShortLink(string $path = "",string $query = "",string $version = "release")
+    {
+        $url = "https://api.weixin.qq.com/wxa/genwxashortlink?access_token={$this->getAccessToken()}";
+        $param = [
+            "page_url"   => $path."?".$query,
+        ];
+        return Http::asJson()->post($url,$param)->array();
+    }
+
+}

+ 143 - 0
app/extra/weMini/Order.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace app\extra\weMini;
+
+use WeChat\Contracts\Tools;
+
+class Order extends BasicWePay
+{
+
+
+    /**
+     * 统一下单
+     * @param array $options
+     * @return array
+     */
+    public function create(array $options)
+    {
+        $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
+        return $this->callPostApi($url, $options, false, 'MD5');
+    }
+
+    /**
+     * 刷卡支付
+     * @param array $options
+     * @return array
+     */
+    public function micropay(array $options)
+    {
+        $url = 'https://api.mch.weixin.qq.com/pay/micropay';
+        return $this->callPostApi($url, $options, false, 'MD5');
+    }
+
+    /**
+     * 查询订单
+     * @param array $options
+     * @return array
+     */
+    public function query(array $options)
+    {
+        $url = 'https://api.mch.weixin.qq.com/pay/orderquery';
+        return $this->callPostApi($url, $options);
+    }
+
+    /**
+     * 关闭订单
+     * @param string $outTradeNo 商户订单号
+     * @return array
+     */
+    public function close($outTradeNo)
+    {
+        $url = 'https://api.mch.weixin.qq.com/pay/closeorder';
+        return $this->callPostApi($url, ['out_trade_no' => $outTradeNo]);
+    }
+
+    /**
+     * 创建JsApi及H5支付参数
+     * @param string $prepayId 统一下单预支付码
+     * @return array
+     */
+    public function jsapiParams($prepayId)
+    {
+        $option = [];
+        $option["appId"] = $this->config->get('appid');
+        $option["timeStamp"] = (string)time();
+        $option["nonceStr"] = Tools::createNoncestr();
+        $option["package"] = "prepay_id={$prepayId}";
+        $option["signType"] = "MD5";
+        $option["paySign"] = $this->getPaySign($option, 'MD5');
+        $option['timestamp'] = $option['timeStamp'];
+        return $option;
+    }
+
+    /**
+     * 获取支付规则二维码
+     * @param string $productId 商户定义的商品id或者订单号
+     * @return string
+     */
+    public function qrcParams($productId)
+    {
+        $data = [
+            'appid'      => $this->config->get('appid'),
+            'mch_id'     => $this->config->get('mch_id'),
+            'time_stamp' => (string)time(),
+            'nonce_str'  => Tools::createNoncestr(),
+            'product_id' => (string)$productId,
+        ];
+        $data['sign'] = $this->getPaySign($data, 'MD5');
+        return "weixin://wxpay/bizpayurl?" . http_build_query($data);
+    }
+
+    /**
+     * 获取微信App支付秘需参数
+     * @param string $prepayId 统一下单预支付码
+     * @return array
+     */
+    public function appParams($prepayId)
+    {
+        $data = [
+            'appid'     => $this->config->get('appid'),
+            'partnerid' => $this->config->get('mch_id'),
+            'prepayid'  => (string)$prepayId,
+            'package'   => 'Sign=WXPay',
+            'timestamp' => (string)time(),
+            'noncestr'  => Tools::createNoncestr(),
+        ];
+        $data['sign'] = $this->getPaySign($data, 'MD5');
+        return $data;
+    }
+
+    /**
+     * 刷卡支付 撤销订单
+     * @param array $options
+     * @return array
+     */
+    public function reverse(array $options)
+    {
+        $url = 'https://api.mch.weixin.qq.com/secapi/pay/reverse';
+        return $this->callPostApi($url, $options, true);
+    }
+
+    /**
+     * 刷卡支付 授权码查询openid
+     * @param string $authCode 扫码支付授权码,设备读取用户微信中的条码或者二维码信息
+     * @return array
+     */
+    public function queryAuthCode($authCode)
+    {
+        $url = 'https://api.mch.weixin.qq.com/tools/authcodetoopenid';
+        return $this->callPostApi($url, ['auth_code' => $authCode], false, 'MD5', false);
+    }
+
+    /**
+     * 刷卡支付 交易保障
+     * @param array $options
+     * @return array
+     */
+    public function report(array $options)
+    {
+        $url = 'https://api.mch.weixin.qq.com/payitil/report';
+        return $this->callPostApi($url, $options);
+    }
+
+}

+ 188 - 0
app/extra/weMini/Pay.php

@@ -0,0 +1,188 @@
+<?php
+
+namespace app\extra\weMini;
+
+
+use WePay\Bill;
+use WePay\Refund;
+use WePay\Transfers;
+use WePay\TransfersBank;
+
+class Pay extends BasicWePay
+{
+
+
+    /**
+     * 统一下单
+     * @param array $options
+     * @return array
+     */
+    public function createOrder(array $options)
+    {
+        $resp = (new Order($this->config->get()))->create($options);
+        print_r($resp);
+        return $resp;
+    }
+
+    /**
+     * 刷卡支付
+     * @param array $options
+     * @return array
+     */
+    public function createMicropay($options)
+    {
+        return Order::instance($this->config->get())->micropay($options);
+    }
+
+    /**
+     * 创建JsApi及H5支付参数
+     * @param string $prepay_id 统一下单预支付码
+     * @return array
+     */
+    public function createParamsForJsApi($prepay_id)
+    {
+        return Order::instance($this->config->get())->jsapiParams($prepay_id);
+    }
+
+    /**
+     * 获取APP支付参数
+     * @param string $prepay_id 统一下单预支付码
+     * @return array
+     */
+    public function createParamsForApp($prepay_id)
+    {
+        return Order::instance($this->config->get())->appParams($prepay_id);
+    }
+
+    /**
+     * 获取支付规则二维码
+     * @param string $product_id 商户定义的商品id 或者订单号
+     * @return string
+     */
+    public function createParamsForRuleQrc($product_id)
+    {
+        return Order::instance($this->config->get())->qrcParams($product_id);
+    }
+
+    /**
+     * 查询订单
+     * @param array $options
+     * @return array
+     */
+    public function queryOrder(array $options)
+    {
+        return Order::instance($this->config->get())->query($options);
+    }
+
+    /**
+     * 关闭订单
+     * @param string $out_trade_no 商户订单号
+     * @return array
+     */
+    public function closeOrder($out_trade_no)
+    {
+        return Order::instance($this->config->get())->close($out_trade_no);
+    }
+
+    /**
+     * 申请退款
+     * @param array $options
+     * @return array
+     */
+    public function createRefund(array $options)
+    {
+        return Refund::instance($this->config->get())->create($options);
+    }
+
+    /**
+     * 查询退款
+     * @param array $options
+     * @return array
+     */
+    public function queryRefund(array $options)
+    {
+        return Refund::instance($this->config->get())->query($options);
+    }
+
+    /**
+     * 交易保障
+     * @param array $options
+     * @return array
+     */
+    public function report(array $options)
+    {
+        return Order::instance($this->config->get())->report($options);
+    }
+
+    /**
+     * 授权码查询openid
+     * @param string $authCode 扫码支付授权码,设备读取用户微信中的条码或者二维码信息
+     * @return array
+     */
+    public function queryAuthCode($authCode)
+    {
+        return Order::instance($this->config->get())->queryAuthCode($authCode);
+    }
+
+    /**
+     * 下载对账单
+     * @param array $options 静音参数
+     * @param null|string $outType 输出类型
+     * @return bool|string
+     */
+    public function billDownload(array $options, $outType = null)
+    {
+        return Bill::instance($this->config->get())->download($options, $outType);
+    }
+
+    /**
+     * 拉取订单评价数据
+     * @param array $options
+     * @return array
+     */
+    public function billCommtent(array $options)
+    {
+        return Bill::instance($this->config->get())->comment($options);
+    }
+
+    /**
+     * 企业付款到零钱
+     * @param array $options
+     * @return array
+     */
+    public function createTransfers(array $options)
+    {
+        return Transfers::instance($this->config->get())->create($options);
+    }
+
+    /**
+     * 查询企业付款到零钱
+     * @param string $partner_trade_no 商户调用企业付款API时使用的商户订单号
+     * @return array
+     */
+    public function queryTransfers($partner_trade_no)
+    {
+        return Transfers::instance($this->config->get())->query($partner_trade_no);
+    }
+
+    /**
+     * 企业付款到银行卡
+     * @param array $options
+     * @return array
+     */
+    public function createTransfersBank(array $options)
+    {
+        return TransfersBank::instance($this->config->get())->create($options);
+    }
+
+    /**
+     * 商户企业付款到银行卡操作进行结果查询
+     * @param string $partner_trade_no 商户订单号,需保持唯一
+     * @return array
+     */
+    public function queryTransFresBank($partner_trade_no)
+    {
+        return TransfersBank::instance($this->config->get())->query($partner_trade_no);
+    }
+
+}

+ 88 - 0
app/extra/weMini/PayExtra.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace app\extra\weMini;
+
+use app\extra\tools\CodeExtend;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+use WeChat\Contracts\DataArray;
+use WeChat\Contracts\Tools;
+use WeChat\Exceptions\InvalidArgumentException;
+
+class PayExtra
+{
+
+
+    /**
+     * 商户配置
+     */
+    protected $config = [];
+
+
+    public function setConfig(array $options)
+    {
+        $options['nonce_str'] = CodeExtend::createNoncestr();
+        $this->config = $options;
+        return $this;
+    }
+
+    public function createOrder(array $options): array
+    {
+        $options = array_merge($this->config,$options);
+        $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
+        $options['sign_type'] = "MD5";
+        $options['sign'] = $this->getPaySign($options);
+        $client = new Client();
+        try {
+            $resp = $client->post($url, [
+                "header" => $client,
+                'body' => CodeExtend::arr2xml($options)
+            ]);
+        } catch (GuzzleException $e) {
+            return [];
+        }
+        return CodeExtend::xml2arr($resp->getBody());
+    }
+
+    /**
+     * 创建JsApi及H5支付参数
+     * @param string $prepayId 统一下单预支付码
+     * @return array
+     */
+    public function createParamsForJsApi($prepayId)
+    {
+        $option = [];
+        $option["appId"] = $this->config['appid'];
+        $option["timeStamp"] = (string)time();
+        $option["nonceStr"] = CodeExtend::createNoncestr();
+        $option["package"] = "prepay_id={$prepayId}";
+        $option["signType"] = "MD5";
+        $option["paySign"] = $this->getPaySign($option, 'MD5');
+        $option['timestamp'] = $option['timeStamp'];
+        return $option;
+    }
+
+    /**
+     * 生成支付签名
+     * @param array $data 参与签名的数据
+     * @param string $signType 参与签名的类型
+     * @param string $buff 参与签名字符串前缀
+     * @return string
+     */
+    public function getPaySign(array $data, $signType = 'MD5', $buff = '')
+    {
+        ksort($data);
+        if (isset($data['sign'])) unset($data['sign']);
+        foreach ($data as $k => $v) {
+            if ('' === $v || null === $v) continue;
+            $buff .= "{$k}={$v}&";
+        }
+        $buff .= ("key=" . $this->config['mch_key']);
+        if (strtoupper($signType) === 'MD5') {
+            return strtoupper(md5($buff));
+        }
+        return strtoupper(hash_hmac('SHA256', $buff, $this->config['mch_key']));
+    }
+
+
+}

+ 24 - 0
app/extra/weMini/Subscribe.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace app\extra\weMini;
+
+use yzh52521\EasyHttp\Http;
+
+class Subscribe extends BasicWeChat
+{
+
+    public function sendMsg(string $id = "",string $openid = "",array $data = [])
+    {
+        $url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={$this->getAccessToken()}";
+        $param = [
+            "template_id"   => $id,
+            "touser"        => $openid,
+            "data"          => $data,
+            "page"          => "pages/order/order",
+            "miniprogram_state" => "formal",
+            "lang"          => "zh_CN"
+        ];
+        return Http::asJson()->post($url,$param)->array();
+    }
+
+}

+ 21 - 0
app/extra/weMini/crypt/errorCode.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace app\extra\weMini\crypt;
+/**
+ * error code 说明.
+ * <ul>
+ *    <li>-41001: encodingAesKey 非法</li>
+ *    <li>-41003: aes 解密失败</li>
+ *    <li>-41004: 解密后得到的buffer非法</li>
+ *    <li>-41005: base64加密失败</li>
+ *    <li>-41016: base64解密失败</li>
+ * </ul>
+ */
+class errorCode
+{
+    public static $OK = 0;
+    public static $IllegalAesKey = -41001;
+    public static $IllegalIv = -41002;
+    public static $IllegalBuffer = -41003;
+    public static $DecodeBase64Error = -41004;
+}

+ 59 - 0
app/extra/weMini/crypt/wxBizDataCrypt.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace app\extra\weMini\crypt;
+/**
+ * 对微信小程序用户加密数据的解密示例代码
+ * Class WXBizDataCrypt
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+class wxBizDataCrypt
+{
+    private $appid;
+    private $sessionKey;
+
+    /**
+     * 构造函数
+     * @param $sessionKey string 用户在小程序登录后获取的会话密钥
+     * @param $appid string 小程序的appid
+     */
+    public function __construct($appid, $sessionKey)
+    {
+        $this->appid = $appid;
+        $this->sessionKey = $sessionKey;
+//        include_once __DIR__ . DIRECTORY_SEPARATOR . "errorCode.php";
+    }
+
+    /**
+     * 检验数据的真实性,并且获取解密后的明文.
+     * @param $encryptedData string 加密的用户数据
+     * @param $iv string 与用户数据一同返回的初始向量
+     * @param $data string 解密后的原文
+     *
+     * @return int 成功0,失败返回对应的错误码
+     */
+    public function decryptData($encryptedData, $iv, &$data)
+    {
+        if (strlen($this->sessionKey) != 24) {
+            return errorCode::$IllegalAesKey;
+        }
+        $aesKey = base64_decode($this->sessionKey);
+        if (strlen($iv) != 24) {
+            return errorCode::$IllegalIv;
+        }
+        $aesIV = base64_decode($iv);
+        $aesCipher = base64_decode($encryptedData);
+        $result = openssl_decrypt($aesCipher, "AES-128-CBC", $aesKey, 1, $aesIV);
+        $dataObj = json_decode($result);
+        if ($dataObj == null) {
+            return errorCode::$IllegalBuffer;
+        }
+        // 兼容新版本无 watermark 的情况
+        if (isset($dataObj->watermark) && $dataObj->watermark->appid != $this->appid) {
+            return errorCode::$IllegalBuffer;
+        }
+        $data = $result;
+        return errorCode::$OK;
+    }
+
+}
+

+ 161 - 0
app/extra/wechat/WechatService.php

@@ -0,0 +1,161 @@
+<?php
+
+namespace app\extra\wechat;
+
+/**
+ * 微信接口调度服务
+ * @package app\extra\wechat
+ * @method \WeChat\Oauth WeChatOauth() static 微信网页授权
+ */
+class WechatService
+{
+
+
+    /**
+     * 静态初始化对象
+     * @param string $name
+     * @param array $arguments
+     * @return mixed
+     * @throws \think\admin\Exception
+     */
+    public static function __callStatic(string $name, array $arguments)
+    {
+        [$type, $base, $class] = static::parseName($name);
+        if ("{$type}{$base}" !== $name) {
+            throw new \Exception("抱歉,实例 {$name} 不符合规则!");
+        }
+        if (class_exists($class)) {
+            return new $class(static::getConfig());
+        } else {
+            throw new \Exception("抱歉,接口模式无法实例 {$class} 对象!");
+        }
+    }
+
+    /**
+     * 解析调用对象名称
+     * @param string $name
+     * @return array
+     */
+    private static function parseName(string $name): array
+    {
+        foreach (['WeChat', 'WeMini', 'WeOpen', 'WePayV3', 'WePay', 'ThinkService'] as $type) {
+            if (strpos($name, $type) === 0) {
+                [, $base] = explode($type, $name);
+                return [$type, $base, "\\{$type}\\{$base}"];
+            }
+        }
+        return ['-', '-', $name];
+    }
+
+    /**
+     * 获取当前微信APPID
+     * @return string
+     */
+    public static function getAppid(): string
+    {
+        return sConf('wechat.appid');
+    }
+
+    /**
+     * 获取接口授权模式
+     * @return string
+     */
+    public static function getType(): string
+    {
+        $type = strtolower(sConf('wechat.type'));
+        if (in_array($type, ['api', 'thr'])) return $type;
+        throw new \Exception('请在后台配置微信对接授权模式');
+    }
+    /**
+     * 获取公众号配置参数
+     * @return array
+     */
+    public static function getConfig(): array
+    {
+        return [
+            'appid'          => static::getAppid(),
+            'token'          => sConf('wechat.token'),
+            'appsecret'      => sConf('wechat.secret'),
+            'encodingaeskey' => sConf('wechat.aeskey'),
+            'mch_id'         => sConf('wechat.mch_id'),
+            'mch_key'        => sConf('wechat.mch_key')
+        ];
+    }
+
+    /**
+     * 通过网页授权获取粉丝信息
+     * @param string $source 回跳URL地址
+     * @param integer $isfull 获取资料模式
+     * @param boolean $redirect 是否直接跳转
+     * @return array
+     */
+    public static function getWebOauthInfo(string $source, int $isfull = 0, bool $redirect = true): array
+    {
+        try {
+            $appid = static::getAppid();
+            $openid = request()->session()->get("{$appid}_openid");
+            $userinfo = request()->session()->get("{$appid}_fansinfo");
+            if ((empty($isfull) && !empty($openid)) || (!empty($isfull) && !empty($openid) && !empty($userinfo))) {
+                return ['openid' => $openid,"url" => "", 'fansinfo' => $userinfo];
+            }
+            // 解析 GET 参数
+            $queryString = parse_url($source, PHP_URL_QUERY);
+            // 初始化参数数组
+            $params = [];
+            // 只有当有查询字符串时才解析
+            if ($queryString !== null && $queryString !== '') {
+                parse_str($queryString, $params);
+            }
+//        parse_str(parse_url($source, PHP_URL_QUERY), $params);
+            $getVars = [
+                'code'  => $params['code'] ?? input('code', ''),
+                'rcode' => $params['rcode'] ?? input('rcode', ''),
+                'state' => $params['state'] ?? input('state', ''),
+            ];
+            $wechat = static::WeChatOauth();
+            if ($getVars['state'] !== $appid || empty($getVars['code'])) {
+                $params['rcode'] = enbase64url($source);
+                $location = strstr("{$source}?", '?', true) . '?' . http_build_query($params);
+                $oauthurl = $wechat->getOauthRedirect($location, $appid, $isfull ? 'snsapi_userinfo' : 'snsapi_base');
+                return ['openid' => "","url" => $oauthurl ,"fansinfo" => ""];
+            } elseif (($token = $wechat->getOauthAccessToken($getVars['code'])) && isset($token['openid'])) {
+                request()->session()->set("{$appid}_openid", $openid = $token['openid']);
+                if ($isfull && isset($token['access_token'])) {
+                    $userinfo = $wechat->getUserInfo($token['access_token'], $openid);
+                    request()->session()->set("{$appid}_fansinfo", $userinfo);
+                }
+                return ['openid' => $openid, 'url' => '', 'fansinfo' => ""];
+            }
+            if ($getVars['rcode']) {
+                $location = debase64url($getVars['rcode']);
+                return ['openid' => "","url" => $location ,"fansinfo" => ""];
+            } elseif ((empty($isfull) && !empty($openid)) || (!empty($isfull) && !empty($openid) && !empty($userinfo))) {
+                return ['openid' => $openid, 'url' => '', 'fansinfo' => $userinfo];
+            } else {
+                throw new \Exception('Query params [rcode] not find.');
+            }
+        } catch (\Throwable $th) {
+            echo $th->getMessage();
+        }
+    }
+//
+//    /**
+//     * 获取微信网页JSSDK签名参数
+//     * @param null|string $location 签名地址
+//     * @return array
+//     * @throws \WeChat\Exceptions\InvalidResponseException
+//     * @throws \WeChat\Exceptions\LocalCacheException
+//     * @throws \think\admin\Exception
+//     */
+//    public static function getWebJssdkSign(?string $location = null): array
+//    {
+//        $location = $location ?: Library::$sapp->request->url(true);
+//        if (static::getType() === 'api') {
+//            return static::WeChatScript()->getJsSign($location);
+//        } else {
+//            return static::ThinkServiceConfig()->jsSign($location);
+//        }
+//    }
+
+
+}

+ 601 - 0
app/functions.php

@@ -0,0 +1,601 @@
+<?php
+
+
+use app\extra\service\system\SystemService;
+use app\extra\tools\CodeExtend;
+use support\Response;
+use Webman\Event\Event;
+
+
+if (!function_exists('msubstr')) {
+    /**
+     * 字符串截取(同时去掉HTML与空白)
+     * @param string $str
+     * @param int $start
+     * @param int $length
+     * @param string $charset
+     * @param bool $suffix
+     * @return string
+     */
+    function msubstr(string $str, int $start = 0, int $length = 100,string $charset = "utf-8", bool $suffix = true): string
+    {
+
+        $str = preg_replace('/<[^>]+>/', '', preg_replace("/[\r\n\t ]{1,}/", ' ', delNt(strip_tags($str))));
+        $str = preg_replace('/&(\w{4});/i', '', $str);
+
+        // 直接返回
+        if ($start == -1) {
+            return $str;
+        }
+
+        if (function_exists("mb_substr")) {
+            $slice = mb_substr($str, $start, $length, $charset);
+        } elseif (function_exists('iconv_substr')) {
+            $slice = iconv_substr($str, $start, $length, $charset);
+
+        } else {
+            $re['utf-8'] = "/[x01-x7f]|[xc2-xdf][x80-xbf]|[xe0-xef][x80-xbf]{2}|[xf0-xff][x80-xbf]{3}/";
+            $re['gb2312'] = "/[x01-x7f]|[xb0-xf7][xa0-xfe]/";
+            $re['gbk'] = "/[x01-x7f]|[x81-xfe][x40-xfe]/";
+            $re['big5'] = "/[x01-x7f]|[x81-xfe]([x40-x7e]|xa1-xfe])/";
+            preg_match_all($re[$charset], $str, $match);
+            $slice = join("", array_slice($match[0], $start, $length));
+        }
+
+        $fix = '';
+        if (strlen($slice) < strlen($str)) {
+            $fix = '...';
+        }
+        return $suffix ? $slice . $fix : $slice;
+    }
+}
+
+if (!function_exists('delNt')) {
+    /**
+     * 去掉连续空白
+     * @param string $str 字符串
+     * @return string
+     */
+    function delNt(string $str): string
+    {
+        $str = trim($str); //清除字符串两边的空格
+        $str = strip_tags($str,""); //利用php自带的函数清除html格式
+        $str = preg_replace("/\t/","",$str); //使用正则表达式替换内容,如:空格,换行,并将替换为空。
+        $str = preg_replace("/\r\n/","",$str);
+        $str = preg_replace("/\r/","",$str);
+        $str = preg_replace("/\n/","",$str);
+        $str = preg_replace("/ /","",$str);
+        $str = preg_replace("/  /","",$str);  //匹配html中的空格
+        return trim($str); //返回字符串
+    }
+}
+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("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('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) {
+            $number = ($number + $counter) % 1000000000000;
+        } else {
+            $lastStr = $str;
+            $counter = 0;
+        }
+        return str_pad($number, 12, '0', STR_PAD_LEFT);
+    }
+}
+
+
+if (!function_exists('enbase64url')) {
+    /**
+     * Base64安全URL编码
+     * @param string $string
+     * @return string
+     */
+    function enbase64url(string $string): string
+    {
+        return CodeExtend::enSafe64($string);
+    }
+}
+if (!function_exists('debase64url')) {
+    /**
+     * Base64安全URL解码
+     * @param string $string
+     * @return string
+     */
+    function debase64url(string $string): string
+    {
+        return CodeExtend::deSafe64($string);
+    }
+}
+if (!function_exists('unique_arr')) {
+
+    /**
+     * 多维数组根据指定字段去重
+     * @param array $array 原始数组
+     * @param string $field 去重字段名
+     * @return array 去重后的数组
+     */
+    function unique_arr(array $array, string $field): array
+    {
+        $tmp = [];
+        foreach ($array as $key => $val) {
+            // 用去重字段的值作为临时键,重复会自动覆盖
+            $tmp[$val[$field]] = $val;
+        }
+        // 重置索引返回
+        return array_values($tmp);
+    }
+}

+ 45 - 0
app/middleware/AuthMiddleware.php

@@ -0,0 +1,45 @@
+<?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")]);
+                (new JWT)->guard("admin")->verify();
+                $user = (new Auth)->guard("admin")->user();
+                if (empty($user)) return json(['code'=>401,'msg'=> trans("error.login")]);
+                $request->user = $user->toArray();
+            }
+        } catch (\ReflectionException $exception) {
+            return json(['code'=> 500,'msg'=> $exception->getMessage()]);
+        }
+        $response = $request->method() == 'OPTIONS' ? response('',204) : $handler($request);
+        // 给响应添加跨域相关的http头
+//        $response->withHeaders([
+//            'Access-Control-Allow-Credentials' => 'true',
+//            'Access-Control-Allow-Origin' => $request->header('origin', '*'),
+//            'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
+//            'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
+//        ]);
+        return $response;
+    }
+
+
+}

+ 44 - 0
app/middleware/DyMiddleware.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace app\middleware;
+
+use Shopwwi\WebmanAuth\Auth;
+use Shopwwi\WebmanAuth\JWT;
+use Webman\Http\Request;
+use Webman\Http\Response;
+use Webman\MiddlewareInterface;
+
+class DyMiddleware 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('platform','');
+                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")]);
+                (new JWT)->guard("member")->verify();
+                $user = (new Auth)->guard("member")->user();
+                if (empty($user)) return json(['code'=>401,'msg'=> trans("error.login")]);
+                $request->user = $user->toArray();
+            }
+        } catch (\ReflectionException $exception) {
+            return json(['code'=> 500,'msg'=> $exception->getMessage()]);
+        }
+        $response = $request->method() == 'OPTIONS' ? response('',204) : $handler($request);
+        // 给响应添加跨域相关的http头
+//        $response->withHeaders([
+//            'Access-Control-Allow-Credentials' => 'true',
+//            'Access-Control-Allow-Origin' => $request->header('origin', '*'),
+//            'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'),
+//            'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'),
+//        ]);
+        return $response;
+    }
+
+
+}

+ 33 - 0
app/middleware/ExeMiddleware.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace app\middleware;
+
+use Shopwwi\WebmanAuth\Auth;
+use Shopwwi\WebmanAuth\JWT;
+use Webman\Http\Request;
+use Webman\Http\Response;
+use Webman\MiddlewareInterface;
+
+class ExeMiddleware 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)) {
+                $token =  $request->header("Authorization","");
+                if (empty($token)) return json(['code'=> 0,'msg'=> trans("error.request")]);
+                $uData = explode(" ",$token);
+                if (!isset($uData[1])) return json(['code'=> 0,'msg'=> trans("error.request")]);
+                $request->uuid = $uData[1];
+            }
+        } catch (\ReflectionException $exception) {
+            return json(['code'=> 500,'msg'=> $exception->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;
+    }
+}

+ 39 - 0
app/middleware/WxMiddleware.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace app\middleware;
+
+use Shopwwi\WebmanAuth\Auth;
+use Shopwwi\WebmanAuth\JWT;
+use Webman\Http\Request;
+use Webman\Http\Response;
+use Webman\MiddlewareInterface;
+
+class WxMiddleware 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('platform','');
+                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")]);
+                if ($type == "mini") {
+                    $typeName = "member";
+                } else {
+                    $typeName = "mp";
+                }
+                (new JWT)->guard($typeName)->verify();
+                $user = (new Auth)->guard($typeName)->user();
+                if (empty($user)) return json(['code'=>401,'msg'=> trans("error.login")]);
+                $request->user = $user->toArray();
+            }
+        } catch (\ReflectionException $exception) {
+            return json(['code'=> 500,'msg'=> $exception->getMessage()]);
+        }
+        return $request->method() == 'OPTIONS' ? response('',204) : $handler($request);
+    }
+
+}

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

@@ -0,0 +1,68 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property integer $uuid 
+ * @property integer $shop_id 
+ * @property string $name 文件名称
+ * @property integer $total_page 总页数
+ * @property integer $page 页数
+ * @property integer $end_page 结束页
+ * @property integer $start_page 起始页
+ * @property integer $color 颜色
+ * @property string $paper_size 纸张
+ * @property integer $duplex 单双面1单面2双面
+ * @property integer $direction 1
+ * @property integer $number 打印份数
+ * @property integer $money 金额
+ * @property string $path 文件
+ * @property string $old_path 原始路径
+ * @property string $extension 
+ * @property float $discount 折扣
+ * @property integer $source 1文档2复印
+ * @property integer $extra_money 额外加价
+ * @property integer $single_money 单价
+ * @property integer $single_id 单价ID
+ * @property string $icon 
+ * @property string $print_name 打印机名称
+ * @property string $view_key 预览id
+ * @property string $job 
+ * @property mixed $create_at
+ */
+class SaasCart 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_cart";
+    
+    /**
+     * 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/SaasCombo.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace app\model\saas;
+
+use app\extra\basic\Model;
+
+
+/**
+ * @property integer $id (主键)
+ * @property string $name 
+ * @property integer $sort 
+ * @property integer $time 
+ * @property integer $unit 1天2年
+ * @property integer $money 金额
+ * @property integer $old_money 原价
+ * @property integer $status 
+ * @property mixed $create_at
+ */
+class SaasCombo 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_combo";
+    
+    /**
+     * 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;
+
+
+}

Some files were not shown because too many files changed in this diff